feat: real-time tray icon color updates based on proxy status (#984)

* feat: real-time tray icon color updates based on proxy status

* Update changelog.md
This commit is contained in:
Memory 2025-08-22 12:46:57 +08:00 committed by GitHub
parent e357700d60
commit a57ea34f1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 92 additions and 13 deletions

View File

@ -3,6 +3,7 @@
### 新功能 (Feat)
- 支持 cron 表达式以自定义订阅更新频率(#766)
- 修复权限检查并优化TUN与自启联动(#977)
- 托盘图标能根据代理状态实时变化颜色
### 修复 (Fix)
- Windows 下当前运行内核权限检测(之前没有正确检测管理员权限运行的内核)
- Windows 下 开机自启 按钮卡顿问题 隐藏运行黑框 现在申请权限会弹通知
@ -48,4 +49,4 @@
### 新功能 (Feat)
- 重构 域名嗅探 卡片模块,改为“覆写”逻辑,当开关打开后,使用 嗅探覆写 设置中的配置覆盖订阅原始配置,关闭开关恢复订阅原始配置
- 订阅/覆写卡片可右键呼出菜单
- MacOS 下“轻触(tap)”触控板可进行开关操作(之前必须“按下(click)”)
- MacOS 下“轻触(tap)”触控板可进行开关操作(之前必须“按下(click)”)

BIN
resources/icon_blue.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
resources/icon_blue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
resources/icon_green.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
resources/icon_green.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
resources/icon_red.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
resources/icon_red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -169,7 +169,9 @@ export const mihomoUpgrade = async (): Promise<void> => {
}
// Smart 内核 API
export const mihomoSmartGroupWeights = async (groupName: string): Promise<Record<string, number>> => {
export const mihomoSmartGroupWeights = async (
groupName: string
): Promise<Record<string, number>> => {
const instance = await getAxios()
return await instance.get(`/group/${encodeURIComponent(groupName)}/weights`)
}
@ -362,3 +364,27 @@ const mihomoConnections = async (): Promise<void> => {
}
}
}
export async function SysProxyStatus(): Promise<boolean> {
const appConfig = await getAppConfig()
return appConfig.sysProxy.enable
}
export const TunStatus = async (): Promise<boolean> => {
const config = await getControledMihomoConfig()
return config?.tun?.enable === true
}
export async function getTrayIconStatus(): Promise<'white' | 'blue' | 'green' | 'red'> {
const [sysProxyEnabled, tunEnabled] = await Promise.all([SysProxyStatus(), TunStatus()])
if (sysProxyEnabled && tunEnabled) {
return 'red' // 系统代理 + TUN 同时启用(警告状态)
} else if (sysProxyEnabled) {
return 'blue' // 仅系统代理启用
} else if (tunEnabled) {
return 'green' // 仅 TUN 启用
} else {
return 'white' // 全关
}
}

View File

@ -7,14 +7,14 @@ import {
patchControledMihomoConfig
} from '../config'
import icoIcon from '../../../resources/icon.ico?asset'
import icoIconBlue from '../../../resources/icon_blue.ico?asset'
import icoIconRed from '../../../resources/icon_red.ico?asset'
import icoIconGreen from '../../../resources/icon_green.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 pngIconBlue from '../../../resources/icon_blue.png?asset'
import pngIconRed from '../../../resources/icon_red.png?asset'
import pngIconGreen from '../../../resources/icon_green.png?asset'
import { mihomoChangeProxy, mihomoCloseAllConnections, mihomoGroups, patchMihomoConfig, getTrayIconStatus } from '../core/mihomoApi'
import { mainWindow, showMainWindow, triggerMainWindow } from '..'
import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron'
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
@ -328,7 +328,8 @@ export async function createTray(): Promise<void> {
tray.setContextMenu(menu)
}
if (process.platform === 'darwin') {
const icon = nativeImage.createFromPath(templateIcon).resize({ height: 16 })
const iconPaths = getIconPaths()
const icon = nativeImage.createFromPath(iconPaths.white).resize({ height: 16 })
icon.setTemplateImage(true)
tray = new Tray(icon)
}
@ -337,6 +338,9 @@ export async function createTray(): Promise<void> {
}
tray?.setToolTip('Mihomo Party')
tray?.setIgnoreDoubleClickEvents(true)
await updateTrayIcon()
if (process.platform === 'darwin') {
if (!useDockIcon) {
hideDockIcon()
@ -377,6 +381,7 @@ async function updateTrayMenu(): Promise<void> {
if (process.platform === 'linux') {
tray?.setContextMenu(menu)
}
await updateTrayIcon()
}
export async function copyEnv(type: 'bash' | 'cmd' | 'powershell'): Promise<void> {
@ -429,3 +434,43 @@ export async function hideDockIcon(): Promise<void> {
app.dock.hide()
}
}
const getIconPaths = () => {
if (process.platform === 'win32') {
return {
white: icoIcon,
blue: icoIconBlue,
green: icoIconGreen,
red: icoIconRed
}
} else {
return {
white: pngIcon,
blue: pngIconBlue,
green: pngIconGreen,
red: pngIconRed
}
}
}
export async function updateTrayIcon(): Promise<void> {
if (!tray) return
const status = await getTrayIconStatus()
const iconPaths = getIconPaths()
const iconPath = iconPaths[status]
try {
if (process.platform === 'darwin') {
const icon = nativeImage.createFromPath(iconPath).resize({ height: 16 })
icon.setTemplateImage(true)
tray.setImage(icon)
} else if (process.platform === 'win32') {
tray.setImage(iconPath)
} else if (process.platform === 'linux') {
tray.setImage(iconPath)
}
} catch (error) {
console.error('更新托盘图标失败:', error)
}
}

View File

@ -84,7 +84,7 @@ import {
import { getRuntimeConfig, getRuntimeConfigStr } from '../core/factory'
import { listWebdavBackups, webdavBackup, webdavDelete, webdavRestore } from '../resolve/backup'
import { getInterfaces } from '../sys/interface'
import { closeTrayIcon, copyEnv, showTrayIcon } from '../resolve/tray'
import { closeTrayIcon, copyEnv, showTrayIcon, updateTrayIcon } from '../resolve/tray'
import { registerShortcut } from '../resolve/shortcut'
import { closeMainWindow, mainWindow, showMainWindow, triggerMainWindow } from '..'
import {
@ -259,6 +259,7 @@ export function registerIpcMainHandlers(): void {
})
ipcMain.handle('showTrayIcon', () => ipcErrorWrapper(showTrayIcon)())
ipcMain.handle('closeTrayIcon', () => ipcErrorWrapper(closeTrayIcon)())
ipcMain.handle('updateTrayIcon', () => ipcErrorWrapper(updateTrayIcon)())
ipcMain.handle('showMainWindow', showMainWindow)
ipcMain.handle('closeMainWindow', closeMainWindow)
ipcMain.handle('triggerMainWindow', triggerMainWindow)

View File

@ -2,7 +2,7 @@ import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
import BorderSwitch from '@renderer/components/base/border-swtich'
import { useLocation, useNavigate } from 'react-router-dom'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import { triggerSysProxy } from '@renderer/utils/ipc'
import { triggerSysProxy, updateTrayIcon } from '@renderer/utils/ipc'
import { AiOutlineGlobal } from 'react-icons/ai'
import React from 'react'
import { useSortable } from '@dnd-kit/sortable'
@ -43,6 +43,7 @@ const SysproxySwitcher: React.FC<Props> = (props) => {
window.electron.ipcRenderer.send('updateFloatingWindow')
window.electron.ipcRenderer.send('updateTrayMenu')
await updateTrayIcon()
} catch (e) {
await patchAppConfig({ sysProxy: { enable: previousState } })
alert(e)

View File

@ -3,7 +3,7 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
import BorderSwitch from '@renderer/components/base/border-swtich'
import { TbDeviceIpadHorizontalBolt } from 'react-icons/tb'
import { useLocation, useNavigate } from 'react-router-dom'
import { restartCore } from '@renderer/utils/ipc'
import { restartCore, updateTrayIcon } from '@renderer/utils/ipc'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import React from 'react'
@ -84,6 +84,7 @@ const TunSwitcher: React.FC<Props> = (props) => {
await restartCore()
window.electron.ipcRenderer.send('updateFloatingWindow')
window.electron.ipcRenderer.send('updateTrayMenu')
await updateTrayIcon()
}
if (iconOnly) {

View File

@ -402,6 +402,10 @@ export async function closeTrayIcon(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('closeTrayIcon'))
}
export async function updateTrayIcon(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('updateTrayIcon'))
}
export async function showMainWindow(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('showMainWindow'))
}