feat: 提交详情能看 diff,也能丢给 AI 审这一次改动。文件检查可单独排除路径,TODO 只认大写,避免把 todo 表和模块当成待办标记。
热力图按容器宽度铺满一年,不再横向滚动。托盘右键换成可换皮肤的弹层;更新下载显示进度,退出后再由脚本拉起安装器,避免还占着 exe。
This commit is contained in:
238
tray_menu.go
Normal file
238
tray_menu.go
Normal file
@@ -0,0 +1,238 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user