refactor: mprove UI settings structure with NestedSettingsGroup

This commit is contained in:
Adam Shiervani 2025-10-29 11:50:27 +01:00 committed by Siyuan
parent 85f7f60618
commit b0e659b76e
7 changed files with 120 additions and 114 deletions

View File

@ -0,0 +1,22 @@
import { cx } from "@/cva.config";
interface NestedSettingsGroupProps {
readonly children: React.ReactNode;
readonly className?: string;
}
export function NestedSettingsGroup(props: NestedSettingsGroupProps) {
const { children, className } = props;
return (
<div
className={cx(
"space-y-4 border-l-2 border-slate-200 ml-2 pl-4 dark:border-slate-700",
className,
)}
>
{children}
</div>
);
}

View File

@ -11,6 +11,7 @@ import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { SettingsSectionHeader } from "@components/SettingsSectionHeader"; import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import { NestedSettingsGroup } from "@components/NestedSettingsGroup";
import { TextAreaWithLabel } from "@components/TextArea"; import { TextAreaWithLabel } from "@components/TextArea";
import api from "@/api"; import api from "@/api";
import notifications from "@/notifications"; import notifications from "@/notifications";
@ -237,39 +238,30 @@ export default function SettingsAccessIndexRoute() {
</SettingsItem> </SettingsItem>
{tlsMode === "custom" && ( {tlsMode === "custom" && (
<div className="mt-4 space-y-4"> <NestedSettingsGroup className="mt-4">
<div className="space-y-4"> <SettingsItem
<SettingsItem title={m.access_tls_certificate_title()}
title={m.access_tls_certificate_title()} description={m.access_tls_certificate_description()}
description={m.access_tls_certificate_description()} />
/> <TextAreaWithLabel
<div className="space-y-4"> label={m.access_certificate_label()}
<TextAreaWithLabel rows={3}
label={m.access_certificate_label()} placeholder={
rows={3} "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
placeholder={ }
"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----" value={tlsCert}
} onChange={e => handleTlsCertChange(e.target.value)}
value={tlsCert} />
onChange={e => handleTlsCertChange(e.target.value)} <TextAreaWithLabel
/> label={m.access_private_key_label()}
</div> description={m.access_private_key_description()}
rows={3}
<div className="space-y-4"> placeholder={
<div className="space-y-4"> "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
<TextAreaWithLabel }
label={m.access_private_key_label()} value={tlsKey}
description={m.access_private_key_description()} onChange={e => handleTlsKeyChange(e.target.value)}
rows={3} />
placeholder={
"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
}
value={tlsKey}
onChange={e => handleTlsKeyChange(e.target.value)}
/>
</div>
</div>
</div>
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<Button <Button
size="SM" size="SM"
@ -278,7 +270,7 @@ export default function SettingsAccessIndexRoute() {
onClick={handleCustomTlsUpdate} onClick={handleCustomTlsUpdate}
/> />
</div> </div>
</div> </NestedSettingsGroup>
)} )}
<SettingsItem <SettingsItem
@ -352,7 +344,7 @@ export default function SettingsAccessIndexRoute() {
</SettingsItem> </SettingsItem>
{selectedProvider === "custom" && ( {selectedProvider === "custom" && (
<div className="mt-4 space-y-4"> <NestedSettingsGroup className="mt-4">
<div className="flex items-end gap-x-2"> <div className="flex items-end gap-x-2">
<InputFieldWithLabel <InputFieldWithLabel
size="SM" size="SM"
@ -371,7 +363,7 @@ export default function SettingsAccessIndexRoute() {
placeholder="https://app.example.com" placeholder="https://app.example.com"
/> />
</div> </div>
</div> </NestedSettingsGroup>
)} )}
</> </>
)} )}

View File

