mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
Stash rating system (#2830)
* add rating100 fields to represent rating range 1-100 * deprecate existing (1-5) rating fields * add half- and quarter-star options for rating system * add decimal rating system option Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -23,6 +23,12 @@ ui/v2.5/src/core/generated-*.tsx
|
||||
# Jetbrains
|
||||
####
|
||||
|
||||
|
||||
####
|
||||
# Visual Studio
|
||||
####
|
||||
/.vs
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
@@ -57,4 +63,4 @@ node_modules
|
||||
|
||||
/stash
|
||||
dist
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
@@ -4,7 +4,7 @@ fragment SlimGalleryData on Gallery {
|
||||
date
|
||||
url
|
||||
details
|
||||
rating
|
||||
rating100
|
||||
organized
|
||||
files {
|
||||
...GalleryFileData
|
||||
|
||||
@@ -6,7 +6,7 @@ fragment GalleryData on Gallery {
|
||||
date
|
||||
url
|
||||
details
|
||||
rating
|
||||
rating100
|
||||
organized
|
||||
|
||||
files {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
fragment SlimImageData on Image {
|
||||
id
|
||||
title
|
||||
rating
|
||||
rating100
|
||||
organized
|
||||
o_counter
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
fragment ImageData on Image {
|
||||
id
|
||||
title
|
||||
rating
|
||||
rating100
|
||||
organized
|
||||
o_counter
|
||||
created_at
|
||||
|
||||
@@ -2,4 +2,5 @@ fragment SlimMovieData on Movie {
|
||||
id
|
||||
name
|
||||
front_image_path
|
||||
rating100
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ fragment MovieData on Movie {
|
||||
aliases
|
||||
duration
|
||||
date
|
||||
rating
|
||||
rating100
|
||||
director
|
||||
|
||||
studio {
|
||||
|
||||
@@ -26,7 +26,7 @@ fragment SlimPerformerData on Performer {
|
||||
endpoint
|
||||
stash_id
|
||||
}
|
||||
rating
|
||||
rating100
|
||||
death_date
|
||||
weight
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ fragment PerformerData on Performer {
|
||||
stash_id
|
||||
endpoint
|
||||
}
|
||||
rating
|
||||
rating100
|
||||
details
|
||||
death_date
|
||||
hair_color
|
||||
|
||||
@@ -6,7 +6,7 @@ fragment SlimSceneData on Scene {
|
||||
director
|
||||
url
|
||||
date
|
||||
rating
|
||||
rating100
|
||||
o_counter
|
||||
organized
|
||||
interactive
|
||||
|
||||
@@ -6,7 +6,7 @@ fragment SceneData on Scene {
|
||||
director
|
||||
url
|
||||
date
|
||||
rating
|
||||
rating100
|
||||
o_counter
|
||||
organized
|
||||
interactive
|
||||
|
||||
@@ -10,6 +10,6 @@ fragment SlimStudioData on Studio {
|
||||
id
|
||||
}
|
||||
details
|
||||
rating
|
||||
rating100
|
||||
aliases
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ fragment StudioData on Studio {
|
||||
endpoint
|
||||
}
|
||||
details
|
||||
rating
|
||||
rating100
|
||||
aliases
|
||||
}
|
||||
|
||||
@@ -92,7 +92,9 @@ input PerformerFilterType {
|
||||
"""Filter by StashID"""
|
||||
stash_id: StringCriterionInput
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"""Filter by url"""
|
||||
url: StringCriterionInput
|
||||
"""Filter by hair color"""
|
||||
@@ -158,7 +160,9 @@ input SceneFilterType {
|
||||
"""Filter by file count"""
|
||||
file_count: IntCriterionInput
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"""Filter by organized"""
|
||||
organized: Boolean
|
||||
"""Filter by o-counter"""
|
||||
@@ -218,7 +222,9 @@ input MovieFilterType {
|
||||
"""Filter by duration (in seconds)"""
|
||||
duration: IntCriterionInput
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"""Filter to only include movies with this studio"""
|
||||
studios: HierarchicalMultiCriterionInput
|
||||
"""Filter to only include movies missing this property"""
|
||||
@@ -249,7 +255,9 @@ input StudioFilterType {
|
||||
"""Filter to only include studios missing this property"""
|
||||
is_missing: String
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"""Filter by scene count"""
|
||||
scene_count: IntCriterionInput
|
||||
"""Filter by image count"""
|
||||
@@ -288,7 +296,9 @@ input GalleryFilterType {
|
||||
"""Filter to include/exclude galleries that were created from zip"""
|
||||
is_zip: Boolean
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"""Filter by organized"""
|
||||
organized: Boolean
|
||||
"""Filter by average image resolution"""
|
||||
@@ -391,7 +401,9 @@ input ImageFilterType {
|
||||
"""Filter by file count"""
|
||||
file_count: IntCriterionInput
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"""Filter by organized"""
|
||||
organized: Boolean
|
||||
"""Filter by o-counter"""
|
||||
|
||||
@@ -7,7 +7,10 @@ type Gallery {
|
||||
url: String
|
||||
date: String
|
||||
details: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean!
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
@@ -32,7 +35,10 @@ input GalleryCreateInput {
|
||||
url: String
|
||||
date: String
|
||||
details: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
scene_ids: [ID!]
|
||||
studio_id: ID
|
||||
@@ -47,7 +53,10 @@ input GalleryUpdateInput {
|
||||
url: String
|
||||
date: String
|
||||
details: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
scene_ids: [ID!]
|
||||
studio_id: ID
|
||||
@@ -63,7 +72,10 @@ input BulkGalleryUpdateInput {
|
||||
url: String
|
||||
date: String
|
||||
details: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
scene_ids: BulkUpdateIds
|
||||
studio_id: ID
|
||||
|
||||
@@ -2,7 +2,10 @@ type Image {
|
||||
id: ID!
|
||||
checksum: String @deprecated(reason: "Use files.fingerprints")
|
||||
title: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
o_counter: Int
|
||||
organized: Boolean!
|
||||
path: String! @deprecated(reason: "Use files.path")
|
||||
@@ -37,7 +40,10 @@ input ImageUpdateInput {
|
||||
clientMutationId: String
|
||||
id: ID!
|
||||
title: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
|
||||
studio_id: ID
|
||||
@@ -52,7 +58,10 @@ input BulkImageUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
title: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
|
||||
studio_id: ID
|
||||
|
||||
@@ -6,7 +6,10 @@ type Movie {
|
||||
"""Duration in seconds"""
|
||||
duration: Int
|
||||
date: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio: Studio
|
||||
director: String
|
||||
synopsis: String
|
||||
@@ -26,7 +29,10 @@ input MovieCreateInput {
|
||||
"""Duration in seconds"""
|
||||
duration: Int
|
||||
date: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
synopsis: String
|
||||
@@ -43,7 +49,10 @@ input MovieUpdateInput {
|
||||
aliases: String
|
||||
duration: Int
|
||||
date: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
synopsis: String
|
||||
@@ -57,7 +66,10 @@ input MovieUpdateInput {
|
||||
input BulkMovieUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
}
|
||||
|
||||
@@ -37,7 +37,10 @@ type Performer {
|
||||
gallery_count: Int # Resolver
|
||||
scenes: [Scene!]!
|
||||
stash_ids: [StashID!]!
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
@@ -72,7 +75,10 @@ input PerformerCreateInput {
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
@@ -105,7 +111,10 @@ input PerformerUpdateInput {
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
@@ -135,7 +144,10 @@ input BulkPerformerUpdateInput {
|
||||
instagram: String
|
||||
favorite: Boolean
|
||||
tag_ids: BulkUpdateIds
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
|
||||
@@ -41,7 +41,10 @@ type Scene {
|
||||
director: String
|
||||
url: String
|
||||
date: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean!
|
||||
o_counter: Int
|
||||
path: String! @deprecated(reason: "Use files.path")
|
||||
@@ -106,7 +109,10 @@ input SceneUpdateInput {
|
||||
director: String
|
||||
url: String
|
||||
date: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
o_counter: Int
|
||||
organized: Boolean
|
||||
studio_id: ID
|
||||
@@ -141,7 +147,10 @@ input BulkSceneUpdateInput {
|
||||
director: String
|
||||
url: String
|
||||
date: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
studio_id: ID
|
||||
gallery_ids: BulkUpdateIds
|
||||
@@ -191,7 +200,10 @@ type SceneParserResult {
|
||||
director: String
|
||||
url: String
|
||||
date: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
gallery_ids: [ID!]
|
||||
performer_ids: [ID!]
|
||||
|
||||
@@ -13,7 +13,10 @@ type Studio {
|
||||
image_count: Int # Resolver
|
||||
gallery_count: Int # Resolver
|
||||
stash_ids: [StashID!]!
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
@@ -28,7 +31,10 @@ input StudioCreateInput {
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
aliases: [String!]
|
||||
ignore_auto_tag: Boolean
|
||||
@@ -42,7 +48,10 @@ input StudioUpdateInput {
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
aliases: [String!]
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
@@ -189,6 +189,36 @@ func (t changesetTranslator) nullInt64(value *int, field string) *sql.NullInt64
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t changesetTranslator) ratingConversion(legacyValue *int, rating100Value *int) *sql.NullInt64 {
|
||||
const (
|
||||
legacyField = "rating"
|
||||
rating100Field = "rating100"
|
||||
)
|
||||
|
||||
legacyRating := t.nullInt64(legacyValue, legacyField)
|
||||
if legacyRating != nil {
|
||||
if legacyRating.Valid {
|
||||
legacyRating.Int64 = int64(models.Rating5To100(int(legacyRating.Int64)))
|
||||
}
|
||||
return legacyRating
|
||||
}
|
||||
return t.nullInt64(rating100Value, rating100Field)
|
||||
}
|
||||
|
||||
func (t changesetTranslator) ratingConversionOptional(legacyValue *int, rating100Value *int) models.OptionalInt {
|
||||
const (
|
||||
legacyField = "rating"
|
||||
rating100Field = "rating100"
|
||||
)
|
||||
|
||||
legacyRating := t.optionalInt(legacyValue, legacyField)
|
||||
if legacyRating.Set && !(legacyRating.Null) {
|
||||
legacyRating.Value = int(models.Rating5To100(int(legacyRating.Value)))
|
||||
return legacyRating
|
||||
}
|
||||
return t.optionalInt(rating100Value, rating100Field)
|
||||
}
|
||||
|
||||
func (t changesetTranslator) optionalInt(value *int, field string) models.OptionalInt {
|
||||
if !t.hasField(field) {
|
||||
return models.OptionalInt{}
|
||||
|
||||
@@ -189,6 +189,18 @@ func (r *galleryResolver) Checksum(ctx context.Context, obj *models.Gallery) (st
|
||||
return obj.PrimaryChecksum(), nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Rating(ctx context.Context, obj *models.Gallery) (*int, error) {
|
||||
if obj.Rating != nil {
|
||||
rating := models.Rating100To5(*obj.Rating)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Rating100(ctx context.Context, obj *models.Gallery) (*int, error) {
|
||||
return obj.Rating, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Scenes(ctx context.Context, obj *models.Gallery) (ret []*models.Scene, err error) {
|
||||
if !obj.SceneIDs.Loaded() {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
|
||||
@@ -144,6 +144,18 @@ func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) (ret [
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *imageResolver) Rating(ctx context.Context, obj *models.Image) (*int, error) {
|
||||
if obj.Rating != nil {
|
||||
rating := models.Rating100To5(*obj.Rating)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Rating100(ctx context.Context, obj *models.Image) (*int, error) {
|
||||
return obj.Rating, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (ret *models.Studio, err error) {
|
||||
if obj.StudioID == nil {
|
||||
return nil, nil
|
||||
|
||||
@@ -48,6 +48,14 @@ func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, e
|
||||
}
|
||||
|
||||
func (r *movieResolver) Rating(ctx context.Context, obj *models.Movie) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := models.Rating100To5(int(obj.Rating.Int64))
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Rating100(ctx context.Context, obj *models.Movie) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := int(obj.Rating.Int64)
|
||||
return &rating, nil
|
||||
|
||||
@@ -107,6 +107,18 @@ func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer)
|
||||
return stashIDsSliceToPtrSlice(ret), nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Rating(ctx context.Context, obj *models.Performer) (*int, error) {
|
||||
if obj.Rating != nil {
|
||||
rating := models.Rating100To5(*obj.Rating)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Rating100(ctx context.Context, obj *models.Performer) (*int, error) {
|
||||
return obj.Rating, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) DeathDate(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.DeathDate != nil {
|
||||
ret := obj.DeathDate.String()
|
||||
|
||||
@@ -141,6 +141,18 @@ func (r *sceneResolver) Files(ctx context.Context, obj *models.Scene) ([]*VideoF
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Rating(ctx context.Context, obj *models.Scene) (*int, error) {
|
||||
if obj.Rating != nil {
|
||||
rating := models.Rating100To5(*obj.Rating)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Rating100(ctx context.Context, obj *models.Scene) (*int, error) {
|
||||
return obj.Rating, nil
|
||||
}
|
||||
|
||||
func resolveFingerprints(f *file.BaseFile) []*Fingerprint {
|
||||
ret := make([]*Fingerprint, len(f.Fingerprints))
|
||||
|
||||
|
||||
@@ -126,6 +126,14 @@ func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) ([]*m
|
||||
}
|
||||
|
||||
func (r *studioResolver) Rating(ctx context.Context, obj *models.Studio) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := models.Rating100To5(int(obj.Rating.Int64))
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Rating100(ctx context.Context, obj *models.Studio) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := int(obj.Rating.Int64)
|
||||
return &rating, nil
|
||||
|
||||
@@ -68,7 +68,13 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
|
||||
d := models.NewDate(*input.Date)
|
||||
newGallery.Date = &d
|
||||
}
|
||||
newGallery.Rating = input.Rating
|
||||
|
||||
if input.Rating100 != nil {
|
||||
newGallery.Rating = input.Rating100
|
||||
} else if input.Rating != nil {
|
||||
rating := models.Rating5To100(*input.Rating)
|
||||
newGallery.Rating = &rating
|
||||
}
|
||||
|
||||
if input.StudioID != nil {
|
||||
studioID, _ := strconv.Atoi(*input.StudioID)
|
||||
@@ -187,7 +193,7 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
|
||||
updatedGallery.Details = translator.optionalString(input.Details, "details")
|
||||
updatedGallery.URL = translator.optionalString(input.URL, "url")
|
||||
updatedGallery.Date = translator.optionalDate(input.Date, "date")
|
||||
updatedGallery.Rating = translator.optionalInt(input.Rating, "rating")
|
||||
updatedGallery.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
@@ -262,8 +268,7 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall
|
||||
updatedGallery.Details = translator.optionalString(input.Details, "details")
|
||||
updatedGallery.URL = translator.optionalString(input.URL, "url")
|
||||
updatedGallery.Date = translator.optionalDate(input.Date, "date")
|
||||
updatedGallery.Rating = translator.optionalInt(input.Rating, "rating")
|
||||
|
||||
updatedGallery.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
var err error
|
||||
updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
|
||||
@@ -103,7 +103,7 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp
|
||||
|
||||
updatedImage := models.NewImagePartial()
|
||||
updatedImage.Title = translator.optionalString(input.Title, "title")
|
||||
updatedImage.Rating = translator.optionalInt(input.Rating, "rating")
|
||||
updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
@@ -189,7 +189,7 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
|
||||
}
|
||||
|
||||
updatedImage.Title = translator.optionalString(input.Title, "title")
|
||||
updatedImage.Rating = translator.optionalInt(input.Rating, "rating")
|
||||
updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
|
||||
@@ -76,9 +76,11 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
|
||||
newMovie.Date = models.SQLiteDate{String: *input.Date, Valid: true}
|
||||
}
|
||||
|
||||
if input.Rating != nil {
|
||||
rating := int64(*input.Rating)
|
||||
newMovie.Rating = sql.NullInt64{Int64: rating, Valid: true}
|
||||
if input.Rating100 != nil {
|
||||
newMovie.Rating = sql.NullInt64{Int64: int64(*input.Rating100), Valid: true}
|
||||
} else if input.Rating != nil {
|
||||
rating := models.Rating5To100(*input.Rating)
|
||||
newMovie.Rating = sql.NullInt64{Int64: int64(rating), Valid: true}
|
||||
}
|
||||
|
||||
if input.StudioID != nil {
|
||||
@@ -166,7 +168,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
|
||||
updatedMovie.Aliases = translator.nullString(input.Aliases, "aliases")
|
||||
updatedMovie.Duration = translator.nullInt64(input.Duration, "duration")
|
||||
updatedMovie.Date = translator.sqliteDate(input.Date, "date")
|
||||
updatedMovie.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedMovie.Rating = translator.ratingConversion(input.Rating, input.Rating100)
|
||||
updatedMovie.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||
updatedMovie.Director = translator.nullString(input.Director, "director")
|
||||
updatedMovie.Synopsis = translator.nullString(input.Synopsis, "synopsis")
|
||||
@@ -239,7 +241,7 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
|
||||
updatedMovie.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedMovie.Rating = translator.ratingConversion(input.Rating, input.Rating100)
|
||||
updatedMovie.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||
updatedMovie.Director = translator.nullString(input.Director, "director")
|
||||
|
||||
|
||||
@@ -114,8 +114,11 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC
|
||||
if input.Favorite != nil {
|
||||
newPerformer.Favorite = *input.Favorite
|
||||
}
|
||||
if input.Rating != nil {
|
||||
newPerformer.Rating = input.Rating
|
||||
if input.Rating100 != nil {
|
||||
newPerformer.Rating = input.Rating100
|
||||
} else if input.Rating != nil {
|
||||
rating := models.Rating5To100(*input.Rating)
|
||||
newPerformer.Rating = &rating
|
||||
}
|
||||
if input.Details != nil {
|
||||
newPerformer.Details = *input.Details
|
||||
@@ -239,7 +242,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU
|
||||
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
|
||||
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
|
||||
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||
updatedPerformer.Rating = translator.optionalInt(input.Rating, "rating")
|
||||
updatedPerformer.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedPerformer.Details = translator.optionalString(input.Details, "details")
|
||||
updatedPerformer.DeathDate = translator.optionalDate(input.DeathDate, "death_date")
|
||||
updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color")
|
||||
@@ -352,7 +355,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
|
||||
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
|
||||
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
|
||||
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||
updatedPerformer.Rating = translator.optionalInt(input.Rating, "rating")
|
||||
updatedPerformer.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedPerformer.Details = translator.optionalString(input.Details, "details")
|
||||
updatedPerformer.DeathDate = translator.optionalDate(input.DeathDate, "death_date")
|
||||
updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color")
|
||||
|
||||
@@ -172,7 +172,7 @@ func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTr
|
||||
updatedScene.Director = translator.optionalString(input.Director, "director")
|
||||
updatedScene.URL = translator.optionalString(input.URL, "url")
|
||||
updatedScene.Date = translator.optionalDate(input.Date, "date")
|
||||
updatedScene.Rating = translator.optionalInt(input.Rating, "rating")
|
||||
updatedScene.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedScene.OCounter = translator.optionalInt(input.OCounter, "o_counter")
|
||||
var err error
|
||||
updatedScene.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
@@ -348,7 +348,7 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU
|
||||
updatedScene.Director = translator.optionalString(input.Director, "director")
|
||||
updatedScene.URL = translator.optionalString(input.URL, "url")
|
||||
updatedScene.Date = translator.optionalDate(input.Date, "date")
|
||||
updatedScene.Rating = translator.optionalInt(input.Rating, "rating")
|
||||
updatedScene.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedScene.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
|
||||
@@ -58,11 +58,18 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input StudioCreateI
|
||||
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.Rating100 != nil {
|
||||
newStudio.Rating = sql.NullInt64{
|
||||
Int64: int64(*input.Rating100),
|
||||
Valid: true,
|
||||
}
|
||||
} else if input.Rating != nil {
|
||||
newStudio.Rating = sql.NullInt64{
|
||||
Int64: int64(models.Rating5To100(*input.Rating)),
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
if input.Details != nil {
|
||||
newStudio.Details = sql.NullString{String: *input.Details, Valid: true}
|
||||
}
|
||||
@@ -150,7 +157,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input StudioUpdateI
|
||||
updatedStudio.URL = translator.nullString(input.URL, "url")
|
||||
updatedStudio.Details = translator.nullString(input.Details, "details")
|
||||
updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id")
|
||||
updatedStudio.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedStudio.Rating = translator.ratingConversion(input.Rating, input.Rating100)
|
||||
updatedStudio.IgnoreAutoTag = input.IgnoreAutoTag
|
||||
|
||||
// Start the transaction and save the studio
|
||||
|
||||
@@ -32,15 +32,19 @@ func toSnakeCase(v string) string {
|
||||
|
||||
func fromSnakeCase(v string) string {
|
||||
var buf bytes.Buffer
|
||||
leadingUnderscore := true
|
||||
capvar := false
|
||||
for i, c := range v {
|
||||
switch {
|
||||
case c == '_' && i > 0:
|
||||
case c == '_' && !leadingUnderscore && i > 0:
|
||||
capvar = true
|
||||
case c == '_' && leadingUnderscore:
|
||||
buf.WriteRune(c)
|
||||
case capvar:
|
||||
buf.WriteRune(unicode.ToUpper(c))
|
||||
capvar = false
|
||||
default:
|
||||
leadingUnderscore = false
|
||||
buf.WriteRune(c)
|
||||
}
|
||||
}
|
||||
@@ -54,7 +58,13 @@ func toSnakeCaseMap(m map[string]interface{}) map[string]interface{} {
|
||||
|
||||
for key, val := range m {
|
||||
adjKey := toSnakeCase(key)
|
||||
nm[adjKey] = val
|
||||
|
||||
switch v := val.(type) {
|
||||
case map[string]interface{}:
|
||||
nm[adjKey] = toSnakeCaseMap(v)
|
||||
default:
|
||||
nm[adjKey] = val
|
||||
}
|
||||
}
|
||||
|
||||
return nm
|
||||
@@ -68,13 +78,15 @@ func convertMapValue(val interface{}) interface{} {
|
||||
case map[interface{}]interface{}:
|
||||
ret := cast.ToStringMap(v)
|
||||
for k, vv := range ret {
|
||||
ret[k] = convertMapValue(vv)
|
||||
adjKey := fromSnakeCase(k)
|
||||
ret[adjKey] = convertMapValue(vv)
|
||||
}
|
||||
return ret
|
||||
case map[string]interface{}:
|
||||
ret := make(map[string]interface{})
|
||||
for k, vv := range v {
|
||||
ret[k] = convertMapValue(vv)
|
||||
adjKey := fromSnakeCase(k)
|
||||
ret[adjKey] = convertMapValue(vv)
|
||||
}
|
||||
return ret
|
||||
case []interface{}:
|
||||
|
||||
@@ -32,6 +32,7 @@ type SceneParserResult struct {
|
||||
URL *string `json:"url"`
|
||||
Date *string `json:"date"`
|
||||
Rating *int `json:"rating"`
|
||||
Rating100 *int `json:"rating100"`
|
||||
StudioID *string `json:"studio_id"`
|
||||
GalleryIds []string `json:"gallery_ids"`
|
||||
PerformerIds []string `json:"performer_ids"`
|
||||
@@ -113,6 +114,7 @@ func initParserFields() {
|
||||
|
||||
ret["d"] = newParserField("d", `(?:\.|-|_)`, false)
|
||||
ret["rating"] = newParserField("rating", `\d`, true)
|
||||
ret["rating100"] = newParserField("rating100", `\d`, true)
|
||||
ret["performer"] = newParserField("performer", ".*", true)
|
||||
ret["studio"] = newParserField("studio", ".*", true)
|
||||
ret["movie"] = newParserField("movie", ".*", true)
|
||||
@@ -256,6 +258,10 @@ func validateRating(rating int) bool {
|
||||
return rating >= 1 && rating <= 5
|
||||
}
|
||||
|
||||
func validateRating100(rating100 int) bool {
|
||||
return rating100 >= 1 && rating100 <= 100
|
||||
}
|
||||
|
||||
func validateDate(dateStr string) bool {
|
||||
splits := strings.Split(dateStr, "-")
|
||||
if len(splits) != 3 {
|
||||
@@ -347,6 +353,13 @@ func (h *sceneHolder) setField(field parserField, value interface{}) {
|
||||
case "rating":
|
||||
rating, _ := strconv.Atoi(value.(string))
|
||||
if validateRating(rating) {
|
||||
// convert to 1-100 scale
|
||||
rating = models.Rating5To100(rating)
|
||||
h.result.Rating = &rating
|
||||
}
|
||||
case "rating100":
|
||||
rating, _ := strconv.Atoi(value.(string))
|
||||
if validateRating100(rating) {
|
||||
h.result.Rating = &rating
|
||||
}
|
||||
case "performer":
|
||||
|
||||
@@ -23,8 +23,10 @@ type GalleryFilterType struct {
|
||||
IsMissing *string `json:"is_missing"`
|
||||
// Filter to include/exclude galleries that were created from zip
|
||||
IsZip *bool `json:"is_zip"`
|
||||
// Filter by rating
|
||||
// Filter by rating expressed as 1-5
|
||||
Rating *IntCriterionInput `json:"rating"`
|
||||
// Filter by rating expressed as 1-100
|
||||
Rating100 *IntCriterionInput `json:"rating100"`
|
||||
// Filter by organized
|
||||
Organized *bool `json:"organized"`
|
||||
// Filter by average image resolution
|
||||
@@ -65,6 +67,7 @@ type GalleryUpdateInput struct {
|
||||
Date *string `json:"date"`
|
||||
Details *string `json:"details"`
|
||||
Rating *int `json:"rating"`
|
||||
Rating100 *int `json:"rating100"`
|
||||
Organized *bool `json:"organized"`
|
||||
SceneIds []string `json:"scene_ids"`
|
||||
StudioID *string `json:"studio_id"`
|
||||
|
||||
@@ -14,8 +14,10 @@ type ImageFilterType struct {
|
||||
Path *StringCriterionInput `json:"path"`
|
||||
// Filter by file count
|
||||
FileCount *IntCriterionInput `json:"file_count"`
|
||||
// Filter by rating
|
||||
// Filter by rating expressed as 1-5
|
||||
Rating *IntCriterionInput `json:"rating"`
|
||||
// Filter by rating expressed as 1-100
|
||||
Rating100 *IntCriterionInput `json:"rating100"`
|
||||
// Filter by organized
|
||||
Organized *bool `json:"organized"`
|
||||
// Filter by o-counter
|
||||
|
||||
@@ -12,13 +12,14 @@ import (
|
||||
type Gallery struct {
|
||||
ID int `json:"id"`
|
||||
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Date *Date `json:"date"`
|
||||
Details string `json:"details"`
|
||||
Rating *int `json:"rating"`
|
||||
Organized bool `json:"organized"`
|
||||
StudioID *int `json:"studio_id"`
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Date *Date `json:"date"`
|
||||
Details string `json:"details"`
|
||||
// Rating expressed in 1-100 scale
|
||||
Rating *int `json:"rating"`
|
||||
Organized bool `json:"organized"`
|
||||
StudioID *int `json:"studio_id"`
|
||||
|
||||
// transient - not persisted
|
||||
Files RelatedFiles
|
||||
@@ -104,10 +105,11 @@ type GalleryPartial struct {
|
||||
// Path OptionalString
|
||||
// Checksum OptionalString
|
||||
// Zip OptionalBool
|
||||
Title OptionalString
|
||||
URL OptionalString
|
||||
Date OptionalDate
|
||||
Details OptionalString
|
||||
Title OptionalString
|
||||
URL OptionalString
|
||||
Date OptionalDate
|
||||
Details OptionalString
|
||||
// Rating expressed in 1-100 scale
|
||||
Rating OptionalInt
|
||||
Organized OptionalBool
|
||||
StudioID OptionalInt
|
||||
|
||||
@@ -14,11 +14,12 @@ import (
|
||||
type Image struct {
|
||||
ID int `json:"id"`
|
||||
|
||||
Title string `json:"title"`
|
||||
Rating *int `json:"rating"`
|
||||
Organized bool `json:"organized"`
|
||||
OCounter int `json:"o_counter"`
|
||||
StudioID *int `json:"studio_id"`
|
||||
Title string `json:"title"`
|
||||
// Rating expressed in 1-100 scale
|
||||
Rating *int `json:"rating"`
|
||||
Organized bool `json:"organized"`
|
||||
OCounter int `json:"o_counter"`
|
||||
StudioID *int `json:"studio_id"`
|
||||
|
||||
// transient - not persisted
|
||||
Files RelatedImageFiles
|
||||
@@ -113,7 +114,8 @@ type ImageCreateInput struct {
|
||||
}
|
||||
|
||||
type ImagePartial struct {
|
||||
Title OptionalString
|
||||
Title OptionalString
|
||||
// Rating expressed in 1-100 scale
|
||||
Rating OptionalInt
|
||||
Organized OptionalBool
|
||||
OCounter OptionalInt
|
||||
|
||||
@@ -8,12 +8,13 @@ import (
|
||||
)
|
||||
|
||||
type Movie struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
Checksum string `db:"checksum" json:"checksum"`
|
||||
Name sql.NullString `db:"name" json:"name"`
|
||||
Aliases sql.NullString `db:"aliases" json:"aliases"`
|
||||
Duration sql.NullInt64 `db:"duration" json:"duration"`
|
||||
Date SQLiteDate `db:"date" json:"date"`
|
||||
ID int `db:"id" json:"id"`
|
||||
Checksum string `db:"checksum" json:"checksum"`
|
||||
Name sql.NullString `db:"name" json:"name"`
|
||||
Aliases sql.NullString `db:"aliases" json:"aliases"`
|
||||
Duration sql.NullInt64 `db:"duration" json:"duration"`
|
||||
Date SQLiteDate `db:"date" json:"date"`
|
||||
// Rating expressed in 1-100 scale
|
||||
Rating sql.NullInt64 `db:"rating" json:"rating"`
|
||||
StudioID sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"`
|
||||
Director sql.NullString `db:"director" json:"director"`
|
||||
@@ -24,12 +25,13 @@ type Movie struct {
|
||||
}
|
||||
|
||||
type MoviePartial struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
Checksum *string `db:"checksum" json:"checksum"`
|
||||
Name *sql.NullString `db:"name" json:"name"`
|
||||
Aliases *sql.NullString `db:"aliases" json:"aliases"`
|
||||
Duration *sql.NullInt64 `db:"duration" json:"duration"`
|
||||
Date *SQLiteDate `db:"date" json:"date"`
|
||||
ID int `db:"id" json:"id"`
|
||||
Checksum *string `db:"checksum" json:"checksum"`
|
||||
Name *sql.NullString `db:"name" json:"name"`
|
||||
Aliases *sql.NullString `db:"aliases" json:"aliases"`
|
||||
Duration *sql.NullInt64 `db:"duration" json:"duration"`
|
||||
Date *SQLiteDate `db:"date" json:"date"`
|
||||
// Rating expressed in 1-100 scale
|
||||
Rating *sql.NullInt64 `db:"rating" json:"rating"`
|
||||
StudioID *sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"`
|
||||
Director *sql.NullString `db:"director" json:"director"`
|
||||
|
||||
@@ -7,59 +7,61 @@ import (
|
||||
)
|
||||
|
||||
type Performer struct {
|
||||
ID int `json:"id"`
|
||||
Checksum string `json:"checksum"`
|
||||
Name string `json:"name"`
|
||||
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"`
|
||||
Aliases string `json:"aliases"`
|
||||
Favorite bool `json:"favorite"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Rating *int `json:"rating"`
|
||||
Details string `json:"details"`
|
||||
DeathDate *Date `json:"death_date"`
|
||||
HairColor string `json:"hair_color"`
|
||||
Weight *int `json:"weight"`
|
||||
IgnoreAutoTag bool `json:"ignore_auto_tag"`
|
||||
ID int `json:"id"`
|
||||
Checksum string `json:"checksum"`
|
||||
Name string `json:"name"`
|
||||
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"`
|
||||
Aliases string `json:"aliases"`
|
||||
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"`
|
||||
DeathDate *Date `json:"death_date"`
|
||||
HairColor string `json:"hair_color"`
|
||||
Weight *int `json:"weight"`
|
||||
IgnoreAutoTag bool `json:"ignore_auto_tag"`
|
||||
}
|
||||
|
||||
// PerformerPartial represents part of a Performer object. It is used to update
|
||||
// the database entry.
|
||||
type PerformerPartial struct {
|
||||
ID int
|
||||
Checksum OptionalString
|
||||
Name OptionalString
|
||||
Gender OptionalString
|
||||
URL OptionalString
|
||||
Twitter OptionalString
|
||||
Instagram OptionalString
|
||||
Birthdate OptionalDate
|
||||
Ethnicity OptionalString
|
||||
Country OptionalString
|
||||
EyeColor OptionalString
|
||||
Height OptionalInt
|
||||
Measurements OptionalString
|
||||
FakeTits OptionalString
|
||||
CareerLength OptionalString
|
||||
Tattoos OptionalString
|
||||
Piercings OptionalString
|
||||
Aliases OptionalString
|
||||
Favorite OptionalBool
|
||||
CreatedAt OptionalTime
|
||||
UpdatedAt OptionalTime
|
||||
ID int
|
||||
Checksum OptionalString
|
||||
Name OptionalString
|
||||
Gender OptionalString
|
||||
URL OptionalString
|
||||
Twitter OptionalString
|
||||
Instagram OptionalString
|
||||
Birthdate OptionalDate
|
||||
Ethnicity OptionalString
|
||||
Country OptionalString
|
||||
EyeColor OptionalString
|
||||
Height OptionalInt
|
||||
Measurements OptionalString
|
||||
FakeTits OptionalString
|
||||
CareerLength OptionalString
|
||||
Tattoos OptionalString
|
||||
Piercings OptionalString
|
||||
Aliases OptionalString
|
||||
Favorite OptionalBool
|
||||
CreatedAt OptionalTime
|
||||
UpdatedAt OptionalTime
|
||||
// Rating expressed in 1-100 scale
|
||||
Rating OptionalInt
|
||||
Details OptionalString
|
||||
DeathDate OptionalDate
|
||||
|
||||
@@ -12,17 +12,18 @@ import (
|
||||
|
||||
// Scene stores the metadata for a single video scene.
|
||||
type Scene struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Code string `json:"code"`
|
||||
Details string `json:"details"`
|
||||
Director string `json:"director"`
|
||||
URL string `json:"url"`
|
||||
Date *Date `json:"date"`
|
||||
Rating *int `json:"rating"`
|
||||
Organized bool `json:"organized"`
|
||||
OCounter int `json:"o_counter"`
|
||||
StudioID *int `json:"studio_id"`
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Code string `json:"code"`
|
||||
Details string `json:"details"`
|
||||
Director string `json:"director"`
|
||||
URL string `json:"url"`
|
||||
Date *Date `json:"date"`
|
||||
// Rating expressed in 1-100 scale
|
||||
Rating *int `json:"rating"`
|
||||
Organized bool `json:"organized"`
|
||||
OCounter int `json:"o_counter"`
|
||||
StudioID *int `json:"studio_id"`
|
||||
|
||||
// transient - not persisted
|
||||
Files RelatedVideoFiles
|
||||
@@ -134,12 +135,13 @@ func (s *Scene) LoadRelationships(ctx context.Context, l SceneReader) error {
|
||||
// ScenePartial represents part of a Scene object. It is used to update
|
||||
// the database entry.
|
||||
type ScenePartial struct {
|
||||
Title OptionalString
|
||||
Code OptionalString
|
||||
Details OptionalString
|
||||
Director OptionalString
|
||||
URL OptionalString
|
||||
Date OptionalDate
|
||||
Title OptionalString
|
||||
Code OptionalString
|
||||
Details OptionalString
|
||||
Director OptionalString
|
||||
URL OptionalString
|
||||
Date OptionalDate
|
||||
// Rating expressed in 1-100 scale
|
||||
Rating OptionalInt
|
||||
Organized OptionalBool
|
||||
OCounter OptionalInt
|
||||
@@ -168,22 +170,25 @@ type SceneMovieInput struct {
|
||||
}
|
||||
|
||||
type SceneUpdateInput struct {
|
||||
ClientMutationID *string `json:"clientMutationId"`
|
||||
ID string `json:"id"`
|
||||
Title *string `json:"title"`
|
||||
Code *string `json:"code"`
|
||||
Details *string `json:"details"`
|
||||
Director *string `json:"director"`
|
||||
URL *string `json:"url"`
|
||||
Date *string `json:"date"`
|
||||
Rating *int `json:"rating"`
|
||||
OCounter *int `json:"o_counter"`
|
||||
Organized *bool `json:"organized"`
|
||||
StudioID *string `json:"studio_id"`
|
||||
GalleryIds []string `json:"gallery_ids"`
|
||||
PerformerIds []string `json:"performer_ids"`
|
||||
Movies []*SceneMovieInput `json:"movies"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
ClientMutationID *string `json:"clientMutationId"`
|
||||
ID string `json:"id"`
|
||||
Title *string `json:"title"`
|
||||
Code *string `json:"code"`
|
||||
Details *string `json:"details"`
|
||||
Director *string `json:"director"`
|
||||
URL *string `json:"url"`
|
||||
Date *string `json:"date"`
|
||||
// Rating expressed in 1-5 scale
|
||||
Rating *int `json:"rating"`
|
||||
// Rating expressed in 1-100 scale
|
||||
Rating100 *int `json:"rating100"`
|
||||
OCounter *int `json:"o_counter"`
|
||||
Organized *bool `json:"organized"`
|
||||
StudioID *string `json:"studio_id"`
|
||||
GalleryIds []string `json:"gallery_ids"`
|
||||
PerformerIds []string `json:"performer_ids"`
|
||||
Movies []*SceneMovieInput `json:"movies"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
// This should be a URL or a base64 encoded data URL
|
||||
CoverImage *string `json:"cover_image"`
|
||||
StashIds []StashID `json:"stash_ids"`
|
||||
@@ -204,7 +209,7 @@ func (s ScenePartial) UpdateInput(id int) SceneUpdateInput {
|
||||
stashIDs = s.StashIDs.StashIDs
|
||||
}
|
||||
|
||||
return SceneUpdateInput{
|
||||
ret := SceneUpdateInput{
|
||||
ID: strconv.Itoa(id),
|
||||
Title: s.Title.Ptr(),
|
||||
Code: s.Code.Ptr(),
|
||||
@@ -212,7 +217,7 @@ func (s ScenePartial) UpdateInput(id int) SceneUpdateInput {
|
||||
Director: s.Director.Ptr(),
|
||||
URL: s.URL.Ptr(),
|
||||
Date: dateStr,
|
||||
Rating: s.Rating.Ptr(),
|
||||
Rating100: s.Rating.Ptr(),
|
||||
Organized: s.Organized.Ptr(),
|
||||
StudioID: s.StudioID.StringPtr(),
|
||||
GalleryIds: s.GalleryIDs.IDStrings(),
|
||||
@@ -221,6 +226,14 @@ func (s ScenePartial) UpdateInput(id int) SceneUpdateInput {
|
||||
TagIds: s.TagIDs.IDStrings(),
|
||||
StashIds: stashIDs,
|
||||
}
|
||||
|
||||
if s.Rating.Set && !s.Rating.Null {
|
||||
// convert to 1-100 scale
|
||||
rating := Rating100To5(s.Rating.Value)
|
||||
ret.Rating = &rating
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// GetTitle returns the title of the scene. If the Title field is empty,
|
||||
|
||||
@@ -12,16 +12,17 @@ func TestScenePartial_UpdateInput(t *testing.T) {
|
||||
)
|
||||
|
||||
var (
|
||||
title = "title"
|
||||
code = "1337"
|
||||
details = "details"
|
||||
director = "director"
|
||||
url = "url"
|
||||
date = "2001-02-03"
|
||||
rating = 4
|
||||
organized = true
|
||||
studioID = 2
|
||||
studioIDStr = "2"
|
||||
title = "title"
|
||||
code = "1337"
|
||||
details = "details"
|
||||
director = "director"
|
||||
url = "url"
|
||||
date = "2001-02-03"
|
||||
ratingLegacy = 4
|
||||
rating100 = 80
|
||||
organized = true
|
||||
studioID = 2
|
||||
studioIDStr = "2"
|
||||
)
|
||||
|
||||
dateObj := NewDate(date)
|
||||
@@ -42,7 +43,7 @@ func TestScenePartial_UpdateInput(t *testing.T) {
|
||||
Director: NewOptionalString(director),
|
||||
URL: NewOptionalString(url),
|
||||
Date: NewOptionalDate(dateObj),
|
||||
Rating: NewOptionalInt(rating),
|
||||
Rating: NewOptionalInt(rating100),
|
||||
Organized: NewOptionalBool(organized),
|
||||
StudioID: NewOptionalInt(studioID),
|
||||
},
|
||||
@@ -54,7 +55,8 @@ func TestScenePartial_UpdateInput(t *testing.T) {
|
||||
Director: &director,
|
||||
URL: &url,
|
||||
Date: &date,
|
||||
Rating: &rating,
|
||||
Rating: &ratingLegacy,
|
||||
Rating100: &rating100,
|
||||
Organized: &organized,
|
||||
StudioID: &studioIDStr,
|
||||
},
|
||||
|
||||
@@ -8,29 +8,31 @@ import (
|
||||
)
|
||||
|
||||
type Studio struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
Checksum string `db:"checksum" json:"checksum"`
|
||||
Name sql.NullString `db:"name" json:"name"`
|
||||
URL sql.NullString `db:"url" json:"url"`
|
||||
ParentID sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
|
||||
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||
Rating sql.NullInt64 `db:"rating" json:"rating"`
|
||||
Details sql.NullString `db:"details" json:"details"`
|
||||
IgnoreAutoTag bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
|
||||
ID int `db:"id" json:"id"`
|
||||
Checksum string `db:"checksum" json:"checksum"`
|
||||
Name sql.NullString `db:"name" json:"name"`
|
||||
URL sql.NullString `db:"url" json:"url"`
|
||||
ParentID sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
|
||||
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||
// Rating expressed in 1-100 scale
|
||||
Rating sql.NullInt64 `db:"rating" json:"rating"`
|
||||
Details sql.NullString `db:"details" json:"details"`
|
||||
IgnoreAutoTag bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
|
||||
}
|
||||
|
||||
type StudioPartial struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
Checksum *string `db:"checksum" json:"checksum"`
|
||||
Name *sql.NullString `db:"name" json:"name"`
|
||||
URL *sql.NullString `db:"url" json:"url"`
|
||||
ParentID *sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
|
||||
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||
Rating *sql.NullInt64 `db:"rating" json:"rating"`
|
||||
Details *sql.NullString `db:"details" json:"details"`
|
||||
IgnoreAutoTag *bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
|
||||
ID int `db:"id" json:"id"`
|
||||
Checksum *string `db:"checksum" json:"checksum"`
|
||||
Name *sql.NullString `db:"name" json:"name"`
|
||||
URL *sql.NullString `db:"url" json:"url"`
|
||||
ParentID *sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
|
||||
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||
// Rating expressed in 1-100 scale
|
||||
Rating *sql.NullInt64 `db:"rating" json:"rating"`
|
||||
Details *sql.NullString `db:"details" json:"details"`
|
||||
IgnoreAutoTag *bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
|
||||
}
|
||||
|
||||
var DefaultStudioImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4wgVBQsJl1CMZAAAASJJREFUeNrt3N0JwyAYhlEj3cj9R3Cm5rbkqtAP+qrnGaCYHPwJpLlaa++mmLpbAERAgAgIEAEBIiBABERAgAgIEAEBIiBABERAgAgIEAHZuVflj40x4i94zhk9vqsVvEq6AsQqMP1EjORx20OACAgQRRx7T+zzcFBxcjNDfoB4ntQqTm5Awo7MlqywZxcgYQ+RlqywJ3ozJAQCSBiEJSsQA0gYBpDAgAARECACAkRAgAgIEAERECACAmSjUv6eAOSB8m8YIGGzBUjYbAESBgMkbBkDEjZbgITBAClcxiqQvEoatreYIWEBASIgJ4Gkf11ntXH3nS9uxfGWfJ5J9hAgAgJEQAQEiIAAERAgAgJEQAQEiIAAERAgAgJEQAQEiL7qBuc6RKLHxr0CAAAAAElFTkSuQmCC"
|
||||
|
||||
@@ -8,8 +8,10 @@ type MovieFilterType struct {
|
||||
Synopsis *StringCriterionInput `json:"synopsis"`
|
||||
// Filter by duration (in seconds)
|
||||
Duration *IntCriterionInput `json:"duration"`
|
||||
// Filter by rating
|
||||
// Filter by rating expressed as 1-5
|
||||
Rating *IntCriterionInput `json:"rating"`
|
||||
// Filter by rating expressed as 1-100
|
||||
Rating100 *IntCriterionInput `json:"rating100"`
|
||||
// Filter to only include movies with this studio
|
||||
Studios *HierarchicalMultiCriterionInput `json:"studios"`
|
||||
// Filter to only include movies missing this property
|
||||
|
||||
@@ -111,8 +111,10 @@ type PerformerFilterType struct {
|
||||
GalleryCount *IntCriterionInput `json:"gallery_count"`
|
||||
// Filter by StashID
|
||||
StashID *StringCriterionInput `json:"stash_id"`
|
||||
// Filter by rating
|
||||
// Filter by rating expressed as 1-5
|
||||
Rating *IntCriterionInput `json:"rating"`
|
||||
// Filter by rating expressed as 1-100
|
||||
Rating100 *IntCriterionInput `json:"rating100"`
|
||||
// Filter by url
|
||||
URL *StringCriterionInput `json:"url"`
|
||||
// Filter by hair color
|
||||
|
||||
69
pkg/models/rating.go
Normal file
69
pkg/models/rating.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type RatingSystem string
|
||||
|
||||
const (
|
||||
FiveStar = "FiveStar"
|
||||
FivePointFiveStar = "FivePointFiveStar"
|
||||
FivePointTwoFiveStar = "FivePointTwoFiveStar"
|
||||
// TenStar = "TenStar"
|
||||
// TenPointFiveStar = "TenPointFiveStar"
|
||||
// TenPointTwoFiveStar = "TenPointTwoFiveStar"
|
||||
TenPointDecimal = "TenPointDecimal"
|
||||
)
|
||||
|
||||
func (e RatingSystem) IsValid() bool {
|
||||
switch e {
|
||||
// case FiveStar, FivePointFiveStar, FivePointTwoFiveStar, TenStar, TenPointFiveStar, TenPointTwoFiveStar, TenPointDecimal:
|
||||
case FiveStar, FivePointFiveStar, FivePointTwoFiveStar, TenPointDecimal:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e RatingSystem) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *RatingSystem) UnmarshalGQL(v interface{}) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = RatingSystem(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid RatingSystem", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e RatingSystem) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
const (
|
||||
maxRating100 = 100
|
||||
maxRating5 = 5
|
||||
minRating5 = 1
|
||||
minRating100 = 20
|
||||
)
|
||||
|
||||
// Rating100To5 converts a 1-100 rating to a 1-5 rating.
|
||||
// Values <= 30 are converted to 1. Otherwise, rating is divided by 20 and rounded to the nearest integer.
|
||||
func Rating100To5(rating100 int) int {
|
||||
val := math.Round((float64(rating100) / 20))
|
||||
return int(math.Max(minRating5, math.Min(maxRating5, val)))
|
||||
}
|
||||
|
||||
// Rating5To100 converts a 1-5 rating to a 1-100 rating
|
||||
func Rating5To100(rating5 int) int {
|
||||
return int(math.Max(minRating100, math.Min(maxRating100, float64(rating5*20))))
|
||||
}
|
||||
55
pkg/models/rating_test.go
Normal file
55
pkg/models/rating_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRating100To5(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rating100 int
|
||||
want int
|
||||
}{
|
||||
{"20", 20, 1},
|
||||
{"100", 100, 5},
|
||||
{"1", 1, 1},
|
||||
{"10", 10, 1},
|
||||
{"11", 11, 1},
|
||||
{"21", 21, 1},
|
||||
{"31", 31, 2},
|
||||
{"0", 0, 1},
|
||||
{"-100", -100, 1},
|
||||
{"120", 120, 5},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := Rating100To5(tt.rating100); got != tt.want {
|
||||
t.Errorf("Rating100To5() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRating5To100(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rating5 int
|
||||
want int
|
||||
}{
|
||||
{"1", 1, 20},
|
||||
{"5", 5, 100},
|
||||
{"2", 2, 40},
|
||||
{"3", 3, 60},
|
||||
{"4", 4, 80},
|
||||
{"6", 6, 100},
|
||||
{"0", 0, 20},
|
||||
{"-1", -1, 20},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := Rating5To100(tt.rating5); got != tt.want {
|
||||
t.Errorf("Rating5To100() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,10 @@ type SceneFilterType struct {
|
||||
Path *StringCriterionInput `json:"path"`
|
||||
// Filter by file count
|
||||
FileCount *IntCriterionInput `json:"file_count"`
|
||||
// Filter by rating
|
||||
// Filter by rating expressed as 1-5
|
||||
Rating *IntCriterionInput `json:"rating"`
|
||||
// Filter by rating expressed as 1-100
|
||||
Rating100 *IntCriterionInput `json:"rating100"`
|
||||
// Filter by organized
|
||||
Organized *bool `json:"organized"`
|
||||
// Filter by o-counter
|
||||
|
||||
@@ -14,8 +14,10 @@ type StudioFilterType struct {
|
||||
StashID *StringCriterionInput `json:"stash_id"`
|
||||
// Filter to only include studios missing this property
|
||||
IsMissing *string `json:"is_missing"`
|
||||
// Filter by rating
|
||||
// Filter by rating expressed as 1-5
|
||||
Rating *IntCriterionInput `json:"rating"`
|
||||
// Filter by rating expressed as 1-100
|
||||
Rating100 *IntCriterionInput `json:"rating100"`
|
||||
// Filter by scene count
|
||||
SceneCount *IntCriterionInput `json:"scene_count"`
|
||||
// Filter by image count
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
var appSchemaVersion uint = 39
|
||||
var appSchemaVersion uint = 40
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsBox embed.FS
|
||||
|
||||
@@ -543,6 +543,25 @@ func boolCriterionHandler(c *bool, column string, addJoinFn func(f *filterBuilde
|
||||
}
|
||||
}
|
||||
|
||||
func rating5CriterionHandler(c *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if c != nil {
|
||||
// make a copy so we can adjust it
|
||||
cc := *c
|
||||
if cc.Value != 0 {
|
||||
cc.Value = models.Rating5To100(cc.Value)
|
||||
}
|
||||
if cc.Value2 != nil {
|
||||
val := models.Rating5To100(*cc.Value2)
|
||||
cc.Value2 = &val
|
||||
}
|
||||
|
||||
clause, args := getIntCriterionWhereClause(column, cc)
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dateCriterionHandler(c *models.DateCriterionInput, column string) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if c != nil {
|
||||
|
||||
@@ -30,11 +30,12 @@ const (
|
||||
)
|
||||
|
||||
type galleryRow struct {
|
||||
ID int `db:"id" goqu:"skipinsert"`
|
||||
Title zero.String `db:"title"`
|
||||
URL zero.String `db:"url"`
|
||||
Date models.SQLiteDate `db:"date"`
|
||||
Details zero.String `db:"details"`
|
||||
ID int `db:"id" goqu:"skipinsert"`
|
||||
Title zero.String `db:"title"`
|
||||
URL zero.String `db:"url"`
|
||||
Date models.SQLiteDate `db:"date"`
|
||||
Details zero.String `db:"details"`
|
||||
// expressed as 1-100
|
||||
Rating null.Int `db:"rating"`
|
||||
Organized bool `db:"organized"`
|
||||
StudioID null.Int `db:"studio_id,omitempty"`
|
||||
@@ -651,7 +652,9 @@ func (qb *GalleryStore) makeFilter(ctx context.Context, galleryFilter *models.Ga
|
||||
|
||||
query.handleCriterion(ctx, qb.galleryPathCriterionHandler(galleryFilter.Path))
|
||||
query.handleCriterion(ctx, galleryFileCountCriterionHandler(qb, galleryFilter.FileCount))
|
||||
query.handleCriterion(ctx, intCriterionHandler(galleryFilter.Rating, "galleries.rating", nil))
|
||||
query.handleCriterion(ctx, intCriterionHandler(galleryFilter.Rating100, "galleries.rating", nil))
|
||||
// legacy rating handler
|
||||
query.handleCriterion(ctx, rating5CriterionHandler(galleryFilter.Rating, "galleries.rating", nil))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(galleryFilter.URL, "galleries.url"))
|
||||
query.handleCriterion(ctx, boolCriterionHandler(galleryFilter.Organized, "galleries.organized", nil))
|
||||
query.handleCriterion(ctx, galleryIsMissingCriterionHandler(qb, galleryFilter.IsMissing))
|
||||
|
||||
@@ -54,7 +54,7 @@ func Test_galleryQueryBuilder_Create(t *testing.T) {
|
||||
var (
|
||||
title = "title"
|
||||
url = "url"
|
||||
rating = 3
|
||||
rating = 60
|
||||
details = "details"
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
@@ -205,7 +205,7 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
|
||||
var (
|
||||
title = "title"
|
||||
url = "url"
|
||||
rating = 3
|
||||
rating = 60
|
||||
details = "details"
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
@@ -399,7 +399,7 @@ func Test_galleryQueryBuilder_UpdatePartial(t *testing.T) {
|
||||
title = "title"
|
||||
details = "details"
|
||||
url = "url"
|
||||
rating = 3
|
||||
rating = 60
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
@@ -1547,7 +1547,7 @@ func TestGalleryQueryPathAndRating(t *testing.T) {
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
And: &models.GalleryFilterType{
|
||||
Rating: &models.IntCriterionInput{
|
||||
Rating100: &models.IntCriterionInput{
|
||||
Value: *galleryRating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
@@ -1588,7 +1588,7 @@ func TestGalleryQueryPathNotRating(t *testing.T) {
|
||||
galleryFilter := models.GalleryFilterType{
|
||||
Path: &pathCriterion,
|
||||
Not: &models.GalleryFilterType{
|
||||
Rating: &ratingCriterion,
|
||||
Rating100: &ratingCriterion,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1699,32 +1699,32 @@ func verifyGalleryQuery(t *testing.T, filter models.GalleryFilterType, verifyFn
|
||||
})
|
||||
}
|
||||
|
||||
func TestGalleryQueryRating(t *testing.T) {
|
||||
func TestGalleryQueryLegacyRating(t *testing.T) {
|
||||
const rating = 3
|
||||
ratingCriterion := models.IntCriterionInput{
|
||||
Value: rating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
}
|
||||
|
||||
verifyGalleriesRating(t, ratingCriterion)
|
||||
verifyGalleriesLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||
verifyGalleriesRating(t, ratingCriterion)
|
||||
verifyGalleriesLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||
verifyGalleriesRating(t, ratingCriterion)
|
||||
verifyGalleriesLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
||||
verifyGalleriesRating(t, ratingCriterion)
|
||||
verifyGalleriesLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
||||
verifyGalleriesRating(t, ratingCriterion)
|
||||
verifyGalleriesLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotNull
|
||||
verifyGalleriesRating(t, ratingCriterion)
|
||||
verifyGalleriesLegacyRating(t, ratingCriterion)
|
||||
}
|
||||
|
||||
func verifyGalleriesRating(t *testing.T, ratingCriterion models.IntCriterionInput) {
|
||||
func verifyGalleriesLegacyRating(t *testing.T, ratingCriterion models.IntCriterionInput) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
sqb := db.Gallery
|
||||
galleryFilter := models.GalleryFilterType{
|
||||
@@ -1736,6 +1736,54 @@ func verifyGalleriesRating(t *testing.T, ratingCriterion models.IntCriterionInpu
|
||||
t.Errorf("Error querying gallery: %s", err.Error())
|
||||
}
|
||||
|
||||
// convert criterion value to the 100 value
|
||||
ratingCriterion.Value = models.Rating5To100(ratingCriterion.Value)
|
||||
|
||||
for _, gallery := range galleries {
|
||||
verifyIntPtr(t, gallery.Rating, ratingCriterion)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestGalleryQueryRating100(t *testing.T) {
|
||||
const rating = 60
|
||||
ratingCriterion := models.IntCriterionInput{
|
||||
Value: rating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
}
|
||||
|
||||
verifyGalleriesRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||
verifyGalleriesRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||
verifyGalleriesRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
||||
verifyGalleriesRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
||||
verifyGalleriesRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotNull
|
||||
verifyGalleriesRating100(t, ratingCriterion)
|
||||
}
|
||||
|
||||
func verifyGalleriesRating100(t *testing.T, ratingCriterion models.IntCriterionInput) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
sqb := db.Gallery
|
||||
galleryFilter := models.GalleryFilterType{
|
||||
Rating100: &ratingCriterion,
|
||||
}
|
||||
|
||||
galleries, _, err := sqb.Query(ctx, &galleryFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying gallery: %s", err.Error())
|
||||
}
|
||||
|
||||
for _, gallery := range galleries {
|
||||
verifyIntPtr(t, gallery.Rating, ratingCriterion)
|
||||
}
|
||||
|
||||
@@ -27,8 +27,9 @@ const (
|
||||
)
|
||||
|
||||
type imageRow struct {
|
||||
ID int `db:"id" goqu:"skipinsert"`
|
||||
Title zero.String `db:"title"`
|
||||
ID int `db:"id" goqu:"skipinsert"`
|
||||
Title zero.String `db:"title"`
|
||||
// expressed as 1-100
|
||||
Rating null.Int `db:"rating"`
|
||||
Organized bool `db:"organized"`
|
||||
OCounter int `db:"o_counter"`
|
||||
@@ -632,7 +633,9 @@ func (qb *ImageStore) makeFilter(ctx context.Context, imageFilter *models.ImageF
|
||||
|
||||
query.handleCriterion(ctx, pathCriterionHandler(imageFilter.Path, "folders.path", "files.basename", qb.addFoldersTable))
|
||||
query.handleCriterion(ctx, imageFileCountCriterionHandler(qb, imageFilter.FileCount))
|
||||
query.handleCriterion(ctx, intCriterionHandler(imageFilter.Rating, "images.rating", nil))
|
||||
query.handleCriterion(ctx, intCriterionHandler(imageFilter.Rating100, "images.rating", nil))
|
||||
// legacy rating handler
|
||||
query.handleCriterion(ctx, rating5CriterionHandler(imageFilter.Rating, "images.rating", nil))
|
||||
query.handleCriterion(ctx, intCriterionHandler(imageFilter.OCounter, "images.o_counter", nil))
|
||||
query.handleCriterion(ctx, boolCriterionHandler(imageFilter.Organized, "images.organized", nil))
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ func loadImageRelationships(ctx context.Context, expected models.Image, actual *
|
||||
func Test_imageQueryBuilder_Create(t *testing.T) {
|
||||
var (
|
||||
title = "title"
|
||||
rating = 3
|
||||
rating = 60
|
||||
ocounter = 5
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
@@ -208,7 +208,7 @@ func makeImageFileWithID(i int) *file.ImageFile {
|
||||
func Test_imageQueryBuilder_Update(t *testing.T) {
|
||||
var (
|
||||
title = "title"
|
||||
rating = 3
|
||||
rating = 60
|
||||
ocounter = 5
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
@@ -382,7 +382,7 @@ func clearImagePartial() models.ImagePartial {
|
||||
func Test_imageQueryBuilder_UpdatePartial(t *testing.T) {
|
||||
var (
|
||||
title = "title"
|
||||
rating = 3
|
||||
rating = 60
|
||||
ocounter = 5
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
@@ -1595,7 +1595,7 @@ func TestImageQueryPathAndRating(t *testing.T) {
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
And: &models.ImageFilterType{
|
||||
Rating: &models.IntCriterionInput{
|
||||
Rating100: &models.IntCriterionInput{
|
||||
Value: int(imageRating.Int64),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
@@ -1607,7 +1607,10 @@ func TestImageQueryPathAndRating(t *testing.T) {
|
||||
|
||||
images := queryImages(ctx, t, sqb, &imageFilter, nil)
|
||||
|
||||
assert.Len(t, images, 1)
|
||||
if !assert.Len(t, images, 1) {
|
||||
return nil
|
||||
}
|
||||
|
||||
assert.Equal(t, imagePath, images[0].Path)
|
||||
assert.Equal(t, int(imageRating.Int64), *images[0].Rating)
|
||||
|
||||
@@ -1633,7 +1636,7 @@ func TestImageQueryPathNotRating(t *testing.T) {
|
||||
imageFilter := models.ImageFilterType{
|
||||
Path: &pathCriterion,
|
||||
Not: &models.ImageFilterType{
|
||||
Rating: &ratingCriterion,
|
||||
Rating100: &ratingCriterion,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1688,32 +1691,32 @@ func TestImageIllegalQuery(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestImageQueryRating(t *testing.T) {
|
||||
func TestImageQueryLegacyRating(t *testing.T) {
|
||||
const rating = 3
|
||||
ratingCriterion := models.IntCriterionInput{
|
||||
Value: rating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
}
|
||||
|
||||
verifyImagesRating(t, ratingCriterion)
|
||||
verifyImagesLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||
verifyImagesRating(t, ratingCriterion)
|
||||
verifyImagesLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||
verifyImagesRating(t, ratingCriterion)
|
||||
verifyImagesLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
||||
verifyImagesRating(t, ratingCriterion)
|
||||
verifyImagesLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
||||
verifyImagesRating(t, ratingCriterion)
|
||||
verifyImagesLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotNull
|
||||
verifyImagesRating(t, ratingCriterion)
|
||||
verifyImagesLegacyRating(t, ratingCriterion)
|
||||
}
|
||||
|
||||
func verifyImagesRating(t *testing.T, ratingCriterion models.IntCriterionInput) {
|
||||
func verifyImagesLegacyRating(t *testing.T, ratingCriterion models.IntCriterionInput) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
sqb := db.Image
|
||||
imageFilter := models.ImageFilterType{
|
||||
@@ -1725,6 +1728,54 @@ func verifyImagesRating(t *testing.T, ratingCriterion models.IntCriterionInput)
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
|
||||
// convert criterion value to the 100 value
|
||||
ratingCriterion.Value = models.Rating5To100(ratingCriterion.Value)
|
||||
|
||||
for _, image := range images {
|
||||
verifyIntPtr(t, image.Rating, ratingCriterion)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestImageQueryRating100(t *testing.T) {
|
||||
const rating = 60
|
||||
ratingCriterion := models.IntCriterionInput{
|
||||
Value: rating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
}
|
||||
|
||||
verifyImagesRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||
verifyImagesRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||
verifyImagesRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
||||
verifyImagesRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
||||
verifyImagesRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotNull
|
||||
verifyImagesRating100(t, ratingCriterion)
|
||||
}
|
||||
|
||||
func verifyImagesRating100(t *testing.T, ratingCriterion models.IntCriterionInput) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
sqb := db.Image
|
||||
imageFilter := models.ImageFilterType{
|
||||
Rating100: &ratingCriterion,
|
||||
}
|
||||
|
||||
images, _, err := queryImagesWithCount(ctx, sqb, &imageFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
|
||||
for _, image := range images {
|
||||
verifyIntPtr(t, image.Rating, ratingCriterion)
|
||||
}
|
||||
|
||||
6
pkg/sqlite/migrations/40_newratings.up.sql
Normal file
6
pkg/sqlite/migrations/40_newratings.up.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
UPDATE `scenes` SET `rating` = (`rating` * 20) WHERE `rating` < 6;
|
||||
UPDATE `galleries` SET `rating` = (`rating` * 20) WHERE `rating` < 6;
|
||||
UPDATE `images` SET `rating` = (`rating` * 20) WHERE `rating` < 6;
|
||||
UPDATE `movies` SET `rating` = (`rating` * 20) WHERE `rating` < 6;
|
||||
UPDATE `performers` SET `rating` = (`rating` * 20) WHERE `rating` < 6;
|
||||
UPDATE `studios` SET `rating` = (`rating` * 20) WHERE `rating` < 6;
|
||||
@@ -147,7 +147,9 @@ func (qb *movieQueryBuilder) makeFilter(ctx context.Context, movieFilter *models
|
||||
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Name, "movies.name"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Director, "movies.director"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Synopsis, "movies.synopsis"))
|
||||
query.handleCriterion(ctx, intCriterionHandler(movieFilter.Rating, "movies.rating", nil))
|
||||
query.handleCriterion(ctx, intCriterionHandler(movieFilter.Rating100, "movies.rating", nil))
|
||||
// legacy rating handler
|
||||
query.handleCriterion(ctx, rating5CriterionHandler(movieFilter.Rating, "movies.rating", nil))
|
||||
query.handleCriterion(ctx, durationCriterionHandler(movieFilter.Duration, "movies.duration", nil))
|
||||
query.handleCriterion(ctx, movieIsMissingCriterionHandler(qb, movieFilter.IsMissing))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.URL, "movies.url"))
|
||||
|
||||
@@ -23,33 +23,34 @@ const performersTagsTable = "performers_tags"
|
||||
const performersImageTable = "performers_image" // performer cover image
|
||||
|
||||
type performerRow struct {
|
||||
ID int `db:"id" goqu:"skipinsert"`
|
||||
Checksum string `db:"checksum"`
|
||||
Name zero.String `db:"name"`
|
||||
Gender zero.String `db:"gender"`
|
||||
URL zero.String `db:"url"`
|
||||
Twitter zero.String `db:"twitter"`
|
||||
Instagram zero.String `db:"instagram"`
|
||||
Birthdate models.SQLiteDate `db:"birthdate"`
|
||||
Ethnicity zero.String `db:"ethnicity"`
|
||||
Country zero.String `db:"country"`
|
||||
EyeColor zero.String `db:"eye_color"`
|
||||
Height null.Int `db:"height"`
|
||||
Measurements zero.String `db:"measurements"`
|
||||
FakeTits zero.String `db:"fake_tits"`
|
||||
CareerLength zero.String `db:"career_length"`
|
||||
Tattoos zero.String `db:"tattoos"`
|
||||
Piercings zero.String `db:"piercings"`
|
||||
Aliases zero.String `db:"aliases"`
|
||||
Favorite sql.NullBool `db:"favorite"`
|
||||
CreatedAt models.SQLiteTimestamp `db:"created_at"`
|
||||
UpdatedAt models.SQLiteTimestamp `db:"updated_at"`
|
||||
Rating null.Int `db:"rating"`
|
||||
Details zero.String `db:"details"`
|
||||
DeathDate models.SQLiteDate `db:"death_date"`
|
||||
HairColor zero.String `db:"hair_color"`
|
||||
Weight null.Int `db:"weight"`
|
||||
IgnoreAutoTag bool `db:"ignore_auto_tag"`
|
||||
ID int `db:"id" goqu:"skipinsert"`
|
||||
Checksum string `db:"checksum"`
|
||||
Name zero.String `db:"name"`
|
||||
Gender zero.String `db:"gender"`
|
||||
URL zero.String `db:"url"`
|
||||
Twitter zero.String `db:"twitter"`
|
||||
Instagram zero.String `db:"instagram"`
|
||||
Birthdate models.SQLiteDate `db:"birthdate"`
|
||||
Ethnicity zero.String `db:"ethnicity"`
|
||||
Country zero.String `db:"country"`
|
||||
EyeColor zero.String `db:"eye_color"`
|
||||
Height null.Int `db:"height"`
|
||||
Measurements zero.String `db:"measurements"`
|
||||
FakeTits zero.String `db:"fake_tits"`
|
||||
CareerLength zero.String `db:"career_length"`
|
||||
Tattoos zero.String `db:"tattoos"`
|
||||
Piercings zero.String `db:"piercings"`
|
||||
Aliases zero.String `db:"aliases"`
|
||||
Favorite sql.NullBool `db:"favorite"`
|
||||
CreatedAt models.SQLiteTimestamp `db:"created_at"`
|
||||
UpdatedAt models.SQLiteTimestamp `db:"updated_at"`
|
||||
// expressed as 1-100
|
||||
Rating null.Int `db:"rating"`
|
||||
Details zero.String `db:"details"`
|
||||
DeathDate models.SQLiteDate `db:"death_date"`
|
||||
HairColor zero.String `db:"hair_color"`
|
||||
Weight null.Int `db:"weight"`
|
||||
IgnoreAutoTag bool `db:"ignore_auto_tag"`
|
||||
}
|
||||
|
||||
func (r *performerRow) fromPerformer(o models.Performer) {
|
||||
@@ -90,27 +91,28 @@ func (r *performerRow) fromPerformer(o models.Performer) {
|
||||
|
||||
func (r *performerRow) resolve() *models.Performer {
|
||||
ret := &models.Performer{
|
||||
ID: r.ID,
|
||||
Checksum: r.Checksum,
|
||||
Name: r.Name.String,
|
||||
Gender: models.GenderEnum(r.Gender.String),
|
||||
URL: r.URL.String,
|
||||
Twitter: r.Twitter.String,
|
||||
Instagram: r.Instagram.String,
|
||||
Birthdate: r.Birthdate.DatePtr(),
|
||||
Ethnicity: r.Ethnicity.String,
|
||||
Country: r.Country.String,
|
||||
EyeColor: r.EyeColor.String,
|
||||
Height: nullIntPtr(r.Height),
|
||||
Measurements: r.Measurements.String,
|
||||
FakeTits: r.FakeTits.String,
|
||||
CareerLength: r.CareerLength.String,
|
||||
Tattoos: r.Tattoos.String,
|
||||
Piercings: r.Piercings.String,
|
||||
Aliases: r.Aliases.String,
|
||||
Favorite: r.Favorite.Bool,
|
||||
CreatedAt: r.CreatedAt.Timestamp,
|
||||
UpdatedAt: r.UpdatedAt.Timestamp,
|
||||
ID: r.ID,
|
||||
Checksum: r.Checksum,
|
||||
Name: r.Name.String,
|
||||
Gender: models.GenderEnum(r.Gender.String),
|
||||
URL: r.URL.String,
|
||||
Twitter: r.Twitter.String,
|
||||
Instagram: r.Instagram.String,
|
||||
Birthdate: r.Birthdate.DatePtr(),
|
||||
Ethnicity: r.Ethnicity.String,
|
||||
Country: r.Country.String,
|
||||
EyeColor: r.EyeColor.String,
|
||||
Height: nullIntPtr(r.Height),
|
||||
Measurements: r.Measurements.String,
|
||||
FakeTits: r.FakeTits.String,
|
||||
CareerLength: r.CareerLength.String,
|
||||
Tattoos: r.Tattoos.String,
|
||||
Piercings: r.Piercings.String,
|
||||
Aliases: r.Aliases.String,
|
||||
Favorite: r.Favorite.Bool,
|
||||
CreatedAt: r.CreatedAt.Timestamp,
|
||||
UpdatedAt: r.UpdatedAt.Timestamp,
|
||||
// expressed as 1-100
|
||||
Rating: nullIntPtr(r.Rating),
|
||||
Details: r.Details.String,
|
||||
DeathDate: r.DeathDate.DatePtr(),
|
||||
@@ -519,7 +521,9 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform
|
||||
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"))
|
||||
query.handleCriterion(ctx, intCriterionHandler(filter.Rating, tableName+".rating", nil))
|
||||
query.handleCriterion(ctx, intCriterionHandler(filter.Rating100, tableName+".rating", nil))
|
||||
// legacy rating handler
|
||||
query.handleCriterion(ctx, rating5CriterionHandler(filter.Rating, tableName+".rating", nil))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.HairColor, tableName+".hair_color"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(filter.URL, tableName+".url"))
|
||||
query.handleCriterion(ctx, intCriterionHandler(filter.Weight, tableName+".weight", nil))
|
||||
|
||||
@@ -440,7 +440,7 @@ func TestPerformerQueryEthnicityAndRating(t *testing.T) {
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
And: &models.PerformerFilterType{
|
||||
Rating: &models.IntCriterionInput{
|
||||
Rating100: &models.IntCriterionInput{
|
||||
Value: performerRating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
@@ -450,7 +450,10 @@ func TestPerformerQueryEthnicityAndRating(t *testing.T) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
performers := queryPerformers(ctx, t, &performerFilter, nil)
|
||||
|
||||
assert.Len(t, performers, 1)
|
||||
if !assert.Len(t, performers, 1) {
|
||||
return nil
|
||||
}
|
||||
|
||||
assert.Equal(t, performerEth, performers[0].Ethnicity)
|
||||
if assert.NotNil(t, performers[0].Rating) {
|
||||
assert.Equal(t, performerRating, *performers[0].Rating)
|
||||
@@ -478,7 +481,7 @@ func TestPerformerQueryEthnicityNotRating(t *testing.T) {
|
||||
performerFilter := models.PerformerFilterType{
|
||||
Ethnicity: ðCriterion,
|
||||
Not: &models.PerformerFilterType{
|
||||
Rating: &ratingCriterion,
|
||||
Rating100: &ratingCriterion,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1173,32 +1176,32 @@ func TestPerformerStashIDs(t *testing.T) {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
func TestPerformerQueryRating(t *testing.T) {
|
||||
func TestPerformerQueryLegacyRating(t *testing.T) {
|
||||
const rating = 3
|
||||
ratingCriterion := models.IntCriterionInput{
|
||||
Value: rating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
}
|
||||
|
||||
verifyPerformersRating(t, ratingCriterion)
|
||||
verifyPerformersLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||
verifyPerformersRating(t, ratingCriterion)
|
||||
verifyPerformersLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||
verifyPerformersRating(t, ratingCriterion)
|
||||
verifyPerformersLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
||||
verifyPerformersRating(t, ratingCriterion)
|
||||
verifyPerformersLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
||||
verifyPerformersRating(t, ratingCriterion)
|
||||
verifyPerformersLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotNull
|
||||
verifyPerformersRating(t, ratingCriterion)
|
||||
verifyPerformersLegacyRating(t, ratingCriterion)
|
||||
}
|
||||
|
||||
func verifyPerformersRating(t *testing.T, ratingCriterion models.IntCriterionInput) {
|
||||
func verifyPerformersLegacyRating(t *testing.T, ratingCriterion models.IntCriterionInput) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
performerFilter := models.PerformerFilterType{
|
||||
Rating: &ratingCriterion,
|
||||
@@ -1206,6 +1209,50 @@ func verifyPerformersRating(t *testing.T, ratingCriterion models.IntCriterionInp
|
||||
|
||||
performers := queryPerformers(ctx, t, &performerFilter, nil)
|
||||
|
||||
// convert criterion value to the 100 value
|
||||
ratingCriterion.Value = models.Rating5To100(ratingCriterion.Value)
|
||||
|
||||
for _, performer := range performers {
|
||||
verifyIntPtr(t, performer.Rating, ratingCriterion)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestPerformerQueryRating100(t *testing.T) {
|
||||
const rating = 60
|
||||
ratingCriterion := models.IntCriterionInput{
|
||||
Value: rating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
}
|
||||
|
||||
verifyPerformersRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||
verifyPerformersRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||
verifyPerformersRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
||||
verifyPerformersRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
||||
verifyPerformersRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotNull
|
||||
verifyPerformersRating100(t, ratingCriterion)
|
||||
}
|
||||
|
||||
func verifyPerformersRating100(t *testing.T, ratingCriterion models.IntCriterionInput) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
performerFilter := models.PerformerFilterType{
|
||||
Rating100: &ratingCriterion,
|
||||
}
|
||||
|
||||
performers := queryPerformers(ctx, t, &performerFilter, nil)
|
||||
|
||||
for _, performer := range performers {
|
||||
verifyIntPtr(t, performer.Rating, ratingCriterion)
|
||||
}
|
||||
|
||||
@@ -52,13 +52,14 @@ ORDER BY files.size DESC
|
||||
`
|
||||
|
||||
type sceneRow struct {
|
||||
ID int `db:"id" goqu:"skipinsert"`
|
||||
Title zero.String `db:"title"`
|
||||
Code zero.String `db:"code"`
|
||||
Details zero.String `db:"details"`
|
||||
Director zero.String `db:"director"`
|
||||
URL zero.String `db:"url"`
|
||||
Date models.SQLiteDate `db:"date"`
|
||||
ID int `db:"id" goqu:"skipinsert"`
|
||||
Title zero.String `db:"title"`
|
||||
Code zero.String `db:"code"`
|
||||
Details zero.String `db:"details"`
|
||||
Director zero.String `db:"director"`
|
||||
URL zero.String `db:"url"`
|
||||
Date models.SQLiteDate `db:"date"`
|
||||
// expressed as 1-100
|
||||
Rating null.Int `db:"rating"`
|
||||
Organized bool `db:"organized"`
|
||||
OCounter int `db:"o_counter"`
|
||||
@@ -844,7 +845,9 @@ func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneF
|
||||
}
|
||||
}))
|
||||
|
||||
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.Rating, "scenes.rating", nil))
|
||||
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.Rating100, "scenes.rating", nil))
|
||||
// legacy rating handler
|
||||
query.handleCriterion(ctx, rating5CriterionHandler(sceneFilter.Rating, "scenes.rating", nil))
|
||||
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.OCounter, "scenes.o_counter", nil))
|
||||
query.handleCriterion(ctx, boolCriterionHandler(sceneFilter.Organized, "scenes.organized", nil))
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
|
||||
details = "details"
|
||||
director = "director"
|
||||
url = "url"
|
||||
rating = 3
|
||||
rating = 60
|
||||
ocounter = 5
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
@@ -304,7 +304,7 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
|
||||
details = "details"
|
||||
director = "director"
|
||||
url = "url"
|
||||
rating = 3
|
||||
rating = 60
|
||||
ocounter = 5
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
@@ -512,7 +512,7 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
|
||||
details = "details"
|
||||
director = "director"
|
||||
url = "url"
|
||||
rating = 3
|
||||
rating = 60
|
||||
ocounter = 5
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
@@ -2295,7 +2295,7 @@ func TestSceneQueryPathAndRating(t *testing.T) {
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
And: &models.SceneFilterType{
|
||||
Rating: &models.IntCriterionInput{
|
||||
Rating100: &models.IntCriterionInput{
|
||||
Value: sceneRating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
@@ -2335,7 +2335,7 @@ func TestSceneQueryPathNotRating(t *testing.T) {
|
||||
sceneFilter := models.SceneFilterType{
|
||||
Path: &pathCriterion,
|
||||
Not: &models.SceneFilterType{
|
||||
Rating: &ratingCriterion,
|
||||
Rating100: &ratingCriterion,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2522,25 +2522,25 @@ func TestSceneQueryRating(t *testing.T) {
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
}
|
||||
|
||||
verifyScenesRating(t, ratingCriterion)
|
||||
verifyScenesLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||
verifyScenesRating(t, ratingCriterion)
|
||||
verifyScenesLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||
verifyScenesRating(t, ratingCriterion)
|
||||
verifyScenesLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
||||
verifyScenesRating(t, ratingCriterion)
|
||||
verifyScenesLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
||||
verifyScenesRating(t, ratingCriterion)
|
||||
verifyScenesLegacyRating(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotNull
|
||||
verifyScenesRating(t, ratingCriterion)
|
||||
verifyScenesLegacyRating(t, ratingCriterion)
|
||||
}
|
||||
|
||||
func verifyScenesRating(t *testing.T, ratingCriterion models.IntCriterionInput) {
|
||||
func verifyScenesLegacyRating(t *testing.T, ratingCriterion models.IntCriterionInput) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
sqb := db.Scene
|
||||
sceneFilter := models.SceneFilterType{
|
||||
@@ -2549,6 +2549,51 @@ func verifyScenesRating(t *testing.T, ratingCriterion models.IntCriterionInput)
|
||||
|
||||
scenes := queryScene(ctx, t, sqb, &sceneFilter, nil)
|
||||
|
||||
// convert criterion value to the 100 value
|
||||
ratingCriterion.Value = models.Rating5To100(ratingCriterion.Value)
|
||||
|
||||
for _, scene := range scenes {
|
||||
verifyIntPtr(t, scene.Rating, ratingCriterion)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestSceneQueryRating100(t *testing.T) {
|
||||
const rating = 60
|
||||
ratingCriterion := models.IntCriterionInput{
|
||||
Value: rating,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
}
|
||||
|
||||
verifyScenesRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||
verifyScenesRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||
verifyScenesRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
||||
verifyScenesRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
||||
verifyScenesRating100(t, ratingCriterion)
|
||||
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotNull
|
||||
verifyScenesRating100(t, ratingCriterion)
|
||||
}
|
||||
|
||||
func verifyScenesRating100(t *testing.T, ratingCriterion models.IntCriterionInput) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
sqb := db.Scene
|
||||
sceneFilter := models.SceneFilterType{
|
||||
Rating100: &ratingCriterion,
|
||||
}
|
||||
|
||||
scenes := queryScene(ctx, t, sqb, &sceneFilter, nil)
|
||||
|
||||
for _, scene := range scenes {
|
||||
verifyIntPtr(t, scene.Rating, ratingCriterion)
|
||||
}
|
||||
|
||||
@@ -823,7 +823,7 @@ func getSceneTitle(index int) string {
|
||||
|
||||
func getRating(index int) sql.NullInt64 {
|
||||
rating := index % 6
|
||||
return sql.NullInt64{Int64: int64(rating), Valid: rating > 0}
|
||||
return sql.NullInt64{Int64: int64(rating * 20), Valid: rating > 0}
|
||||
}
|
||||
|
||||
func getIntPtr(r sql.NullInt64) *int {
|
||||
@@ -967,11 +967,13 @@ func makeScene(i int) *models.Scene {
|
||||
}
|
||||
}
|
||||
|
||||
rating := getRating(i)
|
||||
|
||||
return &models.Scene{
|
||||
Title: title,
|
||||
Details: details,
|
||||
URL: getSceneEmptyString(i, urlField),
|
||||
Rating: getIntPtr(getRating(i)),
|
||||
Rating: getIntPtr(rating),
|
||||
OCounter: getOCounter(i),
|
||||
Date: getObjectDateObject(i),
|
||||
StudioID: studioID,
|
||||
|
||||
@@ -234,7 +234,9 @@ func (qb *studioQueryBuilder) makeFilter(ctx context.Context, studioFilter *mode
|
||||
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.Name, studioTable+".name"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.Details, studioTable+".details"))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.URL, studioTable+".url"))
|
||||
query.handleCriterion(ctx, intCriterionHandler(studioFilter.Rating, studioTable+".rating", nil))
|
||||
query.handleCriterion(ctx, intCriterionHandler(studioFilter.Rating100, studioTable+".rating", nil))
|
||||
// legacy rating handler
|
||||
query.handleCriterion(ctx, rating5CriterionHandler(studioFilter.Rating, studioTable+".rating", nil))
|
||||
query.handleCriterion(ctx, boolCriterionHandler(studioFilter.IgnoreAutoTag, studioTable+".ignore_auto_tag", nil))
|
||||
|
||||
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"no-descending-specificity": null,
|
||||
"no-invalid-double-slash-comments": true,
|
||||
"no-missing-end-of-source-newline": true,
|
||||
"number-max-precision": 2,
|
||||
"number-max-precision": 3,
|
||||
"number-no-trailing-zeros": true,
|
||||
"order/order": [
|
||||
"custom-properties",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"build-ci": "yarn validate && yarn build",
|
||||
"validate": "yarn lint && yarn format-check && tsc --noEmit",
|
||||
"validate": "yarn lint && tsc --noEmit && yarn format-check",
|
||||
"lint": "yarn lint:css && yarn lint:js",
|
||||
"lint:js": "eslint --cache src/**/*.{ts,tsx}",
|
||||
"lint:css": "stylelint \"src/**/*.scss\"",
|
||||
|
||||
0
ui/v2.5/src/App.tsx
Executable file → Normal file
0
ui/v2.5/src/App.tsx
Executable file → Normal file
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from "react";
|
||||
import React, { useContext, useMemo } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import {
|
||||
FrontPageContent,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "src/core/config";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useFindSavedFilter } from "src/core/StashService";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { GalleryRecommendationRow } from "../Galleries/GalleryRecommendationRow";
|
||||
import { ImageRecommendationRow } from "../Images/ImageRecommendationRow";
|
||||
@@ -98,6 +99,7 @@ interface ISavedFilterResults {
|
||||
const SavedFilterResults: React.FC<ISavedFilterResults> = ({
|
||||
savedFilterID,
|
||||
}) => {
|
||||
const { configuration: config } = useContext(ConfigurationContext);
|
||||
const { loading, data } = useFindSavedFilter(savedFilterID.toString());
|
||||
|
||||
const filter = useMemo(() => {
|
||||
@@ -105,12 +107,12 @@ const SavedFilterResults: React.FC<ISavedFilterResults> = ({
|
||||
|
||||
const { mode, filter: filterJSON } = data.findSavedFilter;
|
||||
|
||||
const ret = new ListFilterModel(mode);
|
||||
const ret = new ListFilterModel(mode, config);
|
||||
ret.currentPage = 1;
|
||||
ret.configureFromJSON(filterJSON);
|
||||
ret.randomSeed = -1;
|
||||
return ret;
|
||||
}, [data?.findSavedFilter]);
|
||||
}, [data?.findSavedFilter, config]);
|
||||
|
||||
if (loading || !data?.findSavedFilter || !filter) {
|
||||
return <></>;
|
||||
@@ -128,18 +130,19 @@ interface ICustomFilterProps {
|
||||
const CustomFilterResults: React.FC<ICustomFilterProps> = ({
|
||||
customFilter,
|
||||
}) => {
|
||||
const { configuration: config } = useContext(ConfigurationContext);
|
||||
const intl = useIntl();
|
||||
|
||||
const filter = useMemo(() => {
|
||||
const itemsPerPage = 25;
|
||||
const ret = new ListFilterModel(customFilter.mode);
|
||||
const ret = new ListFilterModel(customFilter.mode, config);
|
||||
ret.sortBy = customFilter.sortBy;
|
||||
ret.sortDirection = customFilter.direction;
|
||||
ret.itemsPerPage = itemsPerPage;
|
||||
ret.currentPage = 1;
|
||||
ret.randomSeed = -1;
|
||||
return ret;
|
||||
}, [customFilter]);
|
||||
}, [customFilter, config]);
|
||||
|
||||
const header = customFilter.message
|
||||
? intl.formatMessage(
|
||||
|
||||
@@ -8,7 +8,7 @@ import { StudioSelect, Modal } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { FormUtils } from "src/utils";
|
||||
import MultiSet from "../Shared/MultiSet";
|
||||
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
import {
|
||||
getAggregateInputIDs,
|
||||
getAggregateInputValue,
|
||||
@@ -29,7 +29,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const [rating, setRating] = useState<number>();
|
||||
const [rating100, setRating] = useState<number>();
|
||||
const [studioId, setStudioId] = useState<string>();
|
||||
const [
|
||||
performerMode,
|
||||
@@ -64,7 +64,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
||||
}),
|
||||
};
|
||||
|
||||
galleryInput.rating = getAggregateInputValue(rating, aggregateRating);
|
||||
galleryInput.rating100 = getAggregateInputValue(rating100, aggregateRating);
|
||||
galleryInput.studio_id = getAggregateInputValue(
|
||||
studioId,
|
||||
aggregateStudioId
|
||||
@@ -121,7 +121,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
||||
let first = true;
|
||||
|
||||
state.forEach((gallery: GQL.SlimGalleryDataFragment) => {
|
||||
const galleryRating = gallery.rating;
|
||||
const galleryRating = gallery.rating100;
|
||||
const GalleriestudioID = gallery?.studio?.id;
|
||||
const galleryPerformerIDs = (gallery.performers ?? [])
|
||||
.map((p) => p.id)
|
||||
@@ -256,14 +256,13 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
value={rating}
|
||||
<RatingSystem
|
||||
value={rating100}
|
||||
onSetRating={(value) => setRating(value)}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "studio" }),
|
||||
|
||||
@@ -160,7 +160,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
|
||||
src={`${props.gallery.cover.paths.thumbnail}`}
|
||||
/>
|
||||
) : undefined}
|
||||
<RatingBanner rating={props.gallery.rating} />
|
||||
<RatingBanner rating={props.gallery.rating100} />
|
||||
</>
|
||||
}
|
||||
overlays={maybeRenderSceneStudioOverlay()}
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as GQL from "src/core/generated-graphql";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { TagLink, TruncatedText } from "src/components/Shared";
|
||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { sortPerformers } from "src/core/performers";
|
||||
import { galleryTitle } from "src/core/galleries";
|
||||
|
||||
@@ -94,10 +94,10 @@ export const GalleryDetailPanel: React.FC<IGalleryDetailProps> = ({
|
||||
/>
|
||||
</h5>
|
||||
) : undefined}
|
||||
{gallery.rating ? (
|
||||
{gallery.rating100 ? (
|
||||
<h6>
|
||||
<FormattedMessage id="rating" />:{" "}
|
||||
<RatingStars value={gallery.rating} />
|
||||
<RatingSystem value={gallery.rating100} disabled />
|
||||
</h6>
|
||||
) : (
|
||||
""
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
import { useToast } from "src/hooks";
|
||||
import { useFormik } from "formik";
|
||||
import { FormUtils } from "src/utils";
|
||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { GalleryScrapeDialog } from "./GalleryScrapeDialog";
|
||||
import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { galleryTitle } from "src/core/galleries";
|
||||
@@ -89,7 +89,7 @@ export const GalleryEditPanel: React.FC<
|
||||
details: yup.string().optional().nullable(),
|
||||
url: yup.string().optional().nullable(),
|
||||
date: yup.string().optional().nullable(),
|
||||
rating: yup.number().optional().nullable(),
|
||||
rating100: yup.number().optional().nullable(),
|
||||
studio_id: yup.string().optional().nullable(),
|
||||
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
tag_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
@@ -101,7 +101,7 @@ export const GalleryEditPanel: React.FC<
|
||||
details: gallery?.details ?? "",
|
||||
url: gallery?.url ?? "",
|
||||
date: gallery?.date ?? "",
|
||||
rating: gallery?.rating ?? null,
|
||||
rating100: gallery?.rating100 ?? null,
|
||||
studio_id: gallery?.studio?.id,
|
||||
performer_ids: (gallery?.performers ?? []).map((p) => p.id),
|
||||
tag_ids: (gallery?.tags ?? []).map((t) => t.id),
|
||||
@@ -117,7 +117,7 @@ export const GalleryEditPanel: React.FC<
|
||||
});
|
||||
|
||||
function setRating(v: number) {
|
||||
formik.setFieldValue("rating", v);
|
||||
formik.setFieldValue("rating100", v);
|
||||
}
|
||||
|
||||
interface ISceneSelectValue {
|
||||
@@ -150,11 +150,11 @@ export const GalleryEditPanel: React.FC<
|
||||
}
|
||||
|
||||
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));
|
||||
Mousetrap.bind("1", () => setRating(20));
|
||||
Mousetrap.bind("2", () => setRating(40));
|
||||
Mousetrap.bind("3", () => setRating(60));
|
||||
Mousetrap.bind("4", () => setRating(80));
|
||||
Mousetrap.bind("5", () => setRating(100));
|
||||
|
||||
setTimeout(() => {
|
||||
Mousetrap.unbind("0");
|
||||
@@ -483,15 +483,14 @@ export const GalleryEditPanel: React.FC<
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
value={formik.values.rating ?? undefined}
|
||||
<RatingSystem
|
||||
value={formik.values.rating100 ?? undefined}
|
||||
onSetRating={(value) =>
|
||||
formik.setFieldValue("rating", value ?? null)
|
||||
formik.setFieldValue("rating100", value ?? null)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "studio" }),
|
||||
|
||||
@@ -2,10 +2,11 @@ import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { RatingStars, TruncatedText } from "src/components/Shared";
|
||||
import { TruncatedText } from "src/components/Shared";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { useGalleryLightbox } from "src/hooks";
|
||||
import { galleryTitle } from "src/core/galleries";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
|
||||
const CLASSNAME = "GalleryWallCard";
|
||||
const CLASSNAME_FOOTER = `${CLASSNAME}-footer`;
|
||||
@@ -45,7 +46,7 @@ const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<RatingStars rating={gallery.rating} />
|
||||
<RatingSystem value={gallery.rating100 ?? undefined} disabled />
|
||||
<img src={cover} alt="" className={CLASSNAME_IMG} />
|
||||
<footer className={CLASSNAME_FOOTER}>
|
||||
<Link
|
||||
|
||||
@@ -204,16 +204,25 @@ $galleryTabWidth: 450px;
|
||||
}
|
||||
}
|
||||
|
||||
.RatingStars {
|
||||
.rating-stars,
|
||||
.rating-number {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
&-unfilled {
|
||||
.rating-stars {
|
||||
.star-fill-0 .unfilled-star {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-filled {
|
||||
.star-fill-25 .unfilled-star,
|
||||
.star-fill-50 .unfilled-star,
|
||||
.star-fill-75 .unfilled-star {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.filled-star {
|
||||
filter: drop-shadow(1px 1px 1px #222);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { StudioSelect, Modal } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { FormUtils } from "src/utils";
|
||||
import MultiSet from "../Shared/MultiSet";
|
||||
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
import {
|
||||
getAggregateInputIDs,
|
||||
getAggregateInputValue,
|
||||
@@ -29,7 +29,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const [rating, setRating] = useState<number>();
|
||||
const [rating100, setRating] = useState<number>();
|
||||
const [studioId, setStudioId] = useState<string>();
|
||||
const [
|
||||
performerMode,
|
||||
@@ -64,7 +64,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
}),
|
||||
};
|
||||
|
||||
imageInput.rating = getAggregateInputValue(rating, aggregateRating);
|
||||
imageInput.rating100 = getAggregateInputValue(rating100, aggregateRating);
|
||||
imageInput.studio_id = getAggregateInputValue(studioId, aggregateStudioId);
|
||||
|
||||
imageInput.performer_ids = getAggregateInputIDs(
|
||||
@@ -112,7 +112,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
let first = true;
|
||||
|
||||
state.forEach((image: GQL.SlimImageDataFragment) => {
|
||||
const imageRating = image.rating;
|
||||
const imageRating = image.rating100;
|
||||
const imageStudioID = image?.studio?.id;
|
||||
const imagePerformerIDs = (image.performers ?? [])
|
||||
.map((p) => p.id)
|
||||
@@ -246,14 +246,13 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
value={rating}
|
||||
<RatingSystem
|
||||
value={rating100}
|
||||
onSetRating={(value) => setRating(value)}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "studio" }),
|
||||
|
||||
@@ -157,7 +157,7 @@ export const ImageCard: React.FC<IImageCardProps> = (
|
||||
</div>
|
||||
) : undefined}
|
||||
</div>
|
||||
<RatingBanner rating={props.image.rating} />
|
||||
<RatingBanner rating={props.image.rating100} />
|
||||
</>
|
||||
}
|
||||
popovers={maybeRenderPopoverButtonGroup()}
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as GQL from "src/core/generated-graphql";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { TagLink, TruncatedText } from "src/components/Shared";
|
||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { sortPerformers } from "src/core/performers";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { objectTitle } from "src/core/files";
|
||||
@@ -91,10 +91,10 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = (props) => {
|
||||
<TruncatedText text={objectTitle(props.image)} />
|
||||
</h3>
|
||||
</div>
|
||||
{props.image.rating ? (
|
||||
{props.image.rating100 ? (
|
||||
<h6>
|
||||
<FormattedMessage id="rating" />:{" "}
|
||||
<RatingStars value={props.image.rating} />
|
||||
<RatingSystem value={props.image.rating100} disabled />
|
||||
</h6>
|
||||
) : (
|
||||
""
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useToast } from "src/hooks";
|
||||
import { FormUtils } from "src/utils";
|
||||
import { useFormik } from "formik";
|
||||
import { Prompt } from "react-router-dom";
|
||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
|
||||
interface IProps {
|
||||
image: GQL.ImageDataFragment;
|
||||
@@ -38,7 +38,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
|
||||
const schema = yup.object({
|
||||
title: yup.string().optional().nullable(),
|
||||
rating: yup.number().optional().nullable(),
|
||||
rating100: yup.number().optional().nullable(),
|
||||
studio_id: yup.string().optional().nullable(),
|
||||
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
tag_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
@@ -46,7 +46,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
|
||||
const initialValues = {
|
||||
title: image.title ?? "",
|
||||
rating: image.rating ?? null,
|
||||
rating100: image.rating100 ?? null,
|
||||
studio_id: image.studio?.id,
|
||||
performer_ids: (image.performers ?? []).map((p) => p.id),
|
||||
tag_ids: (image.tags ?? []).map((t) => t.id),
|
||||
@@ -61,7 +61,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
});
|
||||
|
||||
function setRating(v: number) {
|
||||
formik.setFieldValue("rating", v);
|
||||
formik.setFieldValue("rating100", v);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -81,11 +81,11 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
}
|
||||
|
||||
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));
|
||||
Mousetrap.bind("1", () => setRating(20));
|
||||
Mousetrap.bind("2", () => setRating(40));
|
||||
Mousetrap.bind("3", () => setRating(60));
|
||||
Mousetrap.bind("4", () => setRating(80));
|
||||
Mousetrap.bind("5", () => setRating(100));
|
||||
|
||||
setTimeout(() => {
|
||||
Mousetrap.unbind("0");
|
||||
@@ -194,15 +194,14 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
value={formik.values.rating ?? undefined}
|
||||
<RatingSystem
|
||||
value={formik.values.rating100 ?? undefined}
|
||||
onSetRating={(value) =>
|
||||
formik.setFieldValue("rating", value ?? null)
|
||||
formik.setFieldValue("rating100", value ?? null)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "studio" }),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import cloneDeep from "lodash-es/cloneDeep";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import { Button, Form, Modal } from "react-bootstrap";
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import {
|
||||
@@ -36,6 +36,9 @@ import { DateFilter } from "./Filters/DateFilter";
|
||||
import { TimestampFilter } from "./Filters/TimestampFilter";
|
||||
import { CountryCriterion } from "src/models/list-filter/criteria/country";
|
||||
import { CountrySelect } from "../Shared";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { RatingCriterion } from "../../models/list-filter/criteria/rating";
|
||||
import { RatingFilter } from "./Filters/RatingFilter";
|
||||
|
||||
interface IAddFilterProps {
|
||||
onAddCriterion: (
|
||||
@@ -63,17 +66,18 @@ export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
||||
const { options, modifierOptions } = criterion.criterionOption;
|
||||
|
||||
const valueStage = useRef<CriterionValue>(criterion.value);
|
||||
const { configuration: config } = useContext(ConfigurationContext);
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
// Configure if we are editing an existing criterion
|
||||
useEffect(() => {
|
||||
if (!editingCriterion) {
|
||||
setCriterion(makeCriteria());
|
||||
setCriterion(makeCriteria(config));
|
||||
} else {
|
||||
setCriterion(editingCriterion);
|
||||
}
|
||||
}, [editingCriterion]);
|
||||
}, [config, editingCriterion]);
|
||||
|
||||
useEffect(() => {
|
||||
valueStage.current = criterion.value;
|
||||
@@ -81,7 +85,7 @@ export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
||||
|
||||
function onChangedCriteriaType(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const newCriterionType = event.target.value as CriterionType;
|
||||
const newCriterion = makeCriteria(newCriterionType);
|
||||
const newCriterion = makeCriteria(config, newCriterionType);
|
||||
setCriterion(newCriterion);
|
||||
}
|
||||
|
||||
@@ -196,6 +200,15 @@ export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
||||
<NumberFilter criterion={criterion} onValueChanged={onValueChanged} />
|
||||
);
|
||||
}
|
||||
if (criterion instanceof RatingCriterion) {
|
||||
return (
|
||||
<RatingFilter
|
||||
criterion={criterion}
|
||||
onValueChanged={onValueChanged}
|
||||
configuration={config}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (
|
||||
criterion instanceof CountryCriterion &&
|
||||
(criterion.modifier === CriterionModifier.Equals ||
|
||||
|
||||
135
ui/v2.5/src/components/List/Filters/RatingFilter.tsx
Normal file
135
ui/v2.5/src/components/List/Filters/RatingFilter.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import React, { useRef } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
||||
import { INumberValue } from "../../../models/list-filter/types";
|
||||
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import {
|
||||
convertFromRatingFormat,
|
||||
convertToRatingFormat,
|
||||
defaultRatingSystemOptions,
|
||||
} from "src/utils/rating";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { IUIConfig } from "src/core/config";
|
||||
|
||||
interface IDurationFilterProps {
|
||||
criterion: Criterion<INumberValue>;
|
||||
onValueChanged: (value: INumberValue) => void;
|
||||
configuration: GQL.ConfigDataFragment | undefined;
|
||||
}
|
||||
|
||||
export const RatingFilter: React.FC<IDurationFilterProps> = ({
|
||||
criterion,
|
||||
onValueChanged,
|
||||
configuration,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const ratingSystem =
|
||||
(configuration?.ui as IUIConfig)?.ratingSystemOptions ??
|
||||
defaultRatingSystemOptions;
|
||||
|
||||
const valueStage = useRef<INumberValue>(criterion.value);
|
||||
|
||||
function onChanged(
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
property: "value" | "value2"
|
||||
) {
|
||||
const value = parseInt(event.target.value, 10);
|
||||
valueStage.current[property] = !Number.isNaN(value)
|
||||
? convertFromRatingFormat(value, ratingSystem.type)
|
||||
: 0;
|
||||
}
|
||||
|
||||
function onBlurInput() {
|
||||
onValueChanged(valueStage.current);
|
||||
}
|
||||
|
||||
let equalsControl: JSX.Element | null = null;
|
||||
if (
|
||||
criterion.modifier === CriterionModifier.Equals ||
|
||||
criterion.modifier === CriterionModifier.NotEquals
|
||||
) {
|
||||
equalsControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
className="btn-secondary"
|
||||
type="number"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChanged(e, "value")
|
||||
}
|
||||
onBlur={onBlurInput}
|
||||
defaultValue={
|
||||
convertToRatingFormat(criterion.value?.value, ratingSystem) ?? ""
|
||||
}
|
||||
placeholder={intl.formatMessage({ id: "criterion.value" })}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
let lowerControl: JSX.Element | null = null;
|
||||
if (
|
||||
criterion.modifier === CriterionModifier.GreaterThan ||
|
||||
criterion.modifier === CriterionModifier.Between ||
|
||||
criterion.modifier === CriterionModifier.NotBetween
|
||||
) {
|
||||
lowerControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
className="btn-secondary"
|
||||
type="number"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChanged(e, "value")
|
||||
}
|
||||
onBlur={onBlurInput}
|
||||
defaultValue={
|
||||
convertToRatingFormat(criterion.value?.value, ratingSystem) ?? ""
|
||||
}
|
||||
placeholder={intl.formatMessage({ id: "criterion.greater_than" })}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
let upperControl: JSX.Element | null = null;
|
||||
if (
|
||||
criterion.modifier === CriterionModifier.LessThan ||
|
||||
criterion.modifier === CriterionModifier.Between ||
|
||||
criterion.modifier === CriterionModifier.NotBetween
|
||||
) {
|
||||
upperControl = (
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
className="btn-secondary"
|
||||
type="number"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChanged(
|
||||
e,
|
||||
criterion.modifier === CriterionModifier.LessThan
|
||||
? "value"
|
||||
: "value2"
|
||||
)
|
||||
}
|
||||
onBlur={onBlurInput}
|
||||
defaultValue={
|
||||
convertToRatingFormat(
|
||||
criterion.modifier === CriterionModifier.LessThan
|
||||
? criterion.value?.value
|
||||
: criterion.value?.value2,
|
||||
ratingSystem
|
||||
) ?? ""
|
||||
}
|
||||
placeholder={intl.formatMessage({ id: "criterion.less_than" })}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{equalsControl}
|
||||
{lowerControl}
|
||||
{upperControl}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import * as GQL from "src/core/generated-graphql";
|
||||
import { Modal, StudioSelect } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { FormUtils } from "src/utils";
|
||||
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
import {
|
||||
getAggregateInputValue,
|
||||
getAggregateRating,
|
||||
@@ -24,7 +24,7 @@ export const EditMoviesDialog: React.FC<IListOperationProps> = (
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const [rating, setRating] = useState<number | undefined>();
|
||||
const [rating100, setRating] = useState<number | undefined>();
|
||||
const [studioId, setStudioId] = useState<string | undefined>();
|
||||
const [director, setDirector] = useState<string | undefined>();
|
||||
|
||||
@@ -42,7 +42,7 @@ export const EditMoviesDialog: React.FC<IListOperationProps> = (
|
||||
};
|
||||
|
||||
// if rating is undefined
|
||||
movieInput.rating = getAggregateInputValue(rating, aggregateRating);
|
||||
movieInput.rating100 = getAggregateInputValue(rating100, aggregateRating);
|
||||
movieInput.studio_id = getAggregateInputValue(studioId, aggregateStudioId);
|
||||
|
||||
return movieInput;
|
||||
@@ -77,11 +77,11 @@ export const EditMoviesDialog: React.FC<IListOperationProps> = (
|
||||
state.forEach((movie: GQL.MovieDataFragment) => {
|
||||
if (first) {
|
||||
first = false;
|
||||
updateRating = movie.rating ?? undefined;
|
||||
updateRating = movie.rating100 ?? undefined;
|
||||
updateStudioId = movie.studio?.id ?? undefined;
|
||||
updateDirector = movie.director ?? undefined;
|
||||
} else {
|
||||
if (movie.rating !== updateRating) {
|
||||
if (movie.rating100 !== updateRating) {
|
||||
updateRating = undefined;
|
||||
}
|
||||
if (movie.studio?.id !== updateStudioId) {
|
||||
@@ -124,8 +124,8 @@ export const EditMoviesDialog: React.FC<IListOperationProps> = (
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
value={rating}
|
||||
<RatingSystem
|
||||
value={rating100}
|
||||
onSetRating={(value) => setRating(value)}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
|
||||
@@ -82,7 +82,7 @@ export const MovieCard: FunctionComponent<IProps> = (props: IProps) => {
|
||||
alt={props.movie.name ?? ""}
|
||||
src={props.movie.front_image_path ?? ""}
|
||||
/>
|
||||
<RatingBanner rating={props.movie.rating} />
|
||||
<RatingBanner rating={props.movie.rating100} />
|
||||
</>
|
||||
}
|
||||
details={
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { DurationUtils, TextUtils } from "src/utils";
|
||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { TextField, URLField } from "src/utils/field";
|
||||
|
||||
interface IMovieDetailsPanel {
|
||||
@@ -27,7 +27,7 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({ movie }) => {
|
||||
}
|
||||
|
||||
function renderRatingField() {
|
||||
if (!movie.rating) {
|
||||
if (!movie.rating100) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({ movie }) => {
|
||||
<>
|
||||
<dt>{intl.formatMessage({ id: "rating" })}</dt>
|
||||
<dd>
|
||||
<RatingStars value={movie.rating} disabled />
|
||||
<RatingSystem value={movie.rating100} disabled />
|
||||
</dd>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { useToast } from "src/hooks";
|
||||
import { Modal as BSModal, Form, Button, Col, Row } from "react-bootstrap";
|
||||
import { DurationUtils, FormUtils, ImageUtils } from "src/utils";
|
||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { useFormik } from "formik";
|
||||
import { Prompt } from "react-router-dom";
|
||||
import { MovieScrapeDialog } from "./MovieScrapeDialog";
|
||||
@@ -69,7 +69,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
.optional()
|
||||
.nullable()
|
||||
.matches(/^\d{4}-\d{2}-\d{2}$/),
|
||||
rating: yup.number().optional().nullable(),
|
||||
rating100: yup.number().optional().nullable(),
|
||||
studio_id: yup.string().optional().nullable(),
|
||||
director: yup.string().optional().nullable(),
|
||||
synopsis: yup.string().optional().nullable(),
|
||||
@@ -83,7 +83,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
aliases: movie?.aliases,
|
||||
duration: movie?.duration,
|
||||
date: movie?.date,
|
||||
rating: movie?.rating ?? null,
|
||||
rating100: movie?.rating100 ?? null,
|
||||
studio_id: movie?.studio?.id,
|
||||
director: movie?.director,
|
||||
synopsis: movie?.synopsis,
|
||||
@@ -116,17 +116,17 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
]);
|
||||
|
||||
function setRating(v: number) {
|
||||
formik.setFieldValue("rating", v);
|
||||
formik.setFieldValue("rating100", v);
|
||||
}
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("r 0", () => setRating(NaN));
|
||||
Mousetrap.bind("r 1", () => setRating(1));
|
||||
Mousetrap.bind("r 2", () => setRating(2));
|
||||
Mousetrap.bind("r 3", () => setRating(3));
|
||||
Mousetrap.bind("r 4", () => setRating(4));
|
||||
Mousetrap.bind("r 5", () => setRating(5));
|
||||
Mousetrap.bind("r 1", () => setRating(20));
|
||||
Mousetrap.bind("r 2", () => setRating(40));
|
||||
Mousetrap.bind("r 3", () => setRating(60));
|
||||
Mousetrap.bind("r 4", () => setRating(80));
|
||||
Mousetrap.bind("r 5", () => setRating(100));
|
||||
// Mousetrap.bind("u", (e) => {
|
||||
// setStudioFocus()
|
||||
// e.preventDefault();
|
||||
@@ -164,7 +164,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
function getMovieInput(values: InputValues) {
|
||||
const input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput> = {
|
||||
...values,
|
||||
rating: values.rating ?? null,
|
||||
rating100: values.rating100 ?? null,
|
||||
studio_id: values.studio_id ?? null,
|
||||
};
|
||||
|
||||
@@ -432,15 +432,14 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
value={formik.values.rating ?? undefined}
|
||||
<RatingSystem
|
||||
value={formik.values.rating100 ?? undefined}
|
||||
onSetRating={(value) =>
|
||||
formik.setFieldValue("rating", value ?? null)
|
||||
formik.setFieldValue("rating100", value ?? null)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="url" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "url" }),
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Form, Col, Row } from "react-bootstrap";
|
||||
import { Col, Form, Row } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useBulkPerformerUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Modal } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { FormUtils } from "src/utils";
|
||||
import MultiSet from "../Shared/MultiSet";
|
||||
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
import {
|
||||
getAggregateInputValue,
|
||||
getAggregateState,
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
|
||||
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
|
||||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FormUtils } from "../../utils";
|
||||
|
||||
interface IListOperationProps {
|
||||
selected: GQL.SlimPerformerDataFragment[];
|
||||
@@ -32,7 +32,7 @@ const performerFields = [
|
||||
"url",
|
||||
"instagram",
|
||||
"twitter",
|
||||
"rating",
|
||||
"rating100",
|
||||
"gender",
|
||||
"birthdate",
|
||||
"death_date",
|
||||
@@ -90,9 +90,9 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
|
||||
// we don't have unset functionality for the rating star control
|
||||
// so need to determine if we are setting a rating or not
|
||||
performerInput.rating = getAggregateInputValue(
|
||||
updateInput.rating,
|
||||
aggregateState.rating
|
||||
performerInput.rating100 = getAggregateInputValue(
|
||||
updateInput.rating100,
|
||||
aggregateState.rating100
|
||||
);
|
||||
|
||||
// gender dropdown doesn't have unset functionality
|
||||
@@ -205,9 +205,9 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
value={updateInput.rating ?? undefined}
|
||||
onSetRating={(value) => setUpdateField({ rating: value })}
|
||||
<RatingSystem
|
||||
value={updateInput.rating100 ?? undefined}
|
||||
onSetRating={(value) => setUpdateField({ rating100: value })}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { NavUtils, TextUtils } from "src/utils";
|
||||
import {
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { PopoverCountButton } from "../Shared/PopoverCountButton";
|
||||
import GenderIcon from "./GenderIcon";
|
||||
import { faHeart, faTag } from "@fortawesome/free-solid-svg-icons";
|
||||
import { RatingBanner } from "../Shared/RatingBanner";
|
||||
|
||||
export interface IPerformerCardExtraCriteria {
|
||||
scenes: Criterion<CriterionValue>[];
|
||||
@@ -167,18 +168,10 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
||||
}
|
||||
|
||||
function maybeRenderRatingBanner() {
|
||||
if (!performer.rating) {
|
||||
if (!performer.rating100) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`rating-banner ${
|
||||
performer.rating ? `rating-${performer.rating}` : ""
|
||||
}`}
|
||||
>
|
||||
<FormattedMessage id="rating" />: {performer.rating}
|
||||
</div>
|
||||
);
|
||||
return <RatingBanner rating={performer.rating100} />;
|
||||
}
|
||||
|
||||
function maybeRenderFlag() {
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
import { useLightbox, useToast } from "src/hooks";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
|
||||
import { PerformerScenesPanel } from "./PerformerScenesPanel";
|
||||
import { PerformerGalleriesPanel } from "./PerformerGalleriesPanel";
|
||||
@@ -127,11 +127,11 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||
}
|
||||
|
||||
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));
|
||||
Mousetrap.bind("1", () => setRating(20));
|
||||
Mousetrap.bind("2", () => setRating(40));
|
||||
Mousetrap.bind("3", () => setRating(60));
|
||||
Mousetrap.bind("4", () => setRating(80));
|
||||
Mousetrap.bind("5", () => setRating(100));
|
||||
|
||||
setTimeout(() => {
|
||||
Mousetrap.unbind("0");
|
||||
@@ -327,7 +327,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||
variables: {
|
||||
input: {
|
||||
id: performer.id,
|
||||
rating: v,
|
||||
rating100: v,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -428,8 +428,8 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||
{performer.name}
|
||||
{renderClickableIcons()}
|
||||
</h2>
|
||||
<RatingStars
|
||||
value={performer.rating ?? undefined}
|
||||
<RatingSystem
|
||||
value={performer.rating100 ?? undefined}
|
||||
onSetRating={(value) => setRating(value ?? null)}
|
||||
/>
|
||||
{maybeRenderAliases()}
|
||||
|
||||
@@ -13,7 +13,7 @@ export class ParserField {
|
||||
|
||||
static Title = new ParserField("title");
|
||||
static Ext = new ParserField("ext", "File extension");
|
||||
static Rating = new ParserField("rating");
|
||||
static Rating = new ParserField("rating100");
|
||||
|
||||
static I = new ParserField("i", "Matches any ignored word");
|
||||
static D = new ParserField("d", "Matches any delimiter (.-_)");
|
||||
|
||||
@@ -77,7 +77,7 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
ParserField.fullDateFields.some((f) => {
|
||||
return pattern.includes(`{${f.field}}`);
|
||||
});
|
||||
const ratingSet = pattern.includes("{rating}");
|
||||
const ratingSet = pattern.includes("{rating100}");
|
||||
const performerSet = pattern.includes("{performer}");
|
||||
const tagSet = pattern.includes("{tag}");
|
||||
const studioSet = pattern.includes("{studio}");
|
||||
|
||||
@@ -54,7 +54,7 @@ export class SceneParserResult {
|
||||
this.filename = objectTitle(this.scene);
|
||||
this.title.setOriginalValue(this.scene.title ?? undefined);
|
||||
this.date.setOriginalValue(this.scene.date ?? undefined);
|
||||
this.rating.setOriginalValue(this.scene.rating ?? undefined);
|
||||
this.rating.setOriginalValue(this.scene.rating100 ?? undefined);
|
||||
this.performers.setOriginalValue(this.scene.performers.map((p) => p.id));
|
||||
this.tags.setOriginalValue(this.scene.tags.map((t) => t.id));
|
||||
this.studio.setOriginalValue(this.scene.studio?.id);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { StudioSelect, Modal } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { FormUtils } from "src/utils";
|
||||
import MultiSet from "../Shared/MultiSet";
|
||||
import { RatingStars } from "./SceneDetails/RatingStars";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
import {
|
||||
getAggregateInputIDs,
|
||||
getAggregateInputValue,
|
||||
@@ -30,7 +30,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const [rating, setRating] = useState<number>();
|
||||
const [rating100, setRating] = useState<number>();
|
||||
const [studioId, setStudioId] = useState<string>();
|
||||
const [
|
||||
performerMode,
|
||||
@@ -71,7 +71,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
||||
}),
|
||||
};
|
||||
|
||||
sceneInput.rating = getAggregateInputValue(rating, aggregateRating);
|
||||
sceneInput.rating100 = getAggregateInputValue(rating100, aggregateRating);
|
||||
sceneInput.studio_id = getAggregateInputValue(studioId, aggregateStudioId);
|
||||
|
||||
sceneInput.performer_ids = getAggregateInputIDs(
|
||||
@@ -121,7 +121,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
||||
let first = true;
|
||||
|
||||
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
||||
const sceneRating = scene.rating;
|
||||
const sceneRating = scene.rating100;
|
||||
const sceneStudioID = scene?.studio?.id;
|
||||
const scenePerformerIDs = (scene.performers ?? [])
|
||||
.map((p) => p.id)
|
||||
@@ -271,14 +271,13 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
value={rating}
|
||||
<RatingSystem
|
||||
value={rating100}
|
||||
onSetRating={(value) => setRating(value)}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "studio" }),
|
||||
|
||||
@@ -401,7 +401,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
||||
isPortrait={isPortrait()}
|
||||
soundActive={configuration?.interface?.soundOnPreview ?? false}
|
||||
/>
|
||||
<RatingBanner rating={props.scene.rating} />
|
||||
<RatingBanner rating={props.scene.rating100} />
|
||||
{maybeRenderSceneSpecsOverlay()}
|
||||
{maybeRenderInteractiveSpeedOverlay()}
|
||||
</>
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import Icon from "src/components/Shared/Icon";
|
||||
import { faStar as fasStar } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faStar as farStar } from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
export interface IRatingStarsProps {
|
||||
value?: number;
|
||||
onSetRating?: (value?: number) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const RatingStars: React.FC<IRatingStarsProps> = (
|
||||
props: IRatingStarsProps
|
||||
) => {
|
||||
const [hoverRating, setHoverRating] = useState<number | undefined>();
|
||||
const disabled = props.disabled || !props.onSetRating;
|
||||
|
||||
function setRating(rating: number) {
|
||||
if (!props.onSetRating) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newRating: number | undefined = rating;
|
||||
|
||||
// unset if we're clicking on the current rating
|
||||
if (props.value === rating) {
|
||||
newRating = undefined;
|
||||
}
|
||||
|
||||
// set the hover rating to undefined so that it doesn't immediately clear
|
||||
// the stars
|
||||
setHoverRating(undefined);
|
||||
|
||||
props.onSetRating(newRating);
|
||||
}
|
||||
|
||||
function getIcon(rating: number) {
|
||||
if (hoverRating && hoverRating >= rating) {
|
||||
if (hoverRating === props.value) {
|
||||
return farStar;
|
||||
}
|
||||
|
||||
return fasStar;
|
||||
}
|
||||
|
||||
if (!hoverRating && props.value && props.value >= rating) {
|
||||
return fasStar;
|
||||
}
|
||||
|
||||
return farStar;
|
||||
}
|
||||
|
||||
function onMouseOver(rating: number) {
|
||||
if (!disabled) {
|
||||
setHoverRating(rating);
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseOut(rating: number) {
|
||||
if (!disabled && hoverRating === rating) {
|
||||
setHoverRating(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
function getClassName(rating: number) {
|
||||
if (hoverRating && hoverRating >= rating) {
|
||||
if (hoverRating === props.value) {
|
||||
return "unsetting";
|
||||
}
|
||||
|
||||
return "setting";
|
||||
}
|
||||
|
||||
if (props.value && props.value >= rating) {
|
||||
return "set";
|
||||
}
|
||||
|
||||
return "unset";
|
||||
}
|
||||
|
||||
function getTooltip(rating: number) {
|
||||
if (disabled && props.value) {
|
||||
// always return current rating for disabled control
|
||||
return props.value.toString();
|
||||
}
|
||||
|
||||
if (!disabled) {
|
||||
return rating.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const renderRatingButton = (rating: number) => (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
className="minimal"
|
||||
onClick={() => setRating(rating)}
|
||||
variant="secondary"
|
||||
onMouseOver={() => onMouseOver(rating)}
|
||||
onMouseOut={() => onMouseOut(rating)}
|
||||
onFocus={() => onMouseOver(rating)}
|
||||
onBlur={() => onMouseOut(rating)}
|
||||
title={getTooltip(rating)}
|
||||
key={`star-${rating}`}
|
||||
>
|
||||
<Icon icon={getIcon(rating)} className={getClassName(rating)} />
|
||||
</Button>
|
||||
);
|
||||
|
||||
const maxRating = 5;
|
||||
|
||||
return (
|
||||
<div className="rating-stars align-middle">
|
||||
{Array.from(Array(maxRating)).map((value, index) =>
|
||||
renderRatingButton(index + 1)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import { TagLink } from "src/components/Shared/TagLink";
|
||||
import TruncatedText from "src/components/Shared/TruncatedText";
|
||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||
import { sortPerformers } from "src/core/performers";
|
||||
import { RatingStars } from "./RatingStars";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { objectTitle } from "src/core/files";
|
||||
|
||||
interface ISceneDetailProps {
|
||||
@@ -99,10 +99,10 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
|
||||
/>
|
||||
</h5>
|
||||
) : undefined}
|
||||
{props.scene.rating ? (
|
||||
{props.scene.rating100 ? (
|
||||
<h6>
|
||||
<FormattedMessage id="rating" />:{" "}
|
||||
<RatingStars value={props.scene.rating} />
|
||||
<RatingSystem value={props.scene.rating100} disabled />
|
||||
</h6>
|
||||
) : (
|
||||
""
|
||||
|
||||
@@ -40,7 +40,7 @@ import queryString from "query-string";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { stashboxDisplayName } from "src/utils/stashbox";
|
||||
import { SceneMovieTable } from "./SceneMovieTable";
|
||||
import { RatingStars } from "./RatingStars";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import {
|
||||
faSearch,
|
||||
faSyncAlt,
|
||||
@@ -123,7 +123,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
director: yup.string().optional().nullable(),
|
||||
url: yup.string().optional().nullable(),
|
||||
date: yup.string().optional().nullable(),
|
||||
rating: yup.number().optional().nullable(),
|
||||
rating100: yup.number().optional().nullable(),
|
||||
gallery_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
studio_id: yup.string().optional().nullable(),
|
||||
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
@@ -147,7 +147,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
director: scene.director ?? "",
|
||||
url: scene.url ?? "",
|
||||
date: scene.date ?? "",
|
||||
rating: scene.rating ?? null,
|
||||
rating100: scene.rating100 ?? null,
|
||||
gallery_ids: (scene.galleries ?? []).map((g) => g.id),
|
||||
studio_id: scene.studio?.id,
|
||||
performer_ids: (scene.performers ?? []).map((p) => p.id),
|
||||
@@ -171,7 +171,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
});
|
||||
|
||||
function setRating(v: number) {
|
||||
formik.setFieldValue("rating", v);
|
||||
formik.setFieldValue("rating100", v);
|
||||
}
|
||||
|
||||
interface IGallerySelectValue {
|
||||
@@ -206,11 +206,11 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
}
|
||||
|
||||
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));
|
||||
Mousetrap.bind("1", () => setRating(20));
|
||||
Mousetrap.bind("2", () => setRating(40));
|
||||
Mousetrap.bind("3", () => setRating(60));
|
||||
Mousetrap.bind("4", () => setRating(80));
|
||||
Mousetrap.bind("5", () => setRating(100));
|
||||
|
||||
setTimeout(() => {
|
||||
Mousetrap.unbind("0");
|
||||
@@ -287,7 +287,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
input: {
|
||||
...updateValues,
|
||||
id: scene.id!,
|
||||
rating: input.rating ?? null,
|
||||
rating100: input.rating100 ?? null,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -799,10 +799,10 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
value={formik.values.rating ?? undefined}
|
||||
<RatingSystem
|
||||
value={formik.values.rating100 ?? undefined}
|
||||
onSetRating={(value) =>
|
||||
formik.setFieldValue("rating", value ?? null)
|
||||
formik.setFieldValue("rating100", value ?? null)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@@ -90,7 +90,7 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
|
||||
<h5>{title}</h5>
|
||||
</Link>
|
||||
</td>
|
||||
<td>{scene.rating ? scene.rating : ""}</td>
|
||||
<td>{scene.rating100 ? scene.rating100 : ""}</td>
|
||||
<td>{file?.duration && TextUtils.secondsToTimestamp(file.duration)}</td>
|
||||
<td>{renderTags(scene.tags)}</td>
|
||||
<td>{renderPerformers(scene.performers)}</td>
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
ScrapedTagsRow,
|
||||
} from "./SceneDetails/SceneScrapeDialog";
|
||||
import { galleryTitle } from "src/core/galleries";
|
||||
import { RatingStars } from "./SceneDetails/RatingStars";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
|
||||
interface IStashIDsField {
|
||||
values: GQL.StashId[];
|
||||
@@ -66,7 +66,9 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
||||
new ScrapeResult<string>(dest.date)
|
||||
);
|
||||
|
||||
const [rating, setRating] = useState(new ScrapeResult<number>(dest.rating));
|
||||
const [rating, setRating] = useState(
|
||||
new ScrapeResult<number>(dest.rating100)
|
||||
);
|
||||
const [oCounter, setOCounter] = useState(
|
||||
new ScrapeResult<number>(dest.o_counter)
|
||||
);
|
||||
@@ -194,9 +196,9 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
||||
|
||||
setRating(
|
||||
new ScrapeResult(
|
||||
dest.rating,
|
||||
sources.find((s) => s.rating)?.rating,
|
||||
!dest.rating
|
||||
dest.rating100,
|
||||
sources.find((s) => s.rating100)?.rating100,
|
||||
!dest.rating100
|
||||
)
|
||||
);
|
||||
|
||||
@@ -324,10 +326,10 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
||||
title={intl.formatMessage({ id: "rating" })}
|
||||
result={rating}
|
||||
renderOriginalField={() => (
|
||||
<RatingStars value={rating.originalValue} disabled />
|
||||
<RatingSystem value={rating.originalValue} disabled />
|
||||
)}
|
||||
renderNewField={() => (
|
||||
<RatingStars value={rating.newValue} disabled />
|
||||
<RatingSystem value={rating.newValue} disabled />
|
||||
)}
|
||||
onChange={(value) => setRating(value)}
|
||||
/>
|
||||
@@ -430,7 +432,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
||||
title: title.getNewValue(),
|
||||
url: url.getNewValue(),
|
||||
date: date.getNewValue(),
|
||||
rating: rating.getNewValue(),
|
||||
rating100: rating.getNewValue(),
|
||||
o_counter: oCounter.getNewValue(),
|
||||
gallery_ids: galleries.getNewValue(),
|
||||
studio_id: studio.getNewValue(),
|
||||
|
||||
@@ -479,37 +479,6 @@ input[type="range"].blue-slider {
|
||||
}
|
||||
}
|
||||
|
||||
.rating-stars {
|
||||
display: inline-flex;
|
||||
|
||||
button {
|
||||
font-size: inherit;
|
||||
margin-right: 1px;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: inherit;
|
||||
opacity: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.unsetting {
|
||||
color: gold;
|
||||
}
|
||||
|
||||
.setting {
|
||||
color: gold;
|
||||
}
|
||||
|
||||
.set {
|
||||
color: gold;
|
||||
}
|
||||
}
|
||||
|
||||
#scene-edit-details {
|
||||
.rating-stars {
|
||||
font-size: 1.3em;
|
||||
@@ -662,3 +631,7 @@ input[type="range"].blue-slider {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.scrape-dialog .rating-number.disabled {
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,15 @@ import {
|
||||
connectionStateLabel,
|
||||
InteractiveContext,
|
||||
} from "src/hooks/Interactive/context";
|
||||
import {
|
||||
defaultRatingStarPrecision,
|
||||
defaultRatingSystemOptions,
|
||||
defaultRatingSystemType,
|
||||
RatingStarPrecision,
|
||||
ratingStarPrecisionIntlMap,
|
||||
ratingSystemIntlMap,
|
||||
RatingSystemType,
|
||||
} from "src/utils/rating";
|
||||
|
||||
const allMenuItems = [
|
||||
{ id: "scenes", headingID: "scenes" },
|
||||
@@ -80,6 +89,24 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
});
|
||||
}
|
||||
|
||||
function saveRatingSystemType(t: RatingSystemType) {
|
||||
saveUI({
|
||||
ratingSystemOptions: {
|
||||
...ui.ratingSystemOptions,
|
||||
type: t,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function saveRatingSystemStarPrecision(p: RatingStarPrecision) {
|
||||
saveUI({
|
||||
ratingSystemOptions: {
|
||||
...(ui.ratingSystemOptions ?? defaultRatingSystemOptions),
|
||||
starPrecision: p,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (error) return <h1>{error.message}</h1>;
|
||||
if (loading) return <LoadingIndicator />;
|
||||
|
||||
@@ -415,6 +442,42 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<SelectSetting
|
||||
id="rating_system"
|
||||
headingID="config.ui.editing.rating_system.type.label"
|
||||
value={ui.ratingSystemOptions?.type ?? defaultRatingSystemType}
|
||||
onChange={(v) => saveRatingSystemType(v as RatingSystemType)}
|
||||
>
|
||||
{Array.from(ratingSystemIntlMap.entries()).map((v) => (
|
||||
<option key={v[0]} value={v[0]}>
|
||||
{intl.formatMessage({
|
||||
id: v[1],
|
||||
})}
|
||||
</option>
|
||||
))}
|
||||
</SelectSetting>
|
||||
{(ui.ratingSystemOptions?.type ?? defaultRatingSystemType) ===
|
||||
RatingSystemType.Stars && (
|
||||
<SelectSetting
|
||||
id="rating_system_star_precision"
|
||||
headingID="config.ui.editing.rating_system.star_precision.label"
|
||||
value={
|
||||
ui.ratingSystemOptions?.starPrecision ??
|
||||
defaultRatingStarPrecision
|
||||
}
|
||||
onChange={(v) =>
|
||||
saveRatingSystemStarPrecision(v as RatingStarPrecision)
|
||||
}
|
||||
>
|
||||
{Array.from(ratingStarPrecisionIntlMap.entries()).map((v) => (
|
||||
<option key={v[0]} value={v[0]}>
|
||||
{intl.formatMessage({
|
||||
id: v[1],
|
||||
})}
|
||||
</option>
|
||||
))}
|
||||
</SelectSetting>
|
||||
)}
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection headingID="config.ui.custom_css.heading">
|
||||
|
||||
118
ui/v2.5/src/components/Shared/Rating/RatingNumber.tsx
Normal file
118
ui/v2.5/src/components/Shared/Rating/RatingNumber.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import React, { useRef } from "react";
|
||||
|
||||
export interface IRatingNumberProps {
|
||||
value?: number;
|
||||
onSetRating?: (value?: number) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const RatingNumber: React.FC<IRatingNumberProps> = (
|
||||
props: IRatingNumberProps
|
||||
) => {
|
||||
const text = ((props.value ?? 0) / 10).toFixed(1);
|
||||
const useValidation = useRef(true);
|
||||
|
||||
function stepChange() {
|
||||
useValidation.current = false;
|
||||
}
|
||||
|
||||
function nonStepChange() {
|
||||
useValidation.current = true;
|
||||
}
|
||||
|
||||
function setCursorPosition(
|
||||
target: HTMLInputElement,
|
||||
pos: number,
|
||||
endPos?: number
|
||||
) {
|
||||
// This is a workaround to a missing feature where you can't set cursor position in input numbers.
|
||||
// See https://stackoverflow.com/questions/33406169/failed-to-execute-setselectionrange-on-htmlinputelement-the-input-elements
|
||||
target.type = "text";
|
||||
|
||||
target.setSelectionRange(pos, endPos ?? pos);
|
||||
target.type = "number";
|
||||
}
|
||||
|
||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
if (!props.onSetRating) {
|
||||
return;
|
||||
}
|
||||
|
||||
let val = e.target.value;
|
||||
if (!useValidation.current) {
|
||||
e.target.value = Number(val).toFixed(1);
|
||||
const tempVal = Number(val) * 10;
|
||||
props.onSetRating(tempVal != 0 ? tempVal : undefined);
|
||||
useValidation.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const match = /(\d?)(\d?)(.?)((\d)?)/g.exec(val);
|
||||
const matchOld = /(\d?)(\d?)(.?)((\d{0,2})?)/g.exec(text ?? "");
|
||||
|
||||
if (match == null || props.onSetRating == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (match[2] && !(match[2] == "0" && match[1] == "1")) {
|
||||
match[2] = "";
|
||||
}
|
||||
if (match[4] == null || match[4] == "") {
|
||||
match[4] = "0";
|
||||
}
|
||||
|
||||
let value = match[1] + match[2] + "." + match[4];
|
||||
e.target.value = value;
|
||||
|
||||
if (val.length > 0) {
|
||||
if (Number(value) > 10) {
|
||||
value = "10.0";
|
||||
}
|
||||
e.target.value = Number(value).toFixed(1);
|
||||
let tempVal = Number(value) * 10;
|
||||
props.onSetRating(tempVal != 0 ? tempVal : undefined);
|
||||
|
||||
let cursorPosition = 0;
|
||||
if (match[2] && !match[4]) {
|
||||
cursorPosition = 3;
|
||||
} else if (matchOld != null && match[1] !== matchOld[1]) {
|
||||
cursorPosition = 2;
|
||||
} else if (
|
||||
matchOld != null &&
|
||||
match[1] === matchOld[1] &&
|
||||
match[2] === matchOld[2] &&
|
||||
match[4] === matchOld[4]
|
||||
) {
|
||||
cursorPosition = 2;
|
||||
}
|
||||
|
||||
setCursorPosition(e.target, cursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
if (props.disabled) {
|
||||
return (
|
||||
<div className="rating-number disabled">
|
||||
<span>{Number((props.value ?? 0) / 10).toFixed(1)}</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="rating-number">
|
||||
<input
|
||||
className="text-input form-control"
|
||||
name="ratingnumber"
|
||||
type="number"
|
||||
onMouseDown={stepChange}
|
||||
onKeyDown={nonStepChange}
|
||||
onChange={handleChange}
|
||||
value={text}
|
||||
min="0.0"
|
||||
step="0.1"
|
||||
max="10"
|
||||
placeholder="0.0"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user