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

- The shared movie menu offers the external player in provider-only
  mode too; that mode hides local/download controls, not the provider's
  stream in MPV/VLC.
- The movie and Stalker series reset-progress rows are also disabled
  while a start is still resolving its stream, so a slow create_link
  cannot recreate a just-cleared row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-10-03 11:48:59 +02:00
1 parent 5b8717b69c
commit aaae2b0572
4 files changed
+56 -7

No files matched your search

@@ -283,6 +283,8 @@ export class StalkerSeriesViewComponent implements OnDestroy {
private unsubscribePositionUpdates: (() => void) | null = null;
readonly openingEpisodeId = signal<number | null>(null);
readonly activeEpisodeId = signal<number | null>(null);
/** Starts still resolving their stream (`create_link` round trips). */
private readonly pendingStartCount = signal(0);
readonly seasonWatchBatchRunning = signal(false);
/**
@@ -399,6 +401,7 @@ export class StalkerSeriesViewComponent implements OnDestroy {
),
playbackActive: computed(
() =>
this.pendingStartCount() > 0 ||
this.inlinePlayback() !== null ||
this.openingEpisodeId() !== null ||
this.activeEpisodeId() !== null
@@ -1266,6 +1269,7 @@ export class StalkerSeriesViewComponent implements OnDestroy {
};
if (request.usesEmbeddedPlayer && !request.identity) return;
this.pendingStartCount.update((count) => count + 1);
try {
const playback = await this.stalkerStore.resolveVodPlayback(
cmd,
@@ -1318,6 +1322,8 @@ export class StalkerSeriesViewComponent implements OnDestroy {
this.snackBar.open(errorMessage, undefined, {
duration: 3000,
});
} finally {
this.pendingStartCount.update((count) => count - 1);
}
}
@@ -32,7 +32,8 @@ export interface VodDetailsHeroStateDeps {
readonly formattedPosition: Signal<string>;
readonly isWatched: Signal<boolean>;
readonly isElectron: Signal<boolean>;
readonly providerOnly: Signal<boolean>;
/** Play/Resume clicked, stream still resolving (Stalker `create_link`). */
readonly playbackStartPending: Signal<boolean>;
readonly isOfflinePrimary: Signal<boolean>;
readonly externalLabel: Signal<string | null>;
readonly externalIcon: Signal<string>;
@@ -108,15 +109,19 @@ export function createVodDetailsHeroState(deps: VodDetailsHeroStateDeps) {
externalPlayer,
menuSections: computed(() =>
buildVodMenuSections({
externalPlayerAvailable:
deps.isElectron() && !deps.providerOnly(),
// Provider-only mode hides local/download controls, not the
// provider's own stream in MPV/VLC.
externalPlayerAvailable: deps.isElectron(),
externalPlayerHint: externalPlayer() === 'vlc' ? 'VLC' : 'MPV',
hasPlaybackPosition: deps.hasPlaybackPosition(),
hasStoredProgress:
(deps.playbackPosition() ?? 0) > 0 || deps.isWatched(),
// Inline playback collapses the hero; only an external
// player can still own the row while the menu is reachable.
playbackActive: deps.externalState() !== 'idle',
// Inline playback collapses the hero; an external player, or
// a start still resolving its stream, can still own the row
// while the menu is reachable.
playbackActive:
deps.externalState() !== 'idle' ||
deps.playbackStartPending(),
})
),
};
@@ -0,0 +1,38 @@
import {
buildVodMenuSections,
VOD_DETAILS_MENU_ACTION,
} from './vod-details-presentation';
const BASE = {
externalPlayerAvailable: true,
externalPlayerHint: 'MPV' as const,
hasPlaybackPosition: true,
hasStoredProgress: true,
playbackActive: false,
};
function rows(input: Partial<typeof BASE>) {
return buildVodMenuSections({ ...BASE, ...input }).flatMap(
(section) => section.items
);
}
describe('buildVodMenuSections', () => {
it('offers the external player and the progress reset while nothing plays', () => {
const ids = rows({}).map((row) => row.id);
expect(ids).toContain(VOD_DETAILS_MENU_ACTION.ExternalPlayer);
const reset = rows({}).find(
(row) => row.id === VOD_DETAILS_MENU_ACTION.ResetProgress
);
expect(reset?.disabled).toBeFalsy();
});
it('disables the progress reset while playback owns the position', () => {
// A running player, or a start still resolving its stream, would
// write the position right back.
const reset = rows({ playbackActive: true }).find(
(row) => row.id === VOD_DETAILS_MENU_ACTION.ResetProgress
);
expect(reset?.disabled).toBe(true);
});
});
@@ -325,7 +325,7 @@ export class VodDetailsComponent {
formattedPosition: this.formattedPosition,
isWatched: this.isWatched,
isElectron: this.isElectron,
providerOnly: this.providerOnly,
playbackStartPending: this.playbackStartPending,
isOfflinePrimary: this.isOfflinePrimary,
externalLabel: this.externalPrimaryLabel,
externalIcon: this.externalPrimaryIcon,