mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
docs: update navigation and dashboard documentation for clarity and accuracy
This commit is contained in:
1 parent
73de25db21
commit
549bdd7a9e
4 files changed
+165
-125
No files matched your search
@@ -91,7 +91,7 @@ The search bar disables itself on some routes and changes behavior on others. Th
|
||||
### 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.
|
||||
2. **Keep Global Favorites as a single left-rail destination.** Avoid reintroducing a second header shortcut for the same global destination; reserve header actions for contextual controls.
|
||||
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
|
||||
|
||||
@@ -280,36 +280,48 @@ Xtream search now guards against stale async responses:
|
||||
This prevents an older worker response from repainting over a newer query or a
|
||||
cleared search state.
|
||||
|
||||
### Xtream favorites lookup
|
||||
### Xtream type-aware content lookup
|
||||
|
||||
Xtream favorites must treat `xtream_id` as only partially unique.
|
||||
Xtream UI flows must treat `xtream_id` as only partially unique.
|
||||
|
||||
Current contract:
|
||||
|
||||
1. `xtream_id` can collide across `live`, `movie`, and `series` within the same
|
||||
playlist.
|
||||
2. Any DB-backed lookup that starts from an Xtream result card, favorite button,
|
||||
or detail route must resolve content by:
|
||||
recent-item update, continue-watching flow, or detail route must resolve
|
||||
content by:
|
||||
- `playlist_id`
|
||||
- `xtream_id`
|
||||
- `content.type`
|
||||
3. Favorites UI state for mixed Xtream collections must key entries by
|
||||
`type + xtream_id`, not `xtream_id` alone.
|
||||
3. Mixed Xtream collection identity must key entries by `type + xtream_id`,
|
||||
not `xtream_id` alone. This includes favorites maps, recent-item lists,
|
||||
dashboard collection payloads, and other UI state keyed off persisted
|
||||
Xtream content.
|
||||
|
||||
Why this matters:
|
||||
|
||||
- Search results are already type-filtered, so resolving favorites by only
|
||||
`playlist_id + xtream_id` can favorite the wrong persisted row when IDs
|
||||
collide.
|
||||
- Continue-watching / recently-viewed flows that resolve by only
|
||||
`playlist_id + xtream_id` can store the wrong persisted row when a series or
|
||||
movie ID collides with a live entry.
|
||||
- Mixed favorites maps keyed only by `xtream_id` can mark an unrelated live row
|
||||
as favorited when the actual favorite is a movie or series with the same
|
||||
numeric ID.
|
||||
- Mixed recent/favorites collection items keyed only by `xtream_id` can cause
|
||||
local UI state to remove, reorder, or reactivate the wrong Xtream entry when
|
||||
different content types collide.
|
||||
|
||||
Current implementation paths:
|
||||
|
||||
1. `apps/electron-backend/src/app/database/operations/content.operations.ts`
|
||||
2. `libs/portal/xtream/data-access/src/lib/with-favorites.feature.ts`
|
||||
3. `libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts`
|
||||
3. `libs/portal/xtream/data-access/src/lib/with-recent-items.ts`
|
||||
4. `libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts`
|
||||
5. `libs/portal/shared/util/src/lib/collection/unified-recent-data.service.ts`
|
||||
6. `libs/portal/shared/util/src/lib/collection/unified-favorites-data.service.ts`
|
||||
|
||||
### Busy states
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Workspace Dashboard
|
||||
|
||||
This document records the current dashboard implementation inside the workspace
|
||||
shell. It replaces the earlier dashboard plan document.
|
||||
shell.
|
||||
|
||||
Related:
|
||||
|
||||
@@ -10,143 +10,158 @@ Related:
|
||||
## Summary
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- First-run users see the shared welcome empty-state with a single primary
|
||||
CTA to add their first playlist.
|
||||
|
||||
Core implementation:
|
||||
|
||||
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`
|
||||
1. `libs/workspace/dashboard/feature/src/lib/rails/workspace-dashboard-rails.component.ts`
|
||||
— the page-level facade.
|
||||
2. `libs/workspace/dashboard/feature/src/lib/rails/dashboard-rail.component.ts`
|
||||
— the reusable horizontal rail.
|
||||
3. `libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.ts`
|
||||
— data aggregation (recent items, favorites, playlist stats). Shared across
|
||||
rails.
|
||||
4. `libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.component.ts`
|
||||
— reused welcome state with the primary "Add your first playlist" CTA.
|
||||
|
||||
## Current Widget Set
|
||||
## Page Structure
|
||||
|
||||
Registered widget types:
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Hero — Continue Watching (most recent item) │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Recently Watched · See all → │
|
||||
│ [poster][poster][poster][poster] →→ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Global Favorites · See all → │
|
||||
│ [poster][poster][poster] →→ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Recently Used Sources · See all → │
|
||||
│ [tile][tile][tile][tile] →→ │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ Recently Added on Xtream (aggregated across providers) │
|
||||
│ [poster][poster][poster] →→ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
1. `source-stats`
|
||||
2. `continue-watching`
|
||||
3. `recently-watched`
|
||||
4. `global-favorites`
|
||||
Render rules:
|
||||
|
||||
Default layout:
|
||||
1. `dashboardReady() === false` → render the page-level skeleton rails/hero.
|
||||
The first-load gate waits for playlist metadata plus the first global
|
||||
recent/global favorites reloads and, when Xtream playlists exist, the first
|
||||
Xtream recently-added reload.
|
||||
2. `hasPlaylists() === false` → render `<app-empty-state type="welcome">`
|
||||
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
|
||||
— there is no "empty widget" placeholder.
|
||||
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.
|
||||
|
||||
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`
|
||||
## Rail Contract
|
||||
|
||||
The old refactor summary mentioned `Recent Sources`, but that widget is not in
|
||||
the current registry and should not be documented as shipped behavior.
|
||||
`DashboardRailComponent` is purely presentational:
|
||||
|
||||
## Layout And Persistence Contract
|
||||
1. Inputs: `label`, `items: DashboardRailCard[]`, optional `seeAllLink`,
|
||||
optional `aspectRatio` (default `'2 / 3'`), optional `testId`.
|
||||
2. Behavior: horizontal flex track with `scroll-snap-type: x mandatory`.
|
||||
3. Chevron buttons fade in on hover (desktop only via `@media (hover: none)`).
|
||||
4. Cards are keyboard-focusable router links; `scroll-snap-align: start`
|
||||
means arrow-key nav lands on card boundaries.
|
||||
5. Image handling: `loading="lazy"`, `decoding="async"`, fallback icon tile
|
||||
when `imageUrl` is missing or `error` fires.
|
||||
6. Dashboard hero, rail containers, rail cards, and "Manage all" links expose
|
||||
stable `data-test-id` hooks. Treat these as the supported Electron E2E
|
||||
selector surface; do not target internal CSS class names.
|
||||
|
||||
The dashboard persists a versioned layout object in local storage.
|
||||
## Data Flow
|
||||
|
||||
Current storage details:
|
||||
1. `WorkspaceDashboardRailsComponent` injects `DashboardDataService`.
|
||||
2. It derives five signals via `computed()`:
|
||||
1. `hero` — first item of `globalRecentItems()`.
|
||||
2. `recentlyWatchedCards` — maps `globalRecentItems()` to rail cards.
|
||||
3. `xtreamRecentlyAddedCards` — maps `xtreamRecentlyAddedItems()` to rail
|
||||
cards. Aggregates newly added VOD and series across *all* Xtream
|
||||
playlists via `DashboardDataService.reloadXtreamRecentlyAddedItems()`,
|
||||
which calls `getGlobalRecentlyAdded('all', limit, 'xtream')` with the
|
||||
DB-level `playlists.type = 'xtream'` filter. The rail is Electron-only
|
||||
(PWA returns `[]`) and auto-hides when empty, so users without Xtream
|
||||
playlists never see it. Cards carry a `playlist_name · type` subtitle
|
||||
so users can tell which provider each item came from. Driven by an
|
||||
effect that re-runs whenever the Xtream playlist count changes.
|
||||
4. `favoriteCards` — maps `globalFavoriteItems()` to rail cards.
|
||||
5. `sourceCards` — maps `recentPlaylists()` to rail cards. `recentPlaylists()`
|
||||
ranks M3U, Xtream, and Stalker sources by their latest recent activity
|
||||
from `globalRecentItems()`, then falls back to playlist
|
||||
`updateDate` / `importDate` for sources that have never been used.
|
||||
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.
|
||||
5. Navigation state + deep-link targets come from the existing
|
||||
`getRecentItemLink()` / `getGlobalFavoriteLink()` / `getPlaylistLink()`
|
||||
helpers on `DashboardDataService` and reuse the workspace navigation
|
||||
helpers in `@iptvnator/portal/shared/util`.
|
||||
6. Xtream VOD and series detail pages opportunistically backfill
|
||||
`content.backdrop_url` when metadata exposes a backdrop, but that write
|
||||
must not refresh recently viewed ordering by itself.
|
||||
7. The dashboard feature triggers a fresh reload of DB-backed recent/favorite
|
||||
rows on dashboard entry so newly backfilled backdrop data is visible as soon
|
||||
as the user returns from a detail page.
|
||||
|
||||
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[]`
|
||||
## Empty State
|
||||
|
||||
Normalization rules in `DashboardLayoutService`:
|
||||
The welcome state is rendered via the existing
|
||||
`EmptyStateComponent` (`type="welcome"`) from
|
||||
`libs/playlist/shared/ui`:
|
||||
|
||||
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.
|
||||
|
||||
## Rendering Flow
|
||||
|
||||
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.
|
||||
|
||||
Dashboard detail handoff contract:
|
||||
|
||||
1. Live items continue to activate their existing playback/provider route flows.
|
||||
2. Xtream and Stalker movies/series from `global-favorites` or
|
||||
`recently-watched` should route into `/workspace/global-favorites` or
|
||||
`/workspace/global-recent` with collection detail pre-opened from navigation
|
||||
state.
|
||||
3. Those collection detail opens must not switch the active playlist in the
|
||||
header playlist switcher.
|
||||
4. Those collection detail opens must not show the workspace category sidebar.
|
||||
5. The detail close/back action should return to the dashboard-origin view
|
||||
rather than reopening provider/category navigation.
|
||||
|
||||
## Customize Mode
|
||||
|
||||
Customize mode is part of the current product, not future work.
|
||||
|
||||
Supported actions:
|
||||
|
||||
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.
|
||||
|
||||
Scope-aware widgets currently rely on a provider/playlist filter model rather
|
||||
than per-widget custom query systems.
|
||||
1. Illustration + headline + description from the existing M3U welcome
|
||||
strings (`HOME.PLAYLISTS.WELCOME_*`).
|
||||
2. Primary button emits `addPlaylistClicked`. The dashboard page wires this
|
||||
to `WORKSPACE_SHELL_ACTIONS.openAddPlaylistDialog()`.
|
||||
3. Feature chips (M3U / Xtream / Stalker) are provided by the component.
|
||||
|
||||
## UX Rules
|
||||
|
||||
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. Dashboard favorites/recent movie/series activations should preserve the
|
||||
current playlist context and use the collection-owned detail host instead of
|
||||
forcing provider/category side-navigation.
|
||||
6. New widgets should fit the existing constrained layout model unless the
|
||||
dashboard architecture is explicitly being expanded.
|
||||
1. Rails represent content the user is likely to resume, not provider
|
||||
internals. Never surface raw API objects.
|
||||
2. Each rail must auto-hide when its data source is empty.
|
||||
3. Image assets must degrade to a typed icon fallback — never show broken
|
||||
images or empty tiles.
|
||||
4. The page must never show "No widgets" style text. If there is no content
|
||||
and no playlists, render the welcome state; otherwise render whatever
|
||||
rails have data.
|
||||
5. Navigation from a rail card must deep-link into the appropriate workspace
|
||||
route without switching the active playlist in the header switcher.
|
||||
6. `Recently Used Sources` reflects recent source usage across all provider
|
||||
types, not just recent imports.
|
||||
|
||||
## Adding Or Changing Widgets
|
||||
## Adding Or Changing Rails
|
||||
|
||||
Current workflow:
|
||||
|
||||
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. Add a new `computed()` signal for the card list in
|
||||
`WorkspaceDashboardRailsComponent`, mapping your source data to
|
||||
`DashboardRailCard`.
|
||||
2. Drop a `<lib-dashboard-rail>` in the template, gated by
|
||||
`@if (cards.length > 0)`.
|
||||
3. If the data source is new, extend `DashboardDataService` rather than
|
||||
reaching into DB services directly from the component.
|
||||
4. Provide a `seeAllLink` only if there is a dedicated "manage all" route
|
||||
for that content type.
|
||||
|
||||
## Deferred Work
|
||||
|
||||
These items are intentionally not part of the current contract:
|
||||
Intentionally out of scope:
|
||||
|
||||
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.
|
||||
1. Customizable layout (drag/drop, resize, show/hide toggles, layout
|
||||
persistence). Removed in favor of a curated, opinionated order.
|
||||
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.
|
||||
@@ -62,15 +62,15 @@ Provider route integration:
|
||||
The shell is intentionally split into four persistent regions:
|
||||
|
||||
1. Left rail:
|
||||
1. Static workspace links for dashboard and sources.
|
||||
1. Static workspace links for dashboard, sources, global favorites, and recently viewed.
|
||||
2. Provider-aware context links derived from the active or current playlist.
|
||||
3. Settings remains a persistent footer shortcut in the rail.
|
||||
2. Top header:
|
||||
1. Playlist switcher.
|
||||
2. Route-aware search input and command palette trigger.
|
||||
3. Add source action.
|
||||
4. Global favorites shortcut.
|
||||
4. Optional playlist refresh and route-specific shortcut actions.
|
||||
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.
|
||||
@@ -114,6 +114,19 @@ Rail navigation is also shell-owned:
|
||||
to the currently selected playlist so provider navigation remains available
|
||||
even outside a provider route.
|
||||
|
||||
Command palette behavior is shell-owned but view-extensible:
|
||||
|
||||
1. The shell resolves commands into three groups in fixed order: current view,
|
||||
this playlist, then global.
|
||||
2. Shell-owned commands are derived from route context and current playlist
|
||||
state; empty groups are omitted instead of rendering disabled placeholders.
|
||||
3. Workspace features contribute current-view commands through
|
||||
`WorkspaceViewCommandService`.
|
||||
4. Header shortcut actions can opt into palette exposure by attaching palette
|
||||
metadata through `WorkspaceHeaderContextService`.
|
||||
5. Filtering matches command labels, descriptions, and keywords, and keyboard
|
||||
selection always lands on the first enabled command.
|
||||
|
||||
## Maintenance Guidance
|
||||
|
||||
Use this document as the source of truth when changing workspace shell behavior.
|
||||
|
||||
Reference in new issue
Block a user