import { useEffect, useState } from "react"; import { checkPrereqs, importTakeoutCsv, pickLibraryFolder, pickTakeoutFile, previewTakeoutImport, } from "../api"; import { APPEARANCE_MODES, type Appearance } from "../hooks/useAppearance"; import { QUALITIES, STREAM_QUALITIES, type ImportPreview, type Prereqs, type Quality, } from "../types"; import TakeoutGuide from "./TakeoutGuide"; import { BTN_PRIMARY, CONTROL_H, Dialog, HELP, ICON_BTN, LABEL, SectionHeading, Segmented, SUBPANEL, } from "./ui"; interface Props { onClose: () => void; onImported: (count: number) => void; appearance: Appearance; onAppearance: (a: Appearance) => void; quality: Quality; onQuality: (q: Quality) => void; streamQuality: Quality; onStreamQuality: (q: Quality) => void; onError: (message: string) => void; } const SELECT = `w-full ${CONTROL_H} cursor-pointer rounded-lg border border-slate-300 bg-white px-2 ` + "text-[12px] outline-none dark:border-slate-700 dark:bg-slate-800"; function StatusRow({ label, value }: { label: string; value: string | null }) { return (
{label} {value ?? "Not found"}
); } export default function Settings({ onClose, onImported, appearance, onAppearance, quality, onQuality, streamQuality, onStreamQuality, onError, }: Props) { const [prereqs, setPrereqs] = useState(null); const [pending, setPending] = useState<{ path: string; preview: ImportPreview } | null>(null); const [busy, setBusy] = useState(false); const load = () => checkPrereqs().then(setPrereqs).catch(() => setPrereqs(null)); useEffect(() => { load(); }, []); const startImport = async () => { try { const path = await pickTakeoutFile(); if (!path) return; setPending({ path, preview: await previewTakeoutImport(path) }); } catch (e) { onError(String(e)); } }; const confirmImport = async () => { if (!pending) return; setBusy(true); try { const n = await importTakeoutCsv(pending.path); setPending(null); onImported(n); } catch (e) { setPending(null); onError(String(e)); } finally { setBusy(false); } }; const pickFolder = async () => { try { if (await pickLibraryFolder()) load(); } catch (e) { onError(String(e)); } }; const missing = prereqs && (!prereqs.yt_dlp || !prereqs.ffmpeg); const p = pending?.preview; const destructive = !!p && (p.removed_channels > 0 || p.removed_downloads > 0); return ( <>
e.stopPropagation()} className="flex max-h-[82vh] w-full max-w-lg flex-col rounded-2xl border border-slate-300 bg-white shadow-2xl dark:border-slate-700 dark:bg-slate-900" >

Settings

Get your subscriptions

YouTube has no public API for someone else's subscription list, so FlightTube reads the export Google gives you. It takes about two minutes.

Import

Importing replaces your current subscription list — the CSV becomes the whole truth. Channels no longer in it are removed along with their videos and downloads. You'll see exactly what goes before anything is deleted.

Download quality

Above 1080p YouTube only serves VP9 and AV1. Those play here, but the files are several times larger and older Macs may struggle. Pick 1080p for H.264, which plays anywhere. Audio is AAC at every setting.

Streaming quality applies when you play something you have not downloaded. Best lets the player adapt to your connection; a fixed height pins it.

Appearance
Theme ({ value: m, label: m[0].toUpperCase() + m.slice(1), }))} />
Status
Library

yt-dlp and ffmpeg ship inside the app, so nothing needs installing. A copy on your system is used instead if one is present, which is how you can run a newer yt-dlp than the bundled one.

{missing && (

A bundled tool is missing, which should not happen. Reinstalling the app will restore it; meanwhile brew install yt-dlp ffmpeg also works.

)}
{pending && p && ( setPending(null)} onConfirm={busy ? undefined : confirmImport} confirmLabel={destructive ? "Replace" : "Import"} destructive={destructive} >

The file lists {p.incoming} subscription{p.incoming === 1 ? "" : "s"}, which will become your complete list.

{destructive ? ( ) : (

Nothing currently stored will be removed.

)}
)} ); }