mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Saved filter refactor (#4054)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
parent
fca162f1ca
commit
20520a58b4
70 changed files with 1062 additions and 657 deletions
|
|
@ -118,4 +118,6 @@ models:
|
||||||
model: github.com/stashapp/stash/internal/identify.MetadataOptions
|
model: github.com/stashapp/stash/internal/identify.MetadataOptions
|
||||||
ScraperSourceInput:
|
ScraperSourceInput:
|
||||||
model: github.com/stashapp/stash/pkg/scraper.Source
|
model: github.com/stashapp/stash/pkg/scraper.Source
|
||||||
|
SavedFindFilterType:
|
||||||
|
model: github.com/stashapp/stash/pkg/models.FindFilterType
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,13 @@ fragment SavedFilterData on SavedFilter {
|
||||||
id
|
id
|
||||||
mode
|
mode
|
||||||
name
|
name
|
||||||
filter
|
find_filter {
|
||||||
|
q
|
||||||
|
page
|
||||||
|
per_page
|
||||||
|
sort
|
||||||
|
direction
|
||||||
|
}
|
||||||
|
object_filter
|
||||||
|
ui_options
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,17 @@ input FindFilterType {
|
||||||
direction: SortDirectionEnum
|
direction: SortDirectionEnum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SavedFindFilterType {
|
||||||
|
q: String
|
||||||
|
page: Int
|
||||||
|
"""
|
||||||
|
use per_page = -1 to indicate all results. Defaults to 25.
|
||||||
|
"""
|
||||||
|
per_page: Int
|
||||||
|
sort: String
|
||||||
|
direction: SortDirectionEnum
|
||||||
|
}
|
||||||
|
|
||||||
enum ResolutionEnum {
|
enum ResolutionEnum {
|
||||||
"144p"
|
"144p"
|
||||||
VERY_LOW
|
VERY_LOW
|
||||||
|
|
@ -604,6 +615,13 @@ type SavedFilter {
|
||||||
name: String!
|
name: String!
|
||||||
"JSON-encoded filter string"
|
"JSON-encoded filter string"
|
||||||
filter: String!
|
filter: String!
|
||||||
|
@deprecated(reason: "use find_filter and object_filter instead")
|
||||||
|
find_filter: SavedFindFilterType
|
||||||
|
# maps to any of the AnyFilterInput types
|
||||||
|
# using a generic Map instead of creating and maintaining match types for inputs
|
||||||
|
object_filter: Map
|
||||||
|
# generic map for ui options
|
||||||
|
ui_options: Map
|
||||||
}
|
}
|
||||||
|
|
||||||
input SaveFilterInput {
|
input SaveFilterInput {
|
||||||
|
|
@ -611,8 +629,10 @@ input SaveFilterInput {
|
||||||
id: ID
|
id: ID
|
||||||
mode: FilterMode!
|
mode: FilterMode!
|
||||||
name: String!
|
name: String!
|
||||||
"JSON-encoded filter string"
|
find_filter: FindFilterType
|
||||||
filter: String!
|
object_filter: Map
|
||||||
|
# generic map for ui options
|
||||||
|
ui_options: Map
|
||||||
}
|
}
|
||||||
|
|
||||||
input DestroyFilterInput {
|
input DestroyFilterInput {
|
||||||
|
|
@ -621,6 +641,9 @@ input DestroyFilterInput {
|
||||||
|
|
||||||
input SetDefaultFilterInput {
|
input SetDefaultFilterInput {
|
||||||
mode: FilterMode!
|
mode: FilterMode!
|
||||||
"JSON-encoded filter string - null to clear"
|
"null to clear"
|
||||||
filter: String
|
find_filter: FindFilterType
|
||||||
|
object_filter: Map
|
||||||
|
# generic map for ui options
|
||||||
|
ui_options: Map
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,9 @@ func (r *Resolver) Subscription() SubscriptionResolver {
|
||||||
func (r *Resolver) Tag() TagResolver {
|
func (r *Resolver) Tag() TagResolver {
|
||||||
return &tagResolver{r}
|
return &tagResolver{r}
|
||||||
}
|
}
|
||||||
|
func (r *Resolver) SavedFilter() SavedFilterResolver {
|
||||||
|
return &savedFilterResolver{r}
|
||||||
|
}
|
||||||
|
|
||||||
type mutationResolver struct{ *Resolver }
|
type mutationResolver struct{ *Resolver }
|
||||||
type queryResolver struct{ *Resolver }
|
type queryResolver struct{ *Resolver }
|
||||||
|
|
@ -96,6 +99,7 @@ type imageResolver struct{ *Resolver }
|
||||||
type studioResolver struct{ *Resolver }
|
type studioResolver struct{ *Resolver }
|
||||||
type movieResolver struct{ *Resolver }
|
type movieResolver struct{ *Resolver }
|
||||||
type tagResolver struct{ *Resolver }
|
type tagResolver struct{ *Resolver }
|
||||||
|
type savedFilterResolver struct{ *Resolver }
|
||||||
|
|
||||||
func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) error) error {
|
func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) error) error {
|
||||||
return txn.WithTxn(ctx, r.txnManager, fn)
|
return txn.WithTxn(ctx, r.txnManager, fn)
|
||||||
|
|
|
||||||
11
internal/api/resolver_model_saved_filter.go
Normal file
11
internal/api/resolver_model_saved_filter.go
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *savedFilterResolver) Filter(ctx context.Context, obj *models.SavedFilter) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
@ -14,12 +14,6 @@ func (r *mutationResolver) SaveFilter(ctx context.Context, input SaveFilterInput
|
||||||
return nil, errors.New("name must be non-empty")
|
return nil, errors.New("name must be non-empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
newFilter := models.SavedFilter{
|
|
||||||
Mode: input.Mode,
|
|
||||||
Name: input.Name,
|
|
||||||
Filter: input.Filter,
|
|
||||||
}
|
|
||||||
|
|
||||||
var id *int
|
var id *int
|
||||||
if input.ID != nil {
|
if input.ID != nil {
|
||||||
idv, err := strconv.Atoi(*input.ID)
|
idv, err := strconv.Atoi(*input.ID)
|
||||||
|
|
@ -32,17 +26,27 @@ func (r *mutationResolver) SaveFilter(ctx context.Context, input SaveFilterInput
|
||||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||||
qb := r.repository.SavedFilter
|
qb := r.repository.SavedFilter
|
||||||
|
|
||||||
if id == nil {
|
f := models.SavedFilter{
|
||||||
err = qb.Create(ctx, &newFilter)
|
Mode: input.Mode,
|
||||||
} else {
|
Name: input.Name,
|
||||||
newFilter.ID = *id
|
FindFilter: input.FindFilter,
|
||||||
err = qb.Update(ctx, &newFilter)
|
ObjectFilter: input.ObjectFilter,
|
||||||
|
UIOptions: input.UIOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if id == nil {
|
||||||
|
err = qb.Create(ctx, &f)
|
||||||
|
ret = &f
|
||||||
|
} else {
|
||||||
|
f.ID = *id
|
||||||
|
err = qb.Update(ctx, &f)
|
||||||
|
ret = &f
|
||||||
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ret = &newFilter
|
|
||||||
return ret, err
|
return ret, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,7 +69,7 @@ func (r *mutationResolver) SetDefaultFilter(ctx context.Context, input SetDefaul
|
||||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||||
qb := r.repository.SavedFilter
|
qb := r.repository.SavedFilter
|
||||||
|
|
||||||
if input.Filter == nil {
|
if input.FindFilter == nil && input.ObjectFilter == nil && input.UIOptions == nil {
|
||||||
// clearing
|
// clearing
|
||||||
def, err := qb.FindDefault(ctx, input.Mode)
|
def, err := qb.FindDefault(ctx, input.Mode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -79,12 +83,12 @@ func (r *mutationResolver) SetDefaultFilter(ctx context.Context, input SetDefaul
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := qb.SetDefault(ctx, &models.SavedFilter{
|
return qb.SetDefault(ctx, &models.SavedFilter{
|
||||||
Mode: input.Mode,
|
Mode: input.Mode,
|
||||||
Filter: *input.Filter,
|
FindFilter: input.FindFilter,
|
||||||
|
ObjectFilter: input.ObjectFilter,
|
||||||
|
UIOptions: input.UIOptions,
|
||||||
})
|
})
|
||||||
|
|
||||||
return err
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ func createPerformer(ctx context.Context, pqb models.PerformerWriter) error {
|
||||||
func createStudio(ctx context.Context, qb models.StudioWriter, name string) (*models.Studio, error) {
|
func createStudio(ctx context.Context, qb models.StudioWriter, name string) (*models.Studio, error) {
|
||||||
// create the studio
|
// create the studio
|
||||||
studio := models.Studio{
|
studio := models.Studio{
|
||||||
Name: name,
|
Name: name,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := qb.Create(ctx, &studio)
|
err := qb.Create(ctx, &studio)
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,12 @@ func (e FilterMode) MarshalGQL(w io.Writer) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SavedFilter struct {
|
type SavedFilter struct {
|
||||||
ID int `json:"id"`
|
ID int `db:"id" json:"id"`
|
||||||
Mode FilterMode `json:"mode"`
|
Mode FilterMode `db:"mode" json:"mode"`
|
||||||
Name string `json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
// JSON-encoded filter string
|
FindFilter *FindFilterType `json:"find_filter"`
|
||||||
Filter string `json:"filter"`
|
ObjectFilter map[string]interface{} `json:"object_filter"`
|
||||||
|
UIOptions map[string]interface{} `json:"ui_options"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SavedFilters []*SavedFilter
|
type SavedFilters []*SavedFilter
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ const (
|
||||||
dbConnTimeout = 30
|
dbConnTimeout = 30
|
||||||
)
|
)
|
||||||
|
|
||||||
var appSchemaVersion uint = 48
|
var appSchemaVersion uint = 49
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*.sql
|
||||||
var migrationsBox embed.FS
|
var migrationsBox embed.FS
|
||||||
|
|
@ -74,10 +74,10 @@ type Database struct {
|
||||||
Scene *SceneStore
|
Scene *SceneStore
|
||||||
SceneMarker *SceneMarkerStore
|
SceneMarker *SceneMarkerStore
|
||||||
Performer *PerformerStore
|
Performer *PerformerStore
|
||||||
|
SavedFilter *SavedFilterStore
|
||||||
Studio *StudioStore
|
Studio *StudioStore
|
||||||
Tag *TagStore
|
Tag *TagStore
|
||||||
Movie *MovieStore
|
Movie *MovieStore
|
||||||
SavedFilter *SavedFilterStore
|
|
||||||
|
|
||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
dbPath string
|
dbPath string
|
||||||
|
|
|
||||||
417
pkg/sqlite/migrations/49_postmigrate.go
Normal file
417
pkg/sqlite/migrations/49_postmigrate.go
Normal file
|
|
@ -0,0 +1,417 @@
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
var migrate49TypeResolution = map[string][]string{
|
||||||
|
"Boolean": {
|
||||||
|
/*
|
||||||
|
"organized",
|
||||||
|
"interactive",
|
||||||
|
"ignore_auto_tag",
|
||||||
|
"performer_favorite",
|
||||||
|
"filter_favorites",
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
"Int": {
|
||||||
|
"id",
|
||||||
|
"rating",
|
||||||
|
"rating100",
|
||||||
|
"o_counter",
|
||||||
|
"duration",
|
||||||
|
"tag_count",
|
||||||
|
"age",
|
||||||
|
"height",
|
||||||
|
"height_cm",
|
||||||
|
"weight",
|
||||||
|
"scene_count",
|
||||||
|
"marker_count",
|
||||||
|
"image_count",
|
||||||
|
"gallery_count",
|
||||||
|
"performer_count",
|
||||||
|
"interactive_speed",
|
||||||
|
"resume_time",
|
||||||
|
"play_count",
|
||||||
|
"play_duration",
|
||||||
|
"parent_count",
|
||||||
|
"child_count",
|
||||||
|
"performer_age",
|
||||||
|
"file_count",
|
||||||
|
},
|
||||||
|
"Float": {
|
||||||
|
"penis_length",
|
||||||
|
},
|
||||||
|
"Object": {
|
||||||
|
"tags",
|
||||||
|
"performers",
|
||||||
|
"studios",
|
||||||
|
"movies",
|
||||||
|
"galleries",
|
||||||
|
"parents",
|
||||||
|
"children",
|
||||||
|
"scene_tags",
|
||||||
|
"performer_tags",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var migrate49NameChanges = map[string]string{
|
||||||
|
"rating": "rating100",
|
||||||
|
"parent_studios": "parents",
|
||||||
|
"child_studios": "children",
|
||||||
|
"parent_tags": "parents",
|
||||||
|
"child_tags": "children",
|
||||||
|
"child_tag_count": "child_count",
|
||||||
|
"parent_tag_count": "parent_count",
|
||||||
|
"height": "height_cm",
|
||||||
|
"imageIsMissing": "is_missing",
|
||||||
|
"sceneIsMissing": "is_missing",
|
||||||
|
"galleryIsMissing": "is_missing",
|
||||||
|
"performerIsMissing": "is_missing",
|
||||||
|
"tagIsMissing": "is_missing",
|
||||||
|
"studioIsMissing": "is_missing",
|
||||||
|
"movieIsMissing": "is_missing",
|
||||||
|
"favorite": "filter_favorites",
|
||||||
|
"hasMarkers": "has_markers",
|
||||||
|
"parentTags": "parents",
|
||||||
|
"childTags": "children",
|
||||||
|
"phash": "phash_distance",
|
||||||
|
"scene_code": "code",
|
||||||
|
"hasChapters": "has_chapters",
|
||||||
|
"sceneChecksum": "checksum",
|
||||||
|
"galleryChecksum": "checksum",
|
||||||
|
"sceneTags": "scene_tags",
|
||||||
|
"performerTags": "performer_tags",
|
||||||
|
}
|
||||||
|
|
||||||
|
func post49(ctx context.Context, db *sqlx.DB) error {
|
||||||
|
logger.Info("Running post-migration for schema version 49")
|
||||||
|
|
||||||
|
m := schema49Migrator{
|
||||||
|
migrator: migrator{
|
||||||
|
db: db,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.migrateSavedFilters(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
type schema49Migrator struct {
|
||||||
|
migrator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *schema49Migrator) migrateSavedFilters(ctx context.Context) error {
|
||||||
|
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
|
||||||
|
rows, err := m.db.Query("SELECT id, mode, find_filter FROM saved_filters ORDER BY id")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
id int
|
||||||
|
mode models.FilterMode
|
||||||
|
findFilter string
|
||||||
|
)
|
||||||
|
|
||||||
|
err := rows.Scan(&id, &mode, &findFilter)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
asRawMessage := json.RawMessage(findFilter)
|
||||||
|
|
||||||
|
newFindFilter, err := m.getFindFilter(asRawMessage)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get find filter for saved filter %d: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
objectFilter, err := m.getObjectFilter(mode, asRawMessage)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get object filter for saved filter %d: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uiOptions, err := m.getDisplayOptions(asRawMessage)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get display options for saved filter %d: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = m.db.Exec("UPDATE saved_filters SET find_filter = ?, object_filter = ?, ui_options = ? WHERE id = ?", newFindFilter, objectFilter, uiOptions, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update saved filter %d: %w", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.Err()
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *schema49Migrator) getDisplayOptions(data json.RawMessage) (json.RawMessage, error) {
|
||||||
|
type displayOptions struct {
|
||||||
|
DisplayMode *int `json:"disp"`
|
||||||
|
ZoomIndex *int `json:"z"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts displayOptions
|
||||||
|
if err := json.Unmarshal(data, &opts); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal display options: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := make(map[string]interface{})
|
||||||
|
if opts.DisplayMode != nil {
|
||||||
|
ret["display_mode"] = *opts.DisplayMode
|
||||||
|
}
|
||||||
|
if opts.ZoomIndex != nil {
|
||||||
|
ret["zoom_index"] = *opts.ZoomIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *schema49Migrator) getFindFilter(data json.RawMessage) (json.RawMessage, error) {
|
||||||
|
type findFilterJson struct {
|
||||||
|
Q *string `json:"q"`
|
||||||
|
Page *int `json:"page"`
|
||||||
|
PerPage *int `json:"perPage"`
|
||||||
|
Sort *string `json:"sortby"`
|
||||||
|
Direction *string `json:"sortdir"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ppDefault := 40
|
||||||
|
pageDefault := 1
|
||||||
|
qDefault := ""
|
||||||
|
sortDefault := "date"
|
||||||
|
asc := "asc"
|
||||||
|
ff := findFilterJson{Q: &qDefault, Page: &pageDefault, PerPage: &ppDefault, Sort: &sortDefault, Direction: &asc}
|
||||||
|
if err := json.Unmarshal(data, &ff); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal find filter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newDir := strings.ToUpper(*ff.Direction)
|
||||||
|
ff.Direction = &newDir
|
||||||
|
|
||||||
|
type findFilterRewrite struct {
|
||||||
|
Q *string `json:"q"`
|
||||||
|
Page *int `json:"page"`
|
||||||
|
PerPage *int `json:"per_page"`
|
||||||
|
Sort *string `json:"sort"`
|
||||||
|
Direction *string `json:"direction"`
|
||||||
|
}
|
||||||
|
|
||||||
|
fr := findFilterRewrite(ff)
|
||||||
|
|
||||||
|
return json.Marshal(fr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *schema49Migrator) getObjectFilter(mode models.FilterMode, data json.RawMessage) (json.RawMessage, error) {
|
||||||
|
type criteriaJson struct {
|
||||||
|
Criteria []string `json:"c"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var c criteriaJson
|
||||||
|
if err := json.Unmarshal(data, &c); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal object filter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := make(map[string]interface{})
|
||||||
|
for _, raw := range c.Criteria {
|
||||||
|
if err := m.convertCriterion(mode, ret, raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *schema49Migrator) convertCriterion(mode models.FilterMode, out map[string]interface{}, criterion string) error {
|
||||||
|
// convert to a map
|
||||||
|
ret := make(map[string]interface{})
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(criterion), &ret); err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal criterion: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
field := ret["type"].(string)
|
||||||
|
// Some names are deprecated
|
||||||
|
if newFieldName, ok := migrate49NameChanges[field]; ok {
|
||||||
|
field = newFieldName
|
||||||
|
}
|
||||||
|
delete(ret, "type")
|
||||||
|
|
||||||
|
// Find out whether the object needs some adjustment/has non-string content attached
|
||||||
|
// Only adjust if value is present
|
||||||
|
if v, ok := ret["value"]; ok && v != nil {
|
||||||
|
var err error
|
||||||
|
switch {
|
||||||
|
case arrayContains(migrate49TypeResolution["Boolean"], field):
|
||||||
|
ret["value"], err = m.adjustCriterionValue(ret["value"], "bool")
|
||||||
|
case arrayContains(migrate49TypeResolution["Int"], field):
|
||||||
|
ret["value"], err = m.adjustCriterionValue(ret["value"], "int")
|
||||||
|
case arrayContains(migrate49TypeResolution["Float"], field):
|
||||||
|
ret["value"], err = m.adjustCriterionValue(ret["value"], "float64")
|
||||||
|
case arrayContains(migrate49TypeResolution["Object"], field):
|
||||||
|
ret["value"], err = m.adjustCriterionValue(ret["value"], "object")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to adjust criterion value for %q: %w", field, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out[field] = ret
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func arrayContains(sl []string, name string) bool {
|
||||||
|
for _, value := range sl {
|
||||||
|
if value == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// General Function for converting the types inside a criterion
|
||||||
|
func (m *schema49Migrator) adjustCriterionValue(value interface{}, typ string) (interface{}, error) {
|
||||||
|
if mapvalue, ok := value.(map[string]interface{}); ok {
|
||||||
|
// Primitive values and lists of them
|
||||||
|
var err error
|
||||||
|
for _, next := range []string{"value", "value2"} {
|
||||||
|
if valmap, ok := mapvalue[next].([]string); ok {
|
||||||
|
var valNewMap []interface{}
|
||||||
|
for index, v := range valmap {
|
||||||
|
valNewMap[index], err = m.convertValue(v, typ)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mapvalue[next] = valNewMap
|
||||||
|
} else if _, ok := mapvalue[next]; ok {
|
||||||
|
mapvalue[next], err = m.convertValue(mapvalue[next], typ)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Items
|
||||||
|
for _, next := range []string{"items", "excluded"} {
|
||||||
|
if _, ok := mapvalue[next]; ok {
|
||||||
|
mapvalue[next], err = m.adjustCriterionItem(mapvalue[next])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Those Values are always Int
|
||||||
|
for _, next := range []string{"Distance", "Depth"} {
|
||||||
|
if _, ok := mapvalue[next]; ok {
|
||||||
|
mapvalue[next], err = strconv.ParseInt(mapvalue[next].(string), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mapvalue, nil
|
||||||
|
} else if _, ok := value.(string); ok {
|
||||||
|
// Singular Primitive Values
|
||||||
|
return m.convertValue(value, typ)
|
||||||
|
} else if listvalue, ok := value.([]interface{}); ok {
|
||||||
|
// Items as a singular value, as well as singular lists
|
||||||
|
var err error
|
||||||
|
if typ == "object" {
|
||||||
|
value, err = m.adjustCriterionItem(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for index, val := range listvalue {
|
||||||
|
listvalue[index], err = m.convertValue(val, typ)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value = listvalue
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, nil
|
||||||
|
} else if _, ok := value.(int); ok {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("could not recognize format of value %v", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts values inside a criterion that represent some objects, like performer or studio.
|
||||||
|
func (m *schema49Migrator) adjustCriterionItem(value interface{}) (interface{}, error) {
|
||||||
|
// Basically, this first converts step by step the value, after that it adjusts id and Depth (of parent/child studios) to int
|
||||||
|
if itemlist, ok := value.([]interface{}); ok {
|
||||||
|
var itemNewList []interface{}
|
||||||
|
for _, val := range itemlist {
|
||||||
|
if val, ok := val.(map[string]interface{}); ok {
|
||||||
|
newItem := make(map[string]interface{})
|
||||||
|
for index, v := range val {
|
||||||
|
if v, ok := v.(string); ok {
|
||||||
|
switch index {
|
||||||
|
case "id":
|
||||||
|
if formattedOut, ok := strconv.ParseInt(v, 10, 64); ok == nil {
|
||||||
|
newItem["id"] = formattedOut
|
||||||
|
}
|
||||||
|
case "Depth":
|
||||||
|
if formattedOut, ok := strconv.ParseInt(v, 10, 64); ok == nil {
|
||||||
|
newItem["Depth"] = formattedOut
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
newItem[index] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemNewList = append(itemNewList, newItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return itemNewList, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("could not recognize %v as an item list", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts a value of type string to its according type, given by string
|
||||||
|
func (m *schema49Migrator) convertValue(value interface{}, typ string) (interface{}, error) {
|
||||||
|
valueType := reflect.TypeOf(value).Name()
|
||||||
|
if typ == valueType || (typ == "int" && valueType == "float64") || (typ == "float64" && valueType == "int") {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := value.(string); ok {
|
||||||
|
switch typ {
|
||||||
|
case "float64":
|
||||||
|
return strconv.ParseFloat(val, 64)
|
||||||
|
case "int":
|
||||||
|
return strconv.ParseInt(val, 10, 64)
|
||||||
|
case "bool":
|
||||||
|
return strconv.ParseBool(val)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("no valid conversion type for %v, need bool, int or float64", typ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("cannot convert %v (%T) to %s", value, value, typ)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sqlite.RegisterPostMigration(49, post49)
|
||||||
|
}
|
||||||
34
pkg/sqlite/migrations/49_saved_filter_refactor.up.sql
Normal file
34
pkg/sqlite/migrations/49_saved_filter_refactor.up.sql
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
|
||||||
|
-- remove filter column
|
||||||
|
CREATE TABLE `saved_filters_new` (
|
||||||
|
`id` integer not null primary key autoincrement,
|
||||||
|
`name` varchar(510) not null,
|
||||||
|
`mode` varchar(255) not null,
|
||||||
|
`find_filter` blob,
|
||||||
|
`object_filter` blob,
|
||||||
|
`ui_options` blob
|
||||||
|
);
|
||||||
|
|
||||||
|
-- move filter data into find_filter to be migrated in the post-migration
|
||||||
|
INSERT INTO `saved_filters_new`
|
||||||
|
(
|
||||||
|
`id`,
|
||||||
|
`name`,
|
||||||
|
`mode`,
|
||||||
|
`find_filter`
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
`id`,
|
||||||
|
`name`,
|
||||||
|
`mode`,
|
||||||
|
`filter`
|
||||||
|
FROM `saved_filters`;
|
||||||
|
|
||||||
|
DROP INDEX `index_saved_filters_on_mode_name_unique`;
|
||||||
|
DROP TABLE `saved_filters`;
|
||||||
|
ALTER TABLE `saved_filters_new` rename to `saved_filters`;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX `index_saved_filters_on_mode_name_unique` on `saved_filters` (`mode`, `name`);
|
||||||
|
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
|
@ -291,7 +291,7 @@ func TestMovieUpdateFrontImage(t *testing.T) {
|
||||||
// create movie to test against
|
// create movie to test against
|
||||||
const name = "TestMovieUpdateMovieImages"
|
const name = "TestMovieUpdateMovieImages"
|
||||||
movie := models.Movie{
|
movie := models.Movie{
|
||||||
Name: name,
|
Name: name,
|
||||||
}
|
}
|
||||||
err := qb.Create(ctx, &movie)
|
err := qb.Create(ctx, &movie)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -311,7 +311,7 @@ func TestMovieUpdateBackImage(t *testing.T) {
|
||||||
// create movie to test against
|
// create movie to test against
|
||||||
const name = "TestMovieUpdateMovieImages"
|
const name = "TestMovieUpdateMovieImages"
|
||||||
movie := models.Movie{
|
movie := models.Movie{
|
||||||
Name: name,
|
Name: name,
|
||||||
}
|
}
|
||||||
err := qb.Create(ctx, &movie)
|
err := qb.Create(ctx, &movie)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package sqlite
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
|
@ -10,6 +11,7 @@ import (
|
||||||
"github.com/doug-martin/goqu/v9/exp"
|
"github.com/doug-martin/goqu/v9/exp"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
||||||
)
|
)
|
||||||
|
|
@ -20,25 +22,67 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type savedFilterRow struct {
|
type savedFilterRow struct {
|
||||||
ID int `db:"id" goqu:"skipinsert"`
|
ID int `db:"id" goqu:"skipinsert"`
|
||||||
Mode string `db:"mode"`
|
Mode models.FilterMode `db:"mode"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
Filter string `db:"filter"`
|
FindFilter string `db:"find_filter"`
|
||||||
|
ObjectFilter string `db:"object_filter"`
|
||||||
|
UIOptions string `db:"ui_options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeJSONOrEmpty(v interface{}) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("error encoding json %v: %v", v, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeJSON(s string, v interface{}) {
|
||||||
|
if s == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(s), v); err != nil {
|
||||||
|
logger.Errorf("error decoding json %q: %v", s, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *savedFilterRow) fromSavedFilter(o models.SavedFilter) {
|
func (r *savedFilterRow) fromSavedFilter(o models.SavedFilter) {
|
||||||
r.ID = o.ID
|
r.ID = o.ID
|
||||||
r.Mode = string(o.Mode)
|
r.Mode = o.Mode
|
||||||
r.Name = o.Name
|
r.Name = o.Name
|
||||||
r.Filter = o.Filter
|
|
||||||
|
// encode the filters as json
|
||||||
|
r.FindFilter = encodeJSONOrEmpty(o.FindFilter)
|
||||||
|
r.ObjectFilter = encodeJSONOrEmpty(o.ObjectFilter)
|
||||||
|
r.UIOptions = encodeJSONOrEmpty(o.UIOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *savedFilterRow) resolve() *models.SavedFilter {
|
func (r *savedFilterRow) resolve() *models.SavedFilter {
|
||||||
ret := &models.SavedFilter{
|
ret := &models.SavedFilter{
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
Name: r.Name,
|
Mode: r.Mode,
|
||||||
Mode: models.FilterMode(r.Mode),
|
Name: r.Name,
|
||||||
Filter: r.Filter,
|
}
|
||||||
|
|
||||||
|
// decode the filters from json
|
||||||
|
if r.FindFilter != "" {
|
||||||
|
ret.FindFilter = &models.FindFilterType{}
|
||||||
|
decodeJSON(r.FindFilter, &ret.FindFilter)
|
||||||
|
}
|
||||||
|
if r.ObjectFilter != "" {
|
||||||
|
ret.ObjectFilter = make(map[string]interface{})
|
||||||
|
decodeJSON(r.ObjectFilter, &ret.ObjectFilter)
|
||||||
|
}
|
||||||
|
if r.UIOptions != "" {
|
||||||
|
ret.UIOptions = make(map[string]interface{})
|
||||||
|
decodeJSON(r.UIOptions, &ret.UIOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
@ -46,7 +90,6 @@ func (r *savedFilterRow) resolve() *models.SavedFilter {
|
||||||
|
|
||||||
type SavedFilterStore struct {
|
type SavedFilterStore struct {
|
||||||
repository
|
repository
|
||||||
|
|
||||||
tableMgr *table
|
tableMgr *table
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,7 +120,7 @@ func (qb *SavedFilterStore) Create(ctx context.Context, newObject *models.SavedF
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
updated, err := qb.find(ctx, id)
|
updated, err := qb.Find(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("finding after create: %w", err)
|
return fmt.Errorf("finding after create: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +209,6 @@ func (qb *SavedFilterStore) find(ctx context.Context, id int) (*models.SavedFilt
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns nil, sql.ErrNoRows if not found
|
|
||||||
func (qb *SavedFilterStore) get(ctx context.Context, q *goqu.SelectDataset) (*models.SavedFilter, error) {
|
func (qb *SavedFilterStore) get(ctx context.Context, q *goqu.SelectDataset) (*models.SavedFilter, error) {
|
||||||
ret, err := qb.getMany(ctx, q)
|
ret, err := qb.getMany(ctx, q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -42,15 +42,35 @@ func TestSavedFilterFindByMode(t *testing.T) {
|
||||||
|
|
||||||
func TestSavedFilterDestroy(t *testing.T) {
|
func TestSavedFilterDestroy(t *testing.T) {
|
||||||
const filterName = "filterToDestroy"
|
const filterName = "filterToDestroy"
|
||||||
const testFilter = "{}"
|
filterQ := ""
|
||||||
|
filterPage := 1
|
||||||
|
filterPerPage := 40
|
||||||
|
filterSort := "date"
|
||||||
|
filterDirection := models.SortDirectionEnumAsc
|
||||||
|
findFilter := models.FindFilterType{
|
||||||
|
Q: &filterQ,
|
||||||
|
Page: &filterPage,
|
||||||
|
PerPage: &filterPerPage,
|
||||||
|
Sort: &filterSort,
|
||||||
|
Direction: &filterDirection,
|
||||||
|
}
|
||||||
|
objectFilter := map[string]interface{}{
|
||||||
|
"test": "foo",
|
||||||
|
}
|
||||||
|
uiOptions := map[string]interface{}{
|
||||||
|
"display_mode": 1,
|
||||||
|
"zoom_index": 1,
|
||||||
|
}
|
||||||
var id int
|
var id int
|
||||||
|
|
||||||
// create the saved filter to destroy
|
// create the saved filter to destroy
|
||||||
withTxn(func(ctx context.Context) error {
|
withTxn(func(ctx context.Context) error {
|
||||||
newFilter := models.SavedFilter{
|
newFilter := models.SavedFilter{
|
||||||
Name: filterName,
|
Name: filterName,
|
||||||
Mode: models.FilterModeScenes,
|
Mode: models.FilterModeScenes,
|
||||||
Filter: testFilter,
|
FindFilter: &findFilter,
|
||||||
|
ObjectFilter: objectFilter,
|
||||||
|
UIOptions: uiOptions,
|
||||||
}
|
}
|
||||||
err := db.SavedFilter.Create(ctx, &newFilter)
|
err := db.SavedFilter.Create(ctx, &newFilter)
|
||||||
|
|
||||||
|
|
@ -88,12 +108,32 @@ func TestSavedFilterFindDefault(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSavedFilterSetDefault(t *testing.T) {
|
func TestSavedFilterSetDefault(t *testing.T) {
|
||||||
const newFilter = "foo"
|
filterQ := ""
|
||||||
|
filterPage := 1
|
||||||
|
filterPerPage := 40
|
||||||
|
filterSort := "date"
|
||||||
|
filterDirection := models.SortDirectionEnumAsc
|
||||||
|
findFilter := models.FindFilterType{
|
||||||
|
Q: &filterQ,
|
||||||
|
Page: &filterPage,
|
||||||
|
PerPage: &filterPerPage,
|
||||||
|
Sort: &filterSort,
|
||||||
|
Direction: &filterDirection,
|
||||||
|
}
|
||||||
|
objectFilter := map[string]interface{}{
|
||||||
|
"test": "foo",
|
||||||
|
}
|
||||||
|
uiOptions := map[string]interface{}{
|
||||||
|
"display_mode": 1,
|
||||||
|
"zoom_index": 1,
|
||||||
|
}
|
||||||
|
|
||||||
withTxn(func(ctx context.Context) error {
|
withTxn(func(ctx context.Context) error {
|
||||||
err := db.SavedFilter.SetDefault(ctx, &models.SavedFilter{
|
err := db.SavedFilter.SetDefault(ctx, &models.SavedFilter{
|
||||||
Mode: models.FilterModeMovies,
|
Mode: models.FilterModeMovies,
|
||||||
Filter: newFilter,
|
FindFilter: &findFilter,
|
||||||
|
ObjectFilter: objectFilter,
|
||||||
|
UIOptions: uiOptions,
|
||||||
})
|
})
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|
@ -104,7 +144,7 @@ func TestSavedFilterSetDefault(t *testing.T) {
|
||||||
def, err := db.SavedFilter.FindDefault(ctx, models.FilterModeMovies)
|
def, err := db.SavedFilter.FindDefault(ctx, models.FilterModeMovies)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defID = def.ID
|
defID = def.ID
|
||||||
assert.Equal(t, newFilter, def.Filter)
|
assert.Equal(t, &findFilter, def.FindFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -1714,10 +1714,29 @@ func getSavedFilterName(index int) string {
|
||||||
|
|
||||||
func createSavedFilters(ctx context.Context, qb models.SavedFilterReaderWriter, n int) error {
|
func createSavedFilters(ctx context.Context, qb models.SavedFilterReaderWriter, n int) error {
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
|
filterQ := ""
|
||||||
|
filterPage := i
|
||||||
|
filterPerPage := i * 40
|
||||||
|
filterSort := "date"
|
||||||
|
filterDirection := models.SortDirectionEnumAsc
|
||||||
|
findFilter := models.FindFilterType{
|
||||||
|
Q: &filterQ,
|
||||||
|
Page: &filterPage,
|
||||||
|
PerPage: &filterPerPage,
|
||||||
|
Sort: &filterSort,
|
||||||
|
Direction: &filterDirection,
|
||||||
|
}
|
||||||
savedFilter := models.SavedFilter{
|
savedFilter := models.SavedFilter{
|
||||||
Mode: getSavedFilterMode(i),
|
Mode: getSavedFilterMode(i),
|
||||||
Name: getSavedFilterName(i),
|
Name: getSavedFilterName(i),
|
||||||
Filter: getPrefixedStringValue("savedFilter", i, "Filter"),
|
FindFilter: &findFilter,
|
||||||
|
ObjectFilter: map[string]interface{}{
|
||||||
|
"test": "object",
|
||||||
|
},
|
||||||
|
UIOptions: map[string]interface{}{
|
||||||
|
"display_mode": 1,
|
||||||
|
"zoom_index": 1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := qb.Create(ctx, &savedFilter)
|
err := qb.Create(ctx, &savedFilter)
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
"prefer-destructuring": ["error", { "object": true, "array": false }],
|
"prefer-destructuring": ["error", { "object": true, "array": false }],
|
||||||
"@typescript-eslint/no-use-before-define": [
|
"@typescript-eslint/no-use-before-define": [
|
||||||
"error",
|
"error",
|
||||||
{ "functions": false, "classes": true }
|
{ "functions": false, "classes": false }
|
||||||
],
|
],
|
||||||
"no-nested-ternary": "off"
|
"no-nested-ternary": "off"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,11 +105,11 @@ const SavedFilterResults: React.FC<ISavedFilterResults> = ({
|
||||||
const filter = useMemo(() => {
|
const filter = useMemo(() => {
|
||||||
if (!data?.findSavedFilter) return;
|
if (!data?.findSavedFilter) return;
|
||||||
|
|
||||||
const { mode, filter: filterJSON } = data.findSavedFilter;
|
const { mode } = data.findSavedFilter;
|
||||||
|
|
||||||
const ret = new ListFilterModel(mode, config);
|
const ret = new ListFilterModel(mode, config);
|
||||||
ret.currentPage = 1;
|
ret.currentPage = 1;
|
||||||
ret.configureFromJSON(filterJSON);
|
ret.configureFromSavedFilter(data.findSavedFilter);
|
||||||
ret.randomSeed = -1;
|
ret.randomSeed = -1;
|
||||||
return ret;
|
return ret;
|
||||||
}, [data?.findSavedFilter, config]);
|
}, [data?.findSavedFilter, config]);
|
||||||
|
|
|
||||||
|
|
@ -270,11 +270,11 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
|
||||||
if (existing) {
|
if (existing) {
|
||||||
setCriterion(existing);
|
setCriterion(existing);
|
||||||
} else {
|
} else {
|
||||||
const newCriterion = makeCriteria(configuration, option.type);
|
const newCriterion = makeCriteria(filter.mode, option.type);
|
||||||
setCriterion(newCriterion);
|
setCriterion(newCriterion);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[criteria, configuration]
|
[filter.mode, criteria]
|
||||||
);
|
);
|
||||||
|
|
||||||
const ui = (configuration?.ui ?? {}) as IUIConfig;
|
const ui = (configuration?.ui ?? {}) as IUIConfig;
|
||||||
|
|
|
||||||
|
|
@ -13,20 +13,19 @@ interface IHierarchicalLabelValueFilterProps {
|
||||||
export const HierarchicalLabelValueFilter: React.FC<
|
export const HierarchicalLabelValueFilter: React.FC<
|
||||||
IHierarchicalLabelValueFilterProps
|
IHierarchicalLabelValueFilterProps
|
||||||
> = ({ criterion, onValueChanged }) => {
|
> = ({ criterion, onValueChanged }) => {
|
||||||
|
const { criterionOption } = criterion;
|
||||||
|
const { type, inputType } = criterionOption;
|
||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
criterion.criterionOption.type !== "performers" &&
|
inputType !== "studios" &&
|
||||||
criterion.criterionOption.type !== "studios" &&
|
inputType !== "tags" &&
|
||||||
criterion.criterionOption.type !== "parent_studios" &&
|
inputType !== "scene_tags" &&
|
||||||
criterion.criterionOption.type !== "tags" &&
|
inputType !== "performer_tags"
|
||||||
criterion.criterionOption.type !== "sceneTags" &&
|
) {
|
||||||
criterion.criterionOption.type !== "performerTags" &&
|
|
||||||
criterion.criterionOption.type !== "parentTags" &&
|
|
||||||
criterion.criterionOption.type !== "childTags" &&
|
|
||||||
criterion.criterionOption.type !== "movies"
|
|
||||||
)
|
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
studio_depth: {
|
studio_depth: {
|
||||||
|
|
@ -51,10 +50,10 @@ export const HierarchicalLabelValueFilter: React.FC<
|
||||||
}
|
}
|
||||||
|
|
||||||
function criterionOptionTypeToIncludeID(): string {
|
function criterionOptionTypeToIncludeID(): string {
|
||||||
if (criterion.criterionOption.type === "studios") {
|
if (inputType === "studios") {
|
||||||
return "include-sub-studios";
|
return "include-sub-studios";
|
||||||
}
|
}
|
||||||
if (criterion.criterionOption.type === "childTags") {
|
if (type === "children") {
|
||||||
return "include-parent-tags";
|
return "include-parent-tags";
|
||||||
}
|
}
|
||||||
return "include-sub-tags";
|
return "include-sub-tags";
|
||||||
|
|
@ -62,9 +61,9 @@ export const HierarchicalLabelValueFilter: React.FC<
|
||||||
|
|
||||||
function criterionOptionTypeToIncludeUIString(): MessageDescriptor {
|
function criterionOptionTypeToIncludeUIString(): MessageDescriptor {
|
||||||
const optionType =
|
const optionType =
|
||||||
criterion.criterionOption.type === "studios"
|
inputType === "studios"
|
||||||
? "include_sub_studios"
|
? "include_sub_studios"
|
||||||
: criterion.criterionOption.type === "childTags"
|
: type === "children"
|
||||||
? "include_parent_tags"
|
? "include_parent_tags"
|
||||||
: "include_sub_tags";
|
: "include_sub_tags";
|
||||||
return {
|
return {
|
||||||
|
|
@ -76,7 +75,7 @@ export const HierarchicalLabelValueFilter: React.FC<
|
||||||
<>
|
<>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<FilterSelect
|
<FilterSelect
|
||||||
type={criterion.criterionOption.type}
|
type={inputType}
|
||||||
isMulti
|
isMulti
|
||||||
onSelect={onSelectionChanged}
|
onSelect={onSelectionChanged}
|
||||||
ids={criterion.value.items.map((labeled) => labeled.id)}
|
ids={criterion.value.items.map((labeled) => labeled.id)}
|
||||||
|
|
|
||||||
|
|
@ -13,18 +13,19 @@ export const LabeledIdFilter: React.FC<ILabeledIdFilterProps> = ({
|
||||||
criterion,
|
criterion,
|
||||||
onValueChanged,
|
onValueChanged,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { criterionOption } = criterion;
|
||||||
|
const { inputType } = criterionOption;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
criterion.criterionOption.type !== "performers" &&
|
inputType !== "performers" &&
|
||||||
criterion.criterionOption.type !== "studios" &&
|
inputType !== "studios" &&
|
||||||
criterion.criterionOption.type !== "parent_studios" &&
|
inputType !== "scene_tags" &&
|
||||||
criterion.criterionOption.type !== "tags" &&
|
inputType !== "performer_tags" &&
|
||||||
criterion.criterionOption.type !== "sceneTags" &&
|
inputType !== "tags" &&
|
||||||
criterion.criterionOption.type !== "performerTags" &&
|
inputType !== "movies"
|
||||||
criterion.criterionOption.type !== "parentTags" &&
|
) {
|
||||||
criterion.criterionOption.type !== "childTags" &&
|
|
||||||
criterion.criterionOption.type !== "movies"
|
|
||||||
)
|
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function onSelectionChanged(items: SelectObject[]) {
|
function onSelectionChanged(items: SelectObject[]) {
|
||||||
onValueChanged(
|
onValueChanged(
|
||||||
|
|
@ -38,7 +39,7 @@ export const LabeledIdFilter: React.FC<ILabeledIdFilterProps> = ({
|
||||||
return (
|
return (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<FilterSelect
|
<FilterSelect
|
||||||
type={criterion.criterionOption.type}
|
type={inputType}
|
||||||
isMulti
|
isMulti
|
||||||
onSelect={onSelectionChanged}
|
onSelect={onSelectionChanged}
|
||||||
ids={criterion.value.map((labeled) => labeled.id)}
|
ids={criterion.value.map((labeled) => labeled.id)}
|
||||||
|
|
|
||||||
|
|
@ -320,7 +320,7 @@ export const HierarchicalObjectsFilter = <
|
||||||
if (criterion.criterionOption.type === "studios") {
|
if (criterion.criterionOption.type === "studios") {
|
||||||
return "include-sub-studios";
|
return "include-sub-studios";
|
||||||
}
|
}
|
||||||
if (criterion.criterionOption.type === "childTags") {
|
if (criterion.criterionOption.type === "children") {
|
||||||
return "include-parent-tags";
|
return "include-parent-tags";
|
||||||
}
|
}
|
||||||
return "include-sub-tags";
|
return "include-sub-tags";
|
||||||
|
|
@ -330,7 +330,7 @@ export const HierarchicalObjectsFilter = <
|
||||||
const optionType =
|
const optionType =
|
||||||
criterion.criterionOption.type === "studios"
|
criterion.criterionOption.type === "studios"
|
||||||
? "include_sub_studios"
|
? "include_sub_studios"
|
||||||
: criterion.criterionOption.type === "childTags"
|
: criterion.criterionOption.type === "children"
|
||||||
? "include_parent_tags"
|
? "include_parent_tags"
|
||||||
: "include_sub_tags";
|
: "include_sub_tags";
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -619,8 +619,8 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
|
||||||
if (defaultFilter?.findDefaultFilter) {
|
if (defaultFilter?.findDefaultFilter) {
|
||||||
newFilter.currentPage = 1;
|
newFilter.currentPage = 1;
|
||||||
try {
|
try {
|
||||||
newFilter.configureFromJSON(
|
newFilter.configureFromSavedFilter(
|
||||||
defaultFilter.findDefaultFilter.filter
|
defaultFilter.findDefaultFilter
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,9 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
||||||
id,
|
id,
|
||||||
mode: filter.mode,
|
mode: filter.mode,
|
||||||
name,
|
name,
|
||||||
filter: filterCopy.makeSavedFilterJSON(),
|
find_filter: filterCopy.makeFindFilter(),
|
||||||
|
object_filter: filterCopy.makeSavedFindFilter(),
|
||||||
|
ui_options: filterCopy.makeUIOptions(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -143,7 +145,9 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
mode: filter.mode,
|
mode: filter.mode,
|
||||||
filter: filterCopy.makeSavedFilterJSON(),
|
find_filter: filterCopy.makeFindFilter(),
|
||||||
|
object_filter: filterCopy.makeSavedFindFilter(),
|
||||||
|
ui_options: filterCopy.makeUIOptions(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -166,7 +170,7 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
||||||
newFilter.currentPage = 1;
|
newFilter.currentPage = 1;
|
||||||
// #1795 - reset search term if not present in saved filter
|
// #1795 - reset search term if not present in saved filter
|
||||||
newFilter.searchTerm = "";
|
newFilter.searchTerm = "";
|
||||||
newFilter.configureFromJSON(f.filter);
|
newFilter.configureFromSavedFilter(f);
|
||||||
// #1507 - reset random seed when loaded
|
// #1507 - reset random seed when loaded
|
||||||
newFilter.randomSeed = -1;
|
newFilter.randomSeed = -1;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,9 @@ interface ITypeProps {
|
||||||
type?:
|
type?:
|
||||||
| "performers"
|
| "performers"
|
||||||
| "studios"
|
| "studios"
|
||||||
| "parent_studios"
|
|
||||||
| "tags"
|
| "tags"
|
||||||
| "sceneTags"
|
| "scene_tags"
|
||||||
| "performerTags"
|
| "performer_tags"
|
||||||
| "parentTags"
|
|
||||||
| "childTags"
|
|
||||||
| "movies";
|
| "movies";
|
||||||
}
|
}
|
||||||
interface IFilterProps {
|
interface IFilterProps {
|
||||||
|
|
@ -865,7 +862,7 @@ export const TagSelect: React.FC<
|
||||||
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) => {
|
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) => {
|
||||||
if (props.type === "performers") {
|
if (props.type === "performers") {
|
||||||
return <PerformerSelect {...props} creatable={false} />;
|
return <PerformerSelect {...props} creatable={false} />;
|
||||||
} else if (props.type === "studios" || props.type === "parent_studios") {
|
} else if (props.type === "studios") {
|
||||||
return <StudioSelect {...props} creatable={false} />;
|
return <StudioSelect {...props} creatable={false} />;
|
||||||
} else if (props.type === "movies") {
|
} else if (props.type === "movies") {
|
||||||
return <MovieSelect {...props} creatable={false} />;
|
return <MovieSelect {...props} creatable={false} />;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export const StudioChildrenPanel: React.FC<IStudioChildrenPanel> = ({
|
||||||
const studioValue = { id: studio.id!, label: studio.name! };
|
const studioValue = { id: studio.id!, label: studio.name! };
|
||||||
// if studio is already present, then we modify it, otherwise add
|
// if studio is already present, then we modify it, otherwise add
|
||||||
let parentStudioCriterion = filter.criteria.find((c) => {
|
let parentStudioCriterion = filter.criteria.find((c) => {
|
||||||
return c.criterionOption.type === "parent_studios";
|
return c.criterionOption.type === "parents";
|
||||||
}) as ParentStudiosCriterion;
|
}) as ParentStudiosCriterion;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -877,7 +877,7 @@
|
||||||
"path": "Sti",
|
"path": "Sti",
|
||||||
"perceptual_similarity": "Perceptuel lighed (phash)",
|
"perceptual_similarity": "Perceptuel lighed (phash)",
|
||||||
"performer": "Kunstner",
|
"performer": "Kunstner",
|
||||||
"performerTags": "Kunstner Tags",
|
"performer_tags": "Kunstner Tags",
|
||||||
"performer_age": "kunstnere Alder",
|
"performer_age": "kunstnere Alder",
|
||||||
"performer_count": "Kunstner Antal",
|
"performer_count": "Kunstner Antal",
|
||||||
"performer_favorite": "Foretrukken optrædende",
|
"performer_favorite": "Foretrukken optrædende",
|
||||||
|
|
@ -927,7 +927,7 @@
|
||||||
"resolution": "Opløsning",
|
"resolution": "Opløsning",
|
||||||
"scene": "Scene",
|
"scene": "Scene",
|
||||||
"sceneTagger": "Scenetagger",
|
"sceneTagger": "Scenetagger",
|
||||||
"sceneTags": "Scene-etiketter",
|
"scene_tags": "Scene-etiketter",
|
||||||
"scene_count": "Scene antal",
|
"scene_count": "Scene antal",
|
||||||
"scene_id": "Scene-id",
|
"scene_id": "Scene-id",
|
||||||
"scenes": "Scener",
|
"scenes": "Scener",
|
||||||
|
|
|
||||||
|
|
@ -1033,7 +1033,7 @@
|
||||||
"penis_length_cm": "Penislänge (cm)",
|
"penis_length_cm": "Penislänge (cm)",
|
||||||
"perceptual_similarity": "Wahrnehmungsähnlichkeit (phash)",
|
"perceptual_similarity": "Wahrnehmungsähnlichkeit (phash)",
|
||||||
"performer": "Darsteller",
|
"performer": "Darsteller",
|
||||||
"performerTags": "Darsteller-Tags",
|
"performer_tags": "Darsteller-Tags",
|
||||||
"performer_age": "Alter der Darsteller",
|
"performer_age": "Alter der Darsteller",
|
||||||
"performer_count": "Darstelleranzahl",
|
"performer_count": "Darstelleranzahl",
|
||||||
"performer_favorite": "Darsteller favorisiert",
|
"performer_favorite": "Darsteller favorisiert",
|
||||||
|
|
@ -1088,7 +1088,7 @@
|
||||||
"resume_time": "Zeit fortsetzen",
|
"resume_time": "Zeit fortsetzen",
|
||||||
"scene": "Szene",
|
"scene": "Szene",
|
||||||
"sceneTagger": "Szenen-Tagger",
|
"sceneTagger": "Szenen-Tagger",
|
||||||
"sceneTags": "Szenen-Tags",
|
"scene_tags": "Szenen-Tags",
|
||||||
"scene_code": "Studio Code",
|
"scene_code": "Studio Code",
|
||||||
"scene_count": "Szenenanzahl",
|
"scene_count": "Szenenanzahl",
|
||||||
"scene_created_at": "Szene angelegt am",
|
"scene_created_at": "Szene angelegt am",
|
||||||
|
|
|
||||||
|
|
@ -1075,7 +1075,7 @@
|
||||||
"penis_length_cm": "Penis Length (cm)",
|
"penis_length_cm": "Penis Length (cm)",
|
||||||
"perceptual_similarity": "Perceptual Similarity (phash)",
|
"perceptual_similarity": "Perceptual Similarity (phash)",
|
||||||
"performer": "Performer",
|
"performer": "Performer",
|
||||||
"performerTags": "Performer Tags",
|
"performer_tags": "Performer Tags",
|
||||||
"performer_age": "Performer Age",
|
"performer_age": "Performer Age",
|
||||||
"performer_count": "Performer Count",
|
"performer_count": "Performer Count",
|
||||||
"performer_favorite": "Performer Favourited",
|
"performer_favorite": "Performer Favourited",
|
||||||
|
|
@ -1130,7 +1130,7 @@
|
||||||
"resume_time": "Resume Time",
|
"resume_time": "Resume Time",
|
||||||
"scene": "Scene",
|
"scene": "Scene",
|
||||||
"sceneTagger": "Scene Tagger",
|
"sceneTagger": "Scene Tagger",
|
||||||
"sceneTags": "Scene Tags",
|
"scene_tags": "Scene Tags",
|
||||||
"scene_code": "Studio Code",
|
"scene_code": "Studio Code",
|
||||||
"scene_count": "Scene Count",
|
"scene_count": "Scene Count",
|
||||||
"scene_created_at": "Scene Created At",
|
"scene_created_at": "Scene Created At",
|
||||||
|
|
@ -1355,4 +1355,4 @@
|
||||||
"weight_kg": "Weight (kg)",
|
"weight_kg": "Weight (kg)",
|
||||||
"years_old": "years old",
|
"years_old": "years old",
|
||||||
"zip_file_count": "Zip File Count"
|
"zip_file_count": "Zip File Count"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -929,7 +929,7 @@
|
||||||
"path": "Ruta",
|
"path": "Ruta",
|
||||||
"perceptual_similarity": "Similaridad perceptiva (phash)",
|
"perceptual_similarity": "Similaridad perceptiva (phash)",
|
||||||
"performer": "Actriz/Actor",
|
"performer": "Actriz/Actor",
|
||||||
"performerTags": "Etiquetas de actriz/actor",
|
"performer_tags": "Etiquetas de actriz/actor",
|
||||||
"performer_age": "Edad de la actriz/actor",
|
"performer_age": "Edad de la actriz/actor",
|
||||||
"performer_count": "Número de actrices/actores",
|
"performer_count": "Número de actrices/actores",
|
||||||
"performer_favorite": "Actriz/actor favorita/o",
|
"performer_favorite": "Actriz/actor favorita/o",
|
||||||
|
|
@ -977,7 +977,7 @@
|
||||||
"resolution": "Resolución",
|
"resolution": "Resolución",
|
||||||
"scene": "Escena",
|
"scene": "Escena",
|
||||||
"sceneTagger": "Etiquetador de escenas",
|
"sceneTagger": "Etiquetador de escenas",
|
||||||
"sceneTags": "Etiquetas de escena",
|
"scene_tags": "Etiquetas de escena",
|
||||||
"scene_count": "Número de escenas",
|
"scene_count": "Número de escenas",
|
||||||
"scene_id": "Indentificador de escena",
|
"scene_id": "Indentificador de escena",
|
||||||
"scenes": "Escenas",
|
"scenes": "Escenas",
|
||||||
|
|
|
||||||
|
|
@ -1012,7 +1012,7 @@
|
||||||
"path": "Failitee",
|
"path": "Failitee",
|
||||||
"perceptual_similarity": "Tajutav Sarnasus (phash)",
|
"perceptual_similarity": "Tajutav Sarnasus (phash)",
|
||||||
"performer": "Näitleja",
|
"performer": "Näitleja",
|
||||||
"performerTags": "Näitleja Sildid",
|
"performer_tags": "Näitleja Sildid",
|
||||||
"performer_age": "Näitleja Vanus",
|
"performer_age": "Näitleja Vanus",
|
||||||
"performer_count": "Näitlejate Arv",
|
"performer_count": "Näitlejate Arv",
|
||||||
"performer_favorite": "Lemmiknäitleja",
|
"performer_favorite": "Lemmiknäitleja",
|
||||||
|
|
@ -1067,7 +1067,7 @@
|
||||||
"resume_time": "Jätkamisaeg",
|
"resume_time": "Jätkamisaeg",
|
||||||
"scene": "Stseen",
|
"scene": "Stseen",
|
||||||
"sceneTagger": "Stseeni Sildistaja",
|
"sceneTagger": "Stseeni Sildistaja",
|
||||||
"sceneTags": "Stseeni Sildid",
|
"scene_tags": "Stseeni Sildid",
|
||||||
"scene_code": "Stuudio Kood",
|
"scene_code": "Stuudio Kood",
|
||||||
"scene_count": "Stseenide Arv",
|
"scene_count": "Stseenide Arv",
|
||||||
"scene_created_at": "Stseen Loodud",
|
"scene_created_at": "Stseen Loodud",
|
||||||
|
|
|
||||||
|
|
@ -870,7 +870,7 @@
|
||||||
"path": "Polku",
|
"path": "Polku",
|
||||||
"perceptual_similarity": "Aistinvarainen samankaltaisuus (phash)",
|
"perceptual_similarity": "Aistinvarainen samankaltaisuus (phash)",
|
||||||
"performer": "Esiintyjä",
|
"performer": "Esiintyjä",
|
||||||
"performerTags": "Esiintyjien tunnisteet",
|
"performer_tags": "Esiintyjien tunnisteet",
|
||||||
"performer_age": "Esiintyjän ikä",
|
"performer_age": "Esiintyjän ikä",
|
||||||
"performer_count": "Esiintyjien määrä",
|
"performer_count": "Esiintyjien määrä",
|
||||||
"performer_favorite": "Esiintyjä suosikeissa",
|
"performer_favorite": "Esiintyjä suosikeissa",
|
||||||
|
|
@ -914,7 +914,7 @@
|
||||||
"resolution": "Resoluutio",
|
"resolution": "Resoluutio",
|
||||||
"scene": "Kohtaus",
|
"scene": "Kohtaus",
|
||||||
"sceneTagger": "Kohtauksien tunnistetila",
|
"sceneTagger": "Kohtauksien tunnistetila",
|
||||||
"sceneTags": "Kohtauksen tunnisteet",
|
"scene_tags": "Kohtauksen tunnisteet",
|
||||||
"scene_code": "Studiokoodi",
|
"scene_code": "Studiokoodi",
|
||||||
"scene_count": "Kohtauksien määrä",
|
"scene_count": "Kohtauksien määrä",
|
||||||
"scene_created_at": "Kohtaus luotu",
|
"scene_created_at": "Kohtaus luotu",
|
||||||
|
|
|
||||||
|
|
@ -1072,7 +1072,7 @@
|
||||||
"penis_length_cm": "Longueur du pénis (cm)",
|
"penis_length_cm": "Longueur du pénis (cm)",
|
||||||
"perceptual_similarity": "Similitude perceptuelle (empreinte)",
|
"perceptual_similarity": "Similitude perceptuelle (empreinte)",
|
||||||
"performer": "Performeurs",
|
"performer": "Performeurs",
|
||||||
"performerTags": "Étiquettes de performeur",
|
"performer_tags": "Étiquettes de performeur",
|
||||||
"performer_age": "Âge du performeur",
|
"performer_age": "Âge du performeur",
|
||||||
"performer_count": "Nombre de performeurs",
|
"performer_count": "Nombre de performeurs",
|
||||||
"performer_favorite": "Performeur favori",
|
"performer_favorite": "Performeur favori",
|
||||||
|
|
@ -1127,7 +1127,7 @@
|
||||||
"resume_time": "Reprendre le temps",
|
"resume_time": "Reprendre le temps",
|
||||||
"scene": "Scène",
|
"scene": "Scène",
|
||||||
"sceneTagger": "Étiqueteuse de scènes",
|
"sceneTagger": "Étiqueteuse de scènes",
|
||||||
"sceneTags": "Étiquettes de la scène",
|
"scene_tags": "Étiquettes de la scène",
|
||||||
"scene_code": "Code studio",
|
"scene_code": "Code studio",
|
||||||
"scene_count": "Nombre de scènes",
|
"scene_count": "Nombre de scènes",
|
||||||
"scene_created_at": "Scène créée le",
|
"scene_created_at": "Scène créée le",
|
||||||
|
|
|
||||||
|
|
@ -437,7 +437,7 @@
|
||||||
"parent_tags": "Szülő-címkék",
|
"parent_tags": "Szülő-címkék",
|
||||||
"path": "Elérési Út",
|
"path": "Elérési Út",
|
||||||
"performer": "Szereplő",
|
"performer": "Szereplő",
|
||||||
"performerTags": "Szereplő Címkék",
|
"performer_tags": "Szereplő Címkék",
|
||||||
"performer_age": "Szereplő Kora",
|
"performer_age": "Szereplő Kora",
|
||||||
"performer_count": "Szereplők Száma",
|
"performer_count": "Szereplők Száma",
|
||||||
"performer_favorite": "Szereplő Kedvencek Közt",
|
"performer_favorite": "Szereplő Kedvencek Közt",
|
||||||
|
|
@ -460,7 +460,7 @@
|
||||||
"resolution": "Felbontás",
|
"resolution": "Felbontás",
|
||||||
"scene": "Jelenet",
|
"scene": "Jelenet",
|
||||||
"sceneTagger": "Jelenetcímkéző",
|
"sceneTagger": "Jelenetcímkéző",
|
||||||
"sceneTags": "Jelenetcímkék",
|
"scene_tags": "Jelenetcímkék",
|
||||||
"scene_count": "Jelenetszám",
|
"scene_count": "Jelenetszám",
|
||||||
"scene_id": "Jelenet ID",
|
"scene_id": "Jelenet ID",
|
||||||
"scenes": "Jelenetek",
|
"scenes": "Jelenetek",
|
||||||
|
|
|
||||||
|
|
@ -986,7 +986,7 @@
|
||||||
"resume_time": "Tempo Continuazione",
|
"resume_time": "Tempo Continuazione",
|
||||||
"scene": "Scena",
|
"scene": "Scena",
|
||||||
"sceneTagger": "Tagger Scena",
|
"sceneTagger": "Tagger Scena",
|
||||||
"sceneTags": "Tag Scena",
|
"scene_tags": "Tag Scena",
|
||||||
"scene_code": "Codice dello Studio",
|
"scene_code": "Codice dello Studio",
|
||||||
"scene_count": "Numero Scene",
|
"scene_count": "Numero Scene",
|
||||||
"scene_created_at": "Scena Creata Al",
|
"scene_created_at": "Scena Creata Al",
|
||||||
|
|
|
||||||
|
|
@ -940,7 +940,7 @@
|
||||||
"path": "パス",
|
"path": "パス",
|
||||||
"perceptual_similarity": "知覚的類似性 (phash)",
|
"perceptual_similarity": "知覚的類似性 (phash)",
|
||||||
"performer": "出演者",
|
"performer": "出演者",
|
||||||
"performerTags": "出演者タグ",
|
"performer_tags": "出演者タグ",
|
||||||
"performer_age": "出演者の年齢",
|
"performer_age": "出演者の年齢",
|
||||||
"performer_count": "出演者数",
|
"performer_count": "出演者数",
|
||||||
"performer_favorite": "出演者をお気に入り済み",
|
"performer_favorite": "出演者をお気に入り済み",
|
||||||
|
|
@ -995,7 +995,7 @@
|
||||||
"resume_time": "レジューム時間",
|
"resume_time": "レジューム時間",
|
||||||
"scene": "シーン",
|
"scene": "シーン",
|
||||||
"sceneTagger": "シーン一括タグ付け",
|
"sceneTagger": "シーン一括タグ付け",
|
||||||
"sceneTags": "シーンタグ",
|
"scene_tags": "シーンタグ",
|
||||||
"scene_code": "スタジオコード",
|
"scene_code": "スタジオコード",
|
||||||
"scene_count": "シーン数",
|
"scene_count": "シーン数",
|
||||||
"scene_created_at": "シーンの作成日時",
|
"scene_created_at": "シーンの作成日時",
|
||||||
|
|
|
||||||
|
|
@ -1022,7 +1022,7 @@
|
||||||
"penis_length_cm": "자지 크기 (cm)",
|
"penis_length_cm": "자지 크기 (cm)",
|
||||||
"perceptual_similarity": "유사도 (phash)",
|
"perceptual_similarity": "유사도 (phash)",
|
||||||
"performer": "배우",
|
"performer": "배우",
|
||||||
"performerTags": "배우 태그",
|
"performer_tags": "배우 태그",
|
||||||
"performer_age": "배우 나이",
|
"performer_age": "배우 나이",
|
||||||
"performer_count": "배우 수",
|
"performer_count": "배우 수",
|
||||||
"performer_favorite": "즐겨찾기한 배우",
|
"performer_favorite": "즐겨찾기한 배우",
|
||||||
|
|
@ -1077,7 +1077,7 @@
|
||||||
"resume_time": "재시작 시간",
|
"resume_time": "재시작 시간",
|
||||||
"scene": "영상",
|
"scene": "영상",
|
||||||
"sceneTagger": "영상 태거",
|
"sceneTagger": "영상 태거",
|
||||||
"sceneTags": "영상 태그",
|
"scene_tags": "영상 태그",
|
||||||
"scene_code": "스튜디오 코드",
|
"scene_code": "스튜디오 코드",
|
||||||
"scene_count": "영상 개수",
|
"scene_count": "영상 개수",
|
||||||
"scene_created_at": "영상 생성 날짜",
|
"scene_created_at": "영상 생성 날짜",
|
||||||
|
|
|
||||||
|
|
@ -821,7 +821,7 @@
|
||||||
"path": "Pad",
|
"path": "Pad",
|
||||||
"perceptual_similarity": "Perceptuele gelijkenis (phash)",
|
"perceptual_similarity": "Perceptuele gelijkenis (phash)",
|
||||||
"performer": "Performer",
|
"performer": "Performer",
|
||||||
"performerTags": "Peformer Labels",
|
"performer_tags": "Peformer Labels",
|
||||||
"performer_age": "Leeftijd artiest",
|
"performer_age": "Leeftijd artiest",
|
||||||
"performer_count": "Performer Aantal",
|
"performer_count": "Performer Aantal",
|
||||||
"performer_favorite": "Artiest favoriet",
|
"performer_favorite": "Artiest favoriet",
|
||||||
|
|
@ -855,7 +855,7 @@
|
||||||
"resolution": "Resolutie",
|
"resolution": "Resolutie",
|
||||||
"scene": "Scène",
|
"scene": "Scène",
|
||||||
"sceneTagger": "Scene Labelen",
|
"sceneTagger": "Scene Labelen",
|
||||||
"sceneTags": "Scene Labels",
|
"scene_tags": "Scene Labels",
|
||||||
"scene_count": "Scene Aantal",
|
"scene_count": "Scene Aantal",
|
||||||
"scene_id": "Scene ID",
|
"scene_id": "Scene ID",
|
||||||
"scenes": "Scènes",
|
"scenes": "Scènes",
|
||||||
|
|
|
||||||
|
|
@ -1030,7 +1030,7 @@
|
||||||
"penis_length_cm": "Długość penisa (cm)",
|
"penis_length_cm": "Długość penisa (cm)",
|
||||||
"perceptual_similarity": "Podobieństwo percepcyjne (phash)",
|
"perceptual_similarity": "Podobieństwo percepcyjne (phash)",
|
||||||
"performer": "Aktor",
|
"performer": "Aktor",
|
||||||
"performerTags": "Tagi aktorów",
|
"performer_tags": "Tagi aktorów",
|
||||||
"performer_age": "Wiek aktora",
|
"performer_age": "Wiek aktora",
|
||||||
"performer_count": "Liczba aktorów",
|
"performer_count": "Liczba aktorów",
|
||||||
"performer_favorite": "Ulubiony aktor",
|
"performer_favorite": "Ulubiony aktor",
|
||||||
|
|
@ -1085,7 +1085,7 @@
|
||||||
"resume_time": "Rozpocznij od",
|
"resume_time": "Rozpocznij od",
|
||||||
"scene": "Scena",
|
"scene": "Scena",
|
||||||
"sceneTagger": "Otagowywacz scen",
|
"sceneTagger": "Otagowywacz scen",
|
||||||
"sceneTags": "Tagi sceny",
|
"scene_tags": "Tagi sceny",
|
||||||
"scene_code": "Kod studia",
|
"scene_code": "Kod studia",
|
||||||
"scene_count": "Liczba scen",
|
"scene_count": "Liczba scen",
|
||||||
"scene_created_at": "Scena utworzona",
|
"scene_created_at": "Scena utworzona",
|
||||||
|
|
|
||||||
|
|
@ -847,7 +847,7 @@
|
||||||
"path": "Caminho",
|
"path": "Caminho",
|
||||||
"perceptual_similarity": "Semelhança Perceptiva (phash)",
|
"perceptual_similarity": "Semelhança Perceptiva (phash)",
|
||||||
"performer": "Artista",
|
"performer": "Artista",
|
||||||
"performerTags": "Etiquetas de artistas",
|
"performer_tags": "Etiquetas de artistas",
|
||||||
"performer_age": "Idade do Artista",
|
"performer_age": "Idade do Artista",
|
||||||
"performer_count": "Contagem de artistas",
|
"performer_count": "Contagem de artistas",
|
||||||
"performer_favorite": "Artista Favoritado",
|
"performer_favorite": "Artista Favoritado",
|
||||||
|
|
@ -898,7 +898,7 @@
|
||||||
"resolution": "Resolução",
|
"resolution": "Resolução",
|
||||||
"scene": "Cena",
|
"scene": "Cena",
|
||||||
"sceneTagger": "Etiquetador de cena",
|
"sceneTagger": "Etiquetador de cena",
|
||||||
"sceneTags": "Etiquetas da cena",
|
"scene_tags": "Etiquetas da cena",
|
||||||
"scene_count": "Contagem de cena",
|
"scene_count": "Contagem de cena",
|
||||||
"scene_id": "Cena ID",
|
"scene_id": "Cena ID",
|
||||||
"scenes": "Cenas",
|
"scenes": "Cenas",
|
||||||
|
|
|
||||||
|
|
@ -933,7 +933,7 @@
|
||||||
"path": "Путь",
|
"path": "Путь",
|
||||||
"perceptual_similarity": "Воспринимаемое сходство (phash)",
|
"perceptual_similarity": "Воспринимаемое сходство (phash)",
|
||||||
"performer": "Актер",
|
"performer": "Актер",
|
||||||
"performerTags": "Теги актера",
|
"performer_tags": "Теги актера",
|
||||||
"performer_age": "Возраст актера",
|
"performer_age": "Возраст актера",
|
||||||
"performer_count": "Количество актеров",
|
"performer_count": "Количество актеров",
|
||||||
"performer_favorite": "Участник добавлен в избранное",
|
"performer_favorite": "Участник добавлен в избранное",
|
||||||
|
|
@ -988,7 +988,7 @@
|
||||||
"resume_time": "Таймкод воспроизведения",
|
"resume_time": "Таймкод воспроизведения",
|
||||||
"scene": "Сцена",
|
"scene": "Сцена",
|
||||||
"sceneTagger": "Пометка сцен тэгами",
|
"sceneTagger": "Пометка сцен тэгами",
|
||||||
"sceneTags": "Тэги сцен",
|
"scene_tags": "Тэги сцен",
|
||||||
"scene_code": "Идентификатор сцены",
|
"scene_code": "Идентификатор сцены",
|
||||||
"scene_count": "Количество сцен",
|
"scene_count": "Количество сцен",
|
||||||
"scene_created_at": "Сцена создана",
|
"scene_created_at": "Сцена создана",
|
||||||
|
|
|
||||||
|
|
@ -1033,7 +1033,7 @@
|
||||||
"penis_length_cm": "Penislängd (cm)",
|
"penis_length_cm": "Penislängd (cm)",
|
||||||
"perceptual_similarity": "Perceptuell likhet (phash)",
|
"perceptual_similarity": "Perceptuell likhet (phash)",
|
||||||
"performer": "Stjärna",
|
"performer": "Stjärna",
|
||||||
"performerTags": "Stjärntagg",
|
"performer_tags": "Stjärntagg",
|
||||||
"performer_age": "Ålder på stjärna",
|
"performer_age": "Ålder på stjärna",
|
||||||
"performer_count": "Antal stjärnor",
|
"performer_count": "Antal stjärnor",
|
||||||
"performer_favorite": "Favoritiserad stjärna",
|
"performer_favorite": "Favoritiserad stjärna",
|
||||||
|
|
@ -1088,7 +1088,7 @@
|
||||||
"resume_time": "Återupptagningstid",
|
"resume_time": "Återupptagningstid",
|
||||||
"scene": "Scen",
|
"scene": "Scen",
|
||||||
"sceneTagger": "Scentaggaren",
|
"sceneTagger": "Scentaggaren",
|
||||||
"sceneTags": "Scentaggar",
|
"scene_tags": "Scentaggar",
|
||||||
"scene_code": "Studiokod",
|
"scene_code": "Studiokod",
|
||||||
"scene_count": "Antal scener",
|
"scene_count": "Antal scener",
|
||||||
"scene_created_at": "Scenen Skapad",
|
"scene_created_at": "Scenen Skapad",
|
||||||
|
|
|
||||||
|
|
@ -747,7 +747,7 @@
|
||||||
"part_of": "{parent} öğesinin parçası",
|
"part_of": "{parent} öğesinin parçası",
|
||||||
"path": "Konum",
|
"path": "Konum",
|
||||||
"performer": "Oyuncu",
|
"performer": "Oyuncu",
|
||||||
"performerTags": "Oyuncu Etiketleri",
|
"performer_tags": "Oyuncu Etiketleri",
|
||||||
"performer_count": "Oyuncu Sayısı",
|
"performer_count": "Oyuncu Sayısı",
|
||||||
"performer_image": "Oyuncu Resmi",
|
"performer_image": "Oyuncu Resmi",
|
||||||
"performers": "Oyuncular",
|
"performers": "Oyuncular",
|
||||||
|
|
@ -758,7 +758,7 @@
|
||||||
"resolution": "Çözünürlük",
|
"resolution": "Çözünürlük",
|
||||||
"scene": "Sahne",
|
"scene": "Sahne",
|
||||||
"sceneTagger": "Sahne Etiketleyici",
|
"sceneTagger": "Sahne Etiketleyici",
|
||||||
"sceneTags": "Sahne Etiketleri",
|
"scene_tags": "Sahne Etiketleri",
|
||||||
"scene_count": "Sahne Sayısı",
|
"scene_count": "Sahne Sayısı",
|
||||||
"scene_id": "Sahne Kimliği (ID)",
|
"scene_id": "Sahne Kimliği (ID)",
|
||||||
"scenes": "Sahneler",
|
"scenes": "Sahneler",
|
||||||
|
|
|
||||||
|
|
@ -1018,7 +1018,7 @@
|
||||||
"path": "路径",
|
"path": "路径",
|
||||||
"perceptual_similarity": "感知的类似程度(感知码)",
|
"perceptual_similarity": "感知的类似程度(感知码)",
|
||||||
"performer": "演员",
|
"performer": "演员",
|
||||||
"performerTags": "演员标签",
|
"performer_tags": "演员标签",
|
||||||
"performer_age": "演员年龄",
|
"performer_age": "演员年龄",
|
||||||
"performer_count": "演员数量",
|
"performer_count": "演员数量",
|
||||||
"performer_favorite": "演员已收藏",
|
"performer_favorite": "演员已收藏",
|
||||||
|
|
@ -1073,7 +1073,7 @@
|
||||||
"resume_time": "恢复时间",
|
"resume_time": "恢复时间",
|
||||||
"scene": "短片",
|
"scene": "短片",
|
||||||
"sceneTagger": "短片标记器",
|
"sceneTagger": "短片标记器",
|
||||||
"sceneTags": "短片标记",
|
"scene_tags": "短片标记",
|
||||||
"scene_code": "工作室代码",
|
"scene_code": "工作室代码",
|
||||||
"scene_count": "短片数量",
|
"scene_count": "短片数量",
|
||||||
"scene_created_at": "短片建立在",
|
"scene_created_at": "短片建立在",
|
||||||
|
|
|
||||||
|
|
@ -956,7 +956,7 @@
|
||||||
"path": "路徑",
|
"path": "路徑",
|
||||||
"perceptual_similarity": "感知相似度 (PHash)",
|
"perceptual_similarity": "感知相似度 (PHash)",
|
||||||
"performer": "演員",
|
"performer": "演員",
|
||||||
"performerTags": "演員標籤",
|
"performer_tags": "演員標籤",
|
||||||
"performer_age": "演員年齡",
|
"performer_age": "演員年齡",
|
||||||
"performer_count": "演員數量",
|
"performer_count": "演員數量",
|
||||||
"performer_favorite": "已收藏的演員",
|
"performer_favorite": "已收藏的演員",
|
||||||
|
|
@ -1011,7 +1011,7 @@
|
||||||
"resume_time": "恢復播放時間",
|
"resume_time": "恢復播放時間",
|
||||||
"scene": "短片",
|
"scene": "短片",
|
||||||
"sceneTagger": "短片標籤器",
|
"sceneTagger": "短片標籤器",
|
||||||
"sceneTags": "短片標籤",
|
"scene_tags": "短片標籤",
|
||||||
"scene_code": "番號",
|
"scene_code": "番號",
|
||||||
"scene_count": "短片數量",
|
"scene_count": "短片數量",
|
||||||
"scene_created_at": "短片建立於",
|
"scene_created_at": "短片建立於",
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ class CaptionsCriterionOptionType extends CriterionOption {
|
||||||
super({
|
super({
|
||||||
messageID: value,
|
messageID: value,
|
||||||
type: value,
|
type: value,
|
||||||
parameterName: value,
|
|
||||||
modifierOptions: [
|
modifierOptions: [
|
||||||
CriterionModifier.Includes,
|
CriterionModifier.Includes,
|
||||||
CriterionModifier.Excludes,
|
CriterionModifier.Excludes,
|
||||||
|
|
@ -19,6 +18,7 @@ class CaptionsCriterionOptionType extends CriterionOption {
|
||||||
],
|
],
|
||||||
defaultModifier: CriterionModifier.Includes,
|
defaultModifier: CriterionModifier.Includes,
|
||||||
options: languageStrings,
|
options: languageStrings,
|
||||||
|
makeCriterion: () => new CaptionCriterion(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ export const CircumcisedCriterionOption = new CriterionOption({
|
||||||
CriterionModifier.IsNull,
|
CriterionModifier.IsNull,
|
||||||
CriterionModifier.NotNull,
|
CriterionModifier.NotNull,
|
||||||
],
|
],
|
||||||
|
makeCriterion: () => new CircumcisedCriterion(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export class CircumcisedCriterion extends MultiStringCriterion {
|
export class CircumcisedCriterion extends MultiStringCriterion {
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,7 @@ import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import { getCountryByISO } from "src/utils/country";
|
import { getCountryByISO } from "src/utils/country";
|
||||||
import { StringCriterion, StringCriterionOption } from "./criterion";
|
import { StringCriterion, StringCriterionOption } from "./criterion";
|
||||||
|
|
||||||
const countryCriterionOption = new StringCriterionOption(
|
const countryCriterionOption = new StringCriterionOption("country", "country");
|
||||||
"country",
|
|
||||||
"country",
|
|
||||||
"country"
|
|
||||||
);
|
|
||||||
|
|
||||||
export class CountryCriterion extends StringCriterion {
|
export class CountryCriterion extends StringCriterion {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ export abstract class Criterion<V extends CriterionValue> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getId(): string {
|
public getId(): string {
|
||||||
return `${this.criterionOption.parameterName}-${this.modifier.toString()}`; // TODO add values?
|
return `${this.criterionOption.type}-${this.modifier.toString()}`; // TODO add values?
|
||||||
}
|
}
|
||||||
|
|
||||||
public toJSON() {
|
public toJSON() {
|
||||||
|
|
@ -154,7 +154,7 @@ export abstract class Criterion<V extends CriterionValue> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
public apply(outputFilter: Record<string, any>) {
|
public apply(outputFilter: Record<string, any>) {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
outputFilter[this.criterionOption.parameterName] = this.toCriterionInput();
|
outputFilter[this.criterionOption.type] = this.toCriterionInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|
@ -164,50 +164,68 @@ export abstract class Criterion<V extends CriterionValue> {
|
||||||
modifier: this.modifier,
|
modifier: this.modifier,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
public toSavedFilter(outputFilter: Record<string, any>) {
|
||||||
|
outputFilter[this.criterionOption.type] = {
|
||||||
|
value: this.value,
|
||||||
|
modifier: this.modifier,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InputType = "number" | "text" | undefined;
|
export type InputType =
|
||||||
|
| "number"
|
||||||
|
| "text"
|
||||||
|
| "performers"
|
||||||
|
| "studios"
|
||||||
|
| "tags"
|
||||||
|
| "performer_tags"
|
||||||
|
| "scene_tags"
|
||||||
|
| "movies"
|
||||||
|
| "galleries"
|
||||||
|
| undefined;
|
||||||
|
|
||||||
interface ICriterionOptionsParams {
|
interface ICriterionOptionsParams {
|
||||||
messageID: string;
|
messageID: string;
|
||||||
type: CriterionType;
|
type: CriterionType;
|
||||||
inputType?: InputType;
|
inputType?: InputType;
|
||||||
parameterName?: string;
|
|
||||||
modifierOptions?: CriterionModifier[];
|
modifierOptions?: CriterionModifier[];
|
||||||
defaultModifier?: CriterionModifier;
|
defaultModifier?: CriterionModifier;
|
||||||
options?: Option[];
|
options?: Option[];
|
||||||
|
makeCriterion: () => Criterion<CriterionValue>;
|
||||||
}
|
}
|
||||||
export class CriterionOption {
|
export class CriterionOption {
|
||||||
public readonly messageID: string;
|
public readonly messageID: string;
|
||||||
public readonly type: CriterionType;
|
public readonly type: CriterionType;
|
||||||
public readonly parameterName: string;
|
|
||||||
public readonly modifierOptions: CriterionModifier[];
|
public readonly modifierOptions: CriterionModifier[];
|
||||||
public readonly defaultModifier: CriterionModifier;
|
public readonly defaultModifier: CriterionModifier;
|
||||||
public readonly options: Option[] | undefined;
|
public readonly options: Option[] | undefined;
|
||||||
public readonly inputType: InputType;
|
public readonly inputType: InputType;
|
||||||
|
public readonly makeCriterionFn: (
|
||||||
|
o: CriterionOption
|
||||||
|
) => Criterion<CriterionValue>;
|
||||||
|
|
||||||
constructor(options: ICriterionOptionsParams) {
|
constructor(options: ICriterionOptionsParams) {
|
||||||
this.messageID = options.messageID;
|
this.messageID = options.messageID;
|
||||||
this.type = options.type;
|
this.type = options.type;
|
||||||
this.parameterName = options.parameterName ?? options.type;
|
|
||||||
this.modifierOptions = options.modifierOptions ?? [];
|
this.modifierOptions = options.modifierOptions ?? [];
|
||||||
this.defaultModifier = options.defaultModifier ?? CriterionModifier.Equals;
|
this.defaultModifier = options.defaultModifier ?? CriterionModifier.Equals;
|
||||||
this.options = options.options;
|
this.options = options.options;
|
||||||
this.inputType = options.inputType;
|
this.inputType = options.inputType;
|
||||||
|
this.makeCriterionFn = options.makeCriterion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public makeCriterion() {
|
||||||
|
return this.makeCriterionFn(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StringCriterionOption extends CriterionOption {
|
export class StringCriterionOption extends CriterionOption {
|
||||||
constructor(
|
constructor(messageID: string, type: CriterionType, options?: Option[]) {
|
||||||
messageID: string,
|
|
||||||
value: CriterionType,
|
|
||||||
parameterName?: string,
|
|
||||||
options?: Option[]
|
|
||||||
) {
|
|
||||||
super({
|
super({
|
||||||
messageID,
|
messageID,
|
||||||
type: value,
|
type,
|
||||||
parameterName,
|
|
||||||
modifierOptions: [
|
modifierOptions: [
|
||||||
CriterionModifier.Equals,
|
CriterionModifier.Equals,
|
||||||
CriterionModifier.NotEquals,
|
CriterionModifier.NotEquals,
|
||||||
|
|
@ -221,20 +239,16 @@ export class StringCriterionOption extends CriterionOption {
|
||||||
defaultModifier: CriterionModifier.Equals,
|
defaultModifier: CriterionModifier.Equals,
|
||||||
options,
|
options,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
|
makeCriterion: () => new StringCriterion(this),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createStringCriterionOption(
|
export function createStringCriterionOption(
|
||||||
value: CriterionType,
|
type: CriterionType,
|
||||||
messageID?: string,
|
messageID?: string
|
||||||
parameterName?: string
|
|
||||||
) {
|
) {
|
||||||
return new StringCriterionOption(
|
return new StringCriterionOption(messageID ?? type, type);
|
||||||
messageID ?? value,
|
|
||||||
value,
|
|
||||||
parameterName ?? messageID ?? value
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StringCriterion extends Criterion<string> {
|
export class StringCriterion extends Criterion<string> {
|
||||||
|
|
@ -274,16 +288,10 @@ export class MultiStringCriterion extends Criterion<string[]> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MandatoryStringCriterionOption extends CriterionOption {
|
export class MandatoryStringCriterionOption extends CriterionOption {
|
||||||
constructor(
|
constructor(messageID: string, value: CriterionType, options?: Option[]) {
|
||||||
messageID: string,
|
|
||||||
value: CriterionType,
|
|
||||||
parameterName?: string,
|
|
||||||
options?: Option[]
|
|
||||||
) {
|
|
||||||
super({
|
super({
|
||||||
messageID,
|
messageID,
|
||||||
type: value,
|
type: value,
|
||||||
parameterName,
|
|
||||||
modifierOptions: [
|
modifierOptions: [
|
||||||
CriterionModifier.Equals,
|
CriterionModifier.Equals,
|
||||||
CriterionModifier.NotEquals,
|
CriterionModifier.NotEquals,
|
||||||
|
|
@ -295,45 +303,42 @@ export class MandatoryStringCriterionOption extends CriterionOption {
|
||||||
defaultModifier: CriterionModifier.Equals,
|
defaultModifier: CriterionModifier.Equals,
|
||||||
options,
|
options,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
|
makeCriterion: () => new StringCriterion(this),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMandatoryStringCriterionOption(
|
export function createMandatoryStringCriterionOption(
|
||||||
value: CriterionType,
|
value: CriterionType,
|
||||||
messageID?: string,
|
messageID?: string
|
||||||
parameterName?: string
|
|
||||||
) {
|
) {
|
||||||
return new MandatoryStringCriterionOption(
|
return new MandatoryStringCriterionOption(messageID ?? value, value);
|
||||||
messageID ?? value,
|
|
||||||
value,
|
|
||||||
parameterName ?? messageID ?? value
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PathCriterionOption extends StringCriterionOption {}
|
export class PathCriterionOption extends StringCriterionOption {}
|
||||||
|
|
||||||
export function createPathCriterionOption(
|
export function createPathCriterionOption(
|
||||||
value: CriterionType,
|
type: CriterionType,
|
||||||
messageID?: string,
|
messageID?: string
|
||||||
parameterName?: string
|
|
||||||
) {
|
) {
|
||||||
return new PathCriterionOption(
|
return new PathCriterionOption(messageID ?? type, type);
|
||||||
messageID ?? value,
|
|
||||||
value,
|
|
||||||
parameterName ?? messageID ?? value
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BooleanCriterionOption extends CriterionOption {
|
export class BooleanCriterionOption extends CriterionOption {
|
||||||
constructor(messageID: string, value: CriterionType, parameterName?: string) {
|
constructor(
|
||||||
|
messageID: string,
|
||||||
|
value: CriterionType,
|
||||||
|
makeCriterion?: () => Criterion<CriterionValue>
|
||||||
|
) {
|
||||||
super({
|
super({
|
||||||
messageID,
|
messageID,
|
||||||
type: value,
|
type: value,
|
||||||
parameterName,
|
|
||||||
modifierOptions: [],
|
modifierOptions: [],
|
||||||
defaultModifier: CriterionModifier.Equals,
|
defaultModifier: CriterionModifier.Equals,
|
||||||
options: [true.toString(), false.toString()],
|
options: [true.toString(), false.toString()],
|
||||||
|
makeCriterion: makeCriterion
|
||||||
|
? makeCriterion
|
||||||
|
: () => new BooleanCriterion(this),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -350,27 +355,16 @@ export class BooleanCriterion extends StringCriterion {
|
||||||
|
|
||||||
export function createBooleanCriterionOption(
|
export function createBooleanCriterionOption(
|
||||||
value: CriterionType,
|
value: CriterionType,
|
||||||
messageID?: string,
|
messageID?: string
|
||||||
parameterName?: string
|
|
||||||
) {
|
) {
|
||||||
return new BooleanCriterionOption(
|
return new BooleanCriterionOption(messageID ?? value, value);
|
||||||
messageID ?? value,
|
|
||||||
value,
|
|
||||||
parameterName ?? messageID ?? value
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NumberCriterionOption extends CriterionOption {
|
export class NumberCriterionOption extends CriterionOption {
|
||||||
constructor(
|
constructor(messageID: string, value: CriterionType, options?: Option[]) {
|
||||||
messageID: string,
|
|
||||||
value: CriterionType,
|
|
||||||
parameterName?: string,
|
|
||||||
options?: Option[]
|
|
||||||
) {
|
|
||||||
super({
|
super({
|
||||||
messageID,
|
messageID,
|
||||||
type: value,
|
type: value,
|
||||||
parameterName,
|
|
||||||
modifierOptions: [
|
modifierOptions: [
|
||||||
CriterionModifier.Equals,
|
CriterionModifier.Equals,
|
||||||
CriterionModifier.NotEquals,
|
CriterionModifier.NotEquals,
|
||||||
|
|
@ -384,16 +378,16 @@ export class NumberCriterionOption extends CriterionOption {
|
||||||
defaultModifier: CriterionModifier.Equals,
|
defaultModifier: CriterionModifier.Equals,
|
||||||
options,
|
options,
|
||||||
inputType: "number",
|
inputType: "number",
|
||||||
|
makeCriterion: () => new NumberCriterion(this),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NullNumberCriterionOption extends CriterionOption {
|
export class NullNumberCriterionOption extends CriterionOption {
|
||||||
constructor(messageID: string, value: CriterionType, parameterName?: string) {
|
constructor(messageID: string, value: CriterionType) {
|
||||||
super({
|
super({
|
||||||
messageID,
|
messageID,
|
||||||
type: value,
|
type: value,
|
||||||
parameterName,
|
|
||||||
modifierOptions: [
|
modifierOptions: [
|
||||||
CriterionModifier.Equals,
|
CriterionModifier.Equals,
|
||||||
CriterionModifier.NotEquals,
|
CriterionModifier.NotEquals,
|
||||||
|
|
@ -406,16 +400,17 @@ export class NullNumberCriterionOption extends CriterionOption {
|
||||||
],
|
],
|
||||||
defaultModifier: CriterionModifier.Equals,
|
defaultModifier: CriterionModifier.Equals,
|
||||||
inputType: "number",
|
inputType: "number",
|
||||||
|
makeCriterion: () => new NumberCriterion(this),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createNumberCriterionOption(value: CriterionType) {
|
export function createNumberCriterionOption(value: CriterionType) {
|
||||||
return new NumberCriterionOption(value, value, value);
|
return new NumberCriterionOption(value, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createNullNumberCriterionOption(value: CriterionType) {
|
export function createNullNumberCriterionOption(value: CriterionType) {
|
||||||
return new NullNumberCriterionOption(value, value, value);
|
return new NullNumberCriterionOption(value, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NumberCriterion extends Criterion<INumberValue> {
|
export class NumberCriterion extends Criterion<INumberValue> {
|
||||||
|
|
@ -437,8 +432,8 @@ export class NumberCriterion extends Criterion<INumberValue> {
|
||||||
protected toCriterionInput(): IntCriterionInput {
|
protected toCriterionInput(): IntCriterionInput {
|
||||||
return {
|
return {
|
||||||
modifier: this.modifier,
|
modifier: this.modifier,
|
||||||
value: this.value.value ?? 0,
|
value: this.value?.value ?? 0,
|
||||||
value2: this.value.value2,
|
value2: this.value?.value2,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -487,8 +482,8 @@ export class ILabeledIdCriterionOption extends CriterionOption {
|
||||||
constructor(
|
constructor(
|
||||||
messageID: string,
|
messageID: string,
|
||||||
value: CriterionType,
|
value: CriterionType,
|
||||||
parameterName: string,
|
includeAll: boolean,
|
||||||
includeAll: boolean
|
inputType: InputType
|
||||||
) {
|
) {
|
||||||
const modifierOptions = [
|
const modifierOptions = [
|
||||||
CriterionModifier.Includes,
|
CriterionModifier.Includes,
|
||||||
|
|
@ -506,9 +501,10 @@ export class ILabeledIdCriterionOption extends CriterionOption {
|
||||||
super({
|
super({
|
||||||
messageID,
|
messageID,
|
||||||
type: value,
|
type: value,
|
||||||
parameterName,
|
|
||||||
modifierOptions,
|
modifierOptions,
|
||||||
defaultModifier,
|
defaultModifier,
|
||||||
|
makeCriterion: () => new ILabeledIdCriterion(this),
|
||||||
|
inputType,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -684,11 +680,10 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MandatoryNumberCriterionOption extends CriterionOption {
|
export class MandatoryNumberCriterionOption extends CriterionOption {
|
||||||
constructor(messageID: string, value: CriterionType, parameterName?: string) {
|
constructor(messageID: string, value: CriterionType) {
|
||||||
super({
|
super({
|
||||||
messageID,
|
messageID,
|
||||||
type: value,
|
type: value,
|
||||||
parameterName,
|
|
||||||
modifierOptions: [
|
modifierOptions: [
|
||||||
CriterionModifier.Equals,
|
CriterionModifier.Equals,
|
||||||
CriterionModifier.NotEquals,
|
CriterionModifier.NotEquals,
|
||||||
|
|
@ -699,6 +694,7 @@ export class MandatoryNumberCriterionOption extends CriterionOption {
|
||||||
],
|
],
|
||||||
defaultModifier: CriterionModifier.Equals,
|
defaultModifier: CriterionModifier.Equals,
|
||||||
inputType: "number",
|
inputType: "number",
|
||||||
|
makeCriterion: () => new NumberCriterion(this),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -707,7 +703,7 @@ export function createMandatoryNumberCriterionOption(
|
||||||
value: CriterionType,
|
value: CriterionType,
|
||||||
messageID?: string
|
messageID?: string
|
||||||
) {
|
) {
|
||||||
return new MandatoryNumberCriterionOption(messageID ?? value, value, value);
|
return new MandatoryNumberCriterionOption(messageID ?? value, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DurationCriterion extends Criterion<INumberValue> {
|
export class DurationCriterion extends Criterion<INumberValue> {
|
||||||
|
|
@ -718,8 +714,8 @@ export class DurationCriterion extends Criterion<INumberValue> {
|
||||||
protected toCriterionInput(): IntCriterionInput {
|
protected toCriterionInput(): IntCriterionInput {
|
||||||
return {
|
return {
|
||||||
modifier: this.modifier,
|
modifier: this.modifier,
|
||||||
value: this.value.value ?? 0,
|
value: this.value?.value ?? 0,
|
||||||
value2: this.value.value2,
|
value2: this.value?.value2,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -771,16 +767,10 @@ export class PhashDuplicateCriterion extends StringCriterion {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DateCriterionOption extends CriterionOption {
|
export class DateCriterionOption extends CriterionOption {
|
||||||
constructor(
|
constructor(messageID: string, value: CriterionType, options?: Option[]) {
|
||||||
messageID: string,
|
|
||||||
value: CriterionType,
|
|
||||||
parameterName?: string,
|
|
||||||
options?: Option[]
|
|
||||||
) {
|
|
||||||
super({
|
super({
|
||||||
messageID,
|
messageID,
|
||||||
type: value,
|
type: value,
|
||||||
parameterName,
|
|
||||||
modifierOptions: [
|
modifierOptions: [
|
||||||
CriterionModifier.Equals,
|
CriterionModifier.Equals,
|
||||||
CriterionModifier.NotEquals,
|
CriterionModifier.NotEquals,
|
||||||
|
|
@ -794,12 +784,13 @@ export class DateCriterionOption extends CriterionOption {
|
||||||
defaultModifier: CriterionModifier.Equals,
|
defaultModifier: CriterionModifier.Equals,
|
||||||
options,
|
options,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
|
makeCriterion: () => new DateCriterion(this),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createDateCriterionOption(value: CriterionType) {
|
export function createDateCriterionOption(value: CriterionType) {
|
||||||
return new DateCriterionOption(value, value, value);
|
return new DateCriterionOption(value, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DateCriterion extends Criterion<IDateValue> {
|
export class DateCriterion extends Criterion<IDateValue> {
|
||||||
|
|
@ -813,8 +804,8 @@ export class DateCriterion extends Criterion<IDateValue> {
|
||||||
protected toCriterionInput(): DateCriterionInput {
|
protected toCriterionInput(): DateCriterionInput {
|
||||||
return {
|
return {
|
||||||
modifier: this.modifier,
|
modifier: this.modifier,
|
||||||
value: this.value.value,
|
value: this.value?.value,
|
||||||
value2: this.value.value2,
|
value2: this.value?.value2,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -856,16 +847,10 @@ export class DateCriterion extends Criterion<IDateValue> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TimestampCriterionOption extends CriterionOption {
|
export class TimestampCriterionOption extends CriterionOption {
|
||||||
constructor(
|
constructor(messageID: string, value: CriterionType, options?: Option[]) {
|
||||||
messageID: string,
|
|
||||||
value: CriterionType,
|
|
||||||
parameterName?: string,
|
|
||||||
options?: Option[]
|
|
||||||
) {
|
|
||||||
super({
|
super({
|
||||||
messageID,
|
messageID,
|
||||||
type: value,
|
type: value,
|
||||||
parameterName,
|
|
||||||
modifierOptions: [
|
modifierOptions: [
|
||||||
CriterionModifier.GreaterThan,
|
CriterionModifier.GreaterThan,
|
||||||
CriterionModifier.LessThan,
|
CriterionModifier.LessThan,
|
||||||
|
|
@ -877,19 +862,20 @@ export class TimestampCriterionOption extends CriterionOption {
|
||||||
defaultModifier: CriterionModifier.GreaterThan,
|
defaultModifier: CriterionModifier.GreaterThan,
|
||||||
options,
|
options,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
|
makeCriterion: () => new TimestampCriterion(this),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTimestampCriterionOption(value: CriterionType) {
|
export function createTimestampCriterionOption(value: CriterionType) {
|
||||||
return new TimestampCriterionOption(value, value, value);
|
return new TimestampCriterionOption(value, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TimestampCriterion extends Criterion<ITimestampValue> {
|
export class TimestampCriterion extends Criterion<ITimestampValue> {
|
||||||
public encodeValue() {
|
public encodeValue() {
|
||||||
return {
|
return {
|
||||||
value: this.value.value,
|
value: this.value?.value,
|
||||||
value2: this.value.value2,
|
value2: this.value?.value2,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -950,16 +936,10 @@ export class TimestampCriterion extends Criterion<ITimestampValue> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MandatoryTimestampCriterionOption extends CriterionOption {
|
export class MandatoryTimestampCriterionOption extends CriterionOption {
|
||||||
constructor(
|
constructor(messageID: string, value: CriterionType, options?: Option[]) {
|
||||||
messageID: string,
|
|
||||||
value: CriterionType,
|
|
||||||
parameterName?: string,
|
|
||||||
options?: Option[]
|
|
||||||
) {
|
|
||||||
super({
|
super({
|
||||||
messageID,
|
messageID,
|
||||||
type: value,
|
type: value,
|
||||||
parameterName,
|
|
||||||
modifierOptions: [
|
modifierOptions: [
|
||||||
CriterionModifier.GreaterThan,
|
CriterionModifier.GreaterThan,
|
||||||
CriterionModifier.LessThan,
|
CriterionModifier.LessThan,
|
||||||
|
|
@ -969,10 +949,11 @@ export class MandatoryTimestampCriterionOption extends CriterionOption {
|
||||||
defaultModifier: CriterionModifier.GreaterThan,
|
defaultModifier: CriterionModifier.GreaterThan,
|
||||||
options,
|
options,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
|
makeCriterion: () => new TimestampCriterion(this),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMandatoryTimestampCriterionOption(value: CriterionType) {
|
export function createMandatoryTimestampCriterionOption(value: CriterionType) {
|
||||||
return new MandatoryTimestampCriterionOption(value, value, value);
|
return new MandatoryTimestampCriterionOption(value, value);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,250 +1,36 @@
|
||||||
/* eslint-disable consistent-return, default-case */
|
|
||||||
import {
|
|
||||||
StringCriterion,
|
|
||||||
NumberCriterion,
|
|
||||||
DurationCriterion,
|
|
||||||
NumberCriterionOption,
|
|
||||||
MandatoryStringCriterionOption,
|
|
||||||
NullNumberCriterionOption,
|
|
||||||
MandatoryNumberCriterionOption,
|
|
||||||
StringCriterionOption,
|
|
||||||
ILabeledIdCriterion,
|
|
||||||
BooleanCriterion,
|
|
||||||
BooleanCriterionOption,
|
|
||||||
DateCriterion,
|
|
||||||
DateCriterionOption,
|
|
||||||
TimestampCriterion,
|
|
||||||
MandatoryTimestampCriterionOption,
|
|
||||||
PathCriterionOption,
|
|
||||||
} from "./criterion";
|
|
||||||
import { OrganizedCriterion } from "./organized";
|
|
||||||
import { FavoriteCriterion, PerformerFavoriteCriterion } from "./favorite";
|
|
||||||
import { HasMarkersCriterion } from "./has-markers";
|
|
||||||
import { HasChaptersCriterion } from "./has-chapters";
|
|
||||||
import {
|
|
||||||
PerformerIsMissingCriterionOption,
|
|
||||||
ImageIsMissingCriterionOption,
|
|
||||||
TagIsMissingCriterionOption,
|
|
||||||
SceneIsMissingCriterionOption,
|
|
||||||
IsMissingCriterion,
|
|
||||||
GalleryIsMissingCriterionOption,
|
|
||||||
StudioIsMissingCriterionOption,
|
|
||||||
MovieIsMissingCriterionOption,
|
|
||||||
} from "./is-missing";
|
|
||||||
import { NoneCriterion } from "./none";
|
|
||||||
import { PerformersCriterion } from "./performers";
|
|
||||||
import { AverageResolutionCriterion, ResolutionCriterion } from "./resolution";
|
|
||||||
import { StudiosCriterion, ParentStudiosCriterion } from "./studios";
|
|
||||||
import {
|
|
||||||
ChildTagsCriterionOption,
|
|
||||||
ParentTagsCriterionOption,
|
|
||||||
PerformerTagsCriterionOption,
|
|
||||||
SceneTagsCriterionOption,
|
|
||||||
TagsCriterion,
|
|
||||||
TagsCriterionOption,
|
|
||||||
} from "./tags";
|
|
||||||
import { GenderCriterion } from "./gender";
|
|
||||||
import { CircumcisedCriterion } from "./circumcised";
|
|
||||||
import { MoviesCriterionOption } from "./movies";
|
|
||||||
import { GalleriesCriterion } from "./galleries";
|
|
||||||
import { CriterionType } from "../types";
|
|
||||||
import { InteractiveCriterion } from "./interactive";
|
|
||||||
import { DuplicatedCriterion, PhashCriterion } from "./phash";
|
|
||||||
import { CaptionCriterion } from "./captions";
|
|
||||||
import { RatingCriterion } from "./rating";
|
|
||||||
import { CountryCriterion } from "./country";
|
|
||||||
import { StashIDCriterion } from "./stash-ids";
|
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { IUIConfig } from "src/core/config";
|
import { SceneListFilterOptions } from "../scenes";
|
||||||
import { defaultRatingSystemOptions } from "src/utils/rating";
|
import { MovieListFilterOptions } from "../movies";
|
||||||
|
import { GalleryListFilterOptions } from "../galleries";
|
||||||
|
import { PerformerListFilterOptions } from "../performers";
|
||||||
|
import { ImageListFilterOptions } from "../images";
|
||||||
|
import { SceneMarkerListFilterOptions } from "../scene-markers";
|
||||||
|
import { StudioListFilterOptions } from "../studios";
|
||||||
|
import { TagListFilterOptions } from "../tags";
|
||||||
|
import { CriterionType } from "../types";
|
||||||
|
|
||||||
|
const filterModeOptions = {
|
||||||
|
[GQL.FilterMode.Galleries]: GalleryListFilterOptions.criterionOptions,
|
||||||
|
[GQL.FilterMode.Images]: ImageListFilterOptions.criterionOptions,
|
||||||
|
[GQL.FilterMode.Movies]: MovieListFilterOptions.criterionOptions,
|
||||||
|
[GQL.FilterMode.Performers]: PerformerListFilterOptions.criterionOptions,
|
||||||
|
[GQL.FilterMode.SceneMarkers]: SceneMarkerListFilterOptions.criterionOptions,
|
||||||
|
[GQL.FilterMode.Scenes]: SceneListFilterOptions.criterionOptions,
|
||||||
|
[GQL.FilterMode.Studios]: StudioListFilterOptions.criterionOptions,
|
||||||
|
[GQL.FilterMode.Tags]: TagListFilterOptions.criterionOptions,
|
||||||
|
};
|
||||||
|
|
||||||
export function makeCriteria(
|
export function makeCriteria(
|
||||||
config: GQL.ConfigDataFragment | undefined,
|
mode: GQL.FilterMode,
|
||||||
type: CriterionType = "none"
|
type: CriterionType = "none"
|
||||||
) {
|
) {
|
||||||
switch (type) {
|
const criterionOptions = filterModeOptions[mode];
|
||||||
case "none":
|
|
||||||
return new NoneCriterion();
|
const option = criterionOptions.find((o) => o.type === type);
|
||||||
case "name":
|
|
||||||
return new StringCriterion(
|
if (!option) {
|
||||||
new MandatoryStringCriterionOption(type, type)
|
throw new Error(`Unknown criterion parameter name: ${type}`);
|
||||||
);
|
|
||||||
case "path":
|
|
||||||
return new StringCriterion(new PathCriterionOption(type, type));
|
|
||||||
case "checksum":
|
|
||||||
return new StringCriterion(
|
|
||||||
new MandatoryStringCriterionOption("media_info.checksum", type, type)
|
|
||||||
);
|
|
||||||
case "oshash":
|
|
||||||
return new StringCriterion(
|
|
||||||
new MandatoryStringCriterionOption("media_info.hash", type, type)
|
|
||||||
);
|
|
||||||
case "organized":
|
|
||||||
return new OrganizedCriterion();
|
|
||||||
case "o_counter":
|
|
||||||
case "interactive_speed":
|
|
||||||
case "scene_count":
|
|
||||||
case "marker_count":
|
|
||||||
case "image_count":
|
|
||||||
case "gallery_count":
|
|
||||||
case "performer_count":
|
|
||||||
case "performer_age":
|
|
||||||
case "tag_count":
|
|
||||||
case "file_count":
|
|
||||||
case "play_count":
|
|
||||||
return new NumberCriterion(
|
|
||||||
new MandatoryNumberCriterionOption(type, type)
|
|
||||||
);
|
|
||||||
case "rating":
|
|
||||||
return new NumberCriterion(new NullNumberCriterionOption(type, type));
|
|
||||||
case "rating100":
|
|
||||||
return new RatingCriterion(
|
|
||||||
new NullNumberCriterionOption("rating", type),
|
|
||||||
(config?.ui as IUIConfig)?.ratingSystemOptions ??
|
|
||||||
defaultRatingSystemOptions
|
|
||||||
);
|
|
||||||
case "resolution":
|
|
||||||
return new ResolutionCriterion();
|
|
||||||
case "average_resolution":
|
|
||||||
return new AverageResolutionCriterion();
|
|
||||||
case "video_codec":
|
|
||||||
return new StringCriterion(new StringCriterionOption(type, type));
|
|
||||||
case "audio_codec":
|
|
||||||
return new StringCriterion(new StringCriterionOption(type, type));
|
|
||||||
case "resume_time":
|
|
||||||
case "duration":
|
|
||||||
case "play_duration":
|
|
||||||
return new DurationCriterion(
|
|
||||||
new MandatoryNumberCriterionOption(type, type)
|
|
||||||
);
|
|
||||||
case "favorite":
|
|
||||||
return new FavoriteCriterion();
|
|
||||||
case "hasMarkers":
|
|
||||||
return new HasMarkersCriterion();
|
|
||||||
case "hasChapters":
|
|
||||||
return new HasChaptersCriterion();
|
|
||||||
case "sceneIsMissing":
|
|
||||||
return new IsMissingCriterion(SceneIsMissingCriterionOption);
|
|
||||||
case "imageIsMissing":
|
|
||||||
return new IsMissingCriterion(ImageIsMissingCriterionOption);
|
|
||||||
case "performerIsMissing":
|
|
||||||
return new IsMissingCriterion(PerformerIsMissingCriterionOption);
|
|
||||||
case "galleryIsMissing":
|
|
||||||
return new IsMissingCriterion(GalleryIsMissingCriterionOption);
|
|
||||||
case "tagIsMissing":
|
|
||||||
return new IsMissingCriterion(TagIsMissingCriterionOption);
|
|
||||||
case "studioIsMissing":
|
|
||||||
return new IsMissingCriterion(StudioIsMissingCriterionOption);
|
|
||||||
case "movieIsMissing":
|
|
||||||
return new IsMissingCriterion(MovieIsMissingCriterionOption);
|
|
||||||
case "tags":
|
|
||||||
return new TagsCriterion(TagsCriterionOption);
|
|
||||||
case "sceneTags":
|
|
||||||
return new TagsCriterion(SceneTagsCriterionOption);
|
|
||||||
case "performerTags":
|
|
||||||
return new TagsCriterion(PerformerTagsCriterionOption);
|
|
||||||
case "parentTags":
|
|
||||||
return new TagsCriterion(ParentTagsCriterionOption);
|
|
||||||
case "childTags":
|
|
||||||
return new TagsCriterion(ChildTagsCriterionOption);
|
|
||||||
case "performers":
|
|
||||||
return new PerformersCriterion();
|
|
||||||
case "performer_favorite":
|
|
||||||
return new PerformerFavoriteCriterion();
|
|
||||||
case "studios":
|
|
||||||
return new StudiosCriterion();
|
|
||||||
case "parent_studios":
|
|
||||||
return new ParentStudiosCriterion();
|
|
||||||
case "movies":
|
|
||||||
return new ILabeledIdCriterion(MoviesCriterionOption);
|
|
||||||
case "galleries":
|
|
||||||
return new GalleriesCriterion();
|
|
||||||
case "birth_year":
|
|
||||||
case "death_year":
|
|
||||||
case "weight":
|
|
||||||
return new NumberCriterion(new NumberCriterionOption(type, type));
|
|
||||||
case "penis_length":
|
|
||||||
return new NumberCriterion(new NumberCriterionOption(type, type));
|
|
||||||
case "age":
|
|
||||||
return new NumberCriterion(
|
|
||||||
new MandatoryNumberCriterionOption(type, type)
|
|
||||||
);
|
|
||||||
case "gender":
|
|
||||||
return new GenderCriterion();
|
|
||||||
case "circumcised":
|
|
||||||
return new CircumcisedCriterion();
|
|
||||||
case "sceneChecksum":
|
|
||||||
case "galleryChecksum":
|
|
||||||
return new StringCriterion(
|
|
||||||
new StringCriterionOption("media_info.checksum", type, "checksum")
|
|
||||||
);
|
|
||||||
case "phash":
|
|
||||||
return new PhashCriterion();
|
|
||||||
case "duplicated":
|
|
||||||
return new DuplicatedCriterion();
|
|
||||||
case "country":
|
|
||||||
return new CountryCriterion();
|
|
||||||
case "height":
|
|
||||||
case "height_cm":
|
|
||||||
return new NumberCriterion(
|
|
||||||
new NumberCriterionOption("height", "height_cm", type)
|
|
||||||
);
|
|
||||||
// stash_id is deprecated
|
|
||||||
case "stash_id":
|
|
||||||
case "stash_id_endpoint":
|
|
||||||
return new StashIDCriterion();
|
|
||||||
case "ethnicity":
|
|
||||||
case "hair_color":
|
|
||||||
case "eye_color":
|
|
||||||
case "measurements":
|
|
||||||
case "fake_tits":
|
|
||||||
case "career_length":
|
|
||||||
case "tattoos":
|
|
||||||
case "piercings":
|
|
||||||
case "aliases":
|
|
||||||
case "url":
|
|
||||||
case "details":
|
|
||||||
case "title":
|
|
||||||
case "director":
|
|
||||||
case "synopsis":
|
|
||||||
case "description":
|
|
||||||
case "disambiguation":
|
|
||||||
return new StringCriterion(new StringCriterionOption(type, type));
|
|
||||||
case "scene_code":
|
|
||||||
return new StringCriterion(new StringCriterionOption(type, type, "code"));
|
|
||||||
case "interactive":
|
|
||||||
return new InteractiveCriterion();
|
|
||||||
case "captions":
|
|
||||||
return new CaptionCriterion();
|
|
||||||
case "parent_tag_count":
|
|
||||||
return new NumberCriterion(
|
|
||||||
new MandatoryNumberCriterionOption(
|
|
||||||
"parent_tag_count",
|
|
||||||
"parent_tag_count",
|
|
||||||
"parent_count"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
case "child_tag_count":
|
|
||||||
return new NumberCriterion(
|
|
||||||
new MandatoryNumberCriterionOption(
|
|
||||||
"sub_tag_count",
|
|
||||||
"child_tag_count",
|
|
||||||
"child_count"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
case "ignore_auto_tag":
|
|
||||||
return new BooleanCriterion(new BooleanCriterionOption(type, type));
|
|
||||||
case "date":
|
|
||||||
case "birthdate":
|
|
||||||
case "death_date":
|
|
||||||
case "scene_date":
|
|
||||||
return new DateCriterion(new DateCriterionOption(type, type));
|
|
||||||
case "created_at":
|
|
||||||
case "updated_at":
|
|
||||||
case "scene_created_at":
|
|
||||||
case "scene_updated_at":
|
|
||||||
return new TimestampCriterion(
|
|
||||||
new MandatoryTimestampCriterionOption(type, type)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return option?.makeCriterion();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { BooleanCriterion, BooleanCriterionOption } from "./criterion";
|
||||||
|
|
||||||
export const FavoriteCriterionOption = new BooleanCriterionOption(
|
export const FavoriteCriterionOption = new BooleanCriterionOption(
|
||||||
"favourite",
|
"favourite",
|
||||||
"favorite",
|
|
||||||
"filter_favorites"
|
"filter_favorites"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -13,7 +12,6 @@ export class FavoriteCriterion extends BooleanCriterion {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PerformerFavoriteCriterionOption = new BooleanCriterionOption(
|
export const PerformerFavoriteCriterionOption = new BooleanCriterionOption(
|
||||||
"performer_favorite",
|
|
||||||
"performer_favorite",
|
"performer_favorite",
|
||||||
"performer_favorite"
|
"performer_favorite"
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
|
import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
|
||||||
|
|
||||||
|
const inputType = "galleries";
|
||||||
|
|
||||||
const galleriesCriterionOption = new ILabeledIdCriterionOption(
|
const galleriesCriterionOption = new ILabeledIdCriterionOption(
|
||||||
"galleries",
|
"galleries",
|
||||||
"galleries",
|
"galleries",
|
||||||
"galleries",
|
true,
|
||||||
true
|
inputType
|
||||||
);
|
);
|
||||||
|
|
||||||
export class GalleriesCriterion extends ILabeledIdCriterion {
|
export class GalleriesCriterion extends ILabeledIdCriterion {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export const GenderCriterionOption = new CriterionOption({
|
||||||
messageID: "gender",
|
messageID: "gender",
|
||||||
type: "gender",
|
type: "gender",
|
||||||
options: genderStrings,
|
options: genderStrings,
|
||||||
|
makeCriterion: () => new GenderCriterion(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export class GenderCriterion extends StringCriterion {
|
export class GenderCriterion extends StringCriterion {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ import { CriterionOption, StringCriterion } from "./criterion";
|
||||||
|
|
||||||
export const HasChaptersCriterionOption = new CriterionOption({
|
export const HasChaptersCriterionOption = new CriterionOption({
|
||||||
messageID: "hasChapters",
|
messageID: "hasChapters",
|
||||||
type: "hasChapters",
|
type: "has_chapters",
|
||||||
parameterName: "has_chapters",
|
|
||||||
options: [true.toString(), false.toString()],
|
options: [true.toString(), false.toString()],
|
||||||
|
makeCriterion: () => new HasChaptersCriterion(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export class HasChaptersCriterion extends StringCriterion {
|
export class HasChaptersCriterion extends StringCriterion {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ import { CriterionOption, StringCriterion } from "./criterion";
|
||||||
|
|
||||||
export const HasMarkersCriterionOption = new CriterionOption({
|
export const HasMarkersCriterionOption = new CriterionOption({
|
||||||
messageID: "hasMarkers",
|
messageID: "hasMarkers",
|
||||||
type: "hasMarkers",
|
type: "has_markers",
|
||||||
parameterName: "has_markers",
|
|
||||||
options: [true.toString(), false.toString()],
|
options: [true.toString(), false.toString()],
|
||||||
|
makeCriterion: () => new HasMarkersCriterion(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export class HasMarkersCriterion extends StringCriterion {
|
export class HasMarkersCriterion extends StringCriterion {
|
||||||
|
|
|
||||||
|
|
@ -11,25 +11,19 @@ export class IsMissingCriterion extends StringCriterion {
|
||||||
}
|
}
|
||||||
|
|
||||||
class IsMissingCriterionOptionClass extends CriterionOption {
|
class IsMissingCriterionOptionClass extends CriterionOption {
|
||||||
constructor(
|
constructor(messageID: string, type: CriterionType, options: Option[]) {
|
||||||
messageID: string,
|
|
||||||
value: CriterionType,
|
|
||||||
parameterName: string,
|
|
||||||
options: Option[]
|
|
||||||
) {
|
|
||||||
super({
|
super({
|
||||||
messageID,
|
messageID,
|
||||||
type: value,
|
type,
|
||||||
parameterName,
|
|
||||||
options,
|
options,
|
||||||
defaultModifier: CriterionModifier.Equals,
|
defaultModifier: CriterionModifier.Equals,
|
||||||
|
makeCriterion: () => new IsMissingCriterion(this),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SceneIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
export const SceneIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||||
"isMissing",
|
"isMissing",
|
||||||
"sceneIsMissing",
|
|
||||||
"is_missing",
|
"is_missing",
|
||||||
[
|
[
|
||||||
"title",
|
"title",
|
||||||
|
|
@ -48,73 +42,59 @@ export const SceneIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||||
|
|
||||||
export const ImageIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
export const ImageIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||||
"isMissing",
|
"isMissing",
|
||||||
"imageIsMissing",
|
|
||||||
"is_missing",
|
"is_missing",
|
||||||
["title", "galleries", "studio", "performers", "tags"]
|
["title", "galleries", "studio", "performers", "tags"]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const PerformerIsMissingCriterionOption =
|
export const PerformerIsMissingCriterionOption =
|
||||||
new IsMissingCriterionOptionClass(
|
new IsMissingCriterionOptionClass("isMissing", "is_missing", [
|
||||||
"isMissing",
|
"url",
|
||||||
"performerIsMissing",
|
"twitter",
|
||||||
"is_missing",
|
"instagram",
|
||||||
[
|
"ethnicity",
|
||||||
"url",
|
"country",
|
||||||
"twitter",
|
"hair_color",
|
||||||
"instagram",
|
"eye_color",
|
||||||
"ethnicity",
|
"height",
|
||||||
"country",
|
"weight",
|
||||||
"hair_color",
|
"measurements",
|
||||||
"eye_color",
|
"fake_tits",
|
||||||
"height",
|
"career_length",
|
||||||
"weight",
|
"tattoos",
|
||||||
"measurements",
|
"piercings",
|
||||||
"fake_tits",
|
"aliases",
|
||||||
"career_length",
|
"gender",
|
||||||
"tattoos",
|
"image",
|
||||||
"piercings",
|
"details",
|
||||||
"aliases",
|
"stash_id",
|
||||||
"gender",
|
]);
|
||||||
"image",
|
|
||||||
"details",
|
|
||||||
"stash_id",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
export const GalleryIsMissingCriterionOption =
|
export const GalleryIsMissingCriterionOption =
|
||||||
new IsMissingCriterionOptionClass(
|
new IsMissingCriterionOptionClass("isMissing", "is_missing", [
|
||||||
"isMissing",
|
"title",
|
||||||
"galleryIsMissing",
|
"details",
|
||||||
"is_missing",
|
"url",
|
||||||
[
|
"date",
|
||||||
"title",
|
"studio",
|
||||||
"details",
|
"performers",
|
||||||
"url",
|
"tags",
|
||||||
"date",
|
"scenes",
|
||||||
"studio",
|
]);
|
||||||
"performers",
|
|
||||||
"tags",
|
|
||||||
"scenes",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
export const TagIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
export const TagIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||||
"isMissing",
|
"isMissing",
|
||||||
"tagIsMissing",
|
|
||||||
"is_missing",
|
"is_missing",
|
||||||
["image"]
|
["image"]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const StudioIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
export const StudioIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||||
"isMissing",
|
"isMissing",
|
||||||
"studioIsMissing",
|
|
||||||
"is_missing",
|
"is_missing",
|
||||||
["image", "stash_id", "details"]
|
["image", "stash_id", "details"]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const MovieIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
export const MovieIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||||
"isMissing",
|
"isMissing",
|
||||||
"movieIsMissing",
|
|
||||||
"is_missing",
|
"is_missing",
|
||||||
["front_image", "back_image", "scenes"]
|
["front_image", "back_image", "scenes"]
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
|
import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
|
||||||
|
|
||||||
|
const inputType = "movies";
|
||||||
|
|
||||||
export const MoviesCriterionOption = new ILabeledIdCriterionOption(
|
export const MoviesCriterionOption = new ILabeledIdCriterionOption(
|
||||||
"movies",
|
"movies",
|
||||||
"movies",
|
"movies",
|
||||||
"movies",
|
false,
|
||||||
false
|
inputType
|
||||||
);
|
);
|
||||||
|
|
||||||
export class MoviesCriterion extends ILabeledIdCriterion {
|
export class MoviesCriterion extends ILabeledIdCriterion {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
import { Criterion, StringCriterionOption } from "./criterion";
|
import { Criterion, StringCriterionOption } from "./criterion";
|
||||||
|
|
||||||
export const NoneCriterionOption = new StringCriterionOption(
|
export const NoneCriterionOption = new StringCriterionOption("none", "none");
|
||||||
"none",
|
|
||||||
"none",
|
|
||||||
"none"
|
|
||||||
);
|
|
||||||
export class NoneCriterion extends Criterion<string> {
|
export class NoneCriterion extends Criterion<string> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NoneCriterionOption, "none");
|
super(NoneCriterionOption, "none");
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { BooleanCriterion, BooleanCriterionOption } from "./criterion";
|
import { BooleanCriterion, BooleanCriterionOption } from "./criterion";
|
||||||
|
|
||||||
export const OrganizedCriterionOption = new BooleanCriterionOption(
|
export const OrganizedCriterionOption = new BooleanCriterionOption(
|
||||||
"organized",
|
|
||||||
"organized",
|
"organized",
|
||||||
"organized"
|
"organized"
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,15 @@ const modifierOptions = [
|
||||||
|
|
||||||
const defaultModifier = CriterionModifier.IncludesAll;
|
const defaultModifier = CriterionModifier.IncludesAll;
|
||||||
|
|
||||||
|
const inputType = "performers";
|
||||||
|
|
||||||
export const PerformersCriterionOption = new CriterionOption({
|
export const PerformersCriterionOption = new CriterionOption({
|
||||||
messageID: "performers",
|
messageID: "performers",
|
||||||
type: "performers",
|
type: "performers",
|
||||||
parameterName: "performers",
|
|
||||||
modifierOptions,
|
modifierOptions,
|
||||||
defaultModifier,
|
defaultModifier,
|
||||||
|
makeCriterion: () => new PerformersCriterion(),
|
||||||
|
inputType,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class PerformersCriterion extends Criterion<ILabeledValueListValue> {
|
export class PerformersCriterion extends Criterion<ILabeledValueListValue> {
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,7 @@ import {
|
||||||
|
|
||||||
export const PhashCriterionOption = new CriterionOption({
|
export const PhashCriterionOption = new CriterionOption({
|
||||||
messageID: "media_info.phash",
|
messageID: "media_info.phash",
|
||||||
type: "phash",
|
type: "phash_distance",
|
||||||
parameterName: "phash_distance",
|
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
modifierOptions: [
|
modifierOptions: [
|
||||||
CriterionModifier.Equals,
|
CriterionModifier.Equals,
|
||||||
|
|
@ -21,6 +20,7 @@ export const PhashCriterionOption = new CriterionOption({
|
||||||
CriterionModifier.IsNull,
|
CriterionModifier.IsNull,
|
||||||
CriterionModifier.NotNull,
|
CriterionModifier.NotNull,
|
||||||
],
|
],
|
||||||
|
makeCriterion: () => new PhashCriterion(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export class PhashCriterion extends Criterion<IPhashDistanceValue> {
|
export class PhashCriterion extends Criterion<IPhashDistanceValue> {
|
||||||
|
|
@ -53,7 +53,7 @@ export class PhashCriterion extends Criterion<IPhashDistanceValue> {
|
||||||
export const DuplicatedCriterionOption = new BooleanCriterionOption(
|
export const DuplicatedCriterionOption = new BooleanCriterionOption(
|
||||||
"duplicated_phash",
|
"duplicated_phash",
|
||||||
"duplicated",
|
"duplicated",
|
||||||
"duplicated"
|
() => new DuplicatedCriterion()
|
||||||
);
|
);
|
||||||
|
|
||||||
export class DuplicatedCriterion extends PhashDuplicateCriterion {
|
export class DuplicatedCriterion extends PhashDuplicateCriterion {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,12 @@ import {
|
||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import { stringToResolution, resolutionStrings } from "src/utils/resolution";
|
import { stringToResolution, resolutionStrings } from "src/utils/resolution";
|
||||||
import { CriterionType } from "../types";
|
import { CriterionType } from "../types";
|
||||||
import { CriterionOption, StringCriterion } from "./criterion";
|
import {
|
||||||
|
Criterion,
|
||||||
|
CriterionOption,
|
||||||
|
CriterionValue,
|
||||||
|
StringCriterion,
|
||||||
|
} from "./criterion";
|
||||||
|
|
||||||
abstract class AbstractResolutionCriterion extends StringCriterion {
|
abstract class AbstractResolutionCriterion extends StringCriterion {
|
||||||
protected toCriterionInput(): ResolutionCriterionInput | undefined {
|
protected toCriterionInput(): ResolutionCriterionInput | undefined {
|
||||||
|
|
@ -20,11 +25,13 @@ abstract class AbstractResolutionCriterion extends StringCriterion {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ResolutionCriterionOptionType extends CriterionOption {
|
class ResolutionCriterionOptionType extends CriterionOption {
|
||||||
constructor(value: CriterionType) {
|
constructor(
|
||||||
|
value: CriterionType,
|
||||||
|
makeCriterion: () => Criterion<CriterionValue>
|
||||||
|
) {
|
||||||
super({
|
super({
|
||||||
messageID: value,
|
messageID: value,
|
||||||
type: value,
|
type: value,
|
||||||
parameterName: value,
|
|
||||||
modifierOptions: [
|
modifierOptions: [
|
||||||
CriterionModifier.Equals,
|
CriterionModifier.Equals,
|
||||||
CriterionModifier.NotEquals,
|
CriterionModifier.NotEquals,
|
||||||
|
|
@ -32,12 +39,14 @@ class ResolutionCriterionOptionType extends CriterionOption {
|
||||||
CriterionModifier.LessThan,
|
CriterionModifier.LessThan,
|
||||||
],
|
],
|
||||||
options: resolutionStrings,
|
options: resolutionStrings,
|
||||||
|
makeCriterion,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ResolutionCriterionOption = new ResolutionCriterionOptionType(
|
export const ResolutionCriterionOption = new ResolutionCriterionOptionType(
|
||||||
"resolution"
|
"resolution",
|
||||||
|
() => new ResolutionCriterion()
|
||||||
);
|
);
|
||||||
export class ResolutionCriterion extends AbstractResolutionCriterion {
|
export class ResolutionCriterion extends AbstractResolutionCriterion {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -46,7 +55,10 @@ export class ResolutionCriterion extends AbstractResolutionCriterion {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AverageResolutionCriterionOption =
|
export const AverageResolutionCriterionOption =
|
||||||
new ResolutionCriterionOptionType("average_resolution");
|
new ResolutionCriterionOptionType(
|
||||||
|
"average_resolution",
|
||||||
|
() => new AverageResolutionCriterion()
|
||||||
|
);
|
||||||
|
|
||||||
export class AverageResolutionCriterion extends AbstractResolutionCriterion {
|
export class AverageResolutionCriterion extends AbstractResolutionCriterion {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,13 @@ import { Criterion, CriterionOption } from "./criterion";
|
||||||
export const StashIDCriterionOption = new CriterionOption({
|
export const StashIDCriterionOption = new CriterionOption({
|
||||||
messageID: "stash_id",
|
messageID: "stash_id",
|
||||||
type: "stash_id_endpoint",
|
type: "stash_id_endpoint",
|
||||||
parameterName: "stash_id_endpoint",
|
|
||||||
modifierOptions: [
|
modifierOptions: [
|
||||||
CriterionModifier.Equals,
|
CriterionModifier.Equals,
|
||||||
CriterionModifier.NotEquals,
|
CriterionModifier.NotEquals,
|
||||||
CriterionModifier.IsNull,
|
CriterionModifier.IsNull,
|
||||||
CriterionModifier.NotNull,
|
CriterionModifier.NotNull,
|
||||||
],
|
],
|
||||||
|
makeCriterion: () => new StashIDCriterion(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export class StashIDCriterion extends Criterion<IStashIDValue> {
|
export class StashIDCriterion extends Criterion<IStashIDValue> {
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,15 @@ const modifierOptions = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const defaultModifier = CriterionModifier.Includes;
|
const defaultModifier = CriterionModifier.Includes;
|
||||||
|
const inputType = "studios";
|
||||||
|
|
||||||
export const StudiosCriterionOption = new CriterionOption({
|
export const StudiosCriterionOption = new CriterionOption({
|
||||||
messageID: "studios",
|
messageID: "studios",
|
||||||
type: "studios",
|
type: "studios",
|
||||||
parameterName: "studios",
|
|
||||||
modifierOptions,
|
modifierOptions,
|
||||||
defaultModifier,
|
defaultModifier,
|
||||||
|
makeCriterion: () => new StudiosCriterion(),
|
||||||
|
inputType,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class StudiosCriterion extends IHierarchicalLabeledIdCriterion {
|
export class StudiosCriterion extends IHierarchicalLabeledIdCriterion {
|
||||||
|
|
@ -29,10 +31,10 @@ export class StudiosCriterion extends IHierarchicalLabeledIdCriterion {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ParentStudiosCriterionOption = new ILabeledIdCriterionOption(
|
export const ParentStudiosCriterionOption = new ILabeledIdCriterionOption(
|
||||||
"parent_studios",
|
|
||||||
"parent_studios",
|
"parent_studios",
|
||||||
"parents",
|
"parents",
|
||||||
false
|
false,
|
||||||
|
inputType
|
||||||
);
|
);
|
||||||
export class ParentStudiosCriterion extends ILabeledIdCriterion {
|
export class ParentStudiosCriterion extends ILabeledIdCriterion {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { CriterionModifier } from "src/core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import { CriterionOption, IHierarchicalLabeledIdCriterion } from "./criterion";
|
import { CriterionOption, IHierarchicalLabeledIdCriterion } from "./criterion";
|
||||||
|
import { CriterionType } from "../types";
|
||||||
|
|
||||||
const modifierOptions = [
|
const defaultModifierOptions = [
|
||||||
CriterionModifier.IncludesAll,
|
CriterionModifier.IncludesAll,
|
||||||
CriterionModifier.Includes,
|
CriterionModifier.Includes,
|
||||||
CriterionModifier.Equals,
|
CriterionModifier.Equals,
|
||||||
|
|
@ -17,41 +18,53 @@ const withoutEqualsModifierOptions = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const defaultModifier = CriterionModifier.IncludesAll;
|
const defaultModifier = CriterionModifier.IncludesAll;
|
||||||
|
const inputType = "tags";
|
||||||
|
|
||||||
export const TagsCriterionOption = new CriterionOption({
|
export class TagsCriterionOptionClass extends CriterionOption {
|
||||||
messageID: "tags",
|
constructor(
|
||||||
type: "tags",
|
messageID: string,
|
||||||
parameterName: "tags",
|
type: CriterionType,
|
||||||
modifierOptions,
|
modifierOptions: CriterionModifier[]
|
||||||
defaultModifier,
|
) {
|
||||||
});
|
super({
|
||||||
export const SceneTagsCriterionOption = new CriterionOption({
|
messageID,
|
||||||
messageID: "sceneTags",
|
type,
|
||||||
type: "sceneTags",
|
modifierOptions,
|
||||||
parameterName: "scene_tags",
|
defaultModifier,
|
||||||
modifierOptions,
|
makeCriterion: () => new TagsCriterion(this),
|
||||||
defaultModifier,
|
inputType,
|
||||||
});
|
});
|
||||||
export const PerformerTagsCriterionOption = new CriterionOption({
|
}
|
||||||
messageID: "performerTags",
|
}
|
||||||
type: "performerTags",
|
|
||||||
parameterName: "performer_tags",
|
export const TagsCriterionOption = new TagsCriterionOptionClass(
|
||||||
modifierOptions: withoutEqualsModifierOptions,
|
"tags",
|
||||||
defaultModifier,
|
"tags",
|
||||||
});
|
defaultModifierOptions
|
||||||
export const ParentTagsCriterionOption = new CriterionOption({
|
);
|
||||||
messageID: "parent_tags",
|
|
||||||
type: "parentTags",
|
export const SceneTagsCriterionOption = new TagsCriterionOptionClass(
|
||||||
parameterName: "parents",
|
"scene_tags",
|
||||||
modifierOptions: withoutEqualsModifierOptions,
|
"scene_tags",
|
||||||
defaultModifier,
|
defaultModifierOptions
|
||||||
});
|
);
|
||||||
export const ChildTagsCriterionOption = new CriterionOption({
|
|
||||||
messageID: "sub_tags",
|
export const PerformerTagsCriterionOption = new TagsCriterionOptionClass(
|
||||||
type: "childTags",
|
"performer_tags",
|
||||||
parameterName: "children",
|
"performer_tags",
|
||||||
modifierOptions: withoutEqualsModifierOptions,
|
withoutEqualsModifierOptions
|
||||||
defaultModifier,
|
);
|
||||||
});
|
|
||||||
|
export const ParentTagsCriterionOption = new TagsCriterionOptionClass(
|
||||||
|
"parent_tags",
|
||||||
|
"parents",
|
||||||
|
withoutEqualsModifierOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ChildTagsCriterionOption = new TagsCriterionOptionClass(
|
||||||
|
"sub_tags",
|
||||||
|
"children",
|
||||||
|
withoutEqualsModifierOptions
|
||||||
|
);
|
||||||
|
|
||||||
export class TagsCriterion extends IHierarchicalLabeledIdCriterion {}
|
export class TagsCriterion extends IHierarchicalLabeledIdCriterion {}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,12 @@ import {
|
||||||
ConfigDataFragment,
|
ConfigDataFragment,
|
||||||
FilterMode,
|
FilterMode,
|
||||||
FindFilterType,
|
FindFilterType,
|
||||||
|
SavedFilterDataFragment,
|
||||||
SortDirectionEnum,
|
SortDirectionEnum,
|
||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import { Criterion, CriterionValue } from "./criteria/criterion";
|
import { Criterion, CriterionValue } from "./criteria/criterion";
|
||||||
import { makeCriteria } from "./criteria/factory";
|
import { makeCriteria } from "./criteria/factory";
|
||||||
import { DisplayMode } from "./types";
|
import { CriterionType, DisplayMode } from "./types";
|
||||||
|
|
||||||
interface IDecodedParams {
|
interface IDecodedParams {
|
||||||
perPage?: number;
|
perPage?: number;
|
||||||
|
|
@ -127,7 +128,7 @@ export class ListFilterModel {
|
||||||
for (const jsonString of params.c) {
|
for (const jsonString of params.c) {
|
||||||
try {
|
try {
|
||||||
const encodedCriterion = JSON.parse(jsonString);
|
const encodedCriterion = JSON.parse(jsonString);
|
||||||
const criterion = makeCriteria(this.config, encodedCriterion.type);
|
const criterion = makeCriteria(this.mode, encodedCriterion.type);
|
||||||
// it's possible that we have unsupported criteria. Just skip if so.
|
// it's possible that we have unsupported criteria. Just skip if so.
|
||||||
if (criterion) {
|
if (criterion) {
|
||||||
criterion.setFromEncodedCriterion(encodedCriterion);
|
criterion.setFromEncodedCriterion(encodedCriterion);
|
||||||
|
|
@ -248,8 +249,41 @@ export class ListFilterModel {
|
||||||
this.configureFromDecodedParams(decoded);
|
this.configureFromDecodedParams(decoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
public configureFromJSON(json: string) {
|
public configureFromSavedFilter(savedFilter: SavedFilterDataFragment) {
|
||||||
this.configureFromDecodedParams(JSON.parse(json));
|
const {
|
||||||
|
find_filter: findFilter,
|
||||||
|
object_filter: objectFilter,
|
||||||
|
ui_options: uiOptions,
|
||||||
|
} = savedFilter;
|
||||||
|
|
||||||
|
this.itemsPerPage = findFilter?.per_page ?? this.itemsPerPage;
|
||||||
|
this.sortBy = findFilter?.sort ?? this.sortBy;
|
||||||
|
// parse the random seed if provided
|
||||||
|
const match = this.sortBy?.match(/^random_(\d+)$/);
|
||||||
|
if (match) {
|
||||||
|
this.sortBy = "random";
|
||||||
|
this.randomSeed = Number.parseInt(match[1], 10);
|
||||||
|
}
|
||||||
|
this.sortDirection =
|
||||||
|
(findFilter?.direction as SortDirectionEnum) ?? this.sortDirection;
|
||||||
|
this.searchTerm = findFilter?.q ?? this.searchTerm;
|
||||||
|
|
||||||
|
this.displayMode = uiOptions?.display_mode ?? this.displayMode;
|
||||||
|
this.zoomIndex = uiOptions?.zoom_index ?? this.zoomIndex;
|
||||||
|
|
||||||
|
this.currentPage = 1;
|
||||||
|
|
||||||
|
this.criteria = [];
|
||||||
|
if (objectFilter) {
|
||||||
|
Object.keys(objectFilter).forEach((key) => {
|
||||||
|
const criterion = makeCriteria(this.mode, key as CriterionType);
|
||||||
|
// it's possible that we have unsupported criteria. Just skip if so.
|
||||||
|
if (criterion) {
|
||||||
|
criterion.setFromEncodedCriterion(objectFilter[key]);
|
||||||
|
this.criteria.push(criterion);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setRandomSeed() {
|
private setRandomSeed() {
|
||||||
|
|
@ -405,4 +439,22 @@ export class ListFilterModel {
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public makeSavedFindFilter() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const output: Record<string, any> = {};
|
||||||
|
this.criteria.forEach((criterion) => {
|
||||||
|
criterion.toSavedFilter(output);
|
||||||
|
});
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
public makeUIOptions(): Record<string, any> {
|
||||||
|
return {
|
||||||
|
display_mode: this.displayMode,
|
||||||
|
zoom_index: this.zoomIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,7 @@ const criterionOptions = [
|
||||||
createStringCriterionOption("title"),
|
createStringCriterionOption("title"),
|
||||||
createStringCriterionOption("details"),
|
createStringCriterionOption("details"),
|
||||||
createPathCriterionOption("path"),
|
createPathCriterionOption("path"),
|
||||||
createStringCriterionOption(
|
createStringCriterionOption("checksum", "media_info.checksum"),
|
||||||
"galleryChecksum",
|
|
||||||
"media_info.checksum",
|
|
||||||
"checksum"
|
|
||||||
),
|
|
||||||
new NullNumberCriterionOption("rating", "rating100"),
|
new NullNumberCriterionOption("rating", "rating100"),
|
||||||
OrganizedCriterionOption,
|
OrganizedCriterionOption,
|
||||||
AverageResolutionCriterionOption,
|
AverageResolutionCriterionOption,
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ const criterionOptions = [
|
||||||
createMandatoryNumberCriterionOption("gallery_count"),
|
createMandatoryNumberCriterionOption("gallery_count"),
|
||||||
createMandatoryNumberCriterionOption("o_counter"),
|
createMandatoryNumberCriterionOption("o_counter"),
|
||||||
createBooleanCriterionOption("ignore_auto_tag"),
|
createBooleanCriterionOption("ignore_auto_tag"),
|
||||||
new NumberCriterionOption("height", "height_cm", "height_cm"),
|
new NumberCriterionOption("height", "height_cm"),
|
||||||
...numberCriteria.map((c) => createNumberCriterionOption(c)),
|
...numberCriteria.map((c) => createNumberCriterionOption(c)),
|
||||||
...stringCriteria.map((c) => createStringCriterionOption(c)),
|
...stringCriteria.map((c) => createStringCriterionOption(c)),
|
||||||
createDateCriterionOption("birthdate"),
|
createDateCriterionOption("birthdate"),
|
||||||
|
|
|
||||||
|
|
@ -59,16 +59,12 @@ const displayModeOptions = [
|
||||||
|
|
||||||
const criterionOptions = [
|
const criterionOptions = [
|
||||||
createStringCriterionOption("title"),
|
createStringCriterionOption("title"),
|
||||||
createStringCriterionOption("scene_code"),
|
createStringCriterionOption("code", "scene_code"),
|
||||||
createPathCriterionOption("path"),
|
createPathCriterionOption("path"),
|
||||||
createStringCriterionOption("details"),
|
createStringCriterionOption("details"),
|
||||||
createStringCriterionOption("director"),
|
createStringCriterionOption("director"),
|
||||||
createMandatoryStringCriterionOption("oshash", "media_info.hash"),
|
createMandatoryStringCriterionOption("oshash", "media_info.hash"),
|
||||||
createStringCriterionOption(
|
createStringCriterionOption("checksum", "media_info.checksum"),
|
||||||
"sceneChecksum",
|
|
||||||
"media_info.checksum",
|
|
||||||
"checksum"
|
|
||||||
),
|
|
||||||
PhashCriterionOption,
|
PhashCriterionOption,
|
||||||
DuplicatedCriterionOption,
|
DuplicatedCriterionOption,
|
||||||
OrganizedCriterionOption,
|
OrganizedCriterionOption,
|
||||||
|
|
|
||||||
|
|
@ -53,17 +53,9 @@ const criterionOptions = [
|
||||||
createMandatoryNumberCriterionOption("performer_count"),
|
createMandatoryNumberCriterionOption("performer_count"),
|
||||||
createMandatoryNumberCriterionOption("marker_count"),
|
createMandatoryNumberCriterionOption("marker_count"),
|
||||||
ParentTagsCriterionOption,
|
ParentTagsCriterionOption,
|
||||||
new MandatoryNumberCriterionOption(
|
new MandatoryNumberCriterionOption("parent_tag_count", "parent_count"),
|
||||||
"parent_tag_count",
|
|
||||||
"parent_tag_count",
|
|
||||||
"parent_count"
|
|
||||||
),
|
|
||||||
ChildTagsCriterionOption,
|
ChildTagsCriterionOption,
|
||||||
new MandatoryNumberCriterionOption(
|
new MandatoryNumberCriterionOption("sub_tag_count", "child_count"),
|
||||||
"sub_tag_count",
|
|
||||||
"child_tag_count",
|
|
||||||
"child_count"
|
|
||||||
),
|
|
||||||
createMandatoryTimestampCriterionOption("created_at"),
|
createMandatoryTimestampCriterionOption("created_at"),
|
||||||
createMandatoryTimestampCriterionOption("updated_at"),
|
createMandatoryTimestampCriterionOption("updated_at"),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -112,20 +112,12 @@ export type CriterionType =
|
||||||
| "video_codec"
|
| "video_codec"
|
||||||
| "audio_codec"
|
| "audio_codec"
|
||||||
| "duration"
|
| "duration"
|
||||||
| "favorite"
|
| "filter_favorites"
|
||||||
| "hasMarkers"
|
| "has_markers"
|
||||||
| "sceneIsMissing"
|
| "is_missing"
|
||||||
| "imageIsMissing"
|
|
||||||
| "performerIsMissing"
|
|
||||||
| "galleryIsMissing"
|
|
||||||
| "tagIsMissing"
|
|
||||||
| "studioIsMissing"
|
|
||||||
| "movieIsMissing"
|
|
||||||
| "tags"
|
| "tags"
|
||||||
| "sceneTags"
|
| "scene_tags"
|
||||||
| "performerTags"
|
| "performer_tags"
|
||||||
| "parentTags"
|
|
||||||
| "childTags"
|
|
||||||
| "tag_count"
|
| "tag_count"
|
||||||
| "performers"
|
| "performers"
|
||||||
| "studios"
|
| "studios"
|
||||||
|
|
@ -149,7 +141,8 @@ export type CriterionType =
|
||||||
| "piercings"
|
| "piercings"
|
||||||
| "aliases"
|
| "aliases"
|
||||||
| "gender"
|
| "gender"
|
||||||
| "parent_studios"
|
| "parents"
|
||||||
|
| "children"
|
||||||
| "scene_count"
|
| "scene_count"
|
||||||
| "marker_count"
|
| "marker_count"
|
||||||
| "image_count"
|
| "image_count"
|
||||||
|
|
@ -169,13 +162,11 @@ export type CriterionType =
|
||||||
| "title"
|
| "title"
|
||||||
| "oshash"
|
| "oshash"
|
||||||
| "checksum"
|
| "checksum"
|
||||||
| "sceneChecksum"
|
| "phash_distance"
|
||||||
| "galleryChecksum"
|
|
||||||
| "phash"
|
|
||||||
| "director"
|
| "director"
|
||||||
| "synopsis"
|
| "synopsis"
|
||||||
| "parent_tag_count"
|
| "parent_count"
|
||||||
| "child_tag_count"
|
| "child_count"
|
||||||
| "performer_favorite"
|
| "performer_favorite"
|
||||||
| "performer_age"
|
| "performer_age"
|
||||||
| "duplicated"
|
| "duplicated"
|
||||||
|
|
@ -191,6 +182,6 @@ export type CriterionType =
|
||||||
| "scene_created_at"
|
| "scene_created_at"
|
||||||
| "scene_updated_at"
|
| "scene_updated_at"
|
||||||
| "description"
|
| "description"
|
||||||
| "scene_code"
|
| "code"
|
||||||
| "disambiguation"
|
| "disambiguation"
|
||||||
| "hasChapters";
|
| "has_chapters";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue