From e5a1656644a832f874318801d85f98cc3b2f1a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B5=E6=91=87=E5=B0=8F=E5=AD=90?= <84499436+Wxw-Gu@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:35:26 +0700 Subject: [PATCH] =?UTF-8?q?style:=20=E4=BF=AE=E5=A4=8D=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=20=E6=A0=B7=E5=BC=8F=20#46?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes https://github.com/Wxw-Gu/TraceMemo/issues/46 --- resources/trayTemplate.png | Bin 0 -> 417 bytes resources/trayTemplate.svg | 13 ++++ resources/trayTemplate@2x.png | Bin 0 -> 702 bytes src/main/index.ts | 28 +++++-- src/main/tray-icon.ts | 51 +++++++++++++ .../src/components/account/AccountSummary.tsx | 10 +-- .../ConnectionHealthSection.tsx | 31 +++++++- src/renderer/src/styles/settings.scss | 5 +- .../account-database-controls.test.tsx | 53 ++++++++++++++ tests/unit/tray-icon.test.ts | 69 ++++++++++++++++++ 10 files changed, 241 insertions(+), 19 deletions(-) create mode 100644 resources/trayTemplate.png create mode 100644 resources/trayTemplate.svg create mode 100644 resources/trayTemplate@2x.png create mode 100644 src/main/tray-icon.ts create mode 100644 tests/unit/tray-icon.test.ts diff --git a/resources/trayTemplate.png b/resources/trayTemplate.png new file mode 100644 index 0000000000000000000000000000000000000000..d0fc06b63e9f0201aa017559205582d71aaf5a65 GIT binary patch literal 417 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyy~STEL88gA~qMxhWK=jN8-2F~mY} z?UcQ|hYUnogcqo?hOE5V(C3@LY9+zEL?LhkQ;rj>4uh#I>lTlK_l77btF0^w`3h + + + diff --git a/resources/trayTemplate@2x.png b/resources/trayTemplate@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..71402833dea8575424552f67659fd0a85954ba55 GIT binary patch literal 702 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbB7>k44ofy`glX(f`xTHpSruq6Z zXaU(A42G$TlC0TWzSWdSpS4N|DZ_xKM31CxfQi(^Ox z=h+$7euo_-S`IH}bezI`fbo)pqdT(@uanc1fJ;S9GOf`HiY$v5G)=AqJY{&o(2>sE z^pmIlN3MJ4-fhk9Qs2J5KbI1payjD)Pq6YIomIK+cSNSUoO1oJ=3MPY?YNF9#))Bp zvL6Hgtm^3LWs6e281#B-#MFsTc4REHY=}3y6W0~pd~N=j*^}n|n6%=2bWA||HjDW$ zD%O75_G99T{$&^Ho77olI*U&pU`joflukU4@6ZKfgf7QxBV3)^ze_XwR4 zn*2&WOT#oEQgmUPh414Ds%oZgD-`>_)n;wk&vmQgWN(1;9pl}P41%v*6e|%I-6 { // 设置应用程序用户模型 ID electronApp.setAppUserModelId('com.tracememo.app') - if (process.platform === 'darwin') app.dock?.setIcon(appIconPath) - // 开发环境中默认使用 F12 打开或关闭 DevTools // 生产环境中忽略 CommandOrControl + R // 参见 https://github.com/alex8088/electron-toolkit/tree/master/packages/utils @@ -2875,11 +2888,12 @@ function buildTrayMenu(): Menu { function setupTray(): void { if (tray) return try { - const image = nativeImage.createFromPath(appIconPath) - const traySize = process.platform === 'darwin' ? 20 : 24 - const trayImage = image.isEmpty() - ? nativeImage.createEmpty() - : image.resize({ width: traySize, height: traySize, quality: 'best' }) + const trayImage = createTrayImage( + process.platform, + appIconPath, + { oneX: trayTemplateIconPath, twoX: trayTemplateRetinaIconPath }, + nativeImage + ) tray = new Tray(trayImage) tray.setToolTip('TraceMemo') // macOS may show a Tray context menu on a primary click when it is set diff --git a/src/main/tray-icon.ts b/src/main/tray-icon.ts new file mode 100644 index 0000000..65eaa37 --- /dev/null +++ b/src/main/tray-icon.ts @@ -0,0 +1,51 @@ +import { readFileSync } from 'fs' + +export interface TrayNativeImage { + addRepresentation(options: { scaleFactor: number; dataURL: string }): void + isEmpty(): boolean + resize(options: { width: number; height: number; quality: 'best' }): TrayNativeImage + setTemplateImage(option: boolean): void +} + +export interface TrayNativeImageFactory { + createEmpty(): T + createFromPath(path: string): T +} + +export interface TrayTemplateIconPaths { + oneX: string + twoX: string +} + +type ReadImageFile = (path: string) => Buffer + +function toPngDataUrl(buffer: Buffer): string { + return `data:image/png;base64,${buffer.toString('base64')}` +} + +export function createTrayImage( + platform: NodeJS.Platform, + appIconPath: string, + templateIconPaths: TrayTemplateIconPaths, + nativeImage: TrayNativeImageFactory, + readImageFile: ReadImageFile = readFileSync +): T { + if (platform === 'darwin') { + const image = nativeImage.createEmpty() + image.addRepresentation({ + scaleFactor: 1, + dataURL: toPngDataUrl(readImageFile(templateIconPaths.oneX)) + }) + image.addRepresentation({ + scaleFactor: 2, + dataURL: toPngDataUrl(readImageFile(templateIconPaths.twoX)) + }) + image.setTemplateImage(true) + return image + } + + const image = nativeImage.createFromPath(appIconPath) + return image.isEmpty() + ? nativeImage.createEmpty() + : (image.resize({ width: 24, height: 24, quality: 'best' }) as T) +} diff --git a/src/renderer/src/components/account/AccountSummary.tsx b/src/renderer/src/components/account/AccountSummary.tsx index 06d905d..a0b0687 100644 --- a/src/renderer/src/components/account/AccountSummary.tsx +++ b/src/renderer/src/components/account/AccountSummary.tsx @@ -26,11 +26,7 @@ export function AccountSummary({ const displayName = showAccount && selfInfo ? selfInfo.nickname || selfInfo.wxid || '当前账号' : '未连接' const subtitle = showAccount && selfInfo ? selfInfo.wxid : '打开设置' - const statusText = dbReady - ? '数据库已连接' - : dbConnecting - ? '正在连接数据库' - : '数据库未连接' + const statusText = dbReady ? '数据库已连接' : dbConnecting ? '正在连接数据库' : '数据库未连接' const statusClass = dbReady ? 'ready' : dbConnecting ? 'connecting' : '' const initial = (displayName || '?').charAt(0) const title = `${displayName}\n${subtitle}` @@ -66,8 +62,8 @@ export function AccountSummary({ diff --git a/src/renderer/src/features/settings/account-database/ConnectionHealthSection.tsx b/src/renderer/src/features/settings/account-database/ConnectionHealthSection.tsx index a460193..30a43c2 100644 --- a/src/renderer/src/features/settings/account-database/ConnectionHealthSection.tsx +++ b/src/renderer/src/features/settings/account-database/ConnectionHealthSection.tsx @@ -4,11 +4,36 @@ function DiagnosticIcon({ status }: { status: DiagnosticStatus }): React.ReactEl if (status === 'checking') { return } - const symbol = - status === 'success' ? '✓' : status === 'warning' ? '!' : status === 'error' ? '×' : '—' + + const icon = + status === 'success' ? ( + + ) : status === 'warning' ? ( + <> + + + + ) : status === 'error' ? ( + + ) : ( + + ) + return ( ) } diff --git a/src/renderer/src/styles/settings.scss b/src/renderer/src/styles/settings.scss index 610dd8a..a571ef6 100644 --- a/src/renderer/src/styles/settings.scss +++ b/src/renderer/src/styles/settings.scss @@ -416,8 +416,9 @@ border: 1px solid #929a96; border-radius: 50%; color: #929a96; - font-size: 11px; - font-weight: 700; +} +.settings-diagnostic-icon svg { + display: block; } .settings-diagnostic-icon.success { border-color: #2e8b68; diff --git a/tests/component/account-database-controls.test.tsx b/tests/component/account-database-controls.test.tsx index 160945a..1725859 100644 --- a/tests/component/account-database-controls.test.tsx +++ b/tests/component/account-database-controls.test.tsx @@ -1,7 +1,10 @@ import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { describe, expect, it, vi } from 'vitest' +import { AccountSummary } from '../../src/renderer/src/components/account/AccountSummary' import { AccountOverview } from '../../src/renderer/src/features/settings/account-database/AccountOverview' +import { ConnectionHealthSection } from '../../src/renderer/src/features/settings/account-database/ConnectionHealthSection' +import type { ConnectionDiagnostic } from '../../src/renderer/src/features/settings/account-database/types' import { TooltipProvider } from '../../src/renderer/src/components/ui' describe('account database controls', () => { @@ -61,4 +64,54 @@ describe('account database controls', () => { expect(screen.getByRole('button', { name: '打开账号目录' })).toBeDisabled() expect(screen.getByRole('button', { name: '复制账号目录' })).toBeDisabled() }) + + it('renders connection states with SVG glyphs and keeps checking accessible', () => { + const diagnostics: ConnectionDiagnostic[] = [ + { id: 'db-key', label: '数据库密钥', status: 'success', result: '已用于当前连接' }, + { id: 'contacts', label: '联系人索引', status: 'warning', result: '索引待更新' }, + { id: 'messages', label: '消息数据库', status: 'error', result: '读取失败' }, + { id: 'identity', label: '账号身份', status: 'idle', result: '尚未检测' }, + { id: 'image-key', label: '图片解密密钥', status: 'unavailable', result: '未配置' } + ] + const { container, rerender } = render() + + const icons = container.querySelectorAll('.settings-diagnostic-icon:not(.checking)') + expect(icons).toHaveLength(diagnostics.length) + icons.forEach((icon) => { + expect(icon).toHaveAttribute('aria-hidden', 'true') + expect(icon.querySelector('svg')).toHaveAttribute('viewBox', '0 0 24 24') + }) + expect(container.textContent).not.toMatch(/[✓!×—]/) + + rerender( + + ) + expect(screen.getByLabelText('检测中')).toHaveClass('checking') + expect(container.querySelector('.checking svg')).toBeNull() + }) + + it('keeps the account settings button clickable with a centered-viewBox SVG', async () => { + const user = userEvent.setup() + const onClick = vi.fn() + render( + + ) + + const button = screen.getByRole('button', { name: '设置' }) + expect(button.querySelector('svg')).toHaveAttribute('viewBox', '0 0 24 24') + expect(button.querySelector('svg circle')).toHaveAttribute('cx', '12') + expect(button.querySelector('svg circle')).toHaveAttribute('cy', '12') + await user.click(button) + expect(onClick).toHaveBeenCalledOnce() + }) }) diff --git a/tests/unit/tray-icon.test.ts b/tests/unit/tray-icon.test.ts new file mode 100644 index 0000000..ebcbfc5 --- /dev/null +++ b/tests/unit/tray-icon.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it, vi } from 'vitest' +import { createTrayImage, type TrayNativeImage } from '../../src/main/tray-icon' + +function makeImage(empty = false): TrayNativeImage { + const image = { + addRepresentation: vi.fn(), + isEmpty: vi.fn(() => empty), + resize: vi.fn(), + setTemplateImage: vi.fn() + } + image.resize.mockReturnValue(image) + return image as unknown as TrayNativeImage +} + +describe('tray icon creation', () => { + it('uses a monochrome 1x/2x template image on macOS', () => { + const image = makeImage() + const readImageFile = vi.fn((path: string) => Buffer.from(path)) + const nativeImage = { + createEmpty: vi.fn(() => image), + createFromPath: vi.fn(() => makeImage()) + } + + const result = createTrayImage( + 'darwin', + '/app/icon.png', + { oneX: '/resources/trayTemplate.png', twoX: '/resources/trayTemplate@2x.png' }, + nativeImage, + readImageFile + ) + + expect(result).toBe(image) + expect(readImageFile.mock.calls.map(([path]) => path)).toEqual([ + '/resources/trayTemplate.png', + '/resources/trayTemplate@2x.png' + ]) + expect(image.addRepresentation).toHaveBeenNthCalledWith(1, { + scaleFactor: 1, + dataURL: `data:image/png;base64,${Buffer.from('/resources/trayTemplate.png').toString('base64')}` + }) + expect(image.addRepresentation).toHaveBeenNthCalledWith(2, { + scaleFactor: 2, + dataURL: `data:image/png;base64,${Buffer.from('/resources/trayTemplate@2x.png').toString('base64')}` + }) + expect(image.setTemplateImage).toHaveBeenCalledWith(true) + expect(nativeImage.createFromPath).not.toHaveBeenCalled() + }) + + it('preserves the app icon resize policy on Windows', () => { + const appImage = makeImage() + const nativeImage = { + createEmpty: vi.fn(() => makeImage()), + createFromPath: vi.fn(() => appImage) + } + + const result = createTrayImage( + 'win32', + '/app/icon.png', + { oneX: '/resources/trayTemplate.png', twoX: '/resources/trayTemplate@2x.png' }, + nativeImage, + vi.fn(() => Buffer.alloc(0)) + ) + + expect(nativeImage.createFromPath).toHaveBeenCalledWith('/app/icon.png') + expect(appImage.resize).toHaveBeenCalledWith({ width: 24, height: 24, quality: 'best' }) + expect(appImage.setTemplateImage).not.toHaveBeenCalled() + expect(result).toBe(appImage) + }) +})