fix(portals): fourteenth review round on the cinematic details

- The hero keys its one-time stage/compact decision on a stable content
  identity (provider + id) that every host passes, so a TMDB rename plus
  a late backdrop no longer flips the visible hero.
- An explicit Stalker movie launch joins its host's start generation: it
  supersedes an earlier start and is dropped once a Play, Start Over or
  later launch begins, on the controller, search, collection and catalog
  hosts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-10-03 13:14:07 +02:00
1 parent 4e18306d06
commit 0524429fbd
15 files changed
+170 -42

No files matched your search

@@ -231,6 +231,24 @@ describe('ContentHeroComponent cinematic layout', () => {
expect(mute?.getAttribute('aria-pressed')).toBe('true');
});
it('keys the layout on the content identity when enrichment renames the title', () => {
fixture.componentRef.setInput('contentKey', 'm3u:42');
fixture.componentRef.setInput('title', 'Channel name');
fixture.componentRef.setInput('posterUrl', 'poster.jpg');
fixture.detectChanges();
expect(host().querySelector('.hero--compact')).toBeTruthy();
// TMDB replaces the title and brings a backdrop: same content.
fixture.componentRef.setInput('title', 'Proper Title (2021)');
fixture.componentRef.setInput('backdropUrl', 'wide.jpg');
fixture.detectChanges();
expect(host().querySelector('.hero--compact')).toBeTruthy();
fixture.componentRef.setInput('contentKey', 'm3u:43');
fixture.detectChanges();
expect(host().querySelector('.hero--compact')).toBeNull();
});
it('places the actions before the credits slot', () => {
fixture.componentRef.setInput('title', 'Black Harbor');
fixture.detectChanges();
@@ -47,6 +47,11 @@ export class ContentHeroComponent {
readonly description = input<string>();
readonly posterUrl = input<string>();
readonly backdropUrl = input<string>();
/**
* Stable identity of the shown title (provider + id). The layout is
* decided once per identity; enrichment may replace the title itself.
*/
readonly contentKey = input<string | null>(null);
/** 0–100 watched share; renders the resume bar above the actions. */
readonly progress = input<number | null>(null);
/** With the setting on, this trailer plays muted behind the details. */
@@ -87,10 +92,11 @@ export class ContentHeroComponent {
/**
* Decided once per title: a backdrop that TMDB enrichment adds a moment
* later fills the compact hero instead of growing it under the user's
* cursor. The title survives the merge; the poster may not.
* cursor. Keyed by `contentKey`, falling back to the title for hosts
* without one.
*/
readonly layout = linkedSignal<string | undefined, ContentHeroLayout>({
source: () => this.title(),
source: () => this.contentKey() ?? this.title(),
computation: () =>
untracked(() => this.hasRealBackdrop()) ? 'stage' : 'compact',
});
@@ -9,6 +9,7 @@
[backdropUrl]="backdropUrl()"
[progress]="progress()"
[trailerBackdropUrl]="trailerBackdropUrl()"
[contentKey]="contentKey()"
[isLoading]="isLoading()"
[errorMessage]="errorMessage()"
>
@@ -73,6 +73,8 @@ export class PortalDetailShellComponent {
readonly progress = input<number | null>(null);
/** Trailer embed to play muted behind the hero, when the setting is on. */
readonly trailerBackdropUrl = input<string | null>(null);
/** Stable identity of the shown title; see `ContentHeroComponent.contentKey`. */
readonly contentKey = input<string | null>(null);
readonly isLoading = input(false);
readonly errorMessage = input<string>();
readonly backLabel = input<string>();