mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +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:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -23,6 +23,12 @@ ui/v2.5/src/core/generated-*.tsx
|
|||||||
# Jetbrains
|
# Jetbrains
|
||||||
####
|
####
|
||||||
|
|
||||||
|
|
||||||
|
####
|
||||||
|
# Visual Studio
|
||||||
|
####
|
||||||
|
/.vs
|
||||||
|
|
||||||
# User-specific stuff
|
# User-specific stuff
|
||||||
.idea/**/workspace.xml
|
.idea/**/workspace.xml
|
||||||
.idea/**/tasks.xml
|
.idea/**/tasks.xml
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ fragment SlimGalleryData on Gallery {
|
|||||||
date
|
date
|
||||||
url
|
url
|
||||||
details
|
details
|
||||||
rating
|
rating100
|
||||||
organized
|
organized
|
||||||
files {
|
files {
|
||||||
...GalleryFileData
|
...GalleryFileData
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ fragment GalleryData on Gallery {
|
|||||||
date
|
date
|
||||||
url
|
url
|
||||||
details
|
details
|
||||||
rating
|
rating100
|
||||||
organized
|
organized
|
||||||
|
|
||||||
files {
|
files {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
fragment SlimImageData on Image {
|
fragment SlimImageData on Image {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
rating
|
rating100
|
||||||
organized
|
organized
|
||||||
o_counter
|
o_counter
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
fragment ImageData on Image {
|
fragment ImageData on Image {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
rating
|
rating100
|
||||||
organized
|
organized
|
||||||
o_counter
|
o_counter
|
||||||
created_at
|
created_at
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ fragment SlimMovieData on Movie {
|
|||||||
id
|
id
|
||||||
name
|
name
|
||||||
front_image_path
|
front_image_path
|
||||||
|
rating100
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ fragment MovieData on Movie {
|
|||||||
aliases
|
aliases
|
||||||
duration
|
duration
|
||||||
date
|
date
|
||||||
rating
|
rating100
|
||||||
director
|
director
|
||||||
|
|
||||||
studio {
|
studio {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ fragment SlimPerformerData on Performer {
|
|||||||
endpoint
|
endpoint
|
||||||
stash_id
|
stash_id
|
||||||
}
|
}
|
||||||
rating
|
rating100
|
||||||
death_date
|
death_date
|
||||||
weight
|
weight
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ fragment PerformerData on Performer {
|
|||||||
stash_id
|
stash_id
|
||||||
endpoint
|
endpoint
|
||||||
}
|
}
|
||||||
rating
|
rating100
|
||||||
details
|
details
|
||||||
death_date
|
death_date
|
||||||
hair_color
|
hair_color
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ fragment SlimSceneData on Scene {
|
|||||||
director
|
director
|
||||||
url
|
url
|
||||||
date
|
date
|
||||||
rating
|
rating100
|
||||||
o_counter
|
o_counter
|
||||||
organized
|
organized
|
||||||
interactive
|
interactive
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ fragment SceneData on Scene {
|
|||||||
director
|
director
|
||||||
url
|
url
|
||||||
date
|
date
|
||||||
rating
|
rating100
|
||||||
o_counter
|
o_counter
|
||||||
organized
|
organized
|
||||||
interactive
|
interactive
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ fragment SlimStudioData on Studio {
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
details
|
details
|
||||||
rating
|
rating100
|
||||||
aliases
|
aliases
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,6 @@ fragment StudioData on Studio {
|
|||||||
endpoint
|
endpoint
|
||||||
}
|
}
|
||||||
details
|
details
|
||||||
rating
|
rating100
|
||||||
aliases
|
aliases
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ input PerformerFilterType {
|
|||||||
"""Filter by StashID"""
|
"""Filter by StashID"""
|
||||||
stash_id: StringCriterionInput
|
stash_id: StringCriterionInput
|
||||||
"""Filter by rating"""
|
"""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"""
|
"""Filter by url"""
|
||||||
url: StringCriterionInput
|
url: StringCriterionInput
|
||||||
"""Filter by hair color"""
|
"""Filter by hair color"""
|
||||||
@@ -158,7 +160,9 @@ input SceneFilterType {
|
|||||||
"""Filter by file count"""
|
"""Filter by file count"""
|
||||||
file_count: IntCriterionInput
|
file_count: IntCriterionInput
|
||||||
"""Filter by rating"""
|
"""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"""
|
"""Filter by organized"""
|
||||||
organized: Boolean
|
organized: Boolean
|
||||||
"""Filter by o-counter"""
|
"""Filter by o-counter"""
|
||||||
@@ -218,7 +222,9 @@ input MovieFilterType {
|
|||||||
"""Filter by duration (in seconds)"""
|
"""Filter by duration (in seconds)"""
|
||||||
duration: IntCriterionInput
|
duration: IntCriterionInput
|
||||||
"""Filter by rating"""
|
"""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"""
|
"""Filter to only include movies with this studio"""
|
||||||
studios: HierarchicalMultiCriterionInput
|
studios: HierarchicalMultiCriterionInput
|
||||||
"""Filter to only include movies missing this property"""
|
"""Filter to only include movies missing this property"""
|
||||||
@@ -249,7 +255,9 @@ input StudioFilterType {
|
|||||||
"""Filter to only include studios missing this property"""
|
"""Filter to only include studios missing this property"""
|
||||||
is_missing: String
|
is_missing: String
|
||||||
"""Filter by rating"""
|
"""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"""
|
"""Filter by scene count"""
|
||||||
scene_count: IntCriterionInput
|
scene_count: IntCriterionInput
|
||||||
"""Filter by image count"""
|
"""Filter by image count"""
|
||||||
@@ -288,7 +296,9 @@ input GalleryFilterType {
|
|||||||
"""Filter to include/exclude galleries that were created from zip"""
|
"""Filter to include/exclude galleries that were created from zip"""
|
||||||
is_zip: Boolean
|
is_zip: Boolean
|
||||||
"""Filter by rating"""
|
"""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"""
|
"""Filter by organized"""
|
||||||
organized: Boolean
|
organized: Boolean
|
||||||
"""Filter by average image resolution"""
|
"""Filter by average image resolution"""
|
||||||
@@ -391,7 +401,9 @@ input ImageFilterType {
|
|||||||
"""Filter by file count"""
|
"""Filter by file count"""
|
||||||
file_count: IntCriterionInput
|
file_count: IntCriterionInput
|
||||||
"""Filter by rating"""
|
"""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"""
|
"""Filter by organized"""
|
||||||
organized: Boolean
|
organized: Boolean
|
||||||
"""Filter by o-counter"""
|
"""Filter by o-counter"""
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ type Gallery {
|
|||||||
url: String
|
url: String
|
||||||
date: String
|
date: String
|
||||||
details: 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!
|
organized: Boolean!
|
||||||
created_at: Time!
|
created_at: Time!
|
||||||
updated_at: Time!
|
updated_at: Time!
|
||||||
@@ -32,7 +35,10 @@ input GalleryCreateInput {
|
|||||||
url: String
|
url: String
|
||||||
date: String
|
date: String
|
||||||
details: 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
|
organized: Boolean
|
||||||
scene_ids: [ID!]
|
scene_ids: [ID!]
|
||||||
studio_id: ID
|
studio_id: ID
|
||||||
@@ -47,7 +53,10 @@ input GalleryUpdateInput {
|
|||||||
url: String
|
url: String
|
||||||
date: String
|
date: String
|
||||||
details: 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
|
organized: Boolean
|
||||||
scene_ids: [ID!]
|
scene_ids: [ID!]
|
||||||
studio_id: ID
|
studio_id: ID
|
||||||
@@ -63,7 +72,10 @@ input BulkGalleryUpdateInput {
|
|||||||
url: String
|
url: String
|
||||||
date: String
|
date: String
|
||||||
details: 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
|
organized: Boolean
|
||||||
scene_ids: BulkUpdateIds
|
scene_ids: BulkUpdateIds
|
||||||
studio_id: ID
|
studio_id: ID
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ type Image {
|
|||||||
id: ID!
|
id: ID!
|
||||||
checksum: String @deprecated(reason: "Use files.fingerprints")
|
checksum: String @deprecated(reason: "Use files.fingerprints")
|
||||||
title: String
|
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
|
o_counter: Int
|
||||||
organized: Boolean!
|
organized: Boolean!
|
||||||
path: String! @deprecated(reason: "Use files.path")
|
path: String! @deprecated(reason: "Use files.path")
|
||||||
@@ -37,7 +40,10 @@ input ImageUpdateInput {
|
|||||||
clientMutationId: String
|
clientMutationId: String
|
||||||
id: ID!
|
id: ID!
|
||||||
title: String
|
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
|
organized: Boolean
|
||||||
|
|
||||||
studio_id: ID
|
studio_id: ID
|
||||||
@@ -52,7 +58,10 @@ input BulkImageUpdateInput {
|
|||||||
clientMutationId: String
|
clientMutationId: String
|
||||||
ids: [ID!]
|
ids: [ID!]
|
||||||
title: String
|
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
|
organized: Boolean
|
||||||
|
|
||||||
studio_id: ID
|
studio_id: ID
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ type Movie {
|
|||||||
"""Duration in seconds"""
|
"""Duration in seconds"""
|
||||||
duration: Int
|
duration: Int
|
||||||
date: 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: Studio
|
studio: Studio
|
||||||
director: String
|
director: String
|
||||||
synopsis: String
|
synopsis: String
|
||||||
@@ -26,7 +29,10 @@ input MovieCreateInput {
|
|||||||
"""Duration in seconds"""
|
"""Duration in seconds"""
|
||||||
duration: Int
|
duration: Int
|
||||||
date: 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
|
studio_id: ID
|
||||||
director: String
|
director: String
|
||||||
synopsis: String
|
synopsis: String
|
||||||
@@ -43,7 +49,10 @@ input MovieUpdateInput {
|
|||||||
aliases: String
|
aliases: String
|
||||||
duration: Int
|
duration: Int
|
||||||
date: 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
|
studio_id: ID
|
||||||
director: String
|
director: String
|
||||||
synopsis: String
|
synopsis: String
|
||||||
@@ -57,7 +66,10 @@ input MovieUpdateInput {
|
|||||||
input BulkMovieUpdateInput {
|
input BulkMovieUpdateInput {
|
||||||
clientMutationId: String
|
clientMutationId: String
|
||||||
ids: [ID!]
|
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
|
studio_id: ID
|
||||||
director: String
|
director: String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ type Performer {
|
|||||||
gallery_count: Int # Resolver
|
gallery_count: Int # Resolver
|
||||||
scenes: [Scene!]!
|
scenes: [Scene!]!
|
||||||
stash_ids: [StashID!]!
|
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
|
details: String
|
||||||
death_date: String
|
death_date: String
|
||||||
hair_color: String
|
hair_color: String
|
||||||
@@ -72,7 +75,10 @@ input PerformerCreateInput {
|
|||||||
"""This should be a URL or a base64 encoded data URL"""
|
"""This should be a URL or a base64 encoded data URL"""
|
||||||
image: String
|
image: String
|
||||||
stash_ids: [StashIDInput!]
|
stash_ids: [StashIDInput!]
|
||||||
rating: Int
|
# rating expressed as 1-5
|
||||||
|
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||||
|
# rating expressed as 1-100
|
||||||
|
rating100: Int
|
||||||
details: String
|
details: String
|
||||||
death_date: String
|
death_date: String
|
||||||
hair_color: String
|
hair_color: String
|
||||||
@@ -105,7 +111,10 @@ input PerformerUpdateInput {
|
|||||||
"""This should be a URL or a base64 encoded data URL"""
|
"""This should be a URL or a base64 encoded data URL"""
|
||||||
image: String
|
image: String
|
||||||
stash_ids: [StashIDInput!]
|
stash_ids: [StashIDInput!]
|
||||||
rating: Int
|
# rating expressed as 1-5
|
||||||
|
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||||
|
# rating expressed as 1-100
|
||||||
|
rating100: Int
|
||||||
details: String
|
details: String
|
||||||
death_date: String
|
death_date: String
|
||||||
hair_color: String
|
hair_color: String
|
||||||
@@ -135,7 +144,10 @@ input BulkPerformerUpdateInput {
|
|||||||
instagram: String
|
instagram: String
|
||||||
favorite: Boolean
|
favorite: Boolean
|
||||||
tag_ids: BulkUpdateIds
|
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
|
details: String
|
||||||
death_date: String
|
death_date: String
|
||||||
hair_color: String
|
hair_color: String
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ type Scene {
|
|||||||
director: String
|
director: String
|
||||||
url: String
|
url: String
|
||||||
date: 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!
|
organized: Boolean!
|
||||||
o_counter: Int
|
o_counter: Int
|
||||||
path: String! @deprecated(reason: "Use files.path")
|
path: String! @deprecated(reason: "Use files.path")
|
||||||
@@ -106,7 +109,10 @@ input SceneUpdateInput {
|
|||||||
director: String
|
director: String
|
||||||
url: String
|
url: String
|
||||||
date: 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
|
o_counter: Int
|
||||||
organized: Boolean
|
organized: Boolean
|
||||||
studio_id: ID
|
studio_id: ID
|
||||||
@@ -141,7 +147,10 @@ input BulkSceneUpdateInput {
|
|||||||
director: String
|
director: String
|
||||||
url: String
|
url: String
|
||||||
date: 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
|
organized: Boolean
|
||||||
studio_id: ID
|
studio_id: ID
|
||||||
gallery_ids: BulkUpdateIds
|
gallery_ids: BulkUpdateIds
|
||||||
@@ -191,7 +200,10 @@ type SceneParserResult {
|
|||||||
director: String
|
director: String
|
||||||
url: String
|
url: String
|
||||||
date: 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
|
studio_id: ID
|
||||||
gallery_ids: [ID!]
|
gallery_ids: [ID!]
|
||||||
performer_ids: [ID!]
|
performer_ids: [ID!]
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ type Studio {
|
|||||||
image_count: Int # Resolver
|
image_count: Int # Resolver
|
||||||
gallery_count: Int # Resolver
|
gallery_count: Int # Resolver
|
||||||
stash_ids: [StashID!]!
|
stash_ids: [StashID!]!
|
||||||
rating: Int
|
# rating expressed as 1-5
|
||||||
|
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||||
|
# rating expressed as 1-100
|
||||||
|
rating100: Int
|
||||||
details: String
|
details: String
|
||||||
created_at: Time!
|
created_at: Time!
|
||||||
updated_at: Time!
|
updated_at: Time!
|
||||||
@@ -28,7 +31,10 @@ input StudioCreateInput {
|
|||||||
"""This should be a URL or a base64 encoded data URL"""
|
"""This should be a URL or a base64 encoded data URL"""
|
||||||
image: String
|
image: String
|
||||||
stash_ids: [StashIDInput!]
|
stash_ids: [StashIDInput!]
|
||||||
rating: Int
|
# rating expressed as 1-5
|
||||||
|
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||||
|
# rating expressed as 1-100
|
||||||
|
rating100: Int
|
||||||
details: String
|
details: String
|
||||||
aliases: [String!]
|
aliases: [String!]
|
||||||
ignore_auto_tag: Boolean
|
ignore_auto_tag: Boolean
|
||||||
@@ -42,7 +48,10 @@ input StudioUpdateInput {
|
|||||||
"""This should be a URL or a base64 encoded data URL"""
|
"""This should be a URL or a base64 encoded data URL"""
|
||||||
image: String
|
image: String
|
||||||
stash_ids: [StashIDInput!]
|
stash_ids: [StashIDInput!]
|
||||||
rating: Int
|
# rating expressed as 1-5
|
||||||
|
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||||
|
# rating expressed as 1-100
|
||||||
|
rating100: Int
|
||||||
details: String
|
details: String
|
||||||
aliases: [String!]
|
aliases: [String!]
|
||||||
ignore_auto_tag: Boolean
|
ignore_auto_tag: Boolean
|
||||||
|
|||||||
@@ -189,6 +189,36 @@ func (t changesetTranslator) nullInt64(value *int, field string) *sql.NullInt64
|
|||||||
return ret
|
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 {
|
func (t changesetTranslator) optionalInt(value *int, field string) models.OptionalInt {
|
||||||
if !t.hasField(field) {
|
if !t.hasField(field) {
|
||||||
return models.OptionalInt{}
|
return models.OptionalInt{}
|
||||||
|
|||||||
@@ -189,6 +189,18 @@ func (r *galleryResolver) Checksum(ctx context.Context, obj *models.Gallery) (st
|
|||||||
return obj.PrimaryChecksum(), nil
|
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) {
|
func (r *galleryResolver) Scenes(ctx context.Context, obj *models.Gallery) (ret []*models.Scene, err error) {
|
||||||
if !obj.SceneIDs.Loaded() {
|
if !obj.SceneIDs.Loaded() {
|
||||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
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)
|
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) {
|
func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (ret *models.Studio, err error) {
|
||||||
if obj.StudioID == nil {
|
if obj.StudioID == nil {
|
||||||
return nil, 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) {
|
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 {
|
if obj.Rating.Valid {
|
||||||
rating := int(obj.Rating.Int64)
|
rating := int(obj.Rating.Int64)
|
||||||
return &rating, nil
|
return &rating, nil
|
||||||
|
|||||||
@@ -107,6 +107,18 @@ func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer)
|
|||||||
return stashIDsSliceToPtrSlice(ret), nil
|
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) {
|
func (r *performerResolver) DeathDate(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||||
if obj.DeathDate != nil {
|
if obj.DeathDate != nil {
|
||||||
ret := obj.DeathDate.String()
|
ret := obj.DeathDate.String()
|
||||||
|
|||||||
@@ -141,6 +141,18 @@ func (r *sceneResolver) Files(ctx context.Context, obj *models.Scene) ([]*VideoF
|
|||||||
return ret, nil
|
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 {
|
func resolveFingerprints(f *file.BaseFile) []*Fingerprint {
|
||||||
ret := make([]*Fingerprint, len(f.Fingerprints))
|
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) {
|
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 {
|
if obj.Rating.Valid {
|
||||||
rating := int(obj.Rating.Int64)
|
rating := int(obj.Rating.Int64)
|
||||||
return &rating, nil
|
return &rating, nil
|
||||||
|
|||||||
@@ -68,7 +68,13 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
|
|||||||
d := models.NewDate(*input.Date)
|
d := models.NewDate(*input.Date)
|
||||||
newGallery.Date = &d
|
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 {
|
if input.StudioID != nil {
|
||||||
studioID, _ := strconv.Atoi(*input.StudioID)
|
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.Details = translator.optionalString(input.Details, "details")
|
||||||
updatedGallery.URL = translator.optionalString(input.URL, "url")
|
updatedGallery.URL = translator.optionalString(input.URL, "url")
|
||||||
updatedGallery.Date = translator.optionalDate(input.Date, "date")
|
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")
|
updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
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.Details = translator.optionalString(input.Details, "details")
|
||||||
updatedGallery.URL = translator.optionalString(input.URL, "url")
|
updatedGallery.URL = translator.optionalString(input.URL, "url")
|
||||||
updatedGallery.Date = translator.optionalDate(input.Date, "date")
|
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
|
var err error
|
||||||
updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp
|
|||||||
|
|
||||||
updatedImage := models.NewImagePartial()
|
updatedImage := models.NewImagePartial()
|
||||||
updatedImage.Title = translator.optionalString(input.Title, "title")
|
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")
|
updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
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.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")
|
updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
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}
|
newMovie.Date = models.SQLiteDate{String: *input.Date, Valid: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.Rating != nil {
|
if input.Rating100 != nil {
|
||||||
rating := int64(*input.Rating)
|
newMovie.Rating = sql.NullInt64{Int64: int64(*input.Rating100), Valid: true}
|
||||||
newMovie.Rating = sql.NullInt64{Int64: rating, 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 {
|
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.Aliases = translator.nullString(input.Aliases, "aliases")
|
||||||
updatedMovie.Duration = translator.nullInt64(input.Duration, "duration")
|
updatedMovie.Duration = translator.nullInt64(input.Duration, "duration")
|
||||||
updatedMovie.Date = translator.sqliteDate(input.Date, "date")
|
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.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||||
updatedMovie.Director = translator.nullString(input.Director, "director")
|
updatedMovie.Director = translator.nullString(input.Director, "director")
|
||||||
updatedMovie.Synopsis = translator.nullString(input.Synopsis, "synopsis")
|
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},
|
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.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||||
updatedMovie.Director = translator.nullString(input.Director, "director")
|
updatedMovie.Director = translator.nullString(input.Director, "director")
|
||||||
|
|
||||||
|
|||||||
@@ -114,8 +114,11 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC
|
|||||||
if input.Favorite != nil {
|
if input.Favorite != nil {
|
||||||
newPerformer.Favorite = *input.Favorite
|
newPerformer.Favorite = *input.Favorite
|
||||||
}
|
}
|
||||||
if input.Rating != nil {
|
if input.Rating100 != nil {
|
||||||
newPerformer.Rating = input.Rating
|
newPerformer.Rating = input.Rating100
|
||||||
|
} else if input.Rating != nil {
|
||||||
|
rating := models.Rating5To100(*input.Rating)
|
||||||
|
newPerformer.Rating = &rating
|
||||||
}
|
}
|
||||||
if input.Details != nil {
|
if input.Details != nil {
|
||||||
newPerformer.Details = *input.Details
|
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.Twitter = translator.optionalString(input.Twitter, "twitter")
|
||||||
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
|
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
|
||||||
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
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.Details = translator.optionalString(input.Details, "details")
|
||||||
updatedPerformer.DeathDate = translator.optionalDate(input.DeathDate, "death_date")
|
updatedPerformer.DeathDate = translator.optionalDate(input.DeathDate, "death_date")
|
||||||
updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color")
|
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.Twitter = translator.optionalString(input.Twitter, "twitter")
|
||||||
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
|
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
|
||||||
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
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.Details = translator.optionalString(input.Details, "details")
|
||||||
updatedPerformer.DeathDate = translator.optionalDate(input.DeathDate, "death_date")
|
updatedPerformer.DeathDate = translator.optionalDate(input.DeathDate, "death_date")
|
||||||
updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color")
|
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.Director = translator.optionalString(input.Director, "director")
|
||||||
updatedScene.URL = translator.optionalString(input.URL, "url")
|
updatedScene.URL = translator.optionalString(input.URL, "url")
|
||||||
updatedScene.Date = translator.optionalDate(input.Date, "date")
|
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")
|
updatedScene.OCounter = translator.optionalInt(input.OCounter, "o_counter")
|
||||||
var err error
|
var err error
|
||||||
updatedScene.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
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.Director = translator.optionalString(input.Director, "director")
|
||||||
updatedScene.URL = translator.optionalString(input.URL, "url")
|
updatedScene.URL = translator.optionalString(input.URL, "url")
|
||||||
updatedScene.Date = translator.optionalDate(input.Date, "date")
|
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")
|
updatedScene.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
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}
|
newStudio.ParentID = sql.NullInt64{Int64: parentID, Valid: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.Rating != nil {
|
if input.Rating100 != nil {
|
||||||
newStudio.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
newStudio.Rating = sql.NullInt64{
|
||||||
} else {
|
Int64: int64(*input.Rating100),
|
||||||
newStudio.Rating = sql.NullInt64{Valid: false}
|
Valid: true,
|
||||||
}
|
}
|
||||||
|
} else if input.Rating != nil {
|
||||||
|
newStudio.Rating = sql.NullInt64{
|
||||||
|
Int64: int64(models.Rating5To100(*input.Rating)),
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if input.Details != nil {
|
if input.Details != nil {
|
||||||
newStudio.Details = sql.NullString{String: *input.Details, Valid: true}
|
newStudio.Details = sql.NullString{String: *input.Details, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -150,7 +157,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input StudioUpdateI
|
|||||||
updatedStudio.URL = translator.nullString(input.URL, "url")
|
updatedStudio.URL = translator.nullString(input.URL, "url")
|
||||||
updatedStudio.Details = translator.nullString(input.Details, "details")
|
updatedStudio.Details = translator.nullString(input.Details, "details")
|
||||||
updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id")
|
updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id")
|
||||||
updatedStudio.Rating = translator.nullInt64(input.Rating, "rating")
|
updatedStudio.Rating = translator.ratingConversion(input.Rating, input.Rating100)
|
||||||
updatedStudio.IgnoreAutoTag = input.IgnoreAutoTag
|
updatedStudio.IgnoreAutoTag = input.IgnoreAutoTag
|
||||||
|
|
||||||
// Start the transaction and save the studio
|
// Start the transaction and save the studio
|
||||||
|
|||||||
@@ -32,15 +32,19 @@ func toSnakeCase(v string) string {
|
|||||||
|
|
||||||
func fromSnakeCase(v string) string {
|
func fromSnakeCase(v string) string {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
leadingUnderscore := true
|
||||||
capvar := false
|
capvar := false
|
||||||
for i, c := range v {
|
for i, c := range v {
|
||||||
switch {
|
switch {
|
||||||
case c == '_' && i > 0:
|
case c == '_' && !leadingUnderscore && i > 0:
|
||||||
capvar = true
|
capvar = true
|
||||||
|
case c == '_' && leadingUnderscore:
|
||||||
|
buf.WriteRune(c)
|
||||||
case capvar:
|
case capvar:
|
||||||
buf.WriteRune(unicode.ToUpper(c))
|
buf.WriteRune(unicode.ToUpper(c))
|
||||||
capvar = false
|
capvar = false
|
||||||
default:
|
default:
|
||||||
|
leadingUnderscore = false
|
||||||
buf.WriteRune(c)
|
buf.WriteRune(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,8 +58,14 @@ func toSnakeCaseMap(m map[string]interface{}) map[string]interface{} {
|
|||||||
|
|
||||||
for key, val := range m {
|
for key, val := range m {
|
||||||
adjKey := toSnakeCase(key)
|
adjKey := toSnakeCase(key)
|
||||||
|
|
||||||
|
switch v := val.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
nm[adjKey] = toSnakeCaseMap(v)
|
||||||
|
default:
|
||||||
nm[adjKey] = val
|
nm[adjKey] = val
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nm
|
return nm
|
||||||
}
|
}
|
||||||
@@ -68,13 +78,15 @@ func convertMapValue(val interface{}) interface{} {
|
|||||||
case map[interface{}]interface{}:
|
case map[interface{}]interface{}:
|
||||||
ret := cast.ToStringMap(v)
|
ret := cast.ToStringMap(v)
|
||||||
for k, vv := range ret {
|
for k, vv := range ret {
|
||||||
ret[k] = convertMapValue(vv)
|
adjKey := fromSnakeCase(k)
|
||||||
|
ret[adjKey] = convertMapValue(vv)
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
ret := make(map[string]interface{})
|
ret := make(map[string]interface{})
|
||||||
for k, vv := range v {
|
for k, vv := range v {
|
||||||
ret[k] = convertMapValue(vv)
|
adjKey := fromSnakeCase(k)
|
||||||
|
ret[adjKey] = convertMapValue(vv)
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type SceneParserResult struct {
|
|||||||
URL *string `json:"url"`
|
URL *string `json:"url"`
|
||||||
Date *string `json:"date"`
|
Date *string `json:"date"`
|
||||||
Rating *int `json:"rating"`
|
Rating *int `json:"rating"`
|
||||||
|
Rating100 *int `json:"rating100"`
|
||||||
StudioID *string `json:"studio_id"`
|
StudioID *string `json:"studio_id"`
|
||||||
GalleryIds []string `json:"gallery_ids"`
|
GalleryIds []string `json:"gallery_ids"`
|
||||||
PerformerIds []string `json:"performer_ids"`
|
PerformerIds []string `json:"performer_ids"`
|
||||||
@@ -113,6 +114,7 @@ func initParserFields() {
|
|||||||
|
|
||||||
ret["d"] = newParserField("d", `(?:\.|-|_)`, false)
|
ret["d"] = newParserField("d", `(?:\.|-|_)`, false)
|
||||||
ret["rating"] = newParserField("rating", `\d`, true)
|
ret["rating"] = newParserField("rating", `\d`, true)
|
||||||
|
ret["rating100"] = newParserField("rating100", `\d`, true)
|
||||||
ret["performer"] = newParserField("performer", ".*", true)
|
ret["performer"] = newParserField("performer", ".*", true)
|
||||||
ret["studio"] = newParserField("studio", ".*", true)
|
ret["studio"] = newParserField("studio", ".*", true)
|
||||||
ret["movie"] = newParserField("movie", ".*", true)
|
ret["movie"] = newParserField("movie", ".*", true)
|
||||||
@@ -256,6 +258,10 @@ func validateRating(rating int) bool {
|
|||||||
return rating >= 1 && rating <= 5
|
return rating >= 1 && rating <= 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateRating100(rating100 int) bool {
|
||||||
|
return rating100 >= 1 && rating100 <= 100
|
||||||
|
}
|
||||||
|
|
||||||
func validateDate(dateStr string) bool {
|
func validateDate(dateStr string) bool {
|
||||||
splits := strings.Split(dateStr, "-")
|
splits := strings.Split(dateStr, "-")
|
||||||
if len(splits) != 3 {
|
if len(splits) != 3 {
|
||||||
@@ -347,6 +353,13 @@ func (h *sceneHolder) setField(field parserField, value interface{}) {
|
|||||||
case "rating":
|
case "rating":
|
||||||
rating, _ := strconv.Atoi(value.(string))
|
rating, _ := strconv.Atoi(value.(string))
|
||||||
if validateRating(rating) {
|
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
|
h.result.Rating = &rating
|
||||||
}
|
}
|
||||||
case "performer":
|
case "performer":
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ type GalleryFilterType struct {
|
|||||||
IsMissing *string `json:"is_missing"`
|
IsMissing *string `json:"is_missing"`
|
||||||
// Filter to include/exclude galleries that were created from zip
|
// Filter to include/exclude galleries that were created from zip
|
||||||
IsZip *bool `json:"is_zip"`
|
IsZip *bool `json:"is_zip"`
|
||||||
// Filter by rating
|
// Filter by rating expressed as 1-5
|
||||||
Rating *IntCriterionInput `json:"rating"`
|
Rating *IntCriterionInput `json:"rating"`
|
||||||
|
// Filter by rating expressed as 1-100
|
||||||
|
Rating100 *IntCriterionInput `json:"rating100"`
|
||||||
// Filter by organized
|
// Filter by organized
|
||||||
Organized *bool `json:"organized"`
|
Organized *bool `json:"organized"`
|
||||||
// Filter by average image resolution
|
// Filter by average image resolution
|
||||||
@@ -65,6 +67,7 @@ type GalleryUpdateInput struct {
|
|||||||
Date *string `json:"date"`
|
Date *string `json:"date"`
|
||||||
Details *string `json:"details"`
|
Details *string `json:"details"`
|
||||||
Rating *int `json:"rating"`
|
Rating *int `json:"rating"`
|
||||||
|
Rating100 *int `json:"rating100"`
|
||||||
Organized *bool `json:"organized"`
|
Organized *bool `json:"organized"`
|
||||||
SceneIds []string `json:"scene_ids"`
|
SceneIds []string `json:"scene_ids"`
|
||||||
StudioID *string `json:"studio_id"`
|
StudioID *string `json:"studio_id"`
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ type ImageFilterType struct {
|
|||||||
Path *StringCriterionInput `json:"path"`
|
Path *StringCriterionInput `json:"path"`
|
||||||
// Filter by file count
|
// Filter by file count
|
||||||
FileCount *IntCriterionInput `json:"file_count"`
|
FileCount *IntCriterionInput `json:"file_count"`
|
||||||
// Filter by rating
|
// Filter by rating expressed as 1-5
|
||||||
Rating *IntCriterionInput `json:"rating"`
|
Rating *IntCriterionInput `json:"rating"`
|
||||||
|
// Filter by rating expressed as 1-100
|
||||||
|
Rating100 *IntCriterionInput `json:"rating100"`
|
||||||
// Filter by organized
|
// Filter by organized
|
||||||
Organized *bool `json:"organized"`
|
Organized *bool `json:"organized"`
|
||||||
// Filter by o-counter
|
// Filter by o-counter
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type Gallery struct {
|
|||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Date *Date `json:"date"`
|
Date *Date `json:"date"`
|
||||||
Details string `json:"details"`
|
Details string `json:"details"`
|
||||||
|
// Rating expressed in 1-100 scale
|
||||||
Rating *int `json:"rating"`
|
Rating *int `json:"rating"`
|
||||||
Organized bool `json:"organized"`
|
Organized bool `json:"organized"`
|
||||||
StudioID *int `json:"studio_id"`
|
StudioID *int `json:"studio_id"`
|
||||||
@@ -108,6 +109,7 @@ type GalleryPartial struct {
|
|||||||
URL OptionalString
|
URL OptionalString
|
||||||
Date OptionalDate
|
Date OptionalDate
|
||||||
Details OptionalString
|
Details OptionalString
|
||||||
|
// Rating expressed in 1-100 scale
|
||||||
Rating OptionalInt
|
Rating OptionalInt
|
||||||
Organized OptionalBool
|
Organized OptionalBool
|
||||||
StudioID OptionalInt
|
StudioID OptionalInt
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type Image struct {
|
|||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
|
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
|
// Rating expressed in 1-100 scale
|
||||||
Rating *int `json:"rating"`
|
Rating *int `json:"rating"`
|
||||||
Organized bool `json:"organized"`
|
Organized bool `json:"organized"`
|
||||||
OCounter int `json:"o_counter"`
|
OCounter int `json:"o_counter"`
|
||||||
@@ -114,6 +115,7 @@ type ImageCreateInput struct {
|
|||||||
|
|
||||||
type ImagePartial struct {
|
type ImagePartial struct {
|
||||||
Title OptionalString
|
Title OptionalString
|
||||||
|
// Rating expressed in 1-100 scale
|
||||||
Rating OptionalInt
|
Rating OptionalInt
|
||||||
Organized OptionalBool
|
Organized OptionalBool
|
||||||
OCounter OptionalInt
|
OCounter OptionalInt
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type Movie struct {
|
|||||||
Aliases sql.NullString `db:"aliases" json:"aliases"`
|
Aliases sql.NullString `db:"aliases" json:"aliases"`
|
||||||
Duration sql.NullInt64 `db:"duration" json:"duration"`
|
Duration sql.NullInt64 `db:"duration" json:"duration"`
|
||||||
Date SQLiteDate `db:"date" json:"date"`
|
Date SQLiteDate `db:"date" json:"date"`
|
||||||
|
// Rating expressed in 1-100 scale
|
||||||
Rating sql.NullInt64 `db:"rating" json:"rating"`
|
Rating sql.NullInt64 `db:"rating" json:"rating"`
|
||||||
StudioID sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"`
|
StudioID sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"`
|
||||||
Director sql.NullString `db:"director" json:"director"`
|
Director sql.NullString `db:"director" json:"director"`
|
||||||
@@ -30,6 +31,7 @@ type MoviePartial struct {
|
|||||||
Aliases *sql.NullString `db:"aliases" json:"aliases"`
|
Aliases *sql.NullString `db:"aliases" json:"aliases"`
|
||||||
Duration *sql.NullInt64 `db:"duration" json:"duration"`
|
Duration *sql.NullInt64 `db:"duration" json:"duration"`
|
||||||
Date *SQLiteDate `db:"date" json:"date"`
|
Date *SQLiteDate `db:"date" json:"date"`
|
||||||
|
// Rating expressed in 1-100 scale
|
||||||
Rating *sql.NullInt64 `db:"rating" json:"rating"`
|
Rating *sql.NullInt64 `db:"rating" json:"rating"`
|
||||||
StudioID *sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"`
|
StudioID *sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"`
|
||||||
Director *sql.NullString `db:"director" json:"director"`
|
Director *sql.NullString `db:"director" json:"director"`
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type Performer struct {
|
|||||||
Favorite bool `json:"favorite"`
|
Favorite bool `json:"favorite"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
// Rating expressed in 1-100 scale
|
||||||
Rating *int `json:"rating"`
|
Rating *int `json:"rating"`
|
||||||
Details string `json:"details"`
|
Details string `json:"details"`
|
||||||
DeathDate *Date `json:"death_date"`
|
DeathDate *Date `json:"death_date"`
|
||||||
@@ -60,6 +61,7 @@ type PerformerPartial struct {
|
|||||||
Favorite OptionalBool
|
Favorite OptionalBool
|
||||||
CreatedAt OptionalTime
|
CreatedAt OptionalTime
|
||||||
UpdatedAt OptionalTime
|
UpdatedAt OptionalTime
|
||||||
|
// Rating expressed in 1-100 scale
|
||||||
Rating OptionalInt
|
Rating OptionalInt
|
||||||
Details OptionalString
|
Details OptionalString
|
||||||
DeathDate OptionalDate
|
DeathDate OptionalDate
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type Scene struct {
|
|||||||
Director string `json:"director"`
|
Director string `json:"director"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Date *Date `json:"date"`
|
Date *Date `json:"date"`
|
||||||
|
// Rating expressed in 1-100 scale
|
||||||
Rating *int `json:"rating"`
|
Rating *int `json:"rating"`
|
||||||
Organized bool `json:"organized"`
|
Organized bool `json:"organized"`
|
||||||
OCounter int `json:"o_counter"`
|
OCounter int `json:"o_counter"`
|
||||||
@@ -140,6 +141,7 @@ type ScenePartial struct {
|
|||||||
Director OptionalString
|
Director OptionalString
|
||||||
URL OptionalString
|
URL OptionalString
|
||||||
Date OptionalDate
|
Date OptionalDate
|
||||||
|
// Rating expressed in 1-100 scale
|
||||||
Rating OptionalInt
|
Rating OptionalInt
|
||||||
Organized OptionalBool
|
Organized OptionalBool
|
||||||
OCounter OptionalInt
|
OCounter OptionalInt
|
||||||
@@ -176,7 +178,10 @@ type SceneUpdateInput struct {
|
|||||||
Director *string `json:"director"`
|
Director *string `json:"director"`
|
||||||
URL *string `json:"url"`
|
URL *string `json:"url"`
|
||||||
Date *string `json:"date"`
|
Date *string `json:"date"`
|
||||||
|
// Rating expressed in 1-5 scale
|
||||||
Rating *int `json:"rating"`
|
Rating *int `json:"rating"`
|
||||||
|
// Rating expressed in 1-100 scale
|
||||||
|
Rating100 *int `json:"rating100"`
|
||||||
OCounter *int `json:"o_counter"`
|
OCounter *int `json:"o_counter"`
|
||||||
Organized *bool `json:"organized"`
|
Organized *bool `json:"organized"`
|
||||||
StudioID *string `json:"studio_id"`
|
StudioID *string `json:"studio_id"`
|
||||||
@@ -204,7 +209,7 @@ func (s ScenePartial) UpdateInput(id int) SceneUpdateInput {
|
|||||||
stashIDs = s.StashIDs.StashIDs
|
stashIDs = s.StashIDs.StashIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
return SceneUpdateInput{
|
ret := SceneUpdateInput{
|
||||||
ID: strconv.Itoa(id),
|
ID: strconv.Itoa(id),
|
||||||
Title: s.Title.Ptr(),
|
Title: s.Title.Ptr(),
|
||||||
Code: s.Code.Ptr(),
|
Code: s.Code.Ptr(),
|
||||||
@@ -212,7 +217,7 @@ func (s ScenePartial) UpdateInput(id int) SceneUpdateInput {
|
|||||||
Director: s.Director.Ptr(),
|
Director: s.Director.Ptr(),
|
||||||
URL: s.URL.Ptr(),
|
URL: s.URL.Ptr(),
|
||||||
Date: dateStr,
|
Date: dateStr,
|
||||||
Rating: s.Rating.Ptr(),
|
Rating100: s.Rating.Ptr(),
|
||||||
Organized: s.Organized.Ptr(),
|
Organized: s.Organized.Ptr(),
|
||||||
StudioID: s.StudioID.StringPtr(),
|
StudioID: s.StudioID.StringPtr(),
|
||||||
GalleryIds: s.GalleryIDs.IDStrings(),
|
GalleryIds: s.GalleryIDs.IDStrings(),
|
||||||
@@ -221,6 +226,14 @@ func (s ScenePartial) UpdateInput(id int) SceneUpdateInput {
|
|||||||
TagIds: s.TagIDs.IDStrings(),
|
TagIds: s.TagIDs.IDStrings(),
|
||||||
StashIds: stashIDs,
|
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,
|
// GetTitle returns the title of the scene. If the Title field is empty,
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ func TestScenePartial_UpdateInput(t *testing.T) {
|
|||||||
director = "director"
|
director = "director"
|
||||||
url = "url"
|
url = "url"
|
||||||
date = "2001-02-03"
|
date = "2001-02-03"
|
||||||
rating = 4
|
ratingLegacy = 4
|
||||||
|
rating100 = 80
|
||||||
organized = true
|
organized = true
|
||||||
studioID = 2
|
studioID = 2
|
||||||
studioIDStr = "2"
|
studioIDStr = "2"
|
||||||
@@ -42,7 +43,7 @@ func TestScenePartial_UpdateInput(t *testing.T) {
|
|||||||
Director: NewOptionalString(director),
|
Director: NewOptionalString(director),
|
||||||
URL: NewOptionalString(url),
|
URL: NewOptionalString(url),
|
||||||
Date: NewOptionalDate(dateObj),
|
Date: NewOptionalDate(dateObj),
|
||||||
Rating: NewOptionalInt(rating),
|
Rating: NewOptionalInt(rating100),
|
||||||
Organized: NewOptionalBool(organized),
|
Organized: NewOptionalBool(organized),
|
||||||
StudioID: NewOptionalInt(studioID),
|
StudioID: NewOptionalInt(studioID),
|
||||||
},
|
},
|
||||||
@@ -54,7 +55,8 @@ func TestScenePartial_UpdateInput(t *testing.T) {
|
|||||||
Director: &director,
|
Director: &director,
|
||||||
URL: &url,
|
URL: &url,
|
||||||
Date: &date,
|
Date: &date,
|
||||||
Rating: &rating,
|
Rating: &ratingLegacy,
|
||||||
|
Rating100: &rating100,
|
||||||
Organized: &organized,
|
Organized: &organized,
|
||||||
StudioID: &studioIDStr,
|
StudioID: &studioIDStr,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type Studio struct {
|
|||||||
ParentID sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
|
ParentID sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
|
||||||
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
|
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||||
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||||
|
// Rating expressed in 1-100 scale
|
||||||
Rating sql.NullInt64 `db:"rating" json:"rating"`
|
Rating sql.NullInt64 `db:"rating" json:"rating"`
|
||||||
Details sql.NullString `db:"details" json:"details"`
|
Details sql.NullString `db:"details" json:"details"`
|
||||||
IgnoreAutoTag bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
|
IgnoreAutoTag bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
|
||||||
@@ -28,6 +29,7 @@ type StudioPartial struct {
|
|||||||
ParentID *sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
|
ParentID *sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
|
||||||
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
|
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||||
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||||
|
// Rating expressed in 1-100 scale
|
||||||
Rating *sql.NullInt64 `db:"rating" json:"rating"`
|
Rating *sql.NullInt64 `db:"rating" json:"rating"`
|
||||||
Details *sql.NullString `db:"details" json:"details"`
|
Details *sql.NullString `db:"details" json:"details"`
|
||||||
IgnoreAutoTag *bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
|
IgnoreAutoTag *bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ type MovieFilterType struct {
|
|||||||
Synopsis *StringCriterionInput `json:"synopsis"`
|
Synopsis *StringCriterionInput `json:"synopsis"`
|
||||||
// Filter by duration (in seconds)
|
// Filter by duration (in seconds)
|
||||||
Duration *IntCriterionInput `json:"duration"`
|
Duration *IntCriterionInput `json:"duration"`
|
||||||
// Filter by rating
|
// Filter by rating expressed as 1-5
|
||||||
Rating *IntCriterionInput `json:"rating"`
|
Rating *IntCriterionInput `json:"rating"`
|
||||||
|
// Filter by rating expressed as 1-100
|
||||||
|
Rating100 *IntCriterionInput `json:"rating100"`
|
||||||
// Filter to only include movies with this studio
|
// Filter to only include movies with this studio
|
||||||
Studios *HierarchicalMultiCriterionInput `json:"studios"`
|
Studios *HierarchicalMultiCriterionInput `json:"studios"`
|
||||||
// Filter to only include movies missing this property
|
// Filter to only include movies missing this property
|
||||||
|
|||||||
@@ -111,8 +111,10 @@ type PerformerFilterType struct {
|
|||||||
GalleryCount *IntCriterionInput `json:"gallery_count"`
|
GalleryCount *IntCriterionInput `json:"gallery_count"`
|
||||||
// Filter by StashID
|
// Filter by StashID
|
||||||
StashID *StringCriterionInput `json:"stash_id"`
|
StashID *StringCriterionInput `json:"stash_id"`
|
||||||
// Filter by rating
|
// Filter by rating expressed as 1-5
|
||||||
Rating *IntCriterionInput `json:"rating"`
|
Rating *IntCriterionInput `json:"rating"`
|
||||||
|
// Filter by rating expressed as 1-100
|
||||||
|
Rating100 *IntCriterionInput `json:"rating100"`
|
||||||
// Filter by url
|
// Filter by url
|
||||||
URL *StringCriterionInput `json:"url"`
|
URL *StringCriterionInput `json:"url"`
|
||||||
// Filter by hair color
|
// 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"`
|
Path *StringCriterionInput `json:"path"`
|
||||||
// Filter by file count
|
// Filter by file count
|
||||||
FileCount *IntCriterionInput `json:"file_count"`
|
FileCount *IntCriterionInput `json:"file_count"`
|
||||||
// Filter by rating
|
// Filter by rating expressed as 1-5
|
||||||
Rating *IntCriterionInput `json:"rating"`
|
Rating *IntCriterionInput `json:"rating"`
|
||||||
|
// Filter by rating expressed as 1-100
|
||||||
|
Rating100 *IntCriterionInput `json:"rating100"`
|
||||||
// Filter by organized
|
// Filter by organized
|
||||||
Organized *bool `json:"organized"`
|
Organized *bool `json:"organized"`
|
||||||
// Filter by o-counter
|
// Filter by o-counter
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ type StudioFilterType struct {
|
|||||||
StashID *StringCriterionInput `json:"stash_id"`
|
StashID *StringCriterionInput `json:"stash_id"`
|
||||||
// Filter to only include studios missing this property
|
// Filter to only include studios missing this property
|
||||||
IsMissing *string `json:"is_missing"`
|
IsMissing *string `json:"is_missing"`
|
||||||
// Filter by rating
|
// Filter by rating expressed as 1-5
|
||||||
Rating *IntCriterionInput `json:"rating"`
|
Rating *IntCriterionInput `json:"rating"`
|
||||||
|
// Filter by rating expressed as 1-100
|
||||||
|
Rating100 *IntCriterionInput `json:"rating100"`
|
||||||
// Filter by scene count
|
// Filter by scene count
|
||||||
SceneCount *IntCriterionInput `json:"scene_count"`
|
SceneCount *IntCriterionInput `json:"scene_count"`
|
||||||
// Filter by image count
|
// Filter by image count
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import (
|
|||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var appSchemaVersion uint = 39
|
var appSchemaVersion uint = 40
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*.sql
|
||||||
var migrationsBox embed.FS
|
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 {
|
func dateCriterionHandler(c *models.DateCriterionInput, column string) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if c != nil {
|
if c != nil {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type galleryRow struct {
|
|||||||
URL zero.String `db:"url"`
|
URL zero.String `db:"url"`
|
||||||
Date models.SQLiteDate `db:"date"`
|
Date models.SQLiteDate `db:"date"`
|
||||||
Details zero.String `db:"details"`
|
Details zero.String `db:"details"`
|
||||||
|
// expressed as 1-100
|
||||||
Rating null.Int `db:"rating"`
|
Rating null.Int `db:"rating"`
|
||||||
Organized bool `db:"organized"`
|
Organized bool `db:"organized"`
|
||||||
StudioID null.Int `db:"studio_id,omitempty"`
|
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, qb.galleryPathCriterionHandler(galleryFilter.Path))
|
||||||
query.handleCriterion(ctx, galleryFileCountCriterionHandler(qb, galleryFilter.FileCount))
|
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, stringCriterionHandler(galleryFilter.URL, "galleries.url"))
|
||||||
query.handleCriterion(ctx, boolCriterionHandler(galleryFilter.Organized, "galleries.organized", nil))
|
query.handleCriterion(ctx, boolCriterionHandler(galleryFilter.Organized, "galleries.organized", nil))
|
||||||
query.handleCriterion(ctx, galleryIsMissingCriterionHandler(qb, galleryFilter.IsMissing))
|
query.handleCriterion(ctx, galleryIsMissingCriterionHandler(qb, galleryFilter.IsMissing))
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func Test_galleryQueryBuilder_Create(t *testing.T) {
|
|||||||
var (
|
var (
|
||||||
title = "title"
|
title = "title"
|
||||||
url = "url"
|
url = "url"
|
||||||
rating = 3
|
rating = 60
|
||||||
details = "details"
|
details = "details"
|
||||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
updatedAt = 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 (
|
var (
|
||||||
title = "title"
|
title = "title"
|
||||||
url = "url"
|
url = "url"
|
||||||
rating = 3
|
rating = 60
|
||||||
details = "details"
|
details = "details"
|
||||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
updatedAt = 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"
|
title = "title"
|
||||||
details = "details"
|
details = "details"
|
||||||
url = "url"
|
url = "url"
|
||||||
rating = 3
|
rating = 60
|
||||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
updatedAt = 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,
|
Modifier: models.CriterionModifierEquals,
|
||||||
},
|
},
|
||||||
And: &models.GalleryFilterType{
|
And: &models.GalleryFilterType{
|
||||||
Rating: &models.IntCriterionInput{
|
Rating100: &models.IntCriterionInput{
|
||||||
Value: *galleryRating,
|
Value: *galleryRating,
|
||||||
Modifier: models.CriterionModifierEquals,
|
Modifier: models.CriterionModifierEquals,
|
||||||
},
|
},
|
||||||
@@ -1588,7 +1588,7 @@ func TestGalleryQueryPathNotRating(t *testing.T) {
|
|||||||
galleryFilter := models.GalleryFilterType{
|
galleryFilter := models.GalleryFilterType{
|
||||||
Path: &pathCriterion,
|
Path: &pathCriterion,
|
||||||
Not: &models.GalleryFilterType{
|
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
|
const rating = 3
|
||||||
ratingCriterion := models.IntCriterionInput{
|
ratingCriterion := models.IntCriterionInput{
|
||||||
Value: rating,
|
Value: rating,
|
||||||
Modifier: models.CriterionModifierEquals,
|
Modifier: models.CriterionModifierEquals,
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyGalleriesRating(t, ratingCriterion)
|
verifyGalleriesLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||||
verifyGalleriesRating(t, ratingCriterion)
|
verifyGalleriesLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||||
verifyGalleriesRating(t, ratingCriterion)
|
verifyGalleriesLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
||||||
verifyGalleriesRating(t, ratingCriterion)
|
verifyGalleriesLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
||||||
verifyGalleriesRating(t, ratingCriterion)
|
verifyGalleriesLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierNotNull
|
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 {
|
withTxn(func(ctx context.Context) error {
|
||||||
sqb := db.Gallery
|
sqb := db.Gallery
|
||||||
galleryFilter := models.GalleryFilterType{
|
galleryFilter := models.GalleryFilterType{
|
||||||
@@ -1736,6 +1736,54 @@ func verifyGalleriesRating(t *testing.T, ratingCriterion models.IntCriterionInpu
|
|||||||
t.Errorf("Error querying gallery: %s", err.Error())
|
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 {
|
for _, gallery := range galleries {
|
||||||
verifyIntPtr(t, gallery.Rating, ratingCriterion)
|
verifyIntPtr(t, gallery.Rating, ratingCriterion)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const (
|
|||||||
type imageRow struct {
|
type imageRow struct {
|
||||||
ID int `db:"id" goqu:"skipinsert"`
|
ID int `db:"id" goqu:"skipinsert"`
|
||||||
Title zero.String `db:"title"`
|
Title zero.String `db:"title"`
|
||||||
|
// expressed as 1-100
|
||||||
Rating null.Int `db:"rating"`
|
Rating null.Int `db:"rating"`
|
||||||
Organized bool `db:"organized"`
|
Organized bool `db:"organized"`
|
||||||
OCounter int `db:"o_counter"`
|
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, pathCriterionHandler(imageFilter.Path, "folders.path", "files.basename", qb.addFoldersTable))
|
||||||
query.handleCriterion(ctx, imageFileCountCriterionHandler(qb, imageFilter.FileCount))
|
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, intCriterionHandler(imageFilter.OCounter, "images.o_counter", nil))
|
||||||
query.handleCriterion(ctx, boolCriterionHandler(imageFilter.Organized, "images.organized", 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) {
|
func Test_imageQueryBuilder_Create(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
title = "title"
|
title = "title"
|
||||||
rating = 3
|
rating = 60
|
||||||
ocounter = 5
|
ocounter = 5
|
||||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
updatedAt = 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) {
|
func Test_imageQueryBuilder_Update(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
title = "title"
|
title = "title"
|
||||||
rating = 3
|
rating = 60
|
||||||
ocounter = 5
|
ocounter = 5
|
||||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
updatedAt = 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) {
|
func Test_imageQueryBuilder_UpdatePartial(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
title = "title"
|
title = "title"
|
||||||
rating = 3
|
rating = 60
|
||||||
ocounter = 5
|
ocounter = 5
|
||||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
updatedAt = 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,
|
Modifier: models.CriterionModifierEquals,
|
||||||
},
|
},
|
||||||
And: &models.ImageFilterType{
|
And: &models.ImageFilterType{
|
||||||
Rating: &models.IntCriterionInput{
|
Rating100: &models.IntCriterionInput{
|
||||||
Value: int(imageRating.Int64),
|
Value: int(imageRating.Int64),
|
||||||
Modifier: models.CriterionModifierEquals,
|
Modifier: models.CriterionModifierEquals,
|
||||||
},
|
},
|
||||||
@@ -1607,7 +1607,10 @@ func TestImageQueryPathAndRating(t *testing.T) {
|
|||||||
|
|
||||||
images := queryImages(ctx, t, sqb, &imageFilter, nil)
|
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, imagePath, images[0].Path)
|
||||||
assert.Equal(t, int(imageRating.Int64), *images[0].Rating)
|
assert.Equal(t, int(imageRating.Int64), *images[0].Rating)
|
||||||
|
|
||||||
@@ -1633,7 +1636,7 @@ func TestImageQueryPathNotRating(t *testing.T) {
|
|||||||
imageFilter := models.ImageFilterType{
|
imageFilter := models.ImageFilterType{
|
||||||
Path: &pathCriterion,
|
Path: &pathCriterion,
|
||||||
Not: &models.ImageFilterType{
|
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
|
const rating = 3
|
||||||
ratingCriterion := models.IntCriterionInput{
|
ratingCriterion := models.IntCriterionInput{
|
||||||
Value: rating,
|
Value: rating,
|
||||||
Modifier: models.CriterionModifierEquals,
|
Modifier: models.CriterionModifierEquals,
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyImagesRating(t, ratingCriterion)
|
verifyImagesLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||||
verifyImagesRating(t, ratingCriterion)
|
verifyImagesLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||||
verifyImagesRating(t, ratingCriterion)
|
verifyImagesLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
||||||
verifyImagesRating(t, ratingCriterion)
|
verifyImagesLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
||||||
verifyImagesRating(t, ratingCriterion)
|
verifyImagesLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierNotNull
|
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 {
|
withTxn(func(ctx context.Context) error {
|
||||||
sqb := db.Image
|
sqb := db.Image
|
||||||
imageFilter := models.ImageFilterType{
|
imageFilter := models.ImageFilterType{
|
||||||
@@ -1725,6 +1728,54 @@ func verifyImagesRating(t *testing.T, ratingCriterion models.IntCriterionInput)
|
|||||||
t.Errorf("Error querying image: %s", err.Error())
|
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 {
|
for _, image := range images {
|
||||||
verifyIntPtr(t, image.Rating, ratingCriterion)
|
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.Name, "movies.name"))
|
||||||
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Director, "movies.director"))
|
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Director, "movies.director"))
|
||||||
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Synopsis, "movies.synopsis"))
|
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, durationCriterionHandler(movieFilter.Duration, "movies.duration", nil))
|
||||||
query.handleCriterion(ctx, movieIsMissingCriterionHandler(qb, movieFilter.IsMissing))
|
query.handleCriterion(ctx, movieIsMissingCriterionHandler(qb, movieFilter.IsMissing))
|
||||||
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.URL, "movies.url"))
|
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.URL, "movies.url"))
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ type performerRow struct {
|
|||||||
Favorite sql.NullBool `db:"favorite"`
|
Favorite sql.NullBool `db:"favorite"`
|
||||||
CreatedAt models.SQLiteTimestamp `db:"created_at"`
|
CreatedAt models.SQLiteTimestamp `db:"created_at"`
|
||||||
UpdatedAt models.SQLiteTimestamp `db:"updated_at"`
|
UpdatedAt models.SQLiteTimestamp `db:"updated_at"`
|
||||||
|
// expressed as 1-100
|
||||||
Rating null.Int `db:"rating"`
|
Rating null.Int `db:"rating"`
|
||||||
Details zero.String `db:"details"`
|
Details zero.String `db:"details"`
|
||||||
DeathDate models.SQLiteDate `db:"death_date"`
|
DeathDate models.SQLiteDate `db:"death_date"`
|
||||||
@@ -111,6 +112,7 @@ func (r *performerRow) resolve() *models.Performer {
|
|||||||
Favorite: r.Favorite.Bool,
|
Favorite: r.Favorite.Bool,
|
||||||
CreatedAt: r.CreatedAt.Timestamp,
|
CreatedAt: r.CreatedAt.Timestamp,
|
||||||
UpdatedAt: r.UpdatedAt.Timestamp,
|
UpdatedAt: r.UpdatedAt.Timestamp,
|
||||||
|
// expressed as 1-100
|
||||||
Rating: nullIntPtr(r.Rating),
|
Rating: nullIntPtr(r.Rating),
|
||||||
Details: r.Details.String,
|
Details: r.Details.String,
|
||||||
DeathDate: r.DeathDate.DatePtr(),
|
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.CareerLength, tableName+".career_length"))
|
||||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Tattoos, tableName+".tattoos"))
|
query.handleCriterion(ctx, stringCriterionHandler(filter.Tattoos, tableName+".tattoos"))
|
||||||
query.handleCriterion(ctx, stringCriterionHandler(filter.Piercings, tableName+".piercings"))
|
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.HairColor, tableName+".hair_color"))
|
||||||
query.handleCriterion(ctx, stringCriterionHandler(filter.URL, tableName+".url"))
|
query.handleCriterion(ctx, stringCriterionHandler(filter.URL, tableName+".url"))
|
||||||
query.handleCriterion(ctx, intCriterionHandler(filter.Weight, tableName+".weight", nil))
|
query.handleCriterion(ctx, intCriterionHandler(filter.Weight, tableName+".weight", nil))
|
||||||
|
|||||||
@@ -440,7 +440,7 @@ func TestPerformerQueryEthnicityAndRating(t *testing.T) {
|
|||||||
Modifier: models.CriterionModifierEquals,
|
Modifier: models.CriterionModifierEquals,
|
||||||
},
|
},
|
||||||
And: &models.PerformerFilterType{
|
And: &models.PerformerFilterType{
|
||||||
Rating: &models.IntCriterionInput{
|
Rating100: &models.IntCriterionInput{
|
||||||
Value: performerRating,
|
Value: performerRating,
|
||||||
Modifier: models.CriterionModifierEquals,
|
Modifier: models.CriterionModifierEquals,
|
||||||
},
|
},
|
||||||
@@ -450,7 +450,10 @@ func TestPerformerQueryEthnicityAndRating(t *testing.T) {
|
|||||||
withTxn(func(ctx context.Context) error {
|
withTxn(func(ctx context.Context) error {
|
||||||
performers := queryPerformers(ctx, t, &performerFilter, nil)
|
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)
|
assert.Equal(t, performerEth, performers[0].Ethnicity)
|
||||||
if assert.NotNil(t, performers[0].Rating) {
|
if assert.NotNil(t, performers[0].Rating) {
|
||||||
assert.Equal(t, performerRating, *performers[0].Rating)
|
assert.Equal(t, performerRating, *performers[0].Rating)
|
||||||
@@ -478,7 +481,7 @@ func TestPerformerQueryEthnicityNotRating(t *testing.T) {
|
|||||||
performerFilter := models.PerformerFilterType{
|
performerFilter := models.PerformerFilterType{
|
||||||
Ethnicity: ðCriterion,
|
Ethnicity: ðCriterion,
|
||||||
Not: &models.PerformerFilterType{
|
Not: &models.PerformerFilterType{
|
||||||
Rating: &ratingCriterion,
|
Rating100: &ratingCriterion,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1173,32 +1176,32 @@ func TestPerformerStashIDs(t *testing.T) {
|
|||||||
t.Error(err.Error())
|
t.Error(err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func TestPerformerQueryRating(t *testing.T) {
|
func TestPerformerQueryLegacyRating(t *testing.T) {
|
||||||
const rating = 3
|
const rating = 3
|
||||||
ratingCriterion := models.IntCriterionInput{
|
ratingCriterion := models.IntCriterionInput{
|
||||||
Value: rating,
|
Value: rating,
|
||||||
Modifier: models.CriterionModifierEquals,
|
Modifier: models.CriterionModifierEquals,
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyPerformersRating(t, ratingCriterion)
|
verifyPerformersLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||||
verifyPerformersRating(t, ratingCriterion)
|
verifyPerformersLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||||
verifyPerformersRating(t, ratingCriterion)
|
verifyPerformersLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
||||||
verifyPerformersRating(t, ratingCriterion)
|
verifyPerformersLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
||||||
verifyPerformersRating(t, ratingCriterion)
|
verifyPerformersLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierNotNull
|
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 {
|
withTxn(func(ctx context.Context) error {
|
||||||
performerFilter := models.PerformerFilterType{
|
performerFilter := models.PerformerFilterType{
|
||||||
Rating: &ratingCriterion,
|
Rating: &ratingCriterion,
|
||||||
@@ -1206,6 +1209,50 @@ func verifyPerformersRating(t *testing.T, ratingCriterion models.IntCriterionInp
|
|||||||
|
|
||||||
performers := queryPerformers(ctx, t, &performerFilter, nil)
|
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 {
|
for _, performer := range performers {
|
||||||
verifyIntPtr(t, performer.Rating, ratingCriterion)
|
verifyIntPtr(t, performer.Rating, ratingCriterion)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ type sceneRow struct {
|
|||||||
Director zero.String `db:"director"`
|
Director zero.String `db:"director"`
|
||||||
URL zero.String `db:"url"`
|
URL zero.String `db:"url"`
|
||||||
Date models.SQLiteDate `db:"date"`
|
Date models.SQLiteDate `db:"date"`
|
||||||
|
// expressed as 1-100
|
||||||
Rating null.Int `db:"rating"`
|
Rating null.Int `db:"rating"`
|
||||||
Organized bool `db:"organized"`
|
Organized bool `db:"organized"`
|
||||||
OCounter int `db:"o_counter"`
|
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, intCriterionHandler(sceneFilter.OCounter, "scenes.o_counter", nil))
|
||||||
query.handleCriterion(ctx, boolCriterionHandler(sceneFilter.Organized, "scenes.organized", nil))
|
query.handleCriterion(ctx, boolCriterionHandler(sceneFilter.Organized, "scenes.organized", nil))
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
|
|||||||
details = "details"
|
details = "details"
|
||||||
director = "director"
|
director = "director"
|
||||||
url = "url"
|
url = "url"
|
||||||
rating = 3
|
rating = 60
|
||||||
ocounter = 5
|
ocounter = 5
|
||||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
updatedAt = 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"
|
details = "details"
|
||||||
director = "director"
|
director = "director"
|
||||||
url = "url"
|
url = "url"
|
||||||
rating = 3
|
rating = 60
|
||||||
ocounter = 5
|
ocounter = 5
|
||||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
updatedAt = 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"
|
details = "details"
|
||||||
director = "director"
|
director = "director"
|
||||||
url = "url"
|
url = "url"
|
||||||
rating = 3
|
rating = 60
|
||||||
ocounter = 5
|
ocounter = 5
|
||||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
updatedAt = 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,
|
Modifier: models.CriterionModifierEquals,
|
||||||
},
|
},
|
||||||
And: &models.SceneFilterType{
|
And: &models.SceneFilterType{
|
||||||
Rating: &models.IntCriterionInput{
|
Rating100: &models.IntCriterionInput{
|
||||||
Value: sceneRating,
|
Value: sceneRating,
|
||||||
Modifier: models.CriterionModifierEquals,
|
Modifier: models.CriterionModifierEquals,
|
||||||
},
|
},
|
||||||
@@ -2335,7 +2335,7 @@ func TestSceneQueryPathNotRating(t *testing.T) {
|
|||||||
sceneFilter := models.SceneFilterType{
|
sceneFilter := models.SceneFilterType{
|
||||||
Path: &pathCriterion,
|
Path: &pathCriterion,
|
||||||
Not: &models.SceneFilterType{
|
Not: &models.SceneFilterType{
|
||||||
Rating: &ratingCriterion,
|
Rating100: &ratingCriterion,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2522,25 +2522,25 @@ func TestSceneQueryRating(t *testing.T) {
|
|||||||
Modifier: models.CriterionModifierEquals,
|
Modifier: models.CriterionModifierEquals,
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyScenesRating(t, ratingCriterion)
|
verifyScenesLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||||
verifyScenesRating(t, ratingCriterion)
|
verifyScenesLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||||
verifyScenesRating(t, ratingCriterion)
|
verifyScenesLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
||||||
verifyScenesRating(t, ratingCriterion)
|
verifyScenesLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
||||||
verifyScenesRating(t, ratingCriterion)
|
verifyScenesLegacyRating(t, ratingCriterion)
|
||||||
|
|
||||||
ratingCriterion.Modifier = models.CriterionModifierNotNull
|
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 {
|
withTxn(func(ctx context.Context) error {
|
||||||
sqb := db.Scene
|
sqb := db.Scene
|
||||||
sceneFilter := models.SceneFilterType{
|
sceneFilter := models.SceneFilterType{
|
||||||
@@ -2549,6 +2549,51 @@ func verifyScenesRating(t *testing.T, ratingCriterion models.IntCriterionInput)
|
|||||||
|
|
||||||
scenes := queryScene(ctx, t, sqb, &sceneFilter, nil)
|
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 {
|
for _, scene := range scenes {
|
||||||
verifyIntPtr(t, scene.Rating, ratingCriterion)
|
verifyIntPtr(t, scene.Rating, ratingCriterion)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -823,7 +823,7 @@ func getSceneTitle(index int) string {
|
|||||||
|
|
||||||
func getRating(index int) sql.NullInt64 {
|
func getRating(index int) sql.NullInt64 {
|
||||||
rating := index % 6
|
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 {
|
func getIntPtr(r sql.NullInt64) *int {
|
||||||
@@ -967,11 +967,13 @@ func makeScene(i int) *models.Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rating := getRating(i)
|
||||||
|
|
||||||
return &models.Scene{
|
return &models.Scene{
|
||||||
Title: title,
|
Title: title,
|
||||||
Details: details,
|
Details: details,
|
||||||
URL: getSceneEmptyString(i, urlField),
|
URL: getSceneEmptyString(i, urlField),
|
||||||
Rating: getIntPtr(getRating(i)),
|
Rating: getIntPtr(rating),
|
||||||
OCounter: getOCounter(i),
|
OCounter: getOCounter(i),
|
||||||
Date: getObjectDateObject(i),
|
Date: getObjectDateObject(i),
|
||||||
StudioID: studioID,
|
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.Name, studioTable+".name"))
|
||||||
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.Details, studioTable+".details"))
|
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.Details, studioTable+".details"))
|
||||||
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.URL, studioTable+".url"))
|
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, boolCriterionHandler(studioFilter.IgnoreAutoTag, studioTable+".ignore_auto_tag", nil))
|
||||||
|
|
||||||
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
"no-descending-specificity": null,
|
"no-descending-specificity": null,
|
||||||
"no-invalid-double-slash-comments": true,
|
"no-invalid-double-slash-comments": true,
|
||||||
"no-missing-end-of-source-newline": true,
|
"no-missing-end-of-source-newline": true,
|
||||||
"number-max-precision": 2,
|
"number-max-precision": 3,
|
||||||
"number-no-trailing-zeros": true,
|
"number-no-trailing-zeros": true,
|
||||||
"order/order": [
|
"order/order": [
|
||||||
"custom-properties",
|
"custom-properties",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build-ci": "yarn validate && yarn 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": "yarn lint:css && yarn lint:js",
|
||||||
"lint:js": "eslint --cache src/**/*.{ts,tsx}",
|
"lint:js": "eslint --cache src/**/*.{ts,tsx}",
|
||||||
"lint:css": "stylelint \"src/**/*.scss\"",
|
"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 { useIntl } from "react-intl";
|
||||||
import {
|
import {
|
||||||
FrontPageContent,
|
FrontPageContent,
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from "src/core/config";
|
} from "src/core/config";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { useFindSavedFilter } from "src/core/StashService";
|
import { useFindSavedFilter } from "src/core/StashService";
|
||||||
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { GalleryRecommendationRow } from "../Galleries/GalleryRecommendationRow";
|
import { GalleryRecommendationRow } from "../Galleries/GalleryRecommendationRow";
|
||||||
import { ImageRecommendationRow } from "../Images/ImageRecommendationRow";
|
import { ImageRecommendationRow } from "../Images/ImageRecommendationRow";
|
||||||
@@ -98,6 +99,7 @@ interface ISavedFilterResults {
|
|||||||
const SavedFilterResults: React.FC<ISavedFilterResults> = ({
|
const SavedFilterResults: React.FC<ISavedFilterResults> = ({
|
||||||
savedFilterID,
|
savedFilterID,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { configuration: config } = useContext(ConfigurationContext);
|
||||||
const { loading, data } = useFindSavedFilter(savedFilterID.toString());
|
const { loading, data } = useFindSavedFilter(savedFilterID.toString());
|
||||||
|
|
||||||
const filter = useMemo(() => {
|
const filter = useMemo(() => {
|
||||||
@@ -105,12 +107,12 @@ const SavedFilterResults: React.FC<ISavedFilterResults> = ({
|
|||||||
|
|
||||||
const { mode, filter: filterJSON } = data.findSavedFilter;
|
const { mode, filter: filterJSON } = data.findSavedFilter;
|
||||||
|
|
||||||
const ret = new ListFilterModel(mode);
|
const ret = new ListFilterModel(mode, config);
|
||||||
ret.currentPage = 1;
|
ret.currentPage = 1;
|
||||||
ret.configureFromJSON(filterJSON);
|
ret.configureFromJSON(filterJSON);
|
||||||
ret.randomSeed = -1;
|
ret.randomSeed = -1;
|
||||||
return ret;
|
return ret;
|
||||||
}, [data?.findSavedFilter]);
|
}, [data?.findSavedFilter, config]);
|
||||||
|
|
||||||
if (loading || !data?.findSavedFilter || !filter) {
|
if (loading || !data?.findSavedFilter || !filter) {
|
||||||
return <></>;
|
return <></>;
|
||||||
@@ -128,18 +130,19 @@ interface ICustomFilterProps {
|
|||||||
const CustomFilterResults: React.FC<ICustomFilterProps> = ({
|
const CustomFilterResults: React.FC<ICustomFilterProps> = ({
|
||||||
customFilter,
|
customFilter,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { configuration: config } = useContext(ConfigurationContext);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const filter = useMemo(() => {
|
const filter = useMemo(() => {
|
||||||
const itemsPerPage = 25;
|
const itemsPerPage = 25;
|
||||||
const ret = new ListFilterModel(customFilter.mode);
|
const ret = new ListFilterModel(customFilter.mode, config);
|
||||||
ret.sortBy = customFilter.sortBy;
|
ret.sortBy = customFilter.sortBy;
|
||||||
ret.sortDirection = customFilter.direction;
|
ret.sortDirection = customFilter.direction;
|
||||||
ret.itemsPerPage = itemsPerPage;
|
ret.itemsPerPage = itemsPerPage;
|
||||||
ret.currentPage = 1;
|
ret.currentPage = 1;
|
||||||
ret.randomSeed = -1;
|
ret.randomSeed = -1;
|
||||||
return ret;
|
return ret;
|
||||||
}, [customFilter]);
|
}, [customFilter, config]);
|
||||||
|
|
||||||
const header = customFilter.message
|
const header = customFilter.message
|
||||||
? intl.formatMessage(
|
? intl.formatMessage(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { StudioSelect, Modal } from "src/components/Shared";
|
|||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { FormUtils } from "src/utils";
|
import { FormUtils } from "src/utils";
|
||||||
import MultiSet from "../Shared/MultiSet";
|
import MultiSet from "../Shared/MultiSet";
|
||||||
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
import {
|
import {
|
||||||
getAggregateInputIDs,
|
getAggregateInputIDs,
|
||||||
getAggregateInputValue,
|
getAggregateInputValue,
|
||||||
@@ -29,7 +29,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||||||
) => {
|
) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [rating, setRating] = useState<number>();
|
const [rating100, setRating] = useState<number>();
|
||||||
const [studioId, setStudioId] = useState<string>();
|
const [studioId, setStudioId] = useState<string>();
|
||||||
const [
|
const [
|
||||||
performerMode,
|
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(
|
galleryInput.studio_id = getAggregateInputValue(
|
||||||
studioId,
|
studioId,
|
||||||
aggregateStudioId
|
aggregateStudioId
|
||||||
@@ -121,7 +121,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||||||
let first = true;
|
let first = true;
|
||||||
|
|
||||||
state.forEach((gallery: GQL.SlimGalleryDataFragment) => {
|
state.forEach((gallery: GQL.SlimGalleryDataFragment) => {
|
||||||
const galleryRating = gallery.rating;
|
const galleryRating = gallery.rating100;
|
||||||
const GalleriestudioID = gallery?.studio?.id;
|
const GalleriestudioID = gallery?.studio?.id;
|
||||||
const galleryPerformerIDs = (gallery.performers ?? [])
|
const galleryPerformerIDs = (gallery.performers ?? [])
|
||||||
.map((p) => p.id)
|
.map((p) => p.id)
|
||||||
@@ -256,14 +256,13 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||||||
title: intl.formatMessage({ id: "rating" }),
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingStars
|
<RatingSystem
|
||||||
value={rating}
|
value={rating100}
|
||||||
onSetRating={(value) => setRating(value)}
|
onSetRating={(value) => setRating(value)}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
<Form.Group controlId="studio" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: intl.formatMessage({ id: "studio" }),
|
title: intl.formatMessage({ id: "studio" }),
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
|
|||||||
src={`${props.gallery.cover.paths.thumbnail}`}
|
src={`${props.gallery.cover.paths.thumbnail}`}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<RatingBanner rating={props.gallery.rating} />
|
<RatingBanner rating={props.gallery.rating100} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
overlays={maybeRenderSceneStudioOverlay()}
|
overlays={maybeRenderSceneStudioOverlay()}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as GQL from "src/core/generated-graphql";
|
|||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
import { TagLink, TruncatedText } from "src/components/Shared";
|
import { TagLink, TruncatedText } from "src/components/Shared";
|
||||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
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 { sortPerformers } from "src/core/performers";
|
||||||
import { galleryTitle } from "src/core/galleries";
|
import { galleryTitle } from "src/core/galleries";
|
||||||
|
|
||||||
@@ -94,10 +94,10 @@ export const GalleryDetailPanel: React.FC<IGalleryDetailProps> = ({
|
|||||||
/>
|
/>
|
||||||
</h5>
|
</h5>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{gallery.rating ? (
|
{gallery.rating100 ? (
|
||||||
<h6>
|
<h6>
|
||||||
<FormattedMessage id="rating" />:{" "}
|
<FormattedMessage id="rating" />:{" "}
|
||||||
<RatingStars value={gallery.rating} />
|
<RatingSystem value={gallery.rating100} disabled />
|
||||||
</h6>
|
</h6>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { FormUtils } from "src/utils";
|
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 { GalleryScrapeDialog } from "./GalleryScrapeDialog";
|
||||||
import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { galleryTitle } from "src/core/galleries";
|
import { galleryTitle } from "src/core/galleries";
|
||||||
@@ -89,7 +89,7 @@ export const GalleryEditPanel: React.FC<
|
|||||||
details: yup.string().optional().nullable(),
|
details: yup.string().optional().nullable(),
|
||||||
url: yup.string().optional().nullable(),
|
url: yup.string().optional().nullable(),
|
||||||
date: 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(),
|
studio_id: yup.string().optional().nullable(),
|
||||||
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||||
tag_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 ?? "",
|
details: gallery?.details ?? "",
|
||||||
url: gallery?.url ?? "",
|
url: gallery?.url ?? "",
|
||||||
date: gallery?.date ?? "",
|
date: gallery?.date ?? "",
|
||||||
rating: gallery?.rating ?? null,
|
rating100: gallery?.rating100 ?? null,
|
||||||
studio_id: gallery?.studio?.id,
|
studio_id: gallery?.studio?.id,
|
||||||
performer_ids: (gallery?.performers ?? []).map((p) => p.id),
|
performer_ids: (gallery?.performers ?? []).map((p) => p.id),
|
||||||
tag_ids: (gallery?.tags ?? []).map((t) => t.id),
|
tag_ids: (gallery?.tags ?? []).map((t) => t.id),
|
||||||
@@ -117,7 +117,7 @@ export const GalleryEditPanel: React.FC<
|
|||||||
});
|
});
|
||||||
|
|
||||||
function setRating(v: number) {
|
function setRating(v: number) {
|
||||||
formik.setFieldValue("rating", v);
|
formik.setFieldValue("rating100", v);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISceneSelectValue {
|
interface ISceneSelectValue {
|
||||||
@@ -150,11 +150,11 @@ export const GalleryEditPanel: React.FC<
|
|||||||
}
|
}
|
||||||
|
|
||||||
Mousetrap.bind("0", () => setRating(NaN));
|
Mousetrap.bind("0", () => setRating(NaN));
|
||||||
Mousetrap.bind("1", () => setRating(1));
|
Mousetrap.bind("1", () => setRating(20));
|
||||||
Mousetrap.bind("2", () => setRating(2));
|
Mousetrap.bind("2", () => setRating(40));
|
||||||
Mousetrap.bind("3", () => setRating(3));
|
Mousetrap.bind("3", () => setRating(60));
|
||||||
Mousetrap.bind("4", () => setRating(4));
|
Mousetrap.bind("4", () => setRating(80));
|
||||||
Mousetrap.bind("5", () => setRating(5));
|
Mousetrap.bind("5", () => setRating(100));
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Mousetrap.unbind("0");
|
Mousetrap.unbind("0");
|
||||||
@@ -483,15 +483,14 @@ export const GalleryEditPanel: React.FC<
|
|||||||
title: intl.formatMessage({ id: "rating" }),
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingStars
|
<RatingSystem
|
||||||
value={formik.values.rating ?? undefined}
|
value={formik.values.rating100 ?? undefined}
|
||||||
onSetRating={(value) =>
|
onSetRating={(value) =>
|
||||||
formik.setFieldValue("rating", value ?? null)
|
formik.setFieldValue("rating100", value ?? null)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
<Form.Group controlId="studio" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: intl.formatMessage({ id: "studio" }),
|
title: intl.formatMessage({ id: "studio" }),
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import React from "react";
|
|||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
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 { TextUtils } from "src/utils";
|
||||||
import { useGalleryLightbox } from "src/hooks";
|
import { useGalleryLightbox } from "src/hooks";
|
||||||
import { galleryTitle } from "src/core/galleries";
|
import { galleryTitle } from "src/core/galleries";
|
||||||
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
|
|
||||||
const CLASSNAME = "GalleryWallCard";
|
const CLASSNAME = "GalleryWallCard";
|
||||||
const CLASSNAME_FOOTER = `${CLASSNAME}-footer`;
|
const CLASSNAME_FOOTER = `${CLASSNAME}-footer`;
|
||||||
@@ -45,7 +46,7 @@ const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<RatingStars rating={gallery.rating} />
|
<RatingSystem value={gallery.rating100 ?? undefined} disabled />
|
||||||
<img src={cover} alt="" className={CLASSNAME_IMG} />
|
<img src={cover} alt="" className={CLASSNAME_IMG} />
|
||||||
<footer className={CLASSNAME_FOOTER}>
|
<footer className={CLASSNAME_FOOTER}>
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -204,16 +204,25 @@ $galleryTabWidth: 450px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.RatingStars {
|
.rating-stars,
|
||||||
|
.rating-number {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
top: 1rem;
|
top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
&-unfilled {
|
.rating-stars {
|
||||||
|
.star-fill-0 .unfilled-star {
|
||||||
display: none;
|
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);
|
filter: drop-shadow(1px 1px 1px #222);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { StudioSelect, Modal } from "src/components/Shared";
|
|||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { FormUtils } from "src/utils";
|
import { FormUtils } from "src/utils";
|
||||||
import MultiSet from "../Shared/MultiSet";
|
import MultiSet from "../Shared/MultiSet";
|
||||||
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
import {
|
import {
|
||||||
getAggregateInputIDs,
|
getAggregateInputIDs,
|
||||||
getAggregateInputValue,
|
getAggregateInputValue,
|
||||||
@@ -29,7 +29,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||||||
) => {
|
) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [rating, setRating] = useState<number>();
|
const [rating100, setRating] = useState<number>();
|
||||||
const [studioId, setStudioId] = useState<string>();
|
const [studioId, setStudioId] = useState<string>();
|
||||||
const [
|
const [
|
||||||
performerMode,
|
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.studio_id = getAggregateInputValue(studioId, aggregateStudioId);
|
||||||
|
|
||||||
imageInput.performer_ids = getAggregateInputIDs(
|
imageInput.performer_ids = getAggregateInputIDs(
|
||||||
@@ -112,7 +112,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||||||
let first = true;
|
let first = true;
|
||||||
|
|
||||||
state.forEach((image: GQL.SlimImageDataFragment) => {
|
state.forEach((image: GQL.SlimImageDataFragment) => {
|
||||||
const imageRating = image.rating;
|
const imageRating = image.rating100;
|
||||||
const imageStudioID = image?.studio?.id;
|
const imageStudioID = image?.studio?.id;
|
||||||
const imagePerformerIDs = (image.performers ?? [])
|
const imagePerformerIDs = (image.performers ?? [])
|
||||||
.map((p) => p.id)
|
.map((p) => p.id)
|
||||||
@@ -246,14 +246,13 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||||||
title: intl.formatMessage({ id: "rating" }),
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingStars
|
<RatingSystem
|
||||||
value={rating}
|
value={rating100}
|
||||||
onSetRating={(value) => setRating(value)}
|
onSetRating={(value) => setRating(value)}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
<Form.Group controlId="studio" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: intl.formatMessage({ id: "studio" }),
|
title: intl.formatMessage({ id: "studio" }),
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export const ImageCard: React.FC<IImageCardProps> = (
|
|||||||
</div>
|
</div>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
<RatingBanner rating={props.image.rating} />
|
<RatingBanner rating={props.image.rating100} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
popovers={maybeRenderPopoverButtonGroup()}
|
popovers={maybeRenderPopoverButtonGroup()}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as GQL from "src/core/generated-graphql";
|
|||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
import { TagLink, TruncatedText } from "src/components/Shared";
|
import { TagLink, TruncatedText } from "src/components/Shared";
|
||||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
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 { sortPerformers } from "src/core/performers";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { objectTitle } from "src/core/files";
|
import { objectTitle } from "src/core/files";
|
||||||
@@ -91,10 +91,10 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = (props) => {
|
|||||||
<TruncatedText text={objectTitle(props.image)} />
|
<TruncatedText text={objectTitle(props.image)} />
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
{props.image.rating ? (
|
{props.image.rating100 ? (
|
||||||
<h6>
|
<h6>
|
||||||
<FormattedMessage id="rating" />:{" "}
|
<FormattedMessage id="rating" />:{" "}
|
||||||
<RatingStars value={props.image.rating} />
|
<RatingSystem value={props.image.rating100} disabled />
|
||||||
</h6>
|
</h6>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { useToast } from "src/hooks";
|
|||||||
import { FormUtils } from "src/utils";
|
import { FormUtils } from "src/utils";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
image: GQL.ImageDataFragment;
|
image: GQL.ImageDataFragment;
|
||||||
@@ -38,7 +38,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
title: yup.string().optional().nullable(),
|
title: yup.string().optional().nullable(),
|
||||||
rating: yup.number().optional().nullable(),
|
rating100: yup.number().optional().nullable(),
|
||||||
studio_id: yup.string().optional().nullable(),
|
studio_id: yup.string().optional().nullable(),
|
||||||
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||||
tag_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 = {
|
const initialValues = {
|
||||||
title: image.title ?? "",
|
title: image.title ?? "",
|
||||||
rating: image.rating ?? null,
|
rating100: image.rating100 ?? null,
|
||||||
studio_id: image.studio?.id,
|
studio_id: image.studio?.id,
|
||||||
performer_ids: (image.performers ?? []).map((p) => p.id),
|
performer_ids: (image.performers ?? []).map((p) => p.id),
|
||||||
tag_ids: (image.tags ?? []).map((t) => t.id),
|
tag_ids: (image.tags ?? []).map((t) => t.id),
|
||||||
@@ -61,7 +61,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function setRating(v: number) {
|
function setRating(v: number) {
|
||||||
formik.setFieldValue("rating", v);
|
formik.setFieldValue("rating100", v);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -81,11 +81,11 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
Mousetrap.bind("0", () => setRating(NaN));
|
Mousetrap.bind("0", () => setRating(NaN));
|
||||||
Mousetrap.bind("1", () => setRating(1));
|
Mousetrap.bind("1", () => setRating(20));
|
||||||
Mousetrap.bind("2", () => setRating(2));
|
Mousetrap.bind("2", () => setRating(40));
|
||||||
Mousetrap.bind("3", () => setRating(3));
|
Mousetrap.bind("3", () => setRating(60));
|
||||||
Mousetrap.bind("4", () => setRating(4));
|
Mousetrap.bind("4", () => setRating(80));
|
||||||
Mousetrap.bind("5", () => setRating(5));
|
Mousetrap.bind("5", () => setRating(100));
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Mousetrap.unbind("0");
|
Mousetrap.unbind("0");
|
||||||
@@ -194,15 +194,14 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
title: intl.formatMessage({ id: "rating" }),
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingStars
|
<RatingSystem
|
||||||
value={formik.values.rating ?? undefined}
|
value={formik.values.rating100 ?? undefined}
|
||||||
onSetRating={(value) =>
|
onSetRating={(value) =>
|
||||||
formik.setFieldValue("rating", value ?? null)
|
formik.setFieldValue("rating100", value ?? null)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
<Form.Group controlId="studio" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: intl.formatMessage({ id: "studio" }),
|
title: intl.formatMessage({ id: "studio" }),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import cloneDeep from "lodash-es/cloneDeep";
|
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 { Button, Form, Modal } from "react-bootstrap";
|
||||||
import { CriterionModifier } from "src/core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
@@ -36,6 +36,9 @@ import { DateFilter } from "./Filters/DateFilter";
|
|||||||
import { TimestampFilter } from "./Filters/TimestampFilter";
|
import { TimestampFilter } from "./Filters/TimestampFilter";
|
||||||
import { CountryCriterion } from "src/models/list-filter/criteria/country";
|
import { CountryCriterion } from "src/models/list-filter/criteria/country";
|
||||||
import { CountrySelect } from "../Shared";
|
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 {
|
interface IAddFilterProps {
|
||||||
onAddCriterion: (
|
onAddCriterion: (
|
||||||
@@ -63,17 +66,18 @@ export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
|||||||
const { options, modifierOptions } = criterion.criterionOption;
|
const { options, modifierOptions } = criterion.criterionOption;
|
||||||
|
|
||||||
const valueStage = useRef<CriterionValue>(criterion.value);
|
const valueStage = useRef<CriterionValue>(criterion.value);
|
||||||
|
const { configuration: config } = useContext(ConfigurationContext);
|
||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
// Configure if we are editing an existing criterion
|
// Configure if we are editing an existing criterion
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editingCriterion) {
|
if (!editingCriterion) {
|
||||||
setCriterion(makeCriteria());
|
setCriterion(makeCriteria(config));
|
||||||
} else {
|
} else {
|
||||||
setCriterion(editingCriterion);
|
setCriterion(editingCriterion);
|
||||||
}
|
}
|
||||||
}, [editingCriterion]);
|
}, [config, editingCriterion]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
valueStage.current = criterion.value;
|
valueStage.current = criterion.value;
|
||||||
@@ -81,7 +85,7 @@ export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
|||||||
|
|
||||||
function onChangedCriteriaType(event: React.ChangeEvent<HTMLSelectElement>) {
|
function onChangedCriteriaType(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||||
const newCriterionType = event.target.value as CriterionType;
|
const newCriterionType = event.target.value as CriterionType;
|
||||||
const newCriterion = makeCriteria(newCriterionType);
|
const newCriterion = makeCriteria(config, newCriterionType);
|
||||||
setCriterion(newCriterion);
|
setCriterion(newCriterion);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +200,15 @@ export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
|||||||
<NumberFilter criterion={criterion} onValueChanged={onValueChanged} />
|
<NumberFilter criterion={criterion} onValueChanged={onValueChanged} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (criterion instanceof RatingCriterion) {
|
||||||
|
return (
|
||||||
|
<RatingFilter
|
||||||
|
criterion={criterion}
|
||||||
|
onValueChanged={onValueChanged}
|
||||||
|
configuration={config}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
criterion instanceof CountryCriterion &&
|
criterion instanceof CountryCriterion &&
|
||||||
(criterion.modifier === CriterionModifier.Equals ||
|
(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 { Modal, StudioSelect } from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { FormUtils } from "src/utils";
|
import { FormUtils } from "src/utils";
|
||||||
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
import {
|
import {
|
||||||
getAggregateInputValue,
|
getAggregateInputValue,
|
||||||
getAggregateRating,
|
getAggregateRating,
|
||||||
@@ -24,7 +24,7 @@ export const EditMoviesDialog: React.FC<IListOperationProps> = (
|
|||||||
) => {
|
) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [rating, setRating] = useState<number | undefined>();
|
const [rating100, setRating] = useState<number | undefined>();
|
||||||
const [studioId, setStudioId] = useState<string | undefined>();
|
const [studioId, setStudioId] = useState<string | undefined>();
|
||||||
const [director, setDirector] = useState<string | undefined>();
|
const [director, setDirector] = useState<string | undefined>();
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export const EditMoviesDialog: React.FC<IListOperationProps> = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// if rating is undefined
|
// if rating is undefined
|
||||||
movieInput.rating = getAggregateInputValue(rating, aggregateRating);
|
movieInput.rating100 = getAggregateInputValue(rating100, aggregateRating);
|
||||||
movieInput.studio_id = getAggregateInputValue(studioId, aggregateStudioId);
|
movieInput.studio_id = getAggregateInputValue(studioId, aggregateStudioId);
|
||||||
|
|
||||||
return movieInput;
|
return movieInput;
|
||||||
@@ -77,11 +77,11 @@ export const EditMoviesDialog: React.FC<IListOperationProps> = (
|
|||||||
state.forEach((movie: GQL.MovieDataFragment) => {
|
state.forEach((movie: GQL.MovieDataFragment) => {
|
||||||
if (first) {
|
if (first) {
|
||||||
first = false;
|
first = false;
|
||||||
updateRating = movie.rating ?? undefined;
|
updateRating = movie.rating100 ?? undefined;
|
||||||
updateStudioId = movie.studio?.id ?? undefined;
|
updateStudioId = movie.studio?.id ?? undefined;
|
||||||
updateDirector = movie.director ?? undefined;
|
updateDirector = movie.director ?? undefined;
|
||||||
} else {
|
} else {
|
||||||
if (movie.rating !== updateRating) {
|
if (movie.rating100 !== updateRating) {
|
||||||
updateRating = undefined;
|
updateRating = undefined;
|
||||||
}
|
}
|
||||||
if (movie.studio?.id !== updateStudioId) {
|
if (movie.studio?.id !== updateStudioId) {
|
||||||
@@ -124,8 +124,8 @@ export const EditMoviesDialog: React.FC<IListOperationProps> = (
|
|||||||
title: intl.formatMessage({ id: "rating" }),
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingStars
|
<RatingSystem
|
||||||
value={rating}
|
value={rating100}
|
||||||
onSetRating={(value) => setRating(value)}
|
onSetRating={(value) => setRating(value)}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export const MovieCard: FunctionComponent<IProps> = (props: IProps) => {
|
|||||||
alt={props.movie.name ?? ""}
|
alt={props.movie.name ?? ""}
|
||||||
src={props.movie.front_image_path ?? ""}
|
src={props.movie.front_image_path ?? ""}
|
||||||
/>
|
/>
|
||||||
<RatingBanner rating={props.movie.rating} />
|
<RatingBanner rating={props.movie.rating100} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
details={
|
details={
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { DurationUtils, TextUtils } from "src/utils";
|
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";
|
import { TextField, URLField } from "src/utils/field";
|
||||||
|
|
||||||
interface IMovieDetailsPanel {
|
interface IMovieDetailsPanel {
|
||||||
@@ -27,7 +27,7 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({ movie }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderRatingField() {
|
function renderRatingField() {
|
||||||
if (!movie.rating) {
|
if (!movie.rating100) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({ movie }) => {
|
|||||||
<>
|
<>
|
||||||
<dt>{intl.formatMessage({ id: "rating" })}</dt>
|
<dt>{intl.formatMessage({ id: "rating" })}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<RatingStars value={movie.rating} disabled />
|
<RatingSystem value={movie.rating100} disabled />
|
||||||
</dd>
|
</dd>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { Modal as BSModal, Form, Button, Col, Row } from "react-bootstrap";
|
import { Modal as BSModal, Form, Button, Col, Row } from "react-bootstrap";
|
||||||
import { DurationUtils, FormUtils, ImageUtils } from "src/utils";
|
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 { useFormik } from "formik";
|
||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
import { MovieScrapeDialog } from "./MovieScrapeDialog";
|
import { MovieScrapeDialog } from "./MovieScrapeDialog";
|
||||||
@@ -69,7 +69,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
.optional()
|
.optional()
|
||||||
.nullable()
|
.nullable()
|
||||||
.matches(/^\d{4}-\d{2}-\d{2}$/),
|
.matches(/^\d{4}-\d{2}-\d{2}$/),
|
||||||
rating: yup.number().optional().nullable(),
|
rating100: yup.number().optional().nullable(),
|
||||||
studio_id: yup.string().optional().nullable(),
|
studio_id: yup.string().optional().nullable(),
|
||||||
director: yup.string().optional().nullable(),
|
director: yup.string().optional().nullable(),
|
||||||
synopsis: yup.string().optional().nullable(),
|
synopsis: yup.string().optional().nullable(),
|
||||||
@@ -83,7 +83,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
aliases: movie?.aliases,
|
aliases: movie?.aliases,
|
||||||
duration: movie?.duration,
|
duration: movie?.duration,
|
||||||
date: movie?.date,
|
date: movie?.date,
|
||||||
rating: movie?.rating ?? null,
|
rating100: movie?.rating100 ?? null,
|
||||||
studio_id: movie?.studio?.id,
|
studio_id: movie?.studio?.id,
|
||||||
director: movie?.director,
|
director: movie?.director,
|
||||||
synopsis: movie?.synopsis,
|
synopsis: movie?.synopsis,
|
||||||
@@ -116,17 +116,17 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
function setRating(v: number) {
|
function setRating(v: number) {
|
||||||
formik.setFieldValue("rating", v);
|
formik.setFieldValue("rating100", v);
|
||||||
}
|
}
|
||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind("r 0", () => setRating(NaN));
|
Mousetrap.bind("r 0", () => setRating(NaN));
|
||||||
Mousetrap.bind("r 1", () => setRating(1));
|
Mousetrap.bind("r 1", () => setRating(20));
|
||||||
Mousetrap.bind("r 2", () => setRating(2));
|
Mousetrap.bind("r 2", () => setRating(40));
|
||||||
Mousetrap.bind("r 3", () => setRating(3));
|
Mousetrap.bind("r 3", () => setRating(60));
|
||||||
Mousetrap.bind("r 4", () => setRating(4));
|
Mousetrap.bind("r 4", () => setRating(80));
|
||||||
Mousetrap.bind("r 5", () => setRating(5));
|
Mousetrap.bind("r 5", () => setRating(100));
|
||||||
// Mousetrap.bind("u", (e) => {
|
// Mousetrap.bind("u", (e) => {
|
||||||
// setStudioFocus()
|
// setStudioFocus()
|
||||||
// e.preventDefault();
|
// e.preventDefault();
|
||||||
@@ -164,7 +164,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
function getMovieInput(values: InputValues) {
|
function getMovieInput(values: InputValues) {
|
||||||
const input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput> = {
|
const input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput> = {
|
||||||
...values,
|
...values,
|
||||||
rating: values.rating ?? null,
|
rating100: values.rating100 ?? null,
|
||||||
studio_id: values.studio_id ?? null,
|
studio_id: values.studio_id ?? null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -432,15 +432,14 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
title: intl.formatMessage({ id: "rating" }),
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingStars
|
<RatingSystem
|
||||||
value={formik.values.rating ?? undefined}
|
value={formik.values.rating100 ?? undefined}
|
||||||
onSetRating={(value) =>
|
onSetRating={(value) =>
|
||||||
formik.setFieldValue("rating", value ?? null)
|
formik.setFieldValue("rating100", value ?? null)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="url" as={Row}>
|
<Form.Group controlId="url" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: intl.formatMessage({ id: "url" }),
|
title: intl.formatMessage({ id: "url" }),
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
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 { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useBulkPerformerUpdate } from "src/core/StashService";
|
import { useBulkPerformerUpdate } from "src/core/StashService";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Modal } from "src/components/Shared";
|
import { Modal } from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { FormUtils } from "src/utils";
|
|
||||||
import MultiSet from "../Shared/MultiSet";
|
import MultiSet from "../Shared/MultiSet";
|
||||||
import { RatingStars } from "../Scenes/SceneDetails/RatingStars";
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
import {
|
import {
|
||||||
getAggregateInputValue,
|
getAggregateInputValue,
|
||||||
getAggregateState,
|
getAggregateState,
|
||||||
@@ -21,6 +20,7 @@ import {
|
|||||||
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
|
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
|
||||||
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
|
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
|
||||||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FormUtils } from "../../utils";
|
||||||
|
|
||||||
interface IListOperationProps {
|
interface IListOperationProps {
|
||||||
selected: GQL.SlimPerformerDataFragment[];
|
selected: GQL.SlimPerformerDataFragment[];
|
||||||
@@ -32,7 +32,7 @@ const performerFields = [
|
|||||||
"url",
|
"url",
|
||||||
"instagram",
|
"instagram",
|
||||||
"twitter",
|
"twitter",
|
||||||
"rating",
|
"rating100",
|
||||||
"gender",
|
"gender",
|
||||||
"birthdate",
|
"birthdate",
|
||||||
"death_date",
|
"death_date",
|
||||||
@@ -90,9 +90,9 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
|
|
||||||
// we don't have unset functionality for the rating star control
|
// we don't have unset functionality for the rating star control
|
||||||
// so need to determine if we are setting a rating or not
|
// so need to determine if we are setting a rating or not
|
||||||
performerInput.rating = getAggregateInputValue(
|
performerInput.rating100 = getAggregateInputValue(
|
||||||
updateInput.rating,
|
updateInput.rating100,
|
||||||
aggregateState.rating
|
aggregateState.rating100
|
||||||
);
|
);
|
||||||
|
|
||||||
// gender dropdown doesn't have unset functionality
|
// gender dropdown doesn't have unset functionality
|
||||||
@@ -205,9 +205,9 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
title: intl.formatMessage({ id: "rating" }),
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingStars
|
<RatingSystem
|
||||||
value={updateInput.rating ?? undefined}
|
value={updateInput.rating100 ?? undefined}
|
||||||
onSetRating={(value) => setUpdateField({ rating: value })}
|
onSetRating={(value) => setUpdateField({ rating100: value })}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
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 * as GQL from "src/core/generated-graphql";
|
||||||
import { NavUtils, TextUtils } from "src/utils";
|
import { NavUtils, TextUtils } from "src/utils";
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
import { PopoverCountButton } from "../Shared/PopoverCountButton";
|
import { PopoverCountButton } from "../Shared/PopoverCountButton";
|
||||||
import GenderIcon from "./GenderIcon";
|
import GenderIcon from "./GenderIcon";
|
||||||
import { faHeart, faTag } from "@fortawesome/free-solid-svg-icons";
|
import { faHeart, faTag } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { RatingBanner } from "../Shared/RatingBanner";
|
||||||
|
|
||||||
export interface IPerformerCardExtraCriteria {
|
export interface IPerformerCardExtraCriteria {
|
||||||
scenes: Criterion<CriterionValue>[];
|
scenes: Criterion<CriterionValue>[];
|
||||||
@@ -167,18 +168,10 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderRatingBanner() {
|
function maybeRenderRatingBanner() {
|
||||||
if (!performer.rating) {
|
if (!performer.rating100) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return (
|
return <RatingBanner rating={performer.rating100} />;
|
||||||
<div
|
|
||||||
className={`rating-banner ${
|
|
||||||
performer.rating ? `rating-${performer.rating}` : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="rating" />: {performer.rating}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderFlag() {
|
function maybeRenderFlag() {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
import { useLightbox, useToast } from "src/hooks";
|
import { useLightbox, useToast } from "src/hooks";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { TextUtils } from "src/utils";
|
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 { PerformerDetailsPanel } from "./PerformerDetailsPanel";
|
||||||
import { PerformerScenesPanel } from "./PerformerScenesPanel";
|
import { PerformerScenesPanel } from "./PerformerScenesPanel";
|
||||||
import { PerformerGalleriesPanel } from "./PerformerGalleriesPanel";
|
import { PerformerGalleriesPanel } from "./PerformerGalleriesPanel";
|
||||||
@@ -127,11 +127,11 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Mousetrap.bind("0", () => setRating(NaN));
|
Mousetrap.bind("0", () => setRating(NaN));
|
||||||
Mousetrap.bind("1", () => setRating(1));
|
Mousetrap.bind("1", () => setRating(20));
|
||||||
Mousetrap.bind("2", () => setRating(2));
|
Mousetrap.bind("2", () => setRating(40));
|
||||||
Mousetrap.bind("3", () => setRating(3));
|
Mousetrap.bind("3", () => setRating(60));
|
||||||
Mousetrap.bind("4", () => setRating(4));
|
Mousetrap.bind("4", () => setRating(80));
|
||||||
Mousetrap.bind("5", () => setRating(5));
|
Mousetrap.bind("5", () => setRating(100));
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Mousetrap.unbind("0");
|
Mousetrap.unbind("0");
|
||||||
@@ -327,7 +327,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
id: performer.id,
|
id: performer.id,
|
||||||
rating: v,
|
rating100: v,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -428,8 +428,8 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
{performer.name}
|
{performer.name}
|
||||||
{renderClickableIcons()}
|
{renderClickableIcons()}
|
||||||
</h2>
|
</h2>
|
||||||
<RatingStars
|
<RatingSystem
|
||||||
value={performer.rating ?? undefined}
|
value={performer.rating100 ?? undefined}
|
||||||
onSetRating={(value) => setRating(value ?? null)}
|
onSetRating={(value) => setRating(value ?? null)}
|
||||||
/>
|
/>
|
||||||
{maybeRenderAliases()}
|
{maybeRenderAliases()}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export class ParserField {
|
|||||||
|
|
||||||
static Title = new ParserField("title");
|
static Title = new ParserField("title");
|
||||||
static Ext = new ParserField("ext", "File extension");
|
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 I = new ParserField("i", "Matches any ignored word");
|
||||||
static D = new ParserField("d", "Matches any delimiter (.-_)");
|
static D = new ParserField("d", "Matches any delimiter (.-_)");
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
ParserField.fullDateFields.some((f) => {
|
ParserField.fullDateFields.some((f) => {
|
||||||
return pattern.includes(`{${f.field}}`);
|
return pattern.includes(`{${f.field}}`);
|
||||||
});
|
});
|
||||||
const ratingSet = pattern.includes("{rating}");
|
const ratingSet = pattern.includes("{rating100}");
|
||||||
const performerSet = pattern.includes("{performer}");
|
const performerSet = pattern.includes("{performer}");
|
||||||
const tagSet = pattern.includes("{tag}");
|
const tagSet = pattern.includes("{tag}");
|
||||||
const studioSet = pattern.includes("{studio}");
|
const studioSet = pattern.includes("{studio}");
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export class SceneParserResult {
|
|||||||
this.filename = objectTitle(this.scene);
|
this.filename = objectTitle(this.scene);
|
||||||
this.title.setOriginalValue(this.scene.title ?? undefined);
|
this.title.setOriginalValue(this.scene.title ?? undefined);
|
||||||
this.date.setOriginalValue(this.scene.date ?? 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.performers.setOriginalValue(this.scene.performers.map((p) => p.id));
|
||||||
this.tags.setOriginalValue(this.scene.tags.map((t) => t.id));
|
this.tags.setOriginalValue(this.scene.tags.map((t) => t.id));
|
||||||
this.studio.setOriginalValue(this.scene.studio?.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 { useToast } from "src/hooks";
|
||||||
import { FormUtils } from "src/utils";
|
import { FormUtils } from "src/utils";
|
||||||
import MultiSet from "../Shared/MultiSet";
|
import MultiSet from "../Shared/MultiSet";
|
||||||
import { RatingStars } from "./SceneDetails/RatingStars";
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
import {
|
import {
|
||||||
getAggregateInputIDs,
|
getAggregateInputIDs,
|
||||||
getAggregateInputValue,
|
getAggregateInputValue,
|
||||||
@@ -30,7 +30,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||||||
) => {
|
) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [rating, setRating] = useState<number>();
|
const [rating100, setRating] = useState<number>();
|
||||||
const [studioId, setStudioId] = useState<string>();
|
const [studioId, setStudioId] = useState<string>();
|
||||||
const [
|
const [
|
||||||
performerMode,
|
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.studio_id = getAggregateInputValue(studioId, aggregateStudioId);
|
||||||
|
|
||||||
sceneInput.performer_ids = getAggregateInputIDs(
|
sceneInput.performer_ids = getAggregateInputIDs(
|
||||||
@@ -121,7 +121,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||||||
let first = true;
|
let first = true;
|
||||||
|
|
||||||
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
||||||
const sceneRating = scene.rating;
|
const sceneRating = scene.rating100;
|
||||||
const sceneStudioID = scene?.studio?.id;
|
const sceneStudioID = scene?.studio?.id;
|
||||||
const scenePerformerIDs = (scene.performers ?? [])
|
const scenePerformerIDs = (scene.performers ?? [])
|
||||||
.map((p) => p.id)
|
.map((p) => p.id)
|
||||||
@@ -271,14 +271,13 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||||||
title: intl.formatMessage({ id: "rating" }),
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingStars
|
<RatingSystem
|
||||||
value={rating}
|
value={rating100}
|
||||||
onSetRating={(value) => setRating(value)}
|
onSetRating={(value) => setRating(value)}
|
||||||
disabled={isUpdating}
|
disabled={isUpdating}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
<Form.Group controlId="studio" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: intl.formatMessage({ id: "studio" }),
|
title: intl.formatMessage({ id: "studio" }),
|
||||||
|
|||||||
@@ -401,7 +401,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||||||
isPortrait={isPortrait()}
|
isPortrait={isPortrait()}
|
||||||
soundActive={configuration?.interface?.soundOnPreview ?? false}
|
soundActive={configuration?.interface?.soundOnPreview ?? false}
|
||||||
/>
|
/>
|
||||||
<RatingBanner rating={props.scene.rating} />
|
<RatingBanner rating={props.scene.rating100} />
|
||||||
{maybeRenderSceneSpecsOverlay()}
|
{maybeRenderSceneSpecsOverlay()}
|
||||||
{maybeRenderInteractiveSpeedOverlay()}
|
{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 TruncatedText from "src/components/Shared/TruncatedText";
|
||||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||||
import { sortPerformers } from "src/core/performers";
|
import { sortPerformers } from "src/core/performers";
|
||||||
import { RatingStars } from "./RatingStars";
|
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
import { objectTitle } from "src/core/files";
|
import { objectTitle } from "src/core/files";
|
||||||
|
|
||||||
interface ISceneDetailProps {
|
interface ISceneDetailProps {
|
||||||
@@ -99,10 +99,10 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</h5>
|
</h5>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{props.scene.rating ? (
|
{props.scene.rating100 ? (
|
||||||
<h6>
|
<h6>
|
||||||
<FormattedMessage id="rating" />:{" "}
|
<FormattedMessage id="rating" />:{" "}
|
||||||
<RatingStars value={props.scene.rating} />
|
<RatingSystem value={props.scene.rating100} disabled />
|
||||||
</h6>
|
</h6>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import queryString from "query-string";
|
|||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { stashboxDisplayName } from "src/utils/stashbox";
|
import { stashboxDisplayName } from "src/utils/stashbox";
|
||||||
import { SceneMovieTable } from "./SceneMovieTable";
|
import { SceneMovieTable } from "./SceneMovieTable";
|
||||||
import { RatingStars } from "./RatingStars";
|
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
import {
|
import {
|
||||||
faSearch,
|
faSearch,
|
||||||
faSyncAlt,
|
faSyncAlt,
|
||||||
@@ -123,7 +123,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
director: yup.string().optional().nullable(),
|
director: yup.string().optional().nullable(),
|
||||||
url: yup.string().optional().nullable(),
|
url: yup.string().optional().nullable(),
|
||||||
date: 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(),
|
gallery_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||||
studio_id: yup.string().optional().nullable(),
|
studio_id: yup.string().optional().nullable(),
|
||||||
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||||
@@ -147,7 +147,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
director: scene.director ?? "",
|
director: scene.director ?? "",
|
||||||
url: scene.url ?? "",
|
url: scene.url ?? "",
|
||||||
date: scene.date ?? "",
|
date: scene.date ?? "",
|
||||||
rating: scene.rating ?? null,
|
rating100: scene.rating100 ?? null,
|
||||||
gallery_ids: (scene.galleries ?? []).map((g) => g.id),
|
gallery_ids: (scene.galleries ?? []).map((g) => g.id),
|
||||||
studio_id: scene.studio?.id,
|
studio_id: scene.studio?.id,
|
||||||
performer_ids: (scene.performers ?? []).map((p) => p.id),
|
performer_ids: (scene.performers ?? []).map((p) => p.id),
|
||||||
@@ -171,7 +171,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function setRating(v: number) {
|
function setRating(v: number) {
|
||||||
formik.setFieldValue("rating", v);
|
formik.setFieldValue("rating100", v);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGallerySelectValue {
|
interface IGallerySelectValue {
|
||||||
@@ -206,11 +206,11 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
Mousetrap.bind("0", () => setRating(NaN));
|
Mousetrap.bind("0", () => setRating(NaN));
|
||||||
Mousetrap.bind("1", () => setRating(1));
|
Mousetrap.bind("1", () => setRating(20));
|
||||||
Mousetrap.bind("2", () => setRating(2));
|
Mousetrap.bind("2", () => setRating(40));
|
||||||
Mousetrap.bind("3", () => setRating(3));
|
Mousetrap.bind("3", () => setRating(60));
|
||||||
Mousetrap.bind("4", () => setRating(4));
|
Mousetrap.bind("4", () => setRating(80));
|
||||||
Mousetrap.bind("5", () => setRating(5));
|
Mousetrap.bind("5", () => setRating(100));
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Mousetrap.unbind("0");
|
Mousetrap.unbind("0");
|
||||||
@@ -287,7 +287,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
input: {
|
input: {
|
||||||
...updateValues,
|
...updateValues,
|
||||||
id: scene.id!,
|
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" }),
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingStars
|
<RatingSystem
|
||||||
value={formik.values.rating ?? undefined}
|
value={formik.values.rating100 ?? undefined}
|
||||||
onSetRating={(value) =>
|
onSetRating={(value) =>
|
||||||
formik.setFieldValue("rating", value ?? null)
|
formik.setFieldValue("rating100", value ?? null)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
|
|||||||
<h5>{title}</h5>
|
<h5>{title}</h5>
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td>{scene.rating ? scene.rating : ""}</td>
|
<td>{scene.rating100 ? scene.rating100 : ""}</td>
|
||||||
<td>{file?.duration && TextUtils.secondsToTimestamp(file.duration)}</td>
|
<td>{file?.duration && TextUtils.secondsToTimestamp(file.duration)}</td>
|
||||||
<td>{renderTags(scene.tags)}</td>
|
<td>{renderTags(scene.tags)}</td>
|
||||||
<td>{renderPerformers(scene.performers)}</td>
|
<td>{renderPerformers(scene.performers)}</td>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
ScrapedTagsRow,
|
ScrapedTagsRow,
|
||||||
} from "./SceneDetails/SceneScrapeDialog";
|
} from "./SceneDetails/SceneScrapeDialog";
|
||||||
import { galleryTitle } from "src/core/galleries";
|
import { galleryTitle } from "src/core/galleries";
|
||||||
import { RatingStars } from "./SceneDetails/RatingStars";
|
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
|
|
||||||
interface IStashIDsField {
|
interface IStashIDsField {
|
||||||
values: GQL.StashId[];
|
values: GQL.StashId[];
|
||||||
@@ -66,7 +66,9 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
new ScrapeResult<string>(dest.date)
|
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(
|
const [oCounter, setOCounter] = useState(
|
||||||
new ScrapeResult<number>(dest.o_counter)
|
new ScrapeResult<number>(dest.o_counter)
|
||||||
);
|
);
|
||||||
@@ -194,9 +196,9 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
|
|
||||||
setRating(
|
setRating(
|
||||||
new ScrapeResult(
|
new ScrapeResult(
|
||||||
dest.rating,
|
dest.rating100,
|
||||||
sources.find((s) => s.rating)?.rating,
|
sources.find((s) => s.rating100)?.rating100,
|
||||||
!dest.rating
|
!dest.rating100
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -324,10 +326,10 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
title={intl.formatMessage({ id: "rating" })}
|
title={intl.formatMessage({ id: "rating" })}
|
||||||
result={rating}
|
result={rating}
|
||||||
renderOriginalField={() => (
|
renderOriginalField={() => (
|
||||||
<RatingStars value={rating.originalValue} disabled />
|
<RatingSystem value={rating.originalValue} disabled />
|
||||||
)}
|
)}
|
||||||
renderNewField={() => (
|
renderNewField={() => (
|
||||||
<RatingStars value={rating.newValue} disabled />
|
<RatingSystem value={rating.newValue} disabled />
|
||||||
)}
|
)}
|
||||||
onChange={(value) => setRating(value)}
|
onChange={(value) => setRating(value)}
|
||||||
/>
|
/>
|
||||||
@@ -430,7 +432,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
title: title.getNewValue(),
|
title: title.getNewValue(),
|
||||||
url: url.getNewValue(),
|
url: url.getNewValue(),
|
||||||
date: date.getNewValue(),
|
date: date.getNewValue(),
|
||||||
rating: rating.getNewValue(),
|
rating100: rating.getNewValue(),
|
||||||
o_counter: oCounter.getNewValue(),
|
o_counter: oCounter.getNewValue(),
|
||||||
gallery_ids: galleries.getNewValue(),
|
gallery_ids: galleries.getNewValue(),
|
||||||
studio_id: studio.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 {
|
#scene-edit-details {
|
||||||
.rating-stars {
|
.rating-stars {
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
@@ -662,3 +631,7 @@ input[type="range"].blue-slider {
|
|||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scrape-dialog .rating-number.disabled {
|
||||||
|
padding-left: 0.5em;
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ import {
|
|||||||
connectionStateLabel,
|
connectionStateLabel,
|
||||||
InteractiveContext,
|
InteractiveContext,
|
||||||
} from "src/hooks/Interactive/context";
|
} from "src/hooks/Interactive/context";
|
||||||
|
import {
|
||||||
|
defaultRatingStarPrecision,
|
||||||
|
defaultRatingSystemOptions,
|
||||||
|
defaultRatingSystemType,
|
||||||
|
RatingStarPrecision,
|
||||||
|
ratingStarPrecisionIntlMap,
|
||||||
|
ratingSystemIntlMap,
|
||||||
|
RatingSystemType,
|
||||||
|
} from "src/utils/rating";
|
||||||
|
|
||||||
const allMenuItems = [
|
const allMenuItems = [
|
||||||
{ id: "scenes", headingID: "scenes" },
|
{ 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 (error) return <h1>{error.message}</h1>;
|
||||||
if (loading) return <LoadingIndicator />;
|
if (loading) return <LoadingIndicator />;
|
||||||
|
|
||||||
@@ -415,6 +442,42 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<SettingSection headingID="config.ui.custom_css.heading">
|
<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