From a4ad93008b4faa9d94b8bdd455dc825fdf623e05 Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Mon, 1 Jun 2026 14:57:31 +0200 Subject: [PATCH] add skeleton to launch netbird ui at login and own context --- .../components/switches/FancyToggleSwitch.tsx | 42 +++++++++++++ .../frontend/src/contexts/SettingsContext.tsx | 59 ++++++++++++++++++- .../src/modules/settings/SettingsGeneral.tsx | 59 ++++--------------- .../src/modules/settings/SettingsPage.tsx | 7 ++- 4 files changed, 119 insertions(+), 48 deletions(-) diff --git a/client/ui/frontend/src/components/switches/FancyToggleSwitch.tsx b/client/ui/frontend/src/components/switches/FancyToggleSwitch.tsx index 9bdfaaff3..5d92883d0 100644 --- a/client/ui/frontend/src/components/switches/FancyToggleSwitch.tsx +++ b/client/ui/frontend/src/components/switches/FancyToggleSwitch.tsx @@ -11,6 +11,7 @@ interface Props { label?: React.ReactNode; children?: React.ReactNode; disabled?: boolean; + loading?: boolean; dataCy?: string; className?: string; labelClassName?: string; @@ -24,11 +25,52 @@ export default function FancyToggleSwitch({ label, children, disabled = false, + loading = false, dataCy, className, labelClassName, textWrapperClassName = "max-w-lg", }: Readonly) { + if (loading) { + // Match the global SkeletonTheme in app.tsx (#25282d base / + // #33373e highlight) so the loading row blends in with + // SettingsSkeleton. box-decoration-clone gives every wrapped line + // of text its own rounded corners instead of just the first/last. + const shimmer = + "text-transparent select-none rounded bg-[#25282d] box-decoration-clone animate-pulse"; + return ( +
+
+
+ + + + {helpText} + + +
+
+
+
+
+
+ ); + } + const handleToggle = () => { if (disabled) return; onChange(!value); diff --git a/client/ui/frontend/src/contexts/SettingsContext.tsx b/client/ui/frontend/src/contexts/SettingsContext.tsx index 3df070e11..86eed4cd0 100644 --- a/client/ui/frontend/src/contexts/SettingsContext.tsx +++ b/client/ui/frontend/src/contexts/SettingsContext.tsx @@ -8,7 +8,7 @@ import { type ReactNode, } from "react"; import { Dialogs } from "@wailsio/runtime"; -import { Settings as SettingsSvc } from "@bindings/services"; +import { Autostart, Settings as SettingsSvc } from "@bindings/services"; import type { Config } from "@bindings/services/models.js"; import i18next from "@/lib/i18n"; import { useProfile } from "@/contexts/ProfileContext.tsx"; @@ -17,6 +17,8 @@ import { formatErrorMessage as errorMessage } from "@/lib/errors.ts"; const SAVE_DEBOUNCE_MS = 400; +export type AutostartState = { supported: boolean; enabled: boolean }; + type SettingsContextValue = { config: Config; setField: (k: K, v: Config[K]) => void; @@ -25,7 +27,13 @@ type SettingsContextValue = { saveNow: () => Promise; }; +type AutostartContextValue = { + autostart: AutostartState | null; + setAutostartEnabled: (enabled: boolean) => Promise; +}; + const SettingsContext = createContext(null); +const AutostartContext = createContext(null); export const useSettings = () => { const ctx = useContext(SettingsContext); @@ -35,6 +43,16 @@ export const useSettings = () => { return ctx; }; +export const useAutostartSetting = () => { + const ctx = useContext(AutostartContext); + if (!ctx) { + throw new Error( + "useAutostartSetting must be used inside AutostartSettingsProvider", + ); + } + return ctx; +}; + const useSettingsState = () => { const { username, activeProfile, loaded: profileLoaded } = useProfile(); const [config, setConfig] = useState(null); @@ -167,3 +185,42 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
); }; + +export const AutostartSettingsProvider = ({ children }: { children: ReactNode }) => { + const [autostart, setAutostart] = useState(null); + + useEffect(() => { + let cancelled = false; + (async () => { + const supported = await Autostart.Supported(); + const enabled = supported ? await Autostart.IsEnabled() : false; + if (cancelled) return; + setAutostart({ supported, enabled }); + })().catch(() => { + if (cancelled) return; + setAutostart({ supported: false, enabled: false }); + }); + return () => { + cancelled = true; + }; + }, []); + + const setAutostartEnabled = useCallback(async (enabled: boolean) => { + setAutostart((s) => (s ? { ...s, enabled } : s)); + try { + await Autostart.SetEnabled(enabled); + } catch (e) { + setAutostart((s) => (s ? { ...s, enabled: !enabled } : s)); + await Dialogs.Error({ + Title: i18next.t("settings.general.autostart.errorTitle"), + Message: errorMessage(e), + }); + } + }, []); + + return ( + + {children} + + ); +}; diff --git a/client/ui/frontend/src/modules/settings/SettingsGeneral.tsx b/client/ui/frontend/src/modules/settings/SettingsGeneral.tsx index 1f60b01d8..2976bd316 100644 --- a/client/ui/frontend/src/modules/settings/SettingsGeneral.tsx +++ b/client/ui/frontend/src/modules/settings/SettingsGeneral.tsx @@ -1,56 +1,22 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; -import { Dialogs } from "@wailsio/runtime"; import { Button } from "@/components/buttons/Button"; import FancyToggleSwitch from "@/components/switches/FancyToggleSwitch"; import { HelpText } from "@/components/typography/HelpText"; import { Input } from "@/components/inputs/Input"; import { Label } from "@/components/typography/Label"; import { SectionGroup } from "@/modules/settings/SettingsSection.tsx"; -import { useSettings } from "@/contexts/SettingsContext.tsx"; +import { useAutostartSetting, useSettings } from "@/contexts/SettingsContext.tsx"; import { ManagementServerSwitch } from "@/components/ManagementServerSwitch.tsx"; import { ManagementMode, useManagementUrl } from "@/hooks/useManagementUrl.ts"; import { LanguagePicker } from "@/components/LanguagePicker.tsx"; -import { Autostart } from "@bindings/services"; -import i18next from "@/lib/i18n"; export function SettingsGeneral() { const { t } = useTranslation(); const { config, setField } = useSettings(); + const { autostart, setAutostartEnabled } = useAutostartSetting(); const { mode, setMode, setUrl, displayUrl, showError, canSave, save } = useManagementUrl(); - // Autostart lives in the OS login-item registry, not the daemon config, so - // it has its own read-on-mount state. supported gates whether we render the - // toggle at all (false on server/mobile builds). - const [autostartSupported, setAutostartSupported] = useState(false); - const [autostartEnabled, setAutostartEnabled] = useState(false); - useEffect(() => { - let cancelled = false; - (async () => { - const supported = await Autostart.Supported(); - if (cancelled) return; - setAutostartSupported(supported); - if (!supported) return; - setAutostartEnabled(await Autostart.IsEnabled()); - })().catch(() => {}); - return () => { - cancelled = true; - }; - }, []); - - const onAutostartChange = async (enabled: boolean) => { - setAutostartEnabled(enabled); - try { - await Autostart.SetEnabled(enabled); - } catch (e) { - setAutostartEnabled(!enabled); - await Dialogs.Error({ - Title: i18next.t("settings.general.autostart.errorTitle"), - Message: String(e), - }); - } - }; - const inputRef = useRef(null); const prevMode = useRef(mode); useEffect(() => { @@ -67,22 +33,23 @@ export function SettingsGeneral() { <> - setField("disableAutoConnect", !v)} - label={t("settings.general.connectOnStartup.label")} - helpText={t("settings.general.connectOnStartup.help")} - /> setField("disableNotifications", !v)} label={t("settings.general.notifications.label")} helpText={t("settings.general.notifications.help")} /> - {autostartSupported && ( + setField("disableAutoConnect", !v)} + label={t("settings.general.connectOnStartup.label")} + helpText={t("settings.general.connectOnStartup.help")} + /> + {(autostart === null || autostart.supported) && ( diff --git a/client/ui/frontend/src/modules/settings/SettingsPage.tsx b/client/ui/frontend/src/modules/settings/SettingsPage.tsx index 2d80e9e35..6c3950d37 100644 --- a/client/ui/frontend/src/modules/settings/SettingsPage.tsx +++ b/client/ui/frontend/src/modules/settings/SettingsPage.tsx @@ -7,7 +7,10 @@ import { isMacOS } from "@/lib/platform"; import { AppRightPanel } from "@/layouts/AppRightPanel.tsx"; import { VerticalTabs } from "@/components/VerticalTabs.tsx"; import { SettingsNavigation } from "@/modules/settings/SettingsNavigation.tsx"; -import { SettingsProvider } from "@/contexts/SettingsContext.tsx"; +import { + AutostartSettingsProvider, + SettingsProvider, +} from "@/contexts/SettingsContext.tsx"; import { SettingsGeneral } from "@/modules/settings/SettingsGeneral.tsx"; import { SettingsNetwork } from "@/modules/settings/SettingsNetwork.tsx"; import { SettingsSecurity } from "@/modules/settings/SettingsSecurity.tsx"; @@ -66,6 +69,7 @@ export const SettingsPage = () => { > + { /> +