feat(dashboard): say where you left off instead of which provider it came from (#1646)

Every dashboard title carried a "Xtream · Series" / "Stalker · Movie" subtitle. Provider kind and content kind are the app's own taxonomy, not a property of the title, and they are identical on every card in a rail — so the one line that could tell two cards apart said nothing.

The hero now shows the source name alone, through `playlistDisplayLabel` (a stored playlist name is routinely the pasted URL with credentials, or a MAC). Continue Watching cards show what actually varies: the "S1·E5" chip plus "12 min left". Favorites keep the title alone, Recently Added keeps the source name, and a meta row with nothing in it is no longer rendered.

Stalker shows filed under Movies had no badge, no progress and no resume. An embedded-VOD row announces its episodes through a `series[]` array and carries no `is_series` flag, so `extractStalkerItemType` reports `movie` on purpose — the item must keep routing to the VOD catalog — while its progress is a set of episode positions keyed by the parent id, which the dashboard was looking up as a single `vod` row and never finding.

Split the two questions: `PortalActivityItem.watch_kind` records the progress model when it differs from the routing type, and every reader that has to choose goes through `resolvePortalActivityWatchKind` instead of `type`. That makes the resume handoff reachable for Stalker, so wire it through `STALKER_SERIES_RESUME_TARGET`: consumed once after the series positions are read, hydrating a lazy Ministra season first with a bounded two-attempt retry, and playing nothing at all when the position read failed rather than restarting the episode from zero.

Also fixes the global-recent route template, which bound `[seriesResume]` only on its Xtream branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5 authored and GitHub committed 2026-09-20 22:19:47 +02:00
1 parent 954c8ad65e
commit 4e575a810e
26 files changed
+1514 -160

No files matched your search

+25 -10
View File
@@ -74,13 +74,27 @@ with a return handler keep Back available.
Stalker movies/series into the matching global collection route with detail
pre-opened.
- The dashboard hero CTA and the Continue Watching cards' explicit "Resume
episode" ⋮ action for Xtream series carry a one-shot season/episode resume
target. The collection-owned Xtream detail consumes it after its episode
positions load and starts that exact episode; the cards' default click is
detail-only (movie-like), as is opening the series from the collection grid
itself. If the positions load fails, the target stays unconsumed and the
handoff degrades to detail-only rather than starting the episode at offset
zero. Continue Watching cards also expose "Mark as Watched" (maxes out the
episode" ⋮ action for Xtream and Stalker series carry a one-shot
season/episode resume target. The collection-owned detail consumes it after
its episode positions load and starts that exact episode; the cards' default
click is detail-only (movie-like), as is opening the series from the
collection grid itself. If the positions load fails, the target stays
unconsumed and the handoff degrades to detail-only rather than starting the
episode at offset zero. Xtream reads it from `XTREAM_SERIES_RESUME_TARGET`;
Stalker from `STALKER_SERIES_RESUME_TARGET`, provided by
`StalkerCollectionDetailComponent` and consumed by
`StalkerSeriesViewComponent` (`stalker-series-resume.ts`), which first
hydrates the lazy Ministra season the target lives in. The two Stalker
shapes differ in the handoff state: a lazy `is_series` row carries
`contentType: 'series'` (the flag is read by `extractStalkerItemType`) and
`resolveStalkerCollectionDetailMode` sends it through the VOD detail flow
without changing that type, while an embedded-VOD row — a `series[]`
episode array and no flag — carries `contentType: 'movie'`, because the
type resolver is deliberately blind to that array so the item keeps
routing to the VOD catalog. Only the detail, reading the stored row, can
tell the second shape from a real movie, so
`getOpenCollectionDetailItemState` keeps a resume target for any non-live
Stalker item, not only `contentType: 'series'`. Continue Watching cards also expose "Mark as Watched" (maxes out the
tracked position row) and "Remove from history" in the same ⋮ menu.
- Ready Download Manager cards open one of the three focused
`downloads/:downloadId` routes. These local details hide the workspace
@@ -227,9 +241,10 @@ Dashboard behavior to preserve:
Xtream movie/series items into `/workspace/global-favorites` or
`/workspace/global-recent` with collection detail pre-opened from navigation
state.
- When an Xtream series recent has a saved episode position, the dashboard hero
and Continue Watching card should include that exact series/episode target in
the navigation state. It is a one-shot playback request and must not leak into
- When a series recent (Xtream, or a Stalker row that
`resolvePortalActivityWatchKind` answers `series` for) has a saved episode
position, the dashboard hero and Continue Watching card should include that
exact series/episode target in the navigation state. It is a one-shot playback request and must not leak into
normal favorites, search, category, or collection-grid navigation. Only
position rows that name their parent `seriesXtreamId` produce a target:
episode-keyed recents make `item.xtream_id` an episode id, so legacy rows
+28 -14
View File
@@ -113,17 +113,29 @@ Render rules:
2. `continueWatchingCards` — maps `globalRecentVodItems()` to movie/series
cover cards. Portal playback positions are bulk-loaded per playlist so
hero and cards can show progress, remaining time, and series season/
episode badges. This includes Stalker VOD activity normalized to series
through `is_series`. Series lookup uses keyed maps for both direct
episode ids and parent series ids; card renders must not scan the full
playback-position map. The badge uses saved `seasonNumber` /
episode badges. Whether an item is looked up as a movie (one `vod`
row) or a series (episode rows under the parent id) is its WATCH
kind, `resolvePortalActivityWatchKind`, not its routing `type`. The
shape that needs the distinction is a Stalker embedded-VOD row: its
stored entry carries a `series[]` episode array but no `is_series`
flag, so `extractStalkerItemType` reports `movie` (deliberately — the
item belongs in the VOD catalog) while its progress lives in episode
rows. The mappers give both it and a lazy Ministra `is_series` row
(already typed `series`) `watch_kind: 'series'`. Series lookup uses
keyed maps for both direct episode ids and parent series ids; card
renders must not scan the full playback-position map. The badge uses saved `seasonNumber` /
`episodeNumber` metadata and does not infer it from provider payloads;
legacy rows without that metadata remain badge-less until replay.
Dashboard-originated Xtream series clicks also carry that exact episode
target through the global-recent inline-detail handoff. Once the series
metadata and playback positions load, the detail player consumes the
target once and resumes the saved episode. Opening the same item normally
from the global recent grid remains a detail-only action.
Dashboard-originated Xtream and Stalker series clicks also carry that
exact episode target through the global-recent inline-detail handoff.
Once the series metadata and playback positions load, the detail player
consumes the target once and resumes the saved episode. Opening the same
item normally from the global recent grid remains a detail-only action.
Continue Watching cards carry no provider/content-kind subtitle: their
meta row is the S·E chip plus a "N min left" label (`remainingLabel`,
from `formatRemainingLabel`), and the row is not rendered when both are
absent. The hero subtitle is the source name alone, through
`playlistDisplayLabel`.
3. `liveFavoriteCardsEnriched` and `recentLiveCardsEnriched` — two
independent rails (`dashboard-live-favorites-rail` and
`dashboard-recent-live-rail`); there is no fallback from one to the
@@ -136,8 +148,9 @@ Render rules:
which calls `getGlobalRecentlyAdded('all', limit, 'xtream')` with the
DB-level `playlists.type = 'xtream'` filter. The rail is Electron-only
(PWA returns `[]`) and auto-hides when empty, so users without Xtream
playlists never see it. Cards carry a `playlist_name · type` subtitle
so users can tell which provider each item came from. Driven by an
playlists never see it. Cards carry the source name as their subtitle
(`playlistDisplayLabel`) so users can tell where each item was added;
the content kind is not repeated on every card. Driven by an
effect that re-runs whenever the Xtream playlist count changes, but the
first run waits for `globalFavoritesLoaded()` so the slower
recently-added DB query does not block the live favorites rail on
@@ -217,9 +230,10 @@ The welcome state is rendered via the existing
rails have data.
5. Navigation from a rail card must deep-link into the appropriate workspace
route without switching the active playlist in the header switcher.
6. Xtream series hero/Continue Watching clicks with a saved episode position
must resume that exact episode while preserving the collection-owned detail
and Back behavior. Do not apply autoplay to ordinary collection-grid clicks.
6. Xtream and Stalker series hero/Continue Watching clicks with a saved episode
position must resume that exact episode while preserving the
collection-owned detail and Back behavior. Do not apply autoplay to
ordinary collection-grid clicks.
7. `Recently Used Sources` reflects recent source usage across all provider
types, not just recent imports.
8. The live rail title key must match the rendered source: favorites use