PWA: Create manifest.json in code without using a template #3181
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
77b97f78f7
commit
826addb4c1
31 changed files with 618 additions and 267 deletions
|
@ -1,98 +0,0 @@
|
|||
{
|
||||
"name": "{{ .config.AppName }}",
|
||||
"categories": ["photo"],
|
||||
"short_name": "{{ printf "%.32s" .config.AppName }}",
|
||||
"description": "{{ .config.SiteDescription }}",
|
||||
"icons": [
|
||||
{
|
||||
"src": "{{ .config.StaticUri }}/icons/{{ .config.AppIcon }}/16.png",
|
||||
"sizes": "16x16",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "{{ .config.StaticUri }}/icons/{{ .config.AppIcon }}/32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "{{ .config.StaticUri }}/icons/{{ .config.AppIcon }}/72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "{{ .config.StaticUri }}/icons/{{ .config.AppIcon }}/114.png",
|
||||
"sizes": "114x114",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "{{ .config.StaticUri }}/icons/{{ .config.AppIcon }}/128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "{{ .config.StaticUri }}/icons/{{ .config.AppIcon }}/144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "{{ .config.StaticUri }}/icons/{{ .config.AppIcon }}/152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "{{ .config.StaticUri }}/icons/{{ .config.AppIcon }}/160.png",
|
||||
"sizes": "160x160",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "{{ .config.StaticUri }}/icons/{{ .config.AppIcon }}/167.png",
|
||||
"sizes": "167x167",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "{{ .config.StaticUri }}/icons/{{ .config.AppIcon }}/180.png",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "{{ .config.StaticUri }}/icons/{{ .config.AppIcon }}/192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "{{ .config.StaticUri }}/icons/{{ .config.AppIcon }}/196.png",
|
||||
"sizes": "196x196",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "{{ .config.StaticUri }}/icons/{{ .config.AppIcon }}/256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "{{ .config.StaticUri }}/icons/{{ .config.AppIcon }}/400.png",
|
||||
"sizes": "400x400",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "{{ .config.StaticUri }}/icons/{{ .config.AppIcon }}/512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"scope": "{{ .config.BaseUri }}/",
|
||||
"start_url": "{{ .config.BaseUri }}/library/",
|
||||
"serviceworker": {
|
||||
"src": "sw.js",
|
||||
"scope": "{{ .config.BaseUri }}/",
|
||||
"use_cache": true
|
||||
},
|
||||
"display": "{{ .config.AppMode }}",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#000000",
|
||||
"permissions": [
|
||||
"geolocation",
|
||||
"downloads",
|
||||
"storage"
|
||||
]
|
||||
}
|
|
@ -38,7 +38,7 @@ const testConfig = {
|
|||
downloadToken: "public",
|
||||
cssUri: "/static/build/app.2259c0edcc020e7af593.css",
|
||||
jsUri: "/static/build/app.9bd7132eaee8e4c7c7e3.js",
|
||||
manifestUri: "/manifest.json?0e41a7e5",
|
||||
manifestUri: "/manifest.json",
|
||||
};
|
||||
|
||||
const c = window.__CONFIG__ ? window.__CONFIG__ : testConfig;
|
||||
|
|
|
@ -287,7 +287,7 @@ const clientConfig = {
|
|||
previewToken: "public",
|
||||
cssUri: "/static/build/app.2259c0edcc020e7af593.css",
|
||||
jsUri: "/static/build/app.9bd7132eaee8e4c7c7e3.js",
|
||||
manifestUri: "/manifest.json?0e41a7e5",
|
||||
manifestUri: "/manifest.json",
|
||||
settings: {
|
||||
ui: {
|
||||
scrollbar: true,
|
||||
|
|
|
@ -5,8 +5,6 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
type ClientAssets struct {
|
||||
|
@ -87,5 +85,5 @@ func (c *Config) ClientAssets() ClientAssets {
|
|||
|
||||
// ClientManifestUri returns the frontend manifest.json URI.
|
||||
func (c *Config) ClientManifestUri() string {
|
||||
return fmt.Sprintf("%s?%d", c.BaseUri("/manifest.json"), fs.BirthTime(c.TemplatesPath()+"/manifest.json").Unix())
|
||||
return c.BaseUri("/manifest.json")
|
||||
}
|
||||
|
|
|
@ -85,13 +85,13 @@ func TestConfig_ClientAssets(t *testing.T) {
|
|||
func TestClientManifestUri(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.True(t, strings.HasPrefix(c.ClientManifestUri(), "/manifest.json?"))
|
||||
assert.True(t, strings.HasPrefix(c.ClientManifestUri(), "/manifest.json"))
|
||||
|
||||
c.options.SiteUrl = ""
|
||||
|
||||
assert.True(t, strings.HasPrefix(c.ClientManifestUri(), "/manifest.json?"))
|
||||
assert.True(t, strings.HasPrefix(c.ClientManifestUri(), "/manifest.json"))
|
||||
|
||||
c.options.SiteUrl = "http://myhost/foo"
|
||||
|
||||
assert.True(t, strings.HasPrefix(c.ClientManifestUri(), "/foo/manifest.json?"))
|
||||
assert.True(t, strings.HasPrefix(c.ClientManifestUri(), "/foo/manifest.json"))
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ type ClientConfig struct {
|
|||
AppName string `json:"appName"`
|
||||
AppMode string `json:"appMode"`
|
||||
AppIcon string `json:"appIcon"`
|
||||
AppColor string `json:"appColor"`
|
||||
Debug bool `json:"debug"`
|
||||
Trace bool `json:"trace"`
|
||||
Test bool `json:"test"`
|
||||
|
@ -253,6 +254,7 @@ func (c *Config) ClientPublic() ClientConfig {
|
|||
AppName: c.AppName(),
|
||||
AppMode: c.AppMode(),
|
||||
AppIcon: c.AppIcon(),
|
||||
AppColor: c.AppColor(),
|
||||
WallpaperUri: c.WallpaperUri(),
|
||||
Version: c.Version(),
|
||||
Copyright: c.Copyright(),
|
||||
|
@ -332,6 +334,7 @@ func (c *Config) ClientShare() ClientConfig {
|
|||
AppName: c.AppName(),
|
||||
AppMode: c.AppMode(),
|
||||
AppIcon: c.AppIcon(),
|
||||
AppColor: c.AppColor(),
|
||||
WallpaperUri: c.WallpaperUri(),
|
||||
Version: c.Version(),
|
||||
Copyright: c.Copyright(),
|
||||
|
@ -418,6 +421,7 @@ func (c *Config) ClientUser(withSettings bool) ClientConfig {
|
|||
AppName: c.AppName(),
|
||||
AppMode: c.AppMode(),
|
||||
AppIcon: c.AppIcon(),
|
||||
AppColor: c.AppColor(),
|
||||
WallpaperUri: c.WallpaperUri(),
|
||||
Version: c.Version(),
|
||||
Copyright: c.Copyright(),
|
||||
|
|
|
@ -155,6 +155,7 @@ func (c *Config) Options() *Options {
|
|||
|
||||
// Propagate updates config options in other packages as needed.
|
||||
func (c *Config) Propagate() {
|
||||
FlushCache()
|
||||
log.SetLevel(c.LogLevel())
|
||||
|
||||
// Set thumbnail generation parameters.
|
||||
|
|
105
internal/config/config_app.go
Normal file
105
internal/config/config_app.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/pwa"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// AppName returns the app name when installed on a device.
|
||||
func (c *Config) AppName() string {
|
||||
name := strings.TrimSpace(c.options.AppName)
|
||||
|
||||
if c.NoSponsor() || name == "" {
|
||||
name = c.SiteTitle()
|
||||
}
|
||||
|
||||
name = strings.Map(func(r rune) rune {
|
||||
switch r {
|
||||
case '\'', '"':
|
||||
return -1
|
||||
}
|
||||
|
||||
return r
|
||||
}, name)
|
||||
|
||||
return txt.Clip(name, 32)
|
||||
}
|
||||
|
||||
// AppMode returns the app mode when installed on a device.
|
||||
func (c *Config) AppMode() string {
|
||||
switch c.options.AppMode {
|
||||
case "fullscreen", "standalone", "minimal-ui", "browser":
|
||||
return c.options.AppMode
|
||||
default:
|
||||
return "standalone"
|
||||
}
|
||||
}
|
||||
|
||||
// AppIcon returns the app icon when installed on a device.
|
||||
func (c *Config) AppIcon() string {
|
||||
defaultIcon := "logo"
|
||||
|
||||
if c.NoSponsor() || c.options.AppIcon == "" || c.options.AppIcon == defaultIcon {
|
||||
// Default.
|
||||
} else if strings.Contains(c.options.AppIcon, "/") {
|
||||
return c.options.AppIcon
|
||||
} else if fs.FileExists(c.AppIconsPath(c.options.AppIcon, "16.png")) {
|
||||
return c.options.AppIcon
|
||||
}
|
||||
|
||||
return defaultIcon
|
||||
}
|
||||
|
||||
// AppColor returns the app splash screen color when installed on a device.
|
||||
func (c *Config) AppColor() string {
|
||||
if appColor := clean.Color(c.options.AppColor); appColor == "" {
|
||||
return "#000000"
|
||||
} else {
|
||||
return appColor
|
||||
}
|
||||
}
|
||||
|
||||
// AppIconsPath returns the path to the app icons.
|
||||
func (c *Config) AppIconsPath(name ...string) string {
|
||||
if len(name) > 0 {
|
||||
filePath := []string{c.StaticPath(), "icons"}
|
||||
filePath = append(filePath, name...)
|
||||
return filepath.Join(filePath...)
|
||||
}
|
||||
|
||||
return filepath.Join(c.StaticPath(), "icons")
|
||||
}
|
||||
|
||||
// AppConfig returns the progressive web app config.
|
||||
func (c *Config) AppConfig() pwa.Config {
|
||||
return pwa.Config{
|
||||
Icon: c.AppIcon(),
|
||||
Color: c.AppColor(),
|
||||
Name: c.AppName(),
|
||||
Description: c.SiteDescription(),
|
||||
Mode: c.AppMode(),
|
||||
BaseUri: c.BaseUri("/"),
|
||||
StaticUri: c.StaticUri(),
|
||||
}
|
||||
}
|
||||
|
||||
// AppManifest returns the progressive web app manifest.
|
||||
func (c *Config) AppManifest() *pwa.Manifest {
|
||||
if cacheData, ok := Cache.Get(CacheKeyAppManifest); ok {
|
||||
log.Tracef("config: cache hit for %s", CacheKeyAppManifest)
|
||||
|
||||
return cacheData.(*pwa.Manifest)
|
||||
}
|
||||
result := pwa.NewManifest(c.AppConfig())
|
||||
if result != nil {
|
||||
Cache.SetDefault(CacheKeyAppManifest, result)
|
||||
} else {
|
||||
log.Warnf("config: web app manifest is nil - possible bug")
|
||||
}
|
||||
return result
|
||||
}
|
109
internal/config/config_app_test.go
Normal file
109
internal/config/config_app_test.go
Normal file
|
@ -0,0 +1,109 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/pwa"
|
||||
)
|
||||
|
||||
func TestConfig_AppName(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "PhotoPrism", c.AppName())
|
||||
}
|
||||
|
||||
func TestConfig_AppMode(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "standalone", c.AppMode())
|
||||
}
|
||||
|
||||
func TestConfig_AppIcon(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "logo", c.AppIcon())
|
||||
c.options.AppIcon = "foo"
|
||||
assert.Equal(t, "logo", c.AppIcon())
|
||||
c.options.AppIcon = "app"
|
||||
assert.Equal(t, "app", c.AppIcon())
|
||||
c.options.AppIcon = "crisp"
|
||||
assert.Equal(t, "crisp", c.AppIcon())
|
||||
c.options.AppIcon = "mint"
|
||||
assert.Equal(t, "mint", c.AppIcon())
|
||||
c.options.AppIcon = "bold"
|
||||
assert.Equal(t, "bold", c.AppIcon())
|
||||
c.options.AppIcon = "logo"
|
||||
assert.Equal(t, "logo", c.AppIcon())
|
||||
}
|
||||
|
||||
func TestConfig_AppColor(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "#000000", c.AppColor())
|
||||
c.options.AppColor = "#aBC123"
|
||||
assert.Equal(t, "#abc123", c.AppColor())
|
||||
c.options.AppColor = ""
|
||||
assert.Equal(t, "#000000", c.AppColor())
|
||||
}
|
||||
|
||||
func TestConfig_AppIconsPath(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
if p := c.AppIconsPath(); !strings.HasSuffix(p, "photoprism/assets/static/icons") {
|
||||
t.Fatal("path .../photoprism/assets/static/icons expected")
|
||||
}
|
||||
|
||||
if p := c.AppIconsPath("app"); !strings.HasSuffix(p, "photoprism/assets/static/icons/app") {
|
||||
t.Fatal("path .../photoprism/assets/static/icons/app expected")
|
||||
}
|
||||
|
||||
if p := c.AppIconsPath("app", "512.png"); !strings.HasSuffix(p, "photoprism/assets/static/icons/app/512.png") {
|
||||
t.Fatal("path .../photoprism/assets/static/icons/app/512.png expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_AppConfig(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
result := c.AppConfig()
|
||||
assert.NotEmpty(t, result)
|
||||
assert.Equal(t, c.AppName(), result.Name)
|
||||
assert.Equal(t, c.AppIcon(), result.Icon)
|
||||
assert.Equal(t, c.SiteDescription(), result.Description)
|
||||
assert.Equal(t, c.BaseUri("/"), result.BaseUri)
|
||||
assert.Equal(t, c.StaticUri(), result.StaticUri)
|
||||
}
|
||||
|
||||
func TestConfig_AppManifest(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
appConf := c.AppConfig()
|
||||
assert.NotEmpty(t, appConf)
|
||||
|
||||
t.Run("Cached", func(t *testing.T) {
|
||||
result := c.AppManifest()
|
||||
assert.NotEmpty(t, result)
|
||||
assert.Equal(t, appConf.Name, result.Name)
|
||||
assert.Equal(t, appConf.Name, result.ShortName)
|
||||
assert.Equal(t, appConf.Description, result.Description)
|
||||
assert.Equal(t, appConf.BaseUri, result.Scope)
|
||||
assert.Equal(t, appConf.BaseUri+"library/", result.StartUrl)
|
||||
assert.Len(t, result.Icons, len(pwa.IconSizes))
|
||||
assert.Len(t, result.Categories, len(pwa.Categories))
|
||||
assert.Len(t, result.Permissions, len(pwa.Permissions))
|
||||
|
||||
cached := c.AppManifest()
|
||||
assert.NotEmpty(t, cached)
|
||||
assert.Equal(t, appConf.Name, cached.Name)
|
||||
assert.Equal(t, appConf.Name, cached.ShortName)
|
||||
assert.Equal(t, appConf.Description, cached.Description)
|
||||
assert.Equal(t, appConf.BaseUri, cached.Scope)
|
||||
assert.Equal(t, appConf.BaseUri+"library/", cached.StartUrl)
|
||||
assert.Len(t, cached.Icons, len(pwa.IconSizes))
|
||||
assert.Len(t, cached.Categories, len(pwa.Categories))
|
||||
assert.Len(t, cached.Permissions, len(pwa.Permissions))
|
||||
})
|
||||
}
|
18
internal/config/config_cache.go
Normal file
18
internal/config/config_cache.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
gc "github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
var Cache = gc.New(time.Hour, 15*time.Minute)
|
||||
|
||||
const (
|
||||
CacheKeyAppManifest = "app-manifest"
|
||||
)
|
||||
|
||||
// FlushCache clears the config cache.
|
||||
func FlushCache() {
|
||||
Cache.Flush()
|
||||
}
|
|
@ -8,7 +8,6 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// DefaultTheme returns the default user interface theme name.
|
||||
|
@ -29,60 +28,6 @@ func (c *Config) DefaultLocale() string {
|
|||
return c.options.DefaultLocale
|
||||
}
|
||||
|
||||
// AppIcon returns the app icon when installed on a device.
|
||||
func (c *Config) AppIcon() string {
|
||||
defaultIcon := "logo"
|
||||
|
||||
if c.NoSponsor() || c.options.AppIcon == "" || c.options.AppIcon == defaultIcon {
|
||||
// Default.
|
||||
} else if fs.FileExists(c.AppIconsPath(c.options.AppIcon, "512.png")) {
|
||||
return c.options.AppIcon
|
||||
}
|
||||
|
||||
return defaultIcon
|
||||
}
|
||||
|
||||
// AppIconsPath returns the path to the app icons.
|
||||
func (c *Config) AppIconsPath(name ...string) string {
|
||||
if len(name) > 0 {
|
||||
filePath := []string{c.StaticPath(), "icons"}
|
||||
filePath = append(filePath, name...)
|
||||
return filepath.Join(filePath...)
|
||||
}
|
||||
|
||||
return filepath.Join(c.StaticPath(), "icons")
|
||||
}
|
||||
|
||||
// AppName returns the app name when installed on a device.
|
||||
func (c *Config) AppName() string {
|
||||
name := strings.TrimSpace(c.options.AppName)
|
||||
|
||||
if c.NoSponsor() || name == "" {
|
||||
name = c.SiteTitle()
|
||||
}
|
||||
|
||||
name = strings.Map(func(r rune) rune {
|
||||
switch r {
|
||||
case '\'', '"':
|
||||
return -1
|
||||
}
|
||||
|
||||
return r
|
||||
}, name)
|
||||
|
||||
return txt.Clip(name, 32)
|
||||
}
|
||||
|
||||
// AppMode returns the app mode when installed on a device.
|
||||
func (c *Config) AppMode() string {
|
||||
switch c.options.AppMode {
|
||||
case "fullscreen", "standalone", "minimal-ui", "browser":
|
||||
return c.options.AppMode
|
||||
default:
|
||||
return "standalone"
|
||||
}
|
||||
}
|
||||
|
||||
// WallpaperUri returns the login screen background image `URI`.
|
||||
func (c *Config) WallpaperUri() string {
|
||||
if c.NoSponsor() {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -41,52 +40,6 @@ func TestConfig_DefaultLocale(t *testing.T) {
|
|||
assert.Equal(t, "en", c.DefaultLocale())
|
||||
}
|
||||
|
||||
func TestConfig_AppIcon(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "logo", c.AppIcon())
|
||||
c.options.AppIcon = "foo"
|
||||
assert.Equal(t, "logo", c.AppIcon())
|
||||
c.options.AppIcon = "app"
|
||||
assert.Equal(t, "app", c.AppIcon())
|
||||
c.options.AppIcon = "crisp"
|
||||
assert.Equal(t, "crisp", c.AppIcon())
|
||||
c.options.AppIcon = "mint"
|
||||
assert.Equal(t, "mint", c.AppIcon())
|
||||
c.options.AppIcon = "bold"
|
||||
assert.Equal(t, "bold", c.AppIcon())
|
||||
c.options.AppIcon = "logo"
|
||||
assert.Equal(t, "logo", c.AppIcon())
|
||||
}
|
||||
|
||||
func TestConfig_AppIconsPath(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
if p := c.AppIconsPath(); !strings.HasSuffix(p, "photoprism/assets/static/icons") {
|
||||
t.Fatal("path .../photoprism/assets/static/icons expected")
|
||||
}
|
||||
|
||||
if p := c.AppIconsPath("app"); !strings.HasSuffix(p, "photoprism/assets/static/icons/app") {
|
||||
t.Fatal("path .../pphotoprism/assets/static/icons/app expected")
|
||||
}
|
||||
|
||||
if p := c.AppIconsPath("app", "512.png"); !strings.HasSuffix(p, "photoprism/assets/static/icons/app/512.png") {
|
||||
t.Fatal("path .../photoprism/assets/static/icons/app/512.png expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_AppName(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "PhotoPrism", c.AppName())
|
||||
}
|
||||
|
||||
func TestConfig_AppMode(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "standalone", c.AppMode())
|
||||
}
|
||||
|
||||
func TestConfig_WallpaperUri(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
|
|
|
@ -311,6 +311,13 @@ var Flags = CliFlags{
|
|||
EnvVar: "PHOTOPRISM_DEFAULT_THEME",
|
||||
},
|
||||
Tags: []string{EnvSponsor}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "app-name",
|
||||
Usage: "progressive web app `NAME` when installed on a device",
|
||||
Value: "",
|
||||
EnvVar: "PHOTOPRISM_APP_NAME",
|
||||
},
|
||||
Tags: []string{EnvSponsor}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "app-mode",
|
||||
Usage: "progressive web app `MODE` (fullscreen, standalone, minimal-ui, browser)",
|
||||
|
@ -319,17 +326,16 @@ var Flags = CliFlags{
|
|||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "app-icon",
|
||||
Usage: "progressive web app `ICON` (logo, app, crisp, mint, bold)",
|
||||
Usage: "home screen `ICON` (logo, app, crisp, mint, bold)",
|
||||
EnvVar: "PHOTOPRISM_APP_ICON",
|
||||
},
|
||||
Tags: []string{EnvSponsor}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "app-name",
|
||||
Usage: "progressive web app `NAME` when installed on a device",
|
||||
Value: "",
|
||||
EnvVar: "PHOTOPRISM_APP_NAME",
|
||||
},
|
||||
Tags: []string{EnvSponsor}}, {
|
||||
Name: "app-color",
|
||||
Usage: "splash screen `COLOR` code",
|
||||
EnvVar: "PHOTOPRISM_APP_COLOR",
|
||||
Value: "#000000",
|
||||
}}, {
|
||||
Flag: cli.StringFlag{
|
||||
Name: "imprint",
|
||||
Usage: "legal information `TEXT`, displayed in the page footer",
|
||||
|
|
|
@ -81,9 +81,10 @@ type Options struct {
|
|||
UploadNSFW bool `yaml:"UploadNSFW" json:"-" flag:"upload-nsfw"`
|
||||
DefaultTheme string `yaml:"DefaultTheme" json:"DefaultTheme" flag:"default-theme"`
|
||||
DefaultLocale string `yaml:"DefaultLocale" json:"DefaultLocale" flag:"default-locale"`
|
||||
AppIcon string `yaml:"AppIcon" json:"AppIcon" flag:"app-icon"`
|
||||
AppName string `yaml:"AppName" json:"AppName" flag:"app-name"`
|
||||
AppMode string `yaml:"AppMode" json:"AppMode" flag:"app-mode"`
|
||||
AppIcon string `yaml:"AppIcon" json:"AppIcon" flag:"app-icon"`
|
||||
AppColor string `yaml:"AppColor" json:"AppColor" flag:"app-color"`
|
||||
LegalInfo string `yaml:"LegalInfo" json:"LegalInfo" flag:"legal-info"`
|
||||
LegalUrl string `yaml:"LegalUrl" json:"LegalUrl" flag:"legal-url"`
|
||||
WallpaperUri string `yaml:"WallpaperUri" json:"WallpaperUri" flag:"wallpaper-uri"`
|
||||
|
|
|
@ -112,9 +112,10 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
|||
// Customization.
|
||||
{"default-locale", c.DefaultLocale()},
|
||||
{"default-theme", c.DefaultTheme()},
|
||||
{"app-icon", c.AppIcon()},
|
||||
{"app-name", c.AppName()},
|
||||
{"app-mode", c.AppMode()},
|
||||
{"app-icon", c.AppIcon()},
|
||||
{"app-color", c.AppColor()},
|
||||
{"legal-info", c.LegalInfo()},
|
||||
{"legal-url", c.LegalUrl()},
|
||||
{"wallpaper-uri", c.WallpaperUri()},
|
||||
|
|
8
internal/pwa/categories.go
Normal file
8
internal/pwa/categories.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package pwa
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/pkg/list"
|
||||
)
|
||||
|
||||
// Categories specifies the default web app manifest categories.
|
||||
var Categories = list.List{"photo"}
|
12
internal/pwa/config.go
Normal file
12
internal/pwa/config.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package pwa
|
||||
|
||||
// Config represents progressive web app manifest config values.
|
||||
type Config struct {
|
||||
Icon string `json:"icon"`
|
||||
Color string `json:"color"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Mode string `json:"mode"`
|
||||
BaseUri string `json:"baseUri"`
|
||||
StaticUri string `json:"staticUri"`
|
||||
}
|
43
internal/pwa/icon.go
Normal file
43
internal/pwa/icon.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package pwa
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Icons represents a list of app icons.
|
||||
type Icons []Icon
|
||||
|
||||
// Icon represents an app icon.
|
||||
type Icon struct {
|
||||
Src string `json:"src"`
|
||||
Sizes string `json:"sizes,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// IconSizes represents standard app icon sizes.
|
||||
var IconSizes = []int{16, 32, 77, 114, 128, 144, 152, 160, 167, 180, 192, 196, 256, 400, 512}
|
||||
|
||||
// NewIcons creates new app icons in the default sizes based on the parameters provided.
|
||||
func NewIcons(staticUri, appIcon string) Icons {
|
||||
if appIcon == "" {
|
||||
appIcon = "logo"
|
||||
} else if strings.Contains(appIcon, "/") {
|
||||
return Icons{{
|
||||
Src: appIcon,
|
||||
Type: "image/png",
|
||||
}}
|
||||
}
|
||||
|
||||
icons := make(Icons, len(IconSizes))
|
||||
|
||||
for i, d := range IconSizes {
|
||||
icons[i] = Icon{
|
||||
Src: fmt.Sprintf("%s/icons/%s/%d.png", staticUri, appIcon, d),
|
||||
Sizes: fmt.Sprintf("%dx%d", d, d),
|
||||
Type: "image/png",
|
||||
}
|
||||
}
|
||||
|
||||
return icons
|
||||
}
|
24
internal/pwa/icon_test.go
Normal file
24
internal/pwa/icon_test.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package pwa
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewIcons(t *testing.T) {
|
||||
t.Run("Standard", func(t *testing.T) {
|
||||
result := NewIcons("https://demo-cdn.photoprism.app/static", "test")
|
||||
assert.NotEmpty(t, result)
|
||||
assert.Equal(t, "https://demo-cdn.photoprism.app/static/icons/test/16.png", result[0].Src)
|
||||
assert.Equal(t, "image/png", result[0].Type)
|
||||
assert.Equal(t, "16x16", result[0].Sizes)
|
||||
})
|
||||
t.Run("Custom", func(t *testing.T) {
|
||||
result := NewIcons("https://demo-cdn.photoprism.app/static", "/test.png")
|
||||
assert.NotEmpty(t, result)
|
||||
assert.Equal(t, "/test.png", result[0].Src)
|
||||
assert.Equal(t, "image/png", result[0].Type)
|
||||
assert.Equal(t, "", result[0].Sizes)
|
||||
})
|
||||
}
|
45
internal/pwa/manifest.go
Normal file
45
internal/pwa/manifest.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package pwa
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/list"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// Manifest represents a progressive web app manifest.
|
||||
type Manifest struct {
|
||||
Name string `json:"name"`
|
||||
ShortName string `json:"short_name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Categories list.List `json:"categories"`
|
||||
Display string `json:"display"`
|
||||
ThemeColor string `json:"theme_color"`
|
||||
BackgroundColor string `json:"background_color"`
|
||||
Scope string `json:"scope"`
|
||||
StartUrl string `json:"start_url,omitempty"`
|
||||
Serviceworker Serviceworker `json:"serviceworker,omitempty"`
|
||||
Permissions list.List `json:"permissions"`
|
||||
Icons Icons `json:"icons"`
|
||||
}
|
||||
|
||||
// NewManifest creates a new progressive web app manifest based on the config provided.
|
||||
func NewManifest(c Config) (m *Manifest) {
|
||||
return &Manifest{
|
||||
Name: c.Name,
|
||||
ShortName: txt.Clip(c.Name, 32),
|
||||
Description: c.Description,
|
||||
Categories: Categories,
|
||||
Display: c.Mode,
|
||||
ThemeColor: clean.Color(c.Color),
|
||||
BackgroundColor: clean.Color(c.Color),
|
||||
Scope: c.BaseUri,
|
||||
StartUrl: c.BaseUri + "library/",
|
||||
Serviceworker: Serviceworker{
|
||||
Src: "sw.js",
|
||||
Scope: c.BaseUri,
|
||||
UseCache: true,
|
||||
},
|
||||
Permissions: Permissions,
|
||||
Icons: NewIcons(c.StaticUri, c.Icon),
|
||||
}
|
||||
}
|
32
internal/pwa/manifest_test.go
Normal file
32
internal/pwa/manifest_test.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package pwa
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewManifest(t *testing.T) {
|
||||
c := Config{
|
||||
Icon: "logo",
|
||||
Color: "#aaaaaa",
|
||||
Name: "TestPrism+",
|
||||
Description: "App's Description",
|
||||
Mode: "fullscreen",
|
||||
BaseUri: "/",
|
||||
StaticUri: "/static",
|
||||
}
|
||||
|
||||
t.Run("Standard", func(t *testing.T) {
|
||||
result := NewManifest(c)
|
||||
assert.NotEmpty(t, result)
|
||||
assert.Equal(t, c.Name, result.Name)
|
||||
assert.Equal(t, c.Name, result.ShortName)
|
||||
assert.Equal(t, c.Description, result.Description)
|
||||
assert.Equal(t, c.BaseUri, result.Scope)
|
||||
assert.Equal(t, c.BaseUri+"library/", result.StartUrl)
|
||||
assert.Len(t, result.Icons, len(IconSizes))
|
||||
assert.Len(t, result.Categories, len(Categories))
|
||||
assert.Len(t, result.Permissions, len(Permissions))
|
||||
})
|
||||
}
|
8
internal/pwa/permissions.go
Normal file
8
internal/pwa/permissions.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package pwa
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/pkg/list"
|
||||
)
|
||||
|
||||
// Permissions specifies the default web app manifest permissions.
|
||||
var Permissions = list.List{"geolocation", "downloads", "storage"}
|
25
internal/pwa/pwa.go
Normal file
25
internal/pwa/pwa.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
Package pwa provides data structures and tools for working with progressive web applications.
|
||||
|
||||
Copyright (c) 2018 - 2023 PhotoPrism UG. All rights reserved.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
|
||||
<https://docs.photoprism.app/license/agpl>
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
The AGPL is supplemented by our Trademark and Brand Guidelines,
|
||||
which describe how our Brand Assets may be used:
|
||||
<https://www.photoprism.app/trademark>
|
||||
|
||||
Feel free to send an email to hello@photoprism.app if you have questions,
|
||||
want to support our work, or just want to say hello.
|
||||
|
||||
Additional information can be found in our Developer Guide:
|
||||
<https://docs.photoprism.app/developer-guide/>
|
||||
*/
|
||||
package pwa
|
8
internal/pwa/sw.go
Normal file
8
internal/pwa/sw.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package pwa
|
||||
|
||||
// Serviceworker represents serviceworker options for the PWA manifest.
|
||||
type Serviceworker struct {
|
||||
Src string `json:"src"`
|
||||
Scope string `json:"scope"`
|
||||
UseCache bool `json:"use_cache"`
|
||||
}
|
|
@ -16,6 +16,9 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
// Static assets and templates.
|
||||
registerStaticRoutes(router, conf)
|
||||
|
||||
// Web app bootstrapping and configuration.
|
||||
registerPWARoutes(router, conf)
|
||||
|
||||
// Built-in WebDAV server.
|
||||
registerWebDAVRoutes(router, conf)
|
||||
|
||||
|
|
42
internal/server/routes_pwa.go
Normal file
42
internal/server/routes_pwa.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
)
|
||||
|
||||
// registerPWARoutes configures the progressive web app bootstrap and config routes.
|
||||
func registerPWARoutes(router *gin.Engine, conf *config.Config) {
|
||||
// Loads Progressive Web App (PWA) on all routes beginning with "library".
|
||||
pwa := func(c *gin.Context) {
|
||||
values := gin.H{
|
||||
"signUp": gin.H{"message": config.MsgSponsor, "url": config.SignUpURL},
|
||||
"config": conf.ClientPublic(),
|
||||
}
|
||||
c.HTML(http.StatusOK, conf.TemplateName(), values)
|
||||
}
|
||||
router.Any(conf.BaseUri("/library/*path"), pwa)
|
||||
|
||||
// Progressive Web App (PWA) Manifest.
|
||||
manifest := func(c *gin.Context) {
|
||||
c.Header("Cache-Control", "no-store")
|
||||
c.Header("Content-Type", "application/json")
|
||||
c.IndentedJSON(200, conf.AppManifest())
|
||||
}
|
||||
router.Any(conf.BaseUri("/manifest.json"), manifest)
|
||||
|
||||
// Progressive Web App (PWA) Service Worker.
|
||||
swWorker := func(c *gin.Context) {
|
||||
c.Header("Cache-Control", "no-store")
|
||||
c.File(filepath.Join(conf.BuildPath(), "sw.js"))
|
||||
}
|
||||
router.Any("/sw.js", swWorker)
|
||||
|
||||
if swUri := conf.BaseUri("/sw.js"); swUri != "/sw.js" {
|
||||
router.Any(swUri, swWorker)
|
||||
}
|
||||
}
|
|
@ -2,8 +2,8 @@ package server
|
|||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/api"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/api"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
)
|
||||
|
||||
|
|
|
@ -34,37 +34,6 @@ func registerStaticRoutes(router *gin.Engine, conf *config.Config) {
|
|||
}
|
||||
})
|
||||
|
||||
// Loads Progressive Web App (PWA) on all routes beginning with "library".
|
||||
pwa := func(c *gin.Context) {
|
||||
values := gin.H{
|
||||
"signUp": gin.H{"message": config.MsgSponsor, "url": config.SignUpURL},
|
||||
"config": conf.ClientPublic(),
|
||||
}
|
||||
c.HTML(http.StatusOK, conf.TemplateName(), values)
|
||||
}
|
||||
router.Any(conf.BaseUri("/library/*path"), pwa)
|
||||
|
||||
// Progressive Web App (PWA) Manifest.
|
||||
manifest := func(c *gin.Context) {
|
||||
c.Header("Cache-Control", "no-store")
|
||||
c.Header("Content-Type", "application/json")
|
||||
|
||||
clientConfig := conf.ClientPublic()
|
||||
c.HTML(http.StatusOK, "manifest.json", gin.H{"config": clientConfig})
|
||||
}
|
||||
router.Any(conf.BaseUri("/manifest.json"), manifest)
|
||||
|
||||
// Progressive Web App (PWA) Service Worker.
|
||||
swWorker := func(c *gin.Context) {
|
||||
c.Header("Cache-Control", "no-store")
|
||||
c.File(filepath.Join(conf.BuildPath(), "sw.js"))
|
||||
}
|
||||
router.Any("/sw.js", swWorker)
|
||||
|
||||
if swUri := conf.BaseUri("/sw.js"); swUri != "/sw.js" {
|
||||
router.Any(swUri, swWorker)
|
||||
}
|
||||
|
||||
// Serves static favicon.
|
||||
router.StaticFile(conf.BaseUri("/favicon.ico"), filepath.Join(conf.ImgPath(), "favicon.ico"))
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package server
|
|||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
@ -24,33 +25,35 @@ func TestStaticRoutes(t *testing.T) {
|
|||
// Register routes.
|
||||
registerStaticRoutes(r, conf)
|
||||
|
||||
t.Run("GetHome", func(t *testing.T) {
|
||||
t.Run("GetRoot", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, 307, w.Code)
|
||||
assert.Equal(t, "<a href=\"/library/browse\">Temporary Redirect</a>.\n\n", w.Body.String())
|
||||
})
|
||||
t.Run("HeadHome", func(t *testing.T) {
|
||||
t.Run("HeadRoot", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("HEAD", "/", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, 307, w.Code)
|
||||
})
|
||||
t.Run("GetServiceWorker", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/sw.js", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.NotEmpty(t, w.Body)
|
||||
})
|
||||
t.Run("HeadServiceWorker", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("HEAD", "/sw.js", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Empty(t, w.Body)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPWARoutes(t *testing.T) {
|
||||
// Create router.
|
||||
r := gin.Default()
|
||||
|
||||
// Get test config.
|
||||
conf := config.TestConfig()
|
||||
|
||||
// Find and load templates.
|
||||
r.LoadHTMLFiles(conf.TemplateFiles()...)
|
||||
|
||||
// Register routes.
|
||||
registerPWARoutes(r, conf)
|
||||
|
||||
// Bootstrapping.
|
||||
t.Run("GetLibrary", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/library/", nil)
|
||||
|
@ -58,9 +61,9 @@ func TestStaticRoutes(t *testing.T) {
|
|||
assert.Equal(t, 200, w.Code)
|
||||
assert.NotEmpty(t, w.Body)
|
||||
})
|
||||
t.Run("GetLibrary", func(t *testing.T) {
|
||||
t.Run("HeadLibrary", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/library/", nil)
|
||||
req, _ := http.NewRequest("HEAD", "/library/", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.NotEmpty(t, w.Body)
|
||||
|
@ -78,4 +81,34 @@ func TestStaticRoutes(t *testing.T) {
|
|||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
})
|
||||
|
||||
// Manifest.
|
||||
t.Run("GetManifest", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/manifest.json", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.NotEmpty(t, w.Body.String())
|
||||
manifest := w.Body.String()
|
||||
t.Logf("PWA Manifest: %s", manifest)
|
||||
assert.True(t, strings.Contains(manifest, `"scope": "/",`))
|
||||
assert.True(t, strings.Contains(manifest, `"start_url": "/library/",`))
|
||||
assert.True(t, strings.Contains(manifest, "/static/icons/logo/128.png"))
|
||||
})
|
||||
|
||||
// Service worker.
|
||||
t.Run("GetServiceWorker", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/sw.js", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.NotEmpty(t, w.Body)
|
||||
})
|
||||
t.Run("HeadServiceWorker", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("HEAD", "/sw.js", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Empty(t, w.Body)
|
||||
})
|
||||
}
|
||||
|
|
25
pkg/clean/color.go
Normal file
25
pkg/clean/color.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package clean
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Color sanitizes HTML color codes and returns them in lowercase if they are valid, or an empty string otherwise.
|
||||
func Color(s string) string {
|
||||
s = strings.ToLower(s)
|
||||
|
||||
// Remove unwanted characters.
|
||||
s = strings.Map(func(r rune) rune {
|
||||
if r < 48 && r > 57 && 97 < r && r > 102 && r != 35 {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, s)
|
||||
|
||||
// Invalid?
|
||||
if l := len(s); l != 4 && l != 7 && l != 9 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
31
pkg/clean/color_test.go
Normal file
31
pkg/clean/color_test.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package clean
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestColor(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
assert.Equal(t, "", Color(""))
|
||||
})
|
||||
t.Run("Black", func(t *testing.T) {
|
||||
assert.Equal(t, "#000000", Color("#000000"))
|
||||
})
|
||||
t.Run("White", func(t *testing.T) {
|
||||
assert.Equal(t, "#ffffff", Color("#FFFFFF"))
|
||||
})
|
||||
t.Run("Short", func(t *testing.T) {
|
||||
assert.Equal(t, "#ab1", Color("#aB1"))
|
||||
})
|
||||
t.Run("Alpha", func(t *testing.T) {
|
||||
assert.Equal(t, "#0123456a", Color("#0123456A"))
|
||||
})
|
||||
t.Run("TooLong", func(t *testing.T) {
|
||||
assert.Equal(t, "", Color("#01234567AA"))
|
||||
})
|
||||
t.Run("TooShort", func(t *testing.T) {
|
||||
assert.Equal(t, "", Color("#00"))
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue