import FirRoomDetails from "./FirRoomDetails"; import { ChevronDown } from "lucide-react"; import { toast } from "sonner"; import { firTranslate, firErrorNotice, firNoticeText, type FirNotice } from '@/lib/firLocale'; import { useEffect, useRef, useState } from 'react'; import { useDevice } from '@/contexts/DeviceContext'; import { decodeFirWav, FirError, firRoomName, applyFir, readFirBinary, readFirJson, validateFirBinary, type FirRoom } from '@/lib/fir'; const LIBRARY = (import.meta.env.VITE_FIR_API_BASE || 'https://cs-fir.luxsin.com.cn/api').replace(/\/$/, ''); export default function FirPanel() { const { ip } = useDevice(); return ; } function FirControls() { const { api, isConnected, isDemoMode, deviceState } = useDevice(); const [enabled, setEnabled] = useState(deviceState?.fir_enable === undefined ? null : deviceState.fir_enable === 1); const [rooms, setRooms] = useState([]); const [selected, setSelected] = useState(''); const [detailsOpen, setDetailsOpen] = useState(false); const [source, setSource] = useState<'library' | 'manual'>('library'); const [left, setLeft] = useState(null); const [right, setRight] = useState(null); const [busy, setBusy] = useState(false); const [loading, setLoading] = useState(false); const [status, setStatus] = useState(null); const [error, setError] = useState(null); const t = (code: string, params?: Record) => firTranslate(deviceState?.language, code, params); const lock = useRef(false); const mounted = useRef(true); useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []); useEffect(() => { if (deviceState?.fir_enable !== undefined && !lock.current) setEnabled(deviceState.fir_enable === 1); }, [deviceState?.fir_enable]); const connected = !!api && isConnected && !isDemoMode; const room = rooms.find(item => item.room_id === selected); async function loadRooms() { if (lock.current) return; setLoading(true); setError(null); try { const items = await readFirJson(`${LIBRARY}/rooms/defaults`); if (!Array.isArray(items)) throw new FirError('invalidRooms'); if (mounted.current) { setRooms(items); setSelected(value => items.some(r => r.room_id === value) ? value : items[0]?.room_id || ''); } } catch (e) { if (mounted.current) setError(firErrorNotice(e)); } finally { if (mounted.current) setLoading(false); } } useEffect(() => { void loadRooms(); }, []); async function toggle(value: boolean) { if (!connected || !api || lock.current) return; lock.current = true; setBusy(true); setError(null); setStatus(null); try { await api.setFirEnable(value); if (mounted.current) { setEnabled(value); setStatus(null); } } catch (e) { if (mounted.current) { setEnabled(null); setError(firErrorNotice(e)); } } finally { lock.current = false; if (mounted.current) setBusy(false); } } async function upload() { if (!connected || !api || lock.current) return; lock.current = true; setBusy(true); setError(null); setStatus({ code: 'validating' }); try { let l: ArrayBuffer, r: ArrayBuffer; if (source === 'library') { if (!room) throw new FirError('selectError'); if (room.sample_rate == null) throw new FirError('rateError'); const base = `${LIBRARY}/rooms/${encodeURIComponent(room.room_id)}/device`; [l, r] = await Promise.all([readFirBinary(`${base}/left`, room.sample_rate), readFirBinary(`${base}/right`, room.sample_rate)]); } else { if (!left || !right) throw new FirError('filesError'); // Check sizes before loading potentially large files. if (left.size > 1024 * 1024 || right.size > 1024 * 1024) throw new FirError('binarySize'); [l, r] = await Promise.all([left.arrayBuffer().then(decodeFirWav), right.arrayBuffer().then(decodeFirWav)]); } if (validateFirBinary(l) !== validateFirBinary(r)) throw new FirError('mismatch'); if (!mounted.current) return; await applyFir(api, l, r, message => { if (mounted.current) { setStatus({ code: message }); } }); if (mounted.current) { setStatus(null); toast.success(t('success', { count: l.byteLength / 4 }), { duration: 10000 }); } } catch (e) { if (mounted.current) { setStatus(null); setError(firErrorNotice(e)); } } finally { lock.current = false; if (mounted.current) setBusy(false); } } const control = 'w-full rounded-lg border border-white/20 bg-black p-2 text-sm text-white'; return {t('title')} void toggle(e.target.checked)} /> {(enabled === true || busy) && {t('source')} { setSource(e.target.value as 'library' | 'manual'); setStatus(null); }}> {t('library')}{t('manual')} {source === 'library' ? <> {t('room')} setDetailsOpen(true)} className="text-xs text-cyan-300 hover:underline disabled:opacity-40">{t('details')} { setSelected(e.target.value); setStatus(null); }}> {loading ? t('loading') : t('select')} {rooms.map(item => {firRoomName(item) || t('unnamed')})} > : <> {(['left', 'right'] as const).map(side => {t(side)} .wav { (side === 'left' ? setLeft : setRight)(e.target.files?.[0] || null); setStatus(null); }} /> {t('bytes', { count: (side === 'left' ? left : right)?.size ?? 0 })} )} >} void upload()}>{t('upload')} } {room && } {status && (enabled === true || busy) && {t(status.code, status.params)}} {error && (enabled === true || error.code !== 'libraryError') && {firNoticeText(deviceState?.language, error)}} ; }
{t(status.code, status.params)}
{firNoticeText(deviceState?.language, error)}