From 5e5d893dd200aabcf1111bf82cd2a1559629921d Mon Sep 17 00:00:00 2001 From: Damon <2184238+damongolding@users.noreply.github.com> Date: Fri, 11 Oct 2024 12:59:33 +0100 Subject: [PATCH 01/13] fix whitespace error --- config/config.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/config/config.go b/config/config.go index 896cbd3..1f001e1 100644 --- a/config/config.go +++ b/config/config.go @@ -278,11 +278,10 @@ func (c *Config) checkDebuging() { } func (c *Config) checkAlbumAndPerson() { - newAlbum := []string{} for _, album := range c.Album { if album != "" && album != "ALBUM_ID" { - newAlbum = append(newAlbum, album) + newAlbum = append(newAlbum, strings.TrimSpace(album)) } } c.Album = newAlbum @@ -290,7 +289,7 @@ func (c *Config) checkAlbumAndPerson() { newPerson := []string{} for _, person := range c.Person { if person != "" && person != "PERSON_ID" { - newPerson = append(newPerson, person) + newPerson = append(newPerson, strings.TrimSpace(person)) } } c.Person = newPerson From 46333dea54661030724307b8a4c12782e5259d66 Mon Sep 17 00:00:00 2001 From: Damon <2184238+damongolding@users.noreply.github.com> Date: Sat, 12 Oct 2024 15:21:01 +0100 Subject: [PATCH 02/13] Update config.go --- config/config.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/config/config.go b/config/config.go index 1f001e1..37cd5da 100644 --- a/config/config.go +++ b/config/config.go @@ -365,9 +365,14 @@ func (c *Config) load(configFile string) error { log.Errorf("binding environment variables: %v", err) } - c.v.AddConfigPath(".") + viper.SetConfigName("config") // Looks for 'config.yaml' + + // Optionally, specify the config file type (if you want to load from memory) + viper.SetConfigType("yaml") - c.v.SetConfigFile(configFile) + // Add potential paths for the configuration file + viper.AddConfigPath(".") // Look in the current directory + viper.AddConfigPath("./config") // Look in the 'config/' subdirectory c.v.SetEnvPrefix("kiosk") @@ -375,7 +380,7 @@ func (c *Config) load(configFile string) error { err := c.v.ReadInConfig() if err != nil { - if _, err := os.Stat(configFile); os.IsNotExist(err) { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { log.Infof("Not using %s", configFile) } else if !isValidYAML(configFile) { log.Fatal(err) From f6d80ebc23752afd7161434464b0027303c4b99a Mon Sep 17 00:00:00 2001 From: Damon <2184238+damongolding@users.noreply.github.com> Date: Sat, 12 Oct 2024 15:30:21 +0100 Subject: [PATCH 03/13] Update config.go --- config/config.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/config/config.go b/config/config.go index 37cd5da..659505f 100644 --- a/config/config.go +++ b/config/config.go @@ -175,7 +175,7 @@ func New() *Config { // hasConfigChanged checks if the configuration file has been modified since the last check. func (c *Config) hasConfigChanged() bool { - info, err := os.Stat(defaultConfigFile) + info, err := os.Stat(c.v.ConfigFileUsed()) if err != nil { log.Errorf("Checking config file: %v", err) return false @@ -315,7 +315,9 @@ func (c *Config) LoadWithConfigLocation(configPath string) error { // 4. If changes are detected, it reloads the configuration and updates the ReloadTimeStamp. func (c *Config) WatchConfig() { - fileInfo, err := os.Stat(defaultConfigFile) + configPath := c.v.ConfigFileUsed() + + fileInfo, err := os.Stat(configPath) if os.IsNotExist(err) { return } @@ -325,7 +327,7 @@ func (c *Config) WatchConfig() { return } - info, err := os.Stat(defaultConfigFile) + info, err := os.Stat(configPath) if err != nil { log.Infof("Error getting initial file info: %v", err) } else { From 94b1b97e04f9593d663743704964f05069a7ece7 Mon Sep 17 00:00:00 2001 From: Damon <2184238+damongolding@users.noreply.github.com> Date: Sat, 12 Oct 2024 15:32:13 +0100 Subject: [PATCH 04/13] Update config.go --- config/config.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config/config.go b/config/config.go index 659505f..6a76eec 100644 --- a/config/config.go +++ b/config/config.go @@ -166,10 +166,6 @@ func New() *Config { ReloadTimeStamp: time.Now().Format(time.RFC3339), } defaults.SetDefaults(c) - info, err := os.Stat(defaultConfigFile) - if err == nil { - c.configLastModTime = info.ModTime() - } return c } From d75b92fd33537930d6f2fa1b88a93d506e139e26 Mon Sep 17 00:00:00 2001 From: Damon <2184238+damongolding@users.noreply.github.com> Date: Mon, 14 Oct 2024 12:57:53 +0100 Subject: [PATCH 05/13] implemented and add optional enabling --- config/config.go | 235 ++++++++++++++++++++++++++++-------------- main.go | 5 +- routes/routes_test.go | 2 +- 3 files changed, 160 insertions(+), 82 deletions(-) diff --git a/config/config.go b/config/config.go index 6a76eec..75c7798 100644 --- a/config/config.go +++ b/config/config.go @@ -21,8 +21,11 @@ package config import ( + "crypto/sha256" "encoding/json" "errors" + "fmt" + "io" "os" "strings" "sync" @@ -47,6 +50,9 @@ type KioskSettings struct { // Port which port to use Port int `mapstructure:"port" default:"3000"` + // WatchConfig if kiosk should watch config file for changes + WatchConfig bool `mapstructure:"watch_config" default:"false"` + // Cache enable/disable api call and image caching Cache bool `mapstructure:"cache" default:"true"` @@ -65,14 +71,16 @@ type KioskSettings struct { } type Config struct { - // v is the viper instance used for configuration management - v *viper.Viper + // V is the viper instance used for configuration management + V *viper.Viper // mu is a mutex used to ensure thread-safe access to the configuration mu *sync.Mutex // ReloadTimeStamp timestamp for when the last client reload was called for ReloadTimeStamp string // configLastModTime stores the last modification time of the configuration file configLastModTime time.Time + // configHash stores the SHA-256 hash of the configuration file + configHash string // ImmichApiKey Immich key to access assets ImmichApiKey string `mapstructure:"immich_api_key" default:""` @@ -161,7 +169,7 @@ type Config struct { // New returns a new config pointer instance func New() *Config { c := &Config{ - v: viper.NewWithOptions(viper.ExperimentalBindStruct()), + V: viper.NewWithOptions(viper.ExperimentalBindStruct()), mu: &sync.Mutex{}, ReloadTimeStamp: time.Now().Format(time.RFC3339), } @@ -169,17 +177,6 @@ func New() *Config { return c } -// hasConfigChanged checks if the configuration file has been modified since the last check. -func (c *Config) hasConfigChanged() bool { - info, err := os.Stat(c.v.ConfigFileUsed()) - if err != nil { - log.Errorf("Checking config file: %v", err) - return false - } - - return info.ModTime().After(c.configLastModTime) -} - // bindEnvironmentVariables binds specific environment variables to their corresponding // configuration keys in the Viper instance. This function allows for easy mapping // between environment variables and configuration settings. @@ -203,6 +200,7 @@ func bindEnvironmentVariables(v *viper.Viper) error { envVar string }{ {"kiosk.port", "KIOSK_PORT"}, + {"kiosk.watch_config", "KIOSK_WATCH_CONFIG"}, {"kiosk.password", "KIOSK_PASSWORD"}, {"kiosk.cache", "KIOSK_CACHE"}, {"kiosk.prefetch", "KIOSK_PREFETCH"}, @@ -242,6 +240,46 @@ func isValidYAML(filename string) bool { return true } +// validateConfigFile checks if the given file path is valid and not a directory. +// It returns an error if the file is a directory, and nil if the file doesn't exist. +func validateConfigFile(path string) error { + fileInfo, err := os.Stat(path) + if os.IsNotExist(err) { + return nil + } + if fileInfo.IsDir() { + return fmt.Errorf("Config file is a directory: %s", path) + } + return nil +} + +// hasConfigMtimeChanged checks if the configuration file has been modified since the last check. +func (c *Config) hasConfigMtimeChanged() bool { + info, err := os.Stat(c.V.ConfigFileUsed()) + if err != nil { + log.Errorf("Checking config file: %v", err) + return false + } + + return info.ModTime().After(c.configLastModTime) +} + +// Function to calculate the SHA-256 hash of a file +func (c *Config) configFileHash(filePath string) (string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", err + } + defer file.Close() + + hasher := sha256.New() + if _, err := io.Copy(hasher, file); err != nil { + return "", err + } + + return fmt.Sprintf("%x", hasher.Sum(nil)), nil +} + // checkUrlScheme checks given url has correct scheme and adds http:// if non if found func (c *Config) checkUrlScheme() { @@ -254,7 +292,6 @@ func (c *Config) checkUrlScheme() { default: c.ImmichUrl = defaultScheme + c.ImmichUrl } - } // checkRequiredFields check is required config files are set. @@ -291,101 +328,130 @@ func (c *Config) checkAlbumAndPerson() { c.Person = newPerson } -// Load loads yaml config file into memory, then loads ENV vars. ENV vars overwrites yaml settings. -func (c *Config) Load() error { - return c.load(defaultConfigFile) -} - -// Load loads yaml config file into memory with a custom path, then loads ENV vars. ENV vars overwrites yaml settings. -func (c *Config) LoadWithConfigLocation(configPath string) error { - return c.load(configPath) -} - -// WatchConfig starts a goroutine that periodically checks for changes in the configuration file -// and reloads the configuration if changes are detected. -// -// This function performs the following actions: -// 1. Retrieves the initial modification time of the config file. -// 2. Starts a goroutine that runs indefinitely. -// 3. Uses a ticker to check for config changes every 5 seconds. -// 4. If changes are detected, it reloads the configuration and updates the ReloadTimeStamp. +// WatchConfig sets up a configuration file watcher that monitors for changes +// and reloads the configuration when necessary. func (c *Config) WatchConfig() { + configPath := c.V.ConfigFileUsed() - configPath := c.v.ConfigFileUsed() - - fileInfo, err := os.Stat(configPath) - if os.IsNotExist(err) { + if err := validateConfigFile(configPath); err != nil { + log.Error(err) return } - if fileInfo.IsDir() { - log.Errorf("Config file %s is a directory", defaultConfigFile) + if err := c.initializeConfigState(); err != nil { + log.Error("Failed to initialize config state:", err) return } - info, err := os.Stat(configPath) + go c.watchConfigChanges() +} + +// initializeConfigState sets up the initial state of the configuration, +// including the last modification time and hash of the config file. +func (c *Config) initializeConfigState() error { + info, err := os.Stat(c.V.ConfigFileUsed()) + if err != nil { + return fmt.Errorf("getting initial file mTime: %v", err) + } + c.configLastModTime = info.ModTime() + + configHash, err := c.configFileHash(c.V.ConfigFileUsed()) if err != nil { - log.Infof("Error getting initial file info: %v", err) - } else { - c.configLastModTime = info.ModTime() + return fmt.Errorf("getting initial file hash: %v", err) } + c.configHash = configHash + + return nil +} + +// watchConfigChanges continuously monitors the configuration file for changes +// and triggers a reload when necessary. +func (c *Config) watchConfigChanges() { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() - go func() { - ticker := time.NewTicker(5 * time.Second) - defer ticker.Stop() - - //nolint:gosimple // Using for-select for ticker and potential future cases - for { - select { - case <-ticker.C: - if c.hasConfigChanged() { - log.Info("Config file changed, reloading config") - c.mu.Lock() - err := c.Load() - if err != nil { - log.Errorf("Reloading config: %v", err) - } else { - c.ReloadTimeStamp = time.Now().Format(time.RFC3339) - info, _ := os.Stat(defaultConfigFile) - c.configLastModTime = info.ModTime() - } - c.mu.Unlock() - } + hashCheckCount := 0 + const hashCheckInterval = 12 + + for range ticker.C { + if c.hasConfigMtimeChanged() { + c.reloadConfig("mTime changed") + hashCheckCount = 0 + continue + } + + if hashCheckCount >= hashCheckInterval { + if c.hasConfigHashChanged() { + c.reloadConfig("hash changed") } + hashCheckCount = 0 } - }() + + hashCheckCount++ + } +} + +// hasConfigHashChanged checks if the hash of the config file has changed. +func (c *Config) hasConfigHashChanged() bool { + configHash, err := c.configFileHash(c.V.ConfigFileUsed()) + if err != nil { + log.Error("configFileHash", "err", err) + return false + } + return c.configHash != configHash +} + +// reloadConfig reloads the configuration when a change is detected. +func (c *Config) reloadConfig(reason string) { + log.Infof("Config file %s, reloading config", reason) + c.mu.Lock() + defer c.mu.Unlock() + + if err := c.Load(); err != nil { + log.Error("Failed to reload config:", err) + } + + c.updateConfigState() +} + +// updateConfigState updates the configuration state after a reload. +func (c *Config) updateConfigState() { + configHash, _ := c.configFileHash(c.V.ConfigFileUsed()) + c.configHash = configHash + c.ReloadTimeStamp = time.Now().Format(time.RFC3339) + info, _ := os.Stat(c.V.ConfigFileUsed()) + c.configLastModTime = info.ModTime() } // load loads yaml config file into memory, then loads ENV vars. ENV vars overwrites yaml settings. -func (c *Config) load(configFile string) error { +func (c *Config) Load() error { - if err := bindEnvironmentVariables(c.v); err != nil { + if err := bindEnvironmentVariables(c.V); err != nil { log.Errorf("binding environment variables: %v", err) } - viper.SetConfigName("config") // Looks for 'config.yaml' - - // Optionally, specify the config file type (if you want to load from memory) - viper.SetConfigType("yaml") + c.V.SetConfigName("config") + c.V.SetConfigType("yaml") // Add potential paths for the configuration file - viper.AddConfigPath(".") // Look in the current directory - viper.AddConfigPath("./config") // Look in the 'config/' subdirectory + c.V.AddConfigPath(".") // Look in the current directory + c.V.AddConfigPath("./config/") // Look in the 'config/' subdirectory + c.V.AddConfigPath("../") // Look in the parent directory for testing - c.v.SetEnvPrefix("kiosk") + c.V.SetEnvPrefix("kiosk") - c.v.AutomaticEnv() + c.V.AutomaticEnv() - err := c.v.ReadInConfig() + err := c.V.ReadInConfig() if err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { - log.Infof("Not using %s", configFile) - } else if !isValidYAML(configFile) { + log.Info("Not using config.yaml") + } else if !isValidYAML(c.V.ConfigFileUsed()) { log.Fatal(err) } } - err = c.v.Unmarshal(&c) + err = c.V.Unmarshal(&c) if err != nil { log.Error("Environment can't be loaded", "err", err) return err @@ -422,8 +488,17 @@ func (c *Config) ConfigWithOverrides(e echo.Context) error { } +// String returns a string representation of the Config structure. +// If debug_verbose is not enabled, it returns a message prompting to enable it. +// Otherwise, it returns a JSON-formatted string of the entire Config structure. +// +// This method is useful for debugging and logging purposes, providing a +// detailed view of the current configuration when verbose debugging is enabled. +// +// Returns: +// - A string containing either a prompt to enable debug_verbose or +// the JSON representation of the Config structure. func (c *Config) String() string { - if !c.Kiosk.DebugVerbose { return "use debug_verbose for more info" } diff --git a/main.go b/main.go index 23395a5..0d1deb2 100644 --- a/main.go +++ b/main.go @@ -48,7 +48,10 @@ func main() { log.Error("Failed to load config", "err", err) } - baseConfig.WatchConfig() + if baseConfig.Kiosk.WatchConfig { + log.Infof("Watching %s for changes", baseConfig.V.ConfigFileUsed()) + baseConfig.WatchConfig() + } if baseConfig.Kiosk.Debug { diff --git a/routes/routes_test.go b/routes/routes_test.go index d957440..b8d641a 100644 --- a/routes/routes_test.go +++ b/routes/routes_test.go @@ -29,7 +29,7 @@ func TestNewRawImage(t *testing.T) { baseConfig := config.New() - err := baseConfig.LoadWithConfigLocation("../config.yaml") + err := baseConfig.Load() if err != nil { t.Error("Failed to load config", "err", err) } From 44cb2d9bacc9df09effd06904bb63248ea3623f6 Mon Sep 17 00:00:00 2001 From: Damon <2184238+damongolding@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:12:18 +0100 Subject: [PATCH 06/13] docs --- README.md | 5 +++++ config.example.yaml | 2 ++ taskfile.yml | 5 ++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ddc8a4..2a92880 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,8 @@ services: TZ: "Europe/London" volumes: - ./config.yaml:/config.yaml + # OR you can mount a directory with config.yaml inside + - ./config:/config restart: on-failure ports: - 3000:3000 @@ -178,10 +180,12 @@ services: KIOSK_SHOW_IMAGE_LOCATION: FALSE KIOSK_SHOW_IMAGE_ID: FALSE # Kiosk settings + KIOSK_WATCH_CONFIG: FALSE KIOSK_PASSWORD: "" KIOSK_CACHE: TRUE KIOSK_PREFETCH: TRUE KIOSK_ASSET_WEIGHTING: TRUE + KIOSK_PORT: 3000 ports: - 3000:3000 restart: on-failure @@ -250,6 +254,7 @@ kiosk: | **yaml** | **ENV** | **Value** | **Default** | **Description** | |-------------------|-------------------------|--------------|-------------|--------------------------------------------------------------------------------------------| | port | KIOSK_PORT | int | 3000 | Which port Kiosk should use. NOTE that is port will need to be reflected in your compose file e.g. `KIOSK_PORT:HOST_PORT` | +| watch_config | KIOSK_WATCH_CONFIG | bool | false | Should Kiosk watch config.yaml file for changes. Reloads all connect clients if a change is detected. | | password | KIOSK_PASSWORD | string | "" | Please see FAQs for more info. If set, requests MUST contain the password in the GET parameters e.g. `http://192.168.0.123:3000?password=PASSWORD`. | | cache | KIOSK_CACHE | bool | true | Cache selective Immich api calls to reduce unnecessary calls. | | prefetch | KIOSK_PREFETCH | bool | true | Pre fetch assets in the background so images load much quicker when refresh timer ends. | diff --git a/config.example.yaml b/config.example.yaml index 97f1b6e..e4739d2 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -57,6 +57,8 @@ show_image_id: false # options that can NOT be changed via url params kiosk: + port: 3000 + watch_config: false password: "" cache: true # cache select api calls pre_fetch: true # fetch assets in the background diff --git a/taskfile.yml b/taskfile.yml index a96143b..637ce38 100644 --- a/taskfile.yml +++ b/taskfile.yml @@ -1,6 +1,6 @@ version: "3" env: - VERSION: 0.11.2 + VERSION: 0.11.3-beta.3 tasks: default: deps: [build] @@ -57,6 +57,7 @@ tasks: - CGO_ENABLED=0 go build -ldflags "-X main.version={{.VERSION}}" -o dist/kiosk . docker-image: + deps: [build] cmds: - docker build --no-cache --build-arg VERSION={{.VERSION}} --load -t damongolding/immich-kiosk:{{.VERSION}} -t damongolding/immich-kiosk:latest . @@ -66,10 +67,12 @@ tasks: - docker buildx install docker-image-push: + deps: [build] cmds: - docker build --build-arg VERSION={{.VERSION}} --platform linux/amd64,linux/arm64 --push -t damongolding/immich-kiosk:{{.VERSION}} -t damongolding/immich-kiosk:latest . docker-dev-push: + deps: [build] cmds: - docker build --build-arg VERSION={{.VERSION}}-DEVELOPMENT --platform linux/amd64,linux/arm64 --push -t damongolding/immich-kiosk-development:{{.VERSION}} -t damongolding/immich-kiosk-development:latest . From 2c6c6fbb3b3f838f783b8cbdb0a2c10b9e2ccb05 Mon Sep 17 00:00:00 2001 From: Damon <2184238+damongolding@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:44:11 +0100 Subject: [PATCH 07/13] inset bug on WebOS --- frontend/src/css/menu.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/css/menu.css b/frontend/src/css/menu.css index b254765..6f5e74d 100644 --- a/frontend/src/css/menu.css +++ b/frontend/src/css/menu.css @@ -1,7 +1,10 @@ /* --- menu hit box --- */ #navigation-interaction-area { position: absolute; - inset: 0; + top: 0; + left: 0; + right: 0; + bottom: 0; width: 100vw; height: 100vh; z-index: 99998; From 98877ba349407c1a4f4fd8be8fc04cafa680450d Mon Sep 17 00:00:00 2001 From: Damon <2184238+damongolding@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:46:26 +0100 Subject: [PATCH 08/13] Update kiosk.css --- frontend/public/assets/css/kiosk.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/public/assets/css/kiosk.css b/frontend/public/assets/css/kiosk.css index 86af98b..1fdbf3b 100644 --- a/frontend/public/assets/css/kiosk.css +++ b/frontend/public/assets/css/kiosk.css @@ -567,7 +567,10 @@ body { /* src/css/menu.css */ #navigation-interaction-area { position: absolute; - inset: 0; + top: 0; + left: 0; + right: 0; + bottom: 0; width: 100vw; height: 100vh; z-index: 99998; From 46917a1d989d28df2ad81f219aaa01b758f9f7e2 Mon Sep 17 00:00:00 2001 From: Damon <2184238+damongolding@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:48:57 +0100 Subject: [PATCH 09/13] inset bug --- frontend/public/assets/css/kiosk.css | 5 +---- frontend/src/css/menu.css | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/frontend/public/assets/css/kiosk.css b/frontend/public/assets/css/kiosk.css index 1fdbf3b..1e53988 100644 --- a/frontend/public/assets/css/kiosk.css +++ b/frontend/public/assets/css/kiosk.css @@ -567,10 +567,7 @@ body { /* src/css/menu.css */ #navigation-interaction-area { position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + inset: 0px; width: 100vw; height: 100vh; z-index: 99998; diff --git a/frontend/src/css/menu.css b/frontend/src/css/menu.css index 6f5e74d..c16fe09 100644 --- a/frontend/src/css/menu.css +++ b/frontend/src/css/menu.css @@ -1,10 +1,7 @@ /* --- menu hit box --- */ #navigation-interaction-area { position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + inset: 0px; width: 100vw; height: 100vh; z-index: 99998; From b977ec89ae5dd8a0d9f0708945d45ca1374b9b14 Mon Sep 17 00:00:00 2001 From: Damon <2184238+damongolding@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:49:38 +0100 Subject: [PATCH 10/13] better build cmd --- Dockerfile | 2 +- taskfile.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index bb000f6..6aefb89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ WORKDIR /app COPY . . RUN go mod download -RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-X main.version=${VERSION}" -o dist/kiosk . +RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -a -installsuffix cgo -ldflags "-X main.version=${VERSION}" -o dist/kiosk . FROM alpine:latest diff --git a/taskfile.yml b/taskfile.yml index 637ce38..32c09d1 100644 --- a/taskfile.yml +++ b/taskfile.yml @@ -54,7 +54,7 @@ tasks: deps: [frontend, templ] cmds: - go generate *.go - - CGO_ENABLED=0 go build -ldflags "-X main.version={{.VERSION}}" -o dist/kiosk . + - CGO_ENABLED=0 go build -a -installsuffix cgo -ldflags "-X main.version={{.VERSION}}" -o dist/kiosk . docker-image: deps: [build] From db41a50af08e11de9387a5e8a89ccc268cbe1fa0 Mon Sep 17 00:00:00 2001 From: Damon <2184238+damongolding@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:00:22 +0100 Subject: [PATCH 11/13] added splitview-landscape --- custom.example.css | 11 ++++-- frontend/public/assets/css/kiosk.css | 34 ++++++++++++++++-- frontend/src/css/clock.css | 9 +++-- frontend/src/css/frame.css | 32 +++++++++++++++++ routes/routes_image_helpers.go | 17 +++++++++ taskfile.yml | 2 +- views/views_home.templ | 2 +- views/views_home_templ.go | 2 +- views/views_image.templ | 2 +- views/views_image_templ.go | 54 ++++++++++++++++++---------- 10 files changed, 134 insertions(+), 31 deletions(-) diff --git a/custom.example.css b/custom.example.css index 34c8f19..bee1c22 100644 --- a/custom.example.css +++ b/custom.example.css @@ -21,6 +21,11 @@ body{} .frame--layout-splitview:nth-child(1){} .frame--layout-splitview:nth-child(2){} .frame--layout-splitview .frame--image{} +.layout-splitview-landscape .frame{} +.frame--layout-splitview-landscape{} +.frame--layout-splitview-landscape:nth-child(1){} +.frame--layout-splitview-landscape:nth-child(2){} +.frame--layout-splitview-landscape .frame--image{} /* src/css/image.css */ .image--metadata{} .image--metadata--theme-fade::before{} @@ -56,12 +61,12 @@ body{} .sleep #offline{} /* src/css/clock.css */ #clock{} -.layout-splitview #clock{} +.layout-splitview-landscape #clock{} #clock:empty{} .clock--theme-fade::before{} -.layout-splitview .clock--theme-fade::before{} +.layout-splitview-landscape .clock--theme-fade::before{} .clock--theme-solid{} -.layout-splitview .clock--theme-solid{} +.layout-splitview-landscape .clock--theme-solid{} .clock--date{} .clock--time{} .sleep #clock{} diff --git a/frontend/public/assets/css/kiosk.css b/frontend/public/assets/css/kiosk.css index 1e53988..ca1933f 100644 --- a/frontend/public/assets/css/kiosk.css +++ b/frontend/public/assets/css/kiosk.css @@ -209,6 +209,31 @@ body { width: 50vw; height: 100vh; } +.layout-splitview-landscape .frame { + flex-direction: column; +} +.frame--layout-splitview-landscape { + position: relative; + width: 100%; + height: 50%; + width: 100vw; + height: 50vh; + overflow: hidden; +} +.frame--layout-splitview-landscape:nth-child(1) { + border-bottom: 0.125rem solid black; +} +.frame--layout-splitview-landscape:nth-child(2) { + border-top: 0.125rem solid black; +} +.frame--layout-splitview-landscape .frame--image { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + width: 100vw; + height: 50vh; +} /* src/css/image.css */ .image--metadata { @@ -454,7 +479,8 @@ body { text-shadow: 0 0 1.25rem rgba(0, 0, 0, 0.6); z-index: 10000; } -.layout-splitview #clock { +.layout-splitview #clock, +.layout-splitview-landscape #clock { bottom: unset; top: 0; } @@ -490,7 +516,8 @@ body { hsla(0, 0%, 0%, 0) 100%); z-index: 0; } -.layout-splitview .clock--theme-fade::before { +.layout-splitview .clock--theme-fade::before, +.layout-splitview-landscape .clock--theme-fade::before { content: ""; position: absolute; bottom: unset; @@ -524,7 +551,8 @@ body { background-color: rgba(0, 0, 0, 0.6); border-radius: 0 2rem 0 0; } -.layout-splitview .clock--theme-solid { +.layout-splitview .clock--theme-solid, +.layout-splitview-landscape .clock--theme-solid { border-radius: 0 0 2rem 0; } @media screen and (max-width: 31.25rem) { diff --git a/frontend/src/css/clock.css b/frontend/src/css/clock.css index 998072b..4d75268 100644 --- a/frontend/src/css/clock.css +++ b/frontend/src/css/clock.css @@ -11,7 +11,8 @@ z-index: 10000; } -.layout-splitview #clock { +.layout-splitview #clock, +.layout-splitview-landscape #clock { bottom: unset; top: 0; } @@ -51,7 +52,8 @@ z-index: 0; } -.layout-splitview .clock--theme-fade::before { +.layout-splitview .clock--theme-fade::before, +.layout-splitview-landscape .clock--theme-fade::before { content: ""; position: absolute; bottom: unset; @@ -88,7 +90,8 @@ border-radius: 0 2rem 0 0; } -.layout-splitview .clock--theme-solid { +.layout-splitview .clock--theme-solid, +.layout-splitview-landscape .clock--theme-solid { border-radius: 0 0 2rem 0; } diff --git a/frontend/src/css/frame.css b/frontend/src/css/frame.css index fd0b249..9fde30a 100644 --- a/frontend/src/css/frame.css +++ b/frontend/src/css/frame.css @@ -92,3 +92,35 @@ width: 50vw; height: 100vh; } + +/* Splitview landscape layout */ +.layout-splitview-landscape .frame { + flex-direction: column; +} + +.frame--layout-splitview-landscape { + position: relative; + width: 100%; + height: 50%; + + width: 100vw; + height: 50vh; + overflow: hidden; +} + +.frame--layout-splitview-landscape:nth-child(1) { + border-bottom: 0.125rem solid black; +} + +.frame--layout-splitview-landscape:nth-child(2) { + border-top: 0.125rem solid black; +} + +.frame--layout-splitview-landscape .frame--image { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + width: 100vw; + height: 50vh; +} diff --git a/routes/routes_image_helpers.go b/routes/routes_image_helpers.go index b918cff..c10de70 100644 --- a/routes/routes_image_helpers.go +++ b/routes/routes_image_helpers.go @@ -364,6 +364,23 @@ func generateViewData(requestConfig config.Config, c echo.Context, kioskDeviceID } viewData.Images = append(viewData.Images, viewDataSplitView) + case "splitview-landscape": + viewDataSplitView, err := ProcessViewImageData(requestConfig, c, isPrefetch) + if err != nil { + return viewData, err + } + viewData.Images = append(viewData.Images, viewDataSplitView) + + if viewDataSplitView.ImmichImage.IsPortrait { + return viewData, nil + } + + viewDataSplitView, err = ProcessViewImageDataWithRatio(immich.LandscapeOrientation, requestConfig, c, isPrefetch) + if err != nil { + return viewData, err + } + viewData.Images = append(viewData.Images, viewDataSplitView) + default: viewDataSingle, err := ProcessViewImageData(requestConfig, c, isPrefetch) if err != nil { diff --git a/taskfile.yml b/taskfile.yml index 32c09d1..8b79d78 100644 --- a/taskfile.yml +++ b/taskfile.yml @@ -54,7 +54,7 @@ tasks: deps: [frontend, templ] cmds: - go generate *.go - - CGO_ENABLED=0 go build -a -installsuffix cgo -ldflags "-X main.version={{.VERSION}}" -o dist/kiosk . + - CGO_ENABLED=0 go build -installsuffix cgo -ldflags "-X main.version={{.VERSION}}" -o dist/kiosk . docker-image: deps: [build] diff --git a/views/views_home.templ b/views/views_home.templ index 2e18017..a56e771 100644 --- a/views/views_home.templ +++ b/views/views_home.templ @@ -283,7 +283,7 @@ templ Home(viewData ViewData) { @templ.Raw(customCss(viewData.CustomCss)) } - + switch strings.ToLower(viewData.Transition) { case "cross-fade": @kioskCrossFade(viewData.KioskVersion, utils.GenerateUUID(), viewData.Queries, viewData.CrossFadeTransitionDuration) diff --git a/views/views_home_templ.go b/views/views_home_templ.go index cbc8bac..d44cc4d 100644 --- a/views/views_home_templ.go +++ b/views/views_home_templ.go @@ -789,7 +789,7 @@ func Home(viewData ViewData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var28 = []any{templ.KV("layout-splitview", strings.EqualFold(viewData.Layout, "splitview"))} + var templ_7745c5c3_Var28 = []any{fmt.Sprintf("layout-%s", viewData.Layout)} templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var28...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err diff --git a/views/views_image.templ b/views/views_image.templ index 66a8272..7d42038 100644 --- a/views/views_image.templ +++ b/views/views_image.templ @@ -167,7 +167,7 @@ templ layoutSingleView(viewData ViewData) { templ layoutSplitView(viewData ViewData) {
for imageIndex, imageData := range viewData.Images { -
+
if viewData.BackgroundBlur && !strings.EqualFold(viewData.ImageFit, "cover") && len(imageData.ImageBlurData) > 0 {
Blurred image background diff --git a/views/views_image_templ.go b/views/views_image_templ.go index dd2cfd2..dfbb454 100644 --- a/views/views_image_templ.go +++ b/views/views_image_templ.go @@ -510,7 +510,25 @@ func layoutSplitView(viewData ViewData) templ.Component { return templ_7745c5c3_Err } for imageIndex, imageData := range viewData.Images { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + var templ_7745c5c3_Var21 = []any{fmt.Sprintf("frame--layout-%s", viewData.Layout)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var21...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -519,12 +537,12 @@ func layoutSplitView(viewData ViewData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var21 string - templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(imageData.ImageBlurData) + var templ_7745c5c3_Var23 string + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(imageData.ImageBlurData) if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/views_image.templ`, Line: 173, Col: 40} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -533,8 +551,8 @@ func layoutSplitView(viewData ViewData) templ.Component { return templ_7745c5c3_Err } } - var templ_7745c5c3_Var22 = []any{"frame--image", templ.KV("frame--image-zoom", viewData.ImageZoom), animationDuration(viewData.Refresh), zoomInOrOut()} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...) + var templ_7745c5c3_Var24 = []any{"frame--image", templ.KV("frame--image-zoom", viewData.ImageZoom), animationDuration(viewData.Refresh), zoomInOrOut()} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var24...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -542,12 +560,12 @@ func layoutSplitView(viewData ViewData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var23 string - templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var22).String()) + var templ_7745c5c3_Var25 string + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var24).String()) if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/views_image.templ`, Line: 1, Col: 0} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -618,9 +636,9 @@ func Image(viewData ViewData) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var24 := templ.GetChildren(ctx) - if templ_7745c5c3_Var24 == nil { - templ_7745c5c3_Var24 = templ.NopComponent + templ_7745c5c3_Var26 := templ.GetChildren(ctx) + if templ_7745c5c3_Var26 == nil { + templ_7745c5c3_Var26 = templ.NopComponent } ctx = templ.ClearChildren(ctx) if len(viewData.Images) < 2 { @@ -643,12 +661,12 @@ func Image(viewData ViewData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var25 string - templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(historyEntry) + var templ_7745c5c3_Var27 string + templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(historyEntry) if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/views_image.templ`, Line: 206, Col: 88} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -662,12 +680,12 @@ func Image(viewData ViewData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var26 string - templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(newHistoryEntry.ImmichImage.ID) + var templ_7745c5c3_Var28 string + templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(newHistoryEntry.ImmichImage.ID) if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/views_image.templ`, Line: 209, Col: 106} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } From 4d5e1efd5270045ca4b5c2887e1de6eda761ce62 Mon Sep 17 00:00:00 2001 From: Damon <2184238+damongolding@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:18:29 +0100 Subject: [PATCH 12/13] the fix --- README.md | 4 +++- routes/routes_image.go | 2 +- routes/routes_image_helpers.go | 32 ++++++++++++++++++++++++-------- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 2a92880..035ff16 100644 --- a/README.md +++ b/README.md @@ -443,7 +443,9 @@ Display one image. > Kiosk attempts to determine the orientation of each image. However, if an image lacks EXIF data, > it may be displayed in an incorrect orientation (e.g., a portrait image shown in landscape format). -When a portrait image is fetched, Kiosk automatically retrieves a second portrait image and displays them side by side vertically. Landscape and square images are displayed individually. +When a portrait image is fetched, Kiosk automatically retrieves a second portrait image/* and displays them side by side vertically. Landscape and square images are displayed individually. + +/* If Kiosk is unable to retrieve a second unique image, the first image will be displayed individually. ![Kiosk layout splitview](/assets/layout-splitview.jpg) diff --git a/routes/routes_image.go b/routes/routes_image.go index 5092865..d67974d 100644 --- a/routes/routes_image.go +++ b/routes/routes_image.go @@ -58,7 +58,7 @@ func NewImage(baseConfig *config.Config) echo.HandlerFunc { ViewData, err := generateViewData(requestConfig, c, kioskDeviceID, false) if err != nil { - return RenderError(c, err, "processing image") + return RenderError(c, err, "retrieving image") } if requestConfig.Kiosk.PreFetch { diff --git a/routes/routes_image_helpers.go b/routes/routes_image_helpers.go index c10de70..a35b10a 100644 --- a/routes/routes_image_helpers.go +++ b/routes/routes_image_helpers.go @@ -341,6 +341,8 @@ func renderCachedViewData(c echo.Context, cachedViewData []views.ViewData, reque // generateViewData generates page data for the current request. func generateViewData(requestConfig config.Config, c echo.Context, kioskDeviceID string, isPrefetch bool) (views.ViewData, error) { + const maxImageRetivalAttepmts = 3 + viewData := views.ViewData{ DeviceID: kioskDeviceID, Config: requestConfig, @@ -358,11 +360,18 @@ func generateViewData(requestConfig config.Config, c echo.Context, kioskDeviceID return viewData, nil } - viewDataSplitView, err = ProcessViewImageDataWithRatio(immich.PortraitOrientation, requestConfig, c, isPrefetch) - if err != nil { - return viewData, err + // Second image + for i := 0; i < maxImageRetivalAttepmts; i++ { + viewDataSplitViewSecond, err := ProcessViewImageDataWithRatio(immich.PortraitOrientation, requestConfig, c, isPrefetch) + if err != nil { + return viewData, err + } + + if viewDataSplitView.ImmichImage.ID != viewDataSplitViewSecond.ImmichImage.ID { + viewData.Images = append(viewData.Images, viewDataSplitViewSecond) + break + } } - viewData.Images = append(viewData.Images, viewDataSplitView) case "splitview-landscape": viewDataSplitView, err := ProcessViewImageData(requestConfig, c, isPrefetch) @@ -375,11 +384,18 @@ func generateViewData(requestConfig config.Config, c echo.Context, kioskDeviceID return viewData, nil } - viewDataSplitView, err = ProcessViewImageDataWithRatio(immich.LandscapeOrientation, requestConfig, c, isPrefetch) - if err != nil { - return viewData, err + // Second image + for i := 0; i < maxImageRetivalAttepmts; i++ { + viewDataSplitViewSecond, err := ProcessViewImageDataWithRatio(immich.PortraitOrientation, requestConfig, c, isPrefetch) + if err != nil { + return viewData, err + } + + if viewDataSplitView.ImmichImage.ID != viewDataSplitViewSecond.ImmichImage.ID { + viewData.Images = append(viewData.Images, viewDataSplitViewSecond) + break + } } - viewData.Images = append(viewData.Images, viewDataSplitView) default: viewDataSingle, err := ProcessViewImageData(requestConfig, c, isPrefetch) From cbefc6d4780de3760935f44d0050a887c797bc76 Mon Sep 17 00:00:00 2001 From: Damon <2184238+damongolding@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:22:09 +0100 Subject: [PATCH 13/13] docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 035ff16..d8badec 100644 --- a/README.md +++ b/README.md @@ -443,9 +443,9 @@ Display one image. > Kiosk attempts to determine the orientation of each image. However, if an image lacks EXIF data, > it may be displayed in an incorrect orientation (e.g., a portrait image shown in landscape format). -When a portrait image is fetched, Kiosk automatically retrieves a second portrait image/* and displays them side by side vertically. Landscape and square images are displayed individually. +When a portrait image is fetched, Kiosk automatically retrieves a second portrait image\* and displays them side by side vertically. Landscape and square images are displayed individually. -/* If Kiosk is unable to retrieve a second unique image, the first image will be displayed individually. +\* If Kiosk is unable to retrieve a second unique image, the first image will be displayed individually. ![Kiosk layout splitview](/assets/layout-splitview.jpg)