feat: 接入托盘 / 添加图标生成工具 / 重命名并完善前端

- 托盘:用 getlantern/systray 重写 internal/tray,支持显示/隐藏窗口与退出
- 图标:新增 pkg/icongen 工具,支持 png/jpg/gif/bmp/webp,输出 ico/png 多个尺寸
- 重命名:项目默认名改为「青梧映星」,wails.json/productname/companyname/copyright 同步更新
- 顶栏:新增顶部导航栏(主题切换、设置、用户),iconfont 自托管 fa-solid-900.woff2
- 主题:CSS 变量 + html[data-theme] 控制白昼/黑夜切换,同时调用 WindowSetDarkTheme/LightTheme
- 底部:版权栏包含工作室、开发者与点击跳转客服链接
- 打包:wails build + exe 版本更新至 1.20 MB
This commit is contained in:
2026-09-18 01:30:13 +08:00
parent d8789e9034
commit a2a1161f10
19 changed files with 755 additions and 259 deletions

160
pkg/icongen/main.go Normal file
View File

@@ -0,0 +1,160 @@
// Command icongen 根据一张图片快速生成当前项目的托盘图标与打包图标,
// 已存在的图标文件会被直接覆盖。
//
// 用法(在项目根目录执行):
//
// go run ./pkg/icongen -src /path/to/image.png
//
// 可选参数:
//
// go run ./pkg/icongen -src logo.jpg -root . -appicon 1024 -tray 256
package main
import (
"bytes"
"encoding/binary"
"flag"
"fmt"
"image"
imagedraw "image/draw"
"image/png"
"log"
"os"
"path/filepath"
"golang.org/x/image/draw"
_ "golang.org/x/image/webp"
_ "image/gif"
_ "image/jpeg"
)
// ico 文件中包含的尺寸
var icoSizes = []int{256, 128, 64, 48, 32, 16}
func main() {
src := flag.String("src", "", "源图片路径(png/jpg/jpeg/gif/bmp/webp)")
root := flag.String("root", ".", "项目根目录")
appicon := flag.Int("appicon", 1024, "build/appicon.png 尺寸")
icon := flag.Int("icon", 1024, "build/icon.png 尺寸")
tray := flag.Int("tray", 256, "trayicons/assets 图标尺寸")
flag.Parse()
if *src == "" {
log.Fatal("[-src] 源图片路径不能为空")
}
f, err := os.Open(*src)
if err != nil {
log.Fatalf("打开源图片失败: %v", err)
}
defer f.Close()
srcImg, _, err := image.Decode(f)
if err != nil {
log.Fatalf("解码源图片失败(支持 png/jpg/gif/bmp/webp): %v", err)
}
log.Printf("源图片: %s (%dx%d)", *src, srcImg.Bounds().Dx(), srcImg.Bounds().Dy())
cropped := centerCrop(srcImg)
// PNG 目标:源图居中裁剪后缩放
writePNG(filepath.Join(*root, "build", "appicon.png"), cropped, *appicon)
writePNG(filepath.Join(*root, "build", "icon.png"), cropped, *icon)
writePNG(filepath.Join(*root, "assets", "icon.png"), cropped, *tray)
writePNG(filepath.Join(*root, "trayicons", "icon.png"), cropped, *tray)
// ICO 目标:Windows exe / 托盘图标(多尺寸)
writeICO(filepath.Join(*root, "build", "windows", "icon.ico"), cropped, icoSizes)
log.Println("完成,所有图标已按需覆盖。")
}
// centerCrop 从图像中心裁剪出正方形区域。
func centerCrop(srcImg image.Image) image.Image {
b := srcImg.Bounds()
w, h := b.Dx(), b.Dy()
side := w
if h < side {
side = h
}
x0 := b.Min.X + (w-side)/2
y0 := b.Min.Y + (h-side)/2
rect := image.Rect(0, 0, side, side)
out := image.NewRGBA(rect)
imagedraw.Draw(out, rect, srcImg, image.Pt(x0, y0), imagedraw.Src)
return out
}
// writePNG 按 size 缩放后写出 PNG 文件。
func writePNG(path string, cropped image.Image, size int) {
dst := image.NewRGBA(image.Rect(0, 0, size, size))
draw.CatmullRom.Scale(dst, dst.Bounds(), cropped, cropped.Bounds(), draw.Over, nil)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
log.Fatalf("创建目录失败 %s: %v", filepath.Dir(path), err)
}
out, err := os.Create(path)
if err != nil {
log.Fatalf("创建文件失败 %s: %v", path, err)
}
defer out.Close()
if err := png.Encode(out, dst); err != nil {
log.Fatalf("编码 PNG 失败 %s: %v", path, err)
}
fmt.Printf(" [ok] %s (%dx%d)\n", path, size, size)
}
// writeICO 写出多尺寸 ICO(Windows 图标,PNG 条目压缩,Vista+ 支持)。
func writeICO(path string, cropped image.Image, sizes []int) {
type entry struct {
w, h byte
data []byte
}
entries := make([]entry, 0, len(sizes))
for _, s := range sizes {
dst := image.NewRGBA(image.Rect(0, 0, s, s))
draw.CatmullRom.Scale(dst, dst.Bounds(), cropped, cropped.Bounds(), draw.Over, nil)
var pngBuf bytes.Buffer
if err := png.Encode(&pngBuf, dst); err != nil {
log.Fatalf("编码 ICO 条目失败 (%dpx): %v", s, err)
}
w, h := byte(s), byte(s)
if s == 256 {
w, h = 0, 0 // 0 表示 256
}
entries = append(entries, entry{w: w, h: h, data: pngBuf.Bytes()})
}
var buf bytes.Buffer
// ICONDIR(6 字节头)
binary.Write(&buf, binary.LittleEndian, uint16(0)) // Reserved
binary.Write(&buf, binary.LittleEndian, uint16(1)) // Type: icon
binary.Write(&buf, binary.LittleEndian, uint16(len(entries)))
// ICONDIRENTRY(16 字节 x N)
cur := 6 + 16*len(entries)
for _, e := range entries {
binary.Write(&buf, binary.LittleEndian, e.w)
binary.Write(&buf, binary.LittleEndian, e.h)
binary.Write(&buf, binary.LittleEndian, uint8(0)) // 颜色数(忽略)
binary.Write(&buf, binary.LittleEndian, uint8(0)) // 保留
binary.Write(&buf, binary.LittleEndian, uint16(1)) // 平面数
binary.Write(&buf, binary.LittleEndian, uint16(32))
binary.Write(&buf, binary.LittleEndian, uint32(len(e.data)))
binary.Write(&buf, binary.LittleEndian, uint32(cur))
cur += len(e.data)
}
// PNG 图像数据
for _, e := range entries {
buf.Write(e.data)
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
log.Fatalf("创建目录失败 %s: %v", filepath.Dir(path), err)
}
if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
log.Fatalf("写入 ICO 失败 %s: %v", path, err)
}
fmt.Printf(" [ok] %s (ico %v)\n", path, sizes)
}