diff --git a/src/App.tsx b/src/App.tsx index 0f0856c..266e1ff 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,7 +16,7 @@ import { useDownloads } from "./hooks/useDownloads"; import { useFeed } from "./hooks/useFeed"; import { useWindowFullscreen } from "./hooks/useWindowFullscreen"; import { - QUALITIES, STREAM_QUALITIES, SUB_LANGS, + BULK_LIMITS, DEFAULT_BULK_LIMIT, QUALITIES, STREAM_QUALITIES, SUB_LANGS, type FeedFilter, type FeedItem, type Quality, type RefreshProgress, } from "./types"; @@ -87,6 +87,14 @@ export default function App() { return "best"; } }); + const [bulkLimit, setBulkLimit] = useState(() => { + try { + const stored = Number(localStorage.getItem("flighttube.bulkLimit")); + return BULK_LIMITS.some((b) => b.value === stored) ? stored : DEFAULT_BULK_LIMIT; + } catch { + return DEFAULT_BULK_LIMIT; + } + }); const [refreshing, setRefreshing] = useState(false); const [refreshProgress, setRefreshProgress] = useState(null); const [toast, setToast] = useState(null); @@ -118,6 +126,7 @@ export default function App() { try { localStorage.setItem("flighttube.view", view); localStorage.setItem("flighttube.quality", quality); + localStorage.setItem("flighttube.bulkLimit", String(bulkLimit)); localStorage.setItem("flighttube.streamQuality", streamQuality); localStorage.setItem("flighttube.subLang", subLang); localStorage.setItem("flighttube.browser", browser); @@ -127,7 +136,8 @@ export default function App() { } catch { /* storage blocked */ } - }, [view, quality, streamQuality, subLang, browser, downloadedOnly, hideShorts, sidebarHidden]); + }, [view, quality, bulkLimit, streamQuality, subLang, browser, downloadedOnly, + hideShorts, sidebarHidden]); // Offline, the only videos that can be played are the ones already on disk, // so the feed collapses to those regardless of the toggle. @@ -255,13 +265,23 @@ export default function App() { // Queues the lot in one go. The backend runs two at a time and the rest wait // their turn, so this is a queue rather than a stampede; a video that fails // reports it on its own row instead of throwing a dialog for each one. + const bulkTargets = useMemo( + () => (bulkLimit > 0 ? pendingDownloads.slice(0, bulkLimit) : pendingDownloads), + [pendingDownloads, bulkLimit], + ); + const downloadAll = useCallback(() => { - if (pendingDownloads.length === 0) return; - say(`Queued ${pendingDownloads.length} video${pendingDownloads.length === 1 ? "" : "s"}`); - for (const i of pendingDownloads) { + if (bulkTargets.length === 0) return; + const capped = bulkTargets.length < pendingDownloads.length; + say( + capped + ? `Queued the newest ${bulkTargets.length} of ${pendingDownloads.length} — limit set in Settings` + : `Queued ${bulkTargets.length} video${bulkTargets.length === 1 ? "" : "s"}`, + ); + for (const i of bulkTargets) { downloadVideo(i.id, quality, subLangArg).catch(() => {}); } - }, [pendingDownloads, quality, subLangArg, say]); + }, [bulkTargets, pendingDownloads.length, quality, subLangArg, say]); // Ids on screen still lacking a length, newest first. Joined into a string // so the effect below only re-runs when the set actually changes. @@ -396,10 +416,9 @@ export default function App() { resultCount={items.length} view={view} onView={setView} onDeleteAll={totals.downloaded > 0 ? () => setConfirmWipe(true) : undefined} - onDownloadAll={ - channelId && online && pendingDownloads.length > 0 ? downloadAll : undefined - } - downloadAllCount={pendingDownloads.length} + onDownloadAll={online && bulkTargets.length > 0 ? downloadAll : undefined} + downloadAllCount={bulkTargets.length} + downloadAllTotal={pendingDownloads.length} sidebarHidden={sidebarHidden} onShowSidebar={() => { setSidebarHidden(false); setSidebarPeek(false); }} titleBarInset={titleBarInset} @@ -525,6 +544,8 @@ export default function App() { onAppearance={setMode} quality={quality} onQuality={setQuality} + bulkLimit={bulkLimit} + onBulkLimit={setBulkLimit} streamQuality={streamQuality} onStreamQuality={setStreamQuality} subLang={subLang} diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 5db6e97..a05c93c 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -6,6 +6,7 @@ import { import { APPEARANCE_MODES, type Appearance } from "../hooks/useAppearance"; import { QUALITIES, STREAM_QUALITIES, SUB_LANGS, + BULK_LIMITS, type ImportPreview, type Prereqs, type Quality, type UpdateStatus, } from "../types"; import TakeoutGuide from "./TakeoutGuide"; @@ -21,6 +22,8 @@ interface Props { onAppearance: (a: Appearance) => void; quality: Quality; onQuality: (q: Quality) => void; + bulkLimit: number; + onBulkLimit: (n: number) => void; streamQuality: Quality; onStreamQuality: (q: Quality) => void; subLang: string; @@ -53,6 +56,7 @@ function StatusRow({ label, value }: { label: string; value: string | null }) { export default function Settings({ onClose, onImported, appearance, onAppearance, quality, onQuality, + bulkLimit, onBulkLimit, streamQuality, onStreamQuality, subLang, onSubLang, hideShorts, onHideShorts, browser, onBrowser, onError, }: Props) { @@ -218,6 +222,26 @@ export default function Settings({ + +

+ The cap on one Download all. It takes the newest first, so from + All subscriptions you get the latest across every channel rather than + several hundred videos at once. Press it again for the next batch. +

+