140 lines
3.4 KiB
Go
140 lines
3.4 KiB
Go
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()
|
||
}
|