feat(ws): implement WebSocket context provider and update components to use it
This commit is contained in:
parent
fe2b677bc3
commit
91f6f5366d
|
|
@ -14,6 +14,7 @@ import RealityOverridePage from './components/RealityOverridePage';
|
|||
import RealityOverrideAdminPage from './components/RealityOverrideAdminPage';
|
||||
import logoSvg from '../imports/logo.svg';
|
||||
import { useSettings } from './settings';
|
||||
import { WsProvider } from './ws';
|
||||
|
||||
type Page = 'status' | 'devices' | 'iec' | 'network' | 'other' | 'general' | 'tools' | 'apps' | AppId;
|
||||
|
||||
|
|
@ -203,6 +204,7 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<WsProvider>
|
||||
<div className="size-full flex flex-col bg-neutral-50">
|
||||
<Toaster position="top-center" />
|
||||
<header className="bg-[#4d4d4d] px-0 py-0 flex-shrink-0" style={{ paddingTop: 'env(safe-area-inset-top)' }}>
|
||||
|
|
@ -342,6 +344,7 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
</WsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useWs } from '../ws';
|
||||
import {
|
||||
ChevronLeft, Loader2, Wifi, WifiOff, Send,
|
||||
ChevronLeft, Loader2, Radio, WifiOff, Send,
|
||||
Image as ImageIcon, Film, Music,
|
||||
Crop, RotateCcw, Minimize2,
|
||||
Scissors, FileOutput, LayoutGrid,
|
||||
|
|
@ -89,37 +90,17 @@ const GROUP_HEAD: Record<string, string> = {
|
|||
// ── Component ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function RealityOverrideAdminPage({ onBack }: { onBack: () => void }) {
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
|
||||
const [wsStatus, setWsStatus] = useState<'connecting' | 'connected' | 'disconnected'>('connecting');
|
||||
const { status: wsStatus, send: wsSend } = useWs();
|
||||
const [lastSent, setLastSent] = useState<string | null>(null);
|
||||
const [flashId, setFlashId] = useState(0);
|
||||
const [freeform, setFreeform] = useState('');
|
||||
|
||||
// ── WebSocket ───────────────────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let ws: WebSocket | null = null;
|
||||
|
||||
const connect = () => {
|
||||
if (cancelled) return;
|
||||
ws = new WebSocket(`ws://${window.location.hostname}/ws`);
|
||||
wsRef.current = ws;
|
||||
setWsStatus('connecting');
|
||||
ws.onopen = () => { if (!cancelled) setWsStatus('connected'); };
|
||||
ws.onclose = () => { if (!cancelled) { setWsStatus('disconnected'); setTimeout(connect, 3000); } };
|
||||
ws.onerror = () => ws?.close();
|
||||
};
|
||||
|
||||
connect();
|
||||
return () => { cancelled = true; ws?.close(); };
|
||||
}, []);
|
||||
|
||||
// ── Send command ────────────────────────────────────────────────────────────
|
||||
const send = (payload: string) => {
|
||||
if (wsRef.current?.readyState !== WebSocket.OPEN) return;
|
||||
wsRef.current.send(payload);
|
||||
if (wsStatus !== 'connected') return;
|
||||
wsSend(payload);
|
||||
setLastSent(payload);
|
||||
setFlashId(n => n + 1);
|
||||
clearTimeout(timerRef.current);
|
||||
|
|
@ -153,7 +134,7 @@ export default function RealityOverrideAdminPage({ onBack }: { onBack: () => voi
|
|||
</span>
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
{wsStatus === 'connecting' && <><Loader2 className="w-3.5 h-3.5 text-yellow-400 animate-spin" /><span className="text-yellow-400">Connecting…</span></>}
|
||||
{wsStatus === 'connected' && <><Wifi className="w-3.5 h-3.5 text-green-400" /><span className="text-green-400">Connected</span></>}
|
||||
{wsStatus === 'connected' && <><Radio className="w-3.5 h-3.5 text-green-400" /><span className="text-green-400">Connected</span></>}
|
||||
{wsStatus === 'disconnected' && <><WifiOff className="w-3.5 h-3.5 text-red-400" /><span className="text-red-400">Offline</span></>}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import { ChevronLeft, Loader2, Wifi, WifiOff } from 'lucide-react';
|
||||
import { ChevronLeft, Loader2, Radio, WifiOff } from 'lucide-react';
|
||||
import { useWs } from '../ws';
|
||||
import * as THREE from 'three';
|
||||
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
|
||||
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
|
||||
|
|
@ -97,13 +98,12 @@ export default function RealityOverridePage({ onBack }: { onBack: () => void })
|
|||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const starCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const starsRef = useRef<Star[]>([]);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const msgIdRef = useRef(0);
|
||||
const fadeTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
const pausedRef = useRef(localStorage.getItem('ro-bg') === 'off');
|
||||
const lastTapRef = useRef(0);
|
||||
|
||||
const [wsStatus, setWsStatus] = useState<'connecting' | 'connected' | 'disconnected'>('connecting');
|
||||
const { status: wsStatus, subscribe } = useWs();
|
||||
const [currentCmd, setCurrentCmd] = useState<Msg | null>(null);
|
||||
const [history, setHistory] = useState<Msg[]>([]);
|
||||
const [bgVisible, setBgVisible] = useState(() => localStorage.getItem('ro-bg') !== 'off');
|
||||
|
|
@ -289,33 +289,17 @@ export default function RealityOverridePage({ onBack }: { onBack: () => void })
|
|||
return () => cancelAnimationFrame(animId);
|
||||
}, []);
|
||||
|
||||
// ── WebSocket ───────────────────────────────────────────────────────────────
|
||||
// ── WebSocket (shared connection via context) ───────────────────────────────
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let ws: WebSocket | null = null;
|
||||
|
||||
const connect = () => {
|
||||
if (cancelled) return;
|
||||
ws = new WebSocket(`ws://${window.location.hostname}/ws`);
|
||||
wsRef.current = ws;
|
||||
setWsStatus('connecting');
|
||||
ws.onopen = () => { if (!cancelled) setWsStatus('connected'); };
|
||||
ws.onmessage = (e) => {
|
||||
if (cancelled) return;
|
||||
const id = msgIdRef.current++;
|
||||
const msg: Msg = { text: String(e.data), id };
|
||||
setCurrentCmd(msg);
|
||||
setHistory(prev => [...prev.slice(-9), msg]);
|
||||
clearTimeout(fadeTimer.current);
|
||||
fadeTimer.current = setTimeout(() => setCurrentCmd(null), 5000);
|
||||
};
|
||||
ws.onclose = () => { if (!cancelled) { setWsStatus('disconnected'); setTimeout(connect, 3000); } };
|
||||
ws.onerror = () => ws?.close();
|
||||
};
|
||||
|
||||
connect();
|
||||
return () => { cancelled = true; ws?.close(); };
|
||||
}, []);
|
||||
return subscribe((data) => {
|
||||
const id = msgIdRef.current++;
|
||||
const msg: Msg = { text: data, id };
|
||||
setCurrentCmd(msg);
|
||||
setHistory(prev => [...prev.slice(-9), msg]);
|
||||
clearTimeout(fadeTimer.current);
|
||||
fadeTimer.current = setTimeout(() => setCurrentCmd(null), 5000);
|
||||
});
|
||||
}, [subscribe]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -351,7 +335,7 @@ export default function RealityOverridePage({ onBack }: { onBack: () => void })
|
|||
{/* WS status */}
|
||||
<div className="absolute top-3 right-3 z-10 flex items-center gap-1.5 text-xs px-2 py-1 rounded bg-black/40 border border-white/10">
|
||||
{wsStatus === 'connecting' && <><Loader2 className="w-3.5 h-3.5 text-yellow-400 animate-spin" /><span className="text-yellow-400">Connecting…</span></>}
|
||||
{wsStatus === 'connected' && <><Wifi className="w-3.5 h-3.5 text-green-400" /><span className="text-green-400">Connected</span></>}
|
||||
{wsStatus === 'connected' && <><Radio className="w-3.5 h-3.5 text-green-400" /><span className="text-green-400">Connected</span></>}
|
||||
{wsStatus === 'disconnected' && <><WifiOff className="w-3.5 h-3.5 text-red-400" /><span className="text-red-400">Reconnecting…</span></>}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { HardDrive, Activity, Wifi, Signal, Clock, RefreshCw, FolderOpen, Map, Loader2 } from 'lucide-react';
|
||||
import { HardDrive, Activity, Wifi, Radio, Signal, Clock, RefreshCw, FolderOpen, Map, Loader2 } from 'lucide-react';
|
||||
import { useWs } from '../ws';
|
||||
import DeviceDetailOverlay from './DeviceDetailOverlay';
|
||||
import MediaSet from './MediaSet';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
|
|
@ -13,6 +14,8 @@ interface StatusPageProps {
|
|||
}
|
||||
|
||||
export default function StatusPage({ config, setConfig }: StatusPageProps) {
|
||||
const { status: wsStatus } = useWs();
|
||||
|
||||
// Mock memory stats
|
||||
const memory = {
|
||||
heap: { total: 4096, free: 1024 }, // in KB
|
||||
|
|
@ -444,6 +447,12 @@ export default function StatusPage({ config, setConfig }: StatusPageProps) {
|
|||
<div className="text-sm text-neutral-700">192.168.1.100</div>
|
||||
<div className="text-xs text-neutral-500 mt-1">MAC Address</div>
|
||||
<div className="text-sm text-neutral-700">AA:BB:CC:DD:EE:FF</div>
|
||||
<div className="text-xs text-neutral-500 mt-1">WebSocket</div>
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
{wsStatus === 'connecting' && <><Loader2 className="w-3 h-3 text-yellow-500 animate-spin" /><span className="text-yellow-600">Connecting</span></>}
|
||||
{wsStatus === 'connected' && <><Radio className="w-3 h-3 text-green-600" /><span>Connected</span></>}
|
||||
{wsStatus === 'disconnected' && <><Radio className="w-3 h-3 text-red-500" /><span className="text-red-600">Disconnected</span></>}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-neutral-500">Uptime</div>
|
||||
|
|
|
|||
57
src/app/ws.tsx
Normal file
57
src/app/ws.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export type WsStatus = 'connecting' | 'connected' | 'disconnected';
|
||||
|
||||
interface WsContextValue {
|
||||
status: WsStatus;
|
||||
send: (msg: string) => void;
|
||||
subscribe: (handler: (msg: string) => void) => () => void;
|
||||
}
|
||||
|
||||
const WsContext = createContext<WsContextValue>({
|
||||
status: 'disconnected',
|
||||
send: () => {},
|
||||
subscribe: () => () => {},
|
||||
});
|
||||
|
||||
export function WsProvider({ children }: { children: React.ReactNode }) {
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const listeners = useRef<Set<(msg: string) => void>>(new Set());
|
||||
const [status, setStatus] = useState<WsStatus>('connecting');
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let ws: WebSocket | null = null;
|
||||
|
||||
const connect = () => {
|
||||
if (cancelled) return;
|
||||
ws = new WebSocket(`ws://${window.location.hostname}/ws`);
|
||||
wsRef.current = ws;
|
||||
setStatus('connecting');
|
||||
ws.onopen = () => { if (!cancelled) setStatus('connected'); };
|
||||
ws.onmessage = (e) => { if (!cancelled) listeners.current.forEach(h => h(String(e.data))); };
|
||||
ws.onclose = () => { if (!cancelled) { setStatus('disconnected'); setTimeout(connect, 3000); } };
|
||||
ws.onerror = () => ws?.close();
|
||||
};
|
||||
|
||||
connect();
|
||||
return () => { cancelled = true; ws?.close(); };
|
||||
}, []);
|
||||
|
||||
const send = useCallback((msg: string) => {
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) wsRef.current.send(msg);
|
||||
}, []);
|
||||
|
||||
const subscribe = useCallback((handler: (msg: string) => void) => {
|
||||
listeners.current.add(handler);
|
||||
return () => { listeners.current.delete(handler); };
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WsContext.Provider value={{ status, send, subscribe }}>
|
||||
{children}
|
||||
</WsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useWs = () => useContext(WsContext);
|
||||
Loading…
Reference in New Issue
Block a user