feat(home): tap opens detail, long-press plays from home cards

Home carousel cards route a tap to the item's detail / Episode Focus
View and a ~500ms long-press to a confirm-then-play flow. MediaCard gains
an onLongPress prop with pointer-based detection (cancelled on >10px move
so carousel scroll is unaffected, trailing click suppressed). Episode
taps route to /library/<seriesId>?episode=<id>; the bare-episode detail
page links back to its parent series/season.

TRACES: UR-058 | DR-087
This commit is contained in:
2026-07-24 23:49:03 +02:00
parent e2c9d68311
commit 589f08b873
4 changed files with 136 additions and 17 deletions
+42 -8
View File
@@ -56,18 +56,47 @@
previousServerReachable = serverReachable;
});
// Tap → detail page. Non-playable containers already routed to /library; now
// movies and episodes go to their detail page too instead of playing straight
// away. Channel leaves (no detail page) still go direct to the player.
// TRACES: UR-058 | DR-087
function handleItemClick(item: MediaItem) {
switch (item.kind) {
case "series":
case "season":
case "album":
case "artist":
case "folder":
case "channel":
goto(`/library/${item.id}`);
case "channelItem":
case "liveChannel":
goto(`/player/${item.id}`);
break;
case "episode":
// An episode is never browsed as a bare Episode page — it opens in its
// series' Episode Focus View so the series context loads (ux-flows §5B.1).
if (item.seriesId) {
goto(`/library/${item.seriesId}?episode=${item.id}`);
} else {
goto(`/library/${item.id}`);
}
break;
default:
goto(`/player/${item.id}`);
goto(`/library/${item.id}`);
break;
}
}
// Long press → play immediately, confirming first so an accidental hold on a
// half-watched item doesn't blow away the user's spot without warning.
// TRACES: UR-058 | DR-087
function handleItemLongPress(item: MediaItem) {
switch (item.kind) {
case "movie":
case "episode":
case "channelItem":
case "liveChannel":
if (confirm(`Play "${item.name}" now?`)) {
goto(`/player/${item.id}`);
}
break;
default:
// Containers (series/season/album/…) have no single "play now" target.
goto(`/library/${item.id}`);
break;
}
}
@@ -145,6 +174,7 @@
title="Next Movie"
items={resumeMovies}
onItemClick={handleItemClick}
onItemLongPress={handleItemLongPress}
/>
{/if}
@@ -154,6 +184,7 @@
title="Next Episode"
items={nextUpItems}
onItemClick={handleItemClick}
onItemLongPress={handleItemLongPress}
/>
{/if}
@@ -163,6 +194,7 @@
title="Recently Listened"
items={recentlyPlayedAudio}
onItemClick={handleItemClick}
onItemLongPress={handleItemLongPress}
/>
{/if}
@@ -172,6 +204,7 @@
title="Continue Watching"
items={resumeItems}
onItemClick={handleItemClick}
onItemLongPress={handleItemLongPress}
/>
{/if}
@@ -181,6 +214,7 @@
title="Recently Added"
items={latestItems}
onItemClick={handleItemClick}
onItemLongPress={handleItemLongPress}
/>
{/if}