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 { useEffect, useState } from 'react';
|
||||||
import { X, ChevronLeft, ChevronRight, Printer, HardDrive, Network, Box, FolderOpen, MoreVertical, Play, Pause, SkipForward, SkipBack, RotateCcw } from 'lucide-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 { motion, AnimatePresence } from 'motion/react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { getFileContents, joinPath } from '../webdav';
|
||||||
import FileBrowser from './FileBrowser';
|
import FileBrowser from './FileBrowser';
|
||||||
|
|
||||||
interface Device {
|
interface Device {
|
||||||
|
|
@ -36,7 +38,6 @@ export default function DeviceDetailOverlay({
|
||||||
const [touchEnd, setTouchEnd] = useState(0);
|
const [touchEnd, setTouchEnd] = useState(0);
|
||||||
const [showFileBrowser, setShowFileBrowser] = useState(false);
|
const [showFileBrowser, setShowFileBrowser] = useState(false);
|
||||||
const [showCommandMenu, setShowCommandMenu] = useState(false);
|
const [showCommandMenu, setShowCommandMenu] = useState(false);
|
||||||
const [selectedMediaIndex, setSelectedMediaIndex] = useState(0);
|
|
||||||
|
|
||||||
const minSwipeDistance = 50;
|
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) => {
|
const switchMedia = (file: string) => {
|
||||||
if (!mediaSet) return;
|
|
||||||
const path = getDevicePath();
|
const path = getDevicePath();
|
||||||
updateDeviceSetting([...path, 'url'], mediaSet.files[index]);
|
updateDeviceSetting([...path, 'url'], file);
|
||||||
setSelectedMediaIndex(index);
|
};
|
||||||
|
|
||||||
|
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) => {
|
const sendCommand = (command: string) => {
|
||||||
|
|
@ -322,20 +358,17 @@ export default function DeviceDetailOverlay({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{mediaSet && (
|
{mediaSetFiles && (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<label className="text-sm text-neutral-500 block mb-2">Media Set</label>
|
<label className="text-sm text-neutral-500 block mb-2">Media Set</label>
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{mediaSet.files.slice(0, 5).map((file, index) => {
|
{mediaSetFiles.map((file: string, index: number) => {
|
||||||
// Attempt to extract a title from the filename, fallback to filename
|
|
||||||
// Example: /path/to/Game Disk.d64 or /path/to/disk1.d64
|
|
||||||
const fileName = file.split('/').pop() || file;
|
const fileName = file.split('/').pop() || file;
|
||||||
// If you have a title mapping, replace this logic
|
|
||||||
const title = fileName.replace(/\.[^.]+$/, '');
|
const title = fileName.replace(/\.[^.]+$/, '');
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => switchMedia(index)}
|
onClick={() => switchMedia(file)}
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm ${
|
className={`px-3 py-1.5 rounded-lg text-sm ${
|
||||||
deviceData.url === file
|
deviceData.url === file
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-blue-600 text-white'
|
||||||
|
|
@ -403,10 +436,7 @@ export default function DeviceDetailOverlay({
|
||||||
{showFileBrowser && (
|
{showFileBrowser && (
|
||||||
<FileBrowser
|
<FileBrowser
|
||||||
currentPath={deviceData.url || '/'}
|
currentPath={deviceData.url || '/'}
|
||||||
onSelect={(path) => {
|
onSelect={(path) => { void handleFileSelect(path); setShowFileBrowser(false); }}
|
||||||
const devicePath = getDevicePath();
|
|
||||||
updateDeviceSetting([...devicePath, 'url'], path);
|
|
||||||
}}
|
|
||||||
onClose={() => setShowFileBrowser(false)}
|
onClose={() => setShowFileBrowser(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -528,7 +528,7 @@ export default function FileManager({ initialPath = '/', config, setConfig, onBa
|
||||||
|
|
||||||
// ── Mount ────────────────────────────────────────────────────────────────
|
// ── Mount ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const mountOnDevice = (deviceType: 'drive' | 'meatloaf', key: string) => {
|
const mountOnDevice = async (deviceType: 'drive' | 'meatloaf', key: string) => {
|
||||||
if (!mountEntry || !setConfig || !config) return;
|
if (!mountEntry || !setConfig || !config) return;
|
||||||
const newConfig = JSON.parse(JSON.stringify(config));
|
const newConfig = JSON.parse(JSON.stringify(config));
|
||||||
if (!newConfig.iec) newConfig.iec = {};
|
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]) newConfig.iec.devices[deviceType] = {};
|
||||||
if (!newConfig.iec.devices[deviceType][key]) newConfig.iec.devices[deviceType][key] = {};
|
if (!newConfig.iec.devices[deviceType][key]) newConfig.iec.devices[deviceType][key] = {};
|
||||||
const dev = 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;
|
dev.url = mountEntry.path;
|
||||||
|
delete dev.mediaSet;
|
||||||
|
}
|
||||||
|
|
||||||
if (!dev.enabled) dev.enabled = 1;
|
if (!dev.enabled) dev.enabled = 1;
|
||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
|
setMountEntry(null);
|
||||||
const deviceId = `${deviceType}-${key}`;
|
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
|
action: onNavigateToDevice
|
||||||
? { label: 'View Device', onClick: () => onNavigateToDevice(deviceId) }
|
? { label: 'View Device', onClick: () => onNavigateToDevice(deviceId) }
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
setMountEntry(null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Clipboard ────────────────────────────────────────────────────────────
|
// ── Clipboard ────────────────────────────────────────────────────────────
|
||||||
|
|
@ -1066,7 +1093,7 @@ export default function FileManager({ initialPath = '/', config, setConfig, onBa
|
||||||
{devices.map(dev => (
|
{devices.map(dev => (
|
||||||
<button
|
<button
|
||||||
key={`${dev.type}-${dev.key}`}
|
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"
|
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'}`} />
|
<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