diff --git a/src/app/components/DeviceDetailOverlay.tsx b/src/app/components/DeviceDetailOverlay.tsx index 164cf1d..eac3d25 100644 --- a/src/app/components/DeviceDetailOverlay.tsx +++ b/src/app/components/DeviceDetailOverlay.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { X, ChevronLeft, ChevronRight, Printer, HardDrive, Network, Box, FolderOpen, MoreVertical, Play, Pause, SkipForward, SkipBack, RotateCcw } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import { toast } from 'sonner'; -import { getFileContents, joinPath } from '../webdav'; +import { fileExists, getFileContents, joinPath } from '../webdav'; import MediaBrowser from './MediaBrowser'; import MediaSet from './MediaSet'; @@ -122,38 +122,26 @@ export default function DeviceDetailOverlay({ } }; - // Detect if URL is part of a media set (e.g., disk1.d64, disk2.d64) - const detectMediaSet = () => { - if (!deviceData.url) return null; - - const match = deviceData.url.match(/^(.+?)(\d+)(\.[^.]+)$/); - if (!match) return null; - - const [, prefix, num, ext] = match; - const currentNum = parseInt(num); - - // Generate potential media set - const mediaSet = []; - for (let i = 1; i <= 10; i++) { - mediaSet.push(`${prefix}${i}${ext}`); - } - - return { - prefix, - extension: ext, - currentIndex: currentNum - 1, - files: mediaSet - }; - }; - // Prefer an explicit .lst-derived mediaSet stored in config; fall back to pattern detection. - const mediaSetFiles: string[] | null = (() => { + const [mediaSetFiles, setMediaSetFiles] = useState(null); + + useEffect(() => { if (Array.isArray(deviceData.media_set) && deviceData.media_set.length > 0) { - return deviceData.media_set as string[]; + setMediaSetFiles(deviceData.media_set as string[]); + return; } - const detected = detectMediaSet(); - return detected ? detected.files : null; - })(); + if (!deviceData.url) { setMediaSetFiles(null); return; } + const match = (deviceData.url as string).match(/^(.+?)(\d+)(\.[^.]+)$/); + if (!match) { setMediaSetFiles(null); return; } + const [, prefix, , ext] = match; + const candidates: string[] = []; + for (let i = 1; i <= 10; i++) candidates.push(`${prefix}${i}${ext}`); + let cancelled = false; + Promise.all(candidates.map(f => fileExists(f).catch(() => false))).then(flags => { + if (!cancelled) setMediaSetFiles(candidates.filter((_, i) => flags[i])); + }); + return () => { cancelled = true; }; + }, [deviceData.url, deviceData.media_set]); const switchMedia = (file: string) => { const path = getDevicePath(); @@ -166,11 +154,17 @@ export default function DeviceDetailOverlay({ try { const text = await (await getFileContents(selectedPath)).text(); const dir = selectedPath.split('/').slice(0, -1).join('/') || '/'; - const files = text.split('\n') + const candidates = text.split('\n') .map(l => l.trim()) .filter(l => l.length > 0 && !l.startsWith('#')) .map(l => l.startsWith('/') ? l : joinPath(dir, l)); - if (files.length === 0) { toast.error('Swap list is empty'); return; } + if (candidates.length === 0) { toast.error('Swap list is empty'); return; } + const existsArr = await Promise.all(candidates.map(f => fileExists(f).catch(() => false))); + const files = candidates.filter((_, i) => existsArr[i]); + if (files.length === 0) { toast.error('No files in swap list exist on device'); return; } + if (files.length < candidates.length) { + toast.warning(`${candidates.length - files.length} missing file(s) skipped from swap list`); + } const newConfig = JSON.parse(JSON.stringify(config)); let dev = newConfig; for (const k of devicePath) dev = dev[k]; diff --git a/src/app/components/MediaManager.tsx b/src/app/components/MediaManager.tsx index a174b52..057b4bb 100644 --- a/src/app/components/MediaManager.tsx +++ b/src/app/components/MediaManager.tsx @@ -760,14 +760,23 @@ export default function MediaManager({ initialPath = '/', rootPath, title, confi try { const text = await (await getFileContents(mountEntry.path)).text(); const dir = splitPath(mountEntry.path).parent; - const files = text.split('\n') + const candidates = text.split('\n') .map(l => l.trim()) .filter(l => l.length > 0 && !l.startsWith('#')) .map(l => l.startsWith('/') ? l : joinPath(dir, l)); - if (files.length === 0) { + if (candidates.length === 0) { toast.error(`${mountEntry.name}: swap list is empty`); return; } + const exists = await Promise.all(candidates.map(f => fileExists(f).catch(() => false))); + const files = candidates.filter((_, i) => exists[i]); + if (files.length === 0) { + toast.error(`${mountEntry.name}: no files in swap list exist on device`); + return; + } + if (files.length < candidates.length) { + toast.warning(`${candidates.length - files.length} missing file(s) skipped from swap list`); + } dev.url = files[0]; dev.media_set = files; } catch (e: any) {