mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Gallery cover url (#5182)
* Add default gallery image * Add gallery cover URL path * Use new cover URL in UI * Hide gallery preview scrubber when gallery has no images * Don't try to show lightbox for gallery without images * Ignore unrelated lint issue
This commit is contained in:
parent
010a355e0b
commit
a3c34a51aa
14 changed files with 131 additions and 49 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
type GalleryPathsType {
|
type GalleryPathsType {
|
||||||
|
cover: String!
|
||||||
preview: String! # Resolver
|
preview: String! # Resolver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,10 +195,10 @@ func (r *galleryResolver) Urls(ctx context.Context, obj *models.Gallery) ([]stri
|
||||||
func (r *galleryResolver) Paths(ctx context.Context, obj *models.Gallery) (*GalleryPathsType, error) {
|
func (r *galleryResolver) Paths(ctx context.Context, obj *models.Gallery) (*GalleryPathsType, error) {
|
||||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||||
builder := urlbuilders.NewGalleryURLBuilder(baseURL, obj)
|
builder := urlbuilders.NewGalleryURLBuilder(baseURL, obj)
|
||||||
previewPath := builder.GetPreviewURL()
|
|
||||||
|
|
||||||
return &GalleryPathsType{
|
return &GalleryPathsType{
|
||||||
Preview: previewPath,
|
Cover: builder.GetCoverURL(),
|
||||||
|
Preview: builder.GetPreviewURL(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,12 @@ import (
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/internal/manager/config"
|
||||||
|
"github.com/stashapp/stash/internal/static"
|
||||||
|
"github.com/stashapp/stash/pkg/image"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GalleryFinder interface {
|
type GalleryFinder interface {
|
||||||
|
|
@ -17,15 +21,17 @@ type GalleryFinder interface {
|
||||||
FindByChecksum(ctx context.Context, checksum string) ([]*models.Gallery, error)
|
FindByChecksum(ctx context.Context, checksum string) ([]*models.Gallery, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageByIndexer interface {
|
type GalleryImageFinder interface {
|
||||||
FindByGalleryIDIndex(ctx context.Context, galleryID int, index uint) (*models.Image, error)
|
FindByGalleryIDIndex(ctx context.Context, galleryID int, index uint) (*models.Image, error)
|
||||||
|
image.Queryer
|
||||||
|
image.CoverQueryer
|
||||||
}
|
}
|
||||||
|
|
||||||
type galleryRoutes struct {
|
type galleryRoutes struct {
|
||||||
routes
|
routes
|
||||||
imageRoutes imageRoutes
|
imageRoutes imageRoutes
|
||||||
galleryFinder GalleryFinder
|
galleryFinder GalleryFinder
|
||||||
imageFinder ImageByIndexer
|
imageFinder GalleryImageFinder
|
||||||
fileGetter models.FileGetter
|
fileGetter models.FileGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,12 +41,46 @@ func (rs galleryRoutes) Routes() chi.Router {
|
||||||
r.Route("/{galleryId}", func(r chi.Router) {
|
r.Route("/{galleryId}", func(r chi.Router) {
|
||||||
r.Use(rs.GalleryCtx)
|
r.Use(rs.GalleryCtx)
|
||||||
|
|
||||||
|
r.Get("/cover", rs.Cover)
|
||||||
r.Get("/preview/{imageIndex}", rs.Preview)
|
r.Get("/preview/{imageIndex}", rs.Preview)
|
||||||
})
|
})
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rs galleryRoutes) Cover(w http.ResponseWriter, r *http.Request) {
|
||||||
|
g := r.Context().Value(galleryKey).(*models.Gallery)
|
||||||
|
|
||||||
|
var i *models.Image
|
||||||
|
_ = rs.withReadTxn(r, func(ctx context.Context) error {
|
||||||
|
// Find cover image first
|
||||||
|
i, _ = image.FindGalleryCover(ctx, rs.imageFinder, g.ID, config.GetInstance().GetGalleryCoverRegex())
|
||||||
|
if i == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveThumbnail needs files populated
|
||||||
|
if err := i.LoadPrimaryFile(ctx, rs.fileGetter); err != nil {
|
||||||
|
if !errors.Is(err, context.Canceled) {
|
||||||
|
logger.Errorf("error loading primary file for image %d: %v", i.ID, err)
|
||||||
|
}
|
||||||
|
// set image to nil so that it doesn't try to use the primary file
|
||||||
|
i = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if i == nil {
|
||||||
|
// fallback to default image
|
||||||
|
image := static.ReadAll(static.DefaultGalleryImage)
|
||||||
|
utils.ServeImage(w, r, image)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rs.imageRoutes.serveThumbnail(w, r, i)
|
||||||
|
}
|
||||||
|
|
||||||
func (rs galleryRoutes) Preview(w http.ResponseWriter, r *http.Request) {
|
func (rs galleryRoutes) Preview(w http.ResponseWriter, r *http.Request) {
|
||||||
g := r.Context().Value(galleryKey).(*models.Gallery)
|
g := r.Context().Value(galleryKey).(*models.Gallery)
|
||||||
indexQueryParam := chi.URLParam(r, "imageIndex")
|
indexQueryParam := chi.URLParam(r, "imageIndex")
|
||||||
|
|
@ -55,6 +95,9 @@ func (rs galleryRoutes) Preview(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = rs.withReadTxn(r, func(ctx context.Context) error {
|
_ = rs.withReadTxn(r, func(ctx context.Context) error {
|
||||||
qb := rs.imageFinder
|
qb := rs.imageFinder
|
||||||
i, _ = qb.FindByGalleryIDIndex(ctx, g.ID, uint(index))
|
i, _ = qb.FindByGalleryIDIndex(ctx, g.ID, uint(index))
|
||||||
|
if i == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
// TODO - handle errors?
|
// TODO - handle errors?
|
||||||
|
|
||||||
// serveThumbnail needs files populated
|
// serveThumbnail needs files populated
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,7 @@ func NewGalleryURLBuilder(baseURL string, gallery *models.Gallery) GalleryURLBui
|
||||||
func (b GalleryURLBuilder) GetPreviewURL() string {
|
func (b GalleryURLBuilder) GetPreviewURL() string {
|
||||||
return b.BaseURL + "/gallery/" + b.GalleryID + "/preview"
|
return b.BaseURL + "/gallery/" + b.GalleryID + "/preview"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b GalleryURLBuilder) GetCoverURL() string {
|
||||||
|
return b.BaseURL + "/gallery/" + b.GalleryID + "/cover"
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,10 @@ func (me *Server) ssdpInterface(if_ net.Interface) {
|
||||||
stopped := make(chan struct{})
|
stopped := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
defer close(stopped)
|
defer close(stopped)
|
||||||
|
// FIXME - this currently blocks forever unless it encounters an error
|
||||||
|
// See https://github.com/anacrolix/dms/pull/150
|
||||||
|
// Needs to be fixed upstream
|
||||||
|
//nolint:staticcheck
|
||||||
if err := s.Serve(); err != nil {
|
if err := s.Serve(); err != nil {
|
||||||
logger.Errorf("%q: %q\n", if_.Name, err)
|
logger.Errorf("%q: %q\n", if_.Name, err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed performer performer_male scene image tag studio group
|
//go:embed performer performer_male scene image gallery tag studio group
|
||||||
var data embed.FS
|
var data embed.FS
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -21,6 +21,9 @@ const (
|
||||||
Image = "image"
|
Image = "image"
|
||||||
DefaultImageImage = "image/image.svg"
|
DefaultImageImage = "image/image.svg"
|
||||||
|
|
||||||
|
Gallery = "gallery"
|
||||||
|
DefaultGalleryImage = "gallery/gallery.svg"
|
||||||
|
|
||||||
Tag = "tag"
|
Tag = "tag"
|
||||||
DefaultTagImage = "tag/tag.svg"
|
DefaultTagImage = "tag/tag.svg"
|
||||||
|
|
||||||
|
|
|
||||||
6
internal/static/gallery/gallery.svg
Normal file
6
internal/static/gallery/gallery.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-352 -104 1280 720">
|
||||||
|
<!--! Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc.
|
||||||
|
Modified from https://github.com/FortAwesome/Font-Awesome/blob/6.x/svgs/solid/images.svg
|
||||||
|
Changed view box and fill style.
|
||||||
|
-->
|
||||||
|
<path d="M160 32c-35.3 0-64 28.7-64 64l0 224c0 35.3 28.7 64 64 64l352 0c35.3 0 64-28.7 64-64l0-224c0-35.3-28.7-64-64-64L160 32zM396 138.7l96 144c4.9 7.4 5.4 16.8 1.2 24.6S480.9 320 472 320l-144 0-48 0-80 0c-9.2 0-17.6-5.3-21.6-13.6s-2.9-18.2 2.9-25.4l64-80c4.6-5.7 11.4-9 18.7-9s14.2 3.3 18.7 9l17.3 21.6 56-84C360.5 132 368 128 376 128s15.5 4 20 10.7zM192 128a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zM48 120c0-13.3-10.7-24-24-24S0 106.7 0 120L0 344c0 75.1 60.9 136 136 136l320 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-320 0c-48.6 0-88-39.4-88-88l0-224z" style="fill:#ffffff;fill-opacity:1"/></svg>
|
||||||
|
After Width: | Height: | Size: 996 B |
|
|
@ -7,6 +7,19 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Queryer interface {
|
||||||
|
Query(ctx context.Context, options models.ImageQueryOptions) (*models.ImageQueryResult, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CoverQueryer interface {
|
||||||
|
Queryer
|
||||||
|
CoverByGalleryID(ctx context.Context, galleryId int) (*models.Image, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryCounter interface {
|
||||||
|
QueryCount(ctx context.Context, imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
// QueryOptions returns a ImageQueryResult populated with the provided filters.
|
// QueryOptions returns a ImageQueryResult populated with the provided filters.
|
||||||
func QueryOptions(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType, count bool) models.ImageQueryOptions {
|
func QueryOptions(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType, count bool) models.ImageQueryOptions {
|
||||||
return models.ImageQueryOptions{
|
return models.ImageQueryOptions{
|
||||||
|
|
@ -19,7 +32,7 @@ func QueryOptions(imageFilter *models.ImageFilterType, findFilter *models.FindFi
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query queries for images using the provided filters.
|
// Query queries for images using the provided filters.
|
||||||
func Query(ctx context.Context, qb models.ImageQueryer, imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) ([]*models.Image, error) {
|
func Query(ctx context.Context, qb Queryer, imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) ([]*models.Image, error) {
|
||||||
result, err := qb.Query(ctx, QueryOptions(imageFilter, findFilter, false))
|
result, err := qb.Query(ctx, QueryOptions(imageFilter, findFilter, false))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -33,7 +46,7 @@ func Query(ctx context.Context, qb models.ImageQueryer, imageFilter *models.Imag
|
||||||
return images, nil
|
return images, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CountByPerformerID(ctx context.Context, r models.ImageQueryer, id int) (int, error) {
|
func CountByPerformerID(ctx context.Context, r QueryCounter, id int) (int, error) {
|
||||||
filter := &models.ImageFilterType{
|
filter := &models.ImageFilterType{
|
||||||
Performers: &models.MultiCriterionInput{
|
Performers: &models.MultiCriterionInput{
|
||||||
Value: []string{strconv.Itoa(id)},
|
Value: []string{strconv.Itoa(id)},
|
||||||
|
|
@ -44,7 +57,7 @@ func CountByPerformerID(ctx context.Context, r models.ImageQueryer, id int) (int
|
||||||
return r.QueryCount(ctx, filter, nil)
|
return r.QueryCount(ctx, filter, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CountByStudioID(ctx context.Context, r models.ImageQueryer, id int, depth *int) (int, error) {
|
func CountByStudioID(ctx context.Context, r QueryCounter, id int, depth *int) (int, error) {
|
||||||
filter := &models.ImageFilterType{
|
filter := &models.ImageFilterType{
|
||||||
Studios: &models.HierarchicalMultiCriterionInput{
|
Studios: &models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{strconv.Itoa(id)},
|
Value: []string{strconv.Itoa(id)},
|
||||||
|
|
@ -56,7 +69,7 @@ func CountByStudioID(ctx context.Context, r models.ImageQueryer, id int, depth *
|
||||||
return r.QueryCount(ctx, filter, nil)
|
return r.QueryCount(ctx, filter, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CountByTagID(ctx context.Context, r models.ImageQueryer, id int, depth *int) (int, error) {
|
func CountByTagID(ctx context.Context, r QueryCounter, id int, depth *int) (int, error) {
|
||||||
filter := &models.ImageFilterType{
|
filter := &models.ImageFilterType{
|
||||||
Tags: &models.HierarchicalMultiCriterionInput{
|
Tags: &models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{strconv.Itoa(id)},
|
Value: []string{strconv.Itoa(id)},
|
||||||
|
|
@ -68,7 +81,7 @@ func CountByTagID(ctx context.Context, r models.ImageQueryer, id int, depth *int
|
||||||
return r.QueryCount(ctx, filter, nil)
|
return r.QueryCount(ctx, filter, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindByGalleryID(ctx context.Context, r models.ImageQueryer, galleryID int, sortBy string, sortDir models.SortDirectionEnum) ([]*models.Image, error) {
|
func FindByGalleryID(ctx context.Context, r Queryer, galleryID int, sortBy string, sortDir models.SortDirectionEnum) ([]*models.Image, error) {
|
||||||
perPage := -1
|
perPage := -1
|
||||||
|
|
||||||
findFilter := models.FindFilterType{
|
findFilter := models.FindFilterType{
|
||||||
|
|
@ -91,7 +104,7 @@ func FindByGalleryID(ctx context.Context, r models.ImageQueryer, galleryID int,
|
||||||
}, &findFilter)
|
}, &findFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindGalleryCover(ctx context.Context, r models.ImageQueryer, galleryID int, galleryCoverRegex string) (*models.Image, error) {
|
func FindGalleryCover(ctx context.Context, r CoverQueryer, galleryID int, galleryCoverRegex string) (*models.Image, error) {
|
||||||
const useCoverJpg = true
|
const useCoverJpg = true
|
||||||
img, err := findGalleryCover(ctx, r, galleryID, useCoverJpg, galleryCoverRegex)
|
img, err := findGalleryCover(ctx, r, galleryID, useCoverJpg, galleryCoverRegex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -106,7 +119,7 @@ func FindGalleryCover(ctx context.Context, r models.ImageQueryer, galleryID int,
|
||||||
return findGalleryCover(ctx, r, galleryID, !useCoverJpg, galleryCoverRegex)
|
return findGalleryCover(ctx, r, galleryID, !useCoverJpg, galleryCoverRegex)
|
||||||
}
|
}
|
||||||
|
|
||||||
func findGalleryCover(ctx context.Context, r models.ImageQueryer, galleryID int, useCoverJpg bool, galleryCoverRegex string) (*models.Image, error) {
|
func findGalleryCover(ctx context.Context, r CoverQueryer, galleryID int, useCoverJpg bool, galleryCoverRegex string) (*models.Image, error) {
|
||||||
img, err := r.CoverByGalleryID(ctx, galleryID)
|
img, err := r.CoverByGalleryID(ctx, galleryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,9 @@ type ImageFinder interface {
|
||||||
type ImageQueryer interface {
|
type ImageQueryer interface {
|
||||||
Query(ctx context.Context, options ImageQueryOptions) (*ImageQueryResult, error)
|
Query(ctx context.Context, options ImageQueryOptions) (*ImageQueryResult, error)
|
||||||
QueryCount(ctx context.Context, imageFilter *ImageFilterType, findFilter *FindFilterType) (int, error)
|
QueryCount(ctx context.Context, imageFilter *ImageFilterType, findFilter *FindFilterType) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type GalleryCoverFinder interface {
|
||||||
CoverByGalleryID(ctx context.Context, galleryId int) (*Image, error)
|
CoverByGalleryID(ctx context.Context, galleryId int) (*Image, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,6 +76,8 @@ type ImageReader interface {
|
||||||
TagIDLoader
|
TagIDLoader
|
||||||
FileLoader
|
FileLoader
|
||||||
|
|
||||||
|
GalleryCoverFinder
|
||||||
|
|
||||||
All(ctx context.Context) ([]*Image, error)
|
All(ctx context.Context) ([]*Image, error)
|
||||||
Size(ctx context.Context) (float64, error)
|
Size(ctx context.Context) (float64, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,6 @@ fragment SlimGalleryData on Gallery {
|
||||||
...FolderData
|
...FolderData
|
||||||
}
|
}
|
||||||
image_count
|
image_count
|
||||||
cover {
|
|
||||||
id
|
|
||||||
files {
|
|
||||||
...ImageFileData
|
|
||||||
}
|
|
||||||
paths {
|
|
||||||
thumbnail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chapters {
|
chapters {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
|
|
@ -49,6 +40,7 @@ fragment SlimGalleryData on Gallery {
|
||||||
...SlimSceneData
|
...SlimSceneData
|
||||||
}
|
}
|
||||||
paths {
|
paths {
|
||||||
|
cover
|
||||||
preview
|
preview
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ fragment GalleryData on Gallery {
|
||||||
organized
|
organized
|
||||||
|
|
||||||
paths {
|
paths {
|
||||||
|
cover
|
||||||
preview
|
preview
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,9 +26,6 @@ fragment GalleryData on Gallery {
|
||||||
chapters {
|
chapters {
|
||||||
...GalleryChapterData
|
...GalleryChapterData
|
||||||
}
|
}
|
||||||
cover {
|
|
||||||
...SlimImageData
|
|
||||||
}
|
|
||||||
studio {
|
studio {
|
||||||
...SlimStudioData
|
...SlimStudioData
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export const GalleryPreview: React.FC<IScenePreviewProps> = ({
|
||||||
onScrubberClick,
|
onScrubberClick,
|
||||||
}) => {
|
}) => {
|
||||||
const [imgSrc, setImgSrc] = useState<string | undefined>(
|
const [imgSrc, setImgSrc] = useState<string | undefined>(
|
||||||
gallery.cover?.paths.thumbnail ?? undefined
|
gallery.paths.cover ?? undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -43,13 +43,15 @@ export const GalleryPreview: React.FC<IScenePreviewProps> = ({
|
||||||
src={imgSrc}
|
src={imgSrc}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<GalleryPreviewScrubber
|
{gallery.image_count > 0 && (
|
||||||
previewPath={gallery.paths.preview}
|
<GalleryPreviewScrubber
|
||||||
defaultPath={gallery.cover?.paths.thumbnail ?? ""}
|
previewPath={gallery.paths.preview}
|
||||||
imageCount={gallery.image_count}
|
defaultPath={gallery.paths.cover ?? ""}
|
||||||
onClick={onScrubberClick}
|
imageCount={gallery.image_count}
|
||||||
onPathChanged={setImgSrc}
|
onClick={onScrubberClick}
|
||||||
/>
|
onPathChanged={setImgSrc}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -43,14 +43,12 @@ export const GalleryListTable: React.FC<IGalleryListTableProps> = (
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/galleries/${gallery.id}`}>
|
<Link to={`/galleries/${gallery.id}`}>
|
||||||
{gallery.cover ? (
|
<img
|
||||||
<img
|
loading="lazy"
|
||||||
loading="lazy"
|
alt={title}
|
||||||
alt={title}
|
className="image-thumbnail"
|
||||||
className="image-thumbnail"
|
src={gallery.paths.cover}
|
||||||
src={`${gallery.cover.paths.thumbnail}`}
|
/>
|
||||||
/>
|
|
||||||
) : undefined}
|
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,20 @@ interface IProps {
|
||||||
|
|
||||||
const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
|
const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const [orientation, setOrientation] = React.useState<
|
||||||
|
"landscape" | "portrait"
|
||||||
|
>("landscape");
|
||||||
const showLightbox = useGalleryLightbox(gallery.id, gallery.chapters);
|
const showLightbox = useGalleryLightbox(gallery.id, gallery.chapters);
|
||||||
|
|
||||||
const coverFile = gallery?.cover?.files.length
|
const cover = gallery?.paths.cover;
|
||||||
? gallery.cover.files[0]
|
|
||||||
: undefined;
|
function onImageLoad(e: React.SyntheticEvent<HTMLImageElement, Event>) {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
setOrientation(
|
||||||
|
target.naturalWidth > target.naturalHeight ? "landscape" : "portrait"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const orientation =
|
|
||||||
(coverFile?.width ?? 0) > (coverFile?.height ?? 0)
|
|
||||||
? "landscape"
|
|
||||||
: "portrait";
|
|
||||||
const cover = gallery?.cover?.paths.thumbnail ?? "";
|
|
||||||
const title = galleryTitle(gallery);
|
const title = galleryTitle(gallery);
|
||||||
const performerNames = gallery.performers.map((p) => p.name);
|
const performerNames = gallery.performers.map((p) => p.name);
|
||||||
const performers =
|
const performers =
|
||||||
|
|
@ -38,6 +41,10 @@ const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
|
||||||
: performerNames;
|
: performerNames;
|
||||||
|
|
||||||
async function showLightboxStart() {
|
async function showLightboxStart() {
|
||||||
|
if (gallery.image_count === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
showLightbox(0);
|
showLightbox(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,7 +58,13 @@ const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<RatingSystem value={gallery.rating100} disabled withoutContext />
|
<RatingSystem value={gallery.rating100} disabled withoutContext />
|
||||||
<img loading="lazy" src={cover} alt="" className={CLASSNAME_IMG} />
|
<img
|
||||||
|
loading="lazy"
|
||||||
|
src={cover}
|
||||||
|
alt=""
|
||||||
|
className={CLASSNAME_IMG}
|
||||||
|
onLoad={onImageLoad}
|
||||||
|
/>
|
||||||
<footer className={CLASSNAME_FOOTER}>
|
<footer className={CLASSNAME_FOOTER}>
|
||||||
<Link
|
<Link
|
||||||
to={`/galleries/${gallery.id}`}
|
to={`/galleries/${gallery.id}`}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue