mirror of
https://github.com/glanceapp/glance.git
synced 2025-06-21 18:31:24 +02:00
Merge pull request #602 from dvdpearson/dev
Dynamize manifest.json with configurable options
This commit is contained in:
commit
efd39e1f80
@ -268,6 +268,9 @@ branding:
|
|||||||
<p>Powered by <a href="https://github.com/glanceapp/glance">Glance</a></p>
|
<p>Powered by <a href="https://github.com/glanceapp/glance">Glance</a></p>
|
||||||
logo-url: /assets/logo.png
|
logo-url: /assets/logo.png
|
||||||
favicon-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
|
### Properties
|
||||||
@ -279,6 +282,9 @@ branding:
|
|||||||
| logo-text | string | no | G |
|
| logo-text | string | no | G |
|
||||||
| logo-url | string | no | |
|
| logo-url | string | no | |
|
||||||
| favicon-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`
|
#### `hide-footer`
|
||||||
Hides the footer when set to `true`.
|
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`
|
#### `favicon-url`
|
||||||
Specify a URL to a custom image to use for the favicon.
|
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
|
## 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.
|
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.
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"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 (
|
const (
|
||||||
hslHueMax = 360
|
hslHueMax = 360
|
||||||
@ -23,13 +23,13 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type hslColorField struct {
|
type hslColorField struct {
|
||||||
Hue uint16
|
Hue float64
|
||||||
Saturation uint8
|
Saturation float64
|
||||||
Lightness uint8
|
Lightness float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *hslColorField) String() string {
|
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 {
|
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)
|
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 {
|
if err != nil {
|
||||||
return err
|
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)
|
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 {
|
if err != nil {
|
||||||
return err
|
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)
|
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 {
|
if err != nil {
|
||||||
return err
|
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)
|
return fmt.Errorf("HSL lightness must be between 0 and %d", hslLightnessMax)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Hue = uint16(hue)
|
c.Hue = hue
|
||||||
c.Saturation = uint8(saturation)
|
c.Saturation = saturation
|
||||||
c.Lightness = uint8(lightness)
|
c.Lightness = lightness
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,6 @@ type config struct {
|
|||||||
Port uint16 `yaml:"port"`
|
Port uint16 `yaml:"port"`
|
||||||
AssetsPath string `yaml:"assets-path"`
|
AssetsPath string `yaml:"assets-path"`
|
||||||
BaseURL string `yaml:"base-url"`
|
BaseURL string `yaml:"base-url"`
|
||||||
StartedAt time.Time `yaml:"-"` // used in custom css file
|
|
||||||
} `yaml:"server"`
|
} `yaml:"server"`
|
||||||
|
|
||||||
Document struct {
|
Document struct {
|
||||||
@ -55,6 +54,9 @@ type config struct {
|
|||||||
LogoText string `yaml:"logo-text"`
|
LogoText string `yaml:"logo-text"`
|
||||||
LogoURL string `yaml:"logo-url"`
|
LogoURL string `yaml:"logo-url"`
|
||||||
FaviconURL string `yaml:"favicon-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"`
|
} `yaml:"branding"`
|
||||||
|
|
||||||
Pages []page `yaml:"pages"`
|
Pages []page `yaml:"pages"`
|
||||||
|
@ -18,15 +18,19 @@ var (
|
|||||||
pageTemplate = mustParseTemplate("page.html", "document.html")
|
pageTemplate = mustParseTemplate("page.html", "document.html")
|
||||||
pageContentTemplate = mustParseTemplate("page-content.html")
|
pageContentTemplate = mustParseTemplate("page-content.html")
|
||||||
pageThemeStyleTemplate = mustParseTemplate("theme-style.gotmpl")
|
pageThemeStyleTemplate = mustParseTemplate("theme-style.gotmpl")
|
||||||
|
manifestTemplate = mustParseTemplate("manifest.json")
|
||||||
)
|
)
|
||||||
|
|
||||||
const STATIC_ASSETS_CACHE_DURATION = 24 * time.Hour
|
const STATIC_ASSETS_CACHE_DURATION = 24 * time.Hour
|
||||||
|
|
||||||
type application struct {
|
type application struct {
|
||||||
Version string
|
Version string
|
||||||
|
CreatedAt time.Time
|
||||||
Config config
|
Config config
|
||||||
ParsedThemeStyle template.HTML
|
ParsedThemeStyle template.HTML
|
||||||
|
|
||||||
|
parsedManifest []byte
|
||||||
|
|
||||||
slugToPage map[string]*page
|
slugToPage map[string]*page
|
||||||
widgetByID map[uint64]widget
|
widgetByID map[uint64]widget
|
||||||
}
|
}
|
||||||
@ -34,6 +38,7 @@ type application struct {
|
|||||||
func newApplication(config *config) (*application, error) {
|
func newApplication(config *config) (*application, error) {
|
||||||
app := &application{
|
app := &application{
|
||||||
Version: buildVersion,
|
Version: buildVersion,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
Config: *config,
|
Config: *config,
|
||||||
slugToPage: make(map[string]*page),
|
slugToPage: make(map[string]*page),
|
||||||
widgetByID: make(map[uint64]widget),
|
widgetByID: make(map[uint64]widget),
|
||||||
@ -42,7 +47,7 @@ func newApplication(config *config) (*application, error) {
|
|||||||
app.slugToPage[""] = &config.Pages[0]
|
app.slugToPage[""] = &config.Pages[0]
|
||||||
|
|
||||||
providers := &widgetProviders{
|
providers := &widgetProviders{
|
||||||
assetResolver: app.AssetPath,
|
assetResolver: app.StaticAssetPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@ -88,15 +93,36 @@ func newApplication(config *config) (*application, error) {
|
|||||||
config = &app.Config
|
config = &app.Config
|
||||||
|
|
||||||
config.Server.BaseURL = strings.TrimRight(config.Server.BaseURL, "/")
|
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 == "" {
|
if config.Branding.FaviconURL == "" {
|
||||||
config.Branding.FaviconURL = app.AssetPath("favicon.png")
|
config.Branding.FaviconURL = app.StaticAssetPath("favicon.png")
|
||||||
} else {
|
} 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
|
return app, nil
|
||||||
}
|
}
|
||||||
@ -126,7 +152,7 @@ func (p *page) updateOutdatedWidgets() {
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *application) transformUserDefinedAssetPath(path string) string {
|
func (a *application) resolveUserDefinedAssetPath(path string) string {
|
||||||
if strings.HasPrefix(path, "/assets/") {
|
if strings.HasPrefix(path, "/assets/") {
|
||||||
return a.Config.Server.BaseURL + path
|
return a.Config.Server.BaseURL + path
|
||||||
}
|
}
|
||||||
@ -220,10 +246,15 @@ func (a *application) handleWidgetRequest(w http.ResponseWriter, r *http.Request
|
|||||||
widget.handleRequest(w, r)
|
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
|
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) {
|
func (a *application) server() (func() error, func() error) {
|
||||||
// TODO: add gzip support, static files must have their gzipped contents cached
|
// TODO: add gzip support, static files must have their gzipped contents cached
|
||||||
// TODO: add HTTPS support
|
// 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",
|
"public, max-age=%d",
|
||||||
int(STATIC_ASSETS_CACHE_DURATION.Seconds()),
|
int(STATIC_ASSETS_CACHE_DURATION.Seconds()),
|
||||||
)
|
)
|
||||||
|
|
||||||
mux.HandleFunc(fmt.Sprintf("GET /static/%s/css/bundle.css", staticFSHash), func(w http.ResponseWriter, r *http.Request) {
|
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.Header().Add("Content-Type", "text/css; charset=utf-8")
|
||||||
w.Write(bundledCSSContents)
|
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
|
var absAssetsPath string
|
||||||
if a.Config.Server.AssetsPath != "" {
|
if a.Config.Server.AssetsPath != "" {
|
||||||
absAssetsPath, _ = filepath.Abs(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 {
|
start := func() error {
|
||||||
a.Config.Server.StartedAt = time.Now()
|
|
||||||
log.Printf("Starting server on %s:%d (base-url: \"%s\", assets-path: \"%s\")\n",
|
log.Printf("Starting server on %s:%d (base-url: \"%s\", assets-path: \"%s\")\n",
|
||||||
a.Config.Server.Host,
|
a.Config.Server.Host,
|
||||||
a.Config.Server.Port,
|
a.Config.Server.Port,
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Glance",
|
|
||||||
"display": "standalone",
|
|
||||||
"background_color": "#151519",
|
|
||||||
"scope": "/",
|
|
||||||
"start_url": "/",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "app-icon.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -10,13 +10,13 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<meta name="apple-mobile-web-app-title" content="Glance">
|
<meta name="apple-mobile-web-app-title" content="{{ .App.Config.Branding.AppName }}">
|
||||||
<meta name="theme-color" content="{{ if ne nil .App.Config.Theme.BackgroundColor }}{{ .App.Config.Theme.BackgroundColor }}{{ else }}hsl(240, 8%, 9%){{ end }}">
|
<meta name="theme-color" content="{{ if ne nil .App.Config.Theme.BackgroundColor }}{{ .App.Config.Theme.BackgroundColor }}{{ else }}hsl(240, 8%, 9%){{ end }}">
|
||||||
<link rel="apple-touch-icon" sizes="512x512" href='{{ .App.AssetPath "app-icon.png" }}'>
|
<link rel="apple-touch-icon" sizes="512x512" href='{{ .App.Config.Branding.AppIconURL }}'>
|
||||||
<link rel="manifest" href='{{ .App.AssetPath "manifest.json" }}'>
|
<link rel="manifest" href='{{ .App.VersionedAssetPath "manifest.json" }}'>
|
||||||
<link rel="icon" type="image/png" href="{{ .App.Config.Branding.FaviconURL }}" />
|
<link rel="icon" type="image/png" href="{{ .App.Config.Branding.FaviconURL }}" />
|
||||||
<link rel="stylesheet" href='{{ .App.AssetPath "css/bundle.css" }}'>
|
<link rel="stylesheet" href='{{ .App.StaticAssetPath "css/bundle.css" }}'>
|
||||||
<script type="module" src='{{ .App.AssetPath "js/main.js" }}'></script>
|
<script type="module" src='{{ .App.StaticAssetPath "js/main.js" }}'></script>
|
||||||
{{ block "document-head-after" . }}{{ end }}
|
{{ block "document-head-after" . }}{{ end }}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
14
internal/glance/templates/manifest.json
Normal file
14
internal/glance/templates/manifest.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -17,7 +17,7 @@
|
|||||||
{{ .App.ParsedThemeStyle }}
|
{{ .App.ParsedThemeStyle }}
|
||||||
|
|
||||||
{{ if ne "" .App.Config.Theme.CustomCSSFile }}
|
{{ if ne "" .App.Config.Theme.CustomCSSFile }}
|
||||||
<link rel="stylesheet" href="{{ .App.Config.Theme.CustomCSSFile }}?v={{ .App.Config.Server.StartedAt.Unix }}">
|
<link rel="stylesheet" href="{{ .App.Config.Theme.CustomCSSFile }}?v={{ .App.CreatedAt.Unix }}">
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ if ne "" .App.Config.Document.Head }}{{ .App.Config.Document.Head }}{{ end }}
|
{{ if ne "" .App.Config.Document.Head }}{{ .App.Config.Document.Head }}{{ end }}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user