Repository: oliverschwendener/ueli (ts)

Issue:
[Feature Request] Ability to Hide Tray Icon in Windows
**Is your feature request related to a problem? Please describe.**

How can I hide the tray icon? I believe in version 8 series, users were able to hide the tray icon of the app in Windows. 

**Describe the solution you'd like**

Can there be a check box in settings where when enabled, it won’t show in windows System tray? Thank you. 

Hope this option is added in the next release.

**Describe alternatives you've considered**

None. 

**Additional context**

None. 


Pull request:
feat: add option to show/hide tray icon
This PR adds the ability to show/hide the tray icon.

Fixes #1385

Patch:
diff --git a/src/main/Core/TrayIcon/TrayIconManager.ts b/src/main/Core/TrayIcon/TrayIconManager.ts
index 32d33f9b..d7684f90 100644
--- a/src/main/Core/TrayIcon/TrayIconManager.ts
+++ b/src/main/Core/TrayIcon/TrayIconManager.ts
@@ -29,4 +29,8 @@ export class TrayIconManager {
         const menu = this.contextMenuBuilder.buildFromTemplate(template);
         this.tray?.setContextMenu(menu);
     }
+
+    public destory() {
+        this.tray?.destroy();
+    }
 }
diff --git a/src/main/Core/TrayIcon/TrayIconModule.ts b/src/main/Core/TrayIcon/TrayIconModule.ts
index 28ef9ff0..a88b6517 100644
--- a/src/main/Core/TrayIcon/TrayIconModule.ts
+++ b/src/main/Core/TrayIcon/TrayIconModule.ts
@@ -17,6 +17,8 @@ export class TrayIconModule {
         const nativeTheme = moduleRegistry.get("NativeTheme");
         const assetPathResolver = moduleRegistry.get("AssetPathResolver");
         const operatingSystem = moduleRegistry.get("OperatingSystem");
+        const eventSubscriber = moduleRegistry.get("EventSubscriber");
+        const settingsManager = moduleRegistry.get("SettingsManager");
 
         const trayIconFilePathResolvers: Record<OperatingSystem, () => TrayIconFilePathResolver> = {
             Linux: () => new LinuxTrayIconFilePathResolver(nativeTheme, assetPathResolver),
@@ -36,13 +38,24 @@ export class TrayIconModule {
             new ContextMenuBuilder(),
         );
 
-        await trayIconManager.createTrayIcon();
+        if (settingsManager.getValue("general.tray.showIcon", true)) {
+            await trayIconManager.createTrayIcon();
+        }
 
         nativeTheme.on("updated", () => trayIconManager.updateImage());
 
-        const eventSubscriber = moduleRegistry.get("EventSubscriber");
-
         eventSubscriber.subscribe("settingUpdated[general.language]", () => trayIconManager.updateContextMenu());
         eventSubscriber.subscribe("settingUpdated[general.hotkey.enabled]", () => trayIconManager.updateContextMenu());
+
+        eventSubscriber.subscribe(
+            "settingUpdated[general.tray.showIcon]",
+            ({ value: showTrayIcon }: { value: boolean }) => {
+                if (showTrayIcon) {
+                    trayIconManager.createTrayIcon();
+                } else {
+                    trayIconManager.destory();
+                }
+            },
+        );
     }
 }