@ -8,6 +8,7 @@ import { ConfirmDialog } from "@components/ConfirmDialog";
import { GridCard } from "@components/Card"; import { GridCard } from "@components/Card";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { NestedSettingsGroup } from "@components/NestedSettingsGroup";
import { TextAreaWithLabel } from "@components/TextArea"; import { TextAreaWithLabel } from "@components/TextArea";
import { isOnDevice } from "@/main"; import { isOnDevice } from "@/main";
import notifications from "@/notifications"; import notifications from "@/notifications";
@ -201,41 +202,69 @@ export default function SettingsAdvancedRoute() {
onChange={e => handleDevModeChange(e.target.checked)} onChange={e => handleDevModeChange(e.target.checked)}
/> />
</SettingsItem> </SettingsItem>
{settings.developerMode ? (
{settings.developerMode && ( <NestedSettingsGroup>
<GridCard> <GridCard>
<div className="flex items-start gap-x-4 p-4 select-none"> <div className="flex items-start gap-x-4 p-4 select-none">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
className="mt-1 h-8 w-8 shrink-0 text-amber-600 dark:text-amber-500" className="mt-1 h-8 w-8 shrink-0 text-amber-600 dark:text-amber-500"
> >
<path <path
fillRule="evenodd" fillRule="evenodd"
d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z"
clipRule="evenodd" clipRule="evenodd"
/> />
</svg> </svg>
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-base font-bold text-slate-900 dark:text-white"> <h3 className="text-base font-bold text-slate-900 dark:text-white">
{m.advanced_developer_mode_enabled_title()} {m.advanced_developer_mode_enabled_title()}
</h3> </h3>
<div> <div>
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300"> <ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
<li>{m.advanced_developer_mode_warning_security()}</li> <li>{m.advanced_developer_mode_warning_security()}</li>
<li>{m.advanced_developer_mode_warning_risks()}</li> <li>{m.advanced_developer_mode_warning_risks()}</li>
</ul> </ul>
</div>
</div>
<div className="text-xs text-slate-700 dark:text-slate-300">
{m.advanced_developer_mode_warning_advanced()}
</div> </div>
</div> </div>
<div className="text-xs text-slate-700 dark:text-slate-300"> </div>
{m.advanced_developer_mode_warning_advanced()} </GridCard>
{isOnDevice && (
<div className="space-y-4">
<SettingsItem
title={m.advanced_ssh_access_title()}
description={m.advanced_ssh_access_description()}
/>
<TextAreaWithLabel
label={m.advanced_ssh_public_key_label()}
value={sshKey || ""}
rows={3}
onChange={e => setSSHKey(e.target.value)}
placeholder={m.advanced_ssh_public_key_placeholder()}
/>
<p className="text-xs text-slate-600 dark:text-slate-400">
{m.advanced_ssh_default_user()}<strong>root</strong>.
</p>
<div className="flex items-center gap-x-2">
<Button
size="SM"
theme="primary"
text={m.advanced_update_ssh_key_button()}
onClick={handleUpdateSSHKey}
/>
</div> </div>
</div> </div>
</div> )}
</GridCard> </NestedSettingsGroup>
)} ) : null}
<SettingsItem <SettingsItem
title={m.advanced_loopback_only_title()} title={m.advanced_loopback_only_title()}
@ -247,34 +276,7 @@ export default function SettingsAdvancedRoute() {
/> />
</SettingsItem> </SettingsItem>
{isOnDevice && settings.developerMode && (
<div className="space-y-4">
<SettingsItem
title={m.advanced_ssh_access_title()}
description={m.advanced_ssh_access_description()}
/>
<div className="space-y-4">
<TextAreaWithLabel
label={m.advanced_ssh_public_key_label()}
value={sshKey || ""}
rows={3}
onChange={e => setSSHKey(e.target.value)}
placeholder={m.advanced_ssh_public_key_placeholder()}
/>
<p className="text-xs text-slate-600 dark:text-slate-400">
{m.advanced_ssh_default_user()}<strong>root</strong>.
</p>
<div className="flex items-center gap-x-2">
<Button
size="SM"
theme="primary"
text={m.advanced_update_ssh_key_button()}
onClick={handleUpdateSSHKey}
/>
</div>
</div>
</div>
)}
<SettingsItem <SettingsItem
title={m.advanced_troubleshooting_mode_title()} title={m.advanced_troubleshooting_mode_title()}
@ -289,7 +291,7 @@ export default function SettingsAdvancedRoute() {
</SettingsItem> </SettingsItem>
{settings.debugMode && ( {settings.debugMode && (
<> <NestedSettingsGroup>
<SettingsItem <SettingsItem
title={m.advanced_usb_emulation_title()} title={m.advanced_usb_emulation_title()}
description={m.advanced_usb_emulation_description()} description={m.advanced_usb_emulation_description()}
@ -320,7 +322,7 @@ export default function SettingsAdvancedRoute() {
}} }}
/> />
</SettingsItem> </SettingsItem>
</> </NestedSettingsGroup>
)} )}
</div> </div>

View File

@ -12,13 +12,11 @@ import notifications from "@/notifications";
import { getLocale, setLocale, locales, baseLocale } from '@localizations/runtime.js'; import { getLocale, setLocale, locales, baseLocale } from '@localizations/runtime.js';
import { m } from "@localizations/messages.js"; import { m } from "@localizations/messages.js";
import { deleteCookie, map_locale_code_to_name } from "@/utils"; import { deleteCookie, map_locale_code_to_name } from "@/utils";
import { useVersion } from "@hooks/useVersion";
export default function SettingsGeneralRoute() { export default function SettingsGeneralRoute() {
const { send } = useJsonRpc(); const { send } = useJsonRpc();
const { navigateTo } = useDeviceUiNavigation(); const { navigateTo } = useDeviceUiNavigation();
const [autoUpdate, setAutoUpdate] = useState(true); const [autoUpdate, setAutoUpdate] = useState(true);
const { isOnDevVersion } = useVersion();
const currentVersions = useDeviceStore(state => { const currentVersions = useDeviceStore(state => {
const { appVersion, systemVersion } = state; const { appVersion, systemVersion } = state;
if (!appVersion || !systemVersion) return null; if (!appVersion || !systemVersion) return null;
@ -75,10 +73,6 @@ export default function SettingsGeneralRoute() {
notifications.success(m.locale_change_success({ locale: validLocale || m.locale_auto() })); notifications.success(m.locale_change_success({ locale: validLocale || m.locale_auto() }));
}; };
const downgradeAvailable = useMemo(() => {
return isOnDevVersion;
}, [isOnDevVersion]);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
@ -114,12 +108,6 @@ export default function SettingsGeneralRoute() {
} }
/> />
<div className="flex items-center justify-start gap-x-2"> <div className="flex items-center justify-start gap-x-2">
{downgradeAvailable && <Button
size="SM"
theme="danger"
text={m.general_check_for_stable_updates()}
onClick={() => navigateTo("./update?channel=stable")}
/>}
<Button <Button
size="SM" size="SM"
theme="light" theme="light"

View File

@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useLocation, useNavigate, useSearchParams } from "react-router"; import { useLocation, useNavigate } from "react-router";
import { useJsonRpc } from "@hooks/useJsonRpc"; import { useJsonRpc } from "@hooks/useJsonRpc";
import { UpdateState, useUpdateStore } from "@hooks/stores"; import { UpdateState, useUpdateStore } from "@hooks/stores";

View File

@ -8,6 +8,7 @@ import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { SettingsSectionHeader } from "@components/SettingsSectionHeader"; import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import { NestedSettingsGroup } from "@components/NestedSettingsGroup";
import { UsbDeviceSetting } from "@components/UsbDeviceSetting"; import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
import { UsbInfoSetting } from "@components/UsbInfoSetting"; import { UsbInfoSetting } from "@components/UsbInfoSetting";
import notifications from "@/notifications"; import notifications from "@/notifications";
@ -156,7 +157,7 @@ export default function SettingsHardwareRoute() {
/> />
</SettingsItem> </SettingsItem>
{backlightSettings.max_brightness != 0 && ( {backlightSettings.max_brightness != 0 && (
<> <NestedSettingsGroup>
<SettingsItem <SettingsItem
title={m.hardware_dim_display_after_title()} title={m.hardware_dim_display_after_title()}
description={m.hardware_dim_display_after_description()} description={m.hardware_dim_display_after_description()}
@ -198,7 +199,7 @@ export default function SettingsHardwareRoute() {
}} }}
/> />
</SettingsItem> </SettingsItem>
</> </NestedSettingsGroup>
)} )}
<p className="text-xs text-slate-600 dark:text-slate-400"> <p className="text-xs text-slate-600 dark:text-slate-400">
{m.hardware_display_wake_up_note()} {m.hardware_display_wake_up_note()}

View File

@ -7,6 +7,7 @@ import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { SelectMenuBasic } from "@components/SelectMenuBasic"; import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { NestedSettingsGroup } from "@components/NestedSettingsGroup";
import Fieldset from "@components/Fieldset"; import Fieldset from "@components/Fieldset";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { m } from "@localizations/messages.js"; import { m } from "@localizations/messages.js";
@ -174,7 +175,7 @@ export default function SettingsVideoRoute() {
description={m.video_enhancement_description()} description={m.video_enhancement_description()}
/> />
<div className="space-y-4 pl-4"> <NestedSettingsGroup>
<SettingsItem <SettingsItem
title={m.video_saturation_title()} title={m.video_saturation_title()}
description={m.video_saturation_description({ value: videoSaturation.toFixed(1) })} description={m.video_saturation_description({ value: videoSaturation.toFixed(1) })}
@ -232,7 +233,7 @@ export default function SettingsVideoRoute() {
}} }}
/> />
</div> </div>
</div> </NestedSettingsGroup>
<Fieldset disabled={edidLoading} className="space-y-2"> <Fieldset disabled={edidLoading} className="space-y-2">
<SettingsItem <SettingsItem
title={m.video_edid_title()} title={m.video_edid_title()}