gosora/common/widgets.go
Azareal 4d9dc76392 Begin work on making the analytics panes somewhat usable when having JavaScript disabled.
Very minor refactoring here and there across the software.
Save some allocations here and there.
2019-10-28 09:13:24 +10:00

353 lines
7.1 KiB
Go

/* Copyright Azareal 2017 - 2020 */
package common
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"strings"
"sync"
"sync/atomic"
min "github.com/Azareal/Gosora/common/templates"
"github.com/pkg/errors"
)
// TODO: Clean this file up
var Docks WidgetDocks
var widgetUpdateMutex sync.RWMutex
type WidgetDock struct {
Items []*Widget
Scheduler *WidgetScheduler
}
type WidgetDocks struct {
LeftOfNav []*Widget
RightOfNav []*Widget
LeftSidebar WidgetDock
RightSidebar WidgetDock
//PanelLeft []Menus
Footer WidgetDock
}
type WidgetMenu struct {
Name string
MenuList []WidgetMenuItem
}
type WidgetMenuItem struct {
Text string
Location string
Compact bool
}
type NameTextPair struct {
Name string
Text template.HTML
}
func preparseWidget(w *Widget, wdata string) (err error) {
prebuildWidget := func(name string, data interface{}) (string, error) {
var b bytes.Buffer
err := DefaultTemplates.ExecuteTemplate(&b, name+".html", data)
content := string(b.Bytes())
if Config.MinifyTemplates {
content = min.Minify(content)
}
return content, err
}
sbytes := []byte(wdata)
w.Literal = true
// TODO: Split these hard-coded items out of this file and into the files for the individual widget types
switch w.Type {
case "simple", "about":
var tmp NameTextPair
err = json.Unmarshal(sbytes, &tmp)
if err != nil {
return err
}
w.Body, err = prebuildWidget("widget_"+w.Type, tmp)
case "search_and_filter":
w.Literal = false
w.BuildFunc = widgetSearchAndFilter
case "wol":
w.Literal = false
w.InitFunc = wolInit
w.BuildFunc = wolRender
w.TickFunc = wolTick
case "wol_context":
w.Literal = false
w.BuildFunc = wolContextRender
default:
w.Body = wdata
}
// TODO: Test this
// TODO: Should we toss this through a proper parser rather than crudely replacing it?
w.Location = strings.Replace(w.Location, " ", "", -1)
w.Location = strings.Replace(w.Location, "frontend", "!panel", -1)
w.Location = strings.Replace(w.Location, "!!", "", -1)
// Skip blank zones
locs := strings.Split(w.Location, "|")
if len(locs) > 0 {
w.Location = ""
for _, loc := range locs {
if loc == "" {
continue
}
w.Location += loc + "|"
}
w.Location = w.Location[:len(w.Location)-1]
}
return err
}
func GetDockList() []string {
return []string{
"leftOfNav",
"rightOfNav",
"rightSidebar",
"footer",
}
}
func GetDock(dock string) []*Widget {
switch dock {
case "leftOfNav":
return Docks.LeftOfNav
case "rightOfNav":
return Docks.RightOfNav
case "rightSidebar":
return Docks.RightSidebar.Items
case "footer":
return Docks.Footer.Items
}
return nil
}
func HasDock(dock string) bool {
switch dock {
case "leftOfNav", "rightOfNav", "rightSidebar", "footer":
return true
}
return false
}
// TODO: Find a more optimimal way of doing this...
func HasWidgets(dock string, h *Header) bool {
if !h.Theme.HasDock(dock) {
return false
}
// Let themes forcibly override this slot
sbody := h.Theme.BuildDock(dock)
if sbody != "" {
return true
}
var widgets []*Widget
switch dock {
case "leftOfNav":
widgets = Docks.LeftOfNav
case "rightOfNav":
widgets = Docks.RightOfNav
case "rightSidebar":
widgets = Docks.RightSidebar.Items
case "footer":
widgets = Docks.Footer.Items
}
wcount := 0
for _, widget := range widgets {
if !widget.Enabled {
continue
}
if widget.Allowed(h.Zone, h.ZoneID) {
wcount++
}
}
return wcount > 0
}
func BuildWidget(dock string, h *Header) (sbody string) {
if !h.Theme.HasDock(dock) {
return ""
}
// Let themes forcibly override this slot
sbody = h.Theme.BuildDock(dock)
if sbody != "" {
return sbody
}
var widgets []*Widget
switch dock {
case "leftOfNav":
widgets = Docks.LeftOfNav
case "rightOfNav":
widgets = Docks.RightOfNav
case "topMenu":
// 1 = id for the default menu
mhold, err := Menus.Get(1)
if err == nil {
err := mhold.Build(h.Writer, &h.CurrentUser, h.Path)
if err != nil {
LogError(err)
}
}
return ""
case "rightSidebar":
widgets = Docks.RightSidebar.Items
case "footer":
widgets = Docks.Footer.Items
}
for _, widget := range widgets {
if !widget.Enabled {
continue
}
if widget.Allowed(h.Zone, h.ZoneID) {
item, err := widget.Build(h)
if err != nil {
LogError(err)
}
sbody += item
}
}
return sbody
}
func getDockWidgets(dock string) (widgets []*Widget, err error) {
rows, err := widgetStmts.getDockList.Query(dock)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
w := &Widget{Position: 0, Side: dock}
err = rows.Scan(&w.ID, &w.Position, &w.Type, &w.Enabled, &w.Location, &w.RawBody)
if err != nil {
return nil, err
}
err = preparseWidget(w, w.RawBody)
if err != nil {
return nil, err
}
Widgets.set(w)
widgets = append(widgets, w)
}
return widgets, rows.Err()
}
// TODO: Make a store for this?
func InitWidgets() (fi error) {
// TODO: Let themes set default values for widget docks, and let them lock in particular places with their stuff, e.g. leftOfNav and rightOfNav
f := func(name string) {
if fi != nil {
return
}
dock, err := getDockWidgets(name)
if err != nil {
fi = err
return
}
setDock(name, dock)
}
f("leftOfNav")
f("rightOfNav")
f("leftSidebar")
f("rightSidebar")
f("footer")
if fi != nil {
return fi
}
AddScheduledSecondTask(Docks.LeftSidebar.Scheduler.Tick)
AddScheduledSecondTask(Docks.RightSidebar.Scheduler.Tick)
AddScheduledSecondTask(Docks.Footer.Scheduler.Tick)
return nil
}
func releaseWidgets(widgets []*Widget) {
for _, widget := range widgets {
if widget.ShutdownFunc != nil {
widget.ShutdownFunc(widget)
}
}
}
// TODO: Use atomics
func setDock(dock string, widgets []*Widget) {
dockHandle := func(dockWidgets []*Widget) {
DebugLog(dock, widgets)
releaseWidgets(dockWidgets)
}
dockHandle2 := func(dockWidgets WidgetDock) WidgetDock {
dockHandle(dockWidgets.Items)
if dockWidgets.Scheduler == nil {
dockWidgets.Scheduler = &WidgetScheduler{}
}
for _, widget := range widgets {
if widget.InitFunc != nil {
widget.InitFunc(widget, dockWidgets.Scheduler)
}
}
dockWidgets.Scheduler.Store()
return WidgetDock{widgets, dockWidgets.Scheduler}
}
widgetUpdateMutex.Lock()
defer widgetUpdateMutex.Unlock()
switch dock {
case "leftOfNav":
dockHandle(Docks.LeftOfNav)
Docks.LeftOfNav = widgets
case "rightOfNav":
dockHandle(Docks.RightOfNav)
Docks.RightOfNav = widgets
case "leftSidebar":
Docks.LeftSidebar = dockHandle2(Docks.LeftSidebar)
case "rightSidebar":
Docks.RightSidebar = dockHandle2(Docks.RightSidebar)
case "footer":
Docks.Footer = dockHandle2(Docks.Footer)
default:
fmt.Printf("bad dock '%s'\n", dock)
return
}
}
type WidgetScheduler struct {
widgets []*Widget
store atomic.Value
}
func (s *WidgetScheduler) Add(w *Widget) {
s.widgets = append(s.widgets, w)
}
func (s *WidgetScheduler) Store() {
s.store.Store(s.widgets)
}
func (s *WidgetScheduler) Tick() error {
widgets := s.store.Load().([]*Widget)
for _, widget := range widgets {
if widget.TickFunc == nil {
continue
}
err := widget.TickFunc(widget)
if err != nil {
return errors.WithStack(err)
}
}
return nil
}