Add scene/image/gallery popover count buttons for performer/studio/tag cards (#1293)

* Add counts to graphql schema
* Add count resolvers and query refactor
* Add count popover buttons
This commit is contained in:
WithoutPants 2021-04-15 10:46:31 +10:00 committed by GitHub
parent e6aaa196f3
commit ea54a67798
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 536 additions and 73 deletions

View file

@ -20,6 +20,8 @@ fragment PerformerData on Performer {
favorite
image_path
scene_count
image_count
gallery_count
tags {
...TagData

View file

@ -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

View file

@ -4,5 +4,7 @@ fragment TagData on Tag {
image_path
scene_count
scene_marker_count
image_count
gallery_count
performer_count
}

View file

@ -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!]!
}

View file

@ -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!]!
}

View file

@ -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
}

View file

@ -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)

View file

@ -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

View file

@ -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 {

40
pkg/gallery/query.go Normal file
View file

@ -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)
}

40
pkg/image/query.go Normal file
View file

@ -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)
}

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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 {

View file

@ -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)

View file

@ -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 {

View file

@ -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

View file

@ -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.

View file

@ -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<IPerformerCardProps> = ({
if (!performer.scene_count) return;
return (
<Link to={NavUtils.makePerformerScenesUrl(performer)}>
<Button className="minimal">
<Icon icon="play-circle" />
<span>{performer.scene_count}</span>
</Button>
</Link>
<PopoverCountButton
type="scene"
count={performer.scene_count}
url={NavUtils.makePerformerScenesUrl(performer)}
/>
);
}
function maybeRenderImagesPopoverButton() {
if (!performer.image_count) return;
return (
<PopoverCountButton
type="image"
count={performer.image_count}
url={NavUtils.makePerformerImagesUrl(performer)}
/>
);
}
function maybeRenderGalleriesPopoverButton() {
if (!performer.gallery_count) return;
return (
<PopoverCountButton
type="gallery"
count={performer.gallery_count}
url={NavUtils.makePerformerGalleriesUrl(performer)}
/>
);
}
@ -73,12 +97,19 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
}
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 (
<>
<hr />
<ButtonGroup className="card-popovers">
{maybeRenderScenesPopoverButton()}
{maybeRenderImagesPopoverButton()}
{maybeRenderGalleriesPopoverButton()}
{maybeRenderTagPopoverButton()}
</ButtonGroup>
</>

View file

@ -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<IProps> = ({ 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 (
<Link to={url} title={getTitle()}>
<Button className="minimal">
<Icon icon={getIcon()} />
<span>{count}</span>
</Button>
</Link>
);
};

View file

@ -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<IProps> = ({
selected,
onSelectedChanged,
}) => {
function maybeRenderScenesPopoverButton() {
if (!studio.scene_count) return;
return (
<PopoverCountButton
type="scene"
count={studio.scene_count}
url={NavUtils.makeStudioScenesUrl(studio)}
/>
);
}
function maybeRenderImagesPopoverButton() {
if (!studio.image_count) return;
return (
<PopoverCountButton
type="image"
count={studio.image_count}
url={NavUtils.makeStudioImagesUrl(studio)}
/>
);
}
function maybeRenderGalleriesPopoverButton() {
if (!studio.gallery_count) return;
return (
<PopoverCountButton
type="gallery"
count={studio.gallery_count}
url={NavUtils.makeStudioGalleriesUrl(studio)}
/>
);
}
function maybeRenderPopoverButtonGroup() {
if (studio.scene_count || studio.image_count || studio.gallery_count) {
return (
<>
<hr />
<ButtonGroup className="card-popovers">
{maybeRenderScenesPopoverButton()}
{maybeRenderImagesPopoverButton()}
{maybeRenderGalleriesPopoverButton()}
</ButtonGroup>
</>
);
}
}
return (
<BasicCard
className="studio-card"
@ -68,17 +120,9 @@ export const StudioCard: React.FC<IProps> = ({
<h5>
<TruncatedText text={studio.name} />
</h5>
<span>
{studio.scene_count}&nbsp;
<FormattedPlural
value={studio.scene_count ?? 0}
one="scene"
other="scenes"
/>
.
</span>
{maybeRenderParent(studio, hideParent)}
{maybeRenderChildren(studio)}
{maybeRenderPopoverButtonGroup()}
</>
}
selected={selected}

View file

@ -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<IProps> = ({
if (!tag.scene_count) return;
return (
<Link to={NavUtils.makeTagScenesUrl(tag)}>
<Button className="minimal">
<Icon icon="play-circle" />
<span>{tag.scene_count}</span>
</Button>
</Link>
<PopoverCountButton
type="scene"
count={tag.scene_count}
url={NavUtils.makeTagScenesUrl(tag)}
/>
);
}
@ -47,6 +47,30 @@ export const TagCard: React.FC<IProps> = ({
);
}
function maybeRenderImagesPopoverButton() {
if (!tag.image_count) return;
return (
<PopoverCountButton
type="image"
count={tag.image_count}
url={NavUtils.makeTagImagesUrl(tag)}
/>
);
}
function maybeRenderGalleriesPopoverButton() {
if (!tag.gallery_count) return;
return (
<PopoverCountButton
type="gallery"
count={tag.gallery_count}
url={NavUtils.makeTagGalleriesUrl(tag)}
/>
);
}
function maybeRenderPerformersPopoverButton() {
if (!tag.performer_count) return;
@ -67,6 +91,8 @@ export const TagCard: React.FC<IProps> = ({
<hr />
<ButtonGroup className="card-popovers">
{maybeRenderScenesPopoverButton()}
{maybeRenderImagesPopoverButton()}
{maybeRenderGalleriesPopoverButton()}
{maybeRenderSceneMarkersPopoverButton()}
{maybeRenderPerformersPopoverButton()}
</ButtonGroup>

View file

@ -23,6 +23,32 @@ const makePerformerScenesUrl = (
return `/scenes?${filter.makeQueryParameters()}`;
};
const makePerformerImagesUrl = (
performer: Partial<GQL.PerformerDataFragment>
) => {
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<GQL.PerformerDataFragment>
) => {
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<GQL.PerformerDataFragment>
) => {
@ -45,6 +71,28 @@ const makeStudioScenesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
return `/scenes?${filter.makeQueryParameters()}`;
};
const makeStudioImagesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
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<GQL.StudioDataFragment>) => {
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<GQL.StudioDataFragment>) => {
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,