From 2e69b15de6711eba7473b5c0c7505152f7308ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 23 Aug 2026 16:23:15 +0000 Subject: [PATCH] video download dialog keyed (57 -> 50) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit URL/playlist fetch, per-item preview, and the download-all flow. Most of what the scanner flagged here was the smaller half — the bulk was ternaries and text following a `}` rather than a `>` (the same structural miss "Open" and "Fetch" hit in FileItem), found by reading the file. dlLabel's three-way ternary, Retry/Download, Audio only/Video, and the "· N items" count were all invisible to the tool. Distinguished two classes of error string carefully. `pl.error` and `info.error` from a successful-but-failed server response are LEFT AS IS — server content, same as apiError.message in phase 1's ServerError dialog. Our own fallback defaults for a network failure the server never got to respond to ("Could not fetch info", "Timed out", "Could not start the download") are ours and are translated. One real plural, itemCount, checked at 0/1/2 in both locales. --- .../components/VideoDownloadDialog.tsx | 53 +++++++++++-------- .../officerdev/src/locales/en-GB.json | 27 ++++++++++ .../officerdev/src/locales/pt-PT.json | 27 ++++++++++ 3 files changed, 86 insertions(+), 21 deletions(-) diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/VideoDownloadDialog.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/VideoDownloadDialog.tsx index 50d11480..e3474894 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/VideoDownloadDialog.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/VideoDownloadDialog.tsx @@ -4,6 +4,7 @@ import { Checkbox } from '@/components/ui/checkbox'; import { Download, Loader2, ChevronLeft, AlertCircle, Check, Music } from 'lucide-react'; import type { VideoInfo } from '../../../../hooks/useFilesAPI'; import type { UseFileBrowserAppType } from '../useFileBrowserApp'; +import { useTranslation } from '../../../../locales'; type VideoDownloadDialogProps = { fileBrowserManager: UseFileBrowserAppType; @@ -37,6 +38,7 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); const sanitizeFolder = (name: string) => name.replace(/[/\\]/g, '').replace(/^\.+/, '').trim(); export const VideoDownloadDialog = ({ fileBrowserManager }: VideoDownloadDialogProps) => { + const { t } = useTranslation(); const { showVideoDownload, setShowVideoDownload, currentPath, refresh, files } = fileBrowserManager; const [phase, setPhase] = useState<'input' | 'preview'>('input'); @@ -94,7 +96,9 @@ export const VideoDownloadDialog = ({ fileBrowserManager }: VideoDownloadDialogP // Sequentially (ReClip does yt-dlp per video — parallel would hammer it); cards fill in as they resolve. for (let i = 0; i < urls.length; i++) { - const info = await files.videoInfo(urls[i]!).catch((): VideoInfo => ({ error: 'Could not fetch info' })); + const info = await files + .videoInfo(urls[i]!) + .catch((): VideoInfo => ({ error: t('videoDownload.couldNotFetchInfo') })); if (info.error) patch(i, { status: 'error', error: info.error }); else patch(i, { @@ -121,11 +125,12 @@ export const VideoDownloadDialog = ({ fileBrowserManager }: VideoDownloadDialogP const { jobId } = await files.downloadVideo(entryUrl, targetDir(), audioOnly); const deadline = Date.now() + 60 * 60 * 1000; for (;;) { - if (Date.now() > deadline) return patch(i, { dl: 'error', dlError: 'Timed out' }); + if (Date.now() > deadline) return patch(i, { dl: 'error', dlError: t('videoDownload.timedOut') }); await sleep(2000); const st = await files.downloadVideoStatus(jobId).catch(() => null); if (!st) continue; - if (st.status === 'error') return patch(i, { dl: 'error', dlError: st.error || 'Download failed' }); + if (st.status === 'error') + return patch(i, { dl: 'error', dlError: st.error || t('videoDownload.downloadFailed') }); if (st.status === 'transferring') patch(i, { dl: 'saving' }); if (st.status === 'done') { patch(i, { dl: 'done', filename: st.filename }); @@ -134,7 +139,7 @@ export const VideoDownloadDialog = ({ fileBrowserManager }: VideoDownloadDialogP } } } catch { - patch(i, { dl: 'error', dlError: 'Could not start the download' }); + patch(i, { dl: 'error', dlError: t('videoDownload.couldNotStart') }); } }; @@ -152,17 +157,21 @@ export const VideoDownloadDialog = ({ fileBrowserManager }: VideoDownloadDialogP }; const dlLabel = (e: Entry) => - e.dl === 'downloading' ? 'Downloading…' : e.dl === 'saving' ? 'Saving…' : e.dl === 'done' ? 'Saved' : ''; + e.dl === 'downloading' + ? t('videoDownload.downloading') + : e.dl === 'saving' + ? t('videoDownload.saving') + : e.dl === 'done' + ? t('videoDownload.saved') + : ''; return ( !open && handleClose()}> - Download video + {t('videoDownload.title')} - {phase === 'input' - ? 'Paste a video or playlist URL — it fetches details before downloading.' - : 'Review and download.'} + {phase === 'input' ? t('videoDownload.descriptionInput') : t('videoDownload.descriptionPreview')} @@ -178,12 +187,12 @@ export const VideoDownloadDialog = ({ fileBrowserManager }: VideoDownloadDialogP autoFocus value={url} onChange={(ev) => setUrl(ev.target.value)} - placeholder="https://www.youtube.com/watch?v=… or …/playlist?list=…" + placeholder={t('videoDownload.urlPlaceholder')} 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" />
@@ -211,11 +220,11 @@ export const VideoDownloadDialog = ({ fileBrowserManager }: VideoDownloadDialogP onClick={() => setPhase('input')} className="flex cursor-pointer items-center gap-1 text-sm text-duck-dark/60 hover:text-duck-dark" > - Back + {t('videoDownload.back')} - {audioOnly ? 'Audio only' : 'Video'} - {isPlaylist ? ` · ${entries.length} items` : ''} + {audioOnly ? t('videoDownload.audioOnlyLabel') : t('videoDownload.video')} + {isPlaylist ? ` · ${t('videoDownload.itemCount', { count: entries.length })}` : ''} @@ -224,7 +233,7 @@ export const VideoDownloadDialog = ({ fileBrowserManager }: VideoDownloadDialogP setSubfolder(ev.target.value)} - placeholder="Subfolder (optional) — leave blank for this folder" + placeholder={t('videoDownload.subfolderPlaceholder')} className="h-9 min-w-0 flex-1 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" /> )} @@ -265,7 +274,9 @@ export const VideoDownloadDialog = ({ fileBrowserManager }: VideoDownloadDialogP ) : e.status === 'error' ? ( <> -

Could not fetch

+

+ {t('videoDownload.couldNotFetch')} +

{e.error || e.url}

) : ( @@ -285,7 +296,7 @@ export const VideoDownloadDialog = ({ fileBrowserManager }: VideoDownloadDialogP
{e.dl === 'done' ? ( - Saved + {t('videoDownload.saved')} ) : e.dl === 'downloading' || e.dl === 'saving' ? ( @@ -298,7 +309,7 @@ export const VideoDownloadDialog = ({ fileBrowserManager }: VideoDownloadDialogP className="flex cursor-pointer items-center gap-1.5 rounded-md border border-duck-teal/40 px-2.5 py-1.5 text-xs font-medium text-duck-teal transition-colors hover:bg-duck-teal/10" > - {e.dl === 'error' ? 'Retry' : 'Download'} + {e.dl === 'error' ? t('videoDownload.retry') : t('videoDownload.download')} )}
diff --git a/src/workspaces/officerdev/src/locales/en-GB.json b/src/workspaces/officerdev/src/locales/en-GB.json index 37e1dc0d..20778e1b 100644 --- a/src/workspaces/officerdev/src/locales/en-GB.json +++ b/src/workspaces/officerdev/src/locales/en-GB.json @@ -575,5 +575,32 @@ "placeholder": "https://github.com/owner/repo.git", "submit": "Clone" } + }, + "videoDownload": { + "title": "Download video", + "descriptionInput": "Paste a video or playlist URL — it fetches details before downloading.", + "descriptionPreview": "Review and download.", + "urlPlaceholder": "https://www.youtube.com/watch?v=… or …/playlist?list=…", + "audioOnly": "Extract audio only (mp3)", + "fetch": "Fetch", + "back": "Back", + "audioOnlyLabel": "Audio only", + "video": "Video", + "itemCount": { + "one": "{{count}} item", + "other": "{{count}} items" + }, + "subfolderPlaceholder": "Subfolder (optional) — leave blank for this folder", + "downloadAll": "Download all", + "couldNotFetch": "Could not fetch", + "downloading": "Downloading…", + "saving": "Saving…", + "saved": "Saved", + "retry": "Retry", + "download": "Download", + "couldNotFetchInfo": "Could not fetch info", + "timedOut": "Timed out", + "downloadFailed": "Download failed", + "couldNotStart": "Could not start the download" } } diff --git a/src/workspaces/officerdev/src/locales/pt-PT.json b/src/workspaces/officerdev/src/locales/pt-PT.json index 87f61239..6d3df556 100644 --- a/src/workspaces/officerdev/src/locales/pt-PT.json +++ b/src/workspaces/officerdev/src/locales/pt-PT.json @@ -575,5 +575,32 @@ "placeholder": "https://github.com/owner/repo.git", "submit": "Clonar" } + }, + "videoDownload": { + "title": "Descarregar vídeo", + "descriptionInput": "Cole um URL de vídeo ou de lista de reprodução — os detalhes são obtidos antes de descarregar.", + "descriptionPreview": "Reveja e descarregue.", + "urlPlaceholder": "https://www.youtube.com/watch?v=… ou …/playlist?list=…", + "audioOnly": "Extrair apenas o áudio (mp3)", + "fetch": "Obter", + "back": "Voltar", + "audioOnlyLabel": "Apenas áudio", + "video": "Vídeo", + "itemCount": { + "one": "{{count}} item", + "other": "{{count}} itens" + }, + "subfolderPlaceholder": "Subpasta (opcional) — deixe em branco para esta pasta", + "downloadAll": "Descarregar tudo", + "couldNotFetch": "Não foi possível obter", + "downloading": "A descarregar…", + "saving": "A guardar…", + "saved": "Guardado", + "retry": "Tentar novamente", + "download": "Descarregar", + "couldNotFetchInfo": "Não foi possível obter informação", + "timedOut": "Tempo esgotado", + "downloadFailed": "Falha ao descarregar", + "couldNotStart": "Não foi possível iniciar o descarregamento" } }