mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
feat(switcher): restyle the toolbar trigger and open it with Cmd/Ctrl+P
- The trigger is a 40px secondary button: the active source's tile with its status dot, its name and "type · channels" (or the source count), and a chevron that turns while open. The name comes from sourceDisplayName, so a URL-named playlist no longer shows its URL or credentials in the toolbar. - Cmd+P / Ctrl+P opens the switcher wherever it is shown (Cmd+K stays the command palette) and is listed in the keyboard shortcuts dialog. - The trigger's inline refresh button is gone; refresh stays in the playlist's ⋯ menu and the command palette. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
401295c092
commit
106414c118
34 files changed
+317
-251
No files matched your search
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "نقل تحديد لوحة الأوامر",
|
||||
"COMMAND_PALETTE_RUN": "تشغيل عنصر لوحة الأوامر المحدد",
|
||||
"CLOSE_DIALOGS": "إغلاق مربع الحوار أو إخفاء التراكب",
|
||||
"DOWNLOADS_OPEN_ITEM": "فتح عنصر التنزيل المحدد في المكتبة"
|
||||
"DOWNLOADS_OPEN_ITEM": "فتح عنصر التنزيل المحدد في المكتبة",
|
||||
"OPEN_PLAYLIST_SWITCHER": "تبديل قائمة التشغيل"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "حرك الاختيار فلوحة الأوامر",
|
||||
"COMMAND_PALETTE_RUN": "شغل العنصر المختار فلوحة الأوامر",
|
||||
"CLOSE_DIALOGS": "سد النافذة ولا خبي الطبقة",
|
||||
"DOWNLOADS_OPEN_ITEM": "حل عنصر التحميل المحدد فالمكتبة"
|
||||
"DOWNLOADS_OPEN_ITEM": "حل عنصر التحميل المحدد فالمكتبة",
|
||||
"OPEN_PLAYLIST_SWITCHER": "بدّل الليستة"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "Перамясціць выбар у палітры каманд",
|
||||
"COMMAND_PALETTE_RUN": "Выканаць выбраны элемент палітры каманд",
|
||||
"CLOSE_DIALOGS": "Закрыць дыялог або схаваць накладку",
|
||||
"DOWNLOADS_OPEN_ITEM": "Адкрыць сфакусаваную спампоўку ў бібліятэцы"
|
||||
"DOWNLOADS_OPEN_ITEM": "Адкрыць сфакусаваную спампоўку ў бібліятэцы",
|
||||
"OPEN_PLAYLIST_SWITCHER": "Пераключыць плэйліст"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "Auswahl in der Befehlspalette verschieben",
|
||||
"COMMAND_PALETTE_RUN": "Ausgewählten Eintrag der Befehlspalette ausführen",
|
||||
"CLOSE_DIALOGS": "Dialog schließen oder Overlay ausblenden",
|
||||
"DOWNLOADS_OPEN_ITEM": "Fokussierten Download-Eintrag in der Bibliothek öffnen"
|
||||
"DOWNLOADS_OPEN_ITEM": "Fokussierten Download-Eintrag in der Bibliothek öffnen",
|
||||
"OPEN_PLAYLIST_SWITCHER": "Playlist wechseln"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "Μετακίνηση επιλογής παλέτας εντολών",
|
||||
"COMMAND_PALETTE_RUN": "Εκτέλεση επιλεγμένου στοιχείου παλέτας εντολών",
|
||||
"CLOSE_DIALOGS": "Κλείσιμο διαλόγου ή απόκρυψη overlay",
|
||||
"DOWNLOADS_OPEN_ITEM": "Άνοιγμα εστιασμένου στοιχείου λήψης στη βιβλιοθήκη"
|
||||
"DOWNLOADS_OPEN_ITEM": "Άνοιγμα εστιασμένου στοιχείου λήψης στη βιβλιοθήκη",
|
||||
"OPEN_PLAYLIST_SWITCHER": "Εναλλαγή λίστας"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "Move command palette selection",
|
||||
"COMMAND_PALETTE_RUN": "Run selected command palette item",
|
||||
"CLOSE_DIALOGS": "Close dialog or dismiss overlay",
|
||||
"DOWNLOADS_OPEN_ITEM": "Open focused download item in the library"
|
||||
"DOWNLOADS_OPEN_ITEM": "Open focused download item in the library",
|
||||
"OPEN_PLAYLIST_SWITCHER": "Switch playlist"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "Mover la selección de la paleta de comandos",
|
||||
"COMMAND_PALETTE_RUN": "Ejecutar el elemento seleccionado de la paleta de comandos",
|
||||
"CLOSE_DIALOGS": "Cerrar diálogo o descartar superposición",
|
||||
"DOWNLOADS_OPEN_ITEM": "Abrir en la biblioteca el elemento de descarga enfocado"
|
||||
"DOWNLOADS_OPEN_ITEM": "Abrir en la biblioteca el elemento de descarga enfocado",
|
||||
"OPEN_PLAYLIST_SWITCHER": "Cambiar de lista"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "Déplacer la sélection dans la palette de commandes",
|
||||
"COMMAND_PALETTE_RUN": "Exécuter l’élément sélectionné dans la palette de commandes",
|
||||
"CLOSE_DIALOGS": "Fermer une boîte de dialogue ou masquer une superposition",
|
||||
"DOWNLOADS_OPEN_ITEM": "Ouvrir l’élément de téléchargement ciblé dans la bibliothèque"
|
||||
"DOWNLOADS_OPEN_ITEM": "Ouvrir l’élément de téléchargement ciblé dans la bibliothèque",
|
||||
"OPEN_PLAYLIST_SWITCHER": "Changer de playlist"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "Kijelölés mozgatása a parancspalettán",
|
||||
"COMMAND_PALETTE_RUN": "A parancspalettán kijelölt művelet végrehajtása",
|
||||
"CLOSE_DIALOGS": "Párbeszédpanel bezárása vagy fedőréteg eltüntetése",
|
||||
"DOWNLOADS_OPEN_ITEM": "A fókuszban lévő letöltés megnyitása a médiatárban"
|
||||
"DOWNLOADS_OPEN_ITEM": "A fókuszban lévő letöltés megnyitása a médiatárban",
|
||||
"OPEN_PLAYLIST_SWITCHER": "Lejátszási lista váltása"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "Sposta selezione della palette comandi",
|
||||
"COMMAND_PALETTE_RUN": "Esegui elemento selezionato della palette comandi",
|
||||
"CLOSE_DIALOGS": "Chiudi dialogo o nascondi overlay",
|
||||
"DOWNLOADS_OPEN_ITEM": "Apri nella libreria l’elemento download selezionato"
|
||||
"DOWNLOADS_OPEN_ITEM": "Apri nella libreria l’elemento download selezionato",
|
||||
"OPEN_PLAYLIST_SWITCHER": "Cambia playlist"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "コマンドパレットの選択を移動",
|
||||
"COMMAND_PALETTE_RUN": "選択中のコマンドパレット項目を実行",
|
||||
"CLOSE_DIALOGS": "ダイアログを閉じる、またはオーバーレイを解除",
|
||||
"DOWNLOADS_OPEN_ITEM": "フォーカス中のダウンロード項目をライブラリで開く"
|
||||
"DOWNLOADS_OPEN_ITEM": "フォーカス中のダウンロード項目をライブラリで開く",
|
||||
"OPEN_PLAYLIST_SWITCHER": "プレイリストを切り替え"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "명령 팔레트 선택 이동",
|
||||
"COMMAND_PALETTE_RUN": "선택한 명령 팔레트 항목 실행",
|
||||
"CLOSE_DIALOGS": "대화상자 닫기 또는 오버레이 해제",
|
||||
"DOWNLOADS_OPEN_ITEM": "포커스된 다운로드 항목을 라이브러리에서 열기"
|
||||
"DOWNLOADS_OPEN_ITEM": "포커스된 다운로드 항목을 라이브러리에서 열기",
|
||||
"OPEN_PLAYLIST_SWITCHER": "재생목록 전환"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "Selectie in opdrachtpalet verplaatsen",
|
||||
"COMMAND_PALETTE_RUN": "Geselecteerd item in opdrachtpalet uitvoeren",
|
||||
"CLOSE_DIALOGS": "Dialoogvenster sluiten of overlay verbergen",
|
||||
"DOWNLOADS_OPEN_ITEM": "Gefocust downloaditem in de bibliotheek openen"
|
||||
"DOWNLOADS_OPEN_ITEM": "Gefocust downloaditem in de bibliotheek openen",
|
||||
"OPEN_PLAYLIST_SWITCHER": "Afspeellijst wisselen"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "Przenieś zaznaczenie w palecie poleceń",
|
||||
"COMMAND_PALETTE_RUN": "Uruchom zaznaczoną pozycję palety poleceń",
|
||||
"CLOSE_DIALOGS": "Zamknij okno dialogowe lub ukryj nakładkę",
|
||||
"DOWNLOADS_OPEN_ITEM": "Otwórz aktywne pobranie w bibliotece"
|
||||
"DOWNLOADS_OPEN_ITEM": "Otwórz aktywne pobranie w bibliotece",
|
||||
"OPEN_PLAYLIST_SWITCHER": "Przełącz playlistę"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "Mover seleção da paleta de comandos",
|
||||
"COMMAND_PALETTE_RUN": "Executar item selecionado da paleta de comandos",
|
||||
"CLOSE_DIALOGS": "Fechar diálogo ou dispensar sobreposição",
|
||||
"DOWNLOADS_OPEN_ITEM": "Abrir o download em foco na biblioteca"
|
||||
"DOWNLOADS_OPEN_ITEM": "Abrir o download em foco na biblioteca",
|
||||
"OPEN_PLAYLIST_SWITCHER": "Mudar de lista"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "Переместить выбор в палитре команд",
|
||||
"COMMAND_PALETTE_RUN": "Выполнить выбранный пункт палитры команд",
|
||||
"CLOSE_DIALOGS": "Закрыть диалог или скрыть оверлей",
|
||||
"DOWNLOADS_OPEN_ITEM": "Открыть выбранную загрузку в библиотеке"
|
||||
"DOWNLOADS_OPEN_ITEM": "Открыть выбранную загрузку в библиотеке",
|
||||
"OPEN_PLAYLIST_SWITCHER": "Переключить плейлист"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "Komut paleti seçimini taşı",
|
||||
"COMMAND_PALETTE_RUN": "Seçili komut paleti öğesini çalıştır",
|
||||
"CLOSE_DIALOGS": "İletişim kutusunu kapat veya katmanı gizle",
|
||||
"DOWNLOADS_OPEN_ITEM": "Odaktaki indirme öğesini kitaplıkta aç"
|
||||
"DOWNLOADS_OPEN_ITEM": "Odaktaki indirme öğesini kitaplıkta aç",
|
||||
"OPEN_PLAYLIST_SWITCHER": "Oynatma listesini değiştir"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "移动命令面板选中项",
|
||||
"COMMAND_PALETTE_RUN": "运行选中的命令面板项目",
|
||||
"CLOSE_DIALOGS": "关闭对话框或取消覆盖层",
|
||||
"DOWNLOADS_OPEN_ITEM": "在媒体库中打开聚焦的下载项"
|
||||
"DOWNLOADS_OPEN_ITEM": "在媒体库中打开聚焦的下载项",
|
||||
"OPEN_PLAYLIST_SWITCHER": "切换播放列表"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1996,7 +1996,8 @@
|
||||
"COMMAND_PALETTE_NAVIGATION": "移動命令面板選取項",
|
||||
"COMMAND_PALETTE_RUN": "執行選取的命令面板項目",
|
||||
"CLOSE_DIALOGS": "關閉對話框或取消覆蓋層",
|
||||
"DOWNLOADS_OPEN_ITEM": "在媒體庫中開啟聚焦的下載項目"
|
||||
"DOWNLOADS_OPEN_ITEM": "在媒體庫中開啟聚焦的下載項目",
|
||||
"OPEN_PLAYLIST_SWITCHER": "切換播放清單"
|
||||
}
|
||||
},
|
||||
"GLOBAL_FAVORITES": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './lib/add-playlist-menu/playlist-type';
|
||||
export * from './lib/playlist-switcher/playlist-switcher.component';
|
||||
export * from './lib/playlist-switcher/playlist-switcher-open.service';
|
||||
export * from './lib/recent-playlists/playlist-info/playlist-info.component';
|
||||
export * from './lib/recent-playlists/playlist-info/stalker-playlist-connection-editor.token';
|
||||
export * from './lib/recent-playlists/recent-playlists.component';
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Opens the toolbar playlist switcher from outside it: the ⌘P / Ctrl+P
|
||||
* shortcut. The switcher subscribes while it is on screen (it is hidden on
|
||||
* Settings), so a request with no switcher does nothing.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PlaylistSwitcherOpenService {
|
||||
private readonly requests = new Subject<void>();
|
||||
readonly openRequests = this.requests.asObservable();
|
||||
private listeners = 0;
|
||||
|
||||
/** Whether a switcher is on screen to take the request. */
|
||||
get available(): boolean {
|
||||
return this.listeners > 0;
|
||||
}
|
||||
|
||||
request(): void {
|
||||
this.requests.next();
|
||||
}
|
||||
|
||||
/** Called by the switcher; the returned function unregisters it. */
|
||||
register(): () => void {
|
||||
this.listeners += 1;
|
||||
return () => {
|
||||
this.listeners -= 1;
|
||||
};
|
||||
}
|
||||
}
|
||||
+22
-31
@@ -1,43 +1,34 @@
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
class="playlist-switcher-trigger"
|
||||
data-test-id="playlist-switcher-trigger"
|
||||
[class.is-open]="isMenuOpen()"
|
||||
matRipple
|
||||
[matRippleDisabled]="false"
|
||||
[matMenuTriggerFor]="playlistMenu"
|
||||
#menuTrigger="matMenuTrigger"
|
||||
#triggerElement
|
||||
aria-keyshortcuts="Meta+P Control+P"
|
||||
(menuOpened)="onMenuOpened()"
|
||||
(menuClosed)="onMenuClosed()"
|
||||
>
|
||||
<div class="trigger-icon-container">
|
||||
<mat-icon class="trigger-playlist-icon">playlist_play</mat-icon>
|
||||
</div>
|
||||
<div class="playlist-info">
|
||||
<div class="name">{{ displayTitle() }}</div>
|
||||
<div class="channels-count">{{ subtitle() }}</div>
|
||||
</div>
|
||||
@if (canRefreshActivePlaylist()) {
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
class="trigger-refresh-button"
|
||||
[disabled]="isRefreshingActivePlaylist()"
|
||||
(click)="onRefreshClick($event)"
|
||||
(mousedown)="$event.stopPropagation()"
|
||||
[matTooltip]="'WORKSPACE.SHELL.REFRESH_PLAYLIST' | translate"
|
||||
[attr.aria-label]="'WORKSPACE.SHELL.REFRESH_PLAYLIST' | translate"
|
||||
>
|
||||
<mat-icon
|
||||
class="trigger-refresh-icon"
|
||||
[class.is-spinning]="isRefreshingActivePlaylist()"
|
||||
>sync</mat-icon
|
||||
>
|
||||
</button>
|
||||
@if (activePlaylist(); as active) {
|
||||
<app-source-tile
|
||||
size="sm"
|
||||
[playlist]="active"
|
||||
[status]="activeStatus()"
|
||||
/>
|
||||
} @else {
|
||||
<span class="trigger-placeholder-tile" aria-hidden="true">
|
||||
<mat-icon>playlist_play</mat-icon>
|
||||
</span>
|
||||
}
|
||||
<mat-icon class="dropdown-icon" [class.rotated]="isMenuOpen()">
|
||||
expand_more
|
||||
</mat-icon>
|
||||
</div>
|
||||
<span class="playlist-info">
|
||||
<span class="name">{{ displayTitle() }}</span>
|
||||
<span class="channels-count">{{ triggerSubtitle() }}</span>
|
||||
</span>
|
||||
<mat-icon class="dropdown-icon" [class.rotated]="isMenuOpen()"
|
||||
>expand_more</mat-icon
|
||||
>
|
||||
</button>
|
||||
|
||||
<mat-menu
|
||||
#playlistMenu="matMenu"
|
||||
|
||||
+82
-154
@@ -5,186 +5,125 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// The trigger is a secondary button (UI kit): 40px, 10px radius, a 10%
|
||||
// fill, the source's tile, two lines and a chevron that turns when open.
|
||||
.playlist-switcher-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 10px 0 6px;
|
||||
box-sizing: border-box;
|
||||
padding: 4px 8px 4px 4px;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--app-on-surface) 10%, transparent);
|
||||
color: var(--app-heading-color);
|
||||
font: inherit;
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: color-mix(in srgb, var(--app-on-surface) 5%, transparent);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--mat-sys-outline-variant) 36%, transparent);
|
||||
app-region: no-drag;
|
||||
transition: background 0.12s ease;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--app-on-surface) 12%,
|
||||
transparent
|
||||
);
|
||||
border-color: color-mix(
|
||||
in srgb,
|
||||
var(--mat-sys-outline-variant) 64%,
|
||||
transparent
|
||||
);
|
||||
background: color-mix(in srgb, var(--app-on-surface) 16%, transparent);
|
||||
}
|
||||
|
||||
// Open: one step up, with a 2px inset ring in the selection colour.
|
||||
&.is-open {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--app-on-surface) 12%,
|
||||
transparent
|
||||
);
|
||||
border-color: color-mix(
|
||||
in srgb,
|
||||
var(--mat-sys-outline-variant) 72%,
|
||||
transparent
|
||||
);
|
||||
background: color-mix(in srgb, var(--app-on-surface) 14%, transparent);
|
||||
box-shadow: inset 0 0 0 2px var(--app-selection-color);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
&:focus-visible {
|
||||
@include focus-ring.focus-ring-declarations;
|
||||
}
|
||||
|
||||
app-source-tile {
|
||||
--source-tile-bg: color-mix(
|
||||
in srgb,
|
||||
var(--app-on-surface) 10%,
|
||||
var(--app-header-bg)
|
||||
);
|
||||
--source-tile-ring: var(--app-header-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.trigger-icon-container {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--app-on-surface) 8%,
|
||||
transparent
|
||||
);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.trigger-placeholder-tile {
|
||||
flex: none;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 7px;
|
||||
background: color-mix(in srgb, var(--app-on-surface) 10%, transparent);
|
||||
color: var(--app-body-color);
|
||||
|
||||
.trigger-playlist-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
.mat-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-info {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
|
||||
.name {
|
||||
font-size: 0.95em;
|
||||
font-weight: 600;
|
||||
.name,
|
||||
.channels-count {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.25;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.channels-count {
|
||||
font-size: 0.74em;
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
// A wrapping subtitle pushes the trigger past the 56px header row.
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// On a phone the header row has no width to spare, and the shell allows this
|
||||
// trigger to shrink to 88px there. The decorative type icon goes so that the
|
||||
// trigger's fixed chrome (refresh, chevron, gaps, padding) actually fits that
|
||||
// floor with the name ellipsizing; the subtitle goes because the name is the
|
||||
// only part worth the room.
|
||||
@media (max-width: 640px) {
|
||||
.trigger-icon-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.playlist-info .channels-count {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.trigger-refresh-button {
|
||||
margin-left: auto;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
padding: 0;
|
||||
line-height: 32px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
transition:
|
||||
color 0.15s ease,
|
||||
background 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: var(--mat-sys-on-surface);
|
||||
background: var(--mat-sys-surface-container);
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-button-touch-target {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-button-persistent-ripple,
|
||||
::ng-deep .mat-mdc-focus-indicator {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.trigger-refresh-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.is-spinning {
|
||||
animation: switcher-refresh-spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes switcher-refresh-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
color: var(--app-body-color);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
margin-left: 4px;
|
||||
flex: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
color: var(--app-body-color);
|
||||
transition: transform 0.15s ease;
|
||||
|
||||
&.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.playlist-switcher-trigger,
|
||||
.dropdown-icon {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
// On a phone the header row has no width to spare and the trigger may
|
||||
// shrink to 88px: the tile and the subtitle go, the name ellipsizes.
|
||||
@media (max-width: 640px) {
|
||||
.playlist-switcher-trigger app-source-tile,
|
||||
.trigger-placeholder-tile,
|
||||
.playlist-info .channels-count {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .playlist-switcher-menu.mat-mdc-menu-panel {
|
||||
--mat-menu-container-color: var(--mat-sys-surface-container-high);
|
||||
width: min(
|
||||
@@ -270,11 +209,7 @@
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
margin: 0 8px 4px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--app-on-surface) 6%,
|
||||
transparent
|
||||
);
|
||||
background: color-mix(in srgb, var(--app-on-surface) 6%, transparent);
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
@@ -429,7 +364,8 @@
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
flex-shrink: 0;
|
||||
color: var(--mat-sys-on-surface-variant);padding: 0;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
padding: 0;
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
@@ -454,11 +390,7 @@
|
||||
}
|
||||
|
||||
.playlist-item:hover:not(.selected) {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--app-on-surface) 10%,
|
||||
transparent
|
||||
);
|
||||
background: color-mix(in srgb, var(--app-on-surface) 10%, transparent);
|
||||
}
|
||||
|
||||
.playlist-icon-container {
|
||||
@@ -469,11 +401,7 @@
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--app-on-surface) 6%,
|
||||
transparent
|
||||
);
|
||||
background: color-mix(in srgb, var(--app-on-surface) 6%, transparent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { PlaylistMeta } from '@iptvnator/shared/interfaces';
|
||||
import { PlaylistRefreshActionService } from '../playlist-refresh-action.service';
|
||||
import { PlaylistSwitcherComponent } from './playlist-switcher.component';
|
||||
import { PlaylistSwitcherOpenService } from './playlist-switcher-open.service';
|
||||
|
||||
const SEARCH_QUERY_STORAGE_KEY = 'playlist-switcher:search-query';
|
||||
const PLAYLIST_TYPE_FILTER_STORAGE_KEY = 'playlist-switcher:type-filters';
|
||||
@@ -447,4 +448,43 @@ describe('PlaylistSwitcherComponent', () => {
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('names the active source by its host, never by a credential-bearing URL', async () => {
|
||||
const urlNamed = createPlaylist({
|
||||
_id: 'url-named',
|
||||
count: 3220,
|
||||
title: 'get.php?username=joe&password=hunter2&type=m3u_plus',
|
||||
url: 'http://biggie.test:8080/get.php?username=joe&password=hunter2&type=m3u_plus',
|
||||
});
|
||||
resolvedPlaylistIdSignal.set(urlNamed._id);
|
||||
activePlaylistSignal.set(urlNamed);
|
||||
await createComponent();
|
||||
|
||||
const trigger = (fixture.nativeElement as HTMLElement).querySelector(
|
||||
'[data-test-id="playlist-switcher-trigger"]'
|
||||
) as HTMLElement;
|
||||
expect(trigger.querySelector('.name')?.textContent?.trim()).toBe(
|
||||
'biggie.test:8080'
|
||||
);
|
||||
expect(trigger.querySelector('.channels-count')?.textContent).toContain(
|
||||
'M3U · HOME.PLAYLISTS.CHANNELS_COUNT'
|
||||
);
|
||||
expect(trigger.textContent).not.toMatch(/hunter2|password=/);
|
||||
expect(trigger.getAttribute('aria-keyshortcuts')).toBe(
|
||||
'Meta+P Control+P'
|
||||
);
|
||||
});
|
||||
|
||||
it('opens on a ⌘P / Ctrl+P request while it is on screen', async () => {
|
||||
await createComponent();
|
||||
const switcherOpen = TestBed.inject(PlaylistSwitcherOpenService);
|
||||
expect(switcherOpen.available).toBe(true);
|
||||
|
||||
switcherOpen.request();
|
||||
fixture.detectChanges();
|
||||
expect(component.isMenuOpen()).toBe(true);
|
||||
|
||||
fixture.destroy();
|
||||
expect(switcherOpen.available).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -3,9 +3,14 @@ import {
|
||||
RuntimeCapabilitiesService,
|
||||
injectTranslationTick,
|
||||
} from '@iptvnator/services';
|
||||
import { SourceHealthService } from '@iptvnator/portal/shared/data-access';
|
||||
import {
|
||||
SourceHealthService,
|
||||
SourceStatusService,
|
||||
} from '@iptvnator/portal/shared/data-access';
|
||||
import { foldSearchText, sourceHealthType } from '@iptvnator/shared/interfaces';
|
||||
import { SourceHealthIndicatorComponent } from '../source-health/source-health-indicator.component';
|
||||
import { SourceTileComponent } from '../source-row/source-tile.component';
|
||||
import { PlaylistSwitcherOpenService } from './playlist-switcher-open.service';
|
||||
import { DatePipe, DOCUMENT } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@@ -34,7 +39,11 @@ import { normalizeDateLocale } from '@iptvnator/pipes';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { DialogService } from '@iptvnator/ui/components';
|
||||
import { PlaylistActions } from '@iptvnator/m3u-state';
|
||||
import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util';
|
||||
import {
|
||||
PlaylistContextFacade,
|
||||
sourceDisplayName,
|
||||
sourceMetaParts,
|
||||
} from '@iptvnator/playlist/shared/util';
|
||||
import {
|
||||
PlaylistDeleteActionService,
|
||||
PortalStatus,
|
||||
@@ -65,6 +74,7 @@ const DEFAULT_PLAYLIST_TYPE_FILTERS: Record<PlaylistFilterType, boolean> = {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
SourceHealthIndicatorComponent,
|
||||
SourceTileComponent,
|
||||
DatePipe,
|
||||
FormsModule,
|
||||
MatDivider,
|
||||
@@ -91,6 +101,7 @@ export class PlaylistSwitcherComponent {
|
||||
private readonly store = inject(Store);
|
||||
private focusSearchTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
private readonly languageTick = injectTranslationTick();
|
||||
private readonly statusService = inject(SourceStatusService);
|
||||
|
||||
readonly runtime = inject(RuntimeCapabilitiesService);
|
||||
private readonly healthInjector = inject(Injector);
|
||||
@@ -104,20 +115,11 @@ export class PlaylistSwitcherComponent {
|
||||
readonly showPlaylistInfo = input(false);
|
||||
readonly showAccountInfo = input(false);
|
||||
readonly showAddPlaylist = input(false);
|
||||
readonly canRefreshActivePlaylist = input(false);
|
||||
readonly isRefreshingActivePlaylist = input(false);
|
||||
readonly playlistSelected = output<string>();
|
||||
readonly playlistInfoRequested = output<void>();
|
||||
readonly accountInfoRequested = output<void>();
|
||||
readonly accountInfoForPlaylistRequested = output<PlaylistMeta>();
|
||||
readonly addPlaylistRequested = output<void>();
|
||||
readonly refreshPlaylistRequested = output<void>();
|
||||
|
||||
onRefreshClick(event: Event): void {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.refreshPlaylistRequested.emit();
|
||||
}
|
||||
|
||||
readonly menuTrigger = viewChild.required<MatMenuTrigger>('menuTrigger');
|
||||
readonly playlistMenu = viewChild.required<MatMenu>('playlistMenu');
|
||||
@@ -158,21 +160,42 @@ export class PlaylistSwitcherComponent {
|
||||
new Date(a.importDate ?? 0).getTime()
|
||||
);
|
||||
});
|
||||
/** The active source's name; never its URL or a credential. */
|
||||
readonly displayTitle = computed(() => {
|
||||
this.languageTick();
|
||||
if (!this.activePlaylistId()) {
|
||||
return this.translate.instant('HOME.PLAYLISTS.SELECT_PLAYLIST');
|
||||
}
|
||||
|
||||
const active = this.activePlaylist();
|
||||
return (
|
||||
this.currentTitle() ||
|
||||
this.activePlaylist()?.title ||
|
||||
this.activePlaylist()?.filename ||
|
||||
this.activePlaylist()?.url ||
|
||||
this.activePlaylist()?.portalUrl ||
|
||||
(active ? sourceDisplayName(active) : this.currentTitle()) ||
|
||||
this.translate.instant('HOME.PLAYLISTS.UNTITLED_PLAYLIST')
|
||||
);
|
||||
});
|
||||
/** "Stalker", "M3U · 3,220 channels", or the header's source count. */
|
||||
readonly triggerSubtitle = computed(() => {
|
||||
this.languageTick();
|
||||
const active = this.activePlaylist();
|
||||
if (!active) {
|
||||
return this.subtitle();
|
||||
}
|
||||
return sourceMetaParts(
|
||||
active,
|
||||
{
|
||||
translate: (key, params) => this.translate.instant(key, params),
|
||||
locale: this.currentLocale(),
|
||||
now: Date.now(),
|
||||
},
|
||||
true
|
||||
)
|
||||
.filter((part) => part.kind === 'type' || part.kind === 'channels')
|
||||
.map((part) => part.text)
|
||||
.join(' · ');
|
||||
});
|
||||
readonly activeStatus = computed(() => {
|
||||
const active = this.activePlaylist();
|
||||
return active ? this.statusService.statusOf(active) : null;
|
||||
});
|
||||
|
||||
readonly availablePlaylistTypes = computed(() => {
|
||||
const types = new Set<PlaylistFilterType>();
|
||||
@@ -210,6 +233,13 @@ export class PlaylistSwitcherComponent {
|
||||
});
|
||||
|
||||
constructor() {
|
||||
const openService = inject(PlaylistSwitcherOpenService);
|
||||
const unregister = openService.register();
|
||||
const requests = openService.openRequests.subscribe(() => this.open());
|
||||
this.destroyRef.onDestroy(() => {
|
||||
unregister();
|
||||
requests.unsubscribe();
|
||||
});
|
||||
this.destroyRef.onDestroy(() => {
|
||||
this.clearMenuOverlayWidth();
|
||||
this.clearSearchFocusTimeout();
|
||||
@@ -217,6 +247,11 @@ export class PlaylistSwitcherComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/** Opens the switcher (⌘P / Ctrl+P). */
|
||||
open(): void {
|
||||
this.menuTrigger().openMenu();
|
||||
}
|
||||
|
||||
onMenuOpened(): void {
|
||||
this.isMenuOpen.set(true);
|
||||
this.syncMenuOverlayWidthToTrigger();
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
export type KeyboardShortcutGroupId =
|
||||
| 'global'
|
||||
| 'navigation'
|
||||
| 'playback'
|
||||
| 'dialogs';
|
||||
'global' | 'navigation' | 'playback' | 'dialogs';
|
||||
|
||||
export interface PlatformShortcutChord {
|
||||
mac: readonly string[];
|
||||
@@ -10,9 +7,7 @@ export interface PlatformShortcutChord {
|
||||
}
|
||||
|
||||
export type KeyboardShortcutChord =
|
||||
| string
|
||||
| readonly string[]
|
||||
| PlatformShortcutChord;
|
||||
string | readonly string[] | PlatformShortcutChord;
|
||||
|
||||
export interface KeyboardShortcutDefinition {
|
||||
id: string;
|
||||
@@ -73,6 +68,14 @@ export const APP_KEYBOARD_SHORTCUTS: readonly KeyboardShortcutDefinition[] = [
|
||||
keys: [commandChord('K')],
|
||||
order: 10,
|
||||
},
|
||||
{
|
||||
id: 'open-playlist-switcher',
|
||||
group: 'global',
|
||||
labelKey: 'WORKSPACE.SHORTCUTS.ITEMS.OPEN_PLAYLIST_SWITCHER',
|
||||
icon: 'playlist_play',
|
||||
keys: [commandChord('P')],
|
||||
order: 15,
|
||||
},
|
||||
{
|
||||
id: 'open-global-search',
|
||||
group: 'global',
|
||||
|
||||
-3
@@ -62,15 +62,12 @@
|
||||
[showPlaylistInfo]="canOpenPlaylistInfo()"
|
||||
[showAccountInfo]="canOpenAccountInfo()"
|
||||
[showAddPlaylist]="true"
|
||||
[canRefreshActivePlaylist]="canRefreshPlaylist()"
|
||||
[isRefreshingActivePlaylist]="isRefreshingPlaylist()"
|
||||
(playlistInfoRequested)="onPlaylistInfoRequested()"
|
||||
(accountInfoRequested)="onAccountInfoRequested()"
|
||||
(accountInfoForPlaylistRequested)="
|
||||
onAccountInfoForPlaylistRequested($event)
|
||||
"
|
||||
(addPlaylistRequested)="onAddPlaylistRequested()"
|
||||
(refreshPlaylistRequested)="onRefreshPlaylistRequested()"
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
-3
@@ -34,12 +34,9 @@ class MockPlaylistSwitcherComponent {
|
||||
readonly showPlaylistInfo = input(false);
|
||||
readonly showAccountInfo = input(false);
|
||||
readonly showAddPlaylist = input(false);
|
||||
readonly canRefreshActivePlaylist = input(false);
|
||||
readonly isRefreshingActivePlaylist = input(false);
|
||||
readonly playlistInfoRequested = output<void>();
|
||||
readonly accountInfoRequested = output<void>();
|
||||
readonly addPlaylistRequested = output<void>();
|
||||
readonly refreshPlaylistRequested = output<void>();
|
||||
}
|
||||
|
||||
describe('WorkspaceShellHeaderComponent', () => {
|
||||
|
||||
-7
@@ -63,8 +63,6 @@ export class WorkspaceShellHeaderComponent {
|
||||
readonly headerSidebarToggle = input<WorkspaceHeaderSidebarToggle | null>(
|
||||
null
|
||||
);
|
||||
readonly canRefreshPlaylist = input(false);
|
||||
readonly isRefreshingPlaylist = input(false);
|
||||
readonly isElectron = input(false);
|
||||
readonly hasNoPlaylists = input(false);
|
||||
readonly isDownloadsView = input(false);
|
||||
@@ -144,7 +142,6 @@ export class WorkspaceShellHeaderComponent {
|
||||
readonly headerShortcutRequested = output<void>();
|
||||
readonly headerSidebarToggleRequested = output<void>();
|
||||
readonly parentalLockToggleRequested = output<void>();
|
||||
readonly refreshPlaylistRequested = output<void>();
|
||||
readonly downloadsRequested = output<void>();
|
||||
readonly playlistInfoRequested = output<void>();
|
||||
readonly accountInfoRequested = output<void>();
|
||||
@@ -231,10 +228,6 @@ export class WorkspaceShellHeaderComponent {
|
||||
this.headerSidebarToggleRequested.emit();
|
||||
}
|
||||
|
||||
onRefreshPlaylistRequested(): void {
|
||||
this.refreshPlaylistRequested.emit();
|
||||
}
|
||||
|
||||
onDownloadsRequested(): void {
|
||||
this.downloadsRequested.emit();
|
||||
}
|
||||
|
||||
-1
@@ -111,7 +111,6 @@ export class WorkspaceShellHeaderService {
|
||||
this.routeState.activePlaylist()
|
||||
)
|
||||
);
|
||||
readonly isRefreshingPlaylist = this.playlistRefreshAction.isRefreshing;
|
||||
readonly playlistSubtitle = computed(() => {
|
||||
this.languageTick();
|
||||
|
||||
|
||||
+27
@@ -1,3 +1,4 @@
|
||||
import { PlaylistSwitcherOpenService } from '@iptvnator/playlist/shared/ui';
|
||||
import { signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
@@ -1111,6 +1112,32 @@ describe('WorkspaceShellFacade', () => {
|
||||
expect(dialog.open).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('opens the playlist switcher on Ctrl/Cmd+P when one is on screen', () => {
|
||||
const switcher = TestBed.inject(PlaylistSwitcherOpenService);
|
||||
const opened = jest.fn();
|
||||
const subscription = switcher.openRequests.subscribe(opened);
|
||||
const press = () => {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'p',
|
||||
ctrlKey: true,
|
||||
cancelable: true,
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
return event;
|
||||
};
|
||||
|
||||
// Settings hides the switcher: the key keeps its default there.
|
||||
expect(press().defaultPrevented).toBe(false);
|
||||
expect(opened).not.toHaveBeenCalled();
|
||||
|
||||
const unregister = switcher.register();
|
||||
expect(press().defaultPrevented).toBe(true);
|
||||
expect(opened).toHaveBeenCalledTimes(1);
|
||||
|
||||
unregister();
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it('records the executed command id after the palette closes with a selection', () => {
|
||||
const dialog = TestBed.inject(MatDialog) as unknown as {
|
||||
open: jest.Mock;
|
||||
|
||||
+14
-2
@@ -26,6 +26,7 @@ import { WorkspaceShellRouteStateService } from './workspace-shell-route-state.s
|
||||
import { WorkspaceShellSearchService } from './workspace-shell-search.service';
|
||||
import { WorkspaceShellXtreamImportService } from './workspace-shell-xtream-import.service';
|
||||
import { WorkspaceShellContextDrawerService } from '@iptvnator/workspace/shell/util';
|
||||
import { PlaylistSwitcherOpenService } from '@iptvnator/playlist/shared/ui';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceShellFacade {
|
||||
@@ -56,6 +57,7 @@ export class WorkspaceShellFacade {
|
||||
optional: true,
|
||||
}
|
||||
);
|
||||
private readonly playlistSwitcher = inject(PlaylistSwitcherOpenService);
|
||||
private readonly onDocumentKeydown = (event: KeyboardEvent): void => {
|
||||
if (!(event.ctrlKey || event.metaKey)) {
|
||||
return;
|
||||
@@ -65,9 +67,20 @@ export class WorkspaceShellFacade {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key.toLowerCase() === 'k') {
|
||||
const key = event.key.toLowerCase();
|
||||
if (key === 'k') {
|
||||
event.preventDefault();
|
||||
this.openCommandPalette();
|
||||
} else if (
|
||||
key === 'p' &&
|
||||
!event.shiftKey &&
|
||||
!event.altKey &&
|
||||
this.playlistSwitcher.available
|
||||
) {
|
||||
// ⌘K stays the command palette; ⌘P is the playlist switcher (and
|
||||
// no longer prints the page in the browser build).
|
||||
event.preventDefault();
|
||||
this.playlistSwitcher.request();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -127,7 +140,6 @@ export class WorkspaceShellFacade {
|
||||
readonly canOpenPlaylistInfo = this.header.canOpenPlaylistInfo;
|
||||
readonly canOpenAccountInfo = this.header.canOpenAccountInfo;
|
||||
readonly canRefreshPlaylist = this.header.canRefreshPlaylist;
|
||||
readonly isRefreshingPlaylist = this.header.isRefreshingPlaylist;
|
||||
readonly headerSidebarToggle = this.header.headerSidebarToggle;
|
||||
readonly playlistSubtitle = this.header.playlistSubtitle;
|
||||
readonly activeDownloadsCount = computed(() =>
|
||||
|
||||
@@ -42,8 +42,6 @@
|
||||
[searchStatusLabel]="facade.searchStatusLabel()"
|
||||
[headerShortcut]="facade.headerShortcut()"
|
||||
[headerSidebarToggle]="facade.headerSidebarToggle()"
|
||||
[canRefreshPlaylist]="facade.canRefreshPlaylist()"
|
||||
[isRefreshingPlaylist]="facade.isRefreshingPlaylist()"
|
||||
[isElectron]="facade.isElectron"
|
||||
[hasNoPlaylists]="facade.hasNoPlaylists()"
|
||||
[isDownloadsView]="facade.isDownloadsView()"
|
||||
@@ -56,7 +54,6 @@
|
||||
[contextDrawerTooltipKey]="facade.contextDrawerLabelKeys().tooltip"
|
||||
(backRequested)="facade.goBack()"
|
||||
(contextDrawerToggleRequested)="contextDrawer.toggle()"
|
||||
(refreshPlaylistRequested)="facade.refreshCurrentPlaylist()"
|
||||
(searchChanged)="facade.onSearchInput($event)"
|
||||
(searchSubmitted)="facade.onSearchEnter($event)"
|
||||
(commandPaletteRequested)="facade.openCommandPalette()"
|
||||
|
||||
@@ -58,8 +58,6 @@ class MockWorkspaceShellHeaderComponent {
|
||||
readonly searchScopeLabel = input('');
|
||||
readonly searchStatusLabel = input('');
|
||||
readonly headerShortcut = input<unknown>(null);
|
||||
readonly canRefreshPlaylist = input(false);
|
||||
readonly isRefreshingPlaylist = input(false);
|
||||
readonly isElectron = input(false);
|
||||
readonly hasNoPlaylists = input(false);
|
||||
readonly isDownloadsView = input(false);
|
||||
@@ -78,7 +76,6 @@ class MockWorkspaceShellHeaderComponent {
|
||||
readonly shortcutsRequested = output<void>();
|
||||
readonly addPlaylistRequested = output<void>();
|
||||
readonly headerShortcutRequested = output<void>();
|
||||
readonly refreshPlaylistRequested = output<void>();
|
||||
readonly downloadsRequested = output<void>();
|
||||
readonly headerSidebarToggleRequested = output<void>();
|
||||
readonly parentalLockToggleRequested = output<void>();
|
||||
@@ -169,7 +166,6 @@ class MockWorkspaceShellFacade {
|
||||
readonly searchStatusLabel = signal('');
|
||||
readonly headerShortcut = signal(null);
|
||||
readonly canRefreshPlaylist = signal(false);
|
||||
readonly isRefreshingPlaylist = signal(false);
|
||||
readonly hasNoPlaylists = signal(false);
|
||||
readonly isDownloadsView = signal(false);
|
||||
readonly activeDownloadsCount = signal(3);
|
||||
|
||||
Reference in new issue
Block a user