|
|
@@ -12,6 +12,7 @@ import (
|
|
|
"fmt"
|
|
|
"io"
|
|
|
"log"
|
|
|
+ "net/http"
|
|
|
"os"
|
|
|
"strings"
|
|
|
"sync"
|
|
|
@@ -118,10 +119,11 @@ func (menu *Menu) rebuild(state state) {
|
|
|
systray.AddSeparator()
|
|
|
|
|
|
account := "Account"
|
|
|
- if state.curProfile.Name != "" {
|
|
|
- account += fmt.Sprintf(" (%s)", state.curProfile.Name)
|
|
|
+ if pt := profileTitle(state.curProfile); pt != "" {
|
|
|
+ account = pt
|
|
|
}
|
|
|
accounts := systray.AddMenuItem(account, "")
|
|
|
+ setRemoteIcon(accounts, state.curProfile.UserProfile.ProfilePicURL)
|
|
|
// The dbus message about this menu item must propagate to the receiving
|
|
|
// end before we attach any submenu items. Otherwise the receiver may not
|
|
|
// yet record the parent menu item and error out.
|
|
|
@@ -132,13 +134,14 @@ func (menu *Menu) rebuild(state state) {
|
|
|
// Aggregate all clicks into a shared channel.
|
|
|
menu.accountsCh = make(chan ipn.ProfileID)
|
|
|
for _, profile := range state.allProfiles {
|
|
|
- title := fmt.Sprintf("%s (%s)", profile.Name, profile.NetworkProfile.DomainName)
|
|
|
- // Note: we could use AddSubMenuItemCheckbox instead of this formatting
|
|
|
- // hack, but checkboxes don't work across all desktops unfortunately.
|
|
|
+ title := profileTitle(profile)
|
|
|
+ var item *systray.MenuItem
|
|
|
if profile.ID == state.curProfile.ID {
|
|
|
- title = "* " + title
|
|
|
+ item = accounts.AddSubMenuItemCheckbox(title, "", true)
|
|
|
+ } else {
|
|
|
+ item = accounts.AddSubMenuItem(title, "")
|
|
|
}
|
|
|
- item := accounts.AddSubMenuItem(title, "")
|
|
|
+ setRemoteIcon(item, profile.UserProfile.ProfilePicURL)
|
|
|
go func(profile ipn.LoginProfile) {
|
|
|
for {
|
|
|
select {
|
|
|
@@ -170,6 +173,44 @@ func (menu *Menu) rebuild(state state) {
|
|
|
go menu.eventLoop(ctx)
|
|
|
}
|
|
|
|
|
|
+// profileTitle returns the title string for a profile menu item.
|
|
|
+func profileTitle(profile ipn.LoginProfile) string {
|
|
|
+ title := profile.Name
|
|
|
+ if profile.NetworkProfile.DomainName != "" {
|
|
|
+ title += "\n" + profile.NetworkProfile.DomainName
|
|
|
+ }
|
|
|
+ return title
|
|
|
+}
|
|
|
+
|
|
|
+var (
|
|
|
+ cacheMu sync.Mutex
|
|
|
+ httpCache = map[string][]byte{} // URL => response body
|
|
|
+)
|
|
|
+
|
|
|
+// setRemoteIcon sets the icon for menu to the specified remote image.
|
|
|
+// Remote images are fetched as needed and cached.
|
|
|
+func setRemoteIcon(menu *systray.MenuItem, urlStr string) {
|
|
|
+ if menu == nil || urlStr == "" {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ cacheMu.Lock()
|
|
|
+ b, ok := httpCache[urlStr]
|
|
|
+ if !ok {
|
|
|
+ resp, err := http.Get(urlStr)
|
|
|
+ if err == nil && resp.StatusCode == http.StatusOK {
|
|
|
+ b, _ = io.ReadAll(resp.Body)
|
|
|
+ httpCache[urlStr] = b
|
|
|
+ resp.Body.Close()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ cacheMu.Unlock()
|
|
|
+
|
|
|
+ if len(b) > 0 {
|
|
|
+ menu.SetIcon(b)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// eventLoop is the main event loop for handling click events on menu items
|
|
|
// and responding to Tailscale state changes.
|
|
|
// This method does not return until ctx.Done is closed.
|