feat(SearchComponents, SearchAssembly64, SearchCSDbNG, SearchCommoServe): add clearDirectory function and enhance download progress tracking
This commit is contained in:
parent
76962cc9bd
commit
30c96b8428
|
|
@ -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<ContentItem | null>(null);
|
||||
const [mountEntry, setMountEntry] = useState<{ item: ContentItem; entry: ContentEntry } | null>(null);
|
||||
const [isMounting, setIsMounting] = useState(false);
|
||||
const [mountProgress, setMountProgress] = useState<FetchProgress & { phase: 'fetching' | 'saving' } | null>(null);
|
||||
const [mountProgress, setMountProgress] = useState<FetchProgress & { phase: 'fetching' | 'saving' | 'image' } | null>(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
|
|||
<div className="overflow-y-auto flex-1 min-h-0">
|
||||
{isMounting && (
|
||||
<div className="flex flex-col items-center justify-center py-10 gap-4">
|
||||
{mountProgress?.phase === 'saving' ? (
|
||||
{mountProgress?.phase === 'saving' || mountProgress?.phase === 'image' ? (
|
||||
<>
|
||||
<Loader2 className="w-7 h-7 text-blue-500 animate-spin" />
|
||||
<p className="text-sm text-neutral-500">Saving to device…</p>
|
||||
<p className="text-sm text-neutral-500">{mountProgress.phase === 'image' ? 'Saving cover image…' : 'Saving to device…'}</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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<CsdbRow | null>(null);
|
||||
const [mountEntry, setMountEntry] = useState<{ row: CsdbRow; link: CsdbDownloadLink } | null>(null);
|
||||
const [isMounting, setIsMounting] = useState(false);
|
||||
const [mountProgress, setMountProgress] = useState<FetchProgress & { phase: 'fetching' | 'saving' } | null>(null);
|
||||
const [mountProgress, setMountProgress] = useState<FetchProgress & { phase: 'fetching' | 'saving' | 'image' } | null>(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
|
|||
<div className="overflow-y-auto flex-1 min-h-0">
|
||||
{isMounting && (
|
||||
<div className="flex flex-col items-center justify-center py-10 gap-4">
|
||||
{mountProgress?.phase === 'saving' ? (
|
||||
{mountProgress?.phase === 'saving' || mountProgress?.phase === 'image' ? (
|
||||
<>
|
||||
<Loader2 className="w-7 h-7 text-blue-500 animate-spin" />
|
||||
<p className="text-sm text-neutral-500">Saving to device…</p>
|
||||
<p className="text-sm text-neutral-500">{mountProgress.phase === 'image' ? 'Saving cover image…' : 'Saving to device…'}</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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<ContentItem | null>(null);
|
||||
const [mountEntry, setMountEntry] = useState<{ item: ContentItem; entry: ContentEntry } | null>(null);
|
||||
const [downloading, setDownloading] = useState<number | null>(null);
|
||||
const [downloadProgress, setDownloadProgress] = useState<FetchProgress & { phase: 'fetching' | 'saving' } | null>(null);
|
||||
const [downloadProgress, setDownloadProgress] = useState<FetchProgress & { phase: 'fetching' | 'saving' | 'image' } | null>(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
|
|||
<div className="text-xs text-neutral-500">
|
||||
{downloadProgress.phase === 'saving'
|
||||
? 'Saving to device…'
|
||||
: downloadProgress.phase === 'image'
|
||||
? 'Saving cover image…'
|
||||
: downloadProgress.total
|
||||
? `${humanFileSize(downloadProgress.received)} / ${humanFileSize(downloadProgress.total)}`
|
||||
: humanFileSize(downloadProgress.received) || 'Downloading…'}
|
||||
|
|
|
|||
|
|
@ -427,6 +427,15 @@ export function humanFileSize(bytes: number): string {
|
|||
|
||||
export type FetchProgress = { received: number; total: number };
|
||||
|
||||
export async function clearDirectory(path: string): Promise<void> {
|
||||
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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user