Update App version to 0.0.4

Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
This commit is contained in:
luckfox-eng29
2025-11-11 20:38:22 +08:00
parent 4e82b8a11c
commit 5e17c52afc
41 changed files with 3537 additions and 598 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ import Checkbox from "../components/Checkbox";
import { ConfirmDialog } from "../components/ConfirmDialog";
import { SettingsPageHeader } from "../components/SettingsPageheader";
import { TextAreaWithLabel } from "../components/TextArea";
import { useSettingsStore } from "../hooks/stores";
import { useSettingsStore, useHidStore } from "../hooks/stores";
import { useJsonRpc } from "../hooks/useJsonRpc";
import { isOnDevice } from "../main";
import notifications from "../notifications";
@@ -24,9 +24,12 @@ export default function SettingsAdvancedRoute() {
const [devChannel, setDevChannel] = useState(false);
const [usbEmulationEnabled, setUsbEmulationEnabled] = useState(false);
const [showLoopbackWarning, setShowLoopbackWarning] = useState(false);
const [showRebootConfirm, setShowRebootConfirm] = useState(false);
const [localLoopbackOnly, setLocalLoopbackOnly] = useState(false);
const settings = useSettingsStore();
const isReinitializingGadget = useHidStore(state => state.isReinitializingGadget);
const setIsReinitializingGadget = useHidStore(state => state.setIsReinitializingGadget);
useEffect(() => {
send("getSSHKeyState", {}, resp => {
@@ -209,6 +212,47 @@ export default function SettingsAdvancedRoute() {
/>
</SettingsItem>
<SettingsItem
title={$at("USB Gadget Reinitialize")}
description={$at("Reinitialize USB gadget configuration")}
>
<Button
size="SM"
theme="light"
text={$at("Reinitialize USB Gadget")}
disabled={isReinitializingGadget}
loading={isReinitializingGadget}
onClick={() => {
if (isReinitializingGadget) return;
setIsReinitializingGadget(true);
send("reinitializeUsbGadget", {}, resp => {
setIsReinitializingGadget(false);
if ("error" in resp) {
notifications.error(
`Failed to reinitialize USB gadget: ${resp.error.data || "Unknown error"}`,
);
return;
}
notifications.success("USB gadget reinitialized successfully");
});
}}
/>
</SettingsItem>
<SettingsItem
title={$at("Reboot System")}
description={$at("Restart the device system")}
>
<Button
size="SM"
theme="danger"
text={$at("Reboot")}
onClick={() => {
setShowRebootConfirm(true);
}}
/>
</SettingsItem>
<SettingsItem
title={$at("Reset Configuration")}
description={$at("Reset configuration to default. This will log you out.Some configuration changes will take effect after restart system.")}
@@ -250,6 +294,39 @@ export default function SettingsAdvancedRoute() {
confirmText="I Understand, Enable Anyway"
onConfirm={confirmLoopbackModeEnable}
/>
<ConfirmDialog
open={showRebootConfirm}
onClose={() => {
setShowRebootConfirm(false);
}}
title={$at("Reboot System?")}
description={
<>
<p>
{$at("Are you sure you want to reboot the system?")}
</p>
<p className="text-xs text-slate-600 dark:text-slate-400 mt-2">
{$at("The device will restart and you will be disconnected from the web interface.")}
</p>
</>
}
variant="warning"
cancelText={$at("Cancel")}
confirmText={$at("Reboot")}
onConfirm={() => {
setShowRebootConfirm(false);
send("reboot", { force: false }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to reboot: ${resp.error.data || "Unknown error"}`,
);
return;
}
notifications.success("System rebooting...");
});
}}
/>
</div>
);
}

View File

@@ -1,10 +1,12 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { LuEthernetPort } from "react-icons/lu";
import {
IPv4Mode,
IPv4StaticConfig,
IPv6Mode,
LLDPMode,
mDNSMode,
@@ -84,11 +86,25 @@ export default function SettingsNetworkRoute() {
// We use this to determine whether the settings have changed
const firstNetworkSettings = useRef<NetworkSettings | undefined>(undefined);
// We use this to indicate whether saved settings differ from initial (effective) settings
const initialNetworkSettings = useRef<NetworkSettings | undefined>(undefined);
const [networkSettingsLoaded, setNetworkSettingsLoaded] = useState(false);
const [customDomain, setCustomDomain] = useState<string>("");
const [selectedDomainOption, setSelectedDomainOption] = useState<string>("local");
const { id } = useParams();
const baselineKey = id ? `network_baseline_${id}` : "network_baseline";
const baselineResetKey = id ? `network_baseline_reset_${id}` : "network_baseline_reset";
useEffect(() => {
const url = new URL(window.location.href);
if (url.searchParams.get("networkChanged") === "true") {
localStorage.setItem(baselineResetKey, "1");
url.searchParams.delete("networkChanged");
window.history.replaceState(null, "", url.toString());
}
}, [baselineResetKey]);
useEffect(() => {
if (networkSettings.domain && networkSettingsLoaded) {
@@ -109,10 +125,33 @@ export default function SettingsNetworkRoute() {
if ("error" in resp) return;
console.log(resp.result);
setNetworkSettings(resp.result as NetworkSettings);
if (!firstNetworkSettings.current) {
firstNetworkSettings.current = resp.result as NetworkSettings;
}
const resetFlag = localStorage.getItem(baselineResetKey);
const stored = localStorage.getItem(baselineKey);
if (resetFlag) {
initialNetworkSettings.current = resp.result as NetworkSettings;
localStorage.setItem(baselineKey, JSON.stringify(resp.result));
localStorage.removeItem(baselineResetKey);
} else if (stored) {
try {
const parsed = JSON.parse(stored) as NetworkSettings;
const server = resp.result as NetworkSettings;
if (JSON.stringify(parsed) !== JSON.stringify(server)) {
initialNetworkSettings.current = server;
localStorage.setItem(baselineKey, JSON.stringify(server));
} else {
initialNetworkSettings.current = parsed;
}
} catch {
initialNetworkSettings.current = resp.result as NetworkSettings;
localStorage.setItem(baselineKey, JSON.stringify(resp.result));
}
} else {
initialNetworkSettings.current = resp.result as NetworkSettings;
localStorage.setItem(baselineKey, JSON.stringify(resp.result));
}
setNetworkSettingsLoaded(true);
});
}, [send]);
@@ -164,7 +203,41 @@ export default function SettingsNetworkRoute() {
}, [getNetworkState, getNetworkSettings]);
const handleIpv4ModeChange = (value: IPv4Mode | string) => {
setNetworkSettings({ ...networkSettings, ipv4_mode: value as IPv4Mode });
const newMode = value as IPv4Mode;
const updatedSettings: NetworkSettings = { ...networkSettings, ipv4_mode: newMode };
// Initialize static config if switching to static mode
if (newMode === "static" && !updatedSettings.ipv4_static) {
updatedSettings.ipv4_static = {
address: "",
netmask: "",
gateway: "",
dns: [],
};
}
setNetworkSettings(updatedSettings);
};
const handleIpv4RequestAddressChange = (value: string) => {
setNetworkSettings({ ...networkSettings, ipv4_request_address: value });
};
const handleIpv4StaticChange = (field: keyof IPv4StaticConfig, value: string | string[]) => {
const staticConfig = networkSettings.ipv4_static || {
address: "",
netmask: "",
gateway: "",
dns: [],
};
setNetworkSettings({
...networkSettings,
ipv4_static: {
...staticConfig,
[field]: value,
},
});
};
const handleIpv6ModeChange = (value: IPv6Mode | string) => {
@@ -212,6 +285,55 @@ export default function SettingsNetworkRoute() {
);
const [showRenewLeaseConfirm, setShowRenewLeaseConfirm] = useState(false);
const [applyingRequestAddr, setApplyingRequestAddr] = useState(false);
const [showRequestAddrConfirm, setShowRequestAddrConfirm] = useState(false);
const [showApplyStaticConfirm, setShowApplyStaticConfirm] = useState(false);
const [showIpv4RestartConfirm, setShowIpv4RestartConfirm] = useState(false);
const [pendingIpv4Mode, setPendingIpv4Mode] = useState<IPv4Mode | null>(null);
const [ipv4StaticDnsText, setIpv4StaticDnsText] = useState("");
const isIPv4StaticEqual = (a?: IPv4StaticConfig, b?: IPv4StaticConfig) => {
const na = a || { address: "", netmask: "", gateway: "", dns: [] };
const nb = b || { address: "", netmask: "", gateway: "", dns: [] };
const adns = (na.dns || []).map(x => x.trim()).filter(x => x.length > 0).sort();
const bdns = (nb.dns || []).map(x => x.trim()).filter(x => x.length > 0).sort();
if ((na.address || "").trim() !== (nb.address || "").trim()) return false;
if ((na.netmask || "").trim() !== (nb.netmask || "").trim()) return false;
if ((na.gateway || "").trim() !== (nb.gateway || "").trim()) return false;
if (adns.length !== bdns.length) return false;
for (let i = 0; i < adns.length; i++) {
if (adns[i] !== bdns[i]) return false;
}
return true;
};
const handleApplyRequestAddress = useCallback(() => {
const requested = (networkSettings.ipv4_request_address || "").trim();
if (!requested) {
notifications.error("Please enter a valid Request Address");
return;
}
if (networkSettings.ipv4_mode !== "dhcp") {
notifications.error("Request Address is only available in DHCP mode");
return;
}
setApplyingRequestAddr(true);
send("setNetworkSettings", { settings: networkSettings }, resp => {
if ("error" in resp) {
setApplyingRequestAddr(false);
return notifications.error(
"Failed to save Request Address: " + (resp.error.data ? resp.error.data : resp.error.message),
);
}
setApplyingRequestAddr(false);
notifications.success("Request Address saved. Changes will take effect after restart.");
});
}, [networkSettings, send]);
useEffect(() => {
const dns = (networkSettings.ipv4_static?.dns || []).join(", ");
setIpv4StaticDnsText(dns);
}, [networkSettings.ipv4_static?.dns]);
return (
<>
@@ -337,7 +459,10 @@ export default function SettingsNetworkRoute() {
<Button
size="SM"
theme="primary"
disabled={firstNetworkSettings.current === networkSettings}
disabled={
firstNetworkSettings.current === networkSettings ||
(networkSettings.ipv4_mode === "static" && firstNetworkSettings.current?.ipv4_mode !== "static")
}
text={$at("Save settings")}
onClick={() => setNetworkSettingsRemote(networkSettings)}
/>
@@ -346,46 +471,135 @@ export default function SettingsNetworkRoute() {
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
<div className="space-y-4">
<SettingsItem title={$at("IPv4 Mode")} description={$at("Configure IPv4 mode")}>
<SettingsItem
title={$at("IPv4 Mode")}
description={$at("Configure IPv4 mode")}
// badge={networkSettings.pending_reboot ? $at("Effective Upon Reboot") : undefined}
>
<SelectMenuBasic
size="SM"
value={networkSettings.ipv4_mode}
onChange={e => handleIpv4ModeChange(e.target.value)}
onChange={e => {
const next = e.target.value as IPv4Mode;
setPendingIpv4Mode(next);
setShowIpv4RestartConfirm(true);
}}
options={filterUnknown([
{ value: "dhcp", label: "DHCP" },
// { value: "static", label: "Static" },
{ value: "static", label: "Static" },
])}
/>
</SettingsItem>
<AutoHeight>
{!networkSettingsLoaded && !networkState?.dhcp_lease ? (
{networkSettings.ipv4_mode === "dhcp" && (
<div className="flex items-end gap-x-2">
<InputFieldWithLabel
size="SM"
type="text"
label={$at("Request Address")}
placeholder="192.168.1.100"
value={networkSettings.ipv4_request_address || ""}
onChange={e => {
handleIpv4RequestAddressChange(e.target.value);
}}
/>
<Button
size="SM"
theme="primary"
text={$at("Save")}
disabled={applyingRequestAddr || !networkSettings.ipv4_request_address}
onClick={() => setShowRequestAddrConfirm(true)}
/>
</div>
)}
{networkSettings.ipv4_mode === "static" && (
<AutoHeight>
<GridCard>
<div className="p-4">
<div className="space-y-4">
<h3 className="text-base font-bold text-slate-900 dark:text-white">
{$at("DHCP Lease Information")}
</h3>
<div className="animate-pulse space-y-3">
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-1/2 rounded bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
<div className="p-4 mt-1 space-y-4 border-l border-slate-800/10 pl-4 dark:border-slate-300/20 items-end gap-x-2">
<InputFieldWithLabel
size="SM"
type="text"
label={$at("IP Address")}
placeholder="192.168.1.100"
value={networkSettings.ipv4_static?.address || ""}
onChange={e => {
handleIpv4StaticChange("address", e.target.value);
}}
/>
<InputFieldWithLabel
size="SM"
type="text"
label={$at("Netmask")}
placeholder="255.255.255.0"
value={networkSettings.ipv4_static?.netmask || ""}
onChange={e => {
handleIpv4StaticChange("netmask", e.target.value);
}}
/>
<InputFieldWithLabel
size="SM"
type="text"
label={$at("Gateway")}
placeholder="192.168.1.1"
value={networkSettings.ipv4_static?.gateway || ""}
onChange={e => {
handleIpv4StaticChange("gateway", e.target.value);
}}
/>
<InputFieldWithLabel
size="SM"
type="text"
label={$at("DNS Servers")}
placeholder="8.8.8.8,8.8.4.4"
value={ipv4StaticDnsText}
onChange={e => setIpv4StaticDnsText(e.target.value)}
/>
<div className="flex items-center gap-x-2">
<Button
size="SM"
theme="primary"
text={$at("Save")}
onClick={() => setShowApplyStaticConfirm(true)}
/>
</div>
</div>
</GridCard>
</AutoHeight>
)}
{networkSettings.ipv4_mode === "dhcp" && (
<AutoHeight>
{!networkSettingsLoaded && !networkState?.dhcp_lease ? (
<GridCard>
<div className="p-4">
<div className="space-y-4">
<h3 className="text-base font-bold text-slate-900 dark:text-white">
{$at("DHCP Lease Information")}
</h3>
<div className="animate-pulse space-y-3">
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-1/2 rounded bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
</div>
</div>
</div>
</div>
</GridCard>
) : networkState?.dhcp_lease && networkState.dhcp_lease.ip ? (
<DhcpLeaseCard
networkState={networkState}
setShowRenewLeaseConfirm={setShowRenewLeaseConfirm}
/>
) : (
<EmptyCard
IconElm={LuEthernetPort}
headline={$at("DHCP Information")}
description={$at("No DHCP lease information available")}
/>
)}
</AutoHeight>
</GridCard>
) : networkState?.dhcp_lease && networkState.dhcp_lease.ip ? (
<DhcpLeaseCard
networkState={networkState}
setShowRenewLeaseConfirm={setShowRenewLeaseConfirm}
/>
) : (
<EmptyCard
IconElm={LuEthernetPort}
headline={$at("DHCP Information")}
description={$at("No DHCP lease information available")}
/>
)}
</AutoHeight>
)}
</div>
<div className="space-y-4">
<SettingsItem title={$at("IPv6 Mode")} description={$at("Configure the IPv6 mode")}>
@@ -453,7 +667,7 @@ export default function SettingsNetworkRoute() {
open={showRenewLeaseConfirm}
onClose={() => setShowRenewLeaseConfirm(false)}
title={$at("Renew DHCP Lease")}
description={$at("This will request your DHCP server to assign a new IP address. Your device may lose network connectivity during the process.")}
description={$at("Changes will take effect after a restart.")}
variant="danger"
confirmText={$at("Renew DHCP Lease")}
cancelText={$at("Cancel")}
@@ -462,6 +676,64 @@ export default function SettingsNetworkRoute() {
setShowRenewLeaseConfirm(false);
}}
/>
<ConfirmDialog
open={showApplyStaticConfirm}
onClose={() => setShowApplyStaticConfirm(false)}
title={$at("Save Static IPv4 Settings?")}
description={$at("Changes will take effect after a restart.")}
variant="warning"
confirmText={$at("Confirm")}
cancelText={$at("Cancel")}
onConfirm={() => {
setShowApplyStaticConfirm(false);
const dnsArray = ipv4StaticDnsText
.split(",")
.map(d => d.trim())
.filter(d => d.length > 0);
const updatedSettings: NetworkSettings = {
...networkSettings,
ipv4_static: {
...(networkSettings.ipv4_static || { address: "", netmask: "", gateway: "", dns: [] }),
dns: dnsArray,
},
};
setNetworkSettingsRemote(updatedSettings);
}}
/>
<ConfirmDialog
open={showRequestAddrConfirm}
onClose={() => setShowRequestAddrConfirm(false)}
title={$at("Save Request Address?")}
description={$at("This will save the requested IPv4 address. Changes take effect after a restart.")}
variant="warning"
confirmText={$at("Save")}
cancelText={$at("Cancel")}
onConfirm={() => {
setShowRequestAddrConfirm(false);
handleApplyRequestAddress();
}}
/>
<ConfirmDialog
open={showIpv4RestartConfirm}
onClose={() => setShowIpv4RestartConfirm(false)}
title={$at("Change IPv4 Mode?")}
description={$at("IPv4 mode changes will take effect after a restart.")}
variant="warning"
confirmText={$at("Confirm")}
cancelText={$at("Cancel")}
onConfirm={() => {
setShowIpv4RestartConfirm(false);
if (pendingIpv4Mode) {
const updatedSettings: NetworkSettings = { ...networkSettings, ipv4_mode: pendingIpv4Mode };
if (pendingIpv4Mode === "static" && !updatedSettings.ipv4_static) {
updatedSettings.ipv4_static = { address: "", netmask: "", gateway: "", dns: [] };
}
setNetworkSettings(updatedSettings);
setNetworkSettingsRemote(updatedSettings);
setPendingIpv4Mode(null);
}
}}
/>
</>
);
}

View File

@@ -8,6 +8,7 @@ import { useSettingsStore } from "@/hooks/stores";
import notifications from "../notifications";
import { SelectMenuBasic } from "../components/SelectMenuBasic";
import Checkbox from "../components/Checkbox";
import { SettingsItem } from "./devices.$id.settings";
import {useReactAt} from 'i18n-auto-extractor/react'
@@ -48,6 +49,7 @@ export default function SettingsVideoRoute() {
const [streamQuality, setStreamQuality] = useState("1");
const [customEdidValue, setCustomEdidValue] = useState<string | null>(null);
const [edid, setEdid] = useState<string | null>(null);
const [forceHpd, setForceHpd] = useState(false);
// Video enhancement settings from store
const videoSaturation = useSettingsStore(state => state.videoSaturation);
@@ -85,7 +87,31 @@ export default function SettingsVideoRoute() {
setCustomEdidValue(receivedEdid);
}
});
send("getForceHpd", {}, resp => {
if ("error" in resp) {
notifications.error(`Failed to get force EDID output: ${resp.error.data || "Unknown error"}`);
setForceHpd(false);
return;
}
setForceHpd(resp.result as boolean);
});
}, [send]);
const handleForceHpdChange = (checked: boolean) => {
send("setForceHpd", { forceHpd: checked }, resp => { // 修复参数名称为forceHpd
if ("error" in resp) {
notifications.error(`Failed to set force EDID output: ${resp.error.data || "Unknown error"}`);
setForceHpd(!checked);
return;
}
notifications.success(`Force EDID output ${checked ? "enabled" : "disabled"}`);
setForceHpd(checked);
});
};
const handleStreamQualityChange = (factor: string) => {
send("setStreamQualityFactor", { factor: Number(factor) }, resp => {
@@ -205,6 +231,17 @@ export default function SettingsVideoRoute() {
</div>
</div>
{/* EDID Force Output Setting */}
<SettingsItem
title={$at("Force EDID Output")}
description={$at("Force EDID output even when no display is connected")}
>
<Checkbox
checked={forceHpd}
onChange={e => handleForceHpdChange(e.target.checked)}
/>
</SettingsItem>
<SettingsItem
title="EDID"
description={$at("Adjust the EDID settings for the display")}

View File

@@ -566,12 +566,13 @@ export default function KvmIdRoute() {
const setNetworkState = useNetworkStateStore(state => state.setNetworkState);
const setUsbState = useHidStore(state => state.setUsbState);
const usbState = useHidStore(state => state.usbState);
const setHdmiState = useVideoStore(state => state.setHdmiState);
const keyboardLedState = useHidStore(state => state.keyboardLedState);
const setKeyboardLedState = useHidStore(state => state.setKeyboardLedState);
const setKeyboardLedStateSyncAvailable = useHidStore(state => state.setKeyboardLedStateSyncAvailable);
const setIsReinitializingGadget = useHidStore(state => state.setIsReinitializingGadget);
const [hasUpdated, setHasUpdated] = useState(false);
const { navigateTo } = useDeviceUiNavigation();
@@ -601,6 +602,42 @@ export default function KvmIdRoute() {
setKeyboardLedStateSyncAvailable(true);
}
if (resp.method === "hidDeviceMissing") {
const params = resp.params as { device: string; error: string };
console.error("HID device missing:", params);
send("getUsbEmulationState", {}, stateResp => {
if ("error" in stateResp) return;
const emuEnabled = stateResp.result as boolean;
if (!emuEnabled || usbState !== "configured") {
return;
}
setIsReinitializingGadget(true);
notifications.error(
`USB HID device (${params.device}) is missing. Reinitializing USB gadget...`,
{ duration: 5000 }
);
send("reinitializeUsbGadgetSoft", {}, (resp) => {
setIsReinitializingGadget(false);
if ("error" in resp) {
notifications.error(
`Failed to reinitialize USB gadget (soft): ${resp.error.message}`,
{ duration: 5000 }
);
} else {
notifications.success(
"USB gadget soft reinitialized successfully",
{ duration: 3000 }
);
}
});
});
}
if (resp.method === "otaState") {
const otaState = resp.params as UpdateState["otaState"];
setOtaState(otaState);
@@ -624,6 +661,12 @@ export default function KvmIdRoute() {
window.location.href = currentUrl.toString();
}
}
if (resp.method === "refreshPage") {
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set("networkChanged", "true");
window.location.href = currentUrl.toString();
}
}
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);