"use client"; // Hub offline manager: save an archive's channels for offline use, grouped by // source site. This is the hub consumer for the origin-aware offline API — each // download/evict/status call is scoped to (origin, slug) so channels from // different archives never collide. Kept lean (no per-channel offline search // index) to stay clear of the shipped single-site OfflineManager. import { useEffect, useState } from "react"; import { splitId } from "yt-dlp-transcript-common/components/originId"; import { useSearchData } from "yt-dlp-transcript-common/components/SearchDataContext"; import { channelCachedPages, downloadChannelOffline, evictChannelOffline, isPinned, swSupported, } from "../../lib/offlineCache"; type DlState = { status: "idle" | "downloading" | "cached" | "error"; done: number; total: number; }; const IDLE: DlState = { status: "idle", done: 0, total: 0 }; export default function HubOfflineManager() { const { channels, groups } = useSearchData(); const [states, setStates] = useState>({}); const [supported, setSupported] = useState(true); const set = (key: string, patch: Partial) => setStates((prev) => ({ ...prev, [key]: { ...(prev[key] ?? IDLE), ...patch } })); // Reflect already-pinned/cached channels on mount. useEffect(() => { if (!swSupported()) { setSupported(false); return; } let cancelled = false; (async () => { for (const c of channels) { const { origin, slug } = splitId(c.key); if (!isPinned(origin, slug)) continue; const cached = await channelCachedPages(origin, slug); if (!cancelled && cached > 0) { set(c.key, { status: "cached", done: cached, total: cached }); } } })(); return () => { cancelled = true; }; }, [channels]); async function download(key: string) { const { origin, slug } = splitId(key); set(key, { status: "downloading", done: 0, total: 0 }); const ok = await downloadChannelOffline(origin, slug, ({ done, total }) => set(key, { status: "downloading", done, total }), ).catch(() => false); set(key, { status: ok ? "cached" : "error" }); } async function remove(key: string) { const { origin, slug } = splitId(key); await evictChannelOffline(origin, slug); set(key, { ...IDLE }); } if (!supported || channels.length === 0) return null; // Group channels by their origin (= the group id in hub mode). const byGroup = new Map(); for (const c of channels) { const list = byGroup.get(c.groupId) ?? []; list.push(c); byGroup.set(c.groupId, list); } return (
Save archives for offline

Download a channel to read its transcripts without a connection. Saved channels stay on this device until you remove them.

{groups .filter((g) => byGroup.has(g.id)) .map((g) => (
{g.accent && (
    {(byGroup.get(g.id) ?? []).map((c) => { const st = states[c.key] ?? IDLE; return (
  • {c.name} {st.status === "downloading" ? ( {st.total ? `${st.done}/${st.total}` : "…"} ) : st.status === "cached" ? ( ) : ( )}
  • ); })}
))}
); }