@@ -277,14 +299,10 @@ export const VideoDownloadPanel = () => {
placeholder="https://www.youtube.com/watch?v=… or …/playlist?list=…"
className="h-10 w-full rounded-md border border-duck-dark/20 bg-background/60 px-3 text-sm text-duck-dark outline-none placeholder:text-duck-dark/40 focus:border-duck-teal/50"
/>
-
-
- {audioOnly ? 'Audio only' : 'Video'}
- {isPlaylist ? ` · ${entries.length} items` : ''}
-
+ {isPlaylist && {entries.length} items}
{isPlaylist && (
-
- setSubfolder(ev.target.value)}
- placeholder="Subfolder (optional) — leave blank for this folder"
- className="h-9 w-full rounded-md border border-duck-dark/20 bg-background/60 px-3 text-sm text-duck-dark outline-none placeholder:text-duck-dark/40 focus:border-duck-teal/50"
- />
-
-
+
setSubfolder(ev.target.value)}
+ placeholder="Subfolder (optional) — leave blank for this folder"
+ className="h-9 w-full rounded-md border border-duck-dark/20 bg-background/60 px-3 text-sm text-duck-dark outline-none placeholder:text-duck-dark/40 focus:border-duck-teal/50"
+ />
)}
+ {/* Bulk action row (playlists only) — progress while running, otherwise the format actions. */}
+ {isPlaylist &&
+ (bulk ? (
+
+ ) : mode === 'select' ? (
+
+
+
+
+ ) : (
+
+ {anyReadyVideo && (
+
+ )}
+
+
+
+ ))}
+
{entries.length === 1 ? (
-
void downloadEntry(0, entries[0]!.url)}
- />
+ void downloadFmt(0, fmt)} />
) : (
{entries.map((e, i) => (
@@ -340,8 +393,10 @@ export const VideoDownloadPanel = () => {
key={`${e.url}-${i}`}
e={e}
number={i + 1}
- audioOnly={audioOnly}
- onDownload={() => void downloadEntry(i, e.url)}
+ mode={mode}
+ sel={sel[i] ?? {}}
+ onToggle={(fmt) => toggleSel(i, fmt)}
+ onDownload={(fmt) => void downloadFmt(i, fmt)}
/>
))}
@@ -352,3 +407,63 @@ export const VideoDownloadPanel = () => {
);
};
+
+// ── Cards ──
+
+const GridCard = ({
+ e,
+ number,
+ mode,
+ sel,
+ onToggle,
+ onDownload,
+}: {
+ e: Entry;
+ number: number;
+ mode: 'normal' | 'select';
+ sel: { video?: boolean; audio?: boolean };
+ onToggle: (fmt: Fmt) => void;
+ onDownload: (fmt: Fmt) => void;
+}) => (
+
+
+
+
+ {number}
+
+
+ {e.status !== 'loading' && (
+ <>
+
+ {e.status === 'error' ? 'Could not fetch' : e.title || e.url}
+
+
{e.status === 'error' ? e.error || '' : sub(e)}
+ >
+ )}
+ {e.status === 'ready' &&
}
+
+);
+
+const SingleCard = ({ e, onDownload }: { e: Entry; onDownload: (fmt: Fmt) => void }) => (
+
+
+
+
+ {e.status === 'error' ? (
+
+
Could not fetch
+
{e.error || e.url}
+
+ ) : e.status === 'ready' ? (
+ <>
+
+
+ {e.title || e.url}
+
+ {sub(e) &&
{sub(e)}
}
+
+
{}} onDownload={onDownload} />
+ >
+ ) : null}
+
+);
diff --git a/src/workspaces/officerdev/src/hooks/useFilesAPI.ts b/src/workspaces/officerdev/src/hooks/useFilesAPI.ts
index e602e49e..915d0dcc 100644
--- a/src/workspaces/officerdev/src/hooks/useFilesAPI.ts
+++ b/src/workspaces/officerdev/src/hooks/useFilesAPI.ts
@@ -168,6 +168,7 @@ export type VideoInfo = {
thumbnail?: string;
duration?: number;
uploader?: string;
+ formats?: Array<{ height?: number; id?: string; label?: string }>; // present ⇒ video streams available
error?: string;
};