cchat-gtk/internal/ui/service/session/server/server.go

344 lines
7.9 KiB
Go

package server
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb"
"github.com/diamondburned/cchat-gtk/internal/ui/service/button"
"github.com/diamondburned/cchat-gtk/internal/ui/service/loading"
"github.com/diamondburned/cchat-gtk/internal/ui/service/menu"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
const ChildrenMargin = 24
const IconSize = 20
type Controller interface {
RowSelected(*ServerRow, cchat.ServerMessage)
}
type Row struct {
*gtk.Box
Button *button.ToggleButtonImage
parentcrumb breadcrumb.Breadcrumber
children *Children
serverList cchat.ServerList
loaded bool
}
func NewRow(parent breadcrumb.Breadcrumber, name text.Rich) *Row {
button := button.NewToggleButtonImage(name)
button.Box.SetHAlign(gtk.ALIGN_START)
button.Image.AddProcessors(imgutil.Round(true))
button.Image.SetSize(IconSize)
button.SetRelief(gtk.RELIEF_NONE)
button.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
box.SetMarginStart(ChildrenMargin)
box.PackStart(button, false, false, 0)
row := &Row{
Box: box,
Button: button,
parentcrumb: parent,
}
return row
}
func (r *Row) Breadcrumb() breadcrumb.Breadcrumb {
return breadcrumb.Try(r.parentcrumb, r.Button.GetText())
}
func (r *Row) SetLabelUnsafe(name text.Rich) {
r.Button.SetLabelUnsafe(name)
}
func (r *Row) SetIconer(v interface{}) {
if iconer, ok := v.(cchat.Icon); ok {
r.Button.Image.AsyncSetIconer(iconer, "Error getting server icon URL")
}
}
// SetServerList sets the row to a server list.
func (r *Row) SetServerList(list cchat.ServerList, ctrl Controller) {
r.Button.SetClicked(func(active bool) {
r.SetRevealChild(active)
})
r.children = NewChildren(r, ctrl)
r.children.Show()
r.Box.PackStart(r.children, false, false, 0)
r.serverList = list
}
// Reset clears off all children servers. It's a no-op if there are none.
func (r *Row) Reset() {
if r.children != nil {
// Remove everything from the children container.
r.children.Reset()
// Remove the children container itself.
r.Box.Remove(r.children)
}
// Reset the state.
r.loaded = false
r.serverList = nil
r.children = nil
}
// SetLoading is called by the parent struct.
func (r *Row) SetLoading() {
r.Button.SetLoading()
r.SetSensitive(false)
}
// SetFailed is shared between the parent struct and the children list. This is
// because both of those errors share the same appearance, just different
// callbacks.
func (r *Row) SetFailed(err error, retry func()) {
r.SetSensitive(true)
r.SetTooltipText(err.Error())
r.Button.SetFailed(err, retry)
r.Button.Label.SetMarkup(rich.MakeRed(r.Button.GetLabel()))
}
// SetDone is shared between the parent struct and the children list. This is
// because both will use the same SetFailed.
func (r *Row) SetDone() {
r.Button.SetNormal()
r.SetSensitive(true)
r.SetTooltipText("")
}
func (r *Row) SetNormalExtraMenu(items []menu.Item) {
r.Button.SetNormalExtraMenu(items)
r.SetSensitive(true)
r.SetTooltipText("")
}
func (r *Row) childrenFailed(err error) {
// If the user chooses to retry, the list will automatically expand.
r.SetFailed(err, func() { r.SetRevealChild(true) })
}
func (r *Row) childrenDone() {
r.loaded = true
r.SetDone()
}
// SetSelected is used for highlighting the current message server.
func (r *Row) SetSelected(selected bool) {
// Set the clickability the opposite as the boolean.
r.Button.SetSensitive(!selected)
// Some special edge case that I forgot.
if !selected {
r.Button.SetActive(false)
}
}
func (r *Row) GetActive() bool {
return r.Button.GetActive()
}
// SetRevealChild reveals the list of servers. It does nothing if there are no
// servers, meaning if Row does not represent a ServerList.
func (r *Row) SetRevealChild(reveal bool) {
// Do the above noop check.
if r.children == nil {
return
}
// Actually reveal the children.
r.children.SetRevealChild(reveal)
// If this isn't a reveal, then we don't need to load.
if !reveal {
return
}
// If we haven't loaded yet and we're still not loading, then load.
if !r.loaded && r.children.load == nil {
r.Load()
}
}
// Load loads the row without uncollapsing it.
func (r *Row) Load() {
// Safeguard.
if r.children == nil || r.serverList == nil {
return
}
// Set that we're now loading.
r.children.setLoading()
r.SetSensitive(false)
// Load the list of servers if we're still in loading mode.
go func() {
err := r.serverList.Servers(r.children)
gts.ExecAsync(func() {
// We're not loading anymore, so remove the loading circle.
r.children.setNotLoading()
// Restore clickability.
r.SetSensitive(true)
// Use the childrenX method instead of SetX.
if err != nil {
r.childrenFailed(errors.Wrap(err, "Failed to get servers"))
} else {
r.childrenDone()
}
})
}()
}
// GetRevealChild returns whether or not the server list is expanded, or always
// false if there is no server list.
func (r *Row) GetRevealChild() bool {
if r.children != nil {
return r.children.GetRevealChild()
}
return false
}
type ServerRow struct {
*Row
Server cchat.Server
}
func NewServerRow(p breadcrumb.Breadcrumber, server cchat.Server, ctrl Controller) *ServerRow {
row := NewRow(p, server.Name())
row.Show()
row.SetIconer(server)
primitives.AddClass(row, "server")
var serverRow = &ServerRow{Row: row, Server: server}
switch server := server.(type) {
case cchat.ServerList:
row.SetServerList(server, ctrl)
primitives.AddClass(row, "server-list")
case cchat.ServerMessage:
row.Button.SetClickedIfTrue(func() { ctrl.RowSelected(serverRow, server) })
primitives.AddClass(row, "server-message")
}
return serverRow
}
// Children is a children server with a reference to the parent.
type Children struct {
*gtk.Revealer
Main *gtk.Box
rowctrl Controller
load *loading.Button // only not nil while loading
Rows []*ServerRow
Parent breadcrumb.Breadcrumber
}
func NewChildren(p breadcrumb.Breadcrumber, ctrl Controller) *Children {
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
main.Show()
rev, _ := gtk.RevealerNew()
rev.SetRevealChild(false)
rev.Add(main)
return &Children{
Revealer: rev,
Main: main,
rowctrl: ctrl,
Parent: p,
}
}
// setLoading shows the loading circle as a list child.
func (c *Children) setLoading() {
// Exit if we're already loading.
if c.load != nil {
return
}
// Clear everything.
c.Reset()
// Set the loading circle and stuff.
c.load = loading.NewButton()
c.load.Show()
c.Main.Add(c.load)
}
func (c *Children) Reset() {
// Remove old servers from the list.
for _, row := range c.Rows {
c.Main.Remove(row)
}
// Wipe the list empty.
c.Rows = nil
}
// setNotLoading removes the loading circle, if any. This is not in Reset()
// anymore, since the backend may not necessarily call SetServers.
func (c *Children) setNotLoading() {
// Do we have the spinning circle button? If yes, remove it.
if c.load != nil {
// Stop the loading mode. The reset function should do everything for us.
c.Main.Remove(c.load)
c.load = nil
}
}
func (c *Children) SetServers(servers []cchat.Server) {
gts.ExecAsync(func() {
// Save the current state.
var oldID string
for _, row := range c.Rows {
if row.GetActive() {
oldID = row.Server.ID()
break
}
}
// Reset before inserting new servers.
c.Reset()
c.Rows = make([]*ServerRow, len(servers))
for i, server := range servers {
row := NewServerRow(c, server, c.rowctrl)
c.Rows[i] = row
c.Main.Add(row)
}
// Update parent reference? Only if it's activated.
if oldID != "" {
for _, row := range c.Rows {
if row.Server.ID() == oldID {
row.Button.SetActive(true)
}
}
}
})
}
func (c *Children) Breadcrumb() breadcrumb.Breadcrumb {
return breadcrumb.Try(c.Parent)
}