@@ -50,6 +50,7 @@ import TrailerModal from "../components/TrailerModal";
5050import BlockedStatsModal from "../components/BlockedStatsModal" ;
5151import { useBlockedStats } from "../utils/useBlockedStats" ;
5252import { storage , STORAGE_KEYS } from "../utils/storage" ;
53+ import { useAutoplay } from "../utils/useAutoplay" ;
5354import { fetchAniSkipTimings } from "../utils/aniSkip" ;
5455import {
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