From 30c96b8428b95e8825370762dbc862bfed9d7f58 Mon Sep 17 00:00:00 2001 From: Jaime Idolpx Date: Tue, 16 Jun 2026 16:35:10 -0400 Subject: [PATCH] feat(SearchComponents, SearchAssembly64, SearchCSDbNG, SearchCommoServe): add clearDirectory function and enhance download progress tracking --- src/app/components/SearchAssembly64.tsx | 25 ++++++++++++++++++++----- src/app/components/SearchCSDbNG.tsx | 21 +++++++++++++++++---- src/app/components/SearchCommoServe.tsx | 23 ++++++++++++++++++++--- src/app/webdav.ts | 9 +++++++++ 4 files changed, 66 insertions(+), 12 deletions(-) diff --git a/src/app/components/SearchAssembly64.tsx b/src/app/components/SearchAssembly64.tsx index 750bda4..5153188 100644 --- a/src/app/components/SearchAssembly64.tsx +++ b/src/app/components/SearchAssembly64.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { Search, Loader2, HardDrive, ChevronRight, ChevronDown, Trophy, Calendar, Users, RefreshCw, HelpCircle, X, SlidersHorizontal, MoreVertical, FolderOpen } from 'lucide-react'; import { toast } from 'sonner'; -import { humanFileSize, basename, joinPath, putFileContents, streamFetch, type FetchProgress } from '../webdav'; +import { humanFileSize, basename, clearDirectory, joinPath, putFileContents, streamFetch, type FetchProgress } from '../webdav'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; import { MarqueeText } from './ui/marquee-text'; import { EntryIcon } from './MediaEntry'; @@ -175,7 +175,7 @@ export default function SearchAssembly64({ config, setConfig, onClose }: SearchA const [actionItem, setActionItem] = useState(null); const [mountEntry, setMountEntry] = useState<{ item: ContentItem; entry: ContentEntry } | null>(null); const [isMounting, setIsMounting] = useState(false); - const [mountProgress, setMountProgress] = useState(null); + const [mountProgress, setMountProgress] = useState(null); const [showFilter, setShowFilter] = useState(() => _store.showFilter); const [filterText, setFilterText] = useState(() => _store.filterText); @@ -314,12 +314,27 @@ export default function SearchAssembly64({ config, setConfig, onClose }: SearchA setIsMounting(true); setMountProgress({ received: 0, total: 0, phase: 'fetching' }); try { + await clearDirectory(DOWNLOAD_DIR); const res = await leetFetch(`/search/bin/${item.id}/${item.category}/${entry.id}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await streamFetch(res, p => setMountProgress({ ...p, phase: 'fetching' })); - const dest = joinPath(DOWNLOAD_DIR, entryFilename(entry)); + const mainFname = entryFilename(entry); + const dest = joinPath(DOWNLOAD_DIR, mainFname); setMountProgress(p => p && { ...p, phase: 'saving' }); await putFileContents(dest, data); + const imgEntry = (entries ?? []).find(e => e.id !== entry.id && /\.(png|jpg|jpeg|gif|bmp)$/i.test(e.path)); + if (imgEntry) { + setMountProgress(p => p && { ...p, phase: 'image' }); + try { + const imgRes = await leetFetch(`/search/bin/${item.id}/${item.category}/${imgEntry.id}`); + if (imgRes.ok) { + const imgData = await imgRes.arrayBuffer(); + const mainBase = mainFname.replace(/\.[^.]+$/, ''); + const imgExt = imgEntry.path.split('.').pop()?.toLowerCase() ?? 'png'; + await putFileContents(joinPath(DOWNLOAD_DIR, `${mainBase}.${imgExt}`), imgData); + } + } catch { /* non-fatal */ } + } const newConfig = JSON.parse(JSON.stringify(config)); if (!newConfig.devices) newConfig.devices = {}; if (!newConfig.devices.iec) newConfig.devices.iec = {}; @@ -703,10 +718,10 @@ export default function SearchAssembly64({ config, setConfig, onClose }: SearchA
{isMounting && (
- {mountProgress?.phase === 'saving' ? ( + {mountProgress?.phase === 'saving' || mountProgress?.phase === 'image' ? ( <> -

Saving to device…

+

{mountProgress.phase === 'image' ? 'Saving cover image…' : 'Saving to device…'}

) : ( <> diff --git a/src/app/components/SearchCSDbNG.tsx b/src/app/components/SearchCSDbNG.tsx index 0bf41b8..d3b3539 100644 --- a/src/app/components/SearchCSDbNG.tsx +++ b/src/app/components/SearchCSDbNG.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { Search, Loader2, HardDrive, ChevronRight, X, SlidersHorizontal, MoreVertical, FolderOpen } from 'lucide-react'; import { toast } from 'sonner'; -import { basename, humanFileSize, joinPath, putFileContents, streamFetch, type FetchProgress } from '../webdav'; +import { basename, clearDirectory, humanFileSize, joinPath, putFileContents, streamFetch, type FetchProgress } from '../webdav'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; import { MarqueeText } from './ui/marquee-text'; import { EntryIcon } from './MediaEntry'; @@ -94,7 +94,7 @@ export default function SearchCSDbNG({ config, setConfig, onClose }: SearchCSDbN const [actionRow, setActionRow] = useState(null); const [mountEntry, setMountEntry] = useState<{ row: CsdbRow; link: CsdbDownloadLink } | null>(null); const [isMounting, setIsMounting] = useState(false); - const [mountProgress, setMountProgress] = useState(null); + const [mountProgress, setMountProgress] = useState(null); const [showFilter, setShowFilter] = useState(() => _store.showFilter); const [filterText, setFilterText] = useState(() => _store.filterText); @@ -163,6 +163,7 @@ export default function SearchCSDbNG({ config, setConfig, onClose }: SearchCSDbN setIsMounting(true); setMountProgress({ received: 0, total: 0, phase: 'fetching' }); try { + await clearDirectory(DOWNLOAD_DIR); const url = resolveLink(link.Link); const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); @@ -171,6 +172,18 @@ export default function SearchCSDbNG({ config, setConfig, onClose }: SearchCSDbN const dest = joinPath(DOWNLOAD_DIR, fname); setMountProgress(p => p && { ...p, phase: 'saving' }); await putFileContents(dest, data); + if (release?.ScreenShot?.[0]) { + setMountProgress(p => p && { ...p, phase: 'image' }); + try { + const imgRes = await fetch(release.ScreenShot[0]); + if (imgRes.ok) { + const imgData = await imgRes.arrayBuffer(); + const mainBase = fname.replace(/\.[^.]+$/, ''); + const imgExt = release.ScreenShot[0].split('.').pop()?.split('?')[0]?.toLowerCase() ?? 'png'; + await putFileContents(joinPath(DOWNLOAD_DIR, `${mainBase}.${imgExt}`), imgData); + } + } catch { /* non-fatal */ } + } const newConfig = JSON.parse(JSON.stringify(config)); if (!newConfig.devices) newConfig.devices = {}; if (!newConfig.devices.iec) newConfig.devices.iec = {}; @@ -497,10 +510,10 @@ export default function SearchCSDbNG({ config, setConfig, onClose }: SearchCSDbN
{isMounting && (
- {mountProgress?.phase === 'saving' ? ( + {mountProgress?.phase === 'saving' || mountProgress?.phase === 'image' ? ( <> -

Saving to device…

+

{mountProgress.phase === 'image' ? 'Saving cover image…' : 'Saving to device…'}

) : ( <> diff --git a/src/app/components/SearchCommoServe.tsx b/src/app/components/SearchCommoServe.tsx index 477c241..3954b0d 100644 --- a/src/app/components/SearchCommoServe.tsx +++ b/src/app/components/SearchCommoServe.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { Search, Loader2, HardDrive, Download, ChevronRight, ChevronDown, Trophy, Calendar, Users, RefreshCw, HelpCircle, X, SlidersHorizontal, MoreVertical, FolderOpen } from 'lucide-react'; import { toast } from 'sonner'; -import { humanFileSize, basename, joinPath, putFileContents, streamFetch, type FetchProgress } from '../webdav'; +import { humanFileSize, basename, clearDirectory, joinPath, putFileContents, streamFetch, type FetchProgress } from '../webdav'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; import { MarqueeText } from './ui/marquee-text'; import { EntryIcon } from './MediaEntry'; @@ -179,7 +179,7 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC const [actionItem, setActionItem] = useState(null); const [mountEntry, setMountEntry] = useState<{ item: ContentItem; entry: ContentEntry } | null>(null); const [downloading, setDownloading] = useState(null); - const [downloadProgress, setDownloadProgress] = useState(null); + const [downloadProgress, setDownloadProgress] = useState(null); const [showFilter, setShowFilter] = useState(() => _store.showFilter); const [filterText, setFilterText] = useState(() => _store.filterText); @@ -308,12 +308,27 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC setDownloading(entry.id); setDownloadProgress({ received: 0, total: 0, phase: 'fetching' }); try { + await clearDirectory(DOWNLOAD_DIR); const res = await fetch(downloadUrl(item, entry)); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await streamFetch(res, p => setDownloadProgress({ ...p, phase: 'fetching' })); - const dest = joinPath(DOWNLOAD_DIR, entryFilename(entry)); + const mainFname = entryFilename(entry); + const dest = joinPath(DOWNLOAD_DIR, mainFname); setDownloadProgress(p => p && { ...p, phase: 'saving' }); await putFileContents(dest, data); + const imgEntry = (entries ?? []).find(e => e.id !== entry.id && /\.(png|jpg|jpeg|gif|bmp)$/i.test(e.path)); + if (imgEntry) { + setDownloadProgress(p => p && { ...p, phase: 'image' }); + try { + const imgRes = await fetch(downloadUrl(item, imgEntry)); + if (imgRes.ok) { + const imgData = await imgRes.arrayBuffer(); + const mainBase = mainFname.replace(/\.[^.]+$/, ''); + const imgExt = imgEntry.path.split('.').pop()?.toLowerCase() ?? 'png'; + await putFileContents(joinPath(DOWNLOAD_DIR, `${mainBase}.${imgExt}`), imgData); + } + } catch { /* non-fatal */ } + } toast.success(`Saved to ${dest}`); } catch (e: any) { toast.error(`Download failed: ${e?.message ?? e}`); @@ -669,6 +684,8 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC
{downloadProgress.phase === 'saving' ? 'Saving to device…' + : downloadProgress.phase === 'image' + ? 'Saving cover image…' : downloadProgress.total ? `${humanFileSize(downloadProgress.received)} / ${humanFileSize(downloadProgress.total)}` : humanFileSize(downloadProgress.received) || 'Downloading…'} diff --git a/src/app/webdav.ts b/src/app/webdav.ts index d7c5569..ff20c7e 100644 --- a/src/app/webdav.ts +++ b/src/app/webdav.ts @@ -427,6 +427,15 @@ export function humanFileSize(bytes: number): string { export type FetchProgress = { received: number; total: number }; +export async function clearDirectory(path: string): Promise { + try { + const entries = await listDirectory(path); + await Promise.all(entries.filter(e => e.type === 'file').map(e => deletePath(e.path))); + } catch { + // Directory may not exist yet — ignore + } +} + export async function streamFetch( res: Response, onProgress: (p: FetchProgress) => void,