package main import ( "errors" "strconv" "time" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/events" ) // TrayProjectItem 是自定义托盘弹层里的项目快捷项。 type TrayProjectItem struct { ID int64 `json:"id"` Name string `json:"name"` } // TrayMenuState 是自定义托盘弹层的完整快照。 type TrayMenuState struct { Style string `json:"style"` Locale string `json:"locale"` Theme string `json:"theme"` Status string `json:"status"` StatusLabel string `json:"statusLabel"` Username string `json:"username"` LoggedIn bool `json:"loggedIn"` Unread int `json:"unread"` TodayDue int `json:"todayDue"` Pending int `json:"pending"` Autostart bool `json:"autostart"` Projects []TrayProjectItem `json:"projects"` HasMoreProjects bool `json:"hasMoreProjects"` } func (a *App) trayMenuStyle() string { if a.store == nil { return "native" } st, e := a.store.Settings() if e != nil || !validTrayMenuStyle(st.TrayMenuStyle) { return "native" } return normalizeTrayMenuStyle(st.TrayMenuStyle) } func (a *App) scheduleTrayPopup() { if a.shell == nil || a.trayMenuStyle() == "native" || a.shell.popup != nil { return } if !a.shell.popupScheduled.CompareAndSwap(false, true) { return } go func() { time.Sleep(1500 * time.Millisecond) application.InvokeAsync(func() { if a.shell == nil || a.trayMenuStyle() == "native" || a.shell.popup != nil { return } a.createTrayPopup() }) }() } func (a *App) createTrayPopup() { if a.shell == nil || a.shell.wapp == nil || a.shell.tray == nil || a.shell.popup != nil { return } popup := a.shell.wapp.Window.NewWithOptions(application.WebviewWindowOptions{ Name: "tray-popup", Title: "Tray Menu", Width: 300, Height: 348, URL: "/tray.html", Frameless: true, AlwaysOnTop: true, Hidden: true, HideOnFocusLost: false, HideOnEscape: true, DisableResize: true, DefaultContextMenuDisabled: true, BackgroundColour: application.NewRGBA(13, 18, 28, 255), Windows: application.WindowsWindow{ DisableMenu: true, HiddenOnTaskbar: true, }, }) popup.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { popup.Hide() e.Cancel() }) popup.OnWindowEvent(events.Common.WindowLostFocus, func(*application.WindowEvent) { if a.shell == nil { return } shown := a.shell.popupShownAt.Load() if shown == 0 || time.Since(time.Unix(0, shown)) < 600*time.Millisecond { return } a.hideTrayPopup() }) a.shell.popup = popup } func (a *App) showTrayPopup() { if a.shell == nil || a.shell.popup == nil || !a.shell.popupReady.Load() { return } popup := a.shell.popup if popup.IsVisible() { popup.Hide() return } if a.shell.tray != nil { _ = a.shell.tray.PositionWindow(popup, 8) } a.shell.popupShownAt.Store(time.Now().UnixNano()) popup.Show() activateTrayPopup(popup) a.emit("tray:shown", nil) } func (a *App) hideTrayPopup() { if a.shell == nil || a.shell.popup == nil { return } a.shell.popup.Hide() } // HideTrayPopup 供弹层在失焦时自行关闭。 func (a *App) HideTrayPopup() { a.hideTrayPopup() } // TrayPopupReady 轻量托盘页加载完成后调用;此前右键一律走原生菜单。 func (a *App) TrayPopupReady() { if a.shell != nil { a.shell.popupReady.Store(true) } } // FitTrayPopup 保留给旧绑定;不再改窗口尺寸,避免 WebView2 在 Show 后立刻 SetSize 闪退。 func (a *App) FitTrayPopup(width, height int) { _, _ = width, height } // GetTrayMenuState 返回自定义托盘弹层所需的状态与项目列表。 func (a *App) GetTrayMenuState() (TrayMenuState, error) { out := TrayMenuState{Style: "native", Locale: "zh-CN", Theme: "dark", Projects: []TrayProjectItem{}} if e := a.ready(); e != nil { return out, e } st, e := a.store.Settings() if e != nil { return out, e } out.Style = a.trayMenuStyle() out.Locale = st.Locale out.Theme = st.Theme ts := a.collectTrayStatus() out.Status = trayStatusKey(ts) out.StatusLabel = trayStatusLabel(shellTexts(st.Locale), ts) out.Username = ts.username out.LoggedIn = ts.loggedIn out.Unread = ts.unread out.TodayDue = ts.todayDue out.Pending = ts.pending if auto, err := a.GetAutostart(); err == nil { out.Autostart = auto } if projects, err := a.ListProjects(); err == nil { for i, p := range projects { if i >= trayProjectLimit { out.HasMoreProjects = true break } out.Projects = append(out.Projects, TrayProjectItem{ID: p.ID, Name: p.Name}) } } return out, nil } func trayStatusKey(st trayStatus) string { switch { case st.syncing: return "syncing" case !st.loggedIn: return "signedOut" case st.lastErr != "": return "error" case st.online: return "online" default: return "offline" } } // TrayMenuAction 处理自定义托盘弹层的点击。autostart 保持弹层打开以便看到勾选变化。 func (a *App) TrayMenuAction(action, arg string) error { if e := a.ready(); e != nil { return e } if action != "autostart" { a.hideTrayPopup() } switch action { case "show": a.showMainWindow() case "nav": if arg == "" { return errors.New("TRAY_NAV_REQUIRED") } a.navigateTo(arg) case "sync": a.trayQuickSync() case "batch": _, _ = a.StartBatchAnalysis() case "autostart": cur, err := a.GetAutostart() if err != nil { return err } if e := a.SetAutostart(!cur); e != nil { return e } a.emit("tray:refresh", nil) case "restart": a.RestartApp() case "quit": a.QuitApp() case "project": id, err := strconv.ParseInt(arg, 10, 64) if err != nil || id <= 0 { return errors.New("TRAY_PROJECT_INVALID") } a.navigateTo("/project/" + strconv.FormatInt(id, 10)) default: return errors.New("UNKNOWN_TRAY_ACTION") } return nil }