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() }