mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
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:
parent
e6aaa196f3
commit
ea54a67798
27 changed files with 536 additions and 73 deletions
|
|
@ -20,6 +20,8 @@ fragment PerformerData on Performer {
|
|||
favorite
|
||||
image_path
|
||||
scene_count
|
||||
image_count
|
||||
gallery_count
|
||||
|
||||
tags {
|
||||
...TagData
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,5 +4,7 @@ fragment TagData on Tag {
|
|||
image_path
|
||||
scene_count
|
||||
scene_marker_count
|
||||
image_count
|
||||
gallery_count
|
||||
performer_count
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!]!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!]!
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
40
pkg/gallery/query.go
Normal 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
40
pkg/image/query.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
64
ui/v2.5/src/components/Shared/PopoverCountButton.tsx
Normal file
64
ui/v2.5/src/components/Shared/PopoverCountButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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}
|
||||
<FormattedPlural
|
||||
value={studio.scene_count ?? 0}
|
||||
one="scene"
|
||||
other="scenes"
|
||||
/>
|
||||
.
|
||||
</span>
|
||||
{maybeRenderParent(studio, hideParent)}
|
||||
{maybeRenderChildren(studio)}
|
||||
{maybeRenderPopoverButtonGroup()}
|
||||
</>
|
||||
}
|
||||
selected={selected}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue