import { is } from '@electron-toolkit/utils' import { BrowserWindow, ipcMain } from 'electron' import windowStateKeeper from 'electron-window-state' import { join } from 'path' import { getAppConfig, patchAppConfig } from '../config' import { applyTheme } from './theme' import { buildContextMenu, showTrayIcon } from './tray' import { floatingWindowLogger } from '../utils/logger' export let floatingWindow: BrowserWindow | null = null function logError(message: string, error?: any): void { floatingWindowLogger.log(`FloatingWindow Error: ${message}`, error).catch(() => {}) } async function createFloatingWindow(): Promise { try { const floatingWindowState = windowStateKeeper({ file: 'floating-window-state.json' }) const { customTheme = 'default.css', floatingWindowCompatMode = true } = await getAppConfig() const safeMode = process.env.FLOATING_SAFE_MODE === 'true' const useCompatMode = floatingWindowCompatMode || process.env.FLOATING_COMPAT_MODE === 'true' || safeMode const windowOptions: Electron.BrowserWindowConstructorOptions = { width: 120, height: 42, x: floatingWindowState.x, y: floatingWindowState.y, show: false, frame: safeMode, alwaysOnTop: !safeMode, resizable: safeMode, transparent: !safeMode && !useCompatMode, skipTaskbar: !safeMode, minimizable: safeMode, maximizable: safeMode, fullscreenable: false, closable: safeMode, backgroundColor: safeMode ? '#ffffff' : (useCompatMode ? '#f0f0f0' : '#00000000'), webPreferences: { preload: join(__dirname, '../preload/index.js'), spellcheck: false, sandbox: false, nodeIntegration: false, contextIsolation: true } } if (process.platform === 'win32') { windowOptions.hasShadow = !safeMode windowOptions.webPreferences!.offscreen = false } floatingWindow = new BrowserWindow(windowOptions) floatingWindowState.manage(floatingWindow) // 事件监听器 floatingWindow.webContents.on('render-process-gone', (_, details) => { logError('Render process gone', details.reason) floatingWindow = null }) floatingWindow.on('ready-to-show', () => { applyTheme(customTheme) floatingWindow?.show() floatingWindow?.setAlwaysOnTop(true, 'screen-saver') }) floatingWindow.on('moved', () => { floatingWindow && floatingWindowState.saveState(floatingWindow) }) // IPC 监听器 ipcMain.on('updateFloatingWindow', () => { if (floatingWindow) { floatingWindow.webContents.send('controledMihomoConfigUpdated') floatingWindow.webContents.send('appConfigUpdated') } }) // 加载页面 const url = is.dev && process.env['ELECTRON_RENDERER_URL'] ? `${process.env['ELECTRON_RENDERER_URL']}/floating.html` : join(__dirname, '../renderer/floating.html') is.dev ? await floatingWindow.loadURL(url) : await floatingWindow.loadFile(url) } catch (error) { logError('Failed to create floating window', error) floatingWindow = null throw error } } export async function showFloatingWindow(): Promise { try { if (floatingWindow && !floatingWindow.isDestroyed()) { floatingWindow.show() } else { await createFloatingWindow() } } catch (error) { logError('Failed to show floating window', error) // 如果已经是兼容模式还是崩溃,自动禁用悬浮窗 const { floatingWindowCompatMode = true } = await getAppConfig() if (floatingWindowCompatMode) { await patchAppConfig({ showFloatingWindow: false }) } else { await patchAppConfig({ floatingWindowCompatMode: true }) } throw error } } export async function triggerFloatingWindow(): Promise { if (floatingWindow?.isVisible()) { await patchAppConfig({ showFloatingWindow: false }) await closeFloatingWindow() } else { await patchAppConfig({ showFloatingWindow: true }) await showFloatingWindow() } } export async function closeFloatingWindow(): Promise { if (floatingWindow) { floatingWindow.close() floatingWindow.destroy() floatingWindow = null } await showTrayIcon() await patchAppConfig({ disableTray: false }) } export async function showContextMenu(): Promise { const menu = await buildContextMenu() menu.popup() }