Files
camera-viewer/frontend/src/components/settings/CameraSettings.tsx

136 lines
5.1 KiB
TypeScript

import { useState } from 'react';
import { useConfigStore } from '@/stores/configStore';
import { discoverCameras } from '@/services/api';
import type { CameraConfig } from '@/types/config';
export function CameraSettings() {
const config = useConfigStore((s) => s.config);
const saveConfig = useConfigStore((s) => s.saveConfig);
const [cameras, setCameras] = useState<CameraConfig[]>(config?.cameras ?? []);
const [saving, setSaving] = useState(false);
const [discovering, setDiscovering] = useState(false);
if (!config) return null;
const handleToggle = (idx: number) => {
const updated = [...cameras];
updated[idx] = { ...updated[idx], enabled: !updated[idx].enabled };
setCameras(updated);
};
const handleNameChange = (idx: number, display_name: string) => {
const updated = [...cameras];
updated[idx] = { ...updated[idx], display_name };
setCameras(updated);
};
const handleMoveUp = (idx: number) => {
if (idx === 0) return;
const updated = [...cameras];
[updated[idx - 1], updated[idx]] = [updated[idx], updated[idx - 1]];
updated.forEach((c, i) => c.order = i);
setCameras(updated);
};
const handleMoveDown = (idx: number) => {
if (idx >= cameras.length - 1) return;
const updated = [...cameras];
[updated[idx], updated[idx + 1]] = [updated[idx + 1], updated[idx]];
updated.forEach((c, i) => c.order = i);
setCameras(updated);
};
const handleRemove = (idx: number) => {
setCameras(cameras.filter((_, i) => i !== idx));
};
const handleDiscover = async () => {
setDiscovering(true);
const streams = await discoverCameras(config.go2rtc.url);
const existing = new Set(cameras.map((c) => c.name));
const newCams = streams
.filter((s) => !existing.has(s))
.map((name, i) => ({
name,
display_name: name.replace(/_/g, ' '),
enabled: true,
order: cameras.length + i,
}));
if (newCams.length > 0) {
setCameras([...cameras, ...newCams]);
}
setDiscovering(false);
};
const handleSave = async () => {
setSaving(true);
await saveConfig({ ...config, cameras });
setSaving(false);
};
return (
<div className="max-w-2xl space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-white">{cameras.length} Cameras</h3>
<div className="flex gap-2">
<button
onClick={handleDiscover}
disabled={discovering}
className="px-3 py-1.5 bg-dark-tertiary border border-dark-border hover:border-accent text-sm text-gray-300 rounded-lg transition-colors"
>
{discovering ? 'Discovering...' : 'Auto-Discover'}
</button>
</div>
</div>
<div className="space-y-1">
{cameras.map((cam, idx) => (
<div
key={cam.name}
className={`flex items-center gap-3 p-2 rounded-lg ${cam.enabled ? 'bg-dark-secondary' : 'bg-dark-secondary/50 opacity-60'}`}
>
{/* Toggle */}
<button
onClick={() => handleToggle(idx)}
className={`w-8 h-5 rounded-full transition-colors flex items-center ${cam.enabled ? 'bg-accent justify-end' : 'bg-dark-hover justify-start'}`}
>
<div className="w-4 h-4 bg-white rounded-full mx-0.5" />
</button>
{/* Stream name */}
<span className="text-xs text-gray-500 font-mono w-32 truncate">{cam.name}</span>
{/* Display name */}
<input
className="flex-1 bg-dark-tertiary border border-dark-border rounded px-2 py-1 text-sm text-white focus:outline-none focus:border-accent"
value={cam.display_name}
onChange={(e) => handleNameChange(idx, e.target.value)}
/>
{/* Reorder */}
<button onClick={() => handleMoveUp(idx)} className="text-gray-500 hover:text-white p-1" title="Move up">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" /></svg>
</button>
<button onClick={() => handleMoveDown(idx)} className="text-gray-500 hover:text-white p-1" title="Move down">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
</button>
{/* Remove */}
<button onClick={() => handleRemove(idx)} className="text-gray-500 hover:text-status-error p-1" title="Remove">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
))}
</div>
<button
onClick={handleSave}
disabled={saving}
className="px-6 py-2 bg-accent hover:bg-accent/80 disabled:opacity-50 text-white text-sm font-medium rounded-lg transition-colors"
>
{saving ? 'Saving...' : 'Save Cameras'}
</button>
</div>
);
}