mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 14:16:16 -08:00
refactor(dashboard): live rail now favorites-first; drop mixed Favorites rail
Two related dashboard fixes driven by the v0.22 mockup intent and the
UX-audit feedback that the rail labels lied about their data:
1. Live rail is favorites-first
- Source: globalFavoriteLiveItems() — the channels the user actually
starred. Falls back to globalRecentLiveItems() when no favorites
exist so fresh-install users still see something useful.
- Title flips with the source: "Live now on your favorites" when
pulling from favorites, "Continue with live TV" when pulling from
recent-watch history. The label is always honest about the data.
- "See all" link routes to the right collection page for the source
(/workspace/global-favorites vs /workspace/global-recent).
2. Mixed Global Favorites rail removed from the dashboard
- The rail had movies, series, live channels, and radio all sharing
one row — different card formats fighting for visual attention.
UX wiki principle: similar elements should look alike within a
scanning unit.
- Live favorites are promoted into the live rail above (with current
EPG). The full mixed catalogue is still one click away at
/workspace/global-favorites where the collection page can give it
proper per-type filters.
- Net dashboard density goes from 5 rails to 4 — closer to the
streaming-app sweet spot.
i18n: new WORKSPACE.DASHBOARD.LIVE_CONTINUE key, translated across all
17 locales by per-locale agents (placeholder integrity verified).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
9bb41f59b0
commit
fe849b2de2
22 files changed
+139
-30
No files matched your search
+15
-27
@@ -131,13 +131,19 @@
|
||||
/>
|
||||
}
|
||||
|
||||
<!-- Live now on your favorites (channels you recently watched) -->
|
||||
<!-- Live rail — favorited live channels first, falls back to
|
||||
recently-watched live. Title flips based on source so the label
|
||||
is always honest about what's rendered. -->
|
||||
@if (liveOnFavoritesCardsEnriched().length > 0) {
|
||||
<lib-dashboard-rail
|
||||
[label]="'WORKSPACE.DASHBOARD.LIVE_RECENT' | translate"
|
||||
[label]="liveRailTitleKey() | translate"
|
||||
[items]="liveOnFavoritesCardsEnriched()"
|
||||
[totalCount]="data.globalRecentLiveItems().length"
|
||||
[seeAllLink]="['/workspace/global-recent']"
|
||||
[totalCount]="liveRailTotalCount()"
|
||||
[seeAllLink]="
|
||||
liveRailSource() === 'favorites'
|
||||
? ['/workspace/global-favorites']
|
||||
: ['/workspace/global-recent']
|
||||
"
|
||||
layout="channel"
|
||||
[testId]="'dashboard-live-recent-rail'"
|
||||
/>
|
||||
@@ -162,29 +168,11 @@
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- Favorites rail -->
|
||||
@if (favoriteCards().length > 0) {
|
||||
<lib-dashboard-rail
|
||||
[label]="'WORKSPACE.DASHBOARD.GLOBAL_FAVORITES' | translate"
|
||||
[items]="favoriteCards()"
|
||||
[totalCount]="data.globalFavoriteItems().length"
|
||||
[seeAllLink]="['/workspace/global-favorites']"
|
||||
[testId]="'dashboard-global-favorites-rail'"
|
||||
/>
|
||||
} @else if (data.globalFavoritesLoading()) {
|
||||
<section
|
||||
class="rails-page__skeleton-rail"
|
||||
aria-hidden="true"
|
||||
aria-busy="true"
|
||||
>
|
||||
<div class="rails-page__skeleton-header"></div>
|
||||
<div class="rails-page__skeleton-track">
|
||||
@for (slot of skeletonSlots; track slot) {
|
||||
<div class="rails-page__skeleton-card"></div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
<!-- Mixed Global Favorites rail intentionally NOT rendered on the
|
||||
dashboard. Live favorites are promoted into the live rail above
|
||||
(with current EPG), and the full mixed catalogue lives on the
|
||||
dedicated /workspace/global-favorites page where the collection
|
||||
UI can give it the per-type filters it needs. -->
|
||||
|
||||
<!-- Sources rail -->
|
||||
@if (sourceCards().length > 0) {
|
||||
|
||||
+35
-3
@@ -403,11 +403,43 @@ export class WorkspaceDashboardRailsComponent {
|
||||
.map((item) => this.toRecentCard(item))
|
||||
);
|
||||
|
||||
readonly liveOnFavoritesCards = computed<DashboardRailCard[]>(() =>
|
||||
this.data
|
||||
// Live rail source — favorited live channels take precedence so the EPG
|
||||
// enrichment lands on the channels the user actually cares about. When
|
||||
// no favorites exist (fresh install, or a user who hasn't starred any
|
||||
// channel yet), fall back to recently-watched live so the rail still
|
||||
// has something to show. `liveRailSource` drives the title swap.
|
||||
readonly liveRailSource = computed<'favorites' | 'recent'>(() =>
|
||||
this.data.globalFavoriteLiveItems().length > 0
|
||||
? 'favorites'
|
||||
: 'recent'
|
||||
);
|
||||
|
||||
readonly liveOnFavoritesCards = computed<DashboardRailCard[]>(() => {
|
||||
if (this.liveRailSource() === 'favorites') {
|
||||
return this.data
|
||||
.globalFavoriteLiveItems()
|
||||
.slice(0, RAIL_ITEM_LIMIT)
|
||||
.map((item) => this.toFavoriteCard(item));
|
||||
}
|
||||
return this.data
|
||||
.globalRecentLiveItems()
|
||||
.slice(0, RAIL_ITEM_LIMIT)
|
||||
.map((item) => this.toRecentCard(item))
|
||||
.map((item) => this.toRecentCard(item));
|
||||
});
|
||||
|
||||
// Title key flips with the source — "Live now on your favorites" when
|
||||
// we're rendering favorites, "Continue with live TV" when we're falling
|
||||
// back to recently-watched. Always honest about what the user is seeing.
|
||||
readonly liveRailTitleKey = computed(() =>
|
||||
this.liveRailSource() === 'favorites'
|
||||
? 'WORKSPACE.DASHBOARD.LIVE_RECENT'
|
||||
: 'WORKSPACE.DASHBOARD.LIVE_CONTINUE'
|
||||
);
|
||||
|
||||
readonly liveRailTotalCount = computed(() =>
|
||||
this.liveRailSource() === 'favorites'
|
||||
? this.data.globalFavoriteLiveItems().length
|
||||
: this.data.globalRecentLiveItems().length
|
||||
);
|
||||
|
||||
// Best-effort EPG lookup keyed by the channel's display name. This works
|
||||
|
||||
Reference in new issue
Block a user