add profile switch into settings, scroll to top after profile switch, add netbird icon to linux windows

This commit is contained in:
Eduard Gert
2026-06-02 14:20:36 +02:00
parent 9a76507b14
commit 48265a0143
7 changed files with 142 additions and 27 deletions
@@ -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>
); );
+5
View File
@@ -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",
+8 -2
View File
@@ -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",
+5
View File
@@ -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
View File
@@ -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))
+23 -9
View File
@@ -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()