fix: use sync Canvas rendering for tray traffic icon to prevent flicker

Replace async SVG→Image→Canvas pipeline with synchronous Canvas rendering
to eliminate tray icon flickering. Use showTrafficRef to read showTraffic
value without recreating the IPC listener, preventing icon hiding caused
by appConfig temporarily becoming undefined during config reloads.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Martin 2026-03-20 21:37:08 -07:00
parent e2fbb2a8ad
commit 58d9e564e5

View File

@ -59,9 +59,8 @@ const ConnCard: React.FC<Props> = (props) => {
const currentUploadRef = useRef<number | undefined>(undefined)
const currentDownloadRef = useRef<number | undefined>(undefined)
const hasShowTrafficRef = useRef(false)
const drawingRef = useRef(false)
// 保存待绘制的流量数据,避免跳过更新导致图标闪烁
const pendingTrafficRef = useRef<{ up: number; down: number } | null>(null)
const showTrafficRef = useRef(showTraffic)
showTrafficRef.current = showTraffic
// Chart.js 配置
const chartData = useMemo(() => {
@ -128,9 +127,9 @@ const ConnCard: React.FC<Props> = (props) => {
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
// 使用 useCallback 创建稳定的 handler 引用
// 使用 useCallback 创建稳定的 handler 引用,通过 ref 读取 showTraffic 避免重建
const handleTraffic = useCallback(
async (_e: unknown, ...args: unknown[]) => {
(_e: unknown, ...args: unknown[]) => {
const info = args[0] as IMihomoTrafficInfo
setUpload(info.up)
setDownload(info.down)
@ -140,33 +139,18 @@ const ConnCard: React.FC<Props> = (props) => {
data.push(info.up + info.down)
return data
})
if (platform === 'darwin') {
if (showTraffic) {
// 保存最新流量数据,确保绘制完成后使用最新值
pendingTrafficRef.current = { up: info.up, down: info.down }
if (drawingRef.current) return
drawingRef.current = true
try {
// 循环处理待绘制数据,直到没有新数据
while (pendingTrafficRef.current) {
const { up, down } = pendingTrafficRef.current
pendingTrafficRef.current = null
await drawSvg(up, down, currentUploadRef, currentDownloadRef)
}
hasShowTrafficRef.current = true
} catch {
// ignore
} finally {
drawingRef.current = false
}
} else if (hasShowTrafficRef.current) {
// 只在从 showTraffic=true 切换到 false 时恢复一次原始图标
window.electron.ipcRenderer.send('trayIconUpdate', trayIconBase64, false)
hasShowTrafficRef.current = false
if (platform === 'darwin' && showTrafficRef.current) {
const up = info.up
const down = info.down
if (up !== currentUploadRef.current || down !== currentDownloadRef.current) {
currentUploadRef.current = up
currentDownloadRef.current = down
const png = renderTrafficIcon(up, down)
window.electron.ipcRenderer.send('trayIconUpdate', png, true)
}
}
},
[showTraffic]
[] // eslint-disable-line react-hooks/exhaustive-deps
)
useEffect(() => {
@ -176,6 +160,23 @@ const ConnCard: React.FC<Props> = (props) => {
}
}, [handleTraffic])
// showTraffic 开关切换时统一管理托盘图标
useEffect(() => {
if (platform !== 'darwin') return
if (showTraffic) {
// 开启:立即显示默认流量图标,重置缓存以确保下次流量事件触发更新
currentUploadRef.current = undefined
currentDownloadRef.current = undefined
const png = renderTrafficIcon(0, 0)
window.electron.ipcRenderer.send('trayIconUpdate', png, true)
hasShowTrafficRef.current = true
} else if (hasShowTrafficRef.current) {
// 关闭:恢复原始图标
window.electron.ipcRenderer.send('trayIconUpdate', trayIconBase64, false)
hasShowTrafficRef.current = false
}
}, [showTraffic])
if (iconOnly) {
return (
<div className={`${connectionCardStatus} flex justify-center`}>
@ -294,37 +295,37 @@ const ConnCard: React.FC<Props> = (props) => {
export default ConnCard
const drawSvg = async (
upload: number,
download: number,
currentUploadRef: React.RefObject<number | undefined>,
currentDownloadRef: React.RefObject<number | undefined>
): Promise<void> => {
if (upload === currentUploadRef.current && download === currentDownloadRef.current) return
currentUploadRef.current = upload
currentDownloadRef.current = download
const svg = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156 36"><image height="36" width="36" href="${trayIconBase64}"/><text x="156" y="15" font-size="18" font-family="PingFang SC" font-weight="bold" text-anchor="end" fill="black">${calcTraffic(upload)}/s</text><text x="156" y="34" font-size="18" font-family="PingFang SC" font-weight="bold" text-anchor="end" fill="black">${calcTraffic(download)}/s</text></svg>`
const image = await loadImage(svg)
window.electron.ipcRenderer.send('trayIconUpdate', image, true)
}
const loadImage = (url: string): Promise<string> => {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = (): void => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = 156
canvas.height = 36
ctx?.drawImage(img, 0, 0)
const png = canvas.toDataURL('image/png')
resolve(png)
}
img.onerror = (): void => {
reject()
}
img.src = url
})
}
const trayIconBase64 = `data:image/png;base64,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`
const trayIconBase64 = `data:image/png;base64,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`
// 固定宽高,同步渲染避免闪烁
const ICON_W = 156
const ICON_H = 36
let trafficCanvas: HTMLCanvasElement | null = null
let trafficCtx: CanvasRenderingContext2D | null = null
const trafficIcon = new Image()
let trafficIconLoaded = false
trafficIcon.onload = () => {
trafficIconLoaded = true
}
trafficIcon.src = trayIconBase64
function renderTrafficIcon(upload: number, download: number): string {
if (!trafficCanvas) {
trafficCanvas = document.createElement('canvas')
trafficCanvas.width = ICON_W
trafficCanvas.height = ICON_H
trafficCtx = trafficCanvas.getContext('2d')
}
const ctx = trafficCtx!
ctx.clearRect(0, 0, ICON_W, ICON_H)
if (trafficIconLoaded) {
ctx.drawImage(trafficIcon, 0, 0, ICON_H, ICON_H)
}
ctx.font = 'bold 18px "PingFang SC"'
ctx.fillStyle = 'black'
ctx.textAlign = 'right'
ctx.fillText(`${calcTraffic(upload)}/s`, ICON_W, 15)
ctx.fillText(`${calcTraffic(download)}/s`, ICON_W, 34)
return trafficCanvas.toDataURL('image/png')
}