diff --git a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-menu.service.ts b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-menu.service.ts index 7ea37562d..4490b8dc3 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-menu.service.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-menu.service.ts @@ -66,6 +66,7 @@ export class StalkerSeriesMenuService { hasProgress: this.bindings()?.hasProgress() ?? false, playbackActive: this.bindings()?.playbackActive() ?? false, startPending: this.bindings()?.startPending() ?? false, + watchBatchRunning: container?.seasonWatchBatchRunning() ?? false, sourcesCount: 0, externalPlayerHint: quickStart?.action && diff --git a/libs/portal/xtream/feature/src/lib/serial-details/serial-details-menu.service.ts b/libs/portal/xtream/feature/src/lib/serial-details/serial-details-menu.service.ts index fb1972259..f15789f34 100644 --- a/libs/portal/xtream/feature/src/lib/serial-details/serial-details-menu.service.ts +++ b/libs/portal/xtream/feature/src/lib/serial-details/serial-details-menu.service.ts @@ -163,6 +163,7 @@ export class SerialDetailsMenuService { hasProgress: this.hasProgress(), playbackActive: this.bindings()?.playbackActive() ?? false, startPending: this.bindings()?.startPending() ?? false, + watchBatchRunning: container?.seasonWatchBatchRunning() ?? false, sourcesCount: 0, externalPlayerHint: quickStart && this.runtime.supportsManagedExternalPlayers diff --git a/libs/ui/components/src/lib/detail-ui/series-hero.state.spec.ts b/libs/ui/components/src/lib/detail-ui/series-hero.state.spec.ts index d300225fc..eea5bed29 100644 --- a/libs/ui/components/src/lib/detail-ui/series-hero.state.spec.ts +++ b/libs/ui/components/src/lib/detail-ui/series-hero.state.spec.ts @@ -16,6 +16,7 @@ const BASE = { hasProgress: true, playbackActive: false, startPending: false, + watchBatchRunning: false, sourcesCount: 0, externalPlayerHint: 'MPV' as const, copyUrlEpisodeCode: null, @@ -49,6 +50,12 @@ describe('buildSeriesMenuSections', () => { expect(resetRow({ seriesActionDisabled: true })?.disabled).toBeFalsy(); }); + it('disables the progress reset while a watched batch persists', () => { + // The hosts refuse the reset request meanwhile; an enabled row + // would close the menu and do nothing. + expect(resetRow({ watchBatchRunning: true })?.disabled).toBe(true); + }); + it('hides the reset once every episode is watched', () => { expect(resetRow({ seriesFullyWatched: true })).toBeUndefined(); }); diff --git a/libs/ui/components/src/lib/detail-ui/series-hero.state.ts b/libs/ui/components/src/lib/detail-ui/series-hero.state.ts index 3285d42f4..1005439a9 100644 --- a/libs/ui/components/src/lib/detail-ui/series-hero.state.ts +++ b/libs/ui/components/src/lib/detail-ui/series-hero.state.ts @@ -195,6 +195,8 @@ export interface SeriesMenuInput { readonly playbackActive: boolean; /** A start has not settled: another launch would double it. */ readonly startPending: boolean; + /** A season/series watched batch still persists: a reset request is refused meanwhile. */ + readonly watchBatchRunning: boolean; readonly sourcesCount: number; readonly externalPlayerHint: 'MPV' | 'VLC' | null; readonly copyUrlEpisodeCode: string | null; @@ -242,7 +244,7 @@ export function buildSeriesMenuSections( id: SERIES_MENU_ACTION.ResetProgress, labelKey: 'PORTALS.DETAIL.RESET_PROGRESS', icon: 'history_toggle_off', - disabled: input.playbackActive, + disabled: input.playbackActive || input.watchBatchRunning, testId: 'vod-menu-reset-progress', }); } diff --git a/libs/ui/playback/src/lib/vod-details/vod-details.component.html b/libs/ui/playback/src/lib/vod-details/vod-details.component.html index 45fc9ed38..8dfaa18d4 100644 --- a/libs/ui/playback/src/lib/vod-details/vod-details.component.html +++ b/libs/ui/playback/src/lib/vod-details/vod-details.component.html @@ -158,6 +158,9 @@ " icon="play_arrow" testId="vod-play-provider" + [disabled]=" + isExternalLaunchPending() || playbackStartPending() + " (pressed)="onProviderAction()" /> } diff --git a/libs/ui/playback/src/lib/vod-details/vod-details.component.spec.ts b/libs/ui/playback/src/lib/vod-details/vod-details.component.spec.ts index 89aa94007..659275a3b 100644 --- a/libs/ui/playback/src/lib/vod-details/vod-details.component.spec.ts +++ b/libs/ui/playback/src/lib/vod-details/vod-details.component.spec.ts @@ -160,9 +160,7 @@ describe('VodDetailsComponent offline playback', () => { }; const closeMenu = async (): Promise => { - document - .querySelector('.cdk-overlay-backdrop') - ?.click(); + document.querySelector('.cdk-overlay-backdrop')?.click(); fixture.detectChanges(); await fixture.whenStable(); }; @@ -420,6 +418,23 @@ describe('VodDetailsComponent offline playback', () => { expect(playDownload).not.toHaveBeenCalled(); }); + it('holds the downloaded provider action while a start is pending', async () => { + completeDownload(); + await render(); + expect(providerPlayButton()?.disabled).toBe(false); + + // Same window as the primary action: a second start could not cancel + // a launch already inside the player IPC. + fixture.componentRef.setInput('playbackStartPending', true); + fixture.detectChanges(); + expect(providerPlayButton()?.disabled).toBe(true); + + providerPlayButton()?.click(); + await fixture.whenStable(); + expect(playClicked).not.toHaveBeenCalled(); + expect(resumeClicked).not.toHaveBeenCalled(); + }); + it('resumes the provider source from the downloaded secondary action', async () => { completeDownload(); await render({ playbackPosition: 83 });