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.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")) }