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:
4grayandClaude Fable 5 committed 2026-08-16 08:15:04 +02:00
1 parent 486d04411f
commit ae01203dd0
11 files changed
+285 -159

No files matched your search

@@ -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 {