fix(desktop): open the main window when launching at startup
Drop the tray-only choice and the repeated description. A login launch shows the main window, and an older tray preference is rewritten on the next start. Local web dev still shows the switch for preview only.
This commit is contained in:
1 parent
7a91e7d592
commit
f7f10e64ee
7 files changed
+97
-97
No files matched your search
@@ -1,6 +1,6 @@
|
||||
// Windows can disable a Run value from Task Manager without deleting it.
|
||||
// macOS login items cannot carry a start-hidden argument, so a tray launch is
|
||||
// decided from the saved mode when this process was opened at login.
|
||||
// A login launch always opens the main window. Older installs may still have
|
||||
// saved "tray" or passed --start-hidden; both are treated as a normal window.
|
||||
|
||||
export const LOGIN_ITEM_NAME = "EdgeEver";
|
||||
export const START_HIDDEN_ARG = "--start-hidden";
|
||||
@@ -8,7 +8,7 @@ export const START_HIDDEN_ARG = "--start-hidden";
|
||||
export const loginItemSupported = ({ platform, packaged }) =>
|
||||
packaged === true && (platform === "win32" || platform === "darwin");
|
||||
|
||||
export const normalizeLaunchMode = (value) => (value === "tray" ? "tray" : "window");
|
||||
export const normalizeLaunchMode = () => "window";
|
||||
|
||||
export const parseLoginItemPreference = (raw) => {
|
||||
let value = raw;
|
||||
@@ -26,9 +26,7 @@ export const parseLoginItemPreference = (raw) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const loginItemArgs = (launchMode) => (
|
||||
normalizeLaunchMode(launchMode) === "tray" ? [START_HIDDEN_ARG] : []
|
||||
);
|
||||
export const loginItemArgs = () => [];
|
||||
|
||||
export const normalizeLoginItemArgs = (args, executablePath) => {
|
||||
if (!Array.isArray(args)) return [];
|
||||
@@ -66,16 +64,7 @@ export const commandLineOpensContent = (argv, { isAssociatedFile } = {}) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
export const shouldStartHidden = ({
|
||||
argv,
|
||||
wasOpenedAtLogin = false,
|
||||
launchMode = "window",
|
||||
opensContent = false,
|
||||
} = {}) => {
|
||||
if (opensContent) return false;
|
||||
if (Array.isArray(argv) && argv.includes(START_HIDDEN_ARG)) return true;
|
||||
return wasOpenedAtLogin === true && normalizeLaunchMode(launchMode) === "tray";
|
||||
};
|
||||
export const shouldStartHidden = () => false;
|
||||
|
||||
const unavailablePlan = () => ({
|
||||
supported: false,
|
||||
@@ -201,7 +190,7 @@ export const normalizeLoginItemPatch = (current, patch) => {
|
||||
if (hasMode && patch.launchMode !== "window" && patch.launchMode !== "tray") return null;
|
||||
return {
|
||||
openAtLogin: hasOpen ? patch.openAtLogin : current.openAtLogin === true,
|
||||
launchMode: hasMode ? patch.launchMode : normalizeLaunchMode(current.launchMode),
|
||||
launchMode: "window",
|
||||
};
|
||||
};
|
||||
|
||||
@@ -258,11 +247,22 @@ export const createLoginItemController = ({
|
||||
return {
|
||||
reconcileAtStartup() {
|
||||
return enqueue(async () => {
|
||||
let migrateTray = false;
|
||||
try {
|
||||
saved = parseLoginItemPreference(await readFile(preferencePath, "utf8"));
|
||||
const raw = JSON.parse(await readFile(preferencePath, "utf8"));
|
||||
saved = parseLoginItemPreference(raw);
|
||||
migrateTray = raw?.launchMode === "tray";
|
||||
} catch {
|
||||
saved = { openAtLogin: false, launchMode: "window" };
|
||||
}
|
||||
if (migrateTray) {
|
||||
try {
|
||||
await persist(saved);
|
||||
} catch {
|
||||
// The in-memory preference already opens the window. A failed rewrite
|
||||
// must not turn startup off.
|
||||
}
|
||||
}
|
||||
if (platform === "darwin") {
|
||||
try {
|
||||
openedAtLogin = getLoginItemSettings().wasOpenedAtLogin === true;
|
||||
|
||||
@@ -40,7 +40,7 @@ describe("login item support", () => {
|
||||
expect(parseLoginItemPreference("{")).toEqual({ openAtLogin: false, launchMode: "window" });
|
||||
expect(parseLoginItemPreference({ openAtLogin: true, launchMode: "tray" })).toEqual({
|
||||
openAtLogin: true,
|
||||
launchMode: "tray",
|
||||
launchMode: "window",
|
||||
});
|
||||
expect(parseLoginItemPreference({ openAtLogin: "yes", launchMode: "elsewhere" })).toEqual({
|
||||
openAtLogin: false,
|
||||
@@ -56,7 +56,7 @@ describe("login item support", () => {
|
||||
expect(normalizeLoginItemPatch(current, { openAtLogin: "yes" })).toBeNull();
|
||||
expect(normalizeLoginItemPatch(current, { launchMode: "tray" })).toEqual({
|
||||
openAtLogin: true,
|
||||
launchMode: "tray",
|
||||
launchMode: "window",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -88,7 +88,7 @@ describe("login item startup plan", () => {
|
||||
openAtLogin: true,
|
||||
path: exePath,
|
||||
name: LOGIN_ITEM_NAME,
|
||||
args: [START_HIDDEN_ARG],
|
||||
args: [],
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
@@ -105,6 +105,13 @@ describe("login item startup plan", () => {
|
||||
});
|
||||
|
||||
test("leaves a matching Windows entry untouched and removes one the user turned off", () => {
|
||||
expect(startupLoginItemPlan({
|
||||
platform: "win32",
|
||||
packaged: true,
|
||||
saved: { openAtLogin: true, launchMode: "tray" },
|
||||
os: { launchItems: [windowsItem({ args: [START_HIDDEN_ARG] })] },
|
||||
executablePath: exePath,
|
||||
}).action).toBe("apply");
|
||||
expect(startupLoginItemPlan({
|
||||
platform: "win32",
|
||||
packaged: true,
|
||||
@@ -161,7 +168,7 @@ describe("login item startup plan", () => {
|
||||
os: null,
|
||||
osReadable: false,
|
||||
executablePath: exePath,
|
||||
})).toMatchObject({ action: "none", openAtLogin: true, launchMode: "tray" });
|
||||
})).toMatchObject({ action: "none", openAtLogin: true, launchMode: "window" });
|
||||
expect(startupLoginItemPlan({
|
||||
platform: "linux",
|
||||
packaged: true,
|
||||
@@ -173,19 +180,18 @@ describe("login item startup plan", () => {
|
||||
});
|
||||
|
||||
describe("hidden login launch", () => {
|
||||
test("hides a tray login and shows a launch that opens a note", () => {
|
||||
expect(shouldStartHidden({ argv: ["EdgeEver.exe", START_HIDDEN_ARG], launchMode: "tray" })).toBe(true);
|
||||
test("opens the main window after login, including an older hidden launch", () => {
|
||||
expect(shouldStartHidden({ argv: ["EdgeEver.exe", START_HIDDEN_ARG], launchMode: "tray" })).toBe(false);
|
||||
expect(shouldStartHidden({
|
||||
argv: ["EdgeEver.exe"],
|
||||
wasOpenedAtLogin: true,
|
||||
launchMode: "tray",
|
||||
})).toBe(true);
|
||||
})).toBe(false);
|
||||
expect(shouldStartHidden({
|
||||
argv: ["EdgeEver.exe"],
|
||||
wasOpenedAtLogin: true,
|
||||
launchMode: "window",
|
||||
})).toBe(false);
|
||||
expect(shouldStartHidden({ argv: ["EdgeEver.exe"], launchMode: "tray" })).toBe(false);
|
||||
expect(shouldStartHidden({
|
||||
argv: ["EdgeEver.exe", START_HIDDEN_ARG, "--share-file", "D:\\Notes\\a.md"],
|
||||
opensContent: commandLineOpensContent(
|
||||
@@ -253,12 +259,13 @@ describe("login item controller", () => {
|
||||
const harness = createHarness("win32");
|
||||
harness.files.set("/user/login-item.json", JSON.stringify({ openAtLogin: true, launchMode: "tray" }));
|
||||
const state = await harness.controller.reconcileAtStartup();
|
||||
expect(state).toMatchObject({ supported: true, openAtLogin: true, launchMode: "tray", status: "enabled" });
|
||||
expect(state).toMatchObject({ supported: true, openAtLogin: true, launchMode: "window", status: "enabled" });
|
||||
expect(JSON.parse(harness.files.get("/user/login-item.json")).launchMode).toBe("window");
|
||||
expect(harness.calls).toEqual([{
|
||||
openAtLogin: true,
|
||||
path: exePath,
|
||||
name: LOGIN_ITEM_NAME,
|
||||
args: [START_HIDDEN_ARG],
|
||||
args: [],
|
||||
enabled: true,
|
||||
}]);
|
||||
});
|
||||
@@ -278,20 +285,20 @@ describe("login item controller", () => {
|
||||
await expect(harness.controller.update({ launchMode: "background" })).resolves.toMatchObject({ error: "invalid" });
|
||||
});
|
||||
|
||||
test("keeps macOS approval visible and hides only a login launch", async () => {
|
||||
test("keeps macOS approval visible and opens the window after login", async () => {
|
||||
const harness = createHarness("darwin");
|
||||
harness.files.set("/user/login-item.json", JSON.stringify({ openAtLogin: true, launchMode: "tray" }));
|
||||
harness.setRegistry({ status: "requires-approval" });
|
||||
harness.setOpenedAtLogin(true);
|
||||
const state = await harness.controller.reconcileAtStartup();
|
||||
expect(state).toMatchObject({ openAtLogin: true, launchMode: "tray", status: "requires-approval" });
|
||||
expect(state).toMatchObject({ openAtLogin: true, launchMode: "window", status: "requires-approval" });
|
||||
expect(harness.calls).toEqual([]);
|
||||
expect(harness.controller.wasOpenedAtLogin()).toBe(true);
|
||||
expect(shouldStartHidden({
|
||||
argv: ["EdgeEver"],
|
||||
wasOpenedAtLogin: harness.controller.wasOpenedAtLogin(),
|
||||
launchMode: state.launchMode,
|
||||
})).toBe(true);
|
||||
})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Power } from "lucide-react";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { SETTINGS_ITEM_DESCRIPTION_CLASSNAME, SETTINGS_ITEM_TITLE_CLASSNAME } from "./settings-ui";
|
||||
|
||||
// Lucide Power draws an asymmetric arc and a stem that crowds the opening at 16px.
|
||||
const StartupPowerIcon = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={className}
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
>
|
||||
<path d="M12 3v6.2" />
|
||||
<path d="M16.76 6.93a7.4 7.4 0 1 1-9.51 0" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
type LoginItemLaunchMode = "window" | "tray";
|
||||
|
||||
interface LoginItemState {
|
||||
@@ -15,9 +32,17 @@ interface LoginItemState {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Packaged desktop is the only place this setting can change the OS. Local web
|
||||
// dev still shows the row so the layout can be previewed.
|
||||
const devPreviewState = (): LoginItemState | null => (
|
||||
import.meta.env.DEV && typeof window !== "undefined" && !window.edgeeverDesktop?.loginItem
|
||||
? { supported: true, openAtLogin: false, launchMode: "window", status: "disabled" }
|
||||
: null
|
||||
);
|
||||
|
||||
export const LoginItemSettingsItem = () => {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState<LoginItemState | null>(null);
|
||||
const [state, setState] = useState<LoginItemState | null>(devPreviewState);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
@@ -37,9 +62,22 @@ export const LoginItemSettingsItem = () => {
|
||||
|
||||
if (!state?.supported) return null;
|
||||
|
||||
const update = async (patch: { openAtLogin?: boolean; launchMode?: LoginItemLaunchMode }) => {
|
||||
const approvalNotice = state.status === "requires-approval" ? t("settings.loginItem.requiresApproval") : "";
|
||||
|
||||
const update = async (patch: { openAtLogin: boolean }) => {
|
||||
const bridge = window.edgeeverDesktop;
|
||||
if (!bridge?.setLoginItem) return;
|
||||
if (!bridge?.setLoginItem) {
|
||||
setState((current) => {
|
||||
if (!current || !import.meta.env.DEV) return current;
|
||||
return {
|
||||
...current,
|
||||
openAtLogin: patch.openAtLogin,
|
||||
launchMode: "window",
|
||||
status: patch.openAtLogin ? "enabled" : "disabled",
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
@@ -54,52 +92,23 @@ export const LoginItemSettingsItem = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex min-h-16 flex-col items-start gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<Power className="h-4 w-4 shrink-0 text-slate-500" />
|
||||
<div className="min-w-0">
|
||||
<div className={SETTINGS_ITEM_TITLE_CLASSNAME}>{t("settings.loginItem.title")}</div>
|
||||
<p className={SETTINGS_ITEM_DESCRIPTION_CLASSNAME}>
|
||||
{state.status === "requires-approval"
|
||||
? t("settings.loginItem.requiresApproval")
|
||||
: t("settings.loginItem.description")}
|
||||
</p>
|
||||
{error ? <p role="status" className={SETTINGS_ITEM_DESCRIPTION_CLASSNAME}>{error}</p> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full shrink-0 justify-start sm:w-44 sm:justify-end">
|
||||
<Switch
|
||||
checked={state.openAtLogin}
|
||||
disabled={busy}
|
||||
onCheckedChange={(openAtLogin) => { void update({ openAtLogin }); }}
|
||||
aria-label={t("settings.loginItem.aria")}
|
||||
/>
|
||||
<div className="flex min-h-16 flex-col items-start gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<StartupPowerIcon className="h-4 w-4 shrink-0 text-slate-500" />
|
||||
<div className="min-w-0">
|
||||
<div className={SETTINGS_ITEM_TITLE_CLASSNAME}>{t("settings.loginItem.title")}</div>
|
||||
{approvalNotice ? <p className={SETTINGS_ITEM_DESCRIPTION_CLASSNAME}>{approvalNotice}</p> : null}
|
||||
{error ? <p role="status" className={SETTINGS_ITEM_DESCRIPTION_CLASSNAME}>{error}</p> : null}
|
||||
</div>
|
||||
</div>
|
||||
{state.openAtLogin ? (
|
||||
<div className="flex min-h-16 flex-col items-start gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="h-4 w-4 shrink-0" />
|
||||
<div className={SETTINGS_ITEM_TITLE_CLASSNAME}>{t("settings.loginItem.launchModeLabel")}</div>
|
||||
</div>
|
||||
<div className="w-full shrink-0 sm:w-80">
|
||||
<Select
|
||||
value={state.launchMode}
|
||||
disabled={busy}
|
||||
onValueChange={(launchMode) => { void update({ launchMode: launchMode as LoginItemLaunchMode }); }}
|
||||
>
|
||||
<SelectTrigger aria-label={t("settings.loginItem.launchModeLabel")} className="h-9 bg-card">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="window">{t("settings.loginItem.launchModeWindow")}</SelectItem>
|
||||
<SelectItem value="tray">{t("settings.loginItem.launchModeTray")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
<div className="flex w-full shrink-0 justify-start sm:w-44 sm:justify-end">
|
||||
<Switch
|
||||
checked={state.openAtLogin}
|
||||
disabled={busy}
|
||||
onCheckedChange={(openAtLogin) => { void update({ openAtLogin }); }}
|
||||
aria-label={t("settings.loginItem.aria")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1027,10 +1027,6 @@ export const enUS = {
|
||||
loginItem: {
|
||||
title: "Launch at startup",
|
||||
aria: "Launch EdgeEver at startup",
|
||||
description: "Open EdgeEver after this computer starts.",
|
||||
launchModeLabel: "After startup",
|
||||
launchModeWindow: "Open the main window",
|
||||
launchModeTray: "Stay in the tray",
|
||||
requiresApproval: "Allow EdgeEver in System Settings to finish turning this on.",
|
||||
saveFailed: "Could not save this setting.",
|
||||
},
|
||||
|
||||
@@ -1016,10 +1016,6 @@ export const ja = {
|
||||
loginItem: {
|
||||
title: "起動後に開く",
|
||||
aria: "パソコンの起動後に EdgeEver を開く",
|
||||
description: "パソコンの起動後、EdgeEver を自動で開きます。",
|
||||
launchModeLabel: "起動後",
|
||||
launchModeWindow: "メインウィンドウを開く",
|
||||
launchModeTray: "トレイに残す",
|
||||
requiresApproval: "システム設定で EdgeEver の起動を許可すると、この設定が有効になります。",
|
||||
saveFailed: "この設定を保存できませんでした。",
|
||||
},
|
||||
|
||||
@@ -1016,10 +1016,6 @@ export const pl = {
|
||||
loginItem: {
|
||||
title: "Uruchamiaj po włączeniu komputera",
|
||||
aria: "Uruchamiaj EdgeEver po włączeniu komputera",
|
||||
description: "Otwieraj EdgeEver po uruchomieniu komputera.",
|
||||
launchModeLabel: "Po uruchomieniu",
|
||||
launchModeWindow: "Otwórz główne okno",
|
||||
launchModeTray: "Zostaw w zasobniku",
|
||||
requiresApproval: "Zezwól EdgeEver w ustawieniach systemu, aby dokończyć włączanie.",
|
||||
saveFailed: "Nie udało się zapisać tego ustawienia.",
|
||||
},
|
||||
|
||||
@@ -1027,10 +1027,6 @@ export const zhCN = {
|
||||
loginItem: {
|
||||
title: "开机后启动",
|
||||
aria: "开机后启动 EdgeEver",
|
||||
description: "开机后自动打开 EdgeEver。",
|
||||
launchModeLabel: "启动后",
|
||||
launchModeWindow: "打开主窗口",
|
||||
launchModeTray: "只在托盘运行",
|
||||
requiresApproval: "还需要在系统设置里允许 EdgeEver 开机后启动。",
|
||||
saveFailed: "无法保存这项设置。",
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user