feat(branding): 确定项目图标,并接入 Web / Electron / HarmonyOS
# 唯一源
`client/electron/src/icons/agentmail.svg` 是图标唯一源(24×24 视图框,`currentColor`
跟随文字色)。此前各端用的都是占位物:Electron 的窗口/托盘指向一个**不存在**的
`src/icons/tray-icon.png`(`nativeImage` 拿到空图,托盘不可见),
HarmonyOS 的 `startIcon/foreground/background` 是 1×1 PNG,
Web 端根本没有 favicon。
# 为什么带生成脚本
PNG/ICO 是二进制的,换一次配色要重出十几个尺寸,手工做必然出现
「Web 是旧的、Harmony 是新的」这种不一致,而且没人能复核。
`generate.py` 只认上面那一份源,所有变体都由它推导(本机无 rsvg/ImageMagick,
用 cairosvg + Pillow)。改图标只需改源文件再跑一次脚本。
# 各端产物
- **Web**:`public/assets/{agentmail.svg,favicon.ico,apple-touch-icon.png}` + `index.html` 引用。
放 `assets/` 下而非根目录,是因为 Gateway 只把 `/assets/*` 与 `/` 交给静态处理器
(`server/cmd/server/main.go`),放根下会 404。已实测本机与 LAN 均 200。
- **Electron**:应用图标 `icon.png`(512) / `icon.ico`(16–256) / 各尺寸 PNG /
托盘 `tray-icon.png`(32),`package.json` 里 `win.icon` 与 `linux.icon` 指过去。
托盘用品牌色字形而非白色 —— 浅色面板下白色会消失。
- **HarmonyOS**:`startIcon.png`(512) 用完整应用图标(启动页底色浅 `#FFF` /
深 `#000`,白底蓝图标两套都立得住);分层图标的 `background` 是品牌色整块、
`foreground` 是白色字形并留 12% 安全区,避免被系统圆角裁掉。
- **应用内**:新增 `BrandMarkIcon`(fill 型,与现有描边图标集不同族),
替换登录页与初始化页品牌位的占位 `MailboxIcon`;后者已无引用,一并删除。
图标色 `#2563eb` 与门户 Dashy 主题主色一致。
# 验证
- 前端 typecheck 与 196 项测试全绿;`npm run build` 产物含三个图标文件
- Gateway 重新部署后 `/assets/{agentmail.svg,favicon.ico,apple-touch-icon.png}`
在本机与 `192.168.2.60:8180` 都返回 200,Content-Type 正确
- 所有 PNG/ICO 用 Pillow 复核尺寸与 alpha 边界(合成失败会表现为全透明,
已用 getbbox 排除)
8
.gitignore
vendored
@ -53,6 +53,14 @@ attachments/
|
|||||||
.pi-glla/
|
.pi-glla/
|
||||||
.codegraph
|
.codegraph
|
||||||
|
|
||||||
|
# ---- Python 工具缓存 ----
|
||||||
|
#
|
||||||
|
# client/electron/src/icons/generate.py 是图标生成脚本(cairosvg + Pillow)。
|
||||||
|
# 静态检查器跑 ruff 时会在同目录落 .ruff_cache/,不是项目内容。
|
||||||
|
.ruff_cache/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
# ---- 编辑器与系统 ----
|
# ---- 编辑器与系统 ----
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
|
|||||||
@ -1,8 +1,21 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" />
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content"
|
||||||
|
/>
|
||||||
|
<!--
|
||||||
|
图标放在 public/assets/ 而不是 public/ 根下:Gateway 只把 /assets/* 和 /
|
||||||
|
挂给了静态处理器(见 server/cmd/server/main.go 的静态段),放在根下会 404。
|
||||||
|
Vite 原样拷贝 public/ 到 dist/,所以开发态与内嵌后 URL 都是 /assets/…。
|
||||||
|
品牌源文件与其生成脚本:client/electron/src/icons/。
|
||||||
|
-->
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/agentmail.svg" />
|
||||||
|
<link rel="alternate icon" type="image/x-icon" href="/assets/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" href="/assets/apple-touch-icon.png" />
|
||||||
|
<meta name="theme-color" content="#2563eb" />
|
||||||
<title>AgentMail</title>
|
<title>AgentMail</title>
|
||||||
<script>
|
<script>
|
||||||
/*
|
/*
|
||||||
@ -18,15 +31,15 @@
|
|||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
try {
|
try {
|
||||||
var p = localStorage.getItem('agentmail.theme') || 'system';
|
var p = localStorage.getItem("agentmail.theme") || "system";
|
||||||
var dark =
|
var dark =
|
||||||
p === 'dark' ||
|
p === "dark" ||
|
||||||
(p === 'system' &&
|
(p === "system" &&
|
||||||
window.matchMedia &&
|
window.matchMedia &&
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||||
if (dark) {
|
if (dark) {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add("dark");
|
||||||
document.documentElement.style.colorScheme = 'dark';
|
document.documentElement.style.colorScheme = "dark";
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* localStorage 不可用(隐私模式)时退回浅色,不阻塞渲染 */
|
/* localStorage 不可用(隐私模式)时退回浅色,不阻塞渲染 */
|
||||||
|
|||||||
@ -56,11 +56,13 @@
|
|||||||
"output": "release"
|
"output": "release"
|
||||||
},
|
},
|
||||||
"win": {
|
"win": {
|
||||||
|
"icon": "src/icons/icon.ico",
|
||||||
"target": [
|
"target": [
|
||||||
{ "target": "nsis", "arch": ["x64"] }
|
{ "target": "nsis", "arch": ["x64"] }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
|
"icon": "src/icons",
|
||||||
"target": [
|
"target": [
|
||||||
{ "target": "AppImage", "arch": ["x64"] },
|
{ "target": "AppImage", "arch": ["x64"] },
|
||||||
{ "target": "deb", "arch": ["x64"] }
|
{ "target": "deb", "arch": ["x64"] }
|
||||||
|
|||||||
1
client/electron/public/assets/agentmail.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="#2563eb" transform="translate(0,0) scale(1)"><path fill="#2563eb" d="M12 1.5a1.25 1.25 0 0 1 1.25 1.25c0 .5-.29.93-.71 1.13V5h1.71A2.75 2.75 0 0 1 17 7.75V8h2.5A2.5 2.5 0 0 1 22 10.5v9a2.5 2.5 0 0 1-2.5 2.5h-15A2.5 2.5 0 0 1 2 19.5v-9A2.5 2.5 0 0 1 4.5 8H7v-.25A2.75 2.75 0 0 1 9.75 5h1.71V3.88c-.42-.2-.71-.63-.71-1.13A1.25 1.25 0 0 1 12 1.5m-2.25 5A1.25 1.25 0 0 0 8.5 7.75V8h7v-.25a1.25 1.25 0 0 0-1.25-1.25zM4.5 9.5a1 1 0 0 0-1 1v.34l8.5 4.72l8.5-4.72V10.5a1 1 0 0 0-1-1zm-1 3.05v6.95a1 1 0 0 0 1 1h15a1 1 0 0 0 1-1v-6.95l-8.5 4.72z"/><circle cx="9" cy="11.6" r=".9" fill="#2563eb"/><circle cx="15" cy="11.6" r=".9" fill="#2563eb"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 739 B |
BIN
client/electron/public/assets/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
client/electron/public/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
import { MailboxIcon, SpinnerIcon } from './icons';
|
import { BrandMarkIcon, SpinnerIcon } from './icons';
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const login = useAuthStore(s => s.login);
|
const login = useAuthStore(s => s.login);
|
||||||
@ -56,7 +56,7 @@ export default function LoginPage() {
|
|||||||
<div className="w-[380px] max-w-[92vw] shrink-0 my-auto bg-white rounded-xl shadow-sm border border-gray-200 p-6 sm:p-8">
|
<div className="w-[380px] max-w-[92vw] shrink-0 my-auto bg-white rounded-xl shadow-sm border border-gray-200 p-6 sm:p-8">
|
||||||
<div className="flex flex-col items-center mb-6">
|
<div className="flex flex-col items-center mb-6">
|
||||||
<div className="w-12 h-12 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center">
|
<div className="w-12 h-12 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center">
|
||||||
<MailboxIcon className="w-6 h-6" />
|
<BrandMarkIcon className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="mt-3 text-base font-semibold text-gray-900">AgentMail</h1>
|
<h1 className="mt-3 text-base font-semibold text-gray-900">AgentMail</h1>
|
||||||
<p className="mt-1 text-xs text-gray-500">邮件驱动的多智能体协作平台</p>
|
<p className="mt-1 text-xs text-gray-500">邮件驱动的多智能体协作平台</p>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
import * as api from '../api/client';
|
import * as api from '../api/client';
|
||||||
import { MailboxIcon, SpinnerIcon } from './icons';
|
import { BrandMarkIcon, SpinnerIcon } from './icons';
|
||||||
|
|
||||||
/** 首次初始化向导:系统无任何用户时展示,创建首个管理员 */
|
/** 首次初始化向导:系统无任何用户时展示,创建首个管理员 */
|
||||||
export default function SetupPage({ onDone }: { onDone: () => void }) {
|
export default function SetupPage({ onDone }: { onDone: () => void }) {
|
||||||
@ -56,7 +56,7 @@ export default function SetupPage({ onDone }: { onDone: () => void }) {
|
|||||||
<div className="w-[420px] max-w-[92vw] shrink-0 my-auto bg-white rounded-xl shadow-sm border border-gray-200 p-6 sm:p-8">
|
<div className="w-[420px] max-w-[92vw] shrink-0 my-auto bg-white rounded-xl shadow-sm border border-gray-200 p-6 sm:p-8">
|
||||||
<div className="flex flex-col items-center mb-6">
|
<div className="flex flex-col items-center mb-6">
|
||||||
<div className="w-12 h-12 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center">
|
<div className="w-12 h-12 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center">
|
||||||
<MailboxIcon className="w-6 h-6" />
|
<BrandMarkIcon className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="mt-3 text-base font-semibold text-gray-900">初始化系统</h1>
|
<h1 className="mt-3 text-base font-semibold text-gray-900">初始化系统</h1>
|
||||||
<p className="mt-1 text-xs text-gray-500 text-center">
|
<p className="mt-1 text-xs text-gray-500 text-center">
|
||||||
|
|||||||
@ -170,13 +170,19 @@ export function LockIcon({ className = 'w-4 h-4' }: P) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MailboxIcon({ className = 'w-8 h-8' }: P) {
|
export function BrandMarkIcon({ className = 'w-8 h-8' }: P) {
|
||||||
|
// 品牌标志(fill 型,与上面的描边图标集不同族,所以不包 Svg)。
|
||||||
|
// 用 currentColor 跟随文字色,放在 text-blue-600 的容器里就是品牌蓝。
|
||||||
|
//
|
||||||
|
// 几何来自 client/electron/src/icons/agentmail.svg —— 那份 SVG 是各端图标的
|
||||||
|
// 唯一源(favicon / Electron / HarmonyOS 都由它生成,见同目录 generate.py)。
|
||||||
|
// 改一边就要重跑生成脚本,否则应用内标志与各端图标会不一致。
|
||||||
return (
|
return (
|
||||||
<Svg className={className}>
|
<svg className={className} viewBox="0 0 24 24" fill="currentColor" role="img" aria-label="AgentMail">
|
||||||
<path d="M22 17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9.5C2 7 4 5 6.5 5H18c2.2 0 4 1.8 4 4z" />
|
<path d="M12 1.5a1.25 1.25 0 0 1 1.25 1.25c0 .5-.29.93-.71 1.13V5h1.71A2.75 2.75 0 0 1 17 7.75V8h2.5A2.5 2.5 0 0 1 22 10.5v9a2.5 2.5 0 0 1-2.5 2.5h-15A2.5 2.5 0 0 1 2 19.5v-9A2.5 2.5 0 0 1 4.5 8H7v-.25A2.75 2.75 0 0 1 9.75 5h1.71V3.88c-.42-.2-.71-.63-.71-1.13A1.25 1.25 0 0 1 12 1.5m-2.25 5A1.25 1.25 0 0 0 8.5 7.75V8h7v-.25a1.25 1.25 0 0 0-1.25-1.25zM4.5 9.5a1 1 0 0 0-1 1v.34l8.5 4.72l8.5-4.72V10.5a1 1 0 0 0-1-1zm-1 3.05v6.95a1 1 0 0 0 1 1h15a1 1 0 0 0 1-1v-6.95l-8.5 4.72z" />
|
||||||
<path d="M6 8h4" />
|
<circle cx="9" cy="11.6" r=".9" />
|
||||||
<path d="M12 19V5" />
|
<circle cx="15" cy="11.6" r=".9" />
|
||||||
</Svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
client/electron/src/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
client/electron/src/icons/16x16.png
Normal file
|
After Width: | Height: | Size: 478 B |
BIN
client/electron/src/icons/256x256.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
client/electron/src/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1022 B |
BIN
client/electron/src/icons/48x48.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
client/electron/src/icons/512x512.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
client/electron/src/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
1
client/electron/src/icons/agentmail.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" role="img" aria-label="AgentMail"><title>AgentMail</title><path fill="currentColor" d="M12 1.5a1.25 1.25 0 0 1 1.25 1.25c0 .5-.29.93-.71 1.13V5h1.71A2.75 2.75 0 0 1 17 7.75V8h2.5A2.5 2.5 0 0 1 22 10.5v9a2.5 2.5 0 0 1-2.5 2.5h-15A2.5 2.5 0 0 1 2 19.5v-9A2.5 2.5 0 0 1 4.5 8H7v-.25A2.75 2.75 0 0 1 9.75 5h1.71V3.88c-.42-.2-.71-.63-.71-1.13A1.25 1.25 0 0 1 12 1.5m-2.25 5A1.25 1.25 0 0 0 8.5 7.75V8h7v-.25a1.25 1.25 0 0 0-1.25-1.25zM4.5 9.5a1 1 0 0 0-1 1v.34l8.5 4.72l8.5-4.72V10.5a1 1 0 0 0-1-1zm-1 3.05v6.95a1 1 0 0 0 1 1h15a1 1 0 0 0 1-1v-6.95l-8.5 4.72z"/><circle cx="9" cy="11.6" r=".9" fill="currentColor"/><circle cx="15" cy="11.6" r=".9" fill="currentColor"/></svg>
|
||||||
|
After Width: | Height: | Size: 756 B |
147
client/electron/src/icons/generate.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""从 agentmail.svg 生成各端图标产物。
|
||||||
|
|
||||||
|
cd client/electron/src/icons && python3 generate.py
|
||||||
|
|
||||||
|
依赖 cairosvg 与 Pillow(本机已装;无 rsvg-convert / ImageMagick)。
|
||||||
|
|
||||||
|
为什么要脚本而不是手工导出:PNG/ICO 是二进制,换一次配色就要重出十几个尺寸,
|
||||||
|
手工做必然出现「Web 是旧的、Harmony 是新的」这类不一致,而且没人能复核。
|
||||||
|
这里只认一个源(agentmail.svg),所有变体都由它推导。
|
||||||
|
|
||||||
|
生成物(相对本目录):
|
||||||
|
icon.png 512 应用图标(品牌底 + 白色字形)→ electron-builder Linux
|
||||||
|
icon.ico 16..256 → electron-builder Windows
|
||||||
|
tray-icon.png 32 托盘(透明底 + 品牌字形)
|
||||||
|
16x16.png … 512x512.png electron-builder 的 Linux icons 目录约定
|
||||||
|
../public/assets/agentmail.svg 前端 favicon(SVG)
|
||||||
|
../public/assets/favicon.ico favicon 兜底
|
||||||
|
../public/assets/apple-touch-icon.png 180
|
||||||
|
../../harmony/entry/src/main/resources/base/media/{startIcon,foreground,background}.png
|
||||||
|
../../harmony/AppScope/resources/base/media/{foreground,background}.png
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import cairosvg
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
REPO = HERE.parents[3] # client/electron/src/icons -> repo root
|
||||||
|
|
||||||
|
BRAND = "#2563eb" # 与门户 Dashy 主题的主色一致
|
||||||
|
WHITE = "#ffffff"
|
||||||
|
SRC = HERE / "agentmail.svg"
|
||||||
|
|
||||||
|
# 源文件里字形用的是 currentColor;替换成实际颜色即可复用同一份几何。
|
||||||
|
COLOR_TOKEN = re.compile(r"currentColor")
|
||||||
|
|
||||||
|
|
||||||
|
def inner_markup(svg: str) -> str:
|
||||||
|
"""取出 <svg> 内的绘制内容(不含外层标签)。"""
|
||||||
|
body = svg.split(">", 1)[1].rsplit("</svg>", 1)[0]
|
||||||
|
# 去掉 title,避免每个变体都带一份无障碍文本
|
||||||
|
return re.sub(r"<title>.*?</title>", "", body, flags=re.S)
|
||||||
|
|
||||||
|
|
||||||
|
def glyph_only(inner: str, color: str, *, inset: float = 0) -> str:
|
||||||
|
"""透明底、单色字形;可留安全边距给系统图标遮罩。"""
|
||||||
|
scale = 1 - 2 * inset
|
||||||
|
return (
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">'
|
||||||
|
f'<g fill="{color}" transform="translate({24 * inset},{24 * inset}) scale({scale})">'
|
||||||
|
f"{COLOR_TOKEN.sub(color, inner)}</g></svg>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def on_brand_tile(inner: str, glyph: str, *, inset: float, radius: float) -> str:
|
||||||
|
"""品牌色圆角底 + 居中字形。
|
||||||
|
|
||||||
|
inset 是字形四周留白占边长的比例:桌面应用图标与 Harmony 的 layered
|
||||||
|
foreground 都要求「字形不顶边」,否则会被系统的圆角/遮罩裁掉。
|
||||||
|
"""
|
||||||
|
scale = 1 - 2 * inset
|
||||||
|
return (
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">'
|
||||||
|
f'<rect x="0" y="0" width="24" height="24" rx="{radius}" fill="{BRAND}"/>'
|
||||||
|
f'<g fill="{glyph}" transform="translate({24 * inset},{24 * inset}) scale({scale})">'
|
||||||
|
f"{COLOR_TOKEN.sub(glyph, inner)}</g></svg>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rasterize(svg: str, size: int) -> Image.Image:
|
||||||
|
png = cairosvg.svg2png(
|
||||||
|
bytestring=svg.encode(), output_width=size, output_height=size
|
||||||
|
)
|
||||||
|
if not isinstance(png, bytes):
|
||||||
|
raise RuntimeError("cairosvg 未返回 PNG 字节")
|
||||||
|
return Image.open(io.BytesIO(png)).convert("RGBA")
|
||||||
|
|
||||||
|
|
||||||
|
def write_png(img: Image.Image, path: Path) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
img.save(path, format="PNG", optimize=True)
|
||||||
|
print(f" {path.relative_to(REPO)} {img.width}x{img.height}")
|
||||||
|
|
||||||
|
|
||||||
|
def write_ico(img: Image.Image, path: Path, sizes: list[int]) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
img.save(path, format="ICO", sizes=[(s, s) for s in sizes])
|
||||||
|
print(f" {path.relative_to(REPO)} ico {sizes}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if not SRC.exists():
|
||||||
|
print(f"缺少源文件 {SRC}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
inner = inner_markup(SRC.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
glyph_brand = glyph_only(inner, BRAND)
|
||||||
|
glyph_white = glyph_only(inner, WHITE, inset=0.12)
|
||||||
|
|
||||||
|
# 应用图标:白字形 + 品牌圆角底。圆角 18% 接近各平台图标的观感,
|
||||||
|
# 且留 12% 安全边距 —— Windows/Linux 会把图标压进更小的方块里显示。
|
||||||
|
app_svg = on_brand_tile(inner, WHITE, inset=0.12, radius=4.3)
|
||||||
|
app = rasterize(app_svg, 512)
|
||||||
|
|
||||||
|
print("Electron / electron-builder")
|
||||||
|
write_png(app, HERE / "icon.png")
|
||||||
|
write_ico(app, HERE / "icon.ico", [16, 24, 32, 48, 64, 128, 256])
|
||||||
|
for size in (16, 32, 48, 64, 128, 256, 512):
|
||||||
|
write_png(rasterize(app_svg, size), HERE / f"{size}x{size}.png")
|
||||||
|
# 托盘:透明底,面板底色未知,用品牌色字形;不做白色(浅色面板会消失)
|
||||||
|
write_png(rasterize(glyph_brand, 32), HERE / "tray-icon.png")
|
||||||
|
|
||||||
|
print("前端 favicon(Gateway 只服务 /assets/*,所以放在 public/assets 下)")
|
||||||
|
web = REPO / "client/electron/public/assets"
|
||||||
|
web.mkdir(parents=True, exist_ok=True)
|
||||||
|
(web / "agentmail.svg").write_text(
|
||||||
|
glyph_only(inner, BRAND) + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
print(f" {web.relative_to(REPO)}/agentmail.svg")
|
||||||
|
write_ico(app, web / "favicon.ico", [16, 32, 48])
|
||||||
|
write_png(rasterize(app_svg, 180), web / "apple-touch-icon.png")
|
||||||
|
|
||||||
|
print("HarmonyOS")
|
||||||
|
harmony_entry = REPO / "client/harmony/entry/src/main/resources/base/media"
|
||||||
|
harmony_app = REPO / "client/harmony/AppScope/resources/base/media"
|
||||||
|
# 启动图只在 entry 资源中引用。完整应用图标在浅/深启动页底色上都清楚。
|
||||||
|
write_png(rasterize(app_svg, 512), harmony_entry / "startIcon.png")
|
||||||
|
for base in (harmony_entry, harmony_app):
|
||||||
|
# 分层图标:背景整块品牌色,前景白色字形并留出安全区。
|
||||||
|
write_png(
|
||||||
|
rasterize(on_brand_tile(inner, BRAND, inset=0, radius=0), 1024),
|
||||||
|
base / "background.png",
|
||||||
|
)
|
||||||
|
write_png(rasterize(glyph_white, 1024), base / "foreground.png")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
BIN
client/electron/src/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
client/electron/src/icons/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
client/electron/src/icons/tray-icon.png
Normal file
|
After Width: | Height: | Size: 1003 B |
|
Before Width: | Height: | Size: 65 B After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 65 B After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 65 B After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 65 B After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 65 B After Width: | Height: | Size: 14 KiB |