Files
code-utils/tray_status.go

140 lines
3.4 KiB
Go
Raw Normal View History

2026-08-14 17:54:06 +08:00
package main
import (
"bytes"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"strings"
)
// trayStatus 汇总托盘展示所需的运行态。
type trayStatus struct {
loggedIn bool
online bool
syncing bool
pending int
unread int
todayDue int
lastErr string
username string
}
func (a *App) collectTrayStatus() trayStatus {
st := trayStatus{}
if a.store == nil {
return st
}
st.loggedIn = a.syncUserID() > 0
st.online = a.syncOnline.Load()
st.syncing = a.syncing.Load()
st.username = a.store.Meta("sync_username")
a.syncMu.Lock()
st.lastErr = a.syncLastErr
a.syncMu.Unlock()
_ = a.store.db.QueryRow(`SELECT (SELECT COUNT(*) FROM todos WHERE dirty=1)
+(SELECT COUNT(*) FROM tickets WHERE dirty=1)
+(SELECT COUNT(*) FROM notes WHERE dirty=1)`).Scan(&st.pending)
_ = a.store.db.QueryRow(`SELECT COUNT(*) FROM messages WHERE is_read=0`).Scan(&st.unread)
if badges, e := a.GetNavBadges(); e == nil {
st.todayDue = badges.TodayDue
}
return st
}
func (s trayStatus) tooltip(locale string) string {
base := "年糕崽崽项目管理PMS"
if locale == "en" {
base = "Niangaozai PMS"
}
parts := []string{base}
switch {
case s.syncing:
parts = append(parts, pick(locale, "同步中…", "Syncing…"))
case !s.loggedIn:
parts = append(parts, pick(locale, "未登录", "Signed out"))
case s.lastErr != "":
parts = append(parts, pick(locale, "同步异常", "Sync error"))
case s.online:
parts = append(parts, pick(locale, "在线", "Online"))
default:
parts = append(parts, pick(locale, "离线", "Offline"))
}
if s.pending > 0 {
parts = append(parts, fmt.Sprintf(pick(locale, "待推送 %d", "Pending %d"), s.pending))
}
if s.unread > 0 {
parts = append(parts, fmt.Sprintf(pick(locale, "未读 %d", "Unread %d"), s.unread))
}
if s.todayDue > 0 {
parts = append(parts, fmt.Sprintf(pick(locale, "今日到期 %d", "Due today %d"), s.todayDue))
}
return strings.Join(parts, " · ")
}
func (s trayStatus) badgeColor() color.RGBA {
switch {
case s.syncing:
return color.RGBA{R: 79, G: 157, B: 245, A: 255} // blue
case s.lastErr != "":
return color.RGBA{R: 240, G: 94, B: 104, A: 255} // red
case !s.loggedIn:
return color.RGBA{R: 146, G: 155, B: 172, A: 255} // muted
case !s.online:
return color.RGBA{R: 231, G: 189, B: 53, A: 255} // yellow
case s.pending > 0 || s.unread > 0:
return color.RGBA{R: 231, G: 189, B: 53, A: 255} // yellow attention
default:
return color.RGBA{R: 67, G: 201, B: 150, A: 255} // green
}
}
func pick(locale, zh, en string) string {
if locale == "en" {
return en
}
return zh
}
// trayIconWithBadge 在应用图标右下角叠一个状态色点,供托盘 SetIcon 使用。
func trayIconWithBadge(src []byte, badge color.RGBA) []byte {
img, e := png.Decode(bytes.NewReader(src))
if e != nil {
return src
}
b := img.Bounds()
out := image.NewRGBA(b)
draw.Draw(out, b, img, b.Min, draw.Src)
size := b.Dx()
if size < 8 {
return src
}
r := size / 5
if r < 3 {
r = 3
}
cx := b.Max.X - r - size/12
cy := b.Max.Y - r - size/12
for y := cy - r; y <= cy+r; y++ {
for x := cx - r; x <= cx+r; x++ {
dx, dy := x-cx, y-cy
if dx*dx+dy*dy > r*r {
continue
}
// 外圈深色描边提升对比度。
if dx*dx+dy*dy > (r-1)*(r-1) {
out.Set(x, y, color.RGBA{0, 0, 0, 180})
} else {
out.Set(x, y, badge)
}
}
}
var buf bytes.Buffer
if e := png.Encode(&buf, out); e != nil {
return src
}
return buf.Bytes()
}