From 11821730ec28355263e5523cc6f9211e14773da6 Mon Sep 17 00:00:00 2001 From: yangy Date: Fri, 22 May 2026 17:24:50 +0800 Subject: [PATCH] Enhance ConnectionPlaceholder component to handle missing device IP with localized messages; update DeviceContext to manage IP state; improve CSS for touch actions in various components; update locale files for English and Chinese with new connection prompts. --- .../src/components/ConnectionPlaceholder.tsx | 48 +++++++++++++++---- client/src/components/DesktopAIShell.tsx | 2 +- client/src/contexts/DeviceContext.tsx | 28 +++++++---- client/src/index.css | 1 + client/src/lib/deviceIp.ts | 2 +- client/src/locales/data-en.json | 8 ++++ client/src/locales/data-zh-HK.json | 8 ++++ client/src/locales/data-zh.json | 8 ++++ client/src/pages/Home.tsx | 6 +-- 9 files changed, 89 insertions(+), 22 deletions(-) diff --git a/client/src/components/ConnectionPlaceholder.tsx b/client/src/components/ConnectionPlaceholder.tsx index 564cdf9..8675c75 100644 --- a/client/src/components/ConnectionPlaceholder.tsx +++ b/client/src/components/ConnectionPlaceholder.tsx @@ -1,31 +1,61 @@ +import { useMemo } from "react"; import { useDevice } from "@/contexts/DeviceContext"; import { Loader2, Wifi } from "lucide-react"; import BottomNav from "@/components/BottomNav"; +import localeZh from "@/locales/data-zh.json"; +import localeZhHK from "@/locales/data-zh-HK.json"; +import localeEn from "@/locales/data-en.json"; + +type ConnectLocale = (typeof localeZh)["connect"]; + +function pickConnectLocale(): ConnectLocale { + const params = new URLSearchParams(window.location.search); + const lang = params.get("lang"); + if (lang === "0") return localeEn.connect; + if (lang === "1") return localeZhHK.connect; + if (lang === "2") return localeZh.connect; + const nav = navigator.language.toLowerCase(); + if (nav.startsWith("zh-hk") || nav.startsWith("zh-tw")) return localeZhHK.connect; + if (nav.startsWith("zh")) return localeZh.connect; + return localeEn.connect; +} + +function fmt(template: string, vars: Record): string { + let s = template; + for (const [k, v] of Object.entries(vars)) { + s = s.split(`{${k}}`).join(v); + } + return s; +} /** 自动连接设备时的占位(替代原 ConnectScreen) */ export default function ConnectionPlaceholder() { - const { ip, isConnecting, error } = useDevice(); + const { ip, isConnecting, error, ipMissing } = useDevice(); + const text = useMemo(pickConnectLocale, []); return (
- {isConnecting ? ( + {ipMissing ? ( + <> +

{text.ipMissingTitle}

+

{text.ipMissingDesc}

+

?ip=10.0.0.119

+ + ) : isConnecting ? ( <> -

Connecting to {ip}…

+

{fmt(text.connecting, { ip })}

) : ( <> -

Connection failed

+

{text.failedTitle}

- {error ?? "Check the device IP and your network connection."} -

-

- Specify the device in the URL, e.g.{" "} - ?ip=10.0.0.119 + {error ?? text.failedDesc}

+

{text.failedHint}

)} diff --git a/client/src/components/DesktopAIShell.tsx b/client/src/components/DesktopAIShell.tsx index c961d7b..4aa7805 100644 --- a/client/src/components/DesktopAIShell.tsx +++ b/client/src/components/DesktopAIShell.tsx @@ -92,7 +92,7 @@ export default function DesktopAIShell({ children }: { children: ReactNode }) { "min-w-0", showSplit ? "h-full min-h-0 overflow-y-auto overflow-x-hidden" - : "min-h-dvh overflow-x-hidden", + : "min-h-dvh max-h-dvh overflow-x-hidden overflow-y-auto overscroll-y-contain [touch-action:pan-y]", )} style={ showSplit diff --git a/client/src/contexts/DeviceContext.tsx b/client/src/contexts/DeviceContext.tsx index 2ebf9b9..57f4fde 100644 --- a/client/src/contexts/DeviceContext.tsx +++ b/client/src/contexts/DeviceContext.tsx @@ -14,6 +14,7 @@ import { getDeviceIpFromUrl, resolveInitialDeviceIp } from "@/lib/deviceIp"; interface DeviceContextType { ip: string; setIp: (ip: string) => void; + ipMissing: boolean; isConnected: boolean; isConnecting: boolean; isDemoMode: boolean; @@ -41,6 +42,7 @@ 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); @@ -54,9 +56,8 @@ export function DeviceProvider({ children }: { children: React.ReactNode }) { const pollIntervalRef = useRef | null>(null); const autoConnectStartedRef = useRef(false); - const saveIp = useCallback((newIp: string) => { - setIp(newIp); - localStorage.setItem("luxsin_ip", newIp); + const setDeviceIp = useCallback((newIp: string) => { + setIp(newIp.trim()); }, []); const fetchState = useCallback(async (apiInstance: LuxsinAPI) => { @@ -69,6 +70,13 @@ export function DeviceProvider({ children }: { children: React.ReactNode }) { 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); @@ -209,11 +217,15 @@ export function DeviceProvider({ children }: { children: React.ReactNode }) { if (autoConnectStartedRef.current) return; autoConnectStartedRef.current = true; - const urlIp = getDeviceIpFromUrl(); - const targetIp = urlIp ?? ip; - if (urlIp) saveIp(urlIp); + const targetIp = getDeviceIpFromUrl()?.trim() ?? ""; + if (!targetIp) { + setIpMissing(true); + return; + } + setIp(targetIp); + setIpMissing(false); void connect(false, targetIp); - }, [connect, ip, saveIp]); + }, [connect]); // Polling for changes useEffect(() => { @@ -238,7 +250,7 @@ export function DeviceProvider({ children }: { children: React.ReactNode }) { return ( +
{/* ── Device render image ── */}
-
+
{/* Track fill - 根据按钮位置调整 */}
{/* ── iOS list rows ── */} -
+
}