feat: feed UI, in-app player, offline mode, settings
Adds an end-to-end integration test that runs the real pipeline against live YouTube Atom feeds, verifying the merged feed is newest-first across channels.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { checkPrereqs, pickAndImportTakeout, pickLibraryFolder } from "../api";
|
||||
import type { Prereqs } from "../types";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onImported: (count: number) => void;
|
||||
}
|
||||
|
||||
function StatusRow({ label, value, hint }: { label: string; value: string | null; hint?: string }) {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-4 py-2 border-b border-edge/60">
|
||||
<span className="text-sm text-muted shrink-0">{label}</span>
|
||||
<span className={`text-sm text-right ${value ? "text-emerald-300" : "text-red-300"}`}>
|
||||
{value ?? hint ?? "Not found"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Settings({ onClose, onImported }: Props) {
|
||||
const [prereqs, setPrereqs] = useState<Prereqs | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
const load = () => checkPrereqs().then(setPrereqs).catch(() => setPrereqs(null));
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const doImport = async () => {
|
||||
setBusy(true);
|
||||
setMessage(null);
|
||||
try {
|
||||
const n = await pickAndImportTakeout();
|
||||
if (n != null) {
|
||||
setMessage(`Imported ${n} subscription${n === 1 ? "" : "s"}.`);
|
||||
onImported(n);
|
||||
}
|
||||
} catch (e) {
|
||||
setMessage(String(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const doPickFolder = async () => {
|
||||
try {
|
||||
const p = await pickLibraryFolder();
|
||||
if (p) { setMessage(`Library moved to ${p}`); load(); }
|
||||
} catch (e) {
|
||||
setMessage(String(e));
|
||||
}
|
||||
};
|
||||
|
||||
const missing = prereqs && (!prereqs.yt_dlp || !prereqs.ffmpeg);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-black/60 grid place-items-center p-6" onClick={onClose}>
|
||||
<div onClick={(e) => e.stopPropagation()}
|
||||
className="w-full max-w-lg rounded-2xl bg-surface border border-edge p-6 space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">Settings</h2>
|
||||
<button onClick={onClose}
|
||||
className="rounded-full px-3 py-1.5 text-xs bg-raised hover:bg-edge cursor-pointer">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-medium">Subscriptions</h3>
|
||||
<p className="text-xs text-muted leading-relaxed">
|
||||
Export <span className="text-white">YouTube subscriptions</span> from Google Takeout,
|
||||
then import the <code className="text-white">subscriptions.csv</code> file here.
|
||||
Re-importing merges with what you already have.
|
||||
</p>
|
||||
<button onClick={doImport} disabled={busy}
|
||||
className="rounded-full bg-accent/90 hover:bg-accent text-black px-4 py-2 text-xs
|
||||
font-semibold cursor-pointer disabled:opacity-40">
|
||||
{busy ? "Importing…" : "Import subscriptions.csv"}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section className="space-y-1">
|
||||
<h3 className="text-sm font-medium mb-2">Status</h3>
|
||||
<StatusRow label="yt-dlp" value={prereqs?.yt_dlp ?? null} />
|
||||
<StatusRow label="ffmpeg" value={prereqs?.ffmpeg ?? null} />
|
||||
<div className="flex items-start justify-between gap-4 py-2">
|
||||
<span className="text-sm text-muted shrink-0">Library</span>
|
||||
<button onClick={doPickFolder}
|
||||
className="text-sm text-right text-accent hover:underline cursor-pointer break-all">
|
||||
{prereqs?.library_path ?? "…"}
|
||||
</button>
|
||||
</div>
|
||||
{missing && (
|
||||
<div className="mt-2 rounded-lg bg-red-500/10 border border-red-500/30 p-3">
|
||||
<p className="text-xs text-red-200 leading-relaxed">
|
||||
Downloads need both tools. Install them with:
|
||||
</p>
|
||||
<code className="mt-1.5 block text-xs text-white bg-black/40 rounded px-2 py-1.5">
|
||||
brew install yt-dlp ffmpeg
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{message && <p className="text-xs text-muted break-words">{message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user