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.
This commit is contained in:
@@ -1,31 +1,61 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
import { useDevice } from "@/contexts/DeviceContext";
|
import { useDevice } from "@/contexts/DeviceContext";
|
||||||
import { Loader2, Wifi } from "lucide-react";
|
import { Loader2, Wifi } from "lucide-react";
|
||||||
import BottomNav from "@/components/BottomNav";
|
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, string>): string {
|
||||||
|
let s = template;
|
||||||
|
for (const [k, v] of Object.entries(vars)) {
|
||||||
|
s = s.split(`{${k}}`).join(v);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
/** 自动连接设备时的占位(替代原 ConnectScreen) */
|
/** 自动连接设备时的占位(替代原 ConnectScreen) */
|
||||||
export default function ConnectionPlaceholder() {
|
export default function ConnectionPlaceholder() {
|
||||||
const { ip, isConnecting, error } = useDevice();
|
const { ip, isConnecting, error, ipMissing } = useDevice();
|
||||||
|
const text = useMemo(pickConnectLocale, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-black flex flex-col items-center justify-center px-6 pb-24">
|
<div className="min-h-screen bg-black flex flex-col items-center justify-center px-6 pb-24">
|
||||||
<div className="mb-6 flex size-[4.5rem] items-center justify-center rounded-[16px] border border-white/15">
|
<div className="mb-6 flex size-[4.5rem] items-center justify-center rounded-[16px] border border-white/15">
|
||||||
<Wifi size={28} className="text-[#00FFF6]/80" />
|
<Wifi size={28} className="text-[#00FFF6]/80" />
|
||||||
</div>
|
</div>
|
||||||
{isConnecting ? (
|
{ipMissing ? (
|
||||||
|
<>
|
||||||
|
<p className="text-[15px] text-white/80 mb-2">{text.ipMissingTitle}</p>
|
||||||
|
<p className="text-[13px] text-white/45 text-center max-w-xs">{text.ipMissingDesc}</p>
|
||||||
|
<p className="text-[13px] text-[#00FFF6]/80 mt-3 font-mono">?ip=10.0.0.119</p>
|
||||||
|
</>
|
||||||
|
) : isConnecting ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 size={28} className="animate-spin text-[#00FFF6] mb-3" />
|
<Loader2 size={28} className="animate-spin text-[#00FFF6] mb-3" />
|
||||||
<p className="text-[15px] text-white/70">Connecting to {ip}…</p>
|
<p className="text-[15px] text-white/70">{fmt(text.connecting, { ip })}</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="text-[15px] text-white/80 mb-2">Connection failed</p>
|
<p className="text-[15px] text-white/80 mb-2">{text.failedTitle}</p>
|
||||||
<p className="text-[13px] text-white/45 text-center max-w-xs">
|
<p className="text-[13px] text-white/45 text-center max-w-xs">
|
||||||
{error ?? "Check the device IP and your network connection."}
|
{error ?? text.failedDesc}
|
||||||
</p>
|
|
||||||
<p className="text-[12px] text-white/30 mt-3">
|
|
||||||
Specify the device in the URL, e.g.{" "}
|
|
||||||
<span className="text-white/50">?ip=10.0.0.119</span>
|
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-[12px] text-white/30 mt-3 text-center max-w-xs">{text.failedHint}</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<BottomNav />
|
<BottomNav />
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default function DesktopAIShell({ children }: { children: ReactNode }) {
|
|||||||
"min-w-0",
|
"min-w-0",
|
||||||
showSplit
|
showSplit
|
||||||
? "h-full min-h-0 overflow-y-auto overflow-x-hidden"
|
? "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={
|
style={
|
||||||
showSplit
|
showSplit
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { getDeviceIpFromUrl, resolveInitialDeviceIp } from "@/lib/deviceIp";
|
|||||||
interface DeviceContextType {
|
interface DeviceContextType {
|
||||||
ip: string;
|
ip: string;
|
||||||
setIp: (ip: string) => void;
|
setIp: (ip: string) => void;
|
||||||
|
ipMissing: boolean;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
isConnecting: boolean;
|
isConnecting: boolean;
|
||||||
isDemoMode: boolean;
|
isDemoMode: boolean;
|
||||||
@@ -41,6 +42,7 @@ const DeviceContext = createContext<DeviceContextType | null>(null);
|
|||||||
|
|
||||||
export function DeviceProvider({ children }: { children: React.ReactNode }) {
|
export function DeviceProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [ip, setIp] = useState(resolveInitialDeviceIp);
|
const [ip, setIp] = useState(resolveInitialDeviceIp);
|
||||||
|
const [ipMissing, setIpMissing] = useState(() => !resolveInitialDeviceIp().trim());
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
const [isDemoMode, setDemoMode] = useState(false);
|
const [isDemoMode, setDemoMode] = useState(false);
|
||||||
@@ -54,9 +56,8 @@ export function DeviceProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
const autoConnectStartedRef = useRef(false);
|
const autoConnectStartedRef = useRef(false);
|
||||||
|
|
||||||
const saveIp = useCallback((newIp: string) => {
|
const setDeviceIp = useCallback((newIp: string) => {
|
||||||
setIp(newIp);
|
setIp(newIp.trim());
|
||||||
localStorage.setItem("luxsin_ip", newIp);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchState = useCallback(async (apiInstance: LuxsinAPI) => {
|
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 connect = useCallback(async (forceDemo?: boolean, connectIp?: string) => {
|
||||||
const useDemo = forceDemo !== undefined ? forceDemo : isDemoMode;
|
const useDemo = forceDemo !== undefined ? forceDemo : isDemoMode;
|
||||||
const targetIp = (connectIp ?? ip).trim();
|
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
|
// Demo mode — use mock data immediately, no network call
|
||||||
if (useDemo) {
|
if (useDemo) {
|
||||||
setIsConnecting(true);
|
setIsConnecting(true);
|
||||||
@@ -209,11 +217,15 @@ export function DeviceProvider({ children }: { children: React.ReactNode }) {
|
|||||||
if (autoConnectStartedRef.current) return;
|
if (autoConnectStartedRef.current) return;
|
||||||
autoConnectStartedRef.current = true;
|
autoConnectStartedRef.current = true;
|
||||||
|
|
||||||
const urlIp = getDeviceIpFromUrl();
|
const targetIp = getDeviceIpFromUrl()?.trim() ?? "";
|
||||||
const targetIp = urlIp ?? ip;
|
if (!targetIp) {
|
||||||
if (urlIp) saveIp(urlIp);
|
setIpMissing(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIp(targetIp);
|
||||||
|
setIpMissing(false);
|
||||||
void connect(false, targetIp);
|
void connect(false, targetIp);
|
||||||
}, [connect, ip, saveIp]);
|
}, [connect]);
|
||||||
|
|
||||||
// Polling for changes
|
// Polling for changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -238,7 +250,7 @@ export function DeviceProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DeviceContext.Provider value={{
|
<DeviceContext.Provider value={{
|
||||||
ip, setIp: saveIp,
|
ip, setIp: setDeviceIp, ipMissing,
|
||||||
isConnected, isConnecting, isDemoMode, setDemoMode,
|
isConnected, isConnecting, isDemoMode, setDemoMode,
|
||||||
deviceState, peqState, lastUpdated, error,
|
deviceState, peqState, lastUpdated, error,
|
||||||
connect, disconnect, refresh, api,
|
connect, disconnect, refresh, api,
|
||||||
|
|||||||
@@ -277,6 +277,7 @@
|
|||||||
background: rgba(255, 255, 255, 0.12);
|
background: rgba(255, 255, 255, 0.12);
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
touch-action: pan-y;
|
||||||
}
|
}
|
||||||
.cyan-slider::-webkit-slider-thumb {
|
.cyan-slider::-webkit-slider-thumb {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ export function getDeviceIpFromUrl(): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveInitialDeviceIp(): string {
|
export function resolveInitialDeviceIp(): string {
|
||||||
return getDeviceIpFromUrl() ?? localStorage.getItem("luxsin_ip") ?? "192.168.2.30";
|
return getDeviceIpFromUrl() ?? "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"connect": {
|
||||||
|
"ipMissingTitle": "Device IP not specified",
|
||||||
|
"ipMissingDesc": "Add the device IP in the address bar before opening this page, for example:",
|
||||||
|
"connecting": "Connecting to {ip}…",
|
||||||
|
"failedTitle": "Connection failed",
|
||||||
|
"failedDesc": "Check the device IP and your network connection.",
|
||||||
|
"failedHint": "Specify the device in the URL, e.g. ?ip=10.0.0.119"
|
||||||
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"audioSet": "Audio setting",
|
"audioSet": "Audio setting",
|
||||||
"systemSet": "System",
|
"systemSet": "System",
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"connect": {
|
||||||
|
"ipMissingTitle": "未指定裝置 IP",
|
||||||
|
"ipMissingDesc": "請在網址列加入裝置 IP 參數後再開啟本頁,例如:",
|
||||||
|
"connecting": "正在連線 {ip}…",
|
||||||
|
"failedTitle": "連線失敗",
|
||||||
|
"failedDesc": "請檢查裝置 IP 與網路連線。",
|
||||||
|
"failedHint": "透過網址列指定裝置,例如 ?ip=10.0.0.119"
|
||||||
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"audioSet": "音訊設定",
|
"audioSet": "音訊設定",
|
||||||
"systemSet": "系統設定",
|
"systemSet": "系統設定",
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"connect": {
|
||||||
|
"ipMissingTitle": "未指定设备 IP",
|
||||||
|
"ipMissingDesc": "请在地址栏添加设备 IP 参数后再打开本页,例如:",
|
||||||
|
"connecting": "正在连接 {ip}…",
|
||||||
|
"failedTitle": "连接失败",
|
||||||
|
"failedDesc": "请检查设备 IP 与网络连接。",
|
||||||
|
"failedHint": "通过地址栏指定设备,例如 ?ip=10.0.0.119"
|
||||||
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"audioSet": "音频设置",
|
"audioSet": "音频设置",
|
||||||
"systemSet": "系统设置",
|
"systemSet": "系统设置",
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-black flex flex-col">
|
<div className="min-h-dvh bg-black pb-[calc(5.5rem+env(safe-area-inset-bottom,0px))]">
|
||||||
{/* ── Device render image ── */}
|
{/* ── Device render image ── */}
|
||||||
<div className="px-4 pb-4">
|
<div className="px-4 pb-4">
|
||||||
<div className="w-full rounded-2xl overflow-hidden flex items-center justify-center"
|
<div className="w-full rounded-2xl overflow-hidden flex items-center justify-center"
|
||||||
@@ -570,7 +570,7 @@ export default function Home() {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex-1 relative flex items-center">
|
<div className="flex-1 relative flex items-center touch-pan-y">
|
||||||
{/* Track fill - 根据按钮位置调整 */}
|
{/* Track fill - 根据按钮位置调整 */}
|
||||||
<div className="absolute left-0 h-[4px] rounded-full pointer-events-none"
|
<div className="absolute left-0 h-[4px] rounded-full pointer-events-none"
|
||||||
style={{
|
style={{
|
||||||
@@ -782,7 +782,7 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── iOS list rows ── */}
|
{/* ── iOS list rows ── */}
|
||||||
<div className="px-4 pb-4">
|
<div className="px-4">
|
||||||
<div className="ios-list-group">
|
<div className="ios-list-group">
|
||||||
<ListRow
|
<ListRow
|
||||||
icon={<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-4 h-4"><path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"/></svg>}
|
icon={<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-4 h-4"><path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"/></svg>}
|
||||||
|
|||||||
Reference in New Issue
Block a user