diff --git a/gqlgen.yml b/gqlgen.yml index 2439ebc7c..d10316572 100644 --- a/gqlgen.yml +++ b/gqlgen.yml @@ -118,4 +118,6 @@ models: model: github.com/stashapp/stash/internal/identify.MetadataOptions ScraperSourceInput: model: github.com/stashapp/stash/pkg/scraper.Source + SavedFindFilterType: + model: github.com/stashapp/stash/pkg/models.FindFilterType diff --git a/graphql/documents/data/filter.graphql b/graphql/documents/data/filter.graphql index 4c6236668..1ced5a441 100644 --- a/graphql/documents/data/filter.graphql +++ b/graphql/documents/data/filter.graphql @@ -2,5 +2,13 @@ fragment SavedFilterData on SavedFilter { id mode name - filter + find_filter { + q + page + per_page + sort + direction + } + object_filter + ui_options } diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index 13165fba8..f0b190264 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -12,6 +12,17 @@ input FindFilterType { 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 { "144p" VERY_LOW @@ -604,6 +615,13 @@ type SavedFilter { name: String! "JSON-encoded 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 { @@ -611,8 +629,10 @@ input SaveFilterInput { id: ID mode: FilterMode! name: String! - "JSON-encoded filter string" - filter: String! + find_filter: FindFilterType + object_filter: Map + # generic map for ui options + ui_options: Map } input DestroyFilterInput { @@ -621,6 +641,9 @@ input DestroyFilterInput { input SetDefaultFilterInput { mode: FilterMode! - "JSON-encoded filter string - null to clear" - filter: String + "null to clear" + find_filter: FindFilterType + object_filter: Map + # generic map for ui options + ui_options: Map } diff --git a/internal/api/resolver.go b/internal/api/resolver.go index ff74a4456..ea0bd256c 100644 --- a/internal/api/resolver.go +++ b/internal/api/resolver.go @@ -82,6 +82,9 @@ func (r *Resolver) Subscription() SubscriptionResolver { func (r *Resolver) Tag() TagResolver { return &tagResolver{r} } +func (r *Resolver) SavedFilter() SavedFilterResolver { + return &savedFilterResolver{r} +} type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver } @@ -96,6 +99,7 @@ type imageResolver struct{ *Resolver } type studioResolver struct{ *Resolver } type movieResolver struct{ *Resolver } type tagResolver struct{ *Resolver } +type savedFilterResolver struct{ *Resolver } func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) error) error { return txn.WithTxn(ctx, r.txnManager, fn) diff --git a/internal/api/resolver_model_saved_filter.go b/internal/api/resolver_model_saved_filter.go new file mode 100644 index 000000000..5e1131ab3 --- /dev/null +++ b/internal/api/resolver_model_saved_filter.go @@ -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 +} diff --git a/internal/api/resolver_mutation_saved_filter.go b/internal/api/resolver_mutation_saved_filter.go index a0514546c..890622270 100644 --- a/internal/api/resolver_mutation_saved_filter.go +++ b/internal/api/resolver_mutation_saved_filter.go @@ -14,12 +14,6 @@ func (r *mutationResolver) SaveFilter(ctx context.Context, input SaveFilterInput return nil, errors.New("name must be non-empty") } - newFilter := models.SavedFilter{ - Mode: input.Mode, - Name: input.Name, - Filter: input.Filter, - } - var id *int if input.ID != nil { 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 { qb := r.repository.SavedFilter - if id == nil { - err = qb.Create(ctx, &newFilter) - } else { - newFilter.ID = *id - err = qb.Update(ctx, &newFilter) + f := models.SavedFilter{ + Mode: input.Mode, + Name: input.Name, + FindFilter: input.FindFilter, + 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 }); err != nil { return nil, err } - ret = &newFilter 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 { qb := r.repository.SavedFilter - if input.Filter == nil { + if input.FindFilter == nil && input.ObjectFilter == nil && input.UIOptions == nil { // clearing def, err := qb.FindDefault(ctx, input.Mode) if err != nil { @@ -79,12 +83,12 @@ func (r *mutationResolver) SetDefaultFilter(ctx context.Context, input SetDefaul return nil } - err := qb.SetDefault(ctx, &models.SavedFilter{ - Mode: input.Mode, - Filter: *input.Filter, + return qb.SetDefault(ctx, &models.SavedFilter{ + Mode: input.Mode, + FindFilter: input.FindFilter, + ObjectFilter: input.ObjectFilter, + UIOptions: input.UIOptions, }) - - return err }); err != nil { return false, err } diff --git a/internal/autotag/integration_test.go b/internal/autotag/integration_test.go index 1c7b0ee2d..eb4b0a9ad 100644 --- a/internal/autotag/integration_test.go +++ b/internal/autotag/integration_test.go @@ -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) { // create the studio studio := models.Studio{ - Name: name, + Name: name, } err := qb.Create(ctx, &studio) diff --git a/pkg/models/model_saved_filter.go b/pkg/models/model_saved_filter.go index 23f06e260..51c50be51 100644 --- a/pkg/models/model_saved_filter.go +++ b/pkg/models/model_saved_filter.go @@ -60,11 +60,12 @@ func (e FilterMode) MarshalGQL(w io.Writer) { } type SavedFilter struct { - ID int `json:"id"` - Mode FilterMode `json:"mode"` - Name string `json:"name"` - // JSON-encoded filter string - Filter string `json:"filter"` + ID int `db:"id" json:"id"` + Mode FilterMode `db:"mode" json:"mode"` + Name string `db:"name" json:"name"` + FindFilter *FindFilterType `json:"find_filter"` + ObjectFilter map[string]interface{} `json:"object_filter"` + UIOptions map[string]interface{} `json:"ui_options"` } type SavedFilters []*SavedFilter diff --git a/pkg/sqlite/database.go b/pkg/sqlite/database.go index 40a2555fd..cd87a887c 100644 --- a/pkg/sqlite/database.go +++ b/pkg/sqlite/database.go @@ -33,7 +33,7 @@ const ( dbConnTimeout = 30 ) -var appSchemaVersion uint = 48 +var appSchemaVersion uint = 49 //go:embed migrations/*.sql var migrationsBox embed.FS @@ -74,10 +74,10 @@ type Database struct { Scene *SceneStore SceneMarker *SceneMarkerStore Performer *PerformerStore + SavedFilter *SavedFilterStore Studio *StudioStore Tag *TagStore Movie *MovieStore - SavedFilter *SavedFilterStore db *sqlx.DB dbPath string diff --git a/pkg/sqlite/migrations/49_postmigrate.go b/pkg/sqlite/migrations/49_postmigrate.go new file mode 100644 index 000000000..d500d4707 --- /dev/null +++ b/pkg/sqlite/migrations/49_postmigrate.go @@ -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) +} diff --git a/pkg/sqlite/migrations/49_saved_filter_refactor.up.sql b/pkg/sqlite/migrations/49_saved_filter_refactor.up.sql new file mode 100644 index 000000000..c769a9e4b --- /dev/null +++ b/pkg/sqlite/migrations/49_saved_filter_refactor.up.sql @@ -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; diff --git a/pkg/sqlite/movies_test.go b/pkg/sqlite/movies_test.go index ed0ef7242..9b9615fbd 100644 --- a/pkg/sqlite/movies_test.go +++ b/pkg/sqlite/movies_test.go @@ -291,7 +291,7 @@ func TestMovieUpdateFrontImage(t *testing.T) { // create movie to test against const name = "TestMovieUpdateMovieImages" movie := models.Movie{ - Name: name, + Name: name, } err := qb.Create(ctx, &movie) if err != nil { @@ -311,7 +311,7 @@ func TestMovieUpdateBackImage(t *testing.T) { // create movie to test against const name = "TestMovieUpdateMovieImages" movie := models.Movie{ - Name: name, + Name: name, } err := qb.Create(ctx, &movie) if err != nil { diff --git a/pkg/sqlite/saved_filter.go b/pkg/sqlite/saved_filter.go index f4b55fe72..6b92b7657 100644 --- a/pkg/sqlite/saved_filter.go +++ b/pkg/sqlite/saved_filter.go @@ -3,6 +3,7 @@ package sqlite import ( "context" "database/sql" + "encoding/json" "errors" "fmt" @@ -10,6 +11,7 @@ import ( "github.com/doug-martin/goqu/v9/exp" "github.com/jmoiron/sqlx" + "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/sliceutil/intslice" ) @@ -20,25 +22,67 @@ const ( ) type savedFilterRow struct { - ID int `db:"id" goqu:"skipinsert"` - Mode string `db:"mode"` - Name string `db:"name"` - Filter string `db:"filter"` + ID int `db:"id" goqu:"skipinsert"` + Mode models.FilterMode `db:"mode"` + Name string `db:"name"` + 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) { r.ID = o.ID - r.Mode = string(o.Mode) + r.Mode = o.Mode 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 { ret := &models.SavedFilter{ - ID: r.ID, - Name: r.Name, - Mode: models.FilterMode(r.Mode), - Filter: r.Filter, + ID: r.ID, + Mode: r.Mode, + Name: r.Name, + } + + // 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 @@ -46,7 +90,6 @@ func (r *savedFilterRow) resolve() *models.SavedFilter { type SavedFilterStore struct { repository - tableMgr *table } @@ -77,7 +120,7 @@ func (qb *SavedFilterStore) Create(ctx context.Context, newObject *models.SavedF return err } - updated, err := qb.find(ctx, id) + updated, err := qb.Find(ctx, id) if err != nil { 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 } -// returns nil, sql.ErrNoRows if not found func (qb *SavedFilterStore) get(ctx context.Context, q *goqu.SelectDataset) (*models.SavedFilter, error) { ret, err := qb.getMany(ctx, q) if err != nil { diff --git a/pkg/sqlite/saved_filter_test.go b/pkg/sqlite/saved_filter_test.go index 0a6e32a1c..aa98121fd 100644 --- a/pkg/sqlite/saved_filter_test.go +++ b/pkg/sqlite/saved_filter_test.go @@ -42,15 +42,35 @@ func TestSavedFilterFindByMode(t *testing.T) { func TestSavedFilterDestroy(t *testing.T) { 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 // create the saved filter to destroy withTxn(func(ctx context.Context) error { newFilter := models.SavedFilter{ - Name: filterName, - Mode: models.FilterModeScenes, - Filter: testFilter, + Name: filterName, + Mode: models.FilterModeScenes, + FindFilter: &findFilter, + ObjectFilter: objectFilter, + UIOptions: uiOptions, } err := db.SavedFilter.Create(ctx, &newFilter) @@ -88,12 +108,32 @@ func TestSavedFilterFindDefault(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 { err := db.SavedFilter.SetDefault(ctx, &models.SavedFilter{ - Mode: models.FilterModeMovies, - Filter: newFilter, + Mode: models.FilterModeMovies, + FindFilter: &findFilter, + ObjectFilter: objectFilter, + UIOptions: uiOptions, }) return err @@ -104,7 +144,7 @@ func TestSavedFilterSetDefault(t *testing.T) { def, err := db.SavedFilter.FindDefault(ctx, models.FilterModeMovies) if err == nil { defID = def.ID - assert.Equal(t, newFilter, def.Filter) + assert.Equal(t, &findFilter, def.FindFilter) } return err diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index c57f272c7..e89c9302c 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -1714,10 +1714,29 @@ func getSavedFilterName(index int) string { func createSavedFilters(ctx context.Context, qb models.SavedFilterReaderWriter, n int) error { 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{ - Mode: getSavedFilterMode(i), - Name: getSavedFilterName(i), - Filter: getPrefixedStringValue("savedFilter", i, "Filter"), + Mode: getSavedFilterMode(i), + Name: getSavedFilterName(i), + FindFilter: &findFilter, + ObjectFilter: map[string]interface{}{ + "test": "object", + }, + UIOptions: map[string]interface{}{ + "display_mode": 1, + "zoom_index": 1, + }, } err := qb.Create(ctx, &savedFilter) diff --git a/ui/v2.5/.eslintrc.json b/ui/v2.5/.eslintrc.json index f37f8028c..edce43551 100644 --- a/ui/v2.5/.eslintrc.json +++ b/ui/v2.5/.eslintrc.json @@ -74,7 +74,7 @@ "prefer-destructuring": ["error", { "object": true, "array": false }], "@typescript-eslint/no-use-before-define": [ "error", - { "functions": false, "classes": true } + { "functions": false, "classes": false } ], "no-nested-ternary": "off" } diff --git a/ui/v2.5/src/components/FrontPage/Control.tsx b/ui/v2.5/src/components/FrontPage/Control.tsx index c655d9c3e..3cb2cf021 100644 --- a/ui/v2.5/src/components/FrontPage/Control.tsx +++ b/ui/v2.5/src/components/FrontPage/Control.tsx @@ -105,11 +105,11 @@ const SavedFilterResults: React.FC = ({ const filter = useMemo(() => { if (!data?.findSavedFilter) return; - const { mode, filter: filterJSON } = data.findSavedFilter; + const { mode } = data.findSavedFilter; const ret = new ListFilterModel(mode, config); ret.currentPage = 1; - ret.configureFromJSON(filterJSON); + ret.configureFromSavedFilter(data.findSavedFilter); ret.randomSeed = -1; return ret; }, [data?.findSavedFilter, config]); diff --git a/ui/v2.5/src/components/List/EditFilterDialog.tsx b/ui/v2.5/src/components/List/EditFilterDialog.tsx index 581fd31fb..7ddb7fbdb 100644 --- a/ui/v2.5/src/components/List/EditFilterDialog.tsx +++ b/ui/v2.5/src/components/List/EditFilterDialog.tsx @@ -270,11 +270,11 @@ export const EditFilterDialog: React.FC = ({ if (existing) { setCriterion(existing); } else { - const newCriterion = makeCriteria(configuration, option.type); + const newCriterion = makeCriteria(filter.mode, option.type); setCriterion(newCriterion); } }, - [criteria, configuration] + [filter.mode, criteria] ); const ui = (configuration?.ui ?? {}) as IUIConfig; diff --git a/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx b/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx index bb2625838..abfb74ee7 100644 --- a/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx @@ -13,20 +13,19 @@ interface IHierarchicalLabelValueFilterProps { export const HierarchicalLabelValueFilter: React.FC< IHierarchicalLabelValueFilterProps > = ({ criterion, onValueChanged }) => { + const { criterionOption } = criterion; + const { type, inputType } = criterionOption; + const intl = useIntl(); if ( - criterion.criterionOption.type !== "performers" && - criterion.criterionOption.type !== "studios" && - criterion.criterionOption.type !== "parent_studios" && - criterion.criterionOption.type !== "tags" && - criterion.criterionOption.type !== "sceneTags" && - criterion.criterionOption.type !== "performerTags" && - criterion.criterionOption.type !== "parentTags" && - criterion.criterionOption.type !== "childTags" && - criterion.criterionOption.type !== "movies" - ) + inputType !== "studios" && + inputType !== "tags" && + inputType !== "scene_tags" && + inputType !== "performer_tags" + ) { return null; + } const messages = defineMessages({ studio_depth: { @@ -51,10 +50,10 @@ export const HierarchicalLabelValueFilter: React.FC< } function criterionOptionTypeToIncludeID(): string { - if (criterion.criterionOption.type === "studios") { + if (inputType === "studios") { return "include-sub-studios"; } - if (criterion.criterionOption.type === "childTags") { + if (type === "children") { return "include-parent-tags"; } return "include-sub-tags"; @@ -62,9 +61,9 @@ export const HierarchicalLabelValueFilter: React.FC< function criterionOptionTypeToIncludeUIString(): MessageDescriptor { const optionType = - criterion.criterionOption.type === "studios" + inputType === "studios" ? "include_sub_studios" - : criterion.criterionOption.type === "childTags" + : type === "children" ? "include_parent_tags" : "include_sub_tags"; return { @@ -76,7 +75,7 @@ export const HierarchicalLabelValueFilter: React.FC< <> labeled.id)} diff --git a/ui/v2.5/src/components/List/Filters/LabeledIdFilter.tsx b/ui/v2.5/src/components/List/Filters/LabeledIdFilter.tsx index f06e5c21b..13824e08b 100644 --- a/ui/v2.5/src/components/List/Filters/LabeledIdFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/LabeledIdFilter.tsx @@ -13,18 +13,19 @@ export const LabeledIdFilter: React.FC = ({ criterion, onValueChanged, }) => { + const { criterionOption } = criterion; + const { inputType } = criterionOption; + if ( - criterion.criterionOption.type !== "performers" && - criterion.criterionOption.type !== "studios" && - criterion.criterionOption.type !== "parent_studios" && - criterion.criterionOption.type !== "tags" && - criterion.criterionOption.type !== "sceneTags" && - criterion.criterionOption.type !== "performerTags" && - criterion.criterionOption.type !== "parentTags" && - criterion.criterionOption.type !== "childTags" && - criterion.criterionOption.type !== "movies" - ) + inputType !== "performers" && + inputType !== "studios" && + inputType !== "scene_tags" && + inputType !== "performer_tags" && + inputType !== "tags" && + inputType !== "movies" + ) { return null; + } function onSelectionChanged(items: SelectObject[]) { onValueChanged( @@ -38,7 +39,7 @@ export const LabeledIdFilter: React.FC = ({ return ( labeled.id)} diff --git a/ui/v2.5/src/components/List/Filters/SelectableFilter.tsx b/ui/v2.5/src/components/List/Filters/SelectableFilter.tsx index 2c13eb57e..08f5a2514 100644 --- a/ui/v2.5/src/components/List/Filters/SelectableFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/SelectableFilter.tsx @@ -320,7 +320,7 @@ export const HierarchicalObjectsFilter = < if (criterion.criterionOption.type === "studios") { return "include-sub-studios"; } - if (criterion.criterionOption.type === "childTags") { + if (criterion.criterionOption.type === "children") { return "include-parent-tags"; } return "include-sub-tags"; @@ -330,7 +330,7 @@ export const HierarchicalObjectsFilter = < const optionType = criterion.criterionOption.type === "studios" ? "include_sub_studios" - : criterion.criterionOption.type === "childTags" + : criterion.criterionOption.type === "children" ? "include_parent_tags" : "include_sub_tags"; return { diff --git a/ui/v2.5/src/components/List/ItemList.tsx b/ui/v2.5/src/components/List/ItemList.tsx index e8cabe7ce..8b3aa5898 100644 --- a/ui/v2.5/src/components/List/ItemList.tsx +++ b/ui/v2.5/src/components/List/ItemList.tsx @@ -619,8 +619,8 @@ export function makeItemList({ if (defaultFilter?.findDefaultFilter) { newFilter.currentPage = 1; try { - newFilter.configureFromJSON( - defaultFilter.findDefaultFilter.filter + newFilter.configureFromSavedFilter( + defaultFilter.findDefaultFilter ); } catch (err) { console.log(err); diff --git a/ui/v2.5/src/components/List/SavedFilterList.tsx b/ui/v2.5/src/components/List/SavedFilterList.tsx index 8a5da0473..caa1277d6 100644 --- a/ui/v2.5/src/components/List/SavedFilterList.tsx +++ b/ui/v2.5/src/components/List/SavedFilterList.tsx @@ -75,7 +75,9 @@ export const SavedFilterList: React.FC = ({ id, mode: filter.mode, 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 = ({ variables: { input: { 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 = ({ newFilter.currentPage = 1; // #1795 - reset search term if not present in saved filter newFilter.searchTerm = ""; - newFilter.configureFromJSON(f.filter); + newFilter.configureFromSavedFilter(f); // #1507 - reset random seed when loaded newFilter.randomSeed = -1; diff --git a/ui/v2.5/src/components/Shared/Select.tsx b/ui/v2.5/src/components/Shared/Select.tsx index f7c264609..495df4f5e 100644 --- a/ui/v2.5/src/components/Shared/Select.tsx +++ b/ui/v2.5/src/components/Shared/Select.tsx @@ -44,12 +44,9 @@ interface ITypeProps { type?: | "performers" | "studios" - | "parent_studios" | "tags" - | "sceneTags" - | "performerTags" - | "parentTags" - | "childTags" + | "scene_tags" + | "performer_tags" | "movies"; } interface IFilterProps { @@ -865,7 +862,7 @@ export const TagSelect: React.FC< export const FilterSelect: React.FC = (props) => { if (props.type === "performers") { return ; - } else if (props.type === "studios" || props.type === "parent_studios") { + } else if (props.type === "studios") { return ; } else if (props.type === "movies") { return ; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioChildrenPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioChildrenPanel.tsx index bd5a21a36..134dabe69 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioChildrenPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioChildrenPanel.tsx @@ -17,7 +17,7 @@ export const StudioChildrenPanel: React.FC = ({ const studioValue = { id: studio.id!, label: studio.name! }; // if studio is already present, then we modify it, otherwise add let parentStudioCriterion = filter.criteria.find((c) => { - return c.criterionOption.type === "parent_studios"; + return c.criterionOption.type === "parents"; }) as ParentStudiosCriterion; if ( diff --git a/ui/v2.5/src/locales/da-DK.json b/ui/v2.5/src/locales/da-DK.json index b83b8b480..d3baeaeb4 100644 --- a/ui/v2.5/src/locales/da-DK.json +++ b/ui/v2.5/src/locales/da-DK.json @@ -877,7 +877,7 @@ "path": "Sti", "perceptual_similarity": "Perceptuel lighed (phash)", "performer": "Kunstner", - "performerTags": "Kunstner Tags", + "performer_tags": "Kunstner Tags", "performer_age": "kunstnere Alder", "performer_count": "Kunstner Antal", "performer_favorite": "Foretrukken optrædende", @@ -927,7 +927,7 @@ "resolution": "Opløsning", "scene": "Scene", "sceneTagger": "Scenetagger", - "sceneTags": "Scene-etiketter", + "scene_tags": "Scene-etiketter", "scene_count": "Scene antal", "scene_id": "Scene-id", "scenes": "Scener", diff --git a/ui/v2.5/src/locales/de-DE.json b/ui/v2.5/src/locales/de-DE.json index de6603834..5792d627f 100644 --- a/ui/v2.5/src/locales/de-DE.json +++ b/ui/v2.5/src/locales/de-DE.json @@ -1033,7 +1033,7 @@ "penis_length_cm": "Penislänge (cm)", "perceptual_similarity": "Wahrnehmungsähnlichkeit (phash)", "performer": "Darsteller", - "performerTags": "Darsteller-Tags", + "performer_tags": "Darsteller-Tags", "performer_age": "Alter der Darsteller", "performer_count": "Darstelleranzahl", "performer_favorite": "Darsteller favorisiert", @@ -1088,7 +1088,7 @@ "resume_time": "Zeit fortsetzen", "scene": "Szene", "sceneTagger": "Szenen-Tagger", - "sceneTags": "Szenen-Tags", + "scene_tags": "Szenen-Tags", "scene_code": "Studio Code", "scene_count": "Szenenanzahl", "scene_created_at": "Szene angelegt am", diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index dafaee432..3747b969d 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -1075,7 +1075,7 @@ "penis_length_cm": "Penis Length (cm)", "perceptual_similarity": "Perceptual Similarity (phash)", "performer": "Performer", - "performerTags": "Performer Tags", + "performer_tags": "Performer Tags", "performer_age": "Performer Age", "performer_count": "Performer Count", "performer_favorite": "Performer Favourited", @@ -1130,7 +1130,7 @@ "resume_time": "Resume Time", "scene": "Scene", "sceneTagger": "Scene Tagger", - "sceneTags": "Scene Tags", + "scene_tags": "Scene Tags", "scene_code": "Studio Code", "scene_count": "Scene Count", "scene_created_at": "Scene Created At", @@ -1355,4 +1355,4 @@ "weight_kg": "Weight (kg)", "years_old": "years old", "zip_file_count": "Zip File Count" -} \ No newline at end of file +} diff --git a/ui/v2.5/src/locales/es-ES.json b/ui/v2.5/src/locales/es-ES.json index 50f88ceeb..f1de915a8 100644 --- a/ui/v2.5/src/locales/es-ES.json +++ b/ui/v2.5/src/locales/es-ES.json @@ -929,7 +929,7 @@ "path": "Ruta", "perceptual_similarity": "Similaridad perceptiva (phash)", "performer": "Actriz/Actor", - "performerTags": "Etiquetas de actriz/actor", + "performer_tags": "Etiquetas de actriz/actor", "performer_age": "Edad de la actriz/actor", "performer_count": "Número de actrices/actores", "performer_favorite": "Actriz/actor favorita/o", @@ -977,7 +977,7 @@ "resolution": "Resolución", "scene": "Escena", "sceneTagger": "Etiquetador de escenas", - "sceneTags": "Etiquetas de escena", + "scene_tags": "Etiquetas de escena", "scene_count": "Número de escenas", "scene_id": "Indentificador de escena", "scenes": "Escenas", diff --git a/ui/v2.5/src/locales/et-EE.json b/ui/v2.5/src/locales/et-EE.json index 4754f5fd9..b4794e1be 100644 --- a/ui/v2.5/src/locales/et-EE.json +++ b/ui/v2.5/src/locales/et-EE.json @@ -1012,7 +1012,7 @@ "path": "Failitee", "perceptual_similarity": "Tajutav Sarnasus (phash)", "performer": "Näitleja", - "performerTags": "Näitleja Sildid", + "performer_tags": "Näitleja Sildid", "performer_age": "Näitleja Vanus", "performer_count": "Näitlejate Arv", "performer_favorite": "Lemmiknäitleja", @@ -1067,7 +1067,7 @@ "resume_time": "Jätkamisaeg", "scene": "Stseen", "sceneTagger": "Stseeni Sildistaja", - "sceneTags": "Stseeni Sildid", + "scene_tags": "Stseeni Sildid", "scene_code": "Stuudio Kood", "scene_count": "Stseenide Arv", "scene_created_at": "Stseen Loodud", diff --git a/ui/v2.5/src/locales/fi-FI.json b/ui/v2.5/src/locales/fi-FI.json index 86efb367c..20e03e584 100644 --- a/ui/v2.5/src/locales/fi-FI.json +++ b/ui/v2.5/src/locales/fi-FI.json @@ -870,7 +870,7 @@ "path": "Polku", "perceptual_similarity": "Aistinvarainen samankaltaisuus (phash)", "performer": "Esiintyjä", - "performerTags": "Esiintyjien tunnisteet", + "performer_tags": "Esiintyjien tunnisteet", "performer_age": "Esiintyjän ikä", "performer_count": "Esiintyjien määrä", "performer_favorite": "Esiintyjä suosikeissa", @@ -914,7 +914,7 @@ "resolution": "Resoluutio", "scene": "Kohtaus", "sceneTagger": "Kohtauksien tunnistetila", - "sceneTags": "Kohtauksen tunnisteet", + "scene_tags": "Kohtauksen tunnisteet", "scene_code": "Studiokoodi", "scene_count": "Kohtauksien määrä", "scene_created_at": "Kohtaus luotu", diff --git a/ui/v2.5/src/locales/fr-FR.json b/ui/v2.5/src/locales/fr-FR.json index 6f5155d9e..bb1e9674f 100644 --- a/ui/v2.5/src/locales/fr-FR.json +++ b/ui/v2.5/src/locales/fr-FR.json @@ -1072,7 +1072,7 @@ "penis_length_cm": "Longueur du pénis (cm)", "perceptual_similarity": "Similitude perceptuelle (empreinte)", "performer": "Performeurs", - "performerTags": "Étiquettes de performeur", + "performer_tags": "Étiquettes de performeur", "performer_age": "Âge du performeur", "performer_count": "Nombre de performeurs", "performer_favorite": "Performeur favori", @@ -1127,7 +1127,7 @@ "resume_time": "Reprendre le temps", "scene": "Scène", "sceneTagger": "Étiqueteuse de scènes", - "sceneTags": "Étiquettes de la scène", + "scene_tags": "Étiquettes de la scène", "scene_code": "Code studio", "scene_count": "Nombre de scènes", "scene_created_at": "Scène créée le", diff --git a/ui/v2.5/src/locales/hu-HU.json b/ui/v2.5/src/locales/hu-HU.json index 7fff63874..59bfe7e76 100644 --- a/ui/v2.5/src/locales/hu-HU.json +++ b/ui/v2.5/src/locales/hu-HU.json @@ -437,7 +437,7 @@ "parent_tags": "Szülő-címkék", "path": "Elérési Út", "performer": "Szereplő", - "performerTags": "Szereplő Címkék", + "performer_tags": "Szereplő Címkék", "performer_age": "Szereplő Kora", "performer_count": "Szereplők Száma", "performer_favorite": "Szereplő Kedvencek Közt", @@ -460,7 +460,7 @@ "resolution": "Felbontás", "scene": "Jelenet", "sceneTagger": "Jelenetcímkéző", - "sceneTags": "Jelenetcímkék", + "scene_tags": "Jelenetcímkék", "scene_count": "Jelenetszám", "scene_id": "Jelenet ID", "scenes": "Jelenetek", diff --git a/ui/v2.5/src/locales/it-IT.json b/ui/v2.5/src/locales/it-IT.json index 6f75b5b2e..9b51beb61 100644 --- a/ui/v2.5/src/locales/it-IT.json +++ b/ui/v2.5/src/locales/it-IT.json @@ -986,7 +986,7 @@ "resume_time": "Tempo Continuazione", "scene": "Scena", "sceneTagger": "Tagger Scena", - "sceneTags": "Tag Scena", + "scene_tags": "Tag Scena", "scene_code": "Codice dello Studio", "scene_count": "Numero Scene", "scene_created_at": "Scena Creata Al", diff --git a/ui/v2.5/src/locales/ja-JP.json b/ui/v2.5/src/locales/ja-JP.json index 27c74a3d8..4dd4857f9 100644 --- a/ui/v2.5/src/locales/ja-JP.json +++ b/ui/v2.5/src/locales/ja-JP.json @@ -940,7 +940,7 @@ "path": "パス", "perceptual_similarity": "知覚的類似性 (phash)", "performer": "出演者", - "performerTags": "出演者タグ", + "performer_tags": "出演者タグ", "performer_age": "出演者の年齢", "performer_count": "出演者数", "performer_favorite": "出演者をお気に入り済み", @@ -995,7 +995,7 @@ "resume_time": "レジューム時間", "scene": "シーン", "sceneTagger": "シーン一括タグ付け", - "sceneTags": "シーンタグ", + "scene_tags": "シーンタグ", "scene_code": "スタジオコード", "scene_count": "シーン数", "scene_created_at": "シーンの作成日時", diff --git a/ui/v2.5/src/locales/ko-KR.json b/ui/v2.5/src/locales/ko-KR.json index a21a568d1..b1ba39815 100644 --- a/ui/v2.5/src/locales/ko-KR.json +++ b/ui/v2.5/src/locales/ko-KR.json @@ -1022,7 +1022,7 @@ "penis_length_cm": "자지 크기 (cm)", "perceptual_similarity": "유사도 (phash)", "performer": "배우", - "performerTags": "배우 태그", + "performer_tags": "배우 태그", "performer_age": "배우 나이", "performer_count": "배우 수", "performer_favorite": "즐겨찾기한 배우", @@ -1077,7 +1077,7 @@ "resume_time": "재시작 시간", "scene": "영상", "sceneTagger": "영상 태거", - "sceneTags": "영상 태그", + "scene_tags": "영상 태그", "scene_code": "스튜디오 코드", "scene_count": "영상 개수", "scene_created_at": "영상 생성 날짜", diff --git a/ui/v2.5/src/locales/nl-NL.json b/ui/v2.5/src/locales/nl-NL.json index f2c708e5d..c768a4096 100644 --- a/ui/v2.5/src/locales/nl-NL.json +++ b/ui/v2.5/src/locales/nl-NL.json @@ -821,7 +821,7 @@ "path": "Pad", "perceptual_similarity": "Perceptuele gelijkenis (phash)", "performer": "Performer", - "performerTags": "Peformer Labels", + "performer_tags": "Peformer Labels", "performer_age": "Leeftijd artiest", "performer_count": "Performer Aantal", "performer_favorite": "Artiest favoriet", @@ -855,7 +855,7 @@ "resolution": "Resolutie", "scene": "Scène", "sceneTagger": "Scene Labelen", - "sceneTags": "Scene Labels", + "scene_tags": "Scene Labels", "scene_count": "Scene Aantal", "scene_id": "Scene ID", "scenes": "Scènes", diff --git a/ui/v2.5/src/locales/pl-PL.json b/ui/v2.5/src/locales/pl-PL.json index 1c98751fa..ec6ef8718 100644 --- a/ui/v2.5/src/locales/pl-PL.json +++ b/ui/v2.5/src/locales/pl-PL.json @@ -1030,7 +1030,7 @@ "penis_length_cm": "Długość penisa (cm)", "perceptual_similarity": "Podobieństwo percepcyjne (phash)", "performer": "Aktor", - "performerTags": "Tagi aktorów", + "performer_tags": "Tagi aktorów", "performer_age": "Wiek aktora", "performer_count": "Liczba aktorów", "performer_favorite": "Ulubiony aktor", @@ -1085,7 +1085,7 @@ "resume_time": "Rozpocznij od", "scene": "Scena", "sceneTagger": "Otagowywacz scen", - "sceneTags": "Tagi sceny", + "scene_tags": "Tagi sceny", "scene_code": "Kod studia", "scene_count": "Liczba scen", "scene_created_at": "Scena utworzona", diff --git a/ui/v2.5/src/locales/pt-BR.json b/ui/v2.5/src/locales/pt-BR.json index be494d6ee..fec188b00 100644 --- a/ui/v2.5/src/locales/pt-BR.json +++ b/ui/v2.5/src/locales/pt-BR.json @@ -847,7 +847,7 @@ "path": "Caminho", "perceptual_similarity": "Semelhança Perceptiva (phash)", "performer": "Artista", - "performerTags": "Etiquetas de artistas", + "performer_tags": "Etiquetas de artistas", "performer_age": "Idade do Artista", "performer_count": "Contagem de artistas", "performer_favorite": "Artista Favoritado", @@ -898,7 +898,7 @@ "resolution": "Resolução", "scene": "Cena", "sceneTagger": "Etiquetador de cena", - "sceneTags": "Etiquetas da cena", + "scene_tags": "Etiquetas da cena", "scene_count": "Contagem de cena", "scene_id": "Cena ID", "scenes": "Cenas", diff --git a/ui/v2.5/src/locales/ru-RU.json b/ui/v2.5/src/locales/ru-RU.json index 650b6b1b5..14fa420fe 100644 --- a/ui/v2.5/src/locales/ru-RU.json +++ b/ui/v2.5/src/locales/ru-RU.json @@ -933,7 +933,7 @@ "path": "Путь", "perceptual_similarity": "Воспринимаемое сходство (phash)", "performer": "Актер", - "performerTags": "Теги актера", + "performer_tags": "Теги актера", "performer_age": "Возраст актера", "performer_count": "Количество актеров", "performer_favorite": "Участник добавлен в избранное", @@ -988,7 +988,7 @@ "resume_time": "Таймкод воспроизведения", "scene": "Сцена", "sceneTagger": "Пометка сцен тэгами", - "sceneTags": "Тэги сцен", + "scene_tags": "Тэги сцен", "scene_code": "Идентификатор сцены", "scene_count": "Количество сцен", "scene_created_at": "Сцена создана", diff --git a/ui/v2.5/src/locales/sv-SE.json b/ui/v2.5/src/locales/sv-SE.json index 8870bfed6..50bbc68b8 100644 --- a/ui/v2.5/src/locales/sv-SE.json +++ b/ui/v2.5/src/locales/sv-SE.json @@ -1033,7 +1033,7 @@ "penis_length_cm": "Penislängd (cm)", "perceptual_similarity": "Perceptuell likhet (phash)", "performer": "Stjärna", - "performerTags": "Stjärntagg", + "performer_tags": "Stjärntagg", "performer_age": "Ålder på stjärna", "performer_count": "Antal stjärnor", "performer_favorite": "Favoritiserad stjärna", @@ -1088,7 +1088,7 @@ "resume_time": "Återupptagningstid", "scene": "Scen", "sceneTagger": "Scentaggaren", - "sceneTags": "Scentaggar", + "scene_tags": "Scentaggar", "scene_code": "Studiokod", "scene_count": "Antal scener", "scene_created_at": "Scenen Skapad", diff --git a/ui/v2.5/src/locales/tr-TR.json b/ui/v2.5/src/locales/tr-TR.json index 476b9fe17..bf14087af 100644 --- a/ui/v2.5/src/locales/tr-TR.json +++ b/ui/v2.5/src/locales/tr-TR.json @@ -747,7 +747,7 @@ "part_of": "{parent} öğesinin parçası", "path": "Konum", "performer": "Oyuncu", - "performerTags": "Oyuncu Etiketleri", + "performer_tags": "Oyuncu Etiketleri", "performer_count": "Oyuncu Sayısı", "performer_image": "Oyuncu Resmi", "performers": "Oyuncular", @@ -758,7 +758,7 @@ "resolution": "Çözünürlük", "scene": "Sahne", "sceneTagger": "Sahne Etiketleyici", - "sceneTags": "Sahne Etiketleri", + "scene_tags": "Sahne Etiketleri", "scene_count": "Sahne Sayısı", "scene_id": "Sahne Kimliği (ID)", "scenes": "Sahneler", diff --git a/ui/v2.5/src/locales/zh-CN.json b/ui/v2.5/src/locales/zh-CN.json index 5e10459bc..f2c61f949 100644 --- a/ui/v2.5/src/locales/zh-CN.json +++ b/ui/v2.5/src/locales/zh-CN.json @@ -1018,7 +1018,7 @@ "path": "路径", "perceptual_similarity": "感知的类似程度(感知码)", "performer": "演员", - "performerTags": "演员标签", + "performer_tags": "演员标签", "performer_age": "演员年龄", "performer_count": "演员数量", "performer_favorite": "演员已收藏", @@ -1073,7 +1073,7 @@ "resume_time": "恢复时间", "scene": "短片", "sceneTagger": "短片标记器", - "sceneTags": "短片标记", + "scene_tags": "短片标记", "scene_code": "工作室代码", "scene_count": "短片数量", "scene_created_at": "短片建立在", diff --git a/ui/v2.5/src/locales/zh-TW.json b/ui/v2.5/src/locales/zh-TW.json index e8685a94f..ee446d32a 100644 --- a/ui/v2.5/src/locales/zh-TW.json +++ b/ui/v2.5/src/locales/zh-TW.json @@ -956,7 +956,7 @@ "path": "路徑", "perceptual_similarity": "感知相似度 (PHash)", "performer": "演員", - "performerTags": "演員標籤", + "performer_tags": "演員標籤", "performer_age": "演員年齡", "performer_count": "演員數量", "performer_favorite": "已收藏的演員", @@ -1011,7 +1011,7 @@ "resume_time": "恢復播放時間", "scene": "短片", "sceneTagger": "短片標籤器", - "sceneTags": "短片標籤", + "scene_tags": "短片標籤", "scene_code": "番號", "scene_count": "短片數量", "scene_created_at": "短片建立於", diff --git a/ui/v2.5/src/models/list-filter/criteria/captions.ts b/ui/v2.5/src/models/list-filter/criteria/captions.ts index 13c72dc77..2d0fbdb1d 100644 --- a/ui/v2.5/src/models/list-filter/criteria/captions.ts +++ b/ui/v2.5/src/models/list-filter/criteria/captions.ts @@ -10,7 +10,6 @@ class CaptionsCriterionOptionType extends CriterionOption { super({ messageID: value, type: value, - parameterName: value, modifierOptions: [ CriterionModifier.Includes, CriterionModifier.Excludes, @@ -19,6 +18,7 @@ class CaptionsCriterionOptionType extends CriterionOption { ], defaultModifier: CriterionModifier.Includes, options: languageStrings, + makeCriterion: () => new CaptionCriterion(), }); } } diff --git a/ui/v2.5/src/models/list-filter/criteria/circumcised.ts b/ui/v2.5/src/models/list-filter/criteria/circumcised.ts index c18aa1b01..dd8fbfbb1 100644 --- a/ui/v2.5/src/models/list-filter/criteria/circumcised.ts +++ b/ui/v2.5/src/models/list-filter/criteria/circumcised.ts @@ -16,6 +16,7 @@ export const CircumcisedCriterionOption = new CriterionOption({ CriterionModifier.IsNull, CriterionModifier.NotNull, ], + makeCriterion: () => new CircumcisedCriterion(), }); export class CircumcisedCriterion extends MultiStringCriterion { diff --git a/ui/v2.5/src/models/list-filter/criteria/country.ts b/ui/v2.5/src/models/list-filter/criteria/country.ts index 9c70b621d..2fa247900 100644 --- a/ui/v2.5/src/models/list-filter/criteria/country.ts +++ b/ui/v2.5/src/models/list-filter/criteria/country.ts @@ -3,11 +3,7 @@ import { CriterionModifier } from "src/core/generated-graphql"; import { getCountryByISO } from "src/utils/country"; import { StringCriterion, StringCriterionOption } from "./criterion"; -const countryCriterionOption = new StringCriterionOption( - "country", - "country", - "country" -); +const countryCriterionOption = new StringCriterionOption("country", "country"); export class CountryCriterion extends StringCriterion { constructor() { diff --git a/ui/v2.5/src/models/list-filter/criteria/criterion.ts b/ui/v2.5/src/models/list-filter/criteria/criterion.ts index acc6b3ff1..feee489b9 100644 --- a/ui/v2.5/src/models/list-filter/criteria/criterion.ts +++ b/ui/v2.5/src/models/list-filter/criteria/criterion.ts @@ -121,7 +121,7 @@ export abstract class Criterion { } 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() { @@ -154,7 +154,7 @@ export abstract class Criterion { // eslint-disable-next-line @typescript-eslint/no-explicit-any public apply(outputFilter: Record) { // 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 @@ -164,50 +164,68 @@ export abstract class Criterion { modifier: this.modifier, }; } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toSavedFilter(outputFilter: Record) { + 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 { messageID: string; type: CriterionType; inputType?: InputType; - parameterName?: string; modifierOptions?: CriterionModifier[]; defaultModifier?: CriterionModifier; options?: Option[]; + makeCriterion: () => Criterion; } export class CriterionOption { public readonly messageID: string; public readonly type: CriterionType; - public readonly parameterName: string; public readonly modifierOptions: CriterionModifier[]; public readonly defaultModifier: CriterionModifier; public readonly options: Option[] | undefined; public readonly inputType: InputType; + public readonly makeCriterionFn: ( + o: CriterionOption + ) => Criterion; constructor(options: ICriterionOptionsParams) { this.messageID = options.messageID; this.type = options.type; - this.parameterName = options.parameterName ?? options.type; this.modifierOptions = options.modifierOptions ?? []; this.defaultModifier = options.defaultModifier ?? CriterionModifier.Equals; this.options = options.options; this.inputType = options.inputType; + this.makeCriterionFn = options.makeCriterion; + } + + public makeCriterion() { + return this.makeCriterionFn(this); } } export class StringCriterionOption extends CriterionOption { - constructor( - messageID: string, - value: CriterionType, - parameterName?: string, - options?: Option[] - ) { + constructor(messageID: string, type: CriterionType, options?: Option[]) { super({ messageID, - type: value, - parameterName, + type, modifierOptions: [ CriterionModifier.Equals, CriterionModifier.NotEquals, @@ -221,20 +239,16 @@ export class StringCriterionOption extends CriterionOption { defaultModifier: CriterionModifier.Equals, options, inputType: "text", + makeCriterion: () => new StringCriterion(this), }); } } export function createStringCriterionOption( - value: CriterionType, - messageID?: string, - parameterName?: string + type: CriterionType, + messageID?: string ) { - return new StringCriterionOption( - messageID ?? value, - value, - parameterName ?? messageID ?? value - ); + return new StringCriterionOption(messageID ?? type, type); } export class StringCriterion extends Criterion { @@ -274,16 +288,10 @@ export class MultiStringCriterion extends Criterion { } export class MandatoryStringCriterionOption extends CriterionOption { - constructor( - messageID: string, - value: CriterionType, - parameterName?: string, - options?: Option[] - ) { + constructor(messageID: string, value: CriterionType, options?: Option[]) { super({ messageID, type: value, - parameterName, modifierOptions: [ CriterionModifier.Equals, CriterionModifier.NotEquals, @@ -295,45 +303,42 @@ export class MandatoryStringCriterionOption extends CriterionOption { defaultModifier: CriterionModifier.Equals, options, inputType: "text", + makeCriterion: () => new StringCriterion(this), }); } } export function createMandatoryStringCriterionOption( value: CriterionType, - messageID?: string, - parameterName?: string + messageID?: string ) { - return new MandatoryStringCriterionOption( - messageID ?? value, - value, - parameterName ?? messageID ?? value - ); + return new MandatoryStringCriterionOption(messageID ?? value, value); } export class PathCriterionOption extends StringCriterionOption {} export function createPathCriterionOption( - value: CriterionType, - messageID?: string, - parameterName?: string + type: CriterionType, + messageID?: string ) { - return new PathCriterionOption( - messageID ?? value, - value, - parameterName ?? messageID ?? value - ); + return new PathCriterionOption(messageID ?? type, type); } export class BooleanCriterionOption extends CriterionOption { - constructor(messageID: string, value: CriterionType, parameterName?: string) { + constructor( + messageID: string, + value: CriterionType, + makeCriterion?: () => Criterion + ) { super({ messageID, type: value, - parameterName, modifierOptions: [], defaultModifier: CriterionModifier.Equals, options: [true.toString(), false.toString()], + makeCriterion: makeCriterion + ? makeCriterion + : () => new BooleanCriterion(this), }); } } @@ -350,27 +355,16 @@ export class BooleanCriterion extends StringCriterion { export function createBooleanCriterionOption( value: CriterionType, - messageID?: string, - parameterName?: string + messageID?: string ) { - return new BooleanCriterionOption( - messageID ?? value, - value, - parameterName ?? messageID ?? value - ); + return new BooleanCriterionOption(messageID ?? value, value); } export class NumberCriterionOption extends CriterionOption { - constructor( - messageID: string, - value: CriterionType, - parameterName?: string, - options?: Option[] - ) { + constructor(messageID: string, value: CriterionType, options?: Option[]) { super({ messageID, type: value, - parameterName, modifierOptions: [ CriterionModifier.Equals, CriterionModifier.NotEquals, @@ -384,16 +378,16 @@ export class NumberCriterionOption extends CriterionOption { defaultModifier: CriterionModifier.Equals, options, inputType: "number", + makeCriterion: () => new NumberCriterion(this), }); } } export class NullNumberCriterionOption extends CriterionOption { - constructor(messageID: string, value: CriterionType, parameterName?: string) { + constructor(messageID: string, value: CriterionType) { super({ messageID, type: value, - parameterName, modifierOptions: [ CriterionModifier.Equals, CriterionModifier.NotEquals, @@ -406,16 +400,17 @@ export class NullNumberCriterionOption extends CriterionOption { ], defaultModifier: CriterionModifier.Equals, inputType: "number", + makeCriterion: () => new NumberCriterion(this), }); } } export function createNumberCriterionOption(value: CriterionType) { - return new NumberCriterionOption(value, value, value); + return new NumberCriterionOption(value, value); } export function createNullNumberCriterionOption(value: CriterionType) { - return new NullNumberCriterionOption(value, value, value); + return new NullNumberCriterionOption(value, value); } export class NumberCriterion extends Criterion { @@ -437,8 +432,8 @@ export class NumberCriterion extends Criterion { protected toCriterionInput(): IntCriterionInput { return { modifier: this.modifier, - value: this.value.value ?? 0, - value2: this.value.value2, + value: this.value?.value ?? 0, + value2: this.value?.value2, }; } @@ -487,8 +482,8 @@ export class ILabeledIdCriterionOption extends CriterionOption { constructor( messageID: string, value: CriterionType, - parameterName: string, - includeAll: boolean + includeAll: boolean, + inputType: InputType ) { const modifierOptions = [ CriterionModifier.Includes, @@ -506,9 +501,10 @@ export class ILabeledIdCriterionOption extends CriterionOption { super({ messageID, type: value, - parameterName, modifierOptions, defaultModifier, + makeCriterion: () => new ILabeledIdCriterion(this), + inputType, }); } } @@ -684,11 +680,10 @@ export class IHierarchicalLabeledIdCriterion extends Criterion new NumberCriterion(this), }); } } @@ -707,7 +703,7 @@ export function createMandatoryNumberCriterionOption( value: CriterionType, messageID?: string ) { - return new MandatoryNumberCriterionOption(messageID ?? value, value, value); + return new MandatoryNumberCriterionOption(messageID ?? value, value); } export class DurationCriterion extends Criterion { @@ -718,8 +714,8 @@ export class DurationCriterion extends Criterion { protected toCriterionInput(): IntCriterionInput { return { modifier: this.modifier, - value: this.value.value ?? 0, - value2: this.value.value2, + value: this.value?.value ?? 0, + value2: this.value?.value2, }; } @@ -771,16 +767,10 @@ export class PhashDuplicateCriterion extends StringCriterion { } export class DateCriterionOption extends CriterionOption { - constructor( - messageID: string, - value: CriterionType, - parameterName?: string, - options?: Option[] - ) { + constructor(messageID: string, value: CriterionType, options?: Option[]) { super({ messageID, type: value, - parameterName, modifierOptions: [ CriterionModifier.Equals, CriterionModifier.NotEquals, @@ -794,12 +784,13 @@ export class DateCriterionOption extends CriterionOption { defaultModifier: CriterionModifier.Equals, options, inputType: "text", + makeCriterion: () => new DateCriterion(this), }); } } export function createDateCriterionOption(value: CriterionType) { - return new DateCriterionOption(value, value, value); + return new DateCriterionOption(value, value); } export class DateCriterion extends Criterion { @@ -813,8 +804,8 @@ export class DateCriterion extends Criterion { protected toCriterionInput(): DateCriterionInput { return { modifier: this.modifier, - value: this.value.value, - value2: this.value.value2, + value: this.value?.value, + value2: this.value?.value2, }; } @@ -856,16 +847,10 @@ export class DateCriterion extends Criterion { } export class TimestampCriterionOption extends CriterionOption { - constructor( - messageID: string, - value: CriterionType, - parameterName?: string, - options?: Option[] - ) { + constructor(messageID: string, value: CriterionType, options?: Option[]) { super({ messageID, type: value, - parameterName, modifierOptions: [ CriterionModifier.GreaterThan, CriterionModifier.LessThan, @@ -877,19 +862,20 @@ export class TimestampCriterionOption extends CriterionOption { defaultModifier: CriterionModifier.GreaterThan, options, inputType: "text", + makeCriterion: () => new TimestampCriterion(this), }); } } export function createTimestampCriterionOption(value: CriterionType) { - return new TimestampCriterionOption(value, value, value); + return new TimestampCriterionOption(value, value); } export class TimestampCriterion extends Criterion { public encodeValue() { return { - value: this.value.value, - value2: this.value.value2, + value: this.value?.value, + value2: this.value?.value2, }; } @@ -950,16 +936,10 @@ export class TimestampCriterion extends Criterion { } export class MandatoryTimestampCriterionOption extends CriterionOption { - constructor( - messageID: string, - value: CriterionType, - parameterName?: string, - options?: Option[] - ) { + constructor(messageID: string, value: CriterionType, options?: Option[]) { super({ messageID, type: value, - parameterName, modifierOptions: [ CriterionModifier.GreaterThan, CriterionModifier.LessThan, @@ -969,10 +949,11 @@ export class MandatoryTimestampCriterionOption extends CriterionOption { defaultModifier: CriterionModifier.GreaterThan, options, inputType: "text", + makeCriterion: () => new TimestampCriterion(this), }); } } export function createMandatoryTimestampCriterionOption(value: CriterionType) { - return new MandatoryTimestampCriterionOption(value, value, value); + return new MandatoryTimestampCriterionOption(value, value); } diff --git a/ui/v2.5/src/models/list-filter/criteria/factory.ts b/ui/v2.5/src/models/list-filter/criteria/factory.ts index d45042aa0..1e20451d5 100644 --- a/ui/v2.5/src/models/list-filter/criteria/factory.ts +++ b/ui/v2.5/src/models/list-filter/criteria/factory.ts @@ -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 { IUIConfig } from "src/core/config"; -import { defaultRatingSystemOptions } from "src/utils/rating"; +import { SceneListFilterOptions } from "../scenes"; +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( - config: GQL.ConfigDataFragment | undefined, + mode: GQL.FilterMode, type: CriterionType = "none" ) { - switch (type) { - case "none": - return new NoneCriterion(); - case "name": - return new StringCriterion( - new MandatoryStringCriterionOption(type, 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) - ); + const criterionOptions = filterModeOptions[mode]; + + const option = criterionOptions.find((o) => o.type === type); + + if (!option) { + throw new Error(`Unknown criterion parameter name: ${type}`); } + + return option?.makeCriterion(); } diff --git a/ui/v2.5/src/models/list-filter/criteria/favorite.ts b/ui/v2.5/src/models/list-filter/criteria/favorite.ts index 362ebab93..5479980c3 100644 --- a/ui/v2.5/src/models/list-filter/criteria/favorite.ts +++ b/ui/v2.5/src/models/list-filter/criteria/favorite.ts @@ -2,7 +2,6 @@ import { BooleanCriterion, BooleanCriterionOption } from "./criterion"; export const FavoriteCriterionOption = new BooleanCriterionOption( "favourite", - "favorite", "filter_favorites" ); @@ -13,7 +12,6 @@ export class FavoriteCriterion extends BooleanCriterion { } export const PerformerFavoriteCriterionOption = new BooleanCriterionOption( - "performer_favorite", "performer_favorite", "performer_favorite" ); diff --git a/ui/v2.5/src/models/list-filter/criteria/galleries.ts b/ui/v2.5/src/models/list-filter/criteria/galleries.ts index d2331cd3a..60368853a 100644 --- a/ui/v2.5/src/models/list-filter/criteria/galleries.ts +++ b/ui/v2.5/src/models/list-filter/criteria/galleries.ts @@ -1,10 +1,12 @@ import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion"; +const inputType = "galleries"; + const galleriesCriterionOption = new ILabeledIdCriterionOption( "galleries", "galleries", - "galleries", - true + true, + inputType ); export class GalleriesCriterion extends ILabeledIdCriterion { diff --git a/ui/v2.5/src/models/list-filter/criteria/gender.ts b/ui/v2.5/src/models/list-filter/criteria/gender.ts index 58f2da712..801e92259 100644 --- a/ui/v2.5/src/models/list-filter/criteria/gender.ts +++ b/ui/v2.5/src/models/list-filter/criteria/gender.ts @@ -6,6 +6,7 @@ export const GenderCriterionOption = new CriterionOption({ messageID: "gender", type: "gender", options: genderStrings, + makeCriterion: () => new GenderCriterion(), }); export class GenderCriterion extends StringCriterion { diff --git a/ui/v2.5/src/models/list-filter/criteria/has-chapters.ts b/ui/v2.5/src/models/list-filter/criteria/has-chapters.ts index 12d74cbbb..8f38783f0 100644 --- a/ui/v2.5/src/models/list-filter/criteria/has-chapters.ts +++ b/ui/v2.5/src/models/list-filter/criteria/has-chapters.ts @@ -2,9 +2,9 @@ import { CriterionOption, StringCriterion } from "./criterion"; export const HasChaptersCriterionOption = new CriterionOption({ messageID: "hasChapters", - type: "hasChapters", - parameterName: "has_chapters", + type: "has_chapters", options: [true.toString(), false.toString()], + makeCriterion: () => new HasChaptersCriterion(), }); export class HasChaptersCriterion extends StringCriterion { diff --git a/ui/v2.5/src/models/list-filter/criteria/has-markers.ts b/ui/v2.5/src/models/list-filter/criteria/has-markers.ts index 3f4109ef1..23a72152f 100644 --- a/ui/v2.5/src/models/list-filter/criteria/has-markers.ts +++ b/ui/v2.5/src/models/list-filter/criteria/has-markers.ts @@ -2,9 +2,9 @@ import { CriterionOption, StringCriterion } from "./criterion"; export const HasMarkersCriterionOption = new CriterionOption({ messageID: "hasMarkers", - type: "hasMarkers", - parameterName: "has_markers", + type: "has_markers", options: [true.toString(), false.toString()], + makeCriterion: () => new HasMarkersCriterion(), }); export class HasMarkersCriterion extends StringCriterion { diff --git a/ui/v2.5/src/models/list-filter/criteria/is-missing.ts b/ui/v2.5/src/models/list-filter/criteria/is-missing.ts index e16e79793..acd96ad8c 100644 --- a/ui/v2.5/src/models/list-filter/criteria/is-missing.ts +++ b/ui/v2.5/src/models/list-filter/criteria/is-missing.ts @@ -11,25 +11,19 @@ export class IsMissingCriterion extends StringCriterion { } class IsMissingCriterionOptionClass extends CriterionOption { - constructor( - messageID: string, - value: CriterionType, - parameterName: string, - options: Option[] - ) { + constructor(messageID: string, type: CriterionType, options: Option[]) { super({ messageID, - type: value, - parameterName, + type, options, defaultModifier: CriterionModifier.Equals, + makeCriterion: () => new IsMissingCriterion(this), }); } } export const SceneIsMissingCriterionOption = new IsMissingCriterionOptionClass( "isMissing", - "sceneIsMissing", "is_missing", [ "title", @@ -48,73 +42,59 @@ export const SceneIsMissingCriterionOption = new IsMissingCriterionOptionClass( export const ImageIsMissingCriterionOption = new IsMissingCriterionOptionClass( "isMissing", - "imageIsMissing", "is_missing", ["title", "galleries", "studio", "performers", "tags"] ); export const PerformerIsMissingCriterionOption = - new IsMissingCriterionOptionClass( - "isMissing", - "performerIsMissing", - "is_missing", - [ - "url", - "twitter", - "instagram", - "ethnicity", - "country", - "hair_color", - "eye_color", - "height", - "weight", - "measurements", - "fake_tits", - "career_length", - "tattoos", - "piercings", - "aliases", - "gender", - "image", - "details", - "stash_id", - ] - ); + new IsMissingCriterionOptionClass("isMissing", "is_missing", [ + "url", + "twitter", + "instagram", + "ethnicity", + "country", + "hair_color", + "eye_color", + "height", + "weight", + "measurements", + "fake_tits", + "career_length", + "tattoos", + "piercings", + "aliases", + "gender", + "image", + "details", + "stash_id", + ]); export const GalleryIsMissingCriterionOption = - new IsMissingCriterionOptionClass( - "isMissing", - "galleryIsMissing", - "is_missing", - [ - "title", - "details", - "url", - "date", - "studio", - "performers", - "tags", - "scenes", - ] - ); + new IsMissingCriterionOptionClass("isMissing", "is_missing", [ + "title", + "details", + "url", + "date", + "studio", + "performers", + "tags", + "scenes", + ]); export const TagIsMissingCriterionOption = new IsMissingCriterionOptionClass( "isMissing", - "tagIsMissing", "is_missing", ["image"] ); export const StudioIsMissingCriterionOption = new IsMissingCriterionOptionClass( "isMissing", - "studioIsMissing", "is_missing", ["image", "stash_id", "details"] ); export const MovieIsMissingCriterionOption = new IsMissingCriterionOptionClass( "isMissing", - "movieIsMissing", "is_missing", ["front_image", "back_image", "scenes"] ); diff --git a/ui/v2.5/src/models/list-filter/criteria/movies.ts b/ui/v2.5/src/models/list-filter/criteria/movies.ts index 69cac4f9e..0cd7926ed 100644 --- a/ui/v2.5/src/models/list-filter/criteria/movies.ts +++ b/ui/v2.5/src/models/list-filter/criteria/movies.ts @@ -1,10 +1,12 @@ import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion"; +const inputType = "movies"; + export const MoviesCriterionOption = new ILabeledIdCriterionOption( "movies", "movies", - "movies", - false + false, + inputType ); export class MoviesCriterion extends ILabeledIdCriterion { diff --git a/ui/v2.5/src/models/list-filter/criteria/none.ts b/ui/v2.5/src/models/list-filter/criteria/none.ts index 818f4a665..9aabef0f9 100644 --- a/ui/v2.5/src/models/list-filter/criteria/none.ts +++ b/ui/v2.5/src/models/list-filter/criteria/none.ts @@ -1,10 +1,6 @@ import { Criterion, StringCriterionOption } from "./criterion"; -export const NoneCriterionOption = new StringCriterionOption( - "none", - "none", - "none" -); +export const NoneCriterionOption = new StringCriterionOption("none", "none"); export class NoneCriterion extends Criterion { constructor() { super(NoneCriterionOption, "none"); diff --git a/ui/v2.5/src/models/list-filter/criteria/organized.ts b/ui/v2.5/src/models/list-filter/criteria/organized.ts index 526750208..a62384100 100644 --- a/ui/v2.5/src/models/list-filter/criteria/organized.ts +++ b/ui/v2.5/src/models/list-filter/criteria/organized.ts @@ -1,7 +1,6 @@ import { BooleanCriterion, BooleanCriterionOption } from "./criterion"; export const OrganizedCriterionOption = new BooleanCriterionOption( - "organized", "organized", "organized" ); diff --git a/ui/v2.5/src/models/list-filter/criteria/performers.ts b/ui/v2.5/src/models/list-filter/criteria/performers.ts index b68d5e0b1..612c7b47b 100644 --- a/ui/v2.5/src/models/list-filter/criteria/performers.ts +++ b/ui/v2.5/src/models/list-filter/criteria/performers.ts @@ -17,12 +17,15 @@ const modifierOptions = [ const defaultModifier = CriterionModifier.IncludesAll; +const inputType = "performers"; + export const PerformersCriterionOption = new CriterionOption({ messageID: "performers", type: "performers", - parameterName: "performers", modifierOptions, defaultModifier, + makeCriterion: () => new PerformersCriterion(), + inputType, }); export class PerformersCriterion extends Criterion { diff --git a/ui/v2.5/src/models/list-filter/criteria/phash.ts b/ui/v2.5/src/models/list-filter/criteria/phash.ts index 5a5ec09dc..433b06b9b 100644 --- a/ui/v2.5/src/models/list-filter/criteria/phash.ts +++ b/ui/v2.5/src/models/list-filter/criteria/phash.ts @@ -12,8 +12,7 @@ import { export const PhashCriterionOption = new CriterionOption({ messageID: "media_info.phash", - type: "phash", - parameterName: "phash_distance", + type: "phash_distance", inputType: "text", modifierOptions: [ CriterionModifier.Equals, @@ -21,6 +20,7 @@ export const PhashCriterionOption = new CriterionOption({ CriterionModifier.IsNull, CriterionModifier.NotNull, ], + makeCriterion: () => new PhashCriterion(), }); export class PhashCriterion extends Criterion { @@ -53,7 +53,7 @@ export class PhashCriterion extends Criterion { export const DuplicatedCriterionOption = new BooleanCriterionOption( "duplicated_phash", "duplicated", - "duplicated" + () => new DuplicatedCriterion() ); export class DuplicatedCriterion extends PhashDuplicateCriterion { diff --git a/ui/v2.5/src/models/list-filter/criteria/resolution.ts b/ui/v2.5/src/models/list-filter/criteria/resolution.ts index 5ee0b7254..6961e22b1 100644 --- a/ui/v2.5/src/models/list-filter/criteria/resolution.ts +++ b/ui/v2.5/src/models/list-filter/criteria/resolution.ts @@ -4,7 +4,12 @@ import { } from "src/core/generated-graphql"; import { stringToResolution, resolutionStrings } from "src/utils/resolution"; import { CriterionType } from "../types"; -import { CriterionOption, StringCriterion } from "./criterion"; +import { + Criterion, + CriterionOption, + CriterionValue, + StringCriterion, +} from "./criterion"; abstract class AbstractResolutionCriterion extends StringCriterion { protected toCriterionInput(): ResolutionCriterionInput | undefined { @@ -20,11 +25,13 @@ abstract class AbstractResolutionCriterion extends StringCriterion { } class ResolutionCriterionOptionType extends CriterionOption { - constructor(value: CriterionType) { + constructor( + value: CriterionType, + makeCriterion: () => Criterion + ) { super({ messageID: value, type: value, - parameterName: value, modifierOptions: [ CriterionModifier.Equals, CriterionModifier.NotEquals, @@ -32,12 +39,14 @@ class ResolutionCriterionOptionType extends CriterionOption { CriterionModifier.LessThan, ], options: resolutionStrings, + makeCriterion, }); } } export const ResolutionCriterionOption = new ResolutionCriterionOptionType( - "resolution" + "resolution", + () => new ResolutionCriterion() ); export class ResolutionCriterion extends AbstractResolutionCriterion { constructor() { @@ -46,7 +55,10 @@ export class ResolutionCriterion extends AbstractResolutionCriterion { } export const AverageResolutionCriterionOption = - new ResolutionCriterionOptionType("average_resolution"); + new ResolutionCriterionOptionType( + "average_resolution", + () => new AverageResolutionCriterion() + ); export class AverageResolutionCriterion extends AbstractResolutionCriterion { constructor() { diff --git a/ui/v2.5/src/models/list-filter/criteria/stash-ids.ts b/ui/v2.5/src/models/list-filter/criteria/stash-ids.ts index ac6be309d..82c63c15a 100644 --- a/ui/v2.5/src/models/list-filter/criteria/stash-ids.ts +++ b/ui/v2.5/src/models/list-filter/criteria/stash-ids.ts @@ -10,13 +10,13 @@ import { Criterion, CriterionOption } from "./criterion"; export const StashIDCriterionOption = new CriterionOption({ messageID: "stash_id", type: "stash_id_endpoint", - parameterName: "stash_id_endpoint", modifierOptions: [ CriterionModifier.Equals, CriterionModifier.NotEquals, CriterionModifier.IsNull, CriterionModifier.NotNull, ], + makeCriterion: () => new StashIDCriterion(), }); export class StashIDCriterion extends Criterion { diff --git a/ui/v2.5/src/models/list-filter/criteria/studios.ts b/ui/v2.5/src/models/list-filter/criteria/studios.ts index 8f3bfe7d4..e238943c1 100644 --- a/ui/v2.5/src/models/list-filter/criteria/studios.ts +++ b/ui/v2.5/src/models/list-filter/criteria/studios.ts @@ -13,13 +13,15 @@ const modifierOptions = [ ]; const defaultModifier = CriterionModifier.Includes; +const inputType = "studios"; export const StudiosCriterionOption = new CriterionOption({ messageID: "studios", type: "studios", - parameterName: "studios", modifierOptions, defaultModifier, + makeCriterion: () => new StudiosCriterion(), + inputType, }); export class StudiosCriterion extends IHierarchicalLabeledIdCriterion { @@ -29,10 +31,10 @@ export class StudiosCriterion extends IHierarchicalLabeledIdCriterion { } export const ParentStudiosCriterionOption = new ILabeledIdCriterionOption( - "parent_studios", "parent_studios", "parents", - false + false, + inputType ); export class ParentStudiosCriterion extends ILabeledIdCriterion { constructor() { diff --git a/ui/v2.5/src/models/list-filter/criteria/tags.ts b/ui/v2.5/src/models/list-filter/criteria/tags.ts index d197d11ff..fe19fc286 100644 --- a/ui/v2.5/src/models/list-filter/criteria/tags.ts +++ b/ui/v2.5/src/models/list-filter/criteria/tags.ts @@ -1,7 +1,8 @@ import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionOption, IHierarchicalLabeledIdCriterion } from "./criterion"; +import { CriterionType } from "../types"; -const modifierOptions = [ +const defaultModifierOptions = [ CriterionModifier.IncludesAll, CriterionModifier.Includes, CriterionModifier.Equals, @@ -17,41 +18,53 @@ const withoutEqualsModifierOptions = [ ]; const defaultModifier = CriterionModifier.IncludesAll; +const inputType = "tags"; -export const TagsCriterionOption = new CriterionOption({ - messageID: "tags", - type: "tags", - parameterName: "tags", - modifierOptions, - defaultModifier, -}); -export const SceneTagsCriterionOption = new CriterionOption({ - messageID: "sceneTags", - type: "sceneTags", - parameterName: "scene_tags", - modifierOptions, - defaultModifier, -}); -export const PerformerTagsCriterionOption = new CriterionOption({ - messageID: "performerTags", - type: "performerTags", - parameterName: "performer_tags", - modifierOptions: withoutEqualsModifierOptions, - defaultModifier, -}); -export const ParentTagsCriterionOption = new CriterionOption({ - messageID: "parent_tags", - type: "parentTags", - parameterName: "parents", - modifierOptions: withoutEqualsModifierOptions, - defaultModifier, -}); -export const ChildTagsCriterionOption = new CriterionOption({ - messageID: "sub_tags", - type: "childTags", - parameterName: "children", - modifierOptions: withoutEqualsModifierOptions, - defaultModifier, -}); +export class TagsCriterionOptionClass extends CriterionOption { + constructor( + messageID: string, + type: CriterionType, + modifierOptions: CriterionModifier[] + ) { + super({ + messageID, + type, + modifierOptions, + defaultModifier, + makeCriterion: () => new TagsCriterion(this), + inputType, + }); + } +} + +export const TagsCriterionOption = new TagsCriterionOptionClass( + "tags", + "tags", + defaultModifierOptions +); + +export const SceneTagsCriterionOption = new TagsCriterionOptionClass( + "scene_tags", + "scene_tags", + defaultModifierOptions +); + +export const PerformerTagsCriterionOption = new TagsCriterionOptionClass( + "performer_tags", + "performer_tags", + withoutEqualsModifierOptions +); + +export const ParentTagsCriterionOption = new TagsCriterionOptionClass( + "parent_tags", + "parents", + withoutEqualsModifierOptions +); + +export const ChildTagsCriterionOption = new TagsCriterionOptionClass( + "sub_tags", + "children", + withoutEqualsModifierOptions +); export class TagsCriterion extends IHierarchicalLabeledIdCriterion {} diff --git a/ui/v2.5/src/models/list-filter/filter.ts b/ui/v2.5/src/models/list-filter/filter.ts index 10f420d23..9e6bb8630 100644 --- a/ui/v2.5/src/models/list-filter/filter.ts +++ b/ui/v2.5/src/models/list-filter/filter.ts @@ -2,11 +2,12 @@ import { ConfigDataFragment, FilterMode, FindFilterType, + SavedFilterDataFragment, SortDirectionEnum, } from "src/core/generated-graphql"; import { Criterion, CriterionValue } from "./criteria/criterion"; import { makeCriteria } from "./criteria/factory"; -import { DisplayMode } from "./types"; +import { CriterionType, DisplayMode } from "./types"; interface IDecodedParams { perPage?: number; @@ -127,7 +128,7 @@ export class ListFilterModel { for (const jsonString of params.c) { try { 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. if (criterion) { criterion.setFromEncodedCriterion(encodedCriterion); @@ -248,8 +249,41 @@ export class ListFilterModel { this.configureFromDecodedParams(decoded); } - public configureFromJSON(json: string) { - this.configureFromDecodedParams(JSON.parse(json)); + public configureFromSavedFilter(savedFilter: SavedFilterDataFragment) { + 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() { @@ -405,4 +439,22 @@ export class ListFilterModel { return output; } + + public makeSavedFindFilter() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const output: Record = {}; + this.criteria.forEach((criterion) => { + criterion.toSavedFilter(output); + }); + + return output; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public makeUIOptions(): Record { + return { + display_mode: this.displayMode, + zoom_index: this.zoomIndex, + }; + } } diff --git a/ui/v2.5/src/models/list-filter/galleries.ts b/ui/v2.5/src/models/list-filter/galleries.ts index 36bb65de6..2a597417f 100644 --- a/ui/v2.5/src/models/list-filter/galleries.ts +++ b/ui/v2.5/src/models/list-filter/galleries.ts @@ -45,11 +45,7 @@ const criterionOptions = [ createStringCriterionOption("title"), createStringCriterionOption("details"), createPathCriterionOption("path"), - createStringCriterionOption( - "galleryChecksum", - "media_info.checksum", - "checksum" - ), + createStringCriterionOption("checksum", "media_info.checksum"), new NullNumberCriterionOption("rating", "rating100"), OrganizedCriterionOption, AverageResolutionCriterionOption, diff --git a/ui/v2.5/src/models/list-filter/performers.ts b/ui/v2.5/src/models/list-filter/performers.ts index 2995aebb7..7c66880ee 100644 --- a/ui/v2.5/src/models/list-filter/performers.ts +++ b/ui/v2.5/src/models/list-filter/performers.ts @@ -94,7 +94,7 @@ const criterionOptions = [ createMandatoryNumberCriterionOption("gallery_count"), createMandatoryNumberCriterionOption("o_counter"), createBooleanCriterionOption("ignore_auto_tag"), - new NumberCriterionOption("height", "height_cm", "height_cm"), + new NumberCriterionOption("height", "height_cm"), ...numberCriteria.map((c) => createNumberCriterionOption(c)), ...stringCriteria.map((c) => createStringCriterionOption(c)), createDateCriterionOption("birthdate"), diff --git a/ui/v2.5/src/models/list-filter/scenes.ts b/ui/v2.5/src/models/list-filter/scenes.ts index 106875a12..108958298 100644 --- a/ui/v2.5/src/models/list-filter/scenes.ts +++ b/ui/v2.5/src/models/list-filter/scenes.ts @@ -59,16 +59,12 @@ const displayModeOptions = [ const criterionOptions = [ createStringCriterionOption("title"), - createStringCriterionOption("scene_code"), + createStringCriterionOption("code", "scene_code"), createPathCriterionOption("path"), createStringCriterionOption("details"), createStringCriterionOption("director"), createMandatoryStringCriterionOption("oshash", "media_info.hash"), - createStringCriterionOption( - "sceneChecksum", - "media_info.checksum", - "checksum" - ), + createStringCriterionOption("checksum", "media_info.checksum"), PhashCriterionOption, DuplicatedCriterionOption, OrganizedCriterionOption, diff --git a/ui/v2.5/src/models/list-filter/tags.ts b/ui/v2.5/src/models/list-filter/tags.ts index 8e90a27e7..12daff1c2 100644 --- a/ui/v2.5/src/models/list-filter/tags.ts +++ b/ui/v2.5/src/models/list-filter/tags.ts @@ -53,17 +53,9 @@ const criterionOptions = [ createMandatoryNumberCriterionOption("performer_count"), createMandatoryNumberCriterionOption("marker_count"), ParentTagsCriterionOption, - new MandatoryNumberCriterionOption( - "parent_tag_count", - "parent_tag_count", - "parent_count" - ), + new MandatoryNumberCriterionOption("parent_tag_count", "parent_count"), ChildTagsCriterionOption, - new MandatoryNumberCriterionOption( - "sub_tag_count", - "child_tag_count", - "child_count" - ), + new MandatoryNumberCriterionOption("sub_tag_count", "child_count"), createMandatoryTimestampCriterionOption("created_at"), createMandatoryTimestampCriterionOption("updated_at"), ]; diff --git a/ui/v2.5/src/models/list-filter/types.ts b/ui/v2.5/src/models/list-filter/types.ts index a200024f7..453081ce5 100644 --- a/ui/v2.5/src/models/list-filter/types.ts +++ b/ui/v2.5/src/models/list-filter/types.ts @@ -112,20 +112,12 @@ export type CriterionType = | "video_codec" | "audio_codec" | "duration" - | "favorite" - | "hasMarkers" - | "sceneIsMissing" - | "imageIsMissing" - | "performerIsMissing" - | "galleryIsMissing" - | "tagIsMissing" - | "studioIsMissing" - | "movieIsMissing" + | "filter_favorites" + | "has_markers" + | "is_missing" | "tags" - | "sceneTags" - | "performerTags" - | "parentTags" - | "childTags" + | "scene_tags" + | "performer_tags" | "tag_count" | "performers" | "studios" @@ -149,7 +141,8 @@ export type CriterionType = | "piercings" | "aliases" | "gender" - | "parent_studios" + | "parents" + | "children" | "scene_count" | "marker_count" | "image_count" @@ -169,13 +162,11 @@ export type CriterionType = | "title" | "oshash" | "checksum" - | "sceneChecksum" - | "galleryChecksum" - | "phash" + | "phash_distance" | "director" | "synopsis" - | "parent_tag_count" - | "child_tag_count" + | "parent_count" + | "child_count" | "performer_favorite" | "performer_age" | "duplicated" @@ -191,6 +182,6 @@ export type CriterionType = | "scene_created_at" | "scene_updated_at" | "description" - | "scene_code" + | "code" | "disambiguation" - | "hasChapters"; + | "has_chapters";