Skipping to the next episode left a mid-episode resume point behind, so the skipped episode reappeared in Continue Watching with a partial progress bar. Skipping means "done with this one", not "stopped here". - reportSkippedEpisode marks the outgoing episode played instead of reporting a stop position, and arms a one-shot suppression consumed by the player's stop handler, so VideoPlayer's post-navigation unmount stop report can't overwrite the 100% progress with the partial one. - Continue Watching drops resume entries superseded by Next Up: an in-progress episode whose series has a next-up entry strictly later in series order (season, then episode) is hidden from the Home and TV rows. Movies, series without a next-up entry, and items with unknown or mixed ordering are always kept. Adds UR-059, DR-088, DR-089. TRACES: UR-059 | DR-088, DR-089
65 lines
2.2 KiB
TypeScript
65 lines
2.2 KiB
TypeScript
// Skip-to-next-episode reporting.
|
|
//
|
|
// Skipping an episode is a "done with it" signal, not a "stopped here" one:
|
|
// the user is moving on because they've already seen it. So a manual skip
|
|
// records the outgoing episode as fully played rather than saving the
|
|
// mid-episode position as a resume point.
|
|
//
|
|
// The suppression handshake exists because VideoPlayer.onDestroy fires its
|
|
// final reportStop *after* the skip navigation, with the mid-episode time. If
|
|
// that landed, it would overwrite the just-written 100% progress and the
|
|
// episode would look partially watched again. markSkipped() arms a one-shot
|
|
// suppression that the stop handler consumes.
|
|
//
|
|
// TRACES: UR-059, UR-025 | DR-088
|
|
import { markAsPlayed, reportPlaybackStopped } from "./playbackReporting";
|
|
|
|
/** Item id whose next stop report should be dropped, if any. */
|
|
let suppressedItemId: string | null = null;
|
|
|
|
/**
|
|
* Arm suppression of the next stop report for `itemId`.
|
|
*
|
|
* Exported separately from `reportSkippedEpisode` so callers that already
|
|
* handled their own reporting can still silence the unmount stop.
|
|
*/
|
|
export function markSkipped(itemId: string | null): void {
|
|
if (!itemId) return;
|
|
suppressedItemId = itemId;
|
|
}
|
|
|
|
/**
|
|
* Should the pending stop report for `itemId` be dropped?
|
|
*
|
|
* One-shot: consumes the armed suppression, so a later genuine stop on the
|
|
* same episode still reports its position normally.
|
|
*/
|
|
export function shouldSuppressStopReport(itemId: string | null): boolean {
|
|
if (!itemId) return false;
|
|
if (suppressedItemId !== itemId) return false;
|
|
suppressedItemId = null;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Record a manually skipped episode as fully watched.
|
|
*
|
|
* Deliberately does NOT call `reportPlaybackStopped` — that would write the
|
|
* partial position we are trying to avoid.
|
|
*/
|
|
export async function reportSkippedEpisode(itemId: string | null): Promise<void> {
|
|
if (!itemId) return;
|
|
|
|
markSkipped(itemId);
|
|
await markAsPlayed(itemId);
|
|
}
|
|
|
|
/** Test hook: clear armed suppression between cases. */
|
|
export function resetSkipState(): void {
|
|
suppressedItemId = null;
|
|
}
|
|
|
|
// Re-exported so the module owns the full skip story; callers that need the
|
|
// normal stop path keep importing it from playbackReporting directly.
|
|
export { reportPlaybackStopped };
|