Commit Graph
98 Commits
Author SHA1 Message Date
4gray 2a769ad44e Merge origin/master into PR 955 2026-05-22 01:34:56 +03:00
4gray 5cfe3aef06 fix(dashboard): localize progress labels 2026-05-22 01:02:04 +03:00
4gray 2ecb5c28f4 fix(dashboard): address PR review feedback 2026-05-22 00:59:17 +03:00
4gray 67d512d44c Merge pull request #961 from 4gray/agent/pwa-provider-target-flow
fix(docker): exclude agent worktrees from build context
2026-05-22 00:47:22 +03:00
4gray 4c2666520c fix(pwa): parallelize dashboard xtream loads 2026-05-21 21:40:24 +03:00
4gray ed0b6833d6 fix(pwa): persist xtream collection snapshots 2026-05-21 20:00:38 +03:00
4gray 3ccbfe4f99 fix(pwa): remove dashboard xtream items via data source 2026-05-21 19:52:39 +03:00
4gray 1d4d793aac fix(pwa): restore xtream user collections 2026-05-21 19:23:25 +03:00
4gray b2909a1186 feat(workspace): add keyboard shortcuts help (#957)
* feat(workspace): add keyboard shortcuts help

* style(workspace): polish keyboard shortcuts dialog

* fix(workspace): address shortcuts review feedback

* docs(workspace): clarify desktop shortcut availability

* refactor(workspace): split keyboard shortcut definitions
2026-05-21 19:15:46 +03:00
4grayandClaude Opus 4.7 7083bd2af4 fix(shell): keep Add Playlist reachable on Settings; fix dashboard e2e
Two Electron E2E regressions from this branch's redesigns:

1. Hiding the playlist switcher on /settings (a playlist-scoped control)
   also hid the global "Add playlist" button, which lives in the same
   header-actions block. A user configuring Settings before importing
   their first source had no way to add a playlist — and the settings /
   playlist-switcher E2E suites, which add a portal right after saving a
   setting, timed out waiting for the button. The Add Playlist button is
   a global action, so it now stays visible on Settings; only the
   per-playlist context shortcut and bulk actions are hidden there.

2. dashboard-activation.e2e.ts asserted on `dashboard-global-favorites-
   rail`, the mixed Favorites rail removed earlier in this branch. The
   test now targets the v0.22 rails: live favorites resolve through the
   favorites-first `dashboard-live-recent-rail`, and the played movie /
   series resolve through `dashboard-continue-watching-rail` (renamed
   from `dashboard-recently-watched-rail`). The movie-from-favorites
   assertion was dropped — that dashboard surface no longer exists; the
   favorites collection still has its own page-level coverage.

Verified locally: dashboard-activation, settings, and playlist-switcher
E2E specs all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:55:06 +02:00
4grayandClaude Opus 4.7 8091dec074 feat(dashboard): show S·E badge on Continue Watching series cards
For series in the Continue Watching rail, surface which episode the user
left off on as a small "S2·E5" chip next to the card subtitle. Sources
the season/episode straight off the matched PlaybackPositionData so the
information is always in sync with the resume target.

Fixes a latent bug in the same patch: getPlaybackPositionForItem could
not resolve series whose recent_items row carried the series id (the
landing-page path), because playback_positions are keyed by the episode
id. The lookup now matches both shapes (contentXtreamId === xtreamId OR
seriesXtreamId === xtreamId) and prefers the most recently updated
episode, which also restores the resume progress bar that had been
silently dropping out for series.

Localised in all 17 non-English locales — most use language-specific
short forms (St·F in German, T·E in Spanish/Portuguese, С·Э in
Russian/Belarusian, 시즌N N화 in Korean) rather than the English S·E.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:32:13 +02:00
4grayandClaude Opus 4.7 86ec1685a5 chore(settings): drop uppercase eyebrow on rail title
After switching from mono to sans, the SETTINGS label still used
uppercase + 0.1em letter-spacing — an eyebrow treatment that doesn't
appear anywhere else in the workspace rail. The neighbouring items
("Dashboard", "Sources", "Global favorites") are all sentence case,
so the uppercase header stood out as a one-off.

Use sentence case "Settings" with the same typography family as the
rail items: same font, same case, small + muted so it still reads as
a quiet caption rather than competing with the clickable items below.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 02:05:14 +02:00
4grayandClaude Opus 4.7 231c22f7d4 chore(settings): switch SETTINGS eyebrow to inherited sans-serif
The v0.22 mockup uses JetBrains Mono for eyebrow labels as part of its
broader editorial design language. The previous commit ported that one
element verbatim, but the rest of the workspace shell never uses mono
for chrome — only for code-adjacent data (EPG time codes, kbd glyphs,
port numbers). So the SETTINGS rail title became a one-off mono label
in an otherwise all-sans shell.

Drop the `font-family: 'JetBrains Mono'…` declaration so the eyebrow
inherits DM Sans. Bump the weight from 500 → 600 to compensate for
mono's higher visual density. Uppercase + 0.1em letter-spacing + small
size are kept — those carry the eyebrow effect regardless of typeface.

If we ever want to port the broader mono-eyebrow system across rails
and chips, that's a separate (bigger) design decision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 02:03:50 +02:00
4grayandClaude Opus 4.7 734ab3c580 refactor(settings): align look-and-feel with v0.22 mockup
The earlier Settings polish (kill triple header, compact theme picker,
sticky footer, hide playlist switcher) addressed structure. This round
addresses typography and chrome to bring the look in line with the
v0.22 mockup spec captured in redesign-screens-2.jsx.

Three changes:

1. Flat sections — no card chrome
   - Each .settings-group was a rounded panel (border-radius:22px,
     filled background, box-shadow, .settings-group--active ring +
     glow). Combined with the per-section icon-circle next to the
     title, every section looked like a feature card, not a settings
     region anchor. Strip all of it: the rail's active state on the
     left already announces "you are here".
   - Hide .settings-group__header-icon via display:none so the icon-
     circle markup in each section template stays intact while the
     visual chrome goes away — saves touching 6 templates for a
     CSS-only change.

2. Promote section titles to a large flat heading
   - Was h3 1.08rem 700 weight nested in the card chrome. Now h3
     1.5rem (24px) 600 weight with a 1px bottom-border separator,
     matching the mockup's 28px h1 + sub anchor pattern (24px is a
     reasonable density compromise for the denser app layout). Active-
     section variant keeps the blue title accent so users scrolling
     the right pane don't lose the anchor.
   - .setting-item drops the `margin: 0 18px` card inset so rows
     align flush with the section title — flat list rhythm.

