mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-21 04:49:10 +02:00
add profile switch into settings, scroll to top after profile switch, add netbird icon to linux windows
This commit is contained in:
@@ -11,6 +11,19 @@ type Props = {
|
|||||||
onCreate: (name: string) => void;
|
onCreate: (name: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mirror of the daemon's profilemanager.sanitizeProfileName rule
|
||||||
|
// (client/internal/profilemanager/profilemanager.go): only letters, digits,
|
||||||
|
// `_` and `-` survive on the Go side. We additionally lowercase and convert
|
||||||
|
// spaces to `-` so what the user sees in the input is exactly what the
|
||||||
|
// daemon will store — otherwise the daemon silently sanitizes ("my profile"
|
||||||
|
// → "myprofile") while the UI keeps the raw name in flight, which spawns a
|
||||||
|
// ghost row and breaks subsequent delete.
|
||||||
|
const sanitizeProfileInput = (value: string): string =>
|
||||||
|
value
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, "-")
|
||||||
|
.replace(/[^a-z0-9_-]/g, "");
|
||||||
|
|
||||||
export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) => {
|
export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
@@ -26,18 +39,18 @@ export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) =>
|
|||||||
|
|
||||||
const handleSubmit = (e: FormEvent) => {
|
const handleSubmit = (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const trimmed = name.trim();
|
const sanitized = sanitizeProfileInput(name);
|
||||||
if (trimmed.length === 0) {
|
if (sanitized.length === 0) {
|
||||||
setError(t("profile.dialog.required"));
|
setError(t("profile.dialog.required"));
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onCreate(trimmed);
|
onCreate(sanitized);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (value: string) => {
|
const handleChange = (value: string) => {
|
||||||
setName(value);
|
setName(sanitizeProfileInput(value));
|
||||||
if (error) setError(null);
|
if (error) setError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,6 +73,10 @@ export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) =>
|
|||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => handleChange(e.target.value)}
|
onChange={(e) => handleChange(e.target.value)}
|
||||||
error={error ?? undefined}
|
error={error ?? undefined}
|
||||||
|
maxLength={64}
|
||||||
|
spellCheck={false}
|
||||||
|
autoComplete="off"
|
||||||
|
autoCapitalize="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useLayoutEffect, useRef, useState } from "react";
|
import { useLayoutEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { errorDialog, warningDialog } from "@/lib/dialogs.ts";
|
import { errorDialog, warningDialog } from "@/lib/dialogs.ts";
|
||||||
import { CircleMinus, PlusCircle, Trash2, UserCircle } from "lucide-react";
|
import { CircleMinus, LogIn, PlusCircle, Trash2, UserCircle } from "lucide-react";
|
||||||
import type { Profile } from "@bindings/services/models.js";
|
import type { Profile } from "@bindings/services/models.js";
|
||||||
import { Badge } from "@/components/Badge";
|
import { Badge } from "@/components/Badge";
|
||||||
import { Button } from "@/components/buttons/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
@@ -31,6 +31,20 @@ export function ProfilesTab() {
|
|||||||
|
|
||||||
const [newOpen, setNewOpen] = useState(false);
|
const [newOpen, setNewOpen] = useState(false);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
const tabRootRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// After a successful switch we want to bring the user back to the top of
|
||||||
|
// the tab — the table re-sorts the new active profile to the row 0 and a
|
||||||
|
// user who scrolled to find a target down the list would otherwise lose
|
||||||
|
// visual anchoring. Settings is hosted inside a Radix ScrollArea so we
|
||||||
|
// walk up to the viewport (it owns the actual overflow) instead of
|
||||||
|
// `window.scrollTo`, which is a no-op here.
|
||||||
|
const scrollTabToTop = () => {
|
||||||
|
const el = tabRootRef.current?.closest<HTMLElement>(
|
||||||
|
"[data-radix-scroll-area-viewport]",
|
||||||
|
);
|
||||||
|
el?.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
const sorted = [...profiles].sort((a, b) => {
|
const sorted = [...profiles].sort((a, b) => {
|
||||||
if (a.name === activeProfile) return -1;
|
if (a.name === activeProfile) return -1;
|
||||||
@@ -53,6 +67,22 @@ export function ProfilesTab() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSwitch = async (name: string) => {
|
||||||
|
const cancelLabel = i18next.t("common.cancel");
|
||||||
|
const confirmLabel = i18next.t("profile.switch.confirm");
|
||||||
|
const result = await warningDialog({
|
||||||
|
Title: i18next.t("profile.switch.title"),
|
||||||
|
Message: i18next.t("profile.switch.message", { name }),
|
||||||
|
Buttons: [
|
||||||
|
{ Label: cancelLabel, IsCancel: true },
|
||||||
|
{ Label: confirmLabel, IsDefault: true },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (result !== confirmLabel) return;
|
||||||
|
await guarded(i18next.t("profile.error.switchTitle"), () => switchProfile(name));
|
||||||
|
scrollTabToTop();
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeregister = async (name: string) => {
|
const handleDeregister = async (name: string) => {
|
||||||
const cancelLabel = i18next.t("common.cancel");
|
const cancelLabel = i18next.t("common.cancel");
|
||||||
const confirmLabel = i18next.t("profile.deregister.confirm");
|
const confirmLabel = i18next.t("profile.deregister.confirm");
|
||||||
@@ -97,7 +127,7 @@ export function ProfilesTab() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div ref={tabRootRef}>
|
||||||
<SectionGroup title={t("settings.profiles.section.profiles")}>
|
<SectionGroup title={t("settings.profiles.section.profiles")}>
|
||||||
<HelpText className={"-mt-2 mb-0"}>{t("settings.profiles.intro")}</HelpText>
|
<HelpText className={"-mt-2 mb-0"}>{t("settings.profiles.intro")}</HelpText>
|
||||||
|
|
||||||
@@ -113,6 +143,7 @@ export function ProfilesTab() {
|
|||||||
key={profile.name}
|
key={profile.name}
|
||||||
profile={profile}
|
profile={profile}
|
||||||
isActive={profile.name === activeProfile}
|
isActive={profile.name === activeProfile}
|
||||||
|
onSwitch={() => handleSwitch(profile.name)}
|
||||||
onDeregister={() => handleDeregister(profile.name)}
|
onDeregister={() => handleDeregister(profile.name)}
|
||||||
onDelete={() => handleDelete(profile.name)}
|
onDelete={() => handleDelete(profile.name)}
|
||||||
/>
|
/>
|
||||||
@@ -146,18 +177,19 @@ export function ProfilesTab() {
|
|||||||
</SectionGroup>
|
</SectionGroup>
|
||||||
|
|
||||||
<ProfileCreationModal open={newOpen} onOpenChange={setNewOpen} onCreate={handleCreate} />
|
<ProfileCreationModal open={newOpen} onOpenChange={setNewOpen} onCreate={handleCreate} />
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfileRowProps = {
|
type ProfileRowProps = {
|
||||||
profile: Profile;
|
profile: Profile;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
onSwitch: () => void;
|
||||||
onDeregister: () => void;
|
onDeregister: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProfileRow = ({ profile, isActive, onDeregister, onDelete }: ProfileRowProps) => {
|
const ProfileRow = ({ profile, isActive, onSwitch, onDeregister, onDelete }: ProfileRowProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const Icon = pickProfileIcon(profile.name) ?? UserCircle;
|
const Icon = pickProfileIcon(profile.name) ?? UserCircle;
|
||||||
const showEmail = !!profile.email;
|
const showEmail = !!profile.email;
|
||||||
@@ -192,8 +224,11 @@ const ProfileRow = ({ profile, isActive, onDeregister, onDelete }: ProfileRowPro
|
|||||||
</td>
|
</td>
|
||||||
<td className={"px-4 py-2.5 text-right align-middle"}>
|
<td className={"px-4 py-2.5 text-right align-middle"}>
|
||||||
<RowActions
|
<RowActions
|
||||||
|
canSwitch={!isActive}
|
||||||
canDeregister={!!profile.email}
|
canDeregister={!!profile.email}
|
||||||
canDelete={profile.name !== DEFAULT_PROFILE}
|
isDefault={profile.name === DEFAULT_PROFILE}
|
||||||
|
isActive={isActive}
|
||||||
|
onSwitch={onSwitch}
|
||||||
onDeregister={onDeregister}
|
onDeregister={onDeregister}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
@@ -222,14 +257,31 @@ const TruncatedEmail = ({ email }: { email: string }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type RowActionsProps = {
|
type RowActionsProps = {
|
||||||
|
canSwitch: boolean;
|
||||||
canDeregister: boolean;
|
canDeregister: boolean;
|
||||||
canDelete: boolean;
|
isDefault: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
onSwitch: () => void;
|
||||||
onDeregister: () => void;
|
onDeregister: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const RowActions = ({ canDeregister, canDelete, onDeregister, onDelete }: RowActionsProps) => {
|
const RowActions = ({
|
||||||
|
canSwitch,
|
||||||
|
canDeregister,
|
||||||
|
isDefault,
|
||||||
|
isActive,
|
||||||
|
onSwitch,
|
||||||
|
onDeregister,
|
||||||
|
onDelete,
|
||||||
|
}: RowActionsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const deleteDisabled = isDefault || isActive;
|
||||||
|
const deleteLabel = isDefault
|
||||||
|
? t("profile.delete.disabledDefault")
|
||||||
|
: isActive
|
||||||
|
? t("profile.delete.disabledActive")
|
||||||
|
: t("profile.selector.delete");
|
||||||
return (
|
return (
|
||||||
<div className={"inline-flex items-center gap-1"}>
|
<div className={"inline-flex items-center gap-1"}>
|
||||||
<ActionIconButton
|
<ActionIconButton
|
||||||
@@ -239,11 +291,17 @@ const RowActions = ({ canDeregister, canDelete, onDeregister, onDelete }: RowAct
|
|||||||
hidden={!canDeregister}
|
hidden={!canDeregister}
|
||||||
/>
|
/>
|
||||||
<ActionIconButton
|
<ActionIconButton
|
||||||
label={t("profile.selector.delete")}
|
label={deleteLabel}
|
||||||
icon={Trash2}
|
icon={Trash2}
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
variant={"danger"}
|
variant={"danger"}
|
||||||
hidden={!canDelete}
|
disabled={deleteDisabled}
|
||||||
|
/>
|
||||||
|
<ActionIconButton
|
||||||
|
label={t("profile.selector.switchTo")}
|
||||||
|
icon={LogIn}
|
||||||
|
onClick={onSwitch}
|
||||||
|
hidden={!canSwitch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -257,6 +315,8 @@ type ActionIconButtonProps = {
|
|||||||
/** When true the button still occupies space (preserves row layout)
|
/** When true the button still occupies space (preserves row layout)
|
||||||
* but is invisible and non-interactive. */
|
* but is invisible and non-interactive. */
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
|
/** When true the button is visible but non-interactive (greyed out). */
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ActionIconButton = ({
|
const ActionIconButton = ({
|
||||||
@@ -265,13 +325,15 @@ const ActionIconButton = ({
|
|||||||
onClick,
|
onClick,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
hidden = false,
|
hidden = false,
|
||||||
|
disabled = false,
|
||||||
}: ActionIconButtonProps) => {
|
}: ActionIconButtonProps) => {
|
||||||
const button = (
|
const button = (
|
||||||
<button
|
<button
|
||||||
type={"button"}
|
type={"button"}
|
||||||
onClick={onClick}
|
onClick={disabled ? undefined : onClick}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
aria-hidden={hidden || undefined}
|
aria-hidden={hidden || undefined}
|
||||||
|
aria-disabled={disabled || undefined}
|
||||||
tabIndex={hidden ? -1 : undefined}
|
tabIndex={hidden ? -1 : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-9 w-9 inline-flex items-center justify-center rounded-md cursor-default outline-none",
|
"h-9 w-9 inline-flex items-center justify-center rounded-md cursor-default outline-none",
|
||||||
@@ -280,6 +342,7 @@ const ActionIconButton = ({
|
|||||||
? "text-nb-gray-400 hover:text-red-500 hover:bg-red-500/10"
|
? "text-nb-gray-400 hover:text-red-500 hover:bg-red-500/10"
|
||||||
: "text-nb-gray-400 hover:text-nb-gray-100 hover:bg-nb-gray-900",
|
: "text-nb-gray-400 hover:text-nb-gray-100 hover:bg-nb-gray-900",
|
||||||
hidden && "opacity-0 pointer-events-none",
|
hidden && "opacity-0 pointer-events-none",
|
||||||
|
disabled && "opacity-40 cursor-not-allowed hover:!text-nb-gray-400 hover:!bg-transparent",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon size={16} />
|
<Icon size={16} />
|
||||||
@@ -287,7 +350,12 @@ const ActionIconButton = ({
|
|||||||
);
|
);
|
||||||
if (hidden) return button;
|
if (hidden) return button;
|
||||||
return (
|
return (
|
||||||
<Tooltip content={label} side={"top"}>
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<span className={"block max-w-[260px] leading-snug"}>{label}</span>
|
||||||
|
}
|
||||||
|
side={"top"}
|
||||||
|
>
|
||||||
{button}
|
{button}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -110,11 +110,16 @@
|
|||||||
"profile.dialog.description": "Mit Profilen können Sie mehrere NetBird-Verbindungen nebeneinander verwalten. Geben Sie Ihrem Profil einen aussagekräftigen Namen.",
|
"profile.dialog.description": "Mit Profilen können Sie mehrere NetBird-Verbindungen nebeneinander verwalten. Geben Sie Ihrem Profil einen aussagekräftigen Namen.",
|
||||||
"profile.dialog.placeholder": "z. B. Arbeit",
|
"profile.dialog.placeholder": "z. B. Arbeit",
|
||||||
|
|
||||||
|
"profile.switch.title": "Profil wechseln",
|
||||||
|
"profile.switch.message": "Zu \"{name}\" wechseln? Ihr aktuelles Profil wird getrennt.",
|
||||||
|
"profile.switch.confirm": "Wechseln",
|
||||||
"profile.deregister.title": "Profil abmelden",
|
"profile.deregister.title": "Profil abmelden",
|
||||||
"profile.deregister.message": "Sind Sie sicher, dass Sie \"{name}\" abmelden möchten? Sie müssen sich erneut anmelden, um es zu nutzen.",
|
"profile.deregister.message": "Sind Sie sicher, dass Sie \"{name}\" abmelden möchten? Sie müssen sich erneut anmelden, um es zu nutzen.",
|
||||||
"profile.deregister.confirm": "Abmelden",
|
"profile.deregister.confirm": "Abmelden",
|
||||||
"profile.delete.title": "Profil löschen",
|
"profile.delete.title": "Profil löschen",
|
||||||
"profile.delete.message": "Sind Sie sicher, dass Sie \"{name}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
"profile.delete.message": "Sind Sie sicher, dass Sie \"{name}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"profile.delete.disabledActive": "Aktive Profile können nicht gelöscht werden. Wechseln Sie zu einem anderen Profil, bevor Sie dieses löschen.",
|
||||||
|
"profile.delete.disabledDefault": "Das Standardprofil kann nicht gelöscht werden.",
|
||||||
"profile.error.switchTitle": "Profilwechsel fehlgeschlagen",
|
"profile.error.switchTitle": "Profilwechsel fehlgeschlagen",
|
||||||
"profile.error.deregisterTitle": "Abmeldung fehlgeschlagen",
|
"profile.error.deregisterTitle": "Abmeldung fehlgeschlagen",
|
||||||
"profile.error.deleteTitle": "Löschen des Profils fehlgeschlagen",
|
"profile.error.deleteTitle": "Löschen des Profils fehlgeschlagen",
|
||||||
|
|||||||
@@ -105,23 +105,29 @@
|
|||||||
"profile.selector.moreOptions": "More options",
|
"profile.selector.moreOptions": "More options",
|
||||||
"profile.selector.deregister": "Deregister",
|
"profile.selector.deregister": "Deregister",
|
||||||
"profile.selector.delete": "Delete",
|
"profile.selector.delete": "Delete",
|
||||||
|
"profile.selector.switchTo": "Switch to this profile",
|
||||||
|
|
||||||
"profile.dialog.title": "Enter Profile Name",
|
"profile.dialog.title": "Enter Profile Name",
|
||||||
"profile.dialog.description": "Choose a memorable name.",
|
"profile.dialog.description": "Choose a memorable name.",
|
||||||
"profile.dialog.placeholder": "e.g. Work",
|
"profile.dialog.placeholder": "e.g. work",
|
||||||
"profile.dialog.submit": "Add Profile",
|
"profile.dialog.submit": "Add Profile",
|
||||||
"profile.dialog.required": "Please enter a profile name, e.g. Work, Home",
|
"profile.dialog.required": "Please enter a profile name, e.g. work, home",
|
||||||
|
|
||||||
"header.menu.settings": "Settings...",
|
"header.menu.settings": "Settings...",
|
||||||
"header.menu.defaultView": "Default View",
|
"header.menu.defaultView": "Default View",
|
||||||
"header.menu.advancedView": "Advanced View",
|
"header.menu.advancedView": "Advanced View",
|
||||||
"header.menu.updateAvailable": "Update Available",
|
"header.menu.updateAvailable": "Update Available",
|
||||||
|
|
||||||
|
"profile.switch.title": "Switch Profile",
|
||||||
|
"profile.switch.message": "Switch to \"{name}\"? Your current profile will be disconnected.",
|
||||||
|
"profile.switch.confirm": "Switch",
|
||||||
"profile.deregister.title": "Deregister Profile",
|
"profile.deregister.title": "Deregister Profile",
|
||||||
"profile.deregister.message": "Are you sure you want to deregister \"{name}\"? You will need to log in again to use it.",
|
"profile.deregister.message": "Are you sure you want to deregister \"{name}\"? You will need to log in again to use it.",
|
||||||
"profile.deregister.confirm": "Deregister",
|
"profile.deregister.confirm": "Deregister",
|
||||||
"profile.delete.title": "Delete Profile",
|
"profile.delete.title": "Delete Profile",
|
||||||
"profile.delete.message": "Are you sure you want to delete \"{name}\"? This action cannot be undone.",
|
"profile.delete.message": "Are you sure you want to delete \"{name}\"? This action cannot be undone.",
|
||||||
|
"profile.delete.disabledActive": "Active profiles cannot be deleted. Switch to a different one before deleting this profile.",
|
||||||
|
"profile.delete.disabledDefault": "The default profile cannot be deleted.",
|
||||||
"profile.error.switchTitle": "Switch Profile Failed",
|
"profile.error.switchTitle": "Switch Profile Failed",
|
||||||
"profile.error.deregisterTitle": "Deregister Profile Failed",
|
"profile.error.deregisterTitle": "Deregister Profile Failed",
|
||||||
"profile.error.deleteTitle": "Delete Profile Failed",
|
"profile.error.deleteTitle": "Delete Profile Failed",
|
||||||
|
|||||||
@@ -110,11 +110,16 @@
|
|||||||
"profile.dialog.description": "A profilok lehetővé teszik, hogy különálló NetBird-kapcsolatokat tartson egymás mellett. Adjon profiljának egy könnyen megjegyezhető nevet.",
|
"profile.dialog.description": "A profilok lehetővé teszik, hogy különálló NetBird-kapcsolatokat tartson egymás mellett. Adjon profiljának egy könnyen megjegyezhető nevet.",
|
||||||
"profile.dialog.placeholder": "pl. Munka",
|
"profile.dialog.placeholder": "pl. Munka",
|
||||||
|
|
||||||
|
"profile.switch.title": "Profilváltás",
|
||||||
|
"profile.switch.message": "Átvált erre: \"{name}\"? Az aktuális profilja le lesz választva.",
|
||||||
|
"profile.switch.confirm": "Váltás",
|
||||||
"profile.deregister.title": "Profil leválasztása",
|
"profile.deregister.title": "Profil leválasztása",
|
||||||
"profile.deregister.message": "Biztosan le szeretné választani a következőt: \"{name}\"? Újra be kell jelentkeznie a használatához.",
|
"profile.deregister.message": "Biztosan le szeretné választani a következőt: \"{name}\"? Újra be kell jelentkeznie a használatához.",
|
||||||
"profile.deregister.confirm": "Leválasztás",
|
"profile.deregister.confirm": "Leválasztás",
|
||||||
"profile.delete.title": "Profil törlése",
|
"profile.delete.title": "Profil törlése",
|
||||||
"profile.delete.message": "Biztosan törölni szeretné a következőt: \"{name}\"? Ez a művelet nem vonható vissza.",
|
"profile.delete.message": "Biztosan törölni szeretné a következőt: \"{name}\"? Ez a művelet nem vonható vissza.",
|
||||||
|
"profile.delete.disabledActive": "Aktív profilok nem törölhetők. Váltson másik profilra, mielőtt törölné ezt.",
|
||||||
|
"profile.delete.disabledDefault": "Az alapértelmezett profil nem törölhető.",
|
||||||
"profile.error.switchTitle": "Profilváltás sikertelen",
|
"profile.error.switchTitle": "Profilváltás sikertelen",
|
||||||
"profile.error.deregisterTitle": "Leválasztás sikertelen",
|
"profile.error.deregisterTitle": "Leválasztás sikertelen",
|
||||||
"profile.error.deleteTitle": "Profil törlése sikertelen",
|
"profile.error.deleteTitle": "Profil törlése sikertelen",
|
||||||
|
|||||||
+1
-1
@@ -148,7 +148,7 @@ func main() {
|
|||||||
// (BrowserLogin, Session*, InstallProgress) stay lazy + destroy-on-close
|
// (BrowserLogin, Session*, InstallProgress) stay lazy + destroy-on-close
|
||||||
// so they don't linger as hidden windows that Wails's macOS dock-reopen
|
// so they don't linger as hidden windows that Wails's macOS dock-reopen
|
||||||
// handler would pop back up.
|
// handler would pop back up.
|
||||||
windowManager := services.NewWindowManager(app, window, bundle, prefStore)
|
windowManager := services.NewWindowManager(app, window, bundle, prefStore, iconWindow)
|
||||||
windowManager.WatchLanguage(prefStore)
|
windowManager.WatchLanguage(prefStore)
|
||||||
app.RegisterService(application.NewService(windowManager))
|
app.RegisterService(application.NewService(windowManager))
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,17 @@ func AppleMacOSAppearanceOptions() application.MacWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LinuxAppearanceOptions is the per-window Linux chrome shared by every
|
||||||
|
// NetBird webview window. Icon shows up in the WM task list / minimised
|
||||||
|
// state; WindowIsTranslucent is left off so the opaque background colour
|
||||||
|
// paints reliably on compositors that fake translucency badly.
|
||||||
|
func LinuxAppearanceOptions(icon []byte) application.LinuxWindow {
|
||||||
|
return application.LinuxWindow{
|
||||||
|
Icon: icon,
|
||||||
|
WindowIsTranslucent: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DialogWindowOptions is the baseline for every auxiliary dialog window
|
// DialogWindowOptions is the baseline for every auxiliary dialog window
|
||||||
// (BrowserLogin, SessionExpired, SessionAboutToExpire, InstallProgress).
|
// (BrowserLogin, SessionExpired, SessionAboutToExpire, InstallProgress).
|
||||||
// All four share size (360x320), the no-resize / no-min / no-max chrome,
|
// All four share size (360x320), the no-resize / no-min / no-max chrome,
|
||||||
@@ -96,7 +107,7 @@ func AppleMacOSAppearanceOptions() application.MacWindow {
|
|||||||
// this), and the shared background/Mac/Windows appearance. Callers fill
|
// this), and the shared background/Mac/Windows appearance. Callers fill
|
||||||
// in per-dialog overrides (URL params, screen targeting, etc.) on the
|
// in per-dialog overrides (URL params, screen targeting, etc.) on the
|
||||||
// returned value before passing it to Window.NewWithOptions.
|
// returned value before passing it to Window.NewWithOptions.
|
||||||
func DialogWindowOptions(name, title, url string) application.WebviewWindowOptions {
|
func DialogWindowOptions(name, title, url string, linuxIcon []byte) application.WebviewWindowOptions {
|
||||||
return application.WebviewWindowOptions{
|
return application.WebviewWindowOptions{
|
||||||
Name: name,
|
Name: name,
|
||||||
Title: title,
|
Title: title,
|
||||||
@@ -112,6 +123,7 @@ func DialogWindowOptions(name, title, url string) application.WebviewWindowOptio
|
|||||||
URL: url,
|
URL: url,
|
||||||
Mac: AppleMacOSAppearanceOptions(),
|
Mac: AppleMacOSAppearanceOptions(),
|
||||||
Windows: MicrosoftWindowsAppearanceOptions(),
|
Windows: MicrosoftWindowsAppearanceOptions(),
|
||||||
|
Linux: LinuxAppearanceOptions(linuxIcon),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +144,7 @@ type WindowManager struct {
|
|||||||
mainWindow *application.WebviewWindow
|
mainWindow *application.WebviewWindow
|
||||||
translator ErrorTranslator
|
translator ErrorTranslator
|
||||||
prefs LanguagePreference
|
prefs LanguagePreference
|
||||||
|
linuxIcon []byte
|
||||||
settings *application.WebviewWindow
|
settings *application.WebviewWindow
|
||||||
browserLogin *application.WebviewWindow
|
browserLogin *application.WebviewWindow
|
||||||
sessionExpired *application.WebviewWindow
|
sessionExpired *application.WebviewWindow
|
||||||
@@ -173,8 +186,8 @@ func (s *WindowManager) title(key string) string {
|
|||||||
// The Settings window is created here, hidden, so the first OpenSettings
|
// The Settings window is created here, hidden, so the first OpenSettings
|
||||||
// call paints instantly instead of paying webview construction + asset load
|
// call paints instantly instead of paying webview construction + asset load
|
||||||
// at click time.
|
// at click time.
|
||||||
func NewWindowManager(app *application.App, mainWindow *application.WebviewWindow, translator ErrorTranslator, prefs LanguagePreference) *WindowManager {
|
func NewWindowManager(app *application.App, mainWindow *application.WebviewWindow, translator ErrorTranslator, prefs LanguagePreference, linuxIcon []byte) *WindowManager {
|
||||||
s := &WindowManager{app: app, mainWindow: mainWindow, translator: translator, prefs: prefs}
|
s := &WindowManager{app: app, mainWindow: mainWindow, translator: translator, prefs: prefs, linuxIcon: linuxIcon}
|
||||||
s.settings = app.Window.NewWithOptions(application.WebviewWindowOptions{
|
s.settings = app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||||
Name: "settings",
|
Name: "settings",
|
||||||
Title: s.title("window.title.settings"),
|
Title: s.title("window.title.settings"),
|
||||||
@@ -187,8 +200,9 @@ func NewWindowManager(app *application.App, mainWindow *application.WebviewWindo
|
|||||||
CloseButtonState: application.ButtonEnabled,
|
CloseButtonState: application.ButtonEnabled,
|
||||||
BackgroundColour: WindowBackgroundColour,
|
BackgroundColour: WindowBackgroundColour,
|
||||||
URL: "/#/settings",
|
URL: "/#/settings",
|
||||||
Mac: AppleMacOSAppearanceOptions(),
|
Mac: AppleMacOSAppearanceOptions(),
|
||||||
Windows: MicrosoftWindowsAppearanceOptions(),
|
Windows: MicrosoftWindowsAppearanceOptions(),
|
||||||
|
Linux: LinuxAppearanceOptions(linuxIcon),
|
||||||
})
|
})
|
||||||
// Hide on close instead of destroying — preserves in-window React state
|
// Hide on close instead of destroying — preserves in-window React state
|
||||||
// across reopens. Mirrors the main window's close behaviour. Resetting
|
// across reopens. Mirrors the main window's close behaviour. Resetting
|
||||||
@@ -295,7 +309,7 @@ func (s *WindowManager) OpenBrowserLogin(uri string) {
|
|||||||
screen = sc
|
screen = sc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
opts := DialogWindowOptions("browser-login", s.title("window.title.signIn"), startURL)
|
opts := DialogWindowOptions("browser-login", s.title("window.title.signIn"), startURL, s.linuxIcon)
|
||||||
// SSO popup deliberately is NOT always-on-top — the user moves
|
// SSO popup deliberately is NOT always-on-top — the user moves
|
||||||
// between the browser tab and our popup; pinning it would obscure
|
// between the browser tab and our popup; pinning it would obscure
|
||||||
// the browser at the moment they need to interact with it.
|
// the browser at the moment they need to interact with it.
|
||||||
@@ -405,7 +419,7 @@ func (s *WindowManager) OpenSessionExpired() {
|
|||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
if s.sessionExpired == nil {
|
if s.sessionExpired == nil {
|
||||||
s.sessionExpired = s.app.Window.NewWithOptions(
|
s.sessionExpired = s.app.Window.NewWithOptions(
|
||||||
DialogWindowOptions("session-expired", s.title("window.title.sessionExpired"), "/#/dialog/session-expired"),
|
DialogWindowOptions("session-expired", s.title("window.title.sessionExpired"), "/#/dialog/session-expired", s.linuxIcon),
|
||||||
)
|
)
|
||||||
s.sessionExpired.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) {
|
s.sessionExpired.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
@@ -440,7 +454,7 @@ func (s *WindowManager) OpenSessionAboutToExpire(seconds int) {
|
|||||||
startURL := "/#/dialog/session-about-to-expire?seconds=" + strconv.Itoa(seconds)
|
startURL := "/#/dialog/session-about-to-expire?seconds=" + strconv.Itoa(seconds)
|
||||||
if s.sessionAboutToExpire == nil {
|
if s.sessionAboutToExpire == nil {
|
||||||
s.sessionAboutToExpire = s.app.Window.NewWithOptions(
|
s.sessionAboutToExpire = s.app.Window.NewWithOptions(
|
||||||
DialogWindowOptions("session-about-to-expire", s.title("window.title.sessionExpiring"), startURL),
|
DialogWindowOptions("session-about-to-expire", s.title("window.title.sessionExpiring"), startURL, s.linuxIcon),
|
||||||
)
|
)
|
||||||
s.sessionAboutToExpire.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) {
|
s.sessionAboutToExpire.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
@@ -486,7 +500,7 @@ func (s *WindowManager) OpenInstallProgress(version string) {
|
|||||||
if s.installProgress == nil {
|
if s.installProgress == nil {
|
||||||
s.hideOtherWindowsLocked("install-progress")
|
s.hideOtherWindowsLocked("install-progress")
|
||||||
s.installProgress = s.app.Window.NewWithOptions(
|
s.installProgress = s.app.Window.NewWithOptions(
|
||||||
DialogWindowOptions("install-progress", s.title("window.title.updating"), startURL),
|
DialogWindowOptions("install-progress", s.title("window.title.updating"), startURL, s.linuxIcon),
|
||||||
)
|
)
|
||||||
s.installProgress.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) {
|
s.installProgress.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
|
|||||||
Reference in New Issue
Block a user