settings.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. // ============================================================================
  2. // settings.go — 应用配置
  3. //
  4. // AppSettings 结构体(含各 Tab 设置项)、默认值、字段校验(normalizeSettings)、
  5. // 配置读写(loadSettings/saveSettings)与运行时应用(applySettings)。
  6. // ============================================================================
  7. package main
  8. import (
  9. "crypto/rand"
  10. "encoding/hex"
  11. "encoding/json"
  12. "fmt"
  13. "log"
  14. "os"
  15. "path/filepath"
  16. "strings"
  17. "sync"
  18. )
  19. // 默认配置:首次运行或字段为空时使用
  20. const (
  21. DefaultHotkey = "CmdOrCtrl+Shift+G" // 默认呼出窗口热键
  22. DefaultAPIAddr = "127.0.0.1:17890" // 内置 Gin API 服务默认监听地址(仅本机)
  23. DefaultStockUpColor = "#ff5a5a" // 股票上涨颜色(红涨)
  24. DefaultStockDownColor = "#4d8dff" // 股票下跌颜色(蓝跌)
  25. DefaultStockRefreshSecs = 30 // 股市自动刷新间隔(秒)
  26. DefaultStockClosedRefreshSecs = 7200 // 股市休市时自动刷新间隔(秒),默认 2 小时
  27. DefaultNewsRefreshMinutes = 120 // 新闻自动刷新间隔(分钟)
  28. DefaultNewsCount = 30 // 新闻页显示条数
  29. DefaultGithubRefreshMinutes = 120 // Github 自动刷新间隔(分钟)
  30. AppVersion = "0.1.4" // 应用自身版本(与 build/windows/info.json 的 ProductVersion 一致),用于仓库更新提示
  31. // 鼠标提醒默认配置
  32. DefaultMouseAlertFontSize = 20 // 字号(px)
  33. DefaultMouseAlertWidth = 200 // 气泡窗口宽度(px)
  34. DefaultMouseAlertSpeed = 33 // 超长文字滚动速度(px/s)
  35. DefaultMouseAlertColor = "#ffffff" // 文字颜色(白)
  36. DefaultMouseAlertDuration = 8 // 显示时长(秒)
  37. DefaultMouseAlertOutlineColor = "#000000" // 文字描边颜色(黑边,与白字形成反差)
  38. DefaultMouseAlertOutlineWidth = 2 // 文字描边宽度(px),0 表示关闭
  39. )
  40. // LinkItem 常用网址条目
  41. type LinkItem struct {
  42. Name string `json:"name"` // 网站名称
  43. URL string `json:"url"` // 网站地址
  44. }
  45. // CustomTab 用户自定义 Tab 页面
  46. type CustomTab struct {
  47. Name string `json:"name"` // 页面名称(Tab 按钮显示文字)
  48. URL string `json:"url"` // 页面地址(iframe 加载)
  49. Desc string `json:"desc"` // 备注/提示信息(可空),Tab 说明文字优先显示
  50. }
  51. // RepoItem 关注的 GitHub 仓库条目
  52. type RepoItem struct {
  53. URL string `json:"url"` // 仓库地址,如 https://github.com/wailsapp/wails
  54. Desc string `json:"desc"` // 项目说明
  55. Version string `json:"version"` // 本地已安装版本(用于与最新 release 比较),可为空
  56. }
  57. // TickerConfig 置顶滚动条(股票 / 新闻)的显示配置
  58. type TickerConfig struct {
  59. Enabled bool `json:"enabled"` // 是否显示滚动条窗口
  60. Width int `json:"width"` // 窗口宽度(px),0 表示默认 1400
  61. FontSize int `json:"fontSize"` // 字号(px)
  62. Color string `json:"color"` // 文字颜色
  63. Speed int `json:"speed"` // 滚动速度(px/s)
  64. Transparent bool `json:"transparent"` // 背景透明,不遮挡下方内容
  65. X int `json:"x"` // 窗口位置,-1 表示未设置(系统默认位置)
  66. Y int `json:"y"` // 窗口位置,-1 表示未设置
  67. }
  68. // MouseAlertConfig 系统级鼠标提醒气泡的显示配置
  69. type MouseAlertConfig struct {
  70. FontSize int `json:"fontSize"` // 字号(px)
  71. Width int `json:"width"` // 气泡窗口宽度(px)
  72. Speed int `json:"speed"` // 超长文字滚动速度(px/s)
  73. Color string `json:"color"` // 文字颜色
  74. Background string `json:"background"` // 气泡背景色(COLORKEY 抠成透明,仅作底色参考)
  75. Duration int `json:"duration"` // 显示时长(秒)
  76. OutlineColor string `json:"outlineColor"` // 文字描边颜色(白边/黑边,与文字形成反差)
  77. OutlineWidth int `json:"outlineWidth"` // 文字描边宽度(px),0 表示关闭描边
  78. }
  79. // AppSettings 保存全部可自定义配置,序列化为 JSON 存于用户配置目录
  80. type AppSettings struct {
  81. Theme string `json:"theme"` // 界面主题:dark / light
  82. Autostart bool `json:"autostart"` // 系统启动时运行
  83. AlwaysOnTop bool `json:"alwaysOnTop"` // 窗口置顶
  84. StartCompact bool `json:"startCompact"` // 启动时自动收拢为圆点(窗口缩小)
  85. Hotkey string `json:"hotkey"` // 呼出(显示)窗口的全局热键
  86. TabOrder []string `json:"tabOrder"` // Tab 显示顺序(tabId 列表,未列出的追加到末尾)
  87. StockCodes []string `json:"stockCodes"` // 股市页展示的股票代码列表
  88. Links []LinkItem `json:"links"` // 常用网址列表
  89. NewsKeywords string `json:"newsKeywords"` // 新闻关注关键词,逗号分隔
  90. BiliCookie string `json:"biliCookie"` // B站登录 cookie(含 SESSDATA),用于抓取「关注的人最新动态」
  91. GithubRepos []RepoItem `json:"githubRepos"` // 关注的 GitHub 仓库列表
  92. CustomTabs []CustomTab `json:"customTabs"` // 用户自定义 Tab 页面(名字 + 网址)
  93. APIAddr string `json:"apiAddr"` // 内置 API 服务监听地址
  94. APIAutoStart bool `json:"apiAutoStart"` // 应用启动时自动启动内置 API 服务(默认开启)
  95. UploadPassword string `json:"uploadPassword"` // 文件上传密码(空=不启用上传/下载接口)
  96. UploadDir string `json:"uploadDir"` // 文件上传保存目录(空=用户配置目录/uploads)
  97. UploadMaxSize int `json:"uploadMaxSize"` // 单个上传文件大小上限(MB),<=0 用默认 512
  98. TodoPath string `json:"todoPath"` // 待办(TODO)文件路径
  99. RecordPath string `json:"recordPath"` // 记录(Record)文件路径:每行「姓名|名称|值」(保留兼容)
  100. RecordDBPath string `json:"recordDBPath"` // 记录数据库路径:SQLite3 数据库文件(记录 Tab 实际存储)
  101. BackupDir string `json:"backupDir"` // 文本文件备份目录(文件类自定义 Tab「备份」按钮使用)
  102. StockTicker TickerConfig `json:"stockTicker"` // 股票滚动条配置
  103. NewsTicker TickerConfig `json:"newsTicker"` // 新闻滚动条配置
  104. Poem PoemConfig `json:"poem"` // 随机古诗显示配置
  105. MouseAlert MouseAlertConfig `json:"mouseAlert"` // 系统级鼠标提醒气泡配置
  106. BrowserPath string `json:"browserPath"` // 指定浏览器 exe 路径(空=系统默认浏览器)
  107. LEDColor string `json:"ledColor"` // LED 滚动文字点阵颜色(如 #ff4d4d)
  108. StockUpColor string `json:"stockUpColor"` // 股票上涨颜色
  109. StockDownColor string `json:"stockDownColor"` // 股票下跌颜色
  110. StockRefreshSecs int `json:"stockRefreshSecs"` // 股市自动刷新间隔(秒)
  111. StockClosedRefreshSecs int `json:"stockClosedRefreshSecs"` // 股市休市时自动刷新间隔(秒)
  112. NewsRefreshMinutes int `json:"newsRefreshMinutes"` // 新闻自动刷新间隔(分钟)
  113. NewsCount int `json:"newsCount"` // 新闻页显示条数
  114. GithubRefreshMinutes int `json:"githubRefreshMinutes"` // Github 自动刷新间隔(分钟)
  115. FontDelta int `json:"fontDelta"` // 全局字号偏移(px),+-3 范围内调整
  116. LocalSend LocalSendConfig `json:"localSend"` // LocalSend 文件互传配置(本机名/接收目录/接收模式等)
  117. Croc CrocConfig `json:"croc"` // Croc 传送配置(发送/接收共用密码)
  118. YTDLP YTDLPConfig `json:"ytdlp"` // 视频下载配置(yt-dlp 保存目录)
  119. Memos MemosConfig `json:"memos"` // Memos 速记配置(地址/访问令牌/默认可见性)
  120. ToolOrder []string `json:"toolOrder"` // 工具页卡片显示顺序(工具 id 列表,未列出的追加到末尾)
  121. }
  122. // 当前已注册的热键(用于保存设置时先注销旧热键)
  123. var (
  124. hotkeyMu sync.Mutex
  125. activeHotkey string
  126. )
  127. // defaultSettings 返回默认配置:开机启动默认开启,热键与页面地址使用内置值
  128. func defaultSettings() AppSettings {
  129. return AppSettings{
  130. Theme: "dark",
  131. Autostart: true,
  132. AlwaysOnTop: false,
  133. Hotkey: DefaultHotkey,
  134. TabOrder: []string{"links", "news", "github", "stock", "todo", "record", "tools", "host", "settings"}, // 默认 Tab 顺序(控制/硬件等固定网页已改为自定义 Tab)
  135. ToolOrder: DefaultToolOrder(), // 工具页卡片默认显示顺序
  136. StockCodes: []string{"sh000001", "sh600519", "sz000001"},
  137. Links: []LinkItem{
  138. {Name: "煎蛋", URL: "https://jandan.net/"},
  139. {Name: "头条", URL: "https://www.toutiao.com/"},
  140. {Name: "GitHub", URL: "https://github.com/"},
  141. },
  142. NewsKeywords: "",
  143. GithubRepos: []RepoItem{
  144. {URL: "https://github.com/wailsapp/wails", Desc: "Wails:Go + Web 桌面应用框架"},
  145. },
  146. APIAddr: DefaultAPIAddr,
  147. APIAutoStart: true, // 内置 API 服务随应用启动(默认开启)
  148. UploadPassword: "", // 上传密码:默认空(不启用上传/下载)
  149. UploadDir: "", // 上传保存目录:默认空(回退到用户配置目录/uploads)
  150. UploadMaxSize: 512, // 上传大小上限:默认 512MB
  151. TodoPath: "", // 待办路径:无默认值,用户须在设置中配置(空值时强制进入设置)
  152. RecordPath: "", // 记录路径:无默认值,用户须在设置中配置(空值时强制进入设置)
  153. RecordDBPath: "", // 记录数据库路径:无默认值,用户须在设置中配置(空值时强制进入设置)
  154. BackupDir: "", // 备份目录:无默认值,用户须在设置中配置(空值时强制进入设置)
  155. StockTicker: TickerConfig{Enabled: false, Width: 1400, FontSize: 14, Color: "#ff4d4d", Speed: 80, Transparent: true, X: -1, Y: -1},
  156. NewsTicker: TickerConfig{Enabled: false, Width: 1400, FontSize: 14, Color: "#3ddc84", Speed: 60, Transparent: true, X: -1, Y: -1},
  157. Poem: PoemConfig{Enabled: false, Color: DefaultPoemColor, FontSize: DefaultPoemFontSize, TTS: false, Interval: DefaultPoemInterval, Speed: DefaultPoemSpeed},
  158. MouseAlert: MouseAlertConfig{FontSize: DefaultMouseAlertFontSize, Width: DefaultMouseAlertWidth, Speed: DefaultMouseAlertSpeed, Color: DefaultMouseAlertColor, Duration: DefaultMouseAlertDuration, OutlineColor: DefaultMouseAlertOutlineColor, OutlineWidth: DefaultMouseAlertOutlineWidth},
  159. BrowserPath: "", // 指定浏览器路径:默认空(使用系统默认浏览器)
  160. LEDColor: "#ff4d4d", // LED 滚动文字点阵颜色:默认红色
  161. StockUpColor: DefaultStockUpColor,
  162. StockDownColor: DefaultStockDownColor,
  163. StockRefreshSecs: DefaultStockRefreshSecs,
  164. StockClosedRefreshSecs: DefaultStockClosedRefreshSecs,
  165. NewsRefreshMinutes: DefaultNewsRefreshMinutes,
  166. NewsCount: DefaultNewsCount,
  167. GithubRefreshMinutes: DefaultGithubRefreshMinutes,
  168. FontDelta: 0, // 全局字号偏移默认 0
  169. Memos: MemosConfig{
  170. URL: "https://m.scwy.net", // 默认 Memos 实例
  171. },
  172. }
  173. }
  174. // DefaultToolOrder 返回工具页卡片默认显示顺序(与 index.html 中 data-tool 属性一一对应)。
  175. // 新增工具时需在此追加 id,并在 normalizeSettings 中自动归一化(未列出的追加到末尾)。
  176. func DefaultToolOrder() []string {
  177. return []string{
  178. "websvc", // Web 服务
  179. "translate", // 翻译
  180. "qrcode", // 二维码生成
  181. "ntfy", // 发送 ntfy 消息
  182. "chat", // ntfy 聊天室
  183. "poem", // 古诗词
  184. "screenshot", // 屏幕截图
  185. "shutdown", // 延时关机
  186. "md5", // MD5 加密
  187. "sha256", // SHA256 编码
  188. "led", // LED 滚动文字
  189. "reminder", // 定时提醒
  190. "localsend", // LocalSend 互传
  191. "croc", // Croc 传送(PATH 中存在 croc 时才展示)
  192. "ytdlp", // 视频下载(PATH 中存在 yt-dlp 时才展示)
  193. "memos", // Memos 速记
  194. }
  195. }
  196. // settingsFilePath 返回配置文件路径:<用户配置目录>/TengTuToolbox/settings.json
  197. func settingsFilePath() (string, error) {
  198. dir, err := os.UserConfigDir() // 例如 C:\Users\<用户名>\AppData\Roaming
  199. if err != nil {
  200. return "", err
  201. }
  202. return filepath.Join(dir, "TengTuToolbox", "settings.json"), nil
  203. }
  204. // loadSettings 读取配置文件;文件不存在或损坏时回退到默认配置
  205. func loadSettings() AppSettings {
  206. defaults := defaultSettings()
  207. path, err := settingsFilePath()
  208. if err != nil {
  209. return defaults
  210. }
  211. data, err := os.ReadFile(path)
  212. if err != nil {
  213. return defaults // 首次运行:无配置文件,使用默认值
  214. }
  215. settings := defaults
  216. if err := json.Unmarshal(data, &settings); err != nil {
  217. return defaults // 文件损坏:回退默认值,避免启动失败
  218. }
  219. return normalizeSettings(settings)
  220. }
  221. // saveSettings 将配置写入磁盘(目录不存在时自动创建)
  222. func saveSettings(settings AppSettings) error {
  223. path, err := settingsFilePath()
  224. if err != nil {
  225. return err
  226. }
  227. if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
  228. return err
  229. }
  230. data, err := json.MarshalIndent(settings, "", " ") // 缩进保存,便于手动查看
  231. if err != nil {
  232. return err
  233. }
  234. return os.WriteFile(path, data, 0o644)
  235. }
  236. // normalizeSettings 校正配置字段:空值回填默认、主题取值限定、股票代码规范化
  237. func normalizeSettings(settings AppSettings) AppSettings {
  238. if settings.Theme != "light" {
  239. settings.Theme = "dark" // 只允许 dark / light
  240. }
  241. if strings.TrimSpace(settings.Hotkey) == "" {
  242. settings.Hotkey = DefaultHotkey
  243. }
  244. // 先清洗自定义 Tab(生成 custom-N id 白名单供下方 Tab 顺序校验使用)
  245. tabs := make([]CustomTab, 0, len(settings.CustomTabs))
  246. for _, tab := range settings.CustomTabs {
  247. tab.Name = strings.TrimSpace(tab.Name)
  248. tab.URL = strings.TrimSpace(tab.URL)
  249. tab.Desc = strings.TrimSpace(tab.Desc) // 备注可空
  250. if tab.Name != "" && tab.URL != "" { // 名字与网址都非空才保留
  251. tabs = append(tabs, tab)
  252. }
  253. }
  254. settings.CustomTabs = tabs
  255. // Tab 顺序:只保留已存在的 tabId(内置 + 自定义 custom-N),去重;未列出的追加到末尾,保证全部 Tab 都能显示
  256. validTabs := map[string]bool{"links": true,
  257. "news": true, "github": true, "stock": true, "todo": true, "record": true, "tools": true, "host": true, "settings": true}
  258. for i := range settings.CustomTabs {
  259. validTabs[fmt.Sprintf("custom-%d", i)] = true // 自定义 Tab id:custom-0, custom-1...
  260. }
  261. seen := map[string]bool{}
  262. ordered := make([]string, 0, len(validTabs))
  263. for _, tab := range settings.TabOrder {
  264. if validTabs[tab] && !seen[tab] {
  265. seen[tab] = true
  266. ordered = append(ordered, tab)
  267. }
  268. }
  269. for _, tab := range []string{"links", "news", "github", "stock", "todo", "record", "tools", "host", "settings"} {
  270. if !seen[tab] {
  271. ordered = append(ordered, tab) // 设置中未列出的内置 Tab 追加到末尾
  272. }
  273. }
  274. for i := range settings.CustomTabs {
  275. id := fmt.Sprintf("custom-%d", i)
  276. if !seen[id] {
  277. ordered = append(ordered, id) // 设置中未列出的自定义 Tab 追加到末尾
  278. }
  279. }
  280. settings.TabOrder = ordered
  281. // 工具顺序:只保留已存在的工具 id,去重;未列出的追加到末尾,保证全部工具都能显示
  282. validTools := map[string]bool{}
  283. for _, id := range DefaultToolOrder() {
  284. validTools[id] = true
  285. }
  286. seenTools := map[string]bool{}
  287. toolOrder := make([]string, 0, len(validTools))
  288. for _, id := range settings.ToolOrder {
  289. if validTools[id] && !seenTools[id] {
  290. seenTools[id] = true
  291. toolOrder = append(toolOrder, id)
  292. }
  293. }
  294. for _, id := range DefaultToolOrder() {
  295. if !seenTools[id] {
  296. toolOrder = append(toolOrder, id) // 设置中未列出的工具追加到末尾
  297. }
  298. }
  299. settings.ToolOrder = toolOrder
  300. if strings.TrimSpace(settings.APIAddr) == "" {
  301. settings.APIAddr = DefaultAPIAddr
  302. }
  303. settings.UploadPassword = strings.TrimSpace(settings.UploadPassword) // 上传密码:空表示不启用
  304. settings.UploadDir = strings.TrimSpace(settings.UploadDir) // 上传目录:空回退到默认目录
  305. // TodoPath / RecordPath / RecordDBPath / BackupDir 无默认值:空值保留,前端据此强制用户进入设置配置(不做硬编码回填)
  306. settings.StockTicker = normalizeTicker(settings.StockTicker, "#ff4d4d", 80) // 股票条默认红字
  307. settings.NewsTicker = normalizeTicker(settings.NewsTicker, "#3ddc84", 60) // 新闻条默认绿字
  308. settings.Poem = normalizePoemConfig(settings.Poem) // 古诗显示配置校正
  309. settings.MouseAlert = normalizeMouseAlert(settings.MouseAlert) // 鼠标提醒配置校正
  310. if !isHexColor(settings.StockUpColor) {
  311. settings.StockUpColor = DefaultStockUpColor // 涨色回填默认红
  312. }
  313. if !isHexColor(settings.StockDownColor) {
  314. settings.StockDownColor = DefaultStockDownColor // 跌色回填默认蓝
  315. }
  316. if settings.StockRefreshSecs < 5 || settings.StockRefreshSecs > 3600 {
  317. settings.StockRefreshSecs = DefaultStockRefreshSecs // 股市刷新间隔限定 5 秒 ~ 1 小时
  318. }
  319. if settings.StockClosedRefreshSecs < 60 || settings.StockClosedRefreshSecs > 86400 {
  320. settings.StockClosedRefreshSecs = DefaultStockClosedRefreshSecs // 休市刷新间隔限定 1 分钟 ~ 1 天
  321. }
  322. if settings.NewsRefreshMinutes < 5 || settings.NewsRefreshMinutes > 1440 {
  323. settings.NewsRefreshMinutes = DefaultNewsRefreshMinutes // 新闻刷新间隔限定 5 分钟 ~ 1 天
  324. }
  325. if settings.NewsCount < 5 || settings.NewsCount > 200 {
  326. settings.NewsCount = DefaultNewsCount // 新闻显示条数限定 5 ~ 200 条
  327. }
  328. if settings.GithubRefreshMinutes < 5 || settings.GithubRefreshMinutes > 1440 {
  329. settings.GithubRefreshMinutes = DefaultGithubRefreshMinutes // Github 刷新间隔限定 5 分钟 ~ 1 天
  330. }
  331. if settings.FontDelta < -3 || settings.FontDelta > 3 {
  332. settings.FontDelta = 0 // 全局字号偏移限定 +-3 px
  333. }
  334. // LocalSend 配置校正:本机名未设置时随机生成、端口兜底、接收目录默认
  335. settings.LocalSend = normalizeLocalSend(settings.LocalSend)
  336. // Croc 配置校正:密码与接收目录均去首尾空格
  337. settings.Croc.Password = strings.TrimSpace(settings.Croc.Password)
  338. settings.Croc.Dir = strings.TrimSpace(settings.Croc.Dir)
  339. // 视频下载目录去首尾空格
  340. settings.YTDLP.Dir = strings.TrimSpace(settings.YTDLP.Dir)
  341. // mp3 转换质量:仅允许 0-9(VBR)或常见码率,非法值回退到最佳
  342. settings.YTDLP.Mp3Quality = strings.TrimSpace(settings.YTDLP.Mp3Quality)
  343. if !validMp3Quality(settings.YTDLP.Mp3Quality) {
  344. settings.YTDLP.Mp3Quality = "0"
  345. }
  346. // Memos 配置校正:地址去尾斜杠、令牌去空格
  347. settings.Memos.URL = strings.TrimRight(strings.TrimSpace(settings.Memos.URL), "/")
  348. settings.Memos.Token = strings.TrimSpace(settings.Memos.Token)
  349. if settings.StockCodes == nil {
  350. settings.StockCodes = defaultSettings().StockCodes // 未设置时用默认自选股
  351. }
  352. if settings.Links == nil {
  353. settings.Links = defaultSettings().Links // 未设置时用默认网址
  354. }
  355. if settings.GithubRepos == nil {
  356. settings.GithubRepos = defaultSettings().GithubRepos // 未设置时用默认仓库
  357. }
  358. links := make([]LinkItem, 0, len(settings.Links))
  359. for _, link := range settings.Links {
  360. link.Name = strings.TrimSpace(link.Name)
  361. link.URL = strings.TrimSpace(link.URL)
  362. if link.Name != "" && link.URL != "" {
  363. links = append(links, link)
  364. }
  365. }
  366. settings.Links = links
  367. repos := make([]RepoItem, 0, len(settings.GithubRepos))
  368. for _, repo := range settings.GithubRepos {
  369. repo.URL = strings.TrimSpace(repo.URL)
  370. repo.Desc = strings.TrimSpace(repo.Desc)
  371. repo.Version = strings.TrimSpace(repo.Version) // 本地版本,可为空
  372. if repo.Version == "" {
  373. repo.Version = AppVersion // 未填版本时自动用应用自身版本,用于分析并提示新版本
  374. }
  375. if repo.URL != "" {
  376. repos = append(repos, repo)
  377. }
  378. }
  379. settings.GithubRepos = repos
  380. codes := make([]string, 0, len(settings.StockCodes))
  381. for _, raw := range settings.StockCodes {
  382. code := normalizeStockCode(raw) // 补全 sh / sz / bj 前缀
  383. if code != "" {
  384. codes = append(codes, code)
  385. }
  386. }
  387. settings.StockCodes = codes
  388. return settings
  389. }
  390. // normalizeTicker 校正滚动条配置:宽度、字号、速度下限,颜色默认值
  391. func normalizeTicker(cfg TickerConfig, defaultColor string, defaultSpeed int) TickerConfig {
  392. if cfg.Width < 200 || cfg.Width > 6000 {
  393. cfg.Width = 0 // 宽度限定在 200~6000px,0 表示用默认 1400
  394. }
  395. if cfg.FontSize < 10 || cfg.FontSize > 40 {
  396. cfg.FontSize = 14 // 字号限定在 10~40px
  397. }
  398. if strings.TrimSpace(cfg.Color) == "" {
  399. cfg.Color = defaultColor
  400. }
  401. if cfg.Speed < 20 || cfg.Speed > 400 {
  402. cfg.Speed = defaultSpeed // 速度限定在 20~400 px/s
  403. }
  404. return cfg
  405. }
  406. // normalizeMouseAlert 校正鼠标提醒配置:字号、宽度、速度、显示时长下限,颜色默认值
  407. func normalizeMouseAlert(cfg MouseAlertConfig) MouseAlertConfig {
  408. if cfg.FontSize < 10 || cfg.FontSize > 48 {
  409. cfg.FontSize = DefaultMouseAlertFontSize // 字号限定 10~48px
  410. }
  411. if cfg.Width < 120 || cfg.Width > 1200 {
  412. cfg.Width = DefaultMouseAlertWidth // 宽度限定 120~1200px
  413. }
  414. if cfg.Speed < 5 || cfg.Speed > 300 {
  415. cfg.Speed = DefaultMouseAlertSpeed // 滚动速度限定 5~300 px/s
  416. }
  417. if strings.TrimSpace(cfg.Color) == "" {
  418. cfg.Color = DefaultMouseAlertColor
  419. }
  420. if strings.TrimSpace(cfg.OutlineColor) == "" {
  421. cfg.OutlineColor = DefaultMouseAlertOutlineColor
  422. }
  423. if cfg.OutlineWidth < 0 || cfg.OutlineWidth > 6 {
  424. cfg.OutlineWidth = DefaultMouseAlertOutlineWidth // 描边宽度限定 0~6 px,0 关闭
  425. }
  426. if cfg.Duration < 2 || cfg.Duration > 60 {
  427. cfg.Duration = DefaultMouseAlertDuration // 显示时长限定 2~60 秒
  428. }
  429. return cfg
  430. }
  431. // normalizeLocalSend 校正 LocalSend 配置:本机名未设置时随机生成、端口兜底、接收目录默认
  432. func normalizeLocalSend(cfg LocalSendConfig) LocalSendConfig {
  433. if strings.TrimSpace(cfg.Alias) == "" {
  434. cfg.Alias = defaultLocalSendAlias() // 本机名未设置:随机生成
  435. }
  436. if cfg.Port <= 0 {
  437. cfg.Port = 53317 // 端口兜底:LocalSend 默认端口
  438. }
  439. if strings.TrimSpace(cfg.ReceiveDir) == "" {
  440. cfg.ReceiveDir = defaultLocalSendReceiveDir() // 接收目录:默认到用户配置目录/LocalSend 接收
  441. }
  442. return cfg
  443. }
  444. // defaultLocalSendAlias 随机生成本机名,如「工具箱-3F8A」
  445. func defaultLocalSendAlias() string {
  446. b := make([]byte, 2)
  447. _, _ = rand.Read(b)
  448. return fmt.Sprintf("工具箱-%s", strings.ToUpper(hex.EncodeToString(b)))
  449. }
  450. // defaultLocalSendReceiveDir 返回默认接收目录:<用户配置目录>/TengTuToolbox/LocalSend接收
  451. func defaultLocalSendReceiveDir() string {
  452. dir, err := os.UserConfigDir()
  453. if err != nil {
  454. return "LocalSend接收"
  455. }
  456. return filepath.Join(dir, "TengTuToolbox", "LocalSend接收")
  457. }
  458. // isHexColor 判断是否为 #RGB / #RRGGBB 形式的颜色值
  459. func isHexColor(value string) bool {
  460. value = strings.TrimSpace(value)
  461. if !strings.HasPrefix(value, "#") {
  462. return false
  463. }
  464. return len(value) == 4 || len(value) == 7
  465. }
  466. // mergeTickerPositions 保留磁盘上已保存的滚动条位置:
  467. // 设置表单不提交位置,保存其他设置时不能覆盖拖动后记录的最新坐标
  468. func mergeTickerPositions(settings AppSettings) AppSettings {
  469. prev := loadSettings()
  470. settings.StockTicker.X, settings.StockTicker.Y = prev.StockTicker.X, prev.StockTicker.Y
  471. settings.NewsTicker.X, settings.NewsTicker.Y = prev.NewsTicker.X, prev.NewsTicker.Y
  472. return settings
  473. }
  474. // applySettings 把配置应用到运行时:重新注册热键、设置窗口置顶、同步开机启动
  475. func applySettings(settings AppSettings) {
  476. hotkeyMu.Lock()
  477. if settings.Hotkey != activeHotkey {
  478. if activeHotkey != "" {
  479. _ = app.GlobalShortcut.Unregister(activeHotkey) // 注销旧热键,避免冲突
  480. }
  481. if err := app.GlobalShortcut.Register(settings.Hotkey, showMainWindow); err != nil {
  482. log.Printf("register hotkey %q failed: %v", settings.Hotkey, err)
  483. } else {
  484. activeHotkey = settings.Hotkey // 记录当前生效的热键
  485. }
  486. }
  487. hotkeyMu.Unlock()
  488. if mainWindow != nil {
  489. mainWindow.SetAlwaysOnTop(settings.AlwaysOnTop) // 窗口置顶开关
  490. }
  491. if settings.Autostart {
  492. _ = app.Autostart.Enable() // 写入系统开机启动项
  493. } else {
  494. _ = app.Autostart.Disable() // 移除开机启动项
  495. }
  496. // 内置 API 服务:按「服务自动启动」开关决定启动或停止(地址变化时重启)
  497. if settings.APIAutoStart {
  498. ensureAPIServer(settings.APIAddr)
  499. } else {
  500. StopAPIServer()
  501. }
  502. syncTickerWindows(settings) // 按配置创建 / 关闭 / 刷新滚动条窗口
  503. }