Przeglądaj źródła

cmd/systray: improve profile menu

Bring UI closer to macOS and windows:
- split login and tailnet name over separate lines
- render profile picture (with very simple caching)
- use checkbox to indicate active profile. I've not found any desktops
  that can't render checkboxes, so I'd like to explore other options
  if needed.

Updates #1708

Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d
Signed-off-by: Will Norris <[email protected]>
Will Norris 1 rok temu
rodzic
commit
89adcd853d
1 zmienionych plików z 48 dodań i 7 usunięć
  1. 48 7
      cmd/systray/systray.go

+ 48 - 7
cmd/systray/systray.go

@@ -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.