mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
docs: refactor and document architecture for Stalker and Workspace components
- Moved Stalker-related components and services from apps/web to libs/portal for better modularity. - Introduced SQLite DB Worker to handle non-EPG database operations, improving UI responsiveness. - Updated documentation for the Workspace Dashboard and Shell, detailing current implementation and routing structure. - Added new EPG fixture scenarios to the Xtream mock server for testing purposes. - Enhanced the overall architecture documentation to reflect recent changes and improvements.
This commit is contained in:
1 parent
c42ddeaef2
commit
d366672506
16 files changed
+1493
-200
No files matched your search
@@ -56,7 +56,7 @@ ALTER TABLE categories ADD COLUMN hidden INTEGER DEFAULT 0
|
||||
|
||||
**Category Management Dialog**
|
||||
|
||||
- Path: `apps/web/src/app/xtream-electron/category-management-dialog/`
|
||||
- Path: `libs/portal/xtream/feature/src/lib/category-management-dialog/`
|
||||
- Features:
|
||||
- Checkbox list of all categories
|
||||
- Select All / Deselect All buttons
|
||||
@@ -77,7 +77,7 @@ Both components:
|
||||
|
||||
### Store
|
||||
|
||||
**File**: `apps/web/src/app/xtream-electron/xtream.store.ts`
|
||||
**File**: `libs/portal/xtream/data-access/src/lib/stores/xtream.store.ts`
|
||||
|
||||
Added `reloadCategories()` method to refresh categories from database after visibility changes, ensuring the sidebar updates immediately.
|
||||
|
||||
@@ -96,9 +96,23 @@ When a user refreshes an Xtream playlist, hidden category preferences are preser
|
||||
2. **Temporary storage**: The hidden categories are stored in `localStorage` under key `xtream-restore-{playlistId}` along with favorites and recently viewed data
|
||||
3. **During re-import**: When categories are saved via `DB_SAVE_CATEGORIES`, the data source checks `localStorage` for saved hidden category xtreamIds
|
||||
4. **Restoration**: Categories matching the saved xtreamIds are inserted with `hidden = true`, preserving the user's visibility preferences
|
||||
5. **ID normalization**: Xtream category IDs arrive from the API as strings, while SQLite stores `categories.xtream_id` as an integer. Restoration must normalize incoming `category_id` values before matching them against saved hidden-category xtreamIds.
|
||||
|
||||
This ensures that users don't lose their category visibility customizations when refreshing playlists to get updated content.
|
||||
|
||||
### Debugging Note
|
||||
|
||||
Hidden-category restoration runs through the Electron DB worker. When debugging
|
||||
or validating a fix in a live Electron app:
|
||||
|
||||
1. rebuild the worker-backed Electron runtime
|
||||
2. restart the running Electron process
|
||||
3. reconnect `agent-browser --cdp 9222`
|
||||
|
||||
Otherwise the app may still be using an older
|
||||
`dist/apps/electron-backend/workers/database.worker.js` bundle even though the
|
||||
TypeScript source has already been updated.
|
||||
|
||||
## Files Changed
|
||||
|
||||
```
|
||||
@@ -117,20 +131,21 @@ libs/services/src/lib/
|
||||
libs/ui/components/src/lib/recent-playlists/
|
||||
└── recent-playlists.component.ts # Stores hidden categories to localStorage on refresh
|
||||
|
||||
apps/web/src/app/xtream-electron/
|
||||
├── category-management-dialog/ # Dialog component
|
||||
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
|
||||
├── data-sources/
|
||||
│ └── electron-xtream-data-source.ts # Reads/passes hidden categories on save
|
||||
├── xtream-main-container.component.ts # Added button & dialog
|
||||
├── 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.ts # Added button & dialog
|
||||
│ └── live-stream-layout.component.html
|
||||
├── sidebar.scss # Updated header styles
|
||||
└── xtream.store.ts # Added reloadCategories method
|
||||
|
||||
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
|
||||
|
||||
apps/web/src/assets/i18n/
|
||||
└── en.json # Added translation keys
|
||||
|
||||
@@ -15,7 +15,7 @@ The download manager is a desktop-only feature that layers a curated queue, prog
|
||||
|
||||
- **Downloads service** (`apps/web/src/app/services/downloads.service.ts`)
|
||||
Signals back the current download list while `hasDownloads` and `isAvailable` gates UI rendering. Before each download the service resolves a download folder (stored in `SettingsStore` or fetched via `downloadsGetDefaultFolder`) 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** (`apps/web/src/app/xtream-electron/downloads`)
|
||||
- **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`.
|
||||
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**
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
# Embedded Inline Playback
|
||||
|
||||
This document records the current contract for embedded playback in portal detail views.
|
||||
|
||||
## Summary
|
||||
|
||||
- Embedded web players are `videojs`, `html5`, and `artplayer`.
|
||||
- External players are `mpv` and `vlc`.
|
||||
- Live playback stays inline in dedicated live layouts.
|
||||
- VOD and series detail playback now also stays inline on canonical detail surfaces.
|
||||
- Material dialog playback remains only as a fallback for older non-detail callers.
|
||||
|
||||
## Scope
|
||||
|
||||
The first pass is intentionally limited:
|
||||
|
||||
- Xtream VOD detail route
|
||||
- Xtream series detail route
|
||||
- Stalker VOD detail view
|
||||
- Stalker series detail view
|
||||
|
||||
Not migrated in this pass:
|
||||
|
||||
- Generic non-detail playback entry points that still call `PlayerService.openPlayer(...)`
|
||||
- Any collection/search surface that does not host a canonical detail surface of its own
|
||||
|
||||
## Components
|
||||
|
||||
Shared inline player shell:
|
||||
|
||||
- `/Users/4gray/Code/iptvnator/libs/ui/components/src/lib/portal-inline-player/portal-inline-player.component.ts`
|
||||
|
||||
Xtream detail hosts:
|
||||
|
||||
- `/Users/4gray/Code/iptvnator/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts`
|
||||
|
||||
Stalker detail hosts:
|
||||
|
||||
- `/Users/4gray/Code/iptvnator/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/libs/ui/components/src/lib/stalker-series-view/stalker-series-view.component.ts`
|
||||
|
||||
Fallback dialog path:
|
||||
|
||||
- `/Users/4gray/Code/iptvnator/apps/web/src/app/services/player.service.ts`
|
||||
- `/Users/4gray/Code/iptvnator/libs/portal/xtream/feature/src/lib/player-dialog/player-dialog.component.ts`
|
||||
|
||||
## Playback Decision Rule
|
||||
|
||||
When a detail view starts playback:
|
||||
|
||||
1. Resolve or construct a typed playback payload.
|
||||
2. Check the active player setting.
|
||||
3. If the player is embedded, render the inline player inside the current detail view.
|
||||
4. If the player is external, hand the same payload to `PlayerService` for MPV/VLC playback.
|
||||
|
||||
The detail host owns inline state. `PlayerService` is no longer the primary owner of UI playback state for canonical VOD/series detail screens.
|
||||
|
||||
## Typed Playback Payload
|
||||
|
||||
Shared playback payloads live in:
|
||||
|
||||
- `/Users/4gray/Code/iptvnator/libs/shared/interfaces/src/lib/portal-playback.interface.ts`
|
||||
|
||||
Types introduced:
|
||||
|
||||
- `PlayerContentInfo`
|
||||
- `ResolvedPortalPlayback`
|
||||
|
||||
These provide a single shape for:
|
||||
|
||||
- `streamUrl`
|
||||
- `title`
|
||||
- optional thumbnail and resume start time
|
||||
- playback-position metadata
|
||||
- optional external-player headers and request metadata
|
||||
|
||||
## Xtream Behavior
|
||||
|
||||
Xtream detail views already own canonical routes, so they construct playback locally and decide inline vs external locally.
|
||||
|
||||
Behavior to preserve:
|
||||
|
||||
- resume/playback position continues saving from `timeUpdate`
|
||||
- back navigation clears inline playback with the route
|
||||
- favorites, recent, and search still route into canonical Xtream detail screens before playback
|
||||
|
||||
## Stalker Behavior
|
||||
|
||||
Stalker previously resolved playback and opened UI in the same method.
|
||||
|
||||
Current contract:
|
||||
|
||||
- `resolveVodPlayback(...)` returns a `ResolvedPortalPlayback`
|
||||
- `createLinkToPlayVod(...)` remains as a compatibility wrapper for untouched callers
|
||||
- canonical Stalker detail views use the resolver directly and decide inline vs external locally
|
||||
|
||||
This keeps:
|
||||
|
||||
- inline/store-state detail navigation intact
|
||||
- series and VOD-as-series support intact
|
||||
- non-detail callers working until they are migrated
|
||||
|
||||
## Playback Position Saving
|
||||
|
||||
The old dialog path saved playback positions from inside `PlayerDialogComponent`.
|
||||
|
||||
The new contract is:
|
||||
|
||||
- inline detail hosts listen to `timeUpdate`
|
||||
- each host throttles saves
|
||||
- each host persists via existing playback-position infrastructure
|
||||
|
||||
This avoids coupling inline UI state to a global dialog.
|
||||
|
||||
## Future Migration Rule
|
||||
|
||||
If a non-detail surface is converted away from dialog playback:
|
||||
|
||||
- give that surface a canonical inline host
|
||||
- switch it to `ResolvedPortalPlayback`
|
||||
- do not move portal-specific navigation into `PlayerService`
|
||||
|
||||
The preferred direction is view-owned inline playback, not a larger dialog manager.
|
||||
@@ -0,0 +1,97 @@
|
||||
# External Wiki Sync
|
||||
|
||||
Related:
|
||||
|
||||
- [Workspace Shell](./workspace-shell.md)
|
||||
- [SQLite DB Worker](./sqlite-db-worker.md)
|
||||
|
||||
## Summary
|
||||
|
||||
- Repo docs are canonical, even when they were originally drafted by an LLM.
|
||||
- The external Obsidian wiki imports canonical repo docs read-only into `_repo-context/`.
|
||||
- Higher-level synthesis pages stay outside `_repo-context/` in the wiki's own folders.
|
||||
- Sync is one-way by default: repo docs -> wiki context.
|
||||
|
||||
## Ownership Model
|
||||
|
||||
Canonical repo docs include:
|
||||
|
||||
1. `docs/architecture/**/*.md`
|
||||
2. top-level workflow docs such as `README.md`, `GETTING-STARTED.md`, `AGENTS.md`, and `CLAUDE.md`
|
||||
3. selected module `README.md` files when they describe current code behavior or workflows
|
||||
|
||||
The external wiki can add cross-links, feature pages, decision notes, and synthesis pages, but it must not become a second source of truth for the same implementation details.
|
||||
|
||||
## Export Scope
|
||||
|
||||
The repo-owned exporter writes only to `_repo-context/` inside the external vault.
|
||||
|
||||
Current default export scope:
|
||||
|
||||
1. `docs/architecture/**/*.md`
|
||||
2. `README.md`
|
||||
3. `GETTING-STARTED.md`
|
||||
4. `AGENTS.md`
|
||||
5. `CLAUDE.md`
|
||||
6. selected module `README.md` files
|
||||
|
||||
The exporter also generates:
|
||||
|
||||
1. `_repo-context/index.md`
|
||||
2. `_repo-context/repo-map.md`
|
||||
3. `_repo-context/recent-changes.md`
|
||||
4. `_repo-context/manifest.json`
|
||||
5. `_repo-context/state.json`
|
||||
|
||||
## Running The Exporter
|
||||
|
||||
Set the external vault path in the shell environment:
|
||||
|
||||
```bash
|
||||
export IPTVNATOR_WIKI_VAULT=/absolute/path/to/your/obsidian-vault
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
pnpm wiki:export --mode full
|
||||
pnpm wiki:export --mode changed
|
||||
```
|
||||
|
||||
You can also override the vault path per command:
|
||||
|
||||
```bash
|
||||
pnpm wiki:export --mode changed --vault /absolute/path/to/your/obsidian-vault
|
||||
```
|
||||
|
||||
If the vault path is missing, the exporter skips cleanly and reports why it did not run.
|
||||
|
||||
## Agent Workflow After Changes
|
||||
|
||||
After a meaningful implementation change, agents must assess whether canonical repo docs need updates.
|
||||
|
||||
Documentation-worthy changes include:
|
||||
|
||||
1. new or changed user-visible behavior
|
||||
2. architecture or data-flow changes
|
||||
3. non-obvious maintenance workflows
|
||||
4. new setup, debugging, or operational steps
|
||||
5. new subsystem contracts or boundaries
|
||||
|
||||
Prefer updating an existing authoritative doc before creating a new one:
|
||||
|
||||
1. `README.md` for top-level developer or user workflows
|
||||
2. `docs/architecture/` for architecture, ownership, and behavior contracts
|
||||
3. the nearest module `README.md` for local usage or behavior
|
||||
|
||||
If docs changed and `IPTVNATOR_WIKI_VAULT` is configured, agents should run `pnpm wiki:export --mode changed` before considering the task complete.
|
||||
|
||||
## Promotion Workflow
|
||||
|
||||
If a wiki page becomes stable enough to be canonical:
|
||||
|
||||
1. promote that content back into a repo doc
|
||||
2. treat the repo doc as the source of truth
|
||||
3. export again so `_repo-context/` reflects the promoted canonical doc
|
||||
|
||||
The wiki page can then either link to the repo-backed generated page or remain as a smaller synthesis page that references the canonical doc.
|
||||
@@ -0,0 +1,252 @@
|
||||
# IPTVnator UI Guidelines
|
||||
|
||||
This document captures the current UI language used across IPTVnator, with emphasis on channel lists, EPG views, settings surfaces, and shared selection patterns.
|
||||
|
||||
Use it when changing existing views or introducing new list-based UI in the workspace, Xtream, or Stalker flows.
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. Prefer shared components over duplicated markup.
|
||||
The canonical channel row is `app-channel-list-item`.
|
||||
|
||||
2. Drive emphasis through selection state, not through constant decoration.
|
||||
Neutral rows should stay quiet. Only active or current items should pick up strong color.
|
||||
|
||||
3. Use the same selection language everywhere.
|
||||
Selected nav items, channels, and current EPG cards should feel like the same system.
|
||||
|
||||
4. Keep dark and light themes intentionally different.
|
||||
Dark theme can carry more density and tinted surfaces.
|
||||
Light theme should be flatter and cleaner, with white or near-white cards.
|
||||
|
||||
5. Scroll ownership must be explicit.
|
||||
Headers stay visible. Lists scroll. Do not let nested panes compete for scroll.
|
||||
|
||||
## Canonical References
|
||||
|
||||
- Channel row:
|
||||
`libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.html`
|
||||
- Channel row styles:
|
||||
`libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss`
|
||||
- Shared EPG pane:
|
||||
`libs/ui/shared-portals/src/lib/epg-view/epg-view.component.html`
|
||||
- Shared EPG pane styles:
|
||||
`libs/ui/shared-portals/src/lib/epg-view/epg-view.component.scss`
|
||||
- Shared list selection style:
|
||||
`apps/web/src/nav-list.scss`
|
||||
- Theme tokens:
|
||||
`apps/web/src/m3-theme.scss`
|
||||
- Settings surfaces:
|
||||
`apps/web/src/app/settings/settings.component.scss`
|
||||
|
||||
## Shared Tokens
|
||||
|
||||
These tokens are the base for interactive emphasis:
|
||||
|
||||
- `--app-selection-color`
|
||||
- `--app-selection-surface`
|
||||
- `--app-selection-surface-strong`
|
||||
- `--app-selection-border`
|
||||
- `--app-selection-glow`
|
||||
|
||||
Use Material surface tokens for neutral surfaces:
|
||||
|
||||
- `--mat-sys-surface`
|
||||
- `--mat-sys-surface-container-low`
|
||||
- `--mat-sys-surface-container`
|
||||
- `--mat-sys-surface-container-high`
|
||||
- `--mat-sys-outline-variant`
|
||||
- `--mat-sys-on-surface`
|
||||
- `--mat-sys-on-surface-variant`
|
||||
|
||||
Do not hardcode unrelated accent colors for selected state when these tokens already exist.
|
||||
|
||||
## Selection Pattern
|
||||
|
||||
Apply the same visual recipe to selected list items, active channels, and current EPG items:
|
||||
|
||||
- Background:
|
||||
`linear-gradient(135deg, var(--app-selection-surface-strong), var(--app-selection-surface))`
|
||||
- Border:
|
||||
`var(--app-selection-border)`
|
||||
- Glow:
|
||||
outer shadow using `var(--app-selection-glow)`
|
||||
- Lift:
|
||||
`transform: translateY(-1px)` for selected list items only
|
||||
- Text:
|
||||
selected text should inherit `var(--app-selection-color)`
|
||||
|
||||
Use this pattern for:
|
||||
|
||||
- `.nav-item.selected` / `.nav-item.active`
|
||||
- `.channel-list-item.active`
|
||||
- `.epg-item.current-program`
|
||||
|
||||
Do not add extra badges, left rails, or second selection systems unless there is a strong reason.
|
||||
|
||||
## Channel List Item
|
||||
|
||||
The shared row should be reused instead of rebuilding channel markup per view.
|
||||
|
||||
### Structure
|
||||
|
||||
- Min height:
|
||||
`68px`
|
||||
- Horizontal gap:
|
||||
`12px`
|
||||
- Padding:
|
||||
`8px 10px 8px 12px`
|
||||
- Radius:
|
||||
`12px`
|
||||
- Logo shell:
|
||||
`44x44`, rounded, subtle inset treatment
|
||||
- Compact variant:
|
||||
`52px` min height with slightly tighter padding
|
||||
|
||||
### Content Layout
|
||||
|
||||
- Title is one line, medium-bold, slightly condensed
|
||||
- Program title is a secondary line with lower emphasis
|
||||
- Timeline uses three columns:
|
||||
start time, progress bar, end time
|
||||
- Action buttons sit on the trailing edge and inherit row color
|
||||
|
||||
### Logo Rules
|
||||
|
||||
- Show fallback icon only when no image is available or image loading fails
|
||||
- Do not render placeholder and real logo at the same time
|
||||
- Keep logos contained with `object-fit: contain`
|
||||
|
||||
## EPG Views
|
||||
|
||||
### Shared EPG Pane
|
||||
|
||||
- Header title stays sticky
|
||||
- Program list is the only scrolling region
|
||||
- Add bottom padding so the last program is not clipped
|
||||
- Current program card uses the same selection treatment as selected channels
|
||||
|
||||
### EPG Card
|
||||
|
||||
- Radius:
|
||||
`14px`
|
||||
- Neutral cards use low-contrast surface treatment
|
||||
- Current card uses selection surface and selection border
|
||||
- Description should clamp rather than overflow
|
||||
|
||||
### Sticky Header
|
||||
|
||||
- Keep the title readable above content
|
||||
- Use a solid or near-solid backing surface
|
||||
- Do not let it overlap or cover player controls
|
||||
|
||||
## Progress Bars
|
||||
|
||||
Channel preview progress and EPG current-program progress should stay visually aligned.
|
||||
|
||||
### Track
|
||||
|
||||
- Height:
|
||||
`6px`
|
||||
- Shape:
|
||||
full pill radius
|
||||
- Neutral background:
|
||||
medium gray or neutral surface tint
|
||||
- Include a slight inset edge so the remaining duration is visible
|
||||
|
||||
### Fill
|
||||
|
||||
- Use `--app-selection-color`
|
||||
- Add a subtle sheen, not a heavy gradient
|
||||
- Add a restrained glow, not a neon effect
|
||||
|
||||
The progress bar should clearly communicate:
|
||||
|
||||
- completed duration
|
||||
- remaining duration
|
||||
|
||||
Avoid making the track too faint, especially in dark theme.
|
||||
|
||||
## Navigation Lists
|
||||
|
||||
Use the shared `nav-list.scss` treatment for sidebar and context-panel list items.
|
||||
|
||||
### Rules
|
||||
|
||||
- Keep labels one line with ellipsis
|
||||
- Keep icon area clear from the selection border and any decorative rail
|
||||
- Hover is neutral surface, not the selected color
|
||||
- Selected state uses the shared selection recipe
|
||||
|
||||
If the label is too long for the rail, shorten the label key instead of shrinking the component until it becomes inconsistent.
|
||||
|
||||
## Settings Surfaces
|
||||
|
||||
Settings use the same system but are flatter than content-heavy views.
|
||||
|
||||
### Light Theme
|
||||
|
||||
- Prefer white or near-white cards
|
||||
- Use neutral borders from `--mat-sys-outline-variant`
|
||||
- Keep active sections mostly defined by outline and subtle tint
|
||||
- Avoid dark translucent backgrounds
|
||||
|
||||
### Dark Theme
|
||||
|
||||
- Denser tinted surfaces are acceptable
|
||||
- Neutral rows can use low-opacity dark overlays
|
||||
- Keep strong blue tint reserved for active sections and selected items
|
||||
|
||||
## Theme Guidance
|
||||
|
||||
### Light Theme
|
||||
|
||||
- Flat beats glossy
|
||||
- White and surface-container layers should separate content
|
||||
- Selection should read as a blue outline plus soft tint, not a solid slab
|
||||
|
||||
### Dark Theme
|
||||
|
||||
- Slight translucency is acceptable
|
||||
- Background layers can be deeper and more cinematic
|
||||
- Keep contrast readable without going pure white everywhere
|
||||
|
||||
## Reuse Strategy
|
||||
|
||||
Before creating new markup or CSS:
|
||||
|
||||
1. Check whether `app-channel-list-item` can be reused.
|
||||
2. Check whether `app-epg-view` already provides the correct structure.
|
||||
3. Check whether `nav-list.scss` already solves the list-selection problem.
|
||||
4. Extend tokens first, duplicate styles last.
|
||||
|
||||
## Implementation Workflow
|
||||
|
||||
When updating IPTVnator UI:
|
||||
|
||||
1. Inspect the current shared component first.
|
||||
2. Reuse the shared structure where possible.
|
||||
3. Keep selection, progress, and spacing in sync across Xtream, Stalker, and shared portal views.
|
||||
4. Verify in both light and dark themes.
|
||||
5. Verify in the running Electron app when the change is visual or layout-sensitive.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
Avoid these:
|
||||
|
||||
- introducing a new selected-state color unrelated to the theme tokens
|
||||
- duplicating channel row markup in portal-specific views
|
||||
- showing placeholder logos behind real logos
|
||||
- making entire panes scroll when only the list should scroll
|
||||
- using dark translucent fills unchanged in light theme
|
||||
- solving cramped sidebars with smaller fonts instead of shorter labels
|
||||
|
||||
## Definition Of Done For UI Changes
|
||||
|
||||
A visual change is not done until:
|
||||
|
||||
1. Shared component reuse was considered first.
|
||||
2. Light theme and dark theme both look intentional.
|
||||
3. Selection and progress states match existing IPTVnator patterns.
|
||||
4. Scroll behavior is correct.
|
||||
5. The result was checked in the running app for layout-sensitive work.
|
||||
@@ -16,7 +16,7 @@ The M3U playlist module provides:
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ VIDEO PLAYER PAGE │
|
||||
│ apps/web/src/app/home/video-player/ │
|
||||
│ libs/playlist/m3u/feature-player/src/lib/video-player/ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌──────────────────────┐ ┌────────────────────┐ │
|
||||
│ │ Sidebar │ │ Video Player │ │ EPG List │ │
|
||||
@@ -237,7 +237,7 @@ class EpgService {
|
||||
|
||||
## Video Player
|
||||
|
||||
**Location**: `apps/web/src/app/home/video-player/`
|
||||
**Location**: `libs/playlist/m3u/feature-player/src/lib/video-player/`
|
||||
|
||||
### Supported Players
|
||||
- **ArtPlayer** (default) - Modern player with plugins
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# UX/UI Analysis: Header vs Rail Navigation
|
||||
|
||||
Date: 2026-03-22
|
||||
|
||||
## Overview
|
||||
|
||||
Evaluation of IPTVnator's navigation architecture — specifically the separation between **global actions in the top header** and **playlist-local actions in the left rail sidebar**, assessed from a user understanding perspective.
|
||||
|
||||
## Current Architecture
|
||||
|
||||
| Region | Intended Scope | Actual Contents |
|
||||
|--------|---------------|-----------------|
|
||||
| **Header** (top) | Global / app-wide | Playlist switcher, search, add playlist, global favorites, downloads, **context menu with local actions** |
|
||||
| **Rail** (left) | Local / playlist-specific | Dashboard (global), Sources (global), **dynamic provider links** (local), Settings (global) |
|
||||
|
||||
Neither region is purely global or purely local. Both mix scopes, which muddies the mental model.
|
||||
|
||||
## Strengths
|
||||
|
||||
- **Playlist switcher in the header** is excellent placement. Acts like a "workspace context selector" — similar to Slack's workspace switcher or VS Code's project selector.
|
||||
- **Command palette** nails the global-vs-local distinction with explicit "GLOBAL ACTIONS" and "THIS PLAYLIST" section headers. Clearest articulation of scope in the entire UI.
|
||||
- **Rail dividers** between static workspace links (Dashboard, Sources) and dynamic provider links provide a subtle visual boundary hinting at the scope change.
|
||||
- **Search bar adapting its placeholder text** per route is good contextual affordance.
|
||||
- **Settings at the rail bottom** follows a well-established pattern (Slack, Discord, VS Code).
|
||||
|
||||
## Confusion Points
|
||||
|
||||
### A. Rail Mixes Global and Local Without Explaining Why
|
||||
|
||||
When a user selects an Xtream playlist, the rail shows:
|
||||
```
|
||||
Dashboard <- global
|
||||
Sources <- global
|
||||
-----------------
|
||||
Movies <- local (Xtream)
|
||||
Live TV <- local (Xtream)
|
||||
Series <- local (Xtream)
|
||||
-----------------
|
||||
Search <- local (Xtream)
|
||||
Recently viewed <- local (Xtream)
|
||||
Favorites <- local (Xtream)
|
||||
-----------------
|
||||
Settings <- global
|
||||
```
|
||||
|
||||
When switching to M3U:
|
||||
```
|
||||
Dashboard <- global
|
||||
Sources <- global
|
||||
-----------------
|
||||
All channels <- local (M3U)
|
||||
Groups <- local (M3U)
|
||||
Recently viewed <- local (M3U)
|
||||
Favorites <- local (M3U)
|
||||
-----------------
|
||||
Settings <- global
|
||||
```
|
||||
|
||||
**Issue:** The dynamic links change silently. There's no label like "rucolor.tv" or "clean.m3u" above the provider links to indicate *which* playlist these links belong to. Users who switch playlists via the header dropdown may not immediately notice the rail updated.
|
||||
|
||||
**Severity:** Medium.
|
||||
|
||||
### B. Header's Three-Dot Menu Breaks the "Global Header" Mental Model
|
||||
|
||||
The context actions menu in the header contains:
|
||||
- **Playlist Info** — local to the current playlist
|
||||
- **Account Info** — local to the current Xtream portal
|
||||
- **Clear Recently Viewed** — local bulk action
|
||||
|
||||
These are playlist-scoped actions living in what should be the "global" header area.
|
||||
|
||||
**Severity:** Low-Medium.
|
||||
|
||||
### C. "Favorites" Appears in Both Global and Local Contexts
|
||||
|
||||
- **Header:** Global Favorites star icon (cross-playlist)
|
||||
- **Rail:** Favorites link (playlist-specific)
|
||||
|
||||
A user clicking the star in the header vs the heart in the rail gets *different* favorites views with *no* clear labeling of "global" vs "this playlist."
|
||||
|
||||
**Severity:** Medium-High. Most likely source of user confusion.
|
||||
|
||||
### D. Search Bar Scope Is Invisible
|
||||
|
||||
The search bar disables itself on some routes and changes behavior on others. The placeholder text changes, but "Search in this section..." doesn't clarify *which* section.
|
||||
|
||||
**Severity:** Low.
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Quick Wins (Low Effort, High Impact)
|
||||
|
||||
1. **Add a playlist name label above the dynamic rail links.** Small, muted text showing "clean.m3u" or "rucolor.tv" above the provider-specific navigation.
|
||||
2. **Differentiate the Global Favorites icon from Local Favorites.** Use different icons, tooltip distinction, or a small globe badge on the global favorites star.
|
||||
3. **Add a scope label to the search bar** when active: "Searching in Live TV" or "Searching in clean.m3u" instead of generic "Search in this section..."
|
||||
|
||||
### Medium Effort
|
||||
|
||||
4. **Consider moving the three-dot context menu into the context panel** rather than the header, keeping the header purely global.
|
||||
5. **Animate the rail transition** when switching playlists — a subtle slide or fade to signal that links changed.
|
||||
|
||||
## Overall Assessment
|
||||
|
||||
**Score: 7/10 — Good, with clear improvement opportunities.**
|
||||
|
||||
The architecture follows patterns users will recognize from Slack, VS Code, and Spotify. The main risks are the **silent dynamic rail** and the **favorites scope ambiguity**. Fixing those two issues would bring this to a 9/10 for navigational clarity.
|
||||
|
||||
### Design Principle
|
||||
|
||||
The command palette already has the right model: **explicit scope labels**. Apply this same principle to the rail and header. Anywhere an action's scope isn't obvious from its placement, label it.
|
||||
|
||||
## Key Files
|
||||
|
||||
- `libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html`
|
||||
- `libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.ts`
|
||||
- `libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.ts`
|
||||
- `libs/portal/shared/util/src/lib/navigation/portal-rail-links.ts`
|
||||
- `libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher.component.ts`
|
||||
- `libs/workspace/shell/feature/src/lib/workspace-command-palette/workspace-command-palette.component.ts`
|
||||
@@ -0,0 +1,81 @@
|
||||
# Portal Detail Navigation
|
||||
|
||||
This document records the current navigation contract for Xtream and Stalker detail flows, especially for favorites, recently viewed, search, and category content.
|
||||
|
||||
Related:
|
||||
|
||||
- [Embedded Inline Playback](./embedded-inline-playback.md)
|
||||
|
||||
## Summary
|
||||
|
||||
- Xtream uses a route-first detail model.
|
||||
- Stalker uses an inline/store-state detail model.
|
||||
- Keep favorites/recent/search behavior aligned with the canonical detail model of the same portal.
|
||||
- Do not force both portals into the same behavior unless the full portal detail architecture is being changed.
|
||||
|
||||
## Xtream
|
||||
|
||||
Xtream details are represented by canonical routes.
|
||||
|
||||
Examples:
|
||||
- `/xtreams/:id/vod/:categoryId/:vodId`
|
||||
- `/xtreams/:id/series/:categoryId/:serialId`
|
||||
|
||||
Implication:
|
||||
- Favorites, recently viewed, and search should redirect to the original Xtream content route and item route.
|
||||
- This keeps the URL, browser history, and detail rendering model aligned with normal Xtream browsing.
|
||||
|
||||
Current code paths:
|
||||
- `libs/portal/xtream/feature/src/lib/favorites/favorites.component.ts`
|
||||
- `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`
|
||||
|
||||
Behavior to preserve:
|
||||
- Selecting an Xtream item from favorites/recent/search should navigate to the Xtream content type/category/item route when the item is not a live stream.
|
||||
- Live streams can still open through the player path rather than a detail route.
|
||||
|
||||
## Stalker
|
||||
|
||||
Stalker details are represented by store state and inline detail rendering on the current screen.
|
||||
|
||||
Examples:
|
||||
- Category content sets `selectedItem` and renders details inline.
|
||||
- Search sets `selectedItem` and stays on the search view.
|
||||
- Favorites and recently viewed stay on their current collection screen and open inline detail.
|
||||
|
||||
Implication:
|
||||
- Favorites, recently viewed, and search should remain in the current Stalker view when opening VOD/series details.
|
||||
- This keeps Stalker behavior aligned with its normal category-content and search flow.
|
||||
|
||||
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-search/stalker-search.component.ts`
|
||||
- `libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.ts` (Stalker branch)
|
||||
|
||||
Behavior to preserve:
|
||||
- Favorites/recent/search should not navigate away to a canonical Stalker detail route because Stalker does not currently use one.
|
||||
- ITV/live items can still trigger playback immediately.
|
||||
|
||||
## Decision Rule For Future Changes
|
||||
|
||||
When deciding how a favorites/recent/search click should behave:
|
||||
|
||||
1. Follow the portal's canonical detail model.
|
||||
2. Prefer local consistency within the portal over cross-portal sameness.
|
||||
3. Only unify Xtream and Stalker behavior if the full detail architecture is being unified as well.
|
||||
|
||||
That means:
|
||||
- Xtream: navigate to the canonical route.
|
||||
- Stalker: stay in the current screen and open inline detail.
|
||||
|
||||
## Refactor Guidance
|
||||
|
||||
If a future change proposes that Stalker favorites/recent should deep-link into category routes:
|
||||
- also update Stalker category-content and search behavior
|
||||
- define a canonical Stalker detail route model first
|
||||
- update architecture docs and portal skills together
|
||||
|
||||
If a future change proposes that Xtream favorites/recent should stay inline:
|
||||
- also replace Xtream route-based detail pages with a portal-local inline detail model
|
||||
- verify history/back behavior and deep links still make sense
|
||||
@@ -99,7 +99,7 @@ Type definitions:
|
||||
|
||||
## Shared helpers
|
||||
|
||||
- File: `apps/web/src/app/shared/services/remote-channel-navigation.util.ts`
|
||||
- File: `libs/portal/shared/util/src/lib/remote-channel-navigation.ts`
|
||||
|
||||
Functions:
|
||||
|
||||
@@ -110,7 +110,7 @@ Used by M3U, Xtream, and Stalker live integrations.
|
||||
|
||||
## M3U integration
|
||||
|
||||
- File: `apps/web/src/app/home/video-player/video-player.component.ts`
|
||||
- File: `libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts`
|
||||
|
||||
Implemented behavior:
|
||||
|
||||
@@ -133,7 +133,7 @@ Implemented behavior:
|
||||
|
||||
## Xtream integration (live view)
|
||||
|
||||
- File: `apps/web/src/app/xtream-electron/live-stream-layout/live-stream-layout.component.ts`
|
||||
- File: `libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.ts`
|
||||
|
||||
Implemented behavior:
|
||||
|
||||
@@ -156,7 +156,7 @@ Implemented behavior:
|
||||
|
||||
## Stalker integration (ITV live view)
|
||||
|
||||
- File: `apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.ts`
|
||||
- File: `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts`
|
||||
|
||||
Implemented behavior:
|
||||
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
# SQLite DB Worker
|
||||
|
||||
This document records the current non-EPG SQLite worker implementation in the
|
||||
Electron app.
|
||||
|
||||
Related:
|
||||
|
||||
- [Category Management](./category-management.md)
|
||||
- [Workspace Shell](./workspace-shell.md)
|
||||
|
||||
## Summary
|
||||
|
||||
- Heavy non-EPG SQLite work no longer runs on Electron's main thread.
|
||||
- A dedicated long-lived database worker now handles the slow Xtream and
|
||||
playlist database operations that were freezing the UI.
|
||||
- Renderer APIs stay stable. The main change is that progress and long-running
|
||||
state now flow through a request-scoped `DB_OPERATION_EVENT` contract instead
|
||||
of a single global progress event.
|
||||
|
||||
## Goals
|
||||
|
||||
The worker cutover addresses three concrete problems:
|
||||
|
||||
1. Main-process UI stalls during large SQLite operations.
|
||||
2. Xtream import progress events were global and unsafe for concurrent jobs.
|
||||
3. EPG and non-EPG writers needed shared SQLite concurrency settings so they
|
||||
can coexist without `SQLITE_BUSY` regressions.
|
||||
|
||||
## Current Ownership
|
||||
|
||||
### Main-process runtime wiring
|
||||
|
||||
These files own worker lifecycle and IPC bridging:
|
||||
|
||||
1. `apps/electron-backend/src/app/services/database-worker-client.ts`
|
||||
2. `apps/electron-backend/src/app/events/database/category.events.ts`
|
||||
3. `apps/electron-backend/src/app/events/database/content.events.ts`
|
||||
4. `apps/electron-backend/src/app/events/database/playlist.events.ts`
|
||||
5. `apps/electron-backend/src/app/events/database/xtream.events.ts`
|
||||
6. `apps/electron-backend/src/main.ts`
|
||||
|
||||
### Worker runtime
|
||||
|
||||
These files own the worker protocol and the SQLite work itself:
|
||||
|
||||
1. `apps/electron-backend/src/app/workers/database-worker.types.ts`
|
||||
2. `apps/electron-backend/src/app/workers/database.worker.ts`
|
||||
3. `apps/electron-backend/src/app/workers/database.worker-connection.ts`
|
||||
4. `apps/electron-backend/src/app/workers/worker-runtime-paths.ts`
|
||||
|
||||
### Pure database operation modules
|
||||
|
||||
Keep SQL-heavy logic here so the worker entry remains a thin dispatcher:
|
||||
|
||||
1. `apps/electron-backend/src/app/database/operations/category.operations.ts`
|
||||
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`
|
||||
|
||||
## Worker Architecture
|
||||
|
||||
### Request flow
|
||||
|
||||
1. Renderer calls the existing preload API such as `window.electron.dbSaveContent`.
|
||||
2. `ipcMain.handle(...)` in the Electron backend builds a payload and delegates
|
||||
to `DatabaseWorkerClient`.
|
||||
3. `DatabaseWorkerClient` lazily starts one long-lived `worker_threads` worker
|
||||
and correlates requests with a generated `requestId`.
|
||||
4. The worker executes SQLite work and sends back either:
|
||||
1. `ready`
|
||||
2. `event`
|
||||
3. `response`
|
||||
5. The main process resolves the IPC request and forwards worker events back to
|
||||
the originating renderer process.
|
||||
|
||||
### Why one long-lived worker
|
||||
|
||||
- It avoids worker startup cost on every search/delete/import.
|
||||
- It centralizes failure handling and restart behavior.
|
||||
- It mirrors the existing EPG worker approach without multiplying writable
|
||||
SQLite owners.
|
||||
|
||||
### Packaged worker bootstrap
|
||||
|
||||
Packaged Electron builds do not load worker scripts and native modules from the
|
||||
same place:
|
||||
|
||||
1. worker scripts live under `Resources/dist/apps/electron-backend/workers`
|
||||
2. unpacked native modules live under one of the approved
|
||||
`app.asar.unpacked/.../node_modules` locations
|
||||
|
||||
Both the EPG worker and the DB worker now share the same runtime helper:
|
||||
|
||||
1. `resolveWorkerRuntimeBootstrap(...)` for main-process worker launch
|
||||
2. `loadNativeModuleFromSearchPaths(...)` for worker-side native module loading
|
||||
|
||||
The helper uses `process.resourcesPath` as the primary packaged base and keeps
|
||||
`path.dirname(app.getAppPath())` only as a fallback.
|
||||
|
||||
## Worker Message Contract
|
||||
|
||||
The worker contract lives in
|
||||
`apps/electron-backend/src/app/workers/database-worker.types.ts`.
|
||||
|
||||
### Core message types
|
||||
|
||||
1. `DbWorkerRequestMessage`
|
||||
2. `DbWorkerResponseMessage`
|
||||
3. `DbWorkerEventMessage`
|
||||
4. `DbOperationEvent`
|
||||
|
||||
### Progress event contract
|
||||
|
||||
The worker now emits request-scoped events with:
|
||||
|
||||
- `operationId`
|
||||
- `operation`
|
||||
- `playlistId`
|
||||
- `status`
|
||||
- optional `phase`
|
||||
- optional `current`
|
||||
- optional `total`
|
||||
- optional `increment`
|
||||
|
||||
Current shipped operation names:
|
||||
|
||||
1. `save-content`
|
||||
2. `delete-xtream-content`
|
||||
3. `restore-xtream-user-data`
|
||||
4. `delete-playlist`
|
||||
5. `delete-all-playlists`
|
||||
|
||||
The event is forwarded to the renderer as `DB_OPERATION_EVENT`.
|
||||
|
||||
### Cancellation contract
|
||||
|
||||
Long-running Xtream and playlist operations now support best-effort
|
||||
cancellation.
|
||||
|
||||
Renderer requests cancellation via:
|
||||
|
||||
1. `DB_CANCEL_OPERATION`
|
||||
2. `window.electron.dbCancelOperation(operationId)`
|
||||
3. `DatabaseService.cancelOperation(operationId)`
|
||||
|
||||
If a worker operation is canceled:
|
||||
|
||||
1. the worker emits a final `cancelled` event
|
||||
2. the request rejects with an `AbortError`
|
||||
3. the UI clears its busy state without treating the operation as success
|
||||
|
||||
Cancellation is cooperative and chunk-based. Already committed SQLite batches
|
||||
stay committed.
|
||||
|
||||
## Renderer Contract
|
||||
|
||||
The preload bridge keeps the existing database methods but adds scoped worker
|
||||
events.
|
||||
|
||||
### Important preload APIs
|
||||
|
||||
1. `onDbOperationEvent(callback)`
|
||||
2. `dbSaveContent(playlistId, streams, type, operationId?)`
|
||||
3. `dbDeleteXtreamContent(playlistId, operationId?)`
|
||||
4. `dbRestoreXtreamUserData(..., operationId?)`
|
||||
5. `dbDeletePlaylist(playlistId, operationId?)`
|
||||
6. `dbDeleteAllPlaylists(operationId?)`
|
||||
7. `dbCancelOperation(operationId)`
|
||||
3. legacy compatibility:
|
||||
1. `onDbSaveContentProgress(callback)`
|
||||
2. `removeDbSaveContentProgress()`
|
||||
|
||||
`DatabaseService.saveXtreamContent(...)` now generates an `operationId`,
|
||||
subscribes to `onDbOperationEvent`, filters by that `operationId`, and only
|
||||
falls back to the legacy progress API if the newer event channel is missing.
|
||||
|
||||
`DatabaseService` also owns:
|
||||
|
||||
1. `createOperationId(...)`
|
||||
2. `cancelOperation(operationId)`
|
||||
3. `isDbAbortError(error)`
|
||||
|
||||
## Migrated Operations
|
||||
|
||||
The worker now owns all heavy non-EPG SQLite paths plus the remaining portal
|
||||
state handlers that still used direct main-thread SQLite access.
|
||||
|
||||
### Categories
|
||||
|
||||
1. `DB_HAS_CATEGORIES`
|
||||
2. `DB_GET_CATEGORIES`
|
||||
3. `DB_SAVE_CATEGORIES`
|
||||
4. `DB_GET_ALL_CATEGORIES`
|
||||
5. `DB_UPDATE_CATEGORY_VISIBILITY`
|
||||
|
||||
### Content
|
||||
|
||||
1. `DB_HAS_CONTENT`
|
||||
2. `DB_GET_CONTENT`
|
||||
3. `DB_SAVE_CONTENT`
|
||||
4. `DB_GET_CONTENT_BY_XTREAM_ID`
|
||||
5. `DB_SEARCH_CONTENT`
|
||||
6. `DB_GLOBAL_SEARCH`
|
||||
7. `DB_GET_GLOBAL_RECENTLY_ADDED`
|
||||
|
||||
### Playlist metadata
|
||||
|
||||
1. `DB_CREATE_PLAYLIST`
|
||||
2. `DB_UPSERT_APP_PLAYLIST`
|
||||
3. `DB_UPSERT_APP_PLAYLISTS`
|
||||
4. `DB_GET_APP_PLAYLISTS`
|
||||
5. `DB_GET_APP_PLAYLIST`
|
||||
6. `DB_GET_PLAYLIST`
|
||||
7. `DB_UPDATE_PLAYLIST`
|
||||
8. `DB_DELETE_PLAYLIST`
|
||||
9. `DB_DELETE_ALL_PLAYLISTS`
|
||||
10. `DB_GET_APP_STATE`
|
||||
11. `DB_SET_APP_STATE`
|
||||
|
||||
### Xtream refresh helpers
|
||||
|
||||
1. `DB_DELETE_XTREAM_CONTENT`
|
||||
2. `DB_RESTORE_XTREAM_USER_DATA`
|
||||
|
||||
### Favorites
|
||||
|
||||
1. `DB_ADD_FAVORITE`
|
||||
2. `DB_REMOVE_FAVORITE`
|
||||
3. `DB_IS_FAVORITE`
|
||||
4. `DB_GET_FAVORITES`
|
||||
5. `DB_GET_GLOBAL_FAVORITES`
|
||||
6. `DB_GET_ALL_GLOBAL_FAVORITES`
|
||||
7. `DB_REORDER_GLOBAL_FAVORITES`
|
||||
|
||||
### Recently viewed
|
||||
|
||||
1. `DB_GET_RECENTLY_VIEWED`
|
||||
2. `DB_CLEAR_RECENTLY_VIEWED`
|
||||
3. `DB_GET_RECENT_ITEMS`
|
||||
4. `DB_ADD_RECENT_ITEM`
|
||||
5. `DB_CLEAR_PLAYLIST_RECENT_ITEMS`
|
||||
6. `DB_REMOVE_RECENT_ITEM`
|
||||
|
||||
### Playback positions
|
||||
|
||||
1. `DB_SAVE_PLAYBACK_POSITION`
|
||||
2. `DB_GET_PLAYBACK_POSITION`
|
||||
3. `DB_GET_SERIES_PLAYBACK_POSITIONS`
|
||||
4. `DB_GET_RECENT_PLAYBACK_POSITIONS`
|
||||
5. `DB_GET_ALL_PLAYBACK_POSITIONS`
|
||||
6. `DB_CLEAR_PLAYBACK_POSITION`
|
||||
|
||||
## SQLite Concurrency Rules
|
||||
|
||||
EPG remains on its own worker, so both workers must use compatible SQLite
|
||||
pragmas.
|
||||
|
||||
Applied now in both the shared connection path and worker-owned connections:
|
||||
|
||||
1. `foreign_keys = ON`
|
||||
2. `journal_mode = WAL`
|
||||
3. `busy_timeout = 5000`
|
||||
|
||||
Current sources:
|
||||
|
||||
1. `libs/shared/database/src/lib/connection.ts`
|
||||
2. `apps/electron-backend/src/app/workers/database.worker-connection.ts`
|
||||
3. `apps/electron-backend/src/app/workers/epg-parser.worker.ts`
|
||||
|
||||
## UI Behavior Changes
|
||||
|
||||
### Search
|
||||
|
||||
Xtream search now guards against stale async responses:
|
||||
|
||||
1. local playlist search uses a monotonically increasing request version
|
||||
2. global search uses a separate request version in the dialog component
|
||||
3. clearing search invalidates older pending results
|
||||
|
||||
This prevents an older worker response from repainting over a newer query or a
|
||||
cleared search state.
|
||||
|
||||
### Busy states
|
||||
|
||||
The UI now has explicit long-running state for destructive operations:
|
||||
|
||||
1. recent playlist rows show row-level refresh/delete spinners
|
||||
2. Xtream import overlay shows phase text and a cancel action
|
||||
3. Xtream playlist rows show request-scoped progress and cancel actions
|
||||
4. busy rows block repeat clicks while an operation is in flight
|
||||
5. settings "remove all playlists" owns its own spinner/disabled state
|
||||
|
||||
These changes matter because once SQLite work leaves the main thread, the
|
||||
renderer can actually paint the loading state instead of freezing.
|
||||
|
||||
## Build And Packaging Notes
|
||||
|
||||
### Worker bundling
|
||||
|
||||
`apps/electron-backend/build-worker.js` now bundles both:
|
||||
|
||||
1. `epg-parser.worker.ts`
|
||||
2. `database.worker.ts`
|
||||
|
||||
The worker build also aliases:
|
||||
|
||||
1. `database-schema`
|
||||
2. `database-path-utils`
|
||||
|
||||
These aliases avoid importing the shared database barrel from inside the worker,
|
||||
which would otherwise pull in runtime code that assumes the main Electron
|
||||
process environment.
|
||||
|
||||
### Worker path resolution
|
||||
|
||||
`DatabaseWorkerClient` resolves:
|
||||
|
||||
1. development path from `__dirname`
|
||||
2. packaged path from `process.resourcesPath/dist/apps/electron-backend/workers/...`
|
||||
3. fallback packaged path from `path.dirname(app.getAppPath())`
|
||||
|
||||
### Packaged artifact verification
|
||||
|
||||
`tools/packaging/verify-electron-package-layout.mjs` verifies packaged worker
|
||||
artifacts for:
|
||||
|
||||
1. Linux unpacked resources
|
||||
2. macOS app bundles
|
||||
3. Windows unpacked app resources
|
||||
|
||||
The script checks:
|
||||
|
||||
1. `epg-parser.worker.js`
|
||||
2. `database.worker.js`
|
||||
3. `better-sqlite3` in one approved unpacked node_modules location
|
||||
|
||||
### Development rebuild rule
|
||||
|
||||
Worker-backed database logic is executed from the compiled bundle at:
|
||||
|
||||
1. `dist/apps/electron-backend/workers/database.worker.js`
|
||||
|
||||
Do not assume a source edit is active in the live app. If a fix touches:
|
||||
|
||||
1. `apps/electron-backend/src/app/database/operations/`
|
||||
2. `apps/electron-backend/src/app/workers/`
|
||||
3. `apps/electron-backend/src/app/events/database/`
|
||||
4. preload-backed DB methods consumed by the renderer
|
||||
|
||||
then the safe workflow is:
|
||||
|
||||
1. rebuild the worker bundle, or the full `electron-backend` target if preload,
|
||||
main-process, or web output also changed
|
||||
2. confirm the new `dist/` artifact exists or has a fresh timestamp
|
||||
3. restart the Electron process
|
||||
4. only then rerun CDP/manual checks or Electron E2E
|
||||
|
||||
A running Electron app keeps using the worker bundle it already loaded at
|
||||
startup. This is a common reason a worker fix appears "not working" in manual
|
||||
verification even when the source patch is correct.
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit coverage added
|
||||
|
||||
`apps/electron-backend/src/app/services/database-worker-client.spec.ts`
|
||||
covers:
|
||||
|
||||
1. worker ready -> request -> response flow
|
||||
2. event forwarding to a pending request
|
||||
3. serialized worker error propagation
|
||||
4. `AbortError` propagation for cancelled work
|
||||
5. cancel message routing to the live worker
|
||||
6. worker exit recovery and fresh worker startup
|
||||
|
||||
`apps/electron-backend/src/app/events/epg.events.spec.ts` covers:
|
||||
|
||||
1. shared worker bootstrap usage for the EPG worker
|
||||
2. `nativeModuleSearchPaths` forwarding into workerData
|
||||
3. actionable worker-path resolution failures
|
||||
|
||||
`apps/electron-backend/src/app/workers/worker-runtime-paths.spec.ts` covers:
|
||||
|
||||
1. packaged and development worker path resolution
|
||||
2. packaged native-module search path ordering
|
||||
3. aggregated native module resolution errors
|
||||
|
||||
### Electron responsiveness coverage
|
||||
|
||||
`apps/electron-backend-e2e/src/xtream-responsiveness.e2e.ts` covers:
|
||||
|
||||
1. large Xtream import shows the overlay promptly
|
||||
2. DB worker progress events advance during import
|
||||
3. renderer animation frames continue while import/delete are in progress
|
||||
4. large Xtream playlist delete shows row-level busy UI and completes cleanly
|
||||
|
||||
`apps/electron-backend-e2e/src/electron-test-fixtures.ts` now also captures:
|
||||
|
||||
1. `DB_OPERATION_EVENT` history in the renderer
|
||||
2. a requestAnimationFrame counter for repaint assertions
|
||||
|
||||
For deterministic E2E timing, tests may set:
|
||||
|
||||
```bash
|
||||
IPTVNATOR_DB_WORKER_BATCH_DELAY_MS=20
|
||||
```
|
||||
|
||||
This delay is test-only and disabled by default.
|
||||
|
||||
### Useful verification commands
|
||||
|
||||
```bash
|
||||
pnpm exec jest --config apps/electron-backend/jest.config.ts --runInBand apps/electron-backend/src/app/services/database-worker-client.spec.ts apps/electron-backend/src/app/events/epg.events.spec.ts apps/electron-backend/src/app/workers/worker-runtime-paths.spec.ts
|
||||
pnpm nx run electron-backend:build-worker
|
||||
pnpm exec tsc -p apps/electron-backend/tsconfig.app.json --noEmit
|
||||
pnpm exec tsc -p apps/web/tsconfig.app.json --noEmit
|
||||
pnpm nx run electron-backend-e2e:e2e -- --project=electron --grep "Electron Xtream Responsiveness"
|
||||
pnpm run verify:package-layout -- macos arm64
|
||||
pnpm run verify:package-layout -- linux
|
||||
pnpm run verify:package-layout -- windows
|
||||
```
|
||||
|
||||
### Electron runtime validation
|
||||
|
||||
```bash
|
||||
pnpm nx serve electron-backend
|
||||
agent-browser --cdp 9222 tab list
|
||||
agent-browser --cdp 9222 tab 1
|
||||
agent-browser --cdp 9222 snapshot -i -c -d 3
|
||||
pnpm run smoke:packaged -- macos arm64
|
||||
```
|
||||
|
||||
When worker-backed behavior changed, rebuild and restart before reconnecting:
|
||||
|
||||
```bash
|
||||
pnpm nx run electron-backend:build-worker
|
||||
CI=1 NX_TASKS_RUNNER_DYNAMIC_OUTPUT=false pnpm nx run electron-backend:build --skip-nx-cache
|
||||
stat -f "%Sm %N" dist/apps/electron-backend/workers/database.worker.js
|
||||
```
|
||||
|
||||
Then restart the Electron process and reconnect to `127.0.0.1:9222`.
|
||||
|
||||
### Electron E2E troubleshooting
|
||||
|
||||
If a production-mode Electron or Electron E2E launch shows `ERR_FILE_NOT_FOUND`
|
||||
for hashed `chunk-*.js`, `main-*.js`, or `styles-*.css` assets:
|
||||
|
||||
1. treat `dist/apps/web` as stale first
|
||||
2. rerun a deterministic production build, for example:
|
||||
|
||||
```bash
|
||||
CI=1 NX_TASKS_RUNNER_DYNAMIC_OUTPUT=false pnpm nx run electron-backend:build --skip-nx-cache
|
||||
```
|
||||
|
||||
3. verify `dist/apps/web/index.html` uses `<base href="./">` before relaunching
|
||||
Electron in file-backed mode
|
||||
|
||||
## Current Limitations
|
||||
|
||||
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
|
||||
|
||||
## Extending The Worker
|
||||
|
||||
When adding another heavy SQLite operation:
|
||||
|
||||
1. Put SQL-heavy logic in `apps/electron-backend/src/app/database/operations/`.
|
||||
2. Add the channel name to `database-worker.types.ts`.
|
||||
3. Handle it in `database.worker.ts`.
|
||||
4. Proxy the IPC handler through `DatabaseWorkerClient`.
|
||||
5. If the renderer needs progress, emit a request-scoped `DbOperationEvent`.
|
||||
6. Reuse existing preload/service APIs where possible instead of creating a new
|
||||
renderer-facing contract.
|
||||
7. Re-run the worker unit test and at least one Electron runtime smoke.
|
||||
@@ -16,7 +16,7 @@ The Stalker ITV live stream layout displays EPG data in the right panel when a l
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────────────┐
|
||||
│ StalkerLiveStreamLayoutComponent │
|
||||
│ apps/web/src/app/stalker/stalker-live-stream-layout/ │
|
||||
│ libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/ │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────────────┐ ┌─────────────────────┐ │
|
||||
│ │ Sidebar │ │ Video Player │ │ EPG Panel │ │
|
||||
@@ -129,14 +129,14 @@ interface EpgItem {
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `libs/shared/interfaces/src/lib/stalker-portal-actions.enum.ts` | `GetShortEpg`, `GetEpgInfo` enum values |
|
||||
| `apps/web/src/app/stalker/stalker.store.ts` | `fetchChannelEpg()` method |
|
||||
| `apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` | EPG signals + `loadEpgForChannel()` |
|
||||
| `apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.html` | `<app-epg-view>` integration |
|
||||
| `libs/portal/stalker/data-access/src/lib/stalker.store.ts` | `fetchChannelEpg()` method |
|
||||
| `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` | EPG signals + `loadEpgForChannel()` |
|
||||
| `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.html` | `<app-epg-view>` integration |
|
||||
| `libs/ui/shared-portals/src/lib/epg-view/epg-view.component.ts` | Shared EPG display component |
|
||||
|
||||
### StalkerStore.fetchChannelEpg()
|
||||
|
||||
Location: `apps/web/src/app/stalker/stalker.store.ts` (in `withMethods`)
|
||||
Location: `libs/portal/stalker/data-access/src/lib/stalker.store.ts` (in `withMethods`)
|
||||
|
||||
```typescript
|
||||
async fetchChannelEpg(channelId: number | string): Promise<EpgItem[]>
|
||||
|
||||
@@ -5,6 +5,8 @@ This document describes the Stalker portal implementation in IPTVnator and where
|
||||
## Related Docs
|
||||
|
||||
- [Stalker Portal EPG Architecture](./stalker-epg.md)
|
||||
- [Portal Detail Navigation](./portal-detail-navigation.md)
|
||||
- [Embedded Inline Playback](./embedded-inline-playback.md)
|
||||
- [Remote Control Architecture](./remote-control.md)
|
||||
- [Download Manager](./download-manager.md)
|
||||
- [Category Management](./category-management.md)
|
||||
@@ -25,7 +27,7 @@ Stalker support covers:
|
||||
|
||||
## Routing Structure
|
||||
|
||||
Primary route tree lives in `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker.routes.ts`.
|
||||
Primary route tree lives in `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-feature.routes.ts`.
|
||||
|
||||
- `/stalker/:id/vod`
|
||||
- `/stalker/:id/series`
|
||||
@@ -45,23 +47,23 @@ Primary route tree lives in `/Users/4gray/Code/iptvnator/apps/web/src/app/stalke
|
||||
|
||||
## Main UI Components
|
||||
|
||||
- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-main-container.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-main-container.component.ts`
|
||||
- Category + content layout for `vod` and `series`
|
||||
- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts`
|
||||
- ITV live playback, channel navigation, EPG panel integration
|
||||
- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/libs/ui/components/src/lib/stalker-series-view/stalker-series-view.component.ts`
|
||||
- Season/episode UI for all Stalker series modes
|
||||
- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-favorites/stalker-favorites.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/recently-viewed/recently-viewed.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-search/stalker-search.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-favorites/stalker-favorites.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/recently-viewed/recently-viewed.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts`
|
||||
|
||||
## Store and Data Flow
|
||||
|
||||
Stalker store is now feature-composed:
|
||||
|
||||
- Facade: `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker.store.ts`
|
||||
- Feature slices: `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stores/features/*`
|
||||
- Shared store utils: `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stores/utils/*`
|
||||
- Facade: `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/stalker.store.ts`
|
||||
- Feature slices: `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/stores/features/*`
|
||||
- Shared helpers: `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/*`
|
||||
|
||||
Important store responsibilities:
|
||||
|
||||
@@ -93,8 +95,8 @@ Stalker has multiple real-world data shapes. The current implementation supports
|
||||
|
||||
Core decision logic and normalization are centralized in:
|
||||
|
||||
- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-vod.utils.ts`
|
||||
- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/models/*.ts`
|
||||
- `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.ts`
|
||||
- `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/models/*.ts`
|
||||
|
||||
## Favorites and Recently Viewed
|
||||
|
||||
@@ -109,16 +111,22 @@ Current implementation is shared via Stalker-specific helpers:
|
||||
|
||||
Where this is used:
|
||||
|
||||
- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-favorites/stalker-favorites.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/recently-viewed/recently-viewed.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-search/stalker-search.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/favorites-button/favorites-button.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-favorites/stalker-favorites.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/recently-viewed/recently-viewed.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/libs/ui/components/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts`
|
||||
|
||||
Navigation rule to preserve:
|
||||
|
||||
- Stalker favorites, recently viewed, and search stay in their current screen and open inline detail state.
|
||||
- They should not redirect into a canonical content/category/item route because Stalker detail rendering is currently store-state/inline driven, not route driven.
|
||||
- See [Portal Detail Navigation](./portal-detail-navigation.md).
|
||||
|
||||
## Remote Control Integration
|
||||
|
||||
Stalker live remote control is implemented in:
|
||||
|
||||
- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.ts`
|
||||
- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts`
|
||||
|
||||
Supported today:
|
||||
|
||||
@@ -147,7 +155,7 @@ This reduces duplicate UI logic across portal types and keeps compatibility beha
|
||||
|
||||
Focused regression tests for Stalker VOD mode branching live in:
|
||||
|
||||
- `/Users/4gray/Code/iptvnator/apps/web/src/app/stalker/stalker-vod.utils.spec.ts`
|
||||
- `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.spec.ts`
|
||||
|
||||
Covered scenarios include:
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Stalker Store API Baseline
|
||||
|
||||
This is the compatibility baseline for refactoring `apps/web/src/app/stalker/stalker.store.ts`.
|
||||
This is the compatibility baseline for refactoring `libs/portal/stalker/data-access/src/lib/stalker.store.ts`.
|
||||
|
||||
Goal: keep this public surface stable while splitting to feature stores.
|
||||
|
||||
## Source of Truth
|
||||
|
||||
- Store implementation: `apps/web/src/app/stalker/stalker.store.ts`
|
||||
- Store implementation: `libs/portal/stalker/data-access/src/lib/stalker.store.ts`
|
||||
- Baseline created on current branch state before feature-store extraction.
|
||||
|
||||
## Public State Signals
|
||||
@@ -108,9 +108,10 @@ Top observed store API usage in app code:
|
||||
|
||||
Consumer directories sampled:
|
||||
|
||||
- `apps/web/src/app/stalker/**`
|
||||
- `apps/web/src/app/xtream-electron/**`
|
||||
- `apps/web/src/app/shared/**`
|
||||
- `libs/portal/stalker/**`
|
||||
- `libs/portal/xtream/feature/**`
|
||||
- `libs/portal/catalog/feature/**`
|
||||
- `libs/ui/components/**`
|
||||
|
||||
## Invariants to Preserve During Refactor
|
||||
|
||||
@@ -123,4 +124,3 @@ Consumer directories sampled:
|
||||
- Full-portal auth path continues through `StalkerSessionService`.
|
||||
- Non-auth/simple path continues through `DataService.sendIpcEvent(STALKER_REQUEST, ...)`.
|
||||
- Resource-driven loading signals preserve existing names.
|
||||
|
||||
@@ -1,188 +1,136 @@
|
||||
# Workspace Dashboard Plan
|
||||
# Workspace Dashboard
|
||||
|
||||
## Context
|
||||
This document records the current dashboard implementation inside the workspace
|
||||
shell. It replaces the earlier dashboard plan document.
|
||||
|
||||
The workspace shell is now the primary app entrypoint. The dashboard should evolve from a placeholder into an operational home page that helps users:
|
||||
Related:
|
||||
|
||||
1. Quickly continue playback/work.
|
||||
2. Switch context across M3U, Xtream, and Stalker sources.
|
||||
3. Monitor relevant content (recent items, EPG, status) in one place.
|
||||
- [Workspace Shell](./workspace-shell.md)
|
||||
|
||||
This document defines the implementation plan before Phase 1 work starts.
|
||||
## Summary
|
||||
|
||||
## Status Snapshot (February 22, 2026)
|
||||
- The dashboard is the default `/workspace` landing page.
|
||||
- It is a widget-based surface with persisted layout and widget settings.
|
||||
- The current implementation favors a constrained, stable layout over a full
|
||||
freeform grid system.
|
||||
|
||||
### Delivered
|
||||
Core implementation:
|
||||
|
||||
1. Dashboard route is active in workspace shell with persisted widget layout.
|
||||
2. Widget host and configurable widget model are implemented.
|
||||
3. `Customize` mode supports:
|
||||
1. Enable/disable widgets
|
||||
2. Reorder widgets (up/down)
|
||||
3. Widget scope (provider + playlist selection)
|
||||
4. Active widgets in production:
|
||||
1. Continue Watching
|
||||
2. Recent Sources
|
||||
3. Source Statistics
|
||||
4. Recently Watched (global)
|
||||
5. Global Favorites
|
||||
5. Recently Watched and Global Favorites support:
|
||||
1. Content-kind chips (channels/vod/series)
|
||||
2. List/grid toggle
|
||||
3. Direct deep linking from widget item to target view/detail
|
||||
6. Xtream live deep links now auto-start playback when opened from dashboard widgets.
|
||||
7. Dashboard now uses a shared Material-based widget shell for consistent visual structure.
|
||||
8. Customize mode now supports drag-and-drop ordering and widget size presets (`1/3`, `1/2`, `2/3`, `full`).
|
||||
1. `libs/workspace/dashboard/feature/src/lib/workspace-dashboard.component.ts`
|
||||
2. `libs/workspace/dashboard/feature/src/lib/workspace-dashboard.component.html`
|
||||
3. `libs/workspace/dashboard/ui/src/lib/dashboard-widget-host.component.ts`
|
||||
4. `libs/workspace/dashboard/data-access/src/lib/dashboard-widget.model.ts`
|
||||
5. `libs/workspace/dashboard/data-access/src/lib/dashboard-layout.service.ts`
|
||||
6. `libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.ts`
|
||||
|
||||
### Partially Delivered / Deviation From Initial Phase 1 List
|
||||
## Current Widget Set
|
||||
|
||||
1. EPG Radar was intentionally removed from the current dashboard scope and is postponed.
|
||||
2. Recent Activity / Recently Added widget was intentionally removed from current scope.
|
||||
Registered widget types:
|
||||
|
||||
### Not Started
|
||||
1. `source-stats`
|
||||
2. `continue-watching`
|
||||
3. `recently-watched`
|
||||
4. `global-favorites`
|
||||
|
||||
1. Phase 4 external widgets (RSS/scores/news adapters).
|
||||
Default layout:
|
||||
|
||||
### Immediate Next Widget Tasks (Recommended)
|
||||
1. `continue-watching`
|
||||
1. Enabled by default
|
||||
2. Size `full`
|
||||
2. `recently-watched`
|
||||
1. Enabled by default
|
||||
2. Size `two-thirds`
|
||||
3. Scope-aware
|
||||
3. `global-favorites`
|
||||
1. Enabled by default
|
||||
2. Size `one-third`
|
||||
3. Scope-aware
|
||||
4. `source-stats`
|
||||
1. Present in the registry
|
||||
2. Disabled by default
|
||||
3. Size `one-third`
|
||||
|
||||
1. Expand widget settings UX (scope presets, bulk provider toggles).
|
||||
The old refactor summary mentioned `Recent Sources`, but that widget is not in
|
||||
the current registry and should not be documented as shipped behavior.
|
||||
|
||||
## Product Direction
|
||||
## Layout And Persistence Contract
|
||||
|
||||
The dashboard should be a configurable widget system, but introduced in stages:
|
||||
The dashboard persists a versioned layout object in local storage.
|
||||
|
||||
1. Start with useful, stable widgets and a constrained layout.
|
||||
2. Add edit/customization workflows after widget value is proven.
|
||||
3. Add advanced drag/resize and external integrations later.
|
||||
Current storage details:
|
||||
|
||||
This avoids building heavy layout mechanics before core data widgets are solid.
|
||||
1. Storage key: `workspace-dashboard-layout-v3`
|
||||
2. Schema version: `12`
|
||||
3. Size presets:
|
||||
1. `one-third`
|
||||
2. `half`
|
||||
3. `two-thirds`
|
||||
4. `full`
|
||||
4. Scope settings:
|
||||
1. `providers: Array<'m3u' | 'xtream' | 'stalker'>`
|
||||
2. `playlistIds: string[]`
|
||||
|
||||
## UX Direction
|
||||
Normalization rules in `DashboardLayoutService`:
|
||||
|
||||
Design style: professional operator console (dense, calm, high-signal).
|
||||
1. Stored widgets are merged against `DEFAULT_DASHBOARD_WIDGETS`.
|
||||
2. Missing widgets are restored from defaults.
|
||||
3. Removed or invalid settings fall back to normalized defaults.
|
||||
4. Titles and descriptions come from the current code-defined defaults, not
|
||||
stale stored values.
|
||||
5. Widget order is reindexed after normalization.
|
||||
|
||||
Target layout:
|
||||
## Rendering Flow
|
||||
|
||||
1. Top row: continue actions, recent sources, health/status.
|
||||
2. Middle row: discovery widgets (recently viewed, recently added, favorites).
|
||||
3. Edit mode: add/remove/reorder widgets and configure source scope.
|
||||
1. `WorkspaceDashboardComponent` reads `DashboardLayoutService.state()`.
|
||||
2. Enabled widgets are filtered and rendered in layout order.
|
||||
3. `DashboardWidgetHostComponent` maps `widget.type` to a concrete widget
|
||||
component.
|
||||
4. Widgets receive the normalized config, including size and optional scope.
|
||||
5. Data comes from `DashboardDataService` and existing provider/state services.
|
||||
6. Widget actions deep-link back into workspace/provider routes.
|
||||
|
||||
## Architecture
|
||||
## Customize Mode
|
||||
|
||||
## Core Components
|
||||
Customize mode is part of the current product, not future work.
|
||||
|
||||
1. `DashboardPageComponent` (container/layout/edit mode orchestration)
|
||||
2. `DashboardWidgetHostComponent` (widget factory/renderer by type)
|
||||
3. `DashboardLayoutStore` (signal-based state for layout, settings, edit mode)
|
||||
4. `DashboardPersistenceService` (save/load layout, version migration)
|
||||
5. `DashboardDataFacade` (aggregate provider data for widgets)
|
||||
Supported actions:
|
||||
|
||||
## Widget Contract
|
||||
1. Toggle widget visibility.
|
||||
2. Drag-and-drop reorder for visible widgets.
|
||||
3. Change widget size within the fixed preset list.
|
||||
4. Configure provider scope for scoped widgets.
|
||||
5. Configure playlist scope for scoped widgets.
|
||||
6. Reset the layout to defaults.
|
||||
|
||||
```ts
|
||||
type WidgetSize = 'one-third' | 'half' | 'two-thirds' | 'full';
|
||||
Scope-aware widgets currently rely on a provider/playlist filter model rather
|
||||
than per-widget custom query systems.
|
||||
|
||||
interface WidgetScope {
|
||||
providers: Array<'m3u' | 'xtream' | 'stalker'>;
|
||||
playlistIds?: string[];
|
||||
}
|
||||
## UX Rules
|
||||
|
||||
interface DashboardWidget {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
size: WidgetSize;
|
||||
order: number;
|
||||
enabled: boolean;
|
||||
scope: WidgetScope;
|
||||
settings: Record<string, unknown>;
|
||||
}
|
||||
1. Widgets should represent user tasks, not provider internals.
|
||||
2. Each widget must own its loading, empty, and error states.
|
||||
3. Dashboard failures must stay isolated to the widget that failed.
|
||||
4. Widget navigation should resolve directly into the relevant content context.
|
||||
5. New widgets should fit the existing constrained layout model unless the
|
||||
dashboard architecture is explicitly being expanded.
|
||||
|
||||
interface DashboardLayout {
|
||||
version: number;
|
||||
widgets: DashboardWidget[];
|
||||
}
|
||||
```
|
||||
## Adding Or Changing Widgets
|
||||
|
||||
## Capability Rules
|
||||
Current workflow:
|
||||
|
||||
Widgets must degrade gracefully per provider:
|
||||
1. Add or update the widget type in `dashboard-widget.model.ts`.
|
||||
2. Implement the widget UI under `libs/workspace/dashboard/ui/src/lib/widgets/`.
|
||||
3. Register the widget in `dashboard-widget-host.component.ts`.
|
||||
4. Add default state and migration-safe behavior in
|
||||
`dashboard-layout.service.ts`.
|
||||
5. Extend `dashboard-data.service.ts` or reuse an existing feature service.
|
||||
6. Ensure the widget has explicit empty/error/loading states and valid
|
||||
workspace deep links.
|
||||
|
||||
1. If a source/provider does not support required data (for example EPG), show a clear empty/unsupported state.
|
||||
2. Widgets never hard fail the page; each widget owns loading/error states.
|
||||
3. Scope defaults to "all supported providers" unless user config overrides it.
|
||||
## Deferred Work
|
||||
|
||||
## Phased Delivery
|
||||
These items are intentionally not part of the current contract:
|
||||
|
||||
## Phase 1 (Now): Production Dashboard V1
|
||||
|
||||
Scope:
|
||||
|
||||
1. Replace placeholder dashboard with real widget host + predefined layout.
|
||||
2. Fixed grid slots (no free drag/resize yet).
|
||||
3. Initial widgets:
|
||||
1. Recent Sources
|
||||
2. Continue Watching
|
||||
3. Source Statistics
|
||||
4. Recently Added / Recently Viewed (provider-aware)
|
||||
4. Persist enabled/disabled and order (simple list reorder if needed).
|
||||
|
||||
Out of scope:
|
||||
|
||||
1. Freeform drag-and-drop grid resizing.
|
||||
2. External data providers (RSS/sports/news).
|
||||
3. Full widget marketplace.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
1. Dashboard loads with useful content for at least one active source type.
|
||||
2. Empty states are clear and actionable (links to Sources/settings).
|
||||
3. No route regressions for existing workspace sections.
|
||||
4. Layout/settings survive app restart.
|
||||
|
||||
## Phase 2: Edit Mode and Configuration
|
||||
|
||||
Scope:
|
||||
|
||||
1. "Customize dashboard" mode.
|
||||
2. Enable/disable widgets.
|
||||
3. Widget-level source scoping (providers + selected playlists).
|
||||
4. Order management (move up/down or drag reorder within constrained grid).
|
||||
|
||||
## Phase 3: Advanced Layout (Drag/Resize)
|
||||
|
||||
Scope:
|
||||
|
||||
1. True grid layout manager with size presets and drag repositioning.
|
||||
2. Collision handling and responsive breakpoint behavior.
|
||||
3. Optional "reset layout" and preset templates.
|
||||
|
||||
## Phase 4: External Widgets
|
||||
|
||||
Scope:
|
||||
|
||||
1. Adapter interface for non-playlist data widgets (RSS, scores, news).
|
||||
2. Polling/cache strategy with rate limits.
|
||||
3. User opt-in and failure isolation per external source.
|
||||
|
||||
## Data and Performance Notes
|
||||
|
||||
1. Use memoized/computed selectors for widget inputs.
|
||||
2. Avoid redundant provider fetches; reuse existing stores/services where possible.
|
||||
3. Update widgets incrementally and isolate heavy computations in facade/store utilities.
|
||||
|
||||
## File/Module Placement (Proposed)
|
||||
|
||||
1. `libs/workspace/dashboard/feature/` (page + edit mode orchestration)
|
||||
2. `libs/workspace/dashboard/ui/` (widget host + widget components)
|
||||
3. `libs/workspace/dashboard/data-access/` (state model + persistence + data facade/service)
|
||||
4. Route integration remains in `apps/web/src/app/app.routes.ts` and workspace shell.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. Should dashboard layout be global per app profile, or per active workspace/provider mix?
|
||||
2. Which widgets are enabled by default for new users vs migrated users?
|
||||
|
||||
## Next Step
|
||||
|
||||
Start Phase 1 implementation using this document as the source of truth and track deviations explicitly in follow-up updates.
|
||||
1. Freeform drag/resize grid with collision management.
|
||||
2. External data widgets such as RSS, sports, or news adapters.
|
||||
3. A widget marketplace or plugin system.
|
||||
@@ -0,0 +1,128 @@
|
||||
# Workspace Shell
|
||||
|
||||
This document records the current workspace-first shell contract. It is the
|
||||
stable replacement for the older UI refactor summary.
|
||||
|
||||
Related:
|
||||
|
||||
- [Workspace Dashboard](./workspace-dashboard.md)
|
||||
|
||||
## Summary
|
||||
|
||||
- `/workspace` is the primary app surface.
|
||||
- `WorkspaceShellComponent` owns the persistent frame: rail, header, optional
|
||||
context panel, content outlet, and external playback footer.
|
||||
- Descendant workspace pages inherit `layout = 'workspace'` from the
|
||||
`/workspace` root route.
|
||||
- Provider route trees now bootstrap through route-scoped session providers
|
||||
instead of nested provider shell components.
|
||||
|
||||
Core implementation:
|
||||
|
||||
1. `apps/web/src/app/app.routes.ts`
|
||||
2. `libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.ts`
|
||||
3. `libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html`
|
||||
4. `libs/portal/shared/util/src/lib/navigation/portal-route.utils.ts`
|
||||
5. `libs/portal/shared/util/src/lib/navigation/portal-rail-links.ts`
|
||||
6. `libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.ts`
|
||||
|
||||
## Route Contract
|
||||
|
||||
Current workspace routes:
|
||||
|
||||
1. `/` -> `/workspace`
|
||||
2. `/workspace` -> `/workspace/dashboard`
|
||||
3. `/workspace/dashboard`
|
||||
4. `/workspace/sources`
|
||||
5. `/workspace/playlists/:id/:view`
|
||||
6. `/workspace/global-favorites`
|
||||
7. `/workspace/downloads`
|
||||
8. `/workspace/settings`
|
||||
9. `/workspace/xtreams/:id/...`
|
||||
10. `/workspace/stalker/:id/...`
|
||||
|
||||
Compatibility redirect:
|
||||
|
||||
1. `/settings` -> `/workspace/settings`
|
||||
|
||||
Provider route integration:
|
||||
|
||||
1. `apps/web/src/app/app.routes.ts` marks the `/workspace` root route with
|
||||
`data.layout = 'workspace'`.
|
||||
2. `isWorkspaceLayoutRoute(...)` treats that layout marker as inherited route
|
||||
state for all descendants.
|
||||
3. Xtream and Stalker parent routes attach route-scoped session providers that
|
||||
bootstrap the active playlist, sync provider section state, and clean up
|
||||
provider-local state when the route is destroyed.
|
||||
4. Workspace routes no longer rely on nested provider shell components for
|
||||
hidden local chrome.
|
||||
|
||||
## Shell Structure
|
||||
|
||||
The shell is intentionally split into four persistent regions:
|
||||
|
||||
1. Left rail:
|
||||
1. Static workspace links for dashboard and sources.
|
||||
2. Provider-aware context links derived from the active or current playlist.
|
||||
2. Top header:
|
||||
1. Playlist switcher.
|
||||
2. Route-aware search input and command palette trigger.
|
||||
3. Add source action.
|
||||
4. Global favorites shortcut.
|
||||
5. Downloads shortcut in Electron.
|
||||
6. Context actions menu for playlist/account or section-level actions.
|
||||
3. Main body:
|
||||
1. Optional left context panel.
|
||||
2. Main router outlet content.
|
||||
4. Optional footer:
|
||||
1. External playback session bar when a docked session is visible.
|
||||
|
||||
## Context Panel Rules
|
||||
|
||||
The shell decides which secondary panel to show from the current route:
|
||||
|
||||
1. `/workspace/sources`
|
||||
1. `WorkspaceSourcesFiltersPanelComponent`
|
||||
2. Xtream category sections (`live`, `vod`, `series`)
|
||||
1. `WorkspaceContextPanelComponent`
|
||||
3. Stalker category sections (`itv`, `vod`, `series`)
|
||||
1. `WorkspaceContextPanelComponent`
|
||||
4. `/workspace/settings`
|
||||
1. `WorkspaceSettingsContextPanelComponent`
|
||||
5. Downloads sections
|
||||
1. `WorkspaceCollectionContextPanelComponent`
|
||||
|
||||
The context panel is part of the shell contract. New workspace-level routes
|
||||
should explicitly decide whether they need one rather than adding local
|
||||
sidebars inside feature pages.
|
||||
|
||||
## Search And Navigation Rules
|
||||
|
||||
Search is shell-owned and route-aware:
|
||||
|
||||
1. Disabled on settings routes.
|
||||
2. Enabled on sources routes.
|
||||
3. Enabled for supported Xtream and Stalker content/search views.
|
||||
4. Placeholder text and search handling vary by provider and section.
|
||||
5. Input changes are debounced before route/store updates are applied.
|
||||
|
||||
Rail navigation is also shell-owned:
|
||||
|
||||
1. Workspace-global entries are static.
|
||||
2. Provider entries come from `buildPortalRailLinks(...)`.
|
||||
3. On dashboard, sources, settings, and global favorites, the shell falls back
|
||||
to the currently selected playlist so provider navigation remains available
|
||||
even outside a provider route.
|
||||
|
||||
## Maintenance Guidance
|
||||
|
||||
Use this document as the source of truth when changing workspace shell behavior.
|
||||
|
||||
1. New top-level user destinations should default to child routes under
|
||||
`/workspace`.
|
||||
2. Shared provider navigation logic belongs in portal-shared util/UI libraries,
|
||||
not duplicated inside the shell.
|
||||
3. If a provider route changes how playlist/session bootstrap works, update the
|
||||
route-session provider and shell-facing route contract together.
|
||||
4. Historical migration notes, cleanup lists, and one-off refactor steps should
|
||||
stay out of this file; track them in issues or PR notes instead.
|
||||
@@ -25,6 +25,7 @@ faker.seed(seed) ←── all faker calls use same seed per credentia
|
||||
generateCategories() ←── live / vod / series categories
|
||||
│
|
||||
generateLiveStreams() ←── live TV stream list
|
||||
scenario EPG fixture? ←── optional deterministic per-stream EPG override
|
||||
generateVodStreams() ←── VOD movie list
|
||||
generateSeriesItems() ←── series list
|
||||
generateSeriesInfo() ←── nested seasons + episodes (pre-populated)
|
||||
@@ -57,6 +58,7 @@ apps/xtream-mock-server/
|
||||
├── handlers/
|
||||
│ ├── get-account-info.handler.ts
|
||||
│ ├── get-categories.handler.ts ← live/vod/series categories
|
||||
│ ├── get-full-epg.handler.ts ← full EPG + legacy typo alias
|
||||
│ ├── get-streams.handler.ts ← live/vod/series stream lists
|
||||
│ ├── get-vod-info.handler.ts
|
||||
│ ├── get-series-info.handler.ts
|
||||
@@ -159,6 +161,31 @@ All redirect to a publicly available HLS test stream
|
||||
|
||||
Note: `title` and `description` are **base64-encoded**, matching the real Xtream API.
|
||||
|
||||
### `get_simple_data_table` / `get_simple_date_table`
|
||||
|
||||
Both actions return the same full per-channel schedule shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"epg_listings": [
|
||||
{
|
||||
"id": "10000-current",
|
||||
"epg_id": "channel-10000.mock",
|
||||
"title": "base64encodedTitle",
|
||||
"description": "base64encodedDescription",
|
||||
"start": "2026-04-05 04:30:00",
|
||||
"end": "2026-04-05 05:00:00",
|
||||
"start_timestamp": "1775363400",
|
||||
"stop_timestamp": "1775365200",
|
||||
"channel_id": "channel-10000.mock"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The legacy `get_simple_date_table` typo alias exists because real Xtream panels
|
||||
sometimes only respond to that misspelled action.
|
||||
|
||||
### `get_series_info` (structure)
|
||||
|
||||
```json
|
||||
@@ -193,10 +220,24 @@ Note: `title` and `description` are **base64-encoded**, matching the real Xtream
|
||||
| `large:large` | 9999 | 20 each | 200 | active |
|
||||
| `series:series` | 2002 | live:3, vod:4, series:15 | 30 | active |
|
||||
| `minimal:minimal` | 3003 | 2 each | 5 | active |
|
||||
| `epg:epg` | 6006 | live:2, vod:1, series:1 | 3 | active |
|
||||
| `expired:expired` | 4004 | 4 each | 10 | Expired |
|
||||
| `inactive:inactive` | 5005 | 4 each | 10 | Disabled |
|
||||
| `<any other>` | hash | 6 each | 30 | active |
|
||||
|
||||
### `epg:epg` fixture details
|
||||
|
||||
This scenario is reserved for Xtream EPG tests:
|
||||
|
||||
- live category `EPG Focus` contains deterministic channels such as `Timezone News`
|
||||
- `Timezone News` serves a fixed `get_short_epg` window and a full `get_simple_data_table` schedule
|
||||
- the full schedule includes a program that spans a UTC midnight boundary and another program after midnight
|
||||
- raw `start` / `end` strings are intentionally offset from `start_timestamp` / `stop_timestamp`
|
||||
|
||||
That deliberate mismatch lets Electron tests verify the renderer uses timestamp
|
||||
fields for sorting, current-program selection, progress bars, and local clock
|
||||
labels instead of trusting provider-local strings.
|
||||
|
||||
---
|
||||
|
||||
## Playwright Integration
|
||||
@@ -233,5 +274,6 @@ await page.route('**/localhost:3000/xtream**', async (route) => {
|
||||
|
||||
- **Add new actions**: Implement a handler function and add a `case` in `routes/dispatch.ts`
|
||||
- **Add new scenarios**: Add an entry to `SCENARIOS` in `scenarios.ts`
|
||||
- **Add deterministic EPG fixtures**: Extend `ScenarioConfig.epgFixture` and populate `epgListingsByStreamId` in `data-store.ts`
|
||||
- **Adjust data volume**: Change `itemsPerCategory`, `seasonsPerSeries`, or `episodesPerSeason` per scenario
|
||||
- **Custom stream URLs**: Edit the HLS stub redirect in `main.ts`
|
||||
Reference in new issue
Block a user