Initial commit: 提交控制器版本2核心代码
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
DeviceState,
|
||||
LuxsinAPI,
|
||||
MOCK_DEVICE_STATE,
|
||||
MOCK_PEQ_STATE,
|
||||
PeqFilter,
|
||||
PeqState,
|
||||
} from "@/lib/luxsinApi";
|
||||
|
||||
interface DeviceContextType {
|
||||
ip: string;
|
||||
setIp: (ip: string) => void;
|
||||
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) => Promise<void>;
|
||||
disconnect: () => void;
|
||||
refresh: () => Promise<void>;
|
||||
api: LuxsinAPI | null;
|
||||
updateSetting: (params: Record<string, string | number>) => Promise<void>;
|
||||
updatePeq: (filters: PeqFilter[]) => Promise<void>;
|
||||
// Optimistic state updaters
|
||||
setVolume: (v: number) => void;
|
||||
setInput: (v: number) => void;
|
||||
setOutput: (v: number) => void;
|
||||
setBalance: (v: number) => void;
|
||||
}
|
||||
|
||||
const DeviceContext = createContext<DeviceContextType | null>(null);
|
||||
|
||||
export function DeviceProvider({ children }: { children: React.ReactNode }) {
|
||||
const [ip, setIp] = useState(() => localStorage.getItem("luxsin_ip") || "192.168.2.30");
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [isDemoMode, setDemoMode] = useState(false);
|
||||
const isDemoModeRef = useRef(false);
|
||||
const [deviceState, setDeviceState] = useState<DeviceState | null>(null);
|
||||
const [peqState, setPeqState] = useState<PeqState | null>(null);
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [api, setApi] = useState<LuxsinAPI | null>(null);
|
||||
const lastMsgCountRef = useRef<number>(-1);
|
||||
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const saveIp = useCallback((newIp: string) => {
|
||||
setIp(newIp);
|
||||
localStorage.setItem("luxsin_ip", newIp);
|
||||
}, []);
|
||||
|
||||
const fetchState = useCallback(async (apiInstance: LuxsinAPI) => {
|
||||
const state = await apiInstance.getDeviceState();
|
||||
setDeviceState(state);
|
||||
setLastUpdated(new Date());
|
||||
lastMsgCountRef.current = state.msgCount;
|
||||
}, []);
|
||||
|
||||
const connect = useCallback(async (forceDemo?: boolean) => {
|
||||
const useDemo = forceDemo !== undefined ? forceDemo : isDemoMode;
|
||||
// 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(ip);
|
||||
// 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) {
|
||||
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);
|
||||
} catch (e) {
|
||||
setError(`Refresh failed: ${e instanceof Error ? e.message : String(e)}`);
|
||||
}
|
||||
}, [api, isConnected, ip, isDemoMode, fetchState]);
|
||||
|
||||
const updateSetting = useCallback(async (params: Record<string, string | number>) => {
|
||||
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]);
|
||||
|
||||
// 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]);
|
||||
|
||||
// Polling for changes
|
||||
useEffect(() => {
|
||||
if (!isConnected || isDemoMode || !api) return;
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const count = await api.getMsgCount();
|
||||
if (count !== lastMsgCountRef.current) {
|
||||
await fetchState(api);
|
||||
}
|
||||
} catch {
|
||||
// Silently fail polling
|
||||
}
|
||||
};
|
||||
|
||||
pollIntervalRef.current = setInterval(poll, 3000);
|
||||
return () => {
|
||||
if (pollIntervalRef.current) clearInterval(pollIntervalRef.current);
|
||||
};
|
||||
}, [isConnected, isDemoMode, api, fetchState]);
|
||||
|
||||
return (
|
||||
<DeviceContext.Provider value={{
|
||||
ip, setIp: saveIp,
|
||||
isConnected, isConnecting, isDemoMode, setDemoMode,
|
||||
deviceState, peqState, lastUpdated, error,
|
||||
connect, disconnect, refresh, api,
|
||||
updateSetting, updatePeq,
|
||||
setVolume, setInput, setOutput, setBalance,
|
||||
}}>
|
||||
{children}
|
||||
</DeviceContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useDevice() {
|
||||
const ctx = useContext(DeviceContext);
|
||||
if (!ctx) throw new Error("useDevice must be used within DeviceProvider");
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
toggleTheme?: () => void;
|
||||
switchable: boolean;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
interface ThemeProviderProps {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
switchable?: boolean;
|
||||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "light",
|
||||
switchable = false,
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
if (switchable) {
|
||||
const stored = localStorage.getItem("theme");
|
||||
return (stored as Theme) || defaultTheme;
|
||||
}
|
||||
return defaultTheme;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (theme === "dark") {
|
||||
root.classList.add("dark");
|
||||
} else {
|
||||
root.classList.remove("dark");
|
||||
}
|
||||
|
||||
if (switchable) {
|
||||
localStorage.setItem("theme", theme);
|
||||
}
|
||||
}, [theme, switchable]);
|
||||
|
||||
const toggleTheme = switchable
|
||||
? () => {
|
||||
setTheme(prev => (prev === "light" ? "dark" : "light"));
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme, switchable }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error("useTheme must be used within ThemeProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user