mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 17:36:15 -08:00
refactor(portals): share the discover facet navigation across detail pages
The four render sites each carried their own copy of the year/genre/country click handlers, which also pushed serial-details.component.ts past the 400-line limit. `createDiscoverFacetNavigation()` now owns the navigation, the numeric-tmdb_id gate and the year parsing. Year parsing moves from a fixed 4-char slice to the first four-digit run, so a day-first provider date resolves instead of producing NaN. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
486d04411f
commit
ae01203dd0
11 files changed
+285
-159
No files matched your search
@@ -22,6 +22,7 @@ export * from './lib/workspace-header-context.service';
|
||||
export * from './lib/workspace-view-command.types';
|
||||
export * from './lib/workspace-view-command.service';
|
||||
export * from './lib/navigation/collection-detail-portal-navigation';
|
||||
export * from './lib/navigation/discover-facet-navigation';
|
||||
export * from './lib/navigation/discover-link.util';
|
||||
export * from './lib/navigation/portal-rail-links';
|
||||
export * from './lib/navigation/provider-detail-mode';
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Injector, runInInjectionContext } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
DiscoverFacetTarget,
|
||||
createDiscoverFacetNavigation,
|
||||
} from './discover-facet-navigation';
|
||||
|
||||
describe('createDiscoverFacetNavigation', () => {
|
||||
let navigate: jest.Mock;
|
||||
|
||||
function create(target: () => DiscoverFacetTarget | null) {
|
||||
const injector = Injector.create({
|
||||
providers: [{ provide: Router, useValue: { navigate } }],
|
||||
});
|
||||
return runInInjectionContext(injector, () =>
|
||||
createDiscoverFacetNavigation(target)
|
||||
);
|
||||
}
|
||||
|
||||
const xtreamMovie = (): DiscoverFacetTarget => ({
|
||||
portal: 'xtream',
|
||||
mediaType: 'movie',
|
||||
playlistId: 'pl-1',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
navigate = jest.fn();
|
||||
});
|
||||
|
||||
it('navigates to the portal-scoped discover route for a genre', () => {
|
||||
create(xtreamMovie).openGenre({ id: 18, name: 'Drama' });
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith(
|
||||
['/workspace', 'xtreams', 'pl-1', 'discover'],
|
||||
{
|
||||
queryParams: {
|
||||
type: 'movie',
|
||||
genre: '18',
|
||||
genreLabel: 'Drama',
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('routes Stalker targets to the stalker segment and media type', () => {
|
||||
create(() => ({
|
||||
portal: 'stalker',
|
||||
mediaType: 'tv',
|
||||
playlistId: 'pl-2',
|
||||
})).openCountry({ code: 'DE', name: 'Germany' });
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith(
|
||||
['/workspace', 'stalker', 'pl-2', 'discover'],
|
||||
{
|
||||
queryParams: {
|
||||
type: 'tv',
|
||||
country: 'DE',
|
||||
countryLabel: 'Germany',
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('reads the year out of any provider date shape', () => {
|
||||
const discover = create(xtreamMovie);
|
||||
|
||||
// Day-first dates are why this is a search, not a fixed slice
|
||||
for (const [date, year] of [
|
||||
['1976', '1976'],
|
||||
['1999-03-31', '1999'],
|
||||
['31-03-1999', '1999'],
|
||||
]) {
|
||||
navigate.mockClear();
|
||||
discover.openYear(603, date);
|
||||
expect(navigate).toHaveBeenCalledWith(expect.anything(), {
|
||||
queryParams: { type: 'movie', year },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('refuses the year facet without a merge-written numeric tmdb id', () => {
|
||||
const discover = create(xtreamMovie);
|
||||
|
||||
// Providers ship tmdb_id as an untrusted string; only the merge
|
||||
// writes a number, and only a matched item has a facet identity
|
||||
expect(discover.canOpenYear('603', '1999-03-31')).toBe(false);
|
||||
expect(discover.canOpenYear(undefined, '1999-03-31')).toBe(false);
|
||||
expect(discover.canOpenYear(603, '1999-03-31')).toBe(true);
|
||||
|
||||
discover.openYear('603', '1999-03-31');
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refuses the year facet without a parsable year', () => {
|
||||
const discover = create(xtreamMovie);
|
||||
|
||||
expect(discover.canOpenYear(603, '')).toBe(false);
|
||||
expect(discover.canOpenYear(603, undefined)).toBe(false);
|
||||
expect(discover.canOpenYear(603, 'unknown')).toBe(false);
|
||||
|
||||
discover.openYear(603, 'unknown');
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not navigate while the host cannot resolve its playlist', () => {
|
||||
const discover = create(() => null);
|
||||
|
||||
discover.openGenre({ id: 18, name: 'Drama' });
|
||||
discover.openCountry({ code: 'US', name: 'United States' });
|
||||
discover.openYear(603, '1999');
|
||||
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('re-reads the target on every click', () => {
|
||||
let target: DiscoverFacetTarget | null = null;
|
||||
const discover = create(() => target);
|
||||
|
||||
discover.openGenre({ id: 18, name: 'Drama' });
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
|
||||
target = xtreamMovie();
|
||||
discover.openGenre({ id: 18, name: 'Drama' });
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
TmdbCountryFacet,
|
||||
TmdbGenreFacet,
|
||||
TmdbMediaType,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { discoverLink } from './discover-link.util';
|
||||
|
||||
/**
|
||||
* Where a detail page's facet chips should navigate. `null` means the page
|
||||
* cannot resolve its own playlist yet, so chips must not navigate at all.
|
||||
*/
|
||||
export interface DiscoverFacetTarget {
|
||||
portal: 'xtream' | 'stalker';
|
||||
mediaType: TmdbMediaType;
|
||||
playlistId: string;
|
||||
}
|
||||
|
||||
export interface DiscoverFacetNavigation {
|
||||
canOpenYear(
|
||||
tmdbId: unknown,
|
||||
releaseDate: string | null | undefined
|
||||
): boolean;
|
||||
openYear(tmdbId: unknown, releaseDate: string | null | undefined): void;
|
||||
openGenre(genre: TmdbGenreFacet): void;
|
||||
openCountry(country: TmdbCountryFacet): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Year stated by a provider date field, whatever shape it arrives in —
|
||||
* `1976`, `1999-03-31` and `31-03-1999` all resolve. Reads the first
|
||||
* four-digit run rather than a fixed slice, which the day-first form
|
||||
* would otherwise turn into `NaN`.
|
||||
*/
|
||||
function facetYear(releaseDate: string | null | undefined): number | null {
|
||||
const match = releaseDate?.match(/\d{4}/);
|
||||
return match ? Number(match[0]) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared behavior behind the clickable year/genre/country chips on every
|
||||
* detail page. Call it from a field initializer (injection context).
|
||||
*
|
||||
* The year gate is deliberately `typeof tmdbId === 'number'`: provider
|
||||
* payloads ship `tmdb_id` as an untrusted string, so only a value written
|
||||
* by the TMDB merge proves the item was actually matched — and without a
|
||||
* match there is no facet identity to discover by.
|
||||
*/
|
||||
export function createDiscoverFacetNavigation(
|
||||
target: () => DiscoverFacetTarget | null
|
||||
): DiscoverFacetNavigation {
|
||||
const router = inject(Router);
|
||||
|
||||
const navigate = (
|
||||
facet: Parameters<typeof discoverLink>[3]
|
||||
): void => {
|
||||
const resolved = target();
|
||||
if (!resolved) {
|
||||
return;
|
||||
}
|
||||
const link = discoverLink(
|
||||
resolved.portal,
|
||||
resolved.playlistId,
|
||||
resolved.mediaType,
|
||||
facet
|
||||
);
|
||||
void router.navigate(link.commands, {
|
||||
queryParams: link.queryParams,
|
||||
});
|
||||
};
|
||||
|
||||
const canOpenYear = (
|
||||
tmdbId: unknown,
|
||||
releaseDate: string | null | undefined
|
||||
): boolean => typeof tmdbId === 'number' && facetYear(releaseDate) !== null;
|
||||
|
||||
return {
|
||||
canOpenYear,
|
||||
openYear(tmdbId, releaseDate) {
|
||||
const year = facetYear(releaseDate);
|
||||
if (year === null || !canOpenYear(tmdbId, releaseDate)) {
|
||||
return;
|
||||
}
|
||||
navigate({ kind: 'year', year });
|
||||
},
|
||||
openGenre(genre) {
|
||||
navigate({ kind: 'genre', genre });
|
||||
},
|
||||
openCountry(country) {
|
||||
navigate({ kind: 'country', country });
|
||||
},
|
||||
};
|
||||
}
|
||||
+12
-4
@@ -19,9 +19,17 @@
|
||||
<span
|
||||
class="details__tag"
|
||||
[class.details__tag--clickable]="
|
||||
canDiscoverByYear(serial.info)
|
||||
discover.canOpenYear(
|
||||
serial.info.tmdb_id,
|
||||
serial.info.releasedate
|
||||
)
|
||||
"
|
||||
(click)="
|
||||
discover.openYear(
|
||||
serial.info.tmdb_id,
|
||||
serial.info.releasedate
|
||||
)
|
||||
"
|
||||
(click)="openYearDiscover(serial.info)"
|
||||
>{{ serial.info.releasedate }}</span
|
||||
>
|
||||
}
|
||||
@@ -29,7 +37,7 @@
|
||||
@for (genre of serial.info.tmdb_genres; track genre.id) {
|
||||
<span
|
||||
class="details__tag details__tag--clickable"
|
||||
(click)="openGenreDiscover(genre)"
|
||||
(click)="discover.openGenre(genre)"
|
||||
>{{ genre.name }}</span
|
||||
>
|
||||
}
|
||||
@@ -53,7 +61,7 @@
|
||||
) {
|
||||
<span
|
||||
class="details__tag details__tag--clickable"
|
||||
(click)="openCountryDiscover(country)"
|
||||
(click)="discover.openCountry(country)"
|
||||
>{{ country.name }}</span
|
||||
>
|
||||
}
|
||||
|
||||
+7
-35
@@ -33,10 +33,7 @@ import {
|
||||
PlaybackPositionData,
|
||||
ResolvedPortalPlayback,
|
||||
seriesStatusLabelKey,
|
||||
StalkerVodInfo,
|
||||
TmdbCountryFacet,
|
||||
TmdbEnrichedCastMember,
|
||||
TmdbGenreFacet,
|
||||
XtreamSerieEpisode,
|
||||
youtubeEmbedUrl,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
@@ -49,8 +46,7 @@ import {
|
||||
PORTAL_PLAYER,
|
||||
createLogger,
|
||||
consumeStalkerReturnMarker,
|
||||
DiscoverFacetClick,
|
||||
discoverLink,
|
||||
createDiscoverFacetNavigation,
|
||||
resolveStalkerBackNavigation,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import {
|
||||
@@ -879,37 +875,13 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
]);
|
||||
}
|
||||
|
||||
/** Discover needs TMDB; only a matched series can offer facet chips */
|
||||
canDiscoverByYear(info: StalkerVodInfo): boolean {
|
||||
return typeof info.tmdb_id === 'number' && !!info.releasedate;
|
||||
}
|
||||
|
||||
openYearDiscover(info: StalkerVodInfo): void {
|
||||
const year = Number(info.releasedate?.match(/\d{4}/)?.[0]);
|
||||
if (!this.canDiscoverByYear(info) || !Number.isInteger(year)) {
|
||||
return;
|
||||
}
|
||||
this.openDiscoverFacet({ kind: 'year', year });
|
||||
}
|
||||
|
||||
openGenreDiscover(genre: TmdbGenreFacet): void {
|
||||
this.openDiscoverFacet({ kind: 'genre', genre });
|
||||
}
|
||||
|
||||
openCountryDiscover(country: TmdbCountryFacet): void {
|
||||
this.openDiscoverFacet({ kind: 'country', country });
|
||||
}
|
||||
|
||||
private openDiscoverFacet(facet: DiscoverFacetClick): void {
|
||||
/** Clickable year/genre/country chips (Discover pages) */
|
||||
readonly discover = createDiscoverFacetNavigation(() => {
|
||||
const playlistId = this.stalkerStore.currentPlaylist()?._id;
|
||||
if (!playlistId) {
|
||||
return;
|
||||
}
|
||||
const link = discoverLink('stalker', playlistId, 'tv', facet);
|
||||
void this.router.navigate(link.commands, {
|
||||
queryParams: link.queryParams,
|
||||
});
|
||||
}
|
||||
return playlistId
|
||||
? { portal: 'stalker', mediaType: 'tv', playlistId }
|
||||
: null;
|
||||
});
|
||||
|
||||
goBack() {
|
||||
const back = resolveStalkerBackNavigation(
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
@if (info.releaseDate) {
|
||||
<span
|
||||
class="details__tag"
|
||||
[class.details__tag--clickable]="canDiscoverByYear(info)"
|
||||
(click)="openYearDiscover(info)"
|
||||
[class.details__tag--clickable]="
|
||||
discover.canOpenYear(info.tmdb_id, info.releaseDate)
|
||||
"
|
||||
(click)="discover.openYear(info.tmdb_id, info.releaseDate)"
|
||||
>{{ info.releaseDate | slice: 0 : 4 }}</span
|
||||
>
|
||||
}
|
||||
@@ -28,7 +30,7 @@
|
||||
@for (genre of info.tmdb_genres; track genre.id) {
|
||||
<span
|
||||
class="details__tag details__tag--clickable"
|
||||
(click)="openGenreDiscover(genre)"
|
||||
(click)="discover.openGenre(genre)"
|
||||
>{{ genre.name }}</span
|
||||
>
|
||||
}
|
||||
@@ -49,7 +51,7 @@
|
||||
@for (country of info.tmdb_countries; track country.code) {
|
||||
<span
|
||||
class="details__tag details__tag--clickable"
|
||||
(click)="openCountryDiscover(country)"
|
||||
(click)="discover.openCountry(country)"
|
||||
>{{ country.name }}</span
|
||||
>
|
||||
}
|
||||
|
||||
@@ -39,17 +39,14 @@ import {
|
||||
import {
|
||||
normalizeTitleKeys,
|
||||
seriesStatusLabelKey,
|
||||
TmdbCountryFacet,
|
||||
TmdbEnrichedCastMember,
|
||||
TmdbGenreFacet,
|
||||
XtreamSerieDetails,
|
||||
XtreamSerieEpisode,
|
||||
XtreamSerieInfo,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { buildSeasonDescriptions } from './season-descriptions.util';
|
||||
import {
|
||||
DiscoverFacetClick,
|
||||
discoverLink,
|
||||
createDiscoverFacetNavigation,
|
||||
isProviderOnlyDetailState,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import {
|
||||
@@ -347,37 +344,13 @@ export class SerialDetailsComponent implements OnInit, OnDestroy {
|
||||
]);
|
||||
}
|
||||
|
||||
/** Discover needs TMDB; only a matched series can offer facet chips */
|
||||
canDiscoverByYear(info: XtreamSerieInfo): boolean {
|
||||
return typeof info.tmdb_id === 'number' && !!info.releaseDate;
|
||||
}
|
||||
|
||||
openYearDiscover(info: XtreamSerieInfo): void {
|
||||
const year = Number(info.releaseDate?.slice(0, 4));
|
||||
if (!this.canDiscoverByYear(info) || !Number.isInteger(year)) {
|
||||
return;
|
||||
}
|
||||
this.openDiscoverFacet({ kind: 'year', year });
|
||||
}
|
||||
|
||||
openGenreDiscover(genre: TmdbGenreFacet): void {
|
||||
this.openDiscoverFacet({ kind: 'genre', genre });
|
||||
}
|
||||
|
||||
openCountryDiscover(country: TmdbCountryFacet): void {
|
||||
this.openDiscoverFacet({ kind: 'country', country });
|
||||
}
|
||||
|
||||
private openDiscoverFacet(facet: DiscoverFacetClick): void {
|
||||
/** Clickable year/genre/country chips (Discover pages) */
|
||||
readonly discover = createDiscoverFacetNavigation(() => {
|
||||
const playlistId = this.xtreamStore.currentPlaylist()?.id;
|
||||
if (!playlistId) {
|
||||
return;
|
||||
}
|
||||
const link = discoverLink('xtream', playlistId, 'tv', facet);
|
||||
void this.router.navigate(link.commands, {
|
||||
queryParams: link.queryParams,
|
||||
});
|
||||
}
|
||||
return playlistId
|
||||
? { portal: 'xtream', mediaType: 'tv', playlistId }
|
||||
: null;
|
||||
});
|
||||
|
||||
onSeasonSelected(seasonKey: string): void {
|
||||
// The enrichment call itself runs from the constructor effect keyed
|
||||
|
||||
@@ -297,8 +297,10 @@
|
||||
@if (info.releasedate) {
|
||||
<span
|
||||
class="details__tag"
|
||||
[class.details__tag--clickable]="canDiscoverByYear(info)"
|
||||
(click)="openYearDiscover(info)"
|
||||
[class.details__tag--clickable]="
|
||||
discover.canOpenYear(info.tmdb_id, info.releasedate)
|
||||
"
|
||||
(click)="discover.openYear(info.tmdb_id, info.releasedate)"
|
||||
>{{ info.releasedate | slice: 0 : 4 }}</span
|
||||
>
|
||||
}
|
||||
@@ -306,7 +308,7 @@
|
||||
@for (genre of info.tmdb_genres; track genre.id) {
|
||||
<span
|
||||
class="details__tag details__tag--clickable"
|
||||
(click)="openGenreDiscover(genre)"
|
||||
(click)="discover.openGenre(genre)"
|
||||
>{{ genre.name }}</span
|
||||
>
|
||||
}
|
||||
@@ -325,7 +327,7 @@
|
||||
@for (country of info.tmdb_countries; track country.code) {
|
||||
<span
|
||||
class="details__tag details__tag--clickable"
|
||||
(click)="openCountryDiscover(country)"
|
||||
(click)="discover.openCountry(country)"
|
||||
>{{ country.name }}</span
|
||||
>
|
||||
}
|
||||
|
||||
@@ -27,9 +27,8 @@ import {
|
||||
} from '@iptvnator/ui/components';
|
||||
import { SafePipe } from '@iptvnator/pipes';
|
||||
import {
|
||||
createDiscoverFacetNavigation,
|
||||
createLogger,
|
||||
DiscoverFacetClick,
|
||||
discoverLink,
|
||||
isProviderOnlyDetailState,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import {
|
||||
@@ -52,9 +51,7 @@ import {
|
||||
normalizeTitleKeys,
|
||||
playlistDisplayLabel,
|
||||
reportsPlaybackFailures,
|
||||
TmdbCountryFacet,
|
||||
TmdbEnrichedCastMember,
|
||||
TmdbGenreFacet,
|
||||
XtreamCategory,
|
||||
XtreamVodDetails,
|
||||
XtreamVodInfo,
|
||||
@@ -491,37 +488,13 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy {
|
||||
]);
|
||||
}
|
||||
|
||||
/** Discover needs TMDB; only a matched item can offer facet chips */
|
||||
canDiscoverByYear(info: XtreamVodInfo): boolean {
|
||||
return typeof info.tmdb_id === 'number' && !!info.releasedate;
|
||||
}
|
||||
|
||||
openYearDiscover(info: XtreamVodInfo): void {
|
||||
const year = Number(info.releasedate?.slice(0, 4));
|
||||
if (!this.canDiscoverByYear(info) || !Number.isInteger(year)) {
|
||||
return;
|
||||
}
|
||||
this.openDiscoverFacet({ kind: 'year', year });
|
||||
}
|
||||
|
||||
openGenreDiscover(genre: TmdbGenreFacet): void {
|
||||
this.openDiscoverFacet({ kind: 'genre', genre });
|
||||
}
|
||||
|
||||
openCountryDiscover(country: TmdbCountryFacet): void {
|
||||
this.openDiscoverFacet({ kind: 'country', country });
|
||||
}
|
||||
|
||||
private openDiscoverFacet(facet: DiscoverFacetClick): void {
|
||||
/** Clickable year/genre/country chips (Discover pages) */
|
||||
readonly discover = createDiscoverFacetNavigation(() => {
|
||||
const playlistId = this.xtreamStore.currentPlaylist()?.id;
|
||||
if (!playlistId) {
|
||||
return;
|
||||
}
|
||||
const link = discoverLink('xtream', playlistId, 'movie', facet);
|
||||
void this.router.navigate(link.commands, {
|
||||
queryParams: link.queryParams,
|
||||
});
|
||||
}
|
||||
return playlistId
|
||||
? { portal: 'xtream', mediaType: 'movie', playlistId }
|
||||
: null;
|
||||
});
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.favoritePulseTimer) {
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
@if (meta.year) {
|
||||
<span
|
||||
class="details__tag"
|
||||
[class.details__tag--clickable]="canDiscoverByYear()"
|
||||
(click)="openYearDiscover()"
|
||||
[class.details__tag--clickable]="
|
||||
discover.canOpenYear(meta.tmdbId, meta.year)
|
||||
"
|
||||
(click)="discover.openYear(meta.tmdbId, meta.year)"
|
||||
>{{ meta.year }}</span
|
||||
>
|
||||
}
|
||||
@@ -23,7 +25,7 @@
|
||||
@for (genre of meta.tmdbGenres; track genre.id) {
|
||||
<span
|
||||
class="details__tag details__tag--clickable"
|
||||
(click)="openGenreDiscover(genre)"
|
||||
(click)="discover.openGenre(genre)"
|
||||
>{{ genre.name }}</span
|
||||
>
|
||||
}
|
||||
@@ -42,7 +44,7 @@
|
||||
@for (country of meta.tmdbCountries; track country.code) {
|
||||
<span
|
||||
class="details__tag details__tag--clickable"
|
||||
(click)="openCountryDiscover(country)"
|
||||
(click)="discover.openCountry(country)"
|
||||
>{{ country.name }}</span
|
||||
>
|
||||
}
|
||||
|
||||
@@ -12,10 +12,9 @@ import { MatIcon } from '@angular/material/icon';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { SafePipe } from '@iptvnator/pipes';
|
||||
import {
|
||||
DiscoverFacetClick,
|
||||
PORTAL_EXTERNAL_PLAYBACK,
|
||||
createDiscoverFacetNavigation,
|
||||
createExternalPlaybackButtonState,
|
||||
discoverLink,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import {
|
||||
DetailActionsTemplateDirective,
|
||||
@@ -28,9 +27,7 @@ import { Router } from '@angular/router';
|
||||
import {
|
||||
ExternalPlayerSession,
|
||||
ResolvedPortalPlayback,
|
||||
TmdbCountryFacet,
|
||||
TmdbEnrichedCastMember,
|
||||
TmdbGenreFacet,
|
||||
VodDetailsItem,
|
||||
getVodNumericId,
|
||||
normalizeVodDetails,
|
||||
@@ -323,43 +320,19 @@ export class VodDetailsComponent {
|
||||
this.backClicked.emit();
|
||||
}
|
||||
|
||||
// ============ Discover (clickable metadata chips) ============
|
||||
|
||||
/** Discover needs TMDB; only a matched item can offer facet chips */
|
||||
readonly canDiscoverByYear = computed(
|
||||
() =>
|
||||
this.normalizedMeta().tmdbId !== undefined &&
|
||||
!!this.normalizedMeta().year
|
||||
);
|
||||
|
||||
openYearDiscover(): void {
|
||||
const year = Number(this.normalizedMeta().year);
|
||||
if (!this.canDiscoverByYear() || !Number.isInteger(year)) {
|
||||
return;
|
||||
}
|
||||
this.openDiscover({ kind: 'year', year });
|
||||
}
|
||||
|
||||
openGenreDiscover(genre: TmdbGenreFacet): void {
|
||||
this.openDiscover({ kind: 'genre', genre });
|
||||
}
|
||||
|
||||
openCountryDiscover(country: TmdbCountryFacet): void {
|
||||
this.openDiscover({ kind: 'country', country });
|
||||
}
|
||||
|
||||
private openDiscover(facet: DiscoverFacetClick): void {
|
||||
/** Clickable year/genre/country chips (Discover pages) */
|
||||
readonly discover = createDiscoverFacetNavigation(() => {
|
||||
const item = this.item();
|
||||
const link = discoverLink(
|
||||
item.type === 'stalker' ? 'stalker' : 'xtream',
|
||||
item.playlistId,
|
||||
this.normalizedMeta().tmdbMediaType ?? 'movie',
|
||||
facet
|
||||
);
|
||||
void this.router.navigate(link.commands, {
|
||||
queryParams: link.queryParams,
|
||||
});
|
||||
}
|
||||
return item.playlistId
|
||||
? {
|
||||
portal: item.type === 'stalker' ? 'stalker' : 'xtream',
|
||||
// Stalker embedded-VOD series render here but are matched
|
||||
// as tv, so the merge's verdict decides — not the route
|
||||
mediaType: this.normalizedMeta().tmdbMediaType ?? 'movie',
|
||||
playlistId: item.playlistId,
|
||||
}
|
||||
: null;
|
||||
});
|
||||
|
||||
/** Handle download request */
|
||||
onDownload(): void {
|
||||
|
||||
Reference in new issue
Block a user