fix(playback): show the Up next card near the real end of an episode

The card appeared a fixed 8 minutes before the end: most of a short
episode, and far into the story of a long one, with no way to hide it.

- Adaptive lead: 4% of the episode, clamped to 40 s … 3 min.
- A closing-credits chapter in the last third, when timeline segments
  carry one, brings the card forward to its start (capped at 5 min).
- Close button and Escape dismiss the card for the current next episode.
- After 10 s (not while hovered) the card collapses into a one-line pill.
- Seconds countdown in the last minute.
- New playback setting "Up next card" (default on) turns it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-30 20:07:59 +02:00
1 parent 388fa9e29d
commit 5f6e8b037f
43 files changed
+707 -107

No files matched your search

+17 -2
View File
@@ -143,8 +143,8 @@ test.describe('@xtream inline series fullscreen', () => {
)
.toBeGreaterThanOrEqual(1);
// The 30 s fixture clip is inside the card's threshold from its
// first frame, so the card is up as soon as the duration is known.
// The 30 s fixture clip is shorter than the card's 40 s minimum
// lead, so the card is up as soon as the duration is known.
const card = playerView.locator(
'[data-test-id="player-controls-up-next"]'
);
@@ -169,6 +169,21 @@ test.describe('@xtream inline series fullscreen', () => {
await expect(card).toHaveAttribute('aria-label', /S01E03/, {
timeout: 15_000,
});
// The corner button hides the card for this episode; bringing the
// controls back does not bring it back. The card hides with idle
// controls, so wake them first.
await playerView.hover();
await expect(card).toBeVisible();
await playerView
.locator('[data-test-id="player-controls-up-next-close"]')
.click();
await expect(card).toHaveCount(0);
await playerView.hover({ position: { x: 40, y: 40 } });
await expect(
playerView.locator('[data-test-id="player-controls-next-episode"]')
).toBeVisible();
await expect(card).toHaveCount(0);
});
test('switches episodes from the fullscreen episode panel without leaving fullscreen', async ({