Saved filters (#1474)

* Refactor list filter
* Filter/criterion refactor
* Rename option value to type
* Remove None from options
* Add saved filter button
* Integrate default filters
This commit is contained in:
WithoutPants
2021-06-16 14:53:32 +10:00
committed by GitHub
parent 4fe4da6c01
commit dc7584d77e
74 changed files with 2583 additions and 1263 deletions

View File

@@ -1,6 +1,10 @@
import { CriterionOption, StringCriterion } from "./criterion";
import { StringCriterion, StringCriterionOption } from "./criterion";
const countryCriterionOption = new CriterionOption("country", "country");
const countryCriterionOption = new StringCriterionOption(
"country",
"country",
"country"
);
export class CountryCriterion extends StringCriterion {
constructor() {

View File

@@ -16,106 +16,58 @@ import {
IHierarchicalLabelValue,
} from "../types";
type Option = string | number | IOptionType;
export type Option = string | number | IOptionType;
export type CriterionValue =
| string
| number
| ILabeledId[]
| IHierarchicalLabelValue;
const modifierMessageIDs = {
[CriterionModifier.Equals]: "criterion_modifier.equals",
[CriterionModifier.NotEquals]: "criterion_modifier.not_equals",
[CriterionModifier.GreaterThan]: "criterion_modifier.greater_than",
[CriterionModifier.LessThan]: "criterion_modifier.less_than",
[CriterionModifier.IsNull]: "criterion_modifier.is_null",
[CriterionModifier.NotNull]: "criterion_modifier.not_null",
[CriterionModifier.Includes]: "criterion_modifier.includes",
[CriterionModifier.IncludesAll]: "criterion_modifier.includes_all",
[CriterionModifier.Excludes]: "criterion_modifier.excludes",
[CriterionModifier.MatchesRegex]: "criterion_modifier.matches_regex",
[CriterionModifier.NotMatchesRegex]: "criterion_modifier.not_matches_regex",
};
// V = criterion value type
export abstract class Criterion<V extends CriterionValue> {
public static getModifierOption(
modifier: CriterionModifier = CriterionModifier.Equals
): ILabeledValue {
switch (modifier) {
case CriterionModifier.Equals:
return { value: CriterionModifier.Equals, label: "Equals" };
case CriterionModifier.NotEquals:
return { value: CriterionModifier.NotEquals, label: "Not Equals" };
case CriterionModifier.GreaterThan:
return { value: CriterionModifier.GreaterThan, label: "Greater Than" };
case CriterionModifier.LessThan:
return { value: CriterionModifier.LessThan, label: "Less Than" };
case CriterionModifier.IsNull:
return { value: CriterionModifier.IsNull, label: "Is NULL" };
case CriterionModifier.NotNull:
return { value: CriterionModifier.NotNull, label: "Not NULL" };
case CriterionModifier.IncludesAll:
return { value: CriterionModifier.IncludesAll, label: "Includes All" };
case CriterionModifier.Includes:
return { value: CriterionModifier.Includes, label: "Includes" };
case CriterionModifier.Excludes:
return { value: CriterionModifier.Excludes, label: "Excludes" };
case CriterionModifier.MatchesRegex:
return {
value: CriterionModifier.MatchesRegex,
label: "Matches Regex",
};
case CriterionModifier.NotMatchesRegex:
return {
value: CriterionModifier.NotMatchesRegex,
label: "Not Matches Regex",
};
}
const messageID = modifierMessageIDs[modifier];
return { value: modifier, label: messageID };
}
public criterionOption: CriterionOption;
public abstract modifier: CriterionModifier;
public abstract modifierOptions: ILabeledValue[];
public abstract options: Option[] | undefined;
public abstract value: V;
public inputType: "number" | "text" | undefined;
public modifier: CriterionModifier;
public value: V;
public abstract getLabelValue(): string;
constructor(type: CriterionOption) {
constructor(type: CriterionOption, value: V) {
this.criterionOption = type;
this.modifier = type.defaultModifier;
this.value = value;
}
public static getModifierLabel(intl: IntlShape, modifier: CriterionModifier) {
const modifierMessageID = modifierMessageIDs[modifier];
return modifierMessageID
? intl.formatMessage({ id: modifierMessageID })
: "";
}
public getLabel(intl: IntlShape): string {
let modifierMessageID: string;
switch (this.modifier) {
case CriterionModifier.Equals:
modifierMessageID = "criterion_modifier.equals";
break;
case CriterionModifier.NotEquals:
modifierMessageID = "criterion_modifier.not_equals";
break;
case CriterionModifier.GreaterThan:
modifierMessageID = "criterion_modifier.greater_than";
break;
case CriterionModifier.LessThan:
modifierMessageID = "criterion_modifier.less_than";
break;
case CriterionModifier.IsNull:
modifierMessageID = "criterion_modifier.is_null";
break;
case CriterionModifier.NotNull:
modifierMessageID = "criterion_modifier.not_null";
break;
case CriterionModifier.Includes:
modifierMessageID = "criterion_modifier.includes";
break;
case CriterionModifier.IncludesAll:
modifierMessageID = "criterion_modifier.includes_all";
break;
case CriterionModifier.Excludes:
modifierMessageID = "criterion_modifier.excludes";
break;
case CriterionModifier.MatchesRegex:
modifierMessageID = "criterion_modifier.matches_regex";
break;
case CriterionModifier.NotMatchesRegex:
modifierMessageID = "criterion_modifier.not_matches_regex";
break;
default:
modifierMessageID = "";
}
const modifierString = modifierMessageID
? intl.formatMessage({ id: modifierMessageID })
: "";
const modifierString = Criterion.getModifierLabel(intl, this.modifier);
let valueString = "";
if (
@@ -145,7 +97,7 @@ export abstract class Criterion<V extends CriterionValue> {
public toJSON() {
const encodedCriterion = {
type: this.criterionOption.value,
type: this.criterionOption.type,
// #394 - the presence of a # symbol results in the query URL being
// malformed. We could set encode: true in the queryString.stringify
// call below, but this results in a URL that gets pretty long and ugly.
@@ -171,37 +123,72 @@ export abstract class Criterion<V extends CriterionValue> {
}
}
export type InputType = "number" | "text" | undefined;
interface ICriterionOptionsParams {
messageID: string;
type: CriterionType;
inputType?: InputType;
parameterName?: string;
modifierOptions?: CriterionModifier[];
defaultModifier?: CriterionModifier;
options?: Option[];
}
export class CriterionOption {
public readonly messageID: string;
public readonly value: CriterionType;
public readonly type: CriterionType;
public readonly parameterName: string;
public readonly modifierOptions: ILabeledValue[];
public readonly defaultModifier: CriterionModifier;
public readonly options: Option[] | undefined;
public readonly inputType: InputType;
constructor(messageID: string, value: CriterionType, parameterName?: string) {
this.messageID = messageID;
this.value = value;
this.parameterName = parameterName ?? value;
constructor(options: ICriterionOptionsParams) {
this.messageID = options.messageID;
this.type = options.type;
this.parameterName = options.parameterName ?? options.type;
this.modifierOptions = (options.modifierOptions ?? []).map((o) =>
Criterion.getModifierOption(o)
);
this.defaultModifier = options.defaultModifier ?? CriterionModifier.Equals;
this.options = options.options;
this.inputType = options.inputType;
}
}
export function createCriterionOption(value: CriterionType) {
return new CriterionOption(value, value);
export class StringCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
parameterName?: string,
options?: Option[]
) {
super({
messageID,
type: value,
parameterName,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
CriterionModifier.Includes,
CriterionModifier.Excludes,
CriterionModifier.IsNull,
CriterionModifier.NotNull,
CriterionModifier.MatchesRegex,
CriterionModifier.NotMatchesRegex,
],
defaultModifier: CriterionModifier.Equals,
options,
inputType: "text",
});
}
}
export function createStringCriterionOption(value: CriterionType) {
return new StringCriterionOption(value, value, value);
}
export class StringCriterion extends Criterion<string> {
public modifier = CriterionModifier.Equals;
public modifierOptions = [
StringCriterion.getModifierOption(CriterionModifier.Equals),
StringCriterion.getModifierOption(CriterionModifier.NotEquals),
StringCriterion.getModifierOption(CriterionModifier.Includes),
StringCriterion.getModifierOption(CriterionModifier.Excludes),
StringCriterion.getModifierOption(CriterionModifier.IsNull),
StringCriterion.getModifierOption(CriterionModifier.NotNull),
StringCriterion.getModifierOption(CriterionModifier.MatchesRegex),
StringCriterion.getModifierOption(CriterionModifier.NotMatchesRegex),
];
public options: string[] | undefined;
public value: string = "";
public getLabelValue() {
return this.value;
}
@@ -218,74 +205,125 @@ export class StringCriterion extends Criterion<string> {
return str.replaceAll(c, encodeURIComponent(c));
}
constructor(type: CriterionOption, options?: string[]) {
super(type);
this.options = options;
this.inputType = "text";
constructor(type: CriterionOption) {
super(type, "");
}
}
export class MandatoryStringCriterion extends StringCriterion {
public modifierOptions = [
StringCriterion.getModifierOption(CriterionModifier.Equals),
StringCriterion.getModifierOption(CriterionModifier.NotEquals),
StringCriterion.getModifierOption(CriterionModifier.Includes),
StringCriterion.getModifierOption(CriterionModifier.Excludes),
StringCriterion.getModifierOption(CriterionModifier.MatchesRegex),
StringCriterion.getModifierOption(CriterionModifier.NotMatchesRegex),
];
export class MandatoryStringCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
parameterName?: string,
options?: Option[]
) {
super({
messageID,
type: value,
parameterName,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
CriterionModifier.Includes,
CriterionModifier.Excludes,
CriterionModifier.MatchesRegex,
CriterionModifier.NotMatchesRegex,
],
defaultModifier: CriterionModifier.Equals,
options,
inputType: "text",
});
}
}
export class BooleanCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType, parameterName?: string) {
super({
messageID,
type: value,
parameterName,
modifierOptions: [],
defaultModifier: CriterionModifier.Equals,
options: [true.toString(), false.toString()],
});
}
}
export class BooleanCriterion extends StringCriterion {
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
constructor(type: CriterionOption) {
super(type, [true.toString(), false.toString()]);
}
protected toCriterionInput(): boolean {
return this.value === "true";
}
}
export class NumberCriterion extends Criterion<number> {
public modifier = CriterionModifier.Equals;
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Equals),
Criterion.getModifierOption(CriterionModifier.NotEquals),
Criterion.getModifierOption(CriterionModifier.GreaterThan),
Criterion.getModifierOption(CriterionModifier.LessThan),
Criterion.getModifierOption(CriterionModifier.IsNull),
Criterion.getModifierOption(CriterionModifier.NotNull),
];
public options: number[] | undefined;
public value: number = 0;
export class NumberCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
parameterName?: string,
options?: Option[]
) {
super({
messageID,
type: value,
parameterName,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
CriterionModifier.GreaterThan,
CriterionModifier.LessThan,
CriterionModifier.IsNull,
CriterionModifier.NotNull,
],
defaultModifier: CriterionModifier.Equals,
options,
inputType: "number",
});
}
}
export function createNumberCriterionOption(value: CriterionType) {
return new NumberCriterionOption(value, value, value);
}
export class NumberCriterion extends Criterion<number> {
public getLabelValue() {
return this.value.toString();
}
constructor(type: CriterionOption, options?: number[]) {
super(type);
this.options = options;
this.inputType = "number";
constructor(type: CriterionOption) {
super(type, 0);
}
}
export abstract class ILabeledIdCriterion extends Criterion<ILabeledId[]> {
public modifier = CriterionModifier.IncludesAll;
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.IncludesAll),
Criterion.getModifierOption(CriterionModifier.Includes),
Criterion.getModifierOption(CriterionModifier.Excludes),
];
export class ILabeledIdCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
parameterName: string,
includeAll: boolean
) {
const modifierOptions = [
CriterionModifier.Includes,
CriterionModifier.Excludes,
];
public options: IOptionType[] = [];
public value: ILabeledId[] = [];
let defaultModifier = CriterionModifier.Includes;
if (includeAll) {
modifierOptions.unshift(CriterionModifier.IncludesAll);
defaultModifier = CriterionModifier.IncludesAll;
}
super({
messageID,
type: value,
parameterName,
modifierOptions,
defaultModifier,
});
}
}
export class ILabeledIdCriterion extends Criterion<ILabeledId[]> {
public getLabelValue(): string {
return this.value.map((v) => v.label).join(", ");
}
@@ -303,33 +341,12 @@ export abstract class ILabeledIdCriterion extends Criterion<ILabeledId[]> {
});
}
constructor(type: CriterionOption, includeAll: boolean) {
super(type);
if (!includeAll) {
this.modifier = CriterionModifier.Includes;
this.modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Includes),
Criterion.getModifierOption(CriterionModifier.Excludes),
];
}
constructor(type: CriterionOption) {
super(type, []);
}
}
export abstract class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabelValue> {
public modifier = CriterionModifier.IncludesAll;
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.IncludesAll),
Criterion.getModifierOption(CriterionModifier.Includes),
Criterion.getModifierOption(CriterionModifier.Excludes),
];
public options: IOptionType[] = [];
public value: IHierarchicalLabelValue = {
items: [],
depth: 0,
};
export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabelValue> {
public encodeValue() {
return {
items: this.value.items.map((o) => {
@@ -357,52 +374,41 @@ export abstract class IHierarchicalLabeledIdCriterion extends Criterion<IHierarc
return `${labels} (+${this.value.depth > 0 ? this.value.depth : "all"})`;
}
public toJSON() {
const encodedCriterion = {
type: this.criterionOption.value,
value: this.encodeValue(),
modifier: this.modifier,
constructor(type: CriterionOption) {
const value: IHierarchicalLabelValue = {
items: [],
depth: 0,
};
return JSON.stringify(encodedCriterion);
}
constructor(type: CriterionOption, includeAll: boolean) {
super(type);
if (!includeAll) {
this.modifier = CriterionModifier.Includes;
this.modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Includes),
Criterion.getModifierOption(CriterionModifier.Excludes),
];
}
super(type, value);
}
}
export class MandatoryNumberCriterion extends NumberCriterion {
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Equals),
Criterion.getModifierOption(CriterionModifier.NotEquals),
Criterion.getModifierOption(CriterionModifier.GreaterThan),
Criterion.getModifierOption(CriterionModifier.LessThan),
];
export class MandatoryNumberCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType, parameterName?: string) {
super({
messageID,
type: value,
parameterName,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
CriterionModifier.GreaterThan,
CriterionModifier.LessThan,
],
defaultModifier: CriterionModifier.Equals,
inputType: "number",
});
}
}
export function createMandatoryNumberCriterionOption(value: CriterionType) {
return new MandatoryNumberCriterionOption(value, value, value);
}
export class DurationCriterion extends Criterion<number> {
public modifier = CriterionModifier.Equals;
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Equals),
Criterion.getModifierOption(CriterionModifier.NotEquals),
Criterion.getModifierOption(CriterionModifier.GreaterThan),
Criterion.getModifierOption(CriterionModifier.LessThan),
];
public options: number[] | undefined;
public value: number = 0;
constructor(type: CriterionOption, options?: number[]) {
super(type);
this.options = options;
constructor(type: CriterionOption) {
super(type, 0);
}
public getLabelValue() {

View File

@@ -3,25 +3,27 @@ import {
StringCriterion,
NumberCriterion,
DurationCriterion,
MandatoryStringCriterion,
MandatoryNumberCriterion,
CriterionOption,
NumberCriterionOption,
MandatoryStringCriterionOption,
MandatoryNumberCriterionOption,
StringCriterionOption,
ILabeledIdCriterion,
} from "./criterion";
import { OrganizedCriterion } from "./organized";
import { FavoriteCriterion } from "./favorite";
import { HasMarkersCriterion } from "./has-markers";
import {
PerformerIsMissingCriterion,
SceneIsMissingCriterion,
GalleryIsMissingCriterion,
TagIsMissingCriterion,
StudioIsMissingCriterion,
MovieIsMissingCriterion,
ImageIsMissingCriterion,
PerformerIsMissingCriterionOption,
ImageIsMissingCriterionOption,
TagIsMissingCriterionOption,
SceneIsMissingCriterionOption,
IsMissingCriterion,
GalleryIsMissingCriterionOption,
StudioIsMissingCriterionOption,
MovieIsMissingCriterionOption,
} from "./is-missing";
import { NoneCriterion } from "./none";
import { PerformersCriterion } from "./performers";
import { RatingCriterion } from "./rating";
import { AverageResolutionCriterion, ResolutionCriterion } from "./resolution";
import { StudiosCriterion, ParentStudiosCriterion } from "./studios";
import {
@@ -31,19 +33,22 @@ import {
TagsCriterionOption,
} from "./tags";
import { GenderCriterion } from "./gender";
import { MoviesCriterion } from "./movies";
import { MoviesCriterionOption } from "./movies";
import { GalleriesCriterion } from "./galleries";
import { CriterionType } from "../types";
import { InteractiveCriterion } from "./interactive";
import { RatingCriterionOption } from "./rating";
export function makeCriteria(type: CriterionType = "none") {
switch (type) {
case "none":
return new NoneCriterion();
case "path":
return new MandatoryStringCriterion(new CriterionOption(type, type));
return new StringCriterion(
new MandatoryStringCriterionOption(type, type)
);
case "rating":
return new RatingCriterion();
return new NumberCriterion(RatingCriterionOption);
case "organized":
return new OrganizedCriterion();
case "o_counter":
@@ -53,31 +58,33 @@ export function makeCriteria(type: CriterionType = "none") {
case "gallery_count":
case "performer_count":
case "tag_count":
return new MandatoryNumberCriterion(new CriterionOption(type, type));
return new NumberCriterion(
new MandatoryNumberCriterionOption(type, type)
);
case "resolution":
return new ResolutionCriterion();
case "average_resolution":
return new AverageResolutionCriterion();
case "duration":
return new DurationCriterion(new CriterionOption(type, type));
return new DurationCriterion(new NumberCriterionOption(type, type));
case "favorite":
return new FavoriteCriterion();
case "hasMarkers":
return new HasMarkersCriterion();
case "sceneIsMissing":
return new SceneIsMissingCriterion();
return new IsMissingCriterion(SceneIsMissingCriterionOption);
case "imageIsMissing":
return new ImageIsMissingCriterion();
return new IsMissingCriterion(ImageIsMissingCriterionOption);
case "performerIsMissing":
return new PerformerIsMissingCriterion();
return new IsMissingCriterion(PerformerIsMissingCriterionOption);
case "galleryIsMissing":
return new GalleryIsMissingCriterion();
return new IsMissingCriterion(GalleryIsMissingCriterionOption);
case "tagIsMissing":
return new TagIsMissingCriterion();
return new IsMissingCriterion(TagIsMissingCriterionOption);
case "studioIsMissing":
return new StudioIsMissingCriterion();
return new IsMissingCriterion(StudioIsMissingCriterionOption);
case "movieIsMissing":
return new MovieIsMissingCriterion();
return new IsMissingCriterion(MovieIsMissingCriterionOption);
case "tags":
return new TagsCriterion(TagsCriterionOption);
case "sceneTags":
@@ -91,15 +98,17 @@ export function makeCriteria(type: CriterionType = "none") {
case "parent_studios":
return new ParentStudiosCriterion();
case "movies":
return new MoviesCriterion();
return new ILabeledIdCriterion(MoviesCriterionOption);
case "galleries":
return new GalleriesCriterion();
case "birth_year":
case "death_year":
case "weight":
return new NumberCriterion(new CriterionOption(type, type));
return new NumberCriterion(new NumberCriterionOption(type, type));
case "age":
return new MandatoryNumberCriterion(new CriterionOption(type, type));
return new NumberCriterion(
new MandatoryNumberCriterionOption(type, type)
);
case "gender":
return new GenderCriterion();
case "ethnicity":
@@ -115,7 +124,7 @@ export function makeCriteria(type: CriterionType = "none") {
case "aliases":
case "url":
case "stash_id":
return new StringCriterion(new CriterionOption(type, type));
return new StringCriterion(new StringCriterionOption(type, type));
case "interactive":
return new InteractiveCriterion();
}

View File

@@ -1,6 +1,6 @@
import { BooleanCriterion, CriterionOption } from "./criterion";
import { BooleanCriterion, BooleanCriterionOption } from "./criterion";
export const FavoriteCriterionOption = new CriterionOption(
export const FavoriteCriterionOption = new BooleanCriterionOption(
"favourite",
"favorite",
"filter_favorites"

View File

@@ -1,9 +1,14 @@
import { CriterionOption, ILabeledIdCriterion } from "./criterion";
import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
const galleriesCriterionOption = new CriterionOption("galleries", "galleries");
const galleriesCriterionOption = new ILabeledIdCriterionOption(
"galleries",
"galleries",
"galleries",
true
);
export class GalleriesCriterion extends ILabeledIdCriterion {
constructor() {
super(galleriesCriterionOption, true);
super(galleriesCriterionOption);
}
}

View File

@@ -1,18 +1,16 @@
import {
CriterionModifier,
GenderCriterionInput,
} from "src/core/generated-graphql";
import { getGenderStrings, stringToGender } from "src/core/StashService";
import { GenderCriterionInput } from "src/core/generated-graphql";
import { genderStrings, stringToGender } from "src/utils/gender";
import { CriterionOption, StringCriterion } from "./criterion";
export const GenderCriterionOption = new CriterionOption("gender", "gender");
export const GenderCriterionOption = new CriterionOption({
messageID: "gender",
type: "gender",
options: genderStrings,
});
export class GenderCriterion extends StringCriterion {
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
constructor() {
super(GenderCriterionOption, getGenderStrings());
super(GenderCriterionOption);
}
protected toCriterionInput(): GenderCriterionInput {

View File

@@ -1,14 +1,15 @@
import { CriterionOption, StringCriterion } from "./criterion";
export const HasMarkersCriterionOption = new CriterionOption(
"hasMarkers",
"hasMarkers",
"has_markers"
);
export const HasMarkersCriterionOption = new CriterionOption({
messageID: "hasMarkers",
type: "hasMarkers",
parameterName: "has_markers",
options: [true.toString(), false.toString()],
});
export class HasMarkersCriterion extends StringCriterion {
constructor() {
super(HasMarkersCriterionOption, [true.toString(), false.toString()]);
super(HasMarkersCriterionOption);
}
protected toCriterionInput(): string {

View File

@@ -1,6 +1,6 @@
import { BooleanCriterion, CriterionOption } from "./criterion";
import { BooleanCriterion, BooleanCriterionOption } from "./criterion";
export const InteractiveCriterionOption = new CriterionOption(
export const InteractiveCriterionOption = new BooleanCriterionOption(
"interactive",
"interactive"
);

View File

@@ -1,7 +1,8 @@
import { CriterionModifier } from "src/core/generated-graphql";
import { CriterionOption, StringCriterion } from "./criterion";
import { CriterionType } from "../types";
import { CriterionOption, StringCriterion, Option } from "./criterion";
export abstract class IsMissingCriterion extends StringCriterion {
export class IsMissingCriterion extends StringCriterion {
public modifierOptions = [];
public modifier = CriterionModifier.Equals;
@@ -10,136 +11,98 @@ export abstract class IsMissingCriterion extends StringCriterion {
}
}
export const SceneIsMissingCriterionOption = new CriterionOption(
class IsMissingCriterionOptionClass extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
parameterName: string,
options: Option[]
) {
super({
messageID,
type: value,
parameterName,
options,
});
}
}
export const SceneIsMissingCriterionOption = new IsMissingCriterionOptionClass(
"isMissing",
"sceneIsMissing",
"is_missing"
"is_missing",
[
"title",
"details",
"url",
"date",
"galleries",
"studio",
"movie",
"performers",
"tags",
"stash_id",
]
);
export class SceneIsMissingCriterion extends IsMissingCriterion {
constructor() {
super(SceneIsMissingCriterionOption, [
"title",
"details",
"url",
"date",
"galleries",
"studio",
"movie",
"performers",
"tags",
"stash_id",
]);
}
}
export const ImageIsMissingCriterionOption = new CriterionOption(
export const ImageIsMissingCriterionOption = new IsMissingCriterionOptionClass(
"isMissing",
"imageIsMissing",
"is_missing"
"is_missing",
["title", "galleries", "studio", "performers", "tags"]
);
export class ImageIsMissingCriterion extends IsMissingCriterion {
constructor() {
super(ImageIsMissingCriterionOption, [
"title",
"galleries",
"studio",
"performers",
"tags",
]);
}
}
export const PerformerIsMissingCriterionOption = new CriterionOption(
export const PerformerIsMissingCriterionOption = new IsMissingCriterionOptionClass(
"isMissing",
"performerIsMissing",
"is_missing"
"is_missing",
[
"url",
"twitter",
"instagram",
"ethnicity",
"country",
"hair_color",
"eye_color",
"height",
"weight",
"measurements",
"fake_tits",
"career_length",
"tattoos",
"piercings",
"aliases",
"gender",
"image",
"details",
"stash_id",
]
);
export class PerformerIsMissingCriterion extends IsMissingCriterion {
constructor() {
super(PerformerIsMissingCriterionOption, [
"url",
"twitter",
"instagram",
"ethnicity",
"country",
"hair_color",
"eye_color",
"height",
"weight",
"measurements",
"fake_tits",
"career_length",
"tattoos",
"piercings",
"aliases",
"gender",
"image",
"details",
"stash_id",
]);
}
}
export const GalleryIsMissingCriterionOption = new CriterionOption(
export const GalleryIsMissingCriterionOption = new IsMissingCriterionOptionClass(
"isMissing",
"galleryIsMissing",
"is_missing"
"is_missing",
["title", "details", "url", "date", "studio", "performers", "tags", "scenes"]
);
export class GalleryIsMissingCriterion extends IsMissingCriterion {
constructor() {
super(GalleryIsMissingCriterionOption, [
"title",
"details",
"url",
"date",
"studio",
"performers",
"tags",
"scenes",
]);
}
}
export const TagIsMissingCriterionOption = new CriterionOption(
export const TagIsMissingCriterionOption = new IsMissingCriterionOptionClass(
"isMissing",
"tagIsMissing",
"is_missing"
"is_missing",
["image"]
);
export class TagIsMissingCriterion extends IsMissingCriterion {
constructor() {
super(TagIsMissingCriterionOption, ["image"]);
}
}
export const StudioIsMissingCriterionOption = new CriterionOption(
export const StudioIsMissingCriterionOption = new IsMissingCriterionOptionClass(
"isMissing",
"studioIsMissing",
"is_missing"
"is_missing",
["image", "stash_id", "details"]
);
export class StudioIsMissingCriterion extends IsMissingCriterion {
constructor() {
super(StudioIsMissingCriterionOption, ["image", "stash_id", "details"]);
}
}
export const MovieIsMissingCriterionOption = new CriterionOption(
export const MovieIsMissingCriterionOption = new IsMissingCriterionOptionClass(
"isMissing",
"movieIsMissing",
"is_missing"
"is_missing",
["front_image", "back_image", "scenes"]
);
export class MovieIsMissingCriterion extends IsMissingCriterion {
constructor() {
super(MovieIsMissingCriterionOption, [
"front_image",
"back_image",
"scenes",
]);
}
}

View File

@@ -1,9 +1,14 @@
import { CriterionOption, ILabeledIdCriterion } from "./criterion";
import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
export const MoviesCriterionOption = new CriterionOption("movies", "movies");
export const MoviesCriterionOption = new ILabeledIdCriterionOption(
"movies",
"movies",
"movies",
false
);
export class MoviesCriterion extends ILabeledIdCriterion {
constructor() {
super(MoviesCriterionOption, false);
super(MoviesCriterionOption);
}
}

View File

@@ -1,15 +1,13 @@
import { CriterionModifier } from "src/core/generated-graphql";
import { Criterion, CriterionOption } from "./criterion";
import { Criterion, StringCriterionOption } from "./criterion";
export const NoneCriterionOption = new CriterionOption("none", "none");
export const NoneCriterionOption = new StringCriterionOption(
"none",
"none",
"none"
);
export class NoneCriterion extends Criterion<string> {
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: undefined;
public value: string = "none";
constructor() {
super(NoneCriterionOption);
super(NoneCriterionOption, "none");
}
// eslint-disable-next-line class-methods-use-this

View File

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

View File

@@ -1,12 +1,14 @@
import { CriterionOption, ILabeledIdCriterion } from "./criterion";
import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
export const PerformersCriterionOption = new CriterionOption(
export const PerformersCriterionOption = new ILabeledIdCriterionOption(
"performers",
"performers"
"performers",
"performers",
true
);
export class PerformersCriterion extends ILabeledIdCriterion {
constructor() {
super(PerformersCriterionOption, true);
super(PerformersCriterionOption);
}
}

View File

@@ -1,20 +1,8 @@
import { CriterionModifier } from "src/core/generated-graphql";
import { Criterion, CriterionOption, NumberCriterion } from "./criterion";
import { NumberCriterionOption } from "./criterion";
export const RatingCriterionOption = new CriterionOption("rating", "rating");
export class RatingCriterion extends NumberCriterion {
public modifier = CriterionModifier.Equals;
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Equals),
Criterion.getModifierOption(CriterionModifier.NotEquals),
Criterion.getModifierOption(CriterionModifier.GreaterThan),
Criterion.getModifierOption(CriterionModifier.LessThan),
Criterion.getModifierOption(CriterionModifier.IsNull),
Criterion.getModifierOption(CriterionModifier.NotNull),
];
constructor() {
super(RatingCriterionOption, [1, 2, 3, 4, 5]);
}
}
export const RatingCriterionOption = new NumberCriterionOption(
"rating",
"rating",
"rating",
[1, 2, 3, 4, 5]
);

View File

@@ -1,27 +1,8 @@
import { CriterionModifier, ResolutionEnum } from "src/core/generated-graphql";
import { ResolutionEnum } from "src/core/generated-graphql";
import { CriterionType } from "../types";
import { CriterionOption, StringCriterion } from "./criterion";
abstract class AbstractResolutionCriterion extends StringCriterion {
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
constructor(type: CriterionOption) {
super(type, [
"144p",
"240p",
"360p",
"480p",
"540p",
"720p",
"1080p",
"1440p",
"4k",
"5k",
"6k",
"8k",
]);
}
protected toCriterionInput(): ResolutionEnum | undefined {
switch (this.value) {
case "144p":
@@ -55,21 +36,40 @@ abstract class AbstractResolutionCriterion extends StringCriterion {
}
}
export const ResolutionCriterionOption = new CriterionOption(
"resolution",
class ResolutionCriterionOptionType extends CriterionOption {
constructor(value: CriterionType) {
super({
messageID: value,
type: value,
parameterName: value,
options: [
"144p",
"240p",
"360p",
"480p",
"540p",
"720p",
"1080p",
"1440p",
"4k",
"5k",
"6k",
"8k",
],
});
}
}
export const ResolutionCriterionOption = new ResolutionCriterionOptionType(
"resolution"
);
export class ResolutionCriterion extends AbstractResolutionCriterion {
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
constructor() {
super(ResolutionCriterionOption);
}
}
export const AverageResolutionCriterionOption = new CriterionOption(
"average_resolution",
export const AverageResolutionCriterionOption = new ResolutionCriterionOptionType(
"average_resolution"
);

View File

@@ -1,23 +1,30 @@
import {
CriterionOption,
IHierarchicalLabeledIdCriterion,
ILabeledIdCriterion,
ILabeledIdCriterionOption,
} from "./criterion";
export const StudiosCriterionOption = new CriterionOption("studios", "studios");
export const StudiosCriterionOption = new ILabeledIdCriterionOption(
"studios",
"studios",
"studios",
false
);
export class StudiosCriterion extends IHierarchicalLabeledIdCriterion {
constructor() {
super(StudiosCriterionOption, false);
super(StudiosCriterionOption);
}
}
export const ParentStudiosCriterionOption = new CriterionOption(
export const ParentStudiosCriterionOption = new ILabeledIdCriterionOption(
"parent_studios",
"parent_studios",
"parents"
"parents",
false
);
export class ParentStudiosCriterion extends ILabeledIdCriterion {
constructor() {
super(ParentStudiosCriterionOption, false);
super(ParentStudiosCriterionOption);
}
}

View File

@@ -1,19 +1,22 @@
import { CriterionOption, ILabeledIdCriterion } from "./criterion";
import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
export class TagsCriterion extends ILabeledIdCriterion {
constructor(type: CriterionOption) {
super(type, true);
}
}
export class TagsCriterion extends ILabeledIdCriterion {}
export const TagsCriterionOption = new CriterionOption("tags", "tags");
export const SceneTagsCriterionOption = new CriterionOption(
export const TagsCriterionOption = new ILabeledIdCriterionOption(
"tags",
"tags",
"tags",
true
);
export const SceneTagsCriterionOption = new ILabeledIdCriterionOption(
"sceneTags",
"sceneTags",
"scene_tags"
"scene_tags",
true
);
export const PerformerTagsCriterionOption = new CriterionOption(
export const PerformerTagsCriterionOption = new ILabeledIdCriterionOption(
"performerTags",
"performerTags",
"performer_tags"
"performer_tags",
true
);

View File

@@ -1,3 +1,4 @@
import { FilterMode } from "src/core/generated-graphql";
import { ListFilterOptions } from "./filter-options";
import { GalleryListFilterOptions } from "./galleries";
import { ImageListFilterOptions } from "./images";
@@ -7,7 +8,6 @@ import { SceneMarkerListFilterOptions } from "./scene-markers";
import { SceneListFilterOptions } from "./scenes";
import { StudioListFilterOptions } from "./studios";
import { TagListFilterOptions } from "./tags";
import { FilterMode } from "./types";
export function getFilterOptions(mode: FilterMode): ListFilterOptions {
switch (mode) {

View File

@@ -1,5 +1,9 @@
import queryString, { ParsedQuery } from "query-string";
import { FindFilterType, SortDirectionEnum } from "src/core/generated-graphql";
import {
FilterMode,
FindFilterType,
SortDirectionEnum,
} from "src/core/generated-graphql";
import { Criterion, CriterionValue } from "./criteria/criterion";
import { makeCriteria } from "./criteria/factory";
import { DisplayMode } from "./types";
@@ -23,6 +27,7 @@ const DEFAULT_PARAMS = {
// TODO: handle customCriteria
export class ListFilterModel {
public mode: FilterMode;
public searchTerm?: string;
public currentPage = DEFAULT_PARAMS.currentPage;
public itemsPerPage = DEFAULT_PARAMS.itemsPerPage;
@@ -33,16 +38,22 @@ export class ListFilterModel {
public randomSeed = -1;
public constructor(
mode: FilterMode,
rawParms?: ParsedQuery<string>,
defaultSort?: string,
defaultDisplayMode?: DisplayMode
) {
this.mode = mode;
const params = rawParms as IQueryParameters;
this.sortBy = defaultSort;
if (defaultDisplayMode !== undefined) this.displayMode = defaultDisplayMode;
if (params) this.configureFromQueryParameters(params);
}
public clone() {
return Object.assign(new ListFilterModel(this.mode), this);
}
public configureFromQueryParameters(params: IQueryParameters) {
if (params.sortby !== undefined) {
this.sortBy = params.sortby;
@@ -64,7 +75,7 @@ export class ListFilterModel {
params.sortdir === "desc"
? SortDirectionEnum.Desc
: SortDirectionEnum.Asc;
if (params.disp) {
if (params.disp !== undefined) {
this.displayMode = Number.parseInt(params.disp, 10);
}
if (params.q) {
@@ -153,6 +164,24 @@ export class ListFilterModel {
return result;
}
public getSavedQueryParameters() {
const encodedCriteria: string[] = this.criteria.map((criterion) =>
criterion.toJSON()
);
const result = {
perPage: this.itemsPerPage,
sortby: this.getSortBy() ?? undefined,
sortdir:
this.sortDirection === SortDirectionEnum.Desc ? "desc" : undefined,
disp: this.displayMode,
q: this.searchTerm,
c: encodedCriteria,
};
return result;
}
public makeQueryParameters(): string {
return queryString.stringify(this.getQueryParameters(), { encode: false });
}

View File

@@ -1,6 +1,5 @@
import { createCriterionOption } from "./criteria/criterion";
import { createStringCriterionOption } from "./criteria/criterion";
import { GalleryIsMissingCriterionOption } from "./criteria/is-missing";
import { NoneCriterionOption } from "./criteria/none";
import { OrganizedCriterionOption } from "./criteria/organized";
import { PerformersCriterionOption } from "./criteria/performers";
import { RatingCriterionOption } from "./criteria/rating";
@@ -39,20 +38,19 @@ const displayModeOptions = [
];
const criterionOptions = [
NoneCriterionOption,
createCriterionOption("path"),
createStringCriterionOption("path"),
RatingCriterionOption,
OrganizedCriterionOption,
AverageResolutionCriterionOption,
GalleryIsMissingCriterionOption,
TagsCriterionOption,
createCriterionOption("tag_count"),
createStringCriterionOption("tag_count"),
PerformerTagsCriterionOption,
PerformersCriterionOption,
createCriterionOption("performer_count"),
createCriterionOption("image_count"),
createStringCriterionOption("performer_count"),
createStringCriterionOption("image_count"),
StudiosCriterionOption,
createCriterionOption("url"),
createStringCriterionOption("url"),
];
export const GalleryListFilterOptions = new ListFilterOptions(

View File

@@ -1,6 +1,8 @@
import { createCriterionOption } from "./criteria/criterion";
import {
createMandatoryNumberCriterionOption,
createStringCriterionOption,
} from "./criteria/criterion";
import { ImageIsMissingCriterionOption } from "./criteria/is-missing";
import { NoneCriterionOption } from "./criteria/none";
import { OrganizedCriterionOption } from "./criteria/organized";
import { PerformersCriterionOption } from "./criteria/performers";
import { RatingCriterionOption } from "./criteria/rating";
@@ -29,18 +31,17 @@ const sortByOptions = [
const displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall];
const criterionOptions = [
NoneCriterionOption,
createCriterionOption("path"),
createStringCriterionOption("path"),
RatingCriterionOption,
OrganizedCriterionOption,
createCriterionOption("o_counter"),
createMandatoryNumberCriterionOption("o_counter"),
ResolutionCriterionOption,
ImageIsMissingCriterionOption,
TagsCriterionOption,
createCriterionOption("tag_count"),
createMandatoryNumberCriterionOption("tag_count"),
PerformerTagsCriterionOption,
PerformersCriterionOption,
createCriterionOption("performer_count"),
createMandatoryNumberCriterionOption("performer_count"),
StudiosCriterionOption,
];
export const ImageListFilterOptions = new ListFilterOptions(

View File

@@ -1,6 +1,5 @@
import { createCriterionOption } from "./criteria/criterion";
import { createStringCriterionOption } from "./criteria/criterion";
import { MovieIsMissingCriterionOption } from "./criteria/is-missing";
import { NoneCriterionOption } from "./criteria/none";
import { StudiosCriterionOption } from "./criteria/studios";
import { ListFilterOptions } from "./filter-options";
import { DisplayMode } from "./types";
@@ -17,10 +16,9 @@ const sortByOptions = ["name", "random"]
]);
const displayModeOptions = [DisplayMode.Grid];
const criterionOptions = [
NoneCriterionOption,
StudiosCriterionOption,
MovieIsMissingCriterionOption,
createCriterionOption("url"),
createStringCriterionOption("url"),
];
export const MovieListFilterOptions = new ListFilterOptions(

View File

@@ -1,8 +1,11 @@
import { createCriterionOption } from "./criteria/criterion";
import {
createNumberCriterionOption,
createMandatoryNumberCriterionOption,
createStringCriterionOption,
} from "./criteria/criterion";
import { FavoriteCriterionOption } from "./criteria/favorite";
import { GenderCriterionOption } from "./criteria/gender";
import { PerformerIsMissingCriterionOption } from "./criteria/is-missing";
import { NoneCriterionOption } from "./criteria/none";
import { RatingCriterionOption } from "./criteria/rating";
import { StudiosCriterionOption } from "./criteria/studios";
import { TagsCriterionOption } from "./criteria/tags";
@@ -55,19 +58,19 @@ const stringCriteria: CriterionType[] = [
];
const criterionOptions = [
NoneCriterionOption,
FavoriteCriterionOption,
GenderCriterionOption,
PerformerIsMissingCriterionOption,
TagsCriterionOption,
RatingCriterionOption,
StudiosCriterionOption,
createCriterionOption("url"),
createCriterionOption("tag_count"),
createCriterionOption("scene_count"),
createCriterionOption("image_count"),
createCriterionOption("gallery_count"),
...numberCriteria.concat(stringCriteria).map((c) => createCriterionOption(c)),
createStringCriterionOption("url"),
createMandatoryNumberCriterionOption("tag_count"),
createMandatoryNumberCriterionOption("scene_count"),
createMandatoryNumberCriterionOption("image_count"),
createMandatoryNumberCriterionOption("gallery_count"),
...numberCriteria.map((c) => createNumberCriterionOption(c)),
...stringCriteria.map((c) => createStringCriterionOption(c)),
];
export const PerformerListFilterOptions = new ListFilterOptions(
defaultSortBy,

View File

@@ -1,4 +1,3 @@
import { NoneCriterionOption } from "./criteria/none";
import { PerformersCriterionOption } from "./criteria/performers";
import { SceneTagsCriterionOption, TagsCriterionOption } from "./criteria/tags";
import { ListFilterOptions } from "./filter-options";
@@ -14,7 +13,6 @@ const sortByOptions = [
].map(ListFilterOptions.createSortBy);
const displayModeOptions = [DisplayMode.Wall];
const criterionOptions = [
NoneCriterionOption,
TagsCriterionOption,
SceneTagsCriterionOption,
PerformersCriterionOption,

View File

@@ -1,8 +1,10 @@
import { createCriterionOption } from "./criteria/criterion";
import {
createMandatoryNumberCriterionOption,
createStringCriterionOption,
} from "./criteria/criterion";
import { HasMarkersCriterionOption } from "./criteria/has-markers";
import { SceneIsMissingCriterionOption } from "./criteria/is-missing";
import { MoviesCriterionOption } from "./criteria/movies";
import { NoneCriterionOption } from "./criteria/none";
import { OrganizedCriterionOption } from "./criteria/organized";
import { PerformersCriterionOption } from "./criteria/performers";
import { RatingCriterionOption } from "./criteria/rating";
@@ -44,24 +46,23 @@ const displayModeOptions = [
];
const criterionOptions = [
NoneCriterionOption,
createCriterionOption("path"),
createStringCriterionOption("path"),
RatingCriterionOption,
OrganizedCriterionOption,
createCriterionOption("o_counter"),
createMandatoryNumberCriterionOption("o_counter"),
ResolutionCriterionOption,
createCriterionOption("duration"),
createMandatoryNumberCriterionOption("duration"),
HasMarkersCriterionOption,
SceneIsMissingCriterionOption,
TagsCriterionOption,
createCriterionOption("tag_count"),
createMandatoryNumberCriterionOption("tag_count"),
PerformerTagsCriterionOption,
PerformersCriterionOption,
createCriterionOption("performer_count"),
createMandatoryNumberCriterionOption("performer_count"),
StudiosCriterionOption,
MoviesCriterionOption,
createCriterionOption("url"),
createCriterionOption("stash_id"),
createStringCriterionOption("url"),
createStringCriterionOption("stash_id"),
InteractiveCriterionOption,
];

View File

@@ -1,6 +1,8 @@
import { createCriterionOption } from "./criteria/criterion";
import {
createMandatoryNumberCriterionOption,
createStringCriterionOption,
} from "./criteria/criterion";
import { StudioIsMissingCriterionOption } from "./criteria/is-missing";
import { NoneCriterionOption } from "./criteria/none";
import { RatingCriterionOption } from "./criteria/rating";
import { ParentStudiosCriterionOption } from "./criteria/studios";
import { ListFilterOptions } from "./filter-options";
@@ -26,15 +28,14 @@ const sortByOptions = ["name", "random", "rating"]
const displayModeOptions = [DisplayMode.Grid];
const criterionOptions = [
NoneCriterionOption,
ParentStudiosCriterionOption,
StudioIsMissingCriterionOption,
RatingCriterionOption,
createCriterionOption("scene_count"),
createCriterionOption("image_count"),
createCriterionOption("gallery_count"),
createCriterionOption("url"),
createCriterionOption("stash_id"),
createMandatoryNumberCriterionOption("scene_count"),
createMandatoryNumberCriterionOption("image_count"),
createMandatoryNumberCriterionOption("gallery_count"),
createStringCriterionOption("url"),
createStringCriterionOption("stash_id"),
];
export const StudioListFilterOptions = new ListFilterOptions(

View File

@@ -1,6 +1,5 @@
import { createCriterionOption } from "./criteria/criterion";
import { createMandatoryNumberCriterionOption } from "./criteria/criterion";
import { TagIsMissingCriterionOption } from "./criteria/is-missing";
import { NoneCriterionOption } from "./criteria/none";
import { ListFilterOptions } from "./filter-options";
import { DisplayMode } from "./types";
@@ -34,12 +33,11 @@ const sortByOptions = [
const displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
const criterionOptions = [
NoneCriterionOption,
TagIsMissingCriterionOption,
createCriterionOption("scene_count"),
createCriterionOption("image_count"),
createCriterionOption("gallery_count"),
createCriterionOption("performer_count"),
createMandatoryNumberCriterionOption("scene_count"),
createMandatoryNumberCriterionOption("image_count"),
createMandatoryNumberCriterionOption("gallery_count"),
createMandatoryNumberCriterionOption("performer_count"),
// marker count has been disabled for now due to performance issues
// ListFilterModel.createCriterionOption("marker_count"),
];

View File

@@ -8,17 +8,6 @@ export enum DisplayMode {
Tagger,
}
export enum FilterMode {
Scenes,
Performers,
Studios,
Galleries,
SceneMarkers,
Movies,
Tags,
Images,
}
export interface ILabeledId {
id: string;
label: string;

View File

@@ -1,5 +1,6 @@
import queryString from "query-string";
import { RouteComponentProps } from "react-router-dom";
import { FilterMode } from "src/core/generated-graphql";
import { ListFilterModel } from "./list-filter/filter";
import { SceneListFilterOptions } from "./list-filter/scenes";
@@ -27,7 +28,7 @@ export class SceneQueue {
public static fromListFilterModel(filter: ListFilterModel) {
const ret = new SceneQueue();
const filterCopy = Object.assign(new ListFilterModel(), filter);
const filterCopy = filter.clone();
filterCopy.itemsPerPage = 40;
ret.originalQueryPage = filter.currentPage;
@@ -95,6 +96,7 @@ export class SceneQueue {
if (parsed.qfp) {
const query = new ListFilterModel(
FilterMode.Scenes,
translated as queryString.ParsedQuery,
SceneListFilterOptions.defaultSortBy
);