github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/widgets.go (about)

     1  /* Copyright Azareal 2017 - 2020 */
     2  package common
     3  
     4  import (
     5  	"bytes"
     6  	"encoding/json"
     7  	"fmt"
     8  	"html/template"
     9  	"strings"
    10  	"sync"
    11  	"sync/atomic"
    12  
    13  	min "github.com/Azareal/Gosora/common/templates"
    14  	"github.com/Azareal/Gosora/uutils"
    15  	"github.com/pkg/errors"
    16  )
    17  
    18  // TODO: Clean this file up
    19  var Docks WidgetDocks
    20  var widgetUpdateMutex sync.RWMutex
    21  
    22  type WidgetDock struct {
    23  	Items     []*Widget
    24  	Scheduler *WidgetScheduler
    25  }
    26  
    27  type WidgetDocks struct {
    28  	LeftOfNav    []*Widget
    29  	RightOfNav   []*Widget
    30  	LeftSidebar  WidgetDock
    31  	RightSidebar WidgetDock
    32  	//PanelLeft []Menus
    33  	Footer WidgetDock
    34  }
    35  
    36  type WidgetMenu struct {
    37  	Name     string
    38  	MenuList []WidgetMenuItem
    39  }
    40  
    41  type WidgetMenuItem struct {
    42  	Text     string
    43  	Location string
    44  	Compact  bool
    45  }
    46  
    47  type NameTextPair struct {
    48  	Name string
    49  	Text template.HTML
    50  }
    51  
    52  func preparseWidget(w *Widget, wdata string) (e error) {
    53  	prebuildWidget := func(name string, data interface{}) (string, error) {
    54  		var b bytes.Buffer
    55  		e := DefaultTemplates.ExecuteTemplate(&b, name+".html", data)
    56  		content := b.String()
    57  		if Config.MinifyTemplates {
    58  			content = min.Minify(content)
    59  		}
    60  		return content, e
    61  	}
    62  
    63  	sbytes := []byte(wdata)
    64  	w.Literal = true
    65  	// TODO: Split these hard-coded items out of this file and into the files for the individual widget types
    66  	switch w.Type {
    67  	case "simple", "about":
    68  		var tmp NameTextPair
    69  		e = json.Unmarshal(sbytes, &tmp)
    70  		if e != nil {
    71  			return e
    72  		}
    73  		w.Body, e = prebuildWidget("widget_"+w.Type, tmp)
    74  	case "search_and_filter":
    75  		w.Literal = false
    76  		w.BuildFunc = widgetSearchAndFilter
    77  	case "wol":
    78  		w.Literal = false
    79  		w.InitFunc = wolInit
    80  		w.BuildFunc = wolRender
    81  		w.TickFunc = wolTick
    82  	case "wol_context":
    83  		w.Literal = false
    84  		w.BuildFunc = wolContextRender
    85  	default:
    86  		w.Body = wdata
    87  	}
    88  
    89  	// TODO: Test this
    90  	// TODO: Should we toss this through a proper parser rather than crudely replacing it?
    91  	rep := func(from, to string) {
    92  		w.Location = strings.Replace(w.Location, from, to, -1)
    93  	}
    94  	rep(" ", "")
    95  	rep("frontend", "!panel")
    96  	rep("!!", "")
    97  
    98  	// Skip blank zones
    99  	locs := strings.Split(w.Location, "|")
   100  	if len(locs) > 0 {
   101  		w.Location = ""
   102  		for _, loc := range locs {
   103  			if loc == "" {
   104  				continue
   105  			}
   106  			w.Location += loc + "|"
   107  		}
   108  		w.Location = w.Location[:len(w.Location)-1]
   109  	}
   110  
   111  	return e
   112  }
   113  
   114  func GetDockList() []string {
   115  	return []string{
   116  		"leftOfNav",
   117  		"rightOfNav",
   118  		"rightSidebar",
   119  		"footer",
   120  	}
   121  }
   122  
   123  func GetDock(dock string) []*Widget {
   124  	switch dock {
   125  	case "leftOfNav":
   126  		return Docks.LeftOfNav
   127  	case "rightOfNav":
   128  		return Docks.RightOfNav
   129  	case "rightSidebar":
   130  		return Docks.RightSidebar.Items
   131  	case "footer":
   132  		return Docks.Footer.Items
   133  	}
   134  	return nil
   135  }
   136  
   137  func HasDock(dock string) bool {
   138  	switch dock {
   139  	case "leftOfNav", "rightOfNav", "rightSidebar", "footer":
   140  		return true
   141  	}
   142  	return false
   143  }
   144  
   145  // TODO: Find a more optimimal way of doing this...
   146  func HasWidgets(dock string, h *Header) bool {
   147  	if !h.Theme.HasDock(dock) {
   148  		return false
   149  	}
   150  
   151  	// Let themes forcibly override this slot
   152  	sbody := h.Theme.BuildDock(dock)
   153  	if sbody != "" {
   154  		return true
   155  	}
   156  
   157  	var widgets []*Widget
   158  	switch dock {
   159  	case "leftOfNav":
   160  		widgets = Docks.LeftOfNav
   161  	case "rightOfNav":
   162  		widgets = Docks.RightOfNav
   163  	case "rightSidebar":
   164  		widgets = Docks.RightSidebar.Items
   165  	case "footer":
   166  		widgets = Docks.Footer.Items
   167  	}
   168  
   169  	wcount := 0
   170  	for _, widget := range widgets {
   171  		if !widget.Enabled {
   172  			continue
   173  		}
   174  		if widget.Allowed(h.Zone, h.ZoneID) {
   175  			wcount++
   176  		}
   177  	}
   178  	return wcount > 0
   179  }
   180  
   181  func BuildWidget(dock string, h *Header) (sbody string) {
   182  	if !h.Theme.HasDock(dock) {
   183  		return ""
   184  	}
   185  	// Let themes forcibly override this slot
   186  	sbody = h.Theme.BuildDock(dock)
   187  	if sbody != "" {
   188  		return sbody
   189  	}
   190  
   191  	var widgets []*Widget
   192  	switch dock {
   193  	case "leftOfNav":
   194  		widgets = Docks.LeftOfNav
   195  	case "rightOfNav":
   196  		widgets = Docks.RightOfNav
   197  	case "topMenu":
   198  		// 1 = id for the default menu
   199  		mhold, e := Menus.Get(1)
   200  		if e == nil {
   201  			e := mhold.Build(h.Writer, h.CurrentUser, h.Path)
   202  			if e != nil {
   203  				LogError(e)
   204  			}
   205  		}
   206  		return ""
   207  	case "rightSidebar":
   208  		widgets = Docks.RightSidebar.Items
   209  	case "footer":
   210  		widgets = Docks.Footer.Items
   211  	}
   212  
   213  	for _, widget := range widgets {
   214  		if !widget.Enabled {
   215  			continue
   216  		}
   217  		if widget.Allowed(h.Zone, h.ZoneID) {
   218  			item, e := widget.Build(h)
   219  			if e != nil {
   220  				LogError(e)
   221  			}
   222  			sbody += item
   223  		}
   224  	}
   225  	return sbody
   226  }
   227  
   228  var DockToID = map[string]int{
   229  	"leftOfNav":    0,
   230  	"rightOfNav":   1,
   231  	"topMenu":      2,
   232  	"rightSidebar": 3,
   233  	"footer":       4,
   234  }
   235  
   236  func BuildWidget2(dock int, h *Header) (sbody string) {
   237  	if !h.Theme.HasDockByID(dock) {
   238  		return ""
   239  	}
   240  	// Let themes forcibly override this slot
   241  	sbody = h.Theme.BuildDockByID(dock)
   242  	if sbody != "" {
   243  		return sbody
   244  	}
   245  
   246  	var widgets []*Widget
   247  	switch dock {
   248  	case 0:
   249  		widgets = Docks.LeftOfNav
   250  	case 1:
   251  		widgets = Docks.RightOfNav
   252  	case 2:
   253  		// 1 = id for the default menu
   254  		mhold, e := Menus.Get(1)
   255  		if e == nil {
   256  			e := mhold.Build(h.Writer, h.CurrentUser, h.Path)
   257  			if e != nil {
   258  				LogError(e)
   259  			}
   260  		}
   261  		return ""
   262  	case 3:
   263  		widgets = Docks.RightSidebar.Items
   264  	case 4:
   265  		widgets = Docks.Footer.Items
   266  	}
   267  
   268  	for _, w := range widgets {
   269  		if !w.Enabled {
   270  			continue
   271  		}
   272  		if w.Allowed(h.Zone, h.ZoneID) {
   273  			item, e := w.Build(h)
   274  			if e != nil {
   275  				LogError(e)
   276  			}
   277  			sbody += item
   278  		}
   279  	}
   280  	return sbody
   281  }
   282  
   283  func BuildWidget3(dock int, h *Header) {
   284  	if !h.Theme.HasDockByID(dock) {
   285  		return
   286  	}
   287  	// Let themes forcibly override this slot
   288  	if sbody := h.Theme.BuildDockByID(dock); sbody != "" {
   289  		h.Writer.Write(uutils.StringToBytes(sbody))
   290  		return
   291  	}
   292  
   293  	var widgets []*Widget
   294  	switch dock {
   295  	case 0:
   296  		widgets = Docks.LeftOfNav
   297  	case 1:
   298  		widgets = Docks.RightOfNav
   299  	case 2:
   300  		// 1 = id for the default menu
   301  		mhold, err := Menus.Get(1)
   302  		if err == nil {
   303  			err := mhold.Build(h.Writer, h.CurrentUser, h.Path)
   304  			if err != nil {
   305  				LogError(err)
   306  			}
   307  		}
   308  		return
   309  	case 3:
   310  		widgets = Docks.RightSidebar.Items
   311  	case 4:
   312  		widgets = Docks.Footer.Items
   313  	}
   314  
   315  	for _, w := range widgets {
   316  		if !w.Enabled {
   317  			continue
   318  		}
   319  		if w.Allowed(h.Zone, h.ZoneID) {
   320  			item, e := w.Build(h)
   321  			if e != nil {
   322  				LogError(e)
   323  			}
   324  			if item != "" {
   325  				h.Writer.Write(uutils.StringToBytes(item))
   326  			}
   327  		}
   328  	}
   329  }
   330  
   331  // TODO: Find a more optimimal way of doing this...
   332  func HasWidgets2(dock int, h *Header) bool {
   333  	if !h.Theme.HasDockByID(dock) {
   334  		return false
   335  	}
   336  
   337  	// Let themes forcibly override this slot
   338  	// TODO: Optimise this bit
   339  	sbody := h.Theme.BuildDockByID(dock)
   340  	if sbody != "" {
   341  		return true
   342  	}
   343  
   344  	var widgets []*Widget
   345  	switch dock {
   346  	case 0:
   347  		widgets = Docks.LeftOfNav
   348  	case 1:
   349  		widgets = Docks.RightOfNav
   350  	case 3:
   351  		widgets = Docks.RightSidebar.Items
   352  	case 4:
   353  		widgets = Docks.Footer.Items
   354  	}
   355  
   356  	wcount := 0
   357  	for _, widget := range widgets {
   358  		if !widget.Enabled {
   359  			continue
   360  		}
   361  		if widget.Allowed(h.Zone, h.ZoneID) {
   362  			wcount++
   363  		}
   364  	}
   365  	return wcount > 0
   366  }
   367  
   368  func getDockWidgets(dock string) (widgets []*Widget, e error) {
   369  	rows, e := widgetStmts.getDockList.Query(dock)
   370  	if e != nil {
   371  		return nil, e
   372  	}
   373  	defer rows.Close()
   374  
   375  	for rows.Next() {
   376  		w := &Widget{Position: 0, Side: dock}
   377  		e = rows.Scan(&w.ID, &w.Position, &w.Type, &w.Enabled, &w.Location, &w.RawBody)
   378  		if e != nil {
   379  			return nil, e
   380  		}
   381  		e = preparseWidget(w, w.RawBody)
   382  		if e != nil {
   383  			return nil, e
   384  		}
   385  		Widgets.set(w)
   386  		widgets = append(widgets, w)
   387  	}
   388  	return widgets, rows.Err()
   389  }
   390  
   391  // TODO: Make a store for this?
   392  func InitWidgets() (fi error) {
   393  	// TODO: Let themes set default values for widget docks, and let them lock in particular places with their stuff, e.g. leftOfNav and rightOfNav
   394  	f := func(name string) {
   395  		if fi != nil {
   396  			return
   397  		}
   398  		dock, e := getDockWidgets(name)
   399  		if e != nil {
   400  			fi = e
   401  			return
   402  		}
   403  		setDock(name, dock)
   404  	}
   405  
   406  	f("leftOfNav")
   407  	f("rightOfNav")
   408  	f("leftSidebar")
   409  	f("rightSidebar")
   410  	f("footer")
   411  	if fi != nil {
   412  		return fi
   413  	}
   414  
   415  	Tasks.Sec.Add(Docks.LeftSidebar.Scheduler.Tick)
   416  	Tasks.Sec.Add(Docks.RightSidebar.Scheduler.Tick)
   417  	Tasks.Sec.Add(Docks.Footer.Scheduler.Tick)
   418  
   419  	return nil
   420  }
   421  
   422  func releaseWidgets(ws []*Widget) {
   423  	for _, w := range ws {
   424  		if w.ShutdownFunc != nil {
   425  			w.ShutdownFunc(w)
   426  		}
   427  	}
   428  }
   429  
   430  // TODO: Use atomics
   431  func setDock(dock string, widgets []*Widget) {
   432  	dockHandle := func(dockWidgets []*Widget) {
   433  		DebugLog(dock, widgets)
   434  		releaseWidgets(dockWidgets)
   435  	}
   436  	dockHandle2 := func(dockWidgets WidgetDock) WidgetDock {
   437  		dockHandle(dockWidgets.Items)
   438  		if dockWidgets.Scheduler == nil {
   439  			dockWidgets.Scheduler = &WidgetScheduler{}
   440  		}
   441  		for _, widget := range widgets {
   442  			if widget.InitFunc != nil {
   443  				widget.InitFunc(widget, dockWidgets.Scheduler)
   444  			}
   445  		}
   446  		dockWidgets.Scheduler.Store()
   447  		return WidgetDock{widgets, dockWidgets.Scheduler}
   448  	}
   449  	widgetUpdateMutex.Lock()
   450  	defer widgetUpdateMutex.Unlock()
   451  	switch dock {
   452  	case "leftOfNav":
   453  		dockHandle(Docks.LeftOfNav)
   454  		Docks.LeftOfNav = widgets
   455  	case "rightOfNav":
   456  		dockHandle(Docks.RightOfNav)
   457  		Docks.RightOfNav = widgets
   458  	case "leftSidebar":
   459  		Docks.LeftSidebar = dockHandle2(Docks.LeftSidebar)
   460  	case "rightSidebar":
   461  		Docks.RightSidebar = dockHandle2(Docks.RightSidebar)
   462  	case "footer":
   463  		Docks.Footer = dockHandle2(Docks.Footer)
   464  	default:
   465  		fmt.Printf("bad dock '%s'\n", dock)
   466  		return
   467  	}
   468  }
   469  
   470  type WidgetScheduler struct {
   471  	widgets []*Widget
   472  	store   atomic.Value
   473  }
   474  
   475  func (s *WidgetScheduler) Add(w *Widget) {
   476  	s.widgets = append(s.widgets, w)
   477  }
   478  
   479  func (s *WidgetScheduler) Store() {
   480  	s.store.Store(s.widgets)
   481  }
   482  
   483  func (s *WidgetScheduler) Tick() error {
   484  	widgets := s.store.Load().([]*Widget)
   485  	for _, widget := range widgets {
   486  		if widget.TickFunc == nil {
   487  			continue
   488  		}
   489  		e := widget.TickFunc(widget)
   490  		if e != nil {
   491  			return errors.WithStack(e)
   492  		}
   493  	}
   494  	return nil
   495  }