2024-08-28 22:01:05 +08:00

316 lines
9.1 KiB
TypeScript

import {
getAppConfig,
getControledMihomoConfig,
patchAppConfig,
patchControledMihomoConfig
} from '../config'
import icoIcon from '../../../resources/icon.ico?asset'
import pngIcon from '../../../resources/icon.png?asset'
import templateIcon from '../../../resources/iconTemplate.png?asset'
import {
mihomoChangeProxy,
mihomoCloseAllConnections,
mihomoGroups,
patchMihomoConfig
} from '../core/mihomoApi'
import { mainWindow, showMainWindow } from '..'
import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron'
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
import { triggerSysProxy } from '../sys/sysproxy'
import { restartCore } from '../core/manager'
export let tray: Tray | null = null
const buildContextMenu = async (): Promise<Menu> => {
const { mode, tun } = await getControledMihomoConfig()
const {
sysProxy,
autoCloseConnection,
proxyInTray = true,
triggerSysProxyShortcut = '',
showWindowShortcut = '',
triggerTunShortcut = '',
ruleModeShortcut = '',
globalModeShortcut = '',
directModeShortcut = '',
restartAppShortcut = ''
} = await getAppConfig()
let groupsMenu: Electron.MenuItemConstructorOptions[] = []
if (proxyInTray && process.platform !== 'linux') {
try {
const groups = await mihomoGroups()
groupsMenu = groups.map((group) => {
return {
id: group.name,
label: group.name,
type: 'submenu',
submenu: group.all.map((proxy) => {
const delay = proxy.history.length ? proxy.history[proxy.history.length - 1].delay : -1
let displayDelay = `(${delay}ms)`
if (delay === -1) {
displayDelay = ''
}
if (delay === 0) {
displayDelay = '(Timeout)'
}
return {
id: proxy.name,
label: `${proxy.name} ${displayDelay}`,
type: 'radio',
checked: proxy.name === group.now,
click: async (): Promise<void> => {
await mihomoChangeProxy(group.name, proxy.name)
if (autoCloseConnection) {
await mihomoCloseAllConnections()
}
}
}
})
}
})
groupsMenu.unshift({ type: 'separator' })
} catch (e) {
// ignore
// 避免出错时无法创建托盘菜单
}
}
const contextMenu = [
{
id: 'show',
accelerator: showWindowShortcut,
label: '显示窗口',
type: 'normal',
click: (): void => {
showMainWindow()
}
},
{
id: 'rule',
label: '规则模式',
accelerator: ruleModeShortcut,
type: 'radio',
checked: mode === 'rule',
click: async (): Promise<void> => {
await patchControledMihomoConfig({ mode: 'rule' })
await patchMihomoConfig({ mode: 'rule' })
mainWindow?.webContents.send('controledMihomoConfigUpdated')
ipcMain.emit('updateTrayMenu')
}
},
{
id: 'global',
label: '全局模式',
accelerator: globalModeShortcut,
type: 'radio',
checked: mode === 'global',
click: async (): Promise<void> => {
await patchControledMihomoConfig({ mode: 'global' })
await patchMihomoConfig({ mode: 'global' })
mainWindow?.webContents.send('controledMihomoConfigUpdated')
ipcMain.emit('updateTrayMenu')
}
},
{
id: 'direct',
label: '直连模式',
accelerator: directModeShortcut,
type: 'radio',
checked: mode === 'direct',
click: async (): Promise<void> => {
await patchControledMihomoConfig({ mode: 'direct' })
await patchMihomoConfig({ mode: 'direct' })
mainWindow?.webContents.send('controledMihomoConfigUpdated')
ipcMain.emit('updateTrayMenu')
}
},
{ type: 'separator' },
{
type: 'checkbox',
label: '系统代理',
accelerator: triggerSysProxyShortcut,
checked: sysProxy.enable,
click: async (item): Promise<void> => {
const enable = item.checked
try {
await triggerSysProxy(enable)
await patchAppConfig({ sysProxy: { enable } })
} catch (e) {
// ignore
} finally {
mainWindow?.webContents.send('appConfigUpdated')
ipcMain.emit('updateTrayMenu')
}
}
},
{
type: 'checkbox',
label: '虚拟网卡',
accelerator: triggerTunShortcut,
checked: tun?.enable ?? false,
click: async (item): Promise<void> => {
const enable = item.checked
if (enable) {
await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } })
} else {
await patchControledMihomoConfig({ tun: { enable } })
}
mainWindow?.webContents.send('controledMihomoConfigUpdated')
await restartCore()
ipcMain.emit('updateTrayMenu')
}
},
...groupsMenu,
{ type: 'separator' },
{
type: 'submenu',
label: '打开目录',
submenu: [
{
type: 'normal',
label: '应用目录',
click: (): Promise<string> => shell.openPath(dataDir())
},
{
type: 'normal',
label: '工作目录',
click: (): Promise<string> => shell.openPath(mihomoWorkDir())
},
{
type: 'normal',
label: '内核目录',
click: (): Promise<string> => shell.openPath(mihomoCoreDir())
},
{
type: 'normal',
label: '日志目录',
click: (): Promise<string> => shell.openPath(logDir())
}
]
},
{
id: 'copyenv',
label: '复制环境变量',
type: 'normal',
click: copyEnv
},
{ type: 'separator' },
{
id: 'restart',
label: '重启应用',
type: 'normal',
accelerator: restartAppShortcut,
click: (): void => {
app.relaunch()
app.quit()
}
},
{
id: 'quit',
label: '退出应用',
type: 'normal',
accelerator: 'CommandOrControl+Q',
click: (): void => app.quit()
}
] as Electron.MenuItemConstructorOptions[]
return Menu.buildFromTemplate(contextMenu)
}
export async function createTray(): Promise<void> {
const { useDockIcon = true } = await getAppConfig()
if (process.platform === 'linux') {
tray = new Tray(pngIcon)
const menu = await buildContextMenu()
tray.setContextMenu(menu)
}
if (process.platform === 'darwin') {
const icon = nativeImage.createFromPath(templateIcon).resize({ height: 16 })
icon.setTemplateImage(true)
tray = new Tray(icon)
}
if (process.platform === 'win32') {
tray = new Tray(icoIcon)
}
tray?.setToolTip('Mihomo Party')
tray?.setIgnoreDoubleClickEvents(true)
if (process.platform === 'darwin') {
if (!useDockIcon) {
app.dock.hide()
}
ipcMain.on('trayIconUpdate', async (_, png: string) => {
const image = nativeImage.createFromDataURL(png).resize({ height: 16 })
image.setTemplateImage(true)
tray?.setImage(image)
})
tray?.addListener('right-click', async () => {
if (mainWindow?.isVisible()) {
mainWindow?.close()
} else {
showMainWindow()
}
})
tray?.addListener('click', async () => {
await updateTrayMenu()
})
}
if (process.platform === 'win32') {
tray?.addListener('click', () => {
if (mainWindow?.isVisible()) {
mainWindow?.close()
} else {
showMainWindow()
}
})
tray?.addListener('right-click', async () => {
await updateTrayMenu()
})
}
if (process.platform === 'linux') {
tray?.addListener('click', () => {
if (mainWindow?.isVisible()) {
mainWindow?.close()
} else {
showMainWindow()
}
})
ipcMain.on('updateTrayMenu', async () => {
await updateTrayMenu()
})
}
}
async function updateTrayMenu(): Promise<void> {
const menu = await buildContextMenu()
tray?.popUpContextMenu(menu) // 弹出菜单
if (process.platform === 'linux') {
tray?.setContextMenu(menu)
}
}
export async function copyEnv(): Promise<void> {
const defaultType = process.platform === 'win32' ? 'powershell' : 'bash'
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
const { envType = defaultType, sysProxy } = await getAppConfig()
const { host } = sysProxy
switch (envType) {
case 'bash': {
clipboard.writeText(
`export https_proxy=http://${host || '127.0.0.1'}:${mixedPort} http_proxy=http://${host || '127.0.0.1'}:${mixedPort} all_proxy=http://${host || '127.0.0.1'}:${mixedPort}`
)
break
}
case 'cmd': {
clipboard.writeText(
`set http_proxy=http://${host || '127.0.0.1'}:${mixedPort}\r\nset https_proxy=http://${host || '127.0.0.1'}:${mixedPort}`
)
break
}
case 'powershell': {
clipboard.writeText(
`$env:HTTP_PROXY="http://${host || '127.0.0.1'}:${mixedPort}"; $env:HTTPS_PROXY="http://${host || '127.0.0.1'}:${mixedPort}"`
)
break
}
}
}