diff --git a/libs/portal/shared/ui/src/index.ts b/libs/portal/shared/ui/src/index.ts index 4187709bb..216b620a9 100644 --- a/libs/portal/shared/ui/src/index.ts +++ b/libs/portal/shared/ui/src/index.ts @@ -1,5 +1,6 @@ export * from './lib/components/category-view/category-view.component'; export * from './lib/components/content-card/content-card.component'; +export * from './lib/components/content-rail-shell/content-rail-shell.component'; export * from './lib/components/favorites-layout/favorites-layout.component'; export * from './lib/components/global-favorites-list/global-favorites-list.component'; export * from './lib/components/grid-list/grid-list.component'; diff --git a/libs/portal/shared/util/src/lib/collection/unified-collection-item.interface.ts b/libs/portal/shared/util/src/lib/collection/unified-collection-item.interface.ts index 4966a87b8..1909ad0af 100644 --- a/libs/portal/shared/util/src/lib/collection/unified-collection-item.interface.ts +++ b/libs/portal/shared/util/src/lib/collection/unified-collection-item.interface.ts @@ -86,3 +86,11 @@ export function buildCollectionUid( ): string { return `${sourceType}::${playlistId}::${sourceItemId}`; } + +export function buildXtreamCollectionUid( + playlistId: string, + contentType: CollectionContentType, + xtreamId: string | number +): string { + return buildCollectionUid('xtream', playlistId, `${contentType}:${xtreamId}`); +} diff --git a/libs/portal/shared/util/src/lib/workspace-header-context.service.ts b/libs/portal/shared/util/src/lib/workspace-header-context.service.ts index 32f2a661d..79bba2c1f 100644 --- a/libs/portal/shared/util/src/lib/workspace-header-context.service.ts +++ b/libs/portal/shared/util/src/lib/workspace-header-context.service.ts @@ -1,4 +1,5 @@ import { Injectable, signal } from '@angular/core'; +import { WorkspaceHeaderCommandMetadata } from './workspace-view-command.types'; export interface WorkspaceHeaderAction { id: string; @@ -6,6 +7,7 @@ export interface WorkspaceHeaderAction { tooltipKey: string; ariaLabelKey: string; run: () => void; + palette?: WorkspaceHeaderCommandMetadata; } @Injectable({ providedIn: 'root' }) diff --git a/libs/portal/shared/util/src/lib/workspace-view-command.service.ts b/libs/portal/shared/util/src/lib/workspace-view-command.service.ts new file mode 100644 index 000000000..197b477d9 --- /dev/null +++ b/libs/portal/shared/util/src/lib/workspace-view-command.service.ts @@ -0,0 +1,63 @@ +import { Injectable, computed, inject, signal } from '@angular/core'; +import { + WorkspaceHeaderAction, + WorkspaceHeaderContextService, +} from './workspace-header-context.service'; +import { WorkspaceCommandContribution } from './workspace-view-command.types'; + +interface RegisteredWorkspaceCommand { + token: symbol; + command: WorkspaceCommandContribution; +} + +@Injectable({ providedIn: 'root' }) +export class WorkspaceViewCommandService { + private readonly headerContext = inject(WorkspaceHeaderContextService); + private readonly registeredCommands = signal( + [] + ); + + readonly commands = computed(() => { + const commands = this.registeredCommands().map( + (entry) => entry.command + ); + const headerAction = this.headerContext.action(); + const headerCommand = headerAction + ? this.toHeaderCommand(headerAction) + : null; + + return headerCommand ? [headerCommand, ...commands] : commands; + }); + + registerCommand(command: WorkspaceCommandContribution): () => void { + const token = Symbol(command.id); + this.registeredCommands.update((entries) => [...entries, { token, command }]); + + return () => { + this.registeredCommands.update((entries) => + entries.filter((entry) => entry.token !== token) + ); + }; + } + + private toHeaderCommand( + action: WorkspaceHeaderAction + ): WorkspaceCommandContribution { + const palette = action.palette; + + return { + id: action.id, + group: palette?.group ?? 'view', + icon: action.icon, + labelKey: palette?.labelKey ?? action.tooltipKey, + labelParams: palette?.labelParams, + descriptionKey: palette?.descriptionKey, + descriptionParams: palette?.descriptionParams, + priority: palette?.priority ?? 0, + keywords: palette?.keywords, + visible: palette?.visible, + enabled: palette?.enabled, + run: () => action.run(), + }; + } +} diff --git a/libs/portal/shared/util/src/lib/workspace-view-command.types.ts b/libs/portal/shared/util/src/lib/workspace-view-command.types.ts new file mode 100644 index 000000000..5cd305609 --- /dev/null +++ b/libs/portal/shared/util/src/lib/workspace-view-command.types.ts @@ -0,0 +1,53 @@ +export type WorkspaceCommandGroup = 'view' | 'playlist' | 'global'; + +export interface WorkspaceCommandActionContext { + query: string; +} + +export type WorkspaceCommandParams = Record; + +export type WorkspaceCommandValue = T | (() => T); + +export interface WorkspaceCommandMetadata { + labelKey: string; + labelParams?: + | WorkspaceCommandParams + | (() => WorkspaceCommandParams | undefined); + descriptionKey?: string; + descriptionParams?: + | WorkspaceCommandParams + | (() => WorkspaceCommandParams | undefined); + priority?: number; + keywords?: readonly string[] | (() => readonly string[] | undefined); + visible?: boolean | (() => boolean | undefined); + enabled?: boolean | (() => boolean | undefined); +} + +export interface WorkspaceCommandContribution extends WorkspaceCommandMetadata { + id: string; + group: WorkspaceCommandGroup; + icon: string; + run: (context: WorkspaceCommandActionContext) => void; +} + +export interface WorkspaceHeaderCommandMetadata extends WorkspaceCommandMetadata { + group?: WorkspaceCommandGroup; +} + +export interface WorkspaceResolvedCommandItem { + id: string; + group: WorkspaceCommandGroup; + icon: string; + label: string; + description: string; + keywords: readonly string[]; + priority: number; + visible: boolean; + enabled: boolean; + run: (context: WorkspaceCommandActionContext) => void; +} + +export interface WorkspaceCommandSelection { + commandId: string; + query: string; +}