gosora/common/theme_list.go
Azareal 5fe0c8c95b add BuildWidget2 to avoid allocing as many strings in templates for docks
dynamic load /topics/ page where possible
reduce boilerplate
2020-04-12 15:00:15 +10:00

321 lines
8.3 KiB
Go

package common
import (
"database/sql"
"encoding/json"
"errors"
"html/template"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
qgen "github.com/Azareal/Gosora/query_gen"
)
// TODO: Something more thread-safe
type ThemeList map[string]*Theme
var Themes ThemeList = make(map[string]*Theme) // ? Refactor this into a store?
var DefaultThemeBox atomic.Value
var ChangeDefaultThemeMutex sync.Mutex
// TODO: Fallback to a random theme if this doesn't exist, so admins can remove themes they don't use
// TODO: Use this when the default theme doesn't exist
var fallbackTheme = "cosora"
var overridenTemplates = make(map[string]bool) // ? What is this used for?
type ThemeStmts struct {
getAll *sql.Stmt
isDefault *sql.Stmt
update *sql.Stmt
add *sql.Stmt
}
var themeStmts ThemeStmts
func init() {
DbInits.Add(func(acc *qgen.Accumulator) error {
t := "themes"
themeStmts = ThemeStmts{
getAll: acc.Select(t).Columns("uname,default").Prepare(),
isDefault: acc.Select(t).Columns("default").Where("uname=?").Prepare(),
update: acc.Update(t).Set("default=?").Where("uname=?").Prepare(),
add: acc.Insert(t).Columns("uname,default").Fields("?,?").Prepare(),
}
return acc.FirstError()
})
}
func NewThemeList() (themes ThemeList, err error) {
themes = make(map[string]*Theme)
themeFiles, err := ioutil.ReadDir("./themes")
if err != nil {
return themes, err
}
if len(themeFiles) == 0 {
return themes, errors.New("You don't have any themes")
}
var lastTheme, defaultTheme string
for _, themeFile := range themeFiles {
if !themeFile.IsDir() {
continue
}
themeName := themeFile.Name()
log.Printf("Adding theme '%s'", themeName)
themePath := "./themes/" + themeName
themeFile, err := ioutil.ReadFile(themePath + "/theme.json")
if err != nil {
return themes, err
}
theme := &Theme{}
err = json.Unmarshal(themeFile, theme)
if err != nil {
return themes, err
}
if theme.Name == "" {
return themes, errors.New("Theme " + themePath + " doesn't have a name set in theme.json")
}
if theme.Name == fallbackTheme {
defaultTheme = fallbackTheme
}
lastTheme = theme.Name
// TODO: Implement the static file part of this and fsnotify
if theme.Path != "" {
log.Print("Resolving redirect to " + theme.Path)
themeFile, err := ioutil.ReadFile(theme.Path + "/theme.json")
if err != nil {
return themes, err
}
theme = &Theme{Path: theme.Path}
err = json.Unmarshal(themeFile, theme)
if err != nil {
return themes, err
}
} else {
theme.Path = themePath
}
theme.Active = false // Set this to false, just in case someone explicitly overrode this value in the JSON file
// TODO: Let the theme specify where it's resources are via the JSON file?
// TODO: Let the theme inherit CSS from another theme?
// ? - This might not be too helpful, as it only searches for /public/ and not if /public/ is empty. Still, it might help some people with a slightly less cryptic error
log.Print(theme.Path + "/public/")
_, err = os.Stat(theme.Path + "/public/")
if err != nil {
if os.IsNotExist(err) {
return themes, errors.New("We couldn't find this theme's resources. E.g. the /public/ folder.")
} else {
log.Print("We weren't able to access this theme's resources due to a permissions issue or some other problem")
return themes, err
}
}
if theme.FullImage != "" {
DebugLog("Adding theme image")
err = StaticFiles.Add(theme.Path+"/"+theme.FullImage, themePath)
if err != nil {
return themes, err
}
}
theme.TemplatesMap = make(map[string]string)
theme.TmplPtr = make(map[string]interface{})
if theme.Templates != nil {
for _, themeTmpl := range theme.Templates {
theme.TemplatesMap[themeTmpl.Name] = themeTmpl.Source
theme.TmplPtr[themeTmpl.Name] = TmplPtrMap["o_"+themeTmpl.Source]
}
}
theme.IntTmplHandle = DefaultTemplates
overrides, err := ioutil.ReadDir(theme.Path + "/overrides/")
if err != nil && !os.IsNotExist(err) {
return themes, err
}
if len(overrides) > 0 {
overCount := 0
theme.OverridenMap = make(map[string]bool)
for _, override := range overrides {
if override.IsDir() {
continue
}
ext := filepath.Ext(themePath + "/overrides/" + override.Name())
log.Print("attempting to add " + themePath + "/overrides/" + override.Name())
if ext != ".html" {
log.Print("not a html file")
continue
}
overCount++
nosuf := strings.TrimSuffix(override.Name(), ext)
theme.OverridenTemplates = append(theme.OverridenTemplates, nosuf)
theme.OverridenMap[nosuf] = true
//theme.TmplPtr[nosuf] = TmplPtrMap["o_"+nosuf]
log.Print("succeeded")
}
localTmpls := template.New("")
err = loadTemplates(localTmpls, theme.Name)
if err != nil {
return themes, err
}
theme.IntTmplHandle = localTmpls
log.Printf("theme.OverridenTemplates: %+v\n", theme.OverridenTemplates)
log.Printf("theme.IntTmplHandle: %+v\n", theme.IntTmplHandle)
} else {
log.Print("no overrides for " + theme.Name)
}
for i, res := range theme.Resources {
ext := filepath.Ext(res.Name)
switch ext {
case ".css":
res.Type = ResTypeSheet
case ".js":
res.Type = ResTypeScript
}
switch res.Location {
case "global":
res.LocID = LocGlobal
case "frontend":
res.LocID = LocFront
case "panel":
res.LocID = LocPanel
}
theme.Resources[i] = res
}
for _, dock := range theme.Docks {
id, ok := DockToID[dock]
if ok {
theme.DocksID = append(theme.DocksID, id)
}
}
// TODO: Bind the built template, or an interpreted one for any dock overrides this theme has
themes[theme.Name] = theme
}
if defaultTheme == "" {
defaultTheme = lastTheme
}
DefaultThemeBox.Store(defaultTheme)
return themes, nil
}
// TODO: Make the initThemes and LoadThemes functions less confusing
// ? - Delete themes which no longer exist in the themes folder from the database?
func (t ThemeList) LoadActiveStatus() error {
ChangeDefaultThemeMutex.Lock()
defer ChangeDefaultThemeMutex.Unlock()
rows, err := themeStmts.getAll.Query()
if err != nil {
return err
}
defer rows.Close()
var uname string
var defaultThemeSwitch bool
for rows.Next() {
err = rows.Scan(&uname, &defaultThemeSwitch)
if err != nil {
return err
}
// Was the theme deleted at some point?
theme, ok := t[uname]
if !ok {
continue
}
if defaultThemeSwitch {
DebugLogf("Loading the default theme '%s'", theme.Name)
theme.Active = true
DefaultThemeBox.Store(theme.Name)
theme.MapTemplates()
} else {
DebugLogf("Loading the theme '%s'", theme.Name)
theme.Active = false
}
t[uname] = theme
}
return rows.Err()
}
func (t ThemeList) LoadStaticFiles() error {
for _, theme := range t {
if err := theme.LoadStaticFiles(); err != nil {
return err
}
}
return nil
}
func ResetTemplateOverrides() {
log.Print("Resetting the template overrides")
for name := range overridenTemplates {
log.Print("Resetting '" + name + "' template override")
originPointer, ok := TmplPtrMap["o_"+name]
if !ok {
log.Print("The origin template doesn't exist!")
return
}
destTmplPtr, ok := TmplPtrMap[name]
if !ok {
log.Print("The destination template doesn't exist!")
return
}
// Not really a pointer, more of a function handle, an artifact from one of the earlier versions of themes.go
oPtr, ok := originPointer.(func(interface{}, io.Writer) error)
if !ok {
log.Print("name: ", name)
LogError(errors.New("Unknown destination template type!"))
return
}
dPtr, ok := destTmplPtr.(*func(interface{}, io.Writer) error)
if !ok {
LogError(errors.New("The source and destination templates are incompatible"))
return
}
*dPtr = oPtr
log.Print("The template override was reset")
}
overridenTemplates = make(map[string]bool)
log.Print("All of the template overrides have been reset")
}
// CreateThemeTemplate creates a theme template on the current default theme
func CreateThemeTemplate(theme, name string) {
Themes[theme].TmplPtr[name] = func(pi Page, w http.ResponseWriter) error {
mapping, ok := Themes[DefaultThemeBox.Load().(string)].TemplatesMap[name]
if !ok {
mapping = name
}
return DefaultTemplates.ExecuteTemplate(w, mapping+".html", pi)
}
}
func GetDefaultThemeName() string {
return DefaultThemeBox.Load().(string)
}
func SetDefaultThemeName(name string) {
DefaultThemeBox.Store(name)
}