mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
e9eca1c38674113aa5413c980563077437fe180b
22
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e9eca1c386 |
chore(deps): upgrade Angular to 22.1 and Nx to 23.2 (#1603)
* chore(deps): upgrade Angular to 22.1 and Nx to 23.2 * fix(deps): complete Angular migrations after rebasing on master * fix(ci): use the Node pin for Windows runtime refresh * docs(deps): synchronize the workspace-shell Node requirements |
||
|
|
78587f95ba |
fix(portals): return from a portal handoff without losing the collection view (#1435)
* fix(portals): return from a portal handoff without losing the collection view "View in portal" left `stalkerReturnTo` pointing at the collection URL, and the portal detail's back affordance re-navigated there with `navigateByUrl()`. That starts a stateless history entry, but the collection's active tab, scope and open inline detail live only in `window.history.state` — so back landed on the default `live` tab with the title closed, and the portal page stayed one browser Back away. The handoff now also sets `stalkerReturnByHistory`, and both Stalker back handlers step back a single history entry instead. That entry is the one the handoff itself pushed, so it restores the collection exactly as the user left it and adds nothing to the history stack. The flag is set only by this builder and only alongside `returnTo`, so the dashboard handoff and every other `stalkerReturnTo` caller keeps re-navigating. Reported by Codex on #1422 after it merged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): scope the history-return marker to its own handoff `openStalkerItem` is consumed on arrival, but `stalkerReturnTo` and the new `stalkerReturnByHistory` stay on the history entry, and a Stalker detail opens in place without pushing one. So after Back + browser Forward the same entry can host a different title, whose back affordance would follow the leftover marker out to the collection instead of just closing it. The marker now carries the handed-off item's identity instead of a bare `true`, and a marker that does not match the open title is treated as stale: it suppresses the whole return contract, so back simply closes the detail. Reported by Codex on #1435. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(portals): share the Stalker back-navigation decision Both back handlers duplicated the marker/`returnTo` precedence verbatim, and the catalog view kept its own copy of the identity normalization the marker binding mirrors — two places for one rule to drift. `resolveStalkerBackNavigation()` now owns the decision and both handlers just apply it, while `stalkerItemIdentity()` delegates to the shared `normalizeStalkerHandoffIdentity()`. Behaviour is unchanged; the precedence gains direct unit coverage instead of only being exercised through the two components. Follow-up to Greptile's review notes on #1435. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): make the return marker one-shot and id-shape agnostic Two defects in the marker binding, both reported by Codex on #1435: The comparison identity read only `item.id`, but the marker is built from `extractStalkerItemId()`, which also accepts `stream_id`/`series_id`/ `movie_id`. A collection row carrying only `movie_id` therefore compared against an empty identity, and since a marker was present the handler returned without stepping back or re-navigating — the back affordance simply stopped working. It now follows the same field order. Binding also only fixed a *different* title reopened on the entry: selecting the original title again still matched. Honouring the marker now retires both return keys from the entry, so a browser Forward cannot replay the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): bind the return marker to the id the detail can report The previous attempt widened the comparison to `extractStalkerItemId()`'s field set, but `buildStalkerSelectedVodItem()` — which every opened detail goes through — derives `id` from `id ?? stream_id` and drops `series_id`/`movie_id`. The wider lookup therefore ran after the lossy normalization and still could not match, leaving the back affordance doing nothing for those rows. The marker is now bound to the identity the detail will actually report, and a row whose id cannot survive normalization gets no marker at all: the handoff falls back to re-navigating via `stalkerReturnTo`, which works. The earlier regression tests hid this by injecting a `movie_id`-shaped selection directly, which normalization can never produce; they now build the selection through `buildStalkerSelectedVodItem()`. Reported by Codex on #1435. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): retire the return contract after a native browser Back Leaving the handed-off portal entry with the browser's own Back runs no back affordance, so nothing consumed the marker. A Forward replay then reopened the catalog with the contract intact, and opening the same title again matched the identity — its Back exited to the collection instead of closing the freshly opened detail. `CategoryContentViewComponent` now retires the contract whenever it lands on the entry with no handoff item and no detail open: the handoff is over, so anything opened from the list afterwards is a fresh selection. The guard on an open detail keeps arrival itself from retiring a contract it still needs. Also documents, per Greptile, that a `none` decision still closes the detail and only suppresses the navigation. Reported by Codex on #1435. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(portals): keep alternate-id rows on the history return The previous revision let a row carrying only `movie_id`/`series_id` fall back to re-navigating, which reproduced the exact defect this PR exists to fix: the new entry has no `collectionViewState`, so the collection reopens on its default tab with the title closed. The builder now pins the resolved id onto the handoff state item when the raw row carries neither `id` nor `stream_id`, so `buildStalkerSelectedVodItem()` reports an identity the marker can bind to and those rows get the same history return as every other one. An existing id is never overwritten. Also scopes the arrival-side retirement to handoffs that actually set the marker, so a plain `stalkerReturnTo` caller such as the dashboard keeps its behaviour — I had made that unconditional, which contradicted the scope this PR claims. Reported by Codex on #1435. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(portals): drop a redundant guard in the identity normalizer `split(':')` always yields at least one element, and the workspace does not enable `noUncheckedIndexedAccess`, so the optional chain and `?? ''` fallback were unreachable rather than type-required. Behaviour is unchanged. Spotted by Greptile on #1435. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cf74f7e4a0 |
feat(stalker): append portal pages on scroll and drop pagination everywhere (2/2) (#1395)
* feat(stalker): append portal pages on scroll and drop pagination everywhere Second and final PR of the pagination removal (plan: .plans/2026-08-09-infinite-scroll-catalog.md). Stalker VOD/series grids now feed the shared infinite-scroll contract from server-paged appends: portal pages (server-side size, typically 14) accumulate into one deduplicated paginatedContent list, page 1 replaces it for the skeleton, hasMoreContent derives from accumulated length vs total_items (portals that ignore requested page sizes still terminate), and a failed page > 1 keeps the accumulated pages on screen with a tail retry (retryContentPage reloads the same page; loadMore refuses to skip past an unresolved append error). The facade splits the resource's loading flag by page — skeleton for page one, tail spinner for appends — and keeps per-identity scroll offsets for Stalker's INLINE detail round trips; the shared view re-arms its one-shot restore when a detail opens in the same component instance. The transitional supportsInfiniteScroll flag and every paged member are deleted from PortalCatalogFacade; the shared catalog view loses the mat-paginator, the ?page= round-trip, and the paged query-param branch. The ITV all-channels grid becomes a client-side render window over the cached full list (the app's last paginator), and Stalker search pages past its first capped request via the layout's nearEnd, with a progress guard for portals that report no usable total. Validation: 1600 unit tests across 7 projects green (new: vod/series append + failed-append retry, facade loading split/loadMore guards/scroll snapshots, ITV window model, compat selector update); catalog-sorting e2e 5/5 (Stalker spec rewritten to scroll model with p>=2 network asserts and an inline-detail spot-restore round trip; one unrelated nav-timeout flake reproduced only under parallel machine load), search e2e 16/16, web stalker e2e green (all-channels grid asserts the windowed count instead of a paginator range label); lint clean; release note added and validated; stalker-portal.md, CLAUDE.md, and ui-guidelines updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): reset paging on content-type switch and never skip failed search pages Round-1 review findings on #1395: 1. Codex P1: switching /vod -> /series with the same category id ('*' on both section roots) left page > 1 in place — setSelectedContentType did not touch paging and setSelectedCategory('*') no-ops on an unchanged id — so the new type's FIRST response was treated as an append onto the old type's accumulated list. The type setter now resets the page (and no-ops entirely when the type repeats, keeping detail round-trip restores intact). 2. Greptile P1 + Codex P2: a failed search append left searchHasMore true, so the next near-end advanced to page N+1 and permanently omitted the failed page. The search now tracks searchAppendError: a failed append keeps the accumulated pages and the next near-end RETRIES the same page; a failed fresh search (page 1) clears the previous query's cards instead of rendering them under the new term (Codex P2). The page-merge/failure logic moved into applySearchPageSuccess/Failure methods: Angular resource() never re-fires on params changes in this repo's template-less jest harnesses (store-hosted resources do), so the extracted methods carry the unit coverage — accumulation + dedupe, no-total progress guard, retry-not-skip, fresh-failure clear — plus a selection spec for the type-switch page reset. portal-stalker-feature 260, portal-stalker-data-access 464, lint clean; catalog-sorting e2e 5/5 and web stalker e2e green. search.e2e shows machine-load nav-timeout flakes on unrelated M3U/live specs (a runaway third-party process pegs the host CPU); CI provides the clean independent run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): include the portal in the search paging identity Round-2 Codex P1 on #1395: Angular reuses the search route across /stalker/A/search -> /stalker/B/search, and the paging identity covered only term + filter — the page number and accumulator survived the portal change, so the next near-end fetched portal B at the OLD page number and appended it onto portal A's results while skipping B's first page. The active playlist id now joins the page-reset identity, the resource params, the stale-response guard, and the layout's near-end reset key. Regression spec: switching the active playlist on a reused route resets the page to 1 and rotates the scroll reset key. portal-stalker-feature 261, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): end paging on no-progress appends even with a reported total Round-3 Codex P2s on #1395 (same defect in both accumulators): the no-progress guard only applied when the portal reported no usable total_items. After a mid-list portal mutation, deduplication can leave the unique list permanently shorter than the claimed total — hasMore then stayed true forever and every scroll crossing kept requesting pages past the end of the data. An append that adds no unique items now ends paging in both places: the catalog clamps totalCount to the accumulated length (hasMoreContent turns false and the count badge reflects what is actually reachable), and the search requires append progress in the total-backed branch exactly like the no-total branch. Regression specs cover a duplicate page under a larger claimed total for both. portal-stalker-data-access 465, portal-stalker-feature 262, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): explicit search retry control and per-portal scroll identities Round-4 findings on #1395: 1. Greptile P1: with the results pane parked at the bottom, repeated append failures exhausted the scroll auto-fill budget while the near-end latch stayed armed — the retry path was reachable only through another nearEnd event that could never fire. The search page now renders an explicit retry control under the results whenever an append has failed (same wording as the catalog grid tail), wired to the existing retry-same-page path, so recovery never depends on producing another scroll event. 2. Codex P2: the facade's saved-scroll map survives a same-config portal switch (the vod/series route provider is reused across /stalker/A -> /stalker/B), and its identity lacked the playlist — portal A's offset could restore onto portal B's unrelated catalog. The playlist id now leads the scroll identity; regression spec covers the cross-portal non-restore and the return restore. portal-stalker-feature 263, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): restore the search results scroll after an inline detail Round-5 Codex P2 on #1395: the search layout destroys the results container while an inline detail is shown (showDetails) and recreates it at offset zero — with the new multi-page accumulation a user could load several pages, open a result far down the list, and land back at the top on close even though the accumulated results survived. SearchLayoutComponent now exposes a scroll handoff for hosts whose details replace the results (getResultsScrollTop / restoreResultsScrollTop on the container it owns), and the Stalker search captures the offset when a detail opens and restores it one-shot after the container is recreated on close. Regression specs cover the layout handoff methods and the capture/restore round trip. portal-shared-ui 90, portal-stalker-feature 264, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(stalker): clear accumulated search results for unsearchable portals Round-6 Codex P2 on #1395: the loader's early returns (deleted or malformed playlist on a reused route) predate the accumulator and returned [] without touching it — the previous portal's cards kept rendering under the new context once loading settled. Every no-portal early return now goes through resetSearchAccumulator(), which empties the accumulated list and both paging flags; the short-term path uses it too (and now also clears a stale append error). Regression spec covers the full reset. portal-stalker-feature 265, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8442747c37 |
feat(xtream): replace catalog pagination with infinite scroll (1/2) (#1392)
* feat(xtream): replace catalog pagination with infinite scroll Xtream movie/series/live catalogs now load continuously while scrolling instead of paging. The selection store keeps a growing visibleCount render window over the in-memory catalog (initial 50, +50 per load) plus a saved scroll state, so opening a title and going back restores the exact spot. A shared InfiniteScrollDirective (portal/shared/ui) fires loadMore near the bottom (edge-triggered, mirroring search-layout) and auto-fills viewports taller than the initial window by measuring container overflow — capped at 10 self-initiated loads per list identity, with a ResizeObserver re-check. The shared CategoryContentViewComponent branches on the transitional PortalCatalogFacade.supportsInfiniteScroll flag: Xtream scrolls, Stalker keeps its server-driven paginator and ?page= round-trip untouched until its append lands (PR 2), after which the paged facade members and the flag are deleted. grid-list loses its dead built-in paginator and gains tail states (append spinner, retry-on-error) plus content-visibility on cards. The in-portal search results reuse the search layout's nearEnd hook to window their full result set instead of rendering it unbounded. Validation: portal-xtream-data-access (234), portal-xtream-feature (357), portal-catalog-feature (22), portal-shared-ui (77, incl. new directive spec), portal-stalker-* (253) unit tests green; catalog-sorting e2e 5/5 (new scroll-growth + spot-restore test against the large 200-item mock scenario, Stalker paged spec unchanged); search e2e 16/16; lint green; release note added and validated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(xtream): auto-fill search results and refresh the near-end latch Review findings from #1392: the in-portal search window could stall at its first 60-item chunk when the rendered cards did not overflow the container — nearEnd only fired on real scroll events (Greptile P1), the search layout's edge latch survived a result-set replacement (Codex), and the shared directive's latch went stale after appended content moved the bottom out of the threshold (Codex). The search layout now drives its results container through the shared InfiniteScrollDirective instead of a bespoke scroll handler: the measured auto-fill reveals further chunks on tall viewports without any scroll, the reset key (search term) and item-count changes refresh the latch, and new nearEndHasMore/nearEndAppending inputs let consumers gate emissions. Xtream search wires them for both modes — this also fixes the same latent tall-viewport stall in the global search's 100-item pages — and the Stalker search page (single capped request until PR 2) sets hasMore=false. The directive's fill check now refreshes the latch from the measured state, so an End-key jump straight to the new bottom is a genuine crossing again. New coverage: directive stale-latch regression, search-layout auto-fill + hasMore gating, in-portal window reveal/reset in search-results. Reruns: portal-shared-ui 80, portal-xtream-feature 357, portal-stalker-feature green; search e2e 16/16 (fresh Playwright report verified); lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(xtream): re-measure search auto-fill on the rendered window, not the total Round-2 review finding on #1392 (Greptile P1 + Codex P2, same defect): the search layout bound the constant result-set total to the infinite-scroll directive's item count, so once the in-portal window grew 60 -> 120 no tracked input changed, no further overflow check was scheduled, and results beyond 120 stayed unreachable on tall viewports. The layout now takes an explicit nearEndRenderedCount (falling back to resultsCount for consumers that render everything they report) and feeds THAT to the directive. Xtream search passes the windowed slice length for in-portal mode and the loaded-set length for global mode. Regression specs: layout re-measures when the rendered window grows while the total stays constant; the component exposes the rendered count following the window. portal-shared-ui 81, portal-xtream-feature 357, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(xtream): include filter state in the search reset identity Round-3 Codex P2 on #1392: the near-end latch and auto-fill budget were keyed on the search term alone, so a filter-only transition (type filters or the hidden-categories toggle) replaced the result set without resetting them — a jump straight back into the threshold could be swallowed. The search layout now accepts an explicit nearEndResetKey (defaulting to the term); Xtream search supplies term + type filters + excludeHidden. Regression specs: layout latch resets on an identity change without a new term; the component identity changes on filter-only and hidden-toggle transitions. portal-shared-ui 82, portal-xtream-feature 358, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(xtream): refuse global-search appends while an edited query debounces Round-4 Codex P2 on #1392: after the reset-identity change, the layout's auto-fill can request more results inside the 300ms search debounce. The append then ran with the freshly edited term but the old result count as offset, interleaving a page of the new query into the old query's visible results until the offset-zero search landed. An append now only continues the LAST EXECUTED search: the append guard additionally requires the effective term to equal lastGlobalSearchTerm, so pagination stays suppressed from the first keystroke until the fresh search replaces the result set. Regression spec covers the mid-debounce refusal; the two existing append specs state their precondition explicitly. portal-xtream-feature 359, lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(xtream): per-selection scroll snapshots and progress-based auto-fill stop Round-5 Codex P2s on #1392: 1. The single saved-scroll slot lost the first tab's position on a VOD -> Series -> VOD round trip — the series view's destroy hook overwrote it with series coordinates. Snapshots are now kept per selection identity (bounded to the 8 most recent), so a detour's save can never destroy another list's spot. Store API is unchanged. 2. The fixed 10-load auto-fill budget could strand items on a viewport large enough that ten chunks still do not overflow — with no scrollbar, no real scroll event can ever fire. The auto-fill now terminates on lack of progress instead: loads continue while they grow scrollHeight (until genuine overflow hands off to scroll events) and stop after three consecutive loads without growth, which only a source that reports more but renders nothing can produce. Regression specs: VOD/Series round trip keeps both snapshots; growth keeps filling past the old cap and stops at overflow; no-growth stalls stop at three; reset key clears the stall guard. portal-shared-ui 83, portal-xtream-data-access 235, catalog-sorting e2e 5/5 re-run, lint clean. CLAUDE.md wording updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c741815b97 |
fix(build): include shared UI stylesheets in Nx cache inputs (#1360)
* fix(build): include shared UI stylesheets in Nx cache inputs `libs/ui/styles` held shared SCSS partials but had no `project.json`, so its files belonged to no Nx project and were absent from every task hash. Editing a partial and running `pnpm nx build web` reported 4 of 4 tasks cached and shipped the previous CSS — a silent wrong build rather than a failure. Nx derives its project graph from TypeScript imports only, so a relative Sass `@use` that crosses a project root creates no edge. Verified directly: after adding the project but before declaring anything, `ui-styles` still had zero dependents in the graph. Make it the `ui-styles` project (no targets — it exists to be hashed) and declare `implicitDependencies` on the 8 consumers. Chosen over adding the path to `sharedGlobals`, which would put shared styles into every project's hash and make a one-line SCSS tweak mark the whole workspace affected. A styles edit now marks 15 projects affected and leaves electron-backend, website, the mock servers and the shared libs alone. `libs/ui/styles` was the only projectless directory holding files under `libs/` or `apps/`. Add `pnpm run styles:inputs:validate` to keep it closed: it resolves every relative stylesheet import against Nx's real project graph and fails when one escapes the input closure of a build that compiles it, naming the project to declare. It exits 1 with 21 diagnostics on the pre-fix tree. Imports of `apps/web/src/nav-list.scss` are deliberately accepted — `web` already hashes that file, and a lib -> app edge would make the graph cyclic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(build): spawn git without a shell in the stylesheet check `execSync("git ls-files '*.scss'")` runs through `cmd.exe` on Windows, where single quotes are literal characters rather than quoting. Git received the pathspec with the quotes intact, matched nothing and exited 0, so `styles:inputs:validate` reported success after checking zero stylesheets — silently disabling the check for Windows developers while staying green. Spawn with `execFileSync` so no shell is involved and git expands its own pathspec; verified to return the identical 133 files. Both this and the eslint glob trap next to it in the docs report success while covering nothing, so also make an empty scan fail rather than pass: the workspace always contains SCSS, and a listing that returns none means the scan broke. Reported by Codex review on #1360. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(styles): move nav-list partial into ui-styles (#1361) * fix(build): count every target of a comma-separated Sass @import `@import` is the only rule that takes a list, and the scan read just its first target. A later entry crossing an Nx project boundary escaped the cache key while the check still reported success — the same silent-pass failure the tool exists to prevent. Parse every target of an `@import` list. The obvious "read all quoted strings" fix trades one silent gap for a phantom one, so the rule decides: `@use`/`@forward` load exactly one module and a quoted string after it is `with (...)` configuration, and `url(...)` stays a plain CSS import the browser resolves at runtime. Neither is a module Sass compiles. The workspace has no relative `@import` at all today, so the scan still finds the same 42 imports across 133 files; this closes the gap before someone writes one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8f861a3a1b |
fix(ui): make the workspace usable on phone-sized screens (#1326)
* fix(ui): make the workspace usable on phone-sized screens
The shell was half-adapted below 640px: the rail flipped to a horizontal
bar but the link lists inside it kept stacking downwards, so the navigation
was drawn outside the bar and over the header (#1100).
Three resizable rails — the shell context panel, the live-layout channel
sidebar and the M3U channel drawer — kept their persisted desktop width,
which left the content around 50px on a 375px screen. They now span the
full width and stack above the content. The inline width written by
ResizableDirective is why these rules need `!important`.
Found while walking the rest of the UI at 375px and 768px:
- The detail hero kept poster and details side by side, squeezing the
action row below its own labels until "Play" was clipped to its icon.
- The settings section list did not scroll and painted over the footer,
which also affected short desktop windows.
- Hiding the M3U channel list on a phone was one-way: the restore handle
was hidden and only Cmd/Ctrl+B could bring it back.
- The live header drew the channel count and the paginator on top of each
other up to tablet width, because the paginator does not shrink and the
meta collapsed to zero width and overflowed its box.
- The search scope checkbox was pushed off the right edge.
Live TV states a floor for the player instead of a ceiling for the lists,
so the video keeps a usable share of the screen under the categories panel
and the channel list.
Closes #1100
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): address review — keep the palette reachable and the video visible
Two findings from the Codex review on #1326.
Hiding the command-palette trigger on phones removed the only pointer-driven
way to open it: the rail renders route links plus Settings and emits nothing,
so `commandPaletteRequested` had exactly one source. The button stays and its
keyboard-shortcut label is swapped for an icon instead. Doing that exposed a
latent flex trap in the same row — an <input> keeps an intrinsic min-width
from its `size`, and `min-width: auto` honours it, so the field refused to
shrink and pushed the trigger out onto the buttons beside it.
The M3U drawer released the shared player floor, which on a short landscape
phone (600-640px wide) left the content container at half the shell body.
The inline guide inside it is `flex: 0 0 <basis>` and took its full 180px out
of a container that no longer had it, so the video could reach zero height.
The floor is restored and now yields on short viewports, the video states its
own minimum, and the guide is what gives way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): let the channel list keep its height on a landscape phone
Follow-up to the review: the player floor added in the previous commit was
measured against the viewport, not against what the shell had left. On a
640x360 landscape phone the stacked categories panel already takes 30vh, so
claiming another 50vh here drove the channel sidebar to zero height while it
was still marked expanded — no way to pick another channel — and pushed the
layout past the viewport.
The floor now applies only where the screen can afford it (`min-height:
600px`), the sidebar states a floor of its own so it cannot be squeezed out,
and the collapsed rule clears that floor so hiding the list still works.
Below that height the two panes simply share what is left.
Portrait is unchanged: categories 244px, channel list 220px, player 240px on
a 375x812 screen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): address review — settings nav on landscape, poster dead space
Two more findings from the Codex review.
The stacked settings context panel capped itself at 30vh, which on a 360px
screen is 108px — less than the panel's own title and footer, so the seven
section rows collapsed to nothing behind an overlapping footer. On short
screens the caption gives way (the rail already labels the page), the footer
sheds its tall-screen padding, and the settings variant gets a slightly
larger cap: unlike the live routes there is no player below competing for
height, only a scrollable form.
The poster kept a 330px minimum from the skeleton fallback at the bottom of
the file — sized for the 220px desktop poster — while the stacked phone hero
renders it 140px wide with a ~210px aspect-ratio height. Every loaded detail
page carried ~120px of empty space between the poster and the title. The
override sits after that rule because it wins on source order, not
specificity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): let the playlist switcher yield to the search field on narrow phones
Codex review of
|
||
|
|
760099358b |
feat(downloads): redesign download manager (#1313)
* docs(downloads): specify manager MVP redesign * docs(downloads): plan manager MVP implementation * docs(downloads): tighten manager validation plan * fix(downloads): keep renderer download state global * fix(downloads): make active count accessible * feat(downloads): derive queue and library view model * test(downloads): close view model coverage gaps * fix(downloads): stabilize malformed view model data * refactor(downloads): isolate library navigation * fix(downloads): report library navigation failures * feat(downloads): add ready-to-watch library * feat(downloads): add active download queue * feat(downloads): finish manager MVP * docs(downloads): clarify detail-first offline behavior * docs(downloads): plan detail navigation follow-up * fix(downloads): open completed movies in details * test(downloads): cover pending series navigation * fix(downloads): honor the global cover size * fix(downloads): prefer local playback in shared details * fix(downloads): preserve external launch priority * fix(downloads): prefer local playback in Xtream details * test(downloads): cover offline detail journey * docs(downloads): document offline detail behavior * docs(downloads): format detail navigation plan * fix(downloads): open Stalker items in provider details * docs(downloads): clarify Stalker navigation fallback * fix(xtream): isolate reused detail identities * fix(xtream): ignore stale VOD positions * fix(downloads): keep offline Xtream playback available * docs(downloads): clarify provider playback availability * docs(downloads): design missing-file recovery * docs(downloads): plan missing-file recovery * feat(downloads): derive completed file availability * feat(downloads): recover missing completed files * feat(downloads): refresh missing local files * feat(downloads): separate missing files from ready media * feat(downloads): surface missing files for recovery * refactor(downloads): simplify ready cards * test(downloads): cover missing-file and series journeys * feat(downloads): finish missing-file recovery * docs(downloads): design offline detail views * docs(downloads): plan offline detail views * feat(downloads): persist offline metadata snapshots * fix(downloads): complete metadata snapshot bridge contract * feat(downloads): manage offline metadata snapshots * fix(downloads): harden metadata snapshot updates * fix(downloads): restrict snapshot artwork * fix(downloads): guard restart artwork URL * fix(downloads): refine artwork URL checks * feat(downloads): expose offline metadata updates * fix(downloads): keep metadata service change focused * fix(downloads): preserve metadata error conventions * feat(downloads): derive offline detail content * fix(downloads): preserve unknown episode coordinates * feat(downloads): add focused offline detail routes * fix(downloads): ignore fragments in shell route state * fix(downloads): normalize fragments before queries * feat(downloads): open ready cards in offline details * fix(downloads): use native disabled card styles * feat(downloads): enrich offline detail metadata * fix(downloads): harden offline metadata resolution * fix(downloads): preserve stalker provider titles * fix(downloads): distinguish stalker metadata seeds * fix(downloads): stabilize offline metadata refresh * fix(downloads): throttle sparse metadata refreshes * fix(downloads): type metadata language settings * feat(downloads): render offline movie and series details * fix(downloads): harden offline detail interactions * fix(downloads): close offline detail edge cases * feat(downloads): hand off to provider-only details * fix(downloads): preserve stalker provider handoff * feat(downloads): capture metadata at download time * fix(downloads): preserve snapshot source semantics * fix(downloads): preserve episode snapshot identity * docs(downloads): document offline details flow * docs(downloads): clarify stalker provider fallback * test(downloads): cover offline detail journeys * test(downloads): stabilize offline detail selectors * style(downloads): format changed files * docs(downloads): clean design spec formatting * fix(downloads): preserve offline library ownership * test(downloads): fix Windows workspace navigation * test(database): preserve Electron tsconfig resolution * perf(downloads): avoid blocking file availability probes |
||
|
|
9885178f32 |
fix(stalker): refresh stale embedded-series snapshots from favorites and dashboard (#1253)
Favorites and recently-viewed rows store Stalker items as full JSON snapshots, so a vclub-style embedded series[] episode list froze at the moment the row was written: a series favorited when only episode 1 was out kept showing one episode forever when opened from favorites, recents, Continue Watching, or any dashboard rail. New withStalkerSnapshotRefresh() store feature renders the stored snapshot immediately and re-fetches the item from the portal in the background via a title search (get_ordered_list&type=vod&search=..., matched by id, paginated up to 5 pages, wildcard-category retry), patching fresh episodes and cmd into the active selection. The patch is guarded on both the item id and the active playlist id, since Stalker ids are only unique per portal. Only the in-memory selection is patched — the stored snapshot row is deliberately left alone, because every entry path into the detail view runs this refresh and writing it back would add an uncontrolled background writer to the whole-playlist read-modify-write that every favorite/recent mutation performs. Also fixes the stalker-mock-server embedded-series scenario, which generated series[] as objects the app's vclub adapters filter out instead of the episode-number arrays real portals send. Regular type=series and Ministra is_series items are unaffected; Xtream is unaffected (get_series_info is never cached). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d61fd5db19 |
feat: add strip country prefix setting (#1162)
* feat: add strip country prefix setting
* feat: scope country-prefix stripping to live content and cover missing surfaces
- narrow the heuristic: pipes always strip, dash/colon separators only
when the prefix is a short uppercase tag ("UK - BBC One" strips,
"Sky - Sports F1" and "Mission: Impossible - Fallout" stay intact)
- fall back to the original name when stripping would leave nothing
- scope stripping to live content only: grid type (live/itv/radio),
playback isLive, external sessions without contentInfo, dashboard
cards with contentType 'live'
- cover previously missed surfaces: M3U player EPG timeline header,
M3U inline player title, radio player, dashboard live rails
- replace hardcoded settings strings with translate keys and add
SETTINGS.STRIP_COUNTRY_PREFIX(_DESCRIPTION) to all 18 locales
- add unit specs for the utility plus regression specs for
channel-list-item and external-playback-dock
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: cover strip-country-prefix call sites for codecov
- dashboard-rail: new spec for cardTitle live/movie/series scoping
- grid-list: strip enabled/disabled, VOD passthrough, 'No name' fallback
- portal-inline-player: live strip vs VOD passthrough
- unified-live-tab: timeline channel name strip + M3U name precedence
- video-player: timeline/radio/inline titles with the setting on and off
- settings-store: default false + persisted true round-trip
- settings-form.utils: new spec for form default and ?? false fallback
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <serega05@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
a51c537bb2 |
fix(theme): make scrollbars follow the app theme instead of the OS color scheme (#1179)
On Windows with a light OS theme, scrollbars rendered light even when the app was switched to dark. Two combined causes: - The page never declared `color-scheme`, so Chromium colored native scrollbars from the OS preference. Declare `color-scheme: light` on html and flip it to `dark` via `html:has(> body.dark-theme)` plus the `.dark-theme` block itself. - Scrollbar styling referenced `--mat-sys-*` tokens, which are never emitted by the current Material theme setup (mat.define-theme + all-component-themes does not produce system tokens). Those `scrollbar-color` declarations computed to `auto`, falling back to the native (light) scrollbar. Switch scrollbar styling to the `--app-muted-color` design token (defined for both themes), replace hardcoded white `rgba(255,255,255,.08)` thumbs, and add an explicit `scrollbar-color` where only `scrollbar-width: thin` was set. Verified live in Electron via CDP in both themes: scrollbar-color resolves and scrollbars render dark in dark theme regardless of the OS setting. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ffc07146fe |
feat(xtream): sort and filter the VOD/series catalog by IMDb rating
Adds IMDb rating sort and minimum-rating filtering for Xtream VOD/series catalogs, consolidates refinement controls, and guards rating controls away from Live TV. |
||
|
|
568c03fe0e |
fix: reset catalog pagination on search
Entire-Checkpoint: f957cd9849e0 |
||
|
|
2f7406054d |
perf(ui): enable OnPush across hot list-rendering components
Audit found 10 list-rendering components (each >150 lines, all using signal-based state) on default change detection. Default CD re-checks every binding on every parent CD cycle (mouse moves, EPG progress ticks, etc.); under OnPush these only re-check when their own signals change or when explicitly markedForCheck. All 10 use signals exclusively for state (no manual subscribe-and-mutate patterns), so the conversion is straightforward and safe. Components: - recent-playlists (740L) — homepage playlist list with drag-drop - vod-details-route (552L) — Xtream VOD detail page - playlist-switcher (509L) — workspace shell playlist switcher - season-container (444L) — Stalker/Xtream series episodes (DoCheck preserved) - stalker-search (362L) — Stalker search results - search-results (350L) — Xtream search results - category-management-dialog (199L) — manage Xtream categories - recently-added (162L) — Xtream recently-added rail - category-content-view (Catalog) — category browse page - grid-list (shared) — generic grid renderer used across portals Tests: 124 passed across all touched lib projects. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 2e1a36e2f0f6 |
||
|
|
d1e4c0d306 |
fix(portal): show search-aware empty state when category search yields no results
When a search filter inside a category produces zero results, the grid
@empty fallback used to render the EMPTY_CATEGORY view ("No content in
this category" with the empty-tv illustration). That message was
misleading — the category itself wasn't empty, only the search filter
excluded everything.
grid-list now takes a searchTerm input and branches the @empty fallback:
when a search is active it renders the NO_SEARCH_RESULTS view with the
parameterised "No results found for {term}" title, otherwise it falls
back to EMPTY_CATEGORY as before. category-content-view derives the
searchTerm signal from the ?q= query param and passes it through.
Also: bump the NO_SEARCH_RESULTS icon from a 64px mat-icon to a clamp()
sized icon (120-180px) at 0.5 opacity so it visually balances with the
SVG illustrations used by the other viewTypes, swap the icon to
search_off (struck-through magnifier) so it reads as "no results"
rather than "search now", and fix the EN i18n typo
("change you search request" -> "change your search request").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 2e1a36e2f0f6
|
||
|
|
a2e2959b89 |
fix(xtream): pagination behavior (back & scroll)
Entire-Checkpoint: c6e522b4276c |
||
|
|
76e4e1b032 | feat: enhance playlist management by adding optional title input and updating URL submission logic | ||
|
|
5a070f5550 | feat: enhance routing with reactive route parameter signals and update catalog facade for search query handling | ||
|
|
eef18dea68 | feat: add Jest configuration and tests for category content view component | ||
|
|
91cd650c1a |
feat: enhance EPG and portal features with error suppression and status resolution
- Added suppressErrorLog option to EPG API calls in withEpg feature. - Introduced resolvePortalStatus function to streamline portal status checks in withPortal feature. - Updated checkPortalStatus to return PortalStatusType instead of void. - Enhanced XtreamStore to preserve cancelled content initialization block. - Expanded XtreamState to include contentInitBlockReason for better state management. - Added XtreamContentGateComponent to manage content initialization states. - Implemented tests for XtreamContentGateComponent to ensure correct rendering based on contentInitBlockReason. - Created PlaylistRefreshService to handle playlist refresh operations and events. - Introduced new IPC commands for playlist refresh functionality. - Added interfaces for playlist refresh events and payloads to improve type safety. |
||
|
|
94e9a5119c |
refactor(styles): implement panel header mixins for consistent layout across components
Entire-Checkpoint: 84693b066142 |
||
|
|
7ac2fd66b8 | refactor: update nx monorepo structure | ||
|
|
0ad1eeb785 | refactor: update nx monorepo structure |