mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 12:36:15 -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
@@ -621,6 +621,69 @@ describe('DashboardDataService', () => {
|
||||
expect(seriesPos?.positionSeconds).toBe(720);
|
||||
});
|
||||
|
||||
it('exposes a live-only slice of global favorites so the dashboard can promote favorited channels into the Live rail', async () => {
|
||||
// Seed two favorites: a movie and a live channel.
|
||||
dbServiceMock.getAllGlobalFavorites.mockResolvedValue([
|
||||
{
|
||||
id: 500,
|
||||
category_id: 18,
|
||||
title: 'Favorite Movie',
|
||||
rating: '7.0',
|
||||
added_at: '2026-04-21T10:00:00.000Z',
|
||||
poster_url: 'https://example.com/fav-movie.png',
|
||||
xtream_id: 5500,
|
||||
type: 'movie',
|
||||
playlist_id: 'xtream-1',
|
||||
playlist_name: 'Xtream Playlist',
|
||||
},
|
||||
{
|
||||
id: 501,
|
||||
category_id: 19,
|
||||
title: 'Favorite Channel HD',
|
||||
rating: '0',
|
||||
added_at: '2026-04-22T10:00:00.000Z',
|
||||
poster_url: 'https://example.com/fav-channel.png',
|
||||
xtream_id: 5501,
|
||||
type: 'live',
|
||||
playlist_id: 'xtream-1',
|
||||
playlist_name: 'Xtream Playlist',
|
||||
},
|
||||
]);
|
||||
playlistsSignal.set([
|
||||
...playlistsSignal(),
|
||||
{
|
||||
_id: 'xtream-1',
|
||||
title: 'Xtream Playlist',
|
||||
count: 2,
|
||||
importDate: '2026-01-01T00:00:00.000Z',
|
||||
autoRefresh: false,
|
||||
serverUrl: 'https://example.com',
|
||||
},
|
||||
]);
|
||||
|
||||
await service.reloadGlobalFavorites();
|
||||
|
||||
const liveOnly = service.globalFavoriteLiveItems();
|
||||
// Includes the Xtream "Favorite Channel HD" plus the two M3U live
|
||||
// favorites already seeded by the default fixture.
|
||||
expect(liveOnly.map((it) => it.title)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'Favorite Channel HD',
|
||||
'Channel One',
|
||||
'Channel Two',
|
||||
])
|
||||
);
|
||||
|
||||
// Crucially, the movie favorite is excluded — the rail source must
|
||||
// never leak VOD posters into the channel layout.
|
||||
expect(liveOnly.map((it) => it.title)).not.toContain(
|
||||
'Favorite Movie'
|
||||
);
|
||||
expect(
|
||||
service.globalFavoriteLiveItems().every((it) => it.type === 'live')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns null playback position for live channels (no position tracking in schema)', async () => {
|
||||
// The default M3U playlist contains a live channel. Without
|
||||
// reloading playback positions the lookup still gracefully returns
|
||||
|
||||
@@ -223,6 +223,14 @@ export class DashboardDataService {
|
||||
this.globalRecentItems().filter((item) => item.type === 'live')
|
||||
);
|
||||
|
||||
// Favorited live channels — the dashboard's "Live now" rail prefers this
|
||||
// source so the EPG enrichment lands on the channels users actually care
|
||||
// about. Falls back to globalRecentLiveItems when this is empty so a
|
||||
// fresh-install user still sees something useful.
|
||||
readonly globalFavoriteLiveItems = computed<DashboardFavoriteItem[]>(() =>
|
||||
this.globalFavoriteItems().filter((item) => item.type === 'live')
|
||||
);
|
||||
|
||||
// Playback positions, keyed by `${playlist_id}::${contentXtreamId}::${type}`.
|
||||
// Loaded lazily on dashboard mount (and refreshed when recent items
|
||||
// change) so the hero + Continue Watching cards can show "X min left"
|
||||
|
||||
Reference in new issue
Block a user