feat(FileManager, DeviceDetailOverlay): enhance file handling with .lst support and improve device mounting logic
This commit is contained in:
parent
768c4c2336
commit
52d0e96961
|
|
@ -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({
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{mediaSet && (
|
||||
{mediaSetFiles && (
|
||||
<div className="mt-3">
|
||||
<label className="text-sm text-neutral-500 block mb-2">Media Set</label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{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 (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => 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 && (
|
||||
<FileBrowser
|
||||
currentPath={deviceData.url || '/'}
|
||||
onSelect={(path) => {
|
||||
const devicePath = getDevicePath();
|
||||
updateDeviceSetting([...devicePath, 'url'], path);
|
||||
}}
|
||||
onSelect={(path) => { void handleFileSelect(path); setShowFileBrowser(false); }}
|
||||
onClose={() => setShowFileBrowser(false)}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
||||
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 => (
|
||||
<button
|
||||
key={`${dev.type}-${dev.key}`}
|
||||
onClick={() => 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"
|
||||
>
|
||||
<HardDrive className={`w-5 h-5 flex-shrink-0 ${dev.enabled ? 'text-blue-500' : 'text-neutral-400'}`} />
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user