Compare commits

..

No commits in common. "6c2267c6399204f4597c693f018541e414e54323" and "243a134a9cc0d2f02064217d3ad654403146b60e" have entirely different histories.

3 changed files with 62 additions and 105 deletions

View File

@ -1,5 +1,4 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { SettingsInput } from './ui/settings-input';
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 { toast } from 'sonner';
@ -342,16 +341,14 @@ export default function DeviceDetailOverlay({
<div> <div>
<label className="text-sm text-neutral-500 block mb-2">Device Name</label> <label className="text-sm text-neutral-500 block mb-2">Device Name</label>
<SettingsInput <input
type="text" type="text"
value={deviceData.name || device.name || `Device ${device.number}`} value={deviceData.name || device.name || `Device ${device.number}`}
onCommit={(v) => { onChange={(e) => {
const path = getDevicePath(); const path = getDevicePath();
updateDeviceSetting([...path, 'name'], v); updateDeviceSetting([...path, 'name'], e.target.value);
}} }}
onClear={() => updateDeviceSetting([...getDevicePath(), 'name'], '')} className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
className="px-3 py-2 border border-neutral-300 rounded-lg"
containerClassName="w-full"
/> />
</div> </div>
@ -359,16 +356,14 @@ export default function DeviceDetailOverlay({
<div> <div>
<label className="text-sm text-neutral-500 block mb-2">Base URL</label> <label className="text-sm text-neutral-500 block mb-2">Base URL</label>
<div className="flex gap-2"> <div className="flex gap-2">
<SettingsInput <input
type="text" type="text"
value={deviceData.base_url ?? ''} value={deviceData.base_url ?? ''}
onCommit={(v) => { onChange={(e) => {
const path = getDevicePath(); const path = getDevicePath();
updateDeviceSetting([...path, 'base_url'], v); updateDeviceSetting([...path, 'base_url'], e.target.value);
}} }}
onClear={() => updateDeviceSetting([...getDevicePath(), 'base_url'], '')} className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
containerClassName="flex-1"
className="px-3 py-2 border border-neutral-300 rounded-lg"
/> />
<button <button
onClick={() => setBrowsingField('base_url')} onClick={() => setBrowsingField('base_url')}
@ -377,16 +372,27 @@ export default function DeviceDetailOverlay({
> >
<FolderOpen className="w-5 h-5" /> <FolderOpen className="w-5 h-5" />
</button> </button>
<button
onClick={() => {
const path = getDevicePath();
updateDeviceSetting([...path, 'base_url'], '');
}}
className="px-3 py-2 border border-neutral-300 rounded-lg bg-neutral-50 hover:bg-red-50 hover:border-red-300 hover:text-red-500"
title="Clear Base URL"
>
<X className="w-5 h-5" />
</button>
</div> </div>
</div> </div>
<div> <div>
<label className="text-sm text-neutral-500 block mb-2">URL</label> <label className="text-sm text-neutral-500 block mb-2">URL</label>
<div className="flex gap-2"> <div className="flex gap-2">
<SettingsInput <input
type="text" type="text"
value={deviceData.url ?? ''} value={deviceData.url ?? ''}
onCommit={(newUrl) => { onChange={(e) => {
const newUrl = e.target.value;
const devicePath = getDevicePath(); const devicePath = getDevicePath();
const newConfig = JSON.parse(JSON.stringify(config)); const newConfig = JSON.parse(JSON.stringify(config));
let dev = newConfig; let dev = newConfig;
@ -395,7 +401,17 @@ export default function DeviceDetailOverlay({
dev.url = newUrl; dev.url = newUrl;
setConfig(newConfig); setConfig(newConfig);
}} }}
onClear={() => { className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
/>
<button
onClick={() => setBrowsingField('url')}
className="px-3 py-2 border border-neutral-300 rounded-lg bg-neutral-50 hover:bg-neutral-100"
title="Browse"
>
<FolderOpen className="w-5 h-5" />
</button>
<button
onClick={() => {
const devicePath = getDevicePath(); const devicePath = getDevicePath();
const newConfig = JSON.parse(JSON.stringify(config)); const newConfig = JSON.parse(JSON.stringify(config));
let dev = newConfig; let dev = newConfig;
@ -404,15 +420,10 @@ export default function DeviceDetailOverlay({
delete dev.media_set; delete dev.media_set;
setConfig(newConfig); setConfig(newConfig);
}} }}
containerClassName="flex-1" className="px-3 py-2 border border-neutral-300 rounded-lg bg-neutral-50 hover:bg-red-50 hover:border-red-300 hover:text-red-500"
className="px-3 py-2 border border-neutral-300 rounded-lg" title="Clear URL and media set"
/>
<button
onClick={() => setBrowsingField('url')}
className="px-3 py-2 border border-neutral-300 rounded-lg bg-neutral-50 hover:bg-neutral-100"
title="Browse"
> >
<FolderOpen className="w-5 h-5" /> <X className="w-5 h-5" />
</button> </button>
</div> </div>
@ -429,16 +440,14 @@ export default function DeviceDetailOverlay({
<div> <div>
<label className="text-sm text-neutral-500 block mb-2">Cache</label> <label className="text-sm text-neutral-500 block mb-2">Cache</label>
<div className="flex gap-2"> <div className="flex gap-2">
<SettingsInput <input
type="text" type="text"
value={deviceData.cache ?? ''} value={deviceData.cache ?? ''}
onCommit={(v) => { onChange={(e) => {
const path = getDevicePath(); const path = getDevicePath();
updateDeviceSetting([...path, 'cache'], v); updateDeviceSetting([...path, 'cache'], e.target.value);
}} }}
onClear={() => updateDeviceSetting([...getDevicePath(), 'cache'], '')} className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
containerClassName="flex-1"
className="px-3 py-2 border border-neutral-300 rounded-lg"
/> />
<button <button
onClick={() => setBrowsingField('cache')} onClick={() => setBrowsingField('cache')}
@ -447,6 +456,16 @@ export default function DeviceDetailOverlay({
> >
<FolderOpen className="w-5 h-5" /> <FolderOpen className="w-5 h-5" />
</button> </button>
<button
onClick={() => {
const path = getDevicePath();
updateDeviceSetting([...path, 'cache'], '');
}}
className="px-3 py-2 border border-neutral-300 rounded-lg bg-neutral-50 hover:bg-red-50 hover:border-red-300 hover:text-red-500"
title="Clear Cache"
>
<X className="w-5 h-5" />
</button>
</div> </div>
</div> </div>
)} )}
@ -481,12 +500,12 @@ export default function DeviceDetailOverlay({
{deviceData.baud !== undefined && ( {deviceData.baud !== undefined && (
<div> <div>
<label className="text-sm text-neutral-500 block mb-2">Baud Rate</label> <label className="text-sm text-neutral-500 block mb-2">Baud Rate</label>
<SettingsInput <input
type="number" type="number"
value={String(deviceData.baud ?? '')} value={deviceData.baud}
onCommit={(v) => { onChange={(e) => {
const path = getDevicePath(); const path = getDevicePath();
updateDeviceSetting([...path, 'baud'], parseInt(v)); updateDeviceSetting([...path, 'baud'], parseInt(e.target.value));
}} }}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg" className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
/> />

View File

@ -1,7 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { Cable, Code2, Cpu, FolderOpen, Link, List, Zap } from 'lucide-react'; import { Cable, Code2, Cpu, FolderOpen, Link, List, Zap } from 'lucide-react';
import MediaBrowser from './MediaBrowser'; import MediaBrowser from './MediaBrowser';
import { SettingsInput } from './ui/settings-input';
interface IECPageProps { interface IECPageProps {
config: any; config: any;
@ -93,13 +92,11 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
<div className="p-4"> <div className="p-4">
<label className="text-sm text-neutral-500 block mb-2">Autoboot</label> <label className="text-sm text-neutral-500 block mb-2">Autoboot</label>
<div className="flex gap-2"> <div className="flex gap-2">
<SettingsInput <input
type="text" type="text"
value={settings.autoboot || ''} value={settings.autoboot || ''}
onCommit={(v) => updateSetting(['settings', 'autoboot'], v)} onChange={(e) => updateSetting(['settings', 'autoboot'], e.target.value)}
onClear={() => updateSetting(['settings', 'autoboot'], '')} className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
containerClassName="flex-1"
className="px-3 py-2 border border-neutral-300 rounded-lg"
/> />
<button <button
onClick={() => setShowMediaBrowser(true)} onClick={() => setShowMediaBrowser(true)}
@ -155,13 +152,11 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
{key === 'default' ? 'Default' : key.toUpperCase()} {key === 'default' ? 'Default' : key.toUpperCase()}
</label> </label>
<div className="flex gap-2"> <div className="flex gap-2">
<SettingsInput <input
type="text" type="text"
value={settings.drive_roms?.[key] || ''} value={settings.drive_roms?.[key] || ''}
onCommit={(v) => updateSetting(['settings', 'drive_roms', key], v)} onChange={(e) => updateSetting(['settings', 'drive_roms', key], e.target.value)}
onClear={() => updateSetting(['settings', 'drive_roms', key], '')} className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
containerClassName="flex-1"
className="px-3 py-2 border border-neutral-300 rounded-lg"
/> />
<button <button
onClick={() => setDriveRomBrowsingKey(key)} onClick={() => setDriveRomBrowsingKey(key)}
@ -216,11 +211,11 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
<div className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${value ? 'translate-x-6' : 'translate-x-0.5'}`} /> <div className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${value ? 'translate-x-6' : 'translate-x-0.5'}`} />
</button> </button>
) : ( ) : (
<SettingsInput <input
type="number" type="number"
value={String(value)} value={value}
disabled={isCompatMode} disabled={isCompatMode}
onCommit={(v) => updateSetting(['settings', 'directory', key], parseInt(v))} onChange={(e) => updateSetting(['settings', 'directory', key], parseInt(e.target.value))}
className="w-24 px-3 py-1 border border-neutral-300 rounded-lg text-right disabled:cursor-not-allowed" className="w-24 px-3 py-1 border border-neutral-300 rounded-lg text-right disabled:cursor-not-allowed"
/> />
)} )}

View File

@ -1,57 +0,0 @@
import { useEffect, useState } from 'react';
import { X } from 'lucide-react';
interface SettingsInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'onBlur' | 'value'> {
value: string | number;
onCommit: (value: string) => void;
/** When provided, an inline × button appears and calls this on click. */
onClear?: () => void;
/** className for the outer wrapper div (e.g. "flex-1"). Only used when onClear is set. */
containerClassName?: string;
}
/**
* A controlled input that buffers keystrokes locally and only calls
* `onCommit` when the field loses focus, preventing a settings save
* on every keystroke.
*
* When `onClear` is provided the component wraps itself in a relative div
* and shows an inline × button. Move any flex-sizing classes (e.g. flex-1)
* to `containerClassName` so they apply to the wrapper instead of the input.
*/
export function SettingsInput({ value, onCommit, onClear, containerClassName, className, ...props }: SettingsInputProps) {
const [local, setLocal] = useState(String(value ?? ''));
// Sync if the committed value changes externally (e.g. device switch).
useEffect(() => { setLocal(String(value ?? '')); }, [value]);
const input = (
<input
{...props}
className={`${onClear ? 'w-full pr-7' : ''} ${className ?? ''}`}
value={local}
onChange={e => setLocal(e.target.value)}
onBlur={() => onCommit(local)}
/>
);
if (!onClear) return input;
return (
<div className={`relative ${containerClassName ?? ''}`}>
{input}
{local !== '' && (
<button
type="button"
// mousedown fires before blur; preventDefault keeps focus so onBlur doesn't commit first
onMouseDown={e => { e.preventDefault(); setLocal(''); onClear(); }}
className="absolute right-2 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600"
tabIndex={-1}
aria-label="Clear"
>
<X className="w-3.5 h-3.5" />
</button>
)}
</div>
);
}