mirror of
https://github.com/Xevion/glance.git
synced 2025-12-07 11:15:12 -06:00
It looks like support for hsl color values within the manifest is limited so we have to convert the colors to hex
332 lines
7.7 KiB
Go
332 lines
7.7 KiB
Go
package glance
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"html/template"
|
|
"log"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
pageTemplate = mustParseTemplate("page.html", "document.html")
|
|
pageContentTemplate = mustParseTemplate("page-content.html")
|
|
pageThemeStyleTemplate = mustParseTemplate("theme-style.gotmpl")
|
|
manifestTemplate = mustParseTemplate("manifest.json")
|
|
)
|
|
|
|
const STATIC_ASSETS_CACHE_DURATION = 24 * time.Hour
|
|
|
|
type application struct {
|
|
Version string
|
|
CreatedAt time.Time
|
|
Config config
|
|
ParsedThemeStyle template.HTML
|
|
|
|
parsedManifest []byte
|
|
|
|
slugToPage map[string]*page
|
|
widgetByID map[uint64]widget
|
|
}
|
|
|
|
func newApplication(config *config) (*application, error) {
|
|
app := &application{
|
|
Version: buildVersion,
|
|
CreatedAt: time.Now(),
|
|
Config: *config,
|
|
slugToPage: make(map[string]*page),
|
|
widgetByID: make(map[uint64]widget),
|
|
}
|
|
|
|
app.slugToPage[""] = &config.Pages[0]
|
|
|
|
providers := &widgetProviders{
|
|
assetResolver: app.StaticAssetPath,
|
|
}
|
|
|
|
var err error
|
|
app.ParsedThemeStyle, err = executeTemplateToHTML(pageThemeStyleTemplate, &app.Config.Theme)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing theme style: %v", err)
|
|
}
|
|
|
|
for p := range config.Pages {
|
|
page := &config.Pages[p]
|
|
page.PrimaryColumnIndex = -1
|
|
|
|
if page.Slug == "" {
|
|
page.Slug = titleToSlug(page.Title)
|
|
}
|
|
|
|
app.slugToPage[page.Slug] = page
|
|
|
|
if page.Width == "default" {
|
|
page.Width = ""
|
|
}
|
|
|
|
if page.DesktopNavigationWidth == "" && page.DesktopNavigationWidth != "default" {
|
|
page.DesktopNavigationWidth = page.Width
|
|
}
|
|
|
|
for c := range page.Columns {
|
|
column := &page.Columns[c]
|
|
|
|
if page.PrimaryColumnIndex == -1 && column.Size == "full" {
|
|
page.PrimaryColumnIndex = int8(c)
|
|
}
|
|
|
|
for w := range column.Widgets {
|
|
widget := column.Widgets[w]
|
|
app.widgetByID[widget.GetID()] = widget
|
|
|
|
widget.setProviders(providers)
|
|
}
|
|
}
|
|
}
|
|
|
|
config = &app.Config
|
|
|
|
config.Server.BaseURL = strings.TrimRight(config.Server.BaseURL, "/")
|
|
config.Theme.CustomCSSFile = app.resolveUserDefinedAssetPath(config.Theme.CustomCSSFile)
|
|
config.Branding.LogoURL = app.resolveUserDefinedAssetPath(config.Branding.LogoURL)
|
|
|
|
if config.Theme.BackgroundColor != nil {
|
|
config.Theme.BackgroundColorAsHex = config.Theme.BackgroundColor.ToHex()
|
|
} else {
|
|
config.Theme.BackgroundColorAsHex = "#151519"
|
|
}
|
|
|
|
if config.Branding.FaviconURL == "" {
|
|
config.Branding.FaviconURL = app.StaticAssetPath("favicon.png")
|
|
} else {
|
|
config.Branding.FaviconURL = app.resolveUserDefinedAssetPath(config.Branding.FaviconURL)
|
|
}
|
|
|
|
if config.Branding.AppName == "" {
|
|
config.Branding.AppName = "Glance"
|
|
}
|
|
|
|
if config.Branding.AppIconURL == "" {
|
|
config.Branding.AppIconURL = app.StaticAssetPath("app-icon.png")
|
|
}
|
|
|
|
if config.Branding.AppBackgroundColor == "" {
|
|
config.Branding.AppBackgroundColor = config.Theme.BackgroundColorAsHex
|
|
}
|
|
|
|
var manifestWriter bytes.Buffer
|
|
if err := manifestTemplate.Execute(&manifestWriter, pageTemplateData{App: app}); err != nil {
|
|
return nil, fmt.Errorf("parsing manifest.json: %v", err)
|
|
}
|
|
app.parsedManifest = manifestWriter.Bytes()
|
|
|
|
return app, nil
|
|
}
|
|
|
|
func (p *page) updateOutdatedWidgets() {
|
|
now := time.Now()
|
|
|
|
var wg sync.WaitGroup
|
|
context := context.Background()
|
|
|
|
for c := range p.Columns {
|
|
for w := range p.Columns[c].Widgets {
|
|
widget := p.Columns[c].Widgets[w]
|
|
|
|
if !widget.requiresUpdate(&now) {
|
|
continue
|
|
}
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
widget.update(context)
|
|
}()
|
|
}
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func (a *application) resolveUserDefinedAssetPath(path string) string {
|
|
if strings.HasPrefix(path, "/assets/") {
|
|
return a.Config.Server.BaseURL + path
|
|
}
|
|
|
|
return path
|
|
}
|
|
|
|
type pageTemplateData struct {
|
|
App *application
|
|
Page *page
|
|
}
|
|
|
|
func (a *application) handlePageRequest(w http.ResponseWriter, r *http.Request) {
|
|
page, exists := a.slugToPage[r.PathValue("page")]
|
|
|
|
if !exists {
|
|
a.handleNotFound(w, r)
|
|
return
|
|
}
|
|
|
|
pageData := pageTemplateData{
|
|
Page: page,
|
|
App: a,
|
|
}
|
|
|
|
var responseBytes bytes.Buffer
|
|
err := pageTemplate.Execute(&responseBytes, pageData)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte(err.Error()))
|
|
return
|
|
}
|
|
|
|
w.Write(responseBytes.Bytes())
|
|
}
|
|
|
|
func (a *application) handlePageContentRequest(w http.ResponseWriter, r *http.Request) {
|
|
page, exists := a.slugToPage[r.PathValue("page")]
|
|
|
|
if !exists {
|
|
a.handleNotFound(w, r)
|
|
return
|
|
}
|
|
|
|
pageData := pageTemplateData{
|
|
Page: page,
|
|
}
|
|
|
|
var err error
|
|
var responseBytes bytes.Buffer
|
|
|
|
func() {
|
|
page.mu.Lock()
|
|
defer page.mu.Unlock()
|
|
|
|
page.updateOutdatedWidgets()
|
|
err = pageContentTemplate.Execute(&responseBytes, pageData)
|
|
}()
|
|
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte(err.Error()))
|
|
return
|
|
}
|
|
|
|
w.Write(responseBytes.Bytes())
|
|
}
|
|
|
|
func (a *application) handleNotFound(w http.ResponseWriter, _ *http.Request) {
|
|
// TODO: add proper not found page
|
|
w.WriteHeader(http.StatusNotFound)
|
|
w.Write([]byte("Page not found"))
|
|
}
|
|
|
|
func (a *application) handleWidgetRequest(w http.ResponseWriter, r *http.Request) {
|
|
widgetValue := r.PathValue("widget")
|
|
|
|
widgetID, err := strconv.ParseUint(widgetValue, 10, 64)
|
|
if err != nil {
|
|
a.handleNotFound(w, r)
|
|
return
|
|
}
|
|
|
|
widget, exists := a.widgetByID[widgetID]
|
|
|
|
if !exists {
|
|
a.handleNotFound(w, r)
|
|
return
|
|
}
|
|
|
|
widget.handleRequest(w, r)
|
|
}
|
|
|
|
func (a *application) StaticAssetPath(asset string) string {
|
|
return a.Config.Server.BaseURL + "/static/" + staticFSHash + "/" + asset
|
|
}
|
|
|
|
func (a *application) VersionedAssetPath(asset string) string {
|
|
return a.Config.Server.BaseURL + asset +
|
|
"?v=" + strconv.FormatInt(a.CreatedAt.Unix(), 10)
|
|
}
|
|
|
|
func (a *application) server() (func() error, func() error) {
|
|
// TODO: add gzip support, static files must have their gzipped contents cached
|
|
// TODO: add HTTPS support
|
|
mux := http.NewServeMux()
|
|
|
|
mux.HandleFunc("GET /{$}", a.handlePageRequest)
|
|
mux.HandleFunc("GET /{page}", a.handlePageRequest)
|
|
|
|
mux.HandleFunc("GET /api/pages/{page}/content/{$}", a.handlePageContentRequest)
|
|
mux.HandleFunc("/api/widgets/{widget}/{path...}", a.handleWidgetRequest)
|
|
mux.HandleFunc("GET /api/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
mux.Handle(
|
|
fmt.Sprintf("GET /static/%s/{path...}", staticFSHash),
|
|
http.StripPrefix(
|
|
"/static/"+staticFSHash,
|
|
fileServerWithCache(http.FS(staticFS), STATIC_ASSETS_CACHE_DURATION),
|
|
),
|
|
)
|
|
|
|
assetCacheControlValue := fmt.Sprintf(
|
|
"public, max-age=%d",
|
|
int(STATIC_ASSETS_CACHE_DURATION.Seconds()),
|
|
)
|
|
|
|
mux.HandleFunc(fmt.Sprintf("GET /static/%s/css/bundle.css", staticFSHash), func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Add("Cache-Control", assetCacheControlValue)
|
|
w.Header().Add("Content-Type", "text/css; charset=utf-8")
|
|
w.Write(bundledCSSContents)
|
|
})
|
|
|
|
mux.HandleFunc("GET /manifest.json", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Add("Cache-Control", assetCacheControlValue)
|
|
w.Header().Add("Content-Type", "application/json")
|
|
w.Write(a.parsedManifest)
|
|
})
|
|
|
|
var absAssetsPath string
|
|
if a.Config.Server.AssetsPath != "" {
|
|
absAssetsPath, _ = filepath.Abs(a.Config.Server.AssetsPath)
|
|
assetsFS := fileServerWithCache(http.Dir(a.Config.Server.AssetsPath), 2*time.Hour)
|
|
mux.Handle("/assets/{path...}", http.StripPrefix("/assets/", assetsFS))
|
|
}
|
|
|
|
server := http.Server{
|
|
Addr: fmt.Sprintf("%s:%d", a.Config.Server.Host, a.Config.Server.Port),
|
|
Handler: mux,
|
|
}
|
|
|
|
start := func() error {
|
|
log.Printf("Starting server on %s:%d (base-url: \"%s\", assets-path: \"%s\")\n",
|
|
a.Config.Server.Host,
|
|
a.Config.Server.Port,
|
|
a.Config.Server.BaseURL,
|
|
absAssetsPath,
|
|
)
|
|
|
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
stop := func() error {
|
|
return server.Close()
|
|
}
|
|
|
|
return start, stop
|
|
}
|