mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
More performer filter criteria (#179)
* Add new performer filter criteria to UI * Add backend support for new performer criteria
This commit is contained in:
@@ -22,6 +22,30 @@ enum ResolutionEnum {
|
|||||||
input PerformerFilterType {
|
input PerformerFilterType {
|
||||||
"""Filter by favorite"""
|
"""Filter by favorite"""
|
||||||
filter_favorites: Boolean
|
filter_favorites: Boolean
|
||||||
|
"""Filter by birth year"""
|
||||||
|
birth_year: IntCriterionInput
|
||||||
|
"""Filter by age"""
|
||||||
|
age: IntCriterionInput
|
||||||
|
"""Filter by ethnicity"""
|
||||||
|
ethnicity: StringCriterionInput
|
||||||
|
"""Filter by country"""
|
||||||
|
country: StringCriterionInput
|
||||||
|
"""Filter by eye color"""
|
||||||
|
eye_color: StringCriterionInput
|
||||||
|
"""Filter by height"""
|
||||||
|
height: StringCriterionInput
|
||||||
|
"""Filter by measurements"""
|
||||||
|
measurements: StringCriterionInput
|
||||||
|
"""Filter by fake tits value"""
|
||||||
|
fake_tits: StringCriterionInput
|
||||||
|
"""Filter by career length"""
|
||||||
|
career_length: StringCriterionInput
|
||||||
|
"""Filter by tattoos"""
|
||||||
|
tattoos: StringCriterionInput
|
||||||
|
"""Filter by piercings"""
|
||||||
|
piercings: StringCriterionInput
|
||||||
|
"""Filter by aliases"""
|
||||||
|
aliases: StringCriterionInput
|
||||||
}
|
}
|
||||||
|
|
||||||
input SceneMarkerFilterType {
|
input SceneMarkerFilterType {
|
||||||
@@ -71,6 +95,11 @@ enum CriterionModifier {
|
|||||||
EXCLUDES,
|
EXCLUDES,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input StringCriterionInput {
|
||||||
|
value: String!
|
||||||
|
modifier: CriterionModifier!
|
||||||
|
}
|
||||||
|
|
||||||
input IntCriterionInput {
|
input IntCriterionInput {
|
||||||
value: Int!
|
value: Int!
|
||||||
modifier: CriterionModifier!
|
modifier: CriterionModifier!
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/stashapp/stash/pkg/database"
|
"github.com/stashapp/stash/pkg/database"
|
||||||
@@ -111,30 +113,60 @@ func (qb *PerformerQueryBuilder) Query(performerFilter *PerformerFilterType, fin
|
|||||||
findFilter = &FindFilterType{}
|
findFilter = &FindFilterType{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var whereClauses []string
|
query := queryBuilder{
|
||||||
var havingClauses []string
|
tableName: "performers",
|
||||||
var args []interface{}
|
}
|
||||||
body := selectDistinctIDs("performers")
|
|
||||||
body += `
|
query.body = selectDistinctIDs("performers")
|
||||||
|
query.body += `
|
||||||
left join performers_scenes as scenes_join on scenes_join.performer_id = performers.id
|
left join performers_scenes as scenes_join on scenes_join.performer_id = performers.id
|
||||||
left join scenes on scenes_join.scene_id = scenes.id
|
left join scenes on scenes_join.scene_id = scenes.id
|
||||||
`
|
`
|
||||||
|
|
||||||
if q := findFilter.Q; q != nil && *q != "" {
|
if q := findFilter.Q; q != nil && *q != "" {
|
||||||
searchColumns := []string{"performers.name", "performers.checksum", "performers.birthdate", "performers.ethnicity"}
|
searchColumns := []string{"performers.name", "performers.checksum", "performers.birthdate", "performers.ethnicity"}
|
||||||
whereClauses = append(whereClauses, getSearch(searchColumns, *q))
|
clause, thisArgs := getSearchBinding(searchColumns, *q, false)
|
||||||
|
query.addWhere(clause)
|
||||||
|
query.addArg(thisArgs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if favoritesFilter := performerFilter.FilterFavorites; favoritesFilter != nil {
|
if favoritesFilter := performerFilter.FilterFavorites; favoritesFilter != nil {
|
||||||
|
var favStr string
|
||||||
if *favoritesFilter == true {
|
if *favoritesFilter == true {
|
||||||
whereClauses = append(whereClauses, "performers.favorite = 1")
|
favStr = "1"
|
||||||
} else {
|
} else {
|
||||||
whereClauses = append(whereClauses, "performers.favorite = 0")
|
favStr = "0"
|
||||||
}
|
}
|
||||||
|
query.addWhere("performers.favorite = " + favStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
sortAndPagination := qb.getPerformerSort(findFilter) + getPagination(findFilter)
|
if birthYear := performerFilter.BirthYear; birthYear != nil {
|
||||||
idsResult, countResult := executeFindQuery("performers", body, args, sortAndPagination, whereClauses, havingClauses)
|
clauses, thisArgs := getBirthYearFilterClause(birthYear.Modifier, birthYear.Value)
|
||||||
|
query.addWhere(clauses...)
|
||||||
|
query.addArg(thisArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if age := performerFilter.Age; age != nil {
|
||||||
|
clauses, thisArgs := getAgeFilterClause(age.Modifier, age.Value)
|
||||||
|
query.addWhere(clauses...)
|
||||||
|
query.addArg(thisArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStringCriterion("ethnicity", performerFilter.Ethnicity, &query)
|
||||||
|
handleStringCriterion("country", performerFilter.Country, &query)
|
||||||
|
handleStringCriterion("eye_color", performerFilter.EyeColor, &query)
|
||||||
|
handleStringCriterion("height", performerFilter.Height, &query)
|
||||||
|
handleStringCriterion("measurements", performerFilter.Measurements, &query)
|
||||||
|
handleStringCriterion("fake_tits", performerFilter.FakeTits, &query)
|
||||||
|
handleStringCriterion("career_length", performerFilter.CareerLength, &query)
|
||||||
|
handleStringCriterion("tattoos", performerFilter.Tattoos, &query)
|
||||||
|
handleStringCriterion("piercings", performerFilter.Piercings, &query)
|
||||||
|
|
||||||
|
// TODO - need better handling of aliases
|
||||||
|
handleStringCriterion("aliases", performerFilter.Aliases, &query)
|
||||||
|
|
||||||
|
query.sortAndPagination = qb.getPerformerSort(findFilter) + getPagination(findFilter)
|
||||||
|
idsResult, countResult := query.executeFind()
|
||||||
|
|
||||||
var performers []*Performer
|
var performers []*Performer
|
||||||
for _, id := range idsResult {
|
for _, id := range idsResult {
|
||||||
@@ -145,6 +177,98 @@ func (qb *PerformerQueryBuilder) Query(performerFilter *PerformerFilterType, fin
|
|||||||
return performers, countResult
|
return performers, countResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleStringCriterion(column string, value *StringCriterionInput, query *queryBuilder) {
|
||||||
|
if value != nil {
|
||||||
|
if modifier := value.Modifier.String(); value.Modifier.IsValid() {
|
||||||
|
switch modifier {
|
||||||
|
case "EQUALS":
|
||||||
|
clause, thisArgs := getSearchBinding([]string{column}, value.Value, false)
|
||||||
|
query.addWhere(clause)
|
||||||
|
query.addArg(thisArgs...)
|
||||||
|
case "NOT_EQUALS":
|
||||||
|
clause, thisArgs := getSearchBinding([]string{column}, value.Value, true)
|
||||||
|
query.addWhere(clause)
|
||||||
|
query.addArg(thisArgs...)
|
||||||
|
case "IS_NULL":
|
||||||
|
query.addWhere(column + " IS NULL")
|
||||||
|
case "NOT_NULL":
|
||||||
|
query.addWhere(column + " IS NOT NULL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBirthYearFilterClause(criterionModifier CriterionModifier, value int) ([]string, []interface{}) {
|
||||||
|
var clauses []string
|
||||||
|
var args []interface{}
|
||||||
|
|
||||||
|
yearStr := strconv.Itoa(value)
|
||||||
|
startOfYear := yearStr + "-01-01"
|
||||||
|
endOfYear := yearStr + "-12-31"
|
||||||
|
|
||||||
|
if modifier := criterionModifier.String(); criterionModifier.IsValid() {
|
||||||
|
switch modifier {
|
||||||
|
case "EQUALS":
|
||||||
|
// between yyyy-01-01 and yyyy-12-31
|
||||||
|
clauses = append(clauses, "performers.birthdate >= ?")
|
||||||
|
clauses = append(clauses, "performers.birthdate <= ?")
|
||||||
|
args = append(args, startOfYear)
|
||||||
|
args = append(args, endOfYear)
|
||||||
|
case "NOT_EQUALS":
|
||||||
|
// outside of yyyy-01-01 to yyyy-12-31
|
||||||
|
clauses = append(clauses, "performers.birthdate < ? OR performers.birthdate > ?")
|
||||||
|
args = append(args, startOfYear)
|
||||||
|
args = append(args, endOfYear)
|
||||||
|
case "GREATER_THAN":
|
||||||
|
// > yyyy-12-31
|
||||||
|
clauses = append(clauses, "performers.birthdate > ?")
|
||||||
|
args = append(args, endOfYear)
|
||||||
|
case "LESS_THAN":
|
||||||
|
// < yyyy-01-01
|
||||||
|
clauses = append(clauses, "performers.birthdate < ?")
|
||||||
|
args = append(args, startOfYear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clauses, args
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAgeFilterClause(criterionModifier CriterionModifier, value int) ([]string, []interface{}) {
|
||||||
|
var clauses []string
|
||||||
|
var args []interface{}
|
||||||
|
|
||||||
|
// get the date at which performer would turn the age specified
|
||||||
|
dt := time.Now()
|
||||||
|
birthDate := dt.AddDate(-value-1, 0, 0)
|
||||||
|
yearAfter := birthDate.AddDate(1, 0, 0)
|
||||||
|
|
||||||
|
if modifier := criterionModifier.String(); criterionModifier.IsValid() {
|
||||||
|
switch modifier {
|
||||||
|
case "EQUALS":
|
||||||
|
// between birthDate and yearAfter
|
||||||
|
clauses = append(clauses, "performers.birthdate >= ?")
|
||||||
|
clauses = append(clauses, "performers.birthdate < ?")
|
||||||
|
args = append(args, birthDate)
|
||||||
|
args = append(args, yearAfter)
|
||||||
|
case "NOT_EQUALS":
|
||||||
|
// outside of birthDate and yearAfter
|
||||||
|
clauses = append(clauses, "performers.birthdate < ? OR performers.birthdate >= ?")
|
||||||
|
args = append(args, birthDate)
|
||||||
|
args = append(args, yearAfter)
|
||||||
|
case "GREATER_THAN":
|
||||||
|
// < birthDate
|
||||||
|
clauses = append(clauses, "performers.birthdate < ?")
|
||||||
|
args = append(args, birthDate)
|
||||||
|
case "LESS_THAN":
|
||||||
|
// > yearAfter
|
||||||
|
clauses = append(clauses, "performers.birthdate >= ?")
|
||||||
|
args = append(args, yearAfter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clauses, args
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *PerformerQueryBuilder) getPerformerSort(findFilter *FindFilterType) string {
|
func (qb *PerformerQueryBuilder) getPerformerSort(findFilter *FindFilterType) string {
|
||||||
var sort string
|
var sort string
|
||||||
var direction string
|
var direction string
|
||||||
|
|||||||
@@ -13,6 +13,33 @@ import (
|
|||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type queryBuilder struct {
|
||||||
|
tableName string
|
||||||
|
body string
|
||||||
|
|
||||||
|
whereClauses []string
|
||||||
|
havingClauses []string
|
||||||
|
args []interface{}
|
||||||
|
|
||||||
|
sortAndPagination string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb queryBuilder) executeFind() ([]int, int) {
|
||||||
|
return executeFindQuery(qb.tableName, qb.body, qb.args, qb.sortAndPagination, qb.whereClauses, qb.havingClauses)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *queryBuilder) addWhere(clauses ...string) {
|
||||||
|
qb.whereClauses = append(qb.whereClauses, clauses...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *queryBuilder) addHaving(clauses ...string) {
|
||||||
|
qb.havingClauses = append(qb.havingClauses, clauses...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *queryBuilder) addArg(args ...interface{}) {
|
||||||
|
qb.args = append(qb.args, args...)
|
||||||
|
}
|
||||||
|
|
||||||
var randomSortFloat = rand.Float64()
|
var randomSortFloat = rand.Float64()
|
||||||
|
|
||||||
func selectAll(tableName string) string {
|
func selectAll(tableName string) string {
|
||||||
@@ -92,6 +119,7 @@ func getSort(sort string, direction string, tableName string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getSearch(columns []string, q string) string {
|
func getSearch(columns []string, q string) string {
|
||||||
|
// TODO - susceptible to SQL injection
|
||||||
var likeClauses []string
|
var likeClauses []string
|
||||||
queryWords := strings.Split(q, " ")
|
queryWords := strings.Split(q, " ")
|
||||||
trimmedQuery := strings.Trim(q, "\"")
|
trimmedQuery := strings.Trim(q, "\"")
|
||||||
@@ -113,6 +141,39 @@ func getSearch(columns []string, q string) string {
|
|||||||
return "(" + likes + ")"
|
return "(" + likes + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSearchBinding(columns []string, q string, not bool) (string, []interface{}) {
|
||||||
|
var likeClauses []string
|
||||||
|
var args []interface{}
|
||||||
|
|
||||||
|
notStr := ""
|
||||||
|
binaryType := " OR "
|
||||||
|
if not {
|
||||||
|
notStr = " NOT "
|
||||||
|
binaryType = " AND "
|
||||||
|
}
|
||||||
|
|
||||||
|
queryWords := strings.Split(q, " ")
|
||||||
|
trimmedQuery := strings.Trim(q, "\"")
|
||||||
|
if trimmedQuery == q {
|
||||||
|
// Search for any word
|
||||||
|
for _, word := range queryWords {
|
||||||
|
for _, column := range columns {
|
||||||
|
likeClauses = append(likeClauses, column+notStr+" LIKE ?")
|
||||||
|
args = append(args, "%"+word+"%")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Search the exact query
|
||||||
|
for _, column := range columns {
|
||||||
|
likeClauses = append(likeClauses, column+notStr+" LIKE ?")
|
||||||
|
args = append(args, "%"+trimmedQuery+"%")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
likes := strings.Join(likeClauses, binaryType)
|
||||||
|
|
||||||
|
return "(" + likes + ")", args
|
||||||
|
}
|
||||||
|
|
||||||
func getInBinding(length int) string {
|
func getInBinding(length int) string {
|
||||||
bindings := strings.Repeat("?, ", length)
|
bindings := strings.Repeat("?, ", length)
|
||||||
bindings = strings.TrimRight(bindings, ", ")
|
bindings = strings.TrimRight(bindings, ", ")
|
||||||
@@ -128,22 +189,11 @@ func getCriterionModifierBinding(criterionModifier CriterionModifier, value inte
|
|||||||
length = len(x)
|
length = len(x)
|
||||||
default:
|
default:
|
||||||
length = 1
|
length = 1
|
||||||
logger.Debugf("unsupported type: %T\n", x)
|
|
||||||
}
|
}
|
||||||
if modifier := criterionModifier.String(); criterionModifier.IsValid() {
|
if modifier := criterionModifier.String(); criterionModifier.IsValid() {
|
||||||
switch modifier {
|
switch modifier {
|
||||||
case "EQUALS":
|
case "EQUALS", "NOT_EQUALS", "GREATER_THAN", "LESS_THAN", "IS_NULL", "NOT_NULL":
|
||||||
return "= ?", 1
|
return getSimpleCriterionClause(criterionModifier, "?")
|
||||||
case "NOT_EQUALS":
|
|
||||||
return "!= ?", 1
|
|
||||||
case "GREATER_THAN":
|
|
||||||
return "> ?", 1
|
|
||||||
case "LESS_THAN":
|
|
||||||
return "< ?", 1
|
|
||||||
case "IS_NULL":
|
|
||||||
return "IS NULL", 0
|
|
||||||
case "NOT_NULL":
|
|
||||||
return "IS NOT NULL", 0
|
|
||||||
case "INCLUDES":
|
case "INCLUDES":
|
||||||
return "IN " + getInBinding(length), length // TODO?
|
return "IN " + getInBinding(length), length // TODO?
|
||||||
case "EXCLUDES":
|
case "EXCLUDES":
|
||||||
@@ -156,6 +206,30 @@ func getCriterionModifierBinding(criterionModifier CriterionModifier, value inte
|
|||||||
return "= ?", 1 // TODO
|
return "= ?", 1 // TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSimpleCriterionClause(criterionModifier CriterionModifier, rhs string) (string, int) {
|
||||||
|
if modifier := criterionModifier.String(); criterionModifier.IsValid() {
|
||||||
|
switch modifier {
|
||||||
|
case "EQUALS":
|
||||||
|
return "= " + rhs, 1
|
||||||
|
case "NOT_EQUALS":
|
||||||
|
return "!= " + rhs, 1
|
||||||
|
case "GREATER_THAN":
|
||||||
|
return "> " + rhs, 1
|
||||||
|
case "LESS_THAN":
|
||||||
|
return "< " + rhs, 1
|
||||||
|
case "IS_NULL":
|
||||||
|
return "IS NULL", 0
|
||||||
|
case "NOT_NULL":
|
||||||
|
return "IS NOT NULL", 0
|
||||||
|
default:
|
||||||
|
logger.Errorf("todo")
|
||||||
|
return "= ?", 1 // TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "= ?", 1 // TODO
|
||||||
|
}
|
||||||
|
|
||||||
func getIntCriterionWhereClause(column string, input IntCriterionInput) (string, int) {
|
func getIntCriterionWhereClause(column string, input IntCriterionInput) (string, int) {
|
||||||
binding, count := getCriterionModifierBinding(input.Modifier, input.Value)
|
binding, count := getCriterionModifierBinding(input.Modifier, input.Value)
|
||||||
return column + " " + binding, count
|
return column + " " + binding, count
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Dialog,
|
Dialog,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
HTMLSelect,
|
HTMLSelect,
|
||||||
|
InputGroup,
|
||||||
} from "@blueprintjs/core";
|
} from "@blueprintjs/core";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { FunctionComponent, useEffect, useRef, useState } from "react";
|
import React, { FunctionComponent, useEffect, useRef, useState } from "react";
|
||||||
@@ -31,6 +32,8 @@ export const AddFilter: FunctionComponent<IAddFilterProps> = (props: IAddFilterP
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [criterion, setCriterion] = useState<Criterion<any, any>>(new NoneCriterion());
|
const [criterion, setCriterion] = useState<Criterion<any, any>>(new NoneCriterion());
|
||||||
|
|
||||||
|
const valueStage = useRef<any>(criterion.value);
|
||||||
|
|
||||||
// Configure if we are editing an existing criterion
|
// Configure if we are editing an existing criterion
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.editingCriterion) { return; }
|
if (!props.editingCriterion) { return; }
|
||||||
@@ -56,10 +59,26 @@ export const AddFilter: FunctionComponent<IAddFilterProps> = (props: IAddFilterP
|
|||||||
setCriterion(newCriterion);
|
setCriterion(newCriterion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onChangedInput(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
valueStage.current = event.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBlurInput() {
|
||||||
|
const newCriterion = _.cloneDeep(criterion);
|
||||||
|
newCriterion.value = valueStage.current;
|
||||||
|
setCriterion(newCriterion);
|
||||||
|
}
|
||||||
|
|
||||||
function onAddFilter() {
|
function onAddFilter() {
|
||||||
if (!isArray(criterion.value) && !!singleValueSelect.current) {
|
if (!isArray(criterion.value) && !!singleValueSelect.current) {
|
||||||
const value = singleValueSelect.current.props.defaultValue;
|
const value = singleValueSelect.current.props.defaultValue;
|
||||||
if (value === undefined || value === "" || typeof value === "number") { criterion.value = criterion.options[0]; }
|
if (criterion.options && (value === undefined || value === "" || typeof value === "number")) {
|
||||||
|
criterion.value = criterion.options[0];
|
||||||
|
} else if (typeof value === "number" && value === undefined) {
|
||||||
|
criterion.value = 0;
|
||||||
|
} else if (value === undefined) {
|
||||||
|
criterion.value = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const oldId = !!props.editingCriterion ? props.editingCriterion.getId() : undefined;
|
const oldId = !!props.editingCriterion ? props.editingCriterion.getId() : undefined;
|
||||||
props.onAddCriterion(criterion, oldId);
|
props.onAddCriterion(criterion, oldId);
|
||||||
@@ -119,14 +138,25 @@ export const AddFilter: FunctionComponent<IAddFilterProps> = (props: IAddFilterP
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return (
|
if (criterion.options) {
|
||||||
<HTMLSelect
|
return (
|
||||||
ref={singleValueSelect}
|
<HTMLSelect
|
||||||
options={criterion.options}
|
ref={singleValueSelect}
|
||||||
onChange={onChangedSingleSelect}
|
options={criterion.options}
|
||||||
defaultValue={criterion.value}
|
onChange={onChangedSingleSelect}
|
||||||
/>
|
defaultValue={criterion.value}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<InputGroup
|
||||||
|
type={criterion.inputType}
|
||||||
|
onChange={onChangedInput}
|
||||||
|
onBlur={onBlurInput}
|
||||||
|
defaultValue={criterion.value ? criterion.value : ""}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -12,7 +12,19 @@ export type CriterionType =
|
|||||||
"tags" |
|
"tags" |
|
||||||
"sceneTags" |
|
"sceneTags" |
|
||||||
"performers" |
|
"performers" |
|
||||||
"studios";
|
"studios" |
|
||||||
|
"birth_year" |
|
||||||
|
"age" |
|
||||||
|
"ethnicity" |
|
||||||
|
"country" |
|
||||||
|
"eye_color" |
|
||||||
|
"height" |
|
||||||
|
"measurements" |
|
||||||
|
"fake_tits" |
|
||||||
|
"career_length" |
|
||||||
|
"tattoos" |
|
||||||
|
"piercings" |
|
||||||
|
"aliases";
|
||||||
|
|
||||||
export abstract class Criterion<Option = any, Value = any> {
|
export abstract class Criterion<Option = any, Value = any> {
|
||||||
public static getLabel(type: CriterionType = "none"): string {
|
public static getLabel(type: CriterionType = "none"): string {
|
||||||
@@ -27,6 +39,18 @@ export abstract class Criterion<Option = any, Value = any> {
|
|||||||
case "sceneTags": return "Scene Tags";
|
case "sceneTags": return "Scene Tags";
|
||||||
case "performers": return "Performers";
|
case "performers": return "Performers";
|
||||||
case "studios": return "Studios";
|
case "studios": return "Studios";
|
||||||
|
case "birth_year": return "Birth Year";
|
||||||
|
case "age": return "Age";
|
||||||
|
case "ethnicity": return "Ethnicity";
|
||||||
|
case "country": return "Country";
|
||||||
|
case "eye_color": return "Eye Color";
|
||||||
|
case "height": return "Height";
|
||||||
|
case "measurements": return "Measurements";
|
||||||
|
case "fake_tits": return "Fake Tits";
|
||||||
|
case "career_length": return "Career Length";
|
||||||
|
case "tattoos": return "Tattoos";
|
||||||
|
case "piercings": return "Piercings";
|
||||||
|
case "aliases": return "Aliases";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,8 +72,9 @@ export abstract class Criterion<Option = any, Value = any> {
|
|||||||
public abstract parameterName: string;
|
public abstract parameterName: string;
|
||||||
public abstract modifier: CriterionModifier;
|
public abstract modifier: CriterionModifier;
|
||||||
public abstract modifierOptions: ILabeledValue[];
|
public abstract modifierOptions: ILabeledValue[];
|
||||||
public abstract options: Option[];
|
public abstract options: Option[] | undefined;
|
||||||
public abstract value: Value;
|
public abstract value: Value;
|
||||||
|
public inputType: "number" | "text" | undefined;
|
||||||
|
|
||||||
public getLabel(): string {
|
public getLabel(): string {
|
||||||
let modifierString: string;
|
let modifierString: string;
|
||||||
@@ -102,3 +127,71 @@ export interface ICriterionOption {
|
|||||||
label: string;
|
label: string;
|
||||||
value: CriterionType;
|
value: CriterionType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CriterionOption implements ICriterionOption {
|
||||||
|
public label: string;
|
||||||
|
public value: CriterionType;
|
||||||
|
|
||||||
|
constructor(label : string, value : CriterionType) {
|
||||||
|
this.label = label;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StringCriterion extends Criterion<string, string> {
|
||||||
|
public type: CriterionType;
|
||||||
|
public parameterName: string;
|
||||||
|
public modifier = CriterionModifier.Equals;
|
||||||
|
public modifierOptions = [
|
||||||
|
Criterion.getModifierOption(CriterionModifier.Equals),
|
||||||
|
Criterion.getModifierOption(CriterionModifier.NotEquals),
|
||||||
|
Criterion.getModifierOption(CriterionModifier.IsNull),
|
||||||
|
Criterion.getModifierOption(CriterionModifier.NotNull),
|
||||||
|
];
|
||||||
|
public options: string[] | undefined;
|
||||||
|
public value: string = "";
|
||||||
|
|
||||||
|
constructor(type : CriterionType, parameterName?: string, options? : string[]) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.type = type;
|
||||||
|
this.options = options;
|
||||||
|
this.inputType = "text";
|
||||||
|
|
||||||
|
if (!!parameterName) {
|
||||||
|
this.parameterName = parameterName;
|
||||||
|
} else {
|
||||||
|
this.parameterName = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NumberCriterion extends Criterion<number, number> {
|
||||||
|
public type: CriterionType;
|
||||||
|
public parameterName: string;
|
||||||
|
public modifier = CriterionModifier.Equals;
|
||||||
|
public modifierOptions = [
|
||||||
|
Criterion.getModifierOption(CriterionModifier.Equals),
|
||||||
|
Criterion.getModifierOption(CriterionModifier.NotEquals),
|
||||||
|
Criterion.getModifierOption(CriterionModifier.GreaterThan),
|
||||||
|
Criterion.getModifierOption(CriterionModifier.LessThan),
|
||||||
|
Criterion.getModifierOption(CriterionModifier.IsNull),
|
||||||
|
Criterion.getModifierOption(CriterionModifier.NotNull),
|
||||||
|
];
|
||||||
|
public options: number[] | undefined;
|
||||||
|
public value: number = 0;
|
||||||
|
|
||||||
|
constructor(type : CriterionType, parameterName?: string, options? : number[]) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.type = type;
|
||||||
|
this.options = options;
|
||||||
|
this.inputType = "number";
|
||||||
|
|
||||||
|
if (!!parameterName) {
|
||||||
|
this.parameterName = parameterName;
|
||||||
|
} else {
|
||||||
|
this.parameterName = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import { QueryHookResult } from "react-apollo-hooks";
|
|
||||||
import {
|
import {
|
||||||
AllPerformersForFilterQuery,
|
CriterionModifier,
|
||||||
AllPerformersForFilterVariables,
|
|
||||||
AllTagsForFilterQuery,
|
|
||||||
AllTagsForFilterVariables,
|
|
||||||
} from "../../../core/generated-graphql";
|
} from "../../../core/generated-graphql";
|
||||||
import { StashService } from "../../../core/StashService";
|
import { Criterion, CriterionType, StringCriterion, NumberCriterion } from "./criterion";
|
||||||
import { Criterion, CriterionType } from "./criterion";
|
|
||||||
import { FavoriteCriterion } from "./favorite";
|
import { FavoriteCriterion } from "./favorite";
|
||||||
import { HasMarkersCriterion } from "./has-markers";
|
import { HasMarkersCriterion } from "./has-markers";
|
||||||
import { IsMissingCriterion } from "./is-missing";
|
import { IsMissingCriterion } from "./is-missing";
|
||||||
@@ -29,5 +24,28 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
case "sceneTags": return new TagsCriterion("sceneTags");
|
case "sceneTags": return new TagsCriterion("sceneTags");
|
||||||
case "performers": return new PerformersCriterion();
|
case "performers": return new PerformersCriterion();
|
||||||
case "studios": return new StudiosCriterion();
|
case "studios": return new StudiosCriterion();
|
||||||
|
|
||||||
|
case "birth_year":
|
||||||
|
case "age":
|
||||||
|
var ret = new NumberCriterion(type, type);
|
||||||
|
// null/not null doesn't make sense for these criteria
|
||||||
|
ret.modifierOptions = [
|
||||||
|
Criterion.getModifierOption(CriterionModifier.Equals),
|
||||||
|
Criterion.getModifierOption(CriterionModifier.NotEquals),
|
||||||
|
Criterion.getModifierOption(CriterionModifier.GreaterThan),
|
||||||
|
Criterion.getModifierOption(CriterionModifier.LessThan)
|
||||||
|
];
|
||||||
|
return ret;
|
||||||
|
case "ethnicity":
|
||||||
|
case "country":
|
||||||
|
case "eye_color":
|
||||||
|
case "height":
|
||||||
|
case "measurements":
|
||||||
|
case "fake_tits":
|
||||||
|
case "career_length":
|
||||||
|
case "tattoos":
|
||||||
|
case "piercings":
|
||||||
|
case "aliases":
|
||||||
|
return new StringCriterion(type, type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
SceneMarkerFilterType,
|
SceneMarkerFilterType,
|
||||||
SortDirectionEnum,
|
SortDirectionEnum,
|
||||||
} from "../../core/generated-graphql";
|
} from "../../core/generated-graphql";
|
||||||
import { Criterion, ICriterionOption } from "./criteria/criterion";
|
import { Criterion, ICriterionOption, CriterionType, CriterionOption, NumberCriterion, StringCriterion } from "./criteria/criterion";
|
||||||
import { FavoriteCriterion, FavoriteCriterionOption } from "./criteria/favorite";
|
import { FavoriteCriterion, FavoriteCriterionOption } from "./criteria/favorite";
|
||||||
import { HasMarkersCriterion, HasMarkersCriterionOption } from "./criteria/has-markers";
|
import { HasMarkersCriterion, HasMarkersCriterionOption } from "./criteria/has-markers";
|
||||||
import { IsMissingCriterion, IsMissingCriterionOption } from "./criteria/is-missing";
|
import { IsMissingCriterion, IsMissingCriterionOption } from "./criteria/is-missing";
|
||||||
@@ -75,10 +75,29 @@ export class ListFilterModel {
|
|||||||
DisplayMode.Grid,
|
DisplayMode.Grid,
|
||||||
DisplayMode.List,
|
DisplayMode.List,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
var numberCriteria : CriterionType[] = ["birth_year", "age"];
|
||||||
|
var stringCriteria : CriterionType[] = [
|
||||||
|
"ethnicity",
|
||||||
|
"country",
|
||||||
|
"eye_color",
|
||||||
|
"height",
|
||||||
|
"measurements",
|
||||||
|
"fake_tits",
|
||||||
|
"career_length",
|
||||||
|
"tattoos",
|
||||||
|
"piercings",
|
||||||
|
"aliases"
|
||||||
|
];
|
||||||
|
|
||||||
this.criterionOptions = [
|
this.criterionOptions = [
|
||||||
new NoneCriterionOption(),
|
new NoneCriterionOption(),
|
||||||
new FavoriteCriterionOption(),
|
new FavoriteCriterionOption()
|
||||||
];
|
];
|
||||||
|
|
||||||
|
this.criterionOptions = this.criterionOptions.concat(numberCriteria.concat(stringCriteria).map((c) => {
|
||||||
|
return new CriterionOption(Criterion.getLabel(c), c);
|
||||||
|
}));
|
||||||
break;
|
break;
|
||||||
case FilterMode.Studios:
|
case FilterMode.Studios:
|
||||||
if (!!this.sortBy === false) { this.sortBy = "name"; }
|
if (!!this.sortBy === false) { this.sortBy = "name"; }
|
||||||
@@ -245,6 +264,54 @@ export class ListFilterModel {
|
|||||||
case "favorite":
|
case "favorite":
|
||||||
result.filter_favorites = (criterion as FavoriteCriterion).value === "true";
|
result.filter_favorites = (criterion as FavoriteCriterion).value === "true";
|
||||||
break;
|
break;
|
||||||
|
case "birth_year":
|
||||||
|
const byCrit = criterion as NumberCriterion;
|
||||||
|
result.birth_year = { value: byCrit.value, modifier: byCrit.modifier };
|
||||||
|
break;
|
||||||
|
case "age":
|
||||||
|
const ageCrit = criterion as NumberCriterion;
|
||||||
|
result.age = { value: ageCrit.value, modifier: ageCrit.modifier };
|
||||||
|
break;
|
||||||
|
case "ethnicity":
|
||||||
|
const ethCrit = criterion as StringCriterion;
|
||||||
|
result.ethnicity = { value: ethCrit.value, modifier: ethCrit.modifier };
|
||||||
|
break;
|
||||||
|
case "country":
|
||||||
|
const cntryCrit = criterion as StringCriterion;
|
||||||
|
result.country = { value: cntryCrit.value, modifier: cntryCrit.modifier };
|
||||||
|
break;
|
||||||
|
case "eye_color":
|
||||||
|
const ecCrit = criterion as StringCriterion;
|
||||||
|
result.eye_color = { value: ecCrit.value, modifier: ecCrit.modifier };
|
||||||
|
break;
|
||||||
|
case "height":
|
||||||
|
const hCrit = criterion as StringCriterion;
|
||||||
|
result.height = { value: hCrit.value, modifier: hCrit.modifier };
|
||||||
|
break;
|
||||||
|
case "measurements":
|
||||||
|
const mCrit = criterion as StringCriterion;
|
||||||
|
result.measurements = { value: mCrit.value, modifier: mCrit.modifier };
|
||||||
|
break;
|
||||||
|
case "fake_tits":
|
||||||
|
const ftCrit = criterion as StringCriterion;
|
||||||
|
result.fake_tits = { value: ftCrit.value, modifier: ftCrit.modifier };
|
||||||
|
break;
|
||||||
|
case "career_length":
|
||||||
|
const clCrit = criterion as StringCriterion;
|
||||||
|
result.career_length = { value: clCrit.value, modifier: clCrit.modifier };
|
||||||
|
break;
|
||||||
|
case "tattoos":
|
||||||
|
const tCrit = criterion as StringCriterion;
|
||||||
|
result.tattoos = { value: tCrit.value, modifier: tCrit.modifier };
|
||||||
|
break;
|
||||||
|
case "piercings":
|
||||||
|
const pCrit = criterion as StringCriterion;
|
||||||
|
result.piercings = { value: pCrit.value, modifier: pCrit.modifier };
|
||||||
|
break;
|
||||||
|
case "aliases":
|
||||||
|
const aCrit = criterion as StringCriterion;
|
||||||
|
result.aliases = { value: aCrit.value, modifier: aCrit.modifier };
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
Reference in New Issue
Block a user