feat(settings): grouped navigation, cards and switches from the settings concept (#1853)

* feat(settings): grouped navigation, cards and switches from the settings concept

Implements the Claude Design "Settings concept" handoff.

Navigation: the sidebar is a 20px "Settings" title over App, Library, Devices
and Data groups, with About pinned to the footer beside the installed version
and an update badge. The active item is a soft fill without a border. Esc
leaves settings like the header Back. Reset is no longer a page: its one
destructive action is the last card of "Backup & data".

Pages: a title and one-line subtitle, then rows grouped into titled cards
with one right-aligned control column. Selects are compact without floating
labels, checkboxes are switches, segmented controls are pills, and
descriptions are capped at 56ch. "Show subtitles" moves to Playback, the
Embedded MPV note becomes a callout under the player select (only while it
is selected), and the TMDB attribution becomes the About footer.

EPG: Add and Refresh all sit in the Sources header, the empty state has its
own Add, the format examples are one line, Clear EPG data is its own row and
the offset is a stepper. About: version, update state and channel share one
card; the nightly warning is a callout shown only while Nightly is selected,
with a shortcut to the backup page; the support buttons are neutral with
coloured icons.

Save model: the save bar stays (design option B) and now floats over the
content column, counts the staged changes, answers Cmd/Ctrl+S, and marks
pages with staged edits in the sidebar. A saved change that waits for a
restart shows a dismissible notice.

Rows stack under 600px of pane width (the page is a size container, so the
persistent sidebar is accounted for). Page-specific styles moved into the
About, EPG, Backup and Remote control section stylesheets to stay within the
component style budget. New SETTINGS keys are translated in all 19 locales
and NAV_RESET is removed; brand names and loanwords that stay English are in
the identical-value baseline.

E2E: Material slide toggles report aria-checked after the next render, so
both suites toggle through a setSwitch helper; the settings nav is a
navigation landmark, so the Dashboard rail link is scoped to the rail.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(settings): compare restart notices against the running app, fix CI fallout

The restart notice now lists only the restart controls whose saved value
differs from what the running app uses: the stored values at first load,
or what the embedded engine reports it actually runs for the frame-copy
opt-in. Saving the launch value back withdraws the notice instead of
leaving it up with no chip to explain it. The refresh reads the current
list untracked and writes only a changed one, because the engine probe
effect calls it.

CI: the zoneless checklist dropped the deleted Reset section component, and
the theme-tokens Electron E2E floats the recording folder label on the
Playback page now that the settings selects carry no floating label.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(settings): keep a dismissed restart reminder away until the setting changes

"Later" now records the saved value it dismissed, for the rest of the app
run, so an unrelated save (or reopening Settings) does not bring the same
reminder back. It returns once a restart setting is saved with another
value.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 authored and GitHub committed 2026-10-09 20:39:07 +02:00
1 parent 4c6540679d
commit 8030ced95a
90 files changed
+5957 -3617

No files matched your search

+86 -302
View File
@@ -1,83 +1,84 @@
@use '@angular/material' as mat;
$settings-select-density: mat.define-theme(
(
density: (
scale: -5,
),
)
);
// Settings surfaces: rows in titled cards on the page background. Light
// keeps white cards with hairline borders; dark uses the widget surface with
// a faint light border, per the UI guidelines' theme split.
.settings-container {
// Compact switch, nearer the 22px toggle of the concept than Material's
// default 32px track; the 40px state layer keeps the touch target.
@include mat.slide-toggle-overrides(
(
track-width: 40px,
track-height: 24px,
selected-handle-size: 18px,
unselected-handle-size: 14px,
pressed-handle-size: 20px,
with-icon-handle-size: 18px,
)
);
// Compact select without a floating label (the row title names it).
.settings-select.mat-mdc-form-field {
@include mat.form-field-density($settings-select-density);
}
// Save is the page's one solid call to action: the app CTA pair (light
// on dark, dark on light), not the theme primary. Set through the button
// tokens, which a plain background rule cannot beat.
.settings-save-button {
@include mat.button-overrides(
(
filled-container-color: var(--app-cta-bg),
filled-label-text-color: var(--app-cta-fg),
filled-state-layer-color: var(--app-cta-fg),
filled-container-shape: 8px,
)
);
}
--settings-safe-bottom: env(safe-area-inset-bottom, 0px);
// Action bar is a full-width sticky footer, so it sits flush against
// the bottom edge of the scroll container — no hover offset. The
// previous 24px gap was tuned for the old floating-chip layout where
// the bar visually hovered above the bottom.
--settings-action-bar-offset: 0px;
// 0 by default — the action bar is now the last child of the form
// and acts as the natural bottom edge, so no padding-bottom is
// needed below it. The previous 112px/80px reservation existed to
// push content above the OLD floating-chip layout, but in the new
// sticky-footer model that just added dead scrollable space below
// the last setting row. The bar's own padding (12px) and the
// section title bottom-borders already provide visual breathing
// room when scrolled to the end.
--settings-action-bar-space: 0px;
--settings-page-header-bg: color-mix(
--settings-card-bg: var(--app-widget-bg, #ffffff);
--settings-card-border: rgba(0, 0, 0, 0.08);
--settings-item-border: rgba(0, 0, 0, 0.07);
--settings-control-bg: #ffffff;
--settings-control-border: rgba(0, 0, 0, 0.14);
--settings-control-hover-border: rgba(0, 0, 0, 0.26);
--settings-chip-bg: rgba(0, 0, 0, 0.07);
--settings-chip-fg: var(--app-body-color);
--settings-callout-info-bg: color-mix(
in srgb,
var(--mat-sys-surface-container-low) 94%,
rgba(255, 255, 255, 0.98)
var(--app-selection-color) 6%,
var(--settings-card-bg)
);
--settings-page-header-border: color-mix(
--settings-callout-info-border: color-mix(
in srgb,
var(--mat-sys-outline-variant) 72%,
var(--app-selection-color) 24%,
transparent
);
--settings-page-header-shadow:
0 10px 20px -28px rgba(15, 23, 42, 0.16),
0 1px 0 rgba(255, 255, 255, 0.68) inset;
--settings-group-bg: color-mix(
in srgb,
var(--mat-sys-surface) 96%,
rgba(255, 255, 255, 0.96)
);
--settings-group-border: color-mix(
in srgb,
var(--mat-sys-outline-variant) 78%,
transparent
);
--settings-group-shadow: 0 14px 28px -32px rgba(15, 23, 42, 0.14);
--settings-group-active-bg: color-mix(
in srgb,
var(--app-selection-color) 2%,
rgba(255, 255, 255, 0.98)
);
--settings-group-active-ring: color-mix(
in srgb,
var(--app-selection-border) 68%,
var(--settings-group-border)
);
--settings-group-active-glow: color-mix(
in srgb,
var(--app-selection-glow) 28%,
transparent
);
--settings-group-active-shadow:
inset 0 0 0 1px var(--settings-group-active-ring),
0 12px 24px -28px var(--settings-group-active-glow);
--settings-item-border: var(--app-separator);
--settings-item-danger-bg: color-mix(
in srgb,
var(--mat-sys-error) 4%,
rgba(255, 255, 255, 0.96)
);
--settings-callout-warn-bg: color-mix(in srgb, #d97706 9%, transparent);
--settings-callout-warn-border: color-mix(in srgb, #d97706 32%, transparent);
--settings-callout-warn-icon: #8a5300;
--settings-item-danger-border: color-mix(
in srgb,
var(--mat-sys-error) 14%,
var(--mat-sys-error) 22%,
transparent
);
--settings-action-bar-bg: color-mix(
--settings-action-bar-bg: #ffffff;
--settings-action-bar-border: rgba(0, 0, 0, 0.1);
--settings-action-bar-shadow: 0 16px 40px rgba(15, 23, 42, 0.18);
--settings-restart-bar-bg: color-mix(
in srgb,
var(--mat-sys-surface-container-low) 94%,
rgba(255, 255, 255, 0.9)
var(--app-selection-color) 8%,
var(--app-content-bg)
);
--settings-action-bar-border: color-mix(
in srgb,
var(--mat-sys-outline-variant) 62%,
transparent
);
--settings-action-bar-shadow: 0 10px 26px -26px rgba(15, 23, 42, 0.22);
--settings-muted: color-mix(
in srgb,
var(--mat-sys-on-surface) 64%,
@@ -87,250 +88,33 @@
:where(.dark-theme, [data-theme='dark'], .mat-app-background.dark)
.settings-container {
--settings-page-header-bg: color-mix(
in srgb,
var(--mat-sys-surface-container) 92%,
rgba(12, 16, 24, 0.94)
);
--settings-page-header-border: color-mix(
in srgb,
var(--mat-sys-outline-variant) 38%,
transparent
);
--settings-page-header-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.05),
0 16px 26px -34px rgba(0, 0, 0, 0.4);
--settings-group-bg: color-mix(
in srgb,
var(--mat-sys-surface-container) 94%,
rgba(10, 14, 22, 0.82)
);
--settings-group-border: rgba(255, 255, 255, 0.08);
--settings-group-shadow: 0 16px 28px -32px rgba(0, 0, 0, 0.42);
--settings-group-active-bg: color-mix(
in srgb,
var(--app-selection-surface) 24%,
rgba(255, 255, 255, 0.015)
);
--settings-group-active-ring: color-mix(
in srgb,
var(--app-selection-border) 54%,
rgba(255, 255, 255, 0.08)
);
--settings-group-active-glow: color-mix(
in srgb,
var(--app-selection-glow) 22%,
transparent
);
--settings-group-active-shadow:
inset 0 0 0 1px var(--settings-group-active-ring),
0 10px 22px -28px var(--settings-group-active-glow);
--settings-item-danger-bg: color-mix(
in srgb,
var(--mat-sys-error) 8%,
rgba(24, 10, 12, 0.86)
);
--settings-card-bg: var(--app-widget-bg, #1e2330);
--settings-card-border: rgba(255, 255, 255, 0.07);
--settings-item-border: rgba(255, 255, 255, 0.06);
--settings-control-bg: color-mix(in srgb, var(--app-widget-bg) 90%, white 6%);
--settings-control-border: rgba(255, 255, 255, 0.12);
--settings-control-hover-border: rgba(255, 255, 255, 0.22);
--settings-chip-bg: rgba(255, 255, 255, 0.1);
--settings-chip-fg: var(--app-heading-color);
--settings-callout-warn-bg: color-mix(in srgb, #fbbf24 10%, transparent);
--settings-callout-warn-border: color-mix(in srgb, #fbbf24 30%, transparent);
--settings-callout-warn-icon: #fbbf24;
--settings-item-danger-border: color-mix(
in srgb,
var(--mat-sys-error) 24%,
var(--mat-sys-error) 30%,
transparent
);
--settings-action-bar-bg: color-mix(
--settings-action-bar-bg: var(--app-card-hover-bg, #252b3a);
--settings-action-bar-border: rgba(255, 255, 255, 0.1);
--settings-action-bar-shadow: 0 16px 40px rgba(0, 0, 0, 0.55);
--settings-restart-bar-bg: color-mix(
in srgb,
var(--mat-sys-surface-container) 92%,
transparent
var(--app-selection-color) 12%,
var(--app-content-bg)
);
--settings-action-bar-border: rgba(255, 255, 255, 0.08);
--settings-action-bar-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
--settings-muted: color-mix(
in srgb,
var(--mat-sys-on-surface) 68%,
transparent
);
}
.settings-container {
.support-card {
width: 100%;
box-sizing: border-box;
position: relative;
display: flex;
flex-direction: column;
gap: 14px;
padding: 18px;
border-radius: 18px;
border: 1px solid color-mix(in srgb, #ff4d6d 26%, transparent);
background:
radial-gradient(
120% 140% at 0% 0%,
color-mix(in srgb, #ff4d6d 16%, transparent) 0%,
transparent 55%
),
radial-gradient(
120% 140% at 100% 100%,
color-mix(in srgb, #ffb347 14%, transparent) 0%,
transparent 60%
),
color-mix(
in srgb,
var(--mat-sys-surface-container-low) 92%,
transparent
);
overflow: hidden;
&::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(
80% 100% at 50% -10%,
color-mix(in srgb, #ff4d6d 12%, transparent) 0%,
transparent 70%
);
opacity: 0.8;
}
}
.support-card__intro {
position: relative;
display: flex;
align-items: flex-start;
gap: 14px;
z-index: 1;
}
.support-card__emblem {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 14px;
background: linear-gradient(135deg, #ff4d6d, #ff8b5e);
color: #fff;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.35),
0 10px 18px -10px rgba(255, 77, 109, 0.55);
mat-icon {
font-size: 22px;
width: 22px;
height: 22px;
}
}
.support-card__copy {
min-width: 0;
flex: 1 1 auto;
h4 {
margin: 0;
font-size: 1rem;
font-weight: 700;
letter-spacing: -0.01em;
}
p {
margin: 4px 0 0;
color: var(--settings-muted);
font-size: 0.86rem;
line-height: 1.5;
max-width: 64ch;
}
}
.support-card__actions {
position: relative;
z-index: 1;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.support-btn {
flex: 1 1 220px;
display: inline-flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
border-radius: 14px;
border: 1px solid transparent;
text-decoration: none;
color: #fff;
transition:
transform 0.18s ease,
box-shadow 0.18s ease,
filter 0.18s ease;
&:hover {
transform: translateY(-1px);
filter: brightness(1.04);
}
&:focus-visible {
outline: none;
box-shadow:
0 0 0 2px var(--mat-sys-surface),
0 0 0 4px
color-mix(
in srgb,
var(--app-selection-color) 40%,
transparent
);
}
}
.support-btn__glyph {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.18);
color: #fff;
flex: 0 0 auto;
svg {
width: 18px;
height: 18px;
}
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
}
.support-btn__text {
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
strong {
font-size: 0.92rem;
font-weight: 700;
letter-spacing: -0.01em;
}
small {
color: rgba(255, 255, 255, 0.82);
font-size: 0.75rem;
line-height: 1.3;
}
}
.support-btn--sponsors {
background: linear-gradient(135deg, #d4337a, #ff4d6d);
box-shadow: 0 12px 22px -18px rgba(212, 51, 122, 0.7);
}
.support-btn--kofi {
background: linear-gradient(135deg, #13c3ff, #29abe0);
box-shadow: 0 12px 22px -18px rgba(19, 195, 255, 0.7);
}
}