feat(DeviceDetailOverlay, DevicesPage, MediaManager, SearchOverlay, StatusPage): refactor device handling for improved consistency and add support for new device types

This commit is contained in:
Jaime Idolpx 2026-06-09 18:43:07 -04:00
parent e0ac2549c6
commit ae28de37f5
6 changed files with 128 additions and 182 deletions

View File

@ -94,8 +94,7 @@ export default function DeviceDetailOverlay({
}; };
const getDevicePath = (): string[] => { const getDevicePath = (): string[] => {
const [type, num] = device.id.split('-'); return ['iec', 'devices', device.number];
return ['iec', 'devices', type, num];
}; };
const getDeviceData = () => { const getDeviceData = () => {

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Printer, HardDrive, Network, Box, ChevronRight, RefreshCw, FolderOpen } from 'lucide-react'; import { Printer, HardDrive, Network, Box, ChevronRight, RefreshCw, FolderOpen, Computer } from 'lucide-react';
import DeviceDetailOverlay from './DeviceDetailOverlay'; import DeviceDetailOverlay from './DeviceDetailOverlay';
import MediaBrowser from './MediaBrowser'; import MediaBrowser from './MediaBrowser';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -45,77 +45,24 @@ export default function DevicesPage({ config, setConfig, openDeviceId, onClearOp
const devices: Device[] = []; const devices: Device[] = [];
// Printer devices if (config.iec?.devices) {
if (config.iec?.devices?.printer) { Object.entries(config.iec.devices).forEach(([num, device]: [string, any]) => {
Object.entries(config.iec.devices.printer).forEach(([num, device]: [string, any]) => { const type = device.type as Device['type'];
const name = device.name || (
type === 'drive' ? `Drive ${num}` :
type === 'network' ? `Network ${num}` :
type === 'meatloaf' ? `Meatloaf ${num}` :
undefined
);
devices.push({ devices.push({
id: `printer-${num}`, id: `${type}-${num}`,
number: num, number: num,
type: 'printer', type,
name: device.name, name,
enabled: device.enabled
});
});
}
// Drive devices
if (config.iec?.devices?.drive) {
Object.entries(config.iec.devices.drive).forEach(([key, value]: [string, any]) => {
if (key !== 'vdrive' && key !== 'rom') {
devices.push({
id: `drive-${key}`,
number: key,
type: 'drive',
name: `Drive ${key}`,
enabled: value.enabled,
base_url: value.base_url,
url: value.url,
mode: value.mode
});
}
});
}
// Network devices
if (config.iec?.devices?.network) {
Object.entries(config.iec.devices.network).forEach(([num, device]: [string, any]) => {
devices.push({
id: `network-${num}`,
number: num,
type: 'network',
name: `Network ${num}`,
enabled: device.enabled,
base_url: device.base_url,
url: device.url
});
});
}
// Other devices
if (config.iec?.devices?.other) {
Object.entries(config.iec.devices.other).forEach(([num, device]: [string, any]) => {
devices.push({
id: `other-${num}`,
number: num,
type: 'other',
name: device.name,
enabled: device.enabled
});
});
}
// Meatloaf devices
if (config.iec?.devices?.meatloaf) {
Object.entries(config.iec.devices.meatloaf).forEach(([num, device]: [string, any]) => {
devices.push({
id: `meatloaf-${num}`,
number: num,
type: 'meatloaf',
name: `Meatloaf ${num}`,
enabled: device.enabled, enabled: device.enabled,
base_url: device.base_url, base_url: device.base_url,
url: device.url, url: device.url,
mode: device.mode mode: device.mode,
}); });
}); });
} }
@ -137,6 +84,8 @@ export default function DevicesPage({ config, setConfig, openDeviceId, onClearOp
return <HardDrive className="w-5 h-5" />; return <HardDrive className="w-5 h-5" />;
case 'network': case 'network':
return <Network className="w-5 h-5" />; return <Network className="w-5 h-5" />;
case 'meatloaf':
return <Computer className="w-5 h-5" />;
default: default:
return <Box className="w-5 h-5" />; return <Box className="w-5 h-5" />;
} }
@ -177,20 +126,9 @@ export default function DevicesPage({ config, setConfig, openDeviceId, onClearOp
const toggleDeviceEnabled = (device: Device, e: React.MouseEvent) => { const toggleDeviceEnabled = (device: Device, e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
const [type, num] = device.id.split('-');
const path = ['iec', 'devices', type, num, 'enabled'];
const newConfig = JSON.parse(JSON.stringify(config)); const newConfig = JSON.parse(JSON.stringify(config));
let current = newConfig; newConfig.iec.devices[device.number].enabled = device.enabled ? 0 : 1;
for (let i = 0; i < path.length - 1; i++) {
current = current[path[i]];
}
current[path[path.length - 1]] = device.enabled ? 0 : 1;
setConfig(newConfig); setConfig(newConfig);
toast.success(`Device #${device.number} ${device.enabled ? 'disabled' : 'enabled'}`); toast.success(`Device #${device.number} ${device.enabled ? 'disabled' : 'enabled'}`);
}; };

View File

@ -738,9 +738,8 @@ export default function MediaManager({ initialPath = '/', rootPath, title, confi
const newConfig = JSON.parse(JSON.stringify(config)); const newConfig = JSON.parse(JSON.stringify(config));
if (!newConfig.iec) newConfig.iec = {}; if (!newConfig.iec) newConfig.iec = {};
if (!newConfig.iec.devices) newConfig.iec.devices = {}; if (!newConfig.iec.devices) newConfig.iec.devices = {};
if (!newConfig.iec.devices[deviceType]) newConfig.iec.devices[deviceType] = {}; if (!newConfig.iec.devices[key]) newConfig.iec.devices[key] = { type: deviceType };
if (!newConfig.iec.devices[deviceType][key]) newConfig.iec.devices[deviceType][key] = {}; const dev = newConfig.iec.devices[key];
const dev = newConfig.iec.devices[deviceType][key];
if (mountEntry.name.toLowerCase().endsWith('.lst')) { if (mountEntry.name.toLowerCase().endsWith('.lst')) {
try { try {
@ -1305,10 +1304,12 @@ export default function MediaManager({ initialPath = '/', rootPath, title, confi
<DialogDescription className="truncate">{mountEntry?.name}</DialogDescription> <DialogDescription className="truncate">{mountEntry?.name}</DialogDescription>
</DialogHeader> </DialogHeader>
{(() => { {(() => {
const drives = Object.entries(config?.iec?.devices?.drive ?? {}) const allDevices = Object.entries(config?.iec?.devices ?? {});
.filter(([k]) => k !== 'vdrive' && k !== 'rom') const drives = allDevices
.filter(([, v]: [string, any]) => (v as any)?.type === 'drive')
.map(([k, v]: [string, any]) => ({ type: 'drive' as const, key: k, base_url: v?.base_url as string | undefined, url: v?.url as string | undefined, enabled: !!v?.enabled })); .map(([k, v]: [string, any]) => ({ type: 'drive' as const, key: k, base_url: v?.base_url as string | undefined, url: v?.url as string | undefined, enabled: !!v?.enabled }));
const meatloafs = Object.entries(config?.iec?.devices?.meatloaf ?? {}) const meatloafs = allDevices
.filter(([, v]: [string, any]) => (v as any)?.type === 'meatloaf')
.map(([k, v]: [string, any]) => ({ type: 'meatloaf' as const, key: k, base_url: v?.base_url as string | undefined, url: v?.url as string | undefined, enabled: !!v?.enabled })); .map(([k, v]: [string, any]) => ({ type: 'meatloaf' as const, key: k, base_url: v?.base_url as string | undefined, url: v?.url as string | undefined, enabled: !!v?.enabled }));
const devices = [...drives, ...meatloafs]; const devices = [...drives, ...meatloafs];
if (!devices.length) if (!devices.length)

View File

@ -121,8 +121,8 @@ export default function SearchOverlay({ config, setConfig, onClose }: SearchOver
const handleMount = (deviceNum: string, result: SearchResult) => { const handleMount = (deviceNum: string, result: SearchResult) => {
const newConfig = JSON.parse(JSON.stringify(config)); const newConfig = JSON.parse(JSON.stringify(config));
if (newConfig.iec?.devices?.drive?.[deviceNum]) { if (newConfig.iec?.devices?.[deviceNum]) {
newConfig.iec.devices.drive[deviceNum].url = result.path; newConfig.iec.devices[deviceNum].url = result.path;
setConfig(newConfig); setConfig(newConfig);
toast.success(`Mounted ${result.name} on Device #${deviceNum}`); toast.success(`Mounted ${result.name} on Device #${deviceNum}`);
setShowDeviceMenu(null); setShowDeviceMenu(null);
@ -131,10 +131,11 @@ export default function SearchOverlay({ config, setConfig, onClose }: SearchOver
const getAvailableDevices = () => { const getAvailableDevices = () => {
const devices: { number: string; name: string; url?: string }[] = []; const devices: { number: string; name: string; url?: string }[] = [];
if (config.iec?.devices?.drive) { if (config.iec?.devices) {
for (const [num, device] of Object.entries(config.iec.devices.drive)) { for (const [num, device] of Object.entries(config.iec.devices)) {
if (num !== 'vdrive' && num !== 'rom' && (device as any).enabled) { const d = device as any;
devices.push({ number: num, name: `Drive ${num}`, url: (device as any).url }); if (d.type === 'drive' && d.enabled) {
devices.push({ number: num, name: `Drive ${num}`, url: d.url });
} }
} }
} }

View File

@ -27,10 +27,11 @@ export default function StatusPage({ config, setConfig }: StatusPageProps) {
const [showDeviceOverlay, setShowDeviceOverlay] = useState(false); const [showDeviceOverlay, setShowDeviceOverlay] = useState(false);
// Find the first enabled device as the active device // Find the first enabled device as the active device
const findActiveDevice = () => { const findActiveDevice = () => {
if (config.iec?.devices?.drive) { if (config.iec?.devices) {
for (const [num, device] of Object.entries(config.iec.devices.drive)) { for (const [num, device] of Object.entries(config.iec.devices)) {
if (num !== 'vdrive' && num !== 'rom' && (device as any).enabled) { const d = device as any;
return { number: num, ...device as any, type: 'drive' }; if (d.type === 'drive' && d.enabled) {
return { number: num, ...d };
} }
} }
} }
@ -51,8 +52,8 @@ export default function StatusPage({ config, setConfig }: StatusPageProps) {
const switchActiveMedia = (file: string) => { const switchActiveMedia = (file: string) => {
const newConfig = JSON.parse(JSON.stringify(config)); const newConfig = JSON.parse(JSON.stringify(config));
if (newConfig.iec?.devices?.drive?.[activeDevice!.number]) { if (newConfig.iec?.devices?.[activeDevice!.number]) {
newConfig.iec.devices.drive[activeDevice!.number].url = file; newConfig.iec.devices[activeDevice!.number].url = file;
setConfig(newConfig); setConfig(newConfig);
} }
}; };

View File

@ -137,93 +137,99 @@
"bbs": 1 "bbs": 1
}, },
"devices": { "devices": {
"printer": { "4": {
"4": { "enabled": 1,
"enabled": 1, "type": "printer",
"type": "printer", "name": "MPS803"
"name": "MPS803"
}
}, },
"drive": { "8": {
"8": { "enabled": 1,
"enabled": 1, "type": "drive",
"url": "/sd", "url": "/sd",
"mode": 1 "mode": 1
},
"9": {
"enabled": 1,
"url": "/",
"mode": 1
},
"10": {
"enabled": 1,
"url": "/",
"mode": 1
},
"11": {
"enabled": 1,
"url": "/",
"mode": 1
},
"12": {
"enabled": 1,
"url": "/",
"mode": 1
},
"13": {
"enabled": 1,
"url": "/",
"mode": 1
},
"14": {
"enabled": 1,
"url": "/",
"mode": 1
},
"15": {
"enabled": 1,
"url": "/",
"mode": 1
}
}, },
"network": { "9": {
"16": { "enabled": 1,
"enabled": 1, "type": "drive",
"url": "/sd" "url": "/",
}, "mode": 1
"17": {
"enabled": 1,
"url": "/"
},
"18": {
"enabled": 1,
"url": "/"
},
"19": {
"enabled": 1,
"url": "/"
}
}, },
"other": { "10": {
"20": { "enabled": 1,
"enabled": 1, "type": "drive",
"name": "CP/m" "url": "/",
}, "mode": 1
"21": {
"enabled": 1,
"name": "S.A.M"
},
"29": {
"enabled": 1,
"name": "Clock"
}
}, },
"meatloaf": { "11": {
"30": { "enabled": 1,
"enabled": 1, "type": "drive",
"url": "/", "url": "/",
"mode": 1 "mode": 1
} },
"12": {
"enabled": 1,
"type": "drive",
"url": "/",
"mode": 1
},
"13": {
"enabled": 1,
"type": "drive",
"url": "/",
"mode": 1
},
"14": {
"enabled": 1,
"type": "drive",
"url": "/",
"mode": 1
},
"15": {
"enabled": 1,
"type": "drive",
"url": "/",
"mode": 1
},
"16": {
"enabled": 1,
"type": "network",
"url": "/sd"
},
"17": {
"enabled": 1,
"type": "network",
"url": "/"
},
"18": {
"enabled": 1,
"type": "network",
"url": "/"
},
"19": {
"enabled": 1,
"type": "network",
"url": "/"
},
"20": {
"enabled": 1,
"type": "other",
"name": "CP/m"
},
"21": {
"enabled": 1,
"type": "other",
"name": "S.A.M"
},
"29": {
"enabled": 1,
"type": "other",
"name": "Clock"
},
"30": {
"enabled": 1,
"type": "meatloaf",
"url": "/",
"mode": 1
} }
} }
}, },