mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-09 09:33:42 +00:00
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 <will@tailscale.com>
This commit is contained in:
parent
e8f1721147
commit
89adcd853d
@ -12,6 +12,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -118,10 +119,11 @@ func (menu *Menu) rebuild(state state) {
|
|||||||
systray.AddSeparator()
|
systray.AddSeparator()
|
||||||
|
|
||||||
account := "Account"
|
account := "Account"
|
||||||
if state.curProfile.Name != "" {
|
if pt := profileTitle(state.curProfile); pt != "" {
|
||||||
account += fmt.Sprintf(" (%s)", state.curProfile.Name)
|
account = pt
|
||||||
}
|
}
|
||||||
accounts := systray.AddMenuItem(account, "")
|
accounts := systray.AddMenuItem(account, "")
|
||||||
|
setRemoteIcon(accounts, state.curProfile.UserProfile.ProfilePicURL)
|
||||||
// The dbus message about this menu item must propagate to the receiving
|
// The dbus message about this menu item must propagate to the receiving
|
||||||
// end before we attach any submenu items. Otherwise the receiver may not
|
// end before we attach any submenu items. Otherwise the receiver may not
|
||||||
// yet record the parent menu item and error out.
|
// 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.
|
// Aggregate all clicks into a shared channel.
|
||||||
menu.accountsCh = make(chan ipn.ProfileID)
|
menu.accountsCh = make(chan ipn.ProfileID)
|
||||||
for _, profile := range state.allProfiles {
|
for _, profile := range state.allProfiles {
|
||||||
title := fmt.Sprintf("%s (%s)", profile.Name, profile.NetworkProfile.DomainName)
|
title := profileTitle(profile)
|
||||||
// Note: we could use AddSubMenuItemCheckbox instead of this formatting
|
var item *systray.MenuItem
|
||||||
// hack, but checkboxes don't work across all desktops unfortunately.
|
|
||||||
if profile.ID == state.curProfile.ID {
|
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) {
|
go func(profile ipn.LoginProfile) {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@ -170,6 +173,44 @@ func (menu *Menu) rebuild(state state) {
|
|||||||
go menu.eventLoop(ctx)
|
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
|
// eventLoop is the main event loop for handling click events on menu items
|
||||||
// and responding to Tailscale state changes.
|
// and responding to Tailscale state changes.
|
||||||
// This method does not return until ctx.Done is closed.
|
// This method does not return until ctx.Done is closed.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user