Filter criterion fixes (#4090)

* Reorder
* Remove PhashDuplicateCriterion
* Improve DurationInput
* Register abloop outside of player init function
* Remove none criterion
* Typing improvements
* Move makeCriteria to ListFilterModel
* Separate PathCriterionOption
* Add makeCriterion arg to StringCriterionOption
* Remove unused options args
* Add DurationCriterionOption
* Use createNumberCriterionOption
* Add StringBooleanCriterion
This commit is contained in:
DingDongSoLong4
2023-09-12 02:53:32 +02:00
committed by GitHub
parent 0d13eec9a2
commit 9f4d0af886
49 changed files with 676 additions and 767 deletions

View File

@@ -30,7 +30,7 @@ export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({
// if galleries is already present, then we modify it, otherwise add
let galleryCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "galleries";
}) as GalleriesCriterion;
}) as GalleriesCriterion | undefined;
if (
galleryCriterion &&

View File

@@ -33,7 +33,7 @@ export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({
// if galleries is already present, then we modify it, otherwise add
let galleryCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "galleries";
}) as GalleriesCriterion;
}) as GalleriesCriterion | undefined;
if (
galleryCriterion &&

View File

@@ -12,7 +12,6 @@ import {
DateCriterion,
TimestampCriterion,
BooleanCriterion,
PathCriterionOption,
} from "src/models/list-filter/criteria/criterion";
import { useIntl } from "react-intl";
import {
@@ -47,6 +46,7 @@ import TagsFilter from "./Filters/TagsFilter";
import { PhashCriterion } from "src/models/list-filter/criteria/phash";
import { PhashFilter } from "./Filters/PhashFilter";
import cx from "classnames";
import { PathCriterion } from "src/models/list-filter/criteria/path";
interface IGenericCriterionEditor {
criterion: Criterion<CriterionValue>;
@@ -175,7 +175,7 @@ const GenericCriterionEditor: React.FC<IGenericCriterionEditor> = ({
);
}
}
if (criterion.criterionOption instanceof PathCriterionOption) {
if (criterion instanceof PathCriterion) {
return (
<PathFilter criterion={criterion} onValueChanged={onValueChanged} />
);

View File

@@ -14,7 +14,6 @@ import {
Criterion,
CriterionOption,
} from "src/models/list-filter/criteria/criterion";
import { makeCriteria } from "src/models/list-filter/criteria/factory";
import { FormattedMessage, useIntl } from "react-intl";
import { ConfigurationContext } from "src/hooks/Config";
import { ListFilterModel } from "src/models/list-filter/filter";
@@ -243,17 +242,11 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
}, [currentFilter.mode]);
const criterionOptions = useMemo(() => {
const filteredOptions = filterOptions.criterionOptions.filter((o) => {
return o.type !== "none";
});
filteredOptions.sort((a, b) => {
return [...filterOptions.criterionOptions].sort((a, b) => {
return intl
.formatMessage({ id: a.messageID })
.localeCompare(intl.formatMessage({ id: b.messageID }));
});
return filteredOptions;
}, [intl, filterOptions.criterionOptions]);
const optionSelected = useCallback(
@@ -270,11 +263,11 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
if (existing) {
setCriterion(existing);
} else {
const newCriterion = makeCriteria(filter.mode, option.type);
const newCriterion = filter.makeCriterion(option.type);
setCriterion(newCriterion);
}
},
[filter.mode, criteria]
[filter, criteria]
);
const ui = (configuration?.ui ?? {}) as IUIConfig;

View File

@@ -17,67 +17,50 @@ export const DurationFilter: React.FC<IDurationFilterProps> = ({
}) => {
const intl = useIntl();
function onChanged(valueAsNumber: number, property: "value" | "value2") {
function onChanged(v: number | undefined, property: "value" | "value2") {
const { value } = criterion;
value[property] = valueAsNumber;
value[property] = v;
onValueChanged(value);
}
let equalsControl: JSX.Element | null = null;
if (
criterion.modifier === CriterionModifier.Equals ||
criterion.modifier === CriterionModifier.NotEquals
) {
equalsControl = (
function renderTop() {
let placeholder: string;
if (
criterion.modifier === CriterionModifier.GreaterThan ||
criterion.modifier === CriterionModifier.Between ||
criterion.modifier === CriterionModifier.NotBetween
) {
placeholder = intl.formatMessage({ id: "criterion.greater_than" });
} else if (criterion.modifier === CriterionModifier.LessThan) {
placeholder = intl.formatMessage({ id: "criterion.less_than" });
} else {
placeholder = intl.formatMessage({ id: "criterion.value" });
}
return (
<Form.Group>
<DurationInput
numericValue={criterion.value?.value}
onValueChange={(v: number) => onChanged(v, "value")}
placeholder={intl.formatMessage({ id: "criterion.value" })}
value={criterion.value?.value}
setValue={(v) => onChanged(v, "value")}
placeholder={placeholder}
/>
</Form.Group>
);
}
let lowerControl: JSX.Element | null = null;
if (
criterion.modifier === CriterionModifier.GreaterThan ||
criterion.modifier === CriterionModifier.Between ||
criterion.modifier === CriterionModifier.NotBetween
) {
lowerControl = (
<Form.Group>
<DurationInput
numericValue={criterion.value?.value}
onValueChange={(v: number) => onChanged(v, "value")}
placeholder={intl.formatMessage({ id: "criterion.greater_than" })}
/>
</Form.Group>
);
}
function renderBottom() {
if (
criterion.modifier !== CriterionModifier.Between &&
criterion.modifier !== CriterionModifier.NotBetween
) {
return;
}
let upperControl: JSX.Element | null = null;
if (
criterion.modifier === CriterionModifier.LessThan ||
criterion.modifier === CriterionModifier.Between ||
criterion.modifier === CriterionModifier.NotBetween
) {
upperControl = (
return (
<Form.Group>
<DurationInput
numericValue={
criterion.modifier === CriterionModifier.LessThan
? criterion.value?.value
: criterion.value?.value2
}
onValueChange={(v: number) =>
onChanged(
v,
criterion.modifier === CriterionModifier.LessThan
? "value"
: "value2"
)
}
value={criterion.value?.value2}
setValue={(v) => onChanged(v, "value2")}
placeholder={intl.formatMessage({ id: "criterion.less_than" })}
/>
</Form.Group>
@@ -86,9 +69,8 @@ export const DurationFilter: React.FC<IDurationFilterProps> = ({
return (
<>
{equalsControl}
{lowerControl}
{upperControl}
{renderTop()}
{renderBottom()}
</>
);
};

View File

@@ -135,10 +135,10 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
}
if (state.duration) {
formik.setFieldValue(
"duration",
DurationUtils.stringToSeconds(state.duration)
);
const seconds = DurationUtils.stringToSeconds(state.duration);
if (seconds !== undefined) {
formik.setFieldValue("duration", seconds);
}
}
if (state.date) {
@@ -402,10 +402,8 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
</Form.Label>
<Col xs={fieldXS} xl={fieldXL}>
<DurationInput
numericValue={formik.values.duration ?? undefined}
onValueChange={(valueAsNumber) => {
formik.setFieldValue("duration", valueAsNumber ?? null);
}}
value={formik.values.duration ?? undefined}
setValue={(v) => formik.setFieldValue("duration", v ?? null)}
/>
</Col>
</Form.Group>

View File

@@ -18,7 +18,7 @@ export const MovieScenesPanel: React.FC<IMovieScenesPanel> = ({
// if movie is already present, then we modify it, otherwise add
let movieCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "movies";
}) as MoviesCriterion;
}) as MoviesCriterion | undefined;
if (
movieCriterion &&

View File

@@ -8,7 +8,6 @@ import React, {
useState,
} from "react";
import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from "video.js";
import abLoopPlugin from "videojs-abloop";
import useScript from "src/hooks/useScript";
import "videojs-contrib-dash";
import "videojs-mobile-ui";
@@ -24,12 +23,6 @@ import "./big-buttons";
import "./track-activity";
import "./vrmode";
import cx from "classnames";
// @ts-ignore
import airplay from "@silvermine/videojs-airplay";
// @ts-ignore
import chromecast from "@silvermine/videojs-chromecast";
airplay(videojs);
chromecast(videojs);
import {
useSceneSaveActivity,
useSceneIncrementPlayCount,
@@ -47,6 +40,17 @@ import { languageMap } from "src/utils/caption";
import { VIDEO_PLAYER_ID } from "./util";
import { IUIConfig } from "src/core/config";
// @ts-ignore
import airplay from "@silvermine/videojs-airplay";
// @ts-ignore
import chromecast from "@silvermine/videojs-chromecast";
import abLoopPlugin from "videojs-abloop";
// register videojs plugins
airplay(videojs);
chromecast(videojs);
abLoopPlugin(window, videojs);
function handleHotkeys(player: VideoJsPlayer, event: videojs.KeyboardEvent) {
function seekStep(step: number) {
const time = player.currentTime() + step;
@@ -378,8 +382,6 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
videoEl.classList.add("vjs-big-play-centered");
videoRef.current!.appendChild(videoEl);
abLoopPlugin(window, videojs);
const vjs = videojs(videoEl, options);
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */

View File

@@ -145,15 +145,14 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
</Form.Label>
<div className="col-sm-8 col-xl-12">
<DurationInput
onValueChange={(s) => formik.setFieldValue("seconds", s)}
value={formik.values.seconds ?? 0}
setValue={(v) => formik.setFieldValue("seconds", v ?? null)}
onReset={() =>
formik.setFieldValue(
"seconds",
Math.round(getPlayerPosition() ?? 0)
)
}
numericValue={formik.values.seconds}
mandatory
/>
</div>
</div>

View File

@@ -356,8 +356,8 @@ export const SettingsInterfacePanel: React.FC = () => {
onChange={(v) => saveInterface({ maximumLoopDuration: v })}
renderField={(value, setValue) => (
<DurationInput
numericValue={value}
onValueChange={(duration) => setValue(duration ?? 0)}
value={value}
setValue={(duration) => setValue(duration ?? 0)}
/>
)}
renderValue={(v) => {

View File

@@ -43,17 +43,13 @@ export const SettingsServicesPanel: React.FC = () => {
} = React.useContext(SettingStateContext);
// undefined to hide dialog, true for enable, false for disable
const [enableDisable, setEnableDisable] = useState<boolean | undefined>(
undefined
);
const [enableDisable, setEnableDisable] = useState<boolean>();
const [enableUntilRestart, setEnableUntilRestart] = useState<boolean>(false);
const [enableDuration, setEnableDuration] = useState<number | undefined>(
undefined
);
const [enableDuration, setEnableDuration] = useState<number>(0);
const [ipEntry, setIPEntry] = useState<string>("");
const [tempIP, setTempIP] = useState<string | undefined>();
const [tempIP, setTempIP] = useState<string>();
const { data: statusData, loading, refetch: statusRefetch } = useDLNAStatus();
@@ -273,8 +269,8 @@ export const SettingsServicesPanel: React.FC = () => {
<Form.Group id="temp-enable-duration">
<DurationInput
numericValue={enableDuration ?? 0}
onValueChange={(v) => setEnableDuration(v ?? 0)}
value={enableDuration}
setValue={(v) => setEnableDuration(v ?? 0)}
disabled={enableUntilRestart}
/>
<Form.Text className="text-muted">
@@ -315,8 +311,8 @@ export const SettingsServicesPanel: React.FC = () => {
<Form.Group id="temp-enable-duration">
<DurationInput
numericValue={enableDuration ?? 0}
onValueChange={(v) => setEnableDuration(v ?? 0)}
value={enableDuration}
setValue={(v) => setEnableDuration(v ?? 0)}
disabled={enableUntilRestart}
/>
<Form.Text className="text-muted">

View File

@@ -3,67 +3,58 @@ import {
faChevronUp,
faClock,
} from "@fortawesome/free-solid-svg-icons";
import React, { useState, useEffect } from "react";
import React, { useState } from "react";
import { Button, ButtonGroup, InputGroup, Form } from "react-bootstrap";
import { Icon } from "./Icon";
import DurationUtils from "src/utils/duration";
interface IProps {
disabled?: boolean;
numericValue: number | undefined;
mandatory?: boolean;
onValueChange(
valueAsNumber: number | undefined,
valueAsString?: string
): void;
value: number | undefined;
setValue(value: number | undefined): void;
onReset?(): void;
className?: string;
placeholder?: string;
}
export const DurationInput: React.FC<IProps> = (props: IProps) => {
const [value, setValue] = useState<string | undefined>(
props.numericValue !== undefined
? DurationUtils.secondsToString(props.numericValue)
: undefined
);
export const DurationInput: React.FC<IProps> = ({
disabled,
value,
setValue,
onReset,
className,
placeholder,
}) => {
const [tmpValue, setTmpValue] = useState<string>();
useEffect(() => {
if (props.numericValue !== undefined || props.mandatory) {
setValue(DurationUtils.secondsToString(props.numericValue ?? 0));
} else {
setValue(undefined);
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
setTmpValue(e.currentTarget.value);
}
function onBlur() {
if (tmpValue !== undefined) {
setValue(DurationUtils.stringToSeconds(tmpValue));
setTmpValue(undefined);
}
}, [props.numericValue, props.mandatory]);
}
function increment() {
if (value === undefined) {
return;
}
let seconds = DurationUtils.stringToSeconds(value);
seconds += 1;
props.onValueChange(seconds, DurationUtils.secondsToString(seconds));
setTmpValue(undefined);
setValue((value ?? 0) + 1);
}
function decrement() {
if (value === undefined) {
return;
}
let seconds = DurationUtils.stringToSeconds(value);
seconds -= 1;
props.onValueChange(seconds, DurationUtils.secondsToString(seconds));
setTmpValue(undefined);
setValue((value ?? 0) - 1);
}
function renderButtons() {
if (!props.disabled) {
if (!disabled) {
return (
<ButtonGroup vertical>
<Button
variant="secondary"
className="duration-button"
disabled={props.disabled}
onClick={() => increment()}
>
<Icon icon={faChevronUp} />
@@ -71,7 +62,6 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
<Button
variant="secondary"
className="duration-button"
disabled={props.disabled}
onClick={() => decrement()}
>
<Icon icon={faChevronDown} />
@@ -81,46 +71,33 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
}
}
function onReset() {
if (props.onReset) {
props.onReset();
}
}
function maybeRenderReset() {
if (props.onReset) {
if (onReset) {
return (
<Button variant="secondary" onClick={onReset}>
<Button variant="secondary" onClick={() => onReset()}>
<Icon icon={faClock} />
</Button>
);
}
}
let inputValue = "";
if (tmpValue !== undefined) {
inputValue = tmpValue;
} else if (value !== undefined) {
inputValue = DurationUtils.secondsToString(value);
}
return (
<div className={`duration-input ${props.className}`}>
<div className={`duration-input ${className}`}>
<InputGroup>
<Form.Control
className="duration-control text-input"
disabled={props.disabled}
value={value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setValue(e.currentTarget.value)
}
onBlur={() => {
if (props.mandatory || (value !== undefined && value !== "")) {
props.onValueChange(DurationUtils.stringToSeconds(value), value);
} else {
props.onValueChange(undefined);
}
}}
placeholder={
!props.disabled
? props.placeholder
? `${props.placeholder} (hh:mm:ss)`
: "hh:mm:ss"
: undefined
}
disabled={disabled}
value={inputValue}
onChange={onChange}
onBlur={onBlur}
placeholder={placeholder ? `${placeholder} (hh:mm:ss)` : "hh:mm:ss"}
/>
<InputGroup.Append>
{maybeRenderReset()}

View File

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

View File

@@ -21,7 +21,7 @@ export const TagMarkersPanel: React.FC<ITagMarkersPanel> = ({
// if tag is already present, then we modify it, otherwise add
let tagCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "tags";
}) as TagsCriterion;
}) as TagsCriterion | undefined;
if (
tagCriterion &&

View File

@@ -16,7 +16,7 @@ export const usePerformerFilterHook = (
// if performers is already present, then we modify it, otherwise add
let performerCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "performers";
}) as PerformersCriterion;
}) as PerformersCriterion | undefined;
if (performerCriterion) {
if (

View File

@@ -12,7 +12,7 @@ export const useStudioFilterHook = (studio: GQL.StudioDataFragment) => {
// if studio is already present, then we modify it, otherwise add
let studioCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "studios";
}) as StudiosCriterion;
}) as StudiosCriterion | undefined;
if (studioCriterion) {
// we should be showing studio only. Remove other values

View File

@@ -17,7 +17,7 @@ export const useTagFilterHook = (tag: GQL.TagDataFragment) => {
// if tag is already present, then we modify it, otherwise add
let tagCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "tags";
}) as TagsCriterion;
}) as TagsCriterion | undefined;
if (tagCriterion) {
if (

View File

@@ -1,33 +1,28 @@
import { CriterionModifier } from "src/core/generated-graphql";
import { languageMap, valueToCode } from "src/utils/caption";
import { CriterionType } from "../types";
import { CriterionOption, StringCriterion } from "./criterion";
const languageStrings = Array.from(languageMap.values());
class CaptionsCriterionOptionType extends CriterionOption {
constructor(value: CriterionType) {
super({
messageID: value,
type: value,
modifierOptions: [
CriterionModifier.Includes,
CriterionModifier.Excludes,
CriterionModifier.IsNull,
CriterionModifier.NotNull,
],
defaultModifier: CriterionModifier.Includes,
options: languageStrings,
makeCriterion: () => new CaptionCriterion(),
});
}
}
export const CaptionsCriterionOption = new CaptionsCriterionOptionType(
"captions"
);
export const CaptionsCriterionOption = new CriterionOption({
messageID: "captions",
type: "captions",
modifierOptions: [
CriterionModifier.Includes,
CriterionModifier.Excludes,
CriterionModifier.IsNull,
CriterionModifier.NotNull,
],
defaultModifier: CriterionModifier.Includes,
options: languageStrings,
makeCriterion: () => new CaptionCriterion(),
});
export class CaptionCriterion extends StringCriterion {
constructor() {
super(CaptionsCriterionOption);
}
protected toCriterionInput() {
const value = valueToCode(this.value) ?? "";
@@ -36,8 +31,4 @@ export class CaptionCriterion extends StringCriterion {
modifier: this.modifier,
};
}
constructor() {
super(CaptionsCriterionOption);
}
}

View File

@@ -9,13 +9,13 @@ import { CriterionOption, MultiStringCriterion } from "./criterion";
export const CircumcisedCriterionOption = new CriterionOption({
messageID: "circumcised",
type: "circumcised",
options: circumcisedStrings,
modifierOptions: [
CriterionModifier.Includes,
CriterionModifier.Excludes,
CriterionModifier.IsNull,
CriterionModifier.NotNull,
],
options: circumcisedStrings,
makeCriterion: () => new CircumcisedCriterion(),
});

View File

@@ -1,20 +1,13 @@
import { IntlShape } from "react-intl";
import { CriterionModifier } from "src/core/generated-graphql";
import { getCountryByISO } from "src/utils/country";
import {
CriterionOption,
StringCriterion,
StringCriterionOption,
} from "./criterion";
import { StringCriterion, StringCriterionOption } from "./criterion";
export const CountryCriterionOption = new CriterionOption({
messageID: "country",
type: "country",
modifierOptions: StringCriterionOption.modifierOptions,
defaultModifier: StringCriterionOption.defaultModifier,
makeCriterion: () => new CountryCriterion(),
inputType: StringCriterionOption.inputType,
});
export const CountryCriterionOption = new StringCriterionOption(
"country",
"country",
() => new CountryCriterion()
);
export class CountryCriterion extends StringCriterion {
constructor() {

View File

@@ -1,13 +1,10 @@
/* eslint-disable consistent-return */
/* eslint @typescript-eslint/no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */
import { IntlShape } from "react-intl";
import {
CriterionModifier,
HierarchicalMultiCriterionInput,
IntCriterionInput,
MultiCriterionInput,
PHashDuplicationCriterionInput,
DateCriterionInput,
TimestampCriterionInput,
ConfigDataFragment,
@@ -152,22 +149,18 @@ export abstract class Criterion<V extends CriterionValue> {
this.modifier = encodedCriterion.modifier;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public apply(outputFilter: Record<string, any>) {
// eslint-disable-next-line no-param-reassign
public apply(outputFilter: Record<string, unknown>) {
outputFilter[this.criterionOption.type] = this.toCriterionInput();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected toCriterionInput(): any {
protected toCriterionInput(): unknown {
return {
value: this.value,
modifier: this.modifier,
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toSavedFilter(outputFilter: Record<string, any>) {
public toSavedFilter(outputFilter: Record<string, unknown>) {
outputFilter[this.criterionOption.type] = {
value: this.value,
modifier: this.modifier,
@@ -226,274 +219,13 @@ export class CriterionOption {
}
}
export class StringCriterionOption extends CriterionOption {
public static readonly modifierOptions = [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
CriterionModifier.Includes,
CriterionModifier.Excludes,
CriterionModifier.IsNull,
CriterionModifier.NotNull,
CriterionModifier.MatchesRegex,
CriterionModifier.NotMatchesRegex,
];
public static readonly defaultModifier = CriterionModifier.Equals;
public static readonly inputType = "text";
constructor(messageID: string, type: CriterionType, options?: Option[]) {
super({
messageID,
type,
modifierOptions: StringCriterionOption.modifierOptions,
defaultModifier: StringCriterionOption.defaultModifier,
options,
inputType: StringCriterionOption.inputType,
makeCriterion: () => new StringCriterion(this),
});
}
}
export function createStringCriterionOption(
type: CriterionType,
messageID?: string
) {
return new StringCriterionOption(messageID ?? type, type);
}
export class StringCriterion extends Criterion<string> {
constructor(type: CriterionOption) {
super(type, "");
}
protected getLabelValue(_intl: IntlShape) {
return this.value;
}
public isValid(): boolean {
return (
this.modifier === CriterionModifier.IsNull ||
this.modifier === CriterionModifier.NotNull ||
this.value.length > 0
);
}
}
export class MultiStringCriterion extends Criterion<string[]> {
constructor(type: CriterionOption) {
super(type, []);
}
protected getLabelValue(_intl: IntlShape) {
return this.value.join(", ");
}
public isValid(): boolean {
return (
this.modifier === CriterionModifier.IsNull ||
this.modifier === CriterionModifier.NotNull ||
this.value.length > 0
);
}
}
export class MandatoryStringCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType, options?: Option[]) {
super({
messageID,
type: value,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
CriterionModifier.Includes,
CriterionModifier.Excludes,
CriterionModifier.MatchesRegex,
CriterionModifier.NotMatchesRegex,
],
defaultModifier: CriterionModifier.Equals,
options,
inputType: "text",
makeCriterion: () => new StringCriterion(this),
});
}
}
export function createMandatoryStringCriterionOption(
value: CriterionType,
messageID?: string
) {
return new MandatoryStringCriterionOption(messageID ?? value, value);
}
export class PathCriterionOption extends StringCriterionOption {}
export function createPathCriterionOption(
type: CriterionType,
messageID?: string
) {
return new PathCriterionOption(messageID ?? type, type);
}
export class BooleanCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
makeCriterion?: () => Criterion<CriterionValue>
) {
super({
messageID,
type: value,
modifierOptions: [],
defaultModifier: CriterionModifier.Equals,
options: [true.toString(), false.toString()],
makeCriterion: makeCriterion
? makeCriterion
: () => new BooleanCriterion(this),
});
}
}
export class BooleanCriterion extends StringCriterion {
protected toCriterionInput(): boolean {
return this.value === "true";
}
public isValid() {
return this.value === "true" || this.value === "false";
}
}
export function createBooleanCriterionOption(
value: CriterionType,
messageID?: string
) {
return new BooleanCriterionOption(messageID ?? value, value);
}
export class NumberCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType, options?: Option[]) {
super({
messageID,
type: value,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
CriterionModifier.GreaterThan,
CriterionModifier.LessThan,
CriterionModifier.IsNull,
CriterionModifier.NotNull,
CriterionModifier.Between,
CriterionModifier.NotBetween,
],
defaultModifier: CriterionModifier.Equals,
options,
inputType: "number",
makeCriterion: () => new NumberCriterion(this),
});
}
}
export class NullNumberCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType) {
super({
messageID,
type: value,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
CriterionModifier.GreaterThan,
CriterionModifier.LessThan,
CriterionModifier.Between,
CriterionModifier.NotBetween,
CriterionModifier.IsNull,
CriterionModifier.NotNull,
],
defaultModifier: CriterionModifier.Equals,
inputType: "number",
makeCriterion: () => new NumberCriterion(this),
});
}
}
export function createNumberCriterionOption(value: CriterionType) {
return new NumberCriterionOption(value, value);
}
export function createNullNumberCriterionOption(value: CriterionType) {
return new NullNumberCriterionOption(value, value);
}
export class NumberCriterion extends Criterion<INumberValue> {
public get value(): INumberValue {
return this._value;
}
public set value(newValue: number | INumberValue) {
// backwards compatibility - if this.value is a number, use that
if (typeof newValue !== "object") {
this._value = {
value: newValue,
value2: undefined,
};
} else {
this._value = newValue;
}
}
protected toCriterionInput(): IntCriterionInput {
return {
modifier: this.modifier,
value: this.value?.value ?? 0,
value2: this.value?.value2,
};
}
protected getLabelValue(_intl: IntlShape) {
const { value, value2 } = this.value;
if (
this.modifier === CriterionModifier.Between ||
this.modifier === CriterionModifier.NotBetween
) {
return `${value}, ${value2 ?? 0}`;
} else {
return `${value}`;
}
}
public isValid(): boolean {
if (
this.modifier === CriterionModifier.IsNull ||
this.modifier === CriterionModifier.NotNull
) {
return true;
}
const { value, value2 } = this.value;
if (value === undefined) {
return false;
}
if (
value2 === undefined &&
(this.modifier === CriterionModifier.Between ||
this.modifier === CriterionModifier.NotBetween)
) {
return false;
}
return true;
}
constructor(type: CriterionOption) {
super(type, { value: undefined, value2: undefined });
}
}
export class ILabeledIdCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
includeAll: boolean,
inputType: InputType
inputType: InputType,
makeCriterion?: () => Criterion<CriterionValue>
) {
const modifierOptions = [
CriterionModifier.Includes,
@@ -513,8 +245,10 @@ export class ILabeledIdCriterionOption extends CriterionOption {
type: value,
modifierOptions,
defaultModifier,
makeCriterion: () => new ILabeledIdCriterion(this),
inputType,
makeCriterion: makeCriterion
? makeCriterion
: () => new ILabeledIdCriterion(this),
});
}
}
@@ -689,8 +423,231 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
}
}
export class MandatoryNumberCriterionOption extends CriterionOption {
export class StringCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
makeCriterion?: () => Criterion<CriterionValue>
) {
super({
messageID,
type: value,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
CriterionModifier.Includes,
CriterionModifier.Excludes,
CriterionModifier.IsNull,
CriterionModifier.NotNull,
CriterionModifier.MatchesRegex,
CriterionModifier.NotMatchesRegex,
],
defaultModifier: CriterionModifier.Equals,
inputType: "text",
makeCriterion: makeCriterion
? makeCriterion
: () => new StringCriterion(this),
});
}
}
export function createStringCriterionOption(
type: CriterionType,
messageID?: string
) {
return new StringCriterionOption(messageID ?? type, type);
}
export class MandatoryStringCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType) {
super({
messageID,
type: value,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
CriterionModifier.Includes,
CriterionModifier.Excludes,
CriterionModifier.MatchesRegex,
CriterionModifier.NotMatchesRegex,
],
defaultModifier: CriterionModifier.Equals,
inputType: "text",
makeCriterion: () => new StringCriterion(this),
});
}
}
export function createMandatoryStringCriterionOption(
value: CriterionType,
messageID?: string
) {
return new MandatoryStringCriterionOption(messageID ?? value, value);
}
export class StringCriterion extends Criterion<string> {
constructor(type: CriterionOption) {
super(type, "");
}
protected getLabelValue(_intl: IntlShape) {
return this.value;
}
public isValid(): boolean {
return (
this.modifier === CriterionModifier.IsNull ||
this.modifier === CriterionModifier.NotNull ||
this.value.length > 0
);
}
}
export class MultiStringCriterion extends Criterion<string[]> {
constructor(type: CriterionOption) {
super(type, []);
}
protected getLabelValue(_intl: IntlShape) {
return this.value.join(", ");
}
public isValid(): boolean {
return (
this.modifier === CriterionModifier.IsNull ||
this.modifier === CriterionModifier.NotNull ||
this.value.length > 0
);
}
}
export class BooleanCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
makeCriterion?: () => Criterion<CriterionValue>
) {
super({
messageID,
type: value,
modifierOptions: [],
defaultModifier: CriterionModifier.Equals,
options: ["true", "false"],
makeCriterion: makeCriterion
? makeCriterion
: () => new BooleanCriterion(this),
});
}
}
export function createBooleanCriterionOption(
value: CriterionType,
messageID?: string
) {
return new BooleanCriterionOption(messageID ?? value, value);
}
export class BooleanCriterion extends StringCriterion {
protected toCriterionInput(): boolean {
return this.value === "true";
}
public isValid() {
return this.value === "true" || this.value === "false";
}
}
export class StringBooleanCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
makeCriterion?: () => Criterion<CriterionValue>
) {
super({
messageID,
type: value,
options: ["true", "false"],
makeCriterion: makeCriterion
? makeCriterion
: () => new StringBooleanCriterion(this),
});
}
}
export class StringBooleanCriterion extends StringCriterion {
protected toCriterionInput(): string {
return this.value;
}
public isValid() {
return this.value === "true" || this.value === "false";
}
}
export class NumberCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType) {
super({
messageID,
type: value,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
CriterionModifier.GreaterThan,
CriterionModifier.LessThan,
CriterionModifier.IsNull,
CriterionModifier.NotNull,
CriterionModifier.Between,
CriterionModifier.NotBetween,
],
defaultModifier: CriterionModifier.Equals,
inputType: "number",
makeCriterion: () => new NumberCriterion(this),
});
}
}
export function createNumberCriterionOption(
value: CriterionType,
messageID?: string
) {
return new NumberCriterionOption(messageID ?? value, value);
}
export class NullNumberCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType) {
super({
messageID,
type: value,
modifierOptions: [
CriterionModifier.Equals,
CriterionModifier.NotEquals,
CriterionModifier.GreaterThan,
CriterionModifier.LessThan,
CriterionModifier.Between,
CriterionModifier.NotBetween,
CriterionModifier.IsNull,
CriterionModifier.NotNull,
],
defaultModifier: CriterionModifier.Equals,
inputType: "number",
makeCriterion: () => new NumberCriterion(this),
});
}
}
export function createNullNumberCriterionOption(
value: CriterionType,
messageID?: string
) {
return new NullNumberCriterionOption(messageID ?? value, value);
}
export class MandatoryNumberCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
makeCriterion?: () => Criterion<CriterionValue>
) {
super({
messageID,
type: value,
@@ -704,7 +661,9 @@ export class MandatoryNumberCriterionOption extends CriterionOption {
],
defaultModifier: CriterionModifier.Equals,
inputType: "number",
makeCriterion: () => new NumberCriterion(this),
makeCriterion: makeCriterion
? makeCriterion
: () => new NumberCriterion(this),
});
}
}
@@ -716,6 +675,84 @@ export function createMandatoryNumberCriterionOption(
return new MandatoryNumberCriterionOption(messageID ?? value, value);
}
export class NumberCriterion extends Criterion<INumberValue> {
public get value(): INumberValue {
return this._value;
}
public set value(newValue: number | INumberValue) {
// backwards compatibility - if this.value is a number, use that
if (typeof newValue !== "object") {
this._value = {
value: newValue,
value2: undefined,
};
} else {
this._value = newValue;
}
}
protected toCriterionInput(): IntCriterionInput {
return {
modifier: this.modifier,
value: this.value?.value ?? 0,
value2: this.value?.value2,
};
}
protected getLabelValue(_intl: IntlShape) {
const { value, value2 } = this.value;
if (
this.modifier === CriterionModifier.Between ||
this.modifier === CriterionModifier.NotBetween
) {
return `${value}, ${value2 ?? 0}`;
} else {
return `${value}`;
}
}
public isValid(): boolean {
if (
this.modifier === CriterionModifier.IsNull ||
this.modifier === CriterionModifier.NotNull
) {
return true;
}
const { value, value2 } = this.value;
if (value === undefined) {
return false;
}
if (
value2 === undefined &&
(this.modifier === CriterionModifier.Between ||
this.modifier === CriterionModifier.NotBetween)
) {
return false;
}
return true;
}
constructor(type: CriterionOption) {
super(type, { value: undefined, value2: undefined });
}
}
export class DurationCriterionOption extends MandatoryNumberCriterionOption {
constructor(messageID: string, value: CriterionType) {
super(messageID, value, () => new DurationCriterion(this));
}
}
export function createDurationCriterionOption(
value: CriterionType,
messageID?: string
) {
return new DurationCriterionOption(messageID ?? value, value);
}
export class DurationCriterion extends Criterion<INumberValue> {
constructor(type: CriterionOption) {
super(type, { value: undefined, value2: undefined });
@@ -730,17 +767,16 @@ export class DurationCriterion extends Criterion<INumberValue> {
}
protected getLabelValue(_intl: IntlShape) {
return this.modifier === CriterionModifier.Between ||
const value = DurationUtils.secondsToString(this.value.value ?? 0);
const value2 = DurationUtils.secondsToString(this.value.value2 ?? 0);
if (
this.modifier === CriterionModifier.Between ||
this.modifier === CriterionModifier.NotBetween
? `${DurationUtils.secondsToString(
this.value.value ?? 0
)} ${DurationUtils.secondsToString(this.value.value2 ?? 0)}`
: this.modifier === CriterionModifier.GreaterThan ||
this.modifier === CriterionModifier.LessThan ||
this.modifier === CriterionModifier.Equals ||
this.modifier === CriterionModifier.NotEquals
? DurationUtils.secondsToString(this.value.value ?? 0)
: "?";
) {
return `${value}, ${value2}`;
} else {
return value;
}
}
public isValid(): boolean {
@@ -768,16 +804,8 @@ export class DurationCriterion extends Criterion<INumberValue> {
}
}
export class PhashDuplicateCriterion extends StringCriterion {
protected toCriterionInput(): PHashDuplicationCriterionInput {
return {
duplicated: this.value === "true",
};
}
}
export class DateCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType, options?: Option[]) {
constructor(messageID: string, value: CriterionType) {
super({
messageID,
type: value,
@@ -792,7 +820,6 @@ export class DateCriterionOption extends CriterionOption {
CriterionModifier.NotBetween,
],
defaultModifier: CriterionModifier.Equals,
options,
inputType: "text",
makeCriterion: () => new DateCriterion(this),
});
@@ -857,7 +884,7 @@ export class DateCriterion extends Criterion<IDateValue> {
}
export class TimestampCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType, options?: Option[]) {
constructor(messageID: string, value: CriterionType) {
super({
messageID,
type: value,
@@ -870,7 +897,6 @@ export class TimestampCriterionOption extends CriterionOption {
CriterionModifier.NotBetween,
],
defaultModifier: CriterionModifier.GreaterThan,
options,
inputType: "text",
makeCriterion: () => new TimestampCriterion(this),
});
@@ -881,6 +907,28 @@ export function createTimestampCriterionOption(value: CriterionType) {
return new TimestampCriterionOption(value, value);
}
export class MandatoryTimestampCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType) {
super({
messageID,
type: value,
modifierOptions: [
CriterionModifier.GreaterThan,
CriterionModifier.LessThan,
CriterionModifier.Between,
CriterionModifier.NotBetween,
],
defaultModifier: CriterionModifier.GreaterThan,
inputType: "text",
makeCriterion: () => new TimestampCriterion(this),
});
}
}
export function createMandatoryTimestampCriterionOption(value: CriterionType) {
return new MandatoryTimestampCriterionOption(value, value);
}
export class TimestampCriterion extends Criterion<ITimestampValue> {
public encodeValue() {
return {
@@ -944,26 +992,3 @@ export class TimestampCriterion extends Criterion<ITimestampValue> {
super(type, { value: "", value2: undefined });
}
}
export class MandatoryTimestampCriterionOption extends CriterionOption {
constructor(messageID: string, value: CriterionType, options?: Option[]) {
super({
messageID,
type: value,
modifierOptions: [
CriterionModifier.GreaterThan,
CriterionModifier.LessThan,
CriterionModifier.Between,
CriterionModifier.NotBetween,
],
defaultModifier: CriterionModifier.GreaterThan,
options,
inputType: "text",
makeCriterion: () => new TimestampCriterion(this),
});
}
}
export function createMandatoryTimestampCriterionOption(value: CriterionType) {
return new MandatoryTimestampCriterionOption(value, value);
}

View File

@@ -1,37 +0,0 @@
import * as GQL from "src/core/generated-graphql";
import { SceneListFilterOptions } from "../scenes";
import { MovieListFilterOptions } from "../movies";
import { GalleryListFilterOptions } from "../galleries";
import { PerformerListFilterOptions } from "../performers";
import { ImageListFilterOptions } from "../images";
import { SceneMarkerListFilterOptions } from "../scene-markers";
import { StudioListFilterOptions } from "../studios";
import { TagListFilterOptions } from "../tags";
import { CriterionType } from "../types";
const filterModeOptions = {
[GQL.FilterMode.Galleries]: GalleryListFilterOptions.criterionOptions,
[GQL.FilterMode.Images]: ImageListFilterOptions.criterionOptions,
[GQL.FilterMode.Movies]: MovieListFilterOptions.criterionOptions,
[GQL.FilterMode.Performers]: PerformerListFilterOptions.criterionOptions,
[GQL.FilterMode.SceneMarkers]: SceneMarkerListFilterOptions.criterionOptions,
[GQL.FilterMode.Scenes]: SceneListFilterOptions.criterionOptions,
[GQL.FilterMode.Studios]: StudioListFilterOptions.criterionOptions,
[GQL.FilterMode.Tags]: TagListFilterOptions.criterionOptions,
};
export function makeCriteria(
mode: GQL.FilterMode,
type: CriterionType,
config?: GQL.ConfigDataFragment
) {
const criterionOptions = filterModeOptions[mode];
const option = criterionOptions.find((o) => o.type === type);
if (!option) {
throw new Error(`Unknown criterion parameter name: ${type}`);
}
return option?.makeCriterion(config);
}

View File

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

View File

@@ -2,15 +2,16 @@ import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
const inputType = "galleries";
const galleriesCriterionOption = new ILabeledIdCriterionOption(
export const GalleriesCriterionOption = new ILabeledIdCriterionOption(
"galleries",
"galleries",
true,
inputType
inputType,
() => new GalleriesCriterion()
);
export class GalleriesCriterion extends ILabeledIdCriterion {
constructor() {
super(galleriesCriterionOption);
super(GalleriesCriterionOption);
}
}

View File

@@ -1,18 +1,16 @@
import { CriterionOption, StringCriterion } from "./criterion";
import {
StringBooleanCriterion,
StringBooleanCriterionOption,
} from "./criterion";
export const HasChaptersCriterionOption = new CriterionOption({
messageID: "hasChapters",
type: "has_chapters",
options: [true.toString(), false.toString()],
makeCriterion: () => new HasChaptersCriterion(),
});
export const HasChaptersCriterionOption = new StringBooleanCriterionOption(
"hasChapters",
"has_chapters",
() => new HasChaptersCriterion()
);
export class HasChaptersCriterion extends StringCriterion {
export class HasChaptersCriterion extends StringBooleanCriterion {
constructor() {
super(HasChaptersCriterionOption);
}
protected toCriterionInput(): string {
return this.value;
}
}

View File

@@ -1,18 +1,16 @@
import { CriterionOption, StringCriterion } from "./criterion";
import {
StringBooleanCriterion,
StringBooleanCriterionOption,
} from "./criterion";
export const HasMarkersCriterionOption = new CriterionOption({
messageID: "hasMarkers",
type: "has_markers",
options: [true.toString(), false.toString()],
makeCriterion: () => new HasMarkersCriterion(),
});
export const HasMarkersCriterionOption = new StringBooleanCriterionOption(
"hasMarkers",
"has_markers",
() => new HasMarkersCriterion()
);
export class HasMarkersCriterion extends StringCriterion {
export class HasMarkersCriterion extends StringBooleanCriterion {
constructor() {
super(HasMarkersCriterionOption);
}
protected toCriterionInput(): string {
return this.value;
}
}

View File

@@ -2,7 +2,8 @@ import { BooleanCriterion, BooleanCriterionOption } from "./criterion";
export const InteractiveCriterionOption = new BooleanCriterionOption(
"interactive",
"interactive"
"interactive",
() => new InteractiveCriterion()
);
export class InteractiveCriterion extends BooleanCriterion {

View File

@@ -3,26 +3,25 @@ import { CriterionType } from "../types";
import { CriterionOption, StringCriterion, Option } from "./criterion";
export class IsMissingCriterion extends StringCriterion {
public modifierOptions = [];
protected toCriterionInput(): string {
return this.value;
}
}
class IsMissingCriterionOptionClass extends CriterionOption {
class IsMissingCriterionOption extends CriterionOption {
constructor(messageID: string, type: CriterionType, options: Option[]) {
super({
messageID,
type,
options,
modifierOptions: [],
defaultModifier: CriterionModifier.Equals,
makeCriterion: () => new IsMissingCriterion(this),
});
}
}
export const SceneIsMissingCriterionOption = new IsMissingCriterionOptionClass(
export const SceneIsMissingCriterionOption = new IsMissingCriterionOption(
"isMissing",
"is_missing",
[
@@ -40,14 +39,16 @@ export const SceneIsMissingCriterionOption = new IsMissingCriterionOptionClass(
]
);
export const ImageIsMissingCriterionOption = new IsMissingCriterionOptionClass(
export const ImageIsMissingCriterionOption = new IsMissingCriterionOption(
"isMissing",
"is_missing",
["title", "galleries", "studio", "performers", "tags"]
);
export const PerformerIsMissingCriterionOption =
new IsMissingCriterionOptionClass("isMissing", "is_missing", [
export const PerformerIsMissingCriterionOption = new IsMissingCriterionOption(
"isMissing",
"is_missing",
[
"url",
"twitter",
"instagram",
@@ -67,33 +68,28 @@ export const PerformerIsMissingCriterionOption =
"image",
"details",
"stash_id",
]);
]
);
export const GalleryIsMissingCriterionOption =
new IsMissingCriterionOptionClass("isMissing", "is_missing", [
"title",
"details",
"url",
"date",
"studio",
"performers",
"tags",
"scenes",
]);
export const GalleryIsMissingCriterionOption = new IsMissingCriterionOption(
"isMissing",
"is_missing",
["title", "details", "url", "date", "studio", "performers", "tags", "scenes"]
);
export const TagIsMissingCriterionOption = new IsMissingCriterionOptionClass(
export const TagIsMissingCriterionOption = new IsMissingCriterionOption(
"isMissing",
"is_missing",
["image"]
);
export const StudioIsMissingCriterionOption = new IsMissingCriterionOptionClass(
export const StudioIsMissingCriterionOption = new IsMissingCriterionOption(
"isMissing",
"is_missing",
["image", "stash_id", "details"]
);
export const MovieIsMissingCriterionOption = new IsMissingCriterionOptionClass(
export const MovieIsMissingCriterionOption = new IsMissingCriterionOption(
"isMissing",
"is_missing",
["front_image", "back_image", "scenes"]

View File

@@ -6,7 +6,8 @@ export const MoviesCriterionOption = new ILabeledIdCriterionOption(
"movies",
"movies",
false,
inputType
inputType,
() => new MoviesCriterion()
);
export class MoviesCriterion extends ILabeledIdCriterion {

View File

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

View File

@@ -2,7 +2,8 @@ import { BooleanCriterion, BooleanCriterionOption } from "./criterion";
export const OrganizedCriterionOption = new BooleanCriterionOption(
"organized",
"organized"
"organized",
() => new OrganizedCriterion()
);
export class OrganizedCriterion extends BooleanCriterion {

View File

@@ -0,0 +1,13 @@
import { StringCriterion, StringCriterionOption } from "./criterion";
export const PathCriterionOption = new StringCriterionOption(
"path",
"path",
() => new PathCriterion()
);
export class PathCriterion extends StringCriterion {
constructor() {
super(PathCriterionOption);
}
}

View File

@@ -24,8 +24,8 @@ export const PerformersCriterionOption = new CriterionOption({
type: "performers",
modifierOptions,
defaultModifier,
makeCriterion: () => new PerformersCriterion(),
inputType,
makeCriterion: () => new PerformersCriterion(),
});
export class PerformersCriterion extends Criterion<ILabeledValueListValue> {

View File

@@ -1,13 +1,14 @@
import {
CriterionModifier,
PhashDistanceCriterionInput,
PHashDuplicationCriterionInput,
} from "src/core/generated-graphql";
import { IPhashDistanceValue } from "../types";
import {
BooleanCriterionOption,
Criterion,
CriterionOption,
PhashDuplicateCriterion,
StringCriterion,
} from "./criterion";
export const PhashCriterionOption = new CriterionOption({
@@ -56,8 +57,14 @@ export const DuplicatedCriterionOption = new BooleanCriterionOption(
() => new DuplicatedCriterion()
);
export class DuplicatedCriterion extends PhashDuplicateCriterion {
export class DuplicatedCriterion extends StringCriterion {
constructor() {
super(DuplicatedCriterionOption);
}
protected toCriterionInput(): PHashDuplicationCriterionInput {
return {
duplicated: this.value === "true",
};
}
}

View File

@@ -8,7 +8,7 @@ import {
ConfigDataFragment,
CriterionModifier,
IntCriterionInput,
} from "../../../core/generated-graphql";
} from "src/core/generated-graphql";
import { INumberValue } from "../types";
import { Criterion, CriterionOption } from "./criterion";
import { IUIConfig } from "src/core/config";

View File

@@ -11,20 +11,7 @@ import {
StringCriterion,
} from "./criterion";
abstract class AbstractResolutionCriterion extends StringCriterion {
protected toCriterionInput(): ResolutionCriterionInput | undefined {
const value = stringToResolution(this.value);
if (value !== undefined) {
return {
value,
modifier: this.modifier,
};
}
}
}
class ResolutionCriterionOptionType extends CriterionOption {
class BaseResolutionCriterionOption extends CriterionOption {
constructor(
value: CriterionType,
makeCriterion: () => Criterion<CriterionValue>
@@ -44,23 +31,37 @@ class ResolutionCriterionOptionType extends CriterionOption {
}
}
export const ResolutionCriterionOption = new ResolutionCriterionOptionType(
class BaseResolutionCriterion extends StringCriterion {
protected toCriterionInput(): ResolutionCriterionInput | undefined {
const value = stringToResolution(this.value);
if (value !== undefined) {
return {
value,
modifier: this.modifier,
};
}
}
}
export const ResolutionCriterionOption = new BaseResolutionCriterionOption(
"resolution",
() => new ResolutionCriterion()
);
export class ResolutionCriterion extends AbstractResolutionCriterion {
export class ResolutionCriterion extends BaseResolutionCriterion {
constructor() {
super(ResolutionCriterionOption);
}
}
export const AverageResolutionCriterionOption =
new ResolutionCriterionOptionType(
new BaseResolutionCriterionOption(
"average_resolution",
() => new AverageResolutionCriterion()
);
export class AverageResolutionCriterion extends AbstractResolutionCriterion {
export class AverageResolutionCriterion extends BaseResolutionCriterion {
constructor() {
super(AverageResolutionCriterionOption);
}

View File

@@ -20,8 +20,8 @@ export const StudiosCriterionOption = new CriterionOption({
type: "studios",
modifierOptions,
defaultModifier,
makeCriterion: () => new StudiosCriterion(),
inputType,
makeCriterion: () => new StudiosCriterion(),
});
export class StudiosCriterion extends IHierarchicalLabeledIdCriterion {
@@ -34,8 +34,10 @@ export const ParentStudiosCriterionOption = new ILabeledIdCriterionOption(
"parent_studios",
"parents",
false,
inputType
inputType,
() => new ParentStudiosCriterion()
);
export class ParentStudiosCriterion extends ILabeledIdCriterion {
constructor() {
super(ParentStudiosCriterionOption);

View File

@@ -20,7 +20,7 @@ const withoutEqualsModifierOptions = [
const defaultModifier = CriterionModifier.IncludesAll;
const inputType = "tags";
export class TagsCriterionOptionClass extends CriterionOption {
class BaseTagsCriterionOption extends CriterionOption {
constructor(
messageID: string,
type: CriterionType,
@@ -31,37 +31,37 @@ export class TagsCriterionOptionClass extends CriterionOption {
type,
modifierOptions,
defaultModifier,
makeCriterion: () => new TagsCriterion(this),
inputType,
makeCriterion: () => new TagsCriterion(this),
});
}
}
export const TagsCriterionOption = new TagsCriterionOptionClass(
export const TagsCriterionOption = new BaseTagsCriterionOption(
"tags",
"tags",
defaultModifierOptions
);
export const SceneTagsCriterionOption = new TagsCriterionOptionClass(
export const SceneTagsCriterionOption = new BaseTagsCriterionOption(
"scene_tags",
"scene_tags",
defaultModifierOptions
);
export const PerformerTagsCriterionOption = new TagsCriterionOptionClass(
export const PerformerTagsCriterionOption = new BaseTagsCriterionOption(
"performer_tags",
"performer_tags",
withoutEqualsModifierOptions
);
export const ParentTagsCriterionOption = new TagsCriterionOptionClass(
export const ParentTagsCriterionOption = new BaseTagsCriterionOption(
"parent_tags",
"parents",
withoutEqualsModifierOptions
);
export const ChildTagsCriterionOption = new TagsCriterionOptionClass(
export const ChildTagsCriterionOption = new BaseTagsCriterionOption(
"sub_tags",
"children",
withoutEqualsModifierOptions

View File

@@ -6,7 +6,7 @@ import {
SortDirectionEnum,
} from "src/core/generated-graphql";
import { Criterion, CriterionValue } from "./criteria/criterion";
import { makeCriteria } from "./criteria/factory";
import { getFilterOptions } from "./factory";
import { CriterionType, DisplayMode } from "./types";
interface IDecodedParams {
@@ -128,16 +128,9 @@ export class ListFilterModel {
for (const jsonString of params.c) {
try {
const encodedCriterion = JSON.parse(jsonString);
const criterion = makeCriteria(
this.mode,
encodedCriterion.type,
this.config
);
// it's possible that we have unsupported criteria. Just skip if so.
if (criterion) {
criterion.setFromEncodedCriterion(encodedCriterion);
this.criteria.push(criterion);
}
const criterion = this.makeCriterion(encodedCriterion.type);
criterion.setFromEncodedCriterion(encodedCriterion);
this.criteria.push(criterion);
} catch (err) {
// eslint-disable-next-line no-console
console.error("Failed to parse encoded criterion:", err);
@@ -280,16 +273,9 @@ export class ListFilterModel {
this.criteria = [];
if (objectFilter) {
Object.keys(objectFilter).forEach((key) => {
const criterion = makeCriteria(
this.mode,
key as CriterionType,
this.config
);
// it's possible that we have unsupported criteria. Just skip if so.
if (criterion) {
criterion.setFromEncodedCriterion(objectFilter[key]);
this.criteria.push(criterion);
}
const criterion = this.makeCriterion(key as CriterionType);
criterion.setFromEncodedCriterion(objectFilter[key]);
this.criteria.push(criterion);
});
}
}
@@ -426,6 +412,18 @@ export class ListFilterModel {
return query.join("&");
}
public makeCriterion(type: CriterionType) {
const { criterionOptions } = getFilterOptions(this.mode);
const option = criterionOptions.find((o) => o.type === type);
if (!option) {
throw new Error(`Unknown criterion parameter name: ${type}`);
}
return option.makeCriterion(this.config);
}
// TODO: These don't support multiple of the same criteria, only the last one set is used.
public makeFindFilter(): FindFilterType {
@@ -439,8 +437,7 @@ export class ListFilterModel {
}
public makeFilter() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const output: Record<string, any> = {};
const output: Record<string, unknown> = {};
this.criteria.forEach((criterion) => {
criterion.apply(output);
});
@@ -449,8 +446,7 @@ export class ListFilterModel {
}
public makeSavedFindFilter() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const output: Record<string, any> = {};
const output: Record<string, unknown> = {};
this.criteria.forEach((criterion) => {
criterion.toSavedFilter(output);
});
@@ -458,8 +454,7 @@ export class ListFilterModel {
return output;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public makeUIOptions(): Record<string, any> {
public makeUIOptions(): Record<string, unknown> {
return {
display_mode: this.displayMode,
zoom_index: this.zoomIndex,

View File

@@ -3,7 +3,6 @@ import {
createStringCriterionOption,
createDateCriterionOption,
createMandatoryTimestampCriterionOption,
createPathCriterionOption,
} from "./criteria/criterion";
import { PerformerFavoriteCriterionOption } from "./criteria/favorite";
import { GalleryIsMissingCriterionOption } from "./criteria/is-missing";
@@ -19,6 +18,7 @@ import {
import { ListFilterOptions, MediaSortByOptions } from "./filter-options";
import { DisplayMode } from "./types";
import { RatingCriterionOption } from "./criteria/rating";
import { PathCriterionOption } from "./criteria/path";
const defaultSortBy = "path";
@@ -44,7 +44,7 @@ const displayModeOptions = [
const criterionOptions = [
createStringCriterionOption("title"),
createStringCriterionOption("details"),
createPathCriterionOption("path"),
PathCriterionOption,
createStringCriterionOption("checksum", "media_info.checksum"),
RatingCriterionOption,
OrganizedCriterionOption,
@@ -52,13 +52,13 @@ const criterionOptions = [
GalleryIsMissingCriterionOption,
TagsCriterionOption,
HasChaptersCriterionOption,
createStringCriterionOption("tag_count"),
createMandatoryNumberCriterionOption("tag_count"),
PerformerTagsCriterionOption,
PerformersCriterionOption,
createStringCriterionOption("performer_count"),
createMandatoryNumberCriterionOption("performer_count"),
createMandatoryNumberCriterionOption("performer_age"),
PerformerFavoriteCriterionOption,
createStringCriterionOption("image_count"),
createMandatoryNumberCriterionOption("image_count"),
StudiosCriterionOption,
createStringCriterionOption("url"),
createMandatoryNumberCriterionOption("file_count", "zip_file_count"),

View File

@@ -4,11 +4,11 @@ import {
createStringCriterionOption,
createMandatoryTimestampCriterionOption,
createDateCriterionOption,
createPathCriterionOption,
} from "./criteria/criterion";
import { PerformerFavoriteCriterionOption } from "./criteria/favorite";
import { ImageIsMissingCriterionOption } from "./criteria/is-missing";
import { OrganizedCriterionOption } from "./criteria/organized";
import { PathCriterionOption } from "./criteria/path";
import { PerformersCriterionOption } from "./criteria/performers";
import { RatingCriterionOption } from "./criteria/rating";
import { ResolutionCriterionOption } from "./criteria/resolution";
@@ -34,7 +34,7 @@ const displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall];
const criterionOptions = [
createStringCriterionOption("title"),
createMandatoryStringCriterionOption("checksum", "media_info.checksum"),
createPathCriterionOption("path"),
PathCriterionOption,
OrganizedCriterionOption,
createMandatoryNumberCriterionOption("o_counter"),
ResolutionCriterionOption,

View File

@@ -1,8 +1,8 @@
import {
createMandatoryNumberCriterionOption,
createStringCriterionOption,
createDateCriterionOption,
createMandatoryTimestampCriterionOption,
createDurationCriterionOption,
} from "./criteria/criterion";
import { MovieIsMissingCriterionOption } from "./criteria/is-missing";
import { StudiosCriterionOption } from "./criteria/studios";
@@ -29,7 +29,7 @@ const criterionOptions = [
createStringCriterionOption("name"),
createStringCriterionOption("director"),
createStringCriterionOption("synopsis"),
createMandatoryNumberCriterionOption("duration"),
createDurationCriterionOption("duration"),
RatingCriterionOption,
PerformersCriterionOption,
createDateCriterionOption("date"),

View File

@@ -5,7 +5,6 @@ import {
createBooleanCriterionOption,
createDateCriterionOption,
createMandatoryTimestampCriterionOption,
NumberCriterionOption,
} from "./criteria/criterion";
import { FavoriteCriterionOption } from "./criteria/favorite";
import { GenderCriterionOption } from "./criteria/gender";
@@ -94,9 +93,9 @@ const criterionOptions = [
createMandatoryNumberCriterionOption("gallery_count"),
createMandatoryNumberCriterionOption("o_counter"),
createBooleanCriterionOption("ignore_auto_tag"),
new NumberCriterionOption("height", "height_cm"),
...numberCriteria.map((c) => createNumberCriterionOption(c)),
CountryCriterionOption,
createNumberCriterionOption("height_cm", "height"),
...numberCriteria.map((c) => createNumberCriterionOption(c)),
...stringCriteria.map((c) => createStringCriterionOption(c)),
createDateCriterionOption("birthdate"),
createDateCriterionOption("death_date"),

View File

@@ -4,7 +4,7 @@ import {
createStringCriterionOption,
createDateCriterionOption,
createMandatoryTimestampCriterionOption,
createPathCriterionOption,
createDurationCriterionOption,
} from "./criteria/criterion";
import { HasMarkersCriterionOption } from "./criteria/has-markers";
import { SceneIsMissingCriterionOption } from "./criteria/is-missing";
@@ -28,6 +28,7 @@ import { PerformerFavoriteCriterionOption } from "./criteria/favorite";
import { CaptionsCriterionOption } from "./criteria/captions";
import { StashIDCriterionOption } from "./criteria/stash-ids";
import { RatingCriterionOption } from "./criteria/rating";
import { PathCriterionOption } from "./criteria/path";
const defaultSortBy = "date";
const sortByOptions = [
@@ -60,7 +61,7 @@ const displayModeOptions = [
const criterionOptions = [
createStringCriterionOption("title"),
createStringCriterionOption("code", "scene_code"),
createPathCriterionOption("path"),
PathCriterionOption,
createStringCriterionOption("details"),
createStringCriterionOption("director"),
createMandatoryStringCriterionOption("oshash", "media_info.hash"),
@@ -73,9 +74,9 @@ const criterionOptions = [
ResolutionCriterionOption,
createStringCriterionOption("video_codec"),
createStringCriterionOption("audio_codec"),
createMandatoryNumberCriterionOption("duration"),
createMandatoryNumberCriterionOption("resume_time"),
createMandatoryNumberCriterionOption("play_duration"),
createDurationCriterionOption("duration"),
createDurationCriterionOption("resume_time"),
createDurationCriterionOption("play_duration"),
createMandatoryNumberCriterionOption("play_count"),
HasMarkersCriterionOption,
SceneIsMissingCriterionOption,

View File

@@ -1,5 +1,4 @@
// NOTE: add new enum values to the end, to ensure existing data
// is not impacted
export enum DisplayMode {
Grid,
@@ -60,38 +59,51 @@ export interface IPhashDistanceValue {
}
export function criterionIsHierarchicalLabelValue(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any
value: unknown
): value is IHierarchicalLabelValue {
return typeof value === "object" && "items" in value && "depth" in value;
return (
typeof value === "object" && !!value && "items" in value && "depth" in value
);
}
export function criterionIsNumberValue(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any
): value is INumberValue {
return typeof value === "object" && "value" in value && "value2" in value;
export function criterionIsNumberValue(value: unknown): value is INumberValue {
return (
typeof value === "object" &&
!!value &&
"value" in value &&
"value2" in value
);
}
export function criterionIsStashIDValue(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any
value: unknown
): value is IStashIDValue {
return typeof value === "object" && "endpoint" in value && "stashID" in value;
return (
typeof value === "object" &&
!!value &&
"endpoint" in value &&
"stashID" in value
);
}
export function criterionIsDateValue(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any
): value is IDateValue {
return typeof value === "object" && "value" in value && "value2" in value;
export function criterionIsDateValue(value: unknown): value is IDateValue {
return (
typeof value === "object" &&
!!value &&
"value" in value &&
"value2" in value
);
}
export function criterionIsTimestampValue(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any
value: unknown
): value is ITimestampValue {
return typeof value === "object" && "value" in value && "value2" in value;
return (
typeof value === "object" &&
!!value &&
"value" in value &&
"value2" in value
);
}
export interface IOptionType {
@@ -101,7 +113,6 @@ export interface IOptionType {
}
export type CriterionType =
| "none"
| "path"
| "rating"
| "rating100"

View File

@@ -16,13 +16,13 @@ const secondsToString = (seconds: number) => {
const stringToSeconds = (v?: string) => {
if (!v) {
return 0;
return undefined;
}
const splits = v.split(":");
if (splits.length > 3) {
return 0;
return undefined;
}
let seconds = 0;
@@ -30,12 +30,12 @@ const stringToSeconds = (v?: string) => {
while (splits.length > 0) {
const thisSplit = splits.pop();
if (thisSplit === undefined) {
return 0;
return undefined;
}
const thisInt = parseInt(thisSplit, 10);
if (Number.isNaN(thisInt)) {
return 0;
return undefined;
}
seconds += factor * thisInt;

View File

@@ -79,29 +79,14 @@ const renderInputGroup = (options: {
};
const renderDurationInput = (options: {
value: string | undefined;
value: number | undefined;
isEditing: boolean;
url?: string;
asString?: boolean;
onChange: (value: string | undefined) => void;
onChange: (value: number | undefined) => void;
}) => {
let numericValue: number | undefined;
if (options.value) {
if (!options.asString) {
try {
numericValue = Number.parseInt(options.value, 10);
} catch {
// ignore
}
} else {
numericValue = DurationUtils.stringToSeconds(options.value);
}
}
if (!options.isEditing) {
let durationString;
if (numericValue !== undefined) {
durationString = DurationUtils.secondsToString(numericValue);
if (options.value !== undefined) {
durationString = DurationUtils.secondsToString(options.value);
}
return (
@@ -113,19 +98,11 @@ const renderDurationInput = (options: {
/>
);
}
return (
<DurationInput
disabled={!options.isEditing}
numericValue={numericValue}
onValueChange={(valueAsNumber: number, valueAsString?: string) => {
let value = valueAsString;
if (!options.asString) {
value =
valueAsNumber !== undefined ? valueAsNumber.toString() : undefined;
}
options.onChange(value);
}}
value={options.value}
setValue={(v) => options.onChange(v)}
/>
);
};

View File

@@ -86,10 +86,9 @@ const renderInputGroup = (options: {
const renderDurationInput = (options: {
title: string;
placeholder?: string;
value: string | undefined;
value: number | undefined;
isEditing: boolean;
asString?: boolean;
onChange: (value: string | undefined) => void;
onChange: (value: number | undefined) => void;
labelProps?: FormLabelProps;
inputProps?: ColProps;
}) => {

View File

@@ -41,10 +41,9 @@ const renderInputGroup = (options: {
const renderDurationInput = (options: {
title: string;
placeholder?: string;
value: string | undefined;
value: number | undefined;
isEditing: boolean;
asString?: boolean;
onChange: (value: string | undefined) => void;
onChange: (value: number | undefined) => void;
}) => {
return (
<tr>