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:
160
pkg/icongen/main.go
Normal file
160
pkg/icongen/main.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user