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/README.md b/README.md index fb72525..80c1550 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,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 @@ -179,10 +181,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 @@ -251,6 +255,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. | @@ -439,7 +444,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/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/config/config.go b/config/config.go index 898ce99..2171478 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,15 +169,11 @@ 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), } defaults.SetDefaults(c) - info, err := os.Stat(defaultConfigFile) - if err == nil { - c.configLastModTime = info.ModTime() - } return c } @@ -209,6 +213,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"}, @@ -248,6 +253,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() { @@ -260,7 +305,6 @@ func (c *Config) checkUrlScheme() { default: c.ImmichUrl = defaultScheme + c.ImmichUrl } - } // checkRequiredFields check is required config files are set. @@ -280,11 +324,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 @@ -292,100 +335,136 @@ 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 } -// 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() - fileInfo, err := os.Stat(defaultConfigFile) - 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(defaultConfigFile) + 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 { - log.Errorf("Getting initial file info: %v", err) - } else { - c.configLastModTime = info.ModTime() - } - - 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() - } + return fmt.Errorf("getting initial file mTime: %v", err) + } + c.configLastModTime = info.ModTime() + + configHash, err := c.configFileHash(c.V.ConfigFileUsed()) + if err != nil { + 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() + + 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) } - c.v.AddConfigPath(".") + c.V.SetConfigName("config") + c.V.SetConfigType("yaml") - c.v.SetConfigFile(configFile) + // Add potential paths for the configuration file + 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 _, err := os.Stat(configFile); os.IsNotExist(err) { - log.Infof("Not using %s", configFile) - } else if !isValidYAML(configFile) { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + 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 +501,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/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 86af98b..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) { @@ -567,7 +595,7 @@ body { /* src/css/menu.css */ #navigation-interaction-area { position: absolute; - inset: 0; + inset: 0px; width: 100vw; height: 100vh; z-index: 99998; 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/frontend/src/css/menu.css b/frontend/src/css/menu.css index b254765..c16fe09 100644 --- a/frontend/src/css/menu.css +++ b/frontend/src/css/menu.css @@ -1,7 +1,7 @@ /* --- menu hit box --- */ #navigation-interaction-area { position: absolute; - inset: 0; + inset: 0px; width: 100vw; height: 100vh; z-index: 99998; diff --git a/main.go b/main.go index eccaecb..92799aa 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_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 b918cff..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,12 +360,43 @@ func generateViewData(requestConfig config.Config, c echo.Context, kioskDeviceID return viewData, nil } - viewDataSplitView, err = ProcessViewImageDataWithRatio(immich.PortraitOrientation, requestConfig, c, isPrefetch) + // 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 + } + } + + 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 + } + + // 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 + } + } + default: viewDataSingle, err := ProcessViewImageData(requestConfig, c, isPrefetch) if err != nil { 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) } diff --git a/taskfile.yml b/taskfile.yml index 88e469a..0c90e89 100644 --- a/taskfile.yml +++ b/taskfile.yml @@ -1,6 +1,7 @@ version: "3" env: - VERSION: 0.11.3-beta.2 + VERSION: 0.11.3-beta.3 + tasks: default: deps: [build] @@ -54,9 +55,10 @@ 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 -installsuffix cgo -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 +68,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 . diff --git a/views/views_home.templ b/views/views_home.templ index ab76cb9..940d197 100644 --- a/views/views_home.templ +++ b/views/views_home.templ @@ -288,7 +288,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 087412b..2fd3ab5 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 }