From 22e31afc1d80846809488d5fde7f24657ebcfb90 Mon Sep 17 00:00:00 2001 From: Vincent Rozenberg Date: Fri, 4 Sep 2026 02:03:46 +0200 Subject: [PATCH] feat: remove every subscription from Settings, and reopen the last video MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove all sits beside Import in Settings, since emptying the list is the other half of replacing it. It asks first and counts what goes: 210 channels, 3339 videos, 50 downloaded files, in those words. Nothing changes on YouTube, and the confirmation says so — importing or reading the list again brings it all back. It takes the same path an import does when nothing survives, so a video saved on its own from the menu bar is left alone: its channel was never a subscription, and this is about subscriptions. The app also reopens whatever was playing when it last closed, if that video is still in the feed and still playable — offline, one that was streaming is not. The note is made while the player is open and dropped when it is closed, so a restart only reopens something you were in the middle of, never something you had deliberately finished with. Verified both ways: quitting mid-video came back to it, closing the player first came back to the feed. The panel's Remove all downloads is gone; it was a misreading of the request. --- src-tauri/src/commands.rs | 24 ++++++++++++++++ src-tauri/src/lib.rs | 2 ++ src-tauri/src/tray.rs | 2 +- src/App.tsx | 54 ++++++++++++++++++++++++++++-------- src/api.ts | 6 ++++ src/components/Settings.tsx | 46 ++++++++++++++++++++++++++++-- src/components/TrayPanel.tsx | 23 --------------- 7 files changed, 120 insertions(+), 37 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index c5eec72..722a77f 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1737,6 +1737,30 @@ pub async fn import_scraped( state.db.lock().await.replace_channels(&channels) } +/// What clearing the subscription list would take with it. +#[tauri::command] +pub async fn preview_remove_all_subscriptions( + state: State<'_, AppState>, +) -> Result { + state.db.lock().await.preview_replace(&[]) +} + +/// Empties the subscription list, and the videos and files hanging off it. +/// +/// The same path an import takes when nothing survives it, so a video saved on +/// its own from the menu bar is left alone here too: its channel was never a +/// subscription, and this is about subscriptions. +#[tauri::command] +pub async fn remove_all_subscriptions(state: State<'_, AppState>) -> Result { + let dropped = state.db.lock().await.paths_dropped_by_replace(&[])?; + for path in &dropped { + let _ = tokio::fs::remove_file(path).await; + } + let before = state.db.lock().await.list_channels()?.len(); + state.db.lock().await.replace_channels(&[])?; + Ok(before) +} + /// What deleting one subscription would take with it. #[derive(Serialize)] pub struct RemovalPreview { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8f895a1..0773335 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -144,6 +144,8 @@ pub fn run() { tray::quit_app, commands::preview_scraped_import, commands::import_scraped, + commands::preview_remove_all_subscriptions, + commands::remove_all_subscriptions, commands::delete_channel, commands::preview_delete_channel, commands::set_download_defaults, diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 5c897a5..226b6a6 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -357,7 +357,7 @@ pub fn quit_app(app: AppHandle) { /// The panel's own size. Fixed, because it is a menu: it does not resize. const PANEL_W: f64 = 304.0; -const PANEL_H: f64 = 296.0; +const PANEL_H: f64 = 252.0; /// Opens the panel under the menu bar icon. /// diff --git a/src/App.tsx b/src/App.tsx index 181d988..61fe254 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -318,8 +318,19 @@ export default function App() { const openIndex = useCallback( (i: number) => { - if (playableAt(i)) setPlayingIndex(i); - else setFailure("That video isn't downloaded, and you're offline."); + const at = playableAt(i); + if (!at) { + setFailure("That video isn't downloaded, and you're offline."); + return; + } + setPlayingIndex(i); + // Noted while it is open and forgotten when it is closed, so a restart + // only reopens something you were actually in the middle of. + try { + localStorage.setItem("flighttube.playing", at.item.id); + } catch { + /* storage blocked */ + } }, [playableAt], ); @@ -359,15 +370,6 @@ export default function App() { }); }, [quality, embedLang]); - // The menu bar can ask to clear the downloads; the answering is done here, - // by the same confirmation the toolbar uses. - useEffect(() => { - const un = listen("downloads:wipe-request", () => setConfirmWipe(true)); - return () => { - void un.then((f) => f()); - }; - }, []); - // The menu bar reads the subscription list, but replacing what is here is // not a thing to agree to in a panel that closes when you look away. useEffect(() => { @@ -445,6 +447,26 @@ export default function App() { .catch((e) => setFailure(String(e))); }, [removing, channelId, reload, say]); + // Reopen whatever was playing when the app last closed, once the feed has + // loaded and only if that video is still in it and still playable — offline, + // a video that was streaming is not. + const resumedOnce = useRef(false); + useEffect(() => { + if (resumedOnce.current || items.length === 0 || playingIndex != null) return; + resumedOnce.current = true; + let id: string | null = null; + try { + id = localStorage.getItem("flighttube.playing"); + } catch { + /* storage blocked */ + } + if (!id) return; + const at = items.findIndex((i) => i.id === id); + if (at >= 0 && playableAt(at)) setPlayingIndex(at); + // playableAt changes with every render; the guard above runs this once. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [items]); + // On launch, bring the library in line before the first ten-minute check — // otherwise auto mode looks asleep for the first ten minutes. Waits for the // channel list, since knowing which channels are subscriptions is what keeps @@ -796,6 +818,11 @@ export default function App() { )} onClose={() => { setPlayingIndex(null); + try { + localStorage.removeItem("flighttube.playing"); + } catch { + /* storage blocked */ + } // Coming back from a video is the natural moment to pick up // whatever has been posted since. if (online && !refreshing) void doRefresh(); @@ -835,6 +862,11 @@ export default function App() { reload(); say(`Imported ${n} subscription${n === 1 ? "" : "s"}`); }} + onRemovedAll={(n) => { + setChannelId(null); + reload(); + say(`Removed ${n} subscription${n === 1 ? "" : "s"}`); + }} /> )} diff --git a/src/api.ts b/src/api.ts index 4eb87c7..309d801 100644 --- a/src/api.ts +++ b/src/api.ts @@ -73,6 +73,12 @@ export const previewScrapedImport = (channels: ScrapeResult["channels"]) => export const importScraped = (channels: ScrapeResult["channels"]) => invoke("import_scraped", { channels }); +/** What emptying the subscription list would take with it. */ +export const previewRemoveAllSubscriptions = () => + invoke("preview_remove_all_subscriptions"); + +export const removeAllSubscriptions = () => invoke("remove_all_subscriptions"); + /** The menu bar panel's own actions. */ export const traySaveVideo = () => invoke("tray_save_video"); export const showMainWindow = () => invoke("show_main_window"); diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 62ff155..db24ec6 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -1,7 +1,8 @@ import { useEffect, useState } from "react"; import { checkPrereqs, checkYtDlpUpdate, importTakeoutCsv, listBrowsers, pickLibraryFolder, - pickTakeoutFile, previewTakeoutImport, setCookieSource, testYoutube, updateYtDlp, + pickTakeoutFile, previewRemoveAllSubscriptions, previewTakeoutImport, + removeAllSubscriptions, setCookieSource, testYoutube, updateYtDlp, } from "../api"; import { APPEARANCE_MODES, type Appearance } from "../hooks/useAppearance"; import { @@ -18,6 +19,7 @@ import { interface Props { onClose: () => void; onImported: (count: number) => void; + onRemovedAll: (count: number) => void; appearance: Appearance; onAppearance: (a: Appearance) => void; quality: Quality; @@ -57,7 +59,7 @@ function StatusRow({ label, value }: { label: string; value: string | null }) { } export default function Settings({ - onClose, onImported, appearance, onAppearance, quality, onQuality, + onClose, onImported, onRemovedAll, appearance, onAppearance, quality, onQuality, bulkLimit, onBulkLimit, streamQuality, onStreamQuality, subLang, onSubLang, hideShorts, onHideShorts, autoplayNext, onAutoplayNext, @@ -65,6 +67,8 @@ export default function Settings({ }: Props) { const [prereqs, setPrereqs] = useState(null); const [pending, setPending] = useState<{ path: string; preview: ImportPreview } | null>(null); + // Emptying the list is confirmed against what it would actually remove. + const [wipe, setWipe] = useState(null); const [busy, setBusy] = useState(false); const [browsers, setBrowsers] = useState>([]); const [check, setCheck] = useState<{ ok: boolean; message: string } | null>(null); @@ -199,6 +203,17 @@ export default function Settings({ + @@ -439,6 +454,33 @@ export default function Settings({ + {wipe && ( + setWipe(null)} + onConfirm={() => { + setWipe(null); + setBusy(true); + removeAllSubscriptions() + .then(onRemovedAll) + .catch((e) => onError(String(e))) + .finally(() => setBusy(false)); + }} + confirmLabel="Remove all" + destructive + > +

+ All {wipe.removed_channels} channels leave FlightTube, taking{" "} + {wipe.removed_videos} videos and {wipe.removed_downloads} downloaded + file{wipe.removed_downloads === 1 ? "" : "s"} with them. +

+

+ Nothing changes on YouTube — you stay subscribed there, and importing or reading + the list again brings everything back. +

+
+ )} + {guide && ( setGuide(false)} wide>

diff --git a/src/components/TrayPanel.tsx b/src/components/TrayPanel.tsx index 4db807f..6e92260 100644 --- a/src/components/TrayPanel.tsx +++ b/src/components/TrayPanel.tsx @@ -122,29 +122,6 @@ export default function TrayPanel() { } /> - - run( - "wipe", - async () => { - await emitTo("main", "downloads:wipe-request", null); - await showMainWindow(); - }, - false, - ) - } - icon={ - - - - - - } - /> -