mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 01:09:56 -05:00
Feat: Minimize to Sys-Tray + MultiModal (#76)
This commit is contained in:
+218
-2
@@ -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<string | null>(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<void>("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 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if closeDialogOpen}
|
||||
<div class="close-backdrop" role="presentation" onclick={() => { closeDialogOpen = false; closeRemember = false; }}>
|
||||
<div class="close-dialog" role="dialog" aria-modal="true" onclick={(e) => e.stopPropagation()}>
|
||||
<div class="close-header">
|
||||
<p class="close-title">Close Moku?</p>
|
||||
<p class="close-sub">Choose how the app should exit.</p>
|
||||
</div>
|
||||
<div class="close-actions">
|
||||
<button class="close-btn" onclick={() => confirmClose("tray")}>
|
||||
<span class="close-btn-label">Minimize to Tray</span>
|
||||
<span class="close-btn-desc">Keep running in the background</span>
|
||||
</button>
|
||||
<button class="close-btn close-btn-danger" onclick={() => confirmClose("quit")}>
|
||||
<span class="close-btn-label">Quit</span>
|
||||
<span class="close-btn-desc">Stop Moku entirely</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="close-remember" onclick={() => closeRemember = !closeRemember}>
|
||||
<span class="close-remember-toggle" class:on={closeRemember}><span class="close-remember-thumb"></span></span>
|
||||
<span class="close-remember-label">Remember my choice</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.root { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
||||
.content { flex: 1; overflow: hidden; }
|
||||
|
||||
.close-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: var(--z-modal);
|
||||
}
|
||||
|
||||
.close-dialog {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-base);
|
||||
border-radius: var(--radius-2xl);
|
||||
padding: var(--sp-5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-3);
|
||||
width: 300px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255,255,255,0.04) inset,
|
||||
0 20px 60px rgba(0,0,0,0.65),
|
||||
0 6px 20px rgba(0,0,0,0.35);
|
||||
}
|
||||
|
||||
.close-header { display: flex; flex-direction: column; gap: 3px; }
|
||||
|
||||
.close-title {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--weight-medium);
|
||||
color: var(--text-primary);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-sub {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-actions { display: flex; flex-direction: column; gap: var(--sp-1); }
|
||||
|
||||
.close-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
width: 100%;
|
||||
padding: 10px var(--sp-3);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-dim);
|
||||
background: var(--bg-raised);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background var(--t-base), border-color var(--t-base);
|
||||
}
|
||||
.close-btn:hover { background: var(--bg-overlay); border-color: var(--border-strong); }
|
||||
|
||||
.close-btn-danger { border-color: color-mix(in srgb, var(--color-error) 30%, transparent); }
|
||||
.close-btn-danger:hover { background: var(--color-error-bg); border-color: color-mix(in srgb, var(--color-error) 55%, transparent); }
|
||||
.close-btn-danger .close-btn-label { color: var(--color-error); }
|
||||
.close-btn-danger .close-btn-desc { color: color-mix(in srgb, var(--color-error) 55%, var(--text-faint)); }
|
||||
|
||||
.close-btn-label {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
font-weight: var(--weight-medium);
|
||||
}
|
||||
|
||||
.close-btn-desc {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-2xs);
|
||||
color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
}
|
||||
|
||||
.close-remember {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
padding: var(--sp-1) 0 0;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.close-remember-toggle {
|
||||
position: relative;
|
||||
width: 28px;
|
||||
height: 16px;
|
||||
border-radius: var(--radius-full);
|
||||
border: 1px solid var(--border-strong);
|
||||
background: var(--bg-overlay);
|
||||
flex-shrink: 0;
|
||||
transition: background var(--t-base), border-color var(--t-base);
|
||||
}
|
||||
.close-remember-toggle.on { background: var(--accent); border-color: var(--accent); }
|
||||
|
||||
.close-remember-thumb {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-faint);
|
||||
transition: transform var(--t-base), background var(--t-base);
|
||||
}
|
||||
.close-remember-toggle.on .close-remember-thumb {
|
||||
transform: translateX(12px);
|
||||
background: var(--bg-void);
|
||||
}
|
||||
|
||||
.close-remember-label {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
}
|
||||
</style>
|
||||
@@ -76,6 +76,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="s-section">
|
||||
<p class="s-section-title">Window</p>
|
||||
<div class="s-section-body">
|
||||
<div class="s-row">
|
||||
<div class="s-row-info"><span class="s-label">Close button behavior</span><span class="s-desc">What happens when you click the X button</span></div>
|
||||
<div class="s-seg">
|
||||
{#each [["ask","Ask"],["tray","Tray"],["quit","Quit"]] as [v, l]}
|
||||
<button class="s-seg-btn" class:active={( store.settings.closeAction ?? "ask") === v} onclick={() => updateSettings({ closeAction: v as "ask" | "tray" | "quit" })}>{l}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="s-section">
|
||||
<p class="s-section-title">Integrations</p>
|
||||
<div class="s-section-body">
|
||||
@@ -112,4 +126,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.s-seg { display: flex; border: 1px solid var(--border-strong); border-radius: var(--radius-md); overflow: hidden; }
|
||||
.s-seg-btn { flex: 1; padding: var(--sp-1) var(--sp-3); font-family: var(--font-ui); font-size: var(--text-sm); color: var(--text-faint); background: transparent; cursor: pointer; transition: background var(--t-base), color var(--t-base); border: none; }
|
||||
.s-seg-btn:not(:last-child) { border-right: 1px solid var(--border-strong); }
|
||||
.s-seg-btn.active { background: var(--accent-muted); color: var(--accent-fg); }
|
||||
.s-seg-btn:not(.active):hover { background: var(--bg-raised); color: var(--text-secondary); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user