mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
refactor: rework dashboard view
- Removed global favorites related properties and methods from WorkspaceShellHeaderComponent. - Updated WorkspaceShellFacade to streamline command palette commands and navigation. - Refactored command palette to include type-aware xtream import progress labels. - Enhanced WorkspaceShellComponent to display import progress with improved styling. - Updated tests to reflect changes in command palette and import progress handling.
This commit is contained in:
1 parent
fc3db71e85
commit
0e77f7b98a
69 files changed
+2603
-4939
No files matched your search
+8
-8
@@ -22,10 +22,10 @@
|
||||
<div class="palette-divider"></div>
|
||||
|
||||
<div class="palette-results">
|
||||
@for (group of commandGroups(); track group.scope) {
|
||||
@for (group of commandGroups(); track group.group) {
|
||||
<section class="palette-group">
|
||||
<h3 class="palette-group__title">
|
||||
{{ getScopeTitleKey(group.scope) | translate }}
|
||||
{{ getGroupTitleKey(group.group) | translate }}
|
||||
</h3>
|
||||
@for (command of group.items; track command.id) {
|
||||
<button
|
||||
@@ -37,16 +37,16 @@
|
||||
(click)="onCommandClick(command)"
|
||||
>
|
||||
<span class="palette-command__icon">
|
||||
<mat-icon>{{
|
||||
getCommandIcon(command)
|
||||
}}</mat-icon>
|
||||
<mat-icon>{{ command.icon }}</mat-icon>
|
||||
</span>
|
||||
<span class="palette-command__label">{{
|
||||
command.label
|
||||
}}</span>
|
||||
<span class="palette-command__desc">{{
|
||||
command.description
|
||||
}}</span>
|
||||
@if (command.description) {
|
||||
<span class="palette-command__desc">{{
|
||||
command.description
|
||||
}}</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</section>
|
||||
|
||||
+102
-18
@@ -2,17 +2,69 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { of } from 'rxjs';
|
||||
import { WorkspaceResolvedCommandItem } from '@iptvnator/portal/shared/util';
|
||||
import { WorkspaceCommandPaletteComponent } from './workspace-command-palette.component';
|
||||
|
||||
describe('WorkspaceCommandPaletteComponent', () => {
|
||||
let fixture: ComponentFixture<WorkspaceCommandPaletteComponent>;
|
||||
let component: WorkspaceCommandPaletteComponent;
|
||||
let dialogRef: { close: jest.Mock };
|
||||
let commands: WorkspaceResolvedCommandItem[];
|
||||
|
||||
beforeEach(async () => {
|
||||
dialogRef = {
|
||||
close: jest.fn(),
|
||||
};
|
||||
commands = [
|
||||
{
|
||||
id: 'global-search',
|
||||
label: 'Search all Xtream playlists',
|
||||
description: 'Open global search overlay',
|
||||
group: 'global',
|
||||
icon: 'search',
|
||||
keywords: ['xtream', 'global'],
|
||||
priority: 100,
|
||||
visible: true,
|
||||
enabled: false,
|
||||
run: () => undefined,
|
||||
},
|
||||
{
|
||||
id: 'playlist-search',
|
||||
label: 'Search this playlist',
|
||||
description: 'Open playlist search route',
|
||||
group: 'playlist',
|
||||
icon: 'playlist_play',
|
||||
keywords: ['playlist'],
|
||||
priority: 10,
|
||||
visible: true,
|
||||
enabled: true,
|
||||
run: () => undefined,
|
||||
},
|
||||
{
|
||||
id: 'multi-epg',
|
||||
label: 'Open Multi-EPG',
|
||||
description: '',
|
||||
group: 'view',
|
||||
icon: 'view_list',
|
||||
keywords: ['epg', 'guide'],
|
||||
priority: 0,
|
||||
visible: true,
|
||||
enabled: true,
|
||||
run: () => undefined,
|
||||
},
|
||||
{
|
||||
id: 'hidden-command',
|
||||
label: 'Hidden command',
|
||||
description: 'Should not appear',
|
||||
group: 'global',
|
||||
icon: 'visibility_off',
|
||||
keywords: [],
|
||||
priority: 200,
|
||||
visible: false,
|
||||
enabled: true,
|
||||
run: () => undefined,
|
||||
},
|
||||
];
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [WorkspaceCommandPaletteComponent],
|
||||
@@ -25,22 +77,7 @@ describe('WorkspaceCommandPaletteComponent', () => {
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {
|
||||
query: 'search',
|
||||
commands: [
|
||||
{
|
||||
id: 'global-search',
|
||||
label: 'Search all Xtream playlists',
|
||||
description: 'Open global search overlay',
|
||||
scope: 'global',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'playlist-search',
|
||||
label: 'Search this playlist',
|
||||
description: 'Open playlist search route',
|
||||
scope: 'playlist',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
commands,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -69,13 +106,60 @@ describe('WorkspaceCommandPaletteComponent', () => {
|
||||
expect(component.query()).toBe('search');
|
||||
});
|
||||
|
||||
it('selects the first enabled command by default', () => {
|
||||
expect(component.selectedIndex()).toBe(0);
|
||||
expect(component.flatCommands()[0].id).toBe('playlist-search');
|
||||
});
|
||||
|
||||
it('hides non-visible commands and omits empty groups', () => {
|
||||
expect(component.flatCommands().map((command) => command.id)).toEqual([
|
||||
'playlist-search',
|
||||
'global-search',
|
||||
]);
|
||||
expect(component.commandGroups().map((group) => group.group)).toEqual([
|
||||
'playlist',
|
||||
'global',
|
||||
]);
|
||||
});
|
||||
|
||||
it('filters commands by keywords', () => {
|
||||
component.query.set('guide');
|
||||
|
||||
expect(component.flatCommands().map((command) => command.id)).toEqual([
|
||||
'multi-epg',
|
||||
]);
|
||||
});
|
||||
|
||||
it('skips disabled commands during keyboard navigation', () => {
|
||||
component.query.set('');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.flatCommands().map((command) => command.id)).toEqual([
|
||||
'multi-epg',
|
||||
'playlist-search',
|
||||
'global-search',
|
||||
]);
|
||||
expect(component.selectedIndex()).toBe(0);
|
||||
|
||||
component.onInputKeydown(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowDown' })
|
||||
);
|
||||
expect(component.selectedIndex()).toBe(1);
|
||||
|
||||
component.onInputKeydown(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowDown' })
|
||||
);
|
||||
expect(component.selectedIndex()).toBe(0);
|
||||
});
|
||||
|
||||
it('closes with selected command and query on click', () => {
|
||||
component.query.set('');
|
||||
const command = component.flatCommands()[0];
|
||||
component.onCommandClick(command);
|
||||
|
||||
expect(dialogRef.close).toHaveBeenCalledWith({
|
||||
commandId: 'global-search',
|
||||
query: 'search',
|
||||
commandId: 'multi-epg',
|
||||
query: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
+86
-112
@@ -15,47 +15,20 @@ import {
|
||||
} from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
|
||||
export type WorkspaceCommandScope = 'global' | 'playlist' | 'section';
|
||||
|
||||
export type WorkspaceCommandId =
|
||||
| 'global-search'
|
||||
| 'playlist-search'
|
||||
| 'open-global-favorites'
|
||||
| 'open-downloads'
|
||||
| 'open-global-recent'
|
||||
| 'open-settings'
|
||||
| 'open-sources'
|
||||
| 'open-dashboard'
|
||||
| 'add-playlist'
|
||||
| 'refresh-playlist'
|
||||
| 'playlist-info'
|
||||
| 'account-info'
|
||||
| 'go-to-vod'
|
||||
| 'go-to-live'
|
||||
| 'go-to-series';
|
||||
|
||||
export interface WorkspaceCommandItem {
|
||||
id: WorkspaceCommandId;
|
||||
label: string;
|
||||
description: string;
|
||||
scope: WorkspaceCommandScope;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface WorkspaceCommandSelection {
|
||||
commandId: WorkspaceCommandId;
|
||||
query: string;
|
||||
}
|
||||
import {
|
||||
WorkspaceCommandGroup,
|
||||
WorkspaceCommandSelection,
|
||||
WorkspaceResolvedCommandItem,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
|
||||
interface WorkspaceCommandPaletteData {
|
||||
commands: WorkspaceCommandItem[];
|
||||
commands: WorkspaceResolvedCommandItem[];
|
||||
query?: string;
|
||||
}
|
||||
|
||||
interface WorkspaceCommandGroup {
|
||||
scope: WorkspaceCommandScope;
|
||||
items: WorkspaceCommandItem[];
|
||||
interface WorkspaceCommandGroupSection {
|
||||
group: WorkspaceCommandGroup;
|
||||
items: WorkspaceResolvedCommandItem[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -80,50 +53,72 @@ export class WorkspaceCommandPaletteComponent implements AfterViewInit {
|
||||
readonly query = signal(this.data?.query ?? '');
|
||||
readonly selectedIndex = signal(0);
|
||||
|
||||
readonly visibleCommands = computed(() =>
|
||||
this.data.commands.filter((command) => command.visible)
|
||||
);
|
||||
|
||||
readonly filteredCommands = computed(() => {
|
||||
const term = this.query().trim().toLowerCase();
|
||||
const commands = this.visibleCommands();
|
||||
|
||||
if (!term) {
|
||||
return this.data.commands;
|
||||
return commands;
|
||||
}
|
||||
|
||||
return this.data.commands.filter((command) => {
|
||||
const haystack =
|
||||
`${command.label} ${command.description}`.toLowerCase();
|
||||
return commands.filter((command) => {
|
||||
const haystack = [
|
||||
command.label,
|
||||
command.description,
|
||||
...(command.keywords ?? []),
|
||||
]
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
|
||||
return haystack.includes(term);
|
||||
});
|
||||
});
|
||||
|
||||
readonly commandGroups = computed<WorkspaceCommandGroup[]>(() => {
|
||||
readonly commandGroups = computed<WorkspaceCommandGroupSection[]>(() => {
|
||||
const commands = this.filteredCommands();
|
||||
const grouped: WorkspaceCommandGroup[] = [];
|
||||
const sections: WorkspaceCommandGroupSection[] = [];
|
||||
|
||||
const buildSection = (
|
||||
group: WorkspaceCommandGroup
|
||||
): WorkspaceCommandGroupSection | null => {
|
||||
const items = commands.filter((command) => command.group === group);
|
||||
|
||||
const buildGroup = (
|
||||
scope: WorkspaceCommandScope
|
||||
): WorkspaceCommandGroup | null => {
|
||||
const items = commands.filter((command) => command.scope === scope);
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return { scope, items };
|
||||
|
||||
return { group, items };
|
||||
};
|
||||
|
||||
const groups = [
|
||||
buildGroup('global'),
|
||||
buildGroup('playlist'),
|
||||
buildGroup('section'),
|
||||
].filter((group): group is WorkspaceCommandGroup => group !== null);
|
||||
buildSection('view'),
|
||||
buildSection('playlist'),
|
||||
buildSection('global'),
|
||||
].filter(
|
||||
(group): group is WorkspaceCommandGroupSection => group !== null
|
||||
);
|
||||
|
||||
grouped.push(...groups);
|
||||
return grouped;
|
||||
sections.push(...groups);
|
||||
return sections;
|
||||
});
|
||||
|
||||
readonly flatCommands = computed(() =>
|
||||
this.commandGroups().reduce<WorkspaceCommandItem[]>(
|
||||
this.commandGroups().reduce<WorkspaceResolvedCommandItem[]>(
|
||||
(items, group) => items.concat(group.items),
|
||||
[]
|
||||
)
|
||||
);
|
||||
|
||||
readonly selectableIndices = computed(() =>
|
||||
this.flatCommands()
|
||||
.map((command, index) => (command.enabled ? index : -1))
|
||||
.filter((index) => index >= 0)
|
||||
);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const items = this.flatCommands();
|
||||
@@ -132,9 +127,15 @@ export class WorkspaceCommandPaletteComponent implements AfterViewInit {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectableIndices = this.selectableIndices();
|
||||
if (selectableIndices.length === 0) {
|
||||
this.selectedIndex.set(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIndex = this.selectedIndex();
|
||||
if (currentIndex < 0 || currentIndex >= items.length) {
|
||||
this.selectedIndex.set(0);
|
||||
if (!selectableIndices.includes(currentIndex)) {
|
||||
this.selectedIndex.set(selectableIndices[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -176,7 +177,11 @@ export class WorkspaceCommandPaletteComponent implements AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
onCommandHover(command: WorkspaceCommandItem): void {
|
||||
onCommandHover(command: WorkspaceResolvedCommandItem): void {
|
||||
if (!command.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this.flatCommands().findIndex(
|
||||
(item) => item.id === command.id
|
||||
);
|
||||
@@ -185,7 +190,7 @@ export class WorkspaceCommandPaletteComponent implements AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
onCommandClick(command: WorkspaceCommandItem): void {
|
||||
onCommandClick(command: WorkspaceResolvedCommandItem): void {
|
||||
if (!command.enabled) {
|
||||
return;
|
||||
}
|
||||
@@ -196,23 +201,39 @@ export class WorkspaceCommandPaletteComponent implements AfterViewInit {
|
||||
});
|
||||
}
|
||||
|
||||
isCommandSelected(command: WorkspaceCommandItem): boolean {
|
||||
isCommandSelected(command: WorkspaceResolvedCommandItem): boolean {
|
||||
const index = this.flatCommands().findIndex(
|
||||
(item) => item.id === command.id
|
||||
);
|
||||
return index >= 0 && this.selectedIndex() === index;
|
||||
}
|
||||
|
||||
getGroupTitleKey(group: WorkspaceCommandGroup): string {
|
||||
if (group === 'view') {
|
||||
return 'WORKSPACE.COMMAND_PALETTE.GROUP_VIEW';
|
||||
}
|
||||
if (group === 'playlist') {
|
||||
return 'WORKSPACE.COMMAND_PALETTE.GROUP_PLAYLIST';
|
||||
}
|
||||
return 'WORKSPACE.COMMAND_PALETTE.GROUP_GLOBAL';
|
||||
}
|
||||
|
||||
private moveSelection(direction: 1 | -1): void {
|
||||
const items = this.flatCommands();
|
||||
if (items.length === 0) {
|
||||
const selectableIndices = this.selectableIndices();
|
||||
if (selectableIndices.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIndex = this.selectedIndex();
|
||||
const safeIndex = currentIndex < 0 ? 0 : currentIndex;
|
||||
const nextIndex = (safeIndex + direction + items.length) % items.length;
|
||||
this.selectedIndex.set(nextIndex);
|
||||
const currentPosition = selectableIndices.indexOf(this.selectedIndex());
|
||||
const nextPosition =
|
||||
currentPosition === -1
|
||||
? direction === 1
|
||||
? 0
|
||||
: selectableIndices.length - 1
|
||||
: (currentPosition + direction + selectableIndices.length) %
|
||||
selectableIndices.length;
|
||||
|
||||
this.selectedIndex.set(selectableIndices[nextPosition]);
|
||||
}
|
||||
|
||||
private selectCurrent(): void {
|
||||
@@ -228,51 +249,4 @@ export class WorkspaceCommandPaletteComponent implements AfterViewInit {
|
||||
|
||||
this.onCommandClick(items[index]);
|
||||
}
|
||||
|
||||
getScopeTitleKey(scope: WorkspaceCommandScope): string {
|
||||
if (scope === 'global') {
|
||||
return 'WORKSPACE.COMMAND_PALETTE.GROUP_GLOBAL';
|
||||
}
|
||||
if (scope === 'playlist') {
|
||||
return 'WORKSPACE.COMMAND_PALETTE.GROUP_PLAYLIST';
|
||||
}
|
||||
return 'WORKSPACE.COMMAND_PALETTE.GROUP_SECTION';
|
||||
}
|
||||
|
||||
getCommandIcon(command: WorkspaceCommandItem): string {
|
||||
switch (command.id) {
|
||||
case 'global-search':
|
||||
return 'search';
|
||||
case 'playlist-search':
|
||||
return 'playlist_play';
|
||||
case 'open-global-favorites':
|
||||
return 'star';
|
||||
case 'open-downloads':
|
||||
return 'download';
|
||||
case 'open-global-recent':
|
||||
return 'history';
|
||||
case 'open-settings':
|
||||
return 'settings';
|
||||
case 'open-sources':
|
||||
return 'library_books';
|
||||
case 'open-dashboard':
|
||||
return 'dashboard';
|
||||
case 'add-playlist':
|
||||
return 'add_circle_outline';
|
||||
case 'refresh-playlist':
|
||||
return 'sync';
|
||||
case 'playlist-info':
|
||||
return 'info';
|
||||
case 'account-info':
|
||||
return 'account_circle';
|
||||
case 'go-to-vod':
|
||||
return 'movie';
|
||||
case 'go-to-live':
|
||||
return 'live_tv';
|
||||
case 'go-to-series':
|
||||
return 'video_library';
|
||||
default:
|
||||
return 'bolt';
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-16
@@ -75,22 +75,9 @@
|
||||
<mat-icon>sync</mat-icon>
|
||||
</button>
|
||||
}
|
||||
<span class="header-actions-divider" aria-hidden="true"></span>
|
||||
<button
|
||||
type="button"
|
||||
class="header-shortcut"
|
||||
mat-icon-button
|
||||
[class.is-active]="isGlobalFavoritesActive()"
|
||||
(click)="onGlobalFavoritesRequested()"
|
||||
[matTooltip]="
|
||||
'WORKSPACE.SHELL.OPEN_GLOBAL_FAVORITES' | translate
|
||||
"
|
||||
[attr.aria-label]="
|
||||
'WORKSPACE.SHELL.OPEN_GLOBAL_FAVORITES' | translate
|
||||
"
|
||||
>
|
||||
<mat-icon>star</mat-icon>
|
||||
</button>
|
||||
@if (headerShortcut() || isElectron()) {
|
||||
<span class="header-actions-divider" aria-hidden="true"></span>
|
||||
}
|
||||
@if (headerShortcut(); as action) {
|
||||
<button
|
||||
type="button"
|
||||
|
||||
+31
-8
@@ -23,8 +23,10 @@ class MockPlaylistSwitcherComponent {
|
||||
readonly subtitle = input('');
|
||||
readonly showPlaylistInfo = input(false);
|
||||
readonly showAccountInfo = input(false);
|
||||
readonly showAddPlaylist = input(false);
|
||||
readonly playlistInfoRequested = output<void>();
|
||||
readonly accountInfoRequested = output<void>();
|
||||
readonly addPlaylistRequested = output<void>();
|
||||
}
|
||||
|
||||
describe('WorkspaceShellHeaderComponent', () => {
|
||||
@@ -88,19 +90,40 @@ describe('WorkspaceShellHeaderComponent', () => {
|
||||
expect(emitted).toEqual(['matrix']);
|
||||
});
|
||||
|
||||
it('emits header bulk action requests when the action button is clicked', () => {
|
||||
it('emits add playlist requests when the toolbar add button is clicked', () => {
|
||||
const requested = jest.fn();
|
||||
component.headerBulkActionRequested.subscribe(requested);
|
||||
fixture.componentRef.setInput('headerBulkAction', {
|
||||
icon: 'delete_sweep',
|
||||
tooltip: 'clear',
|
||||
ariaLabel: 'clear recent',
|
||||
disabled: false,
|
||||
component.addPlaylistRequested.subscribe(requested);
|
||||
|
||||
const button: HTMLButtonElement = fixture.nativeElement.querySelector(
|
||||
'button[aria-label="WORKSPACE.SHELL.ADD_PLAYLIST"]'
|
||||
);
|
||||
button.click();
|
||||
|
||||
expect(requested).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not render the removed global favorites shortcut', () => {
|
||||
const button: HTMLButtonElement | null =
|
||||
fixture.nativeElement.querySelector(
|
||||
'button[aria-label="WORKSPACE.SHELL.OPEN_GLOBAL_FAVORITES"]'
|
||||
);
|
||||
|
||||
expect(button).toBeNull();
|
||||
});
|
||||
|
||||
it('emits contextual header shortcut requests when configured', () => {
|
||||
const requested = jest.fn();
|
||||
component.headerShortcutRequested.subscribe(requested);
|
||||
fixture.componentRef.setInput('headerShortcut', {
|
||||
icon: 'tune',
|
||||
tooltipKey: 'shortcut.tooltip',
|
||||
ariaLabelKey: 'shortcut.aria',
|
||||
run: () => undefined,
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
const button: HTMLButtonElement = fixture.nativeElement.querySelector(
|
||||
'button[aria-label="clear recent"]'
|
||||
'button[aria-label="shortcut.aria"]'
|
||||
);
|
||||
button.click();
|
||||
|
||||
|
||||
-6
@@ -39,7 +39,6 @@ export class WorkspaceShellHeaderComponent {
|
||||
readonly searchPlaceholder = input('');
|
||||
readonly searchScopeLabel = input('');
|
||||
readonly searchStatusLabel = input('');
|
||||
readonly isGlobalFavoritesActive = input(false);
|
||||
readonly headerShortcut = input<WorkspaceHeaderAction | null>(null);
|
||||
readonly canRefreshPlaylist = input(false);
|
||||
readonly isRefreshingPlaylist = input(false);
|
||||
@@ -51,7 +50,6 @@ export class WorkspaceShellHeaderComponent {
|
||||
readonly searchSubmitted = output<string>();
|
||||
readonly commandPaletteRequested = output<void>();
|
||||
readonly addPlaylistRequested = output<void>();
|
||||
readonly globalFavoritesRequested = output<void>();
|
||||
readonly headerShortcutRequested = output<void>();
|
||||
readonly refreshPlaylistRequested = output<void>();
|
||||
readonly downloadsRequested = output<void>();
|
||||
@@ -84,10 +82,6 @@ export class WorkspaceShellHeaderComponent {
|
||||
this.commandPaletteRequested.emit();
|
||||
}
|
||||
|
||||
onGlobalFavoritesRequested(): void {
|
||||
this.globalFavoritesRequested.emit();
|
||||
}
|
||||
|
||||
onHeaderShortcutRequested(): void {
|
||||
this.headerShortcutRequested.emit();
|
||||
}
|
||||
|
||||
+154
-10
@@ -6,7 +6,11 @@ import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { of } from 'rxjs';
|
||||
import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util';
|
||||
import { PORTAL_EXTERNAL_PLAYBACK } from '@iptvnator/portal/shared/util';
|
||||
import {
|
||||
PORTAL_EXTERNAL_PLAYBACK,
|
||||
WorkspaceHeaderContextService,
|
||||
WorkspaceViewCommandService,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
|
||||
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
|
||||
import {
|
||||
@@ -25,8 +29,15 @@ class MockXtreamStore {
|
||||
readonly searchTerm = signal('');
|
||||
readonly categorySearchTerm = signal('');
|
||||
readonly isImporting = signal(false);
|
||||
readonly isCancellingImport = signal(false);
|
||||
readonly activeImportSessionId = signal<string | null>(null);
|
||||
readonly currentImportPhase = signal<string | null>(null);
|
||||
readonly activeImportContentType = signal<'live' | 'vod' | 'series' | null>(
|
||||
null
|
||||
);
|
||||
readonly activeImportCurrentCount = signal(0);
|
||||
readonly activeImportTotalCount = signal(0);
|
||||
readonly getImportCount = signal(0);
|
||||
readonly itemsToImport = signal(0);
|
||||
readonly getSelectedCategory = signal<{
|
||||
category_name?: string;
|
||||
@@ -99,7 +110,9 @@ describe('WorkspaceShellFacade', () => {
|
||||
openAccountInfo: jest.Mock;
|
||||
};
|
||||
let storeDispatch: jest.Mock;
|
||||
let activePlaylistSignal: ReturnType<typeof signal<PlaylistSignalMeta>>;
|
||||
let activePlaylistSignal: ReturnType<
|
||||
typeof signal<PlaylistSignalMeta | null>
|
||||
>;
|
||||
let playlistsSignal: ReturnType<typeof signal<PlaylistSignalMeta[]>>;
|
||||
let stalkerStore: MockStalkerStore;
|
||||
let showDashboardSignal: ReturnType<typeof signal<boolean>>;
|
||||
@@ -230,7 +243,19 @@ describe('WorkspaceShellFacade', () => {
|
||||
{
|
||||
provide: TranslateService,
|
||||
useValue: {
|
||||
instant: (key: string) => key,
|
||||
instant: (
|
||||
key: string,
|
||||
params?: Record<string, string | number>
|
||||
) => {
|
||||
if (
|
||||
key === 'WORKSPACE.SHELL.XTREAM_IMPORT_PROGRESS' &&
|
||||
params
|
||||
) {
|
||||
return `${params.type} imported: ${params.current} / ${params.total}`;
|
||||
}
|
||||
|
||||
return key;
|
||||
},
|
||||
get: (key: string) => of(key),
|
||||
stream: (key: string) => of(key),
|
||||
onLangChange: of(null),
|
||||
@@ -299,6 +324,21 @@ describe('WorkspaceShellFacade', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('builds a type-aware xtream import progress label', () => {
|
||||
const xtreamStore = TestBed.inject(XtreamStore) as unknown as MockXtreamStore;
|
||||
|
||||
xtreamStore.activeImportContentType.set('vod');
|
||||
xtreamStore.activeImportCurrentCount.set(20);
|
||||
xtreamStore.activeImportTotalCount.set(12323);
|
||||
|
||||
expect(facade.xtreamImportTypeLabel()).toBe(
|
||||
'WORKSPACE.SHELL.RAIL_MOVIES'
|
||||
);
|
||||
expect(facade.xtreamImportProgressLabel()).toBe(
|
||||
'WORKSPACE.SHELL.RAIL_MOVIES imported: 20 / 12,323'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps stalker local-filter routes on the same page and syncs q', () => {
|
||||
router.navigate.mockClear();
|
||||
router.navigateByUrl.mockClear();
|
||||
@@ -329,18 +369,15 @@ describe('WorkspaceShellFacade', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('prefers provider-scoped global favorites when rail context exists', () => {
|
||||
it('navigates to the global favorites route', () => {
|
||||
router.navigate.mockClear();
|
||||
facade.currentUrl.set('/workspace/xtreams/pl-1/vod');
|
||||
|
||||
facade.navigateToGlobalFavorites();
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith(
|
||||
['/workspace', 'xtreams', 'pl-1', 'favorites'],
|
||||
{
|
||||
queryParams: { scope: 'all' },
|
||||
}
|
||||
);
|
||||
expect(router.navigate).toHaveBeenCalledWith([
|
||||
'/workspace/global-favorites',
|
||||
]);
|
||||
});
|
||||
|
||||
it('clears stalker recent items and refreshes the route', async () => {
|
||||
@@ -410,6 +447,18 @@ describe('WorkspaceShellFacade', () => {
|
||||
tooltip: 'WORKSPACE.SHELL.RAIL_SOURCES',
|
||||
path: ['/workspace/sources'],
|
||||
},
|
||||
{
|
||||
icon: 'favorite',
|
||||
tooltip: 'WORKSPACE.SHELL.RAIL_GLOBAL_FAVORITES',
|
||||
path: ['/workspace/global-favorites'],
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
icon: 'history',
|
||||
tooltip: 'WORKSPACE.SHELL.RAIL_GLOBAL_RECENT',
|
||||
path: ['/workspace/global-recent'],
|
||||
exact: true,
|
||||
},
|
||||
]);
|
||||
expect(facade.brandLink()).toBe('/workspace/sources');
|
||||
});
|
||||
@@ -426,4 +475,99 @@ describe('WorkspaceShellFacade', () => {
|
||||
|
||||
expect(facade.searchScopeLabel()).toBe('PORTALS.RECENTLY_VIEWED');
|
||||
});
|
||||
|
||||
it('shows only actionable global commands on an empty dashboard', () => {
|
||||
activePlaylistSignal.set(null);
|
||||
playlistsSignal.set([]);
|
||||
facade.currentUrl.set('/workspace/dashboard');
|
||||
|
||||
const commands = facade.commandPaletteCommands();
|
||||
|
||||
expect(commands.map((command) => command.id)).toEqual([
|
||||
'open-global-favorites',
|
||||
'open-global-recent',
|
||||
'open-downloads',
|
||||
'open-settings',
|
||||
'open-sources',
|
||||
'add-playlist',
|
||||
]);
|
||||
expect(commands.every((command) => command.group === 'global')).toBe(
|
||||
true
|
||||
);
|
||||
expect(commands.every((command) => command.enabled)).toBe(true);
|
||||
});
|
||||
|
||||
it('includes M3U navigation, playlist actions, and Multi-EPG on playlist routes', () => {
|
||||
const headerContext = TestBed.inject(WorkspaceHeaderContextService);
|
||||
|
||||
activePlaylistSignal.set({
|
||||
_id: 'pl-m3u',
|
||||
title: 'Playlist M3U',
|
||||
count: 10,
|
||||
importDate: '2026-04-22T10:00:00.000Z',
|
||||
autoRefresh: false,
|
||||
});
|
||||
facade.currentUrl.set('/workspace/playlists/pl-m3u/groups');
|
||||
headerContext.setAction({
|
||||
id: 'm3u-multi-epg',
|
||||
icon: 'view_list',
|
||||
tooltipKey: 'TOP_MENU.OPEN_MULTI_EPG',
|
||||
ariaLabelKey: 'TOP_MENU.OPEN_MULTI_EPG',
|
||||
palette: {
|
||||
labelKey: 'TOP_MENU.OPEN_MULTI_EPG',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.OPEN_MULTI_EPG_DESCRIPTION',
|
||||
keywords: ['epg', 'guide', 'schedule'],
|
||||
priority: 10,
|
||||
},
|
||||
run: jest.fn(),
|
||||
});
|
||||
|
||||
const commands = facade.commandPaletteCommands();
|
||||
|
||||
expect(commands.map((command) => command.id)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'm3u-multi-epg',
|
||||
'go-to-all',
|
||||
'go-to-favorites',
|
||||
'go-to-recent',
|
||||
'playlist-info',
|
||||
])
|
||||
);
|
||||
expect(
|
||||
commands.find((command) => command.id === 'm3u-multi-epg')?.group
|
||||
).toBe('view');
|
||||
expect(commands.some((command) => command.id === 'account-info')).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('places registered current-view commands ahead of globals on global favorites routes', () => {
|
||||
const viewCommands = TestBed.inject(WorkspaceViewCommandService);
|
||||
const clearCurrent = jest.fn();
|
||||
const unregister = viewCommands.registerCommand({
|
||||
id: 'clear-current-favorites',
|
||||
group: 'view',
|
||||
icon: 'delete_sweep',
|
||||
labelKey: 'WORKSPACE.SHELL.CLEAR_FAVORITES_TYPE',
|
||||
labelParams: () => ({ type: 'Live TV' }),
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.CLEAR_CURRENT_VIEW_DESCRIPTION',
|
||||
descriptionParams: () => ({ type: 'Live TV' }),
|
||||
priority: 10,
|
||||
run: clearCurrent,
|
||||
});
|
||||
|
||||
facade.currentUrl.set('/workspace/global-favorites');
|
||||
|
||||
const commands = facade.commandPaletteCommands();
|
||||
|
||||
expect(commands[0]?.id).toBe('clear-current-favorites');
|
||||
expect(commands[0]?.group).toBe('view');
|
||||
expect(commands.some((command) => command.id === 'playlist-search')).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
unregister();
|
||||
});
|
||||
});
|
||||
+443
-279
@@ -22,8 +22,12 @@ import {
|
||||
PORTAL_EXTERNAL_PLAYBACK,
|
||||
PortalRailLink,
|
||||
PortalRailSection,
|
||||
WorkspaceCommandContribution,
|
||||
WorkspaceCommandSelection,
|
||||
WorkspaceHeaderAction,
|
||||
WorkspaceHeaderContextService,
|
||||
WorkspaceResolvedCommandItem,
|
||||
WorkspaceViewCommandService,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
|
||||
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
|
||||
@@ -41,13 +45,7 @@ import {
|
||||
WorkspaceAccountInfoData,
|
||||
WORKSPACE_SHELL_ACTIONS,
|
||||
} from '@iptvnator/workspace/shell/util';
|
||||
import {
|
||||
WorkspaceCommandId,
|
||||
WorkspaceCommandItem,
|
||||
WorkspaceCommandPaletteComponent,
|
||||
WorkspaceCommandScope,
|
||||
WorkspaceCommandSelection,
|
||||
} from '../../workspace-command-palette/workspace-command-palette.component';
|
||||
import { WorkspaceCommandPaletteComponent } from '../../workspace-command-palette/workspace-command-palette.component';
|
||||
|
||||
export interface WorkspaceHeaderBulkAction {
|
||||
icon: string;
|
||||
@@ -56,25 +54,6 @@ export interface WorkspaceHeaderBulkAction {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
interface WorkspaceCommandAvailability {
|
||||
context: WorkspacePortalContext | null;
|
||||
dashboardXtreamContext: WorkspacePortalContext | null;
|
||||
hasXtreamPlaylists: boolean;
|
||||
isElectron: boolean;
|
||||
canRefreshPlaylist: boolean;
|
||||
hasActivePlaylist: boolean;
|
||||
isXtreamPlaylist: boolean;
|
||||
isPortalPlaylist: boolean;
|
||||
}
|
||||
|
||||
interface WorkspaceCommandDefinition {
|
||||
id: WorkspaceCommandId;
|
||||
labelKey: string;
|
||||
descriptionKey: string;
|
||||
scope: WorkspaceCommandScope;
|
||||
isEnabled: (state: WorkspaceCommandAvailability) => boolean;
|
||||
}
|
||||
|
||||
type XtreamImportPhaseTone = 'remote' | 'local' | null;
|
||||
|
||||
const SEARCH_INPUT_DEBOUNCE_MS = 350;
|
||||
@@ -104,123 +83,6 @@ const RAIL_TOOLTIP_KEYS: Readonly<Partial<Record<PortalRailSection, string>>> =
|
||||
groups: 'WORKSPACE.SHELL.RAIL_GROUPS',
|
||||
};
|
||||
|
||||
const WORKSPACE_COMMAND_DEFINITIONS: readonly WorkspaceCommandDefinition[] = [
|
||||
{
|
||||
id: 'global-search',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.GLOBAL_SEARCH_LABEL',
|
||||
descriptionKey: 'WORKSPACE.SHELL.COMMANDS.GLOBAL_SEARCH_DESCRIPTION',
|
||||
scope: 'global',
|
||||
isEnabled: (state) => state.hasXtreamPlaylists,
|
||||
},
|
||||
{
|
||||
id: 'playlist-search',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.PLAYLIST_SEARCH_LABEL',
|
||||
descriptionKey: 'WORKSPACE.SHELL.COMMANDS.PLAYLIST_SEARCH_DESCRIPTION',
|
||||
scope: 'playlist',
|
||||
isEnabled: (state) =>
|
||||
Boolean(
|
||||
state.dashboardXtreamContext ||
|
||||
(state.context &&
|
||||
(state.context.provider === 'xtreams' ||
|
||||
state.context.provider === 'stalker'))
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'open-global-favorites',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_GLOBAL_FAVORITES_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.OPEN_GLOBAL_FAVORITES_DESCRIPTION',
|
||||
scope: 'global',
|
||||
isEnabled: () => true,
|
||||
},
|
||||
{
|
||||
id: 'open-downloads',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_DOWNLOADS_LABEL',
|
||||
descriptionKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_DOWNLOADS_DESCRIPTION',
|
||||
scope: 'global',
|
||||
isEnabled: (state) => state.isElectron,
|
||||
},
|
||||
{
|
||||
id: 'open-global-recent',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_GLOBAL_RECENT_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.OPEN_GLOBAL_RECENT_DESCRIPTION',
|
||||
scope: 'global',
|
||||
isEnabled: () => true,
|
||||
},
|
||||
{
|
||||
id: 'open-settings',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_SETTINGS_LABEL',
|
||||
descriptionKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_SETTINGS_DESCRIPTION',
|
||||
scope: 'global',
|
||||
isEnabled: () => true,
|
||||
},
|
||||
{
|
||||
id: 'open-sources',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_SOURCES_LABEL',
|
||||
descriptionKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_SOURCES_DESCRIPTION',
|
||||
scope: 'global',
|
||||
isEnabled: () => true,
|
||||
},
|
||||
{
|
||||
id: 'open-dashboard',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_DASHBOARD_LABEL',
|
||||
descriptionKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_DASHBOARD_DESCRIPTION',
|
||||
scope: 'global',
|
||||
isEnabled: () => true,
|
||||
},
|
||||
{
|
||||
id: 'add-playlist',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_LABEL',
|
||||
descriptionKey: 'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_DESCRIPTION',
|
||||
scope: 'global',
|
||||
isEnabled: () => true,
|
||||
},
|
||||
{
|
||||
id: 'refresh-playlist',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.REFRESH_PLAYLIST_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.REFRESH_PLAYLIST_DESCRIPTION',
|
||||
scope: 'playlist',
|
||||
isEnabled: (state) => state.canRefreshPlaylist,
|
||||
},
|
||||
{
|
||||
id: 'playlist-info',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.PLAYLIST_INFO_LABEL',
|
||||
descriptionKey: 'WORKSPACE.SHELL.COMMANDS.PLAYLIST_INFO_DESCRIPTION',
|
||||
scope: 'playlist',
|
||||
isEnabled: (state) => state.hasActivePlaylist,
|
||||
},
|
||||
{
|
||||
id: 'account-info',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.ACCOUNT_INFO_LABEL',
|
||||
descriptionKey: 'WORKSPACE.SHELL.COMMANDS.ACCOUNT_INFO_DESCRIPTION',
|
||||
scope: 'playlist',
|
||||
isEnabled: (state) => state.isXtreamPlaylist,
|
||||
},
|
||||
{
|
||||
id: 'go-to-vod',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.GO_TO_VOD_LABEL',
|
||||
descriptionKey: 'WORKSPACE.SHELL.COMMANDS.GO_TO_VOD_DESCRIPTION',
|
||||
scope: 'section',
|
||||
isEnabled: (state) => state.isPortalPlaylist,
|
||||
},
|
||||
{
|
||||
id: 'go-to-live',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.GO_TO_LIVE_LABEL',
|
||||
descriptionKey: 'WORKSPACE.SHELL.COMMANDS.GO_TO_LIVE_DESCRIPTION',
|
||||
scope: 'section',
|
||||
isEnabled: (state) => state.isPortalPlaylist,
|
||||
},
|
||||
{
|
||||
id: 'go-to-series',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.GO_TO_SERIES_LABEL',
|
||||
descriptionKey: 'WORKSPACE.SHELL.COMMANDS.GO_TO_SERIES_DESCRIPTION',
|
||||
scope: 'section',
|
||||
isEnabled: (state) => state.isPortalPlaylist,
|
||||
},
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceShellFacade {
|
||||
private readonly router = inject(Router);
|
||||
@@ -239,6 +101,7 @@ export class WorkspaceShellFacade {
|
||||
WorkspaceStartupPreferencesService
|
||||
);
|
||||
readonly headerContext = inject(WorkspaceHeaderContextService);
|
||||
private readonly viewCommands = inject(WorkspaceViewCommandService);
|
||||
private readonly playlistRefreshAction = inject(
|
||||
PlaylistRefreshActionService
|
||||
);
|
||||
@@ -308,6 +171,9 @@ export class WorkspaceShellFacade {
|
||||
);
|
||||
readonly currentContext = computed(() => this.currentRoute().context);
|
||||
readonly currentSection = computed(() => this.currentRoute().section);
|
||||
readonly commandPaletteCommands = computed<WorkspaceResolvedCommandItem[]>(
|
||||
() => this.getCommandPaletteItems()
|
||||
);
|
||||
readonly workspaceLinks = computed<PortalRailLink[]>(() => {
|
||||
this.languageTick();
|
||||
|
||||
@@ -328,6 +194,22 @@ export class WorkspaceShellFacade {
|
||||
path: ['/workspace/sources'],
|
||||
});
|
||||
|
||||
links.push({
|
||||
icon: 'favorite',
|
||||
tooltip: this.translateText(
|
||||
'WORKSPACE.SHELL.RAIL_GLOBAL_FAVORITES'
|
||||
),
|
||||
path: ['/workspace/global-favorites'],
|
||||
exact: true,
|
||||
});
|
||||
|
||||
links.push({
|
||||
icon: 'history',
|
||||
tooltip: this.translateText('WORKSPACE.SHELL.RAIL_GLOBAL_RECENT'),
|
||||
path: ['/workspace/global-recent'],
|
||||
exact: true,
|
||||
});
|
||||
|
||||
return links;
|
||||
});
|
||||
readonly isDashboardRoute = computed(
|
||||
@@ -339,12 +221,6 @@ export class WorkspaceShellFacade {
|
||||
readonly isSettingsRoute = computed(
|
||||
() => this.currentRoute().kind === 'settings'
|
||||
);
|
||||
readonly isGlobalFavoritesRoute = computed(
|
||||
() => this.currentRoute().kind === 'global-favorites'
|
||||
);
|
||||
readonly isPortalFavoritesAllScope = computed(
|
||||
() => this.currentRoute().isPortalFavoritesAllScope
|
||||
);
|
||||
readonly isGlobalDownloadsRoute = computed(
|
||||
() => this.currentRoute().kind === 'downloads'
|
||||
);
|
||||
@@ -400,6 +276,9 @@ export class WorkspaceShellFacade {
|
||||
);
|
||||
readonly xtreamImportCount = this.xtreamStore.getImportCount;
|
||||
readonly xtreamItemsToImport = this.xtreamStore.itemsToImport;
|
||||
readonly xtreamActiveImportCount = this.xtreamStore.activeImportCurrentCount;
|
||||
readonly xtreamActiveItemsToImport =
|
||||
this.xtreamStore.activeImportTotalCount;
|
||||
readonly xtreamImportPhase = this.xtreamStore.currentImportPhase;
|
||||
readonly isCancellingXtreamImport = this.xtreamStore.isCancellingImport;
|
||||
readonly canCancelXtreamImport = computed(
|
||||
@@ -412,6 +291,34 @@ export class WorkspaceShellFacade {
|
||||
readonly xtreamImportTitleLabel = computed(() =>
|
||||
this.translateText('WORKSPACE.SHELL.XTREAM_IMPORT_TITLE')
|
||||
);
|
||||
readonly xtreamImportTypeLabel = computed(() => {
|
||||
this.languageTick();
|
||||
|
||||
switch (this.xtreamStore.activeImportContentType()) {
|
||||
case 'live':
|
||||
return this.translateText('WORKSPACE.SHELL.RAIL_LIVE');
|
||||
case 'vod':
|
||||
return this.translateText('WORKSPACE.SHELL.RAIL_MOVIES');
|
||||
case 'series':
|
||||
return this.translateText('WORKSPACE.SHELL.RAIL_SERIES');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
});
|
||||
readonly xtreamImportProgressLabel = computed(() => {
|
||||
const type = this.xtreamImportTypeLabel();
|
||||
const total = this.xtreamActiveItemsToImport();
|
||||
|
||||
if (!type || total === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.translateText('WORKSPACE.SHELL.XTREAM_IMPORT_PROGRESS', {
|
||||
type,
|
||||
current: this.formatLocalizedNumber(this.xtreamActiveImportCount()),
|
||||
total: this.formatLocalizedNumber(total),
|
||||
});
|
||||
});
|
||||
readonly xtreamImportPhaseTone = computed<XtreamImportPhaseTone>(() => {
|
||||
switch (this.xtreamStore.currentImportPhase()) {
|
||||
case 'loading-categories':
|
||||
@@ -845,9 +752,10 @@ export class WorkspaceShellFacade {
|
||||
}
|
||||
|
||||
openCommandPalette(): void {
|
||||
const commands = this.commandPaletteCommands();
|
||||
const dialogRef = this.dialog.open<
|
||||
WorkspaceCommandPaletteComponent,
|
||||
{ commands: WorkspaceCommandItem[]; query: string },
|
||||
{ commands: WorkspaceResolvedCommandItem[]; query: string },
|
||||
WorkspaceCommandSelection | undefined
|
||||
>(WorkspaceCommandPaletteComponent, {
|
||||
width: 'min(760px, 92vw)',
|
||||
@@ -855,7 +763,7 @@ export class WorkspaceShellFacade {
|
||||
panelClass: 'workspace-command-palette-overlay',
|
||||
autoFocus: false,
|
||||
data: {
|
||||
commands: this.getCommandPaletteItems(),
|
||||
commands,
|
||||
query: this.searchQuery(),
|
||||
},
|
||||
});
|
||||
@@ -868,7 +776,13 @@ export class WorkspaceShellFacade {
|
||||
return;
|
||||
}
|
||||
|
||||
this.runCommandPaletteAction(selection);
|
||||
const command = commands.find(
|
||||
(item) =>
|
||||
item.id === selection.commandId &&
|
||||
item.visible &&
|
||||
item.enabled
|
||||
);
|
||||
command?.run({ query: selection.query.trim() });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -920,23 +834,6 @@ export class WorkspaceShellFacade {
|
||||
}
|
||||
|
||||
navigateToGlobalFavorites(): void {
|
||||
const context = this.railContext();
|
||||
if (
|
||||
context &&
|
||||
(context.provider === 'xtreams' || context.provider === 'stalker')
|
||||
) {
|
||||
void this.router.navigate(
|
||||
[
|
||||
'/workspace',
|
||||
context.provider,
|
||||
context.playlistId,
|
||||
'favorites',
|
||||
],
|
||||
{ queryParams: { scope: 'all' } }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
void this.router.navigate(['/workspace/global-favorites']);
|
||||
}
|
||||
|
||||
@@ -994,129 +891,390 @@ export class WorkspaceShellFacade {
|
||||
this.playlistRefreshAction.refresh(playlist);
|
||||
}
|
||||
|
||||
private getCommandPaletteItems(): WorkspaceCommandItem[] {
|
||||
private getCommandPaletteItems(): WorkspaceResolvedCommandItem[] {
|
||||
this.languageTick();
|
||||
|
||||
return [
|
||||
...this.getViewCommandDefinitions(),
|
||||
...this.getPlaylistCommandDefinitions(),
|
||||
...this.getGlobalCommandDefinitions(),
|
||||
...this.viewCommands.commands(),
|
||||
]
|
||||
.map((command) => this.resolveCommand(command))
|
||||
.filter((command) => command.visible)
|
||||
.sort((left, right) => this.comparePaletteCommands(left, right));
|
||||
}
|
||||
|
||||
private getViewCommandDefinitions(): WorkspaceCommandContribution[] {
|
||||
const context = this.currentContext();
|
||||
const provider = context?.provider;
|
||||
const availability: WorkspaceCommandAvailability = {
|
||||
context,
|
||||
dashboardXtreamContext: this.dashboardXtreamContext(),
|
||||
hasXtreamPlaylists: this.playlists().some(
|
||||
(playlist) => !!playlist.serverUrl
|
||||
),
|
||||
isElectron: this.isElectron,
|
||||
canRefreshPlaylist: this.canRefreshPlaylist(),
|
||||
hasActivePlaylist: !!this.activePlaylist(),
|
||||
isXtreamPlaylist: provider === 'xtreams',
|
||||
isPortalPlaylist:
|
||||
provider === 'xtreams' || provider === 'stalker',
|
||||
const section = this.currentSection();
|
||||
|
||||
if (!context || !section) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (context.provider === 'playlists') {
|
||||
return this.getM3uNavigationCommands(context, section);
|
||||
}
|
||||
|
||||
if (context.provider === 'xtreams' || context.provider === 'stalker') {
|
||||
return this.getPortalNavigationCommands(context, section);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private getPlaylistCommandDefinitions(): WorkspaceCommandContribution[] {
|
||||
const route = this.currentRoute();
|
||||
const context = route.context;
|
||||
|
||||
if (
|
||||
!context ||
|
||||
(context.provider !== 'xtreams' &&
|
||||
context.provider !== 'stalker' &&
|
||||
context.provider !== 'playlists')
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const hasActivePlaylist = !!this.activePlaylist();
|
||||
const canOpenPlaylistSearch =
|
||||
context.provider === 'xtreams' || context.provider === 'stalker';
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'playlist-search',
|
||||
group: 'playlist',
|
||||
icon: 'playlist_play',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.PLAYLIST_SEARCH_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.PLAYLIST_SEARCH_DESCRIPTION',
|
||||
priority: 10,
|
||||
visible: canOpenPlaylistSearch,
|
||||
run: ({ query }) => this.openPlaylistSearchFromPalette(query),
|
||||
},
|
||||
{
|
||||
id: 'refresh-playlist',
|
||||
group: 'playlist',
|
||||
icon: 'sync',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.REFRESH_PLAYLIST_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.REFRESH_PLAYLIST_DESCRIPTION',
|
||||
priority: 20,
|
||||
visible: this.canRefreshPlaylist(),
|
||||
run: () => this.refreshCurrentPlaylist(),
|
||||
},
|
||||
{
|
||||
id: 'playlist-info',
|
||||
group: 'playlist',
|
||||
icon: 'info',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.PLAYLIST_INFO_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.PLAYLIST_INFO_DESCRIPTION',
|
||||
priority: 30,
|
||||
visible: hasActivePlaylist,
|
||||
run: () => this.openPlaylistInfo(),
|
||||
},
|
||||
{
|
||||
id: 'account-info',
|
||||
group: 'playlist',
|
||||
icon: 'account_circle',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.ACCOUNT_INFO_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.ACCOUNT_INFO_DESCRIPTION',
|
||||
priority: 40,
|
||||
visible: context.provider === 'xtreams',
|
||||
run: () => this.openAccountInfo(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private getGlobalCommandDefinitions(): WorkspaceCommandContribution[] {
|
||||
const route = this.currentRoute();
|
||||
const hasXtreamPlaylists = this.playlists().some(
|
||||
(playlist) => !!playlist.serverUrl
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'global-search',
|
||||
group: 'global',
|
||||
icon: 'search',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.GLOBAL_SEARCH_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.GLOBAL_SEARCH_DESCRIPTION',
|
||||
priority: 10,
|
||||
visible: hasXtreamPlaylists,
|
||||
keywords: ['xtream'],
|
||||
run: ({ query }) => this.openGlobalSearch(query),
|
||||
},
|
||||
{
|
||||
id: 'open-global-favorites',
|
||||
group: 'global',
|
||||
icon: 'star',
|
||||
labelKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.OPEN_GLOBAL_FAVORITES_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.OPEN_GLOBAL_FAVORITES_DESCRIPTION',
|
||||
priority: 20,
|
||||
visible: route.kind !== 'global-favorites',
|
||||
run: () => this.navigateToGlobalFavorites(),
|
||||
},
|
||||
{
|
||||
id: 'open-global-recent',
|
||||
group: 'global',
|
||||
icon: 'history',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_GLOBAL_RECENT_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.OPEN_GLOBAL_RECENT_DESCRIPTION',
|
||||
priority: 30,
|
||||
visible: route.kind !== 'global-recent',
|
||||
run: () => this.openGlobalRecent(),
|
||||
},
|
||||
{
|
||||
id: 'open-downloads',
|
||||
group: 'global',
|
||||
icon: 'download',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_DOWNLOADS_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.OPEN_DOWNLOADS_DESCRIPTION',
|
||||
priority: 40,
|
||||
visible: this.isElectron && route.kind !== 'downloads',
|
||||
run: () => this.openDownloadsShortcut(),
|
||||
},
|
||||
{
|
||||
id: 'open-settings',
|
||||
group: 'global',
|
||||
icon: 'settings',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_SETTINGS_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.OPEN_SETTINGS_DESCRIPTION',
|
||||
priority: 50,
|
||||
visible: route.kind !== 'settings',
|
||||
run: () => {
|
||||
void this.router.navigate(['/workspace', 'settings']);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'open-sources',
|
||||
group: 'global',
|
||||
icon: 'library_books',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_SOURCES_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.OPEN_SOURCES_DESCRIPTION',
|
||||
priority: 60,
|
||||
visible: route.kind !== 'sources',
|
||||
run: () => {
|
||||
void this.router.navigate(['/workspace', 'sources']);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'open-dashboard',
|
||||
group: 'global',
|
||||
icon: 'dashboard',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_DASHBOARD_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.OPEN_DASHBOARD_DESCRIPTION',
|
||||
priority: 70,
|
||||
visible: this.showDashboard() && route.kind !== 'dashboard',
|
||||
run: () => {
|
||||
void this.router.navigate(['/workspace', 'dashboard']);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'add-playlist',
|
||||
group: 'global',
|
||||
icon: 'add_circle_outline',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_DESCRIPTION',
|
||||
priority: 80,
|
||||
run: () => this.openAddPlaylistDialog(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private getPortalNavigationCommands(
|
||||
context: WorkspacePortalContext,
|
||||
section: PortalRailSection
|
||||
): WorkspaceCommandContribution[] {
|
||||
const liveSection = context.provider === 'stalker' ? 'itv' : 'live';
|
||||
|
||||
return [
|
||||
this.createNavigationCommand({
|
||||
id: 'go-to-vod',
|
||||
context,
|
||||
targetSection: 'vod',
|
||||
currentSection: section,
|
||||
icon: 'movie',
|
||||
labelKey: 'WORKSPACE.SHELL.RAIL_MOVIES',
|
||||
priority: 100,
|
||||
}),
|
||||
this.createNavigationCommand({
|
||||
id: 'go-to-live',
|
||||
context,
|
||||
targetSection: liveSection,
|
||||
currentSection: section,
|
||||
icon: 'live_tv',
|
||||
labelKey: 'WORKSPACE.SHELL.RAIL_LIVE',
|
||||
priority: 110,
|
||||
}),
|
||||
this.createNavigationCommand({
|
||||
id: 'go-to-series',
|
||||
context,
|
||||
targetSection: 'series',
|
||||
currentSection: section,
|
||||
icon: 'video_library',
|
||||
labelKey: 'WORKSPACE.SHELL.RAIL_SERIES',
|
||||
priority: 120,
|
||||
}),
|
||||
].filter(
|
||||
(command): command is WorkspaceCommandContribution => command !== null
|
||||
);
|
||||
}
|
||||
|
||||
private getM3uNavigationCommands(
|
||||
context: WorkspacePortalContext,
|
||||
section: PortalRailSection
|
||||
): WorkspaceCommandContribution[] {
|
||||
return [
|
||||
this.createNavigationCommand({
|
||||
id: 'go-to-all',
|
||||
context,
|
||||
targetSection: 'all',
|
||||
currentSection: section,
|
||||
icon: 'format_list_bulleted',
|
||||
labelKey: 'WORKSPACE.SHELL.RAIL_ALL_CHANNELS',
|
||||
priority: 100,
|
||||
}),
|
||||
this.createNavigationCommand({
|
||||
id: 'go-to-groups',
|
||||
context,
|
||||
targetSection: 'groups',
|
||||
currentSection: section,
|
||||
icon: 'folder_open',
|
||||
labelKey: 'WORKSPACE.SHELL.RAIL_GROUPS',
|
||||
priority: 110,
|
||||
}),
|
||||
this.createNavigationCommand({
|
||||
id: 'go-to-favorites',
|
||||
context,
|
||||
targetSection: 'favorites',
|
||||
currentSection: section,
|
||||
icon: 'star',
|
||||
labelKey: 'WORKSPACE.SHELL.RAIL_FAVORITES',
|
||||
priority: 120,
|
||||
}),
|
||||
this.createNavigationCommand({
|
||||
id: 'go-to-recent',
|
||||
context,
|
||||
targetSection: 'recent',
|
||||
currentSection: section,
|
||||
icon: 'history',
|
||||
labelKey: 'WORKSPACE.SHELL.RAIL_RECENT',
|
||||
priority: 130,
|
||||
}),
|
||||
].filter(
|
||||
(command): command is WorkspaceCommandContribution => command !== null
|
||||
);
|
||||
}
|
||||
|
||||
private createNavigationCommand(config: {
|
||||
id: string;
|
||||
context: WorkspacePortalContext;
|
||||
targetSection: string;
|
||||
currentSection: PortalRailSection;
|
||||
icon: string;
|
||||
labelKey: string;
|
||||
priority: number;
|
||||
}): WorkspaceCommandContribution | null {
|
||||
if (config.currentSection === config.targetSection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: config.id,
|
||||
group: 'view',
|
||||
icon: config.icon,
|
||||
labelKey: config.labelKey,
|
||||
descriptionKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_VIEW_DESCRIPTION',
|
||||
descriptionParams: () => ({
|
||||
view: this.translateText(config.labelKey),
|
||||
}),
|
||||
priority: config.priority,
|
||||
run: () => {
|
||||
void this.router.navigate([
|
||||
'/workspace',
|
||||
config.context.provider,
|
||||
config.context.playlistId,
|
||||
config.targetSection,
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
return WORKSPACE_COMMAND_DEFINITIONS.map((definition) => ({
|
||||
id: definition.id,
|
||||
label: this.translateText(definition.labelKey),
|
||||
description: this.translateText(definition.descriptionKey),
|
||||
scope: definition.scope,
|
||||
enabled: definition.isEnabled(availability),
|
||||
}));
|
||||
}
|
||||
|
||||
private runCommandPaletteAction(
|
||||
selection: WorkspaceCommandSelection
|
||||
): void {
|
||||
const query = selection.query.trim();
|
||||
private resolveCommand(
|
||||
command: WorkspaceCommandContribution
|
||||
): WorkspaceResolvedCommandItem {
|
||||
const labelParams = this.resolveCommandValue(command.labelParams);
|
||||
const descriptionParams = this.resolveCommandValue(
|
||||
command.descriptionParams
|
||||
);
|
||||
|
||||
if (selection.commandId === 'global-search') {
|
||||
this.openGlobalSearch(query);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.commandId === 'playlist-search') {
|
||||
this.openPlaylistSearchFromPalette(query);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.commandId === 'open-global-favorites') {
|
||||
this.navigateToGlobalFavorites();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.commandId === 'open-downloads') {
|
||||
this.openDownloadsShortcut();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.commandId === 'open-global-recent') {
|
||||
this.openGlobalRecent();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.commandId === 'open-settings') {
|
||||
void this.router.navigate(['/workspace', 'settings']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.commandId === 'open-sources') {
|
||||
void this.router.navigate(['/workspace', 'sources']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.commandId === 'open-dashboard') {
|
||||
void this.router.navigate(['/workspace', 'dashboard']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.commandId === 'add-playlist') {
|
||||
this.openAddPlaylistDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.commandId === 'refresh-playlist') {
|
||||
this.refreshCurrentPlaylist();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.commandId === 'playlist-info') {
|
||||
this.openPlaylistInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.commandId === 'account-info') {
|
||||
this.openAccountInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
this.navigateToPortalSection(selection.commandId);
|
||||
return {
|
||||
id: command.id,
|
||||
group: command.group,
|
||||
icon: command.icon,
|
||||
label: this.translateText(command.labelKey, labelParams),
|
||||
description: command.descriptionKey
|
||||
? this.translateText(command.descriptionKey, descriptionParams)
|
||||
: '',
|
||||
keywords: this.resolveCommandValue(command.keywords) ?? [],
|
||||
priority: command.priority ?? 100,
|
||||
visible: this.resolveCommandValue(command.visible) ?? true,
|
||||
enabled: this.resolveCommandValue(command.enabled) ?? true,
|
||||
run: command.run,
|
||||
};
|
||||
}
|
||||
|
||||
private navigateToPortalSection(commandId: WorkspaceCommandId): void {
|
||||
const context = this.currentContext();
|
||||
if (!context) {
|
||||
return;
|
||||
private comparePaletteCommands(
|
||||
left: WorkspaceResolvedCommandItem,
|
||||
right: WorkspaceResolvedCommandItem
|
||||
): number {
|
||||
const groupOrder =
|
||||
this.getCommandGroupOrder(left.group) -
|
||||
this.getCommandGroupOrder(right.group);
|
||||
|
||||
if (groupOrder !== 0) {
|
||||
return groupOrder;
|
||||
}
|
||||
|
||||
const isStalker = context.provider === 'stalker';
|
||||
let section: string;
|
||||
if (left.priority !== right.priority) {
|
||||
return left.priority - right.priority;
|
||||
}
|
||||
|
||||
switch (commandId) {
|
||||
case 'go-to-vod':
|
||||
section = 'vod';
|
||||
break;
|
||||
case 'go-to-live':
|
||||
section = isStalker ? 'itv' : 'live';
|
||||
break;
|
||||
case 'go-to-series':
|
||||
section = 'series';
|
||||
break;
|
||||
return left.label.localeCompare(right.label);
|
||||
}
|
||||
|
||||
private getCommandGroupOrder(group: WorkspaceResolvedCommandItem['group']) {
|
||||
switch (group) {
|
||||
case 'view':
|
||||
return 0;
|
||||
case 'playlist':
|
||||
return 1;
|
||||
default:
|
||||
return;
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
private resolveCommandValue<T>(
|
||||
value: T | (() => T | undefined) | undefined
|
||||
): T | undefined {
|
||||
if (typeof value === 'function') {
|
||||
return (value as () => T | undefined)();
|
||||
}
|
||||
|
||||
void this.router.navigate([
|
||||
'/workspace',
|
||||
context.provider,
|
||||
context.playlistId,
|
||||
section,
|
||||
]);
|
||||
return value;
|
||||
}
|
||||
|
||||
private openPlaylistSearchFromPalette(query: string): void {
|
||||
@@ -1385,6 +1543,12 @@ export class WorkspaceShellFacade {
|
||||
return this.translateText(this.getRailTooltipKey('playlists', section));
|
||||
}
|
||||
|
||||
private formatLocalizedNumber(value: number): string {
|
||||
const locale =
|
||||
this.translate.currentLang || this.translate.defaultLang || 'en';
|
||||
return new Intl.NumberFormat(locale).format(value);
|
||||
}
|
||||
|
||||
private translateText(
|
||||
key: string,
|
||||
params?: Record<string, string | number>
|
||||
|
||||
+9
-12
@@ -29,24 +29,25 @@
|
||||
</p>
|
||||
}
|
||||
@if (
|
||||
facade.xtreamImportCount() !== 0 &&
|
||||
facade.xtreamItemsToImport() !== 0
|
||||
facade.xtreamActiveImportCount() !== 0 &&
|
||||
facade.xtreamActiveItemsToImport() !== 0
|
||||
) {
|
||||
<mat-progress-bar
|
||||
mode="determinate"
|
||||
[value]="
|
||||
(facade.xtreamImportCount() /
|
||||
facade.xtreamItemsToImport()) *
|
||||
(facade.xtreamActiveImportCount() /
|
||||
facade.xtreamActiveItemsToImport()) *
|
||||
100
|
||||
"
|
||||
/>
|
||||
<p class="workspace-loading-overlay__progress-copy">
|
||||
{{ facade.xtreamImportCount() }} /
|
||||
{{ facade.xtreamItemsToImport() }}
|
||||
</p>
|
||||
} @else {
|
||||
<mat-progress-bar mode="indeterminate" />
|
||||
}
|
||||
@if (facade.xtreamImportProgressLabel()) {
|
||||
<p class="workspace-loading-overlay__progress-copy">
|
||||
{{ facade.xtreamImportProgressLabel() }}
|
||||
</p>
|
||||
}
|
||||
@if (facade.canCancelXtreamImport()) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
@@ -90,9 +91,6 @@
|
||||
[searchPlaceholder]="facade.searchPlaceholder()"
|
||||
[searchScopeLabel]="facade.searchScopeLabel()"
|
||||
[searchStatusLabel]="facade.searchStatusLabel()"
|
||||
[isGlobalFavoritesActive]="
|
||||
facade.isGlobalFavoritesRoute() || facade.isPortalFavoritesAllScope()
|
||||
"
|
||||
[headerShortcut]="facade.headerShortcut()"
|
||||
[canRefreshPlaylist]="facade.canRefreshPlaylist()"
|
||||
[isRefreshingPlaylist]="facade.isRefreshingPlaylist()"
|
||||
@@ -104,7 +102,6 @@
|
||||
(searchSubmitted)="facade.onSearchEnter($event)"
|
||||
(commandPaletteRequested)="facade.openCommandPalette()"
|
||||
(addPlaylistRequested)="facade.openAddPlaylistDialog()"
|
||||
(globalFavoritesRequested)="facade.navigateToGlobalFavorites()"
|
||||
(headerShortcutRequested)="facade.runHeaderShortcut()"
|
||||
(downloadsRequested)="facade.openDownloadsShortcut()"
|
||||
(playlistInfoRequested)="facade.openPlaylistInfo()"
|
||||
|
||||
@@ -157,8 +157,12 @@ app-workspace-shell-context-sidebar {
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__progress-copy {
|
||||
max-width: 30ch;
|
||||
justify-self: center;
|
||||
line-height: 1.45;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__action {
|
||||
|
||||
+55
-5
@@ -5,6 +5,7 @@ import {
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { RouterOutlet, provideRouter } from '@angular/router';
|
||||
import {
|
||||
WorkspacePortalContext,
|
||||
@@ -49,17 +50,19 @@ class MockWorkspaceShellHeaderComponent {
|
||||
readonly searchPlaceholder = input('');
|
||||
readonly searchScopeLabel = input('');
|
||||
readonly searchStatusLabel = input('');
|
||||
readonly isGlobalFavoritesActive = input(false);
|
||||
readonly headerShortcut = input<unknown>(null);
|
||||
readonly canRefreshPlaylist = input(false);
|
||||
readonly isRefreshingPlaylist = input(false);
|
||||
readonly isElectron = input(false);
|
||||
readonly isDownloadsView = input(false);
|
||||
readonly hasActiveDownloads = input(false);
|
||||
readonly headerBulkAction = input<WorkspaceHeaderBulkAction | null>(null);
|
||||
readonly searchChanged = output<string>();
|
||||
readonly searchSubmitted = output<string>();
|
||||
readonly commandPaletteRequested = output<void>();
|
||||
readonly addPlaylistRequested = output<void>();
|
||||
readonly globalFavoritesRequested = output<void>();
|
||||
readonly headerShortcutRequested = output<void>();
|
||||
readonly refreshPlaylistRequested = output<void>();
|
||||
readonly downloadsRequested = output<void>();
|
||||
readonly headerBulkActionRequested = output<void>();
|
||||
readonly playlistInfoRequested = output<void>();
|
||||
@@ -108,10 +111,11 @@ class MockWorkspaceShellFacade {
|
||||
);
|
||||
readonly searchScopeLabel = signal('Movies / All Items');
|
||||
readonly searchStatusLabel = signal('');
|
||||
readonly isGlobalFavoritesRoute = signal(false);
|
||||
readonly isPortalFavoritesAllScope = signal(false);
|
||||
readonly headerShortcut = signal(null);
|
||||
readonly canRefreshPlaylist = signal(false);
|
||||
readonly isRefreshingPlaylist = signal(false);
|
||||
readonly isDownloadsView = signal(false);
|
||||
readonly hasActiveDownloads = signal(false);
|
||||
readonly headerBulkAction = signal<WorkspaceHeaderBulkAction | null>(null);
|
||||
readonly showContextPanel = signal(true);
|
||||
readonly contextPanel = signal<WorkspaceShellContextPanel>('settings');
|
||||
@@ -121,6 +125,8 @@ class MockWorkspaceShellFacade {
|
||||
readonly showXtreamImportOverlay = signal(false);
|
||||
readonly xtreamImportCount = signal(0);
|
||||
readonly xtreamItemsToImport = signal(0);
|
||||
readonly xtreamActiveImportCount = signal(0);
|
||||
readonly xtreamActiveItemsToImport = signal(0);
|
||||
readonly xtreamImportTitleLabel = signal(
|
||||
'WORKSPACE.SHELL.XTREAM_IMPORT_TITLE'
|
||||
);
|
||||
@@ -133,6 +139,7 @@ class MockWorkspaceShellFacade {
|
||||
readonly xtreamImportDetailLabel = signal(
|
||||
'WORKSPACE.SHELL.XTREAM_IMPORT_DETAIL_REMOTE'
|
||||
);
|
||||
readonly xtreamImportProgressLabel = signal('');
|
||||
readonly xtreamImportPhaseTone = signal<'remote' | 'local' | null>(
|
||||
'remote'
|
||||
);
|
||||
@@ -145,8 +152,8 @@ class MockWorkspaceShellFacade {
|
||||
onSearchEnter = jest.fn();
|
||||
openCommandPalette = jest.fn();
|
||||
openAddPlaylistDialog = jest.fn();
|
||||
navigateToGlobalFavorites = jest.fn();
|
||||
runHeaderShortcut = jest.fn();
|
||||
refreshCurrentPlaylist = jest.fn();
|
||||
openDownloadsShortcut = jest.fn();
|
||||
runHeaderBulkAction = jest.fn();
|
||||
openPlaylistInfo = jest.fn();
|
||||
@@ -166,6 +173,7 @@ describe('WorkspaceShellComponent', () => {
|
||||
.overrideComponent(WorkspaceShellComponent, {
|
||||
set: {
|
||||
imports: [
|
||||
MatProgressBarModule,
|
||||
RouterOutlet,
|
||||
MockExternalPlaybackDockComponent,
|
||||
MockWorkspaceShellContextSidebarComponent,
|
||||
@@ -201,4 +209,46 @@ describe('WorkspaceShellComponent', () => {
|
||||
fixture.nativeElement.querySelector('app-external-playback-dock')
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('renders type-aware xtream import progress copy in the overlay', async () => {
|
||||
const facade = new MockWorkspaceShellFacade();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [WorkspaceShellComponent],
|
||||
providers: [provideRouter([])],
|
||||
})
|
||||
.overrideComponent(WorkspaceShellComponent, {
|
||||
set: {
|
||||
imports: [
|
||||
MatProgressBarModule,
|
||||
RouterOutlet,
|
||||
MockExternalPlaybackDockComponent,
|
||||
MockWorkspaceShellContextSidebarComponent,
|
||||
MockWorkspaceShellHeaderComponent,
|
||||
MockWorkspaceShellRailComponent,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: WorkspaceShellFacade,
|
||||
useValue: facade,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
facade.showXtreamImportOverlay.set(true);
|
||||
facade.xtreamActiveImportCount.set(20);
|
||||
facade.xtreamActiveItemsToImport.set(12323);
|
||||
facade.xtreamImportProgressLabel.set('Movies imported: 20 / 12,323');
|
||||
|
||||
const fixture = TestBed.createComponent(WorkspaceShellComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
fixture.nativeElement.querySelector(
|
||||
'.workspace-loading-overlay__progress-copy'
|
||||
)?.textContent
|
||||
).toContain('Movies imported: 20 / 12,323');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user