diff --git a/electron/main.ts b/electron/main.ts index ae44a5d..ce8f0ac 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -27,7 +27,7 @@ import { windowsHelloService } from './services/windowsHelloService' import { exportCardDiagnosticsService } from './services/exportCardDiagnosticsService' import { cloudControlService } from './services/cloudControlService' -import { destroyNotificationWindow, registerNotificationHandlers, showNotification } from './windows/notificationWindow' +import { destroyNotificationWindow, registerNotificationHandlers, showNotification, setNotificationNavigateHandler } from './windows/notificationWindow' import { httpService } from './services/httpService' import { messagePushService } from './services/messagePushService' import { bizService } from './services/bizService' @@ -740,6 +740,14 @@ function createWindow(options: { autoShow?: boolean } = {}) { win.webContents.send('navigate-to-session', sessionId) }) + // 设置用于D-Bus通知的Linux通知导航处理程序 + setNotificationNavigateHandler((sessionId: string) => { + if (win.isMinimized()) win.restore() + win.show() + win.focus() + win.webContents.send('navigate-to-session', sessionId) + }) + // 拦截请求,修改 Referer 和 User-Agent 以通过微信 CDN 鉴权 session.defaultSession.webRequest.onBeforeSendHeaders( { diff --git a/electron/services/linuxNotificationService.ts b/electron/services/linuxNotificationService.ts new file mode 100644 index 0000000..1e4bd22 --- /dev/null +++ b/electron/services/linuxNotificationService.ts @@ -0,0 +1,344 @@ +import dbus from "dbus-native"; +import https from "https"; +import http, { IncomingMessage } from "http"; +import { promises as fs } from "fs"; +import { join } from "path"; +import { app } from "electron"; + +const BUS_NAME = "org.freedesktop.Notifications"; +const OBJECT_PATH = "/org/freedesktop/Notifications"; + +export interface LinuxNotificationData { + sessionId?: string; + title: string; + content: string; + avatarUrl?: string; + expireTimeout?: number; +} + +type NotificationCallback = (sessionId: string) => void; + +let sessionBus: dbus.DBusConnection | null = null; +let notificationCallbacks: NotificationCallback[] = []; +let pendingNotifications: Map = new Map(); + +// 头像缓存:url->localFilePath +const avatarCache: Map = new Map(); +// 缓存目录 +let avatarCacheDir: string | null = null; + +async function getSessionBus(): Promise { + if (!sessionBus) { + sessionBus = dbus.sessionBus(); + + // 挂载底层socket的error事件,防止掉线即可 + sessionBus.connection.on("error", (err: Error) => { + console.error("[LinuxNotification] D-Bus connection error:", err); + sessionBus = null; // 报错清理死对象 + }); + } + return sessionBus; +} + +// 确保缓存目录存在 +async function ensureCacheDir(): Promise { + if (!avatarCacheDir) { + avatarCacheDir = join(app.getPath("temp"), "weflow-avatars"); + try { + await fs.mkdir(avatarCacheDir, { recursive: true }); + } catch (error) { + console.error( + "[LinuxNotification] Failed to create avatar cache dir:", + error, + ); + } + } + return avatarCacheDir; +} + +// 下载头像到本地临时文件 +async function downloadAvatarToLocal(url: string): Promise { + // 检查缓存 + if (avatarCache.has(url)) { + return avatarCache.get(url) || null; + } + + try { + const cacheDir = await ensureCacheDir(); + // 生成唯一文件名 + const fileName = `avatar_${Date.now()}_${Math.random().toString(36).substring(2, 8)}.png`; + const localPath = join(cacheDir, fileName); + + await new Promise((resolve, reject) => { + // 微信 CDN 需要特殊的请求头才能下载图片 + const options = { + headers: { + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) WindowsWechat(0x63090719) XWEB/8351", + Referer: "https://servicewechat.com/", + Accept: + "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "zh-CN,zh;q=0.9", + Connection: "keep-alive", + }, + }; + + const callback = (res: IncomingMessage) => { + if (res.statusCode !== 200) { + reject(new Error(`HTTP ${res.statusCode}`)); + return; + } + const chunks: Buffer[] = []; + res.on("data", (chunk: Buffer) => chunks.push(chunk)); + res.on("end", async () => { + try { + const buffer = Buffer.concat(chunks); + await fs.writeFile(localPath, buffer); + avatarCache.set(url, localPath); + resolve(); + } catch (err) { + reject(err); + } + }); + res.on("error", reject); + }; + + const req = url.startsWith("https") + ? https.get(url, options, callback) + : http.get(url, options, callback); + + req.on("error", reject); + req.setTimeout(10000, () => { + req.destroy(); + reject(new Error("Download timeout")); + }); + }); + + console.log( + `[LinuxNotification] Avatar downloaded: ${url} -> ${localPath}`, + ); + return localPath; + } catch (error) { + console.error("[LinuxNotification] Failed to download avatar:", error); + return null; + } +} + +export async function showLinuxNotification( + data: LinuxNotificationData, +): Promise { + try { + const bus = await getSessionBus(); + + const appName = "WeFlow"; + const replaceId = 0; + const expireTimeout = data.expireTimeout ?? 5000; + + // 处理头像:下载到本地或使用URL + let appIcon = ""; + let hints: any[] = []; + if (data.avatarUrl) { + // 优先尝试下载到本地 + const localPath = await downloadAvatarToLocal(data.avatarUrl); + if (localPath) { + hints = [["image-path", ["s", localPath]]]; + } + } + + return new Promise((resolve, reject) => { + bus.invoke( + { + destination: BUS_NAME, + path: OBJECT_PATH, + interface: "org.freedesktop.Notifications", + member: "Notify", + signature: "susssasa{sv}i", + body: [ + appName, + replaceId, + appIcon, + data.title, + data.content, + ["default", "打开"], // 提供default action,否则系统不会抛出点击事件 + hints, + // [], // 传空数组以避开a{sv}变体的序列化崩溃,有pendingNotifications映射维护保证不出错 + expireTimeout, + ], + }, + (err: Error | null, result: any) => { + if (err) { + console.error("[LinuxNotification] Notify error:", err); + reject(err); + return; + } + const notificationId = + typeof result === "number" ? result : result[0]; + if (data.sessionId) { + // 依赖Map实现点击追踪,没有使用D-Bus hints + pendingNotifications.set(notificationId, data); + } + console.log( + `[LinuxNotification] Shown notification ${notificationId}: ${data.title}, icon: ${appIcon || "none"}`, + ); + resolve(notificationId); + }, + ); + }); + } catch (error) { + console.error("[LinuxNotification] Failed to show notification:", error); + return null; + } +} + +export async function closeLinuxNotification( + notificationId: number, +): Promise { + try { + const bus = await getSessionBus(); + return new Promise((resolve, reject) => { + bus.invoke( + { + destination: BUS_NAME, + path: OBJECT_PATH, + interface: "org.freedesktop.Notifications", + member: "CloseNotification", + signature: "u", + body: [notificationId], + }, + (err: Error | null) => { + if (err) { + console.error("[LinuxNotification] CloseNotification error:", err); + reject(err); + return; + } + pendingNotifications.delete(notificationId); + resolve(); + }, + ); + }); + } catch (error) { + console.error("[LinuxNotification] Failed to close notification:", error); + } +} + +export async function getCapabilities(): Promise { + try { + const bus = await getSessionBus(); + return new Promise((resolve, reject) => { + bus.invoke( + { + destination: BUS_NAME, + path: OBJECT_PATH, + interface: "org.freedesktop.Notifications", + member: "GetCapabilities", + }, + (err: Error | null, result: any) => { + if (err) { + console.error("[LinuxNotification] GetCapabilities error:", err); + reject(err); + return; + } + resolve(result as string[]); + }, + ); + }); + } catch (error) { + console.error("[LinuxNotification] Failed to get capabilities:", error); + return []; + } +} + +export function onNotificationAction(callback: NotificationCallback): void { + notificationCallbacks.push(callback); +} + +export function removeNotificationCallback( + callback: NotificationCallback, +): void { + const index = notificationCallbacks.indexOf(callback); + if (index > -1) { + notificationCallbacks.splice(index, 1); + } +} + +function triggerNotificationCallback(sessionId: string): void { + for (const callback of notificationCallbacks) { + try { + callback(sessionId); + } catch (error) { + console.error("[LinuxNotification] Callback error:", error); + } + } +} + +export async function initLinuxNotificationService(): Promise { + if (process.platform !== "linux") { + console.log("[LinuxNotification] Not on Linux, skipping init"); + return; + } + + try { + const bus = await getSessionBus(); + + // 监听底层connection的message事件 + bus.connection.on("message", (msg: any) => { + // type 4表示SIGNAL + if ( + msg.type === 4 && + msg.path === OBJECT_PATH && + msg.interface === "org.freedesktop.Notifications" + ) { + if (msg.member === "ActionInvoked") { + const [notificationId, actionId] = msg.body; + console.log( + `[LinuxNotification] Action invoked: ${notificationId}, ${actionId}`, + ); + + // 如果用户点击了通知本体,actionId会是'default' + if (actionId === "default") { + const data = pendingNotifications.get(notificationId); + if (data?.sessionId) { + triggerNotificationCallback(data.sessionId); + } + } + } + + if (msg.member === "NotificationClosed") { + const [notificationId] = msg.body; + pendingNotifications.delete(notificationId); + } + } + }); + + // AddMatch用来接收信号 + await new Promise((resolve, reject) => { + bus.invoke( + { + destination: "org.freedesktop.DBus", + path: "/org/freedesktop/DBus", + interface: "org.freedesktop.DBus", + member: "AddMatch", + signature: "s", + body: ["type='signal',interface='org.freedesktop.Notifications'"], + }, + (err: Error | null) => { + if (err) { + console.error("[LinuxNotification] AddMatch error:", err); + reject(err); + return; + } + resolve(); + }, + ); + }); + + console.log("[LinuxNotification] Service initialized"); + + // 打印相关日志 + const caps = await getCapabilities(); + console.log("[LinuxNotification] Server capabilities:", caps); + } catch (error) { + console.error("[LinuxNotification] Failed to initialize:", error); + } +} diff --git a/electron/types/dbus.d.ts b/electron/types/dbus.d.ts new file mode 100644 index 0000000..9585a42 --- /dev/null +++ b/electron/types/dbus.d.ts @@ -0,0 +1,18 @@ +declare module 'dbus-native' { + namespace dbus { + interface DBusConnection { + invoke(options: any, callback: (err: Error | null, result?: any) => void): void; + on(event: string, listener: Function): void; + // 底层connection,用于监听signal + connection: { + on(event: string, listener: Function): void; + }; + } + + // 声明sessionBus方法 + function sessionBus(): DBusConnection; + function systemBus(): DBusConnection; + } + + export = dbus; +} diff --git a/electron/windows/notificationWindow.ts b/electron/windows/notificationWindow.ts index fc31ccc..587f43e 100644 --- a/electron/windows/notificationWindow.ts +++ b/electron/windows/notificationWindow.ts @@ -1,224 +1,333 @@ -import { BrowserWindow, ipcMain, screen } from 'electron' -import { join } from 'path' -import { ConfigService } from '../services/config' +import { BrowserWindow, ipcMain, screen } from "electron"; +import { join } from "path"; +import { ConfigService } from "../services/config"; -let notificationWindow: BrowserWindow | null = null -let closeTimer: NodeJS.Timeout | null = null +// Linux D-Bus通知服务 +const isLinux = process.platform === "linux"; +let linuxNotificationService: + | typeof import("../services/linuxNotificationService") + | null = null; + +// 用于处理通知点击的回调函数(在Linux上用于导航到会话) +let onNotificationNavigate: ((sessionId: string) => void) | null = null; + +export function setNotificationNavigateHandler( + callback: (sessionId: string) => void, +) { + onNotificationNavigate = callback; +} + +let notificationWindow: BrowserWindow | null = null; +let closeTimer: NodeJS.Timeout | null = null; export function destroyNotificationWindow() { - if (closeTimer) { - clearTimeout(closeTimer) - closeTimer = null - } - lastNotificationData = null + if (closeTimer) { + clearTimeout(closeTimer); + closeTimer = null; + } + lastNotificationData = null; - if (!notificationWindow || notificationWindow.isDestroyed()) { - notificationWindow = null - return - } + if (!notificationWindow || notificationWindow.isDestroyed()) { + notificationWindow = null; + return; + } - const win = notificationWindow - notificationWindow = null + const win = notificationWindow; + notificationWindow = null; - try { - win.destroy() - } catch (error) { - console.warn('[NotificationWindow] Failed to destroy window:', error) - } + try { + win.destroy(); + } catch (error) { + console.warn("[NotificationWindow] Failed to destroy window:", error); + } } export function createNotificationWindow() { - if (notificationWindow && !notificationWindow.isDestroyed()) { - return notificationWindow - } + if (notificationWindow && !notificationWindow.isDestroyed()) { + return notificationWindow; + } - const isDev = !!process.env.VITE_DEV_SERVER_URL - const iconPath = isDev - ? join(__dirname, '../../public/icon.ico') - : join(process.resourcesPath, 'icon.ico') + const isDev = !!process.env.VITE_DEV_SERVER_URL; + const iconPath = isDev + ? join(__dirname, "../../public/icon.ico") + : join(process.resourcesPath, "icon.ico"); - console.log('[NotificationWindow] Creating window...') - const width = 344 - const height = 114 + console.log("[NotificationWindow] Creating window..."); + const width = 344; + const height = 114; - // Update default creation size - notificationWindow = new BrowserWindow({ - width: width, - height: height, - type: 'toolbar', // 有助于在某些操作系统上保持置顶 - frame: false, - transparent: true, - resizable: false, - show: false, - alwaysOnTop: true, - skipTaskbar: true, - focusable: false, // 不抢占焦点 - icon: iconPath, - webPreferences: { - preload: join(__dirname, 'preload.js'), // FIX: Use correct relative path (same dir in dist) - contextIsolation: true, - nodeIntegration: false, - // devTools: true // Enable DevTools - } - }) + // Update default creation size + notificationWindow = new BrowserWindow({ + width: width, + height: height, + type: "toolbar", // 有助于在某些操作系统上保持置顶 + frame: false, + transparent: true, + resizable: false, + show: false, + alwaysOnTop: true, + skipTaskbar: true, + focusable: false, // 不抢占焦点 + icon: iconPath, + webPreferences: { + preload: join(__dirname, "preload.js"), // FIX: Use correct relative path (same dir in dist) + contextIsolation: true, + nodeIntegration: false, + // devTools: true // Enable DevTools + }, + }); - // notificationWindow.webContents.openDevTools({ mode: 'detach' }) // DEBUG: Force Open DevTools - notificationWindow.setIgnoreMouseEvents(true, { forward: true }) // 初始点击穿透 + // notificationWindow.webContents.openDevTools({ mode: 'detach' }) // DEBUG: Force Open DevTools + notificationWindow.setIgnoreMouseEvents(true, { forward: true }); // 初始点击穿透 - // 处理鼠标事件 (如果需要从渲染进程转发,但目前特定区域处理?) - // 实际上,我们希望窗口可点击。 - // 我们将在显示时将忽略鼠标事件设为 false。 + // 处理鼠标事件 (如果需要从渲染进程转发,但目前特定区域处理?) + // 实际上,我们希望窗口可点击。 + // 我们将在显示时将忽略鼠标事件设为 false。 - const loadUrl = isDev - ? `${process.env.VITE_DEV_SERVER_URL}#/notification-window` - : `file://${join(__dirname, '../dist/index.html')}#/notification-window` + const loadUrl = isDev + ? `${process.env.VITE_DEV_SERVER_URL}#/notification-window` + : `file://${join(__dirname, "../dist/index.html")}#/notification-window`; - console.log('[NotificationWindow] Loading URL:', loadUrl) - notificationWindow.loadURL(loadUrl) + console.log("[NotificationWindow] Loading URL:", loadUrl); + notificationWindow.loadURL(loadUrl); - notificationWindow.on('closed', () => { - notificationWindow = null - }) + notificationWindow.on("closed", () => { + notificationWindow = null; + }); - return notificationWindow + return notificationWindow; } export async function showNotification(data: any) { - // 先检查配置 - const config = ConfigService.getInstance() - const enabled = await config.get('notificationEnabled') - if (enabled === false) return // 默认为 true + // 先检查配置 + const config = ConfigService.getInstance(); + const enabled = await config.get("notificationEnabled"); + if (enabled === false) return; // 默认为 true - // 检查会话过滤 - const filterMode = config.get('notificationFilterMode') || 'all' - const filterList = config.get('notificationFilterList') || [] - const sessionId = data.sessionId + // 检查会话过滤 + const filterMode = config.get("notificationFilterMode") || "all"; + const filterList = config.get("notificationFilterList") || []; + const sessionId = data.sessionId; - if (sessionId && filterMode !== 'all' && filterList.length > 0) { - const isInList = filterList.includes(sessionId) - if (filterMode === 'whitelist' && !isInList) { - // 白名单模式:不在列表中则不显示 - return - } - if (filterMode === 'blacklist' && isInList) { - // 黑名单模式:在列表中则不显示 - return - } + if (sessionId && filterMode !== "all" && filterList.length > 0) { + const isInList = filterList.includes(sessionId); + if (filterMode === "whitelist" && !isInList) { + // 白名单模式:不在列表中则不显示 + return; } - - let win = notificationWindow - if (!win || win.isDestroyed()) { - win = createNotificationWindow() + if (filterMode === "blacklist" && isInList) { + // 黑名单模式:在列表中则不显示 + return; } + } - if (!win) return + // Linux 使用 D-Bus 通知 + if (isLinux) { + await showLinuxNotification(data); + return; + } - // 确保加载完成 - if (win.webContents.isLoading()) { - win.once('ready-to-show', () => { - showAndSend(win!, data) - }) - } else { - showAndSend(win, data) - } + let win = notificationWindow; + if (!win || win.isDestroyed()) { + win = createNotificationWindow(); + } + + if (!win) return; + + // 确保加载完成 + if (win.webContents.isLoading()) { + win.once("ready-to-show", () => { + showAndSend(win!, data); + }); + } else { + showAndSend(win, data); + } } -let lastNotificationData: any = null +// 显示Linux通知 +async function showLinuxNotification(data: any) { + if (!linuxNotificationService) { + try { + linuxNotificationService = + await import("../services/linuxNotificationService"); + } catch (error) { + console.error( + "[NotificationWindow] Failed to load Linux notification service:", + error, + ); + return; + } + } + + const { showLinuxNotification: showNotification } = linuxNotificationService; + + const notificationData = { + title: data.title, + content: data.content, + avatarUrl: data.avatarUrl, + sessionId: data.sessionId, + expireTimeout: 5000, + }; + + showNotification(notificationData); +} + +let lastNotificationData: any = null; async function showAndSend(win: BrowserWindow, data: any) { - lastNotificationData = data - const config = ConfigService.getInstance() - const position = (await config.get('notificationPosition')) || 'top-right' + lastNotificationData = data; + const config = ConfigService.getInstance(); + const position = (await config.get("notificationPosition")) || "top-right"; - // 更新位置 - const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize - const winWidth = position === 'top-center' ? 280 : 344 - const winHeight = 114 - const padding = 20 + // 更新位置 + const { width: screenWidth, height: screenHeight } = + screen.getPrimaryDisplay().workAreaSize; + const winWidth = position === "top-center" ? 280 : 344; + const winHeight = 114; + const padding = 20; - let x = 0 - let y = 0 + let x = 0; + let y = 0; - switch (position) { - case 'top-center': - x = (screenWidth - winWidth) / 2 - y = padding - break - case 'top-right': - x = screenWidth - winWidth - padding - y = padding - break - case 'bottom-right': - x = screenWidth - winWidth - padding - y = screenHeight - winHeight - padding - break - case 'top-left': - x = padding - y = padding - break - case 'bottom-left': - x = padding - y = screenHeight - winHeight - padding - break + switch (position) { + case "top-center": + x = (screenWidth - winWidth) / 2; + y = padding; + break; + case "top-right": + x = screenWidth - winWidth - padding; + y = padding; + break; + case "bottom-right": + x = screenWidth - winWidth - padding; + y = screenHeight - winHeight - padding; + break; + case "top-left": + x = padding; + y = padding; + break; + case "bottom-left": + x = padding; + y = screenHeight - winHeight - padding; + break; + } + + win.setPosition(Math.floor(x), Math.floor(y)); + win.setSize(winWidth, winHeight); // 确保尺寸 + + // 设为可交互 + win.setIgnoreMouseEvents(false); + win.showInactive(); // 显示但不聚焦 + win.setAlwaysOnTop(true, "screen-saver"); // 最高层级 + + win.webContents.send("notification:show", { ...data, position }); + + // 自动关闭计时器通常由渲染进程管理 + // 渲染进程发送 'notification:close' 来隐藏窗口 +} + +// 注册通知处理 +export async function registerNotificationHandlers() { + // Linux: 初始化D-Bus服务 + if (isLinux) { + try { + const linuxNotificationModule = + await import("../services/linuxNotificationService"); + linuxNotificationService = linuxNotificationModule; + + // 初始化服务 + await linuxNotificationModule.initLinuxNotificationService(); + + // 在Linux上注册通知点击回调 + linuxNotificationModule.onNotificationAction((sessionId: string) => { + console.log( + "[NotificationWindow] Linux notification clicked, sessionId:", + sessionId, + ); + // 如果设置了导航处理程序,则使用该处理程序;否则,回退到ipcMain方法。 + if (onNotificationNavigate) { + onNotificationNavigate(sessionId); + } else { + // 如果尚未设置处理程序,则通过ipcMain发出事件 + // 正常流程中不应该发生这种情况,因为我们在初始化之前设置了处理程序。 + console.warn( + "[NotificationWindow] onNotificationNavigate not set yet", + ); + } + }); + + console.log( + "[NotificationWindow] Linux notification service initialized", + ); + } catch (error) { + console.error( + "[NotificationWindow] Failed to initialize Linux notification service:", + error, + ); } + } - win.setPosition(Math.floor(x), Math.floor(y)) - win.setSize(winWidth, winHeight) // 确保尺寸 + ipcMain.handle("notification:show", (_, data) => { + showNotification(data); + }); - // 设为可交互 - win.setIgnoreMouseEvents(false) - win.showInactive() // 显示但不聚焦 - win.setAlwaysOnTop(true, 'screen-saver') // 最高层级 + ipcMain.handle("notification:close", () => { + if (isLinux && linuxNotificationService) { + // 注册通知点击回调函数。Linux通知通过D-Bus自动关闭,但我们可以根据需要进行跟踪 + return; + } + if (notificationWindow && !notificationWindow.isDestroyed()) { + notificationWindow.hide(); + notificationWindow.setIgnoreMouseEvents(true, { forward: true }); + } + }); - win.webContents.send('notification:show', { ...data, position }) + // Handle renderer ready event (fix race condition) + ipcMain.on("notification:ready", (event) => { + if (isLinux) { + // Linux不需要通知窗口,拦截通知窗口渲染 + return; + } + console.log("[NotificationWindow] Renderer ready, checking cached data"); + if ( + lastNotificationData && + notificationWindow && + !notificationWindow.isDestroyed() + ) { + console.log("[NotificationWindow] Re-sending cached data"); + notificationWindow.webContents.send( + "notification:show", + lastNotificationData, + ); + } + }); - // 自动关闭计时器通常由渲染进程管理 - // 渲染进程发送 'notification:close' 来隐藏窗口 -} - -export function registerNotificationHandlers() { - ipcMain.handle('notification:show', (_, data) => { - showNotification(data) - }) - - ipcMain.handle('notification:close', () => { - if (notificationWindow && !notificationWindow.isDestroyed()) { - notificationWindow.hide() - notificationWindow.setIgnoreMouseEvents(true, { forward: true }) - } - }) - - // Handle renderer ready event (fix race condition) - ipcMain.on('notification:ready', (event) => { - console.log('[NotificationWindow] Renderer ready, checking cached data') - if (lastNotificationData && notificationWindow && !notificationWindow.isDestroyed()) { - console.log('[NotificationWindow] Re-sending cached data') - notificationWindow.webContents.send('notification:show', lastNotificationData) - } - }) - - // Handle resize request from renderer - ipcMain.on('notification:resize', (event, { width, height }) => { - if (notificationWindow && !notificationWindow.isDestroyed()) { - // Enforce max-height if needed, or trust renderer - // Ensure it doesn't go off screen bottom? - // Logic in showAndSend handles position, but we need to keep anchor point (top-right usually). - // If we resize, we should re-calculate position to keep it anchored? - // Actually, setSize changes size. If it's top-right, x/y stays same -> window grows down. That's fine for top-right. - // If bottom-right, growing down pushes it off screen. - - // Simple version: just setSize. For V1 we assume Top-Right. - // But wait, the config supports bottom-right. - // We can re-call setPosition or just let it be. - // If bottom-right, y needs to prevent overflow. - - // Ideally we get current config position - const bounds = notificationWindow.getBounds() - // Check if we need to adjust Y? - // For now, let's just set the size as requested. - notificationWindow.setSize(Math.round(width), Math.round(height)) - } - }) - - // 'notification-clicked' 在 main.ts 中处理 (导航) + // Handle resize request from renderer + ipcMain.on("notification:resize", (event, { width, height }) => { + if (isLinux) { + // Linux 通知通过D-Bus自动调整大小 + return; + } + if (notificationWindow && !notificationWindow.isDestroyed()) { + // Enforce max-height if needed, or trust renderer + // Ensure it doesn't go off screen bottom? + // Logic in showAndSend handles position, but we need to keep anchor point (top-right usually). + // If we resize, we should re-calculate position to keep it anchored? + // Actually, setSize changes size. If it's top-right, x/y stays same -> window grows down. That's fine for top-right. + // If bottom-right, growing down pushes it off screen. + + // Simple version: just setSize. For V1 we assume Top-Right. + // But wait, the config supports bottom-right. + // We can re-call setPosition or just let it be. + // If bottom-right, y needs to prevent overflow. + + // Ideally we get current config position + const bounds = notificationWindow.getBounds(); + // Check if we need to adjust Y? + // For now, let's just set the size as requested. + notificationWindow.setSize(Math.round(width), Math.round(height)); + } + }); + + // 'notification-clicked' 在 main.ts 中处理 (导航) } diff --git a/package.json b/package.json index 17dadf0..3e7e9e2 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@vscode/sudo-prompt": "^9.3.2", + "dbus-native": "^0.4.0", "echarts": "^6.0.0", "echarts-for-react": "^3.0.2", "electron-store": "^11.0.2",