diff --git a/frontend/public/assets/css/kiosk.css b/frontend/public/assets/css/kiosk.css index 7b0c8c1..d985ee8 100644 --- a/frontend/public/assets/css/kiosk.css +++ b/frontend/public/assets/css/kiosk.css @@ -449,7 +449,7 @@ form { align-items: center; gap: 0.5rem; } -.asset--metadata--is-first .asset--metadata--icon { +.right-align-icons .asset--metadata--icon { -moz-box-ordinal-group: 3; order: 2; } diff --git a/frontend/src/css/image.css b/frontend/src/css/image.css index 694ff76..c6d19d2 100644 --- a/frontend/src/css/image.css +++ b/frontend/src/css/image.css @@ -85,7 +85,7 @@ gap: 0.5rem; } -.asset--metadata--is-first .asset--metadata--icon { +.right-align-icons .asset--metadata--icon { order: 2; } diff --git a/internal/common/common.go b/internal/common/common.go index 5b47afb..3cff8ef 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -14,6 +14,7 @@ import ( "github.com/charmbracelet/log" "github.com/damongolding/immich-kiosk/internal/config" "github.com/damongolding/immich-kiosk/internal/immich" + "github.com/damongolding/immich-kiosk/internal/kiosk" "github.com/damongolding/immich-kiosk/internal/utils" "github.com/labstack/echo/v4" ) @@ -99,7 +100,7 @@ type ViewImageData struct { ImageData string // ImageData contains the image as base64 data ImageBlurData string // ImageBlurData contains the blurred image as base64 data ImageDate string // ImageDate contains the date of the image - User string + User string // User the user api key used } // ViewData contains all the data needed to render a view in the application @@ -112,6 +113,13 @@ type ViewData struct { config.Config // Config contains the instance configuration } +type ViewImageDataOptions struct { + RelativeAssetWanted bool + RelativeAssetBucket kiosk.Source + RelativeAssetBucketID string + ImageOrientation immich.ImageOrientation +} + // ContextCopy stores a copy of key HTTP context information including URL and headers type ContextCopy struct { URL url.URL // The request URL diff --git a/internal/config/config.go b/internal/config/config.go index 52c82c4..06b7481 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -430,14 +430,22 @@ func (c *Config) Load() error { return nil } +// ResetBuckets clears all the asset bucket slice fields (Person, Album, Date) +// in the Config structure. This is typically used when applying new query parameters +// to ensure old values don't persist. When querying specific buckets, the previous +// values need to be cleared to avoid mixing unintended assets. +func (c *Config) ResetBuckets() { + c.Person = []string{} + c.Album = []string{} + c.Date = []string{} +} + // ConfigWithOverrides overwrites base config with ones supplied via URL queries func (c *Config) ConfigWithOverrides(queries url.Values, e echo.Context) error { // check for person or album in quries and empty baseconfig slice if found if queries.Has("person") || queries.Has("album") || queries.Has("date") || queries.Has("memories") { - c.Person = []string{} - c.Album = []string{} - c.Date = []string{} + c.ResetBuckets() } err := e.Bind(c) diff --git a/internal/immich/immich.go b/internal/immich/immich.go index 908b5d3..037aeb4 100644 --- a/internal/immich/immich.go +++ b/internal/immich/immich.go @@ -12,6 +12,7 @@ import ( "github.com/damongolding/immich-kiosk/internal/config" "github.com/damongolding/immich-kiosk/internal/immich_open_api" + "github.com/damongolding/immich-kiosk/internal/kiosk" ) type ImageOrientation string @@ -176,6 +177,8 @@ type ImmichAsset struct { IsLandscape bool `json:"-"` MemoryTitle string `json:"-"` AppearsIn []string `json:"-"` + Bucket kiosk.Source `json:"-"` + BucketID string `json:"-"` } type ImmichAlbum struct { diff --git a/internal/immich/immich_album.go b/internal/immich/immich_album.go index d37754b..fa9aa3d 100644 --- a/internal/immich/immich_album.go +++ b/internal/immich/immich_album.go @@ -64,7 +64,7 @@ func (i *ImmichAsset) albums(requestID, deviceID string, shared bool, contains s apiUrl.RawQuery = queryParams.Encode() - immichApiCall := immichApiCallDecorator(i.immichApiCall, requestID, deviceID, albums) + immichApiCall := withImmichApiCache(i.immichApiCall, requestID, deviceID, albums) body, err := immichApiCall("GET", apiUrl.String(), nil) if err != nil { return immichApiFail(albums, err, body, apiUrl.String()) @@ -112,7 +112,7 @@ func (i *ImmichAsset) albumAssets(albumID, requestID, deviceID string) (ImmichAl Path: path.Join("api", "albums", albumID), } - immichApiCall := immichApiCallDecorator(i.immichApiCall, requestID, deviceID, album) + immichApiCall := withImmichApiCache(i.immichApiCall, requestID, deviceID, album) body, err := immichApiCall("GET", apiUrl.String(), nil) if err != nil { return immichApiFail(album, err, body, apiUrl.String()) @@ -269,6 +269,9 @@ func (i *ImmichAsset) ImageFromAlbum(albumID string, albumAssetsOrder ImmichAsse } + asset.Bucket = kiosk.SourceAlbums + asset.BucketID = album.ID + *i = asset return nil diff --git a/internal/immich/immich_date.go b/internal/immich/immich_date.go index 66137dc..127226a 100644 --- a/internal/immich/immich_date.go +++ b/internal/immich/immich_date.go @@ -84,7 +84,7 @@ func (i *ImmichAsset) RandomImageInDateRange(dateRange, requestID, deviceID stri return fmt.Errorf("marshaling request body: %w", err) } - immichApiCall := immichApiCallDecorator(i.immichApiCall, requestID, deviceID, immichAssets) + immichApiCall := withImmichApiCache(i.immichApiCall, requestID, deviceID, immichAssets) apiBody, err := immichApiCall("POST", apiUrl.String(), jsonBody) if err != nil { _, _, err = immichApiFail(immichAssets, err, apiBody, apiUrl.String()) @@ -120,7 +120,6 @@ func (i *ImmichAsset) RandomImageInDateRange(dateRange, requestID, deviceID stri continue } - if requestConfig.Kiosk.Cache { // Remove the current image from the slice immichAssetsToCache := append(immichAssets[:immichAssetIndex], immichAssets[immichAssetIndex+1:]...) @@ -137,6 +136,9 @@ func (i *ImmichAsset) RandomImageInDateRange(dateRange, requestID, deviceID stri } } + asset.Bucket = kiosk.SourceDateRangeAlbum + asset.BucketID = dateRange + *i = asset return nil diff --git a/internal/immich/immich_faces.go b/internal/immich/immich_faces.go index d2cb5ca..05f65c5 100644 --- a/internal/immich/immich_faces.go +++ b/internal/immich/immich_faces.go @@ -77,7 +77,7 @@ func (i *ImmichAsset) CheckForFaces(requestID, deviceID string) { RawQuery: "id=" + i.ID, } - immichApiCall := immichApiCallDecorator(i.immichApiCall, requestID, deviceID, faceResponse) + immichApiCall := withImmichApiCache(i.immichApiCall, requestID, deviceID, faceResponse) body, err := immichApiCall("GET", apiUrl.String(), nil) if err != nil { _, _, err = immichApiFail(faceResponse, err, body, apiUrl.String()) diff --git a/internal/immich/immich_favourites.go b/internal/immich/immich_favourites.go index 9bf96f2..3ef4e8b 100644 --- a/internal/immich/immich_favourites.go +++ b/internal/immich/immich_favourites.go @@ -58,7 +58,7 @@ func (i *ImmichAsset) favouriteImagesCount(requestID, deviceID string) (int, err return allFavouritesCount, err } - immichApiCall := immichApiCallDecorator(i.immichApiCall, requestID, deviceID, favourites) + immichApiCall := withImmichApiCache(i.immichApiCall, requestID, deviceID, favourites) apiBody, err := immichApiCall("POST", apiUrl.String(), jsonBody) if err != nil { _, _, err = immichApiFail(favourites, err, apiBody, apiUrl.String()) @@ -153,7 +153,7 @@ func (i *ImmichAsset) RandomImageFromFavourites(requestID, deviceID string, allo return fmt.Errorf("marshaling request body: %w", err) } - immichApiCall := immichApiCallDecorator(i.immichApiCall, requestID, deviceID, immichAssets) + immichApiCall := withImmichApiCache(i.immichApiCall, requestID, deviceID, immichAssets) apiBody, err := immichApiCall("POST", apiUrl.String(), jsonBody) if err != nil { _, _, err = immichApiFail(immichAssets, err, apiBody, apiUrl.String()) @@ -189,7 +189,6 @@ func (i *ImmichAsset) RandomImageFromFavourites(requestID, deviceID string, allo continue } - if requestConfig.Kiosk.Cache { // Remove the current image from the slice immichAssetsToCache := append(immichAssets[:immichAssetIndex], immichAssets[immichAssetIndex+1:]...) @@ -206,7 +205,11 @@ func (i *ImmichAsset) RandomImageFromFavourites(requestID, deviceID string, allo } } + asset.Bucket = kiosk.SourceAlbums + asset.BucketID = kiosk.AlbumKeywordFavourites + *i = asset + return nil } diff --git a/internal/immich/immich_helpers.go b/internal/immich/immich_helpers.go index 538b460..8beeab1 100644 --- a/internal/immich/immich_helpers.go +++ b/internal/immich/immich_helpers.go @@ -30,8 +30,8 @@ func immichApiFail[T ImmichApiResponse](value T, err error, body []byte, apiUrl return value, apiUrl, fmt.Errorf("%s : %v", immichError.Error, immichError.Message) } -// immichApiCallDecorator Decorator to impliment cache for the immichApiCall func -func immichApiCallDecorator[T ImmichApiResponse](immichApiCall ImmichApiCall, requestID, deviceID string, jsonShape T) ImmichApiCall { +// withImmichApiCache Decorator to implement cache for the immichApiCall func +func withImmichApiCache[T ImmichApiResponse](immichApiCall ImmichApiCall, requestID, deviceID string, jsonShape T) ImmichApiCall { return func(method, apiUrl string, body []byte, headers ...map[string]string) ([]byte, error) { if !requestConfig.Kiosk.Cache { @@ -271,7 +271,7 @@ func (i *ImmichAsset) AssetInfo(requestID, deviceID string) error { Path: path.Join("api", "assets", i.ID), } - immichApiCall := immichApiCallDecorator(i.immichApiCall, requestID, deviceID, immichAsset) + immichApiCall := withImmichApiCache(i.immichApiCall, requestID, deviceID, immichAsset) body, err := immichApiCall("GET", apiUrl.String(), nil) if err != nil { _, _, err = immichApiFail(immichAsset, err, body, apiUrl.String()) diff --git a/internal/immich/immich_memories.go b/internal/immich/immich_memories.go index 738ae4e..44c75a6 100644 --- a/internal/immich/immich_memories.go +++ b/internal/immich/immich_memories.go @@ -41,7 +41,7 @@ func (i *ImmichAsset) memories(requestID, deviceID string, assetCount bool) (Mem apiUrl.RawQuery += "&count=true" } - immichApiCall := immichApiCallDecorator(i.immichApiCall, requestID, deviceID, memoryLane) + immichApiCall := withImmichApiCache(i.immichApiCall, requestID, deviceID, memoryLane) body, err := immichApiCall("GET", apiUrl.String(), nil) if err != nil { return immichApiFail(memoryLane, err, body, apiUrl.String()) @@ -156,9 +156,10 @@ func (i *ImmichAsset) RandomMemoryLaneImage(requestID, deviceID string, isPrefet } } - *i = asset + asset.Bucket = kiosk.SourceMemories + asset.MemoryTitle = memories[pickedMemoryIndex].Title - i.MemoryTitle = memories[pickedMemoryIndex].Title + *i = asset return nil } diff --git a/internal/immich/immich_person.go b/internal/immich/immich_person.go index a8b9a63..0b96a08 100644 --- a/internal/immich/immich_person.go +++ b/internal/immich/immich_person.go @@ -31,7 +31,7 @@ func (i *ImmichAsset) PersonImageCount(personID, requestID, deviceID string) (in Path: path.Join("api", "people", personID, "statistics"), } - immichApiCall := immichApiCallDecorator(i.immichApiCall, requestID, deviceID, personStatistics) + immichApiCall := withImmichApiCache(i.immichApiCall, requestID, deviceID, personStatistics) body, err := immichApiCall("GET", apiUrl.String(), nil) if err != nil { _, _, err = immichApiFail(personStatistics, err, body, apiUrl.String()) @@ -113,7 +113,7 @@ func (i *ImmichAsset) RandomImageOfPerson(personID, requestID, deviceID string, return err } - immichApiCall := immichApiCallDecorator(i.immichApiCall, requestID, deviceID, immichAssets) + immichApiCall := withImmichApiCache(i.immichApiCall, requestID, deviceID, immichAssets) apiBody, err := immichApiCall("POST", apiUrl.String(), jsonBody) if err != nil { _, _, err = immichApiFail(immichAssets, err, apiBody, apiUrl.String()) @@ -149,7 +149,6 @@ func (i *ImmichAsset) RandomImageOfPerson(personID, requestID, deviceID string, continue } - if requestConfig.Kiosk.Cache { // Remove the current image from the slice immichAssetsToCache := append(immichAssets[:immichAssetIndex], immichAssets[immichAssetIndex+1:]...) @@ -166,6 +165,9 @@ func (i *ImmichAsset) RandomImageOfPerson(personID, requestID, deviceID string, } } + asset.Bucket = kiosk.SourcePerson + asset.BucketID = personID + *i = asset return nil diff --git a/internal/immich/immich_random.go b/internal/immich/immich_random.go index bc8f56e..8394470 100644 --- a/internal/immich/immich_random.go +++ b/internal/immich/immich_random.go @@ -73,7 +73,7 @@ func (i *ImmichAsset) RandomImage(requestID, deviceID string, isPrefetch bool) e return err } - immichApiCall := immichApiCallDecorator(i.immichApiCall, requestID, deviceID, immichAssets) + immichApiCall := withImmichApiCache(i.immichApiCall, requestID, deviceID, immichAssets) apiBody, err := immichApiCall("POST", apiUrl.String(), jsonBody) if err != nil { _, _, err = immichApiFail(immichAssets, err, apiBody, apiUrl.String()) @@ -109,7 +109,6 @@ func (i *ImmichAsset) RandomImage(requestID, deviceID string, isPrefetch bool) e continue } - if requestConfig.Kiosk.Cache { // Remove the current image from the slice immichAssetsToCache := append(immichAssets[:immichAssetIndex], immichAssets[immichAssetIndex+1:]...) @@ -126,7 +125,10 @@ func (i *ImmichAsset) RandomImage(requestID, deviceID string, isPrefetch bool) e } } + asset.Bucket = kiosk.SourceRandom + *i = asset + return nil } diff --git a/internal/kiosk/kiosk.go b/internal/kiosk/kiosk.go index b3e0d3b..9beadae 100644 --- a/internal/kiosk/kiosk.go +++ b/internal/kiosk/kiosk.go @@ -14,5 +14,10 @@ const ( SourceRandom Source = "RANDOM" SourceMemories Source = "MEMORIES" + LayoutLandscape string = "landscape" + LayoutPortrait string = "portrait" + LayoutSplitview string = "splitview" + LayoutSplitviewLandscape string = "splitview-landscape" + TagSkip string = "kiosk-skip" ) diff --git a/internal/routes/routes.go b/internal/routes/routes.go index f911194..64fd807 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -38,6 +38,13 @@ type PersonOrAlbum struct { ID string } +// requestMetadata holds information about the current request context +type requestMetadata struct { + requestID string + deviceID string + urlString string +} + func ShouldDrawFacesOnImages() bool { return drawFacesOnImages == "true" } diff --git a/internal/routes/routes_asset_helpers.go b/internal/routes/routes_asset_helpers.go index beaee44..84dd4ef 100644 --- a/internal/routes/routes_asset_helpers.go +++ b/internal/routes/routes_asset_helpers.go @@ -345,49 +345,47 @@ func DrawFaceOnImage(img image.Image, i *immich.ImmichAsset) image.Image { } -// processViewImageData handles the entire process of preparing page data including image processing. -// It returns the ImageData and an error if any step fails. -func processViewImageData(imageOrientation immich.ImageOrientation, requestConfig config.Config, c common.ContextCopy, isPrefetch bool) (common.ViewImageData, error) { - requestID := utils.ColorizeRequestId(c.ResponseHeader.Get(echo.HeaderXRequestID)) - deviceID := c.RequestHeader.Get("kiosk-device-id") - - // if multiple users are given via the url pick a random one - if len(requestConfig.User) > 0 { - randomIndex := rand.IntN(len(requestConfig.User)) - requestConfig.SelectedUser = requestConfig.User[randomIndex] - } else { - requestConfig.SelectedUser = "" - } - - immichAsset := immich.NewAsset(requestConfig) - - switch imageOrientation { - case immich.PortraitOrientation: - immichAsset.RatioWanted = imageOrientation - case immich.LandscapeOrientation: - immichAsset.RatioWanted = imageOrientation +// processViewImageData processes an image request and returns view data for display. +// It handles the complete workflow from selecting an image to preparing it for display, +// including face detection, optimization, and format conversion. +// +// Parameters: +// - requestConfig: Configuration settings for the request +// - c: Copy of the request context +// - isPrefetch: Whether this is a prefetch request +// - options: Additional options for image processing +// +// Returns: +// - ViewImageData containing the processed image and metadata +// - Error if any step fails +func processViewImageData(requestConfig config.Config, c common.ContextCopy, isPrefetch bool, options common.ViewImageDataOptions) (common.ViewImageData, error) { + // Initialize request metadata + metadata := requestMetadata{ + requestID: utils.ColorizeRequestId(c.ResponseHeader.Get(echo.HeaderXRequestID)), + deviceID: c.RequestHeader.Get("kiosk-device-id"), + urlString: c.URL.String(), } - allowedAssetTypes := immich.ImageOnlyAssetTypes + // Set up configuration + setupRequestConfig(&requestConfig) + immichAsset := setupImmichAsset(requestConfig, options.ImageOrientation) + allowedAssetTypes := determineAllowedAssetTypes(requestConfig, isPrefetch) - if requestConfig.ExperimentalAlbumVideo && isPrefetch { - allowedAssetTypes = immich.AllAssetTypes + // Handle relative asset configuration if needed + if options.RelativeAssetWanted { + handleRelativeAssetConfig(&requestConfig, options) } - img, err := processAsset(&immichAsset, allowedAssetTypes, requestConfig, requestID, deviceID, c.URL.String(), isPrefetch) + // Process image + img, err := processAsset(&immichAsset, allowedAssetTypes, requestConfig, metadata.requestID, metadata.deviceID, metadata.urlString, isPrefetch) if err != nil { return common.ViewImageData{}, fmt.Errorf("selecting image: %w", err) } - if strings.EqualFold(requestConfig.ImageEffect, "smart-zoom") && len(immichAsset.People)+len(immichAsset.UnassignedFaces) == 0 { - immichAsset.CheckForFaces(requestID, deviceID) - } - - if ShouldDrawFacesOnImages() { - log.Debug("Drawing faces") - img = DrawFaceOnImage(img, &immichAsset) - } + // Handle face detection and smart zoom + img = handleFaceProcessing(img, &immichAsset, requestConfig, metadata) + // Optimize image if needed if requestConfig.OptimizeImages { img, err = utils.OptimizeImage(img, requestConfig.ClientData.Width, requestConfig.ClientData.Height) if err != nil { @@ -395,12 +393,8 @@ func processViewImageData(imageOrientation immich.ImageOrientation, requestConfi } } - imgString, err := imageToBase64(img, requestConfig, requestID, deviceID, "Converted", isPrefetch) - if err != nil { - return common.ViewImageData{}, err - } - - imgBlurString, err := processBlurredImage(img, immichAsset.Type, requestConfig, requestID, deviceID, isPrefetch) + // Convert images to required formats + imgString, imgBlurString, err := convertImages(img, immichAsset.Type, requestConfig, metadata, isPrefetch) if err != nil { return common.ViewImageData{}, err } @@ -413,14 +407,93 @@ func processViewImageData(imageOrientation immich.ImageOrientation, requestConfi }, nil } +// setupRequestConfig configures the selected user for the request by picking a random +// user from the config if multiple users are provided, otherwise sets to empty string +func setupRequestConfig(config *config.Config) { + if len(config.User) > 0 { + randomIndex := rand.IntN(len(config.User)) + config.SelectedUser = config.User[randomIndex] + } else { + config.SelectedUser = "" + } +} + +// setupImmichAsset creates and configures a new ImmichAsset based on the provided config +// and orientation settings +func setupImmichAsset(config config.Config, orientation immich.ImageOrientation) immich.ImmichAsset { + asset := immich.NewAsset(config) + if orientation == immich.PortraitOrientation || orientation == immich.LandscapeOrientation { + asset.RatioWanted = orientation + } + return asset +} + +// determineAllowedAssetTypes returns the allowed asset types based on config settings +// Returns AllAssetTypes if experimental video is enabled and isPrefetch is true, +// otherwise returns ImageOnlyAssetTypes +func determineAllowedAssetTypes(config config.Config, isPrefetch bool) []immich.ImmichAssetType { + if config.ExperimentalAlbumVideo && isPrefetch { + return immich.AllAssetTypes + } + return immich.ImageOnlyAssetTypes +} + +// handleRelativeAssetConfig updates the config buckets based on the relative asset options. +// Resets existing buckets and configures the appropriate bucket based on the asset source type. +func handleRelativeAssetConfig(config *config.Config, options common.ViewImageDataOptions) { + config.ResetBuckets() + config.Memories = false + + switch options.RelativeAssetBucket { + case kiosk.SourceAlbums: + config.Album = append(config.Album, options.RelativeAssetBucketID) + case kiosk.SourcePerson: + config.Person = append(config.Person, options.RelativeAssetBucketID) + case kiosk.SourceDateRangeAlbum: + config.Date = append(config.Date, options.RelativeAssetBucketID) + case kiosk.SourceMemories: + config.Memories = true + } +} + +// handleFaceProcessing processes face detection and drawing for an image. +// Checks for faces if smart-zoom is enabled and draws faces if configured. +// Returns the processed image. +func handleFaceProcessing(img image.Image, asset *immich.ImmichAsset, config config.Config, metadata requestMetadata) image.Image { + if strings.EqualFold(config.ImageEffect, "smart-zoom") && len(asset.People)+len(asset.UnassignedFaces) == 0 { + asset.CheckForFaces(metadata.requestID, metadata.deviceID) + } + + if ShouldDrawFacesOnImages() { + log.Debug("Drawing faces") + return DrawFaceOnImage(img, asset) + } + return img +} + +// convertImages converts the provided image to base64 strings for both normal and blurred versions. +// Returns the base64 encoded normal image, blurred image, and any error that occurred. +func convertImages(img image.Image, assetType immich.ImmichAssetType, config config.Config, metadata requestMetadata, isPrefetch bool) (string, string, error) { + imgString, err := imageToBase64(img, config, metadata.requestID, metadata.deviceID, "Converted", isPrefetch) + if err != nil { + return "", "", err + } + + imgBlurString, err := processBlurredImage(img, assetType, config, metadata.requestID, metadata.deviceID, isPrefetch) + if err != nil { + return "", "", err + } + + return imgString, imgBlurString, nil +} + // ProcessViewImageData processes view data for an image without orientation constraints func ProcessViewImageData(requestConfig config.Config, c common.ContextCopy, isPrefetch bool) (common.ViewImageData, error) { - return processViewImageData("", requestConfig, c, isPrefetch) + return processViewImageData(requestConfig, c, isPrefetch, common.ViewImageDataOptions{}) } -// ProcessViewImageDataWithRatio processes view data for an image with the specified orientation -func ProcessViewImageDataWithRatio(imageOrientation immich.ImageOrientation, requestConfig config.Config, c common.ContextCopy, isPrefetch bool) (common.ViewImageData, error) { - return processViewImageData(imageOrientation, requestConfig, c, isPrefetch) +func ProcessViewImageDataWithOptions(requestConfig config.Config, c common.ContextCopy, isPrefetch bool, options common.ViewImageDataOptions) (common.ViewImageData, error) { + return processViewImageData(requestConfig, c, isPrefetch, options) } // assetToCache stores view data in the cache and triggers prefetch webhooks @@ -488,29 +561,59 @@ func renderCachedViewData(c echo.Context, cachedViewData []common.ViewData, requ return Render(c, http.StatusOK, imageComponent.Image(viewDataToRender)) } +// fetchSecondSplitViewAsset retrieves a second asset for split view layouts. It will attempt +// to find a unique asset that is different from the first one to avoid duplicates. +// +// Parameters: +// - viewData: The view data object to append the second asset to +// - viewDataSplitView: The first asset's view data to compare against +// - requestConfig: Configuration for the request +// - c: Copy of the request context +// - isPrefetch: Whether this is a prefetch request +// - options: Options for processing the second image +// +// Returns: +// - Error if asset retrieval fails after maximum attempts +func fetchSecondSplitViewAsset(viewData *common.ViewData, viewDataSplitView common.ViewImageData, requestConfig config.Config, c common.ContextCopy, isPrefetch bool, options common.ViewImageDataOptions) error { + const maxImageRetrievalAttempts = 3 + + for i := 0; i < maxImageRetrievalAttempts; i++ { + viewDataSplitViewSecond, err := ProcessViewImageDataWithOptions(requestConfig, c, isPrefetch, options) + if err != nil { + return err + } + + if viewDataSplitView.ImmichAsset.ID != viewDataSplitViewSecond.ImmichAsset.ID { + viewData.Assets = append(viewData.Assets, viewDataSplitViewSecond) + return nil + } + } + return nil +} + // generateViewData generates page data for the current request. func generateViewData(requestConfig config.Config, c common.ContextCopy, deviceID string, isPrefetch bool) (common.ViewData, error) { - const maxImageRetrievalAttepmts = 3 - viewData := common.ViewData{ DeviceID: deviceID, Config: requestConfig, } switch requestConfig.Layout { - case "landscape", "portrait": - orientation := immich.LandscapeOrientation - if requestConfig.Layout == "portrait" { - orientation = immich.PortraitOrientation + case kiosk.LayoutLandscape, kiosk.LayoutPortrait: + options := common.ViewImageDataOptions{ + ImageOrientation: immich.LandscapeOrientation, } - viewDataSingle, err := ProcessViewImageDataWithRatio(orientation, requestConfig, c, isPrefetch) + if requestConfig.Layout == kiosk.LayoutPortrait { + options.ImageOrientation = immich.PortraitOrientation + } + viewDataSingle, err := ProcessViewImageDataWithOptions(requestConfig, c, isPrefetch, options) if err != nil { return viewData, err } viewData.Assets = append(viewData.Assets, viewDataSingle) - case "splitview": + case kiosk.LayoutSplitview: viewDataSplitView, err := ProcessViewImageData(requestConfig, c, isPrefetch) if err != nil { return viewData, err @@ -521,20 +624,19 @@ func generateViewData(requestConfig config.Config, c common.ContextCopy, deviceI return viewData, nil } - // Second image - for i := 0; i < maxImageRetrievalAttepmts; i++ { - viewDataSplitViewSecond, err := ProcessViewImageDataWithRatio(immich.PortraitOrientation, requestConfig, c, isPrefetch) - if err != nil { - return viewData, err - } + options := common.ViewImageDataOptions{ + RelativeAssetWanted: true, + RelativeAssetBucket: viewDataSplitView.ImmichAsset.Bucket, + RelativeAssetBucketID: viewDataSplitView.ImmichAsset.BucketID, + ImageOrientation: immich.PortraitOrientation, + } - if viewDataSplitView.ImmichAsset.ID != viewDataSplitViewSecond.ImmichAsset.ID { - viewData.Assets = append(viewData.Assets, viewDataSplitViewSecond) - break - } + // Second image + if err := fetchSecondSplitViewAsset(&viewData, viewDataSplitView, requestConfig, c, isPrefetch, options); err != nil { + return viewData, err } - case "splitview-landscape": + case kiosk.LayoutSplitviewLandscape: viewDataSplitView, err := ProcessViewImageData(requestConfig, c, isPrefetch) if err != nil { return viewData, err @@ -545,17 +647,16 @@ func generateViewData(requestConfig config.Config, c common.ContextCopy, deviceI return viewData, nil } - // Second image - for i := 0; i < maxImageRetrievalAttepmts; i++ { - viewDataSplitViewSecond, err := ProcessViewImageDataWithRatio(immich.LandscapeOrientation, requestConfig, c, isPrefetch) - if err != nil { - return viewData, err - } + options := common.ViewImageDataOptions{ + RelativeAssetWanted: true, + RelativeAssetBucket: viewDataSplitView.ImmichAsset.Bucket, + RelativeAssetBucketID: viewDataSplitView.ImmichAsset.BucketID, + ImageOrientation: immich.LandscapeOrientation, + } - if viewDataSplitView.ImmichAsset.ID != viewDataSplitViewSecond.ImmichAsset.ID { - viewData.Assets = append(viewData.Assets, viewDataSplitViewSecond) - break - } + // Second image + if err := fetchSecondSplitViewAsset(&viewData, viewDataSplitView, requestConfig, c, isPrefetch, options); err != nil { + return viewData, err } default: diff --git a/internal/templates/partials/metadata.templ b/internal/templates/partials/metadata.templ index 2db9c3a..917412e 100644 --- a/internal/templates/partials/metadata.templ +++ b/internal/templates/partials/metadata.templ @@ -9,6 +9,7 @@ import ( "github.com/damongolding/immich-kiosk/internal/common" "github.com/damongolding/immich-kiosk/internal/config" "github.com/damongolding/immich-kiosk/internal/immich" + "github.com/damongolding/immich-kiosk/internal/kiosk" "github.com/damongolding/immich-kiosk/internal/utils" "github.com/goodsign/monday" "golang.org/x/text/cases" @@ -202,10 +203,10 @@ templ AssetMetadata(viewData common.ViewData, assetIndex int) { // {{ showSourceName := shouldShowSourceName(viewData, assetIndex) }} {{ showDateTime := viewData.ShowImageDate || viewData.ShowImageTime }} {{ showDescription := viewData.ShowImageDescription && viewData.Assets[assetIndex].ImmichAsset.ExifInfo.Description != "" }} - {{ isFirstAsset := len(viewData.Assets) == 1 || len(viewData.Assets) > 1 && assetIndex == 1 }} + {{ rightAlignIcons := len(viewData.Assets) == 1 || len(viewData.Assets) > 1 && assetIndex == 1 || viewData.Layout == kiosk.LayoutSplitviewLandscape }} {{ names := peopleNames(viewData.Assets[assetIndex].ImmichAsset.People) }} {{ showUser := viewData.ShowUser }} -
+
if showUser && viewData.Assets[assetIndex].User != "" {