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