add: gui and tsdiag
This commit is contained in:
+190
@@ -0,0 +1,190 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
|
||||
"tslink/core"
|
||||
)
|
||||
|
||||
type lanPage struct {
|
||||
list widget.List
|
||||
copy map[string]*widget.Clickable
|
||||
}
|
||||
|
||||
func newLanPage() *lanPage {
|
||||
p := &lanPage{copy: make(map[string]*widget.Clickable)}
|
||||
p.list.Axis = layout.Vertical
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *lanPage) copyBtn(key string) *widget.Clickable {
|
||||
c, ok := p.copy[key]
|
||||
if !ok {
|
||||
c = &widget.Clickable{}
|
||||
p.copy[key] = c
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (p *lanPage) Layout(a *App, gtx C, st core.State) D {
|
||||
th := a.th
|
||||
if st.Lan == nil {
|
||||
return th.EmptyState(gtx, IconBroadcast, th.T(KLanEmpty), th.T(KLoading))
|
||||
}
|
||||
|
||||
servers := st.Lan.Servers()
|
||||
scanErr := st.Lan.Err()
|
||||
|
||||
var advertised []core.LanEntry
|
||||
if st.Config != nil {
|
||||
advertised = core.LanEntriesFromRules(st.Config.Connect)
|
||||
}
|
||||
|
||||
items := []layout.Widget{
|
||||
func(gtx C) D { return p.summaryCard(a, gtx, servers, advertised, scanErr) },
|
||||
}
|
||||
if len(servers) == 0 {
|
||||
items = append(items, func(gtx C) D {
|
||||
hint := th.T(KLanSubtitle)
|
||||
if scanErr != "" {
|
||||
hint = scanErr
|
||||
}
|
||||
return th.EmptyState(gtx, IconServer, th.T(KLanEmpty), hint)
|
||||
})
|
||||
}
|
||||
for _, s := range servers {
|
||||
items = append(items, func(gtx C) D { return p.serverCard(a, gtx, s) })
|
||||
}
|
||||
|
||||
return material.List(th.Theme, &p.list).Layout(gtx, len(items), func(gtx C, i int) D {
|
||||
return layout.Inset{Bottom: SpaceMD}.Layout(gtx, items[i])
|
||||
})
|
||||
}
|
||||
|
||||
// summaryCard states what the scanner is doing and, crucially, whether the
|
||||
// advertisements tslink itself emits are being heard back. A rule that is
|
||||
// configured but not audible means the tunnel or the multicast path is broken,
|
||||
// and that is the single most useful thing this page can tell someone.
|
||||
func (p *lanPage) summaryCard(a *App, gtx C, servers []core.LanServer, advertised []core.LanEntry, scanErr string) D {
|
||||
th := a.th
|
||||
selfHeard := 0
|
||||
for _, s := range servers {
|
||||
if s.IsSelf {
|
||||
selfHeard++
|
||||
}
|
||||
}
|
||||
missing := len(advertised) - selfHeard
|
||||
if missing < 0 {
|
||||
missing = 0
|
||||
}
|
||||
|
||||
card := th.Card()
|
||||
card.Title = th.T(KLanTitle)
|
||||
card.Subtitle = th.T(KLanSubtitle)
|
||||
card.Trailing = func(gtx C) D {
|
||||
level, label := LevelOK, th.T(KLanListening)
|
||||
if scanErr != "" {
|
||||
level, label = LevelFail, th.T(KLanBindError)
|
||||
}
|
||||
return th.Chip(gtx, ChipStyle{Text: label, Level: level, Dot: true})
|
||||
}
|
||||
|
||||
rows := []KV{
|
||||
{Key: th.T(KOvLanServers), Value: itoa(len(servers))},
|
||||
{Key: th.T(KLanSelf), Value: itoa(selfHeard) + " / " + itoa(len(advertised)),
|
||||
Hint: th.T(KLanSelfHint),
|
||||
Level: selfLevel(len(advertised), selfHeard)},
|
||||
}
|
||||
if scanErr != "" {
|
||||
rows = append(rows, KV{Key: th.T(KError), Value: scanErr, Level: LevelFail})
|
||||
}
|
||||
return card.Layout(th, gtx, func(gtx C) D {
|
||||
return th.KVList(gtx, rows)
|
||||
})
|
||||
}
|
||||
|
||||
func selfLevel(advertised, heard int) StatusLevel {
|
||||
switch {
|
||||
case advertised == 0:
|
||||
return LevelNeutral
|
||||
case heard >= advertised:
|
||||
return LevelOK
|
||||
case heard == 0:
|
||||
return LevelFail
|
||||
default:
|
||||
return LevelWarn
|
||||
}
|
||||
}
|
||||
|
||||
func (p *lanPage) serverCard(a *App, gtx C, s core.LanServer) D {
|
||||
th := a.th
|
||||
addr := s.Addr.String() + ":" + itoa(s.Port)
|
||||
btn := p.copyBtn(addr)
|
||||
if btn.Clicked(gtx) {
|
||||
a.copyToClipboard(gtx, addr, "")
|
||||
}
|
||||
|
||||
stale := time.Since(s.LastSeen) > 8*time.Second
|
||||
level := LevelOK
|
||||
if stale {
|
||||
level = LevelWarn
|
||||
}
|
||||
|
||||
card := th.Card()
|
||||
card.Pad = SpaceMD
|
||||
if s.IsSelf {
|
||||
accent := th.P.Accent
|
||||
card.Accent = &accent
|
||||
}
|
||||
return card.Layout(th, gtx, func(gtx C) D {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return IconServer(gtx, gtx.Dp(18), th.StatusColor(level))
|
||||
}),
|
||||
HGap(SpaceMD),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(OneLine(th.Body(orDash(s.Motd))).Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if !s.IsSelf {
|
||||
return D{}
|
||||
}
|
||||
return layout.Inset{Left: SpaceSM}.Layout(gtx, func(gtx C) D {
|
||||
return th.Chip(gtx, ChipStyle{
|
||||
Text: th.T(KLanSelf),
|
||||
Level: LevelInfo,
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return OneLine(th.MonoLabel(SizeCaption, th.P.TextSec, addr)).Layout(gtx)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
HGap(SpaceMD),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical, Alignment: layout.End}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return th.Text(SizeCaption, th.StatusColor(level),
|
||||
RelTime(th, s.LastSeen, time.Now())).Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return th.Caption(itoa(s.Count) + " " + th.T(KLanPackets)).Layout(gtx)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
HGap(SpaceSM),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return th.IconButton(gtx, btn, IconCopy, LevelNeutral)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user