feat(DeviceDetailOverlay, IECPage): replace input fields with SettingsInput component for improved handling
This commit is contained in:
parent
243a134a9c
commit
f3b97276c5
|
|
@ -1,4 +1,5 @@
|
|||
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 { motion, AnimatePresence } from 'motion/react';
|
||||
import { toast } from 'sonner';
|
||||
|
|
@ -341,12 +342,12 @@ export default function DeviceDetailOverlay({
|
|||
|
||||
<div>
|
||||
<label className="text-sm text-neutral-500 block mb-2">Device Name</label>
|
||||
<input
|
||||
<SettingsInput
|
||||
type="text"
|
||||
value={deviceData.name || device.name || `Device ${device.number}`}
|
||||
onChange={(e) => {
|
||||
onCommit={(v) => {
|
||||
const path = getDevicePath();
|
||||
updateDeviceSetting([...path, 'name'], e.target.value);
|
||||
updateDeviceSetting([...path, 'name'], v);
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
/>
|
||||
|
|
@ -356,12 +357,12 @@ export default function DeviceDetailOverlay({
|
|||
<div>
|
||||
<label className="text-sm text-neutral-500 block mb-2">Base URL</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
<SettingsInput
|
||||
type="text"
|
||||
value={deviceData.base_url ?? ''}
|
||||
onChange={(e) => {
|
||||
onCommit={(v) => {
|
||||
const path = getDevicePath();
|
||||
updateDeviceSetting([...path, 'base_url'], e.target.value);
|
||||
updateDeviceSetting([...path, 'base_url'], v);
|
||||
}}
|
||||
className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
/>
|
||||
|
|
@ -388,11 +389,10 @@ export default function DeviceDetailOverlay({
|
|||
<div>
|
||||
<label className="text-sm text-neutral-500 block mb-2">URL</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
<SettingsInput
|
||||
type="text"
|
||||
value={deviceData.url ?? ''}
|
||||
onChange={(e) => {
|
||||
const newUrl = e.target.value;
|
||||
onCommit={(newUrl) => {
|
||||
const devicePath = getDevicePath();
|
||||
const newConfig = JSON.parse(JSON.stringify(config));
|
||||
let dev = newConfig;
|
||||
|
|
@ -440,12 +440,12 @@ export default function DeviceDetailOverlay({
|
|||
<div>
|
||||
<label className="text-sm text-neutral-500 block mb-2">Cache</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
<SettingsInput
|
||||
type="text"
|
||||
value={deviceData.cache ?? ''}
|
||||
onChange={(e) => {
|
||||
onCommit={(v) => {
|
||||
const path = getDevicePath();
|
||||
updateDeviceSetting([...path, 'cache'], e.target.value);
|
||||
updateDeviceSetting([...path, 'cache'], v);
|
||||
}}
|
||||
className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
/>
|
||||
|
|
@ -500,12 +500,12 @@ export default function DeviceDetailOverlay({
|
|||
{deviceData.baud !== undefined && (
|
||||
<div>
|
||||
<label className="text-sm text-neutral-500 block mb-2">Baud Rate</label>
|
||||
<input
|
||||
<SettingsInput
|
||||
type="number"
|
||||
value={deviceData.baud}
|
||||
onChange={(e) => {
|
||||
value={String(deviceData.baud ?? '')}
|
||||
onCommit={(v) => {
|
||||
const path = getDevicePath();
|
||||
updateDeviceSetting([...path, 'baud'], parseInt(e.target.value));
|
||||
updateDeviceSetting([...path, 'baud'], parseInt(v));
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useState } from 'react';
|
||||
import { Cable, Code2, Cpu, FolderOpen, Link, List, Zap } from 'lucide-react';
|
||||
import MediaBrowser from './MediaBrowser';
|
||||
import { SettingsInput } from './ui/settings-input';
|
||||
|
||||
interface IECPageProps {
|
||||
config: any;
|
||||
|
|
@ -92,10 +93,10 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
|
|||
<div className="p-4">
|
||||
<label className="text-sm text-neutral-500 block mb-2">Autoboot</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
<SettingsInput
|
||||
type="text"
|
||||
value={settings.autoboot || ''}
|
||||
onChange={(e) => updateSetting(['settings', 'autoboot'], e.target.value)}
|
||||
onCommit={(v) => updateSetting(['settings', 'autoboot'], v)}
|
||||
className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
/>
|
||||
<button
|
||||
|
|
@ -152,10 +153,10 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
|
|||
{key === 'default' ? 'Default' : key.toUpperCase()}
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
<SettingsInput
|
||||
type="text"
|
||||
value={settings.drive_roms?.[key] || ''}
|
||||
onChange={(e) => updateSetting(['settings', 'drive_roms', key], e.target.value)}
|
||||
onCommit={(v) => updateSetting(['settings', 'drive_roms', key], v)}
|
||||
className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
|
||||
/>
|
||||
<button
|
||||
|
|
@ -211,11 +212,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'}`} />
|
||||
</button>
|
||||
) : (
|
||||
<input
|
||||
<SettingsInput
|
||||
type="number"
|
||||
value={value}
|
||||
value={String(value)}
|
||||
disabled={isCompatMode}
|
||||
onChange={(e) => updateSetting(['settings', 'directory', key], parseInt(e.target.value))}
|
||||
onCommit={(v) => updateSetting(['settings', 'directory', key], parseInt(v))}
|
||||
className="w-24 px-3 py-1 border border-neutral-300 rounded-lg text-right disabled:cursor-not-allowed"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
27
src/app/components/ui/settings-input.tsx
Normal file
27
src/app/components/ui/settings-input.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface SettingsInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'onBlur' | 'value'> {
|
||||
value: string | number;
|
||||
onCommit: (value: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A controlled input that buffers keystrokes locally and only calls
|
||||
* `onCommit` when the field loses focus, preventing a settings save
|
||||
* on every keystroke.
|
||||
*/
|
||||
export function SettingsInput({ value, onCommit, ...props }: SettingsInputProps) {
|
||||
const [local, setLocal] = useState(String(value ?? ''));
|
||||
|
||||
// Sync if the committed value changes externally (e.g. device switch).
|
||||
useEffect(() => { setLocal(String(value ?? '')); }, [value]);
|
||||
|
||||
return (
|
||||
<input
|
||||
{...props}
|
||||
value={local}
|
||||
onChange={e => setLocal(e.target.value)}
|
||||
onBlur={() => onCommit(local)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user