feat: add sidebar brand customization

The primary sidebar brand was fixed to the built-in name, subtitle, and logo. Appearance settings now let users preview and save a custom brand name, localized Chinese and English subtitles, and a validated local PNG, JPEG, or WebP logo, with default-logo and full-reset actions.

Customization remains device-local and limited to the top-left brand area; installer, update, Agent, and protocol identities remain GoodBuddy. The desktop package version is now 0.11.6.

Release note: 新增“外观 → 品牌定制”,可修改左上角品牌名称、中英文副标题和本地 Logo,并支持实时预览、恢复默认及本机持久化。
This commit is contained in:
mesalogo
2026-08-27 12:42:26 +08:00
parent 618e505b8b
commit 12463ad593
22 changed files with 1125 additions and 117 deletions
+46
View File
@@ -0,0 +1,46 @@
import { describe, expect, it } from 'vitest'
import { detectSupportedImage } from './image-media-type'
describe('supported image detection', () => {
it.each([
{
bytes: [0xff, 0xd8, 0xff],
extension: 'jpg',
mimeType: 'image/jpeg'
},
{
bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
extension: 'png',
mimeType: 'image/png'
},
{
bytes: [
0x52,
0x49,
0x46,
0x46,
0,
0,
0,
0,
0x57,
0x45,
0x42,
0x50
],
extension: 'webp',
mimeType: 'image/webp'
}
])('detects $mimeType bytes', ({ bytes, extension, mimeType }) => {
expect(detectSupportedImage(Uint8Array.from(bytes))).toEqual({
extension,
mimeType
})
})
it('rejects unsupported bytes', () => {
expect(() =>
detectSupportedImage(Uint8Array.from([1, 2, 3]))
).toThrow('图片格式不受支持')
})
})
+44
View File
@@ -0,0 +1,44 @@
export type SupportedImageFormat = {
extension: 'jpg' | 'png' | 'webp'
mimeType: 'image/jpeg' | 'image/png' | 'image/webp'
}
function matchesAscii(
data: Uint8Array,
offset: number,
value: string
): boolean {
return [...value].every(
(character, index) =>
data[offset + index] === character.charCodeAt(0)
)
}
export function detectSupportedImage(
data: Uint8Array
): SupportedImageFormat {
if (
data.byteLength >= 3 &&
data[0] === 0xff &&
data[1] === 0xd8 &&
data[2] === 0xff
) {
return { extension: 'jpg', mimeType: 'image/jpeg' }
}
if (
data.byteLength >= 8 &&
[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every(
(value, index) => data[index] === value
)
) {
return { extension: 'png', mimeType: 'image/png' }
}
if (
data.byteLength >= 12 &&
matchesAscii(data, 0, 'RIFF') &&
matchesAscii(data, 8, 'WEBP')
) {
return { extension: 'webp', mimeType: 'image/webp' }
}
throw new Error('图片格式不受支持')
}