// ============================================================================ // 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 聊天室:订阅主题后收到消息广播给前端 application.RegisterEvent[int]("handleFlash") // 拖动手柄圆点闪烁:API 触发,前端播放颜色+大小动画 } // 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(&LocalSendService{}), // 注册 LocalSendService,供 LocalSend 页控制发现/接收 application.NewService(NewCrocService()), // 注册 CrocService,供 Croc 传送工具发送/接收文件 application.NewService(NewYTDLPService()), // 注册 YTDLPService,供视频下载工具调用 yt-dlp application.NewService(NewMemosService()), // 注册 MemosService,供 Memos 速记工具调用 REST API 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) // 运行过程中发生错误则记录日志并退出进程 } }