fix: a refresh no longer drags you off a channel; cap Download all

The auto-refresh could throw you back to All subscriptions and then
refuse to let you back in. useFeed rebuilt reload() whenever the filter
changed, so a refresh started before you picked a channel still held
the old closure: half a minute later it re-fetched the unfiltered list
and overwrote yours. The sidebar still showed the channel as selected,
so clicking it changed nothing and there was no way back except via
another channel.

reload() now reads the filter at call time instead of capturing it, and
carries a sequence number so a slow reply cannot overwrite a newer one.
It is stable across filter changes, which also stops the auto-refresh
interval being torn down and restarted every time you click a channel.

Download all now works from All subscriptions too, capped by a new
Settings limit (default 25, up to no limit). It takes the newest first
and says what it did — "Queued the newest 25 of 500" — so a cap is
never silent. Press it again for the next batch.
This commit is contained in:
vincent
2026-08-29 16:13:44 +02:00
parent caea8bad8f
commit 66cb80a044
5 changed files with 100 additions and 22 deletions
+31 -10
View File
@@ -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<RefreshProgress | null>(null);
const [toast, setToast] = useState<string | null>(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}