import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"; import { DeviceState, LuxsinAPI, MOCK_DEVICE_STATE, MOCK_PEQ_STATE, PeqApplyPayload, PeqChangePayload, PeqFilter, PeqState, } from "@/lib/luxsinApi"; import { getDeviceIpFromUrl, resolveInitialDeviceIp } from "@/lib/deviceIp"; interface DeviceContextType { ip: string; setIp: (ip: string) => void; ipMissing: boolean; isConnected: boolean; isConnecting: boolean; isDemoMode: boolean; setDemoMode: (demo: boolean) => void; deviceState: DeviceState | null; peqState: PeqState | null; lastUpdated: Date | null; error: string | null; connect: (forceDemo?: boolean, connectIp?: string) => Promise; disconnect: () => void; refresh: () => Promise; api: LuxsinAPI | null; updateSetting: (params: Record) => Promise; updatePeq: (filters: PeqFilter[]) => Promise; upgradePeqChange: (payload: PeqChangePayload) => Promise; upgradePeqApply: (payload: PeqApplyPayload) => Promise; /** Align global peqState with device syncPeq (e.g. after EQ catalog add/delete). */ syncPeqCatalog: (state: PeqState) => void; // Optimistic state updaters setVolume: (v: number) => void; setInput: (v: number) => void; setOutput: (v: number) => void; setBalance: (v: number) => void; } const DeviceContext = createContext(null); export function DeviceProvider({ children }: { children: React.ReactNode }) { const [ip, setIp] = useState(resolveInitialDeviceIp); const [ipMissing, setIpMissing] = useState(() => !resolveInitialDeviceIp().trim()); const [isConnected, setIsConnected] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [isDemoMode, setDemoMode] = useState(false); const isDemoModeRef = useRef(false); const [deviceState, setDeviceState] = useState(null); const [peqState, setPeqState] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); const [error, setError] = useState(null); const [api, setApi] = useState(null); const lastMsgCountRef = useRef(-1); const pollIntervalRef = useRef | null>(null); const autoConnectStartedRef = useRef(false); const setDeviceIp = useCallback((newIp: string) => { setIp(newIp.trim()); }, []); const fetchState = useCallback(async (apiInstance: LuxsinAPI) => { const state = await apiInstance.getDeviceState(); setDeviceState(state); setLastUpdated(new Date()); lastMsgCountRef.current = state.msgCount; }, []); const fetchPeqState = useCallback(async (apiInstance: LuxsinAPI) => { const peq = await apiInstance.getPeqState(); setPeqState(peq); }, []); const syncPeqCatalog = useCallback((state: PeqState) => { setPeqState(state); }, []); const connect = useCallback(async (forceDemo?: boolean, connectIp?: string) => { const useDemo = forceDemo !== undefined ? forceDemo : isDemoMode; const targetIp = (connectIp ?? ip).trim(); if (!useDemo && !targetIp) { setIpMissing(true); setError(null); setIsConnecting(false); return; } setIpMissing(false); // Demo mode — use mock data immediately, no network call if (useDemo) { setIsConnecting(true); setError(null); await new Promise(r => setTimeout(r, 400)); // brief loading feel setDeviceState({ ...MOCK_DEVICE_STATE }); setPeqState({ ...MOCK_PEQ_STATE }); setIsConnected(true); setLastUpdated(new Date()); setIsConnecting(false); return; } setIsConnecting(true); setError(null); try { const newApi = new LuxsinAPI(targetIp); // Fetch device info from http://{ip}/dev/info.cgi?action=syncData // The response is encoded with custom base64, needs decoding const state = await newApi.getDeviceState(); setDeviceState(state); setLastUpdated(new Date()); const peq = await newApi.getPeqState().catch(() => MOCK_PEQ_STATE); setPeqState(peq); setApi(newApi); setIsConnected(true); } catch (e) { setError(`Connection failed: ${e instanceof Error ? e.message : String(e)}`); setIsConnected(false); } finally { setIsConnecting(false); } }, [ip, isDemoMode]); const disconnect = useCallback(() => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } setIsConnected(false); setApi(null); setDeviceState(null); setPeqState(null); lastMsgCountRef.current = -1; }, []); const refresh = useCallback(async () => { if (isDemoMode) { setDeviceState({ ...MOCK_DEVICE_STATE }); setLastUpdated(new Date()); return; } // If api is null but connected, recreate it with current IP let apiInstance = api; if (!apiInstance && isConnected) { apiInstance = new LuxsinAPI(ip); setApi(apiInstance); } if (!apiInstance) return; try { await fetchState(apiInstance); await fetchPeqState(apiInstance).catch(() => undefined); } catch (e) { setError(`Refresh failed: ${e instanceof Error ? e.message : String(e)}`); } }, [api, isConnected, ip, isDemoMode, fetchState, fetchPeqState]); const updateSetting = useCallback(async (params: Record) => { if (isDemoMode) { // Optimistic update for demo mode setDeviceState(prev => prev ? { ...prev, ...params } : prev); return; } if (!api) return; await api.setSetting(params); // Optimistic update setDeviceState(prev => prev ? { ...prev, ...params } : prev); }, [api, isDemoMode]); const updatePeq = useCallback(async (filters: PeqFilter[]) => { if (isDemoMode) { setPeqState({ filters }); return; } if (!api) return; await api.setPeqFilters(filters); setPeqState({ filters }); }, [api, isDemoMode]); const applyPeqFiltersToState = useCallback((filters: PeqFilter[]) => { setPeqState((prev) => (prev ? { ...prev, filters } : prev)); }, []); const upgradePeqChange = useCallback(async (payload: PeqChangePayload) => { if (isDemoMode) { applyPeqFiltersToState(payload.peqChange.filters); return; } if (!api) return; await api.upgradePeqChange(payload); applyPeqFiltersToState(payload.peqChange.filters); }, [api, isDemoMode, applyPeqFiltersToState]); const upgradePeqApply = useCallback(async (payload: PeqApplyPayload) => { if (isDemoMode) { applyPeqFiltersToState(payload.peqApply.filters); return; } if (!api) return; await api.upgradePeqApply(payload); applyPeqFiltersToState(payload.peqApply.filters); }, [api, isDemoMode, applyPeqFiltersToState]); // Optimistic setters const setVolume = useCallback((v: number) => { setDeviceState(prev => prev ? { ...prev, volume: v } : prev); if (!isDemoMode && api) { api.setVolume(v).catch(console.error); } }, [api, isDemoMode]); const setInput = useCallback((v: number) => { setDeviceState(prev => prev ? { ...prev, input: v } : prev); if (!isDemoMode && api) api.setInput(v).catch(console.error); }, [api, isDemoMode]); const setOutput = useCallback((v: number) => { setDeviceState(prev => prev ? { ...prev, output: v } : prev); if (!isDemoMode && api) api.setOutput(v).catch(console.error); }, [api, isDemoMode]); const setBalance = useCallback((v: number) => { setDeviceState(prev => prev ? { ...prev, balance: v } : prev); if (!isDemoMode && api) api.setBalance(v).catch(console.error); }, [api, isDemoMode]); // 启动时根据 ?ip= 或已存 IP 自动连接(默认进入首页,无需连接页) useEffect(() => { if (autoConnectStartedRef.current) return; autoConnectStartedRef.current = true; const targetIp = getDeviceIpFromUrl()?.trim() ?? ""; if (!targetIp) { setIpMissing(true); return; } setIp(targetIp); setIpMissing(false); void connect(false, targetIp); }, [connect]); // Polling for changes useEffect(() => { if (!isConnected || isDemoMode || !api) return; const poll = async () => { try { const count = await api.getMsgCount(); if (count !== lastMsgCountRef.current) { await Promise.all([ fetchState(api), fetchPeqState(api).catch(() => undefined), ]); } } catch { // Silently fail polling } }; pollIntervalRef.current = setInterval(poll, 3000); return () => { if (pollIntervalRef.current) clearInterval(pollIntervalRef.current); }; }, [isConnected, isDemoMode, api, fetchState, fetchPeqState]); return ( {children} ); } export function useDevice() { const ctx = useContext(DeviceContext); if (!ctx) throw new Error("useDevice must be used within DeviceProvider"); return ctx; }