mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
refactor(settings): kill triple header, compact theme picker, real footer
Four UX-audit fixes for the Settings page:
1. Header dedupe
- The page used to stack THREE headers vertically: the workspace rail's
"Settings" entry, a page-level <h1>Settings</h1> + subtitle, and the
General section's <h3>General</h3> + subtitle. The page-level and
section-level subtitles both used SETTINGS.GENERAL_SUBTITLE
("Change the configuration of the application"), word-for-word.
- Drop the visible page-level header entirely. Keep an a11y-only
<span class="visually-hidden"> for the data-test-id hook so the
existing settings.component.spec selector still resolves. Left rail
announces "Settings"; section header is now the page anchor.
- Add a global .visually-hidden helper to styles.scss for re-use.
2. Theme + Cover-size pickers shrink to a real segmented control
- .theme-switcher was a 3-column grid of ~78px-tall buttons making
"Choose theme" look like the most important action in Settings.
Replaced with a 30px-tall segmented control (track + thumb with
box-shadow on the selected option), matching the standard Material
"compact row" cadence everywhere else.
- Shortened all 18 locales' THEMES.* labels from "Light theme / Dark
theme / System theme" to "Light / Dark / System". The "theme" word
duplicated the section's own h3 ("Visual theme") and forced labels
to wrap into two lines in narrow columns.
3. Save bar becomes a real sticky footer
- The action bar was a `border-radius:18px; margin-left:auto;
position:sticky` chip floating bottom-right that clipped the last
form rows underneath. Now spans the full content column with a
top border separator — sits flush with the bottom edge while
still sticky on scroll. The save-button gradient also drops the
hand-rolled #3b82f6 in favour of var(--app-selection-color) so it
matches the unified blue primary used everywhere else.
4. Playlist switcher hidden on /settings
- Workspace shell header gains an isSettingsRoute input that hides
the playlist switcher block AND the leading actions group
(+ Add source / header shortcut / bulk action). Settings is a
global page — those controls were implying that switching
playlists scopes settings, which it doesn't. Wired through from
the shell facade's existing isSettingsRoute computed.
- Mock header in workspace-shell.component.spec gains the matching
input() declaration so the binding resolves under tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
7399fbfa9e
commit
115911d2a6
24 files changed
+207
-156
No files matched your search
@@ -14,18 +14,20 @@
|
||||
class="settings-layout"
|
||||
novalidate
|
||||
>
|
||||
<!-- Page-level "Settings" h1 intentionally removed: the left rail
|
||||
already labels the page "Settings" (sg-nav-title), and each
|
||||
section below provides its own h3 + subtitle. Stacking another
|
||||
h1 in the right pane reproduced the audit's "vertical ladder
|
||||
of identical labels" complaint. -->
|
||||
@if (!isDialog) {
|
||||
<header
|
||||
class="settings-page-header"
|
||||
<!-- Reserved hook so the page test selector still resolves,
|
||||
even though there's no visible chrome here. -->
|
||||
<span
|
||||
class="visually-hidden"
|
||||
data-test-id="settings-page-header"
|
||||
>
|
||||
<div class="settings-page-header__text">
|
||||
<h1 data-test-id="settings-page-title">
|
||||
{{ 'SETTINGS.TITLE' | translate }}
|
||||
</h1>
|
||||
<p>{{ 'SETTINGS.GENERAL_SUBTITLE' | translate }}</p>
|
||||
</div>
|
||||
</header>
|
||||
{{ 'SETTINGS.TITLE' | translate }}
|
||||
</span>
|
||||
}
|
||||
|
||||
<div class="settings-content">
|
||||
|
||||
@@ -235,50 +235,59 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Compact segmented control — replaces the previous 3 buttons each ~78px
|
||||
// tall. Per the UX audit, "Light/Dark/System" was reading as the most
|
||||
// important setting on the page because of its sheer footprint. Now the
|
||||
// row reads at the same weight as the other compact rows (Language,
|
||||
// Startup behavior, etc.), restoring the visual hierarchy.
|
||||
.theme-switcher {
|
||||
width: min(100%, 540px);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
width: min(100%, 360px);
|
||||
display: inline-flex;
|
||||
padding: 3px;
|
||||
gap: 2px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--mat-sys-surface-container-low) 80%,
|
||||
transparent
|
||||
);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--mat-sys-outline-variant) 60%, transparent);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.theme-switcher__option {
|
||||
flex: 1 1 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 54px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--mat-sys-outline-variant) 70%, transparent);
|
||||
border-radius: 16px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--mat-sys-surface-container-low) 88%,
|
||||
transparent
|
||||
);
|
||||
gap: 6px;
|
||||
min-height: 30px;
|
||||
padding: 4px 12px;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
font: inherit;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
background 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
color 0.18s ease;
|
||||
background 0.16s ease,
|
||||
color 0.16s ease,
|
||||
box-shadow 0.16s ease;
|
||||
|
||||
mat-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
font-size: 18px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--mat-sys-surface-container-high) 86%,
|
||||
var(--mat-sys-surface-container-highest) 70%,
|
||||
transparent
|
||||
);
|
||||
color: var(--mat-sys-on-surface);
|
||||
@@ -286,31 +295,19 @@
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px
|
||||
color-mix(in srgb, var(--app-selection-color) 18%, transparent);
|
||||
box-shadow: 0 0 0 2px
|
||||
color-mix(in srgb, var(--app-selection-color) 50%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-switcher__option--selected {
|
||||
border-color: var(--settings-group-active-ring);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
color-mix(
|
||||
in srgb,
|
||||
var(--app-selection-surface-strong) 72%,
|
||||
var(--mat-sys-surface-container-high)
|
||||
),
|
||||
color-mix(
|
||||
in srgb,
|
||||
var(--app-selection-surface) 72%,
|
||||
var(--mat-sys-surface-container)
|
||||
)
|
||||
);
|
||||
background: var(--app-widget-bg, var(--mat-sys-surface));
|
||||
color: var(--app-selection-color);
|
||||
font-weight: 600;
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.18),
|
||||
inset 0 0 0 1px
|
||||
color-mix(in srgb, var(--app-selection-border) 54%, transparent),
|
||||
0 10px 18px -22px var(--settings-group-active-glow);
|
||||
color-mix(in srgb, var(--app-selection-border) 36%, transparent);
|
||||
}
|
||||
|
||||
.epg-sources {
|
||||
@@ -708,21 +705,31 @@
|
||||
);
|
||||
}
|
||||
|
||||
// Sticky-footer Save bar — full content width so it never overlaps form
|
||||
// rows. The previous chip-style bar (border-radius:18px, margin-left:auto)
|
||||
// floated in the bottom-right corner and clipped fields underneath; the
|
||||
// UX audit called this out as the worst of three available patterns.
|
||||
// Now the bar acts as a proper footer: spans the column, separator line
|
||||
// on top, sits flush with the bottom edge while still sticking on scroll.
|
||||
.action-bar {
|
||||
position: sticky;
|
||||
bottom: calc(
|
||||
var(--settings-action-bar-offset) + var(--settings-safe-bottom)
|
||||
);
|
||||
margin-left: auto;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--settings-action-bar-border);
|
||||
border-radius: 18px;
|
||||
padding: 12px 18px;
|
||||
border-top: 1px solid var(--settings-action-bar-border);
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-bottom: 0;
|
||||
border-radius: 0;
|
||||
background: var(--settings-action-bar-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: var(--settings-action-bar-shadow);
|
||||
@@ -735,16 +742,21 @@
|
||||
box-shadow: none;
|
||||
border-radius: 16px;
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--settings-action-bar-border);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.settings-save-button.mat-mdc-button-base {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
color: #fff;
|
||||
// Use the unified blue primary (--app-selection-color) instead of the
|
||||
// old hand-rolled #3b82f6 gradient that didn't match the rest of the
|
||||
// app's CTAs after the color unification commit.
|
||||
background: var(--app-selection-color);
|
||||
color: var(--app-selection-on-color, #fff);
|
||||
font-weight: 500;
|
||||
padding: 0 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 14px rgba(59, 130, 246, 0.28);
|
||||
box-shadow: 0 4px 14px
|
||||
color-mix(in srgb, var(--app-selection-color) 28%, transparent);
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
@@ -756,7 +768,8 @@
|
||||
|
||||
&:hover:not([disabled]) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 18px rgba(59, 130, 246, 0.38);
|
||||
box-shadow: 0 6px 18px
|
||||
color-mix(in srgb, var(--app-selection-color) 38%, transparent);
|
||||
}
|
||||
|
||||
&:active:not([disabled]) {
|
||||
|
||||
@@ -375,9 +375,9 @@
|
||||
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "السمة الداكنة",
|
||||
"LIGHT_THEME": "السمة الفاتحة",
|
||||
"SYSTEM_THEME": "سمة النظام"
|
||||
"DARK_THEME": "داكن",
|
||||
"LIGHT_THEME": "فاتح",
|
||||
"SYSTEM_THEME": "النظام"
|
||||
},
|
||||
"PLAYBACK_DIAGNOSTICS": {
|
||||
"UNSUPPORTED_CONTAINER": {
|
||||
|
||||
@@ -375,9 +375,9 @@
|
||||
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "المظهر الداكن",
|
||||
"LIGHT_THEME": "المظهر الفاتح",
|
||||
"SYSTEM_THEME": "مظهر النظام"
|
||||
"DARK_THEME": "داكن",
|
||||
"LIGHT_THEME": "فاتح",
|
||||
"SYSTEM_THEME": "النظام"
|
||||
},
|
||||
"PLAYBACK_DIAGNOSTICS": {
|
||||
"UNSUPPORTED_CONTAINER": {
|
||||
|
||||
@@ -375,9 +375,9 @@
|
||||
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "Цёмная тэма",
|
||||
"LIGHT_THEME": "Светлая тэма",
|
||||
"SYSTEM_THEME": "Сістэмная тэма"
|
||||
"DARK_THEME": "Цёмная",
|
||||
"LIGHT_THEME": "Светлая",
|
||||
"SYSTEM_THEME": "Сістэмная"
|
||||
},
|
||||
"PLAYBACK_DIAGNOSTICS": {
|
||||
"UNSUPPORTED_CONTAINER": {
|
||||
|
||||
@@ -375,9 +375,9 @@
|
||||
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "Σκούρο θέμα",
|
||||
"LIGHT_THEME": "Ανοιχτόχρωμο θέμα",
|
||||
"SYSTEM_THEME": "Θέμα συστήματος"
|
||||
"DARK_THEME": "Σκούρο",
|
||||
"LIGHT_THEME": "Ανοιχτό",
|
||||
"SYSTEM_THEME": "Σύστημα"
|
||||
},
|
||||
"PLAYBACK_DIAGNOSTICS": {
|
||||
"UNSUPPORTED_CONTAINER": {
|
||||
|
||||
@@ -375,9 +375,9 @@
|
||||
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "Dark theme",
|
||||
"LIGHT_THEME": "Light theme",
|
||||
"SYSTEM_THEME": "System theme"
|
||||
"DARK_THEME": "Dark",
|
||||
"LIGHT_THEME": "Light",
|
||||
"SYSTEM_THEME": "System"
|
||||
},
|
||||
"PLAYBACK_DIAGNOSTICS": {
|
||||
"UNSUPPORTED_CONTAINER": {
|
||||
|
||||
@@ -375,9 +375,9 @@
|
||||
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "Tema oscuro",
|
||||
"LIGHT_THEME": "Tema claro",
|
||||
"SYSTEM_THEME": "Tema del sistema"
|
||||
"DARK_THEME": "Oscuro",
|
||||
"LIGHT_THEME": "Claro",
|
||||
"SYSTEM_THEME": "Sistema"
|
||||
},
|
||||
"PLAYBACK_DIAGNOSTICS": {
|
||||
"UNSUPPORTED_CONTAINER": {
|
||||
|
||||
@@ -375,9 +375,9 @@
|
||||
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "Thème foncé",
|
||||
"LIGHT_THEME": "Thème clair",
|
||||
"SYSTEM_THEME": "Thème du système"
|
||||
"DARK_THEME": "Sombre",
|
||||
"LIGHT_THEME": "Clair",
|
||||
"SYSTEM_THEME": "Système"
|
||||
},
|
||||
"PLAYBACK_DIAGNOSTICS": {
|
||||
"UNSUPPORTED_CONTAINER": {
|
||||
|
||||
@@ -375,9 +375,9 @@
|
||||
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "Tema scuro",
|
||||
"LIGHT_THEME": "Tema chiaro",
|
||||
"SYSTEM_THEME": "Tema di sistema"
|
||||
"DARK_THEME": "Scuro",
|
||||
"LIGHT_THEME": "Chiaro",
|
||||
"SYSTEM_THEME": "Sistema"
|
||||
},
|
||||
"PLAYBACK_DIAGNOSTICS": {
|
||||
"UNSUPPORTED_CONTAINER": {
|
||||
|
||||
@@ -375,9 +375,9 @@
|
||||
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "ダークテーマ",
|
||||
"LIGHT_THEME": "ライトテーマ",
|
||||
"SYSTEM_THEME": "システムテーマ"
|
||||
"DARK_THEME": "ダーク",
|
||||
"LIGHT_THEME": "ライト",
|
||||
"SYSTEM_THEME": "システム"
|
||||
},
|
||||
"PLAYBACK_DIAGNOSTICS": {
|
||||
"UNSUPPORTED_CONTAINER": {
|
||||
|
||||
@@ -375,9 +375,9 @@
|
||||
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "어두운 테마",
|
||||
"LIGHT_THEME": "밝은 테마",
|
||||
"SYSTEM_THEME": "시스템 테마"
|
||||
"DARK_THEME": "어둡게",
|
||||
"LIGHT_THEME": "밝게",
|
||||
"SYSTEM_THEME": "시스템"
|
||||
},
|
||||
"PLAYBACK_DIAGNOSTICS": {
|
||||
"UNSUPPORTED_CONTAINER": {
|
||||
|
||||
@@ -375,9 +375,9 @@
|
||||
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "Donker thema",
|
||||
"LIGHT_THEME": "Licht thema",
|
||||
"SYSTEM_THEME": "Systeemthema"
|
||||
"DARK_THEME": "Donker",
|
||||
"LIGHT_THEME": "Licht",
|
||||
"SYSTEM_THEME": "Systeem"
|
||||
},
|
||||
"PLAYBACK_DIAGNOSTICS": {
|
||||
"UNSUPPORTED_CONTAINER": {
|
||||
|
||||
@@ -375,9 +375,9 @@
|
||||
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "Ciemny motyw",
|
||||
"LIGHT_THEME": "Jasny motyw",
|
||||
"SYSTEM_THEME": "Motyw systemowy"
|
||||
"DARK_THEME": "Ciemny",
|
||||
"LIGHT_THEME": "Jasny",
|
||||
"SYSTEM_THEME": "Systemowy"
|
||||
},
|
||||
"PLAYBACK_DIAGNOSTICS": {
|
||||
"UNSUPPORTED_CONTAINER": {
|
||||
|
||||
@@ -375,9 +375,9 @@
|
||||
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "Tema escuro",
|
||||
"LIGHT_THEME": "Tema claro",
|
||||
"SYSTEM_THEME": "Tema do sistema"
|
||||
"DARK_THEME": "Escuro",
|
||||
"LIGHT_THEME": "Claro",
|
||||
"SYSTEM_THEME": "Sistema"
|
||||
},
|
||||
"PLAYBACK_DIAGNOSTICS": {
|
||||
"UNSUPPORTED_CONTAINER": {
|
||||
|
||||
@@ -375,9 +375,9 @@
|
||||
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "Тёмная тема",
|
||||
"LIGHT_THEME": "Светлая тема",
|
||||
"SYSTEM_THEME": "Системная тема"
|
||||
"DARK_THEME": "Тёмная",
|
||||
"LIGHT_THEME": "Светлая",
|
||||
"SYSTEM_THEME": "Системная"
|
||||
},
|
||||
"PLAYBACK_DIAGNOSTICS": {
|
||||
"UNSUPPORTED_CONTAINER": {
|
||||
|
||||
@@ -375,9 +375,9 @@
|
||||
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "Koyu tema",
|
||||
"LIGHT_THEME": "Açık tema",
|
||||
"SYSTEM_THEME": "Sistem teması"
|
||||
"DARK_THEME": "Koyu",
|
||||
"LIGHT_THEME": "Açık",
|
||||
"SYSTEM_THEME": "Sistem"
|
||||
},
|
||||
"PLAYBACK_DIAGNOSTICS": {
|
||||
"UNSUPPORTED_CONTAINER": {
|
||||
|
||||
@@ -375,8 +375,8 @@
|
||||
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "暗黑模式",
|
||||
"LIGHT_THEME": "明亮模式",
|
||||
"DARK_THEME": "深色",
|
||||
"LIGHT_THEME": "浅色",
|
||||
"SYSTEM_THEME": "跟随系统"
|
||||
},
|
||||
"PLAYBACK_DIAGNOSTICS": {
|
||||
|
||||
@@ -375,8 +375,8 @@
|
||||
"PLAYLISTS_REMOVE_FAILED": "Could not delete playlists. Please try again."
|
||||
},
|
||||
"THEMES": {
|
||||
"DARK_THEME": "深色主題",
|
||||
"LIGHT_THEME": "亮色主題",
|
||||
"DARK_THEME": "深色",
|
||||
"LIGHT_THEME": "淺色",
|
||||
"SYSTEM_THEME": "跟隨系統"
|
||||
},
|
||||
"PLAYBACK_DIAGNOSTICS": {
|
||||
|
||||
@@ -203,3 +203,20 @@ textarea,
|
||||
.mat-mdc-snack-bar-container.settings-snackbar {
|
||||
margin-bottom: calc(76px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
// WCAG-friendly visually-hidden helper — content stays in the
|
||||
// accessibility tree and DOM query results but occupies no layout box.
|
||||
// Used where a visible heading would duplicate adjacent navigation
|
||||
// context (e.g. Settings page title that the left rail already
|
||||
// announces).
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
+54
-46
@@ -1,18 +1,24 @@
|
||||
<header class="workspace-header">
|
||||
<app-playlist-switcher
|
||||
class="playlist-switcher"
|
||||
[currentTitle]="playlistTitle()"
|
||||
[subtitle]="playlistSubtitle()"
|
||||
[showPlaylistInfo]="canOpenPlaylistInfo()"
|
||||
[showAccountInfo]="canOpenAccountInfo()"
|
||||
[showAddPlaylist]="true"
|
||||
[canRefreshActivePlaylist]="canRefreshPlaylist()"
|
||||
[isRefreshingActivePlaylist]="isRefreshingPlaylist()"
|
||||
(playlistInfoRequested)="onPlaylistInfoRequested()"
|
||||
(accountInfoRequested)="onAccountInfoRequested()"
|
||||
(addPlaylistRequested)="onAddPlaylistRequested()"
|
||||
(refreshPlaylistRequested)="onRefreshPlaylistRequested()"
|
||||
/>
|
||||
<header class="workspace-header" [class.is-settings-route]="isSettingsRoute()">
|
||||
<!-- Playlist switcher hidden on /settings: those controls are
|
||||
playlist-scoped, but Settings is a global page. Per the UX
|
||||
audit, leaving them visible implied switching playlists changes
|
||||
which settings you're editing. -->
|
||||
@if (!isSettingsRoute()) {
|
||||
<app-playlist-switcher
|
||||
class="playlist-switcher"
|
||||
[currentTitle]="playlistTitle()"
|
||||
[subtitle]="playlistSubtitle()"
|
||||
[showPlaylistInfo]="canOpenPlaylistInfo()"
|
||||
[showAccountInfo]="canOpenAccountInfo()"
|
||||
[showAddPlaylist]="true"
|
||||
[canRefreshActivePlaylist]="canRefreshPlaylist()"
|
||||
[isRefreshingActivePlaylist]="isRefreshingPlaylist()"
|
||||
(playlistInfoRequested)="onPlaylistInfoRequested()"
|
||||
(accountInfoRequested)="onAccountInfoRequested()"
|
||||
(addPlaylistRequested)="onAddPlaylistRequested()"
|
||||
(refreshPlaylistRequested)="onRefreshPlaylistRequested()"
|
||||
/>
|
||||
}
|
||||
|
||||
@if (!hasNoPlaylists()) {
|
||||
<label class="search-field">
|
||||
@@ -53,42 +59,44 @@
|
||||
</label>
|
||||
}
|
||||
|
||||
<div class="header-actions header-actions--leading">
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
(click)="onAddPlaylistRequested()"
|
||||
[matTooltip]="'WORKSPACE.SHELL.ADD_PLAYLIST_ALL' | translate"
|
||||
[attr.aria-label]="'WORKSPACE.SHELL.ADD_PLAYLIST' | translate"
|
||||
>
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
@if (headerShortcut(); as action) {
|
||||
@if (!isSettingsRoute()) {
|
||||
<div class="header-actions header-actions--leading">
|
||||
<button
|
||||
type="button"
|
||||
class="header-shortcut"
|
||||
mat-icon-button
|
||||
(click)="onHeaderShortcutRequested()"
|
||||
[matTooltip]="action.tooltipKey | translate"
|
||||
[attr.aria-label]="action.ariaLabelKey | translate"
|
||||
(click)="onAddPlaylistRequested()"
|
||||
[matTooltip]="'WORKSPACE.SHELL.ADD_PLAYLIST_ALL' | translate"
|
||||
[attr.aria-label]="'WORKSPACE.SHELL.ADD_PLAYLIST' | translate"
|
||||
>
|
||||
<mat-icon>{{ action.icon }}</mat-icon>
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@if (headerBulkAction(); as bulk) {
|
||||
<button
|
||||
type="button"
|
||||
class="header-shortcut header-bulk-action"
|
||||
mat-icon-button
|
||||
[disabled]="bulk.disabled"
|
||||
(click)="onHeaderBulkActionRequested()"
|
||||
[matTooltip]="bulk.tooltip"
|
||||
[attr.aria-label]="bulk.ariaLabel"
|
||||
>
|
||||
<mat-icon>{{ bulk.icon }}</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@if (headerShortcut(); as action) {
|
||||
<button
|
||||
type="button"
|
||||
class="header-shortcut"
|
||||
mat-icon-button
|
||||
(click)="onHeaderShortcutRequested()"
|
||||
[matTooltip]="action.tooltipKey | translate"
|
||||
[attr.aria-label]="action.ariaLabelKey | translate"
|
||||
>
|
||||
<mat-icon>{{ action.icon }}</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@if (headerBulkAction(); as bulk) {
|
||||
<button
|
||||
type="button"
|
||||
class="header-shortcut header-bulk-action"
|
||||
mat-icon-button
|
||||
[disabled]="bulk.disabled"
|
||||
(click)="onHeaderBulkActionRequested()"
|
||||
[matTooltip]="bulk.tooltip"
|
||||
[attr.aria-label]="bulk.ariaLabel"
|
||||
>
|
||||
<mat-icon>{{ bulk.icon }}</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isElectron()) {
|
||||
<div class="header-actions header-actions--trailing">
|
||||
|
||||
+9
@@ -48,6 +48,15 @@ export class WorkspaceShellHeaderComponent {
|
||||
readonly hasNoPlaylists = input(false);
|
||||
readonly isDownloadsView = input(false);
|
||||
readonly hasActiveDownloads = input(false);
|
||||
/**
|
||||
* When true the playlist switcher + the "+ Add source" / refresh /
|
||||
* bulk-action buttons are hidden — those controls scope to a
|
||||
* playlist, but Settings is a global page, so leaving them visible
|
||||
* implies (falsely) that switching the playlist changes which
|
||||
* settings you're editing. Driven from the shell facade's existing
|
||||
* isSettingsRoute computed.
|
||||
*/
|
||||
readonly isSettingsRoute = input(false);
|
||||
|
||||
readonly searchChanged = output<string>();
|
||||
readonly searchSubmitted = output<string>();
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
[hasNoPlaylists]="facade.hasNoPlaylists()"
|
||||
[isDownloadsView]="facade.isDownloadsView()"
|
||||
[hasActiveDownloads]="facade.hasActiveDownloads()"
|
||||
[isSettingsRoute]="facade.isSettingsRoute()"
|
||||
(refreshPlaylistRequested)="facade.refreshCurrentPlaylist()"
|
||||
(searchChanged)="facade.onSearchInput($event)"
|
||||
(searchSubmitted)="facade.onSearchEnter($event)"
|
||||
|
||||
@@ -57,6 +57,7 @@ class MockWorkspaceShellHeaderComponent {
|
||||
readonly hasNoPlaylists = input(false);
|
||||
readonly isDownloadsView = input(false);
|
||||
readonly hasActiveDownloads = input(false);
|
||||
readonly isSettingsRoute = input(false);
|
||||
readonly headerBulkAction = input<WorkspaceHeaderBulkAction | null>(null);
|
||||
readonly searchChanged = output<string>();
|
||||
readonly searchSubmitted = output<string>();
|
||||
|
||||
Reference in new issue
Block a user