From afd7156a3603e29948c592a7dfae9e6eb6cd5c74 Mon Sep 17 00:00:00 2001 From: Jaime Idolpx Date: Wed, 17 Jun 2026 01:23:15 -0400 Subject: [PATCH] feat(SearchCommoServe): refactor download handling to mount functionality with improved progress tracking --- src/app/components/SearchAssembly64.tsx | 1 - src/app/components/SearchCommoServe.tsx | 147 ++++++++++-------------- 2 files changed, 62 insertions(+), 86 deletions(-) diff --git a/src/app/components/SearchAssembly64.tsx b/src/app/components/SearchAssembly64.tsx index 5153188..d065343 100644 --- a/src/app/components/SearchAssembly64.tsx +++ b/src/app/components/SearchAssembly64.tsx @@ -696,7 +696,6 @@ export default function SearchAssembly64({ config, setConfig, onClose }: SearchA
{fname}
{humanFileSize(entry.size)}
- {isMounted && Mounted} ); })} diff --git a/src/app/components/SearchCommoServe.tsx b/src/app/components/SearchCommoServe.tsx index 3954b0d..cd05bf3 100644 --- a/src/app/components/SearchCommoServe.tsx +++ b/src/app/components/SearchCommoServe.tsx @@ -1,5 +1,5 @@ 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 { Search, Loader2, HardDrive, ChevronRight, ChevronDown, Trophy, Calendar, Users, RefreshCw, HelpCircle, X, SlidersHorizontal, MoreVertical, FolderOpen } from 'lucide-react'; import { toast } from 'sonner'; import { humanFileSize, basename, clearDirectory, joinPath, putFileContents, streamFetch, type FetchProgress } from '../webdav'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; @@ -9,7 +9,7 @@ import { EntryIcon } from './MediaEntry'; // ─── API ────────────────────────────────────────────────────────────────────── const LEET_BASE = 'https://commoserve.files.commodore.net/leet'; -const PAGE_SIZE = 50; +const PAGE_SIZE = 1000; const DOWNLOAD_DIR = '/sd/downloads/commoserve'; function leetFetch(path: string, query?: Record) { @@ -123,10 +123,6 @@ function entryFilename(e: ContentEntry): string { return basename(e.path) || e.path; } -function downloadUrl(item: ContentItem, entry: ContentEntry): string { - return `${LEET_BASE}/search/bin/${item.id}/${item.category}/${entry.id}`; -} - function RatingStars({ value, max = 10 }: { value?: number; max?: number }) { if (!value) return null; const pct = Math.round((value / max) * 5); @@ -178,8 +174,8 @@ 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 [isMounting, setIsMounting] = useState(false); + const [mountProgress, setMountProgress] = useState(null); const [showFilter, setShowFilter] = useState(() => _store.showFilter); const [filterText, setFilterText] = useState(() => _store.filterText); @@ -302,25 +298,27 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC } }; - // ── Download to SD ─────────────────────────────────────────────────────────── + // ── Mount ──────────────────────────────────────────────────────────────────── - const downloadToSd = async (item: ContentItem, entry: ContentEntry) => { - setDownloading(entry.id); - setDownloadProgress({ received: 0, total: 0, phase: 'fetching' }); + const mountOnDevice = async (deviceType: 'drive' | 'meatloaf', key: string) => { + if (!mountEntry) return; + const { item, entry } = mountEntry; + setIsMounting(true); + setMountProgress({ received: 0, total: 0, phase: 'fetching' }); try { await clearDirectory(DOWNLOAD_DIR); - const res = await fetch(downloadUrl(item, entry)); + 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 => setDownloadProgress({ ...p, phase: 'fetching' })); + const data = await streamFetch(res, p => setMountProgress({ ...p, phase: 'fetching' })); const mainFname = entryFilename(entry); const dest = joinPath(DOWNLOAD_DIR, mainFname); - setDownloadProgress(p => p && { ...p, phase: 'saving' }); + 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) { - setDownloadProgress(p => p && { ...p, phase: 'image' }); + setMountProgress(p => p && { ...p, phase: 'image' }); try { - const imgRes = await fetch(downloadUrl(item, imgEntry)); + const imgRes = await leetFetch(`/search/bin/${item.id}/${item.category}/${imgEntry.id}`); if (imgRes.ok) { const imgData = await imgRes.arrayBuffer(); const mainBase = mainFname.replace(/\.[^.]+$/, ''); @@ -329,33 +327,26 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC } } catch { /* non-fatal */ } } - toast.success(`Saved to ${dest}`); + const newConfig = JSON.parse(JSON.stringify(config)); + if (!newConfig.devices) newConfig.devices = {}; + if (!newConfig.devices.iec) newConfig.devices.iec = {}; + if (!newConfig.devices.iec[key]) newConfig.devices.iec[key] = { type: deviceType }; + const dev = newConfig.devices.iec[key]; + dev.url = dest; + delete dev.media_set; + if (!dev.enabled) dev.enabled = 1; + setConfig(newConfig); + setMountEntry(null); + setSelectedItem(null); + toast.success(`Downloaded and mounted "${mainFname}" on device #${key}`); } catch (e: any) { - toast.error(`Download failed: ${e?.message ?? e}`); + toast.error(`Mount failed: ${e?.message ?? e}`); } finally { - setDownloading(null); - setDownloadProgress(null); + setIsMounting(false); + setMountProgress(null); } }; - // ── Mount ──────────────────────────────────────────────────────────────────── - - const mountOnDevice = (deviceType: 'drive' | 'meatloaf', key: string) => { - if (!mountEntry) return; - const { item, entry } = mountEntry; - const newConfig = JSON.parse(JSON.stringify(config)); - if (!newConfig.devices) newConfig.devices = {}; - if (!newConfig.devices.iec) newConfig.devices.iec = {}; - if (!newConfig.devices.iec[key]) newConfig.devices.iec[key] = { type: deviceType }; - const dev = newConfig.devices.iec[key]; - dev.url = downloadUrl(item, entry); - delete dev.media_set; - if (!dev.enabled) dev.enabled = 1; - setConfig(newConfig); - setMountEntry(null); - toast.success(`Mounted "${entryFilename(entry)}" on ${deviceType} #${key}`); - }; - const mountedUrls = useMemo(() => { const s = new Set(); for (const d of Object.values(config?.devices?.iec ?? {})) { @@ -670,57 +661,19 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC
{entries.map(entry => { const fname = entryFilename(entry); - const isMounted = mountedUrls.has(downloadUrl(selectedItem!, entry)); + const isMounted = mountedUrls.has(joinPath(DOWNLOAD_DIR, entryFilename(entry))); return ( -
setMountEntry({ item: selectedItem!, entry })} + className={`w-full text-left px-4 py-3 rounded-lg border flex items-center gap-3 transition-colors ${isMounted ? 'border-blue-300 bg-blue-50' : 'border-neutral-200 hover:bg-neutral-50 hover:border-neutral-300'}`} >
{fname}
- {downloading === entry.id && downloadProgress ? ( - <> -
- {downloadProgress.phase === 'saving' - ? 'Saving to device…' - : downloadProgress.phase === 'image' - ? 'Saving cover image…' - : downloadProgress.total - ? `${humanFileSize(downloadProgress.received)} / ${humanFileSize(downloadProgress.total)}` - : humanFileSize(downloadProgress.received) || 'Downloading…'} -
- {downloadProgress.phase === 'fetching' && ( -
-
-
- )} - - ) : ( -
{humanFileSize(entry.size)}
- )} +
{humanFileSize(entry.size)}
- - -
+ ); })}
@@ -730,7 +683,7 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC {/* Mount device picker */} - !open && setMountEntry(null)}> + { if (!open && !isMounting) setMountEntry(null); }}> Mount on Virtual Drive @@ -738,7 +691,31 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC {mountEntry ? entryFilename(mountEntry.entry) : ''} -
+ {isMounting && mountProgress ? ( +
+ {mountProgress.phase === 'fetching' ? ( + <> +
+ {mountProgress.total + ? `${humanFileSize(mountProgress.received)} / ${humanFileSize(mountProgress.total)}` + : humanFileSize(mountProgress.received) || 'Downloading…'} +
+
+
+
+ + ) : ( +
+ + {mountProgress.phase === 'image' ? 'Saving cover image…' : 'Saving to device…'} +
+ )} +
+ ) : null} +
{(() => { const allDevices = Object.entries(config?.devices?.iec ?? {}); const drives = allDevices