mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
525ca7bc4470dc8863a81759a4843bdc6b8c9b4e
824
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
525ca7bc44 |
fix(playback): show the Up next card near the real end of an episode (#1768)
* 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> * fix(playback): address Up next card review feedback - Offer the Up next card setting for Embedded MPV only under the frame-copy engine; native view never mounts the shared controls. - Hovering pauses the collapse delay instead of restarting it. - Document that the card stays visible when the controls auto-hide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): restart the Up next collapse delay for a new episode - A card that stays mounted while its next episode changes gives the new item the full delay instead of the previous item's leftover. - The setting description no longer promises credit-based timing: no current series path supplies chapters yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cb317bad4c |
fix(ui): keep dialog action rows on one line (#1762)
* fix(ui): keep dialog action rows on one line Settings "Unsaved changes" dialog: - Cancel / Discard / Save replace the phrase labels in all 19 locales, and the dismiss now comes first; the shared CANCEL key replaces the unused UNSAVED_DIALOG_STAY. - At the 640px phone breakpoint the actions stack one per row, full width, in DOM order. EPG programme dialog: - mat-dialog-title gives the dialog an accessible name. - The footer Close is the only dismiss; it comes first and the primary action last. - The archive copy/download tools move under their notice, so the footer stays on one row. - Channel rows now open it through EpgProgrammeDialogService, which owns the 540px config and a panel class scoping the surface overrides. Adds a web-e2e layout spec (en, de, ru, fr, hu, ar on one row; de and ru stacked on a phone), extends the Electron EPG spec to all three openers, and documents the dialog contract in the UI guidelines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(epg): stack programme dialog actions on phones Below the 640px phone breakpoint the viewport caps the programme dialog, and a long translated primary label ("Regarder depuis le début") no longer fit beside Close. The footer had no wrap, and the dialog hides overflow, so the label was clipped. - At the phone breakpoint, the archive tools and the footer now stack one full-width button per row, in DOM order. - Buttons grow to fit their label, so a long label wraps inside its button instead of being clipped. - On desktop the footer can wrap again as a last resort. The new Electron test opens a past programme in French at a 360px viewport and measures both rows. It fails against the previous stylesheet (the footer buttons are 44px narrower than the row). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d4118ad442 |
feat(playback): give the fullscreen side panel the settings panel's glass look (#1767)
* feat(playback): give the fullscreen side panel the settings panel's glass look The fullscreen channel/episode panel was a full-height graphite strip while the controls' settings panel is an inset, rounded glass card. The side panel now wears the same surface: 16px inset (8px under 560px), 20px corners, the controls' glass fill, hairline border, blur and shadow, the same open motion and a 32px square close button. The edge hint uses the same glass, and the episode list marks the playing row with the settings panel's selected cyan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): dismiss the side panel from the exposed edge gutter The inset card leaves the hot zone's left strip visible beside it, above the scrim, so a click there re-ran show() instead of dismissing. A completed primary press in the zone now closes an open panel; hovering there still counts as inside, since a hover-opened panel leaves the pointer resting in that strip. The playing episode row also keeps its cyan tint on hover/focus. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): decide an edge press from the panel state at pointerdown A press held on the edge past the hover dwell opened the panel before the release, which then read the open state and dismissed it. The press now remembers whether it began on an open panel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0524e72b9f |
perf(dashboard): fill the hero rotation dot on the compositor (#1758)
* perf(dashboard): fill the hero rotation dot on the compositor The active hero dot animated `width` 0 → 18px for every 8 s rotation, so an idle dashboard with two or more slides ran style, layout and paint on every frame. The fill is now a full-width bar that slides in with `transform` under the pill's rounded clip. The `animationend` advance, the pause and reduced-motion behaviour are unchanged. Measured on the E2E build (visible, four slides, 120 s): layouts 10,405-10,677 -> 366-369, renderer process CPU 14.2-16.4 s -> 3.7-4.1 s, GPU process CPU 14.8-18.8 s -> 14.0-14.8 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): drive the real hero rotation animation The unit specs dispatch `animationend` on the dot span by hand, so a fill whose real event no longer reached the handler would still pass. The new Electron spec shortens `--hero-rotation-ms` and checks that the running `::before` fill advances the slide, that pause holds it and that Play resumes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
97f56e219c | perf(dashboard): stop idle dashboard ticks that have nothing to update (#1722) | ||
|
|
78a6648c8d |
perf(electron): let hidden and minimized windows report themselves hidden (#1724)
* perf(electron): let hidden and minimized windows report themselves hidden The main window was created with backgroundThrottling: false (since #1123, without a stated reason). Electron then keeps document.visibilityState at "visible" for a hidden, minimized or fully covered window and never lets Chromium throttle it, so every renderer timer, rAF and CSS transition ran at full rate in the background, and the playback keep-awake gate, which releases the display for a minimized window, could never see one. Use Chromium's default. Audible media and picture-in-picture are exempt from background throttling in Chromium, and a local check confirmed HLS playback continues unchanged through more than six minutes minimized, audible and muted. Playwright's focus emulation pins every page it attaches to as visible, so the new window-visibility E2E launches the app without Playwright and drives it over raw CDP (electron-unautomated-launch.ts). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): harden the unautomated Electron launch Review follow-ups for the window-visibility E2E: - Resolve `electron` in the main process through a require created from the `node:module` builtin instead of `process.mainModule`, which only exists when the app entry is CommonJS. - Bound teardown like closeElectronApplicationAndConfirmExit: SIGTERM, then SIGKILL, 5 s each, then fail instead of waiting forever. - Surface CDP protocol errors from Runtime.evaluate instead of returning undefined. - Wait until the window is actually shown before hiding it. The app shows its window on ready-to-show, and a hide() that lands earlier is undone by that show(); this was the first-attempt failure on the macOS shard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): check the playback display lock is released while hidden Review follow-ups for #1724: - Add an E2E that plays the webm fixture in the unautomated launch, records the main process's prevent-display-sleep blockers, and asserts the keep-awake lock is taken while visible, released when the window is hidden, and taken again when it is shown. The visibility tests alone would still pass if the renderer gate or the bridge stopped updating powerSaveBlocker. - Validate CDP replies before dispatch (CodeQL js/unvalidated-dynamic-method-call): only a numeric id with a pending settle function is called. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): skip killing an Electron that already exited stopElectron now checks the recorded exit state before each signal and tolerates a kill that races the exit: on Windows taskkill throws for a PID that no longer exists. Startup cleanup can no longer replace the startup error that explains the failure; a cleanup failure there is logged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stalker): keep the watchdog cadence while the window is hidden With background throttling on, Chromium wakes a hidden, silent page's timers at most once per minute after five minutes, so a portal that asks for get_events every 30 s would see pings at half its cadence while the window is minimized. Tick the watchdog from a dedicated worker (createBackgroundInterval, an inline blob worker allowed by the renderer CSP), whose timers are not subject to page throttling. It falls back to a page setInterval where no worker is available or the worker fails to load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stalker): keep a stopped watchdog interval stopped A worker error that arrived after stop() started the page fallback interval, which nothing cleared, so pings continued for an inactive playlist. stop() now marks the interval stopped, detaches the worker handlers, and the fallback refuses to start afterwards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4343150170 |
perf(dashboard): place live EPG progress fills without layout or animation (#1721)
* perf(dashboard): slide live EPG progress fills with a compositor transform The live-programme bars on channel rail cards and the hero animated their width over 0.4 s whenever the 30 s live-EPG tick moved them. Width is a layout property, so each tick re-laid out the whole document for about 24 frames, also while the window was minimized. Slide a full-width fill with translateX driven by a --live-progress custom property instead; Chromium runs that transition on the compositor. Reduced motion drops it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(dashboard): format the live progress spec * perf(dashboard): stop animating the live EPG progress fills A live-EPG tick moves the bar by under one percent, so the 0.4 s transform transition was invisible but still produced a burst of compositor frames on every tick. Place the fill without a transition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(dashboard): pin the hero progress fill to its custom property The hero moved into DashboardHeroComponent (#1738); its progress bar gets the same transform fill as the rail cards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
484920f10a | fix(dashboard): read M3U favorites after queued favorite writes (#1754) | ||
|
|
dc3f829807 | fix(parental-lock): focus, tokens and phone width for lock UI (#1761) | ||
|
|
03dfafd68b | fix(player): pin overlay reds to the --pc-* palette (#1759) | ||
|
|
f38c6f86d1 | feat(playback): draw catch-up programmes as seek-bar segments (#1750) | ||
|
|
d60c80746b |
perf(services): share the startup inventory read (J1) (#1716)
* perf(workspace): share the startup inventory read and defer non-first-card IPC (J1) Journey J1 / renderer.ipcCallsToFirstCard: 12 -> 7. - PlaylistsService.getAllPlaylists() shares one in-flight SQLite read between concurrent callers (the playlist effect and the XMLTV source reconciliation at startup). Settled reads are never reused and every SQLite write detaches the pending read. - getM3uFavoriteChannels() stops re-reading the write-once IndexedDB -> SQLite migration receipt once it has been seen. - StartupDeferralService holds the download list, app update status and the dashboard's recent items and favorites until one task after the render that reveals the routed content (5 s safety timeout). IPTVNATOR_DISABLE_STARTUP_DEFERRAL=1 is the kill switch. - reconcileEpgSources and the two distinct migration-flag reads stay on the critical path: the first is the #1548 revision fence, the second are different keys, not duplicates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(services): trust a migration receipt written in this session; correct J1 note - Mark the IndexedDB -> SQLite receipt as confirmed after an empty-store receipt write or a committed dbMigrateAppPlaylists, not only when it was already present, so M3U favorites skip the per-playlist re-read on the first launch after an upgrade too. - The release note no longer claims a wall-clock speedup the J1 benchmark did not show. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(services): keep only the shared startup inventory read (J1) Drop the startup deferral gate, its kill switch and the deferred loads: they lowered renderer.ipcCallsToFirstCard but moved neither spawnToFirstCardMs nor load->card beyond drift, and they grew renderer.initialBytes. Keep the shared in-flight inventory read, inline it in PlaylistsService with short property names, and copy the joiner's result with structuredClone (the Electron renderer has it; the services test setup polyfills it for jsdom). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(performance): describe the shared inventory read and the J1 validation result Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(services): drop the migration receipt memo for M3U favorites Skipping the receipt read let the dashboard ask the database for M3U favorite channels before a just-toggled favorite was written: the write waits in the per-playlist queue and the cross-context lock, and the dashboard does not reload when the store's favorites are unchanged. The extra round trip had been masking that race, and the Windows E2E run hit it (epg.e2e.ts "dashboard live rails find a programme that only another playlist's XMLTV carries"). The memo was off the first card's path, so it bought nothing measurable for J1; restore the per-call read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(services): share only the startup inventory read A pending read was shared with any concurrent caller, but not every playlist write goes through PlaylistsService: the settings reset deletes all playlists through DatabaseService, so a caller could join a read taken before that deletion (Codex review). Share only the first read, which the startup pair (playlist effect and XMLTV reconciliation) needs while the startup screen still hides every writing action; sharing ends when it settles or a PlaylistsService write starts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0f768b6e20 | fix(dashboard): extend rail edge fades to the track clipping edge (#1731) | ||
|
|
bcc6186c88 |
fix(settings): hide cached title matches the parental lock withholds (#1735)
* fix(settings): hide cached title matches the parental lock withholds Cross-playlist title matches are cached by their consumers (Actor and Discover routes, the dashboard trending and recommendation rails and the four "similar in your portals" rails), so matches found while unlocked kept advertising locked titles and their playlist names after a relock. Each consumer now filters on read through a reactive predicate on the match's provider category, so a relock hides them at once and an unlock shows them again, without a re-query. Closes #1723 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fall back to unlocked copies of a withheld title match The dashboard trending and recommendation rails and the "similar in your portals" rails picked one match per title at load, so hiding a withheld match lost a copy of the same title in an unlocked portal. They now keep every row the lookup returned and pick the match on read from the rows the parental lock does not withhold. Adds the release note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): derive the recommendations seed heading from the visible cards The rail kept only the seeds that contributed a card to the original selection, so a seed whose recommendations filled in after a relock (or an offline prune) was missing from the "Because you watched" heading. Every seed of the load is kept in order and the heading lists those that contribute a card now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
36be6495f2 |
perf(dashboard): hold rail skeletons back so empty rails stop shifting the page (#1738)
* perf(dashboard): hold rail skeletons back so empty rails stop shifting the page On every launch with sources the dashboard shifted by about 0.23 (the "good" CLS threshold is 0.1). The rails render as soon as their own data arrives, and each loading rail showed a 328 px skeleton immediately. On a normal profile the live-favorites and recent-content sources resolve empty 15-20 ms later, so their skeletons flashed and collapsed and every rail below jumped up by about 360 px. J1 never saw it: its layout-shift window ends at the first card, which is painted just before the collapse. Rail skeletons now wait out a 300 ms grace period (createRailSkeletonGrace) and appear only for a rail still loading after it; the hero keeps its immediate skeleton because it reserves the top of the page. Recorded over three renderer reloads of a seeded profile, the dashboard's layout shift drops from 0.219-0.234 to 0.0004, with the real rails painted at the same time as before. Plan thread C5, journey J1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(dashboard): gate rail skeletons per rail, never above visible rails Addresses the Codex and Greptile reviews on #1738. The component-wide grace timer started when the dashboard was created, so a rail that begins loading later (Xtream recently added, TMDB) showed its skeleton at once and could still flash and collapse; and after the grace period a slow rail's skeleton could appear above rails that already showed cards, pushing them down and, if it resolved empty, back up. createRailSkeletonGates now keeps one gate per rail, in template order: the grace period counts from that rail's own loading start, a skeleton is never inserted above a rail that already has cards (the real rail inserts at most once instead), and a shown skeleton stays until its own rail finishes so the first arriving rail does not collapse the others in a cascade. Nine specs cover the fast path, per-rail start, the no-content-below rule, latching, reloading, destroy and a zero grace period. The seeded-profile timeline is unchanged at 0.0004 across three renderer reloads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d8229b98fa |
feat(playback): record recently viewed only after the stream plays (#1732)
* feat(playback): record recently viewed only after the stream plays A channel, movie or episode used to enter Recently Viewed (and the dashboard's Continue Watching hero) the moment it was selected or its link was resolved, so streams that failed straight away cluttered the history. Writers now defer the write to a root PlaybackHistoryGate, keyed by the stream URL and/or the playback session key. The inline players confirm those keys once the owned engine's position has advanced by two seconds (seeks, stalls, pauses and a previous stream's progress do not count), the radio player does the same, and a launched MPV/VLC session confirms on `opened`/`playing`. M3U with MPV/VLC configured keeps recording on selection. Covers M3U (live, radio, movie detail), Stalker (live, radio, VOD, series), Xtream VOD and series, and the global live collection. The M3U host's embeddedPlayback is now compared by value: the history write updates the playlist meta mid-playback, and a new but identical playback object remounted the engine and restarted the stream. E2E flows that relied on recording-on-click now play local fixtures (HLS/TS/WebM routed in place of unreachable or public streams) and wait for confirmed playback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): tighten recently viewed confirmation per review - Correlate by session key first: when both the deferred write and the confirmation carry a playbackSessionKey, only that is compared, so the same stream URL played in another playlist no longer records a failed attempt. URLs remain the fallback (portal writes, MPV/VLC sessions). The M3U radio player now receives the host's session key. - Count only playing progress: engines report `playing` (not paused, not seeking) with each time update, so short seeks of paused media no longer confirm a view. - Xtream: a write confirmed after a playlist switch still saves to its own playlist but no longer replaces the current playlist's recent list. - Global live tab: a row confirmed after another row was selected still moves to the top of an open Recently Viewed list (only a disposed tab skips the notification). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): confirm session-keyed history only by its own session A write deferred with a playback session key (M3U) is now confirmed only by that key. The app-wide MPV/VLC session confirmation carries just the URL, so opening the same stream externally from another playlist could still commit an abandoned attempt. An "Open in MPV/VLC" recovery launch is instead confirmed by the WebPlayerViewComponent that requested it, under its own session key, once the launch has opened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(playback): keep the history gate off the initial bundle The `@iptvnator/services` barrel ships in the initial bundle, so adding PlaybackHistoryGate there (and subscribing to it from the app-wide ExternalPlaybackService) grew renderer.initialBytes by 1,141 bytes. - Move the gate to a new lazy-only `playback-data-access` project (`@iptvnator/playback/data-access`; scope:shared, domain:playback, type:data-access) and register it in the coverage policy. - The gate subscribes to MPV/VLC session updates itself; it is created by the first deferred write, which precedes the launch it waits for. ExternalPlaybackService is back to master. - The Xtream "playlist switched before confirmation" check moves to the lazy helper; the initial-path store only takes a `skipListRefresh` flag. Net effect on this branch: +27 bytes over master (master itself is 108 bytes over the ratchet baseline already). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(playback): drop late Xtream confirmations off the initial path The Xtream store ships in the initial bundle, so even the small `skipListRefresh` flag cost 27 bytes there. A confirmation can only arrive after a switch to another playlist from a slow MPV/VLC launch (the inline player goes with the page), so the lazy helper now drops it instead: recording it would misfile the item or replace the other playlist's recent list. with-recent-items is back to master. This branch is now 3 bytes below master on renderer.initialBytes; the ratchet still reports master's pre-existing overage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(playback): pass the spec type-check gate from master - playback-data-access: align tsconfig.spec.json with the epg-data-access config #1705 updated (bundler resolution, global.d.ts for window.electron). - M3U recent-history spec: type the selectSignal override. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): keep late Xtream confirmations in their own playlist history A confirmation that arrives after a switch to another playlist (a slow MPV/VLC launch) is no longer dropped: the lazy helper saves it to the captured playlist through the data source, without reloading the store's recent list, which belongs to the other playlist by then. The store and its barrel ship in the initial bundle, so the save path stays in the feature helper; renderer.initialBytes stays under the baseline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): correlate global live-tab history by its session key The unified Favorites/Recent live tab deferred its history write by stream URL only, so the same URL played from another playlist could confirm a failed selection, and a switch to catch-up before confirmation could never match. It now defers with the tab's playlist-scoped playbackSessionKey (the key its players confirm with), and the tab's radio player receives it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): keep MPV/VLC rows of the live tab confirmable by URL The live tab's session key can only be confirmed by its own inline players; MPV/VLC confirm the launched URL alone. A row that goes to an external player (also later, after a double-click) now defers by URL, and only rows played inline carry the session key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): per-channel M3U history attempts, capability-based Xtream fallback - M3U: the recently-viewed dedupe key now includes the channel id, so a second row of the same URL defers its own write (its session key) and is recorded when it plays after the first row failed. - Xtream late write: key uncached content by Xtream id per supportsXtreamSqliteDataSource (the data-source factory's contract), not by a generic Electron bridge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
be217d5cf5 | feat(playback): Hybrid redesign of the shared player controls (#1709) | ||
|
|
a0562d9f7c |
fix(portal): drop a poster that fails to load from the About block (#1673)
The watch-state About block rendered its poster whenever the URL was non-empty, so a URL that failed to load showed the browser's broken-image glyph with clipped alt text. The hero above already falls back on error. A failed URL now counts as missing, matching the block's own degradation rule. The failure is keyed by URL so a different poster gets a fresh attempt. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
887ac64d18 | feat(workspace): cinematic rotating dashboard hero (#1725) | ||
|
|
1d9a563d1a |
test(performance): count startup phases and SQL statements for the J1 launch journey (#1715)
* test(performance): count startup phases and SQL statements for the J1 launch journey
Implements plan item A2. With IPTVNATOR_PERF_CAPTURE=1 the main process
keeps named counters and registers a main-only performance:read-counters
IPC handler; without the flag nothing is counted and the handler does not
exist.
- debug-trace.ts owns the registry; traceStartupPhase replaces the
trace('startup', ...) sites and counts main.startupPhases.
- The database worker counts executed statements through better-sqlite3's
Statement prototype (the verbose callback expands every statement and
made bulk inserts 2-4x slower) and posts the count over its message
port, flushed before every other worker message. The main-thread shared
connection is counted through a new connection observer in the shared
database library.
- The first main window freezes main.modulesRegisteredBeforeWindow at
creation and main.sqlStatementsBeforeReadyToShow at ready-to-show.
- The journey gate drops the ready-to-show that Electron emits for the
about:blank detour, so the app sees the real document's first paint,
and taps the counters handler; the J1 record reads both counters after
the renderer probe completes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(database): require one SQL statement per exec during initialization
The performance capture counts one exec call as one statement, because
SQL cannot be split reliably in the counter (trigger bodies contain
semicolons). The historical-upgrade driver now wraps exec on every
connection initDatabase opens and fails on a batch, so that counting
assumption holds for the fresh profile and all historical schemas.
Documents the definition in the counter and the architecture docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(performance): count SQL statements only for the launch journey
Codex review: the M3U import, refresh-cancellation and Xtream benchmarks
also run with IPTVNATOR_PERF_CAPTURE=1, so the statement hook wrapped
every row of their bulk inserts and changed what they measure.
SQL counting now also needs IPTVNATOR_PERF_COUNT_SQL=1, which only the
launch journey sets; a harness test fails if another source sets it.
Startup phases, the window snapshot and the read handler stay on the
capture flag. Without SQL counting no ready-to-show listener is attached,
so a zero is never reported for statements nobody counted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
650da4a1d3 |
ci(test): type-check Jest spec programs and gate it in CI (#1705)
* build(test): make spec tsconfigs resolve what Jest resolves Lib spec tsconfigs used module: commonjs with node10 resolution, which cannot see Angular's exports-only secondary entry points, and dropped global.d.ts, so tsc reported thousands of resolution errors and no window.electron typing. Switch them to module: preserve with bundler resolution (ts-jest still forces CommonJS emit outside ESM mode), add global.d.ts to every spec program, type jest.unstable_mockModule for the ESM workspace, include the ui-epg and ui-playback specs that jest.web-esm.workspace.ts runs under the web spec config, and drop the snack-bar stub that shadowed the real Material types. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ci(test): gate spec type-checking with typecheck:spec Add tools/typecheck/spec-typecheck.mjs, which runs tsc --noEmit over every tsconfig.spec.json with a small pool and fails on any diagnostic, wire it into the unit-and-typecheck job after typecheck:ci, and document the gate and the spec tsconfig conventions in the validation map. Also bring the non-Tier-A spec configs (remote-control-web, ui-remote-control, stalker-mock-server) to the same conventions so the gate covers the whole workspace. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: fix the spec type errors surfaced by typecheck:spec With the spec programs resolving modules and ambient typings correctly, tsc reported 432 genuine errors across the Tier A projects: read-only capability flags assigned on Partial<> doubles, signal-store values used as types, fixtures missing required fields, index-signature property access, partial bridge doubles cast through incompatible shapes, and deferred resolvers narrowed to never. Type the doubles instead of casting to any: writable mapped types for capability flags, InstanceType<typeof StalkerStore>, typed jest.fn signatures, protectedState: false on test signal stores, and completed fixtures. Production changes are limited to bracket access for index-signature properties under the libs' noPropertyAccessFromIndexSignature setting and two narrowing guards in the global favorites loader. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(playback): use the ESM setup's jest global in the controls fixtures The fixture imported jest from @jest/globals, which is not a direct dependency. Jest provides that module at runtime, so tests passed, but on a clean pnpm install tsc cannot resolve it and typecheck:spec failed in CI. The ESM test setup already installs import.meta.jest as the global, typed by @types/jest, as the other ESM specs use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: type the parental lock doubles merged since the gate was written The parental lock feature (#1601) and the Stalker actor route landed on master with spec doubles declared as zero-argument jest.fn()s that the tests then drive with the real arguments, plus a copy of the ResizableDirective override imported from a library that does not export it. Give the doubles the lock service's real signatures, drop the dead override as in the sibling layout specs, use bracket access for the actor route's personId param, and keep the Stalker layout spec within the 1200-line limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
9d02f90dfe |
perf(web): keep backup/restore and portal helpers off the initial path (#1734)
* perf(web): keep backup/restore and portal helpers off the initial path #1601 (parental lock) put about 35 KB onto the renderer's initial path by design (the lock service, lock store and enforcement gate the workspace resolver and the catalog data sources) and was merged with the ratchet red: renderer.initialBytes 1,655,428 against the 1,619,993 baseline. Offset it without touching the lock gate. Code splitting puts a module in the chunk shared by every entry that reaches it, so helpers only lazy routes use landed in initial chunks because eager files reach them through barrels: - PlaylistBackupService (only the lazy settings page) moves to @iptvnator/services/playlist-backup and out of the services barrel. - The eager Xtream data layer and root shell import the portal logger and DI tokens through @iptvnator/portal/shared/util/logger and /tokens instead of the barrel, whose navigation, keyboard-shortcut and download helpers (about 45 KB) belong to the lazy portal routes. renderer.initialBytes 1,655,428 -> 1,598,232 bytes (-57,196). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(performance): lower the initial-bytes baseline to 1,598,232 bytes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e45cd85a78 |
feat(settings): PIN-protected parental lock for categories (#285) (#1601)
* feat(settings): add PIN-protected parental lock for categories (#285) Locks are per category (Xtream category ids, Stalker genre ids, M3U group titles) and kept in one renderer lock store persisted to app_state / localStorage; `categories.locked` is the SQLite index re-stamped from it. While the lock is active the DB worker filters every content read, the PWA data source, the Stalker store and the M3U channel list filter in memory, and the enforcement service reloads the stores and steps off withheld selections. Settings → Parental lock sets the PIN (PBKDF2, never in Settings), the relock timeout and Lock now; lock toggles live in the Xtream/M3U management dialogs and a new Stalker lock dialog, all behind the PIN. Backups carry the locks per playlist entry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): harden the parental lock after review - The M3U group dialog opens only after the PIN, like the Xtream and Stalker dialogs: it lists locked group names and can rewrite the locks. - Change PIN and Disable always verify the stored hash, even while the session is unlocked, so an app left unlocked cannot lose its lock. - Stalker paging judges progress on the raw portal page: withheld ids the list has not seen count as progress, a page made only of locked rows requests the next one itself, and the VOD total is reduced by withheld ids so the grid stops asking once every visible row is in. - Parental lock contract linked from the agent context map after the guidance reorganization; bridge helpers split out to stay under the file-size cap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): guard locked categories on routes, PWA search and paging - Xtream and Stalker `:categoryId` routes carry a parental-lock guard: a locked category reached by URL prompts for the PIN and redirects to the section root on refusal (Electron row ids are mapped to provider ids). - PWA search filters withheld categories like the catalog reads. - Electron warm-cache detection confirms an empty, lock-filtered read with the unfiltered existence check instead of refetching from the provider. - A Stalker lock flip past page 1 drops withheld rows at once and restarts the list from page 1 instead of appending onto stale pages. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): compile the PIN hashing helper in the Node backend build The web backend compiles the shared interfaces library without DOM typings, so the DOM-only `SubtleCrypto` / `BufferSource` names broke its Docker build. The helper now describes the WebCrypto surface it needs structurally and reaches it through `globalThis`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): close the remaining parental-lock gaps from review - Detail routes check the item's own category: a locked movie or series paired with an unlocked category id in the URL is still refused. - `requestUnlock()` awaits the settings load before it can answer "not active", so a slow startup cannot open a management dialog unguarded. - `SETTINGS_UPDATE` only persists the `parentalLockEnabled` mirror and releases the worker on switch-off; it no longer re-locks the worker on every ordinary settings save under a renderer that shows "unlocked". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): cover PWA cold navigation, Stalker search and the PWA lock editor - The Xtream detail guard hydrates the PWA session cache before judging an item on a cold navigation and fails closed when the catalog cannot place the item. - The dedicated Stalker search route filters withheld genres, re-fires on lock changes, judges paging on the raw page and restarts from page 1 on a lock flip. - The Xtream category dialog loads its lock candidates through the capability-selected data source; the PWA source now lists its raw categories with lock flags, so locks can be configured there too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): arm the relock timer on enable and harden Stalker search relock - The idle timer follows the unlocked transition instead of `active`, so the session that just enabled the lock still locks itself later. - Stalker search closes an open detail whose genre became withheld on relock and advances by itself past pages made only of locked rows (only while they add ids the list has not seen). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): close lock editors on relock and clear withheld details opened from All The Xtream, Stalker and M3U category editors are gated by the PIN only when they open; an idle relock left them on screen listing locked names with a lock-rewriting Save. Each now closes itself when the session relocks. ParentalLockEnforcementService also judges the selected Xtream/Stalker item by its own category: a detail opened from All, recently added or search has no selected category to vanish with, so it stayed open after a relock. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): fail closed on unreadable settings and finish relock clean-up - Unreadable settings (IndexedDB load failure) left the feature switch at its default and announced "unlocked" to the main process. A stored PIN now stands in for the switch, and without one nothing is announced, so the worker keeps its mirrored locked default. - Lock applies run one at a time and abandon superseded results; the Electron data source keys its in-flight share by lock version so a relock can never reuse an unlock refresh's unfiltered rows. - The stored in-portal Xtream search is re-run on a lock change. - Stalker live/radio selections are judged by tv_genre_id, and both live layouts drop the playback of a channel whose category became withheld. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): fail closed on an unreadable lock store and make lock writes reliable - A lock store that cannot be read is no longer treated as empty: while the lock is active every category is withheld (renderer predicates and set-based filters alike) until the PIN is entered or the store reads again, and writes are refused meanwhile so an empty in-memory store can never wipe the persisted locks. The lock set now lives in its own ParentalLockLockStore service. - The M3U group dialog's lock write is awaited and a failed save is reported in a snackbar instead of being silently dropped. - The Electron categories.locked re-stamp clears and re-locks inside one transaction, so a failed restamp keeps the previous index. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): drop pre-relock Stalker search pages and fail closed on a corrupt lock store - A Stalker search page issued before a relock was filtered with the pre-relock withheld set and could still be applied after it; the staleness check now includes the parental lock version. - A lock store payload that does not parse or is not an object is a failed read (everything withheld until it reads again), no longer an empty store. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): close the startup, re-stamp, relock-refresh and switch-persistence gaps - The window before the initial lock store read settles now withholds everything, like an unreadable store: settings can report the feature as on before the locks are known. - The store commits before the SQLite index re-stamp; a failed re-stamp now rolls the store back, a failed rollback re-stamps on the next access, and every launch re-derives the index from the store. - Xtream category/content reloads fail closed: a rejected reload empties the affected lists (content types drop back to idle) instead of keeping rows read under the previous lock state. - Enabling/disabling the feature persists through one guarded path that undoes the in-memory switch and skips the Electron mirror on a failed settings write. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(xtream): move the parental-lock reload specs beside the content spec The content feature spec sits at the 1200-line spec cap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): await the startup lock-index reconciliation and withhold genre-less rows when failing closed - The lock store is readable only once the SQLite index has been re-derived from it, and a re-stamp that keeps failing keeps the session fail-closed, so catalog reads can never serve rows stamped unlocked by a stale index. - While everything is withheld, Stalker rows without a genre are withheld as well (the store filter and the renderer predicate). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): await the enablement mirror, restore partial lock stamps and validate nested lock-store entries - The Electron mirror of the feature switch is awaited; a mirror that cannot be written undoes the settings write, so a reload never starts from a mirror that disagrees with the persisted switch. - A failed multi-type re-stamp rolls the store back AND re-stamps every touched type from it, since earlier types may already carry the new locks; a failed rollback keeps the playlist stale (fail-closed). - A persisted lock store whose nested entries are not what writeLocks produces is a failed read, not an empty store. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): withhold the Xtream catalog at relock time, keep exact M3U titles in backups, roll back a failed relock-timeout save - A relock now fails closed immediately: the selected detail is stepped off against the lock store, the catalog lists and stored search results are emptied, and the filtered reloads publish only while the captured lock version is still current. - Backups carry M3U lock titles verbatim (exact dedup), since the locks match group titles exactly. - A relock-timeout write that fails reverts the in-memory value and shows the settings save-failure snackbar. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): clear the lock index before a playlist's last lock leaves the store, retry failed PIN reads, guard backups on the lock store - A write that removes a playlist's last lock clears the SQLite index first and drops the store key afterwards, so an interruption between the two can only leave a state the startup reconcile repairs toward locked. - A PIN hash read failure is distinct from an absent PIN: the session stays locked and every PIN-protected step re-reads it first. - Backup export awaits parental lock initialization and refuses to run while the lock store is not readable, since an absent lock field means "no opinion" on restore. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): withhold Electron Xtream reads while locks are unknown, persist the switch when settings are unreadable, re-stamp after a recovered read - ElectronXtreamDataSource serves no categories, content or search hits while the lock store withholds everything; its SQLite index may still carry a stale stamp. - setupPin decides whether to persist the switch from the settings value before the PIN is stored, since enabled follows hasPin while the switch is unknown. - A lock store recovered by a later read marks its playlists stale so the index is re-derived, a persisted entry must carry all three lists, and a stale Stalker search page is dropped before touching the withheld-id bookkeeping. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): keep unlocked category routes reachable and defer a relock reload that overtakes the initial hydration - The Xtream category guard no longer runs the item check on category-only routes (Number(null) is 0), which prompted for the PIN on every unlocked VOD and series category while the lock was active. - A lock change during the initial Xtream hydration withholds the rows the hydration publishes and runs the filtered reload once it has settled, on every path that marks the content initialized. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): resolve hidden live categories before relocking playback and reload categories in the deferred hydration path - The Xtream live layout resolves a playing channel's category through the unfiltered rows when the visible list lacks it (search can play a hidden category's channel); until that lookup lands the category is unknown and a relock stops the channel. - A relock that overtakes the initial hydration now withholds the category publications too and reloads categories with the content. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): step off the M3U channel and Stalker selection before awaiting the Xtream relock reload The Xtream store stays populated after leaving that portal, so its reload runs on every apply; a locked M3U channel no longer keeps playing behind a slow database or provider read. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): gate the workspace on parental lock init, edit only a readable lock store, guard the deferred reload, validate backup lock entries - The workspace route resolver awaits ParentalLockService.initialize() next to the settings load, so no route or catalog activates before the PIN and lock store are known. - Every lock write re-reads a failed store before building its edit, so a recovered store is edited rather than overwritten. - The deferred hydration reload runs under the publish guard of the request that deferred it. - Backup import validates every parental lock entry and rejects a damaged list instead of erasing the persisted locks on restore. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): discard stale hidden-category lookups and key withheld Stalker rows by their real identity - A hidden-category lookup that lands after a later playback (same provider id, another playlist) no longer overwrites the newer channel's category; resolutions are generation- and playlist-checked. - Withheld Stalker rows are keyed by id, stream_id, movie_id, series_id or the row's cmd/name, so id-less rows no longer collapse onto one key and stall paging past locked pages. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): gate the Electron cached category/content reads while locks are unknown The warm-route hydration reads the cache directly; it now returns nothing while the lock store withholds everything, like the live reads. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): retire in-flight searches on relock clearing and publish lock revisions after the stamps - clearSearchResults() advances the search request version, so a search issued under the previous lock state cannot republish what a relock just cleared. - A lock write publishes its store revision only once every touched type is stamped, so a reload triggered by it cannot read a later type through its old stamps. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): retire a resolving Stalker live playback when the session relocks The embedded player defers selecting the channel until its stream resolves, so the enforcement service's cleared selection could not retire the request; it now carries the lock version it was issued under and is dropped when a relock happened meanwhile. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(settings): one lock entry point per rail plus a right-click Lock/Unlock - Stalker's dedicated lock button becomes the same "Manage categories" (tune) button the Xtream rail has; it opens the lock-only dialog, so every portal type shares one entry point and the rail header keeps three actions. - Right-clicking a category (Xtream, Stalker) or an M3U group offers a single-row Lock / Unlock through the shared CategoryLockMenuComponent, behind the same PIN gate and lock store as the dialog. - The settings hint explains where locks are set; group lock strings added to all locales (ru/de translated). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): serialize lock-store writes and drop a deleted playlist's locks - Lock-store mutations run through one write queue: each rewrites the whole persisted store, so overlapping edits could otherwise snapshot the same store and the later write would drop the earlier edit. - Deleting a playlist removes its locks through the PLAYLIST_DELETE_CLEANUP hook; "Remove all playlists" clears the lock store once the deletion has succeeded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): apply single-row lock toggles inside the lock store's write queue The right-click Lock/Unlock (portal categories and M3U groups) built the new list before entering the queue, so two quick toggles shared one snapshot and the second dropped the first. Lock writes now accept an edit of the current list, evaluated inside the queue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): retry a failed settings read before any parental-lock settings write updateSettings writes the whole settings object, which after a failed startup read is the defaults; enabling the lock or changing the relock timeout then replaced the user's persisted preferences. The read is retried first and the write refused while settings stay unreadable. The settings writes move to parental-lock-settings-writer.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): show the locked-groups row on the M3U rail and roll the relock timeout back to the recovered value - The M3U groups rail now renders the same "N locked · Enter PIN to show" row as the portal category rail, so locked groups no longer vanish without an in-context unlock. - A failed relock-timeout write rolls back to the value read after the settings retry, not to the pre-retry default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): retry a failed clear-all of the lock store and keep restored new playlists free of stale locks A lock-store clear that failed after "Remove all playlists" only logged, so a later restore reusing a playlist id could inherit the deleted playlist's locks. The in-memory store now empties at once and the persisted clear is retried on the next access; a restore that creates a playlist starts it from empty locks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): count radio playback as lock activity and read the lock store before a restore's stale-id check - The idle relock no longer interrupts a playing radio station: playing <audio> counts as activity, like video. - A restore retries a failed lock-store read before checking a reused id for stale locks, and aborts while the store stays unreadable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): drop withheld Stalker search rows at relock time and keep the M3U unlock row when every group is locked - A relock during a page-1 Stalker search now filters the rows already on screen at once, so old unlocked results are not clickable while the replacement page is pending. - When every M3U group is locked the groups rail still renders, with its "N locked · Enter PIN to show" row, instead of the plain empty state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(settings): keep the PIN dialog and the Stalker enforcement step off the initial path Master (#1712) moved the UI component barrel and the Stalker data layer out of main.js and tightened the initial budget to 2 MB. The parental-lock prompt imported the PIN dialog through the ui/components barrel and the enforcement service injected the Stalker store at startup, which pulled both back in (2.55 MB, over budget). The PIN dialog now loads through a local lazy file on the first prompt, and the Stalker step loads only while a Stalker route is open: initial total 1.65 MB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): restore the lock index when an emptying write fails and publish rollbacks after re-stamping - Removing a playlist's last lock clears the SQLite index first; if the clear or the store write then fails, the index is re-stamped from the previous locks at once. Title matching and multi-source discovery query the worker directly and trust the index, so the stale flag alone did not protect them. - A rollback publishes its store revision only after every type is re-stamped, so a reload cannot read a later type through the attempted stamps. - docs: restore the index rules the earlier surfaces rewrite dropped from the contract, now in the Lock store lifetime section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): keep the M3U groups view when every group is locked With every group locked the filtered channel list is empty, so the container showed its generic empty state and the groups rail's "N locked · Enter PIN to show" row never appeared. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed when the lazy Stalker enforcement step cannot load A rejected chunk (e.g. a stale PWA page after a deployment) escaped applyStalker(), so a locked Stalker selection kept playing after a relock and the Xtream step was skipped. The step now leaves the Stalker route on a load failure, which clears the selection and stops playback, and the Xtream step still runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): defer a stale Xtream hydration as soon as the catalog is withheld On Electron a relock that overtook the initial content hydration waited for the category reload before the content reload set the deferral flag; the older unlocked hydration could publish its streams in that window. withholdCatalog() now sets the flag itself, before anything is awaited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): restore backup locks after the Xtream merge and snapshot the Stalker lock dialog's categories - A backup restore now writes the parental locks last, so a failed Xtream merge leaves the playlist's previous locks in place instead of the backup's possibly smaller set. - The Stalker lock dialog snapshots the category list before its lazy import and opens only if the route is unchanged, so another portal's categories can never be saved under this playlist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed on relock ahead of the apply queue and judge Xtream selections by the lock store - On relock the synchronous fail-closed steps (M3U channel, Stalker selection, the locked Xtream detail, catalog lists, stored search) run immediately instead of queueing behind an earlier apply that may still wait on a slow or hung read. - The post-reload Xtream checks decide by the lock store through the unfiltered category rows rather than by absence from the reloaded list, which also omits merely hidden categories; unreadable rows fail closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): require the PIN for lock-bearing backup restores and fail closed during index re-stamps - A backup carrying lock lists replaces the matching playlists' locks, possibly with an emptier set; the import now asks for the PIN (after the file was chosen) and aborts when it is refused. - While a write re-stamps the SQLite index the playlist counts as stale, so a relock inside that window reloads fail-closed instead of through the old stamps. The internal store write now needs only a readable store, so a rollback can still land. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): drop parental-lock Xtream reloads once the playlist is switched A reload issued for playlist A no longer publishes into the shared Xtream store after the user opened playlist B: the store's reloads guard on the playlist they read for, and the enforcement apply retires its search refresh and selection checks on a playlist switch as on a newer lock version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): re-ask the PIN before a relocked backup merge and keep the PIN cooldown across prompts A backup merge now asks for the PIN again right before it replaces a playlist's locks when the app relocked during the import, instead of relying on the answer given at the start. The wrong-PIN count and the 30-second pause move from the dialog into the lock service, so dismissing and reopening the prompt no longer resets them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): refuse lock removals that commit after a relock and keep the index stale until the store write lands Lock edits that take a lock away now commit only while the session is unlocked, checked inside the write queue at commit time, so an editor save still in flight (or queued) when the app relocks cannot remove locks. Adding locks stays allowed. The Xtream category dialog drops its lock draft after a relock, and a backup restore re-asks the PIN only when it would remove a lock. Clearing a playlist's last lock keeps its index stale until the store write has landed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): clear an Xtream detail from a hidden category synchronously on relock The synchronous relock step now clears a selected Xtream item whose category the visible category list cannot place (a manually hidden category opened through search), instead of leaving it usable until the awaited reloads and lookup finish. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed when the Electron bridge lacks the parental lock worker filter A new runtime capability requires the lock-state and index-stamping IPC. When Electron reads Xtream through the SQLite worker without it (a partial or older preload), the locked session withholds every category instead of trusting a worker that never learned the lock state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): re-check lock removals at their durable commit points A lock removal that passed the unlocked check before its write is asked again right after the store write and, for Xtream, after the index stamps. A relock in between writes the previous store back or rolls the stamps back before anything is published. The stale-index bookkeeping, index stamping and store merge move into helpers to keep the lock store within the file size limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): retry a failed revert of a refused lock removal and fail closed meanwhile When writing the previous store back after a relock-refused removal fails, the lock store now keeps a pending rewrite, is not readable (the locked session withholds everything) and rewrites the persisted store from memory on the next access, so a restart cannot load the removal. A failed "Remove all playlists" clear shares the same retry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): authorize lock removals when issued and close genre-less Stalker details in fail-closed mode A lock removal is now authorized right before its first write is issued; a relock that lands after that is ordered after the write, which completes. This drops the post-write rollback, whose own failure could leave the persisted store diverged from memory across a restart. The Stalker search closes a detail without a genre on relock while every category is withheld. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): restore the previous locks when an Xtream rollback write fails When an Xtream lock edit's re-stamp fails and the rollback store write fails too, memory now goes back to the previous locks and a pending rewrite persists them on the next store access before the index is re-stamped, so the failed edit cannot take effect through that re-stamp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(stalker): cover page-one rows leaving the screen on relock while the reload hangs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): offer the portal unlock row in fail-closed mode When the lock store cannot be read every portal category is withheld but no locked ids are known, so the rail showed no "Enter PIN to show" row. It now shows the row without a count in that state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed for direct worker title lookups and capture the Stalker lock dialog context before the PIN Catalog title matching and multi-source discovery query the SQLite worker directly; they now return nothing while the parental lock withholds everything (unreadable store or a bridge without the worker filter). The Stalker lock dialog captures its playlist, provider and section before the PIN prompt and re-checks them after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): retire multi-source alternatives on a lock change and keep reconcile off in-flight stamps The VOD multi-source host keys its discovery session to the parental lock version: a lock change drops the discovered sources, retires discoveries and switches in flight, and rediscovers through the worker's new lock state. Stale-index entries of a write still stamping are no longer retried by a concurrent reconcile, which could re-stamp from a store the write had not committed yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed on a rejected worker lock sync, tear down Stalker synchronously and capture the Xtream dialog context before the PIN - A rejected lock-state sync to the SQLite worker makes the locked session withhold everything until a later sync succeeds. - The Stalker enforcement chunk is preloaded when a Stalker route opens; a relock runs it synchronously, or leaves the route at once while it is not loaded, instead of awaiting the chunk. - Xtream "Manage categories" captures playlist, provider and section before the PIN prompt and re-checks them after it and after the dialog import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): keep the relock timeout behind the PIN and bind M3U group lock toggles to their playlist A locked session can no longer change the relock timeout: the Settings selector is disabled until the PIN is entered and the service refuses the change while locked. An M3U right-click lock toggle now captures its playlist before the PIN prompt and is saved only if that playlist is still open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): reset a locked M3U channel however it becomes active The enforcement service now checks the active M3U channel whenever it changes while locked, so numeric zapping, next/previous and remote commands, which select from the full channel list, cannot start a channel of a locked group. Numeric zapping also skips such a channel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): bind the M3U group management result to its playlist The groups view captures the playlist before the PIN prompt and drops the management dialog's hidden and locked group lists once another playlist is open, so they cannot be saved under that playlist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(parental-lock): record the accepted restart case of a failed rollback write Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): build bulk lock drafts only from a readable lock store The Xtream and M3U management dialogs offer lock toggles, and the Stalker lock dialog opens, only once the lock store has been read. A draft built from the empty fail-closed snapshot would otherwise replace the real locks with nothing on Save if storage recovered in between. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): keep the parental lock switch on the saved state and roll back to the recovered value The Settings switch snaps back to the saved state when clicked and follows it once the PIN action succeeds, so a cancelled or refused PIN no longer leaves it showing the opposite state. A failed switch write is undone to the value read after the settings retry instead of the hard-coded inverse. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): match noncanonical PWA Xtream category ids against their locks The PWA data source compared raw provider category ids such as "009" with locks stored as numbers, so such a category stayed visible while locked. Both sides are now compared in canonical numeric form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): close the M3U group editor on any relock The group management dialog lists every group name, locked ones included, even when it opened without lock toggles (unreadable lock store). It now closes on any relock, and the groups view re-checks the lock state before opening it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
f0e51d2806 |
perf(web): keep lazy-only services and SafePipe out of main.js (#1729)
* perf(web): keep lazy-only services and SafePipe out of main.js The eager shell imported barrels that re-export Angular injectables and a pipe it never uses, and their static definitions keep those modules in main.js: PlaylistFileImportService came with PlaylistContextFacade, normalizeDateLocale with SafePipe, and the workspace-shell-util barrel with SettingsContextService, which #1714 grew with match counts. That growth put master 108 bytes over the renderer.initialBytes baseline #1712 had measured on a branch without #1714. Add file-level entries for the three modules and use them from the eager and settings code: renderer.initialBytes 1,626,127 -> 1,619,993 bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(performance): lower the initial-bytes baseline to 1,619,993 bytes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ea51cae3db |
feat(settings): search settings from the header and the command palette (#1714)
* feat(settings): search settings from the header and the command palette The header search on the Settings page was shown but disabled. It now searches a shared index of all 56 settings rows by translated title, description and English synonyms, replaces the section page with ranked results, and opens a result by scrolling to, focusing and briefly highlighting its row. Enter opens the best match, and the section navigation shows per-section match counts. The command palette gains a "Settings" group that lists the best six matches for a non-empty query, so any setting is one Ctrl/Cmd+K away. Rows hidden by the current form state fall back to the control that reveals them; rows the runtime cannot render are never returned. The index ships through a new @iptvnator/workspace/shell/util/settings-search sub-entrypoint so it stays out of the eager bundle, and a registry spec keeps it in step with the section templates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): let search reveals win over pending input and gate embedded MPV rows - A reveal (result click, command palette, Enter) now cancels a search keystroke still waiting for its debounce, so its q navigation can no longer supersede the reveal and leave the results open. - Embedded MPV extra options and auto-reconnect require a lazily probed embedded MPV capability; frame copy also needs frameCopyAvailable, so search never offers a row the settings page cannot render. - Keyboard users keep a focus-visible ring on the revealed row after the highlight fades. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(workspace): wait for palette probes without Promise.allSettled The web tsconfig lib predates Promise.allSettled; use Promise.all over rejection-safe probes instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8ebb7e3424 |
perf(ci): run Tier A coverage concurrently with isolatedModules ts-jest (#1701)
Tier A coverage runs projects a few at a time (largest first, bounded Jest workers, buffered output, fail-fast kept) and ts-jest transpiles with isolatedModules instead of type-checking per process; five type re-exports become export type, two decorated inputs use import type. Unit Tests and Typechecks job: 26 min -> 9 min (Tier A step 23 min -> 6.5 min). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e619a2be86 | fix(xtream): scroll to the selected category (#1663) | ||
|
|
8bc877b625 |
chore(performance): measure initial bytes of the built web app (#1692)
First step of the performance-journeys ratchet (plan thread: J1 `launch`, counter `renderer.initialBytes`).
- `tools/performance/measure-initial-bytes.mjs` reads the built `dist/apps/web/index.html` and sums `index.html` plus every same-origin `<script src>`, `<link rel="stylesheet">` and `<link rel="modulepreload">` it references. Manifest, icons, external URLs and lazy chunks are not counted. A referenced file missing from the build fails the measurement instead of counting as zero bytes.
- `--json` prints the breakdown; `--summary <file>` writes the `journeys.<journey>.counters` shape a ratchet checker will consume (next PR).
- New Nx project `performance-tools` (test + lint targets), Tier B in `tools/coverage/coverage-policy.json`, root scripts `perf:initial-bytes` and `perf:tools:test`.
- New contract `docs/architecture/performance-journeys.md`, linked from the validation map, the agent context map and the README.
- **Review follow-ups:** resources are deduplicated by request URL (query kept, fragment dropped); `index.html` is parsed with parse5 (already a repository dependency, scripting enabled), so comments, bogus comments, raw-text bodies (script/style/noscript/title/textarea), inert `<template>` contents and character references in attributes all follow the HTML5 algorithm instead of a hand-written scanner; the review's edge cases stay as regression tests; docs show the `pnpm --silent` form for JSON output and explain how the counter relates to Angular's rounded "Initial total".
- **Found while measuring:** the environment files and the playback diagnostic panel imported the whole `package.json` (`import packageJson from '@package'`), which esbuild cannot tree-shake, so `main.js` carried the complete file and the counter moved with every script or dependency edit. They now import `{ version }` only (
|
||
|
|
eb61d37dc3 |
docs(tmdb): replace catalog titles in matching examples with stand-ins (#1658)
* docs(tmdb): replace catalog titles in matching examples with stand-ins The Cyrillic and Arabic examples that document title folding were taken straight from a user's own portal catalog while the folding bugs were being diagnosed, and they spread from there into comments, fixtures, assertions and the architecture doc. A public repository is not the place for someone's viewing inventory, and the TMDB ids pinned alongside them identify the exact shows. Swap in stand-ins that reproduce the property under test rather than the title: a word whose "й" folds away, one whose "ё" does, a two-word title carrying a season marker, an Arabic phrase whose initial hamza decomposes into a separate word. Every replacement was run through the real `normalizeTitle` and `cleanTitleForSearch` before being written, so the folded keys, the wire queries and the cache lookup keys are the same shape as before — "Лейка" and "Леика" still meet on one key while staying two different searches, and "AR| أمثلة تجريبية" still keys as a hamza split into a space. Real TMDB ids become synthetic ones. One channel fixture that read as a film title becomes a plain channel name. Deliberately left alone: the leading-tag rule's "Akira | 1988" / "Момо | Momo" examples in `title-normalization.util.ts`. Those are not an inventory — they are the evidence for a regex decision measured over 1.27M catalog titles, and inventing replacements would document a measurement that never happened. No release note: comments, fixtures and docs only, with no behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(tmdb): label the folding stand-ins as illustrative, not as history Review caught that the substitution left synthetic titles inside sentences that assert observed fact: the architecture doc claimed a particular folded query "returns zero results while `Лейка` returns the show" and named the stand-ins as the titles that were searched, missed and negatively cached, and several comments read the same way. The titles never existed, so the reading is wrong in the one direction that matters — a future reader debugging a regression would take them for production evidence. Keep the claim that is actually established, which is a class-level one: under the old single-form design every Cyrillic title carrying "й"/"ё" and every Arabic title carrying a hamza form was searched folded, missed, and cached as missing for the negative TTL. Present the strings themselves as illustrative stand-ins chosen to fold the same way. The verified invariant — what NFD does to those letters, and what the two tiers therefore produce — is unchanged and still assertable, because it is a property of the text rather than of any title. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(tmdb): finish the sweep — a missed cache key and a last observed-fact claim Two spots the first pass left behind: - `tmdb-search-cache-cleanup.spec.ts` kept a catalog-derived lookup key verbatim. Only the TMDB id beside it had been swapped, because the sweep matched the title in its display casing and the key stores it lowercased — so one item of the inventory stayed in the repository, twice. - `SearchTitleVariant`'s doc comment still asserted that one specific folded string finds nothing on TMDB. State the mechanism instead: folding rewrites the letters the search matches on, so the folded form finds nothing there. `content-search.util.sqlite.spec.ts` gets a channel fixture that no longer reads as a film title. `search-text-fold.util.spec.ts` is deliberately left alone. Its "Ёлки" is a common noun sitting in a Unicode corpus beside "Amélie", "İnşaat" and "Ά", not an inventory entry — and its rows are precomposed/decomposed PAIRS (U+0401 against U+0415 U+0308). A textual substitution rewrites only the composed half and silently turns the pair into two different words; doing it failed that spec, which is what a fixture encoding an invisible property is for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(search): finish the fold fixtures by rewriting both halves of the pair The last two occurrences lived in the Unicode fold corpus, which an earlier attempt left alone after a textual substitution failed the spec. The reason it failed is the point: one row is a precomposed/decomposed PAIR — U+0401 against U+0415 U+0308 — asserting that both spellings fold to one string. Replacing the visible text rewrites only the composed half and silently turns the pair into two different words, which is not a thing a reader or a regex can see. Rewrite both halves by code point instead, keeping the decomposed half decomposed: U+0415 U+0308 + the new stem. Verified by decoding every quoted string in the file afterwards, and the spec passes (534/534). A repository-wide sweep now finds no occurrence of the replaced titles in either normalization form. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b02d79805b |
fix(tmdb): a new series no longer matches its older, better-known namesake (#1648)
Metadata is looked up in the app's own language, so an unrelated older foreign series can come back under exactly the same localized name as a recent local-language one. `pickConfidentMatch` admitted the older row through the series "premiered earlier" tolerance — portals report the running season's year while TMDB reports the premiere — and then let `pickMostPopular` decide across every admitted candidate, discarding the year evidence that had just admitted them. The better-known show won on votes, and the newer series rendered its poster, cast, genres and rating. Rank admitted candidates by year evidence first (`yearEvidenceTier`: the provider's exact year, then a year off by one, then the series tolerance) and let popularity break ties only inside the strongest tier any candidate reached. The tolerance stays — three of eight real lookups from one install depend on it — but it is a last resort, not an equal. Measured over 400 Cyrillic series titles sampled from a real catalog, 20 normalized keys had a same-titled older series and 16 of those were the more popular row. The mirror case is accepted knowingly: a long-running show whose stated season year happens to BE another same-titled show's premiere year now resolves to the newer show. Only the older show's season air dates could separate the two and a search response does not carry them, while that shape needs three coincidences at once against one that needs none. Search cache keys move to `|v4` with a matching startup cleanup, because a positive row naming the wrong show stays fresh for 30 days. Merged with `Build on windows x64` red: the checked-in Windows Embedded MPV runtime pin points at an upstream release whose retention expired, so that job fails repository-wide on a cold cache. Unrelated to this change; tracked separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
42b41ebabc |
fix(epg): advance the Xtream channel list's current programme as time passes (#1647)
The "current programme" under each Xtream Live TV row was written only on
scroll-into-view, on a new EPG result, or on an EPG offset change. Nothing
re-evaluated it as wall-clock time passed, so once a programme ended the row
stayed on it until the category was left and re-entered. The progress bar
under the row never moved either.
The rows on screen now re-check themselves once a minute. A programme still on
air only has its progress bar advanced, at no request cost; once it ends the
row is re-picked, and only a programme on air or upcoming may replace it, so a
guide that has run out can never walk the row backwards. An exhausted cache is
dropped and refetched at most once per cache lifetime per channel, while an
empty answer from the provider is left alone. What is on screen stays there
until a replacement arrives, so a refreshing row never blanks out.
A programme occupies [start, stop) in every comparison, the visible slice is
read from the viewport rather than remembered, and the two services behind
this are root-provided because a live layout mounts the channel list more than
once over a single EPG queue: EpgRefillLimiter is the floor on refetching an
exhausted guide, keyed by playlist since stream ids are provider-local, and
EpgRefreshCoordinator owns the one timer and merges every mounted list's
request, because the queue is latest-wins and separate timers would cancel
each other on exactly the boundaries that matter.
Contract: docs/architecture/m3u-playlist-module.md ("Xtream channel-row
programme refresh").
Fixes #767. Supersedes #1610.
Co-Authored-By: Justin Willhite <5132924+thejdubb02@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f80a21beef |
feat(collections): open a Stalker live favorite inside its portal (#1639)
Live channels in Favorites and Recently viewed now offer "Open in <playlist>" for Stalker portals, the counterpart of the VOD "View in portal" action. The chip in the programme panel and the channel's context menu jump to the channel inside its portal's Live TV, with its genre selected and the channel playing. Radio stays hidden: it is a separate legacy-paged section with no open-on-arrival contract. The handoff refuses to guess. The store records which genre the rendered ITV list belongs to, so the deferred play waits for a list that can actually serve the channel instead of inferring it from array identity. An id claimed by one channel as its `id` and another as its `stream_id` is ambiguous, so neither is played and the user still lands in the right genre. The handoff is abandoned when the user changes genre, search, portal or section first. Two pre-existing defects surfaced during review and are fixed here: - A blank provider id shadowed a valid one. `a ?? b` keeps an empty string, so a channel with a blank `stream_id` was stored with no identity at all and could not be selected, played or found again. Six writers had that shape and three private copies of the skip-the-blank rule; all now go through one `firstNonBlankStalkerId` helper. - Route-session readiness could publish before the store held the portal's row. The constructor starts one sync and the first navigation starts another, and the second skipped the bootstrap because the playlist id was claimed before the awaits that install it. Arrivals are now serialized, the id is claimed only after the store write resolves, and only the newest sync publishes readiness. Both fixes carry regression tests that fail on the old behavior. |
||
|
|
6578e4073c |
feat(dashboard): portal EPG on the live rails, loaded lazily per visible card (#1638)
Xtream and Stalker live cards on the dashboard carried no XMLTV key, so the rails never asked anything for them and showed only the LIVE chip. Their programme now comes from the portal, one card at a time and only once the card is on screen. - `lib-dashboard-rail` reports the cards inside its viewport through an IntersectionObserver rooted at the track; `DashboardLiveEpgPresenter` unions them with the pinned hero row and hands the set to `DashboardPortalLiveEpgPresenter`. - `DashboardPortalLiveEpgService` runs the bounded queue — two requests in flight, 200 ms apart, one card each — through `StreamResolverService.loadEpgForItems`, publishing every answer the moment it lands, so the page never waits and a slow portal delays no other card. A card scrolled past before its turn is never requested. - A programme is trusted for 60 s, an empty answer for 30 s (the resolver reports a dead portal and a guide-less channel identically), and a completion captures both the display offset and the EPG source revision, requeueing itself when either moved. - The presenter hands its wanted set back on destroy. Desktop only: the shared collection resolver is gated on the local XMLTV bridge. - A shimmer placeholder shows only before a card's first portal answer; M3U cards keep the batched XMLTV lookup. |
||
|
|
4e575a810e |
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> |
||
|
|
954c8ad65e | fix(dashboard): live rails find XMLTV programmes outside the global EPG sources (#1637) | ||
|
|
b30c783e85 |
fix(search): locale-invariant Turkish case folding in every search path (#1640)
Turkish upper and lower case queries now return the same results everywhere a title can be searched. Lower-casing the dotted capital "İ" (U+0130) leaves a combining dot behind, so "İnş" and "inş" reached different search arms and different results. - Case folding is locale-invariant: `toLowerCase()`, never `toLocaleLowerCase()`, which under a Turkish or Azeri OS locale maps ASCII "I" to the dotless "ı". - The Electron content search composes to NFC and drops the leftover combining marks before tokenizing, and its LIKE/GLOB pattern builders additionally spell the `'tr'`-locale İ forms, since SQLite LIKE folds only ASCII. - A shared `foldSearchText` covers every in-memory filter: channel lists, the Xtream and Stalker catalogs, category filters, collections, the EPG guide, the command palette, sources, the playlist switcher and the download lists. - Composing before the strip keeps canonically equivalent spellings equal while the fold stays accent-sensitive; the Turkish I/ı pair is deliberately left alone, as the FTS index does not fold it either. Covered by a SQLite-backed spec over the real trigram index plus regression cases in the affected renderer specs. Closes #609. Co-Authored-By: Justin Willhite <5132924+thejdubb02@users.noreply.github.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
975b1f7f74 |
refactor(collections): split the unified collection page and live tab (#1642)
* refactor(collections): split the unified collection page and live tab Both files sat far above the workspace's 400-line hard maximum and were only green because `tools/eslint/max-lines-baseline.mjs` exempted them. No behavior change: every template binding, public signal and handler the components exposed still resolves the same way, and the two baseline entries are gone. `unified-collection-page.component.ts` (966 → 398 lines) keeps the view surface and delegates: - `unified-collection-data.service.ts` — component-provided; owns the rows, the favorite uid set, the loading/reload indicators and every mutation, plus `loadedRequest` (the PR #1636 invariant, now read through one `mutationRequest` computed instead of two ad-hoc fallbacks) - `unified-collection-load.ts` — the reload key and the load effect - `unified-collection-scope.ts` — the This-playlist/All-playlists toggle - `unified-collection-content-type.ts` — the Live/Movies/Series tab - `unified-collection-history.ts` — the `window.history.state` view entry - `unified-collection-detail-state.ts` / `-detail-navigation.ts` — the inline detail and where an item this route cannot render goes instead - `unified-collection-clear-action.ts`, `-labels.ts`, `-favorites-sort.ts` `unified-live-tab.component.ts` (1051 → 426 lines) continues the pattern its siblings already established: - `unified-live-selection.ts` (+ `-selection-generation.ts`) — activating a row while the mounted player stays alive - `unified-live-selection-view.ts` — what the selection implies about the source and the player surface - `unified-live-epg-view.ts`, `unified-live-epg-map.ts` — the EPG panel and the rail's now-playing map - `unified-live-catchup.ts` — the timeshift override - `unified-live-recording-metadata.ts`, `unified-live-channel-rows.ts` Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(collections): move the collection data service into data-access Codex review on PR #1642: `UnifiedCollectionDataService` loads and persists favorites/recent rows, which CLAUDE.md places in `@iptvnator/portal/shared/data-access`, not in the `type:ui` project that renders them — and the dialog-scoped `SourceCleanupService` already sets that precedent for a component-provided stateful collection service. It was also the only non-root `@Injectable()` in `portal-shared-ui`. `collection-reload-indicator.ts` moves with it: `type:data-access` may not depend on `type:ui`, and the indicator is the service's own loading state machine rather than a view. Both are exported from the data-access collection barrel, so the boundary is now lint-enforced instead of conventional. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
15d964ef88 |
fix(collections): non-destructive reload feedback for the scope switch (#1636)
Switching the unified Favorites/Recent page between "This playlist" and "All playlists" kept the old items on screen with no feedback until the new query resolved. The skeleton cannot be reused there: it unmounts a playing channel and drops focus from the toggle. Add a separate reload state that keeps content mounted: after a 180 ms grace period an indeterminate progress bar overlays the header separator, the content region carries aria-busy, and the grid or the live tab's channel rail dims — the player never dims. Fast IndexedDB/SQLite answers show nothing. Only the latest request settles the state. Also bind Clear and drag reorder to the request that loaded the rows still on screen (`loadedRequest`) instead of `effectiveScope()`, which moves ahead the moment the toggle is clicked. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
033a08cad9 |
feat(collections): open a favorite or recent live channel inside its playlist (#1634)
* fix(xtream): keep a live-channel handoff alive until its playlist catalog is loaded Arriving at /workspace/xtreams/:id/live from another route with openXtreamLiveItemId in history state silently did nothing: the Xtream shell mounts the live layout after its session bootstrap, i.e. after the arrival's NavigationEnd, so the layout's NavigationEnd subscription never saw it. When the layout was reused instead (playlist switch), the shared store still held the previous playlist's catalog at NavigationEnd and the "not in liveStreams" verdict dropped the pending id. Read the state once at mount as well, carry openXtreamLivePlaylistId in the navigation state, and treat a miss as final only once currentPlaylist is the requested playlist and isContentInitialized is true. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(collections): open a favorite or recent live channel inside its playlist Live channels watched from Favorites / Recently viewed had no visible playlist and no way to jump there, unlike movies and series with "View in portal". Add the live counterpart: - getLiveCollectionPlaylistNavigation() resolves the channel inside its playlist (Xtream via the live layout's auto-open state, M3U via openM3uChannelUrl on the player's all view); Stalker resolves to null until its ITV layout gets an open-on-arrival contract, so nothing is shown there instead of landing on the section root. - app-open-in-playlist-chip, projected into the EPG timeline / list-view toolbar through a new [epgToolbarAction] slot beside the channel name, visible in the collapsed state too. - "Open in <playlist>" entry in the channel row context menu, which also covers radio rows and rows that are not playing. Both label with playlistDisplayLabel and reuse PORTALS.VIEW_IN_PORTAL_TOOLTIP. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(xtream): only match an auto-open channel against the requested playlist's catalog Review finding (Greptile/Codex P1): the playlist check ran only on a miss, so a colliding provider-local xtream_id in the previous playlist's catalog was accepted, played the wrong channel and consumed the handoff. Check the playlist before consulting the catalog at all. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
4cce4acaad |
feat(portal): season thumbnails in the season dropdown + PR #1628 follow-ups (#1633)
* feat(portal): season thumbnails in the season dropdown + PR #1628 follow-ups Follow-ups to the season posters shipped in #1628: - The >6-seasons dropdown (`SeasonTabsComponent`) now carries a 28×42 season thumbnail at the start of each menu row that has a poster and in the closed trigger for the selected season, fed by a new `seasonPosters` input from the season container and the fullscreen episode panel. Rows without a poster get no placeholder, a failed image is dropped, and the pill row stays text-only as the design review decided. - The fullscreen season strip's episode count uses its own `PORTALS.EPISODE_COUNT_ONE/OTHER` keys instead of borrowing the download manager's; all 18 locales filled through the i18n merger from their existing `DOWNLOADS.EPISODE_COUNT_*` translations. - The Stalker mock's serve targets no longer pin `PORT` (an nx:run-commands `env` entry overrides the shell), and `main.ts` resolves `PORT`, then the Playwright-side `MOCK_PORT` alias, then 3210 — so `MOCK_PORT=3310` now relocates the whole E2E run. The Xtream mock honours `XTREAM_MOCK_PORT` the same way. - `resolveAutoSelectedSeason` gets a direct spec covering every branch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(xtream-mock): mint marketing asset URLs on the port the server bound Greptile P1 on #1633: the listener honoured `XTREAM_MOCK_PORT`, but `marketingAssetOrigin()` still read `PORT` alone, so a run relocated only through the alias sent every poster/backdrop/logo/episode URL to 3211. One resolver (`resolveXtreamMockPortString` in `mock-port.ts`: `PORT`, then `XTREAM_MOCK_PORT`, then 3211) now feeds the environment parser, the marketing asset origin and the demo-guide origin fallback. A spec pins the precedence and that `marketingAssetUrl` follows the bound port. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
01c423ac43 |
fix(portals): stop treating a slow panel as a dead host; IPv4 fallback budget in Electron (#1621)
* fix(portals): apply the IPv6->IPv4 fallback budget in the Electron process The 2500 ms happy-eyeballs attempt timeout from #1404 only ever ran in the web backend. The Electron main process and its playlist-refresh and EPG workers kept Node's 250 ms default, so a dual-stack panel hostname behind a VPN or a slow link failed every connection attempt in a row and tripped the host connectivity guard. The module now lives in `@iptvnator/shared/host-health` and every Node isolate that opens connections applies it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): stop treating a slow panel as a dead one in the host guard axios raises the same ECONNABORTED whether the SYN went unanswered or the panel accepted the connection and then thought for longer than the request budget. Two such timeouts opened the breaker and every request to the panel was refused for 30 s with "portal is not responding" — the shape behind the "connection keeps dropping" reports on 0.23 and nightly. Both transports now report whether the TCP connection was established (`onConnect`: Electron through a per-request observed agent instead of the shared keep-alive globalAgent, the web backend through the transport that owns the ClientRequest), and `classifyHostRequestFailure(error, { connected })` downgrades a host-level code observed after the handshake to inconclusive. Redirect attribution keeps precedence. A host that never accepts the connection trips the guard exactly as before. The Xtream mock gains a `silent:silent` scenario whose detail actions accept and never answer, plus a real-socket regression spec for the guard. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): let an accepted connection clear the host-failure streak Review finding: an unanswered SYN, then an accepted-but-slow timeout, then another unanswered SYN still reached the two-failure threshold, because the middle request was merely not counted. An accepted TCP connection is the reachability the guard measures, so it now reads as `responded` and clears the streak like an HTTP response would. Regression coverage for the mixed sequence on one flapping loopback origin (Electron) and through the proxy route (web backend). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): credit an accepted connection when it happens, not when the request settles Review findings. A request that connected and then hung for 30 s cleared, on its eventual timeout, the failures later requests had recorded while it waited — reopening a host that had just died on evidence older than theirs. The connect hook now reports the connection the moment it fires through a new `HostConnectivityGuard.reportConnected`, which clears the failure streak but closes no open or half-open breaker (the trial keeps its slot until it settles), and the settled timeout is inconclusive. Electron also skips the socket observer while an environment proxy (`http_proxy` / `https_proxy` / `all_proxy`) applies to the request: through a proxy the socket connects to the proxy, whose handshake proves nothing about the portal, so those requests keep the pre-observer behaviour. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): decide the proxy exemption with axios' own resolution Review findings. The hand-rolled environment check ignored `no_proxy`, so a LAN portal exempted from the proxy lost its connect observer and slow requests to it still tripped the breaker; it also read the variables with `??`, letting an empty lowercase one mask a populated uppercase one that axios would honour. The decision now calls `proxy-from-env`'s `getProxyForUrl`, the same pinned package axios' http adapter uses, declared as a direct dependency so the packaged app carries it. The validated-axios spec clears and restores every proxy variable around each case, so a runner that exports a proxy cannot change what the cases prove. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
7522c7689f |
fix(settings): honest update-channel check and fresh release notes (#1631)
* fix(settings): make the update-channel check honest and keep release notes fresh Settings → About mixed two commit models: the channel select applied on Save while "Check again" ran immediately against the still-saved channel, so picking Nightly and checking reported "latest version" for Stable under a select reading Nightly. The status now carries a badge naming the channel the verdict describes; while the select shows an unsaved other channel the verdict is dimmed, a hint names both channels, and the check button becomes "Save and check for <channel> updates", which submits the form — the main process already re-checks when the saved channel changes. A download in flight or finished belongs to the previous channel and keeps the plain check. "What's new" for a nightly published after the app started failed with a raw IPC error: each release catalog is a process-lifetime snapshot and a fully paged list never re-read GitHub. findIndex now reloads the catalog once when a version is missing, and a newly found update drops every catalog. The dialog recognises the not-found rejection through the shared marker text, explains it with the version, and links to the channel's release list; other failures keep their reason under a localized headline. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(updater): serialize readers of one release catalog findIndex may rebuild the shared release array while another reader of the same catalog still holds an index into the old one and dereferences it after paging further. Every reader now runs through the catalog's runExclusive queue (getReleaseNotes and the manual-update check), so a reload can no longer pull the list out from under a navigation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): attribute a kept download to the channel it was found on setChannel keeps a download that is running or finished when the saved channel changes, but status.channel already names the new channel, so the About badge attributed a Stable download to Nightly and the pending hint vanished. Every check now stamps status.verdictChannel with the channel it ran on and setChannel leaves it alone; the badge names that channel, and while it differs from the saved one a hint says the shown update came from the other channel and the saved one has not been checked yet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(updater): move release-notes reads out of AppUpdateService AppUpdateReleaseCatalogs (app-update-release-notes.ts) now owns the per-channel catalogs and both reads the updater performs on them: release notes with previous/next paging and the newest release for the manual-install fallback. The service only delegates, shrinking from 610 to 508 lines instead of growing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(updater): name verdictChannel as the badge's source The About paragraph still said the badge reads status.channel while the paragraph below it and the code use status.verdictChannel. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): show the release list, not the earlier release, after a paging failure A failed Previous/Next keeps the earlier notes for navigation while the body shows the error, so the dialog's action offered the earlier release instead of the channel release list. The error is now checked first. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(updater): do not reload the catalog before falling back to latest A read that falls back to the newest release on a miss (the installed version's notes, e.g. an unpublished local build) paged the whole list twice: findIndex reloaded on the miss before index 0 was selected. The reload is now opt-in per call and off on that path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
7790e68147 |
feat(portal): show each season's own poster beside the season tabs (#1628)
Series detail pages now render the selected season's poster as a season
cover next to the season tabs and description, and the fullscreen episode
panel shows the same poster as a season strip above its tabs.
Resolution is TMDB-first, like the show artwork merge: the lazy season
enrichment stores `/tv/{id}/season/{n}` `poster_path` as a w342 URL in
`tmdb_season_posters` (Xtream) or `StalkerSeriesTmdbSeasonsService.posters()`
(Stalker), under the same write-only-if-changed convergence guard as the
season overview. Xtream falls back to the provider's `seasons[].cover_big`/
`cover` when it is an http(s) URL other than the show poster, because panels
repeat the show poster on every season. Stalker is TMDB-only.
The cover column is not rendered for one-season items, seasons without a
poster, or a failed image, so every fallback is today's markup. It is sized
by a new `--season-cover-width` token (96/120/144px per Settings.coverSize).
The hero poster never follows the season.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
c016c73f86 |
feat(shell): zoom shortcuts on Windows and Linux (#1109) (#1623)
* feat(shell): zoom shortcuts on Windows and Linux (#1109) Cmd/Ctrl and +/−/0 (numpad included) now zoom the app on every platform. Windows/Linux run without a menu (`setMenu(null)`), so the shortcuts are a renderer key binding in `WorkspaceKeyboardShortcutsService` calling a new synchronous, preload-local bridge method `adjustZoomLevel`, which steps the frame-bound temporary level through `webFrame.setZoomLevel` — never a main-process `webContents.setZoomLevel`, whose per-URL entry the app's `file://` path routing resets. Step and limits live in `libs/shared/interfaces` (`stepZoomLevel`: 0.5 per press like Electron's zoomIn/zoomOut roles, clamped to levels −4…6). On macOS the renderer sees the key before the application menu, and `preventDefault()` keeps the menu role from stepping a second time (Electron only performs the menu key equivalent in its unhandled-keyboard-event hook). Persistence is unchanged: the main process still reads the live level back on close, quit and reload. The zoom E2E now drives the real shortcuts (in, out, numpad, reset). Help dialog entries added and translated for all locales; contract updated in docs/architecture/workspace-shell.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(shell): never step a stored out-of-range zoom level against the request A level persisted before the shortcuts existed (the macOS menu roles never clamped, and the store restores any finite level) was clamped BEFORE the step, so the first zoom-in from level 7 rendered smaller. Step from the raw level instead: a press further out leaves an out-of-range level where it is, a press back in lands on the limit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(shell): state the zoom bridge's return contract precisely `adjustZoomLevel` steps by `stepZoomLevel`'s rules; a stored out-of-range level is never moved against the request, so the returned level is not itself guaranteed to be within `ZOOM_LEVEL_MIN..ZOOM_LEVEL_MAX`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(release): add a release note for the zoom shortcuts The Release note gate requires an added `.changes/*.md` for runtime changes; the shortcuts are a user-visible feature of their own, so they get their own note and the persistence note stays about persistence. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
9d3266deb7 |
fix(tmdb): search TMDB with the provider spelling, not the folded key (#1626)
The title search sent the folded comparison key (NFD + strip marks + lowercase) as the TMDB query, so every Russian title with й/ё ("Фейк (10 серий)" → "феик") and every Arabic title with hamza missed and was cached as missing for 7 days. Search candidates now carry a provider-spelled wire query beside the folded comparison key; variants are deduplicated and cached per attempted variant by the lowercased query; the search lookup key moves to |v3 and startup deletes the retired |v2 rows under their own app_state marker. Verified on 1.99M live catalog titles (folded key byte-identical). Real-SQLite cleanup coverage runs inside Electron across skipped, previous, pre-person, fresh and repeated startups.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
c76a901e8d |
fix(ui): keep one Back arrow on detail pages (#1627)
Movie and series detail pages showed two arrow_back controls while the inline player was open: the shell's sticky arrow (added in #1576 so Back survives scrolling) meant "Close player" in watch state, while the now-playing bar carried a second arrow that meant route-level Back — the same icon with two meanings, next to a "Close player" button that duplicated the first. The shell's sticky arrow is now route-level Back in browse and watch alike, and the bar carries no arrow of its own. Closing the player is the bar's "Close player" button and Escape, which still unwinds one level (close, then back). Hosts without a browse Back target (M3U, downloads) render no arrow in either state. Unit specs for the shell and the inline player cover the new contract; the Electron and web E2E helpers that pressed Back from watch are updated, and the M3U flow closes the player through the bar's button. Docs, the mirrored CLAUDE.md/AGENTS.md paragraph and a release note follow the change. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
6f987d79d8 |
fix(shell): reload the packaged renderer back onto its in-app route (#1622)
The packaged renderer is index.html over file:// with path routing, so after in-app navigation the document URL names a path with no file behind it. A main-process reload (macOS View > Reload, DevTools) failed with ERR_FILE_NOT_FOUND and stranded the window on Chromium's error page; a renderer-initiated reload (the settings unsaved-changes guard's confirmed location.reload()) was cancelled by the will-navigate trust check and silently did nothing. Both legs now re-load the packaged index with the route in a restoreRoute query parameter, which main.ts restores with history.replaceState before Angular bootstraps. The did-fail-load recovery is deferred to the error page's dom-ready: a load issued from inside the failure event yields a document that never receives animation frames and never paints. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
fa8ce26991 |
feat(playback): fullscreen episode panel for series playback (#1620)
Series playing in fullscreen get the same slide-in side panel the live channel list has, with season tabs and the episode list: rest the mouse on the left edge, click it, or press C; pick an episode and it plays inline without leaving fullscreen. - Panel contract: `FullscreenChannelPanelHost` gains optional `panelSearchEnabled` and `panelKind`; the template context gains `open`. Pointer/keyboard rules and the four live providers are unchanged. - Series host: `PortalInlinePlayerComponent` provides the token through `createEpisodePanelHost()` and stamps `app-fullscreen-episode-panel` (SeasonTabsComponent over rows with TMDB still or numeral tile, label, runtime, clamped overview, progress, watched check, now-playing marker; playing row centred on open). Episode clicks reuse the Up Next rail's inline path; season tab clicks reach the hosts' `onSeasonSelected` (Xtream TMDB season enrichment, Stalker lazy VOD load with a Retry row after a failed request). - Gates: `Settings.fullscreenChannelPanel` (label now covers both lists in all locales), episode content only, native-view Embedded MPV withheld by the view, external players excluded. - Inline-series e2e moved to `xtream-series-playback.e2e.ts` with shared Xtream helpers in a fixture; adds a fullscreen episode switch through the panel. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
c110519412 |
fix(playback): make the fullscreen channel panel discoverable and calmer to close (#1619)
* fix(playback): make the fullscreen channel panel discoverable and calmer to close The left-edge hot zone was an invisible 28px strip with nothing telling the user where the list lived, so "hovering the left side" rarely reached it while `C` always worked. Mouse movement over the stage now reveals a slim edge hint tab (CSS chevron, fades after 2.5 s idle, lit while the pointer rests in the zone), the zone grows to 40px (48px coarse), and a click or tap on the edge opens at once without the dwell. Closing follows the pointer more honestly: the mouse-leave grace grows from 420 ms to 1 s, and it applies only once the pointer has engaged with the panel, so a `C`-opened list no longer closes while the mouse merely roams over the video under the user's typing. Clicks inside the panel never close it. Found on the way: closing with Escape while the mouse still rests on the edge reopened the panel 160 ms later, because Chromium synthesizes a `pointerenter` on the hot zone the aside slid away from. An explicit close now re-arms the zone only on the next real pointer move. Verified live over CDP on HTML5, Video.js, ArtPlayer and Embedded MPV frame-copy; the hover mechanism itself was sound on every engine. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(playback): open the panel only on a primary click begun on the edge Review follow-up. The hot zone opened on every pointerup, so a drag released over the edge, a right or middle click and a pen barrel button all opened the panel; now pointerdown records the primary pointer and pointerup must match it (a leave or cancel forgets the press). The synthetic pointerenter that follows an explicit close no longer arms the hint either: the edge stays clear until the next real pointer move, which arms it and starts the dwell. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
ab239b5043 |
fix(shell): persist the app zoom level as frame-bound temporary zoom (#1109) (#1617)
* fix(shell): persist and restore the app zoom level (#1109) The app-wide zoom (Cmd/Ctrl and +/-) was never saved, so it reset to the default on every restart. Save the webContents zoom level next to the window bounds on window close and before quit, and reapply it once the renderer finishes loading. webContents zoom is per-host, so the restored level then holds across in-app section navigation (SPA route changes never reload). * fix(shell): restore the zoom level as frame-bound temporary zoom (#1109) Under file:// Chromium keys zoom by the full URL, and the packaged renderer routes with pushState, so a level applied through webContents.setZoomLevel belongs to index.html only: the first resize after a section change snapped the renderer back to the default, and the close handler read the current route's entry (usually 0) over the user's choice. The preload now applies the persisted level with webFrame.setZoomLevel, a temporary zoom bound to the frame that survives in-page navigation and resizes; the main process hands it over through the synchronous WINDOW:GET_ZOOM_LEVEL IPC and writes the live level back on close, before-quit and before every cross-document navigation, since a reload drops the temporary level. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(shell): apply the restored zoom level at DOMContentLoaded (#1109) A webFrame.setZoomLevel at preload start left a hidden window without a first frame on Linux and Windows: ready-to-show never fired, the window never showed, and the renderer got no animation frames, so the startup splash removed in a requestAnimationFrame stayed. macOS was unaffected and CDP-driven tests force frames, which is why only the packaged legacy-migration E2E asserting the splash is gone caught it. Applying once the document is parsed is harmless and still lands before the first Angular paint; ownership of the level now follows the preload's WINDOW:ZOOM_LEVEL_APPLIED acknowledgement instead of the request. The release note no longer advertises Ctrl shortcuts Windows and Linux do not have. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Justin Willhite <5132924+thejdubb02@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |