Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -1004,6 +1024,7 @@ export default function App() {
watched={watched}
onMarkWatched={markWatched}
onMarkUnwatched={markUnwatched}
onRemoveHistory={removeHistory}
/>
)}
{page === "settings" && (
Expand Down
4 changes: 3 additions & 1 deletion src/components/MediaCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ const MediaCard = memo(function MediaCard({
{title}
</div>
<div className="card-year">
{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"}`}
</div>
</div>
<span
Expand Down
25 changes: 22 additions & 3 deletions src/pages/LibraryPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default function LibraryPage({
watched,
onMarkWatched,
onMarkUnwatched,
onRemoveHistory,
}) {
const allItems = useMemo(
() => [...inProgress, ...saved],
Expand Down Expand Up @@ -136,7 +137,7 @@ export default function LibraryPage({
const isWatched = !!watched?.[pk];
return (
<div
key={pk}
key={`${pk}_${item.watchedAt}`}
className="history-row"
onClick={() => onSelect(item)}
>
Expand All @@ -160,8 +161,14 @@ export default function LibraryPage({
</div>
<div style={{ fontSize: 12, color: "var(--text3)" }}>
{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",
Expand All @@ -174,6 +181,18 @@ export default function LibraryPage({
>
{item.media_type === "tv" ? "Series" : "Movie"}
</span>
{onRemoveHistory && (
<button
className="history-remove-btn"
title="Remove from history"
onClick={(e) => {
e.stopPropagation();
onRemoveHistory(item);
}}
>
</button>
)}
</div>
);
})}
Expand Down
15 changes: 15 additions & 0 deletions src/pages/TVPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
43 changes: 40 additions & 3 deletions src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -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%);
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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:
Expand Down
Loading