feat(ui): 首次启动加密引导 + 真窗口图标 (.syso)
首次启动: - 配置文件不存在时弹欢迎对话框:选「启用密码保护」或「稍后再说」 - 选启用直接进 setPasswordDialog,OK 时 store.SetPassword 触发首次保存 - io/fs.ErrNotExist 检测新分支 - modalCard 抽出 modalShell 内核以便 welcomeDialog 自定义 footer 真窗口图标: - tools/gen_ico 从 PNG 生成多分辨率 .ico(7 尺寸:16~256) - 提交 rsrc_windows.syso(rsrc 通过 goproxy.cn 生成),go build 自动链入 - tray_windows.go 改用嵌入资源 ID=1,失败 fallback IDI_APPLICATION - README 更新完整的图标生成流程
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
// gen_ico produces a multi-resolution .ico file from a single PNG
|
||||
// source. The output embeds each size as a PNG payload (Vista+ .ico
|
||||
// format) so the result stays small and crisp.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./tools/gen_ico [src.png] [dst.ico]
|
||||
//
|
||||
// Defaults:
|
||||
//
|
||||
// src = internal/ui/assets/app_icon.png
|
||||
// dst = internal/ui/assets/app_icon.ico
|
||||
//
|
||||
// After generating the .ico, embed it into the Windows binary via:
|
||||
//
|
||||
// go run github.com/akavel/rsrc@latest \
|
||||
// -ico internal/ui/assets/app_icon.ico \
|
||||
// -o internal/ui/assets/rsrc_windows.syso
|
||||
//
|
||||
// rsrc_windows.syso is auto-linked by `go build` on Windows.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
// icoSizes are the embedded resolutions. Windows picks the largest size
|
||||
// that fits the target use (title bar = 16, Alt-Tab = 32, Explorer = up
|
||||
// to 256) and scales down. Skipping sizes Windows can't read anyway
|
||||
// keeps the file under 200 KB.
|
||||
var icoSizes = []int{16, 24, 32, 48, 64, 128, 256}
|
||||
|
||||
// icoEntry holds one image's dimensions and its encoded PNG bytes.
|
||||
type icoEntry struct {
|
||||
w, h int
|
||||
png []byte
|
||||
}
|
||||
|
||||
func main() {
|
||||
srcPath := "internal/ui/assets/app_icon.png"
|
||||
dstPath := "internal/ui/assets/app_icon.ico"
|
||||
if len(os.Args) > 1 {
|
||||
srcPath = os.Args[1]
|
||||
}
|
||||
if len(os.Args) > 2 {
|
||||
dstPath = os.Args[2]
|
||||
}
|
||||
if err := run(srcPath, dstPath); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "gen_ico:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(srcPath, dstPath string) error {
|
||||
srcF, err := os.Open(srcPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open src: %w", err)
|
||||
}
|
||||
defer srcF.Close()
|
||||
src, _, err := image.Decode(srcF)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode src: %w", err)
|
||||
}
|
||||
|
||||
var entries []icoEntry
|
||||
for _, size := range icoSizes {
|
||||
dst := image.NewNRGBA(image.Rect(0, 0, size, size))
|
||||
// CatmullRom gives sharper results at small sizes than the
|
||||
// default nearest-neighbour.
|
||||
draw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, dst); err != nil {
|
||||
return fmt.Errorf("encode %dpx: %w", size, err)
|
||||
}
|
||||
entries = append(entries, icoEntry{w: size, h: size, png: buf.Bytes()})
|
||||
}
|
||||
|
||||
out, err := os.Create(dstPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create dst: %w", err)
|
||||
}
|
||||
defer out.Close()
|
||||
return writeICO(out, entries)
|
||||
}
|
||||
|
||||
// writeICO serialises a Vista+ PNG-based .ico file: ICONDIR + one
|
||||
// ICONDIRENTRY per image + the PNG bytes concatenated.
|
||||
func writeICO(w io.Writer, entries []icoEntry) error {
|
||||
if _, err := w.Write([]byte{
|
||||
0, 0, // reserved
|
||||
1, 0, // type = 1 (icon)
|
||||
byte(len(entries)), 0, // count
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const dirHdrLen = 6
|
||||
const dirEntryLen = 16
|
||||
headerLen := dirHdrLen + dirEntryLen*len(entries)
|
||||
|
||||
offset := headerLen
|
||||
for _, e := range entries {
|
||||
wByte := byte(e.w)
|
||||
if e.w >= 256 {
|
||||
// 0 means 256 in the .ico directory.
|
||||
wByte = 0
|
||||
}
|
||||
hByte := byte(e.h)
|
||||
if e.h >= 256 {
|
||||
hByte = 0
|
||||
}
|
||||
size := uint32(len(e.png))
|
||||
|
||||
entry := make([]byte, dirEntryLen)
|
||||
entry[0] = wByte
|
||||
entry[1] = hByte
|
||||
entry[2] = 0 // color palette
|
||||
entry[3] = 0 // reserved
|
||||
binary.LittleEndian.PutUint16(entry[4:], 1) // planes
|
||||
binary.LittleEndian.PutUint16(entry[6:], 32) // bit count
|
||||
binary.LittleEndian.PutUint32(entry[8:], size)
|
||||
binary.LittleEndian.PutUint32(entry[12:], uint32(offset))
|
||||
if _, err := w.Write(entry); err != nil {
|
||||
return err
|
||||
}
|
||||
offset += int(size)
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
if _, err := w.Write(e.png); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user