diff --git a/Dockerfile b/Dockerfile index 83abfa9..bb000f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,6 @@ ARG TARGETARCH WORKDIR /app COPY . . -COPY config.example.yaml /app/config/ RUN go mod download RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-X main.version=${VERSION}" -o dist/kiosk . @@ -21,12 +20,10 @@ ENV TERM=xterm-256color ENV DEBUG_COLORS=true ENV COLORTERM=truecolor -RUN apk add --no-cache tzdata +RUN apk update && apk add --no-cache tzdata ca-certificates && update-ca-certificates WORKDIR / COPY --from=build /app/dist/kiosk . -EXPOSE 3000 - ENTRYPOINT ["/kiosk"] diff --git a/README.md b/README.md index caaf23a..f54ca9a 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,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` | | 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. | @@ -596,8 +597,9 @@ Then to access Kiosk you MUST add the password param in your URL e.g. http://{UR - PWA - monitor config file changes - make apiCalls more resilient -- Ken Burns +- Ken Burns - splitview horizontal mode +- docker/immich healthcheck? ------ diff --git a/config.example.yaml b/config.example.yaml index fe0188d..a979daa 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -14,10 +14,15 @@ disable_screensaver: false # Ask browser to request a lock that prevents device # Asset sources show_archived: false # Allow assets marked as archived to be displayed. -person: # ID(s) of person or people to display - - "" -album: # ID(s) of album or albums to display - - "" + +# ID(s) of person or people to display +person: + - "PERSON_ID" + +# ID(s) of album or albums to display +album: + - "ALBUM_ID" + # UI disable_ui: false # this is just a shortcut for all ui elements (show_time, show_date, show_image_time, show_image_date) hide_cursor: false # Hide cursor/mouse via CSS. diff --git a/config/config.go b/config/config.go index 326adac..896cbd3 100644 --- a/config/config.go +++ b/config/config.go @@ -23,11 +23,15 @@ package config import ( "encoding/json" "errors" + "os" "strings" + "sync" + "time" "github.com/charmbracelet/log" "github.com/mcuadros/go-defaults" "github.com/spf13/viper" + "gopkg.in/yaml.v3" "github.com/labstack/echo/v4" ) @@ -36,9 +40,13 @@ const ( defaultImmichPort = "2283" defaultScheme = "http://" DefaultDateLayout = "02/01/2006" + defaultConfigFile = "config.yaml" ) type KioskSettings struct { + // Port which port to use + Port int `mapstructure:"port" default:"3000"` + // Cache enable/disable api call and image caching Cache bool `mapstructure:"cache" default:"true"` @@ -57,6 +65,15 @@ type KioskSettings struct { } type Config struct { + // 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 + // ImmichApiKey Immich key to access assets ImmichApiKey string `mapstructure:"immich_api_key" default:""` // ImmichUrl Immuch base url @@ -131,6 +148,8 @@ type Config struct { ShowImageExif bool `mapstructure:"show_image_exif" query:"show_image_exif" form:"show_image_exif" default:"false"` // ShowImageLocation display image location data ShowImageLocation bool `mapstructure:"show_image_location" query:"show_image_location" form:"show_image_location" default:"false"` + // ShowImageID display image ID + ShowImageID bool `mapstructure:"show_image_id" query:"show_image_id" form:"show_image_id" default:"false"` // Kiosk settings that are unable to be changed via URL queries Kiosk KioskSettings `mapstructure:"kiosk"` @@ -141,11 +160,30 @@ type Config struct { // New returns a new config pointer instance func New() *Config { - c := &Config{} + c := &Config{ + 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 } +// hasConfigChanged checks if the configuration file has been modified since the last check. +func (c *Config) hasConfigChanged() bool { + info, err := os.Stat(defaultConfigFile) + 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. @@ -168,6 +206,7 @@ func bindEnvironmentVariables(v *viper.Viper) error { configKey string envVar string }{ + {"kiosk.port", "KIOSK_PORT"}, {"kiosk.password", "KIOSK_PASSWORD"}, {"kiosk.cache", "KIOSK_CACHE"}, {"kiosk.prefetch", "KIOSK_PREFETCH"}, @@ -189,6 +228,24 @@ func bindEnvironmentVariables(v *viper.Viper) error { return nil } +// isValidYAML checks if the given file is a valid YAML file. +func isValidYAML(filename string) bool { + content, err := os.ReadFile(filename) + if err != nil { + log.Errorf("Error reading file: %v", err) + return false + } + + var data interface{} + err = yaml.Unmarshal(content, &data) + if err != nil { + log.Fatal(err) + return false + } + + return true +} + // checkUrlScheme checks given url has correct scheme and adds http:// if non if found func (c *Config) checkUrlScheme() { @@ -220,9 +277,28 @@ func (c *Config) checkDebuging() { } } +func (c *Config) checkAlbumAndPerson() { + + newAlbum := []string{} + for _, album := range c.Album { + if album != "" && album != "ALBUM_ID" { + newAlbum = append(newAlbum, album) + } + } + c.Album = newAlbum + + newPerson := []string{} + for _, person := range c.Person { + if person != "" && person != "PERSON_ID" { + newPerson = append(newPerson, 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("config.yaml") + return c.load(defaultConfigFile) } // Load loads yaml config file into memory with a custom path, then loads ENV vars. ENV vars overwrites yaml settings. @@ -230,40 +306,95 @@ 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. +func (c *Config) WatchConfig() { + + fileInfo, err := os.Stat(defaultConfigFile) + if os.IsNotExist(err) { + return + } + + if fileInfo.IsDir() { + log.Errorf("Config file %s is a directory", defaultConfigFile) + return + } + + info, err := os.Stat(defaultConfigFile) + if err != nil { + log.Infof("Error 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() + } + } + } + }() +} + // load loads yaml config file into memory, then loads ENV vars. ENV vars overwrites yaml settings. func (c *Config) load(configFile string) error { - v := viper.NewWithOptions(viper.ExperimentalBindStruct()) - - if err := bindEnvironmentVariables(v); err != nil { + if err := bindEnvironmentVariables(c.v); err != nil { log.Errorf("binding environment variables: %v", err) } - v.AddConfigPath(".") + c.v.AddConfigPath(".") - v.SetConfigFile(configFile) + c.v.SetConfigFile(configFile) - v.SetEnvPrefix("kiosk") + c.v.SetEnvPrefix("kiosk") - v.AutomaticEnv() + c.v.AutomaticEnv() - err := v.ReadInConfig() + err := c.v.ReadInConfig() if err != nil { - log.Debug("config.yaml file not being used") + if _, err := os.Stat(configFile); os.IsNotExist(err) { + log.Infof("Not using %s", configFile) + } else if !isValidYAML(configFile) { + log.Fatal(err) + } } - err = v.Unmarshal(&c) + err = c.v.Unmarshal(&c) if err != nil { log.Error("Environment can't be loaded", "err", err) return err } c.checkRequiredFields() + c.checkAlbumAndPerson() c.checkUrlScheme() c.checkDebuging() return nil - } // ConfigWithOverrides overwrites base config with ones supplied via URL queries diff --git a/config/config_test.go b/config/config_test.go index 763c903..c00cbfc 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -85,7 +85,7 @@ func TestMalformedURLs(t *testing.T) { t.Setenv("KIOSK_IMMICH_URL", test.KIOSK_IMMICH_URL) t.Setenv("KIOSK_IMMICH_API_KEY", "12345") - var c Config + c := New() err := c.Load() assert.NoError(t, err, "Config load should not return an error") @@ -166,3 +166,63 @@ func TestImmichUrlImmichMulitpleAlbum(t *testing.T) { assert.Contains(t, configWithBaseOnly.Album, "BASE_ALBUM_1", "BASE_ALBUM_1 should be present") assert.Contains(t, configWithBaseOnly.Album, "BASE_ALBUM_2", "BASE_ALBUM_2 should be present") } + +func TestAlbumAndPerson(t *testing.T) { + testCases := []struct { + name string + inputAlbum []string + inputPerson []string + expectedAlbum []string + expectedPerson []string + }{ + { + name: "No empty values", + inputAlbum: []string{"album1", "album2"}, + inputPerson: []string{"person1", "person2"}, + expectedAlbum: []string{"album1", "album2"}, + expectedPerson: []string{"person1", "person2"}, + }, + { + name: "Empty values in album", + inputAlbum: []string{"album1", "", "album2", ""}, + inputPerson: []string{"person1", "person2"}, + expectedAlbum: []string{"album1", "album2"}, + expectedPerson: []string{"person1", "person2"}, + }, + { + name: "Empty values in person", + inputAlbum: []string{"album1", "album2"}, + inputPerson: []string{"", "person1", "", "person2"}, + expectedAlbum: []string{"album1", "album2"}, + expectedPerson: []string{"person1", "person2"}, + }, + { + name: "Empty values in both", + inputAlbum: []string{"", "album1", "", "album2"}, + inputPerson: []string{"person1", "", "", "person2"}, + expectedAlbum: []string{"album1", "album2"}, + expectedPerson: []string{"person1", "person2"}, + }, + { + name: "All empty values", + inputAlbum: []string{"", "", ""}, + inputPerson: []string{"", "", ""}, + expectedAlbum: []string{}, + expectedPerson: []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := &Config{ + Album: tc.inputAlbum, + Person: tc.inputPerson, + } + + c.checkAlbumAndPerson() + + assert.Equal(t, tc.expectedAlbum, c.Album, "Album mismatch") + assert.Equal(t, tc.expectedPerson, c.Person, "Person mismatch") + }) + } +} diff --git a/custom.example.css b/custom.example.css index 8ac4079..34c8f19 100644 --- a/custom.example.css +++ b/custom.example.css @@ -28,7 +28,8 @@ body{} .image--metadata--theme-solid{} .frame--layout-splitview:nth-child(1) .image--metadata--theme-solid{} .image--metadata .responsive-break{} -.image--metadata--location:empty{} +.image--metadata div:empty{} +.image--metadata div{} .image--metadata--date{} .image--metadata--exif{} .image--metadata--exif--fnumber{} diff --git a/frontend/public/assets/css/kiosk.css b/frontend/public/assets/css/kiosk.css index 5716cc8..86af98b 100644 --- a/frontend/public/assets/css/kiosk.css +++ b/frontend/public/assets/css/kiosk.css @@ -297,50 +297,29 @@ body { display: none; } .image--metadata:empty, -.image--metadata--date:empty, -.image--metadata--exif:empty, -.image--metadata--location:empty { +.image--metadata div:empty { display: none; padding: 0; } +.image--metadata div { + z-index: 1; +} .image--metadata--date { font-size: 1.3rem; - z-index: 1; } .image--metadata--exif { - z-index: 1; } .image--metadata--exif--fnumber { display: inline-block; font-size: 0.84rem; font-weight: bold; transform: translate(0.0625rem, -0.1875rem); - z-index: 1; } .image--metadata--exif--seperator { opacity: 0.3; padding: 0 0.5rem; - z-index: 1; } .image--metadata--location { - z-index: 1; -} -@media screen and (max-width: 31.25rem) { - .image--metadata { - padding: 0.5rem; - max-width: 50vw; - } - .image--metadata--date, - .image--metadata--exif, - .image--metadata--location { - padding-left: 0.5rem; - } - .image--metadata--location span { - display: none; - } - .image--metadata--location .responsive-break { - display: inline; - } } .frame--layout-splitview:nth-child(1) .image--metadata { position: absolute; @@ -352,7 +331,6 @@ body { left: 0; right: unset; max-width: 70vw; - padding: 1rem 1.375rem 1rem 1rem; color: #fff; font-size: 1.1rem; text-align: left; @@ -370,6 +348,23 @@ body { .frame--image-zoom-out img { animation-name: image-zoom-out; } +@media screen and (max-width: 31.25rem) { + .image--metadata { + padding: 0.5rem !important; + max-width: 50vw; + } + .image--metadata--date, + .image--metadata--exif, + .image--metadata--location { + padding-left: 0.5rem; + } + .image--metadata--location span { + display: none; + } + .image--metadata--location .responsive-break { + display: inline; + } +} /* src/css/error.css */ .error-container { diff --git a/frontend/public/assets/js/kiosk.js b/frontend/public/assets/js/kiosk.js index 9508b91..9a450b8 100644 --- a/frontend/public/assets/js/kiosk.js +++ b/frontend/public/assets/js/kiosk.js @@ -3789,6 +3789,9 @@ var kiosk = (() => { var fullscreenButton = htmx_esm_default.find( ".navigation--fullscreen" ); + var fullScreenButtonSeperator = htmx_esm_default.find( + ".navigation--fullscreen-separator" + ); var kiosk = htmx_esm_default.find("#kiosk"); var menu = htmx_esm_default.find(".navigation"); var menuInteraction = htmx_esm_default.find( @@ -3806,6 +3809,7 @@ var kiosk = (() => { } if (!fullscreenAPI.requestFullscreen) { fullscreenButton && htmx_esm_default.remove(fullscreenButton); + fullScreenButtonSeperator && htmx_esm_default.remove(fullScreenButtonSeperator); } if (pollInterval2) { initPolling(pollInterval2, kiosk, menu, menuPausePlayButton2); diff --git a/frontend/src/css/image.css b/frontend/src/css/image.css index eaf83fb..6d47cd3 100644 --- a/frontend/src/css/image.css +++ b/frontend/src/css/image.css @@ -93,55 +93,34 @@ } .image--metadata:empty, -.image--metadata--date:empty, -.image--metadata--exif:empty, -.image--metadata--location:empty { +.image--metadata div:empty { display: none; padding: 0; } +.image--metadata div { + z-index: 1; +} + .image--metadata--date { font-size: 1.3rem; - - z-index: 1; } + .image--metadata--exif { - z-index: 1; } .image--metadata--exif--fnumber { display: inline-block; font-size: 0.84rem; font-weight: bold; transform: translate(0.0625rem, -0.1875rem); - z-index: 1; } + .image--metadata--exif--seperator { opacity: 0.3; padding: 0 0.5rem; - z-index: 1; -} -.image--metadata--location { - z-index: 1; } -@media screen and (max-width: 31.25rem) { - .image--metadata { - padding: 0.5rem; - max-width: 50vw; - } - - .image--metadata--date, - .image--metadata--exif, - .image--metadata--location { - padding-left: 0.5rem; - } - - .image--metadata--location span { - display: none; - } - .image--metadata--location .responsive-break { - display: inline; - } +.image--metadata--location { } .frame--layout-splitview:nth-child(1) .image--metadata { @@ -154,7 +133,6 @@ left: 0; right: unset; max-width: 70vw; - padding: 1rem 1.375rem 1rem 1rem; color: #fff; font-size: 1.1rem; text-align: left; @@ -169,10 +147,29 @@ transition-timing-function: cubic-bezier(0.455, 0.03, 0.515, 0.955); animation-fill-mode: forwards; } - .frame--image-zoom-in img { animation-name: image-zoom-in; } .frame--image-zoom-out img { animation-name: image-zoom-out; } + +@media screen and (max-width: 31.25rem) { + .image--metadata { + padding: 0.5rem !important; + max-width: 50vw; + } + + .image--metadata--date, + .image--metadata--exif, + .image--metadata--location { + padding-left: 0.5rem; + } + + .image--metadata--location span { + display: none; + } + .image--metadata--location .responsive-break { + display: inline; + } +} diff --git a/frontend/src/ts/kiosk.ts b/frontend/src/ts/kiosk.ts index e24fca3..d6a0c04 100644 --- a/frontend/src/ts/kiosk.ts +++ b/frontend/src/ts/kiosk.ts @@ -31,6 +31,9 @@ const documentBody = document.body; const fullscreenButton = htmx.find( ".navigation--fullscreen", ) as HTMLElement | null; +const fullScreenButtonSeperator = htmx.find( + ".navigation--fullscreen-separator", +) as HTMLElement | null; const kiosk = htmx.find("#kiosk") as HTMLElement | null; const menu = htmx.find(".navigation") as HTMLElement | null; const menuInteraction = htmx.find( @@ -54,6 +57,7 @@ function init() { if (!fullscreenAPI.requestFullscreen) { fullscreenButton && htmx.remove(fullscreenButton); + fullScreenButtonSeperator && htmx.remove(fullScreenButtonSeperator); } if (pollInterval) { diff --git a/go.mod b/go.mod index 117d7ae..c35cd3a 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/spf13/viper v1.20.0-alpha.6 github.com/stretchr/testify v1.9.0 golang.org/x/image v0.20.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -49,5 +50,4 @@ require ( golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect golang.org/x/time v0.6.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/immich/immich.go b/immich/immich.go index f24229f..ae6dd4b 100644 --- a/immich/immich.go +++ b/immich/immich.go @@ -31,7 +31,7 @@ const ( AlbumKeywordAll string = "all" AlbumKeywordShared string = "shared" AlbumKeywordFavourites string = "favourites" - AlbumKeywordFavorites string = "favorites" + AlbumKeywordFavorites string = "favorites" ) var ( @@ -67,7 +67,7 @@ type ExifInfo struct { FNumber float64 `json:"fNumber"` FocalLength float64 `json:"focalLength"` Iso int `json:"iso"` - ExposureTime string `json:"-"` // `json:"exposureTime"` + ExposureTime string `json:"exposureTime"` Latitude float64 `json:"-"` // `json:"latitude"` Longitude float64 `json:"-"` // `json:"longitude"` City string `json:"city"` diff --git a/immich/immich_helpers.go b/immich/immich_helpers.go index 0502256..9f95547 100644 --- a/immich/immich_helpers.go +++ b/immich/immich_helpers.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "net/url" + "time" "github.com/charmbracelet/log" "github.com/patrickmn/go-cache" @@ -18,18 +19,7 @@ func immichApiFail[T ImmichApiResponse](value T, err error, body []byte, apiUrl errorUnmarshalErr := json.Unmarshal(body, &immichError) if errorUnmarshalErr != nil { log.Error("Couldn't read error", "body", string(body), "url", apiUrl) - return value, fmt.Errorf(` - No data or error returned from Immich API. - -

- Full error:

- %w -

- `, err) + return value, err } log.Errorf("%s : %v", immichError.Error, immichError.Message) return value, fmt.Errorf("%s : %v", immichError.Error, immichError.Message) @@ -92,7 +82,9 @@ func (i *ImmichAsset) immichApiCall(method, apiUrl string, body io.Reader) ([]by var responseBody []byte - client := &http.Client{} + client := &http.Client{ + Timeout: time.Second * 10, + } req, err := http.NewRequest(method, apiUrl, body) if err != nil { log.Error(err) @@ -102,20 +94,35 @@ func (i *ImmichAsset) immichApiCall(method, apiUrl string, body io.Reader) ([]by req.Header.Add("Accept", "application/json") req.Header.Add("x-api-key", requestConfig.ImmichApiKey) - if method == "POST" { + if method == "POST" || method == "PUT" || method == "PATCH" { req.Header.Add("Content-Type", "application/json") } - res, err := client.Do(req) + var res *http.Response + for attempts := 0; attempts < 3; attempts++ { + res, err = client.Do(req) + if err == nil { + break + } + log.Error("Request failed, retrying", "attempt", attempts, "URL", apiUrl, "err", err) + time.Sleep(time.Duration(attempts) * time.Second) + } if err != nil { - log.Error(err) + log.Error("Request failed after retries", "err", err) return responseBody, err } + defer res.Body.Close() + if res.StatusCode < 200 || res.StatusCode >= 300 { + err = fmt.Errorf("unexpected status code: %d", res.StatusCode) + log.Error(err) + return responseBody, err + } + responseBody, err = io.ReadAll(res.Body) if err != nil { - log.Error(err) + log.Error("reading response body", "url", apiUrl, "err", err) return responseBody, err } diff --git a/main.go b/main.go index a53dc8d..23395a5 100644 --- a/main.go +++ b/main.go @@ -39,6 +39,8 @@ func init() { func main() { + log.SetTimeFormat("15:04:05") + baseConfig := config.New() err := baseConfig.Load() @@ -46,8 +48,9 @@ func main() { log.Error("Failed to load config", "err", err) } + baseConfig.WatchConfig() + if baseConfig.Kiosk.Debug { - log.SetTimeFormat("15:04:05") log.SetLevel(log.DebugLevel) if baseConfig.Kiosk.DebugVerbose { @@ -107,7 +110,9 @@ func main() { e.GET("/cache/flush", routes.FlushCache) - err = e.Start(":3000") + e.POST("/refresh/check", routes.RefreshCheck(baseConfig)) + + err = e.Start(fmt.Sprintf(":%v", baseConfig.Kiosk.Port)) if err != nil { log.Fatal(err) } diff --git a/routes/routes.go b/routes/routes.go index c5d79c5..4debc90 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -22,7 +22,7 @@ import ( var ( KioskVersion string - viewDataCache *cache.Cache + ViewDataCache *cache.Cache viewDataCacheMutex sync.Mutex ) @@ -38,7 +38,7 @@ type RequestData struct { func init() { // Setting up Immich api cache - viewDataCache = cache.New(5*time.Minute, 10*time.Minute) + ViewDataCache = cache.New(5*time.Minute, 10*time.Minute) } func RenderError(c echo.Context, err error, message string) error { diff --git a/routes/routes_cache.go b/routes/routes_cache.go index 2fa0348..a1246f7 100644 --- a/routes/routes_cache.go +++ b/routes/routes_cache.go @@ -13,12 +13,12 @@ func FlushCache(c echo.Context) error { viewDataCacheMutex.Lock() defer viewDataCacheMutex.Unlock() - log.Info("Cache before flush", "viewDataCache_items", viewDataCache.ItemCount(), "apiCache_items", immich.ApiCacheCount()) + log.Info("Cache before flush", "viewDataCache_items", ViewDataCache.ItemCount(), "apiCache_items", immich.ApiCacheCount()) - viewDataCache.Flush() + ViewDataCache.Flush() immich.FluchApiCache() - log.Info("Cache after flush ", "viewDataCache_items", viewDataCache.ItemCount(), "apiCache_items", immich.ApiCacheCount()) + log.Info("Cache after flush ", "viewDataCache_items", ViewDataCache.ItemCount(), "apiCache_items", immich.ApiCacheCount()) c.Response().Header().Set("HX-Refresh", "true") return c.NoContent(http.StatusOK) diff --git a/routes/routes_clock.go b/routes/routes_clock.go index cd320d9..be24ccd 100644 --- a/routes/routes_clock.go +++ b/routes/routes_clock.go @@ -2,7 +2,6 @@ package routes import ( "net/http" - "time" "github.com/charmbracelet/log" "github.com/labstack/echo/v4" @@ -16,18 +15,11 @@ import ( func Clock(baseConfig *config.Config) echo.HandlerFunc { return func(c echo.Context) error { - kioskVersionHeader := c.Request().Header.Get("kiosk-version") requestID := utils.ColorizeRequestId(c.Response().Header().Get(echo.HeaderXRequestID)) // create a copy of the global config to use with this request requestConfig := *baseConfig - // If kiosk version on client and server do not match refresh client. - if kioskVersionHeader != "" && KioskVersion != kioskVersionHeader { - c.Response().Header().Set("HX-Refresh", "true") - return c.NoContent(http.StatusOK) - } - err := requestConfig.ConfigWithOverrides(c) if err != nil { log.Error("overriding config", "err", err) @@ -43,30 +35,6 @@ func Clock(baseConfig *config.Config) echo.HandlerFunc { "DateFormat", requestConfig.DateFormat, ) - clockTimeFormat := "15:04" - if requestConfig.TimeFormat == "12" { - clockTimeFormat = time.Kitchen - } - - clockDateFormat := utils.DateToLayout(requestConfig.DateFormat) - if clockDateFormat == "" { - clockDateFormat = config.DefaultDateLayout - } - - var data views.ClockData - - t := time.Now() - - switch { - case (requestConfig.ShowTime && requestConfig.ShowDate): - data.ClockTime = t.Format(clockTimeFormat) - data.ClockDate = t.Format(clockDateFormat) - case requestConfig.ShowTime: - data.ClockTime = t.Format(clockTimeFormat) - case requestConfig.ShowDate: - data.ClockDate = t.Format(clockDateFormat) - } - - return Render(c, http.StatusOK, views.Clock(data)) + return Render(c, http.StatusOK, views.Clock(requestConfig)) } } diff --git a/routes/routes_image_helpers.go b/routes/routes_image_helpers.go index d5d6dab..b918cff 100644 --- a/routes/routes_image_helpers.go +++ b/routes/routes_image_helpers.go @@ -252,13 +252,13 @@ func imagePreFetch(requestConfig config.Config, c echo.Context, kioskDeviceID st cacheKey := c.Request().URL.String() + kioskDeviceID - if data, found := viewDataCache.Get(cacheKey); found { + if data, found := ViewDataCache.Get(cacheKey); found { cachedViewData = data.([]views.ViewData) } cachedViewData = append(cachedViewData, viewDataToAdd) - viewDataCache.Set(cacheKey, cachedViewData, cache.DefaultExpiration) + ViewDataCache.Set(cacheKey, cachedViewData, cache.DefaultExpiration) } @@ -309,12 +309,12 @@ func fromCache(c echo.Context, kioskDeviceID string) []views.ViewData { defer viewDataCacheMutex.Unlock() cacheKey := c.Request().URL.String() + kioskDeviceID - if data, found := viewDataCache.Get(cacheKey); found { + if data, found := ViewDataCache.Get(cacheKey); found { cachedPageData := data.([]views.ViewData) if len(cachedPageData) > 0 { return cachedPageData } - viewDataCache.Delete(cacheKey) + ViewDataCache.Delete(cacheKey) } return nil } @@ -329,7 +329,7 @@ func renderCachedViewData(c echo.Context, cachedViewData []views.ViewData, reque cacheKey := c.Request().URL.String() + kioskDeviceID viewDataToRender := cachedViewData[0] - viewDataCache.Set(cacheKey, cachedViewData[1:], cache.DefaultExpiration) + ViewDataCache.Set(cacheKey, cachedViewData[1:], cache.DefaultExpiration) // Update history which will be outdated in cache trimHistory(&requestConfig.History, 10) diff --git a/routes/routes_refresh.go b/routes/routes_refresh.go new file mode 100644 index 0000000..2be178c --- /dev/null +++ b/routes/routes_refresh.go @@ -0,0 +1,38 @@ +package routes + +import ( + "net/http" + + "github.com/charmbracelet/log" + "github.com/labstack/echo/v4" + + "github.com/damongolding/immich-kiosk/config" + "github.com/damongolding/immich-kiosk/utils" +) + +// RefreshCheck endpoint to check if device requires a refresh +func RefreshCheck(baseConfig *config.Config) echo.HandlerFunc { + return func(c echo.Context) error { + + kioskVersionHeader := c.Request().Header.Get("kiosk-version") + kioskRefreshTimestampHeader := c.Request().Header.Get("kiosk-reload-timestamp") + requestID := utils.ColorizeRequestId(c.Response().Header().Get(echo.HeaderXRequestID)) + + // create a copy of the global config to use with this request + requestConfig := *baseConfig + + // If kiosk version on client and server do not match refresh client. + if KioskVersion != kioskVersionHeader || kioskRefreshTimestampHeader != requestConfig.ReloadTimeStamp { + c.Response().Header().Set("HX-Refresh", "true") + return c.NoContent(http.StatusOK) + } + + log.Debug( + requestID, + "method", c.Request().Method, + "path", c.Request().URL.String(), + ) + + return c.NoContent(http.StatusOK) + } +} diff --git a/taskfile.yml b/taskfile.yml index 5221a7e..a96143b 100644 --- a/taskfile.yml +++ b/taskfile.yml @@ -1,12 +1,17 @@ version: "3" env: - VERSION: 0.11.1 + VERSION: 0.11.2 tasks: default: deps: [build] cmds: - KIOSK_DEBUG=true ./dist/kiosk + verbose: + deps: [build] + cmds: + - KIOSK_DEBUG_VERBOSE=true ./dist/kiosk + frontend: deps: [frontend-test, frontend-css, frontend-js] dir: ./frontend @@ -53,7 +58,7 @@ tasks: docker-image: cmds: - - docker build --build-arg VERSION={{.VERSION}} --load -t damongolding/immich-kiosk:{{.VERSION}} -t damongolding/immich-kiosk:latest . + - docker build --no-cache --build-arg VERSION={{.VERSION}} --load -t damongolding/immich-kiosk:{{.VERSION}} -t damongolding/immich-kiosk:latest . docker-buildx: cmds: diff --git a/views/views_clock.templ b/views/views_clock.templ index 3873c94..ce77acd 100644 --- a/views/views_clock.templ +++ b/views/views_clock.templ @@ -1,17 +1,42 @@ package views -import "strings" +import ( + "github.com/damongolding/immich-kiosk/config" + "github.com/damongolding/immich-kiosk/utils" + "strings" + "time" +) + +func clockDate(c config.Config) string { + clockDateFormat := utils.DateToLayout(c.DateFormat) + + if clockDateFormat == "" { + clockDateFormat = config.DefaultDateLayout + } + + t := time.Now() + + return t.Format(clockDateFormat) +} + +func clockTime(c config.Config) string { + clockTimeFormat := "15:04" + + if c.TimeFormat == "12" { + clockTimeFormat = time.Kitchen + } + + t := time.Now() + + return strings.ToLower(t.Format(clockTimeFormat)) -type ClockData struct { - ClockTime string - ClockDate string } -templ Clock(data ClockData) { - if data.ClockDate != "" { -
{ data.ClockDate }
+templ Clock(requestConfig config.Config) { + if requestConfig.ShowDate { +
{ clockDate(requestConfig) }
} - if data.ClockTime != "" { -
{ strings.ToLower(data.ClockTime) }
+ if requestConfig.ShowTime { +
{ clockTime(requestConfig) }
} } diff --git a/views/views_clock_templ.go b/views/views_clock_templ.go index e13f673..9622de9 100644 --- a/views/views_clock_templ.go +++ b/views/views_clock_templ.go @@ -8,14 +8,39 @@ package views import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" -import "strings" +import ( + "github.com/damongolding/immich-kiosk/config" + "github.com/damongolding/immich-kiosk/utils" + "strings" + "time" +) + +func clockDate(c config.Config) string { + clockDateFormat := utils.DateToLayout(c.DateFormat) + + if clockDateFormat == "" { + clockDateFormat = config.DefaultDateLayout + } + + t := time.Now() + + return t.Format(clockDateFormat) +} + +func clockTime(c config.Config) string { + clockTimeFormat := "15:04" + + if c.TimeFormat == "12" { + clockTimeFormat = time.Kitchen + } + + t := time.Now() + + return strings.ToLower(t.Format(clockTimeFormat)) -type ClockData struct { - ClockTime string - ClockDate string } -func Clock(data ClockData) templ.Component { +func Clock(requestConfig config.Config) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -36,15 +61,15 @@ func Clock(data ClockData) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - if data.ClockDate != "" { + if requestConfig.ShowDate { _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var2 string - templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.ClockDate) + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(clockDate(requestConfig)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/views_clock.templ`, Line: 12, Col: 43} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/views_clock.templ`, Line: 37, Col: 53} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -55,15 +80,15 @@ func Clock(data ClockData) templ.Component { return templ_7745c5c3_Err } } - if data.ClockTime != "" { + if requestConfig.ShowTime { _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var3 string - templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(strings.ToLower(data.ClockTime)) + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(clockTime(requestConfig)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/views_clock.templ`, Line: 15, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/views_clock.templ`, Line: 40, Col: 53} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { diff --git a/views/views_error.templ b/views/views_error.templ index 484c08d..c1a79e8 100644 --- a/views/views_error.templ +++ b/views/views_error.templ @@ -18,7 +18,19 @@ templ Error(data ErrorData) {

{ data.Title }

}

- @templ.Raw(data.Message) +

+

+ Full error: +
+
+ + @templ.Raw(data.Message) + +

diff --git a/views/views_error_templ.go b/views/views_error_templ.go index 9f465b3..50a0d59 100644 --- a/views/views_error_templ.go +++ b/views/views_error_templ.go @@ -57,7 +57,7 @@ func Error(data ErrorData) templ.Component { return templ_7745c5c3_Err } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Full error:

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -65,7 +65,7 @@ func Error(data ErrorData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/views/views_home.templ b/views/views_home.templ index 34ce1c1..2e18017 100644 --- a/views/views_home.templ +++ b/views/views_home.templ @@ -201,7 +201,7 @@ templ paramForm(queries url.Values) { } -templ clock(queries url.Values, kioskVersion string, deviceID string, theme string) { +templ clock(queries url.Values, theme string) {
} @@ -236,12 +235,20 @@ templ sleepMode(sleepStart, sleepEnd string, queries url.Values) { } } +templ refreshCheckForm(kioskVersion, reloadTimeStamp string) { +
+} + templ Home(viewData ViewData) { - + Immich Kiosk @@ -288,13 +295,14 @@ templ Home(viewData ViewData) { if viewData.ShowProgress { @progressBar() } - if !viewData.DisableUi && viewData.ShowTime { - @clock(viewData.Queries, viewData.KioskVersion, viewData.DeviceID, viewData.Theme) + if !viewData.DisableUi && (viewData.ShowTime || viewData.ShowDate) { + @clock(viewData.Queries, viewData.Theme) } @menu() @paramForm(viewData.Queries) @sleepMode(viewData.SleepStart, viewData.SleepEnd, viewData.Queries) @historyForm() + @refreshCheckForm(viewData.KioskVersion, viewData.ReloadTimeStamp) @offlineIcon() @kioskData(map[string]any{ "debug": viewData.Kiosk.Debug, diff --git a/views/views_home_templ.go b/views/views_home_templ.go index dc09bdd..cbc8bac 100644 --- a/views/views_home_templ.go +++ b/views/views_home_templ.go @@ -523,7 +523,7 @@ func paramForm(queries url.Values) templ.Component { }) } -func clock(queries url.Values, kioskVersion string, deviceID string, theme string) templ.Component { +func clock(queries url.Values, theme string) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -572,20 +572,7 @@ func clock(queries url.Values, kioskVersion string, deviceID string, theme strin return templ_7745c5c3_Err } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" hx-trigger=\"load, every 13s\" hx-swap=\"innerHTML\" hx-headers=\"") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var19 string - templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(`{"kiosk-version": "%s", "kiosk-device-id": "%s"}`, kioskVersion, deviceID)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/views_home.templ`, Line: 214, Col: 102} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" hx-trigger=\"load, every 13s\" hx-swap=\"innerHTML\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -609,9 +596,9 @@ func progressBar() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var20 := templ.GetChildren(ctx) - if templ_7745c5c3_Var20 == nil { - templ_7745c5c3_Var20 = templ.NopComponent + templ_7745c5c3_Var19 := templ.GetChildren(ctx) + if templ_7745c5c3_Var19 == nil { + templ_7745c5c3_Var19 = templ.NopComponent } ctx = templ.ClearChildren(ctx) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") @@ -638,9 +625,9 @@ func sleepMode(sleepStart, sleepEnd string, queries url.Values) templ.Component }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var21 := templ.GetChildren(ctx) - if templ_7745c5c3_Var21 == nil { - templ_7745c5c3_Var21 = templ.NopComponent + templ_7745c5c3_Var20 := templ.GetChildren(ctx) + if templ_7745c5c3_Var20 == nil { + templ_7745c5c3_Var20 = templ.NopComponent } ctx = templ.ClearChildren(ctx) if sleepStart != "" && sleepEnd != "" { @@ -663,6 +650,48 @@ func sleepMode(sleepStart, sleepEnd string, queries url.Values) templ.Component }) } +func refreshCheckForm(kioskVersion, reloadTimeStamp string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var21 := templ.GetChildren(ctx) + if templ_7745c5c3_Var21 == nil { + templ_7745c5c3_Var21 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + func Home(viewData ViewData) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context @@ -679,17 +708,17 @@ func Home(viewData ViewData) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var22 := templ.GetChildren(ctx) - if templ_7745c5c3_Var22 == nil { - templ_7745c5c3_Var22 = templ.NopComponent + templ_7745c5c3_Var23 := templ.GetChildren(ctx) + if templ_7745c5c3_Var23 == nil { + templ_7745c5c3_Var23 = templ.NopComponent } ctx = templ.ClearChildren(ctx) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var23 = []any{baseFontSize(viewData.FontSize)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var23...) + var templ_7745c5c3_Var24 = []any{baseFontSize(viewData.FontSize)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var24...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -697,25 +726,25 @@ func Home(viewData ViewData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var24 string - templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var23).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_home.templ`, Line: 1, Col: 0} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> 0 { stats.WriteString("|") } - stats.WriteString(fmt.Sprintf("%s s", info.ExposureTime)) + stats.WriteString(fmt.Sprintf("%ss", info.ExposureTime)) } if info.FocalLength != 0 { @@ -110,9 +110,34 @@ func ImageDateTime(viewData ViewData, imageIndex int) string { return imageDate } +templ imageMetadata(viewData ViewData, imageIndex int) { +
+ if viewData.ShowImageDate || viewData.ShowImageTime { + + } + if viewData.ShowImageExif { + + } + if viewData.ShowImageLocation { + + } + if viewData.ShowImageID { + + } +
+} + templ layoutSingleView(viewData ViewData) {
- if viewData.BackgroundBlur && !strings.EqualFold(viewData.ImageFit, "cover") { + if viewData.BackgroundBlur && !strings.EqualFold(viewData.ImageFit, "cover") && len(viewData.Images[0].ImageBlurData) > 0 {
Blurred image background
@@ -132,23 +157,9 @@ templ layoutSingleView(viewData ViewData) { }
if !viewData.DisableUi { -
- if viewData.ShowImageDate || viewData.ShowImageTime { - - } - if viewData.ShowImageExif { - - } - if viewData.ShowImageLocation { - - } -
+ if !viewData.DisableUi { + @imageMetadata(viewData, 0) + } } } @@ -157,7 +168,7 @@ templ layoutSplitView(viewData ViewData) {
for imageIndex, imageData := range viewData.Images {
- if viewData.BackgroundBlur && !strings.EqualFold(viewData.ImageFit, "cover") { + if viewData.BackgroundBlur && !strings.EqualFold(viewData.ImageFit, "cover") && len(imageData.ImageBlurData) > 0 {
Blurred image background
@@ -177,23 +188,7 @@ templ layoutSplitView(viewData ViewData) { }
if !viewData.DisableUi { -
- if viewData.ShowImageDate || viewData.ShowImageTime { - - } - if viewData.ShowImageExif { - - } - if viewData.ShowImageLocation { - - } -
+ @imageMetadata(viewData, imageIndex) }
} diff --git a/views/views_image_templ.go b/views/views_image_templ.go index ae8057b..dd2cfd2 100644 --- a/views/views_image_templ.go +++ b/views/views_image_templ.go @@ -174,7 +174,7 @@ func ImageExif(info immich.ExifInfo) string { if stats.Len() > 0 { stats.WriteString("") } - stats.WriteString(fmt.Sprintf("%s s", info.ExposureTime)) + stats.WriteString(fmt.Sprintf("%ss", info.ExposureTime)) } if info.FocalLength != 0 { @@ -221,7 +221,7 @@ func ImageDateTime(viewData ViewData, imageIndex int) string { return imageDate } -func layoutSingleView(viewData ViewData) templ.Component { +func imageMetadata(viewData ViewData, imageIndex int) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -242,7 +242,7 @@ func layoutSingleView(viewData ViewData) templ.Component { templ_7745c5c3_Var7 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - var templ_7745c5c3_Var8 = []any{"frame", templ.KV("frame-black-bg", !viewData.BackgroundBlur)} + var templ_7745c5c3_Var8 = []any{"image--metadata", fmt.Sprintf("image--metadata--theme-%s", viewData.Theme)} templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -264,27 +264,144 @@ func layoutSingleView(viewData ViewData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if viewData.BackgroundBlur && !strings.EqualFold(viewData.ImageFit, "cover") { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var10 string - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(viewData.Images[0].ImageBlurData) + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(ImageDateTime(viewData, imageIndex)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/views_image.templ`, Line: 117, Col: 47} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/views_image.templ`, Line: 117, Col: 41} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) 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 + } + } + if viewData.ShowImageExif { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if viewData.ShowImageLocation { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if viewData.ShowImageID { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + 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 + } + return templ_7745c5c3_Err + }) +} + +func layoutSingleView(viewData ViewData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var12 := templ.GetChildren(ctx) + if templ_7745c5c3_Var12 == nil { + templ_7745c5c3_Var12 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + var templ_7745c5c3_Var13 = []any{"frame", templ.KV("frame-black-bg", !viewData.BackgroundBlur)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var13...) + 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 + } + if viewData.BackgroundBlur && !strings.EqualFold(viewData.ImageFit, "cover") && len(viewData.Images[0].ImageBlurData) > 0 { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
\"Blurred
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - var templ_7745c5c3_Var11 = []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_Var11...) + var templ_7745c5c3_Var16 = []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_Var16...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -292,12 +409,12 @@ func layoutSingleView(viewData ViewData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var11).String()) + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var16).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_Var12)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -334,79 +451,12 @@ func layoutSingleView(viewData ViewData) templ.Component { return templ_7745c5c3_Err } if !viewData.DisableUi { - var templ_7745c5c3_Var13 = []any{"image--metadata", fmt.Sprintf("image--metadata--theme-%s", viewData.Theme)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var13...) - 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 - } - if viewData.ShowImageDate || viewData.ShowImageTime { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - if viewData.ShowImageExif { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - if viewData.ShowImageLocation { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if !viewData.DisableUi { + templ_7745c5c3_Err = imageMetadata(viewData, 0).Render(ctx, templ_7745c5c3_Buffer) 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 - } } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { @@ -432,13 +482,13 @@ func layoutSplitView(viewData ViewData) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var16 := templ.GetChildren(ctx) - if templ_7745c5c3_Var16 == nil { - templ_7745c5c3_Var16 = templ.NopComponent + templ_7745c5c3_Var18 := templ.GetChildren(ctx) + if templ_7745c5c3_Var18 == nil { + templ_7745c5c3_Var18 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - var templ_7745c5c3_Var17 = []any{"frame", templ.KV("frame-black-bg", !viewData.BackgroundBlur)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...) + var templ_7745c5c3_Var19 = []any{"frame", templ.KV("frame-black-bg", !viewData.BackgroundBlur)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var19...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -446,12 +496,12 @@ func layoutSplitView(viewData ViewData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var17).String()) + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var19).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_Var18)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -464,17 +514,17 @@ func layoutSplitView(viewData ViewData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if viewData.BackgroundBlur && !strings.EqualFold(viewData.ImageFit, "cover") { + if viewData.BackgroundBlur && !strings.EqualFold(viewData.ImageFit, "cover") && len(imageData.ImageBlurData) > 0 { _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if viewData.ShowImageDate || viewData.ShowImageTime { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - if viewData.ShowImageExif { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - if viewData.ShowImageLocation { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + templ_7745c5c3_Err = imageMetadata(viewData, imageIndex).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -637,9 +618,9 @@ func Image(viewData ViewData) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var25 := templ.GetChildren(ctx) - if templ_7745c5c3_Var25 == nil { - templ_7745c5c3_Var25 = templ.NopComponent + templ_7745c5c3_Var24 := templ.GetChildren(ctx) + if templ_7745c5c3_Var24 == nil { + templ_7745c5c3_Var24 = templ.NopComponent } ctx = templ.ClearChildren(ctx) if len(viewData.Images) < 2 { @@ -662,12 +643,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(historyEntry) + var templ_7745c5c3_Var25 string + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(historyEntry) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/views_image.templ`, Line: 211, Col: 88} + 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_Var26)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -681,12 +662,12 @@ func Image(viewData ViewData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var27 string - templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(newHistoryEntry.ImmichImage.ID) + var templ_7745c5c3_Var26 string + templ_7745c5c3_Var26, 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: 214, Col: 106} + 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_Var27)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/views/views_menu.templ b/views/views_menu.templ index fdb4fa2..bbab4b2 100644 --- a/views/views_menu.templ +++ b/views/views_menu.templ @@ -21,7 +21,7 @@ templ menu() { - +