diff --git a/docs/configuration.md b/docs/configuration.md index e51ff36..2ba1204 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -268,6 +268,9 @@ branding:

Powered by Glance

logo-url: /assets/logo.png favicon-url: /assets/logo.png + app-name: "My Dashboard" + app-icon-url: "/assets/app-icon.png" + app-background-color: "#151519" ``` ### Properties @@ -279,6 +282,9 @@ branding: | logo-text | string | no | G | | logo-url | string | no | | | favicon-url | string | no | | +| app-name | string | no | Glance | +| app-icon-url | string | no | Glance's default icon | +| app-background-color | string | no | Glance's default background color | #### `hide-footer` Hides the footer when set to `true`. @@ -295,6 +301,15 @@ Specify a URL to a custom image to use instead of the "G" found in the navigatio #### `favicon-url` Specify a URL to a custom image to use for the favicon. +#### `app-name` +Specify the name of the web app shown in browser tab and PWA. + +#### `app-icon-url` +Specify URL for PWA and browser tab icon (512x512 PNG). + +#### `app-background-color` +Specify background color for PWA. Must be a valid CSS color. + ## Theme Theming is done through a top level `theme` property. Values for the colors are in [HSL](https://giggster.com/guide/basics/hue-saturation-lightness/) (hue, saturation, lightness) format. You can use a color picker [like this one](https://hslpicker.com/) to convert colors from other formats to HSL. The values are separated by a space and `%` is not required for any of the numbers. diff --git a/internal/glance/config-fields.go b/internal/glance/config-fields.go index cd244e5..f3a1649 100644 --- a/internal/glance/config-fields.go +++ b/internal/glance/config-fields.go @@ -14,7 +14,7 @@ import ( "gopkg.in/yaml.v3" ) -var hslColorFieldPattern = regexp.MustCompile(`^(?:hsla?\()?(\d{1,3})(?: |,)+(\d{1,3})%?(?: |,)+(\d{1,3})%?\)?$`) +var hslColorFieldPattern = regexp.MustCompile(`^(?:hsla?\()?([\d\.]+)(?: |,)+([\d\.]+)%?(?: |,)+([\d\.]+)%?\)?$`) const ( hslHueMax = 360 @@ -23,13 +23,13 @@ const ( ) type hslColorField struct { - Hue uint16 - Saturation uint8 - Lightness uint8 + Hue float64 + Saturation float64 + Lightness float64 } func (c *hslColorField) String() string { - return fmt.Sprintf("hsl(%d, %d%%, %d%%)", c.Hue, c.Saturation, c.Lightness) + return fmt.Sprintf("hsl(%.1f, %.1f%%, %.1f%%)", c.Hue, c.Saturation, c.Lightness) } func (c *hslColorField) UnmarshalYAML(node *yaml.Node) error { @@ -45,7 +45,7 @@ func (c *hslColorField) UnmarshalYAML(node *yaml.Node) error { return fmt.Errorf("invalid HSL color format: %s", value) } - hue, err := strconv.ParseUint(matches[1], 10, 16) + hue, err := strconv.ParseFloat(matches[1], 64) if err != nil { return err } @@ -54,7 +54,7 @@ func (c *hslColorField) UnmarshalYAML(node *yaml.Node) error { return fmt.Errorf("HSL hue must be between 0 and %d", hslHueMax) } - saturation, err := strconv.ParseUint(matches[2], 10, 8) + saturation, err := strconv.ParseFloat(matches[2], 64) if err != nil { return err } @@ -63,7 +63,7 @@ func (c *hslColorField) UnmarshalYAML(node *yaml.Node) error { return fmt.Errorf("HSL saturation must be between 0 and %d", hslSaturationMax) } - lightness, err := strconv.ParseUint(matches[3], 10, 8) + lightness, err := strconv.ParseFloat(matches[3], 64) if err != nil { return err } @@ -72,9 +72,9 @@ func (c *hslColorField) UnmarshalYAML(node *yaml.Node) error { return fmt.Errorf("HSL lightness must be between 0 and %d", hslLightnessMax) } - c.Hue = uint16(hue) - c.Saturation = uint8(saturation) - c.Lightness = uint8(lightness) + c.Hue = hue + c.Saturation = saturation + c.Lightness = lightness return nil } diff --git a/internal/glance/config.go b/internal/glance/config.go index a63afff..2053ff9 100644 --- a/internal/glance/config.go +++ b/internal/glance/config.go @@ -27,11 +27,10 @@ const ( type config struct { Server struct { - Host string `yaml:"host"` - Port uint16 `yaml:"port"` - AssetsPath string `yaml:"assets-path"` - BaseURL string `yaml:"base-url"` - StartedAt time.Time `yaml:"-"` // used in custom css file + Host string `yaml:"host"` + Port uint16 `yaml:"port"` + AssetsPath string `yaml:"assets-path"` + BaseURL string `yaml:"base-url"` } `yaml:"server"` Document struct { @@ -50,11 +49,14 @@ type config struct { } `yaml:"theme"` Branding struct { - HideFooter bool `yaml:"hide-footer"` - CustomFooter template.HTML `yaml:"custom-footer"` - LogoText string `yaml:"logo-text"` - LogoURL string `yaml:"logo-url"` - FaviconURL string `yaml:"favicon-url"` + HideFooter bool `yaml:"hide-footer"` + CustomFooter template.HTML `yaml:"custom-footer"` + LogoText string `yaml:"logo-text"` + LogoURL string `yaml:"logo-url"` + FaviconURL string `yaml:"favicon-url"` + AppName string `yaml:"app-name"` + AppIconURL string `yaml:"app-icon-url"` + AppBackgroundColor string `yaml:"app-background-color"` } `yaml:"branding"` Pages []page `yaml:"pages"` diff --git a/internal/glance/glance.go b/internal/glance/glance.go index ab63536..903cfbc 100644 --- a/internal/glance/glance.go +++ b/internal/glance/glance.go @@ -18,15 +18,19 @@ 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 } @@ -34,6 +38,7 @@ type application struct { 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), @@ -42,7 +47,7 @@ func newApplication(config *config) (*application, error) { app.slugToPage[""] = &config.Pages[0] providers := &widgetProviders{ - assetResolver: app.AssetPath, + assetResolver: app.StaticAssetPath, } var err error @@ -88,15 +93,36 @@ func newApplication(config *config) (*application, error) { config = &app.Config config.Server.BaseURL = strings.TrimRight(config.Server.BaseURL, "/") - config.Theme.CustomCSSFile = app.transformUserDefinedAssetPath(config.Theme.CustomCSSFile) + config.Theme.CustomCSSFile = app.resolveUserDefinedAssetPath(config.Theme.CustomCSSFile) + config.Branding.LogoURL = app.resolveUserDefinedAssetPath(config.Branding.LogoURL) if config.Branding.FaviconURL == "" { - config.Branding.FaviconURL = app.AssetPath("favicon.png") + config.Branding.FaviconURL = app.StaticAssetPath("favicon.png") } else { - config.Branding.FaviconURL = app.transformUserDefinedAssetPath(config.Branding.FaviconURL) + config.Branding.FaviconURL = app.resolveUserDefinedAssetPath(config.Branding.FaviconURL) } - config.Branding.LogoURL = app.transformUserDefinedAssetPath(config.Branding.LogoURL) + 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 = ternary( + config.Theme.BackgroundColor != nil, + config.Theme.BackgroundColor.String(), + "hsl(240, 8%, 9%)", + ) + } + + 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 } @@ -126,7 +152,7 @@ func (p *page) updateOutdatedWidgets() { wg.Wait() } -func (a *application) transformUserDefinedAssetPath(path string) string { +func (a *application) resolveUserDefinedAssetPath(path string) string { if strings.HasPrefix(path, "/assets/") { return a.Config.Server.BaseURL + path } @@ -220,10 +246,15 @@ func (a *application) handleWidgetRequest(w http.ResponseWriter, r *http.Request widget.handleRequest(w, r) } -func (a *application) AssetPath(asset string) string { +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 @@ -246,17 +277,23 @@ func (a *application) server() (func() error, func() error) { ), ) - cssBundleCacheControlValue := fmt.Sprintf( + 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", cssBundleCacheControlValue) + 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) @@ -270,7 +307,6 @@ func (a *application) server() (func() error, func() error) { } start := func() error { - a.Config.Server.StartedAt = time.Now() log.Printf("Starting server on %s:%d (base-url: \"%s\", assets-path: \"%s\")\n", a.Config.Server.Host, a.Config.Server.Port, diff --git a/internal/glance/static/manifest.json b/internal/glance/static/manifest.json deleted file mode 100644 index 42e8213..0000000 --- a/internal/glance/static/manifest.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "Glance", - "display": "standalone", - "background_color": "#151519", - "scope": "/", - "start_url": "/", - "icons": [ - { - "src": "app-icon.png", - "type": "image/png", - "sizes": "512x512" - } - ] -} diff --git a/internal/glance/templates/document.html b/internal/glance/templates/document.html index a6896e0..26beab9 100644 --- a/internal/glance/templates/document.html +++ b/internal/glance/templates/document.html @@ -10,13 +10,13 @@ - + - - + + - - + + {{ block "document-head-after" . }}{{ end }} diff --git a/internal/glance/templates/manifest.json b/internal/glance/templates/manifest.json new file mode 100644 index 0000000..eae4d19 --- /dev/null +++ b/internal/glance/templates/manifest.json @@ -0,0 +1,14 @@ +{ + "name": "{{ .App.Config.Branding.AppName }}", + "display": "standalone", + "background_color": "{{ .App.Config.Branding.AppBackgroundColor }}", + "scope": "/", + "start_url": "/", + "icons": [ + { + "src": "{{ .App.Config.Branding.AppIconURL }}", + "type": "image/png", + "sizes": "512x512" + } + ] +} diff --git a/internal/glance/templates/page.html b/internal/glance/templates/page.html index 8e1ddae..24baf78 100644 --- a/internal/glance/templates/page.html +++ b/internal/glance/templates/page.html @@ -17,7 +17,7 @@ {{ .App.ParsedThemeStyle }} {{ if ne "" .App.Config.Theme.CustomCSSFile }} - + {{ end }} {{ if ne "" .App.Config.Document.Head }}{{ .App.Config.Document.Head }}{{ end }}