rpcfetch/fetch/ui.go

445 lines
12 KiB
Go
Raw Normal View History

package main
import (
"encoding/json"
"log"
"strconv"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"git.ophivana.moe/cat/rpcfetch"
"golang.org/x/image/colornames"
)
var (
a fyne.App
w fyne.Window
profile = container.NewBorder(nil, profileStatus, nil, nil, profilePreview)
profileStatus = container.NewStack(profileStatusB, container.NewCenter(profileStatusT))
profileStatusB = canvas.NewRectangle(colornames.Gray)
profileStatusT = widget.NewLabel("Inactive")
profilePreview = container.NewCenter(container.NewVBox(profileT, profileActT))
profileT = widget.NewLabel("")
profileActT = widget.NewLabel("Pending...")
presets = container.NewHSplit(
container.NewBorder(
presetNew,
container.NewGridWithColumns(2, presetUp, presetDown),
nil, nil,
presetList,
),
presetForm,
)
presetNew = widget.NewButton("New", nil)
presetNewFunc = func() {
presetNew.Disable()
defer presetNew.Enable()
confLock.Lock()
conf.Profiles = append(conf.Profiles, copyTemplate())
i := len(conf.Profiles) - 1
u := conf.Profiles[i].UUID
confLock.Unlock()
log.Printf("profile %s created", u)
presetList.Select(i)
presetDestroy.Enable()
}
presetList = widget.NewList(
// length
func() int {
confLock.RLock()
defer confLock.RUnlock()
return len(conf.Profiles)
},
// createItem
func() fyne.CanvasObject {
return container.NewHBox(
widget.NewIcon(theme.SettingsIcon()),
widget.NewLabel("Template Title"),
canvas.NewText("Template UUID", colornames.Gray),
)
},
// updateItem
func(id widget.ListItemID, object fyne.CanvasObject) {
confLock.RLock()
p := conf.Profiles[id]
title := p.Title
if title == "" {
title = "Untitled"
}
u := p.UUID.String()
confLock.RUnlock()
ent := object.(*fyne.Container)
ent.Objects[1].(*widget.Label).SetText(title)
ent.Objects[2].(*canvas.Text).Text = u
},
)
presetForm = container.NewVBox(
widget.NewEntry(),
layout.NewSpacer(),
widget.NewForm(
widget.NewFormItem("Application ID", widget.NewEntry()),
widget.NewFormItem("State", widget.NewEntry()),
widget.NewFormItem("Details", widget.NewEntry()),
widget.NewFormItem("Large Image", container.NewGridWithColumns(2, widget.NewEntry(), widget.NewEntry())),
widget.NewFormItem("Small Image", container.NewGridWithColumns(2, widget.NewEntry(), widget.NewEntry())),
),
layout.NewSpacer(),
container.NewHBox(
presetDestroy,
layout.NewSpacer(),
presetSave,
),
)
presetDestroy = widget.NewButton("Delete", nil)
presetDestroyFunc = func() {
confLock.RLock()
// prevent deletion of final remaining profile
if len(conf.Profiles) == 2 {
presetDestroy.Disable()
}
confLock.RUnlock()
// move selection away
t := presetFormSelected
if t == 0 {
presetList.Select(1)
// index is shifted after deletion in this case
presetFormSelected = 0
} else {
presetList.Select(presetFormSelected - 1)
}
confLock.Lock()
log.Printf("profile %s destroyed", conf.Profiles[t].UUID)
conf.Profiles = append(conf.Profiles[:t], conf.Profiles[t+1:]...)
confLock.Unlock()
// invalidate profile index cache
profLast.Store(profLastCache{})
presetList.Refresh()
}
presetSave = widget.NewButton("Save", nil)
presetSaveFunc = func() {
form := presetForm.Objects[2].(*widget.Form)
title := presetForm.Objects[0].(*widget.Entry)
id := form.Items[0].Widget.(*widget.Entry)
state := form.Items[1].Widget.(*widget.Entry)
details := form.Items[2].Widget.(*widget.Entry)
large := form.Items[3].Widget.(*fyne.Container)
small := form.Items[4].Widget.(*fyne.Container)
confLock.Lock()
prof := &conf.Profiles[presetFormSelected]
prof.Title = title.Text
prof.ID = id.Text
prof.State = state.Text
prof.Details = details.Text
prof.Assets = &rpcfetch.ActivityAssets{
LargeImage: large.Objects[1].(*widget.Entry).Text,
LargeText: large.Objects[0].(*widget.Entry).Text,
SmallImage: small.Objects[1].(*widget.Entry).Text,
SmallText: small.Objects[0].(*widget.Entry).Text,
}
log.Printf("profile %s updated", prof.UUID)
confLock.Unlock()
presetList.Refresh()
}
presetUp = widget.NewButton("", func() {
confLock.Lock()
i := presetFormSelected
conf.Profiles[i], conf.Profiles[i-1] = conf.Profiles[i-1], conf.Profiles[i]
log.Printf("profile %s moved up", conf.Profiles[i-1].UUID)
// invalidate profile index cache
profLast.Store(profLastCache{})
confLock.Unlock()
presetList.Refresh()
presetList.Select(i - 1)
})
presetDown = widget.NewButton("", func() {
confLock.Lock()
i := presetFormSelected
conf.Profiles[i], conf.Profiles[i+1] = conf.Profiles[i+1], conf.Profiles[i]
log.Printf("profile %s moved down", conf.Profiles[i+1].UUID)
// invalidate profile index cache
profLast.Store(profLastCache{})
confLock.Unlock()
presetList.Refresh()
presetList.Select(i + 1)
})
presetFormSelected int
status = container.NewVBox(
widget.NewForm(
widget.NewFormItem("Status", statusText),
widget.NewFormItem("Application ID", statusID),
widget.NewFormItem("API Endpoint", statusEndpoint),
widget.NewFormItem("CDN Host", statusCDN),
widget.NewFormItem("Environment", statusEnvironment),
),
widget.NewSeparator(),
widget.NewForm(
widget.NewFormItem("PID", statusPID),
widget.NewFormItem("Hostname", statusHostname),
widget.NewFormItem("Loadavg", statusLoadavg),
widget.NewFormItem("Memory", statusMemory),
),
widget.NewSeparator(),
widget.NewForm(
widget.NewFormItem("Replaces", statusReplaces),
widget.NewFormItem("Timer", statusTimer),
widget.NewFormItem("Version", widget.NewLabel(Version)),
),
widget.NewSeparator(),
container.NewHBox(
widget.NewButton("Export Configuration", func() {
confLock.RLock()
b, err := json.Marshal(conf)
if err != nil {
log.Printf("error exporting configuration as JSON: %s", err)
return
}
confLock.RUnlock()
confW := widget.NewLabel(string(b))
confW.Wrapping = fyne.TextWrapBreak
confWin := a.NewWindow("Export")
confWin.SetContent(container.NewVBox(
confW,
container.NewHBox(
widget.NewButton("Copy", func() {
confWin.Clipboard().SetContent(string(b))
}),
widget.NewButton("Dismiss", confWin.Close),
),
))
confWin.SetFixedSize(true)
confWin.Show()
// unfortunate workaround for Xwayland bug
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
confWin.Resize(fyne.NewSize(512, confWin.Content().MinSize().Height))
}
}),
widget.NewButton("Internals", func() {
intWin := a.NewWindow("Internals")
intAct := widget.NewLabel(strconv.FormatBool(active.Load()))
intRes := a.NewWindow("Resize")
intRes.Resize(w.Canvas().Size())
intRes.SetContent(widget.NewButton("Commit new size", func() {
w.Resize(intRes.Canvas().Size())
resizeState = true
intRes.Close()
}))
intWin.SetContent(container.NewVBox(
widget.NewForm(
widget.NewFormItem("Active", container.NewHBox(
intAct,
widget.NewButton("Toggle", func() {
active.Store(!active.Load())
intAct.SetText(strconv.FormatBool(active.Load()))
})),
),
),
widget.NewButton("Resize", intRes.Show),
widget.NewButton("Dismiss", intWin.Close),
))
intWin.SetFixedSize(true)
intWin.Show()
// unfortunate workaround for Xwayland bug
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
intWin.Resize(fyne.NewSize(512, intWin.Content().MinSize().Height))
}
}),
),
)
statusText = widget.NewLabel("")
statusID = widget.NewLabel("")
statusEndpoint = widget.NewLabel("")
statusCDN = widget.NewLabel("")
statusEnvironment = widget.NewLabel("")
statusPID = widget.NewLabel("")
statusHostname = widget.NewLabel("")
statusLoadavg = widget.NewLabel("")
statusMemory = widget.NewLabel("")
statusReplaces = widget.NewLabel("")
statusTimer = widget.NewLabel("")
resizeState bool
)
func ui() error {
a = app.New()
w = a.NewWindow("RPCFetch")
var (
content = container.NewAppTabs(
container.NewTabItem("Preview", profile),
container.NewTabItem("Presets", presets),
container.NewTabItem("Status", status),
)
)
statusPID.SetText(pidS)
statusHostname.SetText(hostname)
// set up presets UI
presetNew.OnTapped = presetNewFunc
presetDestroy.OnTapped = presetDestroyFunc
presetSave.OnTapped = presetSaveFunc
presetList.OnSelected = func(id widget.ListItemID) {
confLock.RLock()
p := conf.Profiles[id]
presetFormSelected = id
form := presetForm.Objects[2].(*widget.Form)
presetForm.Objects[0].(*widget.Entry).SetText(p.Title)
form.Items[0].Widget.(*widget.Entry).SetText(p.ID)
form.Items[1].Widget.(*widget.Entry).SetText(p.State)
form.Items[2].Widget.(*widget.Entry).SetText(p.Details)
if p.Assets != nil {
large := form.Items[3].Widget.(*fyne.Container)
large.Objects[0].(*widget.Entry).SetText(p.Assets.LargeText)
large.Objects[1].(*widget.Entry).SetText(p.Assets.LargeImage)
small := form.Items[4].Widget.(*fyne.Container)
small.Objects[0].(*widget.Entry).SetText(p.Assets.SmallText)
small.Objects[1].(*widget.Entry).SetText(p.Assets.SmallImage)
}
confLock.RUnlock()
confLock.Lock()
conf.Current = p.UUID
confLock.Unlock()
// set profile index cache
profLast.Store(profLastCache{
uuid: p.UUID,
index: id,
})
// set movement button inhibitions
if presetFormSelected == 0 {
presetUp.Disable()
} else {
presetUp.Enable()
}
confLock.RLock()
if presetFormSelected == len(conf.Profiles)-1 {
presetDown.Disable()
} else {
presetDown.Enable()
}
confLock.RUnlock()
log.Printf("profile %s activated", p.UUID)
}
confLock.RLock()
if len(conf.Profiles) == 1 {
presetDestroy.Disable()
}
confLock.RUnlock()
conf.profile() // make sure profile index cache is populated
presetList.Select(profLast.Load().(profLastCache).index)
presetForm.Objects[0].(*widget.Entry).SetPlaceHolder("Title")
presetList.ScrollToTop()
presetUp.SetIcon(theme.MoveUpIcon())
presetDown.SetIcon(theme.MoveDownIcon())
go func() {
for {
statusTimer.SetText(time.Now().Sub(time.Unix(launchTime, 0)).Round(time.Second).String())
time.Sleep(1 * time.Second)
}
}()
w.SetMaster()
w.SetIcon(theme.SettingsIcon())
w.SetContent(content)
w.SetFixedSize(true)
w.Resize(fyne.Size(conf.Size))
w.ShowAndRun()
// after window close
confLock.RLock()
csc := fyne.Size(conf.Size)
confLock.RUnlock()
if cs := w.Canvas().Size(); resizeState && cs != csc {
log.Printf("save new window size width %.2f height %.2f", cs.Width, cs.Height)
confLock.Lock()
conf.Size = size(cs)
confLock.Unlock()
}
save()
return nil
}
func failureState(ok bool) {
if ok {
statusText.SetText("Connected.")
profileStatusB.FillColor = colornames.Green
profileStatusT.SetText("Connected")
} else {
statusText.SetText("Retrying in 5 seconds...")
profileStatusB.FillColor = colornames.Darkred
profileStatusT.SetText("Disconnected")
profileActT.SetText("Pending...")
profileT.Hide()
}
}
func updateClientInfo(s *applyState) {
if u, ok := d.User(); ok {
profileT.Show()
profileT.SetText(u.Username)
confLock.RLock()
prof := conf.profile()
tmpl := prof.State + "\n" + prof.Details
confLock.RUnlock()
profileActT.SetText(s.replace(tmpl))
} else {
profileT.Hide()
}
statusID.SetText(d.ID())
if c, ok := d.Config(); ok {
statusEndpoint.SetText(c.APIEndpoint)
statusCDN.SetText(c.CDNHost)
statusEnvironment.SetText(c.Environment)
}
statusLoadavg.SetText(s.replace("%1min %5min %15min"))
statusMemory.SetText(s.replace("%used / %total"))
}