mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
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:
1 parent
5b8717b69c
commit
aaae2b0572
4 files changed
+56
-7
No files matched your search
+6
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user