mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-02-02 05:50:50 +08:00
feat(desktop): close-to-tray setting
This commit is contained in:
BIN
desktop/src/icon.ico
Normal file
BIN
desktop/src/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 279 KiB |
@@ -2,6 +2,7 @@ const {
|
|||||||
app,
|
app,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
Menu,
|
Menu,
|
||||||
|
Tray,
|
||||||
ipcMain,
|
ipcMain,
|
||||||
globalShortcut,
|
globalShortcut,
|
||||||
dialog,
|
dialog,
|
||||||
@@ -19,6 +20,10 @@ const BACKEND_HEALTH_URL = `http://${BACKEND_HOST}:${BACKEND_PORT}/api/health`;
|
|||||||
let backendProc = null;
|
let backendProc = null;
|
||||||
let backendStdioStream = null;
|
let backendStdioStream = null;
|
||||||
let resolvedDataDir = null;
|
let resolvedDataDir = null;
|
||||||
|
let mainWindow = null;
|
||||||
|
let tray = null;
|
||||||
|
let isQuitting = false;
|
||||||
|
let desktopSettings = null;
|
||||||
|
|
||||||
function nowIso() {
|
function nowIso() {
|
||||||
return new Date().toISOString();
|
return new Date().toISOString();
|
||||||
@@ -109,6 +114,163 @@ function logMain(line) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDesktopSettingsPath() {
|
||||||
|
const dir = getUserDataDir();
|
||||||
|
if (!dir) return null;
|
||||||
|
return path.join(dir, "desktop-settings.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDesktopSettings() {
|
||||||
|
if (desktopSettings) return desktopSettings;
|
||||||
|
|
||||||
|
const defaults = {
|
||||||
|
// 'tray' (default): closing the window hides it to the system tray.
|
||||||
|
// 'exit': closing the window quits the app.
|
||||||
|
closeBehavior: "tray",
|
||||||
|
};
|
||||||
|
|
||||||
|
const p = getDesktopSettingsPath();
|
||||||
|
if (!p) {
|
||||||
|
desktopSettings = { ...defaults };
|
||||||
|
return desktopSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(p)) {
|
||||||
|
desktopSettings = { ...defaults };
|
||||||
|
return desktopSettings;
|
||||||
|
}
|
||||||
|
const raw = fs.readFileSync(p, { encoding: "utf8" });
|
||||||
|
const parsed = JSON.parse(raw || "{}");
|
||||||
|
desktopSettings = { ...defaults, ...(parsed && typeof parsed === "object" ? parsed : {}) };
|
||||||
|
} catch (err) {
|
||||||
|
desktopSettings = { ...defaults };
|
||||||
|
logMain(`[main] failed to load settings: ${err?.message || err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return desktopSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistDesktopSettings() {
|
||||||
|
const p = getDesktopSettingsPath();
|
||||||
|
if (!p) return;
|
||||||
|
if (!desktopSettings) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||||
|
fs.writeFileSync(p, JSON.stringify(desktopSettings, null, 2), { encoding: "utf8" });
|
||||||
|
} catch (err) {
|
||||||
|
logMain(`[main] failed to persist settings: ${err?.message || err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCloseBehavior() {
|
||||||
|
const v = String(loadDesktopSettings()?.closeBehavior || "").trim().toLowerCase();
|
||||||
|
return v === "exit" ? "exit" : "tray";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCloseBehavior(next) {
|
||||||
|
const v = String(next || "").trim().toLowerCase();
|
||||||
|
loadDesktopSettings();
|
||||||
|
desktopSettings.closeBehavior = v === "exit" ? "exit" : "tray";
|
||||||
|
persistDesktopSettings();
|
||||||
|
return desktopSettings.closeBehavior;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTrayIconPath() {
|
||||||
|
// Prefer an icon shipped in `src/` so it works both in dev and packaged (asar) builds.
|
||||||
|
const shipped = path.join(__dirname, "icon.ico");
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(shipped)) return shipped;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Dev fallback (not available in packaged builds).
|
||||||
|
const dev = path.resolve(__dirname, "..", "build", "icon.ico");
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(dev)) return dev;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMainWindow() {
|
||||||
|
if (!mainWindow) return;
|
||||||
|
try {
|
||||||
|
mainWindow.setSkipTaskbar(false);
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
mainWindow.show();
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
mainWindow.focus();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTray() {
|
||||||
|
if (tray) return tray;
|
||||||
|
if (!app.isPackaged) return null;
|
||||||
|
|
||||||
|
const iconPath = getTrayIconPath();
|
||||||
|
if (!iconPath) {
|
||||||
|
logMain("[main] tray icon not found; disabling tray behavior");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
tray = new Tray(iconPath);
|
||||||
|
} catch (err) {
|
||||||
|
tray = null;
|
||||||
|
logMain(`[main] failed to create tray: ${err?.message || err}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
tray.setToolTip("WeChatDataAnalysis");
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
tray.setContextMenu(
|
||||||
|
Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: "显示",
|
||||||
|
click: () => showMainWindow(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "退出",
|
||||||
|
click: () => {
|
||||||
|
isQuitting = true;
|
||||||
|
app.quit();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
tray.on("click", () => showMainWindow());
|
||||||
|
tray.on("double-click", () => showMainWindow());
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return tray;
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyTray() {
|
||||||
|
if (!tray) return;
|
||||||
|
try {
|
||||||
|
tray.destroy();
|
||||||
|
} catch {}
|
||||||
|
tray = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureTrayForCloseBehavior() {
|
||||||
|
const behavior = getCloseBehavior();
|
||||||
|
if (behavior === "tray") createTray();
|
||||||
|
else destroyTray();
|
||||||
|
}
|
||||||
|
|
||||||
function getBackendStdioLogPath(dataDir) {
|
function getBackendStdioLogPath(dataDir) {
|
||||||
return path.join(dataDir, "backend-stdio.log");
|
return path.join(dataDir, "backend-stdio.log");
|
||||||
}
|
}
|
||||||
@@ -335,6 +497,26 @@ function createMainWindow() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
win.on("close", (event) => {
|
||||||
|
// In packaged builds, we default to "close -> minimize to tray" unless the user opts out.
|
||||||
|
if (!app.isPackaged) return;
|
||||||
|
if (isQuitting) return;
|
||||||
|
if (getCloseBehavior() !== "tray") return;
|
||||||
|
if (!tray) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
event.preventDefault();
|
||||||
|
win.setSkipTaskbar(true);
|
||||||
|
win.hide();
|
||||||
|
try {
|
||||||
|
tray.displayBalloon({
|
||||||
|
title: "WeChatDataAnalysis",
|
||||||
|
content: "已最小化到托盘,可从托盘图标再次打开。",
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
|
||||||
win.on("closed", () => {
|
win.on("closed", () => {
|
||||||
stopBackend();
|
stopBackend();
|
||||||
});
|
});
|
||||||
@@ -409,6 +591,26 @@ function registerWindowIpc() {
|
|||||||
return on;
|
return on;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("app:getCloseBehavior", () => {
|
||||||
|
try {
|
||||||
|
return getCloseBehavior();
|
||||||
|
} catch (err) {
|
||||||
|
logMain(`[main] getCloseBehavior failed: ${err?.message || err}`);
|
||||||
|
return "tray";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("app:setCloseBehavior", (_event, behavior) => {
|
||||||
|
try {
|
||||||
|
const next = setCloseBehavior(behavior);
|
||||||
|
ensureTrayForCloseBehavior();
|
||||||
|
return next;
|
||||||
|
} catch (err) {
|
||||||
|
logMain(`[main] setCloseBehavior failed: ${err?.message || err}`);
|
||||||
|
return getCloseBehavior();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@@ -428,6 +630,8 @@ async function main() {
|
|||||||
await waitForBackend({ timeoutMs: 30_000 });
|
await waitForBackend({ timeoutMs: 30_000 });
|
||||||
|
|
||||||
const win = createMainWindow();
|
const win = createMainWindow();
|
||||||
|
mainWindow = win;
|
||||||
|
ensureTrayForCloseBehavior();
|
||||||
|
|
||||||
const startUrl =
|
const startUrl =
|
||||||
process.env.ELECTRON_START_URL ||
|
process.env.ELECTRON_START_URL ||
|
||||||
@@ -455,6 +659,8 @@ app.on("will-quit", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.on("before-quit", () => {
|
app.on("before-quit", () => {
|
||||||
|
isQuitting = true;
|
||||||
|
destroyTray();
|
||||||
stopBackend();
|
stopBackend();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,4 +8,7 @@ contextBridge.exposeInMainWorld("wechatDesktop", {
|
|||||||
|
|
||||||
getAutoLaunch: () => ipcRenderer.invoke("app:getAutoLaunch"),
|
getAutoLaunch: () => ipcRenderer.invoke("app:getAutoLaunch"),
|
||||||
setAutoLaunch: (enabled) => ipcRenderer.invoke("app:setAutoLaunch", !!enabled),
|
setAutoLaunch: (enabled) => ipcRenderer.invoke("app:setAutoLaunch", !!enabled),
|
||||||
|
|
||||||
|
getCloseBehavior: () => ipcRenderer.invoke("app:getCloseBehavior"),
|
||||||
|
setCloseBehavior: (behavior) => ipcRenderer.invoke("app:setCloseBehavior", String(behavior || "")),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1600,6 +1600,25 @@
|
|||||||
{{ desktopAutoLaunchError }}
|
{{ desktopAutoLaunchError }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="text-sm font-medium text-gray-900">关闭窗口行为</div>
|
||||||
|
<div class="text-xs text-gray-500">点击关闭按钮时:默认最小化到托盘</div>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
class="text-sm px-2 py-1 rounded-md border border-gray-200"
|
||||||
|
:disabled="desktopCloseBehaviorLoading"
|
||||||
|
:value="desktopCloseBehavior"
|
||||||
|
@change="onDesktopCloseBehaviorChange"
|
||||||
|
>
|
||||||
|
<option value="tray">最小化到托盘</option>
|
||||||
|
<option value="exit">直接退出</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div v-if="desktopCloseBehaviorError" class="text-xs text-red-600 whitespace-pre-wrap">
|
||||||
|
{{ desktopCloseBehaviorError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="text-sm font-medium text-gray-900">启动后自动开启实时获取</div>
|
<div class="text-sm font-medium text-gray-900">启动后自动开启实时获取</div>
|
||||||
@@ -1717,6 +1736,10 @@ const desktopAutoLaunch = ref(false)
|
|||||||
const desktopAutoLaunchLoading = ref(false)
|
const desktopAutoLaunchLoading = ref(false)
|
||||||
const desktopAutoLaunchError = ref('')
|
const desktopAutoLaunchError = ref('')
|
||||||
|
|
||||||
|
const desktopCloseBehavior = ref('tray') // tray | exit
|
||||||
|
const desktopCloseBehaviorLoading = ref(false)
|
||||||
|
const desktopCloseBehaviorError = ref('')
|
||||||
|
|
||||||
const readLocalBool = (key) => {
|
const readLocalBool = (key) => {
|
||||||
if (!process.client) return false
|
if (!process.client) return false
|
||||||
try {
|
try {
|
||||||
@@ -1768,9 +1791,42 @@ const setDesktopAutoLaunch = async (enabled) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshDesktopCloseBehavior = async () => {
|
||||||
|
if (!process.client || typeof window === 'undefined') return
|
||||||
|
if (!window.wechatDesktop?.getCloseBehavior) return
|
||||||
|
desktopCloseBehaviorLoading.value = true
|
||||||
|
desktopCloseBehaviorError.value = ''
|
||||||
|
try {
|
||||||
|
const v = await window.wechatDesktop.getCloseBehavior()
|
||||||
|
desktopCloseBehavior.value = (String(v || '').toLowerCase() === 'exit') ? 'exit' : 'tray'
|
||||||
|
} catch (e) {
|
||||||
|
desktopCloseBehaviorError.value = e?.message || '读取关闭窗口行为失败'
|
||||||
|
} finally {
|
||||||
|
desktopCloseBehaviorLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setDesktopCloseBehavior = async (behavior) => {
|
||||||
|
if (!process.client || typeof window === 'undefined') return
|
||||||
|
if (!window.wechatDesktop?.setCloseBehavior) return
|
||||||
|
const desired = (String(behavior || '').toLowerCase() === 'exit') ? 'exit' : 'tray'
|
||||||
|
desktopCloseBehaviorLoading.value = true
|
||||||
|
desktopCloseBehaviorError.value = ''
|
||||||
|
try {
|
||||||
|
const v = await window.wechatDesktop.setCloseBehavior(desired)
|
||||||
|
desktopCloseBehavior.value = (String(v || '').toLowerCase() === 'exit') ? 'exit' : 'tray'
|
||||||
|
} catch (e) {
|
||||||
|
desktopCloseBehaviorError.value = e?.message || '设置关闭窗口行为失败'
|
||||||
|
await refreshDesktopCloseBehavior()
|
||||||
|
} finally {
|
||||||
|
desktopCloseBehaviorLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const openDesktopSettings = async () => {
|
const openDesktopSettings = async () => {
|
||||||
desktopSettingsOpen.value = true
|
desktopSettingsOpen.value = true
|
||||||
await refreshDesktopAutoLaunch()
|
await refreshDesktopAutoLaunch()
|
||||||
|
await refreshDesktopCloseBehavior()
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeDesktopSettings = () => {
|
const closeDesktopSettings = () => {
|
||||||
@@ -1782,6 +1838,11 @@ const onDesktopAutoLaunchToggle = async (ev) => {
|
|||||||
await setDesktopAutoLaunch(checked)
|
await setDesktopAutoLaunch(checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onDesktopCloseBehaviorChange = async (ev) => {
|
||||||
|
const v = String(ev?.target?.value || '').trim()
|
||||||
|
await setDesktopCloseBehavior(v)
|
||||||
|
}
|
||||||
|
|
||||||
const onDesktopAutoRealtimeToggle = async (ev) => {
|
const onDesktopAutoRealtimeToggle = async (ev) => {
|
||||||
const checked = !!ev?.target?.checked
|
const checked = !!ev?.target?.checked
|
||||||
desktopAutoRealtime.value = checked
|
desktopAutoRealtime.value = checked
|
||||||
|
|||||||
Reference in New Issue
Block a user