mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Added rating to performers and studios (#1308)
This commit is contained in:
@@ -12,4 +12,5 @@ fragment SlimPerformerData on Performer {
|
||||
endpoint
|
||||
stash_id
|
||||
}
|
||||
rating
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ fragment PerformerData on Performer {
|
||||
stash_id
|
||||
endpoint
|
||||
}
|
||||
rating
|
||||
details
|
||||
death_date
|
||||
hair_color
|
||||
|
||||
@@ -10,4 +10,5 @@ fragment SlimStudioData on Studio {
|
||||
id
|
||||
}
|
||||
details
|
||||
rating
|
||||
}
|
||||
|
||||
@@ -32,4 +32,5 @@ fragment StudioData on Studio {
|
||||
endpoint
|
||||
}
|
||||
details
|
||||
rating
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
mutation StudioCreate(
|
||||
$input: StudioCreateInput!) {
|
||||
|
||||
mutation StudioCreate($input: StudioCreateInput!) {
|
||||
studioCreate(input: $input) {
|
||||
...StudioData
|
||||
}
|
||||
}
|
||||
|
||||
mutation StudioUpdate(
|
||||
$input: StudioUpdateInput!) {
|
||||
|
||||
mutation StudioUpdate($input: StudioUpdateInput!) {
|
||||
studioUpdate(input: $input) {
|
||||
...StudioData
|
||||
}
|
||||
|
||||
@@ -71,6 +71,8 @@ input PerformerFilterType {
|
||||
gallery_count: IntCriterionInput
|
||||
"""Filter by StashID"""
|
||||
stash_id: String
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
"""Filter by url"""
|
||||
url: StringCriterionInput
|
||||
"""Filter by hair color"""
|
||||
@@ -149,6 +151,8 @@ input StudioFilterType {
|
||||
stash_id: String
|
||||
"""Filter to only include studios missing this property"""
|
||||
is_missing: String
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
"""Filter by scene count"""
|
||||
scene_count: IntCriterionInput
|
||||
"""Filter by image count"""
|
||||
|
||||
@@ -35,6 +35,7 @@ type Performer {
|
||||
gallery_count: Int # Resolver
|
||||
scenes: [Scene!]!
|
||||
stash_ids: [StashID!]!
|
||||
rating: Int
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
@@ -63,6 +64,7 @@ input PerformerCreateInput {
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
@@ -92,6 +94,7 @@ input PerformerUpdateInput {
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
@@ -118,6 +121,7 @@ input BulkPerformerUpdateInput {
|
||||
instagram: String
|
||||
favorite: Boolean
|
||||
tag_ids: BulkUpdateIds
|
||||
rating: Int
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
|
||||
@@ -11,6 +11,7 @@ type Studio {
|
||||
image_count: Int # Resolver
|
||||
gallery_count: Int # Resolver
|
||||
stash_ids: [StashID!]!
|
||||
rating: Int
|
||||
details: String
|
||||
}
|
||||
|
||||
@@ -21,6 +22,7 @@ input StudioCreateInput {
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
details: String
|
||||
}
|
||||
|
||||
@@ -32,6 +34,7 @@ input StudioUpdateInput {
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
details: String
|
||||
}
|
||||
|
||||
|
||||
@@ -209,6 +209,14 @@ func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Rating(ctx context.Context, obj *models.Performer) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := int(obj.Rating.Int64)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Details(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Details.Valid {
|
||||
return &obj.Details.String, nil
|
||||
|
||||
@@ -117,6 +117,14 @@ func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) (ret
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Rating(ctx context.Context, obj *models.Studio) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := int(obj.Rating.Int64)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Details(ctx context.Context, obj *models.Studio) (*string, error) {
|
||||
if obj.Details.Valid {
|
||||
return &obj.Details.String, nil
|
||||
|
||||
@@ -85,6 +85,11 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
} else {
|
||||
newPerformer.Favorite = sql.NullBool{Bool: false, Valid: true}
|
||||
}
|
||||
if input.Rating != nil {
|
||||
newPerformer.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
} else {
|
||||
newPerformer.Rating = sql.NullInt64{Valid: false}
|
||||
}
|
||||
if input.Details != nil {
|
||||
newPerformer.Details = sql.NullString{String: *input.Details, Valid: true}
|
||||
}
|
||||
@@ -198,6 +203,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||
updatedPerformer.Twitter = translator.nullString(input.Twitter, "twitter")
|
||||
updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram")
|
||||
updatedPerformer.Favorite = translator.nullBool(input.Favorite, "favorite")
|
||||
updatedPerformer.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedPerformer.Details = translator.nullString(input.Details, "details")
|
||||
updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date")
|
||||
updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color")
|
||||
@@ -304,6 +310,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input models
|
||||
updatedPerformer.Twitter = translator.nullString(input.Twitter, "twitter")
|
||||
updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram")
|
||||
updatedPerformer.Favorite = translator.nullBool(input.Favorite, "favorite")
|
||||
updatedPerformer.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedPerformer.Details = translator.nullString(input.Details, "details")
|
||||
updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date")
|
||||
updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color")
|
||||
|
||||
@@ -42,6 +42,11 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
newStudio.ParentID = sql.NullInt64{Int64: parentID, Valid: true}
|
||||
}
|
||||
|
||||
if input.Rating != nil {
|
||||
newStudio.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
} else {
|
||||
newStudio.Rating = sql.NullInt64{Valid: false}
|
||||
}
|
||||
if input.Details != nil {
|
||||
newStudio.Details = sql.NullString{String: *input.Details, Valid: true}
|
||||
}
|
||||
@@ -115,6 +120,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
||||
updatedStudio.URL = translator.nullString(input.URL, "url")
|
||||
updatedStudio.Details = translator.nullString(input.Details, "details")
|
||||
updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id")
|
||||
updatedStudio.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
|
||||
// Start the transaction and save the studio
|
||||
var studio *models.Studio
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
var DB *sqlx.DB
|
||||
var WriteMu *sync.Mutex
|
||||
var dbPath string
|
||||
var appSchemaVersion uint = 21
|
||||
var appSchemaVersion uint = 22
|
||||
var databaseSchemaVersion uint
|
||||
|
||||
var (
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `performers` ADD COLUMN `rating` tinyint;
|
||||
ALTER TABLE `studios` ADD COLUMN `rating` tinyint;
|
||||
@@ -30,6 +30,7 @@ type Performer struct {
|
||||
Image string `json:"image,omitempty"`
|
||||
CreatedAt models.JSONTime `json:"created_at,omitempty"`
|
||||
UpdatedAt models.JSONTime `json:"updated_at,omitempty"`
|
||||
Rating int `json:"rating,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
DeathDate string `json:"death_date,omitempty"`
|
||||
HairColor string `json:"hair_color,omitempty"`
|
||||
|
||||
@@ -15,6 +15,7 @@ type Studio struct {
|
||||
Image string `json:"image,omitempty"`
|
||||
CreatedAt models.JSONTime `json:"created_at,omitempty"`
|
||||
UpdatedAt models.JSONTime `json:"updated_at,omitempty"`
|
||||
Rating int `json:"rating,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ type Performer struct {
|
||||
Favorite sql.NullBool `db:"favorite" json:"favorite"`
|
||||
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||
Rating sql.NullInt64 `db:"rating" json:"rating"`
|
||||
Details sql.NullString `db:"details" json:"details"`
|
||||
DeathDate SQLiteDate `db:"death_date" json:"death_date"`
|
||||
HairColor sql.NullString `db:"hair_color" json:"hair_color"`
|
||||
@@ -57,6 +58,7 @@ type PerformerPartial struct {
|
||||
Favorite *sql.NullBool `db:"favorite" json:"favorite"`
|
||||
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||
Rating *sql.NullInt64 `db:"rating" json:"rating"`
|
||||
Details *sql.NullString `db:"details" json:"details"`
|
||||
DeathDate *SQLiteDate `db:"death_date" json:"death_date"`
|
||||
HairColor *sql.NullString `db:"hair_color" json:"hair_color"`
|
||||
|
||||
@@ -15,6 +15,7 @@ type Studio struct {
|
||||
ParentID sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
|
||||
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||
Rating sql.NullInt64 `db:"rating" json:"rating"`
|
||||
Details sql.NullString `db:"details" json:"details"`
|
||||
}
|
||||
|
||||
@@ -26,6 +27,7 @@ type StudioPartial struct {
|
||||
ParentID *sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
|
||||
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||
Rating *sql.NullInt64 `db:"rating" json:"rating"`
|
||||
Details *sql.NullString `db:"details" json:"details"`
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,9 @@ func ToJSON(reader models.PerformerReader, performer *models.Performer) (*jsonsc
|
||||
if performer.Favorite.Valid {
|
||||
newPerformerJSON.Favorite = performer.Favorite.Bool
|
||||
}
|
||||
if performer.Rating.Valid {
|
||||
newPerformerJSON.Rating = int(performer.Rating.Int64)
|
||||
}
|
||||
if performer.Details.Valid {
|
||||
newPerformerJSON.Details = performer.Details.String
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ const (
|
||||
piercings = "piercings"
|
||||
tattoos = "tattoos"
|
||||
twitter = "twitter"
|
||||
rating = 5
|
||||
details = "details"
|
||||
hairColor = "hairColor"
|
||||
weight = 60
|
||||
@@ -86,6 +87,7 @@ func createFullPerformer(id int, name string) *models.Performer {
|
||||
UpdatedAt: models.SQLiteTimestamp{
|
||||
Timestamp: updateTime,
|
||||
},
|
||||
Rating: models.NullInt64(rating),
|
||||
Details: models.NullString(details),
|
||||
DeathDate: deathDate,
|
||||
HairColor: models.NullString(hairColor),
|
||||
@@ -133,6 +135,7 @@ func createFullJSONPerformer(name string, image string) *jsonschema.Performer {
|
||||
UpdatedAt: models.JSONTime{
|
||||
Time: updateTime,
|
||||
},
|
||||
Rating: rating,
|
||||
Image: image,
|
||||
Details: details,
|
||||
DeathDate: deathDate.String,
|
||||
|
||||
@@ -224,6 +224,9 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform
|
||||
if performerJSON.Instagram != "" {
|
||||
newPerformer.Instagram = sql.NullString{String: performerJSON.Instagram, Valid: true}
|
||||
}
|
||||
if performerJSON.Rating != 0 {
|
||||
newPerformer.Rating = sql.NullInt64{Int64: int64(performerJSON.Rating), Valid: true}
|
||||
}
|
||||
if performerJSON.Details != "" {
|
||||
newPerformer.Details = sql.NullString{String: performerJSON.Details, Valid: true}
|
||||
}
|
||||
|
||||
@@ -279,6 +279,7 @@ func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterTy
|
||||
query.handleStringCriterionInput(performerFilter.CareerLength, tableName+".career_length")
|
||||
query.handleStringCriterionInput(performerFilter.Tattoos, tableName+".tattoos")
|
||||
query.handleStringCriterionInput(performerFilter.Piercings, tableName+".piercings")
|
||||
query.handleIntCriterionInput(performerFilter.Rating, tableName+".rating")
|
||||
query.handleStringCriterionInput(performerFilter.HairColor, tableName+".hair_color")
|
||||
query.handleStringCriterionInput(performerFilter.URL, tableName+".url")
|
||||
query.handleIntCriterionInput(performerFilter.Weight, tableName+".weight")
|
||||
|
||||
@@ -617,6 +617,67 @@ func TestPerformerStashIDs(t *testing.T) {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
func TestPerformerQueryRating(t *testing.T) {
|
||||
const rating = 3
|
||||
ratingCriterion := models.IntCriterionInput{
|
||||
Value: rating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
}
|
||||
|
||||
verifyPerformersRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||
verifyPerformersRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||
verifyPerformersRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
||||
verifyPerformersRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
||||
verifyPerformersRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotNull
|
||||
verifyPerformersRating(t, ratingCriterion)
|
||||
}
|
||||
|
||||
func verifyPerformersRating(t *testing.T, ratingCriterion models.IntCriterionInput) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
sqb := r.Performer()
|
||||
performerFilter := models.PerformerFilterType{
|
||||
Rating: &ratingCriterion,
|
||||
}
|
||||
|
||||
performers := queryPerformers(t, sqb, &performerFilter, nil)
|
||||
|
||||
for _, performer := range performers {
|
||||
verifyInt64(t, performer.Rating, ratingCriterion)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestPerformerQueryIsMissingRating(t *testing.T) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
sqb := r.Performer()
|
||||
isMissing := "rating"
|
||||
performerFilter := models.PerformerFilterType{
|
||||
IsMissing: &isMissing,
|
||||
}
|
||||
|
||||
performers := queryPerformers(t, sqb, &performerFilter, nil)
|
||||
|
||||
assert.True(t, len(performers) > 0)
|
||||
|
||||
for _, performer := range performers {
|
||||
assert.True(t, !performer.Rating.Valid)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// TODO Update
|
||||
// TODO Destroy
|
||||
|
||||
@@ -183,10 +183,12 @@ func (qb *studioQueryBuilder) Query(studioFilter *models.StudioFilterType, findF
|
||||
query.addArg(stashIDFilter)
|
||||
}
|
||||
|
||||
if rating := studioFilter.Rating; rating != nil {
|
||||
query.handleIntCriterionInput(studioFilter.Rating, "studios.rating")
|
||||
}
|
||||
query.handleCountCriterion(studioFilter.SceneCount, studioTable, sceneTable, studioIDColumn)
|
||||
query.handleCountCriterion(studioFilter.ImageCount, studioTable, imageTable, studioIDColumn)
|
||||
query.handleCountCriterion(studioFilter.GalleryCount, studioTable, galleryTable, studioIDColumn)
|
||||
|
||||
query.handleStringCriterionInput(studioFilter.URL, "studios.url")
|
||||
|
||||
if isMissingFilter := studioFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
|
||||
|
||||
@@ -482,17 +482,42 @@ func TestStudioQueryURL(t *testing.T) {
|
||||
verifyStudioQuery(t, filter, verifyFn)
|
||||
}
|
||||
|
||||
func TestStudioQueryRating(t *testing.T) {
|
||||
const rating = 3
|
||||
ratingCriterion := models.IntCriterionInput{
|
||||
Value: rating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
}
|
||||
|
||||
verifyStudiosRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||
verifyStudiosRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||
verifyStudiosRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
||||
verifyStudiosRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
||||
verifyStudiosRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotNull
|
||||
verifyStudiosRating(t, ratingCriterion)
|
||||
}
|
||||
|
||||
func verifyStudioQuery(t *testing.T, filter models.StudioFilterType, verifyFn func(s *models.Studio)) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
t.Helper()
|
||||
sqb := r.Studio()
|
||||
|
||||
galleries := queryStudio(t, sqb, &filter, nil)
|
||||
studios := queryStudio(t, sqb, &filter, nil)
|
||||
|
||||
// assume it should find at least one
|
||||
assert.Greater(t, len(galleries), 0)
|
||||
assert.Greater(t, len(studios), 0)
|
||||
|
||||
for _, studio := range galleries {
|
||||
for _, studio := range studios {
|
||||
verifyFn(studio)
|
||||
}
|
||||
|
||||
@@ -500,6 +525,51 @@ func verifyStudioQuery(t *testing.T, filter models.StudioFilterType, verifyFn fu
|
||||
})
|
||||
}
|
||||
|
||||
func verifyStudiosRating(t *testing.T, ratingCriterion models.IntCriterionInput) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
sqb := r.Studio()
|
||||
studioFilter := models.StudioFilterType{
|
||||
Rating: &ratingCriterion,
|
||||
}
|
||||
|
||||
studios, _, err := sqb.Query(&studioFilter, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Error querying studio: %s", err.Error())
|
||||
}
|
||||
|
||||
for _, studio := range studios {
|
||||
verifyInt64(t, studio.Rating, ratingCriterion)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestStudioQueryIsMissingRating(t *testing.T) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
sqb := r.Studio()
|
||||
isMissing := "rating"
|
||||
studioFilter := models.StudioFilterType{
|
||||
IsMissing: &isMissing,
|
||||
}
|
||||
|
||||
studios, _, err := sqb.Query(&studioFilter, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Error querying studio: %s", err.Error())
|
||||
}
|
||||
|
||||
assert.True(t, len(studios) > 0)
|
||||
|
||||
for _, studio := range studios {
|
||||
assert.True(t, !studio.Rating.Valid)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func queryStudio(t *testing.T, sqb models.StudioReader, studioFilter *models.StudioFilterType, findFilter *models.FindFilterType) []*models.Studio {
|
||||
studios, _, err := sqb.Query(studioFilter, findFilter)
|
||||
if err != nil {
|
||||
|
||||
@@ -38,6 +38,10 @@ func ToJSON(reader models.StudioReader, studio *models.Studio) (*jsonschema.Stud
|
||||
}
|
||||
}
|
||||
|
||||
if studio.Rating.Valid {
|
||||
newStudioJSON.Rating = int(studio.Rating.Int64)
|
||||
}
|
||||
|
||||
image, err := reader.GetImage(studio.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting studio image: %s", err.Error())
|
||||
|
||||
@@ -28,7 +28,7 @@ const (
|
||||
studioName = "testStudio"
|
||||
url = "url"
|
||||
details = "details"
|
||||
|
||||
rating = 5
|
||||
parentStudioName = "parentStudio"
|
||||
)
|
||||
|
||||
@@ -55,6 +55,7 @@ func createFullStudio(id int, parentID int) models.Studio {
|
||||
UpdatedAt: models.SQLiteTimestamp{
|
||||
Timestamp: updateTime,
|
||||
},
|
||||
Rating: models.NullInt64(rating),
|
||||
}
|
||||
|
||||
if parentID != 0 {
|
||||
@@ -89,6 +90,7 @@ func createFullJSONStudio(parentStudio, image string) *jsonschema.Studio {
|
||||
},
|
||||
ParentStudio: parentStudio,
|
||||
Image: image,
|
||||
Rating: rating,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ func (i *Importer) PreImport() error {
|
||||
Details: sql.NullString{String: i.Input.Details, Valid: true},
|
||||
CreatedAt: models.SQLiteTimestamp{Timestamp: i.Input.CreatedAt.GetTime()},
|
||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: i.Input.UpdatedAt.GetTime()},
|
||||
Rating: sql.NullInt64{Int64: int64(i.Input.Rating), Valid: true},
|
||||
}
|
||||
|
||||
if err := i.populateParentStudio(); err != nil {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
### ✨ New Features
|
||||
* Added rating field to performers and studios.
|
||||
* Support serving UI from specific directory location.
|
||||
* Added details, death date, hair color, and weight to Performers.
|
||||
* Added `lbToKg` post-process action for performer scrapers.
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { Form, Col, Row } from "react-bootstrap";
|
||||
import _ from "lodash";
|
||||
import { useBulkPerformerUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Modal } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { FormUtils } from "src/utils";
|
||||
import MultiSet from "../Shared/MultiSet";
|
||||
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
||||
|
||||
interface IListOperationProps {
|
||||
selected: GQL.SlimPerformerDataFragment[];
|
||||
@@ -16,7 +18,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
props: IListOperationProps
|
||||
) => {
|
||||
const Toast = useToast();
|
||||
|
||||
const [rating, setRating] = useState<number>();
|
||||
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
||||
GQL.BulkUpdateIdMode.Add
|
||||
);
|
||||
@@ -43,6 +45,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
function getPerformerInput(): GQL.BulkPerformerUpdateInput {
|
||||
// need to determine what we are actually setting on each performer
|
||||
const aggregateTagIds = getTagIds(props.selected);
|
||||
const aggregateRating = getRating(props.selected);
|
||||
|
||||
const performerInput: GQL.BulkPerformerUpdateInput = {
|
||||
ids: props.selected.map((performer) => {
|
||||
@@ -50,6 +53,19 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
}),
|
||||
};
|
||||
|
||||
// if rating is undefined
|
||||
if (rating === undefined) {
|
||||
// and all galleries have the same rating, then we are unsetting the rating.
|
||||
if (aggregateRating) {
|
||||
// null to unset rating
|
||||
performerInput.rating = null;
|
||||
}
|
||||
// otherwise not setting the rating
|
||||
} else {
|
||||
// if rating is set, then we are setting the rating for all
|
||||
performerInput.rating = rating;
|
||||
}
|
||||
|
||||
// if tagIds non-empty, then we are setting them
|
||||
if (
|
||||
tagMode === GQL.BulkUpdateIdMode.Set &&
|
||||
@@ -106,19 +122,38 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
return ret;
|
||||
}
|
||||
|
||||
function getRating(state: GQL.SlimPerformerDataFragment[]) {
|
||||
let ret: number | undefined;
|
||||
let first = true;
|
||||
|
||||
state.forEach((performer) => {
|
||||
if (first) {
|
||||
ret = performer.rating ?? undefined;
|
||||
first = false;
|
||||
} else if (ret !== performer.rating) {
|
||||
ret = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const state = props.selected;
|
||||
let updateTagIds: string[] = [];
|
||||
let updateFavorite: boolean | undefined;
|
||||
let updateRating: number | undefined;
|
||||
let first = true;
|
||||
|
||||
state.forEach((performer: GQL.SlimPerformerDataFragment) => {
|
||||
const performerTagIDs = (performer.tags ?? []).map((p) => p.id).sort();
|
||||
const performerRating = performer.rating;
|
||||
|
||||
if (first) {
|
||||
updateTagIds = performerTagIDs;
|
||||
first = false;
|
||||
updateFavorite = performer.favorite;
|
||||
updateRating = performerRating ?? undefined;
|
||||
} else {
|
||||
if (!_.isEqual(performerTagIDs, updateTagIds)) {
|
||||
updateTagIds = [];
|
||||
@@ -126,6 +161,9 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
if (performer.favorite !== updateFavorite) {
|
||||
updateFavorite = undefined;
|
||||
}
|
||||
if (performerRating !== updateRating) {
|
||||
updateRating = undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -133,6 +171,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
setTagIds(updateTagIds);
|
||||
}
|
||||
setFavorite(updateFavorite);
|
||||
setRating(updateRating);
|
||||
}, [props.selected, tagMode]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -201,6 +240,18 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
}}
|
||||
isRunning={isUpdating}
|
||||
>
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Rating",
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
value={rating}
|
||||
onSetRating={(value) => setRating(value)}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form>
|
||||
<Form.Group controlId="tags">
|
||||
<Form.Label>Tags</Form.Label>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { NavUtils, TextUtils } from "src/utils";
|
||||
import {
|
||||
@@ -35,13 +34,13 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
||||
);
|
||||
const ageString = `${age} years old${ageFromDate ? " in this scene." : "."}`;
|
||||
|
||||
function maybeRenderFavoriteBanner() {
|
||||
function maybeRenderFavoriteIcon() {
|
||||
if (performer.favorite === false) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div className="rating-banner rating-5">
|
||||
<FormattedMessage id="favourite" defaultMessage="Favourite" />
|
||||
<div className="favorite">
|
||||
<Icon icon="heart" size="2x" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -120,6 +119,21 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderRatingBanner() {
|
||||
if (!performer.rating) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`rating-banner ${
|
||||
performer.rating ? `rating-${performer.rating}` : ""
|
||||
}`}
|
||||
>
|
||||
RATING: {performer.rating}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BasicCard
|
||||
className="performer-card"
|
||||
@@ -131,7 +145,8 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
||||
alt={performer.name ?? ""}
|
||||
src={performer.image_path ?? ""}
|
||||
/>
|
||||
{maybeRenderFavoriteBanner()}
|
||||
{maybeRenderFavoriteIcon()}
|
||||
{maybeRenderRatingBanner()}
|
||||
</>
|
||||
}
|
||||
details={
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as GQL from "src/core/generated-graphql";
|
||||
import { genderToString } from "src/core/StashService";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { TextField, URLField } from "src/utils/field";
|
||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
|
||||
interface IPerformerDetails {
|
||||
performer: Partial<GQL.PerformerDataFragment>;
|
||||
@@ -35,6 +36,21 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
);
|
||||
}
|
||||
|
||||
function renderRating() {
|
||||
if (!performer.rating) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<dl className="row mb-0">
|
||||
<dt className="col-3 col-xl-2">Rating:</dt>
|
||||
<dd className="col-9 col-xl-10">
|
||||
<RatingStars value={performer.rating} />
|
||||
</dd>
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
|
||||
function renderStashIDs() {
|
||||
if (!performer.stash_ids?.length) {
|
||||
return;
|
||||
@@ -139,6 +155,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
TextUtils.instagramURL
|
||||
)}
|
||||
/>
|
||||
{renderRating()}
|
||||
{renderTagsField()}
|
||||
{renderStashIDs()}
|
||||
</>
|
||||
|
||||
@@ -36,6 +36,7 @@ import { ImageUtils } from "src/utils";
|
||||
import { useToast } from "src/hooks";
|
||||
import { Prompt, useHistory } from "react-router-dom";
|
||||
import { useFormik } from "formik";
|
||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import { PerformerScrapeDialog } from "./PerformerScrapeDialog";
|
||||
import PerformerScrapeModal from "./PerformerScrapeModal";
|
||||
|
||||
@@ -62,7 +63,6 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
// Editing state
|
||||
const [scraper, setScraper] = useState<GQL.Scraper | undefined>();
|
||||
const [newTags, setNewTags] = useState<GQL.ScrapedSceneTag[]>();
|
||||
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
|
||||
// Network state
|
||||
@@ -109,6 +109,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
tag_ids: yup.array(yup.string().required()).optional(),
|
||||
stash_ids: yup.mixed<GQL.StashIdInput>().optional(),
|
||||
image: yup.string().optional().nullable(),
|
||||
rating: yup.number().optional().nullable(),
|
||||
details: yup.string().optional(),
|
||||
death_date: yup.string().optional(),
|
||||
hair_color: yup.string().optional(),
|
||||
@@ -135,6 +136,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
tag_ids: (performer.tags ?? []).map((t) => t.id),
|
||||
stash_ids: performer.stash_ids ?? undefined,
|
||||
image: undefined,
|
||||
rating: performer.rating ?? undefined,
|
||||
details: performer.details ?? "",
|
||||
death_date: performer.death_date ?? "",
|
||||
hair_color: performer.hair_color ?? "",
|
||||
@@ -149,6 +151,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
onSubmit: (values) => onSave(values),
|
||||
});
|
||||
|
||||
function setRating(v: number) {
|
||||
formik.setFieldValue("rating", v);
|
||||
}
|
||||
|
||||
function translateScrapedGender(scrapedGender?: string) {
|
||||
if (!scrapedGender) {
|
||||
return;
|
||||
@@ -386,6 +392,30 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
});
|
||||
}
|
||||
|
||||
// numeric keypresses get caught by jwplayer, so blur the element
|
||||
// if the rating sequence is started
|
||||
Mousetrap.bind("r", () => {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
Mousetrap.bind("0", () => setRating(NaN));
|
||||
Mousetrap.bind("1", () => setRating(1));
|
||||
Mousetrap.bind("2", () => setRating(2));
|
||||
Mousetrap.bind("3", () => setRating(3));
|
||||
Mousetrap.bind("4", () => setRating(4));
|
||||
Mousetrap.bind("5", () => setRating(5));
|
||||
|
||||
setTimeout(() => {
|
||||
Mousetrap.unbind("0");
|
||||
Mousetrap.unbind("1");
|
||||
Mousetrap.unbind("2");
|
||||
Mousetrap.unbind("3");
|
||||
Mousetrap.unbind("4");
|
||||
Mousetrap.unbind("5");
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("s s");
|
||||
|
||||
@@ -424,6 +454,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
return {
|
||||
...values,
|
||||
gender: stringToGender(values.gender),
|
||||
rating: values.rating ?? null,
|
||||
weight: Number(values.weight),
|
||||
id: performer.id ?? "",
|
||||
};
|
||||
@@ -909,6 +940,18 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
</Col>
|
||||
</Form.Group>
|
||||
{renderTagsField()}
|
||||
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||
Rating
|
||||
</Form.Label>
|
||||
<Col xs={fieldXS} xl={fieldXL}>
|
||||
<RatingStars
|
||||
value={formik.values.rating ?? undefined}
|
||||
onSetRating={(value) => formik.setFieldValue("rating", value)}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
{renderStashIDs()}
|
||||
|
||||
{renderButtons()}
|
||||
|
||||
@@ -72,6 +72,14 @@
|
||||
right: 1rem;
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.favorite {
|
||||
color: #ff7373;
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9));
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconProp, library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { IconProp, SizeProp, library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faStar as fasStar } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faStar as farStar } from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
@@ -11,13 +11,15 @@ interface IIcon {
|
||||
icon: IconProp;
|
||||
className?: string;
|
||||
color?: string;
|
||||
size?: SizeProp;
|
||||
}
|
||||
|
||||
const Icon: React.FC<IIcon> = ({ icon, className, color }) => (
|
||||
const Icon: React.FC<IIcon> = ({ icon, className, color, size }) => (
|
||||
<FontAwesomeIcon
|
||||
icon={icon}
|
||||
className={`fa-icon ${className}`}
|
||||
color={color}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -45,6 +45,21 @@ function maybeRenderChildren(studio: GQL.StudioDataFragment) {
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderRatingBanner(studio: GQL.StudioDataFragment) {
|
||||
if (!studio.rating) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`rating-banner ${
|
||||
studio.rating ? `rating-${studio.rating}` : ""
|
||||
}`}
|
||||
>
|
||||
RATING: {studio.rating}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const StudioCard: React.FC<IProps> = ({
|
||||
studio,
|
||||
hideParent,
|
||||
@@ -122,6 +137,7 @@ export const StudioCard: React.FC<IProps> = ({
|
||||
</h5>
|
||||
{maybeRenderParent(studio, hideParent)}
|
||||
{maybeRenderChildren(studio)}
|
||||
{maybeRenderRatingBanner(studio)}
|
||||
{maybeRenderPopoverButtonGroup()}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
StudioSelect,
|
||||
} from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import { StudioScenesPanel } from "./StudioScenesPanel";
|
||||
import { StudioGalleriesPanel } from "./StudioGalleriesPanel";
|
||||
import { StudioImagesPanel } from "./StudioImagesPanel";
|
||||
@@ -45,6 +46,7 @@ export const Studio: React.FC = () => {
|
||||
const [name, setName] = useState<string>();
|
||||
const [url, setUrl] = useState<string>();
|
||||
const [parentStudioId, setParentStudioId] = useState<string>();
|
||||
const [rating, setRating] = useState<number | undefined>(undefined);
|
||||
const [details, setDetails] = useState<string>();
|
||||
|
||||
// Studio state
|
||||
@@ -64,6 +66,7 @@ export const Studio: React.FC = () => {
|
||||
setName(state.name);
|
||||
setUrl(state.url ?? undefined);
|
||||
setParentStudioId(state?.parent_studio?.id ?? undefined);
|
||||
setRating(state.rating ?? undefined);
|
||||
setDetails(state.details ?? undefined);
|
||||
}
|
||||
|
||||
@@ -72,6 +75,7 @@ export const Studio: React.FC = () => {
|
||||
updateStudioEditState(studioData);
|
||||
setImagePreview(studioData.image_path ?? undefined);
|
||||
setStudio(studioData);
|
||||
setRating(studioData.rating ?? undefined);
|
||||
}
|
||||
|
||||
// set up hotkeys
|
||||
@@ -83,6 +87,30 @@ export const Studio: React.FC = () => {
|
||||
Mousetrap.bind("e", () => setIsEditing(true));
|
||||
Mousetrap.bind("d d", () => onDelete());
|
||||
|
||||
// numeric keypresses get caught by jwplayer, so blur the element
|
||||
// if the rating sequence is started
|
||||
Mousetrap.bind("r", () => {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
Mousetrap.bind("0", () => setRating(NaN));
|
||||
Mousetrap.bind("1", () => setRating(1));
|
||||
Mousetrap.bind("2", () => setRating(2));
|
||||
Mousetrap.bind("3", () => setRating(3));
|
||||
Mousetrap.bind("4", () => setRating(4));
|
||||
Mousetrap.bind("5", () => setRating(5));
|
||||
|
||||
setTimeout(() => {
|
||||
Mousetrap.unbind("0");
|
||||
Mousetrap.unbind("1");
|
||||
Mousetrap.unbind("2");
|
||||
Mousetrap.unbind("3");
|
||||
Mousetrap.unbind("4");
|
||||
Mousetrap.unbind("5");
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (isEditing) {
|
||||
Mousetrap.unbind("s s");
|
||||
@@ -121,6 +149,7 @@ export const Studio: React.FC = () => {
|
||||
image,
|
||||
details,
|
||||
parent_id: parentStudioId ?? null,
|
||||
rating: rating ?? null,
|
||||
};
|
||||
|
||||
if (!isNew) {
|
||||
@@ -314,6 +343,16 @@ export const Studio: React.FC = () => {
|
||||
<td>Parent Studio</td>
|
||||
<td>{renderStudio()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rating:</td>
|
||||
<td>
|
||||
<RatingStars
|
||||
value={rating}
|
||||
disabled={!isEditing}
|
||||
onSetRating={(value) => setRating(value ?? NaN)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{!isEditing && renderStashIDs()}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
@@ -67,6 +67,7 @@ piercings
|
||||
image (base64 encoding of the image file)
|
||||
created_at
|
||||
updated_at
|
||||
rating (integer)
|
||||
details
|
||||
```
|
||||
|
||||
@@ -77,6 +78,7 @@ url
|
||||
image (base64 encoding of the image file)
|
||||
created_at
|
||||
updated_at
|
||||
rating (integer)
|
||||
details
|
||||
```
|
||||
|
||||
|
||||
@@ -202,6 +202,7 @@ export class ListFilterModel {
|
||||
"scenes_count",
|
||||
"tag_count",
|
||||
"random",
|
||||
"rating",
|
||||
];
|
||||
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
|
||||
|
||||
@@ -231,6 +232,7 @@ export class ListFilterModel {
|
||||
new GenderCriterionOption(),
|
||||
new PerformerIsMissingCriterionOption(),
|
||||
new TagsCriterionOption(),
|
||||
new RatingCriterionOption(),
|
||||
ListFilterModel.createCriterionOption("url"),
|
||||
ListFilterModel.createCriterionOption("tag_count"),
|
||||
ListFilterModel.createCriterionOption("scene_count"),
|
||||
@@ -244,6 +246,8 @@ export class ListFilterModel {
|
||||
break;
|
||||
}
|
||||
case FilterMode.Studios:
|
||||
this.sortBy = "name";
|
||||
this.sortByOptions = ["name", "random", "rating", "scenes_count"];
|
||||
this.sortBy = defaultSort ?? "name";
|
||||
this.sortByOptions = [
|
||||
"name",
|
||||
@@ -257,6 +261,7 @@ export class ListFilterModel {
|
||||
new NoneCriterionOption(),
|
||||
new ParentStudiosCriterionOption(),
|
||||
new StudioIsMissingCriterionOption(),
|
||||
new RatingCriterionOption(),
|
||||
ListFilterModel.createCriterionOption("scene_count"),
|
||||
ListFilterModel.createCriterionOption("image_count"),
|
||||
ListFilterModel.createCriterionOption("gallery_count"),
|
||||
@@ -777,6 +782,14 @@ export class ListFilterModel {
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "rating": {
|
||||
const ratingCrit = criterion as RatingCriterion;
|
||||
result.rating = {
|
||||
value: ratingCrit.value,
|
||||
modifier: ratingCrit.modifier,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "url": {
|
||||
const urlCrit = criterion as StringCriterion;
|
||||
result.url = {
|
||||
@@ -1040,6 +1053,14 @@ export class ListFilterModel {
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "rating": {
|
||||
const ratingCrit = criterion as RatingCriterion;
|
||||
result.rating = {
|
||||
value: ratingCrit.value,
|
||||
modifier: ratingCrit.modifier,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "url": {
|
||||
const urlCrit = criterion as StringCriterion;
|
||||
result.url = {
|
||||
|
||||
Reference in New Issue
Block a user