diff --git a/flake.nix b/flake.nix index 3183c88..4599fc1 100644 --- a/flake.nix +++ b/flake.nix @@ -128,6 +128,7 @@ export NO_STRIP=true export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig''${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" export XDG_DATA_DIRS="${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" + export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath runtimeLibs}''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" echo "Moku dev shell — pnpm install && pnpm tauri:dev" echo "" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 52e7abc..6e6c5c1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -15,7 +15,7 @@ path = "src/main.rs" tauri-build = { version = "2.0", features = [] } [dependencies] -tauri = { version = "2.0", features = [] } +tauri = { version = "2.0", features = ["tray-icon"] } tauri-plugin-shell = "2" tauri-plugin-process = "2" tauri-plugin-http = "2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index a1a1794..baeca1a 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -5,6 +5,10 @@ "windows": ["main"], "permissions": [ "core:default", + "core:tray:default", + "core:app:allow-default-window-icon", + "core:window:allow-hide", + "core:window:allow-show", "shell:allow-open", "shell:allow-kill", "shell:allow-spawn", @@ -38,4 +42,4 @@ "discord-rpc:allow-clear-activity", "discord-rpc:allow-is-running" ] -} +} \ No newline at end of file diff --git a/src/App.svelte b/src/App.svelte index 20263e9..df4ac37 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -3,8 +3,11 @@ import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { getCurrentWindow } from "@tauri-apps/api/window"; + import { defaultWindowIcon } from "@tauri-apps/api/app"; + import { TrayIcon } from "@tauri-apps/api/tray"; + import { Menu } from "@tauri-apps/api/menu"; import { platform } from "@tauri-apps/plugin-os"; - import { store, setActiveDownloads } from "@store/state.svelte"; + import { store, updateSettings, setActiveDownloads } from "@store/state.svelte"; import { downloadStore } from "@features/downloads/store/downloadState.svelte"; import { boot, initStore, startProbe, stopProbe, retryBoot, bypassBoot } from "@store/boot.svelte"; import { initRpc, setIdle, clearReading, destroyRpc } from "@store/discord"; @@ -31,6 +34,9 @@ let themeEditorOpen = $state(false); let themeEditorEditId = $state(null); + let closeDialogOpen = $state(false); + let closeRemember = $state(false); + function openThemeEditor(id?: string | null) { themeEditorEditId = id ?? null; themeEditorOpen = true; @@ -41,6 +47,30 @@ themeEditorEditId = null; } + async function doQuit() { + if (store.settings.autoStartServer) await invoke("kill_server").catch(() => {}); + await win.destroy(); + } + + async function doHide() { + await win.hide(); + } + + async function handleCloseRequested() { + const action = store.settings.closeAction ?? "ask"; + if (action === "tray") { await doHide(); return; } + if (action === "quit") { await doQuit(); return; } + closeDialogOpen = true; + } + + async function confirmClose(choice: "tray" | "quit") { + closeDialogOpen = false; + if (closeRemember) updateSettings({ closeAction: choice }); + closeRemember = false; + if (choice === "tray") await doHide(); + else await doQuit(); + } + $effect(() => { void store.settings.theme; applyTheme(); }); $effect(() => { void store.settings.uiZoom; applyZoom(); }); $effect(() => mountZoomKey()); @@ -93,6 +123,39 @@ applyZoom(); }); + const menu = await Menu.new({ + items: [ + { + id: "show", + text: "Show Moku", + action: async () => { + await win.show(); + await win.setFocus(); + }, + }, + { + id: "quit", + text: "Quit", + action: doQuit, + }, + ], + }); + + await TrayIcon.new({ + icon: await defaultWindowIcon(), + menu, + menuOnLeftClick: false, + tooltip: "Moku", + action: async (e) => { + if (e.type === "Click") { + await win.show(); + await win.setFocus(); + } + }, + }); + + const unlistenClose = await win.listen("tauri://close-requested", handleCloseRequested); + if (store.settings.autoStartServer) { invoke("spawn_server", { binary: store.settings.serverBinary }).catch((err: any) => { if (err?.kind === "NotConfigured") boot.notConfigured = true; @@ -117,8 +180,8 @@ unlistenResize(); unlistenScale(); unlistenDownload(); + unlistenClose(); destroyRpc(); - if (store.settings.autoStartServer) invoke("kill_server").catch(() => {}); delete (window as any).__mokuShowSplash; }; }); @@ -165,7 +228,160 @@ {/if} +{#if closeDialogOpen} + +{/if} + \ No newline at end of file diff --git a/src/features/settings/sections/GeneralSettings.svelte b/src/features/settings/sections/GeneralSettings.svelte index 370a4f8..491eb24 100644 --- a/src/features/settings/sections/GeneralSettings.svelte +++ b/src/features/settings/sections/GeneralSettings.svelte @@ -76,6 +76,20 @@ +
+

Window

+
+
+
Close button behaviorWhat happens when you click the X button
+
+ {#each [["ask","Ask"],["tray","Tray"],["quit","Quit"]] as [v, l]} + + {/each} +
+
+
+
+

Integrations

@@ -112,4 +126,11 @@
- \ No newline at end of file + + \ No newline at end of file