diff --git a/src/renderer/Core/I18n/getCoreResources.ts b/src/renderer/Core/I18n/getCoreResources.ts
index cf71e20c..421543cc 100644
--- a/src/renderer/Core/I18n/getCoreResources.ts
+++ b/src/renderer/Core/I18n/getCoreResources.ts
@@ -128,6 +128,12 @@ export const getCoreResources = (): { namespace: string; resources: Resources<Tr
                     customSettingsFileValidationDialogDescription:
                         "Make sure that the selected file exists and contains valid JSON.",
                     close: "Close",
+                    "trayIcon[macOS]": "Menu bar",
+                    "trayIconShow[macOS]": "Show icon in menu bar",
+                    "trayIcon[Windows]": "Taskbar",
+                    "trayIconShow[Windows]": "Show icon in taskbar",
+                    "trayIcon[Linux]": "Tray",
+                    "trayIconShow[Linux]": "Show icon in tray",
                 },
                 "de-CH": {
                     title: "Allgemein",
@@ -179,6 +185,12 @@ export const getCoreResources = (): { namespace: string; resources: Resources<Tr
                     customSettingsFileValidationDialogDescription:
                         "Stelle sicher, dass die ausgewählte Datei existiert und gültiges JSON enthält.",
                     close: "Schliessen",
+                    "trayIcon[macOS]": "Menüleiste",
+                    "trayIconShow[macOS]": "Symbol in der Menüleiste anzeigen",
+                    "trayIcon[Windows]": "Taskleiste",
+                    "trayIconShow[Windows]": "Symbol im Taskleiste anzeigen",
+                    "trayIcon[Linux]": "Infobereich",
+                    "trayIconShow[Linux]": "Symbol im Infobereich anzeigen",
                 },
                 "ja-JP": {
                     title: "一般",
@@ -215,6 +227,12 @@ export const getCoreResources = (): { namespace: string; resources: Resources<Tr
                     importExportDescription: "全ての設定をJSONでインポート/エクスポートします",
                     importSettings: "インポート",
                     exportSettings: "エクスポート",
+                    "trayIcon[macOS]": "メニューバー",
+                    "trayIconShow[macOS]": "メニューバーにアイコンを表示",
+                    "trayIcon[Windows]": "トレイ",
+                    "trayIconShow[Windows]": "トレイにアイコンを表示",
+                    "trayIcon[Linux]": "トレイ",
+                    "trayIconShow[Linux]": "トレイにアイコンを表示",
                 },
                 "ko-KR": {
                     title: "일반",
@@ -264,6 +282,12 @@ export const getCoreResources = (): { namespace: string; resources: Resources<Tr
                     customSettingsFileValidationDialogDescription:
                         "선택한 파일이 존재하고 유효한 JSON을 포함하는지 확인하세요.",
                     close: "닫기",
+                    "trayIcon[macOS]": "메뉴 막대",
+                    "trayIconShow[macOS]": "메뉴 막대에 아이콘 표시",
+                    "trayIcon[Windows]": "트레이",
+                    "trayIconShow[Windows]": "트레이에 아이콘 표시",
+                    "trayIcon[Linux]": "트레이",
+                    "trayIconShow[Linux]": "트레이에 아이콘 표시",
                 },
             },
         },
diff --git a/src/renderer/Core/Settings/Pages/General/General.tsx b/src/renderer/Core/Settings/Pages/General/General.tsx
index 095acb5f..0b5296a2 100644
--- a/src/renderer/Core/Settings/Pages/General/General.tsx
+++ b/src/renderer/Core/Settings/Pages/General/General.tsx
@@ -9,6 +9,7 @@ import { ImportExport } from "./ImportExport";
 import { Language } from "./Language";
 import { SearchHistory } from "./SearchHistory";
 import { SetCustomSettingsFilePath } from "./SetCustomSettingsFilePath";
+import { TrayIconSettings } from "./TrayIcon";
 import { UrlImageGenerator } from "./UrlImageGenerator";
 
 export const General = () => {
@@ -22,6 +23,7 @@ export const General = () => {
                 <Autostart />
                 {operatingSystem === "macOS" && <DockSettings />}
             </SettingGroup>
+            <TrayIconSettings />
             <HotkeySettings />
             <SettingGroup title={t("searchHistory")}>
                 <SearchHistory />
diff --git a/src/renderer/Core/Settings/Pages/General/TrayIcon/TrayIconSettings.tsx b/src/renderer/Core/Settings/Pages/General/TrayIcon/TrayIconSettings.tsx
new file mode 100644
index 00000000..e8f41944
--- /dev/null
+++ b/src/renderer/Core/Settings/Pages/General/TrayIcon/TrayIconSettings.tsx
@@ -0,0 +1,38 @@
+import type { OperatingSystem } from "@common/Core";
+import { useSetting } from "@Core/Hooks";
+import { Setting } from "@Core/Settings/Setting";
+import { SettingGroup } from "@Core/Settings/SettingGroup";
+import { Switch } from "@fluentui/react-components";
+import { useTranslation } from "react-i18next";
+
+export const TrayIconSettings = () => {
+    const operatingSystem = window.ContextBridge.getOperatingSystem();
+
+    const { t } = useTranslation("settingsGeneral");
+
+    const { value: showTrayIcon, updateValue: setShowTrayIcon } = useSetting({
+        key: "general.tray.showIcon",
+        defaultValue: true,
+    });
+
+    const groupTitles: Record<OperatingSystem, string> = {
+        Linux: t("trayIcon[Linux]"),
+        macOS: t("trayIcon[macOS]"),
+        Windows: t("trayIcon[Windows]"),
+    };
+
+    const switchLabels: Record<OperatingSystem, string> = {
+        Linux: t("trayIconShow[Linux]"),
+        macOS: t("trayIconShow[macOS]"),
+        Windows: t("trayIconShow[Windows]"),
+    };
+
+    return (
+        <SettingGroup title={groupTitles[operatingSystem]}>
+            <Setting
+                label={switchLabels[operatingSystem]}
+                control={<Switch checked={showTrayIcon} onChange={(_, { checked }) => setShowTrayIcon(checked)} />}
+            />
+        </SettingGroup>
+    );
+};
diff --git a/src/renderer/Core/Settings/Pages/General/TrayIcon/index.tsx b/src/renderer/Core/Settings/Pages/General/TrayIcon/index.tsx
new file mode 100644
index 00000000..64ad60df
--- /dev/null
+++ b/src/renderer/Core/Settings/Pages/General/TrayIcon/index.tsx
@@ -0,0 +1,1 @@
+export * from "./TrayIconSettings";


Test patch:
diff --git a/src/main/Core/TrayIcon/TrayIconManager.test.ts b/src/main/Core/TrayIcon/TrayIconManager.test.ts
index e7ded1d5..509f7676 100644
--- a/src/main/Core/TrayIcon/TrayIconManager.test.ts
+++ b/src/main/Core/TrayIcon/TrayIconManager.test.ts
@@ -89,4 +89,36 @@ describe(TrayIconManager, () => {
             expect(setImageMock).toHaveBeenCalledWith("test-tray-icon-file-path");
         });
     });
+
+    describe(TrayIconManager.prototype.destory, () => {
+        it("should destroy the tray if it exists", () => {
+            const destroyMock = vi.fn();
+
+            const trayIconManager = new TrayIconManager(
+                <TrayCreator>{},
+                <TrayIconFilePathResolver>{},
+                <ContextMenuTemplateProvider>{},
+                <ContextMenuBuilder>{},
+            );
+
+            trayIconManager["tray"] = <Tray>{ destroy: () => destroyMock() };
+
+            trayIconManager.destory();
+
+            expect(destroyMock).toHaveBeenCalledOnce();
+        });
+
+        it("should do nothing if the tray doesn't exist", () => {
+            const trayIconManager = new TrayIconManager(
+                <TrayCreator>{},
+                <TrayIconFilePathResolver>{},
+                <ContextMenuTemplateProvider>{},
+                <ContextMenuBuilder>{},
+            );
+
+            trayIconManager.destory();
+
+            expect(trayIconManager["tray"]).toBeUndefined();
+        });
+    });
 });