| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394 |
- // ============================================================================
- // settings.go — 应用配置
- //
- // AppSettings 结构体(含各 Tab 设置项)、默认值、字段校验(normalizeSettings)、
- // 配置读写(loadSettings/saveSettings)与运行时应用(applySettings)。
- // ============================================================================
- package main
- import (
- "encoding/json"
- "log"
- "os"
- "path/filepath"
- "strings"
- "sync"
- )
- // 默认配置:首次运行或字段为空时使用
- const (
- DefaultHotkey = "CmdOrCtrl+Shift+G" // 默认呼出窗口热键
- DefaultNotesURL = "https://m.scwy.net" // 默认笔记页地址
- DefaultControlURL = "https://a7.scwy.net/4191" // 默认控制页地址
- DefaultBlogURL = "https://i.scwy.net" // 默认博客页地址
- DefaultChatURL = "https://c.scwy.net" // 默认聊天页地址
- DefaultAPIAddr = "127.0.0.1:17890" // 内置 Gin API 服务默认监听地址(仅本机)
- DefaultRecordPath = `D:\My\腾图\记录.txt` // 默认记录文件路径
- DefaultTodoPath = `D:\My\腾图\待办.txt` // 默认待办(TODO)文件路径
- DefaultHardwareURL = "http://192.168.100.190/" // 默认硬件页地址
- DefaultStockUpColor = "#ff5a5a" // 股票上涨颜色(红涨)
- DefaultStockDownColor = "#4d8dff" // 股票下跌颜色(蓝跌)
- DefaultStockRefreshSecs = 30 // 股市自动刷新间隔(秒)
- DefaultStockClosedRefreshSecs = 7200 // 股市休市时自动刷新间隔(秒),默认 2 小时
- DefaultNewsRefreshMinutes = 120 // 新闻自动刷新间隔(分钟)
- DefaultNewsCount = 30 // 新闻页显示条数
- DefaultGithubRefreshMinutes = 120 // Github 自动刷新间隔(分钟)
- AppVersion = "0.1.0" // 应用自身版本(与 build/windows/info.json 的 ProductVersion 一致),用于仓库更新提示
- )
- // LinkItem 常用网址条目
- type LinkItem struct {
- Name string `json:"name"` // 网站名称
- URL string `json:"url"` // 网站地址
- }
- // CustomTab 用户自定义 Tab 页面
- type CustomTab struct {
- Name string `json:"name"` // 页面名称(Tab 按钮显示文字)
- URL string `json:"url"` // 页面地址(iframe 加载)
- }
- // RepoItem 关注的 GitHub 仓库条目
- type RepoItem struct {
- URL string `json:"url"` // 仓库地址,如 https://github.com/wailsapp/wails
- Desc string `json:"desc"` // 项目说明
- Version string `json:"version"` // 本地已安装版本(用于与最新 release 比较),可为空
- }
- // TickerConfig 置顶滚动条(股票 / 新闻)的显示配置
- type TickerConfig struct {
- Enabled bool `json:"enabled"` // 是否显示滚动条窗口
- Width int `json:"width"` // 窗口宽度(px),0 表示默认 1400
- FontSize int `json:"fontSize"` // 字号(px)
- Color string `json:"color"` // 文字颜色
- Speed int `json:"speed"` // 滚动速度(px/s)
- Transparent bool `json:"transparent"` // 背景透明,不遮挡下方内容
- X int `json:"x"` // 窗口位置,-1 表示未设置(系统默认位置)
- Y int `json:"y"` // 窗口位置,-1 表示未设置
- }
- // AppSettings 保存全部可自定义配置,序列化为 JSON 存于用户配置目录
- type AppSettings struct {
- Theme string `json:"theme"` // 界面主题:dark / light
- Autostart bool `json:"autostart"` // 系统启动时运行
- AlwaysOnTop bool `json:"alwaysOnTop"` // 窗口置顶
- Hotkey string `json:"hotkey"` // 呼出(显示)窗口的全局热键
- NotesURL string `json:"notesUrl"` // 笔记页地址
- ControlURL string `json:"controlUrl"` // 控制页地址
- BlogURL string `json:"blogUrl"` // 博客页地址
- ChatURL string `json:"chatUrl"` // 聊天页地址
- TabOrder []string `json:"tabOrder"` // Tab 显示顺序(tabId 列表,未列出的追加到末尾)
- StockCodes []string `json:"stockCodes"` // 股市页展示的股票代码列表
- Links []LinkItem `json:"links"` // 常用网址列表
- NewsKeywords string `json:"newsKeywords"` // 新闻关注关键词,逗号分隔
- BiliCookie string `json:"biliCookie"` // B站登录 cookie(含 SESSDATA),用于抓取「关注的人最新动态」
- GithubRepos []RepoItem `json:"githubRepos"` // 关注的 GitHub 仓库列表
- CustomTabs []CustomTab `json:"customTabs"` // 用户自定义 Tab 页面(名字 + 网址)
- APIAddr string `json:"apiAddr"` // 内置 API 服务监听地址
- RecordPath string `json:"recordPath"` // 记录文件路径
- TodoPath string `json:"todoPath"` // 待办(TODO)文件路径
- HardwareURL string `json:"hardwareUrl"` // 硬件页地址
- StockTicker TickerConfig `json:"stockTicker"` // 股票滚动条配置
- NewsTicker TickerConfig `json:"newsTicker"` // 新闻滚动条配置
- Poem PoemConfig `json:"poem"` // 随机古诗显示配置
- StockUpColor string `json:"stockUpColor"` // 股票上涨颜色
- StockDownColor string `json:"stockDownColor"` // 股票下跌颜色
- StockRefreshSecs int `json:"stockRefreshSecs"` // 股市自动刷新间隔(秒)
- StockClosedRefreshSecs int `json:"stockClosedRefreshSecs"` // 股市休市时自动刷新间隔(秒)
- NewsRefreshMinutes int `json:"newsRefreshMinutes"` // 新闻自动刷新间隔(分钟)
- NewsCount int `json:"newsCount"` // 新闻页显示条数
- GithubRefreshMinutes int `json:"githubRefreshMinutes"` // Github 自动刷新间隔(分钟)
- FontDelta int `json:"fontDelta"` // 全局字号偏移(px),+-3 范围内调整
- }
- // 当前已注册的热键(用于保存设置时先注销旧热键)
- var (
- hotkeyMu sync.Mutex
- activeHotkey string
- )
- // defaultSettings 返回默认配置:开机启动默认开启,热键与页面地址使用内置值
- func defaultSettings() AppSettings {
- return AppSettings{
- Theme: "dark",
- Autostart: true,
- AlwaysOnTop: false,
- Hotkey: DefaultHotkey,
- NotesURL: DefaultNotesURL,
- ControlURL: DefaultControlURL,
- BlogURL: DefaultBlogURL,
- ChatURL: DefaultChatURL,
- TabOrder: []string{"notes", "control", "blog", "chat", "links", "news", "github", "stock", "record", "todo", "hardware", "tools", "host", "settings"}, // 默认 Tab 顺序(工具在硬件与主机之间)
- StockCodes: []string{"sh000001", "sh600519", "sz000001"},
- Links: []LinkItem{
- {Name: "煎蛋", URL: "https://jandan.net/"},
- {Name: "头条", URL: "https://www.toutiao.com/"},
- {Name: "GitHub", URL: "https://github.com/"},
- },
- NewsKeywords: "",
- GithubRepos: []RepoItem{
- {URL: "https://github.com/wailsapp/wails", Desc: "Wails:Go + Web 桌面应用框架"},
- },
- APIAddr: DefaultAPIAddr,
- RecordPath: DefaultRecordPath,
- TodoPath: DefaultTodoPath,
- HardwareURL: DefaultHardwareURL,
- StockTicker: TickerConfig{Enabled: false, Width: 1400, FontSize: 14, Color: "#ff4d4d", Speed: 80, Transparent: true, X: -1, Y: -1},
- NewsTicker: TickerConfig{Enabled: false, Width: 1400, FontSize: 14, Color: "#3ddc84", Speed: 60, Transparent: true, X: -1, Y: -1},
- Poem: PoemConfig{Enabled: false, Color: DefaultPoemColor, FontSize: DefaultPoemFontSize, TTS: false, Interval: DefaultPoemInterval, Speed: DefaultPoemSpeed},
- StockUpColor: DefaultStockUpColor,
- StockDownColor: DefaultStockDownColor,
- StockRefreshSecs: DefaultStockRefreshSecs,
- StockClosedRefreshSecs: DefaultStockClosedRefreshSecs,
- NewsRefreshMinutes: DefaultNewsRefreshMinutes,
- NewsCount: DefaultNewsCount,
- GithubRefreshMinutes: DefaultGithubRefreshMinutes,
- FontDelta: 0, // 全局字号偏移默认 0
- }
- }
- // settingsFilePath 返回配置文件路径:<用户配置目录>/TengTuToolbox/settings.json
- func settingsFilePath() (string, error) {
- dir, err := os.UserConfigDir() // 例如 C:\Users\<用户名>\AppData\Roaming
- if err != nil {
- return "", err
- }
- return filepath.Join(dir, "TengTuToolbox", "settings.json"), nil
- }
- // loadSettings 读取配置文件;文件不存在或损坏时回退到默认配置
- func loadSettings() AppSettings {
- defaults := defaultSettings()
- path, err := settingsFilePath()
- if err != nil {
- return defaults
- }
- data, err := os.ReadFile(path)
- if err != nil {
- return defaults // 首次运行:无配置文件,使用默认值
- }
- settings := defaults
- if err := json.Unmarshal(data, &settings); err != nil {
- return defaults // 文件损坏:回退默认值,避免启动失败
- }
- return normalizeSettings(settings)
- }
- // saveSettings 将配置写入磁盘(目录不存在时自动创建)
- func saveSettings(settings AppSettings) error {
- path, err := settingsFilePath()
- if err != nil {
- return err
- }
- if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
- return err
- }
- data, err := json.MarshalIndent(settings, "", " ") // 缩进保存,便于手动查看
- if err != nil {
- return err
- }
- return os.WriteFile(path, data, 0o644)
- }
- // normalizeSettings 校正配置字段:空值回填默认、主题取值限定、股票代码规范化
- func normalizeSettings(settings AppSettings) AppSettings {
- if settings.Theme != "light" {
- settings.Theme = "dark" // 只允许 dark / light
- }
- if strings.TrimSpace(settings.Hotkey) == "" {
- settings.Hotkey = DefaultHotkey
- }
- if strings.TrimSpace(settings.NotesURL) == "" {
- settings.NotesURL = DefaultNotesURL
- }
- if strings.TrimSpace(settings.ControlURL) == "" {
- settings.ControlURL = DefaultControlURL
- }
- if strings.TrimSpace(settings.BlogURL) == "" {
- settings.BlogURL = DefaultBlogURL
- }
- if strings.TrimSpace(settings.ChatURL) == "" {
- settings.ChatURL = DefaultChatURL
- }
- // Tab 顺序:只保留已存在的 tabId,去重;未列出的追加到末尾,保证全部 Tab 都能显示
- validTabs := map[string]bool{"notes": true, "control": true, "blog": true, "chat": true, "links": true,
- "news": true, "github": true, "stock": true, "record": true, "todo": true, "hardware": true, "tools": true, "host": true, "settings": true}
- seen := map[string]bool{}
- ordered := make([]string, 0, len(validTabs))
- for _, tab := range settings.TabOrder {
- if validTabs[tab] && !seen[tab] {
- seen[tab] = true
- ordered = append(ordered, tab)
- }
- }
- for _, tab := range []string{"notes", "control", "blog", "chat", "links", "news", "github", "stock", "record", "todo", "hardware", "tools", "host", "settings"} {
- if !seen[tab] {
- ordered = append(ordered, tab) // 设置中未列出的 Tab 追加到末尾
- }
- }
- settings.TabOrder = ordered
- if strings.TrimSpace(settings.APIAddr) == "" {
- settings.APIAddr = DefaultAPIAddr
- }
- if strings.TrimSpace(settings.RecordPath) == "" {
- settings.RecordPath = DefaultRecordPath
- }
- if strings.TrimSpace(settings.TodoPath) == "" {
- settings.TodoPath = DefaultTodoPath
- }
- if strings.TrimSpace(settings.HardwareURL) == "" {
- settings.HardwareURL = DefaultHardwareURL
- }
- settings.StockTicker = normalizeTicker(settings.StockTicker, "#ff4d4d", 80) // 股票条默认红字
- settings.NewsTicker = normalizeTicker(settings.NewsTicker, "#3ddc84", 60) // 新闻条默认绿字
- settings.Poem = normalizePoemConfig(settings.Poem) // 古诗显示配置校正
- if !isHexColor(settings.StockUpColor) {
- settings.StockUpColor = DefaultStockUpColor // 涨色回填默认红
- }
- if !isHexColor(settings.StockDownColor) {
- settings.StockDownColor = DefaultStockDownColor // 跌色回填默认蓝
- }
- if settings.StockRefreshSecs < 5 || settings.StockRefreshSecs > 3600 {
- settings.StockRefreshSecs = DefaultStockRefreshSecs // 股市刷新间隔限定 5 秒 ~ 1 小时
- }
- if settings.StockClosedRefreshSecs < 60 || settings.StockClosedRefreshSecs > 86400 {
- settings.StockClosedRefreshSecs = DefaultStockClosedRefreshSecs // 休市刷新间隔限定 1 分钟 ~ 1 天
- }
- if settings.NewsRefreshMinutes < 5 || settings.NewsRefreshMinutes > 1440 {
- settings.NewsRefreshMinutes = DefaultNewsRefreshMinutes // 新闻刷新间隔限定 5 分钟 ~ 1 天
- }
- if settings.NewsCount < 5 || settings.NewsCount > 200 {
- settings.NewsCount = DefaultNewsCount // 新闻显示条数限定 5 ~ 200 条
- }
- if settings.GithubRefreshMinutes < 5 || settings.GithubRefreshMinutes > 1440 {
- settings.GithubRefreshMinutes = DefaultGithubRefreshMinutes // Github 刷新间隔限定 5 分钟 ~ 1 天
- }
- if settings.FontDelta < -3 || settings.FontDelta > 3 {
- settings.FontDelta = 0 // 全局字号偏移限定 +-3 px
- }
- if settings.StockCodes == nil {
- settings.StockCodes = defaultSettings().StockCodes // 未设置时用默认自选股
- }
- if settings.Links == nil {
- settings.Links = defaultSettings().Links // 未设置时用默认网址
- }
- if settings.GithubRepos == nil {
- settings.GithubRepos = defaultSettings().GithubRepos // 未设置时用默认仓库
- }
- links := make([]LinkItem, 0, len(settings.Links))
- for _, link := range settings.Links {
- link.Name = strings.TrimSpace(link.Name)
- link.URL = strings.TrimSpace(link.URL)
- if link.Name != "" && link.URL != "" {
- links = append(links, link)
- }
- }
- settings.Links = links
- repos := make([]RepoItem, 0, len(settings.GithubRepos))
- for _, repo := range settings.GithubRepos {
- repo.URL = strings.TrimSpace(repo.URL)
- repo.Desc = strings.TrimSpace(repo.Desc)
- repo.Version = strings.TrimSpace(repo.Version) // 本地版本,可为空
- if repo.Version == "" {
- repo.Version = AppVersion // 未填版本时自动用应用自身版本,用于分析并提示新版本
- }
- if repo.URL != "" {
- repos = append(repos, repo)
- }
- }
- settings.GithubRepos = repos
- tabs := make([]CustomTab, 0, len(settings.CustomTabs))
- for _, tab := range settings.CustomTabs {
- tab.Name = strings.TrimSpace(tab.Name)
- tab.URL = strings.TrimSpace(tab.URL)
- if tab.Name != "" && tab.URL != "" { // 名字与网址都非空才保留
- tabs = append(tabs, tab)
- }
- }
- settings.CustomTabs = tabs
- codes := make([]string, 0, len(settings.StockCodes))
- for _, raw := range settings.StockCodes {
- code := normalizeStockCode(raw) // 补全 sh / sz / bj 前缀
- if code != "" {
- codes = append(codes, code)
- }
- }
- settings.StockCodes = codes
- return settings
- }
- // normalizeTicker 校正滚动条配置:宽度、字号、速度下限,颜色默认值
- func normalizeTicker(cfg TickerConfig, defaultColor string, defaultSpeed int) TickerConfig {
- if cfg.Width < 200 || cfg.Width > 6000 {
- cfg.Width = 0 // 宽度限定在 200~6000px,0 表示用默认 1400
- }
- if cfg.FontSize < 10 || cfg.FontSize > 40 {
- cfg.FontSize = 14 // 字号限定在 10~40px
- }
- if strings.TrimSpace(cfg.Color) == "" {
- cfg.Color = defaultColor
- }
- if cfg.Speed < 20 || cfg.Speed > 400 {
- cfg.Speed = defaultSpeed // 速度限定在 20~400 px/s
- }
- return cfg
- }
- // isHexColor 判断是否为 #RGB / #RRGGBB 形式的颜色值
- func isHexColor(value string) bool {
- value = strings.TrimSpace(value)
- if !strings.HasPrefix(value, "#") {
- return false
- }
- return len(value) == 4 || len(value) == 7
- }
- // mergeTickerPositions 保留磁盘上已保存的滚动条位置:
- // 设置表单不提交位置,保存其他设置时不能覆盖拖动后记录的最新坐标
- func mergeTickerPositions(settings AppSettings) AppSettings {
- prev := loadSettings()
- settings.StockTicker.X, settings.StockTicker.Y = prev.StockTicker.X, prev.StockTicker.Y
- settings.NewsTicker.X, settings.NewsTicker.Y = prev.NewsTicker.X, prev.NewsTicker.Y
- return settings
- }
- // applySettings 把配置应用到运行时:重新注册热键、设置窗口置顶、同步开机启动
- func applySettings(settings AppSettings) {
- hotkeyMu.Lock()
- if settings.Hotkey != activeHotkey {
- if activeHotkey != "" {
- _ = app.GlobalShortcut.Unregister(activeHotkey) // 注销旧热键,避免冲突
- }
- if err := app.GlobalShortcut.Register(settings.Hotkey, showMainWindow); err != nil {
- log.Printf("register hotkey %q failed: %v", settings.Hotkey, err)
- } else {
- activeHotkey = settings.Hotkey // 记录当前生效的热键
- }
- }
- hotkeyMu.Unlock()
- if mainWindow != nil {
- mainWindow.SetAlwaysOnTop(settings.AlwaysOnTop) // 窗口置顶开关
- }
- if settings.Autostart {
- _ = app.Autostart.Enable() // 写入系统开机启动项
- } else {
- _ = app.Autostart.Disable() // 移除开机启动项
- }
- ensureAPIServer(settings.APIAddr) // API 地址变化时重启内置服务
- syncTickerWindows(settings) // 按配置创建 / 关闭 / 刷新滚动条窗口
- }
|