From 05ea3ad636051339b1c609ffec1d5705e95476b5 Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 31 Jul 2026 11:57:47 +0200 Subject: [PATCH] feat(downloads): add focused offline detail routes --- apps/web/src/app/app.routes.ts | 7 + libs/portal/downloads/feature/src/index.ts | 1 + .../download-offline-detail.component.ts | 10 ++ .../feature/src/lib/stalker-feature.routes.ts | 9 ++ .../feature/src/lib/xtream-feature.routes.ts | 21 ++- .../workspace-shell-route.utils.spec.ts | 126 +++++++++++++++++- .../navigation/workspace-shell-route.utils.ts | 38 ++++-- 7 files changed, 191 insertions(+), 21 deletions(-) create mode 100644 libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.component.ts diff --git a/apps/web/src/app/app.routes.ts b/apps/web/src/app/app.routes.ts index 73361f1fc..0fd1f60c6 100644 --- a/apps/web/src/app/app.routes.ts +++ b/apps/web/src/app/app.routes.ts @@ -111,6 +111,13 @@ export const routes: Routes = [ (c) => c.GlobalSearchResultsComponent ), }, + { + path: 'downloads/:downloadId', + loadComponent: () => + import('@iptvnator/portal/downloads/feature').then( + (c) => c.DownloadOfflineDetailComponent + ), + }, { path: 'downloads', loadComponent: () => diff --git a/libs/portal/downloads/feature/src/index.ts b/libs/portal/downloads/feature/src/index.ts index 0499a3ee2..f0a82109d 100644 --- a/libs/portal/downloads/feature/src/index.ts +++ b/libs/portal/downloads/feature/src/index.ts @@ -1 +1,2 @@ export * from './lib/downloads.component'; +export * from './lib/offline-detail/download-offline-detail.component'; diff --git a/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.component.ts b/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.component.ts new file mode 100644 index 000000000..1b96f8f35 --- /dev/null +++ b/libs/portal/downloads/feature/src/lib/offline-detail/download-offline-detail.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { PortalDetailShellComponent } from '@iptvnator/ui/components'; + +@Component({ + selector: 'app-download-offline-detail', + imports: [PortalDetailShellComponent], + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DownloadOfflineDetailComponent {} diff --git a/libs/portal/stalker/feature/src/lib/stalker-feature.routes.ts b/libs/portal/stalker/feature/src/lib/stalker-feature.routes.ts index c2722c818..52d484ea4 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-feature.routes.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-feature.routes.ts @@ -11,6 +11,11 @@ const loadDownloadsComponent: ComponentLoader = () => (c) => c.DownloadsComponent ); +const loadDownloadOfflineDetailComponent: ComponentLoader = () => + import('@iptvnator/portal/downloads/feature').then( + (c) => c.DownloadOfflineDetailComponent + ); + const loadStalkerLiveStreamLayoutComponent: ComponentLoader = () => import('./stalker-live-stream-layout/stalker-live-stream-layout.component').then( (c) => c.StalkerLiveStreamLayoutComponent @@ -129,6 +134,10 @@ export function createStalkerRoutes(): Route[] { path: 'actor/:personId', loadComponent: loadStalkerActorRouteComponent, }, + { + path: 'downloads/:downloadId', + loadComponent: loadDownloadOfflineDetailComponent, + }, { path: 'downloads', loadComponent: loadDownloadsComponent, diff --git a/libs/portal/xtream/feature/src/lib/xtream-feature.routes.ts b/libs/portal/xtream/feature/src/lib/xtream-feature.routes.ts index 9bd1a8d82..6407a92e5 100644 --- a/libs/portal/xtream/feature/src/lib/xtream-feature.routes.ts +++ b/libs/portal/xtream/feature/src/lib/xtream-feature.routes.ts @@ -9,6 +9,11 @@ const loadDownloadsComponent: ComponentLoader = () => (c) => c.DownloadsComponent ); +const loadDownloadOfflineDetailComponent: ComponentLoader = () => + import('@iptvnator/portal/downloads/feature').then( + (c) => c.DownloadOfflineDetailComponent + ); + const loadXtreamContentGateComponent: ComponentLoader = () => import('./xtream-content-gate.component').then( (c) => c.XtreamContentGateComponent @@ -83,11 +88,13 @@ export function createXtreamRoutes(): Route[] { children: [ { path: '', - loadComponent: loadCategoryContentViewComponent, + loadComponent: + loadCategoryContentViewComponent, }, { path: ':categoryId', - loadComponent: loadCategoryContentViewComponent, + loadComponent: + loadCategoryContentViewComponent, }, { path: ':categoryId/:vodId', @@ -101,11 +108,13 @@ export function createXtreamRoutes(): Route[] { children: [ { path: '', - loadComponent: loadCategoryContentViewComponent, + loadComponent: + loadCategoryContentViewComponent, }, { path: ':categoryId', - loadComponent: loadCategoryContentViewComponent, + loadComponent: + loadCategoryContentViewComponent, }, { path: ':categoryId/:serialId', @@ -137,6 +146,10 @@ export function createXtreamRoutes(): Route[] { loadComponent: loadXtreamCollectionRouteComponent, data: { mode: 'recent', portalType: 'xtream' }, }, + { + path: 'downloads/:downloadId', + loadComponent: loadDownloadOfflineDetailComponent, + }, { path: 'downloads', loadComponent: loadDownloadsComponent, diff --git a/libs/workspace/shell/util/src/lib/navigation/workspace-shell-route.utils.spec.ts b/libs/workspace/shell/util/src/lib/navigation/workspace-shell-route.utils.spec.ts index e6421a411..9f9821162 100644 --- a/libs/workspace/shell/util/src/lib/navigation/workspace-shell-route.utils.spec.ts +++ b/libs/workspace/shell/util/src/lib/navigation/workspace-shell-route.utils.spec.ts @@ -54,6 +54,128 @@ describe('workspace-shell-route.utils', () => { expect(parseWorkspaceShellRoute('/workspace/downloads')).toEqual( expect.objectContaining({ kind: 'downloads', + section: null, + contextPanel: 'none', + searchMode: 'local-filter', + usesQuerySearch: true, + }) + ); + }); + + it('disables collection shell state for focused download details', () => { + expect( + parseWorkspaceShellRoute( + '/workspace/downloads/42?query=matrix#technical-details' + ) + ).toEqual( + expect.objectContaining({ + kind: 'downloads', + context: null, + section: null, + contextPanel: 'none', + searchMode: 'none', + usesQuerySearch: false, + }) + ); + + expect( + parseWorkspaceShellRoute('/workspace/xtreams/pl-1/downloads/42') + ).toEqual( + expect.objectContaining({ + kind: 'portal', + context: { + provider: 'xtreams', + playlistId: 'pl-1', + }, + section: 'downloads', + contextPanel: 'none', + searchMode: 'none', + usesQuerySearch: false, + }) + ); + + expect( + parseWorkspaceShellRoute('/workspace/stalker/pl-1/downloads/42') + ).toEqual( + expect.objectContaining({ + kind: 'portal', + context: { + provider: 'stalker', + playlistId: 'pl-1', + }, + section: 'downloads', + contextPanel: 'none', + searchMode: 'none', + usesQuerySearch: false, + }) + ); + }); + + it('keeps manager shell state outside exact focused download routes', () => { + expect(parseWorkspaceShellRoute('/workspace/downloads/')).toEqual( + expect.objectContaining({ + kind: 'downloads', + contextPanel: 'none', + searchMode: 'local-filter', + usesQuerySearch: true, + }) + ); + expect( + parseWorkspaceShellRoute('/workspace/downloads/42/extra') + ).toEqual( + expect.objectContaining({ + kind: 'downloads', + contextPanel: 'none', + searchMode: 'local-filter', + usesQuerySearch: true, + }) + ); + expect( + parseWorkspaceShellRoute('/workspace/xtreams/pl-1/downloads') + ).toEqual( + expect.objectContaining({ + kind: 'portal', + section: 'downloads', + contextPanel: 'collection', + searchMode: 'local-filter', + usesQuerySearch: true, + }) + ); + expect( + parseWorkspaceShellRoute( + '/workspace/xtreams/pl-1/downloads/42/extra' + ) + ).toEqual( + expect.objectContaining({ + kind: 'portal', + section: 'downloads', + contextPanel: 'collection', + searchMode: 'local-filter', + usesQuerySearch: true, + }) + ); + expect( + parseWorkspaceShellRoute('/workspace/stalker/pl-1/downloads') + ).toEqual( + expect.objectContaining({ + kind: 'portal', + section: 'downloads', + contextPanel: 'collection', + searchMode: 'local-filter', + usesQuerySearch: true, + }) + ); + expect( + parseWorkspaceShellRoute( + '/workspace/stalker/pl-1/downloads/42/extra' + ) + ).toEqual( + expect.objectContaining({ + kind: 'portal', + section: 'downloads', + contextPanel: 'collection', + searchMode: 'local-filter', + usesQuerySearch: true, }) ); }); @@ -103,9 +225,7 @@ describe('workspace-shell-route.utils', () => { }); it('parses Stalker portal routes and detects context-panel/query-search state', () => { - expect( - parseWorkspaceShellRoute('/workspace/stalker/pl-2/itv') - ).toEqual( + expect(parseWorkspaceShellRoute('/workspace/stalker/pl-2/itv')).toEqual( expect.objectContaining({ kind: 'portal', context: { diff --git a/libs/workspace/shell/util/src/lib/navigation/workspace-shell-route.utils.ts b/libs/workspace/shell/util/src/lib/navigation/workspace-shell-route.utils.ts index 0bbd49cce..7de345d4f 100644 --- a/libs/workspace/shell/util/src/lib/navigation/workspace-shell-route.utils.ts +++ b/libs/workspace/shell/util/src/lib/navigation/workspace-shell-route.utils.ts @@ -15,11 +15,7 @@ export type WorkspaceShellPageKind = | 'unknown'; export type WorkspaceShellContextPanel = - | 'none' - | 'sources' - | 'settings' - | 'category' - | 'collection'; + 'none' | 'sources' | 'settings' | 'category' | 'collection'; export interface WorkspacePortalContext { provider: PortalProvider; @@ -27,10 +23,7 @@ export interface WorkspacePortalContext { } export type WorkspaceShellSearchMode = - | 'none' - | 'local-filter' - | 'remote-search' - | 'advanced-only'; + 'none' | 'local-filter' | 'remote-search' | 'advanced-only'; export interface WorkspaceShellRoute { kind: WorkspaceShellPageKind; @@ -266,6 +259,11 @@ export function parseWorkspaceShellRoute(url: string): WorkspaceShellRoute { playlistId: segments[2], }; const section = asPortalRailSection(sectionSegment); + const isFocusedDownloadDetail = + (provider === 'xtreams' || provider === 'stalker') && + section === 'downloads' && + segments.length === 5 && + Boolean(segments[4]); return { kind: 'portal', @@ -275,9 +273,15 @@ export function parseWorkspaceShellRoute(url: string): WorkspaceShellRoute { section === 'favorites' && (provider === 'xtreams' || provider === 'stalker') && queryParams.get('scope') === 'all', - searchMode: resolveRouteSearchMode('portal', context, section), - usesQuerySearch: usesWorkspaceRouteQuerySearch(context, section), - contextPanel: resolveContextPanel('portal', context, section), + searchMode: isFocusedDownloadDetail + ? 'none' + : resolveRouteSearchMode('portal', context, section), + usesQuerySearch: isFocusedDownloadDetail + ? false + : usesWorkspaceRouteQuerySearch(context, section), + contextPanel: isFocusedDownloadDetail + ? 'none' + : resolveContextPanel('portal', context, section), }; } @@ -298,7 +302,11 @@ export function parseWorkspaceShellRoute(url: string): WorkspaceShellRoute { : page === 'downloads' ? 'downloads' : 'unknown'; - const searchMode = resolveRouteSearchMode(kind, null, null); + const isFocusedDownloadDetail = + kind === 'downloads' && segments.length === 3 && Boolean(segments[2]); + const searchMode = isFocusedDownloadDetail + ? 'none' + : resolveRouteSearchMode(kind, null, null); return { kind, @@ -308,6 +316,8 @@ export function parseWorkspaceShellRoute(url: string): WorkspaceShellRoute { searchMode, usesQuerySearch: searchMode === 'local-filter' || searchMode === 'remote-search', - contextPanel: resolveContextPanel(kind, null, null), + contextPanel: isFocusedDownloadDetail + ? 'none' + : resolveContextPanel(kind, null, null), }; }