mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-06 23:15:58 +00:00
fix: 更改linux中的消息通知走D-bus总线
This commit is contained in:
@@ -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(
|
||||
{
|
||||
|
||||
344
electron/services/linuxNotificationService.ts
Normal file
344
electron/services/linuxNotificationService.ts
Normal file
@@ -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<number, LinuxNotificationData> = new Map();
|
||||
|
||||
// 头像缓存:url->localFilePath
|
||||
const avatarCache: Map<string, string> = new Map();
|
||||
// 缓存目录
|
||||
let avatarCacheDir: string | null = null;
|
||||
|
||||
async function getSessionBus(): Promise<dbus.DBusConnection> {
|
||||
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<string> {
|
||||
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<string | null> {
|
||||
// 检查缓存
|
||||
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<void>((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<number | null> {
|
||||
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<void> {
|
||||
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<string[]> {
|
||||
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<void> {
|
||||
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<void>((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);
|
||||
}
|
||||
}
|
||||
18
electron/types/dbus.d.ts
vendored
Normal file
18
electron/types/dbus.d.ts
vendored
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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 中处理 (导航)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user