diff --git a/graphql/documents/data/studio.graphql b/graphql/documents/data/studio.graphql index 52fd8b418..34b2400d8 100644 --- a/graphql/documents/data/studio.graphql +++ b/graphql/documents/data/studio.graphql @@ -19,6 +19,7 @@ fragment StudioData on Studio { scene_count image_count gallery_count + performer_count movie_count stash_ids { stash_id diff --git a/graphql/schema/types/studio.graphql b/graphql/schema/types/studio.graphql index 097ea8340..f9b72544e 100644 --- a/graphql/schema/types/studio.graphql +++ b/graphql/schema/types/studio.graphql @@ -12,6 +12,7 @@ type Studio { scene_count: Int # Resolver image_count: Int # Resolver gallery_count: Int # Resolver + performer_count: Int # Resolver stash_ids: [StashID!]! # rating expressed as 1-5 rating: Int @deprecated(reason: "Use 1-100 range with rating100") diff --git a/internal/api/resolver_model_studio.go b/internal/api/resolver_model_studio.go index 282e5a46e..4d689df77 100644 --- a/internal/api/resolver_model_studio.go +++ b/internal/api/resolver_model_studio.go @@ -9,6 +9,7 @@ import ( "github.com/stashapp/stash/pkg/gallery" "github.com/stashapp/stash/pkg/image" "github.com/stashapp/stash/pkg/models" + "github.com/stashapp/stash/pkg/performer" ) func (r *studioResolver) Name(ctx context.Context, obj *models.Studio) (string, error) { @@ -93,6 +94,18 @@ func (r *studioResolver) GalleryCount(ctx context.Context, obj *models.Studio) ( return &res, nil } +func (r *studioResolver) PerformerCount(ctx context.Context, obj *models.Studio) (ret *int, err error) { + var res int + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + res, err = performer.CountByStudioID(ctx, r.repository.Performer, 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/models/mocks/PerformerReaderWriter.go b/pkg/models/mocks/PerformerReaderWriter.go index 313ed73bd..b579ab562 100644 --- a/pkg/models/mocks/PerformerReaderWriter.go +++ b/pkg/models/mocks/PerformerReaderWriter.go @@ -427,6 +427,27 @@ func (_m *PerformerReaderWriter) Query(ctx context.Context, performerFilter *mod return r0, r1, r2 } +// QueryCount provides a mock function with given fields: ctx, galleryFilter, findFilter +func (_m *PerformerReaderWriter) QueryCount(ctx context.Context, galleryFilter *models.PerformerFilterType, findFilter *models.FindFilterType) (int, error) { + ret := _m.Called(ctx, galleryFilter, findFilter) + + var r0 int + if rf, ok := ret.Get(0).(func(context.Context, *models.PerformerFilterType, *models.FindFilterType) int); ok { + r0 = rf(ctx, galleryFilter, findFilter) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *models.PerformerFilterType, *models.FindFilterType) error); ok { + r1 = rf(ctx, galleryFilter, findFilter) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // QueryForAutoTag provides a mock function with given fields: ctx, words func (_m *PerformerReaderWriter) QueryForAutoTag(ctx context.Context, words []string) ([]*models.Performer, error) { ret := _m.Called(ctx, words) diff --git a/pkg/models/performer.go b/pkg/models/performer.go index e94d0939c..de0f278e1 100644 --- a/pkg/models/performer.go +++ b/pkg/models/performer.go @@ -160,6 +160,7 @@ type PerformerReader interface { // support the query needed QueryForAutoTag(ctx context.Context, words []string) ([]*Performer, error) Query(ctx context.Context, performerFilter *PerformerFilterType, findFilter *FindFilterType) ([]*Performer, int, error) + QueryCount(ctx context.Context, galleryFilter *PerformerFilterType, findFilter *FindFilterType) (int, error) AliasLoader GetImage(ctx context.Context, performerID int) ([]byte, error) StashIDLoader diff --git a/pkg/performer/query.go b/pkg/performer/query.go new file mode 100644 index 000000000..d790c6d52 --- /dev/null +++ b/pkg/performer/query.go @@ -0,0 +1,27 @@ +package performer + +import ( + "context" + "strconv" + + "github.com/stashapp/stash/pkg/models" +) + +type Queryer interface { + Query(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) +} + +type CountQueryer interface { + QueryCount(ctx context.Context, galleryFilter *models.PerformerFilterType, findFilter *models.FindFilterType) (int, error) +} + +func CountByStudioID(ctx context.Context, r CountQueryer, id int) (int, error) { + filter := &models.PerformerFilterType{ + Studios: &models.HierarchicalMultiCriterionInput{ + Value: []string{strconv.Itoa(id)}, + Modifier: models.CriterionModifierIncludes, + }, + } + + return r.QueryCount(ctx, filter, nil) +} diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index aed5d3b25..58f9f5e81 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -617,7 +617,7 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform return query } -func (qb *PerformerStore) Query(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) { +func (qb *PerformerStore) makeQuery(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) { if performerFilter == nil { performerFilter = &models.PerformerFilterType{} } @@ -635,13 +635,23 @@ func (qb *PerformerStore) Query(ctx context.Context, performerFilter *models.Per } if err := qb.validateFilter(performerFilter); err != nil { - return nil, 0, err + return nil, err } filter := qb.makeFilter(ctx, performerFilter) query.addFilter(filter) query.sortAndPagination = qb.getPerformerSort(findFilter) + getPagination(findFilter) + + return &query, nil +} + +func (qb *PerformerStore) Query(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) { + query, err := qb.makeQuery(ctx, performerFilter, findFilter) + if err != nil { + return nil, 0, err + } + idsResult, countResult, err := query.executeFind(ctx) if err != nil { return nil, 0, err @@ -655,6 +665,15 @@ func (qb *PerformerStore) Query(ctx context.Context, performerFilter *models.Per return performers, countResult, nil } +func (qb *PerformerStore) QueryCount(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) (int, error) { + query, err := qb.makeQuery(ctx, performerFilter, findFilter) + if err != nil { + return 0, err + } + + return query.executeCount(ctx) +} + func performerIsMissingCriterionHandler(qb *PerformerStore, isMissing *string) criterionHandlerFunc { return func(ctx context.Context, f *filterBuilder) { if isMissing != nil && *isMissing != "" { diff --git a/ui/v2.5/src/components/Shared/PopoverCountButton.tsx b/ui/v2.5/src/components/Shared/PopoverCountButton.tsx index 901424b98..33822898f 100644 --- a/ui/v2.5/src/components/Shared/PopoverCountButton.tsx +++ b/ui/v2.5/src/components/Shared/PopoverCountButton.tsx @@ -3,6 +3,7 @@ import { faImage, faImages, faPlayCircle, + faUser, } from "@fortawesome/free-solid-svg-icons"; import React, { useMemo } from "react"; import { Button } from "react-bootstrap"; @@ -13,7 +14,7 @@ import { ConfigurationContext } from "src/hooks/Config"; import { TextUtils } from "src/utils"; import Icon from "./Icon"; -type PopoverLinkType = "scene" | "image" | "gallery" | "movie"; +type PopoverLinkType = "scene" | "image" | "gallery" | "movie" | "performer"; interface IProps { className?: string; @@ -44,6 +45,8 @@ export const PopoverCountButton: React.FC = ({ return faImages; case "movie": return faFilm; + case "performer": + return faUser; } } @@ -69,6 +72,11 @@ export const PopoverCountButton: React.FC = ({ one: "movie", other: "movies", }; + case "performer": + return { + one: "performer", + other: "performers", + }; } } diff --git a/ui/v2.5/src/components/Studios/StudioCard.tsx b/ui/v2.5/src/components/Studios/StudioCard.tsx index 7ce73f24c..49d8cc359 100644 --- a/ui/v2.5/src/components/Studios/StudioCard.tsx +++ b/ui/v2.5/src/components/Studios/StudioCard.tsx @@ -116,12 +116,26 @@ export const StudioCard: React.FC = ({ ); } + function maybeRenderPerformersPopoverButton() { + if (!studio.performer_count) return; + + return ( + + ); + } + function maybeRenderPopoverButtonGroup() { if ( studio.scene_count || studio.image_count || studio.gallery_count || - studio.movie_count + studio.movie_count || + studio.performer_count ) { return ( <> @@ -131,6 +145,7 @@ export const StudioCard: React.FC = ({ {maybeRenderMoviesPopoverButton()} {maybeRenderImagesPopoverButton()} {maybeRenderGalleriesPopoverButton()} + {maybeRenderPerformersPopoverButton()} ); diff --git a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx index 2d87ddc97..84bd6e8d3 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx @@ -269,7 +269,15 @@ const StudioPage: React.FC = ({ studio }) => { + {intl.formatMessage({ id: "performers" })} + + + } > diff --git a/ui/v2.5/src/utils/navigation.ts b/ui/v2.5/src/utils/navigation.ts index bbdd34ef8..eefcf994b 100644 --- a/ui/v2.5/src/utils/navigation.ts +++ b/ui/v2.5/src/utils/navigation.ts @@ -148,6 +148,18 @@ const makeStudioMoviesUrl = (studio: Partial) => { return `/movies?${filter.makeQueryParameters()}`; }; +const makeStudioPerformersUrl = (studio: Partial) => { + if (!studio.id) return "#"; + const filter = new ListFilterModel(GQL.FilterMode.Performers, undefined); + const criterion = new StudiosCriterion(); + criterion.value = { + items: [{ id: studio.id, label: studio.name || `Studio ${studio.id}` }], + depth: 0, + }; + filter.criteria.push(criterion); + return `/performers?${filter.makeQueryParameters()}`; +}; + const makeChildStudiosUrl = (studio: Partial) => { if (!studio.id) return "#"; const filter = new ListFilterModel(GQL.FilterMode.Studios, undefined); @@ -307,6 +319,7 @@ export default { makeStudioImagesUrl, makeStudioGalleriesUrl, makeStudioMoviesUrl, + makeStudioPerformersUrl, makeParentTagsUrl, makeChildTagsUrl, makeTagSceneMarkersUrl,