239 lines
6.1 KiB
Go
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
|
|
}
|