From 46b1d62138f920e4b7258453c41c1e361bfeb5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 28 Jul 2026 15:15:21 +0000 Subject: [PATCH] =?UTF-8?q?video=20download=20panel:=20grid=20layout=20?= =?UTF-8?q?=E2=80=94=20numbered=20playlist=20cells=20+=20large=20single=20?= =?UTF-8?q?card?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses the resizable panel + container queries instead of a cramped vertical list: - Single item → one large card (big 16:9 thumbnail, title, uploader·duration, full-width download button). - Playlist → a responsive grid (2 cols, 3 at @520px, 4 at @760px of panel width) of compact cells, each with a position number badge and an overlay download/ status chip. Loading skeletons, error, and per-cell download states (download → downloading → saving → saved / retry) carry over. Co-Authored-By: Claude Opus 4.8 --- .../apps/FileBrowser/VideoDownloadPanel.tsx | 197 ++++++++++++------ 1 file changed, 129 insertions(+), 68 deletions(-) diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/VideoDownloadPanel.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/VideoDownloadPanel.tsx index b91661e9..45d913d2 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/VideoDownloadPanel.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/VideoDownloadPanel.tsx @@ -36,6 +36,116 @@ const fmtDuration = (sec?: number): string => { const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); const sanitizeFolder = (name: string) => name.replace(/[/\\]/g, '').replace(/^\.+/, '').trim(); +// The thumbnail contents for a card (loading skeleton spinner / error / cover art / audio icon). +const Thumb = ({ e, audioOnly, size }: { e: Entry; audioOnly: boolean; size: number }) => { + if (e.status === 'loading') return ; + if (e.status === 'error') return ; + if (e.thumbnail && !audioOnly) return ; + return ; +}; + +// Download control, in two flavors: `overlay` (a chip pinned in a grid thumbnail) or full-width (single). +const CardAction = ({ e, onDownload, overlay }: { e: Entry; onDownload: () => void; overlay?: boolean }) => { + if (e.dl === 'done') + return overlay ? ( + + Saved + + ) : ( +
+ Saved +
+ ); + if (e.dl === 'downloading' || e.dl === 'saving') { + const label = e.dl === 'saving' ? 'Saving…' : 'Downloading…'; + return overlay ? ( + + {label} + + ) : ( +
+ {label} +
+ ); + } + const label = e.dl === 'error' ? 'Retry' : 'Download'; + return overlay ? ( + + ) : ( + + ); +}; + +const sub = (e: Entry) => [e.uploader, fmtDuration(e.duration)].filter(Boolean).join(' · '); + +// Compact grid cell (playlist), with a position number badge. +const GridCard = ({ + e, + number, + audioOnly, + onDownload, +}: { + e: Entry; + number: number; + audioOnly: boolean; + onDownload: () => void; +}) => ( +
+
+ + + {number} + + {e.status === 'ready' && } +
+ {e.status !== 'loading' && ( + <> +

+ {e.status === 'error' ? 'Could not fetch' : e.title || e.url} +

+

{e.status === 'error' ? e.error || '' : sub(e)}

+ + )} +
+); + +// Large single-item card. +const SingleCard = ({ e, audioOnly, onDownload }: { e: Entry; audioOnly: boolean; onDownload: () => void }) => ( +
+
+ +
+ {e.status === 'error' ? ( +
+

Could not fetch

+

{e.error || e.url}

+
+ ) : e.status === 'ready' ? ( + <> +
+

+ {e.title || e.url} +

+ {sub(e) &&

{sub(e)}

} +
+ + + ) : null} +
+); + export const VideoDownloadPanelHeader = () => ( <> @@ -145,9 +255,6 @@ export const VideoDownloadPanel = () => { setDownloadingAll(false); }; - const dlLabel = (e: Entry) => - e.dl === 'downloading' ? 'Downloading…' : e.dl === 'saving' ? 'Saving…' : e.dl === 'done' ? 'Saved' : ''; - return (
@@ -219,72 +326,26 @@ export const VideoDownloadPanel = () => {
)} -
- {entries.map((e, i) => ( -
-
- {e.status === 'loading' ? ( - - ) : e.status === 'error' ? ( - - ) : e.thumbnail && !audioOnly ? ( - - ) : ( - - )} -
- -
- {e.status === 'loading' ? ( -
-
-
-
- ) : e.status === 'error' ? ( - <> -

Could not fetch

-

{e.error || e.url}

- - ) : ( - <> -

- {e.title || e.url} -

-

- {[e.uploader, fmtDuration(e.duration)].filter(Boolean).join(' · ')} -

- {e.dl === 'error' &&

{e.dlError}

} - - )} -
- - {e.status === 'ready' && ( -
- {e.dl === 'done' ? ( - - Saved - - ) : e.dl === 'downloading' || e.dl === 'saving' ? ( - - {dlLabel(e)} - - ) : ( - - )} -
- )} +
+ {entries.length === 1 ? ( + void downloadEntry(0, entries[0]!.url)} + /> + ) : ( +
+ {entries.map((e, i) => ( + void downloadEntry(i, e.url)} + /> + ))}
- ))} + )}
)}