| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- // ============================================================================
- // main.go — 应用入口
- //
- // 创建 Wails 应用、主窗口与系统托盘,注册服务与窗口事件;
- // 启动每秒时间广播与待办提醒调度协程,随后进入事件循环。
- // ============================================================================
- package main
- import (
- "embed" // Go 内嵌资源包:将前端静态文件打包进可执行文件
- "fmt" // 格式化 I/O:用于控制台打印调试信息
- "image/color"
- "log" // 日志:记录并输出错误
- "time" // 时间处理:用于每秒定时发送事件
- "github.com/wailsapp/wails/v3/pkg/application" // Wails3 核心:应用/窗口/菜单/对话框/事件/托盘
- "github.com/wailsapp/wails/v3/pkg/events" // Wails3 事件常量:窗口关闭、文件拖放等
- "github.com/wailsapp/wails/v3/pkg/services/dock"
- )
- // ============================================================================
- // 前端资源嵌入
- // ============================================================================
- // Wails 使用 Go 的 `embed` 包将前端文件嵌入到二进制文件中。
- // frontend/dist 目录下的所有文件都会被嵌入到可执行文件里,
- // 并由 Wails 内置的资源服务器(Asset Server)提供给 WebView 前端访问。
- //go:embed all:frontend/dist
- var assets embed.FS // assets 持有 frontend/dist 下全部前端文件的只读文件系统
- //go:embed frontend/public/wails.png
- var trayIcon []byte // 嵌入系统托盘图标(PNG 格式)
- // 全局运行时对象:服务、API、托盘菜单都要用到应用实例与主窗口
- var (
- app *application.App // 应用实例
- mainWindow *application.WebviewWindow // 主窗口
- // 窗口隐藏前的位置,恢复显示时还原
- savedX, savedY int
- hasSavedPos bool
- )
- // showMainWindow 统一显示入口:先还原位置,再显示并聚焦。
- // 托盘、热键、API 接口都通过它唤出窗口。
- func showMainWindow() {
- if mainWindow == nil {
- return
- }
- if hasSavedPos {
- mainWindow.SetPosition(savedX, savedY) // 还原隐藏前的窗口坐标
- }
- mainWindow.Show() // 显示窗口
- mainWindow.Focus() // 聚焦到前台
- }
- // init 在 main 函数之前自动执行,用于应用启动前的注册工作。
- func init() {
- application.RegisterEvent[string]("time") // 注册 string 类型的自定义事件,绑定生成器据此为前端生成强类型事件 API
- application.RegisterEvent[string]("apilog") // Gin 日志行实时推送(「主机」页订阅)
- application.RegisterEvent[string]("todoReminder") // 待办提醒:到点时向前端广播提醒内容
- application.RegisterEvent[string]("ntfyChat") // ntfy 聊天室:订阅主题后收到消息广播给前端
- }
- // main 是应用程序的入口,整体流程如下:
- // 1. 创建 Wails 应用实例(注册服务、配置资源服务器)
- // 2. 创建主窗口
- // 3. 应用已保存的配置(热键、置顶、开机启动、API 服务、滚动条窗口)
- // 4. 创建系统托盘(关闭时隐藏到托盘,双击恢复显示)
- // 5. 注册窗口事件(文件拖放、关闭→隐藏托盘、显示时还原位置)
- // 6. 启动后台协程,运行应用(阻塞直到退出)
- func main() {
- // 创建 dock service 实例,自定义 badge 选项
- dockService := dock.NewWithOptions(dock.BadgeOptions{
- TextColour: color.RGBA{255, 255, 255, 255}, // 白色文字
- BackgroundColour: color.RGBA{0, 0, 255, 255}, // 蓝色背景
- FontName: "consolab.ttf", // 粗体 Consolas 字体
- FontSize: 20, // 单字符字号
- SmallFontSize: 14, // 多字符字号
- })
- // ------------------------------------------------------------------------
- // 第 1 步:创建 Wails 应用实例
- // ------------------------------------------------------------------------
- app = application.New(application.Options{
- Name: "TengTu Toolbox",
- Description: "TengTu Toolbox for OS",
- Services: []application.Service{
- application.NewService(&GreetService{}), // 注册 GreetService,前端可通过 bindings 直接调用其 Greet 方法
- application.NewService(&SettingsService{}), // 注册 SettingsService,供设置页读写配置、拉取行情
- application.NewService(dockService), // 注册 dockService,前端可通过 bindings 直接调用其 SetBadge 方法
- },
- Assets: application.AssetOptions{
- Handler: application.AssetFileServerFS(assets), // 使用嵌入的前端文件作为资源服务器处理器
- },
- Mac: application.MacOptions{
- ApplicationShouldTerminateAfterLastWindowClosed: false, // 最后一个窗口关闭后不退出应用(托盘常驻)
- },
- })
- // ------------------------------------------------------------------------
- // 第 2 步:创建主窗口
- // ------------------------------------------------------------------------
- win := app.Window.NewWithOptions(application.WebviewWindowOptions{
- Title: "腾图工具箱", // 窗口标题
- Width: 1000, // 窗口宽度(与高度构成黄金比例 1000/618≈1.618)
- Height: 618, // 窗口高度
- Mac: application.MacWindow{
- InvisibleTitleBarHeight: 50, // 顶部 50px 为不可见但可拖拽的标题栏区域
- Backdrop: application.MacBackdropTranslucent, // 半透明背景材质
- TitleBar: application.MacTitleBarHiddenInset, // 隐藏标题栏但保留红绿灯按钮
- },
- Windows: application.WindowsWindow{
- DisableFramelessWindowDecorations: false, // 禁用无边框窗口装饰(标题栏、边框等)
- },
- BackgroundColour: application.NewRGB(6, 7, 15), // 窗口背景色(深色)
- URL: "/", // WebView 加载的页面路径("/" 即前端首页 index.html)
- DevToolsEnabled: false, // 关闭开发者工具(生产环境不开放)
- DefaultContextMenuDisabled: true, // 禁用默认上下文菜单,防止右键点击打开菜单
- UseApplicationMenu: true, // 启用应用菜单
- EnableFileDrop: true, // 启用文件拖拽,需在 HTML 中添加 data-file-drop-target 标签
- ContentProtectionEnabled: true, // 启用内容保护,防止截取窗口内容
- Permissions: map[application.PermissionType]application.Permission{
- application.PermissionMicrophone: application.PermissionAllow,
- application.PermissionNotifications: application.PermissionAllow,
- application.PermissionCamera: application.PermissionAllow,
- application.PermissionGeolocation: application.PermissionAllow,
- application.PermissionClipboardRead: application.PermissionAllow,
- },
- Frameless: true, // 无边框窗口
- BackgroundType: application.BackgroundTypeTransparent, // 透明背景
- })
- mainWindow = win // 暴露给热键、托盘、API 使用
- // ------------------------------------------------------------------------
- // 第 3 步:应用已保存的配置(热键、置顶、开机启动)
- // ------------------------------------------------------------------------
- settings := loadSettings() // 读取用户配置,首次运行返回默认值(开机启动默认开启)
- applySettings(settings) // 注册热键、设置置顶、同步开机启动
- // ------------------------------------------------------------------------
- // 第 4 步:创建系统托盘
- // ------------------------------------------------------------------------
- // 关闭窗口时隐藏到托盘;双击托盘图标恢复窗口显示
- // 托盘菜单提供「显示 / 退出」两个入口
- tray := app.SystemTray.New()
- tray.SetIcon(trayIcon) // 设置托盘图标(嵌入的 wails.png)
- tray.SetTooltip("腾图工具箱") // 设置鼠标悬停提示
- tray.AttachWindow(win) // 绑定窗口:点击托盘图标自动切换窗口显隐
- // 构建托盘右键菜单
- trayMenu := application.NewMenu()
- trayMenu.Add("显示主窗口").OnClick(func(_ *application.Context) {
- showMainWindow() // 显示窗口到隐藏前的位置并聚焦
- })
- trayMenu.Add("退出").OnClick(func(_ *application.Context) {
- app.Quit() // 彻底退出应用
- })
- tray.SetMenu(trayMenu) // 设置托盘右键菜单
- // ------------------------------------------------------------------------
- // 第 5 步:注册窗口事件处理
- // ------------------------------------------------------------------------
- // 监听「文件拖放完成」事件,打印拖放目标详情与所有被拖入的文件
- win.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) {
- files := event.Context().DroppedFiles() // 获取拖入的文件路径列表
- details := event.Context().DropTargetDetails() // 获取拖放目标位置等详情
- fmt.Println("Drop target details:", details)
- // 遍历并打印每个被拖入的文件
- for _, file := range files {
- fmt.Println("Dropped:", file)
- }
- })
- // 注册「窗口关闭前」勾子:记录位置后取消关闭,改为隐藏到托盘
- win.RegisterHook(events.Common.WindowClosing, func(event *application.WindowEvent) {
- savedX, savedY = win.Position() // 记录隐藏前的窗口坐标
- hasSavedPos = true // 标记位置已记录
- event.Cancel() // 拦截关闭事件,阻止窗口被系统销毁
- win.Hide() // 隐藏窗口到托盘
- })
- // 窗口重新显示时(含托盘图标点击恢复),还原到隐藏前的位置
- win.OnWindowEvent(events.Common.WindowShow, func(_ *application.WindowEvent) {
- if hasSavedPos {
- win.SetPosition(savedX, savedY) // 还原窗口位置
- }
- })
- // 监听「窗口正在关闭」事件,执行清理逻辑
- win.OnWindowEvent(events.Common.WindowClosing, func(event *application.WindowEvent) {
- fmt.Printf("Window %s is closing\n", win.Name()) // 清理代码写在这里,此处仅打印关闭日志
- })
- // API 服务与滚动条窗口已在 applySettings(第 3 步)中按配置启动
- // 时间事件:footer 只需显示到分,故仅在分钟变化时广播,避免每秒唤醒前端
- lastMinute := ""
- go func() {
- for {
- now := time.Now().Format(time.DateTime) // 格式化为 DateTime 时间字符串
- minute := now[:len(now)-3] // 去掉 ":SS" 秒部分,取到分
- if minute != lastMinute { // 仅跨分钟时发送一次
- app.Event.Emit("time", now) // 向所有前端窗口广播 "time" 事件
- lastMinute = minute
- }
- time.Sleep(time.Second) // 每秒检查一次分钟边界
- }
- }()
- // 待办提醒调度:每 15 秒扫描一次待办文件,到点的提醒通过事件广播给前端弹窗。
- // 放在后台协程,即使窗口隐藏/最小化也能触发。
- go startTodoReminderLoop()
- // 运行应用(阻塞,直到退出)
- if err := app.Run(); err != nil {
- log.Fatal(err) // 运行过程中发生错误则记录日志并退出进程
- }
- }
|