3. Quiet mono eyebrow for the left nav title
   - .panel-title was a 20px 500 weight h2 ("Settings") competing for
     "biggest text on screen" against the section titles on the right.
     Switch to mono 11px uppercase 0.1em letter-spaced text-secondary
     — quiet rail header per the mockup. Now the section titles on
     the right are unambiguously the dominant heading.

Visual confirmation via agent-browser shows a much closer match to the
mockup: subtle SETTINGS eyebrow above the rail list, large flat
"General" anchor with bottom border, compact pickers, full-width sticky
footer.

50/50 settings tests still pass; build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:41:42 +02:00
4grayandClaude Opus 4.7 115911d2a6 refactor(settings): kill triple header, compact theme picker, real footer
Four UX-audit fixes for the Settings page:

1. Header dedupe
   - The page used to stack THREE headers vertically: the workspace rail's
     "Settings" entry, a page-level <h1>Settings</h1> + subtitle, and the
     General section's <h3>General</h3> + subtitle. The page-level and
     section-level subtitles both used SETTINGS.GENERAL_SUBTITLE
     ("Change the configuration of the application"), word-for-word.
   - Drop the visible page-level header entirely. Keep an a11y-only
     <span class="visually-hidden"> for the data-test-id hook so the
     existing settings.component.spec selector still resolves. Left rail
     announces "Settings"; section header is now the page anchor.
   - Add a global .visually-hidden helper to styles.scss for re-use.

2. Theme + Cover-size pickers shrink to a real segmented control
   - .theme-switcher was a 3-column grid of ~78px-tall buttons making
     "Choose theme" look like the most important action in Settings.
     Replaced with a 30px-tall segmented control (track + thumb with
     box-shadow on the selected option), matching the standard Material
     "compact row" cadence everywhere else.
   - Shortened all 18 locales' THEMES.* labels from "Light theme / Dark
     theme / System theme" to "Light / Dark / System". The "theme" word
     duplicated the section's own h3 ("Visual theme") and forced labels
     to wrap into two lines in narrow columns.

3. Save bar becomes a real sticky footer
   - The action bar was a `border-radius:18px; margin-left:auto;
     position:sticky` chip floating bottom-right that clipped the last
     form rows underneath. Now spans the full content column with a
     top border separator — sits flush with the bottom edge while
     still sticky on scroll. The save-button gradient also drops the
     hand-rolled #3b82f6 in favour of var(--app-selection-color) so it
     matches the unified blue primary used everywhere else.

4. Playlist switcher hidden on /settings
   - Workspace shell header gains an isSettingsRoute input that hides
     the playlist switcher block AND the leading actions group
     (+ Add source / header shortcut / bulk action). Settings is a
     global page — those controls were implying that switching
     playlists scopes settings, which it doesn't. Wired through from
     the shell facade's existing isSettingsRoute computed.
   - Mock header in workspace-shell.component.spec gains the matching
     input() declaration so the binding resolves under tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:33:40 +02:00
4grayandClaude Opus 4.7 fe849b2de2 refactor(dashboard): live rail now favorites-first; drop mixed Favorites rail
Two related dashboard fixes driven by the v0.22 mockup intent and the
UX-audit feedback that the rail labels lied about their data:

1. Live rail is favorites-first
   - Source: globalFavoriteLiveItems() — the channels the user actually
     starred. Falls back to globalRecentLiveItems() when no favorites
     exist so fresh-install users still see something useful.
   - Title flips with the source: "Live now on your favorites" when
     pulling from favorites, "Continue with live TV" when pulling from
     recent-watch history. The label is always honest about the data.
   - "See all" link routes to the right collection page for the source
     (/workspace/global-favorites vs /workspace/global-recent).

2. Mixed Global Favorites rail removed from the dashboard
   - The rail had movies, series, live channels, and radio all sharing
     one row — different card formats fighting for visual attention.
     UX wiki principle: similar elements should look alike within a
     scanning unit.
   - Live favorites are promoted into the live rail above (with current
     EPG). The full mixed catalogue is still one click away at
     /workspace/global-favorites where the collection page can give it
     proper per-type filters.
   - Net dashboard density goes from 5 rails to 4 — closer to the
     streaming-app sweet spot.

i18n: new WORKSPACE.DASHBOARD.LIVE_CONTINUE key, translated across all
17 locales by per-locale agents (placeholder integrity verified).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:01:22 +02:00
4grayandClaude Opus 4.7 9bb41f59b0 feat(dashboard): show watch progress on hero and Continue Watching cards
Wire the playback-position store into DashboardDataService so the hero
and Continue Watching cards can show how far through each title the user
is. Per-playlist bulk fetch via getAllPlaybackPositions (one IPC call
each, no N+1) cached in an in-memory Map keyed by playlist + content id
+ content type — supports both VOD and episode keys, since the same
xtream-id can map to either.

Hero:
- Thin 4px progress bar under the subtitle when a position is known
- "1h 04m left · 38% watched" meta line below it (formatRemainingLabel
  handles sub-minute, minute, hour, and hour+minute remainders)
- Tighter padding (16 vs 20) and smaller poster (140 vs 160) per the
  v0.22 mockup spec — frees ~30px of vertical space without losing
  legibility

Continue Watching cards:
- 3px progress overlay pinned to the bottom of the poster art, painted
  in the unified blue primary (--app-selection-color)
- Renders only when watchProgress is set, so live channels and untracked
  M3U items remain unaffected

Live channels and M3U items never have positions in the schema, so the
new UI is purely additive — both surfaces degrade gracefully to the
prior layout when the lookup misses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:50:11 +02:00
4grayandClaude Opus 4.7 bb6d1427fa feat(dashboard): compact channel cards with lazy-loaded current EPG
The "Live now on your favorites" rail no longer reuses the 2:3 poster
card built for movies. TV station logos are typically 100–256px square,
so inflating them into portrait cards wastes most of the height and
hides the data users actually care about (what's airing right now).

Introduce a `layout: 'cover' | 'channel'` input on DashboardRailComponent.
The new channel layout is a wider, much shorter card: logo + channel
name + current program title + LIVE chip + progress bar + time range.

Current programs are lazy-loaded via EpgService.getCurrentProgramsForChannels
(already batched + 60s-cached at the service), keyed by the recent item's
display name — works out of the box for M3U sources whose XMLTV channels
resolve through the tvg-id → tvg-name → name fallback chain. A 30s tick
keeps the progress bar fresh between program boundaries.

The card renders gracefully without EPG enrichment (idle progress bar,
no program title) so Xtream/Stalker live items without an XMLTV side
channel still look fine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:34:43 +02:00
4grayandClaude Opus 4.7 a048c18763 refactor(dashboard): split recently-watched into VOD and live rails
Per UX audit: a single "Recently Watched" rail mixed channels and VOD
into a grid where the two card formats fought for visual attention.
Split it into two rails — "Continue watching" (movies/series) and
"Live now on your favorites" (live channels) — so each surface uses
the card format that fits its content type.

Also relabel the rail header link from "Manage all" to "See all {N}"
when the total count exceeds what's rendered, so the user knows how
many items the link expands into; falls back to "See all" otherwise.

i18n: three new keys (LIVE_RECENT, SEE_ALL, SEE_ALL_COUNT) translated
across all 17 locales by per-locale agents reusing each file's existing
glossary; placeholder integrity verified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:18:46 +02:00
4gray 2d5c4fa4f9 chore: tighten validation and runtime logging
* chore: tighten validation and runtime logging

* fix(i18n): localize new settings labels
2026-05-15 17:43:42 +02:00
4gray d24c77a143 chore(nx): enforce scoped workspace boundaries (#942) 2026-05-15 09:59:06 +02:00
4gray 8232a86a1e feat(portal): add category sorting controls (#940)
* feat(portal): add category sort modes

* fix(portal): address category sort review feedback
2026-05-15 00:28:32 +02:00
4gray 2a5b1d1da7 fix(xtream): keep live playback on category switch (#936) 2026-05-14 20:52:02 +02:00
4gray aead89e3e6 fix(xtream): keep category management available
Fixes #913
2026-05-09 18:34:57 +02:00
4gray ab905af285 feat(stalker): add radio support
Entire-Checkpoint: f957cd9849e0
2026-05-09 01:28:39 +02:00
Eliran Sapir a3a4c6b5ea Fix Xtream section search across categories 2026-05-07 17:42:29 -04:00
4gray 2efdfea0c1 fix: preserve file paths for local playlist refresh
Refs https://github.com/4gray/iptvnator/issues/891

Entire-Checkpoint: f957cd9849e0
2026-05-04 10:06:39 +02:00
4gray d0f590d8fe refactor: improve Xtream refresh functionality with localization support
- Introduced new localization keys for Xtream refresh actions in multiple languages (ar, ary, by, de, el, en, es, fr, it, ja, ko, nl, pl, pt, ru, tr, zh, zhtw).
- Implemented refresh preparation state management in PlaylistRefreshActionService.
- Enhanced WorkspaceShellXtreamImportService to handle refresh preparation states and display appropriate labels.
- Updated tests to cover new refresh preparation scenarios and ensure correct overlay display during refresh operations.

Entire-Checkpoint: f957cd9849e0
2026-05-04 00:51:29 +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
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
4grayandClaude Opus 4.7 13acbe7e3d perf(dashboard): memoize M3U favorite parses + stream per-playlist updates
Two compounding wins for the dashboard's M3U favorites path.

#3 — Memoize loadM3uPlaylistFavorites
The function calls getPlaylistById() which deserializes the entire
M3U playlist payload (potentially 90K channels) just to read the
favorites field and the matching items. On every dashboard mount, it
re-paid that cost for every M3U playlist with favorites — even though
nothing relevant had changed.

Add a per-playlist cache keyed by playlist ID with a fingerprint of
{updateDate, favorites JSON}. A cache hit skips the heavyweight
playlist read entirely; a refresh (updateDate change) or favorites
add/remove naturally invalidates without explicit busting. First
mount still pays the parse; subsequent dashboard visits and
re-renders are effectively free.

#4 — Stream per-playlist favorites into the rail
reloadM3uGlobalFavorites used Promise.all over loadM3uPlaylistFavorites
and wrote one flat array at the end, so a single slow M3U playlist
pinned the entire M3U favorites contribution behind it.

Replace the writable m3uGlobalFavorites signal-of-array with a
WritableSignal<Map<playlistId, items[]>> + computed flatten. Each
playlist's contribution lands in the map as soon as ITS load resolves
(usually instant via the new cache), so the favorites rail re-renders
incrementally — Xtream favorites first, then each M3U playlist's
favorites appear as it streams in.

Cache + map entries for playlists that no longer have favorites are
pruned at the start of each reload, so removed playlists don't leave
stale state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: fb28be89e785
2026-05-02 09:18:08 +02:00
4grayandClaude Opus 4.7 d67150d38f perf(dashboard): per-rail skeletons + drop CAST that blocked content index
Two independent fixes that compound: the dashboard now renders progressively
as each rail's data resolves, and the slowest of those rails (Xtream
recently-added) no longer scans the entire content table.

Per-rail skeletons (template restructure):
The old @if (!ready()) gate hid the whole dashboard until ALL FOUR loading
flags resolved (playlistsLoaded, globalRecentLoaded, globalFavoritesLoaded,
xtreamRecentlyAddedLoaded). The slowest one pinned the entire skeleton up
for the full tail latency — visibly seconds of "loading" even when 3 of 4
rails could have rendered immediately.

Replace with per-rail conditionals:
- Hero: renders the moment globalRecentItems[0] is available; skeleton
  shows only while data.globalRecentLoading() is true and no item exists.
- Each rail: shows real content if its cards are non-empty, its own
  skeleton if its dedicated loading flag is true, nothing otherwise.
- The Xtream recently-added rail's skeleton is gated on having Xtream
  playlists at all, so M3U-only users never see a skeleton for it.

The loading signals were already exposed on DashboardDataService
(globalRecentLoading, globalFavoritesLoading, xtreamRecentlyAddedLoading)
but went unused because of the monolithic gate. Same skeleton markup is
reused per rail; no styling changes.

Drop CAST(added AS INTEGER) in getGlobalRecentlyAdded:
The query ordered by sql<number>\`CAST(content.added AS INTEGER)\`. SQLite
cannot use an index on a column wrapped in a function, so the existing
idx_content_type_added index was bypassed and the planner did a full table
scan + sort on content (10k–100k+ rows for a typical Xtream catalog) on
every dashboard load.

Sort by schema.content.added directly. Xtream stores Unix-epoch timestamps
as 10-digit numeric strings (anything since 2001-09-09), so lexicographic
and numeric sort are equivalent. The (type, added) index now drives the
ORDER BY too — full table scan becomes an index range scan + LIMIT 20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 1e3392724bc8
2026-05-02 09:09:10 +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
4gray 1af287205f perf: optimize playlist favorite lookups
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
2026-05-01 20:44:30 +02:00
4gray 6ef9e8c4b0 fix(web): lazy load route feature boundaries
Entire-Checkpoint: c2cea9c530e6
2026-05-01 20:38:14 +02:00
4grayandClaude Opus 4.7 d24c67c6e8 test(workspace-shell): repair stale component-spec mocks and cover new overlay child
The two component specs in this lib had been carrying 7 long-standing
failures rooted in three stale-mock issues, all unrelated to public
behaviour but enough to make the suite red on every run.

Header spec (5 failures, NG0303 on app-playlist-switcher):
- MockPlaylistSwitcherComponent was missing canRefreshActivePlaylist /
  isRefreshingActivePlaylist inputs and refreshPlaylistRequested
  output. The real PlaylistSwitcherComponent grew them when refresh
  was added to the header; the mock never caught up. Re-aligned.

Workspace-shell component spec (2 failures):
- NG0301 on #dropZone="playlistDropZone" because overrideComponent's
  set.imports list dropped PlaylistDropZoneDirective and the drop
  overlay component. Added MockPlaylistDropZoneDirective and
  MockPlaylistDropOverlayComponent.
- After the recent option-D refactor that lifted the import overlay
  into its own child component, the spec also needed
  MockWorkspaceShellImportOverlayComponent so the parent template
  could resolve <app-workspace-shell-import-overlay />.
- MockWorkspaceShellHeaderComponent gained hasNoPlaylists input and
  MockWorkspaceShellContextSidebarComponent gained hasPlaylists input
  to match the real components.
- The "renders type-aware xtream import progress copy" test asserted
  on inline overlay markup that no longer lives in the parent
  template. Replaced with a structural test that the overlay child
  renders/unmounts based on facade.showXtreamImportOverlay().

New focused spec for WorkspaceShellImportOverlayComponent (6 tests)
covers the moved progress-copy assertion at the right level by mocking
the WorkspaceShellXtreamImportService directly: title, progress copy,
determinate vs indeterminate progress bar, badge tone, and cancel
forwarding.

Final state: full feature lib suite is 66/66 (was 53/60 with 7
chronic failures). pnpm nx test web, pnpm nx lint
workspace-shell-feature, pnpm nx build web --configuration=production,
and the basic + settings web-e2e suites all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: c2cea9c530e6
2026-05-01 19:34:45 +02:00
4grayandClaude Opus 4.7 63f5173139 refactor(workspace-shell): push xtream import overlay and bulk action into child components
Continues the workspace-shell facade decomposition by relocating two
clusters from the parent template into the components that own them.

Xtream import overlay:
- New WorkspaceShellImportOverlayComponent injects
  WorkspaceShellXtreamImportService directly. The 70-line inline
  overlay block in workspace-shell.component.html collapses to a
  single <app-workspace-shell-import-overlay /> element, gated by
  facade.showXtreamImportOverlay().
- Overlay-specific styles move from workspace-shell.component.scss
  into the child component (~125 lines relocated).
- Facade drops 14 import re-export lines plus the cancelXtreamImport
  delegation. The two facade tests that exercised label generation
  now resolve the service via TestBed.inject and assert against it
  directly.

Header bulk action:
- WorkspaceShellHeaderComponent gains headerBulkAction input and
  headerBulkActionRequested output, plus a button that renders the
  action when present (next to the existing headerShortcut button).
- The parent template wires facade.headerBulkAction() →
  facade.runHeaderBulkAction(). The facade keeps the cross-cutting
  computed and the multi-store dispatch method since both span
  multiple stores.

Verification:
- workspace-shell.facade.spec.ts: 20/20 passing (two assertions
  retargeted at WorkspaceShellXtreamImportService).
- Full feature lib: 53/60 passing — same 7 pre-existing failures.
- pnpm nx build web --configuration=production: success.
- pnpm nx lint workspace-shell-feature: clean.
- pnpm nx run web-e2e:e2e-ci--src/basic.e2e.ts: 3/3 across browsers.
- pnpm nx run web-e2e:e2e-ci--src/settings.e2e.ts: 12/12 across
  browsers.

Facade: 917 → 897 lines. The bulk-action button now actually renders
in the header — closing a latent gap where the computed existed on
the facade but had no template binding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: c2cea9c530e6
2026-05-01 19:12:19 +02:00
4grayandClaude Opus 4.7 0ad54e7939 fix(workspace-shell): accept readonly viewCommands in palette builder
Production build (stricter than test/dev) flagged the helper signature
as requiring a mutable array while WorkspaceViewCommandService.commands
returns readonly. The body uses spread, so widening the parameter to
readonly is the correct fix and unblocks `nx build web` plus E2E.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: c2cea9c530e6
2026-05-01 18:59:24 +02:00
4grayandClaude Opus 4.7 d1cd805622 refactor(workspace-shell): split 1666-line facade into helpers and sub-services
The WorkspaceShellFacade had grown to 1666 lines, well past the 300/400
LOC target in CLAUDE.md. The component template binds to 54 facade
members directly, so the split preserves the public API by reference
re-export and keeps the template untouched.

Phase 1 — extract pure helpers (no DI, no behavior change):
  - helpers/workspace-shell-constants.ts: i18n keys, RAIL_TOOLTIP_KEYS,
    XtreamImportPhaseTone, WorkspaceHeaderBulkAction
  - helpers/workspace-shell-route-utils.ts: toQueryString,
    getRouteQueryParam, syncSearchQueryParam, bumpRefreshQueryParam,
    getProviderFromPlaylist
  - helpers/workspace-shell-search-labels.ts: resolveSearchPlaceholderKey,
    resolveSearchScopeLabel, resolveActiveCategoryLabel, getRailTooltipKey,
    translateRailLinks (each takes TranslateFn + snapshots as params)
  - helpers/workspace-shell-import-labels.ts: build* helpers for the 14
    Xtream import overlay labels and formatLocalizedNumber
  - helpers/workspace-shell-command-builders.ts: buildCommandPaletteItems
    plus the three command-definition builders, navigation command
    factory, resolveCommand, comparePaletteCommands, getCommandGroupOrder

Phase 2 — extract the two component-scoped sub-services with the
cleanest boundaries:
  - WorkspaceShellXtreamImportService: read-only labels, cancel action,
    isImportRunning. Reads only XtreamStore + TranslateService.
  - WorkspaceShellCommandPaletteService: MatDialog lifecycle, recent
    commands integration, eager WorkspacePlayerCommandsContributor
    bootstrap. Receives a typed CommandBuilderContext via function
    argument from the parent — no circular DI.

The parent facade re-exposes every member by reference assignment so
signal identity is preserved for change detection. workspace-shell.
component.html is unchanged. workspace-shell.component.ts gains only
two provider entries for the new sub-services.

Verification:
  - workspace-shell.facade.spec.ts: 20/20 passing
  - pnpm nx lint workspace-shell-feature: clean
  - workspace-shell-feature suite: 53/60 passing — same 7 pre-existing
    failures (header & shell component specs hit unrelated NG0301
    on playlistDropZone export); no regressions introduced
  - CDP smoke: command palette opens with Recently Used + Global
    Actions groups, settings nav disables search bar correctly

Facade reduces from 1666 to 917 lines. Each new helper/service is
under 250 lines except command-builders.ts (462) which is mostly
declarative command definitions. The parent facade is still over the
400-line hard max because cross-cutting orchestration
(searchCapability, runHeaderBulkAction, rail link computeds) reads
from multiple clusters; further reduction would require moving logic
into child components, which is deferred per the plan.

Plan: ~/.claude/plans/workspace-shell-facade-ts-is-now-1668-crispy-planet.md
(also copied locally to .plans/2026-05-01-workspace-shell-facade-split.md;
the working-tree .gitignore change excludes .plans from tracking).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: c2cea9c530e6
2026-05-01 18:48:53 +02:00
4grayandClaude Opus 4.7 2f47ed16cb feat(command-palette): add recently used section and switch-player commands
Adds a "Recently used" section at the top of the command palette (capped at 5,
persisted via StorageMap) and five "Switch player to ..." commands for one-step
player switching from anywhere. MPV/VLC are gated to Electron; the active player
entry is shown disabled. Switching applies to the next playback session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: c2cea9c530e6
2026-04-30 23:54:02 +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
4gray cbedb22698 refactor: enhance live stream layout with loading state and caching
- Added `XtreamCachedContentScope` type to manage content loading states.
- Updated live stream layout to show a loading skeleton when content is being fetched.
- Introduced styles for loading content in the live stream layout.
- Enhanced unit tests to cover loading states and caching behavior.
- Modified `XtreamWorkspaceRouteSession` to handle cached content loading and state management.
- Improved handling of optional connection values in playlist data.
- Updated workspace context panel to reflect import states and loading conditions.

Entire-Checkpoint: c2cea9c530e6
2026-04-30 18:54:58 +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
4grayandClaude Opus 4.7 d163a1fbf2 feat(dashboard): contained logo + blurred backdrop for live channels
Render live TV channel logos with object-fit: contain on a soft blurred
wash sampled from the logo itself, instead of stretching small (100-256px)
logos to fill 2:3 portrait cards with object-fit: cover. Mirrors the
treatment used by Apple Podcasts and Plex Live TV. VOD/series posters
still get the full-bleed cover treatment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 6bb6f5796090
2026-04-28 20:14:04 +02:00
4gray 313ff7a24f fix(command-palette): enhance command palette management with dialog reference
Entire-Checkpoint: c6e522b4276c
2026-04-26 01:06:36 +02:00
4gray 4bc733d9c2 refactor(external-playback): update styles
Entire-Checkpoint: c6e522b4276c
2026-04-25 23:48:21 +02:00
4gray 92f21bd237 feat(workspace): enhance dashboard and sources components with playlist management and UI improvements
Entire-Checkpoint: c6e522b4276c
2026-04-25 22:11:31 +02:00
4gray caed2ce396 feat(playlist-drop-overlay): add component and directive for file drop overlay functionality
Entire-Checkpoint: c6e522b4276c
2026-04-25 22:08:03 +02:00
4gray a76f09a8ce style(dashboard): enhance hero artwork handling with fallback logic and image failure management
Entire-Checkpoint: c6e522b4276c
2026-04-25 15:11:25 +02:00