From b9e8a9db84fea878afc9ba3450e8a20a8da87c60 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 1 Sep 2026 18:57:07 +0200 Subject: [PATCH] feat: play the next video automatically when one ends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A toggle under Playback in Settings, off by default and remembered. Verified offline with forced offline mode on: a downloaded video ran to its end and the next one started by itself, 6/51 to 7/51, with no network. It follows the same list the Next button does, which already steps to the next *playable* video — offline that is the next download, so it plays through what is on the Mac one after another. Online it will stream the next video, and the Settings text says so: a toggle that quietly does nothing outside one hidden condition is how the subtitle preference went wrong three times. The last video in the list simply stops; there is nowhere to go and Next is already absent there. --- src/App.tsx | 7 ++++++- src/components/Player.tsx | 10 +++++++++- src/components/Settings.tsx | 21 +++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 9031569..f4335b9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -49,6 +49,7 @@ export default function App() { const [search, setSearch] = useState(""); const [downloadedOnly, setDownloadedOnly] = useState(() => remembered("downloadedOnly")); const [hideShorts, setHideShorts] = useState(() => remembered("hideShorts")); + const [autoplayNext, setAutoplayNext] = useState(() => remembered("autoplayNext")); const [sidebarHidden, setSidebarHidden] = useState(() => remembered("sidebarHidden")); const [sidebarPeek, setSidebarPeek] = useState(false); const [view, setView] = useState(() => { @@ -161,12 +162,13 @@ export default function App() { localStorage.setItem("flighttube.browser", browser); localStorage.setItem("flighttube.downloadedOnly", downloadedOnly ? "1" : "0"); localStorage.setItem("flighttube.hideShorts", hideShorts ? "1" : "0"); + localStorage.setItem("flighttube.autoplayNext", autoplayNext ? "1" : "0"); localStorage.setItem("flighttube.sidebarHidden", sidebarHidden ? "1" : "0"); } catch { /* storage blocked */ } }, [view, quality, bulkLimit, streamQuality, subLang, subStyle, browser, downloadedOnly, - hideShorts, sidebarHidden]); + hideShorts, autoplayNext, sidebarHidden]); // The language a download embeds. Like the player's fetch, this does not // depend on the on/off preference: that says what is shown, and a file @@ -624,6 +626,7 @@ export default function App() { onSubLang={setSubLang} subStyle={subStyle} onSubStyle={setSubStyle} + autoplayNext={autoplayNext} onOpenChannel={() => { setChannelId(playing.item.channel_id); setSearch(""); @@ -683,6 +686,8 @@ export default function App() { onSubLang={setSubLang} hideShorts={hideShorts} onHideShorts={setHideShorts} + autoplayNext={autoplayNext} + onAutoplayNext={setAutoplayNext} browser={browser} onBrowser={setBrowser} onError={setFailure} diff --git a/src/components/Player.tsx b/src/components/Player.tsx index 9905359..a22e4a5 100644 --- a/src/components/Player.tsx +++ b/src/components/Player.tsx @@ -27,6 +27,8 @@ interface Props { onSubStyle: (s: SubStyle) => void; /** Leaves the player for this video's channel. */ onOpenChannel: () => void; + /** Roll straight on to the next video when this one ends. */ + autoplayNext: boolean; /** Persists a subtitle choice made from the transport bar. */ onSubLang: (l: string) => void; /** Position in the current feed, for the "3 of 180" readout. */ @@ -122,7 +124,7 @@ function placeCues(vtt: string, line: number | null): string { export default function Player({ item, path, onClose, onDelete, onPrev, onNext, onDownload, downloading, - maxHeight, subLang, onSubLang, subStyle, onSubStyle, onOpenChannel, + maxHeight, subLang, onSubLang, subStyle, onSubStyle, onOpenChannel, autoplayNext, index, total, titleBarInset, }: Props) { const streaming = path === null; @@ -457,6 +459,12 @@ export default function Player({ onTimeUpdate={onTimeUpdate} onLoadedMetadata={onLoadedMetadata} onPause={persist} + onEnded={() => { + persist(); + // Only where there is somewhere to go: the last video in the + // list simply stops. + if (autoplayNext) onNext?.(); + }} onLoadStart={() => setBuffering(true)} onWaiting={() => setBuffering(true)} onStalled={() => setBuffering(true)} diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index f95b9ce..62ff155 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -30,6 +30,8 @@ interface Props { onSubLang: (l: string) => void; hideShorts: boolean; onHideShorts: (v: boolean) => void; + autoplayNext: boolean; + onAutoplayNext: (v: boolean) => void; browser: string; onBrowser: (b: string) => void; onError: (message: string) => void; @@ -58,6 +60,7 @@ export default function Settings({ onClose, onImported, appearance, onAppearance, quality, onQuality, bulkLimit, onBulkLimit, streamQuality, onStreamQuality, subLang, onSubLang, hideShorts, onHideShorts, + autoplayNext, onAutoplayNext, browser, onBrowser, onError, }: Props) { const [prereqs, setPrereqs] = useState(null); @@ -301,6 +304,24 @@ export default function Settings({

+
+ Playback + +

+ When a video ends, the next one in the list starts on its own. Meant for + offline viewing, where it plays through your downloads one after another; + online it will stream the next video, and it stops at the end of the list. +

+
+
Sign in to YouTube