diff --git a/src/app/components/SearchAssembly64.tsx b/src/app/components/SearchAssembly64.tsx index 79f6d77..750bda4 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 } from '../webdav'; +import { humanFileSize, basename, 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'; @@ -10,7 +10,7 @@ import { EntryIcon } from './MediaEntry'; const LEET_BASE = 'https://hackerswithstyle.se/leet'; const PAGE_SIZE = 1000; -const DOWNLOAD_DIR = '/sd/downloads'; +const DOWNLOAD_DIR = '/sd/downloads/a64'; function leetFetch(path: string, query?: Record) { const url = new URL(LEET_BASE + path); @@ -175,6 +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 [showFilter, setShowFilter] = useState(() => _store.showFilter); const [filterText, setFilterText] = useState(() => _store.filterText); @@ -311,11 +312,13 @@ export default function SearchAssembly64({ config, setConfig, onClose }: SearchA if (!mountEntry) return; const { item, entry } = mountEntry; setIsMounting(true); + setMountProgress({ received: 0, total: 0, phase: 'fetching' }); try { const res = await leetFetch(`/search/bin/${item.id}/${item.category}/${entry.id}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); - const data = await res.arrayBuffer(); + const data = await streamFetch(res, p => setMountProgress({ ...p, phase: 'fetching' })); const dest = joinPath(DOWNLOAD_DIR, entryFilename(entry)); + setMountProgress(p => p && { ...p, phase: 'saving' }); await putFileContents(dest, data); const newConfig = JSON.parse(JSON.stringify(config)); if (!newConfig.devices) newConfig.devices = {}; @@ -333,6 +336,7 @@ export default function SearchAssembly64({ config, setConfig, onClose }: SearchA toast.error(`Mount failed: ${e?.message ?? e}`); } finally { setIsMounting(false); + setMountProgress(null); } }; @@ -698,9 +702,27 @@ export default function SearchAssembly64({ config, setConfig, onClose }: SearchA
{isMounting && ( -
- -

Downloading…

+
+ {mountProgress?.phase === 'saving' ? ( + <> + +

Saving to device…

+ + ) : ( + <> +

+ {mountProgress?.total + ? `${humanFileSize(mountProgress.received)} / ${humanFileSize(mountProgress.total)}` + : mountProgress?.received ? humanFileSize(mountProgress.received) : 'Downloading…'} +

+
+
+
+ + )}
)} {!isMounting && (() => { diff --git a/src/app/components/SearchCSDbNG.tsx b/src/app/components/SearchCSDbNG.tsx index 466a302..0bf41b8 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, joinPath, putFileContents } from '../webdav'; +import { basename, 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'; @@ -10,7 +10,7 @@ import { EntryIcon } from './MediaEntry'; const CSDB_API = 'https://api.idolpx.com/csdb'; const CSDB_DOWNLOAD = 'https://csdb.idolpx.com/data'; -const DOWNLOAD_DIR = '/sd/downloads'; +const DOWNLOAD_DIR = '/sd/downloads/csdbng'; function resolveLink(link: string): string { return CSDB_DOWNLOAD + link; @@ -94,6 +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 [showFilter, setShowFilter] = useState(() => _store.showFilter); const [filterText, setFilterText] = useState(() => _store.filterText); @@ -160,13 +161,15 @@ export default function SearchCSDbNG({ config, setConfig, onClose }: SearchCSDbN if (!mountEntry) return; const { row, link } = mountEntry; setIsMounting(true); + setMountProgress({ received: 0, total: 0, phase: 'fetching' }); try { const url = resolveLink(link.Link); const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); - const data = await res.arrayBuffer(); + const data = await streamFetch(res, p => setMountProgress({ ...p, phase: 'fetching' })); const fname = link.Filename || basename(link.Link) || row.name; const dest = joinPath(DOWNLOAD_DIR, fname); + setMountProgress(p => p && { ...p, phase: 'saving' }); await putFileContents(dest, data); const newConfig = JSON.parse(JSON.stringify(config)); if (!newConfig.devices) newConfig.devices = {}; @@ -184,6 +187,7 @@ export default function SearchCSDbNG({ config, setConfig, onClose }: SearchCSDbN toast.error(`Mount failed: ${e?.message ?? e}`); } finally { setIsMounting(false); + setMountProgress(null); } }; @@ -492,9 +496,27 @@ export default function SearchCSDbNG({ config, setConfig, onClose }: SearchCSDbN
{isMounting && ( -
- -

Downloading…

+
+ {mountProgress?.phase === 'saving' ? ( + <> + +

Saving to device…

+ + ) : ( + <> +

+ {mountProgress?.total + ? `${humanFileSize(mountProgress.received)} / ${humanFileSize(mountProgress.total)}` + : mountProgress?.received ? humanFileSize(mountProgress.received) : 'Downloading…'} +

+
+
+
+ + )}
)} {!isMounting && (() => { diff --git a/src/app/components/SearchCommoServe.tsx b/src/app/components/SearchCommoServe.tsx index 5b817b7..477c241 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 } from '../webdav'; +import { humanFileSize, basename, 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'; @@ -10,7 +10,7 @@ import { EntryIcon } from './MediaEntry'; const LEET_BASE = 'https://commoserve.files.commodore.net/leet'; const PAGE_SIZE = 50; -const DOWNLOAD_DIR = '/sd/downloads'; +const DOWNLOAD_DIR = '/sd/downloads/commoserve'; function leetFetch(path: string, query?: Record) { const url = new URL(LEET_BASE + path); @@ -179,6 +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 [showFilter, setShowFilter] = useState(() => _store.showFilter); const [filterText, setFilterText] = useState(() => _store.filterText); @@ -305,17 +306,20 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC const downloadToSd = async (item: ContentItem, entry: ContentEntry) => { setDownloading(entry.id); + setDownloadProgress({ received: 0, total: 0, phase: 'fetching' }); try { const res = await fetch(downloadUrl(item, entry)); if (!res.ok) throw new Error(`HTTP ${res.status}`); - const data = await res.arrayBuffer(); + const data = await streamFetch(res, p => setDownloadProgress({ ...p, phase: 'fetching' })); const dest = joinPath(DOWNLOAD_DIR, entryFilename(entry)); + setDownloadProgress(p => p && { ...p, phase: 'saving' }); await putFileContents(dest, data); toast.success(`Saved to ${dest}`); } catch (e: any) { toast.error(`Download failed: ${e?.message ?? e}`); } finally { setDownloading(null); + setDownloadProgress(null); } }; @@ -660,7 +664,27 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC
{fname}
-
{humanFileSize(entry.size)}
+ {downloading === entry.id && downloadProgress ? ( + <> +
+ {downloadProgress.phase === 'saving' + ? 'Saving to device…' + : downloadProgress.total + ? `${humanFileSize(downloadProgress.received)} / ${humanFileSize(downloadProgress.total)}` + : humanFileSize(downloadProgress.received) || 'Downloading…'} +
+ {downloadProgress.phase === 'fetching' && ( +
+
+
+ )} + + ) : ( +
{humanFileSize(entry.size)}
+ )}