Files
code-utils/tray_menu.go
李琦 7c4109f687 feat: 提交详情能看 diff,也能丢给 AI 审这一次改动。文件检查可单独排除路径,TODO 只认大写,避免把 todo 表和模块当成待办标记。
热力图按容器宽度铺满一年,不再横向滚动。托盘右键换成可换皮肤的弹层;更新下载显示进度,退出后再由脚本拉起安装器,避免还占着 exe。
2026-08-19 15:46:03 +08:00

239 lines
6.1 KiB
Go

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
}