188 lines
5.7 KiB
Go
188 lines
5.7 KiB
Go
package gio
|
|
|
|
import (
|
|
"image"
|
|
|
|
"gioui.org/layout"
|
|
"gioui.org/op/paint"
|
|
"gioui.org/unit"
|
|
"gioui.org/widget"
|
|
"gioui.org/widget/material"
|
|
|
|
"softbox.local/core/application"
|
|
)
|
|
|
|
// AppShell is the modern software catalog window.
|
|
type AppShell struct {
|
|
edition string
|
|
model *application.CatalogListModel
|
|
|
|
search widget.Editor
|
|
appList layout.List
|
|
categoryList layout.List
|
|
|
|
viewAll widget.Clickable
|
|
viewInstalled widget.Clickable
|
|
viewUpdates widget.Clickable
|
|
resetFilters widget.Clickable
|
|
closeDetail widget.Clickable
|
|
|
|
categoryControls map[string]*widget.Clickable
|
|
rows map[string]*rowControls
|
|
icons map[string]paint.ImageOp
|
|
iconReferences map[string]string
|
|
iconRequests map[string]application.IconEventIdentity
|
|
iconApplied map[string]application.IconEventIdentity
|
|
iconFailures map[string]iconFailureState
|
|
lastRendered int
|
|
detailRendered bool
|
|
}
|
|
|
|
// NewAppShell creates the catalog shell with an optional in-memory snapshot.
|
|
func NewAppShell(
|
|
edition string,
|
|
items ...application.CatalogListItem,
|
|
) *AppShell {
|
|
shell := &AppShell{
|
|
edition: edition,
|
|
model: application.NewCatalogListModel(nil),
|
|
appList: layout.List{Axis: layout.Vertical},
|
|
categoryList: layout.List{Axis: layout.Horizontal},
|
|
categoryControls: make(map[string]*widget.Clickable),
|
|
rows: make(map[string]*rowControls),
|
|
icons: make(map[string]paint.ImageOp),
|
|
iconReferences: make(map[string]string),
|
|
iconRequests: make(map[string]application.IconEventIdentity),
|
|
iconApplied: make(map[string]application.IconEventIdentity),
|
|
iconFailures: make(map[string]iconFailureState),
|
|
}
|
|
shell.search.SingleLine = true
|
|
shell.SetItems(items)
|
|
return shell
|
|
}
|
|
|
|
// ApplyIcon stores a decoded image for future Layout calls.
|
|
// It is UI-goroutine-only; background workers must publish application events.
|
|
func (shell *AppShell) ApplyIcon(appID string, icon image.Image) {
|
|
delete(shell.iconApplied, appID)
|
|
delete(shell.iconFailures, appID)
|
|
if icon == nil {
|
|
delete(shell.icons, appID)
|
|
return
|
|
}
|
|
shell.icons[appID] = paint.NewImageOp(icon)
|
|
}
|
|
|
|
// SetItems applies a prepared, IO-free catalog/status snapshot.
|
|
func (shell *AppShell) SetItems(items []application.CatalogListItem) {
|
|
shell.model.SetItems(items)
|
|
|
|
nextRows := make(map[string]*rowControls, len(items))
|
|
nextIcons := make(map[string]paint.ImageOp, len(items))
|
|
nextReferences := make(map[string]string, len(items))
|
|
nextRequests := make(map[string]application.IconEventIdentity, len(items))
|
|
nextApplied := make(map[string]application.IconEventIdentity, len(items))
|
|
nextFailures := make(map[string]iconFailureState, len(items))
|
|
for _, item := range items {
|
|
controls := shell.rows[item.ID]
|
|
if controls == nil {
|
|
controls = new(rowControls)
|
|
}
|
|
nextRows[item.ID] = controls
|
|
reference := canonicalIconReference(item.IconRef)
|
|
nextReferences[item.ID] = reference
|
|
if previous, exists := shell.iconReferences[item.ID]; exists && previous == reference {
|
|
if icon, exists := shell.icons[item.ID]; exists {
|
|
nextIcons[item.ID] = icon
|
|
}
|
|
if request, exists := shell.iconRequests[item.ID]; exists && request.Reference == reference {
|
|
nextRequests[item.ID] = request
|
|
}
|
|
if applied, exists := shell.iconApplied[item.ID]; exists && applied.Reference == reference {
|
|
nextApplied[item.ID] = applied
|
|
}
|
|
if failure, exists := shell.iconFailures[item.ID]; exists &&
|
|
failure.Identity.Reference == reference {
|
|
nextFailures[item.ID] = failure
|
|
}
|
|
}
|
|
}
|
|
shell.rows = nextRows
|
|
shell.icons = nextIcons
|
|
shell.iconReferences = nextReferences
|
|
shell.iconRequests = nextRequests
|
|
shell.iconApplied = nextApplied
|
|
shell.iconFailures = nextFailures
|
|
|
|
nextCategories := make(map[string]*widget.Clickable)
|
|
for _, category := range append([]string{""}, shell.model.Categories()...) {
|
|
control := shell.categoryControls[category]
|
|
if control == nil {
|
|
control = new(widget.Clickable)
|
|
}
|
|
nextCategories[category] = control
|
|
}
|
|
shell.categoryControls = nextCategories
|
|
}
|
|
|
|
// Layout drains input first and performs no disk, network or hash IO.
|
|
func (shell *AppShell) Layout(gtx layout.Context, theme *material.Theme) layout.Dimensions {
|
|
shell.drainInput(gtx)
|
|
shell.lastRendered = 0
|
|
shell.detailRendered = false
|
|
paint.Fill(gtx.Ops, shellColors.background)
|
|
|
|
return layout.UniformInset(unit.Dp(20)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
|
return layout.Flex{Axis: layout.Vertical}.Layout(
|
|
gtx,
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return shell.layoutHeader(gtx, theme)
|
|
}),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(16)}.Layout),
|
|
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
|
return shell.layoutContent(gtx, theme)
|
|
}),
|
|
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
return shell.layoutFooter(gtx, theme)
|
|
}),
|
|
)
|
|
})
|
|
}
|
|
|
|
func (shell *AppShell) drainInput(gtx layout.Context) {
|
|
for {
|
|
if _, ok := shell.search.Update(gtx); !ok {
|
|
break
|
|
}
|
|
}
|
|
shell.model.SetQuery(shell.search.Text())
|
|
|
|
for shell.viewAll.Clicked(gtx) {
|
|
shell.model.SetView(application.CatalogViewAll)
|
|
}
|
|
for shell.viewInstalled.Clicked(gtx) {
|
|
shell.model.SetView(application.CatalogViewInstalled)
|
|
}
|
|
for shell.viewUpdates.Clicked(gtx) {
|
|
shell.model.SetView(application.CatalogViewUpdates)
|
|
}
|
|
for category, control := range shell.categoryControls {
|
|
for control.Clicked(gtx) {
|
|
shell.model.SetCategory(category)
|
|
}
|
|
}
|
|
for appID, controls := range shell.rows {
|
|
for controls.open.Clicked(gtx) {
|
|
shell.model.Select(appID)
|
|
}
|
|
}
|
|
for shell.resetFilters.Clicked(gtx) {
|
|
shell.search.SetText("")
|
|
shell.model.ResetFilters()
|
|
}
|
|
for shell.closeDetail.Clicked(gtx) {
|
|
shell.model.Select("")
|
|
}
|
|
}
|