diff --git a/graphql/documents/data/performer.graphql b/graphql/documents/data/performer.graphql index 253412b8a..2048da256 100644 --- a/graphql/documents/data/performer.graphql +++ b/graphql/documents/data/performer.graphql @@ -20,6 +20,8 @@ fragment PerformerData on Performer { favorite image_path scene_count + image_count + gallery_count tags { ...TagData diff --git a/graphql/documents/data/studio.graphql b/graphql/documents/data/studio.graphql index d2f60a44b..2c6c8d0a3 100644 --- a/graphql/documents/data/studio.graphql +++ b/graphql/documents/data/studio.graphql @@ -10,6 +10,8 @@ fragment StudioData on Studio { url image_path scene_count + image_count + gallery_count } child_studios { id @@ -18,9 +20,13 @@ fragment StudioData on Studio { url image_path scene_count + image_count + gallery_count } image_path scene_count + image_count + gallery_count stash_ids { stash_id endpoint diff --git a/graphql/documents/data/tag.graphql b/graphql/documents/data/tag.graphql index 3a0e84e1c..17d65b908 100644 --- a/graphql/documents/data/tag.graphql +++ b/graphql/documents/data/tag.graphql @@ -4,5 +4,7 @@ fragment TagData on Tag { image_path scene_count scene_marker_count + image_count + gallery_count performer_count } diff --git a/graphql/schema/types/performer.graphql b/graphql/schema/types/performer.graphql index e9ea68519..1c6d642e0 100644 --- a/graphql/schema/types/performer.graphql +++ b/graphql/schema/types/performer.graphql @@ -31,6 +31,8 @@ type Performer { image_path: String # Resolver scene_count: Int # Resolver + image_count: Int # Resolver + gallery_count: Int # Resolver scenes: [Scene!]! stash_ids: [StashID!]! } diff --git a/graphql/schema/types/studio.graphql b/graphql/schema/types/studio.graphql index 8eca28659..1adb0aa63 100644 --- a/graphql/schema/types/studio.graphql +++ b/graphql/schema/types/studio.graphql @@ -8,6 +8,8 @@ type Studio { image_path: String # Resolver scene_count: Int # Resolver + image_count: Int # Resolver + gallery_count: Int # Resolver stash_ids: [StashID!]! } diff --git a/graphql/schema/types/tag.graphql b/graphql/schema/types/tag.graphql index 2f1f0d0d5..1b855fd36 100644 --- a/graphql/schema/types/tag.graphql +++ b/graphql/schema/types/tag.graphql @@ -5,6 +5,8 @@ type Tag { image_path: String # Resolver scene_count: Int # Resolver scene_marker_count: Int # Resolver + image_count: Int # Resolver + gallery_count: Int # Resolver performer_count: Int } diff --git a/pkg/api/resolver_model_performer.go b/pkg/api/resolver_model_performer.go index cef67c22a..ab3d2363f 100644 --- a/pkg/api/resolver_model_performer.go +++ b/pkg/api/resolver_model_performer.go @@ -4,6 +4,8 @@ import ( "context" "github.com/stashapp/stash/pkg/api/urlbuilders" + "github.com/stashapp/stash/pkg/gallery" + "github.com/stashapp/stash/pkg/image" "github.com/stashapp/stash/pkg/models" ) @@ -161,6 +163,30 @@ func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performe return &res, nil } +func (r *performerResolver) ImageCount(ctx context.Context, obj *models.Performer) (ret *int, err error) { + var res int + if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { + res, err = image.CountByPerformerID(repo.Image(), obj.ID) + return err + }); err != nil { + return nil, err + } + + return &res, nil +} + +func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Performer) (ret *int, err error) { + var res int + if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { + res, err = gallery.CountByPerformerID(repo.Gallery(), obj.ID) + return err + }); err != nil { + return nil, err + } + + return &res, nil +} + func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) (ret []*models.Scene, err error) { if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { ret, err = repo.Scene().FindByPerformerID(obj.ID) diff --git a/pkg/api/resolver_model_studio.go b/pkg/api/resolver_model_studio.go index 1f866b004..553c8cc5c 100644 --- a/pkg/api/resolver_model_studio.go +++ b/pkg/api/resolver_model_studio.go @@ -4,6 +4,8 @@ import ( "context" "github.com/stashapp/stash/pkg/api/urlbuilders" + "github.com/stashapp/stash/pkg/gallery" + "github.com/stashapp/stash/pkg/image" "github.com/stashapp/stash/pkg/models" ) @@ -54,6 +56,30 @@ func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio) (re return &res, err } +func (r *studioResolver) ImageCount(ctx context.Context, obj *models.Studio) (ret *int, err error) { + var res int + if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { + res, err = image.CountByStudioID(repo.Image(), obj.ID) + return err + }); err != nil { + return nil, err + } + + return &res, nil +} + +func (r *studioResolver) GalleryCount(ctx context.Context, obj *models.Studio) (ret *int, err error) { + var res int + if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { + res, err = gallery.CountByStudioID(repo.Gallery(), obj.ID) + return err + }); err != nil { + return nil, err + } + + return &res, nil +} + func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) { if !obj.ParentID.Valid { return nil, nil diff --git a/pkg/api/resolver_model_tag.go b/pkg/api/resolver_model_tag.go index 1cbb3acf3..a4fb2cc4e 100644 --- a/pkg/api/resolver_model_tag.go +++ b/pkg/api/resolver_model_tag.go @@ -4,6 +4,8 @@ import ( "context" "github.com/stashapp/stash/pkg/api/urlbuilders" + "github.com/stashapp/stash/pkg/gallery" + "github.com/stashapp/stash/pkg/image" "github.com/stashapp/stash/pkg/models" ) @@ -31,6 +33,30 @@ func (r *tagResolver) SceneMarkerCount(ctx context.Context, obj *models.Tag) (re return &count, err } +func (r *tagResolver) ImageCount(ctx context.Context, obj *models.Tag) (ret *int, err error) { + var res int + if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { + res, err = image.CountByTagID(repo.Image(), obj.ID) + return err + }); err != nil { + return nil, err + } + + return &res, nil +} + +func (r *tagResolver) GalleryCount(ctx context.Context, obj *models.Tag) (ret *int, err error) { + var res int + if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { + res, err = gallery.CountByTagID(repo.Gallery(), obj.ID) + return err + }); err != nil { + return nil, err + } + + return &res, nil +} + func (r *tagResolver) PerformerCount(ctx context.Context, obj *models.Tag) (ret *int, err error) { var count int if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { diff --git a/pkg/gallery/query.go b/pkg/gallery/query.go new file mode 100644 index 000000000..6cae24321 --- /dev/null +++ b/pkg/gallery/query.go @@ -0,0 +1,40 @@ +package gallery + +import ( + "strconv" + + "github.com/stashapp/stash/pkg/models" +) + +func CountByPerformerID(r models.GalleryReader, id int) (int, error) { + filter := &models.GalleryFilterType{ + Performers: &models.MultiCriterionInput{ + Value: []string{strconv.Itoa(id)}, + Modifier: models.CriterionModifierIncludes, + }, + } + + return r.QueryCount(filter, nil) +} + +func CountByStudioID(r models.GalleryReader, id int) (int, error) { + filter := &models.GalleryFilterType{ + Studios: &models.MultiCriterionInput{ + Value: []string{strconv.Itoa(id)}, + Modifier: models.CriterionModifierIncludes, + }, + } + + return r.QueryCount(filter, nil) +} + +func CountByTagID(r models.GalleryReader, id int) (int, error) { + filter := &models.GalleryFilterType{ + Tags: &models.MultiCriterionInput{ + Value: []string{strconv.Itoa(id)}, + Modifier: models.CriterionModifierIncludes, + }, + } + + return r.QueryCount(filter, nil) +} diff --git a/pkg/image/query.go b/pkg/image/query.go new file mode 100644 index 000000000..58e276632 --- /dev/null +++ b/pkg/image/query.go @@ -0,0 +1,40 @@ +package image + +import ( + "strconv" + + "github.com/stashapp/stash/pkg/models" +) + +func CountByPerformerID(r models.ImageReader, id int) (int, error) { + filter := &models.ImageFilterType{ + Performers: &models.MultiCriterionInput{ + Value: []string{strconv.Itoa(id)}, + Modifier: models.CriterionModifierIncludes, + }, + } + + return r.QueryCount(filter, nil) +} + +func CountByStudioID(r models.ImageReader, id int) (int, error) { + filter := &models.ImageFilterType{ + Studios: &models.MultiCriterionInput{ + Value: []string{strconv.Itoa(id)}, + Modifier: models.CriterionModifierIncludes, + }, + } + + return r.QueryCount(filter, nil) +} + +func CountByTagID(r models.ImageReader, id int) (int, error) { + filter := &models.ImageFilterType{ + Tags: &models.MultiCriterionInput{ + Value: []string{strconv.Itoa(id)}, + Modifier: models.CriterionModifierIncludes, + }, + } + + return r.QueryCount(filter, nil) +} diff --git a/pkg/models/gallery.go b/pkg/models/gallery.go index 71f19a666..75fcfc896 100644 --- a/pkg/models/gallery.go +++ b/pkg/models/gallery.go @@ -11,6 +11,7 @@ type GalleryReader interface { Count() (int, error) All() ([]*Gallery, error) Query(galleryFilter *GalleryFilterType, findFilter *FindFilterType) ([]*Gallery, int, error) + QueryCount(galleryFilter *GalleryFilterType, findFilter *FindFilterType) (int, error) GetPerformerIDs(galleryID int) ([]int, error) GetTagIDs(galleryID int) ([]int, error) GetSceneIDs(galleryID int) ([]int, error) diff --git a/pkg/models/image.go b/pkg/models/image.go index d160aeba5..c3f3c5b2e 100644 --- a/pkg/models/image.go +++ b/pkg/models/image.go @@ -17,6 +17,7 @@ type ImageReader interface { // CountByTagID(tagID int) (int, error) All() ([]*Image, error) Query(imageFilter *ImageFilterType, findFilter *FindFilterType) ([]*Image, int, error) + QueryCount(imageFilter *ImageFilterType, findFilter *FindFilterType) (int, error) GetGalleryIDs(imageID int) ([]int, error) GetTagIDs(imageID int) ([]int, error) GetPerformerIDs(imageID int) ([]int, error) diff --git a/pkg/models/mocks/GalleryReaderWriter.go b/pkg/models/mocks/GalleryReaderWriter.go index 3585c3036..8bbd2e78b 100644 --- a/pkg/models/mocks/GalleryReaderWriter.go +++ b/pkg/models/mocks/GalleryReaderWriter.go @@ -376,6 +376,27 @@ func (_m *GalleryReaderWriter) Query(galleryFilter *models.GalleryFilterType, fi return r0, r1, r2 } +// QueryCount provides a mock function with given fields: galleryFilter, findFilter +func (_m *GalleryReaderWriter) QueryCount(galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) (int, error) { + ret := _m.Called(galleryFilter, findFilter) + + var r0 int + if rf, ok := ret.Get(0).(func(*models.GalleryFilterType, *models.FindFilterType) int); ok { + r0 = rf(galleryFilter, findFilter) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(*models.GalleryFilterType, *models.FindFilterType) error); ok { + r1 = rf(galleryFilter, findFilter) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Update provides a mock function with given fields: updatedGallery func (_m *GalleryReaderWriter) Update(updatedGallery models.Gallery) (*models.Gallery, error) { ret := _m.Called(updatedGallery) diff --git a/pkg/models/mocks/ImageReaderWriter.go b/pkg/models/mocks/ImageReaderWriter.go index b00cacfc9..a8a8c4b4a 100644 --- a/pkg/models/mocks/ImageReaderWriter.go +++ b/pkg/models/mocks/ImageReaderWriter.go @@ -370,6 +370,27 @@ func (_m *ImageReaderWriter) Query(imageFilter *models.ImageFilterType, findFilt return r0, r1, r2 } +// QueryCount provides a mock function with given fields: imageFilter, findFilter +func (_m *ImageReaderWriter) QueryCount(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) (int, error) { + ret := _m.Called(imageFilter, findFilter) + + var r0 int + if rf, ok := ret.Get(0).(func(*models.ImageFilterType, *models.FindFilterType) int); ok { + r0 = rf(imageFilter, findFilter) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(*models.ImageFilterType, *models.FindFilterType) error); ok { + r1 = rf(imageFilter, findFilter) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // ResetOCounter provides a mock function with given fields: id func (_m *ImageReaderWriter) ResetOCounter(id int) (int, error) { ret := _m.Called(id) diff --git a/pkg/models/mocks/SceneReaderWriter.go b/pkg/models/mocks/SceneReaderWriter.go index b5c3af191..796c23878 100644 --- a/pkg/models/mocks/SceneReaderWriter.go +++ b/pkg/models/mocks/SceneReaderWriter.go @@ -415,6 +415,29 @@ func (_m *SceneReaderWriter) FindByPerformerID(performerID int) ([]*models.Scene return r0, r1 } +// FindDuplicates provides a mock function with given fields: distance +func (_m *SceneReaderWriter) FindDuplicates(distance int) ([][]*models.Scene, error) { + ret := _m.Called(distance) + + var r0 [][]*models.Scene + if rf, ok := ret.Get(0).(func(int) [][]*models.Scene); ok { + r0 = rf(distance) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([][]*models.Scene) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int) error); ok { + r1 = rf(distance) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // FindMany provides a mock function with given fields: ids func (_m *SceneReaderWriter) FindMany(ids []int) ([]*models.Scene, error) { ret := _m.Called(ids) @@ -438,30 +461,6 @@ func (_m *SceneReaderWriter) FindMany(ids []int) ([]*models.Scene, error) { return r0, r1 } -// FindDuplicates provides a mock function with given fields: distance -func (_m *SceneReaderWriter) FindDuplicates(distance int) ([][]*models.Scene, error) { - ret := _m.Called(distance) - - var r0 [][]*models.Scene - if rf, ok := ret.Get(0).(func(int) [][]*models.Scene); ok { - r0 = rf(distance) - - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([][]*models.Scene) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(int) error); ok { - r1 = rf(distance) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // GetCover provides a mock function with given fields: sceneID func (_m *SceneReaderWriter) GetCover(sceneID int) ([]byte, error) { ret := _m.Called(sceneID) @@ -651,29 +650,6 @@ func (_m *SceneReaderWriter) Query(sceneFilter *models.SceneFilterType, findFilt return r0, r1, r2 } -// QueryForAutoTag provides a mock function with given fields: regex, pathPrefixes -func (_m *SceneReaderWriter) QueryForAutoTag(regex string, pathPrefixes []string) ([]*models.Scene, error) { - ret := _m.Called(regex, pathPrefixes) - - var r0 []*models.Scene - if rf, ok := ret.Get(0).(func(string, []string) []*models.Scene); ok { - r0 = rf(regex, pathPrefixes) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*models.Scene) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(string, []string) error); ok { - r1 = rf(regex, pathPrefixes) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // ResetOCounter provides a mock function with given fields: id func (_m *SceneReaderWriter) ResetOCounter(id int) (int, error) { ret := _m.Called(id) diff --git a/pkg/sqlite/gallery.go b/pkg/sqlite/gallery.go index 2fb59bcc0..d2e475ffe 100644 --- a/pkg/sqlite/gallery.go +++ b/pkg/sqlite/gallery.go @@ -159,7 +159,7 @@ func (qb *galleryQueryBuilder) All() ([]*models.Gallery, error) { return qb.queryGalleries(selectAll("galleries")+qb.getGallerySort(nil), nil) } -func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) ([]*models.Gallery, int, error) { +func (qb *galleryQueryBuilder) makeQuery(galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) queryBuilder { if galleryFilter == nil { galleryFilter = &models.GalleryFilterType{} } @@ -283,6 +283,13 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi handleGalleryPerformerTagsCriterion(&query, galleryFilter.PerformerTags) query.sortAndPagination = qb.getGallerySort(findFilter) + getPagination(findFilter) + + return query +} + +func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) ([]*models.Gallery, int, error) { + query := qb.makeQuery(galleryFilter, findFilter) + idsResult, countResult, err := query.executeFind() if err != nil { return nil, 0, err @@ -301,6 +308,12 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi return galleries, countResult, nil } +func (qb *galleryQueryBuilder) QueryCount(galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) (int, error) { + query := qb.makeQuery(galleryFilter, findFilter) + + return query.executeCount() +} + func (qb *galleryQueryBuilder) handleAverageResolutionFilter(query *queryBuilder, resolutionFilter *models.ResolutionEnum) { if resolutionFilter == nil { return diff --git a/pkg/sqlite/image.go b/pkg/sqlite/image.go index bf7815bb5..261fedd95 100644 --- a/pkg/sqlite/image.go +++ b/pkg/sqlite/image.go @@ -216,7 +216,7 @@ func (qb *imageQueryBuilder) All() ([]*models.Image, error) { return qb.queryImages(selectAll(imageTable)+qb.getImageSort(nil), nil) } -func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) ([]*models.Image, int, error) { +func (qb *imageQueryBuilder) makeQuery(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) queryBuilder { if imageFilter == nil { imageFilter = &models.ImageFilterType{} } @@ -383,6 +383,13 @@ func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilt handleImagePerformerTagsCriterion(&query, imageFilter.PerformerTags) query.sortAndPagination = qb.getImageSort(findFilter) + getPagination(findFilter) + + return query +} + +func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) ([]*models.Image, int, error) { + query := qb.makeQuery(imageFilter, findFilter) + idsResult, countResult, err := query.executeFind() if err != nil { return nil, 0, err @@ -401,6 +408,12 @@ func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilt return images, countResult, nil } +func (qb *imageQueryBuilder) QueryCount(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) (int, error) { + query := qb.makeQuery(imageFilter, findFilter) + + return query.executeCount() +} + func handleImagePerformerTagsCriterion(query *queryBuilder, performerTagsFilter *models.MultiCriterionInput) { if performerTagsFilter != nil && len(performerTagsFilter.Value) > 0 { for _, tagID := range performerTagsFilter.Value { diff --git a/pkg/sqlite/image_test.go b/pkg/sqlite/image_test.go index 153162420..d7260e477 100644 --- a/pkg/sqlite/image_test.go +++ b/pkg/sqlite/image_test.go @@ -95,6 +95,12 @@ func imageQueryQ(t *testing.T, sqb models.ImageReader, q string, expectedImageId image := images[0] assert.Equal(t, imageIDs[expectedImageIdx], image.ID) + count, err := sqb.QueryCount(nil, &filter) + if err != nil { + t.Errorf("Error querying image: %s", err.Error()) + } + assert.Equal(t, len(images), count) + // no Q should return all results filter.Q = nil images, _, err = sqb.Query(nil, &filter) diff --git a/pkg/sqlite/query.go b/pkg/sqlite/query.go index cf4cf0b5f..4c549cdf1 100644 --- a/pkg/sqlite/query.go +++ b/pkg/sqlite/query.go @@ -33,6 +33,19 @@ func (qb queryBuilder) executeFind() ([]int, int, error) { return qb.repository.executeFindQuery(body, qb.args, qb.sortAndPagination, qb.whereClauses, qb.havingClauses) } +func (qb queryBuilder) executeCount() (int, error) { + if qb.err != nil { + return 0, qb.err + } + + body := qb.body + body += qb.joins.toSQL() + + body = qb.repository.buildQueryBody(body, qb.whereClauses, qb.havingClauses) + countQuery := qb.repository.buildCountQuery(body) + return qb.repository.runCountQuery(countQuery, qb.args) +} + func (qb *queryBuilder) addWhere(clauses ...string) { for _, clause := range clauses { if len(clause) > 0 { diff --git a/pkg/sqlite/repository.go b/pkg/sqlite/repository.go index 681e68376..568d9c30b 100644 --- a/pkg/sqlite/repository.go +++ b/pkg/sqlite/repository.go @@ -234,7 +234,7 @@ func (r *repository) querySimple(query string, args []interface{}, out interface return nil } -func (r *repository) executeFindQuery(body string, args []interface{}, sortAndPagination string, whereClauses []string, havingClauses []string) ([]int, int, error) { +func (r *repository) buildQueryBody(body string, whereClauses []string, havingClauses []string) string { if len(whereClauses) > 0 { body = body + " WHERE " + strings.Join(whereClauses, " AND ") // TODO handle AND or OR } @@ -243,6 +243,12 @@ func (r *repository) executeFindQuery(body string, args []interface{}, sortAndPa body = body + " HAVING " + strings.Join(havingClauses, " AND ") // TODO handle AND or OR } + return body +} + +func (r *repository) executeFindQuery(body string, args []interface{}, sortAndPagination string, whereClauses []string, havingClauses []string) ([]int, int, error) { + body = r.buildQueryBody(body, whereClauses, havingClauses) + countQuery := r.buildCountQuery(body) idsQuery := body + sortAndPagination diff --git a/ui/v2.5/src/components/Changelog/versions/v070.md b/ui/v2.5/src/components/Changelog/versions/v070.md index acce2e74c..680e64e81 100644 --- a/ui/v2.5/src/components/Changelog/versions/v070.md +++ b/ui/v2.5/src/components/Changelog/versions/v070.md @@ -4,6 +4,7 @@ * Added scene queue. ### 🎨 Improvements +* Add popover buttons for scenes/images/galleries on performer/studio/tag cards. * Add slideshow to image wall view. * Support API key via URL query parameter, and added API key to stream link in Scene File Info. * Revamped setup wizard and migration UI. diff --git a/ui/v2.5/src/components/Performers/PerformerCard.tsx b/ui/v2.5/src/components/Performers/PerformerCard.tsx index cff36a880..4abf9bcf4 100644 --- a/ui/v2.5/src/components/Performers/PerformerCard.tsx +++ b/ui/v2.5/src/components/Performers/PerformerCard.tsx @@ -12,6 +12,7 @@ import { TruncatedText, } from "src/components/Shared"; import { Button, ButtonGroup } from "react-bootstrap"; +import { PopoverCountButton } from "../Shared/PopoverCountButton"; interface IPerformerCardProps { performer: GQL.PerformerDataFragment; @@ -46,12 +47,35 @@ export const PerformerCard: React.FC = ({ if (!performer.scene_count) return; return ( - - - + + ); + } + + function maybeRenderImagesPopoverButton() { + if (!performer.image_count) return; + + return ( + + ); + } + + function maybeRenderGalleriesPopoverButton() { + if (!performer.gallery_count) return; + + return ( + ); } @@ -73,12 +97,19 @@ export const PerformerCard: React.FC = ({ } function maybeRenderPopoverButtonGroup() { - if (performer.scene_count || performer.tags.length > 0) { + if ( + performer.scene_count || + performer.image_count || + performer.gallery_count || + performer.tags.length > 0 + ) { return ( <>
{maybeRenderScenesPopoverButton()} + {maybeRenderImagesPopoverButton()} + {maybeRenderGalleriesPopoverButton()} {maybeRenderTagPopoverButton()} diff --git a/ui/v2.5/src/components/Shared/PopoverCountButton.tsx b/ui/v2.5/src/components/Shared/PopoverCountButton.tsx new file mode 100644 index 000000000..6031e23fe --- /dev/null +++ b/ui/v2.5/src/components/Shared/PopoverCountButton.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { Button } from "react-bootstrap"; +import { useIntl } from "react-intl"; +import { Link } from "react-router-dom"; +import Icon from "./Icon"; + +type PopoverLinkType = "scene" | "image" | "gallery"; + +interface IProps { + url: string; + type: PopoverLinkType; + count: number; +} + +export const PopoverCountButton: React.FC = ({ url, type, count }) => { + const intl = useIntl(); + + function getIcon() { + switch (type) { + case "scene": + return "play-circle"; + case "image": + return "image"; + case "gallery": + return "images"; + } + } + + function getPluralOptions() { + switch (type) { + case "scene": + return { + one: "scene", + other: "scenes", + }; + case "image": + return { + one: "image", + other: "images", + }; + case "gallery": + return { + one: "gallery", + other: "galleries", + }; + } + } + + function getTitle() { + const pluralCategory = intl.formatPlural(count); + const options = getPluralOptions(); + const plural = options[pluralCategory as "one"] || options.other; + return `${count} ${plural}`; + } + + return ( + + + + ); +}; diff --git a/ui/v2.5/src/components/Studios/StudioCard.tsx b/ui/v2.5/src/components/Studios/StudioCard.tsx index 5e8db8fa9..da6cb03ee 100644 --- a/ui/v2.5/src/components/Studios/StudioCard.tsx +++ b/ui/v2.5/src/components/Studios/StudioCard.tsx @@ -1,9 +1,10 @@ import React from "react"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; -import { FormattedPlural } from "react-intl"; import { NavUtils } from "src/utils"; import { BasicCard, TruncatedText } from "src/components/Shared"; +import { ButtonGroup } from "react-bootstrap"; +import { PopoverCountButton } from "../Shared/PopoverCountButton"; interface IProps { studio: GQL.StudioDataFragment; @@ -51,6 +52,57 @@ export const StudioCard: React.FC = ({ selected, onSelectedChanged, }) => { + function maybeRenderScenesPopoverButton() { + if (!studio.scene_count) return; + + return ( + + ); + } + + function maybeRenderImagesPopoverButton() { + if (!studio.image_count) return; + + return ( + + ); + } + + function maybeRenderGalleriesPopoverButton() { + if (!studio.gallery_count) return; + + return ( + + ); + } + + function maybeRenderPopoverButtonGroup() { + if (studio.scene_count || studio.image_count || studio.gallery_count) { + return ( + <> +
+ + {maybeRenderScenesPopoverButton()} + {maybeRenderImagesPopoverButton()} + {maybeRenderGalleriesPopoverButton()} + + + ); + } + } + return ( = ({
- - {studio.scene_count}  - - . - {maybeRenderParent(studio, hideParent)} {maybeRenderChildren(studio)} + {maybeRenderPopoverButtonGroup()} } selected={selected} diff --git a/ui/v2.5/src/components/Tags/TagCard.tsx b/ui/v2.5/src/components/Tags/TagCard.tsx index 5f5db0358..8d2561389 100644 --- a/ui/v2.5/src/components/Tags/TagCard.tsx +++ b/ui/v2.5/src/components/Tags/TagCard.tsx @@ -5,6 +5,7 @@ import * as GQL from "src/core/generated-graphql"; import { NavUtils } from "src/utils"; import { Icon, TruncatedText } from "../Shared"; import { BasicCard } from "../Shared/BasicCard"; +import { PopoverCountButton } from "../Shared/PopoverCountButton"; interface IProps { tag: GQL.TagDataFragment; @@ -25,12 +26,11 @@ export const TagCard: React.FC = ({ if (!tag.scene_count) return; return ( - - - + ); } @@ -47,6 +47,30 @@ export const TagCard: React.FC = ({ ); } + function maybeRenderImagesPopoverButton() { + if (!tag.image_count) return; + + return ( + + ); + } + + function maybeRenderGalleriesPopoverButton() { + if (!tag.gallery_count) return; + + return ( + + ); + } + function maybeRenderPerformersPopoverButton() { if (!tag.performer_count) return; @@ -67,6 +91,8 @@ export const TagCard: React.FC = ({
{maybeRenderScenesPopoverButton()} + {maybeRenderImagesPopoverButton()} + {maybeRenderGalleriesPopoverButton()} {maybeRenderSceneMarkersPopoverButton()} {maybeRenderPerformersPopoverButton()} diff --git a/ui/v2.5/src/utils/navigation.ts b/ui/v2.5/src/utils/navigation.ts index a85e54be7..31f420987 100644 --- a/ui/v2.5/src/utils/navigation.ts +++ b/ui/v2.5/src/utils/navigation.ts @@ -23,6 +23,32 @@ const makePerformerScenesUrl = ( return `/scenes?${filter.makeQueryParameters()}`; }; +const makePerformerImagesUrl = ( + performer: Partial +) => { + if (!performer.id) return "#"; + const filter = new ListFilterModel(FilterMode.Images); + const criterion = new PerformersCriterion(); + criterion.value = [ + { id: performer.id, label: performer.name || `Performer ${performer.id}` }, + ]; + filter.criteria.push(criterion); + return `/images?${filter.makeQueryParameters()}`; +}; + +const makePerformerGalleriesUrl = ( + performer: Partial +) => { + if (!performer.id) return "#"; + const filter = new ListFilterModel(FilterMode.Galleries); + const criterion = new PerformersCriterion(); + criterion.value = [ + { id: performer.id, label: performer.name || `Performer ${performer.id}` }, + ]; + filter.criteria.push(criterion); + return `/galleries?${filter.makeQueryParameters()}`; +}; + const makePerformersCountryUrl = ( performer: Partial ) => { @@ -45,6 +71,28 @@ const makeStudioScenesUrl = (studio: Partial) => { return `/scenes?${filter.makeQueryParameters()}`; }; +const makeStudioImagesUrl = (studio: Partial) => { + if (!studio.id) return "#"; + const filter = new ListFilterModel(FilterMode.Images); + const criterion = new StudiosCriterion(); + criterion.value = [ + { id: studio.id, label: studio.name || `Studio ${studio.id}` }, + ]; + filter.criteria.push(criterion); + return `/images?${filter.makeQueryParameters()}`; +}; + +const makeStudioGalleriesUrl = (studio: Partial) => { + if (!studio.id) return "#"; + const filter = new ListFilterModel(FilterMode.Galleries); + const criterion = new StudiosCriterion(); + criterion.value = [ + { id: studio.id, label: studio.name || `Studio ${studio.id}` }, + ]; + filter.criteria.push(criterion); + return `/galleries?${filter.makeQueryParameters()}`; +}; + const makeChildStudiosUrl = (studio: Partial) => { if (!studio.id) return "#"; const filter = new ListFilterModel(FilterMode.Studios); @@ -121,8 +169,12 @@ const makeSceneMarkerUrl = ( export default { makePerformerScenesUrl, + makePerformerImagesUrl, + makePerformerGalleriesUrl, makePerformersCountryUrl, makeStudioScenesUrl, + makeStudioImagesUrl, + makeStudioGalleriesUrl, makeTagSceneMarkersUrl, makeTagScenesUrl, makeTagPerformersUrl,