mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-10 01:42:49 +08:00
Binary file not shown.
|
After Width: | Height: | Size: 417 B |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
|
||||||
|
<rect
|
||||||
|
x="1.5"
|
||||||
|
y="1.5"
|
||||||
|
width="15"
|
||||||
|
height="15"
|
||||||
|
rx="3.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="#000"
|
||||||
|
stroke-width="1.75"
|
||||||
|
/>
|
||||||
|
<circle cx="9" cy="9" r="2.3" fill="#000" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 277 B |
Binary file not shown.
|
After Width: | Height: | Size: 702 B |
+21
-7
@@ -21,7 +21,10 @@ import { dirname, extname, join } from 'path'
|
|||||||
import { existsSync, promises as fsPromises } from 'fs'
|
import { existsSync, promises as fsPromises } from 'fs'
|
||||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||||
import icon from '../../resources/icon.png?asset'
|
import icon from '../../resources/icon.png?asset'
|
||||||
|
import trayTemplateIcon from '../../resources/trayTemplate.png?asset'
|
||||||
|
import trayTemplateRetinaIcon from '../../resources/trayTemplate@2x.png?asset'
|
||||||
import { WechatDb } from './wechat-db'
|
import { WechatDb } from './wechat-db'
|
||||||
|
import { createTrayImage } from './tray-icon'
|
||||||
import { bootstrapWcdbNativeAsync, Wcdb4Client } from './wcdb4-client'
|
import { bootstrapWcdbNativeAsync, Wcdb4Client } from './wcdb4-client'
|
||||||
import { VoiceService } from './voice-service'
|
import { VoiceService } from './voice-service'
|
||||||
import { StickerService } from './sticker-service'
|
import { StickerService } from './sticker-service'
|
||||||
@@ -370,6 +373,18 @@ function configureRecallProtection(
|
|||||||
|
|
||||||
const packagedIconPath = join(process.resourcesPath, 'resources', 'icon.png')
|
const packagedIconPath = join(process.resourcesPath, 'resources', 'icon.png')
|
||||||
const appIconPath = existsSync(packagedIconPath) ? packagedIconPath : icon
|
const appIconPath = existsSync(packagedIconPath) ? packagedIconPath : icon
|
||||||
|
const packagedTrayTemplateIconPath = join(process.resourcesPath, 'resources', 'trayTemplate.png')
|
||||||
|
const packagedTrayTemplateRetinaIconPath = join(
|
||||||
|
process.resourcesPath,
|
||||||
|
'resources',
|
||||||
|
'trayTemplate@2x.png'
|
||||||
|
)
|
||||||
|
const trayTemplateIconPath = existsSync(packagedTrayTemplateIconPath)
|
||||||
|
? packagedTrayTemplateIconPath
|
||||||
|
: trayTemplateIcon
|
||||||
|
const trayTemplateRetinaIconPath = existsSync(packagedTrayTemplateRetinaIconPath)
|
||||||
|
? packagedTrayTemplateRetinaIconPath
|
||||||
|
: trayTemplateRetinaIcon
|
||||||
|
|
||||||
protocol.registerSchemesAsPrivileged([
|
protocol.registerSchemesAsPrivileged([
|
||||||
{
|
{
|
||||||
@@ -921,8 +936,6 @@ app.whenReady().then(async () => {
|
|||||||
// 设置应用程序用户模型 ID
|
// 设置应用程序用户模型 ID
|
||||||
electronApp.setAppUserModelId('com.tracememo.app')
|
electronApp.setAppUserModelId('com.tracememo.app')
|
||||||
|
|
||||||
if (process.platform === 'darwin') app.dock?.setIcon(appIconPath)
|
|
||||||
|
|
||||||
// 开发环境中默认使用 F12 打开或关闭 DevTools
|
// 开发环境中默认使用 F12 打开或关闭 DevTools
|
||||||
// 生产环境中忽略 CommandOrControl + R
|
// 生产环境中忽略 CommandOrControl + R
|
||||||
// 参见 https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
// 参见 https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||||
@@ -2875,11 +2888,12 @@ function buildTrayMenu(): Menu {
|
|||||||
function setupTray(): void {
|
function setupTray(): void {
|
||||||
if (tray) return
|
if (tray) return
|
||||||
try {
|
try {
|
||||||
const image = nativeImage.createFromPath(appIconPath)
|
const trayImage = createTrayImage(
|
||||||
const traySize = process.platform === 'darwin' ? 20 : 24
|
process.platform,
|
||||||
const trayImage = image.isEmpty()
|
appIconPath,
|
||||||
? nativeImage.createEmpty()
|
{ oneX: trayTemplateIconPath, twoX: trayTemplateRetinaIconPath },
|
||||||
: image.resize({ width: traySize, height: traySize, quality: 'best' })
|
nativeImage
|
||||||
|
)
|
||||||
tray = new Tray(trayImage)
|
tray = new Tray(trayImage)
|
||||||
tray.setToolTip('TraceMemo')
|
tray.setToolTip('TraceMemo')
|
||||||
// macOS may show a Tray context menu on a primary click when it is set
|
// macOS may show a Tray context menu on a primary click when it is set
|
||||||
|
|||||||
@@ -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<T extends TrayNativeImage = TrayNativeImage> {
|
||||||
|
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<T extends TrayNativeImage>(
|
||||||
|
platform: NodeJS.Platform,
|
||||||
|
appIconPath: string,
|
||||||
|
templateIconPaths: TrayTemplateIconPaths,
|
||||||
|
nativeImage: TrayNativeImageFactory<T>,
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -26,11 +26,7 @@ export function AccountSummary({
|
|||||||
const displayName =
|
const displayName =
|
||||||
showAccount && selfInfo ? selfInfo.nickname || selfInfo.wxid || '当前账号' : '未连接'
|
showAccount && selfInfo ? selfInfo.nickname || selfInfo.wxid || '当前账号' : '未连接'
|
||||||
const subtitle = showAccount && selfInfo ? selfInfo.wxid : '打开设置'
|
const subtitle = showAccount && selfInfo ? selfInfo.wxid : '打开设置'
|
||||||
const statusText = dbReady
|
const statusText = dbReady ? '数据库已连接' : dbConnecting ? '正在连接数据库' : '数据库未连接'
|
||||||
? '数据库已连接'
|
|
||||||
: dbConnecting
|
|
||||||
? '正在连接数据库'
|
|
||||||
: '数据库未连接'
|
|
||||||
const statusClass = dbReady ? 'ready' : dbConnecting ? 'connecting' : ''
|
const statusClass = dbReady ? 'ready' : dbConnecting ? 'connecting' : ''
|
||||||
const initial = (displayName || '?').charAt(0)
|
const initial = (displayName || '?').charAt(0)
|
||||||
const title = `${displayName}\n${subtitle}`
|
const title = `${displayName}\n${subtitle}`
|
||||||
@@ -66,8 +62,8 @@ export function AccountSummary({
|
|||||||
</span>
|
</span>
|
||||||
<button type="button" className="account-summary-settings" onClick={onClick} title="设置">
|
<button type="button" className="account-summary-settings" onClick={onClick} title="设置">
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
<path d="M12 8.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z" />
|
<path d="M12.22 2h-.44a2 2 0 0 0-2 1.72l-.15 1.58a8 8 0 0 0-1.77 1.03l-1.48-.6a2 2 0 0 0-2.5.88l-.22.39a2 2 0 0 0 .51 2.68l1.25.98a8 8 0 0 0 0 2.08l-1.25.98a2 2 0 0 0-.51 2.68l.22.39a2 2 0 0 0 2.5.88l1.48-.6c.54.43 1.14.78 1.77 1.03l.15 1.58a2 2 0 0 0 2 1.72h.44a2 2 0 0 0 2-1.72l.15-1.58c.63-.25 1.23-.6 1.77-1.03l1.48.6a2 2 0 0 0 2.5-.88l.22-.39a2 2 0 0 0-.51-2.68l-1.25-.98a8 8 0 0 0 0-2.08l1.25-.98a2 2 0 0 0 .51-2.68l-.22-.39a2 2 0 0 0-2.5-.88l-1.48.6a8 8 0 0 0-1.77-1.03l-.15-1.58a2 2 0 0 0-2-1.72Z" />
|
||||||
<path d="M19.4 15a8.2 8.2 0 0 0 .1-1l2-1.5-2-3.5-2.4 1a7.5 7.5 0 0 0-1.7-1l-.3-2.5h-4l-.4 2.5a7.5 7.5 0 0 0-1.7 1l-2.3-1-2 3.5 2 1.5a8.2 8.2 0 0 0 0 2l-2 1.5 2 3.5 2.3-1a7.5 7.5 0 0 0 1.7 1l.4 2.5h4l.3-2.5a7.5 7.5 0 0 0 1.7-1l2.4 1 2-3.5-2.1-1.5Z" />
|
<circle cx="12" cy="12" r="3" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,11 +4,36 @@ function DiagnosticIcon({ status }: { status: DiagnosticStatus }): React.ReactEl
|
|||||||
if (status === 'checking') {
|
if (status === 'checking') {
|
||||||
return <span className="settings-diagnostic-icon checking" aria-label="检测中" />
|
return <span className="settings-diagnostic-icon checking" aria-label="检测中" />
|
||||||
}
|
}
|
||||||
const symbol =
|
|
||||||
status === 'success' ? '✓' : status === 'warning' ? '!' : status === 'error' ? '×' : '—'
|
const icon =
|
||||||
|
status === 'success' ? (
|
||||||
|
<path d="m5 12.5 4.5 4.5L19 7" />
|
||||||
|
) : status === 'warning' ? (
|
||||||
|
<>
|
||||||
|
<path d="M12 6v7" />
|
||||||
|
<circle cx="12" cy="17" r="1.5" fill="currentColor" stroke="none" />
|
||||||
|
</>
|
||||||
|
) : status === 'error' ? (
|
||||||
|
<path d="m7 7 10 10M17 7 7 17" />
|
||||||
|
) : (
|
||||||
|
<path d="M6 12h12" />
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={`settings-diagnostic-icon ${status}`} aria-hidden="true">
|
<span className={`settings-diagnostic-icon ${status}`} aria-hidden="true">
|
||||||
{symbol}
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -416,8 +416,9 @@
|
|||||||
border: 1px solid #929a96;
|
border: 1px solid #929a96;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
color: #929a96;
|
color: #929a96;
|
||||||
font-size: 11px;
|
}
|
||||||
font-weight: 700;
|
.settings-diagnostic-icon svg {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
.settings-diagnostic-icon.success {
|
.settings-diagnostic-icon.success {
|
||||||
border-color: #2e8b68;
|
border-color: #2e8b68;
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
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 { 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'
|
import { TooltipProvider } from '../../src/renderer/src/components/ui'
|
||||||
|
|
||||||
describe('account database controls', () => {
|
describe('account database controls', () => {
|
||||||
@@ -61,4 +64,54 @@ describe('account database controls', () => {
|
|||||||
expect(screen.getByRole('button', { name: '打开账号目录' })).toBeDisabled()
|
expect(screen.getByRole('button', { name: '打开账号目录' })).toBeDisabled()
|
||||||
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(<ConnectionHealthSection diagnostics={diagnostics} />)
|
||||||
|
|
||||||
|
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(
|
||||||
|
<ConnectionHealthSection
|
||||||
|
diagnostics={[{ id: 'db-key', label: '数据库密钥', status: 'checking', result: '检测中' }]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
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(
|
||||||
|
<AccountSummary
|
||||||
|
selfInfo={{
|
||||||
|
wxid: 'wxid_fixture',
|
||||||
|
nickname: '测试账号',
|
||||||
|
accountRoot: '/fixture/account'
|
||||||
|
}}
|
||||||
|
dbReady
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user