Saved filter refactor (#4054)

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
yoshnopa 2023-09-01 02:04:56 +02:00 committed by GitHub
parent fca162f1ca
commit 20520a58b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 1062 additions and 657 deletions

View file

@ -118,4 +118,6 @@ models:
model: github.com/stashapp/stash/internal/identify.MetadataOptions model: github.com/stashapp/stash/internal/identify.MetadataOptions
ScraperSourceInput: ScraperSourceInput:
model: github.com/stashapp/stash/pkg/scraper.Source model: github.com/stashapp/stash/pkg/scraper.Source
SavedFindFilterType:
model: github.com/stashapp/stash/pkg/models.FindFilterType

View file

@ -2,5 +2,13 @@ fragment SavedFilterData on SavedFilter {
id id
mode mode
name name
filter find_filter {
q
page
per_page
sort
direction
}
object_filter
ui_options
} }

View file

@ -12,6 +12,17 @@ input FindFilterType {
direction: SortDirectionEnum direction: SortDirectionEnum
} }
type SavedFindFilterType {
q: String
page: Int
"""
use per_page = -1 to indicate all results. Defaults to 25.
"""
per_page: Int
sort: String
direction: SortDirectionEnum
}
enum ResolutionEnum { enum ResolutionEnum {
"144p" "144p"
VERY_LOW VERY_LOW
@ -604,6 +615,13 @@ type SavedFilter {
name: String! name: String!
"JSON-encoded filter string" "JSON-encoded filter string"
filter: String! filter: String!
@deprecated(reason: "use find_filter and object_filter instead")
find_filter: SavedFindFilterType
# maps to any of the AnyFilterInput types
# using a generic Map instead of creating and maintaining match types for inputs
object_filter: Map
# generic map for ui options
ui_options: Map
} }
input SaveFilterInput { input SaveFilterInput {
@ -611,8 +629,10 @@ input SaveFilterInput {
id: ID id: ID
mode: FilterMode! mode: FilterMode!
name: String! name: String!
"JSON-encoded filter string" find_filter: FindFilterType
filter: String! object_filter: Map
# generic map for ui options
ui_options: Map
} }
input DestroyFilterInput { input DestroyFilterInput {
@ -621,6 +641,9 @@ input DestroyFilterInput {
input SetDefaultFilterInput { input SetDefaultFilterInput {
mode: FilterMode! mode: FilterMode!
"JSON-encoded filter string - null to clear" "null to clear"
filter: String find_filter: FindFilterType
object_filter: Map
# generic map for ui options
ui_options: Map
} }

View file

@ -82,6 +82,9 @@ func (r *Resolver) Subscription() SubscriptionResolver {
func (r *Resolver) Tag() TagResolver { func (r *Resolver) Tag() TagResolver {
return &tagResolver{r} return &tagResolver{r}
} }
func (r *Resolver) SavedFilter() SavedFilterResolver {
return &savedFilterResolver{r}
}
type mutationResolver struct{ *Resolver } type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver } type queryResolver struct{ *Resolver }
@ -96,6 +99,7 @@ type imageResolver struct{ *Resolver }
type studioResolver struct{ *Resolver } type studioResolver struct{ *Resolver }
type movieResolver struct{ *Resolver } type movieResolver struct{ *Resolver }
type tagResolver struct{ *Resolver } type tagResolver struct{ *Resolver }
type savedFilterResolver struct{ *Resolver }
func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) error) error { func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) error) error {
return txn.WithTxn(ctx, r.txnManager, fn) return txn.WithTxn(ctx, r.txnManager, fn)

View file

@ -0,0 +1,11 @@
package api
import (
"context"
"github.com/stashapp/stash/pkg/models"
)
func (r *savedFilterResolver) Filter(ctx context.Context, obj *models.SavedFilter) (string, error) {
return "", nil
}

View file

@ -14,12 +14,6 @@ func (r *mutationResolver) SaveFilter(ctx context.Context, input SaveFilterInput
return nil, errors.New("name must be non-empty") return nil, errors.New("name must be non-empty")
} }
newFilter := models.SavedFilter{
Mode: input.Mode,
Name: input.Name,
Filter: input.Filter,
}
var id *int var id *int
if input.ID != nil { if input.ID != nil {
idv, err := strconv.Atoi(*input.ID) idv, err := strconv.Atoi(*input.ID)
@ -32,17 +26,27 @@ func (r *mutationResolver) SaveFilter(ctx context.Context, input SaveFilterInput
if err := r.withTxn(ctx, func(ctx context.Context) error { if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.SavedFilter qb := r.repository.SavedFilter
if id == nil { f := models.SavedFilter{
err = qb.Create(ctx, &newFilter) Mode: input.Mode,
} else { Name: input.Name,
newFilter.ID = *id FindFilter: input.FindFilter,
err = qb.Update(ctx, &newFilter) ObjectFilter: input.ObjectFilter,
UIOptions: input.UIOptions,
} }
if id == nil {
err = qb.Create(ctx, &f)
ret = &f
} else {
f.ID = *id
err = qb.Update(ctx, &f)
ret = &f
}
return err return err
}); err != nil { }); err != nil {
return nil, err return nil, err
} }
ret = &newFilter
return ret, err return ret, err
} }
@ -65,7 +69,7 @@ func (r *mutationResolver) SetDefaultFilter(ctx context.Context, input SetDefaul
if err := r.withTxn(ctx, func(ctx context.Context) error { if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.SavedFilter qb := r.repository.SavedFilter
if input.Filter == nil { if input.FindFilter == nil && input.ObjectFilter == nil && input.UIOptions == nil {
// clearing // clearing
def, err := qb.FindDefault(ctx, input.Mode) def, err := qb.FindDefault(ctx, input.Mode)
if err != nil { if err != nil {
@ -79,12 +83,12 @@ func (r *mutationResolver) SetDefaultFilter(ctx context.Context, input SetDefaul
return nil return nil
} }
err := qb.SetDefault(ctx, &models.SavedFilter{ return qb.SetDefault(ctx, &models.SavedFilter{
Mode: input.Mode, Mode: input.Mode,
Filter: *input.Filter, FindFilter: input.FindFilter,
ObjectFilter: input.ObjectFilter,
UIOptions: input.UIOptions,
}) })
return err
}); err != nil { }); err != nil {
return false, err return false, err
} }

View file

@ -99,7 +99,7 @@ func createPerformer(ctx context.Context, pqb models.PerformerWriter) error {
func createStudio(ctx context.Context, qb models.StudioWriter, name string) (*models.Studio, error) { func createStudio(ctx context.Context, qb models.StudioWriter, name string) (*models.Studio, error) {
// create the studio // create the studio
studio := models.Studio{ studio := models.Studio{
Name: name, Name: name,
} }
err := qb.Create(ctx, &studio) err := qb.Create(ctx, &studio)

View file

@ -60,11 +60,12 @@ func (e FilterMode) MarshalGQL(w io.Writer) {
} }
type SavedFilter struct { type SavedFilter struct {
ID int `json:"id"` ID int `db:"id" json:"id"`
Mode FilterMode `json:"mode"` Mode FilterMode `db:"mode" json:"mode"`
Name string `json:"name"` Name string `db:"name" json:"name"`
// JSON-encoded filter string FindFilter *FindFilterType `json:"find_filter"`
Filter string `json:"filter"` ObjectFilter map[string]interface{} `json:"object_filter"`
UIOptions map[string]interface{} `json:"ui_options"`
} }
type SavedFilters []*SavedFilter type SavedFilters []*SavedFilter

View file

@ -33,7 +33,7 @@ const (
dbConnTimeout = 30 dbConnTimeout = 30
) )
var appSchemaVersion uint = 48 var appSchemaVersion uint = 49
//go:embed migrations/*.sql //go:embed migrations/*.sql
var migrationsBox embed.FS var migrationsBox embed.FS
@ -74,10 +74,10 @@ type Database struct {
Scene *SceneStore Scene *SceneStore
SceneMarker *SceneMarkerStore SceneMarker *SceneMarkerStore
Performer *PerformerStore Performer *PerformerStore
SavedFilter *SavedFilterStore
Studio *StudioStore Studio *StudioStore
Tag *TagStore Tag *TagStore
Movie *MovieStore Movie *MovieStore
SavedFilter *SavedFilterStore
db *sqlx.DB db *sqlx.DB
dbPath string dbPath string

View file

@ -0,0 +1,417 @@
package migrations
import (
"context"
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sqlite"
)
var migrate49TypeResolution = map[string][]string{
"Boolean": {
/*
"organized",
"interactive",
"ignore_auto_tag",
"performer_favorite",
"filter_favorites",
*/
},
"Int": {
"id",
"rating",
"rating100",
"o_counter",
"duration",
"tag_count",
"age",
"height",
"height_cm",
"weight",
"scene_count",
"marker_count",
"image_count",
"gallery_count",
"performer_count",
"interactive_speed",
"resume_time",
"play_count",
"play_duration",
"parent_count",
"child_count",
"performer_age",
"file_count",
},
"Float": {
"penis_length",
},
"Object": {
"tags",
"performers",
"studios",
"movies",
"galleries",
"parents",
"children",
"scene_tags",
"performer_tags",
},
}
var migrate49NameChanges = map[string]string{
"rating": "rating100",
"parent_studios": "parents",
"child_studios": "children",
"parent_tags": "parents",
"child_tags": "children",
"child_tag_count": "child_count",
"parent_tag_count": "parent_count",
"height": "height_cm",
"imageIsMissing": "is_missing",
"sceneIsMissing": "is_missing",
"galleryIsMissing": "is_missing",
"performerIsMissing": "is_missing",
"tagIsMissing": "is_missing",
"studioIsMissing": "is_missing",
"movieIsMissing": "is_missing",
"favorite": "filter_favorites",
"hasMarkers": "has_markers",
"parentTags": "parents",
"childTags": "children",
"phash": "phash_distance",
"scene_code": "code",
"hasChapters": "has_chapters",
"sceneChecksum": "checksum",
"galleryChecksum": "checksum",
"sceneTags": "scene_tags",
"performerTags": "performer_tags",
}
func post49(ctx context.Context, db *sqlx.DB) error {
logger.Info("Running post-migration for schema version 49")
m := schema49Migrator{
migrator: migrator{
db: db,
},
}
return m.migrateSavedFilters(ctx)
}
type schema49Migrator struct {
migrator
}
func (m *schema49Migrator) migrateSavedFilters(ctx context.Context) error {
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
rows, err := m.db.Query("SELECT id, mode, find_filter FROM saved_filters ORDER BY id")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
id int
mode models.FilterMode
findFilter string
)
err := rows.Scan(&id, &mode, &findFilter)
if err != nil {
return err
}
asRawMessage := json.RawMessage(findFilter)
newFindFilter, err := m.getFindFilter(asRawMessage)
if err != nil {
return fmt.Errorf("failed to get find filter for saved filter %d: %w", id, err)
}
objectFilter, err := m.getObjectFilter(mode, asRawMessage)
if err != nil {
return fmt.Errorf("failed to get object filter for saved filter %d: %w", id, err)
}
uiOptions, err := m.getDisplayOptions(asRawMessage)
if err != nil {
return fmt.Errorf("failed to get display options for saved filter %d: %w", id, err)
}
_, err = m.db.Exec("UPDATE saved_filters SET find_filter = ?, object_filter = ?, ui_options = ? WHERE id = ?", newFindFilter, objectFilter, uiOptions, id)
if err != nil {
return fmt.Errorf("failed to update saved filter %d: %w", id, err)
}
}
return rows.Err()
}); err != nil {
return err
}
return nil
}
func (m *schema49Migrator) getDisplayOptions(data json.RawMessage) (json.RawMessage, error) {
type displayOptions struct {
DisplayMode *int `json:"disp"`
ZoomIndex *int `json:"z"`
}
var opts displayOptions
if err := json.Unmarshal(data, &opts); err != nil {
return nil, fmt.Errorf("failed to unmarshal display options: %w", err)
}
ret := make(map[string]interface{})
if opts.DisplayMode != nil {
ret["display_mode"] = *opts.DisplayMode
}
if opts.ZoomIndex != nil {
ret["zoom_index"] = *opts.ZoomIndex
}
return json.Marshal(ret)
}
func (m *schema49Migrator) getFindFilter(data json.RawMessage) (json.RawMessage, error) {
type findFilterJson struct {
Q *string `json:"q"`
Page *int `json:"page"`
PerPage *int `json:"perPage"`
Sort *string `json:"sortby"`
Direction *string `json:"sortdir"`
}
ppDefault := 40
pageDefault := 1
qDefault := ""
sortDefault := "date"
asc := "asc"
ff := findFilterJson{Q: &qDefault, Page: &pageDefault, PerPage: &ppDefault, Sort: &sortDefault, Direction: &asc}
if err := json.Unmarshal(data, &ff); err != nil {
return nil, fmt.Errorf("failed to unmarshal find filter: %w", err)
}
newDir := strings.ToUpper(*ff.Direction)
ff.Direction = &newDir
type findFilterRewrite struct {
Q *string `json:"q"`
Page *int `json:"page"`
PerPage *int `json:"per_page"`
Sort *string `json:"sort"`
Direction *string `json:"direction"`
}
fr := findFilterRewrite(ff)
return json.Marshal(fr)
}
func (m *schema49Migrator) getObjectFilter(mode models.FilterMode, data json.RawMessage) (json.RawMessage, error) {
type criteriaJson struct {
Criteria []string `json:"c"`
}
var c criteriaJson
if err := json.Unmarshal(data, &c); err != nil {
return nil, fmt.Errorf("failed to unmarshal object filter: %w", err)
}
ret := make(map[string]interface{})
for _, raw := range c.Criteria {
if err := m.convertCriterion(mode, ret, raw); err != nil {
return nil, err
}
}
return json.Marshal(ret)
}
func (m *schema49Migrator) convertCriterion(mode models.FilterMode, out map[string]interface{}, criterion string) error {
// convert to a map
ret := make(map[string]interface{})
if err := json.Unmarshal([]byte(criterion), &ret); err != nil {
return fmt.Errorf("failed to unmarshal criterion: %w", err)
}
field := ret["type"].(string)
// Some names are deprecated
if newFieldName, ok := migrate49NameChanges[field]; ok {
field = newFieldName
}
delete(ret, "type")
// Find out whether the object needs some adjustment/has non-string content attached
// Only adjust if value is present
if v, ok := ret["value"]; ok && v != nil {
var err error
switch {
case arrayContains(migrate49TypeResolution["Boolean"], field):
ret["value"], err = m.adjustCriterionValue(ret["value"], "bool")
case arrayContains(migrate49TypeResolution["Int"], field):
ret["value"], err = m.adjustCriterionValue(ret["value"], "int")
case arrayContains(migrate49TypeResolution["Float"], field):
ret["value"], err = m.adjustCriterionValue(ret["value"], "float64")
case arrayContains(migrate49TypeResolution["Object"], field):
ret["value"], err = m.adjustCriterionValue(ret["value"], "object")
}
if err != nil {
return fmt.Errorf("failed to adjust criterion value for %q: %w", field, err)
}
}
out[field] = ret
return nil
}
func arrayContains(sl []string, name string) bool {
for _, value := range sl {
if value == name {
return true
}
}
return false
}
// General Function for converting the types inside a criterion
func (m *schema49Migrator) adjustCriterionValue(value interface{}, typ string) (interface{}, error) {
if mapvalue, ok := value.(map[string]interface{}); ok {
// Primitive values and lists of them
var err error
for _, next := range []string{"value", "value2"} {
if valmap, ok := mapvalue[next].([]string); ok {
var valNewMap []interface{}
for index, v := range valmap {
valNewMap[index], err = m.convertValue(v, typ)
if err != nil {
return nil, err
}
}
mapvalue[next] = valNewMap
} else if _, ok := mapvalue[next]; ok {
mapvalue[next], err = m.convertValue(mapvalue[next], typ)
if err != nil {
return nil, err
}
}
}
// Items
for _, next := range []string{"items", "excluded"} {
if _, ok := mapvalue[next]; ok {
mapvalue[next], err = m.adjustCriterionItem(mapvalue[next])
if err != nil {
return nil, err
}
}
}
// Those Values are always Int
for _, next := range []string{"Distance", "Depth"} {
if _, ok := mapvalue[next]; ok {
mapvalue[next], err = strconv.ParseInt(mapvalue[next].(string), 10, 64)
if err != nil {
return nil, err
}
}
}
return mapvalue, nil
} else if _, ok := value.(string); ok {
// Singular Primitive Values
return m.convertValue(value, typ)
} else if listvalue, ok := value.([]interface{}); ok {
// Items as a singular value, as well as singular lists
var err error
if typ == "object" {
value, err = m.adjustCriterionItem(value)
if err != nil {
return nil, err
}
} else {
for index, val := range listvalue {
listvalue[index], err = m.convertValue(val, typ)
if err != nil {
return nil, err
}
}
value = listvalue
}
return value, nil
} else if _, ok := value.(int); ok {
return value, nil
}
return nil, fmt.Errorf("could not recognize format of value %v", value)
}
// Converts values inside a criterion that represent some objects, like performer or studio.
func (m *schema49Migrator) adjustCriterionItem(value interface{}) (interface{}, error) {
// Basically, this first converts step by step the value, after that it adjusts id and Depth (of parent/child studios) to int
if itemlist, ok := value.([]interface{}); ok {
var itemNewList []interface{}
for _, val := range itemlist {
if val, ok := val.(map[string]interface{}); ok {
newItem := make(map[string]interface{})
for index, v := range val {
if v, ok := v.(string); ok {
switch index {
case "id":
if formattedOut, ok := strconv.ParseInt(v, 10, 64); ok == nil {
newItem["id"] = formattedOut
}
case "Depth":
if formattedOut, ok := strconv.ParseInt(v, 10, 64); ok == nil {
newItem["Depth"] = formattedOut
}
default:
newItem[index] = v
}
}
}
itemNewList = append(itemNewList, newItem)
}
}
return itemNewList, nil
}
return nil, fmt.Errorf("could not recognize %v as an item list", value)
}
// Converts a value of type string to its according type, given by string
func (m *schema49Migrator) convertValue(value interface{}, typ string) (interface{}, error) {
valueType := reflect.TypeOf(value).Name()
if typ == valueType || (typ == "int" && valueType == "float64") || (typ == "float64" && valueType == "int") {
return value, nil
}
if val, ok := value.(string); ok {
switch typ {
case "float64":
return strconv.ParseFloat(val, 64)
case "int":
return strconv.ParseInt(val, 10, 64)
case "bool":
return strconv.ParseBool(val)
default:
return nil, fmt.Errorf("no valid conversion type for %v, need bool, int or float64", typ)
}
}
return nil, fmt.Errorf("cannot convert %v (%T) to %s", value, value, typ)
}
func init() {
sqlite.RegisterPostMigration(49, post49)
}

View file

@ -0,0 +1,34 @@
PRAGMA foreign_keys=OFF;
-- remove filter column
CREATE TABLE `saved_filters_new` (
`id` integer not null primary key autoincrement,
`name` varchar(510) not null,
`mode` varchar(255) not null,
`find_filter` blob,
`object_filter` blob,
`ui_options` blob
);
-- move filter data into find_filter to be migrated in the post-migration
INSERT INTO `saved_filters_new`
(
`id`,
`name`,
`mode`,
`find_filter`
)
SELECT
`id`,
`name`,
`mode`,
`filter`
FROM `saved_filters`;
DROP INDEX `index_saved_filters_on_mode_name_unique`;
DROP TABLE `saved_filters`;
ALTER TABLE `saved_filters_new` rename to `saved_filters`;
CREATE UNIQUE INDEX `index_saved_filters_on_mode_name_unique` on `saved_filters` (`mode`, `name`);
PRAGMA foreign_keys=ON;

View file

@ -291,7 +291,7 @@ func TestMovieUpdateFrontImage(t *testing.T) {
// create movie to test against // create movie to test against
const name = "TestMovieUpdateMovieImages" const name = "TestMovieUpdateMovieImages"
movie := models.Movie{ movie := models.Movie{
Name: name, Name: name,
} }
err := qb.Create(ctx, &movie) err := qb.Create(ctx, &movie)
if err != nil { if err != nil {
@ -311,7 +311,7 @@ func TestMovieUpdateBackImage(t *testing.T) {
// create movie to test against // create movie to test against
const name = "TestMovieUpdateMovieImages" const name = "TestMovieUpdateMovieImages"
movie := models.Movie{ movie := models.Movie{
Name: name, Name: name,
} }
err := qb.Create(ctx, &movie) err := qb.Create(ctx, &movie)
if err != nil { if err != nil {

View file

@ -3,6 +3,7 @@ package sqlite
import ( import (
"context" "context"
"database/sql" "database/sql"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -10,6 +11,7 @@ import (
"github.com/doug-martin/goqu/v9/exp" "github.com/doug-martin/goqu/v9/exp"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/intslice" "github.com/stashapp/stash/pkg/sliceutil/intslice"
) )
@ -20,25 +22,67 @@ const (
) )
type savedFilterRow struct { type savedFilterRow struct {
ID int `db:"id" goqu:"skipinsert"` ID int `db:"id" goqu:"skipinsert"`
Mode string `db:"mode"` Mode models.FilterMode `db:"mode"`
Name string `db:"name"` Name string `db:"name"`
Filter string `db:"filter"` FindFilter string `db:"find_filter"`
ObjectFilter string `db:"object_filter"`
UIOptions string `db:"ui_options"`
}
func encodeJSONOrEmpty(v interface{}) string {
if v == nil {
return ""
}
encoded, err := json.Marshal(v)
if err != nil {
logger.Errorf("error encoding json %v: %v", v, err)
}
return string(encoded)
}
func decodeJSON(s string, v interface{}) {
if s == "" {
return
}
if err := json.Unmarshal([]byte(s), v); err != nil {
logger.Errorf("error decoding json %q: %v", s, err)
}
} }
func (r *savedFilterRow) fromSavedFilter(o models.SavedFilter) { func (r *savedFilterRow) fromSavedFilter(o models.SavedFilter) {
r.ID = o.ID r.ID = o.ID
r.Mode = string(o.Mode) r.Mode = o.Mode
r.Name = o.Name r.Name = o.Name
r.Filter = o.Filter
// encode the filters as json
r.FindFilter = encodeJSONOrEmpty(o.FindFilter)
r.ObjectFilter = encodeJSONOrEmpty(o.ObjectFilter)
r.UIOptions = encodeJSONOrEmpty(o.UIOptions)
} }
func (r *savedFilterRow) resolve() *models.SavedFilter { func (r *savedFilterRow) resolve() *models.SavedFilter {
ret := &models.SavedFilter{ ret := &models.SavedFilter{
ID: r.ID, ID: r.ID,
Name: r.Name, Mode: r.Mode,
Mode: models.FilterMode(r.Mode), Name: r.Name,
Filter: r.Filter, }
// decode the filters from json
if r.FindFilter != "" {
ret.FindFilter = &models.FindFilterType{}
decodeJSON(r.FindFilter, &ret.FindFilter)
}
if r.ObjectFilter != "" {
ret.ObjectFilter = make(map[string]interface{})
decodeJSON(r.ObjectFilter, &ret.ObjectFilter)
}
if r.UIOptions != "" {
ret.UIOptions = make(map[string]interface{})
decodeJSON(r.UIOptions, &ret.UIOptions)
} }
return ret return ret
@ -46,7 +90,6 @@ func (r *savedFilterRow) resolve() *models.SavedFilter {
type SavedFilterStore struct { type SavedFilterStore struct {
repository repository
tableMgr *table tableMgr *table
} }
@ -77,7 +120,7 @@ func (qb *SavedFilterStore) Create(ctx context.Context, newObject *models.SavedF
return err return err
} }
updated, err := qb.find(ctx, id) updated, err := qb.Find(ctx, id)
if err != nil { if err != nil {
return fmt.Errorf("finding after create: %w", err) return fmt.Errorf("finding after create: %w", err)
} }
@ -166,7 +209,6 @@ func (qb *SavedFilterStore) find(ctx context.Context, id int) (*models.SavedFilt
return ret, nil return ret, nil
} }
// returns nil, sql.ErrNoRows if not found
func (qb *SavedFilterStore) get(ctx context.Context, q *goqu.SelectDataset) (*models.SavedFilter, error) { func (qb *SavedFilterStore) get(ctx context.Context, q *goqu.SelectDataset) (*models.SavedFilter, error) {
ret, err := qb.getMany(ctx, q) ret, err := qb.getMany(ctx, q)
if err != nil { if err != nil {

View file

@ -42,15 +42,35 @@ func TestSavedFilterFindByMode(t *testing.T) {
func TestSavedFilterDestroy(t *testing.T) { func TestSavedFilterDestroy(t *testing.T) {
const filterName = "filterToDestroy" const filterName = "filterToDestroy"
const testFilter = "{}" filterQ := ""
filterPage := 1
filterPerPage := 40
filterSort := "date"
filterDirection := models.SortDirectionEnumAsc
findFilter := models.FindFilterType{
Q: &filterQ,
Page: &filterPage,
PerPage: &filterPerPage,
Sort: &filterSort,
Direction: &filterDirection,
}
objectFilter := map[string]interface{}{
"test": "foo",
}
uiOptions := map[string]interface{}{
"display_mode": 1,
"zoom_index": 1,
}
var id int var id int
// create the saved filter to destroy // create the saved filter to destroy
withTxn(func(ctx context.Context) error { withTxn(func(ctx context.Context) error {
newFilter := models.SavedFilter{ newFilter := models.SavedFilter{
Name: filterName, Name: filterName,
Mode: models.FilterModeScenes, Mode: models.FilterModeScenes,
Filter: testFilter, FindFilter: &findFilter,
ObjectFilter: objectFilter,
UIOptions: uiOptions,
} }
err := db.SavedFilter.Create(ctx, &newFilter) err := db.SavedFilter.Create(ctx, &newFilter)
@ -88,12 +108,32 @@ func TestSavedFilterFindDefault(t *testing.T) {
} }
func TestSavedFilterSetDefault(t *testing.T) { func TestSavedFilterSetDefault(t *testing.T) {
const newFilter = "foo" filterQ := ""
filterPage := 1
filterPerPage := 40
filterSort := "date"
filterDirection := models.SortDirectionEnumAsc
findFilter := models.FindFilterType{
Q: &filterQ,
Page: &filterPage,
PerPage: &filterPerPage,
Sort: &filterSort,
Direction: &filterDirection,
}
objectFilter := map[string]interface{}{
"test": "foo",
}
uiOptions := map[string]interface{}{
"display_mode": 1,
"zoom_index": 1,
}
withTxn(func(ctx context.Context) error { withTxn(func(ctx context.Context) error {
err := db.SavedFilter.SetDefault(ctx, &models.SavedFilter{ err := db.SavedFilter.SetDefault(ctx, &models.SavedFilter{
Mode: models.FilterModeMovies, Mode: models.FilterModeMovies,
Filter: newFilter, FindFilter: &findFilter,
ObjectFilter: objectFilter,
UIOptions: uiOptions,
}) })
return err return err
@ -104,7 +144,7 @@ func TestSavedFilterSetDefault(t *testing.T) {
def, err := db.SavedFilter.FindDefault(ctx, models.FilterModeMovies) def, err := db.SavedFilter.FindDefault(ctx, models.FilterModeMovies)
if err == nil { if err == nil {
defID = def.ID defID = def.ID
assert.Equal(t, newFilter, def.Filter) assert.Equal(t, &findFilter, def.FindFilter)
} }
return err return err

View file

@ -1714,10 +1714,29 @@ func getSavedFilterName(index int) string {
func createSavedFilters(ctx context.Context, qb models.SavedFilterReaderWriter, n int) error { func createSavedFilters(ctx context.Context, qb models.SavedFilterReaderWriter, n int) error {
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
filterQ := ""
filterPage := i
filterPerPage := i * 40
filterSort := "date"
filterDirection := models.SortDirectionEnumAsc
findFilter := models.FindFilterType{
Q: &filterQ,
Page: &filterPage,
PerPage: &filterPerPage,
Sort: &filterSort,
Direction: &filterDirection,
}
savedFilter := models.SavedFilter{ savedFilter := models.SavedFilter{
Mode: getSavedFilterMode(i), Mode: getSavedFilterMode(i),
Name: getSavedFilterName(i), Name: getSavedFilterName(i),
Filter: getPrefixedStringValue("savedFilter", i, "Filter"), FindFilter: &findFilter,
ObjectFilter: map[string]interface{}{
"test": "object",
},
UIOptions: map[string]interface{}{
"display_mode": 1,
"zoom_index": 1,
},
} }
err := qb.Create(ctx, &savedFilter) err := qb.Create(ctx, &savedFilter)

View file

@ -74,7 +74,7 @@
"prefer-destructuring": ["error", { "object": true, "array": false }], "prefer-destructuring": ["error", { "object": true, "array": false }],
"@typescript-eslint/no-use-before-define": [ "@typescript-eslint/no-use-before-define": [
"error", "error",
{ "functions": false, "classes": true } { "functions": false, "classes": false }
], ],
"no-nested-ternary": "off" "no-nested-ternary": "off"
} }

View file

@ -105,11 +105,11 @@ const SavedFilterResults: React.FC<ISavedFilterResults> = ({
const filter = useMemo(() => { const filter = useMemo(() => {
if (!data?.findSavedFilter) return; if (!data?.findSavedFilter) return;
const { mode, filter: filterJSON } = data.findSavedFilter; const { mode } = data.findSavedFilter;
const ret = new ListFilterModel(mode, config); const ret = new ListFilterModel(mode, config);
ret.currentPage = 1; ret.currentPage = 1;
ret.configureFromJSON(filterJSON); ret.configureFromSavedFilter(data.findSavedFilter);
ret.randomSeed = -1; ret.randomSeed = -1;
return ret; return ret;
}, [data?.findSavedFilter, config]); }, [data?.findSavedFilter, config]);

View file

@ -270,11 +270,11 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
if (existing) { if (existing) {
setCriterion(existing); setCriterion(existing);
} else { } else {
const newCriterion = makeCriteria(configuration, option.type); const newCriterion = makeCriteria(filter.mode, option.type);
setCriterion(newCriterion); setCriterion(newCriterion);
} }
}, },
[criteria, configuration] [filter.mode, criteria]
); );
const ui = (configuration?.ui ?? {}) as IUIConfig; const ui = (configuration?.ui ?? {}) as IUIConfig;

View file

@ -13,20 +13,19 @@ interface IHierarchicalLabelValueFilterProps {
export const HierarchicalLabelValueFilter: React.FC< export const HierarchicalLabelValueFilter: React.FC<
IHierarchicalLabelValueFilterProps IHierarchicalLabelValueFilterProps
> = ({ criterion, onValueChanged }) => { > = ({ criterion, onValueChanged }) => {
const { criterionOption } = criterion;
const { type, inputType } = criterionOption;
const intl = useIntl(); const intl = useIntl();
if ( if (
criterion.criterionOption.type !== "performers" && inputType !== "studios" &&
criterion.criterionOption.type !== "studios" && inputType !== "tags" &&
criterion.criterionOption.type !== "parent_studios" && inputType !== "scene_tags" &&
criterion.criterionOption.type !== "tags" && inputType !== "performer_tags"
criterion.criterionOption.type !== "sceneTags" && ) {
criterion.criterionOption.type !== "performerTags" &&
criterion.criterionOption.type !== "parentTags" &&
criterion.criterionOption.type !== "childTags" &&
criterion.criterionOption.type !== "movies"
)
return null; return null;
}
const messages = defineMessages({ const messages = defineMessages({
studio_depth: { studio_depth: {
@ -51,10 +50,10 @@ export const HierarchicalLabelValueFilter: React.FC<
} }
function criterionOptionTypeToIncludeID(): string { function criterionOptionTypeToIncludeID(): string {
if (criterion.criterionOption.type === "studios") { if (inputType === "studios") {
return "include-sub-studios"; return "include-sub-studios";
} }
if (criterion.criterionOption.type === "childTags") { if (type === "children") {
return "include-parent-tags"; return "include-parent-tags";
} }
return "include-sub-tags"; return "include-sub-tags";
@ -62,9 +61,9 @@ export const HierarchicalLabelValueFilter: React.FC<
function criterionOptionTypeToIncludeUIString(): MessageDescriptor { function criterionOptionTypeToIncludeUIString(): MessageDescriptor {
const optionType = const optionType =
criterion.criterionOption.type === "studios" inputType === "studios"
? "include_sub_studios" ? "include_sub_studios"
: criterion.criterionOption.type === "childTags" : type === "children"
? "include_parent_tags" ? "include_parent_tags"
: "include_sub_tags"; : "include_sub_tags";
return { return {
@ -76,7 +75,7 @@ export const HierarchicalLabelValueFilter: React.FC<
<> <>
<Form.Group> <Form.Group>
<FilterSelect <FilterSelect
type={criterion.criterionOption.type} type={inputType}
isMulti isMulti
onSelect={onSelectionChanged} onSelect={onSelectionChanged}
ids={criterion.value.items.map((labeled) => labeled.id)} ids={criterion.value.items.map((labeled) => labeled.id)}

View file

@ -13,18 +13,19 @@ export const LabeledIdFilter: React.FC<ILabeledIdFilterProps> = ({
criterion, criterion,
onValueChanged, onValueChanged,
}) => { }) => {
const { criterionOption } = criterion;
const { inputType } = criterionOption;
if ( if (
criterion.criterionOption.type !== "performers" && inputType !== "performers" &&
criterion.criterionOption.type !== "studios" && inputType !== "studios" &&
criterion.criterionOption.type !== "parent_studios" && inputType !== "scene_tags" &&
criterion.criterionOption.type !== "tags" && inputType !== "performer_tags" &&
criterion.criterionOption.type !== "sceneTags" && inputType !== "tags" &&
criterion.criterionOption.type !== "performerTags" && inputType !== "movies"
criterion.criterionOption.type !== "parentTags" && ) {
criterion.criterionOption.type !== "childTags" &&
criterion.criterionOption.type !== "movies"
)
return null; return null;
}
function onSelectionChanged(items: SelectObject[]) { function onSelectionChanged(items: SelectObject[]) {
onValueChanged( onValueChanged(
@ -38,7 +39,7 @@ export const LabeledIdFilter: React.FC<ILabeledIdFilterProps> = ({
return ( return (
<Form.Group> <Form.Group>
<FilterSelect <FilterSelect
type={criterion.criterionOption.type} type={inputType}
isMulti isMulti
onSelect={onSelectionChanged} onSelect={onSelectionChanged}
ids={criterion.value.map((labeled) => labeled.id)} ids={criterion.value.map((labeled) => labeled.id)}

View file

@ -320,7 +320,7 @@ export const HierarchicalObjectsFilter = <
if (criterion.criterionOption.type === "studios") { if (criterion.criterionOption.type === "studios") {
return "include-sub-studios"; return "include-sub-studios";
} }
if (criterion.criterionOption.type === "childTags") { if (criterion.criterionOption.type === "children") {
return "include-parent-tags"; return "include-parent-tags";
} }
return "include-sub-tags"; return "include-sub-tags";
@ -330,7 +330,7 @@ export const HierarchicalObjectsFilter = <
const optionType = const optionType =
criterion.criterionOption.type === "studios" criterion.criterionOption.type === "studios"
? "include_sub_studios" ? "include_sub_studios"
: criterion.criterionOption.type === "childTags" : criterion.criterionOption.type === "children"
? "include_parent_tags" ? "include_parent_tags"
: "include_sub_tags"; : "include_sub_tags";
return { return {

View file

@ -619,8 +619,8 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
if (defaultFilter?.findDefaultFilter) { if (defaultFilter?.findDefaultFilter) {
newFilter.currentPage = 1; newFilter.currentPage = 1;
try { try {
newFilter.configureFromJSON( newFilter.configureFromSavedFilter(
defaultFilter.findDefaultFilter.filter defaultFilter.findDefaultFilter
); );
} catch (err) { } catch (err) {
console.log(err); console.log(err);

View file

@ -75,7 +75,9 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
id, id,
mode: filter.mode, mode: filter.mode,
name, name,
filter: filterCopy.makeSavedFilterJSON(), find_filter: filterCopy.makeFindFilter(),
object_filter: filterCopy.makeSavedFindFilter(),
ui_options: filterCopy.makeUIOptions(),
}, },
}, },
}); });
@ -143,7 +145,9 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
variables: { variables: {
input: { input: {
mode: filter.mode, mode: filter.mode,
filter: filterCopy.makeSavedFilterJSON(), find_filter: filterCopy.makeFindFilter(),
object_filter: filterCopy.makeSavedFindFilter(),
ui_options: filterCopy.makeUIOptions(),
}, },
}, },
}); });
@ -166,7 +170,7 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
newFilter.currentPage = 1; newFilter.currentPage = 1;
// #1795 - reset search term if not present in saved filter // #1795 - reset search term if not present in saved filter
newFilter.searchTerm = ""; newFilter.searchTerm = "";
newFilter.configureFromJSON(f.filter); newFilter.configureFromSavedFilter(f);
// #1507 - reset random seed when loaded // #1507 - reset random seed when loaded
newFilter.randomSeed = -1; newFilter.randomSeed = -1;

View file

@ -44,12 +44,9 @@ interface ITypeProps {
type?: type?:
| "performers" | "performers"
| "studios" | "studios"
| "parent_studios"
| "tags" | "tags"
| "sceneTags" | "scene_tags"
| "performerTags" | "performer_tags"
| "parentTags"
| "childTags"
| "movies"; | "movies";
} }
interface IFilterProps { interface IFilterProps {
@ -865,7 +862,7 @@ export const TagSelect: React.FC<
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) => { export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) => {
if (props.type === "performers") { if (props.type === "performers") {
return <PerformerSelect {...props} creatable={false} />; return <PerformerSelect {...props} creatable={false} />;
} else if (props.type === "studios" || props.type === "parent_studios") { } else if (props.type === "studios") {
return <StudioSelect {...props} creatable={false} />; return <StudioSelect {...props} creatable={false} />;
} else if (props.type === "movies") { } else if (props.type === "movies") {
return <MovieSelect {...props} creatable={false} />; return <MovieSelect {...props} creatable={false} />;

View file

@ -17,7 +17,7 @@ export const StudioChildrenPanel: React.FC<IStudioChildrenPanel> = ({
const studioValue = { id: studio.id!, label: studio.name! }; const studioValue = { id: studio.id!, label: studio.name! };
// if studio is already present, then we modify it, otherwise add // if studio is already present, then we modify it, otherwise add
let parentStudioCriterion = filter.criteria.find((c) => { let parentStudioCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "parent_studios"; return c.criterionOption.type === "parents";
}) as ParentStudiosCriterion; }) as ParentStudiosCriterion;
if ( if (

View file

@ -877,7 +877,7 @@
"path": "Sti", "path": "Sti",
"perceptual_similarity": "Perceptuel lighed (phash)", "perceptual_similarity": "Perceptuel lighed (phash)",
"performer": "Kunstner", "performer": "Kunstner",
"performerTags": "Kunstner Tags", "performer_tags": "Kunstner Tags",
"performer_age": "kunstnere Alder", "performer_age": "kunstnere Alder",
"performer_count": "Kunstner Antal", "performer_count": "Kunstner Antal",
"performer_favorite": "Foretrukken optrædende", "performer_favorite": "Foretrukken optrædende",
@ -927,7 +927,7 @@
"resolution": "Opløsning", "resolution": "Opløsning",
"scene": "Scene", "scene": "Scene",
"sceneTagger": "Scenetagger", "sceneTagger": "Scenetagger",
"sceneTags": "Scene-etiketter", "scene_tags": "Scene-etiketter",
"scene_count": "Scene antal", "scene_count": "Scene antal",
"scene_id": "Scene-id", "scene_id": "Scene-id",
"scenes": "Scener", "scenes": "Scener",

View file

@ -1033,7 +1033,7 @@
"penis_length_cm": "Penislänge (cm)", "penis_length_cm": "Penislänge (cm)",
"perceptual_similarity": "Wahrnehmungsähnlichkeit (phash)", "perceptual_similarity": "Wahrnehmungsähnlichkeit (phash)",
"performer": "Darsteller", "performer": "Darsteller",
"performerTags": "Darsteller-Tags", "performer_tags": "Darsteller-Tags",
"performer_age": "Alter der Darsteller", "performer_age": "Alter der Darsteller",
"performer_count": "Darstelleranzahl", "performer_count": "Darstelleranzahl",
"performer_favorite": "Darsteller favorisiert", "performer_favorite": "Darsteller favorisiert",
@ -1088,7 +1088,7 @@
"resume_time": "Zeit fortsetzen", "resume_time": "Zeit fortsetzen",
"scene": "Szene", "scene": "Szene",
"sceneTagger": "Szenen-Tagger", "sceneTagger": "Szenen-Tagger",
"sceneTags": "Szenen-Tags", "scene_tags": "Szenen-Tags",
"scene_code": "Studio Code", "scene_code": "Studio Code",
"scene_count": "Szenenanzahl", "scene_count": "Szenenanzahl",
"scene_created_at": "Szene angelegt am", "scene_created_at": "Szene angelegt am",

View file

@ -1075,7 +1075,7 @@
"penis_length_cm": "Penis Length (cm)", "penis_length_cm": "Penis Length (cm)",
"perceptual_similarity": "Perceptual Similarity (phash)", "perceptual_similarity": "Perceptual Similarity (phash)",
"performer": "Performer", "performer": "Performer",
"performerTags": "Performer Tags", "performer_tags": "Performer Tags",
"performer_age": "Performer Age", "performer_age": "Performer Age",
"performer_count": "Performer Count", "performer_count": "Performer Count",
"performer_favorite": "Performer Favourited", "performer_favorite": "Performer Favourited",
@ -1130,7 +1130,7 @@
"resume_time": "Resume Time", "resume_time": "Resume Time",
"scene": "Scene", "scene": "Scene",
"sceneTagger": "Scene Tagger", "sceneTagger": "Scene Tagger",
"sceneTags": "Scene Tags", "scene_tags": "Scene Tags",
"scene_code": "Studio Code", "scene_code": "Studio Code",
"scene_count": "Scene Count", "scene_count": "Scene Count",
"scene_created_at": "Scene Created At", "scene_created_at": "Scene Created At",
@ -1355,4 +1355,4 @@
"weight_kg": "Weight (kg)", "weight_kg": "Weight (kg)",
"years_old": "years old", "years_old": "years old",
"zip_file_count": "Zip File Count" "zip_file_count": "Zip File Count"
} }

View file

@ -929,7 +929,7 @@
"path": "Ruta", "path": "Ruta",
"perceptual_similarity": "Similaridad perceptiva (phash)", "perceptual_similarity": "Similaridad perceptiva (phash)",
"performer": "Actriz/Actor", "performer": "Actriz/Actor",
"performerTags": "Etiquetas de actriz/actor", "performer_tags": "Etiquetas de actriz/actor",
"performer_age": "Edad de la actriz/actor", "performer_age": "Edad de la actriz/actor",
"performer_count": "Número de actrices/actores", "performer_count": "Número de actrices/actores",
"performer_favorite": "Actriz/actor favorita/o", "performer_favorite": "Actriz/actor favorita/o",
@ -977,7 +977,7 @@
"resolution": "Resolución", "resolution": "Resolución",
"scene": "Escena", "scene": "Escena",
"sceneTagger": "Etiquetador de escenas", "sceneTagger": "Etiquetador de escenas",
"sceneTags": "Etiquetas de escena", "scene_tags": "Etiquetas de escena",
"scene_count": "Número de escenas", "scene_count": "Número de escenas",
"scene_id": "Indentificador de escena", "scene_id": "Indentificador de escena",
"scenes": "Escenas", "scenes": "Escenas",

View file

@ -1012,7 +1012,7 @@
"path": "Failitee", "path": "Failitee",
"perceptual_similarity": "Tajutav Sarnasus (phash)", "perceptual_similarity": "Tajutav Sarnasus (phash)",
"performer": "Näitleja", "performer": "Näitleja",
"performerTags": "Näitleja Sildid", "performer_tags": "Näitleja Sildid",
"performer_age": "Näitleja Vanus", "performer_age": "Näitleja Vanus",
"performer_count": "Näitlejate Arv", "performer_count": "Näitlejate Arv",
"performer_favorite": "Lemmiknäitleja", "performer_favorite": "Lemmiknäitleja",
@ -1067,7 +1067,7 @@
"resume_time": "Jätkamisaeg", "resume_time": "Jätkamisaeg",
"scene": "Stseen", "scene": "Stseen",
"sceneTagger": "Stseeni Sildistaja", "sceneTagger": "Stseeni Sildistaja",
"sceneTags": "Stseeni Sildid", "scene_tags": "Stseeni Sildid",
"scene_code": "Stuudio Kood", "scene_code": "Stuudio Kood",
"scene_count": "Stseenide Arv", "scene_count": "Stseenide Arv",
"scene_created_at": "Stseen Loodud", "scene_created_at": "Stseen Loodud",

View file

@ -870,7 +870,7 @@
"path": "Polku", "path": "Polku",
"perceptual_similarity": "Aistinvarainen samankaltaisuus (phash)", "perceptual_similarity": "Aistinvarainen samankaltaisuus (phash)",
"performer": "Esiintyjä", "performer": "Esiintyjä",
"performerTags": "Esiintyjien tunnisteet", "performer_tags": "Esiintyjien tunnisteet",
"performer_age": "Esiintyjän ikä", "performer_age": "Esiintyjän ikä",
"performer_count": "Esiintyjien määrä", "performer_count": "Esiintyjien määrä",
"performer_favorite": "Esiintyjä suosikeissa", "performer_favorite": "Esiintyjä suosikeissa",
@ -914,7 +914,7 @@
"resolution": "Resoluutio", "resolution": "Resoluutio",
"scene": "Kohtaus", "scene": "Kohtaus",
"sceneTagger": "Kohtauksien tunnistetila", "sceneTagger": "Kohtauksien tunnistetila",
"sceneTags": "Kohtauksen tunnisteet", "scene_tags": "Kohtauksen tunnisteet",
"scene_code": "Studiokoodi", "scene_code": "Studiokoodi",
"scene_count": "Kohtauksien määrä", "scene_count": "Kohtauksien määrä",
"scene_created_at": "Kohtaus luotu", "scene_created_at": "Kohtaus luotu",

View file

@ -1072,7 +1072,7 @@
"penis_length_cm": "Longueur du pénis (cm)", "penis_length_cm": "Longueur du pénis (cm)",
"perceptual_similarity": "Similitude perceptuelle (empreinte)", "perceptual_similarity": "Similitude perceptuelle (empreinte)",
"performer": "Performeurs", "performer": "Performeurs",
"performerTags": "Étiquettes de performeur", "performer_tags": "Étiquettes de performeur",
"performer_age": "Âge du performeur", "performer_age": "Âge du performeur",
"performer_count": "Nombre de performeurs", "performer_count": "Nombre de performeurs",
"performer_favorite": "Performeur favori", "performer_favorite": "Performeur favori",
@ -1127,7 +1127,7 @@
"resume_time": "Reprendre le temps", "resume_time": "Reprendre le temps",
"scene": "Scène", "scene": "Scène",
"sceneTagger": "Étiqueteuse de scènes", "sceneTagger": "Étiqueteuse de scènes",
"sceneTags": "Étiquettes de la scène", "scene_tags": "Étiquettes de la scène",
"scene_code": "Code studio", "scene_code": "Code studio",
"scene_count": "Nombre de scènes", "scene_count": "Nombre de scènes",
"scene_created_at": "Scène créée le", "scene_created_at": "Scène créée le",

View file

@ -437,7 +437,7 @@
"parent_tags": "Szülő-címkék", "parent_tags": "Szülő-címkék",
"path": "Elérési Út", "path": "Elérési Út",
"performer": "Szereplő", "performer": "Szereplő",
"performerTags": "Szereplő Címkék", "performer_tags": "Szereplő Címkék",
"performer_age": "Szereplő Kora", "performer_age": "Szereplő Kora",
"performer_count": "Szereplők Száma", "performer_count": "Szereplők Száma",
"performer_favorite": "Szereplő Kedvencek Közt", "performer_favorite": "Szereplő Kedvencek Közt",
@ -460,7 +460,7 @@
"resolution": "Felbontás", "resolution": "Felbontás",
"scene": "Jelenet", "scene": "Jelenet",
"sceneTagger": "Jelenetcímkéző", "sceneTagger": "Jelenetcímkéző",
"sceneTags": "Jelenetcímkék", "scene_tags": "Jelenetcímkék",
"scene_count": "Jelenetszám", "scene_count": "Jelenetszám",
"scene_id": "Jelenet ID", "scene_id": "Jelenet ID",
"scenes": "Jelenetek", "scenes": "Jelenetek",

View file

@ -986,7 +986,7 @@
"resume_time": "Tempo Continuazione", "resume_time": "Tempo Continuazione",
"scene": "Scena", "scene": "Scena",
"sceneTagger": "Tagger Scena", "sceneTagger": "Tagger Scena",
"sceneTags": "Tag Scena", "scene_tags": "Tag Scena",
"scene_code": "Codice dello Studio", "scene_code": "Codice dello Studio",
"scene_count": "Numero Scene", "scene_count": "Numero Scene",
"scene_created_at": "Scena Creata Al", "scene_created_at": "Scena Creata Al",

View file

@ -940,7 +940,7 @@
"path": "パス", "path": "パス",
"perceptual_similarity": "知覚的類似性 (phash)", "perceptual_similarity": "知覚的類似性 (phash)",
"performer": "出演者", "performer": "出演者",
"performerTags": "出演者タグ", "performer_tags": "出演者タグ",
"performer_age": "出演者の年齢", "performer_age": "出演者の年齢",
"performer_count": "出演者数", "performer_count": "出演者数",
"performer_favorite": "出演者をお気に入り済み", "performer_favorite": "出演者をお気に入り済み",
@ -995,7 +995,7 @@
"resume_time": "レジューム時間", "resume_time": "レジューム時間",
"scene": "シーン", "scene": "シーン",
"sceneTagger": "シーン一括タグ付け", "sceneTagger": "シーン一括タグ付け",
"sceneTags": "シーンタグ", "scene_tags": "シーンタグ",
"scene_code": "スタジオコード", "scene_code": "スタジオコード",
"scene_count": "シーン数", "scene_count": "シーン数",
"scene_created_at": "シーンの作成日時", "scene_created_at": "シーンの作成日時",

View file

@ -1022,7 +1022,7 @@
"penis_length_cm": "자지 크기 (cm)", "penis_length_cm": "자지 크기 (cm)",
"perceptual_similarity": "유사도 (phash)", "perceptual_similarity": "유사도 (phash)",
"performer": "배우", "performer": "배우",
"performerTags": "배우 태그", "performer_tags": "배우 태그",
"performer_age": "배우 나이", "performer_age": "배우 나이",
"performer_count": "배우 수", "performer_count": "배우 수",
"performer_favorite": "즐겨찾기한 배우", "performer_favorite": "즐겨찾기한 배우",
@ -1077,7 +1077,7 @@
"resume_time": "재시작 시간", "resume_time": "재시작 시간",
"scene": "영상", "scene": "영상",
"sceneTagger": "영상 태거", "sceneTagger": "영상 태거",
"sceneTags": "영상 태그", "scene_tags": "영상 태그",
"scene_code": "스튜디오 코드", "scene_code": "스튜디오 코드",
"scene_count": "영상 개수", "scene_count": "영상 개수",
"scene_created_at": "영상 생성 날짜", "scene_created_at": "영상 생성 날짜",

View file

@ -821,7 +821,7 @@
"path": "Pad", "path": "Pad",
"perceptual_similarity": "Perceptuele gelijkenis (phash)", "perceptual_similarity": "Perceptuele gelijkenis (phash)",
"performer": "Performer", "performer": "Performer",
"performerTags": "Peformer Labels", "performer_tags": "Peformer Labels",
"performer_age": "Leeftijd artiest", "performer_age": "Leeftijd artiest",
"performer_count": "Performer Aantal", "performer_count": "Performer Aantal",
"performer_favorite": "Artiest favoriet", "performer_favorite": "Artiest favoriet",
@ -855,7 +855,7 @@
"resolution": "Resolutie", "resolution": "Resolutie",
"scene": "Scène", "scene": "Scène",
"sceneTagger": "Scene Labelen", "sceneTagger": "Scene Labelen",
"sceneTags": "Scene Labels", "scene_tags": "Scene Labels",
"scene_count": "Scene Aantal", "scene_count": "Scene Aantal",
"scene_id": "Scene ID", "scene_id": "Scene ID",
"scenes": "Scènes", "scenes": "Scènes",

View file

@ -1030,7 +1030,7 @@
"penis_length_cm": "Długość penisa (cm)", "penis_length_cm": "Długość penisa (cm)",
"perceptual_similarity": "Podobieństwo percepcyjne (phash)", "perceptual_similarity": "Podobieństwo percepcyjne (phash)",
"performer": "Aktor", "performer": "Aktor",
"performerTags": "Tagi aktorów", "performer_tags": "Tagi aktorów",
"performer_age": "Wiek aktora", "performer_age": "Wiek aktora",
"performer_count": "Liczba aktorów", "performer_count": "Liczba aktorów",
"performer_favorite": "Ulubiony aktor", "performer_favorite": "Ulubiony aktor",
@ -1085,7 +1085,7 @@
"resume_time": "Rozpocznij od", "resume_time": "Rozpocznij od",
"scene": "Scena", "scene": "Scena",
"sceneTagger": "Otagowywacz scen", "sceneTagger": "Otagowywacz scen",
"sceneTags": "Tagi sceny", "scene_tags": "Tagi sceny",
"scene_code": "Kod studia", "scene_code": "Kod studia",
"scene_count": "Liczba scen", "scene_count": "Liczba scen",
"scene_created_at": "Scena utworzona", "scene_created_at": "Scena utworzona",

View file

@ -847,7 +847,7 @@
"path": "Caminho", "path": "Caminho",
"perceptual_similarity": "Semelhança Perceptiva (phash)", "perceptual_similarity": "Semelhança Perceptiva (phash)",
"performer": "Artista", "performer": "Artista",
"performerTags": "Etiquetas de artistas", "performer_tags": "Etiquetas de artistas",
"performer_age": "Idade do Artista", "performer_age": "Idade do Artista",
"performer_count": "Contagem de artistas", "performer_count": "Contagem de artistas",
"performer_favorite": "Artista Favoritado", "performer_favorite": "Artista Favoritado",
@ -898,7 +898,7 @@
"resolution": "Resolução", "resolution": "Resolução",
"scene": "Cena", "scene": "Cena",
"sceneTagger": "Etiquetador de cena", "sceneTagger": "Etiquetador de cena",
"sceneTags": "Etiquetas da cena", "scene_tags": "Etiquetas da cena",
"scene_count": "Contagem de cena", "scene_count": "Contagem de cena",
"scene_id": "Cena ID", "scene_id": "Cena ID",
"scenes": "Cenas", "scenes": "Cenas",

View file

@ -933,7 +933,7 @@
"path": "Путь", "path": "Путь",
"perceptual_similarity": "Воспринимаемое сходство (phash)", "perceptual_similarity": "Воспринимаемое сходство (phash)",
"performer": "Актер", "performer": "Актер",
"performerTags": "Теги актера", "performer_tags": "Теги актера",
"performer_age": "Возраст актера", "performer_age": "Возраст актера",
"performer_count": "Количество актеров", "performer_count": "Количество актеров",
"performer_favorite": "Участник добавлен в избранное", "performer_favorite": "Участник добавлен в избранное",
@ -988,7 +988,7 @@
"resume_time": "Таймкод воспроизведения", "resume_time": "Таймкод воспроизведения",
"scene": "Сцена", "scene": "Сцена",
"sceneTagger": "Пометка сцен тэгами", "sceneTagger": "Пометка сцен тэгами",
"sceneTags": "Тэги сцен", "scene_tags": "Тэги сцен",
"scene_code": "Идентификатор сцены", "scene_code": "Идентификатор сцены",
"scene_count": "Количество сцен", "scene_count": "Количество сцен",
"scene_created_at": "Сцена создана", "scene_created_at": "Сцена создана",

View file

@ -1033,7 +1033,7 @@
"penis_length_cm": "Penislängd (cm)", "penis_length_cm": "Penislängd (cm)",
"perceptual_similarity": "Perceptuell likhet (phash)", "perceptual_similarity": "Perceptuell likhet (phash)",
"performer": "Stjärna", "performer": "Stjärna",
"performerTags": "Stjärntagg", "performer_tags": "Stjärntagg",
"performer_age": "Ålder på stjärna", "performer_age": "Ålder på stjärna",
"performer_count": "Antal stjärnor", "performer_count": "Antal stjärnor",
"performer_favorite": "Favoritiserad stjärna", "performer_favorite": "Favoritiserad stjärna",
@ -1088,7 +1088,7 @@
"resume_time": "Återupptagningstid", "resume_time": "Återupptagningstid",
"scene": "Scen", "scene": "Scen",
"sceneTagger": "Scentaggaren", "sceneTagger": "Scentaggaren",
"sceneTags": "Scentaggar", "scene_tags": "Scentaggar",
"scene_code": "Studiokod", "scene_code": "Studiokod",
"scene_count": "Antal scener", "scene_count": "Antal scener",
"scene_created_at": "Scenen Skapad", "scene_created_at": "Scenen Skapad",

View file

@ -747,7 +747,7 @@
"part_of": "{parent} öğesinin parçası", "part_of": "{parent} öğesinin parçası",
"path": "Konum", "path": "Konum",
"performer": "Oyuncu", "performer": "Oyuncu",
"performerTags": "Oyuncu Etiketleri", "performer_tags": "Oyuncu Etiketleri",
"performer_count": "Oyuncu Sayısı", "performer_count": "Oyuncu Sayısı",
"performer_image": "Oyuncu Resmi", "performer_image": "Oyuncu Resmi",
"performers": "Oyuncular", "performers": "Oyuncular",
@ -758,7 +758,7 @@
"resolution": "Çözünürlük", "resolution": "Çözünürlük",
"scene": "Sahne", "scene": "Sahne",
"sceneTagger": "Sahne Etiketleyici", "sceneTagger": "Sahne Etiketleyici",
"sceneTags": "Sahne Etiketleri", "scene_tags": "Sahne Etiketleri",
"scene_count": "Sahne Sayısı", "scene_count": "Sahne Sayısı",
"scene_id": "Sahne Kimliği (ID)", "scene_id": "Sahne Kimliği (ID)",
"scenes": "Sahneler", "scenes": "Sahneler",

View file

@ -1018,7 +1018,7 @@
"path": "路径", "path": "路径",
"perceptual_similarity": "感知的类似程度(感知码)", "perceptual_similarity": "感知的类似程度(感知码)",
"performer": "演员", "performer": "演员",
"performerTags": "演员标签", "performer_tags": "演员标签",
"performer_age": "演员年龄", "performer_age": "演员年龄",
"performer_count": "演员数量", "performer_count": "演员数量",
"performer_favorite": "演员已收藏", "performer_favorite": "演员已收藏",
@ -1073,7 +1073,7 @@
"resume_time": "恢复时间", "resume_time": "恢复时间",
"scene": "短片", "scene": "短片",
"sceneTagger": "短片标记器", "sceneTagger": "短片标记器",
"sceneTags": "短片标记", "scene_tags": "短片标记",
"scene_code": "工作室代码", "scene_code": "工作室代码",
"scene_count": "短片数量", "scene_count": "短片数量",
"scene_created_at": "短片建立在", "scene_created_at": "短片建立在",

View file

@ -956,7 +956,7 @@
"path": "路徑", "path": "路徑",
"perceptual_similarity": "感知相似度 (PHash)", "perceptual_similarity": "感知相似度 (PHash)",
"performer": "演員", "performer": "演員",
"performerTags": "演員標籤", "performer_tags": "演員標籤",
"performer_age": "演員年齡", "performer_age": "演員年齡",
"performer_count": "演員數量", "performer_count": "演員數量",
"performer_favorite": "已收藏的演員", "performer_favorite": "已收藏的演員",
@ -1011,7 +1011,7 @@
"resume_time": "恢復播放時間", "resume_time": "恢復播放時間",
"scene": "短片", "scene": "短片",
"sceneTagger": "短片標籤器", "sceneTagger": "短片標籤器",
"sceneTags": "短片標籤", "scene_tags": "短片標籤",
"scene_code": "番號", "scene_code": "番號",
"scene_count": "短片數量", "scene_count": "短片數量",
"scene_created_at": "短片建立於", "scene_created_at": "短片建立於",

View file

@ -10,7 +10,6 @@ class CaptionsCriterionOptionType extends CriterionOption {
super({ super({
messageID: value, messageID: value,
type: value, type: value,
parameterName: value,
modifierOptions: [ modifierOptions: [
CriterionModifier.Includes, CriterionModifier.Includes,
CriterionModifier.Excludes, CriterionModifier.Excludes,
@ -19,6 +18,7 @@ class CaptionsCriterionOptionType extends CriterionOption {
], ],
defaultModifier: CriterionModifier.Includes, defaultModifier: CriterionModifier.Includes,
options: languageStrings, options: languageStrings,
makeCriterion: () => new CaptionCriterion(),
}); });
} }
} }

View file

@ -16,6 +16,7 @@ export const CircumcisedCriterionOption = new CriterionOption({
CriterionModifier.IsNull, CriterionModifier.IsNull,
CriterionModifier.NotNull, CriterionModifier.NotNull,
], ],
makeCriterion: () => new CircumcisedCriterion(),
}); });
export class CircumcisedCriterion extends MultiStringCriterion { export class CircumcisedCriterion extends MultiStringCriterion {

View file

@ -3,11 +3,7 @@ import { CriterionModifier } from "src/core/generated-graphql";
import { getCountryByISO } from "src/utils/country"; import { getCountryByISO } from "src/utils/country";
import { StringCriterion, StringCriterionOption } from "./criterion"; import { StringCriterion, StringCriterionOption } from "./criterion";
const countryCriterionOption = new StringCriterionOption( const countryCriterionOption = new StringCriterionOption("country", "country");
"country",
"country",
"country"
);
export class CountryCriterion extends StringCriterion { export class CountryCriterion extends StringCriterion {
constructor() { constructor() {

View file

@ -121,7 +121,7 @@ export abstract class Criterion<V extends CriterionValue> {
} }
public getId(): string { public getId(): string {
return `${this.criterionOption.parameterName}-${this.modifier.toString()}`; // TODO add values? return `${this.criterionOption.type}-${this.modifier.toString()}`; // TODO add values?
} }
public toJSON() { public toJSON() {
@ -154,7 +154,7 @@ export abstract class Criterion<V extends CriterionValue> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
public apply(outputFilter: Record<string, any>) { public apply(outputFilter: Record<string, any>) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
outputFilter[this.criterionOption.parameterName] = this.toCriterionInput(); outputFilter[this.criterionOption.type] = this.toCriterionInput();
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -164,50 +164,68 @@ export abstract class Criterion<V extends CriterionValue> {
modifier: this.modifier, modifier: this.modifier,
}; };
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toSavedFilter(outputFilter: Record<string, any>) {
outputFilter[this.criterionOption.type] = {
value: this.value,
modifier: this.modifier,
};
}
} }
export type InputType = "number" | "text" | undefined; export type InputType =
| "number"
| "text"
| "performers"
| "studios"
| "tags"
| "performer_tags"
| "scene_tags"
| "movies"
| "galleries"
| undefined;
interface ICriterionOptionsParams { interface ICriterionOptionsParams {
messageID: string; messageID: string;
type: CriterionType; type: CriterionType;
inputType?: InputType; inputType?: InputType;
parameterName?: string;
modifierOptions?: CriterionModifier[]; modifierOptions?: CriterionModifier[];
defaultModifier?: CriterionModifier; defaultModifier?: CriterionModifier;
options?: Option[]; options?: Option[];
makeCriterion: () => Criterion<CriterionValue>;
} }
export class CriterionOption { export class CriterionOption {
public readonly messageID: string; public readonly messageID: string;
public readonly type: CriterionType; public readonly type: CriterionType;
public readonly parameterName: string;
public readonly modifierOptions: CriterionModifier[]; public readonly modifierOptions: CriterionModifier[];
public readonly defaultModifier: CriterionModifier; public readonly defaultModifier: CriterionModifier;
public readonly options: Option[] | undefined; public readonly options: Option[] | undefined;
public readonly inputType: InputType; public readonly inputType: InputType;
public readonly makeCriterionFn: (
o: CriterionOption
) => Criterion<CriterionValue>;
constructor(options: ICriterionOptionsParams) { constructor(options: ICriterionOptionsParams) {
this.messageID = options.messageID; this.messageID = options.messageID;
this.type = options.type; this.type = options.type;
this.parameterName = options.parameterName ?? options.type;
this.modifierOptions = options.modifierOptions ?? []; this.modifierOptions = options.modifierOptions ?? [];
this.defaultModifier = options.defaultModifier ?? CriterionModifier.Equals; this.defaultModifier = options.defaultModifier ?? CriterionModifier.Equals;
this.options = options.options; this.options = options.options;
this.inputType = options.inputType; this.inputType = options.inputType;
this.makeCriterionFn = options.makeCriterion;
}
public makeCriterion() {
return this.makeCriterionFn(this);
} }
} }
export class StringCriterionOption extends CriterionOption { export class StringCriterionOption extends CriterionOption {
constructor( constructor(messageID: string, type: CriterionType, options?: Option[]) {
messageID: string,
value: CriterionType,
parameterName?: string,
options?: Option[]
) {
super({ super({
messageID, messageID,
type: value, type,
parameterName,
modifierOptions: [ modifierOptions: [
CriterionModifier.Equals, CriterionModifier.Equals,
CriterionModifier.NotEquals, CriterionModifier.NotEquals,
@ -221,20 +239,16 @@ export class StringCriterionOption extends CriterionOption {
defaultModifier: CriterionModifier.Equals, defaultModifier: CriterionModifier.Equals,
options, options,
inputType: "text", inputType: "text",
makeCriterion: () => new StringCriterion(this),
}); });
} }
} }
export function createStringCriterionOption( export function createStringCriterionOption(
value: CriterionType, type: CriterionType,
messageID?: string, messageID?: string
parameterName?: string
) { ) {
return new StringCriterionOption( return new StringCriterionOption(messageID ?? type, type);
messageID ?? value,
value,
parameterName ?? messageID ?? value
);
} }
export class StringCriterion extends Criterion<string> { export class StringCriterion extends Criterion<string> {
@ -274,16 +288,10 @@ export class MultiStringCriterion extends Criterion<string[]> {
} }
export class MandatoryStringCriterionOption extends CriterionOption { export class MandatoryStringCriterionOption extends CriterionOption {
constructor( constructor(messageID: string, value: CriterionType, options?: Option[]) {
messageID: string,
value: CriterionType,
parameterName?: string,
options?: Option[]
) {
super({ super({
messageID, messageID,
type: value, type: value,
parameterName,
modifierOptions: [ modifierOptions: [
CriterionModifier.Equals, CriterionModifier.Equals,
CriterionModifier.NotEquals, CriterionModifier.NotEquals,
@ -295,45 +303,42 @@ export class MandatoryStringCriterionOption extends CriterionOption {
defaultModifier: CriterionModifier.Equals, defaultModifier: CriterionModifier.Equals,
options, options,
inputType: "text", inputType: "text",
makeCriterion: () => new StringCriterion(this),
}); });
} }
} }
export function createMandatoryStringCriterionOption( export function createMandatoryStringCriterionOption(
value: CriterionType, value: CriterionType,
messageID?: string, messageID?: string
parameterName?: string
) { ) {
return new MandatoryStringCriterionOption( return new MandatoryStringCriterionOption(messageID ?? value, value);
messageID ?? value,
value,
parameterName ?? messageID ?? value
);
} }
export class PathCriterionOption extends StringCriterionOption {} export class PathCriterionOption extends StringCriterionOption {}
export function createPathCriterionOption( export function createPathCriterionOption(
value: CriterionType, type: CriterionType,
messageID?: string, messageID?: string
parameterName?: string
) { ) {
return new PathCriterionOption( return new PathCriterionOption(messageID ?? type, type);
messageID ?? value,
value,
parameterName ?? messageID ?? value
);
} }
export class BooleanCriterionOption extends CriterionOption { export class BooleanCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType, parameterName?: string) { constructor(
messageID: string,
value: CriterionType,
makeCriterion?: () => Criterion<CriterionValue>
) {
super({ super({
messageID, messageID,
type: value, type: value,
parameterName,
modifierOptions: [], modifierOptions: [],
defaultModifier: CriterionModifier.Equals, defaultModifier: CriterionModifier.Equals,
options: [true.toString(), false.toString()], options: [true.toString(), false.toString()],
makeCriterion: makeCriterion
? makeCriterion
: () => new BooleanCriterion(this),
}); });
} }
} }
@ -350,27 +355,16 @@ export class BooleanCriterion extends StringCriterion {
export function createBooleanCriterionOption( export function createBooleanCriterionOption(
value: CriterionType, value: CriterionType,
messageID?: string, messageID?: string
parameterName?: string
) { ) {
return new BooleanCriterionOption( return new BooleanCriterionOption(messageID ?? value, value);
messageID ?? value,
value,
parameterName ?? messageID ?? value
);
} }
export class NumberCriterionOption extends CriterionOption { export class NumberCriterionOption extends CriterionOption {
constructor( constructor(messageID: string, value: CriterionType, options?: Option[]) {
messageID: string,
value: CriterionType,
parameterName?: string,
options?: Option[]
) {
super({ super({
messageID, messageID,
type: value, type: value,
parameterName,
modifierOptions: [ modifierOptions: [
CriterionModifier.Equals, CriterionModifier.Equals,
CriterionModifier.NotEquals, CriterionModifier.NotEquals,
@ -384,16 +378,16 @@ export class NumberCriterionOption extends CriterionOption {
defaultModifier: CriterionModifier.Equals, defaultModifier: CriterionModifier.Equals,
options, options,
inputType: "number", inputType: "number",
makeCriterion: () => new NumberCriterion(this),
}); });
} }
} }
export class NullNumberCriterionOption extends CriterionOption { export class NullNumberCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType, parameterName?: string) { constructor(messageID: string, value: CriterionType) {
super({ super({
messageID, messageID,
type: value, type: value,
parameterName,
modifierOptions: [ modifierOptions: [
CriterionModifier.Equals, CriterionModifier.Equals,
CriterionModifier.NotEquals, CriterionModifier.NotEquals,
@ -406,16 +400,17 @@ export class NullNumberCriterionOption extends CriterionOption {
], ],
defaultModifier: CriterionModifier.Equals, defaultModifier: CriterionModifier.Equals,
inputType: "number", inputType: "number",
makeCriterion: () => new NumberCriterion(this),
}); });
} }
} }
export function createNumberCriterionOption(value: CriterionType) { export function createNumberCriterionOption(value: CriterionType) {
return new NumberCriterionOption(value, value, value); return new NumberCriterionOption(value, value);
} }
export function createNullNumberCriterionOption(value: CriterionType) { export function createNullNumberCriterionOption(value: CriterionType) {
return new NullNumberCriterionOption(value, value, value); return new NullNumberCriterionOption(value, value);
} }
export class NumberCriterion extends Criterion<INumberValue> { export class NumberCriterion extends Criterion<INumberValue> {
@ -437,8 +432,8 @@ export class NumberCriterion extends Criterion<INumberValue> {
protected toCriterionInput(): IntCriterionInput { protected toCriterionInput(): IntCriterionInput {
return { return {
modifier: this.modifier, modifier: this.modifier,
value: this.value.value ?? 0, value: this.value?.value ?? 0,
value2: this.value.value2, value2: this.value?.value2,
}; };
} }
@ -487,8 +482,8 @@ export class ILabeledIdCriterionOption extends CriterionOption {
constructor( constructor(
messageID: string, messageID: string,
value: CriterionType, value: CriterionType,
parameterName: string, includeAll: boolean,
includeAll: boolean inputType: InputType
) { ) {
const modifierOptions = [ const modifierOptions = [
CriterionModifier.Includes, CriterionModifier.Includes,
@ -506,9 +501,10 @@ export class ILabeledIdCriterionOption extends CriterionOption {
super({ super({
messageID, messageID,
type: value, type: value,
parameterName,
modifierOptions, modifierOptions,
defaultModifier, defaultModifier,
makeCriterion: () => new ILabeledIdCriterion(this),
inputType,
}); });
} }
} }
@ -684,11 +680,10 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
} }
export class MandatoryNumberCriterionOption extends CriterionOption { export class MandatoryNumberCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType, parameterName?: string) { constructor(messageID: string, value: CriterionType) {
super({ super({
messageID, messageID,
type: value, type: value,
parameterName,
modifierOptions: [ modifierOptions: [
CriterionModifier.Equals, CriterionModifier.Equals,
CriterionModifier.NotEquals, CriterionModifier.NotEquals,
@ -699,6 +694,7 @@ export class MandatoryNumberCriterionOption extends CriterionOption {
], ],
defaultModifier: CriterionModifier.Equals, defaultModifier: CriterionModifier.Equals,
inputType: "number", inputType: "number",
makeCriterion: () => new NumberCriterion(this),
}); });
} }
} }
@ -707,7 +703,7 @@ export function createMandatoryNumberCriterionOption(
value: CriterionType, value: CriterionType,
messageID?: string messageID?: string
) { ) {
return new MandatoryNumberCriterionOption(messageID ?? value, value, value); return new MandatoryNumberCriterionOption(messageID ?? value, value);
} }
export class DurationCriterion extends Criterion<INumberValue> { export class DurationCriterion extends Criterion<INumberValue> {
@ -718,8 +714,8 @@ export class DurationCriterion extends Criterion<INumberValue> {
protected toCriterionInput(): IntCriterionInput { protected toCriterionInput(): IntCriterionInput {
return { return {
modifier: this.modifier, modifier: this.modifier,
value: this.value.value ?? 0, value: this.value?.value ?? 0,
value2: this.value.value2, value2: this.value?.value2,
}; };
} }
@ -771,16 +767,10 @@ export class PhashDuplicateCriterion extends StringCriterion {
} }
export class DateCriterionOption extends CriterionOption { export class DateCriterionOption extends CriterionOption {
constructor( constructor(messageID: string, value: CriterionType, options?: Option[]) {
messageID: string,
value: CriterionType,
parameterName?: string,
options?: Option[]
) {
super({ super({
messageID, messageID,
type: value, type: value,
parameterName,
modifierOptions: [ modifierOptions: [
CriterionModifier.Equals, CriterionModifier.Equals,
CriterionModifier.NotEquals, CriterionModifier.NotEquals,
@ -794,12 +784,13 @@ export class DateCriterionOption extends CriterionOption {
defaultModifier: CriterionModifier.Equals, defaultModifier: CriterionModifier.Equals,
options, options,
inputType: "text", inputType: "text",
makeCriterion: () => new DateCriterion(this),
}); });
} }
} }
export function createDateCriterionOption(value: CriterionType) { export function createDateCriterionOption(value: CriterionType) {
return new DateCriterionOption(value, value, value); return new DateCriterionOption(value, value);
} }
export class DateCriterion extends Criterion<IDateValue> { export class DateCriterion extends Criterion<IDateValue> {
@ -813,8 +804,8 @@ export class DateCriterion extends Criterion<IDateValue> {
protected toCriterionInput(): DateCriterionInput { protected toCriterionInput(): DateCriterionInput {
return { return {
modifier: this.modifier, modifier: this.modifier,
value: this.value.value, value: this.value?.value,
value2: this.value.value2, value2: this.value?.value2,
}; };
} }
@ -856,16 +847,10 @@ export class DateCriterion extends Criterion<IDateValue> {
} }
export class TimestampCriterionOption extends CriterionOption { export class TimestampCriterionOption extends CriterionOption {
constructor( constructor(messageID: string, value: CriterionType, options?: Option[]) {
messageID: string,
value: CriterionType,
parameterName?: string,
options?: Option[]
) {
super({ super({
messageID, messageID,
type: value, type: value,
parameterName,
modifierOptions: [ modifierOptions: [
CriterionModifier.GreaterThan, CriterionModifier.GreaterThan,
CriterionModifier.LessThan, CriterionModifier.LessThan,
@ -877,19 +862,20 @@ export class TimestampCriterionOption extends CriterionOption {
defaultModifier: CriterionModifier.GreaterThan, defaultModifier: CriterionModifier.GreaterThan,
options, options,
inputType: "text", inputType: "text",
makeCriterion: () => new TimestampCriterion(this),
}); });
} }
} }
export function createTimestampCriterionOption(value: CriterionType) { export function createTimestampCriterionOption(value: CriterionType) {
return new TimestampCriterionOption(value, value, value); return new TimestampCriterionOption(value, value);
} }
export class TimestampCriterion extends Criterion<ITimestampValue> { export class TimestampCriterion extends Criterion<ITimestampValue> {
public encodeValue() { public encodeValue() {
return { return {
value: this.value.value, value: this.value?.value,
value2: this.value.value2, value2: this.value?.value2,
}; };
} }
@ -950,16 +936,10 @@ export class TimestampCriterion extends Criterion<ITimestampValue> {
} }
export class MandatoryTimestampCriterionOption extends CriterionOption { export class MandatoryTimestampCriterionOption extends CriterionOption {
constructor( constructor(messageID: string, value: CriterionType, options?: Option[]) {
messageID: string,
value: CriterionType,
parameterName?: string,
options?: Option[]
) {
super({ super({
messageID, messageID,
type: value, type: value,
parameterName,
modifierOptions: [ modifierOptions: [
CriterionModifier.GreaterThan, CriterionModifier.GreaterThan,
CriterionModifier.LessThan, CriterionModifier.LessThan,
@ -969,10 +949,11 @@ export class MandatoryTimestampCriterionOption extends CriterionOption {
defaultModifier: CriterionModifier.GreaterThan, defaultModifier: CriterionModifier.GreaterThan,
options, options,
inputType: "text", inputType: "text",
makeCriterion: () => new TimestampCriterion(this),
}); });
} }
} }
export function createMandatoryTimestampCriterionOption(value: CriterionType) { export function createMandatoryTimestampCriterionOption(value: CriterionType) {
return new MandatoryTimestampCriterionOption(value, value, value); return new MandatoryTimestampCriterionOption(value, value);
} }

View file

@ -1,250 +1,36 @@
/* eslint-disable consistent-return, default-case */
import {
StringCriterion,
NumberCriterion,
DurationCriterion,
NumberCriterionOption,
MandatoryStringCriterionOption,
NullNumberCriterionOption,
MandatoryNumberCriterionOption,
StringCriterionOption,
ILabeledIdCriterion,
BooleanCriterion,
BooleanCriterionOption,
DateCriterion,
DateCriterionOption,
TimestampCriterion,
MandatoryTimestampCriterionOption,
PathCriterionOption,
} from "./criterion";
import { OrganizedCriterion } from "./organized";
import { FavoriteCriterion, PerformerFavoriteCriterion } from "./favorite";
import { HasMarkersCriterion } from "./has-markers";
import { HasChaptersCriterion } from "./has-chapters";
import {
PerformerIsMissingCriterionOption,
ImageIsMissingCriterionOption,
TagIsMissingCriterionOption,
SceneIsMissingCriterionOption,
IsMissingCriterion,
GalleryIsMissingCriterionOption,
StudioIsMissingCriterionOption,
MovieIsMissingCriterionOption,
} from "./is-missing";
import { NoneCriterion } from "./none";
import { PerformersCriterion } from "./performers";
import { AverageResolutionCriterion, ResolutionCriterion } from "./resolution";
import { StudiosCriterion, ParentStudiosCriterion } from "./studios";
import {
ChildTagsCriterionOption,
ParentTagsCriterionOption,
PerformerTagsCriterionOption,
SceneTagsCriterionOption,
TagsCriterion,
TagsCriterionOption,
} from "./tags";
import { GenderCriterion } from "./gender";
import { CircumcisedCriterion } from "./circumcised";
import { MoviesCriterionOption } from "./movies";
import { GalleriesCriterion } from "./galleries";
import { CriterionType } from "../types";
import { InteractiveCriterion } from "./interactive";
import { DuplicatedCriterion, PhashCriterion } from "./phash";
import { CaptionCriterion } from "./captions";
import { RatingCriterion } from "./rating";
import { CountryCriterion } from "./country";
import { StashIDCriterion } from "./stash-ids";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { IUIConfig } from "src/core/config"; import { SceneListFilterOptions } from "../scenes";
import { defaultRatingSystemOptions } from "src/utils/rating"; import { MovieListFilterOptions } from "../movies";
import { GalleryListFilterOptions } from "../galleries";
import { PerformerListFilterOptions } from "../performers";
import { ImageListFilterOptions } from "../images";
import { SceneMarkerListFilterOptions } from "../scene-markers";
import { StudioListFilterOptions } from "../studios";
import { TagListFilterOptions } from "../tags";
import { CriterionType } from "../types";
const filterModeOptions = {
[GQL.FilterMode.Galleries]: GalleryListFilterOptions.criterionOptions,
[GQL.FilterMode.Images]: ImageListFilterOptions.criterionOptions,
[GQL.FilterMode.Movies]: MovieListFilterOptions.criterionOptions,
[GQL.FilterMode.Performers]: PerformerListFilterOptions.criterionOptions,
[GQL.FilterMode.SceneMarkers]: SceneMarkerListFilterOptions.criterionOptions,
[GQL.FilterMode.Scenes]: SceneListFilterOptions.criterionOptions,
[GQL.FilterMode.Studios]: StudioListFilterOptions.criterionOptions,
[GQL.FilterMode.Tags]: TagListFilterOptions.criterionOptions,
};
export function makeCriteria( export function makeCriteria(
config: GQL.ConfigDataFragment | undefined, mode: GQL.FilterMode,
type: CriterionType = "none" type: CriterionType = "none"
) { ) {
switch (type) { const criterionOptions = filterModeOptions[mode];
case "none":
return new NoneCriterion(); const option = criterionOptions.find((o) => o.type === type);
case "name":
return new StringCriterion( if (!option) {
new MandatoryStringCriterionOption(type, type) throw new Error(`Unknown criterion parameter name: ${type}`);
);
case "path":
return new StringCriterion(new PathCriterionOption(type, type));
case "checksum":
return new StringCriterion(
new MandatoryStringCriterionOption("media_info.checksum", type, type)
);
case "oshash":
return new StringCriterion(
new MandatoryStringCriterionOption("media_info.hash", type, type)
);
case "organized":
return new OrganizedCriterion();
case "o_counter":
case "interactive_speed":
case "scene_count":
case "marker_count":
case "image_count":
case "gallery_count":
case "performer_count":
case "performer_age":
case "tag_count":
case "file_count":
case "play_count":
return new NumberCriterion(
new MandatoryNumberCriterionOption(type, type)
);
case "rating":
return new NumberCriterion(new NullNumberCriterionOption(type, type));
case "rating100":
return new RatingCriterion(
new NullNumberCriterionOption("rating", type),
(config?.ui as IUIConfig)?.ratingSystemOptions ??
defaultRatingSystemOptions
);
case "resolution":
return new ResolutionCriterion();
case "average_resolution":
return new AverageResolutionCriterion();
case "video_codec":
return new StringCriterion(new StringCriterionOption(type, type));
case "audio_codec":
return new StringCriterion(new StringCriterionOption(type, type));
case "resume_time":
case "duration":
case "play_duration":
return new DurationCriterion(
new MandatoryNumberCriterionOption(type, type)
);
case "favorite":
return new FavoriteCriterion();
case "hasMarkers":
return new HasMarkersCriterion();
case "hasChapters":
return new HasChaptersCriterion();
case "sceneIsMissing":
return new IsMissingCriterion(SceneIsMissingCriterionOption);
case "imageIsMissing":
return new IsMissingCriterion(ImageIsMissingCriterionOption);
case "performerIsMissing":
return new IsMissingCriterion(PerformerIsMissingCriterionOption);
case "galleryIsMissing":
return new IsMissingCriterion(GalleryIsMissingCriterionOption);
case "tagIsMissing":
return new IsMissingCriterion(TagIsMissingCriterionOption);
case "studioIsMissing":
return new IsMissingCriterion(StudioIsMissingCriterionOption);
case "movieIsMissing":
return new IsMissingCriterion(MovieIsMissingCriterionOption);
case "tags":
return new TagsCriterion(TagsCriterionOption);
case "sceneTags":
return new TagsCriterion(SceneTagsCriterionOption);
case "performerTags":
return new TagsCriterion(PerformerTagsCriterionOption);
case "parentTags":
return new TagsCriterion(ParentTagsCriterionOption);
case "childTags":
return new TagsCriterion(ChildTagsCriterionOption);
case "performers":
return new PerformersCriterion();
case "performer_favorite":
return new PerformerFavoriteCriterion();
case "studios":
return new StudiosCriterion();
case "parent_studios":
return new ParentStudiosCriterion();
case "movies":
return new ILabeledIdCriterion(MoviesCriterionOption);
case "galleries":
return new GalleriesCriterion();
case "birth_year":
case "death_year":
case "weight":
return new NumberCriterion(new NumberCriterionOption(type, type));
case "penis_length":
return new NumberCriterion(new NumberCriterionOption(type, type));
case "age":
return new NumberCriterion(
new MandatoryNumberCriterionOption(type, type)
);
case "gender":
return new GenderCriterion();
case "circumcised":
return new CircumcisedCriterion();
case "sceneChecksum":
case "galleryChecksum":
return new StringCriterion(
new StringCriterionOption("media_info.checksum", type, "checksum")
);
case "phash":
return new PhashCriterion();
case "duplicated":
return new DuplicatedCriterion();
case "country":
return new CountryCriterion();
case "height":
case "height_cm":
return new NumberCriterion(
new NumberCriterionOption("height", "height_cm", type)
);
// stash_id is deprecated
case "stash_id":
case "stash_id_endpoint":
return new StashIDCriterion();
case "ethnicity":
case "hair_color":
case "eye_color":
case "measurements":
case "fake_tits":
case "career_length":
case "tattoos":
case "piercings":
case "aliases":
case "url":
case "details":
case "title":
case "director":
case "synopsis":
case "description":
case "disambiguation":
return new StringCriterion(new StringCriterionOption(type, type));
case "scene_code":
return new StringCriterion(new StringCriterionOption(type, type, "code"));
case "interactive":
return new InteractiveCriterion();
case "captions":
return new CaptionCriterion();
case "parent_tag_count":
return new NumberCriterion(
new MandatoryNumberCriterionOption(
"parent_tag_count",
"parent_tag_count",
"parent_count"
)
);
case "child_tag_count":
return new NumberCriterion(
new MandatoryNumberCriterionOption(
"sub_tag_count",
"child_tag_count",
"child_count"
)
);
case "ignore_auto_tag":
return new BooleanCriterion(new BooleanCriterionOption(type, type));
case "date":
case "birthdate":
case "death_date":
case "scene_date":
return new DateCriterion(new DateCriterionOption(type, type));
case "created_at":
case "updated_at":
case "scene_created_at":
case "scene_updated_at":
return new TimestampCriterion(
new MandatoryTimestampCriterionOption(type, type)
);
} }
return option?.makeCriterion();
} }

View file

@ -2,7 +2,6 @@ import { BooleanCriterion, BooleanCriterionOption } from "./criterion";
export const FavoriteCriterionOption = new BooleanCriterionOption( export const FavoriteCriterionOption = new BooleanCriterionOption(
"favourite", "favourite",
"favorite",
"filter_favorites" "filter_favorites"
); );
@ -13,7 +12,6 @@ export class FavoriteCriterion extends BooleanCriterion {
} }
export const PerformerFavoriteCriterionOption = new BooleanCriterionOption( export const PerformerFavoriteCriterionOption = new BooleanCriterionOption(
"performer_favorite",
"performer_favorite", "performer_favorite",
"performer_favorite" "performer_favorite"
); );

View file

@ -1,10 +1,12 @@
import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion"; import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
const inputType = "galleries";
const galleriesCriterionOption = new ILabeledIdCriterionOption( const galleriesCriterionOption = new ILabeledIdCriterionOption(
"galleries", "galleries",
"galleries", "galleries",
"galleries", true,
true inputType
); );
export class GalleriesCriterion extends ILabeledIdCriterion { export class GalleriesCriterion extends ILabeledIdCriterion {

View file

@ -6,6 +6,7 @@ export const GenderCriterionOption = new CriterionOption({
messageID: "gender", messageID: "gender",
type: "gender", type: "gender",
options: genderStrings, options: genderStrings,
makeCriterion: () => new GenderCriterion(),
}); });
export class GenderCriterion extends StringCriterion { export class GenderCriterion extends StringCriterion {

View file

@ -2,9 +2,9 @@ import { CriterionOption, StringCriterion } from "./criterion";
export const HasChaptersCriterionOption = new CriterionOption({ export const HasChaptersCriterionOption = new CriterionOption({
messageID: "hasChapters", messageID: "hasChapters",
type: "hasChapters", type: "has_chapters",
parameterName: "has_chapters",
options: [true.toString(), false.toString()], options: [true.toString(), false.toString()],
makeCriterion: () => new HasChaptersCriterion(),
}); });
export class HasChaptersCriterion extends StringCriterion { export class HasChaptersCriterion extends StringCriterion {

View file

@ -2,9 +2,9 @@ import { CriterionOption, StringCriterion } from "./criterion";
export const HasMarkersCriterionOption = new CriterionOption({ export const HasMarkersCriterionOption = new CriterionOption({
messageID: "hasMarkers", messageID: "hasMarkers",
type: "hasMarkers", type: "has_markers",
parameterName: "has_markers",
options: [true.toString(), false.toString()], options: [true.toString(), false.toString()],
makeCriterion: () => new HasMarkersCriterion(),
}); });
export class HasMarkersCriterion extends StringCriterion { export class HasMarkersCriterion extends StringCriterion {

View file

@ -11,25 +11,19 @@ export class IsMissingCriterion extends StringCriterion {
} }
class IsMissingCriterionOptionClass extends CriterionOption { class IsMissingCriterionOptionClass extends CriterionOption {
constructor( constructor(messageID: string, type: CriterionType, options: Option[]) {
messageID: string,
value: CriterionType,
parameterName: string,
options: Option[]
) {
super({ super({
messageID, messageID,
type: value, type,
parameterName,
options, options,
defaultModifier: CriterionModifier.Equals, defaultModifier: CriterionModifier.Equals,
makeCriterion: () => new IsMissingCriterion(this),
}); });
} }
} }
export const SceneIsMissingCriterionOption = new IsMissingCriterionOptionClass( export const SceneIsMissingCriterionOption = new IsMissingCriterionOptionClass(
"isMissing", "isMissing",
"sceneIsMissing",
"is_missing", "is_missing",
[ [
"title", "title",
@ -48,73 +42,59 @@ export const SceneIsMissingCriterionOption = new IsMissingCriterionOptionClass(
export const ImageIsMissingCriterionOption = new IsMissingCriterionOptionClass( export const ImageIsMissingCriterionOption = new IsMissingCriterionOptionClass(
"isMissing", "isMissing",
"imageIsMissing",
"is_missing", "is_missing",
["title", "galleries", "studio", "performers", "tags"] ["title", "galleries", "studio", "performers", "tags"]
); );
export const PerformerIsMissingCriterionOption = export const PerformerIsMissingCriterionOption =
new IsMissingCriterionOptionClass( new IsMissingCriterionOptionClass("isMissing", "is_missing", [
"isMissing", "url",
"performerIsMissing", "twitter",
"is_missing", "instagram",
[ "ethnicity",
"url", "country",
"twitter", "hair_color",
"instagram", "eye_color",
"ethnicity", "height",
"country", "weight",
"hair_color", "measurements",
"eye_color", "fake_tits",
"height", "career_length",
"weight", "tattoos",
"measurements", "piercings",
"fake_tits", "aliases",
"career_length", "gender",
"tattoos", "image",
"piercings", "details",
"aliases", "stash_id",
"gender", ]);
"image",
"details",
"stash_id",
]
);
export const GalleryIsMissingCriterionOption = export const GalleryIsMissingCriterionOption =
new IsMissingCriterionOptionClass( new IsMissingCriterionOptionClass("isMissing", "is_missing", [
"isMissing", "title",
"galleryIsMissing", "details",
"is_missing", "url",
[ "date",
"title", "studio",
"details", "performers",
"url", "tags",
"date", "scenes",
"studio", ]);
"performers",
"tags",
"scenes",
]
);
export const TagIsMissingCriterionOption = new IsMissingCriterionOptionClass( export const TagIsMissingCriterionOption = new IsMissingCriterionOptionClass(
"isMissing", "isMissing",
"tagIsMissing",
"is_missing", "is_missing",
["image"] ["image"]
); );
export const StudioIsMissingCriterionOption = new IsMissingCriterionOptionClass( export const StudioIsMissingCriterionOption = new IsMissingCriterionOptionClass(
"isMissing", "isMissing",
"studioIsMissing",
"is_missing", "is_missing",
["image", "stash_id", "details"] ["image", "stash_id", "details"]
); );
export const MovieIsMissingCriterionOption = new IsMissingCriterionOptionClass( export const MovieIsMissingCriterionOption = new IsMissingCriterionOptionClass(
"isMissing", "isMissing",
"movieIsMissing",
"is_missing", "is_missing",
["front_image", "back_image", "scenes"] ["front_image", "back_image", "scenes"]
); );

View file

@ -1,10 +1,12 @@
import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion"; import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
const inputType = "movies";
export const MoviesCriterionOption = new ILabeledIdCriterionOption( export const MoviesCriterionOption = new ILabeledIdCriterionOption(
"movies", "movies",
"movies", "movies",
"movies", false,
false inputType
); );
export class MoviesCriterion extends ILabeledIdCriterion { export class MoviesCriterion extends ILabeledIdCriterion {

View file

@ -1,10 +1,6 @@
import { Criterion, StringCriterionOption } from "./criterion"; import { Criterion, StringCriterionOption } from "./criterion";
export const NoneCriterionOption = new StringCriterionOption( export const NoneCriterionOption = new StringCriterionOption("none", "none");
"none",
"none",
"none"
);
export class NoneCriterion extends Criterion<string> { export class NoneCriterion extends Criterion<string> {
constructor() { constructor() {
super(NoneCriterionOption, "none"); super(NoneCriterionOption, "none");

View file

@ -1,7 +1,6 @@
import { BooleanCriterion, BooleanCriterionOption } from "./criterion"; import { BooleanCriterion, BooleanCriterionOption } from "./criterion";
export const OrganizedCriterionOption = new BooleanCriterionOption( export const OrganizedCriterionOption = new BooleanCriterionOption(
"organized",
"organized", "organized",
"organized" "organized"
); );

View file

@ -17,12 +17,15 @@ const modifierOptions = [
const defaultModifier = CriterionModifier.IncludesAll; const defaultModifier = CriterionModifier.IncludesAll;
const inputType = "performers";
export const PerformersCriterionOption = new CriterionOption({ export const PerformersCriterionOption = new CriterionOption({
messageID: "performers", messageID: "performers",
type: "performers", type: "performers",
parameterName: "performers",
modifierOptions, modifierOptions,
defaultModifier, defaultModifier,
makeCriterion: () => new PerformersCriterion(),
inputType,
}); });
export class PerformersCriterion extends Criterion<ILabeledValueListValue> { export class PerformersCriterion extends Criterion<ILabeledValueListValue> {

View file

@ -12,8 +12,7 @@ import {
export const PhashCriterionOption = new CriterionOption({ export const PhashCriterionOption = new CriterionOption({
messageID: "media_info.phash", messageID: "media_info.phash",
type: "phash", type: "phash_distance",
parameterName: "phash_distance",
inputType: "text", inputType: "text",
modifierOptions: [ modifierOptions: [
CriterionModifier.Equals, CriterionModifier.Equals,
@ -21,6 +20,7 @@ export const PhashCriterionOption = new CriterionOption({
CriterionModifier.IsNull, CriterionModifier.IsNull,
CriterionModifier.NotNull, CriterionModifier.NotNull,
], ],
makeCriterion: () => new PhashCriterion(),
}); });
export class PhashCriterion extends Criterion<IPhashDistanceValue> { export class PhashCriterion extends Criterion<IPhashDistanceValue> {
@ -53,7 +53,7 @@ export class PhashCriterion extends Criterion<IPhashDistanceValue> {
export const DuplicatedCriterionOption = new BooleanCriterionOption( export const DuplicatedCriterionOption = new BooleanCriterionOption(
"duplicated_phash", "duplicated_phash",
"duplicated", "duplicated",
"duplicated" () => new DuplicatedCriterion()
); );
export class DuplicatedCriterion extends PhashDuplicateCriterion { export class DuplicatedCriterion extends PhashDuplicateCriterion {

View file

@ -4,7 +4,12 @@ import {
} from "src/core/generated-graphql"; } from "src/core/generated-graphql";
import { stringToResolution, resolutionStrings } from "src/utils/resolution"; import { stringToResolution, resolutionStrings } from "src/utils/resolution";
import { CriterionType } from "../types"; import { CriterionType } from "../types";
import { CriterionOption, StringCriterion } from "./criterion"; import {
Criterion,
CriterionOption,
CriterionValue,
StringCriterion,
} from "./criterion";
abstract class AbstractResolutionCriterion extends StringCriterion { abstract class AbstractResolutionCriterion extends StringCriterion {
protected toCriterionInput(): ResolutionCriterionInput | undefined { protected toCriterionInput(): ResolutionCriterionInput | undefined {
@ -20,11 +25,13 @@ abstract class AbstractResolutionCriterion extends StringCriterion {
} }
class ResolutionCriterionOptionType extends CriterionOption { class ResolutionCriterionOptionType extends CriterionOption {
constructor(value: CriterionType) { constructor(
value: CriterionType,
makeCriterion: () => Criterion<CriterionValue>
) {
super({ super({
messageID: value, messageID: value,
type: value, type: value,
parameterName: value,
modifierOptions: [ modifierOptions: [
CriterionModifier.Equals, CriterionModifier.Equals,
CriterionModifier.NotEquals, CriterionModifier.NotEquals,
@ -32,12 +39,14 @@ class ResolutionCriterionOptionType extends CriterionOption {
CriterionModifier.LessThan, CriterionModifier.LessThan,
], ],
options: resolutionStrings, options: resolutionStrings,
makeCriterion,
}); });
} }
} }
export const ResolutionCriterionOption = new ResolutionCriterionOptionType( export const ResolutionCriterionOption = new ResolutionCriterionOptionType(
"resolution" "resolution",
() => new ResolutionCriterion()
); );
export class ResolutionCriterion extends AbstractResolutionCriterion { export class ResolutionCriterion extends AbstractResolutionCriterion {
constructor() { constructor() {
@ -46,7 +55,10 @@ export class ResolutionCriterion extends AbstractResolutionCriterion {
} }
export const AverageResolutionCriterionOption = export const AverageResolutionCriterionOption =
new ResolutionCriterionOptionType("average_resolution"); new ResolutionCriterionOptionType(
"average_resolution",
() => new AverageResolutionCriterion()
);
export class AverageResolutionCriterion extends AbstractResolutionCriterion { export class AverageResolutionCriterion extends AbstractResolutionCriterion {
constructor() { constructor() {

View file

@ -10,13 +10,13 @@ import { Criterion, CriterionOption } from "./criterion";
export const StashIDCriterionOption = new CriterionOption({ export const StashIDCriterionOption = new CriterionOption({
messageID: "stash_id", messageID: "stash_id",
type: "stash_id_endpoint", type: "stash_id_endpoint",
parameterName: "stash_id_endpoint",
modifierOptions: [ modifierOptions: [
CriterionModifier.Equals, CriterionModifier.Equals,
CriterionModifier.NotEquals, CriterionModifier.NotEquals,
CriterionModifier.IsNull, CriterionModifier.IsNull,
CriterionModifier.NotNull, CriterionModifier.NotNull,
], ],
makeCriterion: () => new StashIDCriterion(),
}); });
export class StashIDCriterion extends Criterion<IStashIDValue> { export class StashIDCriterion extends Criterion<IStashIDValue> {

View file

@ -13,13 +13,15 @@ const modifierOptions = [
]; ];
const defaultModifier = CriterionModifier.Includes; const defaultModifier = CriterionModifier.Includes;
const inputType = "studios";
export const StudiosCriterionOption = new CriterionOption({ export const StudiosCriterionOption = new CriterionOption({
messageID: "studios", messageID: "studios",
type: "studios", type: "studios",
parameterName: "studios",
modifierOptions, modifierOptions,
defaultModifier, defaultModifier,
makeCriterion: () => new StudiosCriterion(),
inputType,
}); });
export class StudiosCriterion extends IHierarchicalLabeledIdCriterion { export class StudiosCriterion extends IHierarchicalLabeledIdCriterion {
@ -29,10 +31,10 @@ export class StudiosCriterion extends IHierarchicalLabeledIdCriterion {
} }
export const ParentStudiosCriterionOption = new ILabeledIdCriterionOption( export const ParentStudiosCriterionOption = new ILabeledIdCriterionOption(
"parent_studios",
"parent_studios", "parent_studios",
"parents", "parents",
false false,
inputType
); );
export class ParentStudiosCriterion extends ILabeledIdCriterion { export class ParentStudiosCriterion extends ILabeledIdCriterion {
constructor() { constructor() {

View file

@ -1,7 +1,8 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { CriterionOption, IHierarchicalLabeledIdCriterion } from "./criterion"; import { CriterionOption, IHierarchicalLabeledIdCriterion } from "./criterion";
import { CriterionType } from "../types";
const modifierOptions = [ const defaultModifierOptions = [
CriterionModifier.IncludesAll, CriterionModifier.IncludesAll,
CriterionModifier.Includes, CriterionModifier.Includes,
CriterionModifier.Equals, CriterionModifier.Equals,
@ -17,41 +18,53 @@ const withoutEqualsModifierOptions = [
]; ];
const defaultModifier = CriterionModifier.IncludesAll; const defaultModifier = CriterionModifier.IncludesAll;
const inputType = "tags";
export const TagsCriterionOption = new CriterionOption({ export class TagsCriterionOptionClass extends CriterionOption {
messageID: "tags", constructor(
type: "tags", messageID: string,
parameterName: "tags", type: CriterionType,
modifierOptions, modifierOptions: CriterionModifier[]
defaultModifier, ) {
}); super({
export const SceneTagsCriterionOption = new CriterionOption({ messageID,
messageID: "sceneTags", type,
type: "sceneTags", modifierOptions,
parameterName: "scene_tags", defaultModifier,
modifierOptions, makeCriterion: () => new TagsCriterion(this),
defaultModifier, inputType,
}); });
export const PerformerTagsCriterionOption = new CriterionOption({ }
messageID: "performerTags", }
type: "performerTags",
parameterName: "performer_tags", export const TagsCriterionOption = new TagsCriterionOptionClass(
modifierOptions: withoutEqualsModifierOptions, "tags",
defaultModifier, "tags",
}); defaultModifierOptions
export const ParentTagsCriterionOption = new CriterionOption({ );
messageID: "parent_tags",
type: "parentTags", export const SceneTagsCriterionOption = new TagsCriterionOptionClass(
parameterName: "parents", "scene_tags",
modifierOptions: withoutEqualsModifierOptions, "scene_tags",
defaultModifier, defaultModifierOptions
}); );
export const ChildTagsCriterionOption = new CriterionOption({
messageID: "sub_tags", export const PerformerTagsCriterionOption = new TagsCriterionOptionClass(
type: "childTags", "performer_tags",
parameterName: "children", "performer_tags",
modifierOptions: withoutEqualsModifierOptions, withoutEqualsModifierOptions
defaultModifier, );
});
export const ParentTagsCriterionOption = new TagsCriterionOptionClass(
"parent_tags",
"parents",
withoutEqualsModifierOptions
);
export const ChildTagsCriterionOption = new TagsCriterionOptionClass(
"sub_tags",
"children",
withoutEqualsModifierOptions
);
export class TagsCriterion extends IHierarchicalLabeledIdCriterion {} export class TagsCriterion extends IHierarchicalLabeledIdCriterion {}

View file

@ -2,11 +2,12 @@ import {
ConfigDataFragment, ConfigDataFragment,
FilterMode, FilterMode,
FindFilterType, FindFilterType,
SavedFilterDataFragment,
SortDirectionEnum, SortDirectionEnum,
} from "src/core/generated-graphql"; } from "src/core/generated-graphql";
import { Criterion, CriterionValue } from "./criteria/criterion"; import { Criterion, CriterionValue } from "./criteria/criterion";
import { makeCriteria } from "./criteria/factory"; import { makeCriteria } from "./criteria/factory";
import { DisplayMode } from "./types"; import { CriterionType, DisplayMode } from "./types";
interface IDecodedParams { interface IDecodedParams {
perPage?: number; perPage?: number;
@ -127,7 +128,7 @@ export class ListFilterModel {
for (const jsonString of params.c) { for (const jsonString of params.c) {
try { try {
const encodedCriterion = JSON.parse(jsonString); const encodedCriterion = JSON.parse(jsonString);
const criterion = makeCriteria(this.config, encodedCriterion.type); const criterion = makeCriteria(this.mode, encodedCriterion.type);
// it's possible that we have unsupported criteria. Just skip if so. // it's possible that we have unsupported criteria. Just skip if so.
if (criterion) { if (criterion) {
criterion.setFromEncodedCriterion(encodedCriterion); criterion.setFromEncodedCriterion(encodedCriterion);
@ -248,8 +249,41 @@ export class ListFilterModel {
this.configureFromDecodedParams(decoded); this.configureFromDecodedParams(decoded);
} }
public configureFromJSON(json: string) { public configureFromSavedFilter(savedFilter: SavedFilterDataFragment) {
this.configureFromDecodedParams(JSON.parse(json)); const {
find_filter: findFilter,
object_filter: objectFilter,
ui_options: uiOptions,
} = savedFilter;
this.itemsPerPage = findFilter?.per_page ?? this.itemsPerPage;
this.sortBy = findFilter?.sort ?? this.sortBy;
// parse the random seed if provided
const match = this.sortBy?.match(/^random_(\d+)$/);
if (match) {
this.sortBy = "random";
this.randomSeed = Number.parseInt(match[1], 10);
}
this.sortDirection =
(findFilter?.direction as SortDirectionEnum) ?? this.sortDirection;
this.searchTerm = findFilter?.q ?? this.searchTerm;
this.displayMode = uiOptions?.display_mode ?? this.displayMode;
this.zoomIndex = uiOptions?.zoom_index ?? this.zoomIndex;
this.currentPage = 1;
this.criteria = [];
if (objectFilter) {
Object.keys(objectFilter).forEach((key) => {
const criterion = makeCriteria(this.mode, key as CriterionType);
// it's possible that we have unsupported criteria. Just skip if so.
if (criterion) {
criterion.setFromEncodedCriterion(objectFilter[key]);
this.criteria.push(criterion);
}
});
}
} }
private setRandomSeed() { private setRandomSeed() {
@ -405,4 +439,22 @@ export class ListFilterModel {
return output; return output;
} }
public makeSavedFindFilter() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const output: Record<string, any> = {};
this.criteria.forEach((criterion) => {
criterion.toSavedFilter(output);
});
return output;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public makeUIOptions(): Record<string, any> {
return {
display_mode: this.displayMode,
zoom_index: this.zoomIndex,
};
}
} }

View file

@ -45,11 +45,7 @@ const criterionOptions = [
createStringCriterionOption("title"), createStringCriterionOption("title"),
createStringCriterionOption("details"), createStringCriterionOption("details"),
createPathCriterionOption("path"), createPathCriterionOption("path"),
createStringCriterionOption( createStringCriterionOption("checksum", "media_info.checksum"),
"galleryChecksum",
"media_info.checksum",
"checksum"
),
new NullNumberCriterionOption("rating", "rating100"), new NullNumberCriterionOption("rating", "rating100"),
OrganizedCriterionOption, OrganizedCriterionOption,
AverageResolutionCriterionOption, AverageResolutionCriterionOption,

View file

@ -94,7 +94,7 @@ const criterionOptions = [
createMandatoryNumberCriterionOption("gallery_count"), createMandatoryNumberCriterionOption("gallery_count"),
createMandatoryNumberCriterionOption("o_counter"), createMandatoryNumberCriterionOption("o_counter"),
createBooleanCriterionOption("ignore_auto_tag"), createBooleanCriterionOption("ignore_auto_tag"),
new NumberCriterionOption("height", "height_cm", "height_cm"), new NumberCriterionOption("height", "height_cm"),
...numberCriteria.map((c) => createNumberCriterionOption(c)), ...numberCriteria.map((c) => createNumberCriterionOption(c)),
...stringCriteria.map((c) => createStringCriterionOption(c)), ...stringCriteria.map((c) => createStringCriterionOption(c)),
createDateCriterionOption("birthdate"), createDateCriterionOption("birthdate"),

View file

@ -59,16 +59,12 @@ const displayModeOptions = [
const criterionOptions = [ const criterionOptions = [
createStringCriterionOption("title"), createStringCriterionOption("title"),
createStringCriterionOption("scene_code"), createStringCriterionOption("code", "scene_code"),
createPathCriterionOption("path"), createPathCriterionOption("path"),
createStringCriterionOption("details"), createStringCriterionOption("details"),
createStringCriterionOption("director"), createStringCriterionOption("director"),
createMandatoryStringCriterionOption("oshash", "media_info.hash"), createMandatoryStringCriterionOption("oshash", "media_info.hash"),
createStringCriterionOption( createStringCriterionOption("checksum", "media_info.checksum"),
"sceneChecksum",
"media_info.checksum",
"checksum"
),
PhashCriterionOption, PhashCriterionOption,
DuplicatedCriterionOption, DuplicatedCriterionOption,
OrganizedCriterionOption, OrganizedCriterionOption,

View file

@ -53,17 +53,9 @@ const criterionOptions = [
createMandatoryNumberCriterionOption("performer_count"), createMandatoryNumberCriterionOption("performer_count"),
createMandatoryNumberCriterionOption("marker_count"), createMandatoryNumberCriterionOption("marker_count"),
ParentTagsCriterionOption, ParentTagsCriterionOption,
new MandatoryNumberCriterionOption( new MandatoryNumberCriterionOption("parent_tag_count", "parent_count"),
"parent_tag_count",
"parent_tag_count",
"parent_count"
),
ChildTagsCriterionOption, ChildTagsCriterionOption,
new MandatoryNumberCriterionOption( new MandatoryNumberCriterionOption("sub_tag_count", "child_count"),
"sub_tag_count",
"child_tag_count",
"child_count"
),
createMandatoryTimestampCriterionOption("created_at"), createMandatoryTimestampCriterionOption("created_at"),
createMandatoryTimestampCriterionOption("updated_at"), createMandatoryTimestampCriterionOption("updated_at"),
]; ];

View file

@ -112,20 +112,12 @@ export type CriterionType =
| "video_codec" | "video_codec"
| "audio_codec" | "audio_codec"
| "duration" | "duration"
| "favorite" | "filter_favorites"
| "hasMarkers" | "has_markers"
| "sceneIsMissing" | "is_missing"
| "imageIsMissing"
| "performerIsMissing"
| "galleryIsMissing"
| "tagIsMissing"
| "studioIsMissing"
| "movieIsMissing"
| "tags" | "tags"
| "sceneTags" | "scene_tags"
| "performerTags" | "performer_tags"
| "parentTags"
| "childTags"
| "tag_count" | "tag_count"
| "performers" | "performers"
| "studios" | "studios"
@ -149,7 +141,8 @@ export type CriterionType =
| "piercings" | "piercings"
| "aliases" | "aliases"
| "gender" | "gender"
| "parent_studios" | "parents"
| "children"
| "scene_count" | "scene_count"
| "marker_count" | "marker_count"
| "image_count" | "image_count"
@ -169,13 +162,11 @@ export type CriterionType =
| "title" | "title"
| "oshash" | "oshash"
| "checksum" | "checksum"
| "sceneChecksum" | "phash_distance"
| "galleryChecksum"
| "phash"
| "director" | "director"
| "synopsis" | "synopsis"
| "parent_tag_count" | "parent_count"
| "child_tag_count" | "child_count"
| "performer_favorite" | "performer_favorite"
| "performer_age" | "performer_age"
| "duplicated" | "duplicated"
@ -191,6 +182,6 @@ export type CriterionType =
| "scene_created_at" | "scene_created_at"
| "scene_updated_at" | "scene_updated_at"
| "description" | "description"
| "scene_code" | "code"
| "disambiguation" | "disambiguation"
| "hasChapters"; | "has_chapters";