main.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. // ============================================================================
  2. // main.go — 应用入口
  3. //
  4. // 创建 Wails 应用、主窗口与系统托盘,注册服务与窗口事件;
  5. // 启动每秒时间广播与待办提醒调度协程,随后进入事件循环。
  6. // ============================================================================
  7. package main
  8. import (
  9. "embed" // Go 内嵌资源包:将前端静态文件打包进可执行文件
  10. "fmt" // 格式化 I/O:用于控制台打印调试信息
  11. "image/color"
  12. "log" // 日志:记录并输出错误
  13. "time" // 时间处理:用于每秒定时发送事件
  14. "github.com/wailsapp/wails/v3/pkg/application" // Wails3 核心:应用/窗口/菜单/对话框/事件/托盘
  15. "github.com/wailsapp/wails/v3/pkg/events" // Wails3 事件常量:窗口关闭、文件拖放等
  16. "github.com/wailsapp/wails/v3/pkg/services/dock"
  17. )
  18. // ============================================================================
  19. // 前端资源嵌入
  20. // ============================================================================
  21. // Wails 使用 Go 的 `embed` 包将前端文件嵌入到二进制文件中。
  22. // frontend/dist 目录下的所有文件都会被嵌入到可执行文件里,
  23. // 并由 Wails 内置的资源服务器(Asset Server)提供给 WebView 前端访问。
  24. //go:embed all:frontend/dist
  25. var assets embed.FS // assets 持有 frontend/dist 下全部前端文件的只读文件系统
  26. //go:embed frontend/public/wails.png
  27. var trayIcon []byte // 嵌入系统托盘图标(PNG 格式)
  28. // 全局运行时对象:服务、API、托盘菜单都要用到应用实例与主窗口
  29. var (
  30. app *application.App // 应用实例
  31. mainWindow *application.WebviewWindow // 主窗口
  32. // 窗口隐藏前的位置,恢复显示时还原
  33. savedX, savedY int
  34. hasSavedPos bool
  35. )
  36. // showMainWindow 统一显示入口:先还原位置,再显示并聚焦。
  37. // 托盘、热键、API 接口都通过它唤出窗口。
  38. func showMainWindow() {
  39. if mainWindow == nil {
  40. return
  41. }
  42. if hasSavedPos {
  43. mainWindow.SetPosition(savedX, savedY) // 还原隐藏前的窗口坐标
  44. }
  45. mainWindow.Show() // 显示窗口
  46. mainWindow.Focus() // 聚焦到前台
  47. }
  48. // init 在 main 函数之前自动执行,用于应用启动前的注册工作。
  49. func init() {
  50. application.RegisterEvent[string]("time") // 注册 string 类型的自定义事件,绑定生成器据此为前端生成强类型事件 API
  51. application.RegisterEvent[string]("apilog") // Gin 日志行实时推送(「主机」页订阅)
  52. application.RegisterEvent[string]("todoReminder") // 待办提醒:到点时向前端广播提醒内容
  53. application.RegisterEvent[string]("ntfyChat") // ntfy 聊天室:订阅主题后收到消息广播给前端
  54. }
  55. // main 是应用程序的入口,整体流程如下:
  56. // 1. 创建 Wails 应用实例(注册服务、配置资源服务器)
  57. // 2. 创建主窗口
  58. // 3. 应用已保存的配置(热键、置顶、开机启动、API 服务、滚动条窗口)
  59. // 4. 创建系统托盘(关闭时隐藏到托盘,双击恢复显示)
  60. // 5. 注册窗口事件(文件拖放、关闭→隐藏托盘、显示时还原位置)
  61. // 6. 启动后台协程,运行应用(阻塞直到退出)
  62. func main() {
  63. // 创建 dock service 实例,自定义 badge 选项
  64. dockService := dock.NewWithOptions(dock.BadgeOptions{
  65. TextColour: color.RGBA{255, 255, 255, 255}, // 白色文字
  66. BackgroundColour: color.RGBA{0, 0, 255, 255}, // 蓝色背景
  67. FontName: "consolab.ttf", // 粗体 Consolas 字体
  68. FontSize: 20, // 单字符字号
  69. SmallFontSize: 14, // 多字符字号
  70. })
  71. // ------------------------------------------------------------------------
  72. // 第 1 步:创建 Wails 应用实例
  73. // ------------------------------------------------------------------------
  74. app = application.New(application.Options{
  75. Name: "TengTu Toolbox",
  76. Description: "TengTu Toolbox for OS",
  77. Services: []application.Service{
  78. application.NewService(&GreetService{}), // 注册 GreetService,前端可通过 bindings 直接调用其 Greet 方法
  79. application.NewService(&SettingsService{}), // 注册 SettingsService,供设置页读写配置、拉取行情
  80. application.NewService(&LocalSendService{}), // 注册 LocalSendService,供 LocalSend 页控制发现/接收
  81. application.NewService(dockService), // 注册 dockService,前端可通过 bindings 直接调用其 SetBadge 方法
  82. },
  83. Assets: application.AssetOptions{
  84. Handler: application.AssetFileServerFS(assets), // 使用嵌入的前端文件作为资源服务器处理器
  85. },
  86. Mac: application.MacOptions{
  87. ApplicationShouldTerminateAfterLastWindowClosed: false, // 最后一个窗口关闭后不退出应用(托盘常驻)
  88. },
  89. })
  90. // ------------------------------------------------------------------------
  91. // 第 2 步:创建主窗口
  92. // ------------------------------------------------------------------------
  93. win := app.Window.NewWithOptions(application.WebviewWindowOptions{
  94. Title: "腾图工具箱", // 窗口标题
  95. Width: 1000, // 窗口宽度(与高度构成黄金比例 1000/618≈1.618)
  96. Height: 618, // 窗口高度
  97. Mac: application.MacWindow{
  98. InvisibleTitleBarHeight: 50, // 顶部 50px 为不可见但可拖拽的标题栏区域
  99. Backdrop: application.MacBackdropTranslucent, // 半透明背景材质
  100. TitleBar: application.MacTitleBarHiddenInset, // 隐藏标题栏但保留红绿灯按钮
  101. },
  102. Windows: application.WindowsWindow{
  103. DisableFramelessWindowDecorations: false, // 禁用无边框窗口装饰(标题栏、边框等)
  104. },
  105. BackgroundColour: application.NewRGB(6, 7, 15), // 窗口背景色(深色)
  106. URL: "/", // WebView 加载的页面路径("/" 即前端首页 index.html)
  107. DevToolsEnabled: false, // 关闭开发者工具(生产环境不开放)
  108. DefaultContextMenuDisabled: true, // 禁用默认上下文菜单,防止右键点击打开菜单
  109. UseApplicationMenu: true, // 启用应用菜单
  110. EnableFileDrop: true, // 启用文件拖拽,需在 HTML 中添加 data-file-drop-target 标签
  111. ContentProtectionEnabled: true, // 启用内容保护,防止截取窗口内容
  112. Permissions: map[application.PermissionType]application.Permission{
  113. application.PermissionMicrophone: application.PermissionAllow,
  114. application.PermissionNotifications: application.PermissionAllow,
  115. application.PermissionCamera: application.PermissionAllow,
  116. application.PermissionGeolocation: application.PermissionAllow,
  117. application.PermissionClipboardRead: application.PermissionAllow,
  118. },
  119. Frameless: true, // 无边框窗口
  120. BackgroundType: application.BackgroundTypeTransparent, // 透明背景
  121. })
  122. mainWindow = win // 暴露给热键、托盘、API 使用
  123. // ------------------------------------------------------------------------
  124. // 第 3 步:应用已保存的配置(热键、置顶、开机启动)
  125. // ------------------------------------------------------------------------
  126. settings := loadSettings() // 读取用户配置,首次运行返回默认值(开机启动默认开启)
  127. applySettings(settings) // 注册热键、设置置顶、同步开机启动
  128. // ------------------------------------------------------------------------
  129. // 第 4 步:创建系统托盘
  130. // ------------------------------------------------------------------------
  131. // 关闭窗口时隐藏到托盘;双击托盘图标恢复窗口显示
  132. // 托盘菜单提供「显示 / 退出」两个入口
  133. tray := app.SystemTray.New()
  134. tray.SetIcon(trayIcon) // 设置托盘图标(嵌入的 wails.png)
  135. tray.SetTooltip("腾图工具箱") // 设置鼠标悬停提示
  136. tray.AttachWindow(win) // 绑定窗口:点击托盘图标自动切换窗口显隐
  137. // 构建托盘右键菜单
  138. trayMenu := application.NewMenu()
  139. trayMenu.Add("显示主窗口").OnClick(func(_ *application.Context) {
  140. showMainWindow() // 显示窗口到隐藏前的位置并聚焦
  141. })
  142. trayMenu.Add("退出").OnClick(func(_ *application.Context) {
  143. app.Quit() // 彻底退出应用
  144. })
  145. tray.SetMenu(trayMenu) // 设置托盘右键菜单
  146. // ------------------------------------------------------------------------
  147. // 第 5 步:注册窗口事件处理
  148. // ------------------------------------------------------------------------
  149. // 监听「文件拖放完成」事件,打印拖放目标详情与所有被拖入的文件
  150. win.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) {
  151. files := event.Context().DroppedFiles() // 获取拖入的文件路径列表
  152. details := event.Context().DropTargetDetails() // 获取拖放目标位置等详情
  153. fmt.Println("Drop target details:", details)
  154. // 遍历并打印每个被拖入的文件
  155. for _, file := range files {
  156. fmt.Println("Dropped:", file)
  157. }
  158. })
  159. // 注册「窗口关闭前」勾子:记录位置后取消关闭,改为隐藏到托盘
  160. win.RegisterHook(events.Common.WindowClosing, func(event *application.WindowEvent) {
  161. savedX, savedY = win.Position() // 记录隐藏前的窗口坐标
  162. hasSavedPos = true // 标记位置已记录
  163. event.Cancel() // 拦截关闭事件,阻止窗口被系统销毁
  164. win.Hide() // 隐藏窗口到托盘
  165. })
  166. // 窗口重新显示时(含托盘图标点击恢复),还原到隐藏前的位置
  167. win.OnWindowEvent(events.Common.WindowShow, func(_ *application.WindowEvent) {
  168. if hasSavedPos {
  169. win.SetPosition(savedX, savedY) // 还原窗口位置
  170. }
  171. })
  172. // 监听「窗口正在关闭」事件,执行清理逻辑
  173. win.OnWindowEvent(events.Common.WindowClosing, func(event *application.WindowEvent) {
  174. fmt.Printf("Window %s is closing\n", win.Name()) // 清理代码写在这里,此处仅打印关闭日志
  175. })
  176. // API 服务与滚动条窗口已在 applySettings(第 3 步)中按配置启动
  177. // 时间事件:footer 只需显示到分,故仅在分钟变化时广播,避免每秒唤醒前端
  178. lastMinute := ""
  179. go func() {
  180. for {
  181. now := time.Now().Format(time.DateTime) // 格式化为 DateTime 时间字符串
  182. minute := now[:len(now)-3] // 去掉 ":SS" 秒部分,取到分
  183. if minute != lastMinute { // 仅跨分钟时发送一次
  184. app.Event.Emit("time", now) // 向所有前端窗口广播 "time" 事件
  185. lastMinute = minute
  186. }
  187. time.Sleep(time.Second) // 每秒检查一次分钟边界
  188. }
  189. }()
  190. // 待办提醒调度:每 15 秒扫描一次待办文件,到点的提醒通过事件广播给前端弹窗。
  191. // 放在后台协程,即使窗口隐藏/最小化也能触发。
  192. go startTodoReminderLoop()
  193. // 运行应用(阻塞,直到退出)
  194. if err := app.Run(); err != nil {
  195. log.Fatal(err) // 运行过程中发生错误则记录日志并退出进程
  196. }
  197. }