mihomo-party/src/main/resolve/floatingWindow.ts
2025-08-12 09:20:36 +08:00

141 lines
4.4 KiB
TypeScript

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<void> {
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<void> {
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<void> {
if (floatingWindow?.isVisible()) {
await patchAppConfig({ showFloatingWindow: false })
await closeFloatingWindow()
} else {
await patchAppConfig({ showFloatingWindow: true })
await showFloatingWindow()
}
}
export async function closeFloatingWindow(): Promise<void> {
if (floatingWindow) {
floatingWindow.close()
floatingWindow.destroy()
floatingWindow = null
}
await showTrayIcon()
await patchAppConfig({ disableTray: false })
}
export async function showContextMenu(): Promise<void> {
const menu = await buildContextMenu()
menu.popup()
}