mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 20:16:15 -08:00
Merge remote-tracking branch 'origin/master' into claude/iptvnator-vod-hero-redesign-2c8950
# Conflicts: # apps/electron-backend-e2e/src/detail-back-lane.e2e.ts # apps/web-e2e/src/m3u-movie-details.e2e.ts # docs/architecture/iptvnator-ui-guidelines.md # libs/ui/components/src/lib/content-hero/content-hero.component.scss # libs/ui/components/src/lib/portal-detail-shell/portal-detail-shell.component.scss
This commit is contained in:
commit
87d7e1d46c
122 files changed
+2080
-1040
No files matched your search
+25
-2
@@ -1,8 +1,31 @@
|
||||
<header class="workspace-header" [class.is-settings-route]="isSettingsRoute()">
|
||||
<!-- The page's Back, at the leading edge where desktop apps and Material's
|
||||
top app bar keep navigation (beside the macOS traffic lights). The
|
||||
header never scrolls, so the control stays put over long pages. -->
|
||||
@if (backTarget(); as back) {
|
||||
@let backLabel = back.label() || ('BACK' | translate);
|
||||
<button
|
||||
type="button"
|
||||
class="header-back"
|
||||
mat-icon-button
|
||||
data-test-id="workspace-header-back"
|
||||
(click)="backRequested.emit()"
|
||||
(keydown.escape)="onBackEscape($event)"
|
||||
[matTooltip]="
|
||||
back.escapeShortcut() ? backLabel + ' (Esc)' : backLabel
|
||||
"
|
||||
[attr.aria-label]="backLabel"
|
||||
[attr.aria-keyshortcuts]="back.escapeShortcut() ? 'Escape' : null"
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- Phone-only (≤640px, CSS-hidden above): on phones the context panel
|
||||
is an off-canvas drawer, and this button is the only way to open
|
||||
it, so it must never be dropped while the route has a panel. -->
|
||||
@if (showContextDrawerToggle()) {
|
||||
it. A page with Back takes its slot (one navigation icon, as in
|
||||
Material's top app bar); the list Back returns to shows it again. -->
|
||||
@if (showContextDrawerToggle() && !backTarget()) {
|
||||
<button
|
||||
#contextDrawerToggle
|
||||
type="button"
|
||||
|
||||
+8
@@ -20,6 +20,14 @@
|
||||
app-region: no-drag;
|
||||
}
|
||||
|
||||
// Pulled toward the edge so the arrow glyph, not the button's padding, sits
|
||||
// on the header's inset.
|
||||
.header-back {
|
||||
flex-shrink: 0;
|
||||
margin-inline-start: -8px;
|
||||
app-region: no-drag;
|
||||
}
|
||||
|
||||
// Above the phone breakpoint the context panel is always visible beside the
|
||||
// content, so its drawer toggle would open nothing — it exists only at ≤640px.
|
||||
.context-drawer-toggle {
|
||||
|
||||
+105
@@ -4,6 +4,7 @@ import {
|
||||
Component,
|
||||
input,
|
||||
output,
|
||||
signal,
|
||||
ChangeDetectionStrategy,
|
||||
} from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
@@ -14,6 +15,7 @@ import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { of } from 'rxjs';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { WorkspaceBackTarget } from '@iptvnator/portal/shared/util';
|
||||
import { WorkspaceShellHeaderComponent } from './workspace-shell-header.component';
|
||||
|
||||
@Component({
|
||||
@@ -376,6 +378,109 @@ describe('WorkspaceShellHeaderComponent', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('page Back', () => {
|
||||
const backButton = (): HTMLButtonElement | null =>
|
||||
fixture.nativeElement.querySelector(
|
||||
'[data-test-id="workspace-header-back"]'
|
||||
);
|
||||
|
||||
function setBackTarget(
|
||||
label: string | null,
|
||||
escapeShortcut: boolean
|
||||
): WorkspaceBackTarget {
|
||||
const target: WorkspaceBackTarget = {
|
||||
label: signal(label),
|
||||
escapeShortcut: signal(escapeShortcut),
|
||||
run: jest.fn(),
|
||||
};
|
||||
fixture.componentRef.setInput('backTarget', target);
|
||||
fixture.detectChanges();
|
||||
return target;
|
||||
}
|
||||
|
||||
it('renders nothing while the page has no Back', () => {
|
||||
expect(backButton()).toBeNull();
|
||||
});
|
||||
|
||||
it('leads the header and emits Back requests', () => {
|
||||
const requested = jest.fn();
|
||||
component.backRequested.subscribe(requested);
|
||||
setBackTarget('Back to Downloads', true);
|
||||
|
||||
const button = backButton();
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('header')?.firstElementChild
|
||||
).toBe(button);
|
||||
expect(button?.type).toBe('button');
|
||||
button?.click();
|
||||
|
||||
expect(requested).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('names the page Back and announces Escape only while it applies', () => {
|
||||
setBackTarget('Back to Downloads', true);
|
||||
expect(backButton()?.getAttribute('aria-label')).toBe(
|
||||
'Back to Downloads'
|
||||
);
|
||||
expect(backButton()?.getAttribute('aria-keyshortcuts')).toBe(
|
||||
'Escape'
|
||||
);
|
||||
|
||||
setBackTarget(null, false);
|
||||
// No host label: the generic translated Back.
|
||||
expect(backButton()?.getAttribute('aria-label')).toBe('BACK');
|
||||
expect(backButton()?.hasAttribute('aria-keyshortcuts')).toBe(false);
|
||||
});
|
||||
|
||||
it('runs the advertised Escape from the focused Back only while it applies', () => {
|
||||
const requested = jest.fn();
|
||||
component.backRequested.subscribe(requested);
|
||||
const press = (init: KeyboardEventInit = {}) => {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
...init,
|
||||
});
|
||||
backButton()?.dispatchEvent(event);
|
||||
return event;
|
||||
};
|
||||
|
||||
setBackTarget(null, true);
|
||||
press({ repeat: true });
|
||||
press({ shiftKey: true });
|
||||
expect(requested).not.toHaveBeenCalled();
|
||||
expect(press().defaultPrevented).toBe(true);
|
||||
expect(requested).toHaveBeenCalledTimes(1);
|
||||
|
||||
// In watch, Escape belongs to the player's close shortcut.
|
||||
setBackTarget(null, false);
|
||||
expect(press().defaultPrevented).toBe(false);
|
||||
expect(requested).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('takes the drawer toggle slot while the page offers Back', () => {
|
||||
fixture.componentRef.setInput('showContextDrawerToggle', true);
|
||||
setBackTarget(null, true);
|
||||
|
||||
expect(
|
||||
fixture.nativeElement.querySelector(
|
||||
'[data-test-id="context-drawer-toggle"]'
|
||||
)
|
||||
).toBeNull();
|
||||
|
||||
fixture.componentRef.setInput('backTarget', null);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(backButton()).toBeNull();
|
||||
expect(
|
||||
fixture.nativeElement.querySelector(
|
||||
'[data-test-id="context-drawer-toggle"]'
|
||||
)
|
||||
).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the paired Material primary tokens for the download badge', () => {
|
||||
const styleSource = readFileSync(
|
||||
join(__dirname, 'workspace-shell-header.component.scss'),
|
||||
|
||||
+28
-1
@@ -12,7 +12,10 @@ import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { PlaylistSwitcherComponent } from '@iptvnator/playlist/shared/ui';
|
||||
import { WorkspaceHeaderAction } from '@iptvnator/portal/shared/util';
|
||||
import {
|
||||
WorkspaceBackTarget,
|
||||
WorkspaceHeaderAction,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import { PlaylistMeta } from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
WorkspaceHeaderBulkAction,
|
||||
@@ -46,6 +49,8 @@ export class WorkspaceShellHeaderComponent {
|
||||
/Mac|iPhone|iPad|iPod/i.test(navigator.platform);
|
||||
readonly commandShortcutLabel = this.isMac ? '⌘K' : 'Ctrl+K';
|
||||
|
||||
/** The current page's Back action; null when the page has none. */
|
||||
readonly backTarget = input<WorkspaceBackTarget | null>(null);
|
||||
readonly playlistTitle = input('');
|
||||
readonly playlistSubtitle = input('');
|
||||
readonly canOpenPlaylistInfo = input(false);
|
||||
@@ -105,6 +110,7 @@ export class WorkspaceShellHeaderComponent {
|
||||
'WORKSPACE.SHELL.CONTEXT_DRAWER_CATEGORIES_TOOLTIP'
|
||||
);
|
||||
|
||||
readonly backRequested = output<void>();
|
||||
readonly searchChanged = output<string>();
|
||||
readonly searchSubmitted = output<string>();
|
||||
readonly commandPaletteRequested = output<void>();
|
||||
@@ -148,6 +154,27 @@ export class WorkspaceShellHeaderComponent {
|
||||
this.searchSubmitted.emit(target?.value ?? this.searchQuery());
|
||||
}
|
||||
|
||||
/**
|
||||
* The tooltip advertises Escape for Back. The page only handles it with
|
||||
* focus inside the page, so the button keeps that promise itself.
|
||||
*/
|
||||
onBackEscape(event: Event): void {
|
||||
const keyboard = event as KeyboardEvent;
|
||||
if (
|
||||
!this.backTarget()?.escapeShortcut() ||
|
||||
keyboard.defaultPrevented ||
|
||||
keyboard.repeat ||
|
||||
keyboard.altKey ||
|
||||
keyboard.ctrlKey ||
|
||||
keyboard.metaKey ||
|
||||
keyboard.shiftKey
|
||||
) {
|
||||
return;
|
||||
}
|
||||
keyboard.preventDefault();
|
||||
this.backRequested.emit();
|
||||
}
|
||||
|
||||
onPlaylistInfoRequested(): void {
|
||||
this.playlistInfoRequested.emit();
|
||||
}
|
||||
|
||||
+2
-13
@@ -1,17 +1,6 @@
|
||||
<!-- No brand mark: it linked to the page the first workspace link opens.
|
||||
The page's Back lives in the header's leading slot. -->
|
||||
<aside class="app-rail" [class.is-macos]="isMacOS()">
|
||||
<a
|
||||
class="brand"
|
||||
[routerLink]="brandLink()"
|
||||
[matTooltip]="brandTooltipKey() | translate"
|
||||
[matTooltipPosition]="'right'"
|
||||
[attr.aria-label]="brandAriaLabelKey() | translate"
|
||||
>
|
||||
<img
|
||||
src="./assets/icons/icon-tv-256.png"
|
||||
[attr.alt]="'WORKSPACE.SHELL.BRAND_ALT' | translate"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<app-workspace-shell-rail-links [links]="workspaceLinks()" />
|
||||
|
||||
@if (primaryContextLinks().length > 0) {
|
||||
|
||||
-22
@@ -98,22 +98,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.brand {
|
||||
margin-bottom: 10px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.brand img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.rail-context-region {
|
||||
--region-accent: var(--mat-sys-outline-variant);
|
||||
display: flex;
|
||||
@@ -239,12 +223,6 @@
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
// The brand links to the dashboard, which the first workspace link
|
||||
// already does. On a bar this narrow that duplicate costs a section.
|
||||
.brand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
+9
-12
@@ -61,11 +61,6 @@ describe('WorkspaceShellRailComponent', () => {
|
||||
});
|
||||
|
||||
it('renders provider context region and active settings shortcut state', () => {
|
||||
fixture.componentRef.setInput('brandLink', '/workspace/sources');
|
||||
fixture.componentRef.setInput(
|
||||
'brandAriaLabelKey',
|
||||
'WORKSPACE.SHELL.OPEN_SOURCES'
|
||||
);
|
||||
fixture.componentRef.setInput('primaryContextLinks', [
|
||||
{
|
||||
icon: 'movie',
|
||||
@@ -87,13 +82,15 @@ describe('WorkspaceShellRailComponent', () => {
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('.rail-shortcut.is-active')
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('has no brand mark duplicating the first workspace link', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.querySelector('img')).toBeNull();
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('.brand')?.getAttribute('href')
|
||||
).toContain('/workspace/sources');
|
||||
expect(
|
||||
fixture.nativeElement
|
||||
.querySelector('.brand')
|
||||
?.getAttribute('aria-label')
|
||||
).toBe('WORKSPACE.SHELL.OPEN_SOURCES');
|
||||
fixture.nativeElement.querySelector('.app-rail')?.firstElementChild
|
||||
?.tagName
|
||||
).toBe('APP-WORKSPACE-SHELL-RAIL-LINKS');
|
||||
});
|
||||
});
|
||||
+1
-8
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { RouterLink } from '@angular/router';
|
||||
@@ -28,9 +24,6 @@ import { WorkspaceShellRailLinksComponent } from '../workspace-shell-rail-links/
|
||||
})
|
||||
export class WorkspaceShellRailComponent {
|
||||
readonly isMacOS = input(false);
|
||||
readonly brandLink = input('/workspace/dashboard');
|
||||
readonly brandTooltipKey = input('WORKSPACE.SHELL.RAIL_DASHBOARD');
|
||||
readonly brandAriaLabelKey = input('WORKSPACE.SHELL.OPEN_DASHBOARD');
|
||||
readonly workspaceLinks = input<PortalRailLink[]>([]);
|
||||
readonly primaryContextLinks = input<PortalRailLink[]>([]);
|
||||
readonly secondaryContextLinks = input<PortalRailLink[]>([]);
|
||||
|
||||
+4
-3
@@ -8,6 +8,7 @@ import {
|
||||
WorkspacePortalContext,
|
||||
WorkspaceShellRoute,
|
||||
} from '@iptvnator/workspace/shell/util';
|
||||
import { SOURCE_TYPE_ICONS } from '@iptvnator/shared/interfaces';
|
||||
import { TranslateFn } from './workspace-shell-search-labels';
|
||||
|
||||
export interface CommandBuilderActions {
|
||||
@@ -275,7 +276,7 @@ export function getGlobalCommandDefinitions(
|
||||
{
|
||||
id: 'add-playlist-m3u',
|
||||
group: 'global',
|
||||
icon: 'folder_open',
|
||||
icon: SOURCE_TYPE_ICONS.m3u,
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_M3U_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_M3U_DESCRIPTION',
|
||||
@@ -286,7 +287,7 @@ export function getGlobalCommandDefinitions(
|
||||
{
|
||||
id: 'add-playlist-xtream',
|
||||
group: 'global',
|
||||
icon: 'cloud',
|
||||
icon: SOURCE_TYPE_ICONS.xtream,
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_XTREAM_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_XTREAM_DESCRIPTION',
|
||||
@@ -297,7 +298,7 @@ export function getGlobalCommandDefinitions(
|
||||
{
|
||||
id: 'add-playlist-stalker',
|
||||
group: 'global',
|
||||
icon: 'cast',
|
||||
icon: SOURCE_TYPE_ICONS.stalker,
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_STALKER_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_STALKER_DESCRIPTION',
|
||||
|
||||
-15
@@ -55,21 +55,6 @@ export class WorkspaceShellRouteStateService {
|
||||
readonly showDashboard = computed(() =>
|
||||
this.startupPreferences.showDashboard()
|
||||
);
|
||||
readonly brandLink = computed(() =>
|
||||
this.startupPreferences.getFirstAvailableWorkspacePath(
|
||||
this.showDashboard()
|
||||
)
|
||||
);
|
||||
readonly brandTooltipKey = computed(() =>
|
||||
this.showDashboard()
|
||||
? 'WORKSPACE.SHELL.RAIL_DASHBOARD'
|
||||
: 'WORKSPACE.SHELL.RAIL_SOURCES'
|
||||
);
|
||||
readonly brandAriaLabelKey = computed(() =>
|
||||
this.showDashboard()
|
||||
? 'WORKSPACE.SHELL.OPEN_DASHBOARD'
|
||||
: 'WORKSPACE.SHELL.OPEN_SOURCES'
|
||||
);
|
||||
readonly workspaceLinks = computed<PortalRailLink[]>(() => {
|
||||
this.languageTick();
|
||||
|
||||
|
||||
-1
@@ -777,7 +777,6 @@ describe('WorkspaceShellFacade', () => {
|
||||
exact: true,
|
||||
},
|
||||
]);
|
||||
expect(facade.brandLink()).toBe('/workspace/sources');
|
||||
});
|
||||
|
||||
it('hides the Electron-only global search rail link in the web runtime', () => {
|
||||
|
||||
+7
-3
@@ -3,6 +3,7 @@ import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { startWith } from 'rxjs';
|
||||
import { WorkspaceBackNavigationService } from '@iptvnator/portal/shared/data-access';
|
||||
import {
|
||||
PORTAL_EXTERNAL_PLAYBACK,
|
||||
WorkspaceHeaderContextService,
|
||||
@@ -47,6 +48,7 @@ export class WorkspaceShellFacade {
|
||||
private readonly header = inject(WorkspaceShellHeaderService);
|
||||
private readonly xtreamImport = inject(WorkspaceShellXtreamImportService);
|
||||
readonly headerContext = inject(WorkspaceHeaderContextService);
|
||||
private readonly backNavigation = inject(WorkspaceBackNavigationService);
|
||||
|
||||
private readonly languageTick = toSignal(
|
||||
this.translate.onLangChange.pipe(startWith(null)),
|
||||
@@ -84,9 +86,7 @@ export class WorkspaceShellFacade {
|
||||
readonly currentUrl = this.routeState.currentUrl;
|
||||
readonly currentRoute = this.routeState.currentRoute;
|
||||
readonly showDashboard = this.routeState.showDashboard;
|
||||
readonly brandLink = this.routeState.brandLink;
|
||||
readonly brandTooltipKey = this.routeState.brandTooltipKey;
|
||||
readonly brandAriaLabelKey = this.routeState.brandAriaLabelKey;
|
||||
readonly backTarget = this.backNavigation.target;
|
||||
readonly currentContext = this.routeState.currentContext;
|
||||
readonly currentSection = this.routeState.currentSection;
|
||||
readonly commandPaletteCommands = computed<WorkspaceResolvedCommandItem[]>(
|
||||
@@ -218,6 +218,10 @@ export class WorkspaceShellFacade {
|
||||
this.header.toggleLiveSidebar();
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
this.backNavigation.goBack();
|
||||
}
|
||||
|
||||
navigateToGlobalFavorites(): void {
|
||||
this.header.navigateToGlobalFavorites();
|
||||
}
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
<app-workspace-shell-rail
|
||||
[attr.inert]="contextDrawer.isOpen() ? '' : null"
|
||||
[isMacOS]="facade.isMacOS"
|
||||
[brandLink]="facade.brandLink()"
|
||||
[brandTooltipKey]="facade.brandTooltipKey()"
|
||||
[brandAriaLabelKey]="facade.brandAriaLabelKey()"
|
||||
[workspaceLinks]="facade.workspaceLinks()"
|
||||
[primaryContextLinks]="facade.primaryContextLinks()"
|
||||
[secondaryContextLinks]="facade.secondaryContextLinks()"
|
||||
@@ -28,6 +25,7 @@
|
||||
<app-workspace-shell-header
|
||||
#workspaceHeader
|
||||
[attr.inert]="contextDrawer.isOpen() ? '' : null"
|
||||
[backTarget]="facade.backTarget()"
|
||||
[playlistTitle]="facade.playlistTitle()"
|
||||
[playlistSubtitle]="facade.playlistSubtitle()"
|
||||
[canOpenPlaylistInfo]="facade.canOpenPlaylistInfo()"
|
||||
@@ -52,6 +50,7 @@
|
||||
[isContextDrawerOpen]="contextDrawer.isOpen()"
|
||||
[contextDrawerToggleAriaKey]="facade.contextDrawerLabelKeys().aria"
|
||||
[contextDrawerTooltipKey]="facade.contextDrawerLabelKeys().tooltip"
|
||||
(backRequested)="facade.goBack()"
|
||||
(contextDrawerToggleRequested)="contextDrawer.toggle()"
|
||||
(refreshPlaylistRequested)="facade.refreshCurrentPlaylist()"
|
||||
(searchChanged)="facade.onSearchInput($event)"
|
||||
|
||||
+4
-6
@@ -33,9 +33,6 @@ import { WorkspaceKeyboardShortcutsService } from '../workspace-keyboard-shortcu
|
||||
})
|
||||
class MockWorkspaceShellRailComponent {
|
||||
readonly isMacOS = input(false);
|
||||
readonly brandLink = input('/workspace/dashboard');
|
||||
readonly brandTooltipKey = input('WORKSPACE.SHELL.RAIL_DASHBOARD');
|
||||
readonly brandAriaLabelKey = input('WORKSPACE.SHELL.OPEN_DASHBOARD');
|
||||
readonly workspaceLinks = input<unknown[]>([]);
|
||||
readonly primaryContextLinks = input<unknown[]>([]);
|
||||
readonly secondaryContextLinks = input<unknown[]>([]);
|
||||
@@ -51,6 +48,7 @@ class MockWorkspaceShellRailComponent {
|
||||
standalone: true,
|
||||
})
|
||||
class MockWorkspaceShellHeaderComponent {
|
||||
readonly backTarget = input<unknown>(null);
|
||||
readonly playlistTitle = input('');
|
||||
readonly playlistSubtitle = input('');
|
||||
readonly canOpenPlaylistInfo = input(false);
|
||||
@@ -75,6 +73,7 @@ class MockWorkspaceShellHeaderComponent {
|
||||
readonly headerBulkAction = input<WorkspaceHeaderBulkAction | null>(null);
|
||||
readonly headerSidebarToggle = input<unknown>(null);
|
||||
readonly parentalLockState = input<'off' | 'locked' | 'unlocked'>('off');
|
||||
readonly backRequested = output<void>();
|
||||
readonly searchChanged = output<string>();
|
||||
readonly searchSubmitted = output<string>();
|
||||
readonly commandPaletteRequested = output<void>();
|
||||
@@ -152,9 +151,8 @@ class MockWorkspaceKeyboardShortcutsService {
|
||||
}
|
||||
|
||||
class MockWorkspaceShellFacade {
|
||||
readonly brandLink = signal('/workspace/dashboard');
|
||||
readonly brandTooltipKey = signal('WORKSPACE.SHELL.RAIL_DASHBOARD');
|
||||
readonly brandAriaLabelKey = signal('WORKSPACE.SHELL.OPEN_DASHBOARD');
|
||||
readonly backTarget = signal(null);
|
||||
goBack = jest.fn();
|
||||
readonly workspaceLinks = signal([]);
|
||||
readonly primaryContextLinks = signal([]);
|
||||
readonly secondaryContextLinks = signal([]);
|
||||
|
||||
+4
-3
@@ -14,6 +14,7 @@ import {
|
||||
} from '@iptvnator/m3u-state';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { WorkspaceShellContextDrawerService } from '@iptvnator/workspace/shell/util';
|
||||
import { SOURCE_TYPE_ICONS } from '@iptvnator/shared/interfaces';
|
||||
|
||||
type PlaylistFilterId = 'all' | 'm3u' | 'xtream' | 'stalker';
|
||||
|
||||
@@ -55,17 +56,17 @@ export class WorkspaceSourcesFiltersPanelComponent {
|
||||
},
|
||||
{
|
||||
id: 'm3u',
|
||||
icon: 'playlist_play',
|
||||
icon: SOURCE_TYPE_ICONS.m3u,
|
||||
translationKey: 'HOME.PLAYLIST_TYPES.M3U',
|
||||
},
|
||||
{
|
||||
id: 'xtream',
|
||||
icon: 'cloud',
|
||||
icon: SOURCE_TYPE_ICONS.xtream,
|
||||
translationKey: 'HOME.PLAYLIST_TYPES.XTREAM',
|
||||
},
|
||||
{
|
||||
id: 'stalker',
|
||||
icon: 'router',
|
||||
icon: SOURCE_TYPE_ICONS.stalker,
|
||||
translationKey: 'HOME.PLAYLIST_TYPES.STALKER',
|
||||
},
|
||||
];
|
||||
|
||||
Reference in new issue
Block a user