import { useCallback, useEffect, useState } from "react"; import { useSettingsStore } from "@hooks/stores"; import { JsonRpcError, JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc"; import { useDeviceUiNavigation } from "@hooks/useAppNavigation"; import { Button } from "@components/Button"; import Checkbox, { CheckboxWithLabel } from "@components/Checkbox"; import { ConfirmDialog } from "@components/ConfirmDialog"; import { GridCard } from "@components/Card"; import { SettingsItem } from "@components/SettingsItem"; import { SettingsPageHeader } from "@components/SettingsPageheader"; 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 notifications from "@/notifications"; import { m } from "@localizations/messages.js"; import { sleep } from "@/utils"; import { checkUpdateComponents, UpdateComponents } from "@/utils/jsonrpc"; import { SystemVersionInfo } from "@hooks/useVersion"; export default function SettingsAdvancedRoute() { 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(); 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( enabled ? m.advanced_error_usb_emulation_enable({ error: resp.error.data || m.unknown_error() }) : m.advanced_error_usb_emulation_disable({ error: resp.error.data || m.unknown_error() }) ); return; } setUsbEmulationEnabled(enabled); getUsbEmulationState(); }); }, [getUsbEmulationState, send], ); const handleResetConfig = useCallback(() => { send("resetConfig", {}, (resp: JsonRpcResponse) => { if ("error" in resp) { notifications.error( m.advanced_error_reset_config({ error: resp.error.data || m.unknown_error() }) ); return; } notifications.success(m.advanced_success_reset_config()); }); }, [send]); const handleUpdateSSHKey = useCallback(() => { send("setSSHKeyState", { sshKey }, (resp: JsonRpcResponse) => { if ("error" in resp) { notifications.error( m.advanced_error_update_ssh_key({ error: resp.error.data || m.unknown_error() }) ); return; } notifications.success(m.advanced_success_update_ssh_key()); }); }, [send, sshKey]); const handleDevModeChange = useCallback( (developerMode: boolean) => { send("setDevModeState", { enabled: developerMode }, (resp: JsonRpcResponse) => { if ("error" in resp) { notifications.error( m.advanced_error_set_dev_mode({ error: resp.error.data || m.unknown_error() }) ); return; } setDeveloperMode(developerMode); }); }, [send, setDeveloperMode], ); const handleDevChannelChange = useCallback( (enabled: boolean) => { send("setDevChannelState", { enabled }, (resp: JsonRpcResponse) => { if ("error" in resp) { notifications.error( m.advanced_error_set_dev_channel({ error: resp.error.data || m.unknown_error() }) ); return; } setDevChannel(enabled); }); }, [send, setDevChannel], ); const applyLoopbackOnlyMode = useCallback( (enabled: boolean) => { send("setLocalLoopbackOnly", { enabled }, (resp: JsonRpcResponse) => { if ("error" in resp) { notifications.error( enabled ? m.advanced_error_loopback_enable({ error: resp.error.data || m.unknown_error() }) : m.advanced_error_loopback_disable({ error: resp.error.data || m.unknown_error() }) ); return; } setLocalLoopbackOnly(enabled); if (enabled) { notifications.success(m.advanced_success_loopback_enabled()); } else { notifications.success(m.advanced_success_loopback_disabled()); } }); }, [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); console.log("versionInfo", versionInfo); } 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); }} /> 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); }} /> {settings.debugMode && (
{ setShowLoopbackWarning(false); }} title={m.advanced_loopback_warning_title()} description={ <>

{m.advanced_loopback_warning_description()}

{m.advanced_loopback_warning_before()}

  • {m.advanced_loopback_warning_ssh()}
  • {m.advanced_loopback_warning_cloud()}
} variant="warning" confirmText={m.advanced_loopback_warning_confirm()} onConfirm={confirmLoopbackModeEnable} />
); }