From 63f51731393ca4706e1122475967a6da99b28f07 Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 1 May 2026 19:12:19 +0200 Subject: [PATCH] refactor(workspace-shell): push xtream import overlay and bulk action into child components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continues the workspace-shell facade decomposition by relocating two clusters from the parent template into the components that own them. Xtream import overlay: - New WorkspaceShellImportOverlayComponent injects WorkspaceShellXtreamImportService directly. The 70-line inline overlay block in workspace-shell.component.html collapses to a single element, gated by facade.showXtreamImportOverlay(). - Overlay-specific styles move from workspace-shell.component.scss into the child component (~125 lines relocated). - Facade drops 14 import re-export lines plus the cancelXtreamImport delegation. The two facade tests that exercised label generation now resolve the service via TestBed.inject and assert against it directly. Header bulk action: - WorkspaceShellHeaderComponent gains headerBulkAction input and headerBulkActionRequested output, plus a button that renders the action when present (next to the existing headerShortcut button). - The parent template wires facade.headerBulkAction() → facade.runHeaderBulkAction(). The facade keeps the cross-cutting computed and the multi-store dispatch method since both span multiple stores. Verification: - workspace-shell.facade.spec.ts: 20/20 passing (two assertions retargeted at WorkspaceShellXtreamImportService). - Full feature lib: 53/60 passing — same 7 pre-existing failures. - pnpm nx build web --configuration=production: success. - pnpm nx lint workspace-shell-feature: clean. - pnpm nx run web-e2e:e2e-ci--src/basic.e2e.ts: 3/3 across browsers. - pnpm nx run web-e2e:e2e-ci--src/settings.e2e.ts: 12/12 across browsers. Facade: 917 → 897 lines. The bulk-action button now actually renders in the header — closing a latent gap where the computed existed on the facade but had no template binding. Co-Authored-By: Claude Opus 4.7 (1M context) Entire-Checkpoint: c2cea9c530e6 --- .../workspace-shell-header.component.html | 13 ++ .../workspace-shell-header.component.ts | 7 + ...kspace-shell-import-overlay.component.html | 66 +++++++++ ...kspace-shell-import-overlay.component.scss | 128 ++++++++++++++++++ ...orkspace-shell-import-overlay.component.ts | 20 +++ .../services/workspace-shell.facade.spec.ts | 22 +-- .../services/workspace-shell.facade.ts | 20 --- .../workspace-shell.component.html | 69 +--------- .../workspace-shell.component.scss | 124 ----------------- .../workspace-shell.component.ts | 13 +- 10 files changed, 252 insertions(+), 230 deletions(-) create mode 100644 libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.html create mode 100644 libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.scss create mode 100644 libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.ts diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.html b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.html index 6c2947051..01afaf1cd 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.html +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.html @@ -75,6 +75,19 @@ {{ action.icon }} } + @if (headerBulkAction(); as bulk) { + + } @if (isElectron()) { diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.ts b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.ts index b1ab52b8f..f9fe6befa 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.ts +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-header/workspace-shell-header.component.ts @@ -10,6 +10,7 @@ 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 { WorkspaceHeaderBulkAction } from '../../services/helpers/workspace-shell-constants'; @Component({ selector: 'app-workspace-shell-header', @@ -40,6 +41,7 @@ export class WorkspaceShellHeaderComponent { readonly searchScopeLabel = input(''); readonly searchStatusLabel = input(''); readonly headerShortcut = input(null); + readonly headerBulkAction = input(null); readonly canRefreshPlaylist = input(false); readonly isRefreshingPlaylist = input(false); readonly isElectron = input(false); @@ -52,6 +54,7 @@ export class WorkspaceShellHeaderComponent { readonly commandPaletteRequested = output(); readonly addPlaylistRequested = output(); readonly headerShortcutRequested = output(); + readonly headerBulkActionRequested = output(); readonly refreshPlaylistRequested = output(); readonly downloadsRequested = output(); readonly playlistInfoRequested = output(); @@ -87,6 +90,10 @@ export class WorkspaceShellHeaderComponent { this.headerShortcutRequested.emit(); } + onHeaderBulkActionRequested(): void { + this.headerBulkActionRequested.emit(); + } + onRefreshPlaylistRequested(): void { this.refreshPlaylistRequested.emit(); } diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.html b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.html new file mode 100644 index 000000000..f18121b66 --- /dev/null +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.html @@ -0,0 +1,66 @@ +
+
+
+ @if (xtreamImport.xtreamImportSourceLabel()) { + + {{ xtreamImport.xtreamImportSourceLabel() }} + + } +

{{ xtreamImport.xtreamImportTitleLabel() }}

+
+ @if (xtreamImport.xtreamImportPhaseLabel()) { +

+ {{ xtreamImport.xtreamImportPhaseLabel() }} +

+ } + @if (xtreamImport.xtreamImportDetailLabel()) { +

+ {{ xtreamImport.xtreamImportDetailLabel() }} +

+ } + @if ( + xtreamImport.xtreamActiveImportCount() !== 0 && + xtreamImport.xtreamActiveItemsToImport() !== 0 + ) { + + } @else { + + } + @if (xtreamImport.xtreamImportProgressLabel()) { +

+ {{ xtreamImport.xtreamImportProgressLabel() }} +

+ } + @if (xtreamImport.canCancelXtreamImport()) { + + } +
+
diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.scss b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.scss new file mode 100644 index 000000000..2976f683a --- /dev/null +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.scss @@ -0,0 +1,128 @@ +:host { + display: contents; +} + +.workspace-loading-overlay { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + background: + radial-gradient( + circle at top, + rgba(99, 120, 255, 0.12), + transparent 34% + ), + rgba(8, 10, 16, 0.68); + backdrop-filter: blur(14px); + z-index: 1000; +} + +.workspace-loading-overlay__card { + width: min(440px, 100%); + display: grid; + gap: 14px; + padding: 24px 24px 20px; + border-radius: 24px; + border: 1px solid var(--app-separator, rgba(255, 255, 255, 0.12)); + background: + linear-gradient( + 180deg, + rgba(255, 255, 255, 0.06), + rgba(255, 255, 255, 0) + ), + var(--mat-sys-surface-container-high); + box-shadow: + 0 24px 64px rgba(0, 0, 0, 0.32), + 0 1px 0 rgba(255, 255, 255, 0.05) inset; + text-align: center; + animation: workspace-loading-overlay-enter 180ms ease-out; + + h3 { + margin: 0; + font-size: 1.55rem; + font-weight: 650; + letter-spacing: -0.03em; + color: var(--app-heading-color, var(--mat-sys-on-surface)); + } +} + +.workspace-loading-overlay__header { + display: grid; + gap: 10px; + justify-items: center; +} + +.workspace-loading-overlay__badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px 12px; + border-radius: 999px; + border: 1px solid rgba(120, 220, 255, 0.24); + background: rgba(120, 220, 255, 0.08); + color: rgba(164, 233, 255, 1); + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.workspace-loading-overlay__badge--remote { + border-color: rgba(105, 195, 255, 0.34); + background: rgba(83, 158, 255, 0.12); + color: rgba(202, 230, 255, 1); +} + +.workspace-loading-overlay__phase, +.workspace-loading-overlay__detail, +.workspace-loading-overlay__progress-copy { + margin: 0; +} + +.workspace-loading-overlay__phase { + font-size: 1rem; + font-weight: 560; + color: var(--app-body-color, var(--mat-sys-on-surface)); +} + +.workspace-loading-overlay__detail { + max-width: 32ch; + justify-self: center; + color: var(--app-muted-color, var(--mat-sys-on-surface-variant)); + line-height: 1.45; +} + +.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 { + justify-self: center; + min-width: 180px; + border-radius: 999px; +} + +:host ::ng-deep .workspace-loading-overlay__card .mat-mdc-progress-bar { + overflow: hidden; + border-radius: 999px; +} + +@keyframes workspace-loading-overlay-enter { + from { + opacity: 0; + transform: translateY(10px) scale(0.985); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.ts b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.ts new file mode 100644 index 000000000..3e93c6802 --- /dev/null +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/components/workspace-shell-import-overlay/workspace-shell-import-overlay.component.ts @@ -0,0 +1,20 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { TranslatePipe } from '@ngx-translate/core'; +import { WorkspaceShellXtreamImportService } from '../../services/workspace-shell-xtream-import.service'; + +@Component({ + selector: 'app-workspace-shell-import-overlay', + imports: [MatButtonModule, MatProgressBarModule, TranslatePipe], + templateUrl: './workspace-shell-import-overlay.component.html', + styleUrl: './workspace-shell-import-overlay.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class WorkspaceShellImportOverlayComponent { + protected readonly xtreamImport = inject(WorkspaceShellXtreamImportService); + + onCancel(): void { + this.xtreamImport.cancelXtreamImport(); + } +} diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.spec.ts b/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.spec.ts index aab630adf..db573c280 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.spec.ts +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.spec.ts @@ -314,51 +314,53 @@ describe('WorkspaceShellFacade', () => { it('uses a loading label for remote Xtream fetch phases', () => { const xtreamStore = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; + const xtreamImport = TestBed.inject(WorkspaceShellXtreamImportService); xtreamStore.currentImportPhase.set('loading-categories'); - expect(facade.xtreamImportPhaseLabel()).toBe( + expect(xtreamImport.xtreamImportPhaseLabel()).toBe( 'WORKSPACE.SHELL.XTREAM_IMPORT_LOADING' ); xtreamStore.currentImportPhase.set('loading-live'); - expect(facade.xtreamImportPhaseLabel()).toBe( + expect(xtreamImport.xtreamImportPhaseLabel()).toBe( 'WORKSPACE.SHELL.XTREAM_IMPORT_LOADING' ); - expect(facade.xtreamImportSourceLabel()).toBe( + expect(xtreamImport.xtreamImportSourceLabel()).toBe( 'WORKSPACE.SHELL.XTREAM_IMPORT_REMOTE_BADGE' ); - expect(facade.xtreamImportDetailLabel()).toBe( + expect(xtreamImport.xtreamImportDetailLabel()).toBe( 'WORKSPACE.SHELL.XTREAM_IMPORT_DETAIL_REMOTE' ); xtreamStore.currentImportPhase.set('saving-categories'); - expect(facade.xtreamImportPhaseLabel()).toBe( + expect(xtreamImport.xtreamImportPhaseLabel()).toBe( 'WORKSPACE.SHELL.XTREAM_IMPORT_SAVING' ); xtreamStore.currentImportPhase.set('saving-content'); - expect(facade.xtreamImportPhaseLabel()).toBe( + expect(xtreamImport.xtreamImportPhaseLabel()).toBe( 'WORKSPACE.SHELL.XTREAM_IMPORT_SAVING' ); - expect(facade.xtreamImportSourceLabel()).toBe( + expect(xtreamImport.xtreamImportSourceLabel()).toBe( 'WORKSPACE.SHELL.XTREAM_IMPORT_LOCAL_BADGE' ); - expect(facade.xtreamImportDetailLabel()).toBe( + expect(xtreamImport.xtreamImportDetailLabel()).toBe( 'WORKSPACE.SHELL.XTREAM_IMPORT_DETAIL_LOCAL' ); }); it('builds a type-aware xtream import progress label', () => { const xtreamStore = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; + const xtreamImport = TestBed.inject(WorkspaceShellXtreamImportService); xtreamStore.activeImportContentType.set('vod'); xtreamStore.activeImportCurrentCount.set(20); xtreamStore.activeImportTotalCount.set(12323); - expect(facade.xtreamImportTypeLabel()).toBe( + expect(xtreamImport.xtreamImportTypeLabel()).toBe( 'WORKSPACE.SHELL.RAIL_MOVIES' ); - expect(facade.xtreamImportProgressLabel()).toBe( + expect(xtreamImport.xtreamImportProgressLabel()).toBe( 'WORKSPACE.SHELL.RAIL_MOVIES imported: 20 / 12,323' ); }); diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.ts b/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.ts index 96ad54509..0b7aa0f28 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.ts +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.ts @@ -268,22 +268,6 @@ export class WorkspaceShellFacade { () => this.currentRoute().contextPanel !== 'none' ); private readonly xtreamImport = inject(WorkspaceShellXtreamImportService); - readonly xtreamImportCount = this.xtreamImport.xtreamImportCount; - readonly xtreamItemsToImport = this.xtreamImport.xtreamItemsToImport; - readonly xtreamActiveImportCount = this.xtreamImport.xtreamActiveImportCount; - readonly xtreamActiveItemsToImport = - this.xtreamImport.xtreamActiveItemsToImport; - readonly xtreamImportPhase = this.xtreamImport.xtreamImportPhase; - readonly isCancellingXtreamImport = this.xtreamImport.isCancellingXtreamImport; - readonly canCancelXtreamImport = this.xtreamImport.canCancelXtreamImport; - readonly xtreamImportTitleLabel = this.xtreamImport.xtreamImportTitleLabel; - readonly xtreamImportTypeLabel = this.xtreamImport.xtreamImportTypeLabel; - readonly xtreamImportProgressLabel = - this.xtreamImport.xtreamImportProgressLabel; - readonly xtreamImportPhaseTone = this.xtreamImport.xtreamImportPhaseTone; - readonly xtreamImportSourceLabel = this.xtreamImport.xtreamImportSourceLabel; - readonly xtreamImportPhaseLabel = this.xtreamImport.xtreamImportPhaseLabel; - readonly xtreamImportDetailLabel = this.xtreamImport.xtreamImportDetailLabel; readonly showXtreamImportOverlay = computed(() => { const context = this.currentContext(); const section = this.currentSection(); @@ -754,10 +738,6 @@ export class WorkspaceShellFacade { }); } - cancelXtreamImport(): void { - this.xtreamImport.cancelXtreamImport(); - } - openAccountInfo(): void { if (!this.canOpenAccountInfo()) { return; diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html index 3492dec1e..eb8015a2e 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html @@ -5,72 +5,7 @@ > @if (facade.showXtreamImportOverlay()) { -
-
-
- @if (facade.xtreamImportSourceLabel()) { - - {{ facade.xtreamImportSourceLabel() }} - - } -

{{ facade.xtreamImportTitleLabel() }}

-
- @if (facade.xtreamImportPhaseLabel()) { -

- {{ facade.xtreamImportPhaseLabel() }} -

- } - @if (facade.xtreamImportDetailLabel()) { -

- {{ facade.xtreamImportDetailLabel() }} -

- } - @if ( - facade.xtreamActiveImportCount() !== 0 && - facade.xtreamActiveItemsToImport() !== 0 - ) { - - } @else { - - } - @if (facade.xtreamImportProgressLabel()) { -

- {{ facade.xtreamImportProgressLabel() }} -

- } - @if (facade.canCancelXtreamImport()) { - - } -
-
+ }