Commit Graph
77 Commits
Author SHA1 Message Date
4gray 48ff4b9cc5 fix(portal): remove redundant catalog type badges (#1217)
Remove redundant LIVE, VOD, and SERIES badges from homogeneous Xtream and Stalker catalog grids while preserving mixed-content badges and type-driven grid behavior.
2026-07-20 21:52:00 +02:00
4grayandClaude Fable 5 c707af1334 feat(epg): manual EPG mapping for Stalker portals (#1173)
* feat(epg): manual EPG mapping for Stalker portals

Extends the manual EPG-to-channel mapping (PR #1165) to Stalker:

- shared key helper buildStalkerEpgMappingKey — playlist-scoped
  stalker:{playlistId}:{channelId} keys, mirroring the Xtream scheme
- ITV sidebar: right-click context menu with "Map EPG channel"; after
  the dialog closes with a change, the bulk EPG cache is rebuilt so the
  panel and row previews reflect the new mapping immediately
- withStalkerEpg().applyMappedItvEpg(): batch-resolves mappings for
  rendered channels (one getEpgMappingsBatch IPC per new id set) and
  overlays uploaded-XMLTV programs onto bulkItvEpgByChannel; overrides
  survive ensureBulkItvEpg reloads
- stream-resolver: mapping check in loadStalkerEpgItems (detail) and
  batched prefetch in loadStalkerEpgBatch (previews);
  mappingCandidateKeys/prefetchEpgMappings generalized beyond Xtream
- global favorites: stalker branch for the Map-EPG menu entry (item id
  extracted from the stalker::{playlistId}::{id} uid)
- docs: manual-mapping section in docs/architecture/stalker-epg.md and
  a CLAUDE.md EPG bullet covering the whole mapping feature
- tests: applyMappedItvEpg suite, stalker preview mapping in the
  stream-resolver spec, key-builder specs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(epg): harden Stalker mapping edges found in adversarial review

- stream-resolver: stalker items resolve only the playlist-scoped
  mapping key — bare tvgId/name candidates (tvgId mirrors the raw
  provider id) could only produce false matches against unrelated M3U
  mappings, and previews would disagree with the detail path
- applyMappedItvEpg: staleness guard after every await so an in-flight
  call cannot write portal A's mapped EPG into portal B's state after
  a playlist switch (the store is a root singleton)
- applyMappedItvEpg: ids are marked checked only after a successful
  lookup — a transient IPC failure no longer suppresses the mapping
  for the rest of the session
- live layout: post-dialog refresh re-applies overrides for the
  unfiltered channel list, so an active search cannot drop the playing
  channel's mapping
- global favorites: new epgMappingChanged output emitted when the
  dialog actually changed a mapping; unified live tab reloads its EPG
  previews in response

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 18:29:23 +02:00
ec6fc403a3 feat: manual EPG-to-channel mapping with mapping fallback (#1165)
* feat(epg): manual EPG-to-channel mapping with mapping fallback in all EPG paths

* fix: epg mapping in live tv list

* fix(epg): harden manual EPG mapping — upgrade safety, perf, playlist-scoped keys

Follow-up fixes on top of the manual EPG-to-channel mapping feature:

- epg-database: dedupe existing epg_programs rows before creating the
  unique (channel_id, start, title) index — a plain CREATE UNIQUE INDEX
  crashed the EPG worker on upgrade when historical duplicates exist;
  replace INSERT OR REPLACE with ON CONFLICT DO UPDATE so the
  epg_programs_fts delete trigger is not bypassed (REPLACE skips delete
  triggers unless recursive_triggers is on), with a plain-INSERT
  fallback when the index cannot be created
- db: add idx_content_epg_channel — the mapping fallback scanned the
  whole content table on every single-channel EPG lookup
- keys: scope Xtream mapping keys per playlist via shared
  buildXtreamEpgMappingKey (xtream:{playlistId}:{id}) — bare stream ids
  collide across portals; the backend fallback now joins categories to
  resolve the playlist id
- pwa: hide "Map EPG channel" entries behind the supportsEpgMapping
  capability — the menu item was a dead end in the PWA
- parser: parse the XMLTV offset sign from the string — Math.sign(0)
  dropped the minutes of ±00:xx offsets
- cleanup: typed window.electron access instead of ad-hoc casts, drop
  unused resolveChannelId and dialog data field, shared
  EpgMappingDialogComponent.open() for all seven call sites
- dialog UX: minimum-characters search hint, save/remove snackbars,
  current mapping shows the EPG channel display name,
  takeUntilDestroyed on the search stream
- i18n: fill the new keys in all 17 locales
- tests: cover mapping CRUD/search escaping, the dedup-index guard,
  offset parsing and playlist-scoped keys; update stale stream-resolver
  specs for the new 50-item limit and 10s timeout

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(epg): escape backslashes in EPG channel search LIKE pattern

CodeQL js/incomplete-sanitization: a lone trailing backslash in the
search term paired with the closing wildcard under the ESCAPE clause
and corrupted the pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf(epg): batch mapping lookups in the viewport preview queue

Expose the existing getEpgMappingsBatch operation over a new
EPG_MAPPING_GET_BATCH IPC channel and use it in resolveManualMappings —
the per-entry lookup issued one IPC round-trip per visible channel on
every scroll event.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf(epg): batch mapping prefetch in the collection preview loader

Resolve all candidate mapping keys for an Xtream preview batch with a
single getEpgMappingsBatch IPC call instead of per-channel lookups.
Also fix a worker early-exit: a channel without tvgId/name returned out
of the shared-iterator loop and silently killed one of the three
concurrent preview workers.

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>
2026-07-19 09:00:14 +02:00
StefanandClaude 2f8aee72df feat(xtream): add catch-up playback to favorites and recent tabs (#1166)
Enables Xtream catch-up/timeshift from the Favorites and Recent surfaces (per-playlist and global), not just Live TV, and adds start-over replay of the currently-airing programme. Carries tv_archive/tv_archive_duration through the favorites and recently-viewed DB projections and maps them onto UnifiedCollectionItem; tv_archive_duration is interpreted as days, matching live-stream-layout.controlledArchiveDays.

Closes #1138.

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-19 06:38:19 +02:00
MahdiHrmandClaude Fable 5 643dee1be3 feat(xtream): resume the latest series episode (#1187)
Dashboard Continue Watching now carries the exact saved season/episode into
Xtream series details and starts it at the persisted offset. Successful
external MPV/VLC launches persist the launched episode and retarget the
series CTA to "Play episode N". Recent-history rows keyed by an episode id
resolve their parent series before navigation.

Includes maintainer follow-ups: no zero-offset resume on failed position
loads, seriesXtreamId-gated resume targets for legacy rows, and patch
coverage raised from 76.7% to 93.9%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 15:49:22 +02:00
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>
2026-07-18 15:43:37 +02:00
4grayandClaude Fable 5 09764e24d4 feat(portals): back button on the in-portal search page (#1142)
* feat(portals): back button on the in-portal search page

Opening a "grey" (unmatched) film from an actor page navigates to the
portal search prefilled with ?q=, but the search page had no way back —
users had to re-navigate through the sidebar to reach the actor's
filmography again.

SearchLayoutComponent gains a showBackButton input + backClick output;
both the Xtream in-portal search (SearchResultsComponent) and the Stalker
search wire it to Location.back(). Shown only on the nested in-portal
search — the top-level global search (sidebar destination) and the dialog
mode (own close button) are excluded. location.back() unwinds the full
history, so search → actor → movie detail → … all chain correctly.

Tests: search-layout spec covers the conditional button + backClick.
Docs: CLAUDE.md actor-pages note updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(portals): address search back-button review (dialog guard, RTL-safe spacing)

- showBackButton now also checks !dialogRef, so a hypothetical dialog
  opened with isGlobalSearch:false from a workspace route can't show a
  back arrow that navigates the page behind the dialog (greptile).
- Replaced the fragile negative margin with a header-title-group flex
  wrapper (gap: 4px) that keeps the arrow and title together regardless
  of the outer gap or Material padding, and is RTL-safe (greptile).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 08:20:34 +02:00
4grayandClaude Fable 5 c55acfb1d9 fix(epg): wire catch-up Watch button in unified live tab, surface resolve failures (#1133)
* fix(epg): wire catch-up Watch button in unified live tab, surface resolve failures

The favorites/recent live views (UnifiedLiveTabComponent) passed
archivePlaybackAvailable/archiveDays to the EPG timeline and list view, so
the "Watch" button and the dialog's "Watch from start" action rendered —
but neither (programActivated) nor (returnToLive) was bound. Clicking
Watch emitted an event nobody listened to: no error, no feedback, no
playback change.

- Handle programActivated in the unified live tab: resolve the catch-up
  URL via resolveM3uCatchupUrl, swap the inline player's playback target
  (or hand it to the configured external player), show the
  "Archive playback" summary label, "From archive" block tag and
  "Return to live"; clear the override on channel switch/close/return.
- Never fail silently: both the unified tab and the m3u-state effect now
  show an EPG.TIMELINE.CATCHUP_FAILED snackbar when the replay URL can't
  be resolved. New i18n key translated into all 17 locales via the
  i18n-fill workflow.
- Extract the effect's resolve branch into a pure
  resolveActiveEpgProgramAction util and the unified tab's EPG summary
  helpers into unified-live-epg-summary.util.ts (component shrinks
  598 → 572 lines despite the new feature).
- Regression coverage: 6 component tests for the catch-up flow,
  3 unit tests for the resolver action; docs updated in
  m3u-playlist-module.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(epg): address PR review — untrack progress tick in archive summary, open live external without prior timeshift

- liveEpgPanelSummary no longer tracks the 30s progressTick while an
  archived programme is shown (Greptile P2): the archive summary is
  frozen, so re-running the computed each tick only produced needless
  re-renders.
- returnToLivePlayback no longer early-returns when no timeshift is
  active (Codex P2): with an external player configured and
  openStreamOnDoubleClick enabled, the programme dialog's "Watch live"
  action now launches the external player instead of being dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 14:47:34 +02:00
4grayandClaude Fable 5 1c710d69ef chore(cleanup): delete nine dead components, orphaned i18n keys and unused deps (#1116)
* chore(cleanup): delete nine dead components, orphaned i18n keys and unused deps

Removes verified-dead components (0 class/selector references outside
their own files): EpgListComponent (+ epg-list-item), EpgViewComponent,
LiveEpgPanelComponent, StalkerCollectionChannelsListComponent,
NavigationComponent, FilterSortMenuComponent, video-player
ToolbarComponent, PortalCollectionShellComponent and
LoadingOverlayComponent, together with their barrel exports.

Alive code extracted from the deleted trees:
- LiveEpgPanelSummary -> libs/ui/shared-portals/src/lib/live-epg-panel-summary.ts
- EpgProgramActivationEvent -> libs/ui/epg/src/lib/epg-program-activation-event.ts
- epg-list.utils.ts trimmed to the three timeline-used helpers and moved
  to libs/ui/epg/src/lib/epg-program.utils.ts
- epg-item-description/ moved up out of the deleted epg-list/ folder

Also removes 18 i18n keys now unused (from all 18 locales), dead CSS
selectors targeting the deleted elements, and unused dependencies:
lodash (+ @types/lodash), semver, @ngrx/component-store and
@videojs/http-streaming (videojs-quality-selector-hls declares no peer
dependency on it; video.js 8 bundles VHS).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(coverage): move shared-portals to Tier C, fix stale doc references

The Tier A gate failed in CI because deleting the dead epg-view and
live-epg-panel components removed the only specs in libs/ui/shared-portals.
The lib now contains a single type-only interface (LiveEpgPanelSummary),
so there is no runtime code to unit test; reclassify it to Tier C with a
documented reason, matching the gate's own guidance.

Also update remaining doc references to the deleted components in
docs/architecture/stalker-epg.md, iptvnator-ui-guidelines.md and
CLAUDE.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 11:09:50 +02:00
4grayandClaude Opus 4.8 0b526b119a feat(epg): add vertical list view for the live EPG panel (#1115)
* feat(epg): add vertical list view for the live EPG panel

Add an EPG list view — a vertical, single-day programme list — as an
alternative rendering of the live EPG panel, selectable via a new
Settings → EPG → "Guide view" toggle (epgViewMode: 'timeline' | 'list',
default 'timeline' so existing users see no change).

- New EpgListViewComponent (app-epg-list-view) mirrors
  EpgTimelineComponent's input/output contract 1:1, so all four live
  hosts (M3U player, unified live tab, Xtream, Stalker) swap the panel
  with a plain @if and identical bindings.
- Reuses the shared view-agnostic EPG modules (classifyTimelineWhen,
  hasProgramsForDateKey, epg-archive.util catch-up gating,
  epg-summary.util collapsed-summary maths, epg-date helpers,
  EpgProgrammeDialogService, app-epg-timeline-empty-state) — no
  duplicated logic.
- Rows show time range, title, optional description, live progress on
  the on-air row, catch-up "Watch" on past rows when archive playback
  is available, and a details dialog; keyboard activation guards
  nested buttons (target === currentTarget).
- Auto-focuses the on-air row on channel select, restores it across
  collapse/expand remounts, and shows a sticky in-flow "On now" strip
  (never overlaying rows) when the current programme is scrolled away;
  all scroll maths is rect-based relative to the scroller.
- List mode raises only the inline panel height via an epg--list
  modifier (--epg-inline-height clamp); timeline and collapsed heights
  are unchanged.
- Setting flows end-to-end (Settings interface → DEFAULT_SETTINGS →
  SettingsStore/StorageMap → segmented control in the EPG section);
  Electron-only UI, PWA stays on the timeline default. i18n keys added
  to all 18 locales.
- Tests: new component/row/utils/scroll-controller specs, settings
  persistence spec, swap tests in all four host specs, and Electron
  E2E for the settings round-trip and the rendered list view. Docs
  updated (m3u-playlist-module.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(epg): address list-view review findings from Codex and Greptile

- Reset the list view to today when a new channel's programme set
  arrives while the user is parked on another day (timeline parity):
  the scroll controller now keys by the full programme-set identity
  (programsFocusKey) and commits today before focusing, instead of
  silently stranding the new channel on the stale day. (Codex P2)
- Centralise the 'timeline' fallback as a resolvedEpgViewMode computed
  on SettingsStore; the four live hosts consume the derived signal
  instead of duplicating the `?? 'timeline'` expression. (Greptile P2)
- Extract the component's reactive plumbing into
  registerEpgListViewEffects(), bringing the component back under the
  300-line guideline (290). (Greptile P2)
- Controller spec rewritten around programme-set fixtures with new
  coverage: return-to-today on channel switch, day navigation left
  alone, no-takeover when today has no data, empty-set no-op.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(epg): drop malformed programmes from the list-view day filter

Reject programmes whose stop is not after their start in
buildEpgListRows — same as the timeline's buildTimelineBlocks. Bad
provider data would otherwise render impossible time ranges and could
even be offered as catch-up playable. (Codex P2 on e6fd0d08)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 10:05:08 +02:00
4gray 401012eac4 feat(epg): rework live EPG panel into a horizontal timeline (#1102)
Replaces the vertical EPG list with a shared horizontal `app-epg-timeline`
ribbon across all live surfaces (M3U player, unified live tab, Xtream, Stalker):
zoom, day navigation, short-programme grouping, catch-up/timeshift, and
per-state empty views. Backend gains timezone-aware `datetime()` comparisons,
unscoped source fallback, non-ASCII candidate matching, and chunked candidate
queries.

Timeline split into reusable, view-agnostic modules (archive/summary/dialog
service/render util/scroll controller) for the future EPG list view.

Fixes landed during review:
- honor the controlled `selectedDate` input (seed via linkedSignal)
- restore ribbon position across collapse/expand
- keep the ribbon mounted when scrolling across a gap day
- don't trigger block playback on Enter from nested watch/info buttons
- don't reset timeshift playback on the 30s now-tick during EPG gaps

Greptile 5/5 (safe to merge); Codex clean; CI green.
2026-07-01 21:09:49 +02:00
4gray 89916af9d8 feat(search): add workspace global search with M3U support
Moves global search into a routed workspace view, adds M3U live/radio results, lazy pagination, and DB-backed matching improvements.
2026-06-20 23:05:48 +02:00
4gray 2edc430935 feat(xtream): add live all-items grid 2026-06-20 09:36:34 +02:00
4gray 45ee1ac3e4 refactor(portal-ui): use runtime capabilities 2026-05-22 11:47:33 +03:00
4gray c571c57c5b refactor(runtime): centralize platform capabilities 2026-05-22 11:00:34 +03:00
4gray 36bce47764 merge: resolve master conflicts for pwa hardening
- merge origin/master into PR #964 and keep embedded MPV test on the isolated playback sub-entrypoint

- centralize EPG capability through DataService.supportsEpg and update PWA web-e2e expectations

- split BrowserAccessError copy between Electron and PWA diagnostics
2026-05-22 10:20:03 +03:00
4gray 4ab8915483 chore(web): enable strict TypeScript mode 2026-05-22 02:58:12 +03:00
4gray 55efc24608 fix(pwa): harden self-hosted runtime boundaries 2026-05-22 02:09:57 +03:00
4gray 20e7322353 refactor(portal): split collection data access (#953) 2026-05-19 00:01:34 +02:00
4gray 118d4fce32 fix(playback): improve inline stream diagnostics (#952)
* fix(playback): improve inline stream diagnostics

* fix(playback): preserve headers in diagnostics wrapper

* fix(portal): align unified live player imports
2026-05-18 23:59:20 +02:00
4gray 5e5a89a300 refactor(ui): remove dead portal code and share detail styles (#946) 2026-05-16 00:09:29 +02:00
4gray d24c77a143 chore(nx): enforce scoped workspace boundaries (#942) 2026-05-15 09:59:06 +02:00
4gray a33d506266 fix(stalker): play radio collections with audio layout (#930)
* fix(stalker): play radio collections with audio layout

* fix(stalker): address radio review feedback
2026-05-12 19:48:38 +02:00
Tedd Johnson 626948f2f5 Add double-click stream open setting 2026-05-07 20:03:33 -07:00
4gray 6cbf2a1cfb feat(m3u): add context menus to recent channels
Entire-Checkpoint: f957cd9849e0
2026-05-05 00:27:15 +02:00
4gray e36c953865 feat(sidebar): implement shared collapse state for live-TV sidebar across components
Entire-Checkpoint: f957cd9849e0
2026-05-03 23:26:52 +02:00
4gray 15b6ba985a feat(live-tv): collapsible sidebar in live tv views
Entire-Checkpoint: f957cd9849e0
2026-05-02 23:53:05 +02:00
4grayandClaude Opus 4.7 cf30c129f1 perf(ui): lazy-load content-card images + OnPush on sources filters panel
Two small focused changes from an Angular perf audit.

content-card lazy loading
ContentCard is the building block for catalog grids and rails — Xtream
recently-added (3 rails × 20 = 60 cards on first paint), Xtream/Stalker
search results, unified favorites/recent grids. The poster <img> had
no loading attribute, so every off-screen card eagerly fetched its
poster from the Xtream/Stalker server on initial render. Same fix
already in place on the dashboard rail's separate <img>; mirror it on
the shared ContentCard so every consumer benefits.

Add loading="lazy" decoding="async" to both <img> tags (real poster
and the default-poster fallback). Browser defers off-screen requests;
no JS changes.

OnPush on workspace-sources-filters-panel
The sources filters panel renders one row per filter option and
displays {{ getTypeCount(option.id) }} per row — that method is a
plain Map lookup but fires every CD cycle under default change
detection. The component is signal-only (computed + selectSignal, no
subscribes) so the OnPush conversion is purely additive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-02 17:58:59 +02:00
4gray ec2f6a702a chore: resolve master conflicts for embedded mpv branch
Entire-Checkpoint: 5b514fe72836
2026-05-02 02:19:54 +02:00
4grayandClaude Opus 4.7 e888841b21 refactor(ui): unify live-tv channel skeleton across m3u, xtream, stalker
The loading placeholders for live channel rows were inconsistent and
visibly heavier than the real channel-list-item: each module rendered
its own skeleton with a colored card surface, larger padding/radius,
mismatched logo border-radius, and only a single tiny right-side dot
instead of the real 34×34 action buttons. Stalker had no initial-load
skeleton at all.

Extract a shared app-channel-list-item-skeleton that mirrors
channel-list-item 1:1 (transparent surface, 8/10/8/12 padding, 12px
radius, 44×44 logo with 10px radius, optional EPG progress placeholder,
0–3 button placeholders) plus a thin app-channel-list-skeleton wrapper
that renders N rows with deterministic width variance, and route every
live-tv loading state through it (m3u channel-list-loading-state,
xtream portal-channels-list, stalker live-stream-layout via a derived
isInitialChannelsLoading signal, and the unified favorites/recent
collection page). Drops the bespoke channel-skeleton-shimmer keyframes
in favor of the shared skeleton-shimmer animation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 58a2d50756dd
2026-05-02 00:52:38 +02:00
4grayandClaude Opus 4.7 94a0756424 fix(ui): unify the three side-by-side empty states in m3u groups view
Three placeholders were rendering at once when a search filter cleared
both the groups rail and the channel list, and they each used a
different visual treatment:

1. Groups rail (.groups-nav-empty) was bare 0.82rem muted text with no
   icon and no hint, and routed through CHANNELS.NO_SEARCH_RESULTS
   ("No channels found") which is wrong copy for the *groups* column.
2. Channel list (.groups-content-empty-state) was the canonical L4
   icon + title + hint pattern (correct).
3. Player area (<app-portal-empty-state>) was a 72px icon at 0.2
   opacity, which read as ghosted on dark theme.

Bring the groups rail into the L4 family scaled down for the narrow
column: 28px search_off / folder_off icon at 0.5 opacity, 0.85rem
title, 0.78rem hint at 0.65 opacity, centered. Add a CHANNELS.NO_GROUPS_FOUND
i18n key so the copy reflects the column ("No groups found").

Bump portal-empty-state's icon opacity from 0.2 -> 0.35 and title
opacity from 0.5 -> 0.6 so the player-area placeholder feels present
rather than abandoned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 6719280e397b
2026-05-01 23:43:36 +02:00
4grayandClaude Opus 4.7 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
2026-05-01 23:24:10 +02:00
4grayandClaude Opus 4.7 c44e4e6f83 fix(portal): scale down NO_SEARCH_RESULTS icon
The previous clamp(120, 18vw, 180) made the search_off icon visually
compete with the SVG illustrations used for ERROR / EMPTY_CATEGORY,
which is wrong — a no-search-results state is a transient filter view
and should recede compared to a true page-level error.

Drop to clamp(72, 11vw, 112) at 0.45 opacity so the icon is clearly
present but smaller and lighter than the SVG illustrations above it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 2e1a36e2f0f6
2026-05-01 23:24:03 +02:00
4grayandClaude Opus 4.7 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
2026-05-01 23:17:22 +02:00
4grayandClaude Opus 4.7 39ddaa2f9c perf(recents): batch Xtream remove-recent-item into a single IPC call
Clearing all recents of a given content type fired N concurrent IPC calls,
each opening its own implicit transaction in the recently_viewed table.
For users with hundreds of recently-watched VOD/series rows this added
real overhead even though the UI updates optimistically.

Add a new DB_REMOVE_RECENT_ITEMS_BATCH path end-to-end:
- removeRecentItemsBatch() Drizzle op: one transaction, one prepared
  statement reused per row
- Wire through worker → IPC handler → preload → window.electron typings
  → DatabaseService
- UnifiedRecentDataService.removeRecentItemsBatch() groups items by
  source. Xtream items go through the new batch IPC. M3U/Stalker items
  still go per-playlist because they update a JSON column on the
  playlist row, not the recently_viewed table — but they now run in
  parallel with the Xtream batch via a single Promise.all.
- Single call site updated: unified-collection-page "Clear all of type"
  confirmation handler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4bfb62f988b7
2026-05-01 23:00:32 +02:00
4gray 290843ef3f fix: restore recent live favorite toggles
Entire-Checkpoint: c2cea9c530e6
2026-04-30 21:35:39 +02:00
4gray b5a85fdf5f style: add shared count-badge style and update component usage
Entire-Checkpoint: c2cea9c530e6
2026-04-30 20:32:55 +02:00
4gray 4b2076fa53 feat(settings): add cover size settings and implement responsive cover sizing
Entire-Checkpoint: c2cea9c530e6
2026-04-30 18:56:06 +02:00
4grayandClaude Opus 4.7 f7c6cb3572 refactor(unified-collection): drop redundant player toolbar, sharpen header divider
The "channel name + close" bar above the right-pane player was redundant
with the active row in the left list (which already shows the title and
the selection chrome) and was inconsistent with the rest of the app's
live-tv views, which never had it. Closing playback was already
achievable by selecting another channel; the X added cognitive load
without earning its space (ux-similarity-consistency,
ux-cognitive-load-reduce).

Also swap the collection-header border-bottom from
--mat-sys-outline-variant to --app-separator so the divider under
"Favorites" / "Recently viewed" actually carries enough contrast to
read in dark mode (matching the live-epg-panel summary-bar pattern).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: c2cea9c530e6
2026-04-29 01:26:17 +02:00
4gray 0babd92304 feat: add live epg panel to collections
Entire-Checkpoint: c2cea9c530e6
2026-04-29 01:23:42 +02:00
4grayandClaude Opus 4.7 2db99769f9 fix(scrollbar): make .app-scrollbar override component-scoped overflow
Two issues kept the scrollbar invisible on the global Favorites and
Recently viewed live-tv lists:

1. The wrong files. The visible Favorites/Recent pages render through
   <app-global-favorites-list> from portal/shared/ui (and the
   workspace-shell variant), not the channel-list-container's tabs.
   Apply .app-scrollbar to both of those components.

2. Specificity. Component CSS gets an Angular [_ngcontent-xxx]
   attribute selector, so `.nav-list { overflow-y: auto }` resolves to
   (0,2,0) — same as the doubled `.app-scrollbar.app-scrollbar` and
   wins on source order (component CSS loads after globals). Mark
   overflow-y: scroll !important on the utility so the contract holds:
   applying .app-scrollbar always reserves the gutter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 8ae011808369
2026-04-29 01:13:58 +02:00
4gray 9f873e6bed feat(player): add embedded-mpv player for macOS as experimental feature
- Introduced tooling for building and staging the macOS `libmpv` runtime for IPTVnator's embedded MPV player.
- Added `build-macos-runtime.mjs` for building an LGPL-compatible runtime from source.
- Created `stage-macos-runtime.mjs` for staging the built runtime artifacts.
- Implemented validation for the packaged embedded MPV runtime in `electron-after-pack.cjs` and `embedded-mpv-macos.cjs`.
- Updated packaging scripts to ensure the embedded MPV runtime is correctly integrated and validated during the build process.
- Added README files to document the expected layout and usage for the embedded MPV runtime artifacts.

Entire-Checkpoint: c6e522b4276c
2026-04-27 00:32:14 +02:00
4gray b1d519183f style(empty-state): enhance empty state component with new types and responsive design
- Expanded EmptyStateType to include 'welcome-dashboard', 'welcome-sources', 'no-data'.
- Introduced FeatureCard and SourceCard interfaces for better structure.
- Added FEATURE_CARDS and SOURCE_CARDS constants for feature and source card configurations.
- Updated empty-state.component.ts to handle new types and actions.
- Created responsive styles in empty-state.responsive.scss for better mobile support.
- Added theme styles in empty-state.themes.scss for light mode.
- Developed specific styles for welcome dashboard and sources in empty-state.welcome-dashboard.scss and empty-state.welcome-sources.scss.
- Updated recent-playlists.component.html to utilize new empty state types.
- Modified recent-playlists.component.ts to support new playlist actions.
- Introduced PlaylistFileImportService for handling playlist file imports.
- Updated downloads.component.html and downloads.component.ts to integrate new empty state handling.
- Enhanced unified-collection-page.component.html and unified-collection-page.component.ts to utilize empty state for no results.

Entire-Checkpoint: c6e522b4276c
2026-04-25 22:18:13 +02:00
4gray 7caa18308d feat: enhance unified collection with workspace commands and backdrop handling
- Added WorkspaceViewCommandService to UnifiedCollectionPageComponent for workspace layout commands.
- Implemented clear current view command for favorites and recent items.
- Updated unified favorites data service to use buildXtreamCollectionUid for UID generation.
- Enhanced unified recent data service to normalize timestamps and handle backdrop URLs.
- Introduced withRecentItems feature to manage recent item additions with backdrop support.
- Updated VodDetailsRouteComponent and SerialDetailsComponent to backfill content backdrops.
- Modified IXtreamDataSource interface to include backdrop URL handling for favorites and recent items.
- Added tests for recent items feature to ensure correct functionality and backdrop handling.
2026-04-22 22:23:50 +02:00
4gray 6904b81c47 feat: implement grid rating formatting and resolution functions 2026-04-22 22:23:30 +02:00
4gray 8d6a387a9f feat: add workspace view command service and related types 2026-04-22 22:22:22 +02:00
4gray 3f79b01a9d feat: implement bulk clear favorites functionality with confirmation dialog
Entire-Checkpoint: c6e522b4276c
2026-04-19 23:38:32 +02:00
4gray 4c6e65be9b feat: implement favorites channel sorting functionality and UI updates
Entire-Checkpoint: c6e522b4276c
2026-04-19 22:31:45 +02:00
4gray 21a9cdb0c6 feat: add confirmation dialog for clearing recently viewed items
Entire-Checkpoint: c6e522b4276c
2026-04-18 10:34:34 +02:00
4gray 5e41e3d1ce refactor(favs-recent): adapt UI and routing behavior
- Introduced StalkerCollectionDetailComponent to handle detailed views for Stalker collections.
- Added XtreamCollectionDetailComponent for managing Xtream collection details.
- Updated routing components to integrate new detail components for both Stalker and Xtream.
- Enhanced global favorites and recently viewed functionalities to support new detail navigation for Stalker and Xtream items.
- Improved styling and structure for better responsiveness and user experience in detail views.

Entire-Checkpoint: c6e522b4276c
2026-04-18 10:16:09 +02:00