342 lines
10 KiB
TypeScript
342 lines
10 KiB
TypeScript
import { BrowserWindow, ipcMain, screen } from "electron";
|
|
import { join } from "path";
|
|
import { ConfigService } from "../services/config";
|
|
|
|
// 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;
|
|
|
|
// Linux:关闭通知服务并清理缓存(fire-and-forget,不阻塞退出)
|
|
if (isLinux && linuxNotificationService) {
|
|
linuxNotificationService.shutdownLinuxNotificationService().catch((error) => {
|
|
console.warn("[NotificationWindow] Failed to shutdown Linux notification service:", error);
|
|
});
|
|
linuxNotificationService = null;
|
|
}
|
|
|
|
if (!notificationWindow || notificationWindow.isDestroyed()) {
|
|
notificationWindow = null;
|
|
return;
|
|
}
|
|
|
|
const win = notificationWindow;
|
|
notificationWindow = null;
|
|
|
|
try {
|
|
win.destroy();
|
|
} catch (error) {
|
|
console.warn("[NotificationWindow] Failed to destroy window:", error);
|
|
}
|
|
}
|
|
|
|
export function createNotificationWindow() {
|
|
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");
|
|
|
|
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
|
|
},
|
|
});
|
|
|
|
// notificationWindow.webContents.openDevTools({ mode: 'detach' }) // DEBUG: Force Open DevTools
|
|
notificationWindow.setIgnoreMouseEvents(true, { forward: true }); // 初始点击穿透
|
|
|
|
// 处理鼠标事件 (如果需要从渲染进程转发,但目前特定区域处理?)
|
|
// 实际上,我们希望窗口可点击。
|
|
// 我们将在显示时将忽略鼠标事件设为 false。
|
|
|
|
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);
|
|
|
|
notificationWindow.on("closed", () => {
|
|
notificationWindow = null;
|
|
});
|
|
|
|
return notificationWindow;
|
|
}
|
|
|
|
export async function showNotification(data: any) {
|
|
// 先检查配置
|
|
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;
|
|
|
|
if (sessionId && filterMode !== "all" && filterList.length > 0) {
|
|
const isInList = filterList.includes(sessionId);
|
|
if (filterMode === "whitelist" && !isInList) {
|
|
// 白名单模式:不在列表中则不显示
|
|
return;
|
|
}
|
|
if (filterMode === "blacklist" && isInList) {
|
|
// 黑名单模式:在列表中则不显示
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Linux 使用 D-Bus 通知
|
|
if (isLinux) {
|
|
await showLinuxNotification(data);
|
|
return;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// 显示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";
|
|
|
|
// 更新位置
|
|
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;
|
|
|
|
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,
|
|
);
|
|
}
|
|
}
|
|
|
|
ipcMain.handle("notification:show", (_, data) => {
|
|
showNotification(data);
|
|
});
|
|
|
|
ipcMain.handle("notification:close", () => {
|
|
if (isLinux && linuxNotificationService) {
|
|
// 注册通知点击回调函数。Linux通知通过D-Bus自动关闭,但我们可以根据需要进行跟踪
|
|
return;
|
|
}
|
|
if (notificationWindow && !notificationWindow.isDestroyed()) {
|
|
notificationWindow.hide();
|
|
notificationWindow.setIgnoreMouseEvents(true, { forward: true });
|
|
}
|
|
});
|
|
|
|
// 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,
|
|
);
|
|
}
|
|
});
|
|
|
|
// 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 中处理 (导航)
|
|
}
|