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: