diff --git a/internal/api/urlbuilders/gallery.go b/internal/api/urlbuilders/gallery.go index 3e6c5ef08..2723781f2 100644 --- a/internal/api/urlbuilders/gallery.go +++ b/internal/api/urlbuilders/gallery.go @@ -9,12 +9,14 @@ import ( type GalleryURLBuilder struct { BaseURL string GalleryID string + UpdatedAt string } func NewGalleryURLBuilder(baseURL string, gallery *models.Gallery) GalleryURLBuilder { return GalleryURLBuilder{ BaseURL: baseURL, GalleryID: strconv.Itoa(gallery.ID), + UpdatedAt: strconv.FormatInt(gallery.UpdatedAt.Unix(), 10), } } @@ -23,5 +25,5 @@ func (b GalleryURLBuilder) GetPreviewURL() string { } func (b GalleryURLBuilder) GetCoverURL() string { - return b.BaseURL + "/gallery/" + b.GalleryID + "/cover" + return b.BaseURL + "/gallery/" + b.GalleryID + "/cover?t=" + b.UpdatedAt } diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx index b9eea8f5d..5b9fa9da1 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx @@ -162,6 +162,21 @@ export const GalleryEditPanel: React.FC = ({ ); }, [scrapers]); + const cover = useMemo(() => { + if (gallery?.paths?.cover) { + return ( +
+ {intl.formatMessage({ +
+ ); + } + + return
; + }, [gallery?.paths?.cover, intl]); + async function onSave(input: InputValues) { setIsLoading(true); try { @@ -463,6 +478,12 @@ export const GalleryEditPanel: React.FC = ({ {renderDetailsField()} + + + + + {cover} + diff --git a/ui/v2.5/src/components/Galleries/styles.scss b/ui/v2.5/src/components/Galleries/styles.scss index 58116e936..5e8618326 100644 --- a/ui/v2.5/src/components/Galleries/styles.scss +++ b/ui/v2.5/src/components/Galleries/styles.scss @@ -206,6 +206,21 @@ $galleryTabWidth: 450px; } } +.gallery-cover { + aspect-ratio: 4 / 3; + display: block; + height: auto; + width: 100%; +} + +.gallery-cover img { + height: auto; + max-height: 100%; + max-width: 100%; + object-fit: contain; + width: auto; +} + div.GalleryWall { display: flex; flex-wrap: wrap; diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index a7679a5d5..8db471792 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -1613,17 +1613,30 @@ export const mutateAddGalleryImages = (input: GQL.GalleryAddInput) => }, }); +function evictCover(cache: ApolloCache, gallery_id: string) { + const fields: Pick, "paths" | "cover"> = {}; + fields.paths = (paths) => { + if (!("cover" in paths)) { + return paths; + } + const coverUrl = new URL(paths.cover); + coverUrl.search = "?t=" + Math.floor(Date.now() / 1000); + return { ...paths, cover: coverUrl.toString() }; + }; + fields.cover = (_value, { DELETE }) => DELETE; + cache.modify({ + id: cache.identify({ __typename: "Gallery", id: gallery_id }), + fields, + }); +} + export const mutateSetGalleryCover = (input: GQL.GallerySetCoverInput) => client.mutate({ mutation: GQL.SetGalleryCoverDocument, variables: input, update(cache, result) { if (!result.data?.setGalleryCover) return; - - cache.evict({ - id: cache.identify({ __typename: "Gallery", id: input.gallery_id }), - fieldName: "cover", - }); + evictCover(cache, input.gallery_id); }, }); @@ -1633,11 +1646,7 @@ export const mutateResetGalleryCover = (input: GQL.GalleryResetCoverInput) => variables: input, update(cache, result) { if (!result.data?.resetGalleryCover) return; - - cache.evict({ - id: cache.identify({ __typename: "Gallery", id: input.gallery_id }), - fieldName: "cover", - }); + evictCover(cache, input.gallery_id); }, }); diff --git a/ui/v2.5/src/docs/en/Manual/Images.md b/ui/v2.5/src/docs/en/Manual/Images.md index 7b384596b..e5733fc2e 100644 --- a/ui/v2.5/src/docs/en/Manual/Images.md +++ b/ui/v2.5/src/docs/en/Manual/Images.md @@ -13,6 +13,8 @@ For best results, images in zip file should be stored without compression (copy, If a filename of an image in the gallery zip file ends with `cover.jpg`, it will be treated like a cover and presented first in the gallery view page and as a gallery cover in the gallery list view. If more than one images match the name the first one found in natural sort order is selected. +You can also manually select any image from a gallery as its cover. On the gallery details page, select the desired cover image, and then select **Set as Cover** in the ⋯ menu. + ## Image clips/gifs Images can also be clips/gifs. These are meant to be short video loops. Right now they are not possible in zipfiles. To declare video files to be images, there are two ways: