diff --git a/src/app/components/DeviceDetailOverlay.tsx b/src/app/components/DeviceDetailOverlay.tsx
index 517504a..9e1430d 100644
--- a/src/app/components/DeviceDetailOverlay.tsx
+++ b/src/app/components/DeviceDetailOverlay.tsx
@@ -1,6 +1,8 @@
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 FileBrowser from './FileBrowser';
interface Device {
@@ -36,7 +38,6 @@ export default function DeviceDetailOverlay({
const [touchEnd, setTouchEnd] = useState(0);
const [showFileBrowser, setShowFileBrowser] = useState(false);
const [showCommandMenu, setShowCommandMenu] = useState(false);
- const [selectedMediaIndex, setSelectedMediaIndex] = useState(0);
const minSwipeDistance = 50;
@@ -144,13 +145,48 @@ export default function DeviceDetailOverlay({
};
};
- const mediaSet = detectMediaSet();
+ // Prefer an explicit .lst-derived mediaSet stored in config; fall back to pattern detection.
+ const mediaSetFiles: string[] | null = (() => {
+ if (Array.isArray(deviceData.mediaSet) && deviceData.mediaSet.length > 0) {
+ return deviceData.mediaSet as string[];
+ }
+ const detected = detectMediaSet();
+ return detected ? detected.files : null;
+ })();
- const switchMedia = (index: number) => {
- if (!mediaSet) return;
+ const switchMedia = (file: string) => {
const path = getDevicePath();
- updateDeviceSetting([...path, 'url'], mediaSet.files[index]);
- setSelectedMediaIndex(index);
+ updateDeviceSetting([...path, 'url'], file);
+ };
+
+ const handleFileSelect = async (selectedPath: string) => {
+ const devicePath = getDevicePath();
+ if (selectedPath.toLowerCase().endsWith('.lst')) {
+ try {
+ const text = await (await getFileContents(selectedPath)).text();
+ const dir = selectedPath.split('/').slice(0, -1).join('/') || '/';
+ const files = 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; }
+ const newConfig = JSON.parse(JSON.stringify(config));
+ let dev = newConfig;
+ for (const k of devicePath) dev = dev[k];
+ dev.url = files[0];
+ dev.mediaSet = files;
+ setConfig(newConfig);
+ } catch (e: any) {
+ toast.error(`Failed to read swap list: ${e?.message ?? e}`);
+ }
+ } else {
+ const newConfig = JSON.parse(JSON.stringify(config));
+ let dev = newConfig;
+ for (const k of devicePath) dev = dev[k];
+ dev.url = selectedPath;
+ delete dev.mediaSet;
+ setConfig(newConfig);
+ }
};
const sendCommand = (command: string) => {
@@ -322,20 +358,17 @@ export default function DeviceDetailOverlay({
- {mediaSet && (
+ {mediaSetFiles && (
Media Set
- {mediaSet.files.slice(0, 5).map((file, index) => {
- // Attempt to extract a title from the filename, fallback to filename
- // Example: /path/to/Game Disk.d64 or /path/to/disk1.d64
+ {mediaSetFiles.map((file: string, index: number) => {
const fileName = file.split('/').pop() || file;
- // If you have a title mapping, replace this logic
const title = fileName.replace(/\.[^.]+$/, '');
return (
switchMedia(index)}
+ onClick={() => switchMedia(file)}
className={`px-3 py-1.5 rounded-lg text-sm ${
deviceData.url === file
? 'bg-blue-600 text-white'
@@ -403,10 +436,7 @@ export default function DeviceDetailOverlay({
{showFileBrowser && (
{
- const devicePath = getDevicePath();
- updateDeviceSetting([...devicePath, 'url'], path);
- }}
+ onSelect={(path) => { void handleFileSelect(path); setShowFileBrowser(false); }}
onClose={() => setShowFileBrowser(false)}
/>
)}
diff --git a/src/app/components/FileManager.tsx b/src/app/components/FileManager.tsx
index 222f1bf..c922cdc 100644
--- a/src/app/components/FileManager.tsx
+++ b/src/app/components/FileManager.tsx
@@ -528,7 +528,7 @@ export default function FileManager({ initialPath = '/', config, setConfig, onBa
// ── Mount ────────────────────────────────────────────────────────────────
- const mountOnDevice = (deviceType: 'drive' | 'meatloaf', key: string) => {
+ const mountOnDevice = async (deviceType: 'drive' | 'meatloaf', key: string) => {
if (!mountEntry || !setConfig || !config) return;
const newConfig = JSON.parse(JSON.stringify(config));
if (!newConfig.iec) newConfig.iec = {};
@@ -536,16 +536,43 @@ export default function FileManager({ initialPath = '/', config, setConfig, onBa
if (!newConfig.iec.devices[deviceType]) newConfig.iec.devices[deviceType] = {};
if (!newConfig.iec.devices[deviceType][key]) newConfig.iec.devices[deviceType][key] = {};
const dev = newConfig.iec.devices[deviceType][key];
- dev.url = mountEntry.path;
+
+ if (mountEntry.name.toLowerCase().endsWith('.lst')) {
+ try {
+ const text = await (await getFileContents(mountEntry.path)).text();
+ const dir = splitPath(mountEntry.path).parent;
+ const files = 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(`${mountEntry.name}: swap list is empty`);
+ return;
+ }
+ dev.url = files[0];
+ dev.mediaSet = files;
+ } catch (e: any) {
+ toast.error(`Failed to read ${mountEntry.name}: ${e?.message ?? e}`);
+ return;
+ }
+ } else {
+ dev.url = mountEntry.path;
+ delete dev.mediaSet;
+ }
+
if (!dev.enabled) dev.enabled = 1;
setConfig(newConfig);
+ setMountEntry(null);
const deviceId = `${deviceType}-${key}`;
- toast.success(`Mounted "${mountEntry.name}" on ${deviceType} #${key}`, {
+ const isLst = mountEntry.name.toLowerCase().endsWith('.lst');
+ const label = isLst
+ ? `Loaded swap list "${mountEntry.name}" (${(dev.mediaSet as string[]).length} disks) on ${deviceType} #${key}`
+ : `Mounted "${mountEntry.name}" on ${deviceType} #${key}`;
+ toast.success(label, {
action: onNavigateToDevice
? { label: 'View Device', onClick: () => onNavigateToDevice(deviceId) }
: undefined,
});
- setMountEntry(null);
};
// ── Clipboard ────────────────────────────────────────────────────────────
@@ -1066,7 +1093,7 @@ export default function FileManager({ initialPath = '/', config, setConfig, onBa
{devices.map(dev => (
mountOnDevice(dev.type, dev.key)}
+ onClick={() => void mountOnDevice(dev.type, dev.key)}
className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-blue-50 hover:border-blue-300 inline-flex items-center gap-3"
>