diff --git a/src/App.jsx b/src/App.jsx index 898061f..d7cfead 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -761,10 +761,16 @@ export default function App() { }; // Functional update - never reads stale history from closure setHistory((prev) => { - const filtered = prev.filter( - (h) => !(h.id === entry.id && h.media_type === entry.media_type), - ); - const next = [entry, ...filtered].slice(0, 50); + // For TV: dedupe per episode (same show+season+episode). + // For movies: dedupe by id+media_type as before. + const filtered = prev.filter((h) => { + if (h.id !== entry.id || h.media_type !== entry.media_type) return true; + if (entry.media_type === "tv") { + return !(h.season === entry.season && h.episode === entry.episode); + } + return false; + }); + const next = [entry, ...filtered].slice(0, 100); storage.set("history", next); return next; }); @@ -799,6 +805,20 @@ export default function App() { }); }, []); + const removeHistory = useCallback((item) => { + setHistory((prev) => { + const next = prev.filter((h) => { + if (h.id !== item.id || h.media_type !== item.media_type) return true; + if (item.media_type === "tv") { + return !(h.season === item.season && h.episode === item.episode); + } + return false; + }); + storage.set("history", next); + return next; + }); + }, []); + // Memoized, avoids re-filtering on every download-progress event // Pre-compute progress keys for history items once; only re-runs when history changes. const historyWithKeys = useMemo( @@ -1004,6 +1024,7 @@ export default function App() { watched={watched} onMarkWatched={markWatched} onMarkUnwatched={markUnwatched} + onRemoveHistory={removeHistory} /> )} {page === "settings" && ( diff --git a/src/components/MediaCard.jsx b/src/components/MediaCard.jsx index 2b08c1d..bc2ab0d 100644 --- a/src/components/MediaCard.jsx +++ b/src/components/MediaCard.jsx @@ -143,7 +143,9 @@ const MediaCard = memo(function MediaCard({ {title}
- {year} · {isTV ? "Series" : "Movie"} + {isTV && item.season != null && item.episode != null + ? `S${item.season}E${item.episode}${item.episodeName ? ` · ${item.episodeName}` : ""}` + : `${year} · ${isTV ? "Series" : "Movie"}`}
[...inProgress, ...saved], @@ -136,7 +137,7 @@ export default function LibraryPage({ const isWatched = !!watched?.[pk]; return (
onSelect(item)} > @@ -160,8 +161,14 @@ export default function LibraryPage({
{item.media_type === "tv" && - item.season && - `S${item.season}E${item.episode} · `} + item.season != null && + item.episode != null && ( + <> + {`S${item.season}E${item.episode}`} + {item.episodeName ? ` · ${item.episodeName}` : ""} + {" · "} + + )} {new Date(item.watchedAt).toLocaleDateString("en-US", { year: "numeric", month: "short", @@ -174,6 +181,18 @@ export default function LibraryPage({ > {item.media_type === "tv" ? "Series" : "Movie"} + {onRemoveHistory && ( + + )}
); })} diff --git a/src/pages/TVPage.jsx b/src/pages/TVPage.jsx index bb720c1..9491c0f 100644 --- a/src/pages/TVPage.jsx +++ b/src/pages/TVPage.jsx @@ -925,6 +925,21 @@ export default function TVPage({ seasonData, ]); + // Auto-select specific episode when navigating from "Continue Watching" / history. + // Key includes item.id + item.episode so the ref resets whenever the target changes. + const autoSelectKeyRef = useRef(null); + useEffect(() => { + if (!item.episode || currentSeasonEpisodes.length === 0) return; + const key = `${item.id}_e${item.episode}`; + if (autoSelectKeyRef.current === key) return; // already handled this target + const target = Number(item.episode); + const ep = currentSeasonEpisodes.find((e) => e.episode_number === target); + if (ep) { + autoSelectKeyRef.current = key; + setSelectedEp(ep); + } + }, [item.id, item.episode, currentSeasonEpisodes]); + // ── Downloads lookup map: O(1) per episode instead of O(n) ─────────────── const downloadsByEpisodeKey = useMemo(() => { const map = new Map(); diff --git a/src/styles/global.css b/src/styles/global.css index 21cda32..fbd38ad 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -307,7 +307,12 @@ body, position: absolute; inset: 0; background: - linear-gradient(to right, var(--bg) 0%, color-mix(in srgb, var(--bg) 90%, transparent) 25%, transparent 65%), + linear-gradient( + to right, + var(--bg) 0%, + color-mix(in srgb, var(--bg) 90%, transparent) 25%, + transparent 65% + ), linear-gradient(to top, var(--bg) 0%, transparent 45%); } @@ -535,7 +540,11 @@ body.no-anim *::after { .card-overlay { position: absolute; inset: 0; - background: linear-gradient(to top, color-mix(in srgb, var(--bg) 88%, transparent) 0%, transparent 55%); + background: linear-gradient( + to top, + color-mix(in srgb, var(--bg) 88%, transparent) 0%, + transparent 55% + ); opacity: 0; transition: opacity 0.25s; display: flex; @@ -1429,6 +1438,32 @@ body.no-anim *::after { gap: 8px; } +.history-remove-btn { + background: none; + border: none; + color: var(--text3); + cursor: pointer; + font-size: 13px; + padding: 4px 6px; + border-radius: 4px; + line-height: 1; + opacity: 0; + transition: + opacity 0.15s, + color 0.15s, + background 0.15s; + flex-shrink: 0; +} + +.history-row:hover .history-remove-btn { + opacity: 1; +} + +.history-remove-btn:hover { + color: var(--red); + background: color-mix(in srgb, var(--red) 12%, transparent); +} + /* ── API Key Setup ───────────────────────────────────────────────────────── */ .apikey-modal { position: fixed; @@ -3307,7 +3342,9 @@ kbd { } .carousel-item--active { - filter: drop-shadow(0 8px 36px color-mix(in srgb, var(--bg) 75%, transparent)); + filter: drop-shadow( + 0 8px 36px color-mix(in srgb, var(--bg) 75%, transparent) + ); } .carousel-item--active .carousel-poster-wrap { box-shadow: