@@ -302,17 +301,18 @@ function UpdatingDeviceState({
const verifiedAt = otaState[`${type}VerifiedAt`];
const updatedAt = otaState[`${type}UpdatedAt`];
+ const update_type = () => (type === "system" ? "System" : "App");
if (!otaState.metadataFetchedAt) {
- return "Fetching update information...";
+ return "Fetching update information…";
} else if (!downloadFinishedAt) {
- return `Downloading ${type} update...`;
+ return `Downloading ${update_type()} update…`;
} else if (!verifiedAt) {
- return `Verifying ${type} update...`;
+ return `Verifying ${update_type()} update…`;
} else if (!updatedAt) {
- return `Installing ${type} update...`;
+ return `Installing ${update_type()} update…`;
} else {
- return `Awaiting reboot`;
+ return "Awaiting reboot";
}
};
@@ -352,7 +352,6 @@ function UpdatingDeviceState({
};
}, [otaState]);
-
return (
@@ -361,7 +360,7 @@ function UpdatingDeviceState({
Updating your device
- Please don{"'"}t turn off your device. This process may take a few minutes.
+ Please don't turn off your device. This process may take a few minutes.
@@ -370,7 +369,7 @@ function UpdatingDeviceState({
- Rebooting to complete the update...
+ Rebooting to complete the update…
- Update Available
+ Update available
- A new update is available to enhance system performance and improve
- compatibility. We recommend updating to ensure everything runs smoothly.
+ A new update is available to enhance system performance and improve compatibility. We recommend updating to ensure everything runs smoothly.
{versionInfo?.systemUpdateAvailable ? (
<>
- Linux System Update: {versionInfo?.local?.systemVersion} → {versionInfo?.remote?.systemVersion}
+ System: {versionInfo?.local?.systemVersion} → {versionInfo?.remote?.systemVersion}
>
) : null}
{versionInfo?.appUpdateAvailable ? (
<>
- App Update: {versionInfo?.local?.appVersion} → {versionInfo?.remote?.appVersion}
+ App: {versionInfo?.local?.appVersion} → {versionInfo?.remote?.appVersion}
>
) : null}
{versionInfo?.willDisableAutoUpdate ? (
- You{"'"}re about to manually change your device version. Auto-update will be disabled after the update is completed to prevent accidental updates.
+ You're about to manually change your device version. Auto-update will be disabled after the update is completed to prevent accidental updates.
) : null}
@@ -490,8 +488,7 @@ function UpdateCompletedState({ onClose }: { onClose: () => void }) {
Update Completed Successfully
- Your device has been successfully updated to the latest version. Enjoy the new
- features and improvements!
+ Your device has been successfully updated to the latest version. Enjoy the new features and improvements!
@@ -529,4 +526,4 @@ function UpdateErrorState({
);
-}
+}
\ No newline at end of file
diff --git a/ui/src/routes/devices.$id.settings.hardware.tsx b/ui/src/routes/devices.$id.settings.hardware.tsx
index 5c625bb5..62e05c27 100644
--- a/ui/src/routes/devices.$id.settings.hardware.tsx
+++ b/ui/src/routes/devices.$id.settings.hardware.tsx
@@ -1,502 +1,237 @@
-import { useCallback, useEffect, useState } from "react";
-import { useSettingsStore } from "@hooks/stores";
-import { JsonRpcError, JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
-import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
-import { SystemVersionInfo } from "@hooks/useVersion";
+import { useEffect, useState } from "react";
+import { BacklightSettings, useSettingsStore } from "@hooks/stores";
+import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
-import { Button } from "@components/Button";
-import Checkbox, { CheckboxWithLabel } from "@components/Checkbox";
-import { ConfirmDialog } from "@components/ConfirmDialog";
-import { GridCard } from "@components/Card";
+import { Checkbox } from "@components/Checkbox";
+import { FeatureFlag } from "@components/FeatureFlag";
+import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader";
+import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import { NestedSettingsGroup } from "@components/NestedSettingsGroup";
-import { TextAreaWithLabel } from "@components/TextArea";
-import { InputFieldWithLabel } from "@components/InputField";
-import { SelectMenuBasic } from "@components/SelectMenuBasic";
-import { isOnDevice } from "@/main";
+import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
+import { UsbInfoSetting } from "@components/UsbInfoSetting";
import notifications from "@/notifications";
-import { sleep } from "@/utils";
-import { checkUpdateComponents, UpdateComponents } from "@/utils/jsonrpc";
-
-import { FeatureFlag } from "../components/FeatureFlag";
-
-export default function SettingsAdvancedRoute() {
+export default function SettingsHardwareRoute() {
const { send } = useJsonRpc();
- const { navigateTo } = useDeviceUiNavigation();
-
- const [sshKey, setSSHKey] = useState
("");
- const { setDeveloperMode } = useSettingsStore();
- const [devChannel, setDevChannel] = useState(false);
- const [usbEmulationEnabled, setUsbEmulationEnabled] = useState(false);
- const [showLoopbackWarning, setShowLoopbackWarning] = useState(false);
- const [localLoopbackOnly, setLocalLoopbackOnly] = useState(false);
- const [updateTarget, setUpdateTarget] = useState("app");
- const [appVersion, setAppVersion] = useState("");
- const [systemVersion, setSystemVersion] = useState("");
- const [resetConfig, setResetConfig] = useState(false);
- const [versionChangeAcknowledged, setVersionChangeAcknowledged] = useState(false);
- const [customVersionUpdateLoading, setCustomVersionUpdateLoading] = useState(false);
const settings = useSettingsStore();
+ const { displayRotation, setDisplayRotation } = useSettingsStore();
+ const [powerSavingEnabled, setPowerSavingEnabled] = useState(false);
+
+ const handleDisplayRotationChange = (rotation: string) => {
+ setDisplayRotation(rotation);
+ handleDisplayRotationSave();
+ };
+
+ const handleDisplayRotationSave = () => {
+ send("setDisplayRotation", { params: { rotation: displayRotation } }, (resp: JsonRpcResponse) => {
+ if ("error" in resp) {
+ notifications.error(
+ `Failed to set display orientation: ${resp.error.data || "Unknown error"}`,
+ );
+ return;
+ }
+ notifications.success("Display orientation updated successfully");
+ });
+ };
+
+ const { backlightSettings, setBacklightSettings } = useSettingsStore();
+
+ const handleBacklightSettingsChange = (settings: BacklightSettings) => {
+ // If the user has set the display to dim after it turns off, set the dim_after
+ // value to never.
+ if (settings.dim_after > settings.off_after && settings.off_after != 0) {
+ settings.dim_after = 0;
+ }
+
+ setBacklightSettings(settings);
+ handleBacklightSettingsSave(settings);
+ };
+
+ const handleBacklightSettingsSave = (backlightSettings: BacklightSettings) => {
+ send("setBacklightSettings", { params: backlightSettings }, (resp: JsonRpcResponse) => {
+ if ("error" in resp) {
+ notifications.error(
+ `Failed to set backlight settings: ${resp.error.data || "Unknown error"}`,
+ );
+ return;
+ }
+ notifications.success("Backlight settings updated successfully");
+ });
+ };
+
+ const handleBacklightMaxBrightnessChange = (max_brightness: number) => {
+ const settings = { ...backlightSettings, max_brightness };
+ handleBacklightSettingsChange(settings);
+ };
+
+ const handleBacklightDimAfterChange = (dim_after: number) => {
+ const settings = { ...backlightSettings, dim_after };
+ handleBacklightSettingsChange(settings);
+ };
+
+ const handleBacklightOffAfterChange = (off_after: number) => {
+ const settings = { ...backlightSettings, off_after };
+ handleBacklightSettingsChange(settings);
+ };
+
+ const handlePowerSavingChange = (enabled: boolean) => {
+ setPowerSavingEnabled(enabled);
+ const duration = enabled ? 90 : -1;
+ send("setVideoSleepMode", { duration }, (resp: JsonRpcResponse) => {
+ if ("error" in resp) {
+ notifications.error(`Failed to set power saving mode: ${resp.error.data || "Unknown error"}`);
+ setPowerSavingEnabled(!enabled); // Attempt to revert on error
+ return;
+ }
+ notifications.success(enabled ? 'Power saving mode enabled' : 'Power saving mode disabled');
+ });
+ };
useEffect(() => {
- send("getDevModeState", {}, (resp: JsonRpcResponse) => {
- if ("error" in resp) return;
- const result = resp.result as { enabled: boolean };
- setDeveloperMode(result.enabled);
- });
-
- send("getSSHKeyState", {}, (resp: JsonRpcResponse) => {
- if ("error" in resp) return;
- setSSHKey(resp.result as string);
- });
-
- send("getUsbEmulationState", {}, (resp: JsonRpcResponse) => {
- if ("error" in resp) return;
- setUsbEmulationEnabled(resp.result as boolean);
- });
-
- send("getDevChannelState", {}, (resp: JsonRpcResponse) => {
- if ("error" in resp) return;
- setDevChannel(resp.result as boolean);
- });
-
- send("getLocalLoopbackOnly", {}, (resp: JsonRpcResponse) => {
- if ("error" in resp) return;
- setLocalLoopbackOnly(resp.result as boolean);
- });
- }, [send, setDeveloperMode]);
-
- const getUsbEmulationState = useCallback(() => {
- send("getUsbEmulationState", {}, (resp: JsonRpcResponse) => {
- if ("error" in resp) return;
- setUsbEmulationEnabled(resp.result as boolean);
- });
- }, [send]);
-
- const handleUsbEmulationToggle = useCallback(
- (enabled: boolean) => {
- send("setUsbEmulationState", { enabled: enabled }, (resp: JsonRpcResponse) => {
- if ("error" in resp) {
- notifications.error(
- `Failed to ${enabled ? "enable" : "disable"} USB emulation: ${resp.error.data || "Unknown error"}`,
- );
- return;
- }
- setUsbEmulationEnabled(enabled);
- getUsbEmulationState();
- });
- },
- [getUsbEmulationState, send],
- );
-
- const handleResetConfig = useCallback(() => {
- send("resetConfig", {}, (resp: JsonRpcResponse) => {
+ send("getBacklightSettings", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
- notifications.error(
- `Failed to reset configuration: ${resp.error.data || "Unknown error"}`,
+ return notifications.error(
+ `Failed to get backlight settings: ${resp.error.data || "Unknown error"}`,
);
+ }
+ const result = resp.result as BacklightSettings;
+ setBacklightSettings(result);
+ });
+ }, [send, setBacklightSettings]);
+
+ useEffect(() => {
+ send("getVideoSleepMode", {}, (resp: JsonRpcResponse) => {
+ if ("error" in resp) {
+ console.error("Failed to get power saving mode:", resp.error);
return;
}
- notifications.success("Configuration reset to default successfully");
+ const result = resp.result as { enabled: boolean; duration: number };
+ setPowerSavingEnabled(result.duration >= 0);
});
}, [send]);
- const handleUpdateSSHKey = useCallback(() => {
- send("setSSHKeyState", { sshKey }, (resp: JsonRpcResponse) => {
- if ("error" in resp) {
- notifications.error(
- `Failed to update SSH key: ${resp.error.data || "Unknown error"}`,
- );
- return;
- }
- notifications.success("SSH key updated successfully");
- });
- }, [send, sshKey]);
-
- const handleDevModeChange = useCallback(
- (developerMode: boolean) => {
- send("setDevModeState", { enabled: developerMode }, (resp: JsonRpcResponse) => {
- if ("error" in resp) {
- notifications.error(
- `Failed to set dev mode: ${resp.error.data || "Unknown error"}`,
- );
- return;
- }
- setDeveloperMode(developerMode);
- });
- },
- [send, setDeveloperMode],
- );
-
- const handleDevChannelChange = useCallback(
- (enabled: boolean) => {
- send("setDevChannelState", { enabled }, (resp: JsonRpcResponse) => {
- if ("error" in resp) {
- notifications.error(
- `Failed to set dev channel state: ${resp.error.data || "Unknown error"}`,
- );
- return;
- }
- setDevChannel(enabled);
- });
- },
- [send, setDevChannel],
- );
-
- const applyLoopbackOnlyMode = useCallback(
- (enabled: boolean) => {
- send("setLocalLoopbackOnly", { enabled }, (resp: JsonRpcResponse) => {
- if ("error" in resp) {
- notifications.error(
- `Failed to ${enabled ? "enable" : "disable"} loopback-only mode: ${resp.error.data || "Unknown error"}`,
- );
- return;
- }
- setLocalLoopbackOnly(enabled);
- if (enabled) {
- notifications.success(
- "Loopback-only mode enabled. Restart your device to apply.",
- );
- } else {
- notifications.success(
- "Loopback-only mode disabled. Restart your device to apply.",
- );
- }
- });
- },
- [send, setLocalLoopbackOnly],
- );
-
- const handleLoopbackOnlyModeChange = useCallback(
- (enabled: boolean) => {
- // If trying to enable loopback-only mode, show warning first
- if (enabled) {
- setShowLoopbackWarning(true);
- } else {
- // If disabling, just proceed
- applyLoopbackOnlyMode(false);
- }
- },
- [applyLoopbackOnlyMode, setShowLoopbackWarning],
- );
-
- const confirmLoopbackModeEnable = useCallback(() => {
- applyLoopbackOnlyMode(true);
- setShowLoopbackWarning(false);
- }, [applyLoopbackOnlyMode, setShowLoopbackWarning]);
-
- const handleVersionUpdateError = useCallback((error?: JsonRpcError | string) => {
- notifications.error(
- m.advanced_error_version_update({
- error: typeof error === "string" ? error : (error?.data ?? error?.message ?? m.unknown_error())
- }),
- { duration: 1000 * 15 } // 15 seconds
- );
- setCustomVersionUpdateLoading(false);
- }, []);
-
- const handleCustomVersionUpdate = useCallback(async () => {
- const components: UpdateComponents = {};
- if (["app", "both"].includes(updateTarget) && appVersion) components.app = appVersion;
- if (["system", "both"].includes(updateTarget) && systemVersion) components.system = systemVersion;
- let versionInfo: SystemVersionInfo | undefined;
-
- try {
- // we do not need to set it to false if check succeeds,
- // because it will be redirected to the update page later
- setCustomVersionUpdateLoading(true);
- versionInfo = await checkUpdateComponents({
- components,
- }, devChannel);
- } catch (error: unknown) {
- const jsonRpcError = error as JsonRpcError;
- handleVersionUpdateError(jsonRpcError);
- return;
- }
-
- let hasUpdate = false;
-
- const pageParams = new URLSearchParams();
- if (components.app && versionInfo?.remote?.appVersion && versionInfo?.appUpdateAvailable) {
- hasUpdate = true;
- pageParams.set("custom_app_version", versionInfo.remote?.appVersion);
- }
- if (components.system && versionInfo?.remote?.systemVersion && versionInfo?.systemUpdateAvailable) {
- hasUpdate = true;
- pageParams.set("custom_system_version", versionInfo.remote?.systemVersion);
- }
- pageParams.set("reset_config", resetConfig.toString());
-
- if (!hasUpdate) {
- handleVersionUpdateError("No update available");
- return;
- }
-
- // Navigate to update page
- navigateTo(`/settings/general/update?${pageParams.toString()}`);
- }, [
- updateTarget, appVersion, systemVersion, devChannel,
- navigateTo, resetConfig, handleVersionUpdateError,
- setCustomVersionUpdateLoading
- ]);
-
return (
-
- {
- handleDevChannelChange(e.target.checked);
+ handleDisplayRotationChange(e.target.value);
}}
/>
- handleDevModeChange(e.target.checked)}
- />
-
- {settings.developerMode ? (
-
-
-
-
-
-
-
- {m.advanced_developer_mode_enabled_title()}
-
-
-
- - {m.advanced_developer_mode_warning_security()}
- - {m.advanced_developer_mode_warning_risks()}
-
-
-
-
- {m.advanced_developer_mode_warning_advanced()}
-
-
-
-
-
- {isOnDevice && (
-
-
-
setSSHKey(e.target.value)}
- placeholder={m.advanced_ssh_public_key_placeholder()}
- />
-
- {m.advanced_ssh_default_user()}root.
-
-
-
-
-
- )}
-
-
-
-
-
-
setUpdateTarget(e.target.value)}
- />
-
- {(updateTarget === "app" || updateTarget === "both") && (
- setAppVersion(e.target.value)}
- />
- )}
-
- {(updateTarget === "system" || updateTarget === "both") && (
- setSystemVersion(e.target.value)}
- />
- )}
-
-
- {m.advanced_version_update_helper()}{" "}
-
- {m.advanced_version_update_github_link()}
-
-
-
-
- setResetConfig(e.target.checked)}
- />
-
-
-
- setVersionChangeAcknowledged(e.target.checked)}
- />
-
-
-
-
-
-
- ) : null}
-
-
- handleLoopbackOnlyModeChange(e.target.checked)}
- />
-
-
-
-
-
- {
- settings.setDebugMode(e.target.checked);
+ handleBacklightMaxBrightnessChange(Number.parseInt(e.target.value));
}}
/>
-
- {settings.debugMode && (
+ {backlightSettings.max_brightness != 0 && (
-
-
-
)}
+
+ The display will wake up when the connection state changes, or when touched.
+
-
{
- setShowLoopbackWarning(false);
- }}
- title="Enable Loopback-Only Mode?"
- description={
- <>
-
- WARNING: This will restrict web interface access to localhost (127.0.0.1)
- only.
-
- Before enabling this feature, make sure you have either:
-
- - SSH access configured and tested
- - Cloud access enabled and working
-
- >
- }
- variant="warning"
- confirmText="I Understand, Enable Anyway"
- onConfirm={confirmLoopbackModeEnable}
- />
+
+
+
+
+
+ handlePowerSavingChange(e.target.checked)}
+ />
+
+
+
+
+
+
+
+
+
+
+
);
}
\ No newline at end of file