add skeleton to launch netbird ui at login and own context

This commit is contained in:
Eduard Gert
2026-06-01 14:57:31 +02:00
parent 101e04f9fb
commit a4ad93008b
4 changed files with 119 additions and 48 deletions

View File

@@ -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<Props>) {
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 (
<div
className={cn("inline-block text-left w-full", className)}
aria-busy
>
<div className={"flex justify-between gap-10"}>
<div className={cn(textWrapperClassName)}>
<Label className={labelClassName}>
<span className={shimmer}>{label}</span>
</Label>
<HelpText margin={false}>
<span
className={cn(
shimmer,
"text-[0.6rem] leading-relaxed",
)}
>
{helpText}
</span>
</HelpText>
</div>
<div className={"mt-2 pr-1"}>
<div
className={
"h-[24px] w-[44px] rounded-full bg-[#25282d] animate-pulse"
}
/>
</div>
</div>
</div>
);
}
const handleToggle = () => {
if (disabled) return;
onChange(!value);

View File

@@ -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 extends keyof Config>(k: K, v: Config[K]) => void;
@@ -25,7 +27,13 @@ type SettingsContextValue = {
saveNow: () => Promise<void>;
};
type AutostartContextValue = {
autostart: AutostartState | null;
setAutostartEnabled: (enabled: boolean) => Promise<void>;
};
const SettingsContext = createContext<SettingsContextValue | null>(null);
const AutostartContext = createContext<AutostartContextValue | null>(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<Config | null>(null);
@@ -167,3 +185,42 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
</div>
);
};
export const AutostartSettingsProvider = ({ children }: { children: ReactNode }) => {
const [autostart, setAutostart] = useState<AutostartState | null>(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 (
<AutostartContext.Provider value={{ autostart, setAutostartEnabled }}>
{children}
</AutostartContext.Provider>
);
};

View File

@@ -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<HTMLInputElement>(null);
const prevMode = useRef(mode);
useEffect(() => {
@@ -67,22 +33,23 @@ export function SettingsGeneral() {
<>
<SectionGroup title={t("settings.general.section.general")}>
<LanguagePicker />
<FancyToggleSwitch
value={!config.disableAutoConnect}
onChange={(v) => setField("disableAutoConnect", !v)}
label={t("settings.general.connectOnStartup.label")}
helpText={t("settings.general.connectOnStartup.help")}
/>
<FancyToggleSwitch
value={!config.disableNotifications}
onChange={(v) => setField("disableNotifications", !v)}
label={t("settings.general.notifications.label")}
helpText={t("settings.general.notifications.help")}
/>
{autostartSupported && (
<FancyToggleSwitch
value={!config.disableAutoConnect}
onChange={(v) => setField("disableAutoConnect", !v)}
label={t("settings.general.connectOnStartup.label")}
helpText={t("settings.general.connectOnStartup.help")}
/>
{(autostart === null || autostart.supported) && (
<FancyToggleSwitch
value={autostartEnabled}
onChange={onAutostartChange}
value={autostart?.enabled ?? false}
onChange={setAutostartEnabled}
loading={autostart === null}
label={t("settings.general.autostart.label")}
helpText={t("settings.general.autostart.help")}
/>

View File

@@ -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 = () => {
>
<SettingsNavigation />
<AppRightPanel>
<AutostartSettingsProvider>
<ScrollArea.Root
key={active}
type={"auto"}
@@ -117,6 +121,7 @@ export const SettingsPage = () => {
/>
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</AutostartSettingsProvider>
</AppRightPanel>
</VerticalTabs>
</>