mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
ef3f98d02651846c0ef7a363a804d500c57723d0
2859
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ef3f98d026 |
feat(portals): posters-only cover wall for movie and series grids (#1604)
* feat(portals): posters-only cover wall for movie and series grids Add `Settings.showCoverTitles` (Settings > General, default on). Turning it off drops the title row under VOD/series covers in catalog, favorites and recent grids and reveals the title as a bottom-gradient overlay on hover and keyboard focus, pinned open for items whose cover is missing or failed. `CoverTitlesService` is the single resolver: the opt-out AND a hover-capable pointer, so touch-only devices keep their titles. Live channel grids, search results, "recently added" rails and dashboard rails always keep labels. Catalog and collection cards become keyboard buttons (role, tabindex, aria-label, Enter/Space, focus ring) and poster alt text is the title. The default-on boolean coercion moves into `settings-opt-out.util.ts` because the settings store reached the max-lines limit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): keep nested Remove key presses from activating the card Enter/Space on the content card's nested Remove button bubbled into the card's own key handlers: Enter opened the item before removing it and Space opened it while cancelling the removal. Only keys pressed on the card element itself now activate it. Regression spec added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): keep cover titles while an in-section search filters the grid The posters-only wall exempts search results because they are identified by the name the user typed; the category grid's own in-section filter is the same case, so `app-grid-list` now keeps the title row while its `searchTerm` is non-blank. Contract docs updated, regression spec added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): keep cover titles while the collection tab search is active The unified favorites/recent tab filters by its own search term, so its matches are identified by name like every other search result. The tab now opts its cards out of the posters-only wall while the term is non-blank. Contract docs updated, regression spec added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): move the card Remove control out of the button surface An interactive control nested inside a role="button" is an invalid accessibility structure. The content card's activation surface is now its own inner element and the Remove button a sibling positioned over the poster corner, labelled by its tooltip text. Spec asserts the control is never a descendant of the button. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): draw the collection card focus ring where it is not clipped The card's overflow: hidden clipped an outline drawn on the inner activation surface on every edge, so keyboard users saw no focus indication. The ring now sits on the outer card via :has(> .content-card__activation:focus-visible). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): detect any hover-capable pointer for the posters-only wall `hover` describes only the primary pointer, so a touch-first tablet with a mouse or hover-capable stylus attached lost the wall. The resolver now reads `(any-hover: hover)`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
40092f6efc | docs(tmdb): clarify user-provided API key policy (#1581) | ||
|
|
d438f5c655 |
feat(epg): accept local XMLTV files as EPG sources (#1600)
* feat(epg): accept local XMLTV files as EPG sources Settings → EPG and the playlist dialog accepted `file://` in their form pattern, but the main process rejected everything except http(s), so a local XMLTV entry saved fine and then failed on import. Both surfaces now take a remote link, a `file:` URL, an absolute POSIX path or a Windows drive/UNC path (`classifyEpgSourceReference` in shared/interfaces), and the settings section spells out the accepted formats with examples. The EPG worker opens every source through `openEpgSourceStream`: remote links keep the validated-redirect client and trust policy, local files are read from disk behind the signature-sniffing optional gunzip stage, so .xml, .xml.gz and extension-less gzip all parse. Only hand-typed sources may be local: `extractM3uEpgUrls` harvests http(s) links only from M3U headers, since the local branch bypasses `validateRemoteUrl`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(epg): pick local XMLTV files with a native file dialog A folder button beside each EPG source row (Settings → EPG and the playlist dialog) opens the native open-file dialog and writes the chosen absolute path into the row. New `EPG_OPEN_FILE_DIALOG` IPC behind `ElectronBridgeApi.openEpgFileDialog`, gated in the renderer by `RuntimeCapabilitiesService.supportsEpgFilePicker`. The row's refresh/remove buttons carry `data-test-id`s now, and the EPG e2e suites address them by id instead of index, since the folder button became the first button in a row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): authorize local XMLTV files in the main process Review follow-up (Greptile P1, Codex P1). The renderer hands source strings to FETCH_EPG/EPG_FORCE_FETCH unchanged, so the form validator alone could not enforce the provenance rule: a compromised renderer, or a legacy `file://` entry an older version stored from an M3U header, could name any file on disk. `EpgWorkerService.startFetch` now asks a main-process `EpgLocalSourceAuthorizer` before a local path reaches the worker: a path the native picker returned is trusted at once, a hand-typed path is confirmed once in a native message box the renderer cannot fake, and a refusal is reported in the progress panel. Allowed paths persist under TRUSTED_LOCAL_EPG_SOURCES in the main-process config. The worker opens its local branch only when main set `allowLocalFile`; the service defaults to deny-all until epg.events installs the persisted authorizer. `resolvePlaylistEpgSourceState` and `filterPlaylistEpgUrlsForFetch` drop a stored non-remote entry unless it is also in `manualEpgUrls`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): fail a refused local EPG fetch instead of resolving it Review follow-up (Codex P2). A denied native confirmation now rejects the fetch after reporting the error row, so handleFetchEpg and the renderer's fetch result cannot claim the file was read. Also restores the unrelated CLAUDE.md paragraph an earlier formatter pass had reflowed into a list. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): cancel a local source retired during its authorization prompt Review follow-up (Codex P2). startFetch keeps the request generation captured before awaiting the native confirmation and rechecks it afterwards: a source retired meanwhile ends as cancelled instead of starting an import that a pending clear would then have to await. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(epg): leave the local XMLTV e2e with a pristine settings form The local-file test ended with the EPG source field still dirty, which arms the main-process close guard: the app then waited for the unsaved changes dialog instead of closing, the close timeout killed it, and on Windows the killed process kept iptvnator.db busy (EBUSY on the data-dir cleanup) and hung the Playwright worker teardown. Discarding the form before the app closes takes the test from 15 s to 4 s locally. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
d9c8ba4d0b | fix(epg): keep the import panel free of horizontal scrolling (#1599) | ||
|
|
17b8aa309d | fix(m3u): restore DASH playback from favorites and recently viewed (#1597) | ||
|
|
bd848aaad6 | feat(playlist): clean up selected inactive desktop sources (#1593) (#1596) | ||
|
|
cbc918b096 | test(electron): synchronize source refresh completion (#1595) | ||
|
|
62655a8b5d | feat(playlist): show desktop health indicators for network sources (#1592) | ||
|
|
a417826b01 | fix(m3u): determine VOD playback independently of TMDB (#1594) | ||
|
|
6e3f0d258f | fix: prevent fullscreen hover and playlist refresh races (#1591) | ||
|
|
621436fb1d |
docs: name the copyright holder in the licence
The MIT notice carried "Copyright 2020-2021" with no holder, which is the one line the licence needs to be attributable: the permission grant refers back to "the above copyright notice", and a notice naming nobody is weak to point at when the project is republished without attribution. Uses the handle the project already publishes as its author in package.json. The range now starts at 2019, the year of the first commit, and runs to the current year. The canonical "MIT License" title line is added so automated licence detection matches the file exactly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
61ac15372c |
perf(website): serve fonts and the avatar locally, load comments on demand
Every page pulled its three typefaces from fonts.googleapis.com and fonts.gstatic.com, each blog post fetched the author avatar from githubusercontent.com, and the giscus client script ran on page load. That is four outside origins contacted before a reader does anything, each costing a DNS lookup and a TLS handshake on the critical path. Fonts now come from the @fontsource packages the app already uses and are emitted as .woff2 beside the site; the avatar is a 3 KB file in public/; and the giscus embed is created by a "Show comments" button that carries the configuration as data attributes, so the script is only built when a reader asks for it. A delivered page now makes no third-party request at all, verified across the whole build. The variable Bricolage package names itself "Bricolage Grotesque Variable", so that exact name leads the display stack in the Tailwind config. The giscus test now checks the button configuration and asserts the client script is absent from the delivered HTML. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7d1265d566 | fix(xtream): detect HTTP portals during explicit connection tests (#1588) | ||
|
|
0700ba966e | fix(epg): decode gzip files wrapped in HTTP gzip (#1589) | ||
|
|
0c531276a0 |
feat(website): name the Android TV players on the computer vs TV box page
"TiviMate for PC" is the query behind most of this page's traffic, so the page now names the apps people mean instead of describing a category. What it says about them is what could actually be verified: TiviMate's Google Play listing (Android and Android TV, reads M3U, Xtream Codes and Stalker, "a media player that provides no content"), and IPTV Smarters publishing Windows and macOS builds — which is why the page does not claim TV apps have no desktop version. The useful half is the part no affiliate site writes: these names are heavily abused, the stores carry copycat listings, and much of the search result for any of them is someone selling "subscriptions" under the app's brand. The developers are player authors who do not sell channels, which is exactly what IPTVnator says about itself — the page links to our own unofficial-websites post for the same reason. Three FAQ entries answer the query directly, and the page joins the NAMES_THIRD_PARTY_SOFTWARE set so the test requires its dated disclaimer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f833e16cc4 |
feat(website): add the computer vs TV box comparison
Third page in the set, and the one that answers the question people ask before installing anything: where should IPTV actually run. It compares the two places rather than two products — everything that needs a keyboard (adding a provider, attaching a guide, mapping a channel, working out why a stream fails) against everything that needs a couch — and states plainly that IPTVnator has no TV build, with the phone remote as the answer for a laptop wired to the television. No third-party software is named, so the page carries no ThirdPartyNote: the rows describe the ordinary experience of each kind of device and say so in the caption. The compare hub's lead no longer claims every page is a choice inside the app, and the three newest entries get distinct icons. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3b0e9056ed |
feat(website): add the IPTVnator vs Kodi comparison
Second page under the third-party rules. Kodi is a media center that reaches IPTV through a PVR add-on, so the comparison is about shape rather than score: where the source comes in, what each program is built for, and the platforms. The tables say plainly what Kodi does that IPTVnator does not — a local library with scraped metadata, add-ons and skins, a remote-friendly interface and builds for TV boxes and a Raspberry Pi — and the verdict tells a reader already running Kodi on a TV to stay there. Kodi's IPTV capabilities come from whichever PVR client is installed, so rows that depend on one say "Depends on the add-on" instead of claiming something this page cannot verify. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
87289eaaa1 |
feat(website): add the IPTVnator vs VLC comparison
First comparison page that names other software. It answers the question every "IPTV on a computer" guide raises — VLC opens the playlist, so why a dedicated player — with three tables (what each can connect to, what a playlist file cannot carry, and the practical differences) and eight FAQ entries, then ends where the honest answer is: IPTVnator hands VLC the stream, with the playlist's headers and the resume position, and reads the position back. Naming another project brings rules, now recorded in the registry doc comment and a new README section: a dated ThirdPartyNote stating the other project is independent and endorses nothing, only stable publicly documented facts, no claim of a missing feature that was not checked, and no logos, brand styling or links to the other project. The compare test enforces the note and the date for every page in its NAMES_THIRD_PARTY_SOFTWARE set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fadcbb4673 |
test(database): guard upgrades across skipped releases (#1582)
* docs(database): require upgrades across skipped releases * test(database): cover direct upgrades from 0.19 through 0.23 * test(database): verify upgraded schemas against current contract |
||
|
|
fff022afe4 | fix(ui): keep detail back navigation available while scrolling (#1576) | ||
|
|
e76447975b | feat(playback): stream-info popover in the player overlay (#1578) | ||
|
|
bad8a0991e |
feat(downloads): download completed Xtream catch-up programmes as TS (#1572)
* feat(epg): copy catch-up programme URLs without changing playback * feat(downloads): save completed Xtream archive programmes as TS * fix(epg): let newer archive copy requests supersede pending work * fix(downloads): protect archive partials and independent submissions * fix(downloads): verify archive identity through finalization * fix(downloads): bound archive storage and capture cleanup entries * fix(downloads): preserve archive ownership across failure paths * fix(downloads): recover explicitly verified archive completions * fix(downloads): journal archive promotion before publishing files * fix(downloads): reset archive proof before an explicit restart * fix(downloads): preserve archive recovery ownership and interruption * fix(downloads): verify durable archive identity at resume open * fix(downloads): fence archive commands during completion commit * fix(downloads): persist archive ownership throughout its lifecycle * fix(downloads): protect archive removal and missing-file recovery * fix(downloads): journal private cleanup captures for recovery * fix(downloads): journal active archive cleanup before removal * fix(downloads): clean settled archives before deleting stale rows * fix(downloads): preserve archive ownership on removal and resubmission * fix(downloads): recover proven archive completions before retry * fix(downloads): recover local archives before remote transfer checks * test(downloads): resolve archive fixture from workspace root * fix(downloads): distinguish reused archive inodes by creation time * fix(downloads): bind fresh archive reservations to owned files * fix(downloads): clean reservations when ownership writes fail * fix(downloads): commit archive reservation and ownership atomically * fix(downloads): retain captures until replacement restoration succeeds * fix(downloads): require durable ownership before cleanup relocation * fix(downloads): preserve 64-bit archive file identities on Windows * refactor(release): keep capture fixture constants in their shared module * fix(downloads): preserve the last link of captured foreign files * fix(downloads): expose retained archive recovery files * fix(downloads): keep recovery instructions open while copying |
||
|
|
a7f3860102 |
feat(website): add the TMDB metadata guide with the required attribution
New guide at /blog/tmdb-metadata-guide/: a sent/not-sent table for the opt-in enrichment (title, year, app language and the build's API key leave the machine; nothing about the user, the provider or the playlist does), how to switch it on, when to use your own free key, what the feature unlocks, and what the local cache holds. Eight FAQ entries, and a section for readers who would rather leave it off. The post states plainly that TMDB is a metadata database and not a content source, and carries TMDB's required attribution through a reusable TmdbAttribution component (their logo plus "This product uses the TMDB API but is not endorsed or certified by TMDB."), the same wording the app shows in Settings and About. Its screenshot is the settings section itself: the capture's G5 guard keeps enrichment disabled for the whole run, so no licensed poster or still can reach a published frame. The new action stages the switch in the form only, which reveals the key field, the cache panel and the attribution while the stored setting stays off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
93e759e1da |
fix(m3u): accept standard Base64 ClearKey values (#1575)
* fix(m3u): accept standard Base64 ClearKey values * refactor(release): move M3U fixture generation out of capture driver |
||
|
|
c952b55da1 | feat(playback): enrich failure diagnostics and add safe support reports (#1574) | ||
|
|
7f06690e72 |
feat(epg): copy catch-up programme URLs (#1569)
* feat(epg): copy catch-up programme URLs without changing playback * fix(epg): let newer archive copy requests supersede pending work |
||
|
|
186497bf42 |
feat(website): add the EPG troubleshooting post, fix the mock identity check
New post at /blog/epg-wrong-program-fix/: a table separating the three symptoms (an empty row, a channel showing another station's schedule, and every programme shifted by a fixed amount), how the tvg-id → tvg-name → name lookup chain produces the first two, the manual "Map EPG channel" flow, and the display-only EPG time offset with the sign to use. Seven FAQ entries. The three screenshots are mock-backed. The Xtream mock gains /demo/guide.xml, an XMLTV guide for its marketing live channels whose ids deliberately match no playlist tvg-id, which is what makes the manual mapping worth showing; the capture imports it through the settings, accepting the private-network confirmation a loopback source raises, then right-clicks a channel of the M3U fixture and searches the dialog. The new actions live in capture-navigation-epg-actions.ts, alongside the setup, portal and download modules, and borrow one entry point from each of its two neighbours instead of duplicating their navigation. Also fixes assertMockServerIdentity: it checked both expected categories against get_vod_categories, but "Urban Drama" is a series category, so the reuse path rejected every already-running mock and a capture could only run when the port happened to be free. Each category is now checked against its own endpoint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9f950a1530 |
fix(migration): show startup preparation and recover source read failures (#1568)
* fix(migration): show startup preparation and recover source read failures * fix(migration): keep inventory reload failures recoverable * test(migration): type the deferred recovery hook * fix(migration): reconcile failed EPG cleanup before recovery * fix(migration): await settings before inventory readiness * style(migration): add a subtle theme-aware startup watermark |
||
|
|
97b0264dee |
fix(xtream): render catch-up start times in the panel timezone (#1563)
* fix(xtream): render catch-up start times in the panel timezone
The `{Y-m-d:H-M}` segment of an Xtream timeshift URL is read by the panel
with `strtotime()` in ITS timezone (`server_info.timezone`), never the
viewer's. The timezone was learned in memory only, by the store's
`checkPortalStatus()`, so the Favorites / Recent catch-up resolver — which
reads the STORED playlist row — always fell back to the viewer's local
clock and asked the panel for the wrong programme (#1562).
- Normalize the panel's clock once (`resolveXtreamServerTimezone`): an
ICU-resolvable name is kept, otherwise a `UTC±HH:MM` offset is derived
from the `time_now` / `timestamp_now` clock pair, so spellings such as
`UTC+3` no longer silently mean "local time".
- Persist it on the playlist row through `transformPlaylistMeta` (no-op
when unchanged) and project it back from the payload in
`DB_GET_PLAYLIST`, so both catch-up entry points and a restart see it.
- Format with `hourCycle: 'h23'` (server midnight is `00`, never `24`) and
read timestamp-less EPG `start`/`end` strings in the panel's clock.
- Mock: `tzoffset:tzoffset` scenario with an unusable timezone name and a
+03:00 clock pair; Electron e2e covers Live TV, Favorites, a restart into
Global favorites, and the clock-pair derivation at a UTC-3 viewer.
Closes #1562
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream): guard the account-info answer by playlist identity and reject rolled-over dates
Review follow-ups (Greptile):
- A source switch while `get_account_info` is in flight no longer hands
playlist A's status or clock to playlist B: the store is patched only
while the asking playlist is still selected, the timezone is persisted
under the asking playlist's id regardless, and a late failure cannot mark
the newly selected playlist unavailable.
- `parseNaiveUtcMs` reads the constructed date back, so out-of-range panel
strings (`2026-13-01 25:00:00`) are rejected instead of silently rolling
over into a real instant.
- Document that a clock-derived fixed offset is a DST-less snapshot, refreshed
by every account-info check and only ever used for non-standard servers.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream): drop a panel clock that no longer belongs to the source
Review follow-ups (Codex + Greptile):
- A metadata update or DB_UPDATE_PLAYLIST that points the source at another
server drops the persisted `serverTimezone` (payload-only) until the next
account-info check, so Favorites / Recent cannot keep rendering the OLD
panel's clock; an update that supplies a clock keeps it.
- A late account-info answer is persisted only onto a row that still points
at the panel it came from — an edit that moved the source during the
request keeps the clock the edit flow dropped.
- The PWA data source and the route-session converter carry the persisted
timezone into the store playlist, so a later response without a usable
clock has a previous value to preserve.
- Mirror the catch-up timezone contract into AGENTS.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream): drop the stale panel clock inside the UPDATE statement
Review follow-up (Codex): the database worker interleaves requests, so a
read-modify-write of the playlist payload could hand a concurrent upsert's
newer payload back to the past. The `serverTimezone` removal on a server
URL change is now one `CASE … json_remove(payload, '$.serverTimezone')`
expression inside the same UPDATE, guarded by `json_valid`; the spec runs
the real statement against Electron's SQLite on the actual `playlists`
table (moved, renamed, clock-less, malformed-payload and NULL-URL rows).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(xtream): split the server-clock primitives out of the timezone util
Review follow-up (Greptile): `xtream-server-timezone.util.ts` had grown past
the 300-line file guideline. The zone-agnostic wall-clock primitives (stored
forms, Intl parts, naive parsing) now live in `xtream-server-clock.util.ts`;
the timezone util keeps the Xtream policy and re-exports the public helpers,
so every import and the spec are unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream): offer the learned panel clock to storage on every check
Review follow-up (Codex): a transient storage failure left the clock in the
store but not on the row, and the next check compared the answer with the
in-memory value and never retried. The resolved timezone is now always
handed to `transformPlaylistMeta`, whose row-level equality check keeps the
common case a read without a write; a failed write is retried by the next
account-info check.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream): apply an account-info answer only to the panel it came from
Review follow-up (Codex): an in-place edit keeps the playlist id while
moving the source, so an answer already on the wire for the OLD panel
passed the id-only guard and patched the new panel's status and clock into
the store. One `answersFor(candidate, credentials)` predicate now gates the
store patch, the error path and the persisted-row transform alike.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream): never report another panel's status for the selected playlist
Review follow-up (Greptile): callers gate content initialization on the
value `checkPortalStatus()` returns for whatever is selected NOW. When the
answer no longer describes the selected playlist (source switch or in-place
edit during the request), the store's own verdict about the current
selection is returned instead of the old panel's status — on success and on
failure alike.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream): persist the panel clock with one conditional UPDATE
Review follow-up (Codex): `transformPlaylistMeta` reads the row and then
upserts it whole, while the Xtream edit dialog saves through
`DB_UPDATE_PLAYLIST` outside `PlaylistsService`'s queue and the database
worker interleaves requests — an edit landing between that read and the
upsert was silently undone.
Persistence now goes through `IXtreamDataSource.rememberServerTimezone`:
- Electron: new `DB_SET_PLAYLIST_SERVER_TIMEZONE` worker op — one UPDATE
that `json_set`s the payload only while the row still points at the
request's connection and does not already carry the value; a malformed
payload is never rewritten (CASE, not AND, so json_extract cannot run
before json_valid). Wired through the worker types, main handler,
preload, bridge interface, both IPC contract tables and
`DatabaseService.setXtreamPlaylistServerTimezone`.
- PWA: `transformPlaylistMeta`, whose read and write share one IndexedDB
readwrite cursor transaction, plus the localStorage copy.
The store no longer injects `PlaylistsService`; it offers the resolved clock
to the data source and keeps only its in-memory guards. Real-SQLite coverage
for the op (fresh / same / moved / NULL / malformed / missing rows),
delegation specs for both data sources, docs updated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(xtream): keep the stored panel clock across clockless full upserts
Review follow-up (Codex): a `PlaylistsService` mutation that read the row
before `DB_SET_PLAYLIST_SERVER_TIMEZONE` landed and upserted afterwards
replaced the payload with its clockless snapshot. `DB_UPSERT_APP_PLAYLIST(S)`
now carry the STORED clock into a snapshot that has none while the row still
points at the same connection (`playlistConflictUpdate`, nested CASE so the
json_* readers never run on a malformed payload); a snapshot with its own
clock, or one that moves the source, wins as is. Real-SQLite coverage for
kept / moved / own-clock / batch rows.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(release): split capture-navigation under the max-lines cap
`tools/release/capture-navigation.ts` had grown to 567 counted lines, past
the 400-line rule, which failed `release-tools:lint` and — because the file
was not in the baseline — the max-lines baseline test on master and on
every PR branched from it. The 19 named setup actions are now grouped by
subject over one leaf module of shared page helpers:
- `capture-navigation-helpers.ts`: playlist-id registry, dialog handling,
navigation moves, `settleUi`
- `capture-navigation-setup-actions.ts`: add-playlist dialogs, settings
sections, remote control
- `capture-navigation-portal-actions.ts`: portal catalogs, live lists,
alternative sources (the two identical live-category flows share one
helper)
- `capture-navigation-download-actions.ts`: the download manager shots
- `capture-navigation.ts`: the `runAction` dispatcher, theme switching and
the re-exported API the seeding driver and the capture script import
Actions call their siblings directly instead of recursing through
`runAction`, so no module depends on the dispatcher. The action vocabulary
is unchanged (same 19 names, same waits and timeouts); every file is under
300 lines and the new modules are listed in the `release-tools` lint target.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(electron): move the panel-clock SQL into its own operations module
Review follow-up (Greptile): the timezone persistence, invalidation,
upsert-preservation and row projection had landed in
`playlist.operations.ts`, a baselined 1,000-line file. They now live in
`playlist-server-timezone.operations.ts` (155 lines) — the three SQL
shapes plus the payload projection — and the playlist operations compose
them; the baselined file shrinks by 107 lines. Behaviour and the
real-SQLite coverage are unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
98da686cea |
feat(website): add the phone remote control guide
New guide at /blog/remote-control-guide/: enabling the remote in Settings, opening it on a phone from the QR code, what each control does and which list it navigates, a checklist for a page that does not load, and why the remote must stay on the local network. Eight FAQ entries. The remote-control feature page now links to it instead of the M3U guide. Both screenshots are mock-backed. The phone view is the first "browser" shot: a manifest entry names a loopback URL and a mobile viewport, and the capture frames it in a separate Chromium page behind the same network and content guards, with the manifest validator accepting loopback origins only. The setup saves the remote-control setting so the app's own server answers, then selects a live channel; the Xtream mock's marketing scenario now serves live stream URLs from local bytes so that selection never leaves the machine. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
c7f1784dbd |
feat(website): add the alternative sources guide
New guide at /blog/alternative-sources-guide/: where the Sources chip comes from (a local lookup across the reader's own Xtream playlists, never a search outside them), how to read fact tags versus ~guesses, check availability, switch playlists mid-film without losing the timecode, pin a preferred copy per movie, and what the opt-in auto-switch does and on which players. Eight FAQ entries, opening with the general ContentDisclaimer. The two screenshots are mock-backed: the Xtream mock gains a marketing2 scenario that serves the identical marketing catalog under a second credential pair (with a spec proving the catalogs match), the capture seeds it as a "Fictional Xtream Backup" source only for shots that walk into it, opens its category so the movies reach the local content cache that discovery reads, and two new setup actions open the chip and the checked popover. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
0d03140661 |
feat(website): add the offline downloads guide with a reusable content disclaimer
New guide at /blog/offline-downloads-guide/: what the desktop download manager can save, choosing the folder, downloading a movie, episodes and seasons, following the queue (pause, resume, automatic reconnects), the offline library, and the Needs attention states, with a nine-question FAQ. The prose frames the feature as offline viewing of content the reader already streams and defers legality to the provider's terms and local law. ContentDisclaimer.astro carries that notice in a general and an offline variant so later guides reuse it instead of rewording it. The three screenshots are mock-backed captures. The Xtream mock's marketing scenario now serves movie and episode stream URLs from generated local bytes (downloadStreamFixture: 'local-media'), because the capture's network gate rejects the public HLS stub every other scenario redirects to; the capture stubs Electron's folder dialog so "Change Folder" authorizes a folder inside the isolated data dir rather than the real OS Downloads folder; two new setup actions queue a movie and two episodes and open the manager and the offline detail. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
15c2ac1f39 |
feat(packaging): add AppManager discovery metadata to AppImages (#1559)
* feat(packaging): add AppManager discovery metadata to AppImages * test(xtream): restore live queue URL service mock * test(xtream): extract live layout component stubs |
||
|
|
7d1503fd31 |
feat(epg): rebuild the programme guide for M3U playlists (#1560)
* docs(epg): add programme guide redesign spec for the M3U host Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(epg): add programme guide implementation plan Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(epg): add window-scoped guide programme queries Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): harden guide query scoping, caps and row mapping - Scoped guide programme/coverage queries now include legacy (unsourced) rows via source_url IN (...) OR IS NULL OR '', mirroring EpgQueryService's legacy fallback. - getProgramsForChannels/getProgramCoverage build their result from the normalized, capped window.channelIds instead of the raw request, so a key cut by the cap is absent rather than [] — an invalid window now returns {}. Truncation logs counts only. - Split the 100-channel guide cap from a new 2000-key coverage cap, and cap sourceUrls at 50; normalizeGuideWindow takes the cap as a parameter and moved (with guideWindowOverlapSqlText) into epg-guide-window.util.ts. - Extracted shared row mapping (toEpgProgramFromRow/isValidEpgProgram) into epg-program-row.util.ts, used by both EpgQueryService and EpgGuideQueryService so invalid start/stop rows are dropped identically in both. - Added a real-SQLite-backed test for the overlap predicate's exact text, plus per-key array copies in the response. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): render the guide predicate in tests and document its scope Correct the guide query's JSDoc: it runs one query accepting the union of requested-source and unsourced legacy rows, unlike EpgQueryService's two-query scoped-then-legacy fallback. Replace the hand-maintained plain-SQL twin of the Drizzle overlap predicate with a rendered copy of the real predicate (SQLiteSyncDialect().sqlToQuery) in the spec, add a source-scoping case, and drop the now-redundant operator-sequence test. warnIfTruncated reuses uniqueTrimmedStrings and names which read (programme/coverage) was truncated. Rename epg-query.service.ts's local EpgProgramRow to EpgProgramSelectRow so it isn't confused with the shared EpgProgramRow type, and document getProgramCoverage like its sibling. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(epg): expose guide programme and coverage reads over the bridge Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(epg): separate coverage chunk size in the guide plan Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(epg): add guide source contract, day layout maths and preferences Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): key guide IPC answers by trimmed, present keys only Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(epg): guide search hits carry a row id Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): make guide geometry DST-safe and tighten the contract Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(epg): cache guide programmes per day with batched loading Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(epg): add guide keyboard navigation controller Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): make guide programme cache robust to first-run effects and coverage failures Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(epg): add the programme guide grid components Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(epg): add a Guide button to the timeline toolbar Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(m3u): adapt the playlist channel list to the guide contract Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(m3u): guard the guide's initial group scope and track language changes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(m3u): open the programme guide in place with a docked player Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): scope guide keys to the grid, clip the now-line and re-measure on resize Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(epg): remove the multi-EPG overlay and the channel-range IPC Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(epg): document the programme guide and its release note Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * i18n(epg): translate the programme guide Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(m3u): let the guide own the keyboard and gate its entry points While the programme guide is open the docked player carries `data-player-shortcuts-suspended`, which `ControlsShortcuts` now honours alongside `[inert]` — the arrows moved the player's volume instead of the guide's row focus. The external-player strip loses its Collapse toggle (nothing to reveal, no preference to write), the header action and its palette command report `disabled` when the guide cannot open, the docked strip derives its programme from the active channel's own schedule instead of the retained NgRx value, switching playlists closes the guide, and the collapsed strip can reach 48 px on phones. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(m3u): keep the sidebar mounted while the guide is open Guide mode wrapped the sidebar in `@if (!guideOpen())`, so opening the guide destroyed `app-channel-list-container`, whose `ngOnDestroy` dispatches `resetActiveChannel()`. That cleared the active channel, which unmounted the block hosting `app-epg-guide` and tripped the `!canOpenGuide()` effect into closing the guide again: the guide never appeared and the page dropped to "Please select a channel". The sidebar now stays mounted and is hidden with `.sidebar--guide-hidden` plus `inert`, so it is neither focusable nor read by assistive technology while the guide owns the layout. Hiding also preserves the channel list's scroll position across guide toggles. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(e2e): cover the programme guide flow Imports a two-channel playlist with XMLTV, opens the guide from the timeline toolbar and asserts the row list, the "Only with EPG" filter, a channel switch that keeps the guide open, the hidden-but-mounted sidebar, and that the player element survives both the mode and channel switches. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(epg): tidy guide docs, palette gating and the unbound output Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): match guide favorites by channel URL and skip re-activating the playing row Favorites are persisted by channel URL (FavoritesActions.updateFavorites), so the Favorites scope compared the wrong key; the id stays as a legacy fallback. A double-click arrives as click, click, dblclick and each activate restarts playback, so the guide now leaves the already-playing row alone and the commit path only closes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * perf(epg): let the guide window predicate use the programme time index Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(m3u): stabilise guide row identity, seed the sidebar group and provide translations in every player fixture Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(epg): split the guide shell, add a roving focus model and offset-aware search times The shell component now owns rows, focus and the viewport only: the day, zoom, density, filters, clock and day geometry move to EpgGuideViewState, and every programme-dialog entry point to EpgGuideDialogController. Keyboard navigation is reachable by assistive technology: exactly one grid cell carries tabindex="0" (the focused cell, else the playing row's channel cell, else the first row's), the guide moves DOM focus with it after each handled key, a click hands the roving index to the clicked cell, and the viewport, rows and cells expose grid/row/gridcell roles. Search results were formatting raw provider instants, so they ignored the EPG display offset; they go through getProgramTimeMs like every other time the guide renders. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(m3u): make guide row ids collision-proof and gate the G shortcut Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): keep guide keys on the grid, reconcile focus with filtered rows and wrap the toolbar Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(epg): describe guide row ids as scope-local Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): clear guide search on scope change, match the active duplicate by url, keep failed coverage unknown Search hits carry scope-local row ids, so a scope change drops them. Two playlist entries can share an id but not a stream, so the active row is matched by id + url before falling back to the id. A failed coverage query now rejects instead of answering an empty set, which the guide already treats as "coverage unknown" (every row stays visible). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): tell duplicate guide rows apart by group, keep G out of dialogs, use prototype-safe answers The store spreads the selected channel, so the active row is matched by id, url, group and name before widening; G no longer closes the guide from a dialog or menu; guide answers use null-prototype records so a key named __proto__ stays an own property. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): let coverage reject on lookup failures and compare whole entries for the active guide row EpgQueryService.getChannelMetadata swallowed database errors into {}, so the guide's coverage read could publish an empty set after a transient failure; the guide now uses the strict resolveChannelMetadata (getChannelMetadata is the fail-soft wrapper around it). The active guide row is matched on the whole channel entry (all fields except the reducer-rewritten epgParams) before widening to url and id, so copies that differ only in playback headers or logo are told apart. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): let the guide return catch-up to live and normalise programme-search rows The guide source contract gains an optional livePlayback signal: while the host plays a catch-up URL, the active row may be activated again, which is how the M3U host returns to live. EPG_DB_SEARCH_PROGRAMS now maps the raw snake_case rows to the EpgProgram shape the bridge promises (plus the joined channel name), so search hits resolve their channel and keep descriptions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): name search hits, keep guide coverage strict on mapping failures - Search results and the unresolved programme dialog show the channel's display name (playlist row name, else the XMLTV display name the search joined in) instead of the raw XMLTV id. - The guide coverage read resolves manual mappings through a strict variant that rejects on database failure, so a mapped channel can never be reported as uncovered and hidden by "Only with EPG". - Architecture doc describes the tiered active-row resolution. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): offer the Guide action in the list view too The EPG list view mirrors the timeline's input/output contract, but the Guide action was bound only in the timeline branch, so Settings → EPG → Guide view = List lost the in-panel entry point. The list toolbar now carries the same icon-only Guide button behind `guideAvailable`/`openGuide`, and the M3U host binds it in both branches. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
0a2373f192 |
feat(portals): fold live TV panels in nested levels with a category dropdown (#1556)
## Summary Live TV panels now fold from the outside in, in three nested levels, instead of one toggle that hid the categories rail and the channel list together: 1. **Categories + channels + player** (browse, unchanged). 2. **Channels + player** — a new `chevron_left` in the categories rail header hides only that rail. The channels header then turns its title into a **category dropdown** that opens the same shell panel as a popover (search, sort, counts, selection are one implementation), plus a `chevron_right` that brings the rail back. 3. **Player only** — the channels header chevron, as before. The floating restore handle and `Cmd/Ctrl+B` return to the level the user collapsed from, not always to level 1. Every level is restored as stored, per surface (`live-sidebar-state:<surface>`, from #1555): a hidden rail is discoverable through the workspace header toggle and the hidden-list empty state that #1555 added, so this PR no longer needs its original "player-only never restores" rule. The level `Cmd/Ctrl+B` comes back to is seeded from the restored level and kept for the session. ## Design notes - Nested levels rather than two independent booleans: "channels hidden, categories visible" makes no sense since a category click has to bring the channels back anyway. The model follows the outside-in collapse of three-pane apps (Mail, Slack, Plex). - The categories rail folds at level 2 **only while a category is selected**: the live root ("All Items" grid) has no channels header to host the way back, so folding there would strand the user. Level 3 folds it regardless, because the floating restore handle lives in the content area. - `LIVE_CATEGORIES_POPOVER` (`@iptvnator/portal/shared/util`) is the DI bridge: the workspace shell provides `WorkspaceLiveCategoriesPopoverService` (CDK overlay hosting `WorkspaceContextPanelComponent` in `presentation="popover"`), the Xtream and Stalker live layouts inject it optionally and keep their plain heading without a provider. - M3U and the unified live tab have no categories rail and treat level 2 like level 1; their code is untouched. ## Merged with #1555 (per-surface rail state) #1555 landed while this PR was open and reworked the same service: state per surface (`m3u` / `portal` / `collection`), a workspace header toggle, the hidden-list empty state, and the legacy shared key forgotten on startup. This PR keeps that model and layers the three levels onto the `portal` surface (`areCategoriesHiddenFor`, `hideCategories` / `showCategories` / `collapse` / `expand` per surface; `toggle(surface)` returns to the level the surface collapsed from). "Show playing channel" uses `expand('portal')` so it keeps a deliberately hidden categories rail folded, and the category sort preference moved to `PortalCategorySortStateService` so the popover copy of the context panel and the retained rail agree. ## Also fixed along the way - The channels header showed "Channels" instead of the category name: provider category ids are strings, the selection is numeric. Compared via `String()` now. - A collapsed context panel left a 22px padding strip beside the channels rail. - The panel toggle labels said "Hide channels list" while also hiding categories; labels and tooltips are honest now (8 new i18n keys, all 18 locales). Docs: `docs/architecture/iptvnator-ui-guidelines.md` ("Collapsible Live Sidebar" rewritten), `docs/architecture/workspace-shell.md`. Release note: `.changes/portals-live-panel-collapse-levels.md`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
0dcfba7045 |
fix(live-tv): keep a hidden channel list discoverable and scoped per surface (#1555)
* fix(live-tv): keep a hidden channel list discoverable and scoped per surface The second report in #1458 ("all channels disappear after clearing the playback history, reset does not bring them back") was not data loss: the history write never touches playlist items. The reporter's screenshot shows a collapsed channel rail, a state persisted under one localStorage key shared by the M3U player, the Xtream/Stalker live layouts and the favorites/recent live tab. It survived restart, "Remove all playlists" and re-import, and the only way back was a 32px chevron or Ctrl/Cmd+B. - LiveLayoutSidebarStateService keeps the state per surface (m3u / portal / collection) under live-sidebar-state:<surface>; the M3U player now goes through the service instead of its own signal. The legacy shared key is forgotten on startup and never read, so the update itself restores the list for everyone who got stuck. - The workspace header renders a view_sidebar toggle on every route that renders its own rail (M3U all/groups, Xtream live, Stalker itv/radio), so the control exists in both states instead of disappearing with the rail. Collection pages keep their own toggle beside the content switch. - While the rail is collapsed and nothing plays, every live host shows app-channel-list-hidden-state (title, shortcut hint, full-size "Show channels list" button) instead of asking to pick from a list that is not on screen. app-portal-empty-state gained optional hint/action inputs. - New LAYOUT.CHANNELS_LIST_HIDDEN(_HINT) strings in en plus 18 locales. Tests: service, empty-state, hidden-state and header component specs, a separate video-player-sidebar spec (the main M3U spec sits at the test line budget), and an Electron E2E covering history clearing, restore via button/header/shortcut across restart and re-import, per-surface scoping against an Xtream portal, and legacy-key cleanup. Refs #1458 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(live-tv): mirror the EPG offset setting in the sidebar spec mock Master's player reads `resolvedEpgOffsetMinutes` from the settings store; the new sidebar spec was cloned from the movie-gate harness before that field landed, so its playing-channel case threw inside the EPG effect. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(web-e2e): scope the Stalker radio rail toggles to the rail The workspace header now carries a second "Hide/Show channels list" toggle, so the role+name locators matched more than one button and tripped Playwright's strict mode. Target the rail's own chevron and the floating restore button, and assert the header toggle mirrors the state. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(live-tv): honour Cmd/Ctrl+B on collection pages and hide the header rail toggle on phones Codex review follow-ups on #1555: - The hidden-list state advertises Cmd/Ctrl+B, but the favorites/recent collection page had no handler; only the routed M3U/Xtream/Stalker live layouts did. The page now toggles the collection surface while its live tab is on screen, with the same typing/inert guards as the other hosts. - At the phone breakpoint the header already holds the drawer toggle, switcher, search and Add; the live rail is a bottom drawer with its own toggle there, so the header rail toggle is hidden below 640px. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(live-tv): migrate the live navigation helpers to the per-surface sidebar API master (#1554) added `XtreamLiveChannelNavigationService` and `stalker-live-navigation.ts`, which expand the rail through `sidebar.setState('expanded')` on the pre-split signature. Point them at the `portal` surface and update their specs; drop the now-unused hidden-state stub from the Xtream layout spec, which master pushed to the max-lines budget. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
436825bdec |
fix(xtream): try advertised TS after initial web HLS HTTP failure (#1558)
* fix(xtream): try advertised TS after initial web HLS HTTP failure * refactor(playback): extract fullscreen channel panel state * test(xtream): keep synthetic media within the mock project |
||
|
|
ef2418a2c5 |
feat(epg): compact timeline toolbar with icon-only Now and cycling zoom (#1557)
* feat(epg): compact timeline toolbar with icon-only Now and cycling zoom Replace the 96px zoom range slider with a single icon button that cycles day overview → by hour → detailed, make the Now button icon-only (label in tooltip + aria-label), and add Ctrl/⌘ + wheel zoom around the cursor on the ribbon (preventDefault keeps Chromium from page-zooming the same gesture). Zoom math moves into TimelineZoomController so the component stays under the max-lines limit. The channel/programme heading no longer carries a 60% width cap and takes the space the controls free up. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(epg): coalesce wheel-zoom anchoring and make the e2e zoom reset deterministic - TimelineZoomController keeps a logical pending scrollLeft across a burst of wheel/pinch events and applies it in one animation frame, so every event anchors on the same minute instead of the stale DOM position - the e2e settles on the detail preset from whichever band the wheel landed in (contrast checks need the block's time line, hidden at narrower tiers) - zoom tooltip advertises ⌘/Ctrl per the repo's shortcut convention Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
61b06b9f31 |
fix(portals): preserve live channel navigation while browsing (#1554)
* fix(portals): preserve live channel navigation while browsing * test(portals): await media source assertion in remote E2E * fix(xtream): capture destination queue for live auto-open |
||
|
|
5a8c5ca4a4 |
fix(stalker): keep live search within the selected category (#1552)
* fix(stalker): keep live search within the selected category * test(stalker): assert retained video ownership without source timing * test(stalker): distinguish paged All Items from the initial cache grid * fix(stalker): reveal remote selections in uncached search results * test(stalker): wait for category rows and retain settled playback |
||
|
|
5febe28eba |
fix(web-backend): validate and pin provider redirect hops (#1553)
* fix(web-backend): validate and pin every provider redirect hop * fix(web-backend): separate provider metadata from connection authority |
||
|
|
9de480826c |
fix(epg): remove cached XMLTV data after source deletion (#1548)
* fix(epg): remove cached XMLTV data after source deletion * fix(epg): close source reconciliation review races * fix(epg): serialize cleanup with replacement imports * fix(epg): report retired worker exits as cancellations * fix(epg): preserve source metadata through cache cleanup * refactor(epg): separate worker runtime and import lifecycle * fix(epg): skip cleanup for unchanged source settings * fix(epg): cancel retired error rows and pending retries * fix(epg): redact diagnostics and mirror committed settings after cleanup errors * fix(epg): preserve metadata writer order independently of timestamps |
||
|
|
baba0529ef |
feat(website): rebuild the hero, features, download and support sections (#1551)
* feat(website): rebuild the hero, features, download and support sections Third landing redesign PR: the home page now speaks the app's own language instead of a template's. - Hero: left-aligned headline, a primary button that follows the visitor's OS (server-rendered fallback goes to /download/), an "All platforms" button and a text link for self-hosting. Project numbers (stars, downloads, languages, license) are set in the page's own typography from the GitHub API at build time (`lib/github-stats.ts`); anything the build cannot resolve is left out rather than faked. A small "Now playing" card with fictional demo content replaces the mascot, badges and social chips; the screenshot is lit by a blurred copy of itself, like the player's ambient mode. - Features: a bento of interface fragments (EPG rows with progress, posters with a resume bar, a download queue with a REC dot, a remote D-pad) instead of numbered cards with icon watermarks and a marquee. The marquee CSS is gone. - Download: one row per platform with the release's file names and a "Detected" badge for the visitor's OS (`lib/detect-platform.ts`), package-manager commands in the same panel. - Support becomes a single row; the disclaimer moves into the footer, which also gets the mascot and a link list. - Home sections drop the decorative eyebrows and the divider rules; the unused broadcast-wave, support-signal and watermark illustrations are removed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(website): cover the rebuilt home sections and the OS-aware download CTA `website-home-sections.test.mjs` (in the website test target) reads the built home page — generic hero CTA to /download/, project facts, no badge images or dashed borders, every feature page linked from the bento, one download row per platform with release file names and the Detected badge hidden, the disclaimer and mascot in the footer — and then drives it in Chromium under Windows, macOS, Linux and Android user agents: the primary button and the Detected row follow the platform, the phone keeps the generic markup, and the copy button writes the command to the clipboard. `detectPlatform()` now consults the user-agent string before the client-hints platform and the deprecated `navigator.platform`, so the source a visitor's tools actually change decides first; silent sources fall through instead of vetoing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(website): keep iPads on the generic download path iPadOS asks for desktop sites with a Macintosh user agent and a `MacIntel` platform, so the OS guess sent iPad visitors to the macOS installers. A "mac" verdict is now trusted only on a device without touch points: no Mac has a touch screen, every iPad reports several. The browser test adds an iPad-in-desktop-mode row to the generic-device matrix next to the Android phone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
9bcdbc0efb |
fix(migration): preserve and recover legacy desktop sources (#1550)
* fix(migration): recover legacy desktop sources without replacing current data * test(migration): cover legacy recovery IPC contracts * test(migration): use static legacy Electron bootstrap |
||
|
|
002faafbd7 | fix(xtream): improve sync overlay contrast in both themes (#1546) | ||
|
|
e40f31db97 | fix(host-health): retain trial ownership until requests settle (#1547) | ||
|
|
0140146716 |
fix(ui): restore detail surface boundaries in light theme (#1549)
* fix(ui): restore detail surface boundaries in light theme * test(ui): measure detail action edges over rendered artwork |
||
|
|
eb96b4c9f2 |
feat(website): turn the screenshot showcase into a channel switcher (#1540)
* feat(website): turn the screenshot showcase into a channel switcher Second landing redesign PR. "See it in action" was a tab strip in a dashed frame showing one screenshot inside a drawn window chrome that duplicated the chrome already in the shot. It is now a channel list: number, screen name and one line about what the screen is for on the left, a single frame on the right, and a caption linking to the matching feature page. - Channels advance on their own with a progress hairline under the active row; hovering, focusing or scrolling the block out of view pauses it and `prefers-reduced-motion` disables autoplay entirely. - A brief on-screen "CH 03" badge confirms every switch. - Arrow keys, Home and End move between channels; the list is a proper vertical tablist with roving tabindex, panels carry `aria-hidden`. - Six screens: Dashboard, Live TV, Program guide, Movies & series, Downloads, Settings. Same files from `public/screenshots`; the add-playlist shot is replaced by the movie detail and the download manager. - On phones the screen comes first and the list follows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(website): cover the channel switcher, keep focus pausing after mouseleave Hover and focus now pause autoplay independently: clicking a channel focused it, but moving the pointer away resumed the timer and seven seconds later the selection moved under a still-focused tab. New `website-screenshot-showcase.test.mjs` (in the website test target): a structural half over the built HTML (vertical tablist, roving tabindex, one aria-hidden panel per channel) and a browser half that serves the build and drives it in Chromium — autoplay advances, hover pauses, click + mouseleave keeps the pause while focused, arrow/Home/End keys move selection, focus, panel, caption and badge together, blur resumes, and `prefers-reduced-motion` disables autoplay. The browser half falls back to the system Chrome channel and skips when no Chromium exists, so the structural checks run everywhere. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * perf(website): lazy-load every showcase screenshot The block sits below the hero and the feature grid, so an eager first frame only competed with above-the-fold assets for visitors who never scroll to it. Native look-ahead loading brings it in well before the switcher is on screen. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(website): share the browser harness, fail in CI without Chromium `website-browser-support.mjs` owns the loopback static server and the Chromium launcher for the website browser tests. The server resolves every request against the build root and answers 404 for anything that escapes it (CodeQL js/path-injection on the previous inline copy). The launcher tries the Playwright download, then the system Chrome and Chromium channels; when none launches it returns null locally so the structural half still runs, and throws under `CI` so the interaction assertions can never turn into a silent skip on the runner. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * perf(website): defer inactive showcase frames, document the browser tests The six frames are stacked with opacity, so native lazy loading treated all of them as near-viewport and fetched every screenshot at once. Only the first frame now ships with a `src`; the switcher assigns it from `data-src` when a channel is shown and preloads the one after it, so at most two frames are ever in flight. The showcase test asserts the markup and the runtime behaviour. The website README now describes the browser-dependent suites, the shared harness, and the skip-locally / fail-in-CI rule. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(website): describe only the suite this PR adds Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(website): resume the switcher from where it paused, keep DOM order on phones The dwell clock now stops while the switcher is hovered or focused and the pause time is added back on resume, so the progress hairline continues from its frozen position instead of snapping to zero and granting a fresh seven seconds. The test asserts the resume. On phones the list stays before the screen in the DOM and on screen (tabs before their panels, focus order equals reading order); it hides the per-channel descriptions and the keyboard hint there so the screen stays close instead of being reordered with `order-first`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(website): never crop a showcase frame The screen box stretches to the channel list's row height, and with `object-cover` a wide screenshot lost its right side just above the `lg` breakpoint. Frames are now contained on a dark stage (a letterbox, as on a TV), and the per-channel descriptions are hidden between `lg` and `xl` so the row stays close to the frame's own height. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(website): pause the switcher dwell while offscreen instead of resetting it Leaving the viewport is now a pause like hover and focus: the frame loop stops, the progress hairline keeps its width, and the clock resumes from the same mark when the block scrolls back in. Only a channel change resets the dwell. The interaction test scrolls away and back to assert it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * perf(website): no next-frame prefetch when reduced motion disables autoplay Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(website): persistent pause control for the channel switcher Hover and focus only pause while they last, which is no use to touch or screen-reader visitors, so the list footer now carries a Pause/Resume toggle (`aria-pressed`, full `aria-label`) that keeps autoplay stopped until pressed again (WCAG 2.2.2). It is removed under reduced motion, where nothing advances. The interaction test now waits for the seven-second rollover and checks that tab, panel, caption, badge and the preloaded frame all move to channel 02, and that the toggle holds through mouseleave and blur. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(website): keep the tablist to its tabs, no successor prefetch when autoplay is off The channel list's header and the Pause/Resume control sat inside the `role="tablist"` container; the tablist now wraps only the six tabs so assistive technology reads the toggle as an ordinary button beside the list. `show()` preloads the next frame only while autoplay can reach it (neither reduced motion nor the toggle has stopped it). Tests assert both. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(website): preload the successor when auto-advance resumes A channel picked while the switcher is paused deliberately skips its successor; resuming now fetches that frame so the next automatic switch does not land on a blank screen. The interaction test covers pause → manual selection → resume. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(website): Resume restarts autoplay at once; same-channel progress in the test The transient hover/focus pauses now watch only the channel list and the screen. The footer with the Pause/Resume control is not one of those regions, so after pressing Resume — with the pointer and the focus still on the button — autoplay visibly restarts instead of waiting for the visitor to leave the whole block. The interaction test compares progress within one channel (a paused Movies before and after Resume) rather than across channels, which could fail on a slow runner, and asserts that autoplay runs while the toggle keeps focus and hover. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * perf(website): one scheduler for the switcher, no frames while paused Every pause reason (hover, focus, the toggle, leaving the viewport) now goes through a single `sync()`: the animation-frame loop runs only while the block is visible and nothing pauses it, and is cancelled otherwise, so a switcher left paused schedules no frames at all. The dwell clock still resumes from where it froze, and the successor frame is fetched only when the loop actually starts — so scrolling away and back under a persistent pause loads nothing. Tests count scheduled frames while paused and cover pause → manual selection → offscreen → return. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(website): a channel picked while paused gets its full dwell after Resume `show()` reset the pause mark, so the time a visitor spent paused after picking a channel counted toward that channel's dwell and Resume could advance immediately. When the loop is not running the new channel now starts out paused at that moment. The interaction test asserts the progress is still near zero right after Resume. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(website): follow reduced-motion changes and hidden tabs in the switcher The reduced-motion preference is read from a live MediaQueryList: when it changes while the page is open the scheduler stops or restarts and the Pause/Resume control is hidden or shown (it is hidden, not removed, for that reason). A hidden document counts as a pause too — background tabs throttle animation frames while the clock keeps running, so without it the first frame back would skip a channel. The interaction test flips both at runtime. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(website): plain action button for the switcher pause, no manual animations under reduced motion `aria-pressed` on a button whose name changes between "Pause auto-advance" and "Resume auto-advance" announced the wrong thing; the control is now an ordinary action button whose name says what pressing it does next. The OSD slide and the panel fade get `motion-reduce:transition-none`, so a manual selection under reduced motion moves nothing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
249cd38a66 | fix(stalker): align season markers and preserve episode loading (#1545) | ||
|
|
812ec69fd4 |
feat(website): turn blog tags into topic hubs
Tags were decorative: a plain span on every card and post header, with no page, filter or search behind them, and a vocabulary of 32 slugs across 16 posts where 22 slugs appeared once. They now form a closed vocabulary of ten topics (src/lib/blog-tags.ts) that the content collection schema enforces, so an unknown slug fails the build instead of minting a new tag. Every used tag gets a hub at /blog/tag/<tag>/ with CollectionPage and BreadcrumbList structured data; the blog index and the hubs show a "Topics" rail with post counts; and every chip links to its hub. The cards become <article> elements with the title link stretched over the card, because chip links cannot nest inside a card that is one big <a>. Posts are retagged to the new vocabulary. A structural test covers the rail, each hub, the chip targets, the sitemap and the absence of nested anchors. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |