mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 14:16:16 -08:00
docs: sync CLAUDE.md, AGENTS.md and architecture docs with actual code
Full audit of CLAUDE.md, AGENTS.md, README.md and docs/architecture/ against the codebase; every fix is backed by current code: - remove documented-but-unimplemented IPTVNATOR_DISABLE_HARDWARE_ACCELERATION flag (no reads anywhere in apps/, libs/, tools/) - CLAUDE.md: add epg_channel_mappings to the schema table list - m3u-playlist-module: *-tab dirs -> *-view (+recent-view), selectActivePlaylist, real PlaylistState shape, ChannelEpgMetadata instead of removed EnrichedChannel, actual /workspace/playlists routes, per-view outputs, live-epg-panel-state key - workspace-dashboard: per-rail Settings.dashboardRails toggles, three missing rails in the diagram, split live-favorites/recent-live rails, welcome-dashboard empty-state type, RECENTLY_WATCHED_LIVE_TV title key - stalker-portal: CategoryContentViewComponent for vod/series, collection-route components for favorites/recent, corrected series-view/favorites-button paths, actor/:personId route, epg panel selectors - category-management: reloadCategories lives in with-content.feature.ts, workspace-context-panel owns the dialog, XtreamPendingRestoreService flow - stalker-mock-server (+app README): scenario-seeded faker, resetAll() clears content cache too, ordinal season episode ids, handlers/ dir location - sqlite-db-worker: cancellation shipped (drop from out-of-scope), full operations module list - portal-detail-navigation: replace three removed component paths - tmdb-metadata-enrichment: details cache keys are id:<tmdbId>|v2 - electron-security: CSP frame-src youtube-nocookie exception, sandbox: !frameCopyExperiment nuance - download-manager: libs/portal/xtream instead of xtream-electron folder, data-driven downloads nav, drop removed app-search-result-item note - playlist-backup-restore: settings-backup facade owns the import handoff - workspace-shell: functional workspaceEntryRedirect, playlists route children - iptvnator-ui-guidelines: EPG card radius 11px, detail-view mixin is `base` - embedded-mpv-native, player-controls-contract: minor precision fixes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
db70b07093
commit
59c15493a7
19 files changed
+217
-162
No files matched your search
@@ -77,12 +77,6 @@ IPTVNATOR_TRACE_STARTUP=1 nx serve electron-backend
|
||||
`@iptvnator/shared/logging` or the redacting portal logger before reaching
|
||||
`console.*`; never log raw credentials while debugging.
|
||||
|
||||
- GPU/compositor debugging:
|
||||
|
||||
```bash
|
||||
IPTVNATOR_DISABLE_HARDWARE_ACCELERATION=1 nx serve electron-backend
|
||||
```
|
||||
|
||||
- If local Nx state gets weird before a rerun:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -134,12 +134,6 @@ Settings, portal request/response, and trace payloads must use
|
||||
`@iptvnator/shared/logging` or the redacting portal logger before reaching
|
||||
`console.*`; never log raw credentials while debugging.
|
||||
|
||||
For GPU/compositor debugging:
|
||||
|
||||
```bash
|
||||
IPTVNATOR_DISABLE_HARDWARE_ACCELERATION=1 nx serve electron-backend
|
||||
```
|
||||
|
||||
If the Nx daemon gets into a bad state before rerunning Electron:
|
||||
|
||||
```bash
|
||||
@@ -578,6 +572,7 @@ This project uses modern Angular signal-based APIs and patterns. **ALWAYS** use
|
||||
- `favorites` - User favorites
|
||||
- `recentlyViewed` - Watch history
|
||||
- `epgChannels`, `epgPrograms` - Persisted EPG data
|
||||
- `epgChannelMappings` (`epg_channel_mappings`) - Manual EPG channel mappings (defined in `epg-mapping.schema.ts`, re-exported by `schema.ts`)
|
||||
- `playbackPositions` - Resume positions
|
||||
- `downloads` - Download manager state
|
||||
- `appState` - Key-value app state (also tracks one-off data migrations)
|
||||
|
||||
@@ -300,13 +300,6 @@ This redirects the SQLite database, Electron user data, and local config under
|
||||
the given directory. Delete that directory whenever you want a fresh empty
|
||||
state.
|
||||
|
||||
If you need to debug renderer freezes or GPU/compositor issues in Electron, you
|
||||
can disable hardware acceleration for a run:
|
||||
|
||||
```
|
||||
$ IPTVNATOR_DISABLE_HARDWARE_ACCELERATION=1 pnpm run serve:backend
|
||||
```
|
||||
|
||||
If you need startup diagnostics for a white screen or a frozen route, you can
|
||||
also turn on opt-in Electron tracing. These logs are written to the Electron
|
||||
terminal output so they still help when the renderer DevTools never open:
|
||||
|
||||
@@ -127,19 +127,20 @@ apps/stalker-mock-server/
|
||||
│ ├── scenarios.ts # MAC → scenario config mapping
|
||||
│ ├── data-generator.ts # Seeded faker data generation
|
||||
│ ├── data-store.ts # Lazy per-MAC in-memory cache
|
||||
│ └── routes/
|
||||
│ ├── portal.route.ts # /portal.php dispatcher
|
||||
│ └── handlers/
|
||||
│ ├── handshake.handler.ts
|
||||
│ ├── do-auth.handler.ts
|
||||
│ ├── get-categories.handler.ts
|
||||
│ ├── get-ordered-list.handler.ts
|
||||
│ ├── get-seasons.handler.ts
|
||||
│ ├── create-link.handler.ts
|
||||
│ ├── favorites.handler.ts
|
||||
│ ├── get-epg-info.handler.ts
|
||||
│ ├── get-short-epg.handler.ts
|
||||
│ └── get-genres.handler.ts
|
||||
│ ├── routes/
|
||||
│ │ ├── portal.route.ts # /portal.php route
|
||||
│ │ └── dispatch.ts # Shared Stalker action dispatcher
|
||||
│ └── handlers/
|
||||
│ ├── handshake.handler.ts
|
||||
│ ├── do-auth.handler.ts
|
||||
│ ├── get-categories.handler.ts
|
||||
│ ├── get-ordered-list.handler.ts
|
||||
│ ├── get-seasons.handler.ts
|
||||
│ ├── create-link.handler.ts
|
||||
│ ├── favorites.handler.ts
|
||||
│ ├── get-epg-info.handler.ts
|
||||
│ ├── get-short-epg.handler.ts
|
||||
│ └── get-genres.handler.ts
|
||||
├── project.json
|
||||
├── tsconfig.json
|
||||
└── README.md
|
||||
|
||||
@@ -75,9 +75,9 @@ ALTER TABLE categories ADD COLUMN hidden INTEGER DEFAULT 0
|
||||
|
||||
### Store
|
||||
|
||||
**File**: `libs/portal/xtream/data-access/src/lib/stores/xtream.store.ts`
|
||||
**File**: `libs/portal/xtream/data-access/src/lib/stores/features/with-content.feature.ts`
|
||||
|
||||
Added `reloadCategories()` method to refresh categories from database after visibility changes, ensuring the sidebar updates immediately.
|
||||
`reloadCategories()` (exposed on the `XtreamStore` facade via feature composition) refreshes categories from the database after visibility changes, ensuring the sidebar updates immediately.
|
||||
|
||||
## Behavior Notes
|
||||
|
||||
@@ -133,24 +133,28 @@ apps/electron-backend/src/app/
|
||||
libs/services/src/lib/
|
||||
└── database-electron.service.ts # Service methods (with hidden category support)
|
||||
|
||||
libs/ui/components/src/lib/recent-playlists/
|
||||
└── recent-playlists.component.ts # Stores hidden categories to localStorage on refresh
|
||||
libs/playlist/shared/ui/src/lib/
|
||||
├── recent-playlists/
|
||||
│ └── recent-playlists.component.ts # Persists hidden categories (restore state) via XtreamPendingRestoreService on refresh
|
||||
└── playlist-refresh-action.service.ts # Same restore-state persistence for the header refresh action
|
||||
|
||||
libs/services/src/lib/
|
||||
└── xtream-pending-restore.service.ts # localStorage keyed `xtream-restore-{playlistId}`
|
||||
|
||||
libs/workspace/shell/feature/src/lib/
|
||||
└── workspace-context-panel/
|
||||
└── workspace-context-panel.component.ts # Tune button; lazy-loads the dialog, calls reloadCategories()
|
||||
|
||||
libs/portal/xtream/feature/src/lib/
|
||||
├── category-management-dialog/ # Dialog component
|
||||
│ ├── category-management-dialog.component.ts
|
||||
│ ├── category-management-dialog.component.html
|
||||
│ └── category-management-dialog.component.scss
|
||||
├── xtream-main-container.component.ts # Added button & dialog
|
||||
├── xtream-main-container.component.html
|
||||
├── live-stream-layout/
|
||||
│ ├── live-stream-layout.component.ts # Added button & dialog
|
||||
│ └── live-stream-layout.component.html
|
||||
└── category-management-dialog/ # Dialog component
|
||||
├── category-management-dialog.component.ts
|
||||
├── category-management-dialog.component.html
|
||||
└── category-management-dialog.component.scss
|
||||
|
||||
libs/portal/xtream/data-access/src/lib/
|
||||
├── data-sources/
|
||||
│ └── electron-xtream-data-source.ts # Reads/passes hidden categories on save
|
||||
└── stores/xtream.store.ts # Added reloadCategories method
|
||||
└── stores/features/with-content.feature.ts # reloadCategories() (exposed on XtreamStore)
|
||||
|
||||
apps/web/src/assets/i18n/
|
||||
└── en.json # Added translation keys
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Download Manager Architecture
|
||||
|
||||
The download manager is a desktop-only feature that layers a curated queue, progress tracking, storage configuration, and playback controls on top of the existing Xtream (xtream-electron folder) + Stalker viewers. Backend work is handled in the Electron process while the Angular renderer surface exposes a dedicated `/downloads` route, contextual buttons, and theme-aware styling.
|
||||
The download manager is a desktop-only feature that layers a curated queue, progress tracking, storage configuration, and playback controls on top of the existing Xtream (`libs/portal/xtream`) + Stalker (`libs/portal/stalker`) portal views. Backend work is handled in the Electron process while the Angular renderer surface exposes a dedicated `/downloads` route, contextual buttons, and theme-aware styling.
|
||||
|
||||
## Backend responsibilities
|
||||
|
||||
- **Queue control (`apps/electron-backend/src/app/events/database/download-runtime.ts`)**
|
||||
`DownloadTask` mirrors the shared `DownloadItem` table plus transient cancel/progress helpers. Request validation and row creation live in `download-requests.ts`, while `downloads.events.ts` stays focused on IPC registration. `enqueueDownload()` pushes the task onto `downloadQueue` and triggers `processQueue()`. `processQueue()` keeps one active download, updates the row to `downloading`, and calls `startDownload()`.
|
||||
`DownloadTask` mirrors a row of the shared `downloads` table (type `Download` in `libs/shared/database/src/lib/schema.ts`) plus transient cancel/progress helpers. Request validation and row creation live in `download-requests.ts`, while `downloads.events.ts` stays focused on IPC registration. `enqueueDownload()` pushes the task onto `downloadQueue` and triggers `processQueue()`. `processQueue()` keeps one active download, updates the row to `downloading`, and calls `startDownload()`.
|
||||
- **electron-dl integration**
|
||||
`startDownload()` calls `electron-dl`'s `download()` helper. Headers (user agent, referer, origin) are attached, and the `onStarted`, `onProgress`, `onCompleted`, and `onCancel` callbacks translate the helper's payload into Drizzle updates. A cancellation requested before `onStarted` is remembered and applied as soon as Electron supplies the `DownloadItem`, so the request cannot be lost in the startup race.
|
||||
- **Destination collision policy**
|
||||
@@ -24,10 +24,8 @@ The download manager is a desktop-only feature that layers a curated queue, prog
|
||||
- **Downloads service** (`libs/services/src/lib/downloads.service.ts`)
|
||||
Signals back the current download list while `hasDownloads` and `isAvailable` gates UI rendering. Before each download the service asks the main process for the authorized folder and calls `downloadsStart`. The backend extracts the file extension from the URL or falls back to `mp4`. `onDownloadsUpdate` updates the signal, while helper methods `retryDownload`, `removeDownload`, `cancelDownload`, and `playDownload` talk to the corresponding IPC commands so retries reuse existing rows and completed items can open the recorded path.
|
||||
- **Downloads view** (`libs/portal/downloads/feature`)
|
||||
A standalone page exposes the queue, desktop-only messaging, folder picker, and action buttons. `downloads.component.html` now wraps the list inside a scrollable panel (`downloads__list-wrapper`) so long queues stay reachable, and `downloads.component.scss` drives a bold two-tone aesthetic inspired by the frontend-design mandate—gradient cards, floating avatars, and theme-aware variables triggered via `body.dark-theme`.
|
||||
A standalone page exposes the queue, desktop-only messaging, folder picker, and action buttons. `downloads.component.html` wraps the list inside a scrollable panel (`downloads__list-wrapper`) so long queues stay reachable, and `downloads.component.scss` drives gradient cards with theme-aware styling through Angular Material system CSS variables (`var(--mat-sys-*)`, `var(--app-*)`, `color-mix`) — theming tracks the active Material theme rather than a `body.dark-theme` hook.
|
||||
Failed/canceled cards now show retry/delete controls, queued/downloading cards show a cancel icon, and completed cards render inline play/open buttons with `mat-icon` cues. The header also shows the resolved download folder and a `CHANGE FOLDER` action.
|
||||
- **Theme fixes**
|
||||
To keep typography legible in both modes, `app-search-result-item` now inherits color from `:host-context(body.dark-theme)` and `:host-context(body:not(.dark-theme))`, ensuring dense light-theme grids no longer show white text on white backgrounds.
|
||||
|
||||
## Global API surface
|
||||
|
||||
@@ -37,7 +35,7 @@ The download manager is a desktop-only feature that layers a curated queue, prog
|
||||
## Routing and navigation
|
||||
|
||||
- `/downloads` is available under both portal flavors: the Xtream routes already load `DownloadsComponent`, and the Stalker routes now import the same component so the sidebar link can target `/stalker/:id/downloads` without returning to the startup screen.
|
||||
- The navigation component already points `routerLink="./downloads"` inside the shared nav pane, so both portals reuse the same download page.
|
||||
- Downloads navigation is data-driven: `libs/portal/shared/util/src/lib/navigation/portal-rail-links.ts` emits a `downloads` section link (`path: [...root, 'downloads']`) for both portals, so they reuse the same download page.
|
||||
|
||||
## Queuing, persistence, and UX notes
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ explicit hardened `webPreferences` object:
|
||||
|
||||
- `contextIsolation: true`
|
||||
- `nodeIntegration: false`
|
||||
- `sandbox: true`
|
||||
- `sandbox: !frameCopyExperiment` — `true` by default; the opt-in Embedded MPV
|
||||
frame-copy experiment is the one path that disables the renderer sandbox
|
||||
(`contextIsolation`/`nodeIntegration` stay hardened regardless)
|
||||
- `webSecurity: true`
|
||||
- `preload: apps/electron-backend/src/app/api/main.preload.ts`
|
||||
|
||||
@@ -98,11 +100,13 @@ produce both `dmg` and `zip` targets, and publish a single merged
|
||||
|
||||
The Angular shell defines a baseline CSP in `apps/web/src/index.html`.
|
||||
|
||||
The policy keeps the application self-hosted for scripts, blocks object and
|
||||
frame embedding, limits forms to the app origin, and allows IPTV playback
|
||||
sources through `media-src` and `connect-src` for `http:`, `https:`, `blob:`,
|
||||
and `data:`. The policy keeps `script-src` self-hosted and currently keeps
|
||||
`unsafe-inline` for existing inline styles.
|
||||
The policy keeps the application self-hosted for scripts, blocks object
|
||||
embedding (`object-src 'none'`) while allowing frames only from
|
||||
`https://www.youtube-nocookie.com` (`frame-src`, used for TMDB trailers),
|
||||
limits forms to the app origin, and allows IPTV playback sources through
|
||||
`media-src` and `connect-src` for `http:`, `https:`, `blob:`, and `data:`. The
|
||||
policy keeps `script-src` self-hosted and currently keeps `unsafe-inline` for
|
||||
existing inline styles.
|
||||
|
||||
Angular production builds must not rely on inline event handlers for stylesheet
|
||||
activation. Keep `web:build:production` and `web:build:pwa` configured without
|
||||
|
||||
@@ -209,8 +209,9 @@ the frame-copy canvas.
|
||||
embedded MPV experiment flag) switches macOS/arm64, Linux and Windows to a
|
||||
second rendering engine that replaces the native-view compositing entirely
|
||||
(gate: `isFrameCopyPlatformSupported()` in
|
||||
`embedded-mpv-frame-copy-platform.util.ts`, shared by `main.ts`, the
|
||||
service and the adapter):
|
||||
`embedded-mpv-frame-copy-platform.util.ts`; the adapter imports it directly,
|
||||
while `main.ts` and the service call it transitively through the same util
|
||||
module's `isFrameCopyRuntimeUsable()` / `getFrameCopyRuntimeAvailability()`):
|
||||
|
||||
- `apps/electron-backend/native/helper/` — `iptvnator_mpv_helper`, a
|
||||
one-process-per-session libmpv host. It decodes (hwdec), renders
|
||||
@@ -259,7 +260,8 @@ service and the adapter):
|
||||
bounds sync.
|
||||
|
||||
Enabling it: the `Settings > Playback > Embedded MPV: frame-copy engine`
|
||||
checkbox (shown only when support reports `frameCopyAvailable`) persists to
|
||||
checkbox (shown when support reports `frameCopyAvailable` or the option is
|
||||
already enabled, so it stays visible for turning off) persists to
|
||||
the main-process config store (`electron-conf`), which `main.ts` reads before
|
||||
creating the window and translates into the env flag; an explicitly set env
|
||||
var (including `0`) wins over the stored preference, but cannot bypass the
|
||||
|
||||
@@ -88,10 +88,10 @@ Do not add extra badges, left rails, or second selection systems unless there is
|
||||
|
||||
## Detail Views
|
||||
|
||||
VOD and series detail screens share the `detail-view` Sass mixin from
|
||||
`libs/ui/styles/_detail-view.scss`. Feature-local `styles/detail-view.scss`
|
||||
files should only import that mixin and pass small typography overrides when a
|
||||
provider needs them.
|
||||
VOD and series detail screens share the detail-view Sass mixin (`@mixin base`)
|
||||
from `libs/ui/styles/_detail-view.scss`. Feature-local `styles/detail-view.scss`
|
||||
files should only `@use` that module and `@include detail-view.base(...)` with
|
||||
small typography overrides when a provider needs them.
|
||||
|
||||
Do not copy the full detail-view stylesheet into feature libraries. Add shared
|
||||
layout changes to the mixin, and keep provider-specific differences explicit in
|
||||
@@ -201,7 +201,8 @@ The shared row should be reused instead of rebuilding channel markup per view.
|
||||
### EPG Card
|
||||
|
||||
- Radius:
|
||||
`14px`
|
||||
`11px` (`.epg-timeline__block` in
|
||||
`libs/ui/epg/src/lib/epg-timeline/epg-timeline-track.component.scss`)
|
||||
- Neutral cards use low-contrast surface treatment
|
||||
- Current card uses selection surface and selection border
|
||||
- Description should clamp rather than overflow
|
||||
|
||||
@@ -60,31 +60,23 @@ The behavioral contract is guarded by `apps/web/src/app/iptv-playlist-parser.con
|
||||
### State Structure
|
||||
|
||||
```typescript
|
||||
// libs/m3u-state/src/lib/state.ts
|
||||
interface PlaylistState {
|
||||
// Active channel being played
|
||||
active: Channel | undefined;
|
||||
active: Channel | undefined; // Active channel being played
|
||||
activePlaybackUrl: string | null;
|
||||
activeEpgProgram: EpgProgram | undefined;
|
||||
currentEpgProgram: EpgProgram | undefined;
|
||||
epgAvailable: boolean;
|
||||
channelsLoading: boolean; // Route still resolving channel data
|
||||
channels: Channel[]; // All channels from current playlist
|
||||
playlists: PlaylistMetaState; // Playlist metadata (entity adapter)
|
||||
}
|
||||
|
||||
// Whether the current route is still resolving channel data
|
||||
channelsLoading: boolean;
|
||||
|
||||
// All channels from current playlist
|
||||
channels: Channel[];
|
||||
|
||||
// EPG state
|
||||
epg: {
|
||||
epgAvailable: boolean;
|
||||
activeEpgProgram: EpgProgram | undefined;
|
||||
currentEpgProgram: EpgProgram | undefined;
|
||||
};
|
||||
|
||||
// Playlist metadata (entity adapter)
|
||||
playlistsMeta: {
|
||||
ids: string[];
|
||||
entities: Record<string, PlaylistMeta>;
|
||||
selectedId: string | undefined;
|
||||
allPlaylistsLoaded: boolean;
|
||||
selectedFilters: PlaylistSourceFilter[];
|
||||
};
|
||||
// libs/m3u-state/src/lib/playlists.state.ts
|
||||
interface PlaylistMetaState extends EntityState<PlaylistMeta> {
|
||||
selectedId: string;
|
||||
allPlaylistsLoaded: boolean;
|
||||
selectedFilters: string[]; // 'm3u' | 'xtream' | 'stalker'
|
||||
}
|
||||
```
|
||||
|
||||
@@ -114,7 +106,7 @@ selectFavorites; // Favorite channel URLs
|
||||
// Playlist selectors
|
||||
selectAllPlaylistsMeta; // All playlists
|
||||
selectActivePlaylistId; // Selected playlist ID
|
||||
selectCurrentPlaylist; // Active playlist object
|
||||
selectActivePlaylist; // Active playlist object
|
||||
selectPlaylistTitle; // Title with "Global favorites" fallback
|
||||
|
||||
// EPG selectors
|
||||
@@ -134,20 +126,25 @@ channel-list-container/
|
||||
├── channel-list-container.component.html
|
||||
├── channel-list-container.component.scss
|
||||
│
|
||||
├── all-channels-tab/ # Virtual scroll + search
|
||||
│ ├── all-channels-tab.component.ts
|
||||
│ ├── all-channels-tab.component.html
|
||||
│ └── all-channels-tab.component.scss
|
||||
├── all-channels-view/ # Virtual scroll + debounced search
|
||||
│ ├── all-channels-view.component.ts
|
||||
│ ├── all-channels-view.component.html
|
||||
│ └── all-channels-view.component.scss
|
||||
│
|
||||
├── groups-tab/ # Expansion panels + infinite scroll
|
||||
│ ├── groups-tab.component.ts
|
||||
│ ├── groups-tab.component.html
|
||||
│ └── groups-tab.component.scss
|
||||
├── groups-view/ # Expansion panels + infinite scroll
|
||||
│ ├── groups-view.component.ts
|
||||
│ ├── groups-view.component.html
|
||||
│ └── groups-view.component.scss
|
||||
│
|
||||
├── favorites-tab/ # Drag-drop reordering
|
||||
│ ├── favorites-tab.component.ts
|
||||
│ ├── favorites-tab.component.html
|
||||
│ └── favorites-tab.component.scss
|
||||
├── favorites-view/ # Drag-drop reordering
|
||||
│ ├── favorites-view.component.ts
|
||||
│ ├── favorites-view.component.html
|
||||
│ └── favorites-view.component.scss
|
||||
│
|
||||
├── recent-view/ # Recently viewed channels
|
||||
│ ├── recent-view.component.ts
|
||||
│ ├── recent-view.component.html
|
||||
│ └── recent-view.component.scss
|
||||
│
|
||||
└── channel-list-item/ # Individual channel display
|
||||
├── channel-list-item.component.ts
|
||||
@@ -235,14 +232,17 @@ channel-list-container/
|
||||
rendering. Playlist order avoids cloning the full list when no search term is
|
||||
active.
|
||||
|
||||
### EnrichedChannel Pattern
|
||||
### ChannelEpgMetadata Pattern
|
||||
|
||||
For performance optimization, channels are pre-enriched with EPG data:
|
||||
For performance optimization, EPG data is kept in a side-car map instead of
|
||||
being cloned onto every channel (the older `EnrichedChannel` pattern that
|
||||
spread-cloned every channel on every ~30 s tick was removed —
|
||||
`channel-list-container/epg-enrichment.util.ts`):
|
||||
|
||||
```typescript
|
||||
interface EnrichedChannel extends Channel {
|
||||
// libs/ui/components/src/lib/channel-list-container/epg-enrichment.util.ts
|
||||
interface ChannelEpgMetadata {
|
||||
epgProgram: EpgProgram | null | undefined;
|
||||
logo: string; // Playlist tvg-logo first, XMLTV icon fallback second
|
||||
progressPercentage: number; // Pre-computed by parent
|
||||
}
|
||||
```
|
||||
@@ -354,7 +354,7 @@ activation, and the details dialog behave identically to the timeline.
|
||||
`isLivePlayback`, `loading`, `emptyReason`, `selectedDate`, `collapsed`,
|
||||
`summary` and emits `programActivated`, `returnToLive`, `selectedDateChange`,
|
||||
`openEpgSettings`, `retry`, `collapsedChange`. The host layout owns playback,
|
||||
persists the collapse state (`liveEpgPanelState` in localStorage), and (for
|
||||
persists the collapse state (`live-epg-panel-state` in localStorage), and (for
|
||||
the M3U player) the `EpgActions.setCurrentEpgProgram` / `setEpgAvailableFlag`
|
||||
/ `setActiveEpgProgram` dispatches. The timeline owns the **single** panel
|
||||
bar — collapse chevron + channel name on the left, return-to-live / jump /
|
||||
@@ -557,25 +557,25 @@ These URLs are playlist-scoped by default:
|
||||
#### AllChannelsViewComponent
|
||||
|
||||
- **Inputs**: `channels`, `channelEpgMap`, `channelIconMap`, `progressTick`, `shouldShowEpg`, `itemSize`, `activeChannelUrl`, `favoriteIds`
|
||||
- **Outputs**: `channelSelected`, `favoriteToggled`
|
||||
- **Outputs**: `channelSelected`, `channelPlaybackRequested`, `favoriteToggled`, `sidebarToggleRequested`
|
||||
- **Features**: Workspace search, persisted channel sorting, virtual scrolling, no-results placeholder
|
||||
|
||||
#### GroupsViewComponent
|
||||
|
||||
- **Inputs**: Same as AllChannelsTab + `groupedChannels`
|
||||
- **Outputs**: `channelSelected`, `favoriteToggled`
|
||||
- **Inputs**: Same as AllChannelsViewComponent + `groupedChannels`
|
||||
- **Outputs**: `channelSelected`, `channelPlaybackRequested`, `favoriteToggled`, `hiddenGroupTitlesChanged`, sidebar sizing outputs
|
||||
- **Features**: Resizable groups rail, local group search, group visibility management, persisted selected-group channel sorting
|
||||
|
||||
#### FavoritesViewComponent
|
||||
|
||||
- **Inputs**: `favorites`, `channelEpgMap`, `channelIconMap`, `progressTick`, `shouldShowEpg`, `activeChannelUrl`
|
||||
- **Outputs**: `channelSelected`, `favoriteToggled`, `favoritesReordered`
|
||||
- **Outputs**: `channelSelected`, `channelPlaybackRequested`, `favoriteToggled`, `favoritesReordered`
|
||||
- **Features**: Drag-and-drop reordering with CDK DragDrop, read-only channel details context menu
|
||||
|
||||
#### RecentViewComponent
|
||||
|
||||
- **Inputs**: recent channels, `channelEpgMap`, `channelIconMap`, `progressTick`, `shouldShowEpg`, `activeChannelUrl`
|
||||
- **Outputs**: `channelSelected`, `favoriteToggled`, `recentItemRemoved`
|
||||
- **Outputs**: `channelSelected`, `channelPlaybackRequested`, `removeRecent`
|
||||
- **Features**: Read-only channel details context menu, row-level and context-menu removal
|
||||
|
||||
## EPG Integration
|
||||
@@ -736,16 +736,21 @@ interface EpgProgram {
|
||||
|
||||
## Routes
|
||||
|
||||
Routes live in `libs/playlist/m3u/feature-player/src/lib/m3u-workspace.routes.ts`
|
||||
(`createM3uWorkspaceRoutes()`), nested under the workspace shell:
|
||||
|
||||
```
|
||||
/playlists/:id # Video player with playlist
|
||||
/iptv # Default IPTV route
|
||||
/workspace/playlists/:id # M3U player (redirects to .../all)
|
||||
/workspace/playlists/:id/favorites # Favorites collection view
|
||||
/workspace/playlists/:id/recent # Recently viewed collection view
|
||||
/workspace/playlists/:id/:view # Video player with channel list view
|
||||
```
|
||||
|
||||
## Adding New Features
|
||||
|
||||
### To add a new tab to channel list:
|
||||
### To add a new view to channel list:
|
||||
|
||||
1. Create component in `channel-list-container/new-tab/`
|
||||
1. Create component in `channel-list-container/new-view/`
|
||||
2. Accept inputs: `channels`, `channelEpgMap`, `progressTick`, `shouldShowEpg`, `activeChannelUrl`
|
||||
3. Emit `channelSelected` output
|
||||
4. Add to parent template and imports
|
||||
|
||||
@@ -635,6 +635,7 @@ The guarded ArtPlayer integration lives in:
|
||||
|
||||
```text
|
||||
libs/ui/playback/src/lib/art-player/
|
||||
├── art-player-audio-tracks.ts
|
||||
├── art-player-setup.ts
|
||||
├── art-player-source-session.ts
|
||||
├── art-player-video-session.ts
|
||||
|
||||
@@ -5,7 +5,9 @@ settings screen.
|
||||
|
||||
## Entry Points
|
||||
|
||||
- UI: `/Users/4gray/Code/iptvnator/apps/web/src/app/settings/settings.component.ts`
|
||||
- UI: `/Users/4gray/Code/iptvnator/apps/web/src/app/settings/settings-backup-section.component.ts`
|
||||
(embedded in `settings.component.html`), with the file read/handoff in
|
||||
`/Users/4gray/Code/iptvnator/apps/web/src/app/settings/settings-backup.facade.ts`
|
||||
- Backup service: `/Users/4gray/Code/iptvnator/libs/services/src/lib/playlist-backup.service.ts`
|
||||
- Manifest types: `/Users/4gray/Code/iptvnator/libs/shared/interfaces/src/lib/playlist-backup.interface.ts`
|
||||
- Xtream pending restore storage:
|
||||
@@ -102,7 +104,9 @@ Only EPG source URLs are backed up at the app-settings level.
|
||||
|
||||
## Import Flow
|
||||
|
||||
The settings component hands file contents to `PlaylistBackupService`.
|
||||
The settings backup facade (`settings-backup.facade.ts`, driven by
|
||||
`settings-backup-section.component.ts`) reads the file (`file.text()`) and
|
||||
hands its contents to `PlaylistBackupService.importBackup()`.
|
||||
|
||||
The service:
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ Implication:
|
||||
|
||||
Current code paths:
|
||||
|
||||
- `libs/portal/xtream/feature/src/lib/favorites/favorites.component.ts`
|
||||
- `libs/portal/xtream/feature/src/lib/xtream-collection-detail.component.ts` (favorites + recent, with shared UI from `libs/portal/shared/ui/src/lib/components/favorites-layout/`)
|
||||
- `libs/portal/xtream/feature/src/lib/search-results/search-results.component.ts`
|
||||
- `libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.ts`
|
||||
|
||||
@@ -113,8 +113,7 @@ Implication:
|
||||
|
||||
Current code paths:
|
||||
|
||||
- `libs/portal/stalker/feature/src/lib/stalker-favorites/stalker-favorites.component.ts`
|
||||
- `libs/portal/stalker/feature/src/lib/recently-viewed/recently-viewed.component.ts`
|
||||
- `libs/portal/stalker/feature/src/lib/stalker-collection-route.component.ts` (favorites + recent via `mode` route data) -> `stalker-collection-detail.component.ts`
|
||||
- `libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts`
|
||||
- `libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.ts` (Stalker branch)
|
||||
|
||||
|
||||
@@ -56,6 +56,15 @@ Keep SQL-heavy logic here so the worker entry remains a thin dispatcher:
|
||||
2. `apps/electron-backend/src/app/database/operations/content.operations.ts`
|
||||
3. `apps/electron-backend/src/app/database/operations/playlist.operations.ts`
|
||||
4. `apps/electron-backend/src/app/database/operations/xtream.operations.ts`
|
||||
5. `apps/electron-backend/src/app/database/operations/favorites.operations.ts`
|
||||
6. `apps/electron-backend/src/app/database/operations/recently-viewed.operations.ts`
|
||||
7. `apps/electron-backend/src/app/database/operations/playback-position.operations.ts`
|
||||
8. `apps/electron-backend/src/app/database/operations/content-backdrop.operations.ts`
|
||||
9. `apps/electron-backend/src/app/database/operations/title-match.operations.ts`
|
||||
10. `apps/electron-backend/src/app/database/operations/tmdb.operations.ts`
|
||||
11. `apps/electron-backend/src/app/database/operations/epg-mapping.operations.ts`
|
||||
|
||||
(plus the shared cancellation helper `operation-control.ts` in the same directory)
|
||||
|
||||
## Worker Architecture
|
||||
|
||||
@@ -675,11 +684,16 @@ CI=1 NX_TASKS_RUNNER_DYNAMIC_OUTPUT=false pnpm nx run electron-backend:build --s
|
||||
|
||||
These are intentionally still out of scope for this first cut:
|
||||
|
||||
1. request cancellation
|
||||
2. moving network-heavy Xtream fetches off the current path
|
||||
3. migrating every remaining small SQLite IPC handler to the worker
|
||||
4. richer delete progress reporting for bulk destructive operations
|
||||
5. repo-wide Angular/Jest cleanup for the currently failing web test baseline
|
||||
1. moving network-heavy Xtream fetches off the current path
|
||||
2. migrating every remaining small SQLite IPC handler to the worker
|
||||
3. richer delete progress reporting for bulk destructive operations
|
||||
4. repo-wide Angular/Jest cleanup for the currently failing web test baseline
|
||||
|
||||
(Request cancellation, originally listed here, has since shipped — see the
|
||||
"Cancellation contract" section above: `DB_CANCEL_OPERATION` in
|
||||
`apps/electron-backend/src/app/api/main.preload.ts`, `AbortError` production in
|
||||
`database.worker.ts`, and `DatabaseService.cancelOperation` in
|
||||
`libs/services/src/lib/database-electron.service.ts`.)
|
||||
|
||||
## Extending The Worker
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ The mock server enables:
|
||||
Per-request random data would break navigation: if category IDs change between calls, content fetched under a category ID won't match the category list. Instead:
|
||||
|
||||
- Data is generated **once per MAC address** on first request, then cached in memory.
|
||||
- `@faker-js/faker` is seeded with a numeric value derived from the MAC address before generation.
|
||||
- `@faker-js/faker` is seeded with the scenario's `seed` value before generation: predefined scenario MACs use fixed seeds from `scenarios.ts`; unknown MACs derive the seed from the MAC via `macToSeed()`.
|
||||
- Same MAC → identical data on every server restart.
|
||||
- Restart the server to reshuffle all data.
|
||||
|
||||
@@ -41,7 +41,7 @@ No files or databases are written. All state (generated content + favorites) liv
|
||||
## Data Generation Pipeline
|
||||
|
||||
```
|
||||
faker.seed(macToNumber(mac))
|
||||
faker.seed(config.seed) // scenario seed; unknown MACs: macToSeed(mac)
|
||||
│
|
||||
├── generateCategories('itv', N) → itvCategories[]
|
||||
│ └── generateChannels() → channels Map<categoryId, channel[]>
|
||||
@@ -123,7 +123,7 @@ marker and an `ffrt4://radio/...` command.
|
||||
"id": "30001-s1",
|
||||
"name": "Season 1",
|
||||
"cmd": "ffrt4://series/30001/season/1",
|
||||
"series": ["30001-s1-e1", "30001-s1-e2", ...],
|
||||
"series": ["1", "2", "3", ...],
|
||||
"screenshot_uri": "https://picsum.photos/seed/30001-s1/300/200",
|
||||
"director": "...",
|
||||
"actors": "...",
|
||||
@@ -217,9 +217,19 @@ interface ScenarioConfig {
|
||||
episodesPerSeason: number;
|
||||
isSeriesFraction: number; // 0–1: fraction of VOD with is_series=1
|
||||
embeddedSeriesFraction: number; // 0–1: fraction of VOD with embedded series[]
|
||||
supportsGetAllChannels?: boolean; // default true; false mimics legacy portals
|
||||
// without the ITV get_all_channels action
|
||||
}
|
||||
```
|
||||
|
||||
The `legacy-pagination` scenario (`00:1A:79:00:00:06`) sets
|
||||
`supportsGetAllChannels: false`: `get_all_channels` then answers with an error
|
||||
payload so clients fall back to the paginated `get_ordered_list` crawl. For
|
||||
supporting scenarios, `get_all_channels` (`get-all-channels.handler.ts`,
|
||||
`type=itv` only) returns the complete ITV channel list in one
|
||||
`{ js: { data, total_items } }` response, excluding channels from censored
|
||||
(adult) genres.
|
||||
|
||||
### Adding a New Scenario
|
||||
|
||||
1. Add an entry to the `SCENARIOS` map in `src/app/scenarios.ts`.
|
||||
@@ -257,7 +267,7 @@ Playwright waits for both servers to be healthy before starting tests. If either
|
||||
|
||||
Each stalker e2e test calls `POST http://localhost:3210/reset` in `beforeEach` to clear in-memory state. This ensures tests don't bleed favorites or other mutable state into each other.
|
||||
|
||||
The generated content (categories, items) is **not** cleared on reset — it's deterministic and doesn't need to be. Only in-memory favorites are cleared.
|
||||
`resetAll()` clears both the generated-content cache and in-memory favorites (`data-store.ts`). Because generation is seed-deterministic, the next request regenerates identical content, so the observable data does not change across resets.
|
||||
|
||||
### Recommended Test Structure
|
||||
|
||||
|
||||
@@ -32,14 +32,15 @@ Stalker support covers:
|
||||
Primary route tree lives in
|
||||
`libs/portal/stalker/feature/src/lib/stalker-feature.routes.ts`.
|
||||
|
||||
- `/stalker/:id/vod`
|
||||
- `/stalker/:id/series`
|
||||
- `/stalker/:id/vod` (plus `vod/:categoryId` child)
|
||||
- `/stalker/:id/series` (plus `series/:categoryId` child)
|
||||
- `/stalker/:id/itv`
|
||||
- `/stalker/:id/radio`
|
||||
- `/stalker/:id/favorites`
|
||||
- `/stalker/:id/recent`
|
||||
- `/stalker/:id/search`
|
||||
- `/stalker/:id/downloads` (shared downloads module from Xtream UI)
|
||||
- `/stalker/:id/actor/:personId`
|
||||
- `/stalker/:id/downloads` (shared `DownloadsComponent` from `@iptvnator/portal/downloads/feature`)
|
||||
|
||||
## Runtime Architecture
|
||||
|
||||
@@ -48,20 +49,20 @@ Primary route tree lives in
|
||||
3. Requests go through `DataService.sendIpcEvent(STALKER_REQUEST, ...)` or `StalkerSessionService` (full portal auth).
|
||||
4. Electron main process handles `STALKER_REQUEST` in
|
||||
`apps/electron-backend/src/app/events/stalker.events.ts`.
|
||||
5. Axios calls Stalker `load.php` API with required headers/cookies and returns normalized payloads to renderer.
|
||||
5. Axios calls Stalker `load.php` API with required headers/cookies and returns the raw `response.data` to the renderer; normalization happens in the store feature slices.
|
||||
|
||||
## Main UI Components
|
||||
|
||||
- `libs/portal/stalker/feature/src/lib/stalker-main-container.component.ts`
|
||||
- Category + content layout for `vod` and `series`
|
||||
- `CategoryContentViewComponent` from `@iptvnator/portal/catalog/feature` (`libs/portal/catalog/feature`)
|
||||
- Shared category + content layout used by the `vod` and `series` routes (wired in `stalker-feature.routes.ts` via `loadCategoryContentViewComponent`)
|
||||
- `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts`
|
||||
- ITV live playback, radio playback, channel/station navigation, EPG panel integration
|
||||
- `libs/ui/playback/src/lib/audio-player/audio-player.component.ts`
|
||||
- Shared inline audio player used by M3U radio channels and Stalker radio stations
|
||||
- `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts`
|
||||
- Season/episode UI for all Stalker series modes
|
||||
- `libs/portal/stalker/feature/src/lib/stalker-favorites/stalker-favorites.component.ts`
|
||||
- `libs/portal/stalker/feature/src/lib/recently-viewed/recently-viewed.component.ts`
|
||||
- `libs/portal/stalker/feature/src/lib/stalker-collection-route.component.ts`
|
||||
- Favorites and recently-viewed collection views (`mode = 'favorites' | 'recent'` route data), rendering `stalker-collection-detail.component.ts`
|
||||
- `libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts`
|
||||
|
||||
## Store and Data Flow
|
||||
@@ -342,9 +343,9 @@ Current implementation is shared via Stalker-specific helpers:
|
||||
|
||||
Where this is used:
|
||||
|
||||
- `libs/portal/stalker/feature/src/lib/stalker-favorites/stalker-favorites.component.ts`
|
||||
- `libs/portal/stalker/feature/src/lib/recently-viewed/recently-viewed.component.ts`
|
||||
- `libs/portal/stalker/feature/src/lib/stalker-collection-detail.component.ts` (favorites + recently viewed)
|
||||
- `libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts`
|
||||
- `libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts`
|
||||
- `libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts`
|
||||
|
||||
Navigation rule to preserve:
|
||||
@@ -408,7 +409,7 @@ See full backend and web-remote flow in [Remote Control Architecture](./remote-c
|
||||
Stalker ITV now splits EPG usage:
|
||||
|
||||
- active channel panel: bulk `get_epg_info` cached once per playlist and rendered
|
||||
through shared `app-epg-list`
|
||||
through the shared EPG panel (`app-epg-timeline`, or `app-epg-list-view` in list mode)
|
||||
- channel row preview: no pre-playback network requests; previews are derived
|
||||
from cached bulk EPG only after the first active-channel fetch succeeds
|
||||
- active panel fallback: `get_short_epg` when bulk EPG is missing or unsupported
|
||||
|
||||
@@ -215,7 +215,7 @@ Single table with two row kinds discriminated by `lookup_key` prefix:
|
||||
```
|
||||
tmdb_metadata (
|
||||
media_type 'movie' | 'tv' | 'person',
|
||||
lookup_key 'id:<tmdbId>' -- details payload row
|
||||
lookup_key 'id:<tmdbId>|v2' -- details payload row
|
||||
'title:<normalized>|year:<y>|v2' -- search resolution row
|
||||
'person:<personId>' -- person payload row
|
||||
language TEXT, -- TMDB language code
|
||||
@@ -229,8 +229,11 @@ tmdb_metadata (
|
||||
TTLs (enforced at read time in `TmdbCacheService.isFresh`): details and
|
||||
positive matches 30 days, negative matches 7 days.
|
||||
|
||||
Search keys carry a version suffix so normalization changes cannot reuse stale
|
||||
positive or negative resolutions. Database startup deletes the obsolete
|
||||
Search and details keys carry a `|v2` version suffix (`buildDetailsLookupKey`
|
||||
in `tmdb-matcher.ts`): for search rows so normalization changes cannot reuse
|
||||
stale positive or negative resolutions, for details rows because payloads now
|
||||
include videos via `append_to_response` and pre-videos cache rows had to be
|
||||
invalidated. Database startup deletes the obsolete
|
||||
unversioned search rows once and records
|
||||
`migration:tmdb-search-lookup-v2-cache-cleanup:v1` in `app_state`; details and
|
||||
person cache rows are unaffected.
|
||||
|
||||
@@ -12,8 +12,12 @@ Related:
|
||||
- The dashboard is the default `/workspace` landing page.
|
||||
- It is a **rail-based** content surface (Netflix / Apple TV pattern), not a
|
||||
customizable widget grid.
|
||||
- Layout is static and curated — there is no edit mode, drag-drop, size
|
||||
stepper, show/hide toggle, or persisted layout. Rails auto-hide when empty.
|
||||
- Layout order is static and curated — there is no edit mode, drag-drop, or
|
||||
size stepper. Each rail has a persisted show/hide toggle
|
||||
(`Settings.dashboardRails`, `DashboardRailsSettings` in
|
||||
`libs/shared/interfaces/src/lib/settings.interface.ts`, surfaced under
|
||||
Settings → Dashboard); every template rail is gated by
|
||||
`dashboardRails().<key>`. Rails additionally auto-hide when empty.
|
||||
- First-run users see the shared welcome empty-state with a single primary
|
||||
CTA to add their first playlist.
|
||||
|
||||
@@ -38,14 +42,23 @@ Core implementation:
|
||||
│ Continue Watching · See all → │
|
||||
│ [poster][poster][poster][poster] →→ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Live now on your favorites / Continue with live TV · See all → │
|
||||
│ Live now on your favorites · See all → │
|
||||
│ [channel][channel][channel][channel] →→ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Recently watched live TV · See all → │
|
||||
│ [channel][channel][channel][channel] →→ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Favorite movies & series · See all → │
|
||||
│ [poster][poster][poster][poster] →→ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Recently Used Sources · See all → │
|
||||
│ [tile][tile][tile][tile] →→ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Recently Added on Xtream (aggregated across providers) │
|
||||
│ [poster][poster][poster] →→ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Trending this week (TMDB, opt-in, Electron-only) │
|
||||
│ [poster][poster][poster] →→ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -55,7 +68,7 @@ Render rules:
|
||||
page no longer uses `dashboardReady()` as a page-wide skeleton gate.
|
||||
Initial hero/recent/favorites loading states render scoped skeletons so one
|
||||
slow rail does not hide already available content.
|
||||
2. `hasPlaylists() === false` → render `<app-empty-state type="welcome">`
|
||||
2. `hasPlaylists() === false` → render `<app-empty-state [type]="'welcome-dashboard'">`
|
||||
full-bleed. All rails and the hero are skipped.
|
||||
3. `hero()` = `globalRecentItems()[0]`. If present, render the hero panel.
|
||||
4. Each rail is emitted via `@if (cards.length > 0)`. Empty rails are hidden
|
||||
@@ -63,9 +76,11 @@ Render rules:
|
||||
5. The continue-watching hero prefers a stored Xtream `backdrop_url`; when it
|
||||
is missing the UI falls back to a blurred poster treatment instead of
|
||||
showing a flat panel.
|
||||
6. The mixed global favorites rail is not rendered on the dashboard. Live
|
||||
favorites are promoted into the live rail, while mixed favorites stay on
|
||||
`/workspace/global-favorites`.
|
||||
6. Live favorites are promoted into their own live rail; movie/series
|
||||
favorites render in a separate `Favorite movies & series` rail
|
||||
(`favoriteMoviesAndSeriesCards`, `data-test-id="dashboard-favorite-vod-rail"`,
|
||||
mapped from `globalFavoriteItems()` filtered to movie/series). Full mixed
|
||||
favorites management stays on `/workspace/global-favorites`.
|
||||
7. The live favorites rail keeps its scoped skeleton until the initial global
|
||||
favorites load has completed for both Xtream-backed and playlist-backed
|
||||
favorites. This avoids first-paint partial counts such as a single Stalker
|
||||
@@ -106,9 +121,10 @@ Render rules:
|
||||
metadata and playback positions load, the detail player consumes the
|
||||
target once and resumes the saved episode. Opening the same item normally
|
||||
from the global recent grid remains a detail-only action.
|
||||
3. `liveOnFavoritesCardsEnriched` — maps favorited live channels first,
|
||||
falling back to recently watched live channels when no live favorites
|
||||
exist. M3U cards carry an `epg_lookup_key` using the app-wide XMLTV
|
||||
3. `liveFavoriteCardsEnriched` and `recentLiveCardsEnriched` — two
|
||||
independent rails (`dashboard-live-favorites-rail` and
|
||||
`dashboard-recent-live-rail`); there is no fallback from one to the
|
||||
other. M3U cards carry an `epg_lookup_key` using the app-wide XMLTV
|
||||
fallback order (`tvg-id` -> `tvg-name` -> channel name); EPG enrichment
|
||||
must use that key before falling back to the card title.
|
||||
4. `xtreamRecentlyAddedCards` — maps `xtreamRecentlyAddedItems()` to rail
|
||||
@@ -130,7 +146,10 @@ Render rules:
|
||||
3. `DashboardDataService` is passive on construction. The dashboard feature
|
||||
owns the initial reloads for recent items, favorites, and Xtream recently
|
||||
added rows on page entry.
|
||||
4. No `Layout` state, no localStorage keys, no migrations.
|
||||
4. No dashboard-local `Layout` state, no localStorage keys, no migrations.
|
||||
Per-rail visibility is the one persisted preference, and it lives in the
|
||||
global settings store (`Settings.dashboardRails`), not in a
|
||||
dashboard-owned layout blob.
|
||||
5. Navigation state + deep-link targets come from the existing
|
||||
`getRecentItemLink()` / `getGlobalFavoriteLink()` / `getPlaylistLink()`
|
||||
helpers on `DashboardDataService` and reuse the workspace navigation
|
||||
@@ -162,7 +181,7 @@ Render rules:
|
||||
## Empty State
|
||||
|
||||
The welcome state is rendered via the existing
|
||||
`EmptyStateComponent` (`type="welcome"`) from
|
||||
`EmptyStateComponent` (`type="welcome-dashboard"`) from
|
||||
`libs/playlist/shared/ui`:
|
||||
|
||||
1. Illustration + headline + description from the existing M3U welcome
|
||||
@@ -189,8 +208,9 @@ The welcome state is rendered via the existing
|
||||
7. `Recently Used Sources` reflects recent source usage across all provider
|
||||
types, not just recent imports.
|
||||
8. The live rail title key must match the rendered source: favorites use
|
||||
`WORKSPACE.DASHBOARD.LIVE_FAVORITES`; recently watched fallback uses
|
||||
`WORKSPACE.DASHBOARD.LIVE_RECENT`.
|
||||
`WORKSPACE.DASHBOARD.LIVE_FAVORITES`; the recently-watched-live rail uses
|
||||
`WORKSPACE.DASHBOARD.RECENTLY_WATCHED_LIVE_TV`
|
||||
(`liveRailTitleKeyForSource` in `rails/dashboard-rail.utils.ts`).
|
||||
|
||||
## Adding Or Changing Rails
|
||||
|
||||
@@ -210,8 +230,9 @@ Current workflow:
|
||||
|
||||
Intentionally out of scope:
|
||||
|
||||
1. Customizable layout (drag/drop, resize, show/hide toggles, layout
|
||||
persistence). Removed in favor of a curated, opinionated order.
|
||||
1. Customizable layout (drag/drop, resize, freeform reordering). The rail
|
||||
order stays curated and opinionated. (Per-rail show/hide toggles have
|
||||
since shipped via `Settings.dashboardRails` — see Summary.)
|
||||
2. Freeform widget grid with collision management.
|
||||
3. External data rails such as RSS, sports, or news adapters.
|
||||
4. Per-user A/B variants of rail ordering.
|
||||
@@ -38,10 +38,15 @@ Core implementation:
|
||||
Current workspace routes:
|
||||
|
||||
1. `/` -> `/workspace`
|
||||
2. `/workspace` -> `/workspace/dashboard`
|
||||
2. `/workspace` -> functional redirect `workspaceEntryRedirect`
|
||||
(`WorkspaceStartupPreferencesService.resolveInitialWorkspacePath()`;
|
||||
`/workspace/dashboard` by default, `/workspace/sources` when the dashboard
|
||||
is disabled, or the last restorable route under
|
||||
`StartupBehavior.RestoreLastView` — `dashboard` itself is guarded by
|
||||
`dashboardAccessGuard`)
|
||||
3. `/workspace/dashboard`
|
||||
4. `/workspace/sources`
|
||||
5. `/workspace/playlists/:id/:view`
|
||||
5. `/workspace/playlists/:id/:view` (plus `favorites` and `recent` siblings)
|
||||
6. `/workspace/global-favorites`
|
||||
7. `/workspace/global-recent`
|
||||
8. `/workspace/search`
|
||||
|
||||
Reference in new issue
Block a user