Follow-up to #1516 for the vendor-chrome path (shared controls opted out). With
Video.js's own controls, Chromium leaves a clicked control-bar button focused,
and a focused Video.js component captures the keyboard entirely, so after
clicking fullscreen Space left fullscreen instead of pausing and the seek,
volume and mute keys did nothing until the user clicked the video. ArtPlayer
and the native HTML5 controls were verified unaffected.
The legacy Video.js chrome now releases the focus a pointer interaction leaves
on a control (vjs-pointer-focus-release.ts). The release is scoped to the
.vjs-control-bar and pointer-attributed, and runs on both focusin (focus
landing on a control, e.g. a menu handing focus to its button) and click (a
control clicked while already focused, which fires no focusin); keyboard Tab
focus and modal-dialog focus traps are preserved. The eligibility helper is
shared with ControlsSurface via pointer-focus-release.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Chromium focuses a clicked <button>, and a focused control captures the
keyboard: Space and Enter activate it again, and ControlsShortcuts yields
to any interactive element in the key's path. After a click on the
fullscreen button, Space left fullscreen instead of pausing and the seek,
volume and mute keys did nothing until a click on the video took focus
away. Follow-up to #1512, which stopped that focus from pinning the bar
but left it on the button.
A completed pointer click now releases the focus it left on the control
(onBarClick -> ControlsSurface.releasePointerFocus). The click is
attributed by its pointerType (empty for Enter/Space activation and
element.click()), with the legacy MouseEvent fallback answered once per
recorded press, so keyboard activation keeps focus where Tab put it.
Only buttons and range sliders are released. Chromium keeps its
sequential-focus starting point at the blurred control, so a later Tab
continues from it. The release dispatches a focusout while the pointer
still rests on the control, so the volume anchor ignores it instead of
closing the popover under the hovering mouse.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
WebPlayerViewComponent remounts the engine component for every playback
application, and the DOM Fullscreen API exits the moment its element leaves
the document. The fullscreen element was the engine shell, so every next-
episode click, autoplay hand-off, channel zap and alternative-source switch
dropped the viewer back to the page.
app-player-controls gains a `fullscreenTarget` input; HTML5, Video.js,
ArtPlayer and Embedded MPV forward it, and WebPlayerViewComponent passes its
own host element, which spans all applications of one mount. Keeping
fullscreen exposed a latent bug: the Electron header handoff set plain
fields under OnPush hosts and was only rendered thanks to the fullscreen
exit's stage resize; `channel`/`vjsOptions` are signals now.
Covered by unit regressions (fullscreen target, WebPlayerView remount, OnPush
handoff), a web-e2e run through a manual and an automatic episode switch, and
a manual Electron check. Docs and release note updated.
Closes#1498
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(dashboard): detail-first continue watching cards with quick actions (#1441)
Continue Watching cards now open the detail page on click like movie
cards; resuming the saved episode, marking it watched, and removing the
entry from history move into a per-card ⋮ menu. Series details land on
the earliest season with unwatched episodes (or the latest once all are
watched) instead of always season 1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): address review findings and season auto-select regressions
- A session's own watched toggles no longer re-resolve the selected
season when the positions map first fills — marking season 1 watched
used to jump the view to season 2 (CI regression in the web and
Electron season-watched-toggle E2Es).
- The all-watched season fallback skips loaded-but-empty seasons and
picks the latest season that has episodes (Greptile P1).
- Mark as Watched uses the strict failure-propagating save boundary
(Codex P2), and both card mutations surface persistence failures via
a snackbar with the new WORKSPACE.DASHBOARD.ACTION_FAILED key in all
19 languages (Greptile P2).
- Season E2Es now assert the intended post-reload behavior: the fresh
mount lands on the earliest unwatched season while season 1 keeps its
watched state behind its tab.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(portals): mark a whole series as watched in one click
Adds a series-level watched toggle to the season header's new overflow
menu on both Xtream and Stalker series detail pages (issue #1442 v2,
building on the season-level toggle from #1447).
- Shared: buildSeriesWatchToggleRequest flattens every loaded season
with the season builder's mark/unmark semantics; the direction is
always the one the label advertised, never re-inferred at persist
time. Watch-toggle state math for both scopes moves into the new
component-provided SeasonWatchPresenter (the container component sat
at the max-lines cap).
- Xtream: the series request reuses SerialDetailsSeasonWatchService
through a scope-parameterized handle(), the same stillCurrent
ownership guard, and the XtreamStore.loadAllPositions badge refresh.
- Stalker: the season handler's core is extracted into
runWatchToggleBatch (feedback keys per scope). Lazy Ministra VOD
hydrates unloaded seasons sequentially first (zero writes on a failed
fetch, silent abort on navigation), re-runs the position reconcile
synchronously so newly hydrated episodes' legacy rows are cleaned,
then rebuilds the request keeping the clicked direction; an
all-watched outcome reports an honest count-0 snackbar.
- Container: new hasUnloadedSeasons input blocks the fully-watched
verdict and the count label while lazy seasons are unloaded, and the
empty mark request contract lets the host hydrate-then-rebuild.
Six new XTREAM i18n keys, synced to all 18 locales via the i18n-fill
workflow. No new IPC: the existing playback-position batch channels are
season-agnostic.
Refs #1442
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(portals): treat an empty Stalker season answer as loaded, not pending
A lazy season the portal ANSWERS for with zero episodes was still
counted as unloaded (episodes.length === 0 heuristic): the series label
stayed countless forever and every series toggle re-fetched the empty
season, while a glitch-empty answer could silently skip a season and
still report success as if nothing remained.
VodSeriesSeasonVm gains an episodesLoaded flag set by every successful
episode fetch — including an empty one — and the series toggle's
pending predicate, hydration re-check, and hasUnloadedVodSeasons now
key on it. A loaded-and-empty season unblocks the count label and the
fully-watched verdict instead of re-fetching; a fresh detail mount
still re-fetches, so a one-off glitch self-corrects next session.
Addresses the Greptile P1 on PR #1451.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(portals): harden lazy season hydration against malformed and racing loads
Two review findings on the series watched toggle:
- fetchVodSeriesEpisodes now trusts an empty answer only when the
envelope actually carried a well-formed array; a malformed envelope or
an answer whose rows contain no recognizable episode rejects, so the
load fails instead of the season being recorded loaded-and-empty and
silently skipped by the series batch.
- loadEpisodesForSeason is single-flight per season: a tab click, the
spillover prefetch, the quick-start recursion, and the series-toggle
hydration join one in-flight request instead of duplicating portal
traffic — previously a second request's failure could abort a series
toggle whose original request succeeded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(portals): mark a full season as watched in one click
Series detail pages on both Xtream and Stalker portals get a season-level
watched toggle next to "Download season": marking writes full-progress
rows for the unwatched episodes only (real durations survive), a fully
watched season flips the action to unwatch-all.
Persistence goes through new batch IPC channels
(DB_SAVE/CLEAR_PLAYBACK_POSITIONS_BATCH, one SQLite transaction with
onConflictDoUpdate().run(); the PWA data source rewrites its
localStorage blob once). Stalker deliberately bypasses the batch IPC
and loops the existing position-mutation queue so legacy-row
reconciliation still runs and the queue coalesces to a single reload;
partial failures surface a dedicated snackbar.
Also removes the dead toggleEpisodeWatched store method, splits
season-container/serial-details-playback under the max-lines cap
(season-watch-toggle.util.ts, SerialDetailsSeasonWatchService), and
classifies *.spec-data.ts fixtures under the test max-lines ceiling
(baseline shrinks by main.preload.spec-data.ts).
Closes#1442
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(portals): guard stale season batches and split partial-unwatch feedback
Review follow-up (Codex on #1447):
- A season batch completing after the user navigated to another series
or playlist no longer writes the old series' rows into the freshly
reset position state (episode ids can collide across playlists); the
Xtream host captures the playlist/series identity before awaiting and
skips the rendered-state mutation when it changed. The DB write is
unaffected — it carries its own playlistId.
- A partially failed "mark season as unwatched" on Stalker now reports
a dedicated SEASON_MARKED_UNWATCHED_PARTIAL message instead of the
watch-direction "marked" text; translated into all 18 locales.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(portals): exclude the playing episode from season marking and count partial saves
Second review round (Codex on #1447):
- The episode currently playing (inline or in an external session, or
with a launch in flight) is excluded from a season's mark-watched
batch: the player persists its live position every ~15 s and would
immediately overwrite the just-written full-progress row. The button
count reflects the exclusion and the action disables when nothing is
markable. Unmarking still clears such an episode — the recreated
in-progress row reflects live playback truthfully.
- A Stalker StalkerSeriesPositionPartialSaveError (scoped watched row
saved and published, only legacy cleanup failed) now counts as a
watched success instead of feeding false total-failure feedback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(portals): gate stale season-batch snackbars on the originating page
Third review round (Codex on #1447): a batch resolving after the user
navigated away no longer shows its contextless success/error snackbar
on the newly opened detail page — the same ownership check that guards
the state mutation now guards the feedback too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(portals): sync catalog progress badges after toggles and gate Stalker feedback
Fourth review round (Codex on #1447):
- Any Xtream watched toggle (single episode or season batch) now
refreshes XtreamStore.loadAllPositions after persisting — the catalog
reads series-progress badges from the store, which otherwise loads
positions once per playlist, so returning from the detail kept stale
badges. Skipped when the playlist changed mid-flight (the store then
belongs to the other playlist; its own init reloads positions).
- Stalker's season snackbars are gated on the captured playlist/series
identity, matching the Xtream ownership guard — a batch draining after
navigation no longer reports on the newly opened page.
- Stalker season-toggle specs moved to stalker-series-view.season-watch
.spec.ts with their own harness; both prior spec files sat at the
1200-line test ceiling.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: describe the season watched toggle in CLAUDE.md
Fifth review round (Codex on #1447): the canonical Seasons entry in the
VOD/Series detail section now covers the bulk toggle, its playing-episode
exclusion, both persistence paths, catalog badge sync, and the
stale-completion contract.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(portals): let only the latest positions load patch the Xtream store
Sixth review round (Codex on #1447): loadAllPositions is now
latest-load-wins — a fetch superseded while in flight (playlist switch
before getAllPlaybackPositions resolves) no longer patches the singleton
store with the previous playlist's position maps, which could leave the
new catalog showing the old playlist's progress badges.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: reflect the spec-data max-lines classification in CLAUDE.md and AGENTS.md
Seventh review round (Codex on #1447): both canonical max-lines
descriptions now list **/*.spec-data.ts among the test-ceiling globs so
future agents neither treat these fixtures as production files nor
remove the exemption unknowingly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(portals): parse "N min" durations when marking episodes watched
Eighth review round (Codex on #1447): Stalker VOD episodes report
durations like "45 min", which parseDuration could not read — bulk (and
single) mark-watched then persisted 1/1-second rows. The minute format
now parses to seconds, matching what the removed legacy store method
already handled.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(portals): parse compound hour durations and cover the toggle end-to-end
Ninth review round (Codex on #1447):
- parseDuration now reads the compound "1h 30min" form the Xtream
fixtures emit (hour group optional, so "45 min" keeps working) —
bulk-marked episodes no longer persist a minutes-only duration.
- New Playwright coverage exercises the season toggle through the real
UI on both portals: Xtream (category → series detail → mark →
reload-persistence → unmark) and Stalker (embedded-series flow,
mark → unmark with the item's actual episode count).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(portals): refresh Stalker catalog progress badges after watched toggles
Tenth review round (Codex on #1447): the Stalker mirror of the Xtream
catalog sync — StalkerCatalogFacadeService loads its position maps once
per playlist and the runtime bridge only pushes external-player updates,
so renderer-initiated toggles left grid badges stale. The series view
now calls the facade's new ownership-checked refreshPositions after the
season batch (including partial successes) and after single toggles;
the reload is latest-load-wins like the Xtream store fix. Optional
injection keeps collection-detail mounts outside the catalog working.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(portals): cover the season toggle batch IPC end-to-end in Electron
Eleventh review round (Codex on #1447): the new Electron E2E marks a
season through the real UI, asserts the eight SQLite rows written by
DB_SAVE_PLAYBACK_POSITIONS_BATCH directly through the preload bridge,
proves persistence with a full app relaunch (renderer and main process
die, so state can only come from the database file), and clears again
through DB_CLEAR_PLAYBACK_POSITIONS_BATCH back to zero rows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): keep watched rows out of the series resume target
Twelfth review round (Codex on #1447): a watched position row — a
natural finish or a manual/bulk "mark watched" marker — is a completion
record, not resumable progress. Continue Watching no longer auto-plays
such an episode at its end; the handoff stays detail-only and the series
page's quick-start picks the first unwatched episode instead. Card
progress bars and SxxEyy badges keep their current source.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(portals): fail closed on refresh reads and gate batch APIs by capability
Thirteenth review round (Codex on #1447):
- Position-cache refreshes now use a failure-propagating read
(getAllPlaybackPositionsOrThrow through the Electron data source): a
transient IPC failure rejects instead of masquerading as an empty
list, so a populated store/facade cache stays stale-but-populated
rather than being wiped. All load/refresh call sites handle the new
rejection (init loads may retry on the next activation; post-toggle
refreshes log and keep the snackbar flow).
- The season-batch bridge methods joined playbackPositionStorageMethods,
so a bridge lacking them degrades to the in-memory path wholesale
instead of throwing mid-action.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(playback): apply saved player changes to mounted web players
WebPlayerViewComponent resolved the saved engine from a one-shot
StorageMap snapshot taken at mount, so a player switch from the command
palette or settings page confirmed via snackbar and persisted the
setting while an already-mounted Xtream/Stalker player silently kept
the previous engine. The same snapshot also made first play mount the
default Video.js engine and swap to the saved one once the async read
landed.
Resolve the player (and recording folder) from the live SettingsStore
signal instead and drop the snapshot entirely. Precedence is unchanged:
temporary recovery override -> host playerOverride -> saved player ->
Video.js. Hosts passing no override (Xtream/Stalker live layouts, the
portal inline detail player) now track saved changes in place; first
mount reads the already-loaded store, so the default engine no longer
flashes.
Regression coverage (all verified to fail with the fix reverted):
three unit tests on the component and two Xtream live-route e2e tests —
a palette switch reaching the mounted player without a layout remount,
and a MutationObserver engines-ever-seen assertion that the saved
engine mounts first time.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(playback): retain mounted engine when saved player becomes MPV/VLC
With the saved player now live-tracked, a mid-session palette switch to
managed MPV/VLC cleared the inline binding on hosts without a
playerOverride and left a blank viewport — the web player view can
neither render nor launch external players. resolveRenderableWebPlayer
keeps the mounted engine in that case; the external choice applies when
the host starts the next playback. Renderable players, including
Embedded MPV, still apply live. Raised by Codex review (P2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- save Xtream playlist details through browser-safe metadata persistence in PWA\n- keep PWA Xtream data source cache in sync with current playlist metadata\n- cover dialog close timing, stale cache, and PWA data-source bootstrap regression
The redesign rollup in this branch replaced the Add Playlist dialog's
2-level type × subtype tabs with a single flat 5-card radiogroup, and
replaced the Settings theme list with a compact segmented control whose
options are just "Light"/"Dark"/"System" (no "theme" suffix). E2E tests
that pinned to the old roles/labels failed in CI:
- basic / xtream / stalker / self-hosted: getByRole('tab', ...) on
"Add via file upload" / "Xtream" / "Stalker" — there are no tabs in
the new dialog. Switched to getByRole('radio', { name: /M3U file/i })
and friends, matched against the new radio labels.
- settings: getByRole('radio', { name: 'System theme' }) — the new
picker uses just "System". Scoped to the [data-test-id="select-theme"]
radiogroup so it doesn't collide with the identically-labelled
cover-size options below.
The Electron `clickDialogSegmentedOption` helper grew a radio-role lookup
as its primary path and keeps the old tab/button/legacy-selector
fallbacks so a future redesign won't break every fixture again. The
"M3U" parent category becomes a no-op (the new picker has no standalone
M3U tile — callers always specialise to M3U URL/file/text immediately
after, which is what we want anyway).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mat-button-toggle elements inside a group render their internal buttons
with role="radio", not role="button", so getByRole('button') selectors
fail. Switch to attribute selectors targeting the value attribute instead.
https://claude.ai/code/session_01VU6ZZTG7YrhKhKBQvBfmpp
- Implemented tests for importing M3U playlists via native dialog.
- Added smoke tests for loading Xtream and Stalker content through IPC.
- Created tests to verify persistence of remote control settings across app restarts.
- Updated smoke tests to check for main window properties and workspace content rendering.
refactor(electron-backend): enhance store service and database path handling
- Modified store service to use a dynamic configuration directory based on environment.
- Refactored database connection to utilize new path utility functions for better path management.
fix(e2e): improve test selectors and structure for better reliability
- Updated test selectors in web-e2e tests to use role-based queries for better accessibility.
- Refactored settings tests to reduce redundancy and improve clarity.
chore(e2e): update TypeScript configuration for better inclusion of test files
- Adjusted tsconfig.json to ensure proper inclusion of e2e test files.
Entire-Checkpoint: bb9a8e2e351e
- Introduced `stalker-mock-server` with TypeScript configuration for local development and testing.
- Updated `playwright.config.ts` to run both Angular app and mock server in parallel during e2e tests.
- Created comprehensive e2e tests for Stalker portal functionality, including health checks, portal addition, and content loading.
- Added detailed architecture documentation for both `stalker-mock-server` and `xtream-mock-server`, outlining design decisions, data flow, and API protocols.
- Implemented `xtream-mock-server` to simulate Xtream Codes API for local development and testing, with corresponding e2e tests.
- Enhanced test isolation by resetting mock server state before each test run.