mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-13 09:59:37 +08:00
✨ feat(font): 新增系统字体枚举与全局字体配置能力
- 新增 Go 侧已安装字体扫描接口,支持前端读取系统真实字体列表 - 接入 Wails 字体查询导出,补齐 App.d.ts 与 App.js 调用声明 - 新增字体选项构建与匹配工具,区分 UI 字体与等宽字体候选 - 外观设置支持按平台加载字体列表,并支持搜索匹配与默认字体回退 - Store 增加自定义 UI 字体与代码字体配置,持久化全局字体选择
This commit is contained in:
172
internal/app/methods_fonts.go
Normal file
172
internal/app/methods_fonts.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
|
||||
"golang.org/x/image/font/sfnt"
|
||||
)
|
||||
|
||||
const maxScannedFontFiles = 4096
|
||||
|
||||
type installedFontFamily struct {
|
||||
Family string `json:"family"`
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
func (a *App) ListInstalledFontFamilies() connection.QueryResult {
|
||||
families, err := listInstalledFontFamilies()
|
||||
if err != nil {
|
||||
return connection.QueryResult{
|
||||
Success: false,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
return connection.QueryResult{
|
||||
Success: true,
|
||||
Data: families,
|
||||
}
|
||||
}
|
||||
|
||||
func listInstalledFontFamilies() ([]installedFontFamily, error) {
|
||||
fontPaths := resolveSystemFontDirs()
|
||||
if len(fontPaths) == 0 {
|
||||
return []installedFontFamily{}, nil
|
||||
}
|
||||
|
||||
seenDirs := make(map[string]struct{}, len(fontPaths))
|
||||
familyByName := make(map[string]installedFontFamily)
|
||||
scannedFiles := 0
|
||||
|
||||
for _, rawDir := range fontPaths {
|
||||
dir := strings.TrimSpace(rawDir)
|
||||
if dir == "" {
|
||||
continue
|
||||
}
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
absDir = dir
|
||||
}
|
||||
cleanDir := filepath.Clean(absDir)
|
||||
if _, ok := seenDirs[cleanDir]; ok {
|
||||
continue
|
||||
}
|
||||
seenDirs[cleanDir] = struct{}{}
|
||||
|
||||
info, err := os.Stat(cleanDir)
|
||||
if err != nil || !info.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
walkErr := filepath.WalkDir(cleanDir, func(path string, d os.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return nil
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if scannedFiles >= maxScannedFontFiles {
|
||||
return fs.SkipAll
|
||||
}
|
||||
if !isFontFile(path) {
|
||||
return nil
|
||||
}
|
||||
scannedFiles++
|
||||
family := readFontFamilyName(path)
|
||||
if family == "" {
|
||||
return nil
|
||||
}
|
||||
if _, exists := familyByName[family]; exists {
|
||||
return nil
|
||||
}
|
||||
familyByName[family] = installedFontFamily{
|
||||
Family: family,
|
||||
Path: path,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if walkErr == fs.SkipAll {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]installedFontFamily, 0, len(familyByName))
|
||||
for _, item := range familyByName {
|
||||
result = append(result, item)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return strings.ToLower(result[i].Family) < strings.ToLower(result[j].Family)
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func resolveSystemFontDirs() []string {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
home, _ := os.UserHomeDir()
|
||||
return []string{
|
||||
"/System/Library/Fonts",
|
||||
"/Library/Fonts",
|
||||
filepath.Join(home, "Library", "Fonts"),
|
||||
}
|
||||
case "windows":
|
||||
winDir := os.Getenv("WINDIR")
|
||||
localAppData := os.Getenv("LOCALAPPDATA")
|
||||
return []string{
|
||||
filepath.Join(winDir, "Fonts"),
|
||||
filepath.Join(localAppData, "Microsoft", "Windows", "Fonts"),
|
||||
}
|
||||
default:
|
||||
home, _ := os.UserHomeDir()
|
||||
return []string{
|
||||
"/usr/share/fonts",
|
||||
"/usr/local/share/fonts",
|
||||
filepath.Join(home, ".fonts"),
|
||||
filepath.Join(home, ".local", "share", "fonts"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isFontFile(path string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
switch ext {
|
||||
case ".ttf", ".otf", ".ttc", ".otc":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func readFontFamilyName(path string) string {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
collection, err := sfnt.ParseCollectionReaderAt(file)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
var buf sfnt.Buffer
|
||||
fontCount := collection.NumFonts()
|
||||
for i := range fontCount {
|
||||
font, fontErr := collection.Font(i)
|
||||
if fontErr != nil {
|
||||
continue
|
||||
}
|
||||
for _, nameID := range []sfnt.NameID{sfnt.NameIDTypographicFamily, sfnt.NameIDFamily} {
|
||||
family, nameErr := font.Name(&buf, nameID)
|
||||
if nameErr == nil && strings.TrimSpace(family) != "" {
|
||||
return strings.TrimSpace(family)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user