diff --git a/apps/desktop/src/main/login-item.mjs b/apps/desktop/src/main/login-item.mjs index 583607ca..34d3aa75 100644 --- a/apps/desktop/src/main/login-item.mjs +++ b/apps/desktop/src/main/login-item.mjs @@ -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; diff --git a/apps/desktop/src/main/login-item.test.mjs b/apps/desktop/src/main/login-item.test.mjs index 162a8ecf..e3ab6273 100644 --- a/apps/desktop/src/main/login-item.test.mjs +++ b/apps/desktop/src/main/login-item.test.mjs @@ -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); }); }); diff --git a/apps/web/src/components/settings/LoginItemSettingsItem.tsx b/apps/web/src/components/settings/LoginItemSettingsItem.tsx index 4df1bbdc..4ca78033 100644 --- a/apps/web/src/components/settings/LoginItemSettingsItem.tsx +++ b/apps/web/src/components/settings/LoginItemSettingsItem.tsx @@ -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 }) => ( + +); + 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(null); + const [state, setState] = useState(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 ( - <> -
-
- -
-
{t("settings.loginItem.title")}
-

- {state.status === "requires-approval" - ? t("settings.loginItem.requiresApproval") - : t("settings.loginItem.description")} -

- {error ?

{error}

: null} -
-
-
- { void update({ openAtLogin }); }} - aria-label={t("settings.loginItem.aria")} - /> +
+
+ +
+
{t("settings.loginItem.title")}
+ {approvalNotice ?

{approvalNotice}

: null} + {error ?

{error}

: null}
- {state.openAtLogin ? ( -
-
-
-
{t("settings.loginItem.launchModeLabel")}
-
-
- -
-
- ) : null} - +
+ { void update({ openAtLogin }); }} + aria-label={t("settings.loginItem.aria")} + /> +
+
); }; diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index 9d2e5d76..803d6f8c 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -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.", }, diff --git a/packages/shared/src/i18n/ja.ts b/packages/shared/src/i18n/ja.ts index 75682a48..5cb2dea1 100644 --- a/packages/shared/src/i18n/ja.ts +++ b/packages/shared/src/i18n/ja.ts @@ -1016,10 +1016,6 @@ export const ja = { loginItem: { title: "起動後に開く", aria: "パソコンの起動後に EdgeEver を開く", - description: "パソコンの起動後、EdgeEver を自動で開きます。", - launchModeLabel: "起動後", - launchModeWindow: "メインウィンドウを開く", - launchModeTray: "トレイに残す", requiresApproval: "システム設定で EdgeEver の起動を許可すると、この設定が有効になります。", saveFailed: "この設定を保存できませんでした。", }, diff --git a/packages/shared/src/i18n/pl.ts b/packages/shared/src/i18n/pl.ts index fe9374ff..41a86176 100644 --- a/packages/shared/src/i18n/pl.ts +++ b/packages/shared/src/i18n/pl.ts @@ -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.", }, diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index 08ea32ef..2be15dca 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -1027,10 +1027,6 @@ export const zhCN = { loginItem: { title: "开机后启动", aria: "开机后启动 EdgeEver", - description: "开机后自动打开 EdgeEver。", - launchModeLabel: "启动后", - launchModeWindow: "打开主窗口", - launchModeTray: "只在托盘运行", requiresApproval: "还需要在系统设置里允许 EdgeEver 开机后启动。", saveFailed: "无法保存这项设置。", },