mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
feat: enhance routing with reactive route parameter signals and update catalog facade for search query handling
This commit is contained in:
1 parent
c58c7468e2
commit
5a070f5550
7 files changed
+374
-85
No files matched your search
+21
-4
@@ -1,5 +1,12 @@
|
||||
import { NgComponentOutlet } from '@angular/common';
|
||||
import { Component, computed, inject, OnInit } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
computed,
|
||||
DestroyRef,
|
||||
inject,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
@@ -49,6 +56,7 @@ interface CategoryContentItem {
|
||||
})
|
||||
export class CategoryContentViewComponent implements OnInit {
|
||||
private readonly activatedRoute = inject(ActivatedRoute);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly router = inject(Router);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly catalog = inject(PORTAL_CATALOG_FACADE) as PortalCatalogFacade<
|
||||
@@ -102,9 +110,18 @@ export class CategoryContentViewComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const { categoryId } = this.activatedRoute.snapshot.params;
|
||||
this.catalog.initialize(categoryId ?? null);
|
||||
this.openStalkerItemFromNavigationState();
|
||||
this.activatedRoute.paramMap
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((params) => {
|
||||
this.catalog.initialize(params.get('categoryId'));
|
||||
this.openStalkerItemFromNavigationState();
|
||||
});
|
||||
|
||||
this.activatedRoute.queryParamMap
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((params) => {
|
||||
this.catalog.setSearchQuery?.(params.get('q') ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent): void {
|
||||
|
||||
+130
-12
@@ -20,7 +20,7 @@ import {
|
||||
selectAllPlaylistsMeta,
|
||||
selectPlaylistsLoadingFlag,
|
||||
} from 'm3u-state';
|
||||
import { of } from 'rxjs';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { PlaylistMeta } from 'shared-interfaces';
|
||||
import { UnifiedCollectionPageComponent } from './unified-collection-page.component';
|
||||
import { UnifiedGridTabComponent } from './unified-grid-tab.component';
|
||||
@@ -61,13 +61,26 @@ describe('UnifiedCollectionPageComponent', () => {
|
||||
let fixture: ComponentFixture<UnifiedCollectionPageComponent>;
|
||||
let route: ActivatedRoute & {
|
||||
snapshot: {
|
||||
paramMap: ReturnType<typeof convertToParamMap>;
|
||||
queryParamMap: ReturnType<typeof convertToParamMap>;
|
||||
queryParams: Record<string, unknown>;
|
||||
params: Record<string, unknown>;
|
||||
data: Record<string, unknown>;
|
||||
parent: null;
|
||||
};
|
||||
paramMap: ReturnType<
|
||||
BehaviorSubject<ReturnType<typeof convertToParamMap>>['asObservable']
|
||||
>;
|
||||
queryParamMap: ReturnType<
|
||||
BehaviorSubject<ReturnType<typeof convertToParamMap>>['asObservable']
|
||||
>;
|
||||
pathFromRoot: ActivatedRoute[];
|
||||
};
|
||||
let routeParamMap$: BehaviorSubject<ReturnType<typeof convertToParamMap>>;
|
||||
let routeQueryParamMap$: BehaviorSubject<
|
||||
ReturnType<typeof convertToParamMap>
|
||||
>;
|
||||
let workspaceParamMap$: BehaviorSubject<ReturnType<typeof convertToParamMap>>;
|
||||
const playlistsLoaded = signal(false);
|
||||
const playlists = signal<PlaylistMeta[]>([]);
|
||||
const favoritesData = {
|
||||
@@ -81,36 +94,83 @@ describe('UnifiedCollectionPageComponent', () => {
|
||||
clearRecentItems: jest.fn(),
|
||||
};
|
||||
|
||||
function toParamMapRecord(
|
||||
values: Record<string, unknown>
|
||||
): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(values)
|
||||
.filter(([, value]) => value != null)
|
||||
.map(([key, value]) => [key, String(value)])
|
||||
);
|
||||
}
|
||||
|
||||
function setRouteParams(params: Record<string, unknown>): void {
|
||||
const nextParamMap = convertToParamMap(toParamMapRecord(params));
|
||||
route.snapshot.params = params;
|
||||
route.snapshot.paramMap = nextParamMap;
|
||||
routeParamMap$.next(nextParamMap);
|
||||
}
|
||||
|
||||
function setRouteQueryParams(queryParams: Record<string, unknown>): void {
|
||||
const nextQueryParamMap = convertToParamMap(
|
||||
toParamMapRecord(queryParams)
|
||||
);
|
||||
route.snapshot.queryParams = queryParams;
|
||||
route.snapshot.queryParamMap = nextQueryParamMap;
|
||||
routeQueryParamMap$.next(nextQueryParamMap);
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
playlistsLoaded.set(false);
|
||||
playlists.set([]);
|
||||
jest.clearAllMocks();
|
||||
routeParamMap$ = new BehaviorSubject(convertToParamMap({}));
|
||||
routeQueryParamMap$ = new BehaviorSubject(convertToParamMap({}));
|
||||
workspaceParamMap$ = new BehaviorSubject(convertToParamMap({}));
|
||||
|
||||
const workspaceRoute = {
|
||||
snapshot: {
|
||||
data: { layout: 'workspace' },
|
||||
paramMap: convertToParamMap({}),
|
||||
params: {},
|
||||
},
|
||||
paramMap: workspaceParamMap$.asObservable(),
|
||||
} as ActivatedRoute;
|
||||
|
||||
route = {
|
||||
snapshot: {
|
||||
paramMap: convertToParamMap({}),
|
||||
queryParamMap: convertToParamMap({}),
|
||||
queryParams: {},
|
||||
params: {},
|
||||
data: {},
|
||||
parent: null,
|
||||
},
|
||||
queryParamMap: of(convertToParamMap({})),
|
||||
pathFromRoot: [
|
||||
{
|
||||
snapshot: {
|
||||
data: { layout: 'workspace' },
|
||||
},
|
||||
},
|
||||
],
|
||||
paramMap: routeParamMap$.asObservable(),
|
||||
queryParamMap: routeQueryParamMap$.asObservable(),
|
||||
pathFromRoot: [],
|
||||
} as ActivatedRoute & {
|
||||
snapshot: {
|
||||
paramMap: ReturnType<typeof convertToParamMap>;
|
||||
queryParamMap: ReturnType<typeof convertToParamMap>;
|
||||
queryParams: Record<string, unknown>;
|
||||
params: Record<string, unknown>;
|
||||
data: Record<string, unknown>;
|
||||
parent: null;
|
||||
};
|
||||
paramMap: ReturnType<
|
||||
BehaviorSubject<
|
||||
ReturnType<typeof convertToParamMap>
|
||||
>['asObservable']
|
||||
>;
|
||||
queryParamMap: ReturnType<
|
||||
BehaviorSubject<
|
||||
ReturnType<typeof convertToParamMap>
|
||||
>['asObservable']
|
||||
>;
|
||||
pathFromRoot: ActivatedRoute[];
|
||||
};
|
||||
route.pathFromRoot = [workspaceRoute, route];
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@@ -205,9 +265,8 @@ describe('UnifiedCollectionPageComponent', () => {
|
||||
});
|
||||
|
||||
it('does not reload when local item state changes on empty playlist favorites', async () => {
|
||||
route.snapshot.params = { id: 'playlist-1' };
|
||||
route.snapshot.queryParams = { scope: 'playlist' };
|
||||
route.snapshot.queryParamMap = convertToParamMap({ scope: 'playlist' });
|
||||
setRouteParams({ id: 'playlist-1' });
|
||||
setRouteQueryParams({ scope: 'playlist' });
|
||||
playlistsLoaded.set(true);
|
||||
playlists.set([
|
||||
{
|
||||
@@ -239,4 +298,63 @@ describe('UnifiedCollectionPageComponent', () => {
|
||||
|
||||
expect(favoritesData.getFavorites).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('reloads favorites when the playlist id changes in place', async () => {
|
||||
setRouteParams({ id: 'playlist-1' });
|
||||
setRouteQueryParams({ scope: 'playlist' });
|
||||
playlistsLoaded.set(true);
|
||||
fixture.componentRef.setInput('portalType', 'm3u');
|
||||
fixture.componentRef.setInput('defaultScope', undefined);
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(favoritesData.getFavorites).toHaveBeenCalledTimes(1);
|
||||
expect(favoritesData.getFavorites).toHaveBeenLastCalledWith(
|
||||
'playlist',
|
||||
'playlist-1',
|
||||
'm3u'
|
||||
);
|
||||
|
||||
setRouteParams({ id: 'playlist-2' });
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(favoritesData.getFavorites).toHaveBeenCalledTimes(2);
|
||||
expect(favoritesData.getFavorites).toHaveBeenLastCalledWith(
|
||||
'playlist',
|
||||
'playlist-2',
|
||||
'm3u'
|
||||
);
|
||||
});
|
||||
|
||||
it('reloads recent items when the playlist id changes in place', async () => {
|
||||
setRouteParams({ id: 'playlist-1' });
|
||||
setRouteQueryParams({ scope: 'playlist' });
|
||||
playlistsLoaded.set(true);
|
||||
fixture.componentRef.setInput('mode', 'recent');
|
||||
fixture.componentRef.setInput('portalType', 'm3u');
|
||||
fixture.componentRef.setInput('defaultScope', undefined);
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(recentData.getRecentItems).toHaveBeenCalledTimes(1);
|
||||
expect(recentData.getRecentItems).toHaveBeenLastCalledWith(
|
||||
'playlist',
|
||||
'playlist-1',
|
||||
'm3u'
|
||||
);
|
||||
|
||||
setRouteParams({ id: 'playlist-2' });
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(recentData.getRecentItems).toHaveBeenCalledTimes(2);
|
||||
expect(recentData.getRecentItems).toHaveBeenLastCalledWith(
|
||||
'playlist',
|
||||
'playlist-2',
|
||||
'm3u'
|
||||
);
|
||||
});
|
||||
});
|
||||
+52
-61
@@ -2,17 +2,13 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
DestroyRef,
|
||||
effect,
|
||||
inject,
|
||||
Injector,
|
||||
linkedSignal,
|
||||
input,
|
||||
OnInit,
|
||||
signal,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
takeUntilDestroyed,
|
||||
toObservable,
|
||||
} from '@angular/core/rxjs-interop';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
@@ -20,7 +16,6 @@ import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { distinctUntilChanged } from 'rxjs';
|
||||
import {
|
||||
clearNavigationStateKeys,
|
||||
CollectionContentType,
|
||||
@@ -29,6 +24,7 @@ import {
|
||||
isWorkspaceLayoutRoute,
|
||||
OPEN_LIVE_COLLECTION_ITEM_STATE_KEY,
|
||||
queryParamSignal,
|
||||
routeParamSignal,
|
||||
ScopeToggleService,
|
||||
UnifiedCollectionItem,
|
||||
UnifiedFavoritesDataService,
|
||||
@@ -56,14 +52,15 @@ import { UnifiedGridTabComponent } from './unified-grid-tab.component';
|
||||
UnifiedLiveTabComponent,
|
||||
],
|
||||
})
|
||||
export class UnifiedCollectionPageComponent implements OnInit {
|
||||
export class UnifiedCollectionPageComponent {
|
||||
readonly mode = input<'favorites' | 'recent'>('favorites');
|
||||
readonly portalType = input<string>();
|
||||
readonly playlistIdInput = input<string | undefined>(undefined, {
|
||||
alias: 'playlistId',
|
||||
});
|
||||
readonly defaultScope = input<CollectionScope>();
|
||||
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly injector = inject(Injector);
|
||||
private readonly store = inject(Store);
|
||||
private readonly scopeService = inject(ScopeToggleService);
|
||||
private readonly favoritesData = inject(UnifiedFavoritesDataService);
|
||||
@@ -73,11 +70,25 @@ export class UnifiedCollectionPageComponent implements OnInit {
|
||||
selectPlaylistsLoadingFlag
|
||||
);
|
||||
readonly isWorkspaceLayout = isWorkspaceLayoutRoute(this.route);
|
||||
private readonly queryScope = queryParamSignal<CollectionScope | null>(
|
||||
this.route,
|
||||
'scope',
|
||||
(value) =>
|
||||
value === 'all' || value === 'playlist' ? value : null
|
||||
);
|
||||
private readonly routeSearchTerm = queryParamSignal(
|
||||
this.route,
|
||||
'q',
|
||||
(value) => (value ?? '').trim()
|
||||
);
|
||||
private readonly routePlaylistId = routeParamSignal<string | undefined>(
|
||||
this.route,
|
||||
'id',
|
||||
(value) => value ?? undefined
|
||||
);
|
||||
readonly playlistId = computed(
|
||||
() => this.playlistIdInput() ?? this.routePlaylistId()
|
||||
);
|
||||
readonly workspaceSearchTerm = computed(() =>
|
||||
this.isWorkspaceLayout ? this.routeSearchTerm() : ''
|
||||
);
|
||||
@@ -92,19 +103,27 @@ export class UnifiedCollectionPageComponent implements OnInit {
|
||||
readonly skeletonRows = Array.from({ length: 12 }, (_, i) => i);
|
||||
readonly skeletonCards = Array.from({ length: 8 }, (_, i) => i);
|
||||
|
||||
readonly playlistId = computed(() => {
|
||||
let current = this.route.snapshot;
|
||||
while (current) {
|
||||
if (current.params['id']) {
|
||||
return current.params['id'] as string;
|
||||
}
|
||||
current = current.parent!;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
readonly scopeKey = computed(() => this.mode());
|
||||
readonly scope = signal<CollectionScope>('playlist');
|
||||
private readonly persistedScope = computed(() =>
|
||||
this.scopeService.getScope(this.scopeKey())()
|
||||
);
|
||||
readonly scope = linkedSignal<CollectionScope>(() => {
|
||||
if (!this.showScopeToggle()) {
|
||||
return 'all';
|
||||
}
|
||||
|
||||
const queryScope = this.queryScope();
|
||||
if (queryScope) {
|
||||
return queryScope;
|
||||
}
|
||||
|
||||
const defaultScope = this.defaultScope();
|
||||
if (defaultScope) {
|
||||
return defaultScope;
|
||||
}
|
||||
|
||||
return this.persistedScope();
|
||||
});
|
||||
readonly showScopeToggle = computed(() => Boolean(this.playlistId()));
|
||||
readonly effectiveScope = computed<CollectionScope>(() =>
|
||||
this.showScopeToggle() ? this.scope() : 'all'
|
||||
@@ -171,45 +190,17 @@ export class UnifiedCollectionPageComponent implements OnInit {
|
||||
|
||||
private loadRequestId = 0;
|
||||
|
||||
ngOnInit(): void {
|
||||
const queryScope = this.route.snapshot.queryParams['scope'] as
|
||||
| CollectionScope
|
||||
| undefined;
|
||||
if (
|
||||
this.showScopeToggle() &&
|
||||
(queryScope === 'all' || queryScope === 'playlist')
|
||||
) {
|
||||
this.scope.set(queryScope);
|
||||
} else if (this.defaultScope()) {
|
||||
this.scope.set(this.defaultScope()!);
|
||||
} else {
|
||||
const persisted = this.scopeService.getScope(this.scopeKey());
|
||||
this.scope.set(persisted());
|
||||
}
|
||||
|
||||
toObservable(this.loadRequest, {
|
||||
injector: this.injector,
|
||||
})
|
||||
.pipe(
|
||||
distinctUntilChanged(
|
||||
(previous, current) =>
|
||||
previous.mode === current.mode &&
|
||||
previous.portalType === current.portalType &&
|
||||
previous.playlistId === current.playlistId &&
|
||||
previous.scope === current.scope &&
|
||||
previous.reloadKey === current.reloadKey
|
||||
),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
)
|
||||
.subscribe(({ mode, portalType, playlistId, scope }) => {
|
||||
void this.loadData({
|
||||
mode,
|
||||
portalType,
|
||||
playlistId,
|
||||
scope,
|
||||
});
|
||||
private readonly loadEffect = effect(() => {
|
||||
const { mode, portalType, playlistId, scope } = this.loadRequest();
|
||||
untracked(() => {
|
||||
void this.loadData({
|
||||
mode,
|
||||
portalType,
|
||||
playlistId,
|
||||
scope,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onScopeChange(value: CollectionScope): void {
|
||||
if (!this.showScopeToggle()) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
extractPortalSection,
|
||||
isWorkspaceLayoutRoute,
|
||||
queryParamSignal,
|
||||
routeParamSignal,
|
||||
resolveCurrentPortalPlaylistId,
|
||||
resolveCurrentPortalSection,
|
||||
} from './portal-route.utils';
|
||||
@@ -226,4 +227,110 @@ describe('portal-route.utils', () => {
|
||||
|
||||
childInjector.destroy();
|
||||
});
|
||||
|
||||
it('creates route-param signals that react to current route param changes', () => {
|
||||
TestBed.configureTestingModule({});
|
||||
|
||||
const parentInjector = TestBed.inject(EnvironmentInjector);
|
||||
const childInjector = createEnvironmentInjector([], parentInjector);
|
||||
const paramMap$ = new BehaviorSubject(
|
||||
convertToParamMap({
|
||||
id: 'playlist-1',
|
||||
})
|
||||
);
|
||||
const route = {
|
||||
snapshot: {
|
||||
data: {},
|
||||
paramMap: convertToParamMap({
|
||||
id: 'playlist-1',
|
||||
}),
|
||||
params: {
|
||||
id: 'playlist-1',
|
||||
},
|
||||
},
|
||||
paramMap: paramMap$.asObservable(),
|
||||
} as unknown as ActivatedRoute;
|
||||
|
||||
const playlistId = runInInjectionContext(childInjector, () =>
|
||||
routeParamSignal(route, 'id', (value) => value ?? '')
|
||||
);
|
||||
|
||||
expect(playlistId()).toBe('playlist-1');
|
||||
|
||||
route.snapshot.paramMap = convertToParamMap({
|
||||
id: 'playlist-2',
|
||||
});
|
||||
route.snapshot.params = {
|
||||
id: 'playlist-2',
|
||||
};
|
||||
paramMap$.next(
|
||||
convertToParamMap({
|
||||
id: 'playlist-2',
|
||||
})
|
||||
);
|
||||
|
||||
expect(playlistId()).toBe('playlist-2');
|
||||
|
||||
childInjector.destroy();
|
||||
});
|
||||
|
||||
it('creates route-param signals that react to ancestor route param changes', () => {
|
||||
TestBed.configureTestingModule({});
|
||||
|
||||
const parentInjector = TestBed.inject(EnvironmentInjector);
|
||||
const childInjector = createEnvironmentInjector([], parentInjector);
|
||||
const parentParamMap$ = new BehaviorSubject(
|
||||
convertToParamMap({
|
||||
id: 'playlist-1',
|
||||
})
|
||||
);
|
||||
const childParamMap$ = new BehaviorSubject(convertToParamMap({}));
|
||||
const parentRoute = {
|
||||
snapshot: {
|
||||
data: {},
|
||||
paramMap: convertToParamMap({
|
||||
id: 'playlist-1',
|
||||
}),
|
||||
params: {
|
||||
id: 'playlist-1',
|
||||
},
|
||||
},
|
||||
paramMap: parentParamMap$.asObservable(),
|
||||
} as ActivatedRoute;
|
||||
const route = {
|
||||
snapshot: {
|
||||
data: {},
|
||||
paramMap: convertToParamMap({}),
|
||||
params: {},
|
||||
},
|
||||
paramMap: childParamMap$.asObservable(),
|
||||
pathFromRoot: [] as ActivatedRoute[],
|
||||
} as ActivatedRoute & {
|
||||
pathFromRoot: ActivatedRoute[];
|
||||
};
|
||||
route.pathFromRoot = [parentRoute, route];
|
||||
|
||||
const playlistId = runInInjectionContext(childInjector, () =>
|
||||
routeParamSignal(route, 'id', (value) => value ?? '')
|
||||
);
|
||||
|
||||
expect(playlistId()).toBe('playlist-1');
|
||||
|
||||
(parentRoute.snapshot as ActivatedRoute['snapshot']).paramMap =
|
||||
convertToParamMap({
|
||||
id: 'playlist-2',
|
||||
});
|
||||
(parentRoute.snapshot as ActivatedRoute['snapshot']).params = {
|
||||
id: 'playlist-2',
|
||||
};
|
||||
parentParamMap$.next(
|
||||
convertToParamMap({
|
||||
id: 'playlist-2',
|
||||
})
|
||||
);
|
||||
|
||||
expect(playlistId()).toBe('playlist-2');
|
||||
|
||||
childInjector.destroy();
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Signal } from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { map } from 'rxjs';
|
||||
import { ActivatedRoute, convertToParamMap } from '@angular/router';
|
||||
import { combineLatest, map, of } from 'rxjs';
|
||||
import {
|
||||
PortalProvider,
|
||||
PortalRailSection,
|
||||
} from './portal-rail-links';
|
||||
|
||||
type QueryParamNormalizer<T> = (value: string | null) => T;
|
||||
type RouteParamNormalizer<T> = (value: string | null) => T;
|
||||
|
||||
const PORTAL_SECTIONS = new Set<PortalRailSection>([
|
||||
'all',
|
||||
@@ -54,6 +55,37 @@ export function queryParamSignal<T = string>(
|
||||
);
|
||||
}
|
||||
|
||||
export function routeParamSignal<T = string>(
|
||||
route: ActivatedRoute,
|
||||
key: string,
|
||||
normalizer?: RouteParamNormalizer<T>
|
||||
): Signal<T> {
|
||||
const normalize =
|
||||
normalizer ??
|
||||
(((
|
||||
value: string | null
|
||||
) => (value ?? '') as T) as RouteParamNormalizer<T>);
|
||||
const routeChain =
|
||||
Array.isArray(route.pathFromRoot) && route.pathFromRoot.length > 0
|
||||
? route.pathFromRoot
|
||||
: [route];
|
||||
|
||||
return toSignal(
|
||||
combineLatest(
|
||||
routeChain.map((currentRoute) =>
|
||||
currentRoute.paramMap ??
|
||||
of(
|
||||
currentRoute.snapshot.paramMap ??
|
||||
convertToParamMap(currentRoute.snapshot.params ?? {})
|
||||
)
|
||||
)
|
||||
).pipe(map(() => normalize(resolveCurrentRouteParam(route, key)))),
|
||||
{
|
||||
initialValue: normalize(resolveCurrentRouteParam(route, key)),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function extractPortalSection(
|
||||
url: string,
|
||||
provider: PortalProvider
|
||||
@@ -76,6 +108,18 @@ export function resolveCurrentPortalPlaylistId(
|
||||
route: ActivatedRoute,
|
||||
routerUrl: string,
|
||||
provider: PortalProvider
|
||||
): string | null {
|
||||
const resolved = resolveCurrentRouteParam(route, 'id');
|
||||
if (resolved) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
return extractPortalPlaylistId(routerUrl, provider);
|
||||
}
|
||||
|
||||
function resolveCurrentRouteParam(
|
||||
route: ActivatedRoute,
|
||||
key: string
|
||||
): string | null {
|
||||
const routeChain =
|
||||
Array.isArray(route.pathFromRoot) && route.pathFromRoot.length > 0
|
||||
@@ -84,15 +128,15 @@ export function resolveCurrentPortalPlaylistId(
|
||||
|
||||
for (let index = routeChain.length - 1; index >= 0; index -= 1) {
|
||||
const currentRoute = routeChain[index];
|
||||
const playlistId =
|
||||
currentRoute.snapshot.paramMap.get('id') ??
|
||||
currentRoute.snapshot.params['id'];
|
||||
if (playlistId) {
|
||||
return playlistId;
|
||||
const value =
|
||||
currentRoute.snapshot.paramMap?.get(key) ??
|
||||
currentRoute.snapshot.params?.[key];
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return extractPortalPlaylistId(routerUrl, provider);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveCurrentPortalSection(
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface PortalCatalogFacade<
|
||||
readonly playlist: Signal<PortalCatalogPlaylistMeta | null>;
|
||||
|
||||
initialize(categoryId?: string | null): void;
|
||||
setSearchQuery?(query: string): void;
|
||||
clearSelectedItem(): void;
|
||||
setPage(page: number): void;
|
||||
setLimit(limit: number): void;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import {
|
||||
inject,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { UnifiedCollectionPageComponent } from '@iptvnator/portal/shared/ui';
|
||||
import { routeParamSignal } from '@iptvnator/portal/shared/util';
|
||||
|
||||
@Component({
|
||||
selector: 'app-stalker-collection-route',
|
||||
@@ -12,11 +15,19 @@ import { UnifiedCollectionPageComponent } from '@iptvnator/portal/shared/ui';
|
||||
<app-unified-collection-page
|
||||
[mode]="mode()"
|
||||
[portalType]="portalType()"
|
||||
[playlistId]="playlistId()"
|
||||
/>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class StalkerCollectionRouteComponent {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
|
||||
readonly mode = input<'favorites' | 'recent'>('favorites');
|
||||
readonly portalType = input('stalker');
|
||||
readonly playlistId = routeParamSignal<string | undefined>(
|
||||
this.route,
|
||||
'id',
|
||||
(value) => value ?? undefined
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user