import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { CheckCircle2, Circle, Lightbulb, Loader2, Play, RefreshCw, XCircle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { api, ApiError } from '../api/client'; import type { WLEDConfig, WLEDPreset, WLEDPresets } from '../api/client'; import { Button } from './Button'; import { Card, CardContent, CardHeader } from './Card'; import { useToast } from '../contexts/ToastContext'; import { registerSettingsSearch } from '../lib/settingsSearch'; registerSettingsSearch({ labelKey: 'wled.title', tab: 'wled', keywords: 'wled led light preset printer status', anchor: 'card-wled', }); const emptyPresets: WLEDPresets = { idle: null, prepare: null, printing: null, paused: null, finished: null, error: null, queue_waiting: null, filament_problem: null, hms_error: null, offline: null, }; const emptyConfig = (): WLEDConfig => ({ enabled: false, base_url: null, presets: { ...emptyPresets }, finished_timeout_seconds: 0, }); const mappingKeys: (keyof WLEDPresets)[] = [ 'idle', 'prepare', 'printing', 'paused', 'finished', 'error', 'queue_waiting', 'filament_problem', 'hms_error', 'offline', ]; type RequestKind = 'save' | 'presets' | 'connection' | 'presetTest'; type RequestTicket = { printerId: number; url: string; context: number; sequence: number }; export function WLEDSettings() { const { t } = useTranslation(); const { showToast } = useToast(); const queryClient = useQueryClient(); const { data: printers = [], isLoading } = useQuery({ queryKey: ['printers'], queryFn: api.getPrinters }); const [printerId, setPrinterId] = useState(null); const selectedPrinterId = useRef(printerId); selectedPrinterId.current = printerId; const contextGeneration = useRef(0); const currentUrl = useRef(''); const requestSequence = useRef>({ save: 0, presets: 0, connection: 0, presetTest: 0 }); const nextRequest = useCallback((kind: RequestKind, url: string): RequestTicket => ({ printerId: selectedPrinterId.current!, url, context: contextGeneration.current, sequence: ++requestSequence.current[kind], }), []); const isCurrentRequest = useCallback((kind: RequestKind, request: RequestTicket) => request.printerId === selectedPrinterId.current && request.url === currentUrl.current && request.context === contextGeneration.current && request.sequence === requestSequence.current[kind], []); useEffect(() => () => { contextGeneration.current += 1; }, []); const initializedPrinterId = useRef(null); const [draftsByPrinterId, setDraftsByPrinterId] = useState>({}); const printer = useMemo(() => printers.find((item) => item.id === printerId), [printers, printerId]); const config = printerId === null ? emptyConfig() : draftsByPrinterId[printerId] ?? printer?.wled_config ?? emptyConfig(); const currentConfig = useRef(config); currentConfig.current = config; if (printer && draftsByPrinterId[printer.id] === undefined) currentUrl.current = config.base_url?.trim() ?? ''; const setConfig = (change: (current: WLEDConfig) => WLEDConfig) => { if (printerId === null) return; setDraftsByPrinterId((drafts) => ({ ...drafts, [printerId]: change(drafts[printerId] ?? config), })); }; const [presets, setPresets] = useState([]); const [presetState, setPresetState] = useState<'idle' | 'online' | 'offline'>('idle'); const [connection, setConnection] = useState<{ state: 'idle' | 'online' | 'offline'; name?: string | null; version?: string | null }>({ state: 'idle' }); useEffect(() => { if (printerId === null ? !printers.length : printers.some((item) => item.id === printerId)) return; contextGeneration.current += 1; selectedPrinterId.current = printers[0]?.id ?? null; setPrinterId(selectedPrinterId.current); }, [printerId, printers]); useEffect(() => { if (!printer || initializedPrinterId.current === printer.id) return; initializedPrinterId.current = printer.id; currentUrl.current = config.base_url?.trim() ?? ''; setPresets([]); setPresetState('idle'); setConnection({ state: 'idle' }); }, [printer, config.base_url]); const saveMutation = useMutation({ mutationFn: (request: RequestTicket & { config: WLEDConfig }) => api.updatePrinter(request.printerId, { wled_config: request.config }), onSuccess: (updated, request) => { const isCurrent = isCurrentRequest('save', request); const draftUnchanged = currentConfig.current === request.config; if (request.sequence === requestSequence.current.save) { queryClient.setQueryData(['printers'], (old = []) => old.map((item) => (item.id === request.printerId ? updated : item)), ); setDraftsByPrinterId((drafts) => { if (drafts[request.printerId] !== request.config) return drafts; const next = { ...drafts }; delete next[request.printerId]; return next; }); } if (isCurrent && draftUnchanged) showToast(t('wled.saved'), 'success'); }, onError: (error, request) => { if (!isCurrentRequest('save', request) || currentConfig.current !== request.config) return; showToast(error instanceof ApiError && error.status === 422 ? error.message : t('wled.saveError'), 'error'); }, }); const presetsMutation = useMutation({ mutationFn: (request: RequestTicket) => api.getWledPresets(request.printerId, request.url), onSuccess: (loaded, request) => { if (!isCurrentRequest('presets', request)) return; setPresets(loaded); setPresetState('online'); }, onError: (error, request) => { if (!isCurrentRequest('presets', request)) return; if (error instanceof ApiError && error.status === 422) { showToast(error.message, 'error'); return; } setPresetState('offline'); showToast(t('wled.offlineHint'), 'error'); }, }); const loadPresets = presetsMutation.mutate; useEffect(() => { const savedConfig = printer?.wled_config; if (savedConfig?.enabled && savedConfig.base_url && savedConfig.base_url === currentUrl.current) { loadPresets(nextRequest('presets', savedConfig.base_url)); } }, [printer, loadPresets, nextRequest]); const connectionMutation = useMutation({ mutationFn: (request: RequestTicket) => api.testWledConnection(request.printerId, request.url), onSuccess: (info, request) => { if (isCurrentRequest('connection', request)) setConnection({ state: 'online', ...info }); }, onError: (error, request) => { if (!isCurrentRequest('connection', request)) return; if (error instanceof ApiError && error.status === 422) showToast(error.message, 'error'); else setConnection({ state: 'offline' }); }, }); const presetTestMutation = useMutation({ mutationFn: (request: RequestTicket & { presetId: number }) => api.testWledPreset(request.printerId, request.url, request.presetId), onSuccess: (_result, request) => { if (isCurrentRequest('presetTest', request)) showToast(t('wled.presetTested'), 'success'); }, onError: (error, request) => { if (!isCurrentRequest('presetTest', request)) return; showToast(error instanceof ApiError && error.status === 422 ? error.message : t('wled.presetTestFailed'), 'error'); }, }); const setPreset = (key: keyof WLEDPresets, value: string) => { setConfig((current) => ({ ...current, presets: { ...current.presets, [key]: value ? Number(value) : null }, })); }; if (isLoading) return

{t('common.loading')}

; const configuredCount = mappingKeys.filter((key) => config.presets[key] !== null).length; return (

{t('wled.title')}

{t('wled.description')}

{!printers.length ? (

{t('wled.noPrinters')}

) : ( <>

{t('wled.connectionSection')}

{connection.state === 'idle' && <>{t('wled.notTested')}} {connection.state === 'online' && <>{t('wled.connected', { name: connection.name ? ` (${connection.name})` : '', version: connection.version ? ` ยท v${connection.version}` : '' })}} {connection.state === 'offline' && <>{t('wled.notReachable')}}
{presetState === 'online' && {t('wled.online', { count: presets.length })}} {presetState === 'offline' && {t('wled.offlineHint')}}

{t('wled.mappingSection')}

{t('wled.mappingSummary', { configured: configuredCount, disabled: mappingKeys.length - configuredCount })}

{t('wled.presetSegmentHint')}

{mappingKeys.map((key) => { const selected = config.presets[key]; const missing = selected !== null && !presets.some((preset) => preset.id === selected); return (
{t(`wled.status.${key}`)}
); })}
)}
); }