mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
Add penis length and circumcision stats to performers. (#3627)
* Add penis length stat to performers. * Modified the UI to display and edit the stat. * Added the ability to filter floats to allow filtering by penis length. * Add circumcision stat to performer. * Refactor enum filtering * Change boolean filter to radio buttons * Return null for empty enum values --------- Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
@@ -16,6 +16,8 @@ fragment SlimPerformerData on Performer {
|
||||
eye_color
|
||||
height_cm
|
||||
fake_tits
|
||||
penis_length
|
||||
circumcised
|
||||
career_length
|
||||
tattoos
|
||||
piercings
|
||||
|
||||
@@ -14,6 +14,8 @@ fragment PerformerData on Performer {
|
||||
height_cm
|
||||
measurements
|
||||
fake_tits
|
||||
penis_length
|
||||
circumcised
|
||||
career_length
|
||||
tattoos
|
||||
piercings
|
||||
|
||||
@@ -13,6 +13,8 @@ fragment ScrapedPerformerData on ScrapedPerformer {
|
||||
height
|
||||
measurements
|
||||
fake_tits
|
||||
penis_length
|
||||
circumcised
|
||||
career_length
|
||||
tattoos
|
||||
piercings
|
||||
@@ -43,6 +45,8 @@ fragment ScrapedScenePerformerData on ScrapedPerformer {
|
||||
height
|
||||
measurements
|
||||
fake_tits
|
||||
penis_length
|
||||
circumcised
|
||||
career_length
|
||||
tattoos
|
||||
piercings
|
||||
|
||||
@@ -76,6 +76,10 @@ input PerformerFilterType {
|
||||
measurements: StringCriterionInput
|
||||
"""Filter by fake tits value"""
|
||||
fake_tits: StringCriterionInput
|
||||
"""Filter by penis length value"""
|
||||
penis_length: FloatCriterionInput
|
||||
"""Filter by ciricumcision"""
|
||||
circumcised: CircumcisionCriterionInput
|
||||
"""Filter by career length"""
|
||||
career_length: StringCriterionInput
|
||||
"""Filter by tattoos"""
|
||||
@@ -505,6 +509,12 @@ input IntCriterionInput {
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
||||
input FloatCriterionInput {
|
||||
value: Float!
|
||||
value2: Float
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
||||
input MultiCriterionInput {
|
||||
value: [ID!]
|
||||
modifier: CriterionModifier!
|
||||
@@ -514,6 +524,11 @@ input GenderCriterionInput {
|
||||
value: GenderEnum
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
||||
input CircumcisionCriterionInput {
|
||||
value: [CircumisedEnum!]
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
||||
input HierarchicalMultiCriterionInput {
|
||||
value: [ID!]
|
||||
|
||||
@@ -6,6 +6,11 @@ enum GenderEnum {
|
||||
INTERSEX
|
||||
NON_BINARY
|
||||
}
|
||||
|
||||
enum CircumisedEnum {
|
||||
CUT
|
||||
UNCUT
|
||||
}
|
||||
|
||||
type Performer {
|
||||
id: ID!
|
||||
@@ -24,6 +29,8 @@ type Performer {
|
||||
height_cm: Int
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
penis_length: Float
|
||||
circumcised: CircumisedEnum
|
||||
career_length: String
|
||||
tattoos: String
|
||||
piercings: String
|
||||
@@ -69,6 +76,8 @@ input PerformerCreateInput {
|
||||
height_cm: Int
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
penis_length: Float
|
||||
circumcised: CircumisedEnum
|
||||
career_length: String
|
||||
tattoos: String
|
||||
piercings: String
|
||||
@@ -107,6 +116,8 @@ input PerformerUpdateInput {
|
||||
height_cm: Int
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
penis_length: Float
|
||||
circumcised: CircumisedEnum
|
||||
career_length: String
|
||||
tattoos: String
|
||||
piercings: String
|
||||
@@ -150,6 +161,8 @@ input BulkPerformerUpdateInput {
|
||||
height_cm: Int
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
penis_length: Float
|
||||
circumcised: CircumisedEnum
|
||||
career_length: String
|
||||
tattoos: String
|
||||
piercings: String
|
||||
|
||||
@@ -15,6 +15,8 @@ type ScrapedPerformer {
|
||||
height: String
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
penis_length: String
|
||||
circumcised: String
|
||||
career_length: String
|
||||
tattoos: String
|
||||
piercings: String
|
||||
@@ -48,6 +50,8 @@ input ScrapedPerformerInput {
|
||||
height: String
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
penis_length: String
|
||||
circumcised: String
|
||||
career_length: String
|
||||
tattoos: String
|
||||
piercings: String
|
||||
|
||||
@@ -87,7 +87,7 @@ func initialiseCustomImages() {
|
||||
}
|
||||
}
|
||||
|
||||
func getRandomPerformerImageUsingName(name string, gender models.GenderEnum, customPath string) ([]byte, error) {
|
||||
func getRandomPerformerImageUsingName(name string, gender *models.GenderEnum, customPath string) ([]byte, error) {
|
||||
var box *imageBox
|
||||
|
||||
// If we have a custom path, we should return a new box in the given path.
|
||||
@@ -95,8 +95,13 @@ func getRandomPerformerImageUsingName(name string, gender models.GenderEnum, cus
|
||||
box = performerBoxCustom
|
||||
}
|
||||
|
||||
var g models.GenderEnum
|
||||
if gender != nil {
|
||||
g = *gender
|
||||
}
|
||||
|
||||
if box == nil {
|
||||
switch gender {
|
||||
switch g {
|
||||
case models.GenderEnumFemale:
|
||||
box = performerBox
|
||||
case models.GenderEnumMale:
|
||||
|
||||
@@ -67,7 +67,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC
|
||||
newPerformer.URL = *input.URL
|
||||
}
|
||||
if input.Gender != nil {
|
||||
newPerformer.Gender = *input.Gender
|
||||
newPerformer.Gender = input.Gender
|
||||
}
|
||||
if input.Birthdate != nil {
|
||||
d := models.NewDate(*input.Birthdate)
|
||||
@@ -98,6 +98,12 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC
|
||||
if input.FakeTits != nil {
|
||||
newPerformer.FakeTits = *input.FakeTits
|
||||
}
|
||||
if input.PenisLength != nil {
|
||||
newPerformer.PenisLength = input.PenisLength
|
||||
}
|
||||
if input.Circumcised != nil {
|
||||
newPerformer.Circumcised = input.Circumcised
|
||||
}
|
||||
if input.CareerLength != nil {
|
||||
newPerformer.CareerLength = *input.CareerLength
|
||||
}
|
||||
@@ -222,6 +228,16 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU
|
||||
|
||||
updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity")
|
||||
updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits")
|
||||
updatedPerformer.PenisLength = translator.optionalFloat64(input.PenisLength, "penis_length")
|
||||
|
||||
if translator.hasField("circumcised") {
|
||||
if input.Circumcised != nil {
|
||||
updatedPerformer.Circumcised = models.NewOptionalString(input.Circumcised.String())
|
||||
} else {
|
||||
updatedPerformer.Circumcised = models.NewOptionalStringPtr(nil)
|
||||
}
|
||||
}
|
||||
|
||||
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
|
||||
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
|
||||
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
|
||||
@@ -339,6 +355,16 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
|
||||
|
||||
updatedPerformer.Measurements = translator.optionalString(input.Measurements, "measurements")
|
||||
updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits")
|
||||
updatedPerformer.PenisLength = translator.optionalFloat64(input.PenisLength, "penis_length")
|
||||
|
||||
if translator.hasField("circumcised") {
|
||||
if input.Circumcised != nil {
|
||||
updatedPerformer.Circumcised = models.NewOptionalString(input.Circumcised.String())
|
||||
} else {
|
||||
updatedPerformer.Circumcised = models.NewOptionalStringPtr(nil)
|
||||
}
|
||||
}
|
||||
|
||||
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
|
||||
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
|
||||
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
|
||||
|
||||
@@ -65,7 +65,8 @@ func scrapedToPerformerInput(performer *models.ScrapedPerformer) models.Performe
|
||||
ret.DeathDate = &d
|
||||
}
|
||||
if performer.Gender != nil {
|
||||
ret.Gender = models.GenderEnum(*performer.Gender)
|
||||
v := models.GenderEnum(*performer.Gender)
|
||||
ret.Gender = &v
|
||||
}
|
||||
if performer.Ethnicity != nil {
|
||||
ret.Ethnicity = *performer.Ethnicity
|
||||
@@ -97,6 +98,16 @@ func scrapedToPerformerInput(performer *models.ScrapedPerformer) models.Performe
|
||||
if performer.FakeTits != nil {
|
||||
ret.FakeTits = *performer.FakeTits
|
||||
}
|
||||
if performer.PenisLength != nil {
|
||||
h, err := strconv.ParseFloat(*performer.PenisLength, 64)
|
||||
if err == nil {
|
||||
ret.PenisLength = &h
|
||||
}
|
||||
}
|
||||
if performer.Circumcised != nil {
|
||||
v := models.CircumisedEnum(*performer.Circumcised)
|
||||
ret.Circumcised = &v
|
||||
}
|
||||
if performer.CareerLength != nil {
|
||||
ret.CareerLength = *performer.CareerLength
|
||||
}
|
||||
|
||||
@@ -228,6 +228,10 @@ func Test_scrapedToPerformerInput(t *testing.T) {
|
||||
return &d
|
||||
}
|
||||
|
||||
genderPtr := func(g models.GenderEnum) *models.GenderEnum {
|
||||
return &g
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
performer *models.ScrapedPerformer
|
||||
@@ -259,7 +263,7 @@ func Test_scrapedToPerformerInput(t *testing.T) {
|
||||
Name: name,
|
||||
Birthdate: dateToDatePtr(models.NewDate(*nextVal())),
|
||||
DeathDate: dateToDatePtr(models.NewDate(*nextVal())),
|
||||
Gender: models.GenderEnum(*nextVal()),
|
||||
Gender: genderPtr(models.GenderEnum(*nextVal())),
|
||||
Ethnicity: *nextVal(),
|
||||
Country: *nextVal(),
|
||||
EyeColor: *nextVal(),
|
||||
|
||||
@@ -131,7 +131,6 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) {
|
||||
EyeColor: getString(performer.EyeColor),
|
||||
HairColor: getString(performer.HairColor),
|
||||
FakeTits: getString(performer.FakeTits),
|
||||
Gender: models.GenderEnum(getString(performer.Gender)),
|
||||
Height: getIntPtr(performer.Height),
|
||||
Weight: getIntPtr(performer.Weight),
|
||||
Instagram: getString(performer.Instagram),
|
||||
@@ -150,6 +149,11 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) {
|
||||
UpdatedAt: currentTime,
|
||||
}
|
||||
|
||||
if performer.Gender != nil {
|
||||
v := models.GenderEnum(getString(performer.Gender))
|
||||
newPerformer.Gender = &v
|
||||
}
|
||||
|
||||
err := txn.WithTxn(ctx, instance.Repository, func(ctx context.Context) error {
|
||||
r := instance.Repository
|
||||
err := r.Performer.Create(ctx, &newPerformer)
|
||||
|
||||
@@ -109,6 +109,20 @@ func (i IntCriterionInput) ValidModifier() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type FloatCriterionInput struct {
|
||||
Value float64 `json:"value"`
|
||||
Value2 *float64 `json:"value2"`
|
||||
Modifier CriterionModifier `json:"modifier"`
|
||||
}
|
||||
|
||||
func (i FloatCriterionInput) ValidModifier() bool {
|
||||
switch i.Modifier {
|
||||
case CriterionModifierEquals, CriterionModifierNotEquals, CriterionModifierGreaterThan, CriterionModifierLessThan, CriterionModifierIsNull, CriterionModifierNotNull, CriterionModifierBetween, CriterionModifierNotBetween:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ResolutionCriterionInput struct {
|
||||
Value ResolutionEnum `json:"value"`
|
||||
Modifier CriterionModifier `json:"modifier"`
|
||||
|
||||
@@ -48,6 +48,8 @@ type Performer struct {
|
||||
Height string `json:"height,omitempty"`
|
||||
Measurements string `json:"measurements,omitempty"`
|
||||
FakeTits string `json:"fake_tits,omitempty"`
|
||||
PenisLength float64 `json:"penis_length,omitempty"`
|
||||
Circumcised string `json:"circumcised,omitempty"`
|
||||
CareerLength string `json:"career_length,omitempty"`
|
||||
Tattoos string `json:"tattoos,omitempty"`
|
||||
Piercings string `json:"piercings,omitempty"`
|
||||
|
||||
@@ -6,26 +6,28 @@ import (
|
||||
)
|
||||
|
||||
type Performer struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Disambiguation string `json:"disambiguation"`
|
||||
Gender GenderEnum `json:"gender"`
|
||||
URL string `json:"url"`
|
||||
Twitter string `json:"twitter"`
|
||||
Instagram string `json:"instagram"`
|
||||
Birthdate *Date `json:"birthdate"`
|
||||
Ethnicity string `json:"ethnicity"`
|
||||
Country string `json:"country"`
|
||||
EyeColor string `json:"eye_color"`
|
||||
Height *int `json:"height"`
|
||||
Measurements string `json:"measurements"`
|
||||
FakeTits string `json:"fake_tits"`
|
||||
CareerLength string `json:"career_length"`
|
||||
Tattoos string `json:"tattoos"`
|
||||
Piercings string `json:"piercings"`
|
||||
Favorite bool `json:"favorite"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Disambiguation string `json:"disambiguation"`
|
||||
Gender *GenderEnum `json:"gender"`
|
||||
URL string `json:"url"`
|
||||
Twitter string `json:"twitter"`
|
||||
Instagram string `json:"instagram"`
|
||||
Birthdate *Date `json:"birthdate"`
|
||||
Ethnicity string `json:"ethnicity"`
|
||||
Country string `json:"country"`
|
||||
EyeColor string `json:"eye_color"`
|
||||
Height *int `json:"height"`
|
||||
Measurements string `json:"measurements"`
|
||||
FakeTits string `json:"fake_tits"`
|
||||
PenisLength *float64 `json:"penis_length"`
|
||||
Circumcised *CircumisedEnum `json:"circumcised"`
|
||||
CareerLength string `json:"career_length"`
|
||||
Tattoos string `json:"tattoos"`
|
||||
Piercings string `json:"piercings"`
|
||||
Favorite bool `json:"favorite"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// Rating expressed in 1-100 scale
|
||||
Rating *int `json:"rating"`
|
||||
Details string `json:"details"`
|
||||
@@ -90,6 +92,8 @@ type PerformerPartial struct {
|
||||
Height OptionalInt
|
||||
Measurements OptionalString
|
||||
FakeTits OptionalString
|
||||
PenisLength OptionalFloat64
|
||||
Circumcised OptionalString
|
||||
CareerLength OptionalString
|
||||
Tattoos OptionalString
|
||||
Piercings OptionalString
|
||||
|
||||
@@ -32,6 +32,8 @@ type ScrapedPerformer struct {
|
||||
Height *string `json:"height"`
|
||||
Measurements *string `json:"measurements"`
|
||||
FakeTits *string `json:"fake_tits"`
|
||||
PenisLength *string `json:"penis_length"`
|
||||
Circumcised *string `json:"circumcised"`
|
||||
CareerLength *string `json:"career_length"`
|
||||
Tattoos *string `json:"tattoos"`
|
||||
Piercings *string `json:"piercings"`
|
||||
|
||||
@@ -61,6 +61,52 @@ type GenderCriterionInput struct {
|
||||
Modifier CriterionModifier `json:"modifier"`
|
||||
}
|
||||
|
||||
type CircumisedEnum string
|
||||
|
||||
const (
|
||||
CircumisedEnumCut CircumisedEnum = "CUT"
|
||||
CircumisedEnumUncut CircumisedEnum = "UNCUT"
|
||||
)
|
||||
|
||||
var AllCircumcisionEnum = []CircumisedEnum{
|
||||
CircumisedEnumCut,
|
||||
CircumisedEnumUncut,
|
||||
}
|
||||
|
||||
func (e CircumisedEnum) IsValid() bool {
|
||||
switch e {
|
||||
case CircumisedEnumCut, CircumisedEnumUncut:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e CircumisedEnum) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *CircumisedEnum) UnmarshalGQL(v interface{}) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = CircumisedEnum(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid CircumisedEnum", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e CircumisedEnum) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
type CircumcisionCriterionInput struct {
|
||||
Value []CircumisedEnum `json:"value"`
|
||||
Modifier CriterionModifier `json:"modifier"`
|
||||
}
|
||||
|
||||
type PerformerFilterType struct {
|
||||
And *PerformerFilterType `json:"AND"`
|
||||
Or *PerformerFilterType `json:"OR"`
|
||||
@@ -88,6 +134,10 @@ type PerformerFilterType struct {
|
||||
Measurements *StringCriterionInput `json:"measurements"`
|
||||
// Filter by fake tits value
|
||||
FakeTits *StringCriterionInput `json:"fake_tits"`
|
||||
// Filter by penis length value
|
||||
PenisLength *FloatCriterionInput `json:"penis_length"`
|
||||
// Filter by circumcision
|
||||
Circumcised *CircumcisionCriterionInput `json:"circumcised"`
|
||||
// Filter by career length
|
||||
CareerLength *StringCriterionInput `json:"career_length"`
|
||||
// Filter by tattoos
|
||||
|
||||
@@ -23,7 +23,6 @@ func ToJSON(ctx context.Context, reader ImageAliasStashIDGetter, performer *mode
|
||||
newPerformerJSON := jsonschema.Performer{
|
||||
Name: performer.Name,
|
||||
Disambiguation: performer.Disambiguation,
|
||||
Gender: performer.Gender.String(),
|
||||
URL: performer.URL,
|
||||
Ethnicity: performer.Ethnicity,
|
||||
Country: performer.Country,
|
||||
@@ -43,6 +42,14 @@ func ToJSON(ctx context.Context, reader ImageAliasStashIDGetter, performer *mode
|
||||
UpdatedAt: json.JSONTime{Time: performer.UpdatedAt},
|
||||
}
|
||||
|
||||
if performer.Gender != nil {
|
||||
newPerformerJSON.Gender = performer.Gender.String()
|
||||
}
|
||||
|
||||
if performer.Circumcised != nil {
|
||||
newPerformerJSON.Circumcised = performer.Circumcised.String()
|
||||
}
|
||||
|
||||
if performer.Birthdate != nil {
|
||||
newPerformerJSON.Birthdate = performer.Birthdate.String()
|
||||
}
|
||||
@@ -61,6 +68,10 @@ func ToJSON(ctx context.Context, reader ImageAliasStashIDGetter, performer *mode
|
||||
newPerformerJSON.Weight = *performer.Weight
|
||||
}
|
||||
|
||||
if performer.PenisLength != nil {
|
||||
newPerformerJSON.PenisLength = *performer.PenisLength
|
||||
}
|
||||
|
||||
if err := performer.LoadAliases(ctx, reader); err != nil {
|
||||
return nil, fmt.Errorf("loading performer aliases: %w", err)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ const (
|
||||
ethnicity = "ethnicity"
|
||||
eyeColor = "eyeColor"
|
||||
fakeTits = "fakeTits"
|
||||
gender = "gender"
|
||||
instagram = "instagram"
|
||||
measurements = "measurements"
|
||||
piercings = "piercings"
|
||||
@@ -42,10 +41,15 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
aliases = []string{"alias1", "alias2"}
|
||||
rating = 5
|
||||
height = 123
|
||||
weight = 60
|
||||
genderEnum = models.GenderEnumFemale
|
||||
gender = genderEnum.String()
|
||||
aliases = []string{"alias1", "alias2"}
|
||||
rating = 5
|
||||
height = 123
|
||||
weight = 60
|
||||
penisLength = 1.23
|
||||
circumcisedEnum = models.CircumisedEnumCut
|
||||
circumcised = circumcisedEnum.String()
|
||||
)
|
||||
|
||||
var imageBytes = []byte("imageBytes")
|
||||
@@ -81,8 +85,10 @@ func createFullPerformer(id int, name string) *models.Performer {
|
||||
Ethnicity: ethnicity,
|
||||
EyeColor: eyeColor,
|
||||
FakeTits: fakeTits,
|
||||
PenisLength: &penisLength,
|
||||
Circumcised: &circumcisedEnum,
|
||||
Favorite: true,
|
||||
Gender: gender,
|
||||
Gender: &genderEnum,
|
||||
Height: &height,
|
||||
Instagram: instagram,
|
||||
Measurements: measurements,
|
||||
@@ -125,6 +131,8 @@ func createFullJSONPerformer(name string, image string) *jsonschema.Performer {
|
||||
Ethnicity: ethnicity,
|
||||
EyeColor: eyeColor,
|
||||
FakeTits: fakeTits,
|
||||
PenisLength: penisLength,
|
||||
Circumcised: circumcised,
|
||||
Favorite: true,
|
||||
Gender: gender,
|
||||
Height: strconv.Itoa(height),
|
||||
|
||||
@@ -189,7 +189,6 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform
|
||||
newPerformer := models.Performer{
|
||||
Name: performerJSON.Name,
|
||||
Disambiguation: performerJSON.Disambiguation,
|
||||
Gender: models.GenderEnum(performerJSON.Gender),
|
||||
URL: performerJSON.URL,
|
||||
Ethnicity: performerJSON.Ethnicity,
|
||||
Country: performerJSON.Country,
|
||||
@@ -213,6 +212,16 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform
|
||||
StashIDs: models.NewRelatedStashIDs(performerJSON.StashIDs),
|
||||
}
|
||||
|
||||
if performerJSON.Gender != "" {
|
||||
v := models.GenderEnum(performerJSON.Gender)
|
||||
newPerformer.Gender = &v
|
||||
}
|
||||
|
||||
if performerJSON.Circumcised != "" {
|
||||
v := models.CircumisedEnum(performerJSON.Circumcised)
|
||||
newPerformer.Circumcised = &v
|
||||
}
|
||||
|
||||
if performerJSON.Birthdate != "" {
|
||||
d, err := utils.ParseDateStringAsTime(performerJSON.Birthdate)
|
||||
if err == nil {
|
||||
@@ -237,6 +246,10 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform
|
||||
newPerformer.Weight = &performerJSON.Weight
|
||||
}
|
||||
|
||||
if performerJSON.PenisLength != 0 {
|
||||
newPerformer.PenisLength = &performerJSON.PenisLength
|
||||
}
|
||||
|
||||
if performerJSON.Height != "" {
|
||||
h, err := strconv.Atoi(performerJSON.Height)
|
||||
if err == nil {
|
||||
|
||||
@@ -41,7 +41,7 @@ func autotagMatchPerformers(ctx context.Context, path string, performerReader ma
|
||||
Name: &pp.Name,
|
||||
StoredID: &id,
|
||||
}
|
||||
if pp.Gender.IsValid() {
|
||||
if pp.Gender != nil && pp.Gender.IsValid() {
|
||||
v := pp.Gender.String()
|
||||
sp.Gender = &v
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ type ScrapedPerformerInput struct {
|
||||
Height *string `json:"height"`
|
||||
Measurements *string `json:"measurements"`
|
||||
FakeTits *string `json:"fake_tits"`
|
||||
PenisLength *string `json:"penis_length"`
|
||||
Circumcised *string `json:"circumcised"`
|
||||
CareerLength *string `json:"career_length"`
|
||||
Tattoos *string `json:"tattoos"`
|
||||
Piercings *string `json:"piercings"`
|
||||
|
||||
@@ -69,6 +69,8 @@ type scrapedPerformerStash struct {
|
||||
Height *string `graphql:"height" json:"height"`
|
||||
Measurements *string `graphql:"measurements" json:"measurements"`
|
||||
FakeTits *string `graphql:"fake_tits" json:"fake_tits"`
|
||||
PenisLength *string `graphql:"penis_length" json:"penis_length"`
|
||||
Circumcised *string `graphql:"circumcised" json:"circumcised"`
|
||||
CareerLength *string `graphql:"career_length" json:"career_length"`
|
||||
Tattoos *string `graphql:"tattoos" json:"tattoos"`
|
||||
Piercings *string `graphql:"piercings" json:"piercings"`
|
||||
|
||||
@@ -1009,7 +1009,7 @@ func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Perf
|
||||
if performer.FakeTits != "" {
|
||||
draft.BreastType = &performer.FakeTits
|
||||
}
|
||||
if performer.Gender.IsValid() {
|
||||
if performer.Gender != nil && performer.Gender.IsValid() {
|
||||
v := performer.Gender.String()
|
||||
draft.Gender = &v
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ const (
|
||||
dbConnTimeout = 30
|
||||
)
|
||||
|
||||
var appSchemaVersion uint = 45
|
||||
var appSchemaVersion uint = 46
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsBox embed.FS
|
||||
|
||||
@@ -426,6 +426,29 @@ func stringCriterionHandler(c *models.StringCriterionInput, column string) crite
|
||||
}
|
||||
}
|
||||
|
||||
func enumCriterionHandler(modifier models.CriterionModifier, values []string, column string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if modifier.IsValid() {
|
||||
switch modifier {
|
||||
case models.CriterionModifierIncludes, models.CriterionModifierEquals:
|
||||
if len(values) > 0 {
|
||||
f.whereClauses = append(f.whereClauses, getEnumSearchClause(column, values, false))
|
||||
}
|
||||
case models.CriterionModifierExcludes, models.CriterionModifierNotEquals:
|
||||
if len(values) > 0 {
|
||||
f.whereClauses = append(f.whereClauses, getEnumSearchClause(column, values, true))
|
||||
}
|
||||
case models.CriterionModifierIsNull:
|
||||
f.addWhere("(" + column + " IS NULL OR TRIM(" + column + ") = '')")
|
||||
case models.CriterionModifierNotNull:
|
||||
f.addWhere("(" + column + " IS NOT NULL AND TRIM(" + column + ") != '')")
|
||||
default:
|
||||
panic("unsupported string filter modifier")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pathCriterionHandler(c *models.StringCriterionInput, pathColumn string, basenameColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if c != nil {
|
||||
@@ -525,6 +548,15 @@ func intCriterionHandler(c *models.IntCriterionInput, column string, addJoinFn f
|
||||
}
|
||||
}
|
||||
|
||||
func floatCriterionHandler(c *models.FloatCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if c != nil {
|
||||
clause, args := getFloatCriterionWhereClause(column, *c)
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func boolCriterionHandler(c *bool, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if c != nil {
|
||||
|
||||
2
pkg/sqlite/migrations/46_penis_stats.up.sql
Normal file
2
pkg/sqlite/migrations/46_penis_stats.up.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `performers` ADD COLUMN `penis_length` float;
|
||||
ALTER TABLE `performers` ADD COLUMN `circumcised` varchar[10];
|
||||
@@ -42,6 +42,8 @@ type performerRow struct {
|
||||
Height null.Int `db:"height"`
|
||||
Measurements zero.String `db:"measurements"`
|
||||
FakeTits zero.String `db:"fake_tits"`
|
||||
PenisLength null.Float `db:"penis_length"`
|
||||
Circumcised zero.String `db:"circumcised"`
|
||||
CareerLength zero.String `db:"career_length"`
|
||||
Tattoos zero.String `db:"tattoos"`
|
||||
Piercings zero.String `db:"piercings"`
|
||||
@@ -64,7 +66,7 @@ func (r *performerRow) fromPerformer(o models.Performer) {
|
||||
r.ID = o.ID
|
||||
r.Name = o.Name
|
||||
r.Disambigation = zero.StringFrom(o.Disambiguation)
|
||||
if o.Gender.IsValid() {
|
||||
if o.Gender != nil && o.Gender.IsValid() {
|
||||
r.Gender = zero.StringFrom(o.Gender.String())
|
||||
}
|
||||
r.URL = zero.StringFrom(o.URL)
|
||||
@@ -79,6 +81,10 @@ func (r *performerRow) fromPerformer(o models.Performer) {
|
||||
r.Height = intFromPtr(o.Height)
|
||||
r.Measurements = zero.StringFrom(o.Measurements)
|
||||
r.FakeTits = zero.StringFrom(o.FakeTits)
|
||||
r.PenisLength = null.FloatFromPtr(o.PenisLength)
|
||||
if o.Circumcised != nil && o.Circumcised.IsValid() {
|
||||
r.Circumcised = zero.StringFrom(o.Circumcised.String())
|
||||
}
|
||||
r.CareerLength = zero.StringFrom(o.CareerLength)
|
||||
r.Tattoos = zero.StringFrom(o.Tattoos)
|
||||
r.Piercings = zero.StringFrom(o.Piercings)
|
||||
@@ -100,7 +106,6 @@ func (r *performerRow) resolve() *models.Performer {
|
||||
ID: r.ID,
|
||||
Name: r.Name,
|
||||
Disambiguation: r.Disambigation.String,
|
||||
Gender: models.GenderEnum(r.Gender.String),
|
||||
URL: r.URL.String,
|
||||
Twitter: r.Twitter.String,
|
||||
Instagram: r.Instagram.String,
|
||||
@@ -111,6 +116,7 @@ func (r *performerRow) resolve() *models.Performer {
|
||||
Height: nullIntPtr(r.Height),
|
||||
Measurements: r.Measurements.String,
|
||||
FakeTits: r.FakeTits.String,
|
||||
PenisLength: nullFloatPtr(r.PenisLength),
|
||||
CareerLength: r.CareerLength.String,
|
||||
Tattoos: r.Tattoos.String,
|
||||
Piercings: r.Piercings.String,
|
||||
@@ -126,6 +132,16 @@ func (r *performerRow) resolve() *models.Performer {
|
||||
IgnoreAutoTag: r.IgnoreAutoTag,
|
||||
}
|
||||
|
||||
if r.Gender.ValueOrZero() != "" {
|
||||
v := models.GenderEnum(r.Gender.String)
|
||||
ret.Gender = &v
|
||||
}
|
||||
|
||||
if r.Circumcised.ValueOrZero() != "" {
|
||||
v := models.CircumisedEnum(r.Circumcised.String)
|
||||
ret.Circumcised = &v
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -147,6 +163,8 @@ func (r *performerRowRecord) fromPartial(o models.PerformerPartial) {
|
||||
r.setNullInt("height", o.Height)
|
||||
r.setNullString("measurements", o.Measurements)
|
||||
r.setNullString("fake_tits", o.FakeTits)
|
||||
r.setNullFloat64("penis_length", o.PenisLength)
|
||||
r.setNullString("circumcised", o.Circumcised)
|
||||
r.setNullString("career_length", o.CareerLength)
|
||||
r.setNullString("tattoos", o.Tattoos)
|
||||
r.setNullString("piercings", o.Piercings)
|
||||
@@ -597,6 +615,15 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform
|
||||
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Measurements, tableName+".measurements"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.FakeTits, tableName+".fake_tits"))
|
||||
query.handleCriterion(ctx, floatCriterionHandler(filter.PenisLength, tableName+".penis_length", nil))
|
||||
|
||||
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if circumcised := filter.Circumcised; circumcised != nil {
|
||||
v := utils.StringerSliceToStringSlice(circumcised.Value)
|
||||
enumCriterionHandler(circumcised.Modifier, v, tableName+".circumcised")(ctx, f)
|
||||
}
|
||||
}))
|
||||
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.CareerLength, tableName+".career_length"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Tattoos, tableName+".tattoos"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Piercings, tableName+".piercings"))
|
||||
|
||||
@@ -52,6 +52,8 @@ func Test_PerformerStore_Create(t *testing.T) {
|
||||
height = 134
|
||||
measurements = "measurements"
|
||||
fakeTits = "fakeTits"
|
||||
penisLength = 1.23
|
||||
circumcised = models.CircumisedEnumCut
|
||||
careerLength = "careerLength"
|
||||
tattoos = "tattoos"
|
||||
piercings = "piercings"
|
||||
@@ -81,7 +83,7 @@ func Test_PerformerStore_Create(t *testing.T) {
|
||||
models.Performer{
|
||||
Name: name,
|
||||
Disambiguation: disambiguation,
|
||||
Gender: gender,
|
||||
Gender: &gender,
|
||||
URL: url,
|
||||
Twitter: twitter,
|
||||
Instagram: instagram,
|
||||
@@ -92,6 +94,8 @@ func Test_PerformerStore_Create(t *testing.T) {
|
||||
Height: &height,
|
||||
Measurements: measurements,
|
||||
FakeTits: fakeTits,
|
||||
PenisLength: &penisLength,
|
||||
Circumcised: &circumcised,
|
||||
CareerLength: careerLength,
|
||||
Tattoos: tattoos,
|
||||
Piercings: piercings,
|
||||
@@ -196,6 +200,8 @@ func Test_PerformerStore_Update(t *testing.T) {
|
||||
height = 134
|
||||
measurements = "measurements"
|
||||
fakeTits = "fakeTits"
|
||||
penisLength = 1.23
|
||||
circumcised = models.CircumisedEnumCut
|
||||
careerLength = "careerLength"
|
||||
tattoos = "tattoos"
|
||||
piercings = "piercings"
|
||||
@@ -226,7 +232,7 @@ func Test_PerformerStore_Update(t *testing.T) {
|
||||
ID: performerIDs[performerIdxWithGallery],
|
||||
Name: name,
|
||||
Disambiguation: disambiguation,
|
||||
Gender: gender,
|
||||
Gender: &gender,
|
||||
URL: url,
|
||||
Twitter: twitter,
|
||||
Instagram: instagram,
|
||||
@@ -237,6 +243,8 @@ func Test_PerformerStore_Update(t *testing.T) {
|
||||
Height: &height,
|
||||
Measurements: measurements,
|
||||
FakeTits: fakeTits,
|
||||
PenisLength: &penisLength,
|
||||
Circumcised: &circumcised,
|
||||
CareerLength: careerLength,
|
||||
Tattoos: tattoos,
|
||||
Piercings: piercings,
|
||||
@@ -327,6 +335,7 @@ func clearPerformerPartial() models.PerformerPartial {
|
||||
nullString := models.OptionalString{Set: true, Null: true}
|
||||
nullDate := models.OptionalDate{Set: true, Null: true}
|
||||
nullInt := models.OptionalInt{Set: true, Null: true}
|
||||
nullFloat := models.OptionalFloat64{Set: true, Null: true}
|
||||
|
||||
// leave mandatory fields
|
||||
return models.PerformerPartial{
|
||||
@@ -342,6 +351,8 @@ func clearPerformerPartial() models.PerformerPartial {
|
||||
Height: nullInt,
|
||||
Measurements: nullString,
|
||||
FakeTits: nullString,
|
||||
PenisLength: nullFloat,
|
||||
Circumcised: nullString,
|
||||
CareerLength: nullString,
|
||||
Tattoos: nullString,
|
||||
Piercings: nullString,
|
||||
@@ -372,6 +383,8 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
||||
height = 143
|
||||
measurements = "measurements"
|
||||
fakeTits = "fakeTits"
|
||||
penisLength = 1.23
|
||||
circumcised = models.CircumisedEnumCut
|
||||
careerLength = "careerLength"
|
||||
tattoos = "tattoos"
|
||||
piercings = "piercings"
|
||||
@@ -415,6 +428,8 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
||||
Height: models.NewOptionalInt(height),
|
||||
Measurements: models.NewOptionalString(measurements),
|
||||
FakeTits: models.NewOptionalString(fakeTits),
|
||||
PenisLength: models.NewOptionalFloat64(penisLength),
|
||||
Circumcised: models.NewOptionalString(circumcised.String()),
|
||||
CareerLength: models.NewOptionalString(careerLength),
|
||||
Tattoos: models.NewOptionalString(tattoos),
|
||||
Piercings: models.NewOptionalString(piercings),
|
||||
@@ -453,7 +468,7 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
||||
ID: performerIDs[performerIdxWithDupName],
|
||||
Name: name,
|
||||
Disambiguation: disambiguation,
|
||||
Gender: gender,
|
||||
Gender: &gender,
|
||||
URL: url,
|
||||
Twitter: twitter,
|
||||
Instagram: instagram,
|
||||
@@ -464,6 +479,8 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
||||
Height: &height,
|
||||
Measurements: measurements,
|
||||
FakeTits: fakeTits,
|
||||
PenisLength: &penisLength,
|
||||
Circumcised: &circumcised,
|
||||
CareerLength: careerLength,
|
||||
Tattoos: tattoos,
|
||||
Piercings: piercings,
|
||||
@@ -957,16 +974,30 @@ func TestPerformerQuery(t *testing.T) {
|
||||
false,
|
||||
},
|
||||
{
|
||||
"alias",
|
||||
"circumcised (cut)",
|
||||
nil,
|
||||
&models.PerformerFilterType{
|
||||
Aliases: &models.StringCriterionInput{
|
||||
Value: getPerformerStringValue(performerIdxWithGallery, "alias"),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
Circumcised: &models.CircumcisionCriterionInput{
|
||||
Value: []models.CircumisedEnum{models.CircumisedEnumCut},
|
||||
Modifier: models.CriterionModifierIncludes,
|
||||
},
|
||||
},
|
||||
[]int{performerIdxWithGallery},
|
||||
[]int{performerIdxWithScene},
|
||||
[]int{performerIdx1WithScene},
|
||||
[]int{performerIdxWithScene, performerIdx2WithScene},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"circumcised (excludes cut)",
|
||||
nil,
|
||||
&models.PerformerFilterType{
|
||||
Circumcised: &models.CircumcisionCriterionInput{
|
||||
Value: []models.CircumisedEnum{models.CircumisedEnumCut},
|
||||
Modifier: models.CriterionModifierExcludes,
|
||||
},
|
||||
},
|
||||
[]int{performerIdx2WithScene},
|
||||
// performerIdxWithScene has null value
|
||||
[]int{performerIdx1WithScene, performerIdxWithScene},
|
||||
false,
|
||||
},
|
||||
}
|
||||
@@ -995,6 +1026,107 @@ func TestPerformerQuery(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerformerQueryPenisLength(t *testing.T) {
|
||||
var upper = 4.0
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
modifier models.CriterionModifier
|
||||
value float64
|
||||
value2 *float64
|
||||
}{
|
||||
{
|
||||
"equals",
|
||||
models.CriterionModifierEquals,
|
||||
1,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"not equals",
|
||||
models.CriterionModifierNotEquals,
|
||||
1,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"greater than",
|
||||
models.CriterionModifierGreaterThan,
|
||||
1,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"between",
|
||||
models.CriterionModifierBetween,
|
||||
2,
|
||||
&upper,
|
||||
},
|
||||
{
|
||||
"greater than",
|
||||
models.CriterionModifierNotBetween,
|
||||
2,
|
||||
&upper,
|
||||
},
|
||||
{
|
||||
"null",
|
||||
models.CriterionModifierIsNull,
|
||||
0,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"not null",
|
||||
models.CriterionModifierNotNull,
|
||||
0,
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||
filter := &models.PerformerFilterType{
|
||||
PenisLength: &models.FloatCriterionInput{
|
||||
Modifier: tt.modifier,
|
||||
Value: tt.value,
|
||||
Value2: tt.value2,
|
||||
},
|
||||
}
|
||||
|
||||
performers, _, err := db.Performer.Query(ctx, filter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("PerformerStore.Query() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, p := range performers {
|
||||
verifyFloat(t, p.PenisLength, *filter.PenisLength)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func verifyFloat(t *testing.T, value *float64, criterion models.FloatCriterionInput) bool {
|
||||
t.Helper()
|
||||
assert := assert.New(t)
|
||||
switch criterion.Modifier {
|
||||
case models.CriterionModifierEquals:
|
||||
return assert.NotNil(value) && assert.Equal(criterion.Value, *value)
|
||||
case models.CriterionModifierNotEquals:
|
||||
return assert.NotNil(value) && assert.NotEqual(criterion.Value, *value)
|
||||
case models.CriterionModifierGreaterThan:
|
||||
return assert.NotNil(value) && assert.Greater(*value, criterion.Value)
|
||||
case models.CriterionModifierLessThan:
|
||||
return assert.NotNil(value) && assert.Less(*value, criterion.Value)
|
||||
case models.CriterionModifierBetween:
|
||||
return assert.NotNil(value) && assert.GreaterOrEqual(*value, criterion.Value) && assert.LessOrEqual(*value, *criterion.Value2)
|
||||
case models.CriterionModifierNotBetween:
|
||||
return assert.NotNil(value) && assert.True(*value < criterion.Value || *value > *criterion.Value2)
|
||||
case models.CriterionModifierIsNull:
|
||||
return assert.Nil(value)
|
||||
case models.CriterionModifierNotNull:
|
||||
return assert.NotNil(value)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func TestPerformerQueryForAutoTag(t *testing.T) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
tqb := db.Performer
|
||||
|
||||
@@ -3,6 +3,7 @@ package sqlite
|
||||
import (
|
||||
"github.com/doug-martin/goqu/v9/exp"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"gopkg.in/guregu/null.v4"
|
||||
"gopkg.in/guregu/null.v4/zero"
|
||||
)
|
||||
|
||||
@@ -77,11 +78,11 @@ func (r *updateRecord) setFloat64(destField string, v models.OptionalFloat64) {
|
||||
}
|
||||
}
|
||||
|
||||
// func (r *updateRecord) setNullFloat64(destField string, v models.OptionalFloat64) {
|
||||
// if v.Set {
|
||||
// r.set(destField, null.FloatFromPtr(v.Ptr()))
|
||||
// }
|
||||
// }
|
||||
func (r *updateRecord) setNullFloat64(destField string, v models.OptionalFloat64) {
|
||||
if v.Set {
|
||||
r.set(destField, null.FloatFromPtr(v.Ptr()))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *updateRecord) setSQLiteTimestamp(destField string, v models.OptionalTime) {
|
||||
if v.Set {
|
||||
|
||||
@@ -1331,6 +1331,29 @@ func getPerformerCareerLength(index int) *string {
|
||||
return &ret
|
||||
}
|
||||
|
||||
func getPerformerPenisLength(index int) *float64 {
|
||||
if index%5 == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := float64(index)
|
||||
return &ret
|
||||
}
|
||||
|
||||
func getPerformerCircumcised(index int) *models.CircumisedEnum {
|
||||
var ret models.CircumisedEnum
|
||||
switch {
|
||||
case index%3 == 0:
|
||||
return nil
|
||||
case index%3 == 1:
|
||||
ret = models.CircumisedEnumCut
|
||||
default:
|
||||
ret = models.CircumisedEnumUncut
|
||||
}
|
||||
|
||||
return &ret
|
||||
}
|
||||
|
||||
func getIgnoreAutoTag(index int) bool {
|
||||
return index%5 == 0
|
||||
}
|
||||
@@ -1372,6 +1395,8 @@ func createPerformers(ctx context.Context, n int, o int) error {
|
||||
DeathDate: getPerformerDeathDate(i),
|
||||
Details: getPerformerStringValue(i, "Details"),
|
||||
Ethnicity: getPerformerStringValue(i, "Ethnicity"),
|
||||
PenisLength: getPerformerPenisLength(i),
|
||||
Circumcised: getPerformerCircumcised(i),
|
||||
Rating: getIntPtr(getRating(i)),
|
||||
IgnoreAutoTag: getIgnoreAutoTag(i),
|
||||
TagIDs: models.NewRelatedIDs(tids),
|
||||
|
||||
@@ -159,6 +159,22 @@ func getStringSearchClause(columns []string, q string, not bool) sqlClause {
|
||||
return makeClause("("+likes+")", args...)
|
||||
}
|
||||
|
||||
func getEnumSearchClause(column string, enumVals []string, not bool) sqlClause {
|
||||
var args []interface{}
|
||||
|
||||
notStr := ""
|
||||
if not {
|
||||
notStr = " NOT"
|
||||
}
|
||||
|
||||
clause := fmt.Sprintf("(%s%s IN %s)", column, notStr, getInBinding(len(enumVals)))
|
||||
for _, enumVal := range enumVals {
|
||||
args = append(args, enumVal)
|
||||
}
|
||||
|
||||
return makeClause(clause, args...)
|
||||
}
|
||||
|
||||
func getInBinding(length int) string {
|
||||
bindings := strings.Repeat("?, ", length)
|
||||
bindings = strings.TrimRight(bindings, ", ")
|
||||
@@ -175,8 +191,26 @@ func getIntWhereClause(column string, modifier models.CriterionModifier, value i
|
||||
upper = &u
|
||||
}
|
||||
|
||||
args := []interface{}{value}
|
||||
betweenArgs := []interface{}{value, *upper}
|
||||
args := []interface{}{value, *upper}
|
||||
return getNumericWhereClause(column, modifier, args)
|
||||
}
|
||||
|
||||
func getFloatCriterionWhereClause(column string, input models.FloatCriterionInput) (string, []interface{}) {
|
||||
return getFloatWhereClause(column, input.Modifier, input.Value, input.Value2)
|
||||
}
|
||||
|
||||
func getFloatWhereClause(column string, modifier models.CriterionModifier, value float64, upper *float64) (string, []interface{}) {
|
||||
if upper == nil {
|
||||
u := 0.0
|
||||
upper = &u
|
||||
}
|
||||
|
||||
args := []interface{}{value, *upper}
|
||||
return getNumericWhereClause(column, modifier, args)
|
||||
}
|
||||
|
||||
func getNumericWhereClause(column string, modifier models.CriterionModifier, args []interface{}) (string, []interface{}) {
|
||||
singleArgs := args[0:1]
|
||||
|
||||
switch modifier {
|
||||
case models.CriterionModifierIsNull:
|
||||
@@ -184,20 +218,20 @@ func getIntWhereClause(column string, modifier models.CriterionModifier, value i
|
||||
case models.CriterionModifierNotNull:
|
||||
return fmt.Sprintf("%s IS NOT NULL", column), nil
|
||||
case models.CriterionModifierEquals:
|
||||
return fmt.Sprintf("%s = ?", column), args
|
||||
return fmt.Sprintf("%s = ?", column), singleArgs
|
||||
case models.CriterionModifierNotEquals:
|
||||
return fmt.Sprintf("%s != ?", column), args
|
||||
return fmt.Sprintf("%s != ?", column), singleArgs
|
||||
case models.CriterionModifierBetween:
|
||||
return fmt.Sprintf("%s BETWEEN ? AND ?", column), betweenArgs
|
||||
return fmt.Sprintf("%s BETWEEN ? AND ?", column), args
|
||||
case models.CriterionModifierNotBetween:
|
||||
return fmt.Sprintf("%s NOT BETWEEN ? AND ?", column), betweenArgs
|
||||
return fmt.Sprintf("%s NOT BETWEEN ? AND ?", column), args
|
||||
case models.CriterionModifierLessThan:
|
||||
return fmt.Sprintf("%s < ?", column), args
|
||||
return fmt.Sprintf("%s < ?", column), singleArgs
|
||||
case models.CriterionModifierGreaterThan:
|
||||
return fmt.Sprintf("%s > ?", column), args
|
||||
return fmt.Sprintf("%s > ?", column), singleArgs
|
||||
}
|
||||
|
||||
panic("unsupported int modifier type " + modifier)
|
||||
panic("unsupported numeric modifier type " + modifier)
|
||||
}
|
||||
|
||||
func getDateCriterionWhereClause(column string, input models.DateCriterionInput) (string, []interface{}) {
|
||||
|
||||
@@ -24,6 +24,15 @@ func nullIntPtr(i null.Int) *int {
|
||||
return &v
|
||||
}
|
||||
|
||||
func nullFloatPtr(i null.Float) *float64 {
|
||||
if !i.Valid {
|
||||
return nil
|
||||
}
|
||||
|
||||
v := float64(i.Float64)
|
||||
return &v
|
||||
}
|
||||
|
||||
func nullIntFolderIDPtr(i null.Int) *file.FolderID {
|
||||
if !i.Valid {
|
||||
return nil
|
||||
|
||||
@@ -31,3 +31,13 @@ func StrFormat(format string, m StrFormatMap) string {
|
||||
|
||||
return strings.NewReplacer(args...).Replace(format)
|
||||
}
|
||||
|
||||
// StringerSliceToStringSlice converts a slice of fmt.Stringers to a slice of strings.
|
||||
func StringerSliceToStringSlice[V fmt.Stringer](v []V) []string {
|
||||
ret := make([]string, len(v))
|
||||
for i, vv := range v {
|
||||
ret[i] = vv.String()
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ import { StashIDFilter } from "./Filters/StashIDFilter";
|
||||
import { RatingCriterion } from "../../models/list-filter/criteria/rating";
|
||||
import { RatingFilter } from "./Filters/RatingFilter";
|
||||
import { BooleanFilter } from "./Filters/BooleanFilter";
|
||||
import { OptionsListFilter } from "./Filters/OptionsListFilter";
|
||||
import { OptionFilter, OptionListFilter } from "./Filters/OptionFilter";
|
||||
import { PathFilter } from "./Filters/PathFilter";
|
||||
import { PhashCriterion } from "src/models/list-filter/criteria/phash";
|
||||
import { PhashFilter } from "./Filters/PhashFilter";
|
||||
@@ -132,18 +132,17 @@ const GenericCriterionEditor: React.FC<IGenericCriterionEditor> = ({
|
||||
!criterionIsNumberValue(criterion.value) &&
|
||||
!criterionIsStashIDValue(criterion.value) &&
|
||||
!criterionIsDateValue(criterion.value) &&
|
||||
!criterionIsTimestampValue(criterion.value) &&
|
||||
!Array.isArray(criterion.value)
|
||||
!criterionIsTimestampValue(criterion.value)
|
||||
) {
|
||||
// if (!modifierOptions || modifierOptions.length === 0) {
|
||||
return (
|
||||
<OptionsListFilter criterion={criterion} setCriterion={setCriterion} />
|
||||
);
|
||||
// }
|
||||
|
||||
// return (
|
||||
// <OptionsFilter criterion={criterion} onValueChanged={onValueChanged} />
|
||||
// );
|
||||
if (!Array.isArray(criterion.value)) {
|
||||
return (
|
||||
<OptionFilter criterion={criterion} setCriterion={setCriterion} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<OptionListFilter criterion={criterion} setCriterion={setCriterion} />
|
||||
);
|
||||
}
|
||||
}
|
||||
if (criterion.criterionOption instanceof PathCriterionOption) {
|
||||
return (
|
||||
|
||||
@@ -30,14 +30,14 @@ export const BooleanFilter: React.FC<IBooleanFilter> = ({
|
||||
id={`${criterion.getId()}-true`}
|
||||
onChange={() => onSelect(true)}
|
||||
checked={criterion.value === "true"}
|
||||
type="checkbox"
|
||||
type="radio"
|
||||
label={<FormattedMessage id="true" />}
|
||||
/>
|
||||
<Form.Check
|
||||
id={`${criterion.getId()}-false`}
|
||||
onChange={() => onSelect(false)}
|
||||
checked={criterion.value === "false"}
|
||||
type="checkbox"
|
||||
type="radio"
|
||||
label={<FormattedMessage id="false" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
85
ui/v2.5/src/components/List/Filters/OptionFilter.tsx
Normal file
85
ui/v2.5/src/components/List/Filters/OptionFilter.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import cloneDeep from "lodash-es/cloneDeep";
|
||||
import React from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import {
|
||||
CriterionValue,
|
||||
Criterion,
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
|
||||
interface IOptionsFilter {
|
||||
criterion: Criterion<CriterionValue>;
|
||||
setCriterion: (c: Criterion<CriterionValue>) => void;
|
||||
}
|
||||
|
||||
export const OptionFilter: React.FC<IOptionsFilter> = ({
|
||||
criterion,
|
||||
setCriterion,
|
||||
}) => {
|
||||
function onSelect(v: string) {
|
||||
const c = cloneDeep(criterion);
|
||||
if (c.value === v) {
|
||||
c.value = "";
|
||||
} else {
|
||||
c.value = v;
|
||||
}
|
||||
|
||||
setCriterion(c);
|
||||
}
|
||||
|
||||
const { options } = criterion.criterionOption;
|
||||
|
||||
return (
|
||||
<div className="option-list-filter">
|
||||
{options?.map((o) => (
|
||||
<Form.Check
|
||||
id={`${criterion.getId()}-${o.toString()}`}
|
||||
key={o.toString()}
|
||||
onChange={() => onSelect(o.toString())}
|
||||
checked={criterion.value === o.toString()}
|
||||
type="radio"
|
||||
label={o.toString()}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IOptionsListFilter {
|
||||
criterion: Criterion<CriterionValue>;
|
||||
setCriterion: (c: Criterion<CriterionValue>) => void;
|
||||
}
|
||||
|
||||
export const OptionListFilter: React.FC<IOptionsListFilter> = ({
|
||||
criterion,
|
||||
setCriterion,
|
||||
}) => {
|
||||
function onSelect(v: string) {
|
||||
const c = cloneDeep(criterion);
|
||||
const cv = c.value as string[];
|
||||
if (cv.includes(v)) {
|
||||
c.value = cv.filter((x) => x !== v);
|
||||
} else {
|
||||
c.value = [...cv, v];
|
||||
}
|
||||
|
||||
setCriterion(c);
|
||||
}
|
||||
|
||||
const { options } = criterion.criterionOption;
|
||||
const value = criterion.value as string[];
|
||||
|
||||
return (
|
||||
<div className="option-list-filter">
|
||||
{options?.map((o) => (
|
||||
<Form.Check
|
||||
id={`${criterion.getId()}-${o.toString()}`}
|
||||
key={o.toString()}
|
||||
onChange={() => onSelect(o.toString())}
|
||||
checked={value.includes(o.toString())}
|
||||
type="checkbox"
|
||||
label={o.toString()}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionValue,
|
||||
} from "../../../models/list-filter/criteria/criterion";
|
||||
|
||||
interface IOptionsFilterProps {
|
||||
criterion: Criterion<CriterionValue>;
|
||||
onValueChanged: (value: CriterionValue) => void;
|
||||
}
|
||||
|
||||
export const OptionsFilter: React.FC<IOptionsFilterProps> = ({
|
||||
criterion,
|
||||
onValueChanged,
|
||||
}) => {
|
||||
function onChanged(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
onValueChanged(event.target.value);
|
||||
}
|
||||
|
||||
const options = useMemo(() => {
|
||||
const ret = criterion.criterionOption.options?.slice() ?? [];
|
||||
|
||||
ret.unshift("");
|
||||
|
||||
return ret;
|
||||
}, [criterion.criterionOption.options]);
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="select"
|
||||
onChange={onChanged}
|
||||
value={criterion.value.toString()}
|
||||
className="btn-secondary"
|
||||
>
|
||||
{options.map((c) => (
|
||||
<option key={c.toString()} value={c.toString()}>
|
||||
{c ? c : "---"}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import cloneDeep from "lodash-es/cloneDeep";
|
||||
import React from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import {
|
||||
CriterionValue,
|
||||
Criterion,
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
|
||||
interface IOptionsListFilter {
|
||||
criterion: Criterion<CriterionValue>;
|
||||
setCriterion: (c: Criterion<CriterionValue>) => void;
|
||||
}
|
||||
|
||||
export const OptionsListFilter: React.FC<IOptionsListFilter> = ({
|
||||
criterion,
|
||||
setCriterion,
|
||||
}) => {
|
||||
function onSelect(v: string) {
|
||||
const c = cloneDeep(criterion);
|
||||
if (c.value === v) {
|
||||
c.value = "";
|
||||
} else {
|
||||
c.value = v;
|
||||
}
|
||||
|
||||
setCriterion(c);
|
||||
}
|
||||
|
||||
const { options } = criterion.criterionOption;
|
||||
|
||||
return (
|
||||
<div className="option-list-filter">
|
||||
{options?.map((o) => (
|
||||
<Form.Check
|
||||
id={`${criterion.getId()}-${o.toString()}`}
|
||||
key={o.toString()}
|
||||
onChange={() => onSelect(o.toString())}
|
||||
checked={criterion.value === o.toString()}
|
||||
type="checkbox"
|
||||
label={o.toString()}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -17,6 +17,11 @@ import {
|
||||
genderToString,
|
||||
stringToGender,
|
||||
} from "src/utils/gender";
|
||||
import {
|
||||
circumcisedStrings,
|
||||
circumcisedToString,
|
||||
stringToCircumcised,
|
||||
} from "src/utils/circumcised";
|
||||
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
|
||||
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
|
||||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -45,6 +50,8 @@ const performerFields = [
|
||||
// "weight",
|
||||
"measurements",
|
||||
"fake_tits",
|
||||
"penis_length",
|
||||
"circumcised",
|
||||
"hair_color",
|
||||
"tattoos",
|
||||
"piercings",
|
||||
@@ -64,10 +71,12 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
useState<GQL.BulkPerformerUpdateInput>({});
|
||||
// weight needs conversion to/from number
|
||||
const [weight, setWeight] = useState<string | undefined>();
|
||||
const [penis_length, setPenisLength] = useState<string | undefined>();
|
||||
const [updateInput, setUpdateInput] = useState<GQL.BulkPerformerUpdateInput>(
|
||||
{}
|
||||
);
|
||||
const genderOptions = [""].concat(genderStrings);
|
||||
const circumcisedOptions = [""].concat(circumcisedStrings);
|
||||
|
||||
const [updatePerformers] = useBulkPerformerUpdate(getPerformerInput());
|
||||
|
||||
@@ -100,11 +109,19 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
updateInput.gender,
|
||||
aggregateState.gender
|
||||
);
|
||||
performerInput.circumcised = getAggregateInputValue(
|
||||
updateInput.circumcised,
|
||||
aggregateState.circumcised
|
||||
);
|
||||
|
||||
if (weight !== undefined) {
|
||||
performerInput.weight = parseFloat(weight);
|
||||
}
|
||||
|
||||
if (penis_length !== undefined) {
|
||||
performerInput.penis_length = parseFloat(penis_length);
|
||||
}
|
||||
|
||||
return performerInput;
|
||||
}
|
||||
|
||||
@@ -135,6 +152,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
const state = props.selected;
|
||||
let updateTagIds: string[] = [];
|
||||
let updateWeight: string | undefined | null = undefined;
|
||||
let updatePenisLength: string | undefined | null = undefined;
|
||||
let first = true;
|
||||
|
||||
state.forEach((performer: GQL.SlimPerformerDataFragment) => {
|
||||
@@ -151,6 +169,16 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
: performer.weight;
|
||||
updateWeight = getAggregateState(updateWeight, thisWeight, first);
|
||||
|
||||
const thisPenisLength =
|
||||
performer.penis_length !== undefined && performer.penis_length !== null
|
||||
? performer.penis_length.toString()
|
||||
: performer.penis_length;
|
||||
updatePenisLength = getAggregateState(
|
||||
updatePenisLength,
|
||||
thisPenisLength,
|
||||
first
|
||||
);
|
||||
|
||||
first = false;
|
||||
});
|
||||
|
||||
@@ -270,6 +298,32 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
{renderTextField("measurements", updateInput.measurements, (v) =>
|
||||
setUpdateField({ measurements: v })
|
||||
)}
|
||||
{renderTextField("penis_length", penis_length, (v) =>
|
||||
setPenisLength(v)
|
||||
)}
|
||||
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="circumcised" />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
className="input-control"
|
||||
value={circumcisedToString(updateInput.circumcised)}
|
||||
onChange={(event) =>
|
||||
setUpdateField({
|
||||
circumcised: stringToCircumcised(event.currentTarget.value),
|
||||
})
|
||||
}
|
||||
>
|
||||
{circumcisedOptions.map((opt) => (
|
||||
<option value={opt} key={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
|
||||
{renderTextField("fake_tits", updateInput.fake_tits, (v) =>
|
||||
setUpdateField({ fake_tits: v })
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,7 @@ import TextUtils from "src/utils/text";
|
||||
import { getStashboxBase } from "src/utils/stashbox";
|
||||
import { getCountryByISO } from "src/utils/country";
|
||||
import { TextField, URLField } from "src/utils/field";
|
||||
import { cmToImperial, kgToLbs } from "src/utils/units";
|
||||
import { cmToImperial, cmToInches, kgToLbs } from "src/utils/units";
|
||||
|
||||
interface IPerformerDetails {
|
||||
performer: GQL.PerformerDataFragment;
|
||||
@@ -133,6 +133,49 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const formatPenisLength = (penis_length?: number | null) => {
|
||||
if (!penis_length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const inches = cmToInches(penis_length);
|
||||
|
||||
return (
|
||||
<span className="performer-penis-length">
|
||||
<span className="penis-length-metric">
|
||||
{intl.formatNumber(penis_length, {
|
||||
style: "unit",
|
||||
unit: "centimeter",
|
||||
unitDisplay: "short",
|
||||
maximumFractionDigits: 2,
|
||||
})}
|
||||
</span>
|
||||
<span className="penis-length-imperial">
|
||||
{intl.formatNumber(inches, {
|
||||
style: "unit",
|
||||
unit: "inch",
|
||||
unitDisplay: "narrow",
|
||||
maximumFractionDigits: 2,
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const formatCircumcised = (circumcised?: GQL.CircumisedEnum | null) => {
|
||||
if (!circumcised) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="penis-circumcised">
|
||||
{intl.formatMessage({
|
||||
id: "circumcised_types." + performer.circumcised,
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<dl className="details-list">
|
||||
<TextField
|
||||
@@ -179,6 +222,17 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{(performer.penis_length || performer.circumcised) && (
|
||||
<>
|
||||
<dt>
|
||||
<FormattedMessage id="penis" />:
|
||||
</dt>
|
||||
<dd>
|
||||
{formatPenisLength(performer.penis_length)}
|
||||
{formatCircumcised(performer.circumcised)}
|
||||
</dd>
|
||||
</>
|
||||
)}
|
||||
<TextField id="measurements" value={performer.measurements} />
|
||||
<TextField id="fake_tits" value={performer.fake_tits} />
|
||||
<TextField id="career_length" value={performer.career_length} />
|
||||
|
||||
@@ -31,6 +31,11 @@ import {
|
||||
stringGenderMap,
|
||||
stringToGender,
|
||||
} from "src/utils/gender";
|
||||
import {
|
||||
circumcisedToString,
|
||||
stringCircumMap,
|
||||
stringToCircumcised,
|
||||
} from "src/utils/circumcised";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { PerformerScrapeDialog } from "./PerformerScrapeDialog";
|
||||
import PerformerScrapeModal from "./PerformerScrapeModal";
|
||||
@@ -153,6 +158,8 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
weight: yup.number().nullable().defined().default(null),
|
||||
measurements: yup.string().ensure(),
|
||||
fake_tits: yup.string().ensure(),
|
||||
penis_length: yup.number().nullable().defined().default(null),
|
||||
circumcised: yup.string<GQL.CircumisedEnum | "">().ensure(),
|
||||
tattoos: yup.string().ensure(),
|
||||
piercings: yup.string().ensure(),
|
||||
career_length: yup.string().ensure(),
|
||||
@@ -181,6 +188,8 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
weight: performer.weight ?? null,
|
||||
measurements: performer.measurements ?? "",
|
||||
fake_tits: performer.fake_tits ?? "",
|
||||
penis_length: performer.penis_length ?? null,
|
||||
circumcised: (performer.circumcised as GQL.CircumisedEnum) ?? "",
|
||||
tattoos: performer.tattoos ?? "",
|
||||
piercings: performer.piercings ?? "",
|
||||
career_length: performer.career_length ?? "",
|
||||
@@ -219,6 +228,21 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
}
|
||||
}
|
||||
|
||||
function translateScrapedCircumcised(scrapedCircumcised?: string) {
|
||||
if (!scrapedCircumcised) {
|
||||
return;
|
||||
}
|
||||
|
||||
const upperCircumcised = scrapedCircumcised.toUpperCase();
|
||||
const asEnum = circumcisedToString(upperCircumcised);
|
||||
if (asEnum) {
|
||||
return stringToCircumcised(asEnum);
|
||||
} else {
|
||||
const caseInsensitive = true;
|
||||
return stringToCircumcised(scrapedCircumcised, caseInsensitive);
|
||||
}
|
||||
}
|
||||
|
||||
function renderNewTags() {
|
||||
if (!newTags || newTags.length === 0) {
|
||||
return;
|
||||
@@ -355,6 +379,13 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
formik.setFieldValue("gender", newGender);
|
||||
}
|
||||
}
|
||||
if (state.circumcised) {
|
||||
// circumcised is a string in the scraper data
|
||||
const newCircumcised = translateScrapedCircumcised(state.circumcised);
|
||||
if (newCircumcised) {
|
||||
formik.setFieldValue("circumcised", newCircumcised);
|
||||
}
|
||||
}
|
||||
if (state.tags) {
|
||||
// map tags to their ids and filter out those not found
|
||||
const newTagIds = state.tags.map((t) => t.stored_id).filter((t) => t);
|
||||
@@ -387,6 +418,9 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
if (state.weight) {
|
||||
formik.setFieldValue("weight", state.weight);
|
||||
}
|
||||
if (state.penis_length) {
|
||||
formik.setFieldValue("penis_length", state.penis_length);
|
||||
}
|
||||
|
||||
const remoteSiteID = state.remote_site_id;
|
||||
if (remoteSiteID && (scraper as IStashBox).endpoint) {
|
||||
@@ -431,6 +465,8 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
gender: input.gender || null,
|
||||
height_cm: input.height_cm || null,
|
||||
weight: input.weight || null,
|
||||
penis_length: input.penis_length || null,
|
||||
circumcised: input.circumcised || null,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -446,6 +482,8 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
gender: input.gender || null,
|
||||
height_cm: input.height_cm || null,
|
||||
weight: input.weight || null,
|
||||
penis_length: input.penis_length || null,
|
||||
circumcised: input.circumcised || null,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -663,6 +701,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
const currentPerformer = {
|
||||
...formik.values,
|
||||
gender: formik.values.gender || null,
|
||||
circumcised: formik.values.circumcised || null,
|
||||
image: formik.values.image ?? performer.image_path,
|
||||
};
|
||||
|
||||
@@ -990,6 +1029,31 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
type: "number",
|
||||
messageID: "weight_kg",
|
||||
})}
|
||||
{renderField("penis_length", {
|
||||
type: "number",
|
||||
messageID: "penis_length_cm",
|
||||
})}
|
||||
|
||||
<Form.Group as={Row}>
|
||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||
<FormattedMessage id="circumcised" />
|
||||
</Form.Label>
|
||||
<Col xs="auto">
|
||||
<Form.Control
|
||||
as="select"
|
||||
className="input-control"
|
||||
{...formik.getFieldProps("circumcised")}
|
||||
>
|
||||
<option value="" key=""></option>
|
||||
{Array.from(stringCircumMap.entries()).map(([name, value]) => (
|
||||
<option value={value} key={value}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
{renderField("measurements")}
|
||||
{renderField("fake_tits")}
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@ import {
|
||||
genderToString,
|
||||
stringToGender,
|
||||
} from "src/utils/gender";
|
||||
import {
|
||||
circumcisedStrings,
|
||||
circumcisedToString,
|
||||
stringToCircumcised,
|
||||
} from "src/utils/circumcised";
|
||||
import { IStashBox } from "./PerformerStashBoxModal";
|
||||
|
||||
function renderScrapedGender(
|
||||
@@ -120,6 +125,55 @@ function renderScrapedTagsRow(
|
||||
);
|
||||
}
|
||||
|
||||
function renderScrapedCircumcised(
|
||||
result: ScrapeResult<string>,
|
||||
isNew?: boolean,
|
||||
onChange?: (value: string) => void
|
||||
) {
|
||||
const selectOptions = [""].concat(circumcisedStrings);
|
||||
|
||||
return (
|
||||
<Form.Control
|
||||
as="select"
|
||||
className="input-control"
|
||||
disabled={!isNew}
|
||||
plaintext={!isNew}
|
||||
value={isNew ? result.newValue : result.originalValue}
|
||||
onChange={(e) => {
|
||||
if (isNew && onChange) {
|
||||
onChange(e.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectOptions.map((opt) => (
|
||||
<option value={opt} key={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
);
|
||||
}
|
||||
|
||||
function renderScrapedCircumcisedRow(
|
||||
title: string,
|
||||
result: ScrapeResult<string>,
|
||||
onChange: (value: ScrapeResult<string>) => void
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedCircumcised(result)}
|
||||
renderNewField={() =>
|
||||
renderScrapedCircumcised(result, true, (value) =>
|
||||
onChange(result.cloneWithValue(value))
|
||||
)
|
||||
}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface IPerformerScrapeDialogProps {
|
||||
performer: Partial<GQL.PerformerUpdateInput>;
|
||||
scraped: GQL.ScrapedPerformer;
|
||||
@@ -165,6 +219,27 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||
return genderToString(retEnum);
|
||||
}
|
||||
|
||||
function translateScrapedCircumcised(scrapedCircumcised?: string | null) {
|
||||
if (!scrapedCircumcised) {
|
||||
return;
|
||||
}
|
||||
|
||||
let retEnum: GQL.CircumisedEnum | undefined;
|
||||
|
||||
// try to translate from enum values first
|
||||
const upperCircumcised = scrapedCircumcised.toUpperCase();
|
||||
const asEnum = circumcisedToString(upperCircumcised);
|
||||
if (asEnum) {
|
||||
retEnum = stringToCircumcised(asEnum);
|
||||
} else {
|
||||
// try to match against circumcised strings
|
||||
const caseInsensitive = true;
|
||||
retEnum = stringToCircumcised(scrapedCircumcised, caseInsensitive);
|
||||
}
|
||||
|
||||
return circumcisedToString(retEnum);
|
||||
}
|
||||
|
||||
const [name, setName] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.name, props.scraped.name)
|
||||
);
|
||||
@@ -216,6 +291,12 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||
props.scraped.weight
|
||||
)
|
||||
);
|
||||
const [penisLength, setPenisLength] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(
|
||||
props.performer.penis_length?.toString(),
|
||||
props.scraped.penis_length
|
||||
)
|
||||
);
|
||||
const [measurements, setMeasurements] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(
|
||||
props.performer.measurements,
|
||||
@@ -252,6 +333,12 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||
translateScrapedGender(props.scraped.gender)
|
||||
)
|
||||
);
|
||||
const [circumcised, setCircumcised] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(
|
||||
circumcisedToString(props.performer.circumcised),
|
||||
translateScrapedCircumcised(props.scraped.circumcised)
|
||||
)
|
||||
);
|
||||
const [details, setDetails] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.details, props.scraped.details)
|
||||
);
|
||||
@@ -338,6 +425,8 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||
height,
|
||||
measurements,
|
||||
fakeTits,
|
||||
penisLength,
|
||||
circumcised,
|
||||
careerLength,
|
||||
tattoos,
|
||||
piercings,
|
||||
@@ -426,6 +515,8 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||
death_date: deathDate.getNewValue(),
|
||||
hair_color: hairColor.getNewValue(),
|
||||
weight: weight.getNewValue(),
|
||||
penis_length: penisLength.getNewValue(),
|
||||
circumcised: circumcised.getNewValue(),
|
||||
remote_site_id: remoteSiteID.getNewValue(),
|
||||
};
|
||||
}
|
||||
@@ -493,6 +584,16 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||
result={height}
|
||||
onChange={(value) => setHeight(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title={intl.formatMessage({ id: "penis_length" })}
|
||||
result={penisLength}
|
||||
onChange={(value) => setPenisLength(value)}
|
||||
/>
|
||||
{renderScrapedCircumcisedRow(
|
||||
intl.formatMessage({ id: "circumcised" }),
|
||||
circumcised,
|
||||
(value) => setCircumcised(value)
|
||||
)}
|
||||
<ScrapedInputGroupRow
|
||||
title={intl.formatMessage({ id: "measurements" })}
|
||||
result={measurements}
|
||||
|
||||
@@ -190,27 +190,21 @@
|
||||
color: #c8a2c8;
|
||||
}
|
||||
|
||||
.performer-height {
|
||||
.height-imperial {
|
||||
&::before {
|
||||
content: " (";
|
||||
}
|
||||
.performer-height .height-imperial,
|
||||
.performer-weight .weight-imperial,
|
||||
.performer-penis-length .penis-length-imperial {
|
||||
&::before {
|
||||
content: " (";
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: ")";
|
||||
}
|
||||
&::after {
|
||||
content: ")";
|
||||
}
|
||||
}
|
||||
|
||||
.performer-weight {
|
||||
.weight-imperial {
|
||||
&::before {
|
||||
content: " (";
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: ")";
|
||||
}
|
||||
.penis-circumcised {
|
||||
&::before {
|
||||
content: " ";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getOperationName,
|
||||
} from "@apollo/client/utilities";
|
||||
import { stringToGender } from "src/utils/gender";
|
||||
import { stringToCircumcised } from "src/utils/circumcised";
|
||||
import { filterData } from "../utils/data";
|
||||
import { ListFilterModel } from "../models/list-filter/filter";
|
||||
import * as GQL from "./generated-graphql";
|
||||
@@ -1314,6 +1315,10 @@ export const makePerformerCreateInput = (toCreate: GQL.ScrapedPerformer) => {
|
||||
death_date: toCreate.death_date,
|
||||
hair_color: toCreate.hair_color,
|
||||
weight: toCreate.weight ? Number(toCreate.weight) : undefined,
|
||||
penis_length: toCreate.penis_length
|
||||
? Number(toCreate.penis_length)
|
||||
: undefined,
|
||||
circumcised: stringToCircumcised(toCreate.circumcised),
|
||||
};
|
||||
return input;
|
||||
};
|
||||
|
||||
@@ -141,6 +141,11 @@
|
||||
"captions": "Captions",
|
||||
"career_length": "Career Length",
|
||||
"chapters": "Chapters",
|
||||
"circumcised": "Circumcised",
|
||||
"circumcised_types": {
|
||||
"UNCUT": "Uncut",
|
||||
"CUT": "Cut"
|
||||
},
|
||||
"component_tagger": {
|
||||
"config": {
|
||||
"active_instance": "Active stash-box instance:",
|
||||
@@ -1016,6 +1021,9 @@
|
||||
"parent_tags": "Parent Tags",
|
||||
"part_of": "Part of {parent}",
|
||||
"path": "Path",
|
||||
"penis": "Penis",
|
||||
"penis_length": "Penis Length",
|
||||
"penis_length_cm": "Penis Length (cm)",
|
||||
"perceptual_similarity": "Perceptual Similarity (phash)",
|
||||
"performer": "Performer",
|
||||
"performerTags": "Performer Tags",
|
||||
|
||||
36
ui/v2.5/src/models/list-filter/criteria/circumcised.ts
Normal file
36
ui/v2.5/src/models/list-filter/criteria/circumcised.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
CircumcisionCriterionInput,
|
||||
CircumisedEnum,
|
||||
CriterionModifier,
|
||||
} from "src/core/generated-graphql";
|
||||
import { circumcisedStrings, stringToCircumcised } from "src/utils/circumcised";
|
||||
import { CriterionOption, MultiStringCriterion } from "./criterion";
|
||||
|
||||
export const CircumcisedCriterionOption = new CriterionOption({
|
||||
messageID: "circumcised",
|
||||
type: "circumcised",
|
||||
options: circumcisedStrings,
|
||||
modifierOptions: [
|
||||
CriterionModifier.Includes,
|
||||
CriterionModifier.Excludes,
|
||||
CriterionModifier.IsNull,
|
||||
CriterionModifier.NotNull,
|
||||
],
|
||||
});
|
||||
|
||||
export class CircumcisedCriterion extends MultiStringCriterion {
|
||||
constructor() {
|
||||
super(CircumcisedCriterionOption);
|
||||
}
|
||||
|
||||
protected toCriterionInput(): CircumcisionCriterionInput {
|
||||
const value = this.value.map((v) =>
|
||||
stringToCircumcised(v)
|
||||
) as CircumisedEnum[];
|
||||
|
||||
return {
|
||||
value,
|
||||
modifier: this.modifier,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
export type Option = string | number | IOptionType;
|
||||
export type CriterionValue =
|
||||
| string
|
||||
| string[]
|
||||
| ILabeledId[]
|
||||
| IHierarchicalLabelValue
|
||||
| INumberValue
|
||||
@@ -243,6 +244,24 @@ export class StringCriterion extends Criterion<string> {
|
||||
}
|
||||
}
|
||||
|
||||
export class MultiStringCriterion extends Criterion<string[]> {
|
||||
constructor(type: CriterionOption) {
|
||||
super(type, []);
|
||||
}
|
||||
|
||||
public getLabelValue(_intl: IntlShape) {
|
||||
return this.value.join(", ");
|
||||
}
|
||||
|
||||
public isValid(): boolean {
|
||||
return (
|
||||
this.modifier === CriterionModifier.IsNull ||
|
||||
this.modifier === CriterionModifier.NotNull ||
|
||||
this.value.length > 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class MandatoryStringCriterionOption extends CriterionOption {
|
||||
constructor(
|
||||
messageID: string,
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
TagsCriterionOption,
|
||||
} from "./tags";
|
||||
import { GenderCriterion } from "./gender";
|
||||
import { CircumcisedCriterion } from "./circumcised";
|
||||
import { MoviesCriterionOption } from "./movies";
|
||||
import { GalleriesCriterion } from "./galleries";
|
||||
import { CriterionType } from "../types";
|
||||
@@ -155,12 +156,16 @@ export function makeCriteria(
|
||||
case "death_year":
|
||||
case "weight":
|
||||
return new NumberCriterion(new NumberCriterionOption(type, type));
|
||||
case "penis_length":
|
||||
return new NumberCriterion(new NumberCriterionOption(type, type));
|
||||
case "age":
|
||||
return new NumberCriterion(
|
||||
new MandatoryNumberCriterionOption(type, type)
|
||||
);
|
||||
case "gender":
|
||||
return new GenderCriterion();
|
||||
case "circumcised":
|
||||
return new CircumcisedCriterion();
|
||||
case "sceneChecksum":
|
||||
case "galleryChecksum":
|
||||
return new StringCriterion(
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "./criteria/criterion";
|
||||
import { FavoriteCriterionOption } from "./criteria/favorite";
|
||||
import { GenderCriterionOption } from "./criteria/gender";
|
||||
import { CircumcisedCriterionOption } from "./criteria/circumcised";
|
||||
import { PerformerIsMissingCriterionOption } from "./criteria/is-missing";
|
||||
import { StashIDCriterionOption } from "./criteria/stash-ids";
|
||||
import { StudiosCriterionOption } from "./criteria/studios";
|
||||
@@ -25,6 +26,7 @@ const sortByOptions = [
|
||||
"tag_count",
|
||||
"random",
|
||||
"rating",
|
||||
"penis_length",
|
||||
]
|
||||
.map(ListFilterOptions.createSortBy)
|
||||
.concat([
|
||||
@@ -57,6 +59,7 @@ const numberCriteria: CriterionType[] = [
|
||||
"death_year",
|
||||
"age",
|
||||
"weight",
|
||||
"penis_length",
|
||||
];
|
||||
|
||||
const stringCriteria: CriterionType[] = [
|
||||
@@ -78,6 +81,7 @@ const stringCriteria: CriterionType[] = [
|
||||
const criterionOptions = [
|
||||
FavoriteCriterionOption,
|
||||
GenderCriterionOption,
|
||||
CircumcisedCriterionOption,
|
||||
PerformerIsMissingCriterionOption,
|
||||
TagsCriterionOption,
|
||||
StudiosCriterionOption,
|
||||
|
||||
@@ -134,6 +134,8 @@ export type CriterionType =
|
||||
| "weight"
|
||||
| "measurements"
|
||||
| "fake_tits"
|
||||
| "penis_length"
|
||||
| "circumcised"
|
||||
| "career_length"
|
||||
| "tattoos"
|
||||
| "piercings"
|
||||
|
||||
51
ui/v2.5/src/utils/circumcised.ts
Normal file
51
ui/v2.5/src/utils/circumcised.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import * as GQL from "../core/generated-graphql";
|
||||
|
||||
export const stringCircumMap = new Map<string, GQL.CircumisedEnum>([
|
||||
["Uncut", GQL.CircumisedEnum.Uncut],
|
||||
["Cut", GQL.CircumisedEnum.Cut],
|
||||
]);
|
||||
|
||||
export const circumcisedToString = (
|
||||
value?: GQL.CircumisedEnum | String | null
|
||||
) => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const foundEntry = Array.from(stringCircumMap.entries()).find((e) => {
|
||||
return e[1] === value;
|
||||
});
|
||||
|
||||
if (foundEntry) {
|
||||
return foundEntry[0];
|
||||
}
|
||||
};
|
||||
|
||||
export const stringToCircumcised = (
|
||||
value?: string | null,
|
||||
caseInsensitive?: boolean
|
||||
): GQL.CircumisedEnum | undefined => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const existing = Object.entries(GQL.CircumisedEnum).find(
|
||||
(e) => e[1] === value
|
||||
);
|
||||
if (existing) return existing[1];
|
||||
|
||||
const ret = stringCircumMap.get(value);
|
||||
if (ret || !caseInsensitive) {
|
||||
return ret;
|
||||
}
|
||||
const asUpper = value.toUpperCase();
|
||||
const foundEntry = Array.from(stringCircumMap.entries()).find((e) => {
|
||||
return e[0].toUpperCase() === asUpper;
|
||||
});
|
||||
|
||||
if (foundEntry) {
|
||||
return foundEntry[1];
|
||||
}
|
||||
};
|
||||
|
||||
export const circumcisedStrings = Array.from(stringCircumMap.keys());
|
||||
@@ -9,3 +9,9 @@ export function cmToImperial(cm: number) {
|
||||
export function kgToLbs(kg: number) {
|
||||
return Math.round(kg * 2.20462262185);
|
||||
}
|
||||
|
||||
export function cmToInches(cm: number) {
|
||||
const cmInInches = 0.393700787;
|
||||
const inches = cm * cmInInches;
|
||||
return inches;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user