meatloaf-config/src/app/components/DevicesPage.tsx
2026-04-13 23:55:36 -04:00

304 lines
10 KiB
TypeScript

import { useState } from 'react';
import { Printer, HardDrive, Network, Box, ChevronRight, RefreshCw } from 'lucide-react';
import DeviceDetailOverlay from './DeviceDetailOverlay';
import { toast } from 'sonner';
interface Device {
id: string;
number: string;
type: 'printer' | 'drive' | 'network' | 'other' | 'meatloaf';
name?: string;
enabled: boolean | number;
url?: string;
mode?: number;
}
interface DevicesPageProps {
config: any;
setConfig: (config: any) => void;
}
export default function DevicesPage({ config, setConfig }: DevicesPageProps) {
// Host Settings update function
const updateSetting = (path: string[], value: any) => {
const newConfig = JSON.parse(JSON.stringify(config));
let current = newConfig;
for (let i = 0; i < path.length - 1; i++) {
current = current[path[i]];
}
current[path[path.length - 1]] = value;
setConfig(newConfig);
};
const [selectedDeviceIndex, setSelectedDeviceIndex] = useState<number | null>(null);
const [isScanning, setIsScanning] = useState(false);
const devices: Device[] = [];
// Printer devices
if (config.iec?.devices?.printer) {
Object.entries(config.iec.devices.printer).forEach(([num, device]: [string, any]) => {
devices.push({
id: `printer-${num}`,
number: num,
type: 'printer',
name: device.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,
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,
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,
url: device.url,
mode: device.mode
});
});
}
const getDeviceIcon = (type: Device['type']) => {
switch (type) {
case 'printer':
return <Printer className="w-5 h-5" />;
case 'drive':
return <HardDrive className="w-5 h-5" />;
case 'network':
return <Network className="w-5 h-5" />;
default:
return <Box className="w-5 h-5" />;
}
};
const handleDeviceClick = (index: number) => {
setSelectedDeviceIndex(index);
};
const handleCloseOverlay = () => {
setSelectedDeviceIndex(null);
};
const handleNavigate = (direction: 'prev' | 'next') => {
if (selectedDeviceIndex === null) return;
if (direction === 'prev' && selectedDeviceIndex > 0) {
setSelectedDeviceIndex(selectedDeviceIndex - 1);
} else if (direction === 'next' && selectedDeviceIndex < devices.length - 1) {
setSelectedDeviceIndex(selectedDeviceIndex + 1);
}
};
const rescanBus = async () => {
setIsScanning(true);
toast.loading('Scanning IEC bus...');
// Simulate bus scan
await new Promise(resolve => setTimeout(resolve, 2000));
setIsScanning(false);
toast.dismiss();
toast.success(`Found ${devices.length} devices on the bus`);
};
const toggleDeviceEnabled = (device: Device, e: React.MouseEvent) => {
e.stopPropagation();
const [type, num] = device.id.split('-');
const path = ['iec', 'devices', type, num, 'enabled'];
const newConfig = JSON.parse(JSON.stringify(config));
let current = newConfig;
for (let i = 0; i < path.length - 1; i++) {
current = current[path[i]];
}
current[path[path.length - 1]] = device.enabled ? 0 : 1;
setConfig(newConfig);
toast.success(`Device #${device.number} ${device.enabled ? 'disabled' : 'enabled'}`);
};
return (
<div className="p-4">
{/* Host Settings section moved from GeneralPage */}
<h2 className="text-sm text-neutral-500 mb-2">Host Settings</h2>
<div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200 mb-6">
<div className="p-4">
<label className="text-sm text-neutral-500 block mb-2">Model</label>
<select
value={config.host?.model?.split('|')[0] || 'c64'}
onChange={(e) => updateSetting(['host', 'model'], e.target.value)}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
>
<option value="c64">C64</option>
<option value="c64c">C64C</option>
<option value="c128">C128</option>
<option value="sx64">SX64</option>
<option value="plus4">Plus/4</option>
<option value="c16">C16</option>
<option value="cx16">CX16</option>
<option value="foenix">Foenix</option>
<option value="dtv">DTV</option>
<option value="pet">PET</option>
</select>
</div>
<div className="p-4">
<label className="text-sm text-neutral-500 block mb-2">Video</label>
<select
value={config.host?.video?.split('|')[0] || 'ntsc'}
onChange={(e) => updateSetting(['host', 'video'], e.target.value)}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
>
<option value="ntsc">NTSC</option>
<option value="pal">PAL</option>
</select>
</div>
<div className="p-4">
<label className="text-sm text-neutral-500 block mb-2">Kernal</label>
<select
value={config.host?.kernal?.split('|')[0] || 'stock'}
onChange={(e) => updateSetting(['host', 'kernal'], e.target.value)}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
>
<option value="stock">Stock</option>
<option value="jiffydos">JiffyDOS</option>
<option value="dolphindos">DolphinDOS</option>
<option value="speeddos">SpeedDOS</option>
</select>
</div>
<div className="p-4">
<label className="text-sm text-neutral-500 block mb-2">BASIC</label>
<select
value={config.host?.basic?.split('|')[0] || '2'}
onChange={(e) => updateSetting(['host', 'basic'], e.target.value)}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
>
<option value="2">BASIC 2</option>
<option value="3">BASIC 3</option>
<option value="7">BASIC 7</option>
<option value="10">BASIC 10</option>
</select>
</div>
</div>
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm text-neutral-500">All Devices</h2>
<button
onClick={rescanBus}
disabled={isScanning}
className="flex items-center gap-2 px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${isScanning ? 'animate-spin' : ''}`} />
Rescan Bus
</button>
</div>
<div className="space-y-2">
{devices.map((device, index) => (
<div
key={device.id}
className="w-full bg-white border border-neutral-200 rounded-lg p-4 flex items-center gap-3"
>
<div className={`${device.enabled ? 'text-blue-600' : 'text-neutral-400'}`}>
{getDeviceIcon(device.type)}
</div>
<button
onClick={() => handleDeviceClick(index)}
className="flex-1 min-w-0 text-left"
>
<div className="flex items-center gap-2">
<span className={device.enabled ? 'text-neutral-900' : 'text-neutral-400'}>
{device.name || `Device ${device.number}`}
</span>
<span className="text-xs text-neutral-500 px-2 py-0.5 bg-neutral-100 rounded">
#{device.number}
</span>
</div>
{device.url && (
<div className="text-sm text-neutral-500 truncate mt-0.5">{device.url}</div>
)}
</button>
<div className="flex items-center gap-3">
<button
onClick={(e) => toggleDeviceEnabled(device, e)}
className={`relative w-11 h-6 rounded-full transition-colors ${
device.enabled ? 'bg-blue-600' : 'bg-neutral-300'
}`}
>
<div
className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${
device.enabled ? 'translate-x-5' : 'translate-x-0.5'
}`}
/>
</button>
<button onClick={() => handleDeviceClick(index)}>
<ChevronRight className="w-4 h-4 text-neutral-400" />
</button>
</div>
</div>
))}
</div>
{selectedDeviceIndex !== null && (
<DeviceDetailOverlay
device={devices[selectedDeviceIndex]}
config={config}
setConfig={setConfig}
onClose={handleCloseOverlay}
onNavigate={handleNavigate}
hasPrev={selectedDeviceIndex > 0}
hasNext={selectedDeviceIndex < devices.length - 1}
/>
)}
</div>
);
}