Skip to content

Commit 2516f2c

Browse files
rogueburger21claude
andcommitted
feat(player): wire autoplay-next into settings and refactor into hook
Addresses review feedback on PR truelockmc#57: - Move autoplay countdown logic into a dedicated src/utils/useAutoplay hook - Add an "Autoplay Next Episode" section to Settings > Playback: enable/disable toggle, configurable countdown duration, and overlay side (left/right). A duration of 0 shows the overlay with buttons only and does not auto-advance. - Persist the new keys (autoplayNextEnabled / autoplayNextDuration / autoplayNextLayout) and add them to backup.js - Trigger the overlay using the existing watchedThreshold instead of a hardcoded 5s - Read nextEp from an always-current ref inside the countdown interval so the correct episode plays if season data reloads mid-countdown - Redesign the overlay: darkened player with a side panel showing the next episode's still, title, and larger Cancel / Play Now actions Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 329d1bd commit 2516f2c

5 files changed

Lines changed: 371 additions & 62 deletions

File tree

src/pages/SettingsPage.jsx

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2830,6 +2830,15 @@ export default function SettingsPage({
28302830
const [introSkipMode, setIntroSkipMode] = useState(
28312831
() => storage.get(STORAGE_KEYS.INTRO_SKIP_MODE) || "off",
28322832
);
2833+
const [autoplayNextEnabled, setAutoplayNextEnabled] = useState(
2834+
() => storage.get(STORAGE_KEYS.AUTOPLAY_NEXT_ENABLED) ?? true,
2835+
);
2836+
const [autoplayNextDuration, setAutoplayNextDuration] = useState(
2837+
() => storage.get(STORAGE_KEYS.AUTOPLAY_NEXT_DURATION) ?? 5,
2838+
);
2839+
const [autoplayNextLayout, setAutoplayNextLayout] = useState(
2840+
() => storage.get(STORAGE_KEYS.AUTOPLAY_NEXT_LAYOUT) || "right",
2841+
);
28332842
const [saved, setSaved] = useState(false);
28342843
const [showResetConfirm, setShowResetConfirm] = useState(false);
28352844
const [resetHovered, setResetHovered] = useState(false);
@@ -3400,6 +3409,103 @@ export default function SettingsPage({
34003409
)}
34013410
</div>
34023411

3412+
<Divider />
3413+
3414+
{/* Autoplay Next Episode */}
3415+
<div style={{ marginBottom: 40 }}>
3416+
<div className="settings-section-title">Autoplay Next Episode</div>
3417+
<div
3418+
style={{
3419+
fontSize: 13,
3420+
color: "var(--text3)",
3421+
marginBottom: 16,
3422+
lineHeight: 1.6,
3423+
}}
3424+
>
3425+
Configure how the player behaves when an episode finishes.
3426+
</div>
3427+
3428+
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
3429+
{/* Enable/Disable Toggle */}
3430+
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
3431+
<Toggle
3432+
value={autoplayNextEnabled}
3433+
onChange={(val) => {
3434+
setAutoplayNextEnabled(val);
3435+
storage.set(STORAGE_KEYS.AUTOPLAY_NEXT_ENABLED, val);
3436+
flash();
3437+
}}
3438+
/>
3439+
<div>
3440+
<div style={{ fontSize: 14, fontWeight: 500, color: "var(--text)" }}>
3441+
Enable Autoplay Next
3442+
</div>
3443+
<div style={{ fontSize: 12, color: "var(--text3)", marginTop: 2 }}>
3444+
Automatically play the next episode when the current one ends.
3445+
</div>
3446+
</div>
3447+
</div>
3448+
3449+
{autoplayNextEnabled && (
3450+
<>
3451+
{/* Duration input */}
3452+
<div style={{ display: "flex", flexDirection: "column", gap: 8, marginTop: 8 }}>
3453+
<div style={{ fontSize: 13, fontWeight: 600, color: "var(--text2)" }}>
3454+
Countdown Duration
3455+
</div>
3456+
<div style={{ fontSize: 12, color: "var(--text3)", marginBottom: 4 }}>
3457+
Number of seconds to display the countdown. Set to 0 to only show the buttons and not autoplay automatically.
3458+
</div>
3459+
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
3460+
<input
3461+
type="number"
3462+
min={0}
3463+
max={60}
3464+
className="apikey-input"
3465+
style={{ width: 90, marginBottom: 0 }}
3466+
value={autoplayNextDuration}
3467+
onChange={(e) => setAutoplayNextDuration(e.target.value)}
3468+
onBlur={() => {
3469+
const num = Math.max(0, Math.min(60, parseInt(autoplayNextDuration, 10) || 0));
3470+
setAutoplayNextDuration(num);
3471+
storage.set(STORAGE_KEYS.AUTOPLAY_NEXT_DURATION, num);
3472+
flash();
3473+
}}
3474+
/>
3475+
<span style={{ fontSize: 14, color: "var(--text2)" }}>
3476+
seconds
3477+
</span>
3478+
</div>
3479+
</div>
3480+
3481+
{/* Overlay Layout selection */}
3482+
<div style={{ display: "flex", flexDirection: "column", gap: 8, marginTop: 8 }}>
3483+
<div style={{ fontSize: 13, fontWeight: 600, color: "var(--text2)" }}>
3484+
Overlay Position
3485+
</div>
3486+
<div style={{ fontSize: 12, color: "var(--text3)", marginBottom: 4 }}>
3487+
Choose which side of the player the next episode thumbnail and details are shown.
3488+
</div>
3489+
<SettingsSelect
3490+
value={autoplayNextLayout}
3491+
onChange={(val) => {
3492+
setAutoplayNextLayout(val);
3493+
storage.set(STORAGE_KEYS.AUTOPLAY_NEXT_LAYOUT, val);
3494+
flash();
3495+
}}
3496+
options={[
3497+
{ value: "left", label: "Left Side" },
3498+
{ value: "right", label: "Right Side" },
3499+
]}
3500+
/>
3501+
</div>
3502+
</>
3503+
)}
3504+
</div>
3505+
</div>
3506+
3507+
<Divider />
3508+
34033509
{/* Intro Skip */}
34043510
<div style={{ marginBottom: 40 }}>
34053511
<div className="settings-section-title">Anime Intro Skip</div>

src/pages/TVPage.jsx

Lines changed: 154 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import TrailerModal from "../components/TrailerModal";
5050
import BlockedStatsModal from "../components/BlockedStatsModal";
5151
import { useBlockedStats } from "../utils/useBlockedStats";
5252
import { storage, STORAGE_KEYS } from "../utils/storage";
53+
import { useAutoplay } from "../utils/useAutoplay";
5354
import { fetchAniSkipTimings } from "../utils/aniSkip";
5455
import {
5556
fetchTVRating,
@@ -455,10 +456,10 @@ export default function TVPage({
455456
const restricted = isRestricted(rating.minAge, ageLimitSetting);
456457
const [seasonMenu, setSeasonMenu] = useState(null); // { x, y, seasonNum }
457458

458-
const [autoplayCountdown, setAutoplayCountdown] = useState(null);
459-
const countdownIntervalRef = useRef(null);
460-
const countdownStartedRef = useRef(false);
461-
const startAutoplayRef = useRef(() => {});
459+
const resetAutoplayRef = useRef(() => {});
460+
const triggerAutoplayRef = useRef(() => {});
461+
const setCountdownStartedRef = useRef(() => {});
462+
const localCountdownStartedRef = useRef(false);
462463

463464
// Read threshold from settings (default 20s), stable across renders
464465
const [watchedThreshold] = useState(
@@ -1007,18 +1008,14 @@ export default function TVPage({
10071008
) ?? null)
10081009
: null;
10091010

1010-
// Reset auto-mark guard when episode changes
1011+
// Reset auto-mark guard and autoplay when episode changes
10111012
useEffect(() => {
10121013
autoMarkedRef.current = false;
10131014
lastKnownTimeRef.current = 0;
10141015
seekBackCooldownRef.current = 0;
10151016
durationRef.current = 0;
1016-
setAutoplayCountdown(null);
1017-
countdownStartedRef.current = false;
1018-
if (countdownIntervalRef.current) {
1019-
clearInterval(countdownIntervalRef.current);
1020-
countdownIntervalRef.current = null;
1021-
}
1017+
localCountdownStartedRef.current = false;
1018+
resetAutoplayRef.current?.();
10221019
}, [currentProgressKey]);
10231020

10241021
// Show loader instantly when playback starts
@@ -1285,9 +1282,10 @@ export default function TVPage({
12851282
}
12861283

12871284
// Autoplay trigger
1288-
if (remaining <= 5 && !countdownStartedRef.current) {
1289-
countdownStartedRef.current = true;
1290-
startAutoplayRef.current?.();
1285+
if (remaining <= watchedThreshold && !localCountdownStartedRef.current) {
1286+
localCountdownStartedRef.current = true;
1287+
setCountdownStartedRef.current?.(true);
1288+
triggerAutoplayRef.current?.();
12911289
}
12921290
}
12931291
} catch {}
@@ -1381,26 +1379,28 @@ export default function TVPage({
13811379
return null;
13821380
}, [selectedEp, currentSeasonEpisodes]);
13831381

1382+
const {
1383+
autoplayCountdown,
1384+
countdownStarted,
1385+
setCountdownStarted,
1386+
triggerAutoplay,
1387+
cancelAutoplay,
1388+
playNow,
1389+
resetAutoplay,
1390+
} = useAutoplay({ nextEp, playEpisode, restricted });
1391+
13841392
useEffect(() => {
1385-
startAutoplayRef.current = () => {
1386-
if (!nextEp) return;
1387-
const epUnreleased = nextEp.air_date ? new Date(nextEp.air_date) > _todayForEpisodes : false;
1388-
if (restricted || epUnreleased) return;
1389-
1390-
setAutoplayCountdown(5);
1391-
countdownIntervalRef.current = setInterval(() => {
1392-
setAutoplayCountdown((prev) => {
1393-
if (prev <= 1) {
1394-
clearInterval(countdownIntervalRef.current);
1395-
countdownIntervalRef.current = null;
1396-
playEpisode(nextEp);
1397-
return null;
1398-
}
1399-
return prev - 1;
1400-
});
1401-
}, 1000);
1402-
};
1403-
}, [nextEp, restricted, playEpisode]);
1393+
resetAutoplayRef.current = resetAutoplay;
1394+
triggerAutoplayRef.current = triggerAutoplay;
1395+
setCountdownStartedRef.current = setCountdownStarted;
1396+
}, [resetAutoplay, triggerAutoplay, setCountdownStarted]);
1397+
1398+
useEffect(() => {
1399+
localCountdownStartedRef.current = countdownStarted;
1400+
}, [countdownStarted]);
1401+
1402+
const autoplayNextLayout =
1403+
storage.get(STORAGE_KEYS.AUTOPLAY_NEXT_LAYOUT) || "right";
14041404

14051405
const handleSetDownloaderFolder = useCallback((folder) => {
14061406
setDownloaderFolder(folder);
@@ -1719,42 +1719,134 @@ export default function TVPage({
17191719
inset: 0,
17201720
zIndex: 30,
17211721
display: "flex",
1722-
flexDirection: "column",
1723-
alignItems: "center",
1724-
justifyContent: "center",
1725-
background: "rgba(0,0,0,0.85)",
1726-
gap: 16,
1722+
justifyContent: autoplayNextLayout === "left" ? "flex-start" : "flex-end",
1723+
background: "rgba(0,0,0,0.88)",
17271724
borderRadius: "inherit",
1728-
backdropFilter: "blur(4px)",
1725+
backdropFilter: "blur(6px)",
1726+
animation: "fadeIn 0.4s ease",
17291727
}}
17301728
>
1731-
<span style={{ fontSize: 24, fontWeight: "bold", color: "white" }}>
1732-
Up Next: {nextEp?.name}
1733-
</span>
1734-
<span style={{ fontSize: 16, color: "var(--text2)" }}>
1735-
Starting in {autoplayCountdown}...
1736-
</span>
1737-
<div style={{ display: "flex", gap: 12, marginTop: 12 }}>
1738-
<button
1739-
className="player-overlay-btn"
1740-
onClick={() => {
1741-
if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current);
1742-
setAutoplayCountdown(null);
1729+
<div
1730+
style={{
1731+
width: "40%",
1732+
minWidth: "320px",
1733+
maxWidth: "480px",
1734+
height: "100%",
1735+
background: autoplayNextLayout === "left"
1736+
? "linear-gradient(90deg, rgba(0,0,0,0.95) 0%, rgba(0,0,0,0.85) 60%, rgba(0,0,0,0) 100%)"
1737+
: "linear-gradient(270deg, rgba(0,0,0,0.95) 0%, rgba(0,0,0,0.85) 60%, rgba(0,0,0,0) 100%)",
1738+
display: "flex",
1739+
flexDirection: "column",
1740+
justifyContent: "center",
1741+
padding: "40px",
1742+
boxSizing: "border-box",
1743+
textAlign: "left",
1744+
}}
1745+
>
1746+
<div
1747+
style={{
1748+
fontSize: "11px",
1749+
fontWeight: "700",
1750+
letterSpacing: "1.5px",
1751+
textTransform: "uppercase",
1752+
color: "var(--red)",
1753+
marginBottom: "8px",
17431754
}}
17441755
>
1745-
Cancel
1746-
</button>
1747-
<button
1748-
className="player-overlay-btn"
1749-
style={{ background: "var(--red)", color: "white", borderColor: "var(--red)" }}
1750-
onClick={() => {
1751-
if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current);
1752-
setAutoplayCountdown(null);
1753-
playEpisode(nextEp);
1756+
Up Next
1757+
</div>
1758+
1759+
{/* Cover Still */}
1760+
<div
1761+
style={{
1762+
width: "100%",
1763+
aspectRatio: "16/9",
1764+
borderRadius: "8px",
1765+
overflow: "hidden",
1766+
border: "1px solid rgba(255,255,255,0.1)",
1767+
marginBottom: "18px",
1768+
boxShadow: "0 8px 24px rgba(0,0,0,0.6)",
1769+
background: "var(--surface3)",
1770+
display: "flex",
1771+
alignItems: "center",
1772+
justifyContent: "center",
17541773
}}
17551774
>
1756-
<PlayIcon /> Play Now
1757-
</button>
1775+
{nextEp?.still_path ? (
1776+
<img
1777+
src={imgUrl(nextEp.still_path, "w300")}
1778+
alt={nextEp?.name}
1779+
style={{ width: "100%", height: "100%", objectFit: "cover" }}
1780+
/>
1781+
) : (
1782+
<span
1783+
style={{
1784+
display: "inline-flex",
1785+
width: 32,
1786+
height: 32,
1787+
color: "var(--text3)",
1788+
}}
1789+
>
1790+
<PlayIcon />
1791+
</span>
1792+
)}
1793+
</div>
1794+
1795+
{/* Episode Meta */}
1796+
<div style={{ fontSize: "13px", fontWeight: "600", color: "var(--text2)", marginBottom: "4px" }}>
1797+
Season {selectedSeason} · Episode {nextEp?.episode_number}
1798+
</div>
1799+
<div
1800+
style={{
1801+
fontSize: "20px",
1802+
fontWeight: "700",
1803+
color: "white",
1804+
marginBottom: "8px",
1805+
lineHeight: "1.3",
1806+
wordBreak: "break-word",
1807+
}}
1808+
>
1809+
{nextEp?.name}
1810+
</div>
1811+
1812+
{/* Countdown */}
1813+
{autoplayCountdown > 0 ? (
1814+
<div style={{ fontSize: "14px", color: "var(--text3)", marginBottom: "20px" }}>
1815+
Starting in <span style={{ color: "white", fontWeight: "600" }}>{autoplayCountdown}</span>s...
1816+
</div>
1817+
) : (
1818+
<div style={{ height: "20px", marginBottom: "20px" }} />
1819+
)}
1820+
1821+
{/* Buttons */}
1822+
<div style={{ display: "flex", gap: "12px", marginTop: "4px" }}>
1823+
<button
1824+
className="btn btn-primary"
1825+
style={{
1826+
padding: "10px 22px",
1827+
fontSize: "14px",
1828+
fontWeight: "600",
1829+
background: "var(--red)",
1830+
borderColor: "var(--red)",
1831+
boxShadow: "var(--red-glow)",
1832+
}}
1833+
onClick={playNow}
1834+
>
1835+
Play Now
1836+
</button>
1837+
<button
1838+
className="btn btn-ghost"
1839+
style={{
1840+
padding: "10px 22px",
1841+
fontSize: "14px",
1842+
fontWeight: "600",
1843+
background: "rgba(255,255,255,0.05)",
1844+
}}
1845+
onClick={cancelAutoplay}
1846+
>
1847+
Cancel
1848+
</button>
1849+
</div>
17581850
</div>
17591851
</div>
17601852
)}

0 commit comments

Comments
 (0)