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
The three channel-list views (all-channels, groups, favorites) each had
an enrichedXxx computed signal that, on every progressTick (~30 s),
spread-cloned every channel in scope to attach { epgProgram, logo,
progressPercentage }. On a 90 K-channel M3U playlist, all-channels-view
allocated ~90 K objects per tick — persistent young-gen pressure and
visible GC stutter on large lists.
Replace the pattern with a side-car ChannelEpgMetadata map keyed by EPG
lookup key, holding only entries for channels that actually have EPG
data (typically a small fraction of the playlist). Templates iterate
the raw channel array and look up { epgProgram, progressPercentage }
inline. Logo resolution moves to a per-row method call — under OnPush
+ virtual scroll only ~50 visible rows check at a time.
groups-view's selectedGroupChannels also lost its progressTick
dependency: sorting + filtering only reruns when the selected group or
sort mode changes, no longer every 30 s.
The EnrichedChannel interface is replaced with a smaller
ChannelEpgMetadata. Specs that asserted on the cloned `.logo` field now
call `getLogoForChannel(channel)`.
Inspired by matracey/iptvnator@8d78cc0, @b9e6e82, @296d61e — ported as
a single coherent change. Their version missed our second site
(selectedGroupChannels) and didn't address the per-tick logo cloning.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: e400bd558b10
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
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
Replace the dashed-bordered "Ooops" placeholder in the EPG side panel
(rendered when the selected date has no programmes) with the icon +
title + hint pattern already used by groups view, season container,
and portal empty states.
The dashed border was a visual outlier — every other empty state in
the codebase is borderless, and a dashed stroke conventionally signals
a drop-zone or "add new" affordance, neither of which applies to a
passive informational state. The new layout uses a 48px event_busy
icon at 0.5 opacity, a 1.05rem title, and a 0.85rem hint at 0.65
opacity, centered in the panel via flex.
Soften the EN microcopy ("Ooops, EPG is not available for the selected
date" -> "No program guide", which also fixes the "Ooops" typo) and
add a NO_GUIDE_HINT key with localized strings across all 18 locales,
nudging the user toward the date arrows or playlist refresh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4bfb62f988b7
PortalChannelsListComponent renders a virtual-scrolled channel list (Xtream
live/VOD/series, can be 1000+ items). It was using default change detection,
so every CD cycle in the parent tree (EPG ticks, progress updates) re-checked
every binding. Switch to OnPush — the existing cdr.detectChanges() calls
already cover the async update paths, and signal-based state marks for check
automatically.
Replace track $index with track program.start in two EPG @for loops
(epg-list, multi-epg-container search results). $index causes Angular to
re-create DOM nodes whenever the list reorders (e.g. on EPG refresh or
timezone correction). program.start is the ISO datetime — unique per
program within a channel and stable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3d6901072049
Use Map and Set lookups for M3U favorite aggregation, dashboard global favorite mapping, and channel-list favorite/group resolution.
Remove the duplicate NgxIndexedDB provider registration and add regression coverage for duplicate URL and earliest-match behavior.
Entire-Checkpoint: c2cea9c530e6
The favorites and recent views had local #id { overflow-y: auto }
which has higher CSS specificity than the global .app-scrollbar class.
On macOS, overflow-y: auto means overlay scrollbars that auto-fade,
so the gutter and themed thumb were never reserved.
Bake overflow-y: scroll into the utility itself (its whole purpose) and
drop the now-redundant overflow-y: auto from the two local rules.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b2d2124cd992
Round out the channel-list-container views so the favorites and recent
panels get the same persistent scrollbar gutter and themed thumb as the
all-channels and groups views.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 2301533420c9
Move the always-visible themed scrollbar from epg-list into a global
.app-scrollbar utility class in styles.scss. Apply it to the All
channels and Groups view virtual-scroll viewports so the channel list
gets the same persistent gutter and themed thumb as the EPG list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: c470abf78379
Switch to overflow-y: scroll with scrollbar-gutter: stable so the gutter
is reserved (no layout shift between scrollable and non-scrollable
days). Locally style the thumb with --app-muted-color so it's clearly
visible across themes and platforms, overriding the global 6px
near-transparent style which was being hidden by the macOS overlay
auto-fade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 918ee7ade2f3
Separation: replace 4px gap with hairline dividers via ::before, hidden
around the active and current-program rows so they read as a continuous
emphasized block. Title line-height 1.4 → 1.3 + letter-spacing -0.01em
to match channel-name. Time uses en-dash, 0.78rem (parity with summary
bar), token-driven --app-muted-color. Description clamped to one line —
full text remains one click away via the info icon.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b0389ce441a1
Drop the gradient + bevel + drop-shadow chrome from EPG rows so the panel
speaks the same flat-by-default visual language as the channel list. Add
--app-live-color token, tokenise the LIVE badge, tighten typography
(weight 600, text-wrap balance/pretty), and bump the info icon to 32px
for Fitts-compliant hit area.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4cc5371f549e
- 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
- 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
- Implemented a loading state for channel lists in various components, enhancing user experience during data fetching.
- Added `ChannelListLoadingStateComponent` to display loading skeletons for channels and groups.
- Updated `M3uWorkspaceRouteSession` to manage loading states more effectively when fetching playlists.
- Modified `VideoPlayerComponent`, `SidebarComponent`, and `ChannelListContainerComponent` to utilize the new loading state.
- Enhanced tests to cover new loading behaviors and ensure proper dispatching of loading actions.
- Replaced moment.js usage with Intl.DateTimeFormat for date formatting in various components.
- Introduced utility functions `normalizeDateLocale` and `formatWithIntl` for locale normalization and date formatting.
- Updated PlaylistSwitcher, RecentPlaylists, ContentCard, EPG components, and MultiEpgContainer to utilize the new date formatting approach.
- Removed moment-date pipe and related tests, simplifying date handling logic.
- Ensured all date displays respect the current locale settings.
Entire-Checkpoint: 271bd0162027
- Updated video player component to conditionally render audio player for radio channels.
- Added radio-specific styles to video player and unified live tab components.
- Introduced new layout classes for radio and external players.
- Enhanced audio player component with improved UI and volume controls.
- Implemented logic to preserve radio flag in M3U playback details.
- Updated tests to cover new radio functionality and ensure proper rendering.
- Updated PlaylistErrorViewComponent to use PlaylistContextFacade for accessing the current playlist.
- Refactored StalkerSearchComponent to utilize PlaylistContextFacade for retrieving the active playlist.
- Modified StalkerWorkspaceRouteSession to sync route context using PlaylistContextFacade.
- Adjusted XtreamStore to remove NgRx store dependencies and use PlaylistContextFacade for current playlist access.
- Updated ChannelListContainerComponent to resolve playlist ID using PlaylistContextFacade.
- Refactored SidebarComponent and ToolbarComponent to eliminate NgRx store usage for playlist-related data.
- Enhanced unit tests in with-content.feature to ensure proper loading and cancellation of content imports.