UI filter refactor (#1406)

* Refactor Criterion
* Separate filter options from filter
* Rename utils to factory
* Sort sort by options by alphabetical
* Refactor criterion options
* Simplify list filter options
* Refactor i8n
* Simplify ILabeledIdCriterion
This commit is contained in:
WithoutPants
2021-05-31 13:46:21 +10:00
committed by GitHub
parent c5fed1bbdc
commit 3e81d45ae9
51 changed files with 1330 additions and 1963 deletions

View File

@@ -44,13 +44,27 @@ const intlFormats = {
}, },
}; };
function languageMessageString(language: string) {
return language.replace(/-/, "");
}
export const App: React.FC = () => { export const App: React.FC = () => {
const config = useConfiguration(); const config = useConfiguration();
const { data: systemStatusData } = useSystemStatus(); const { data: systemStatusData } = useSystemStatus();
const language = config.data?.configuration?.interface?.language ?? "en-GB"; const defaultLocale = "en-GB";
const messageLanguage = language.replace(/-/, ""); const language =
// eslint-disable-next-line @typescript-eslint/no-explicit-any config.data?.configuration?.interface?.language ?? defaultLocale;
const messages = flattenMessages((locales as any)[messageLanguage]); const defaultMessageLanguage = languageMessageString(defaultLocale);
const messageLanguage = languageMessageString(language);
// use en-GB as default messages if any messages aren't found in the chosen language
const mergedMessages = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...(locales as any)[defaultMessageLanguage],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...(locales as any)[messageLanguage],
};
const messages = flattenMessages(mergedMessages);
const setupMatch = useRouteMatch(["/setup", "/migrate"]); const setupMatch = useRouteMatch(["/setup", "/migrate"]);

View File

@@ -7,6 +7,7 @@
* Added [DLNA server](/settings?tab=dlna). ([#1364](https://github.com/stashapp/stash/pull/1364)) * Added [DLNA server](/settings?tab=dlna). ([#1364](https://github.com/stashapp/stash/pull/1364))
### 🎨 Improvements ### 🎨 Improvements
* Filter modifiers and sort by options are now sorted alphabetically. ([#1406](https://github.com/stashapp/stash/pull/1406))
* Add `CreatedAt` and `UpdatedAt` (and `FileModTime` where applicable) to API objects. ([#1421](https://github.com/stashapp/stash/pull/1421)) * Add `CreatedAt` and `UpdatedAt` (and `FileModTime` where applicable) to API objects. ([#1421](https://github.com/stashapp/stash/pull/1421))
* Add Studios Performer filter criterion. ([#1405](https://github.com/stashapp/stash/pull/1405)) * Add Studios Performer filter criterion. ([#1405](https://github.com/stashapp/stash/pull/1405))
* Add `subtractDays` post-process scraper action. ([#1399](https://github.com/stashapp/stash/pull/1399)) * Add `subtractDays` post-process scraper action. ([#1399](https://github.com/stashapp/stash/pull/1399))

View File

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

View File

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

View File

@@ -5,20 +5,24 @@ import Mousetrap from "mousetrap";
import { Icon, FilterSelect, DurationInput } from "src/components/Shared"; import { Icon, FilterSelect, DurationInput } from "src/components/Shared";
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { import {
Criterion,
CriterionType,
DurationCriterion, DurationCriterion,
CriterionValue, CriterionValue,
Criterion,
} from "src/models/list-filter/criteria/criterion"; } from "src/models/list-filter/criteria/criterion";
import { NoneCriterion } from "src/models/list-filter/criteria/none"; import { NoneCriterion } from "src/models/list-filter/criteria/none";
import { makeCriteria } from "src/models/list-filter/criteria/utils"; import { makeCriteria } from "src/models/list-filter/criteria/factory";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterOptions } from "src/models/list-filter/filter-options";
import { useIntl } from "react-intl";
import { CriterionType } from "src/models/list-filter/types";
interface IAddFilterProps { interface IAddFilterProps {
onAddCriterion: (criterion: Criterion, oldId?: string) => void; onAddCriterion: (
criterion: Criterion<CriterionValue>,
oldId?: string
) => void;
onCancel: () => void; onCancel: () => void;
filter: ListFilterModel; filterOptions: ListFilterOptions;
editingCriterion?: Criterion; editingCriterion?: Criterion<CriterionValue>;
} }
export const AddFilter: React.FC<IAddFilterProps> = ( export const AddFilter: React.FC<IAddFilterProps> = (
@@ -27,10 +31,14 @@ export const AddFilter: React.FC<IAddFilterProps> = (
const defaultValue = useRef<string | number | undefined>(); const defaultValue = useRef<string | number | undefined>();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [criterion, setCriterion] = useState<Criterion>(new NoneCriterion()); const [criterion, setCriterion] = useState<Criterion<CriterionValue>>(
new NoneCriterion()
);
const valueStage = useRef<CriterionValue>(criterion.value); const valueStage = useRef<CriterionValue>(criterion.value);
const intl = useIntl();
// configure keyboard shortcuts // configure keyboard shortcuts
useEffect(() => { useEffect(() => {
Mousetrap.bind("f", () => setIsOpen(true)); Mousetrap.bind("f", () => setIsOpen(true));
@@ -114,7 +122,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
} }
const maybeRenderFilterPopoverContents = () => { const maybeRenderFilterPopoverContents = () => {
if (criterion.type === "none") { if (criterion.criterionOption.value === "none") {
return; return;
} }
@@ -149,19 +157,19 @@ export const AddFilter: React.FC<IAddFilterProps> = (
if (Array.isArray(criterion.value)) { if (Array.isArray(criterion.value)) {
if ( if (
criterion.type !== "performers" && criterion.criterionOption.value !== "performers" &&
criterion.type !== "studios" && criterion.criterionOption.value !== "studios" &&
criterion.type !== "parent_studios" && criterion.criterionOption.value !== "parent_studios" &&
criterion.type !== "tags" && criterion.criterionOption.value !== "tags" &&
criterion.type !== "sceneTags" && criterion.criterionOption.value !== "sceneTags" &&
criterion.type !== "performerTags" && criterion.criterionOption.value !== "performerTags" &&
criterion.type !== "movies" criterion.criterionOption.value !== "movies"
) )
return; return;
return ( return (
<FilterSelect <FilterSelect
type={criterion.type} type={criterion.criterionOption.value}
isMulti isMulti
onSelect={(items) => { onSelect={(items) => {
const newCriterion = _.cloneDeep(criterion); const newCriterion = _.cloneDeep(criterion);
@@ -223,18 +231,28 @@ export const AddFilter: React.FC<IAddFilterProps> = (
if (props.editingCriterion) { if (props.editingCriterion) {
return; return;
} }
const options = props.filterOptions.criterionOptions
.map((c) => {
return {
value: c.value,
text: intl.formatMessage({ id: c.messageID }),
};
})
.sort((a, b) => a.text.localeCompare(b.text));
return ( return (
<Form.Group controlId="filter"> <Form.Group controlId="filter">
<Form.Label>Filter</Form.Label> <Form.Label>Filter</Form.Label>
<Form.Control <Form.Control
as="select" as="select"
onChange={onChangedCriteriaType} onChange={onChangedCriteriaType}
value={criterion.type} value={criterion.criterionOption.value}
className="btn-secondary" className="btn-secondary"
> >
{props.filter.criterionOptions.map((c) => ( {options.map((c) => (
<option key={c.value} value={c.value}> <option key={c.value} value={c.value}>
{c.label} {c.text}
</option> </option>
))} ))}
</Form.Control> </Form.Control>
@@ -263,7 +281,10 @@ export const AddFilter: React.FC<IAddFilterProps> = (
</div> </div>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button onClick={onAddFilter} disabled={criterion.type === "none"}> <Button
onClick={onAddFilter}
disabled={criterion.criterionOption.value === "none"}
>
{title} {title}
</Button> </Button>
</Modal.Footer> </Modal.Footer>

View File

@@ -16,10 +16,15 @@ import {
} from "react-bootstrap"; } from "react-bootstrap";
import { Icon } from "src/components/Shared"; import { Icon } from "src/components/Shared";
import { Criterion } from "src/models/list-filter/criteria/criterion";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types"; import { DisplayMode } from "src/models/list-filter/types";
import { useFocus } from "src/utils"; import { useFocus } from "src/utils";
import { ListFilterOptions } from "src/models/list-filter/filter-options";
import { useIntl } from "react-intl";
import {
Criterion,
CriterionValue,
} from "src/models/list-filter/criteria/criterion";
import { AddFilter } from "./AddFilter"; import { AddFilter } from "./AddFilter";
interface IListFilterOperation { interface IListFilterOperation {
@@ -38,6 +43,7 @@ interface IListFilterProps {
onDelete?: () => void; onDelete?: () => void;
otherOperations?: IListFilterOperation[]; otherOperations?: IListFilterOperation[];
filter: ListFilterModel; filter: ListFilterModel;
filterOptions: ListFilterOptions;
itemsSelected?: boolean; itemsSelected?: boolean;
} }
@@ -58,9 +64,11 @@ export const ListFilter: React.FC<IListFilterProps> = (
}, 500); }, 500);
const [editingCriterion, setEditingCriterion] = useState< const [editingCriterion, setEditingCriterion] = useState<
Criterion | undefined Criterion<CriterionValue> | undefined
>(undefined); >(undefined);
const intl = useIntl();
useEffect(() => { useEffect(() => {
Mousetrap.bind("/", (e) => { Mousetrap.bind("/", (e) => {
setQueryFocus(); setQueryFocus();
@@ -69,17 +77,17 @@ export const ListFilter: React.FC<IListFilterProps> = (
Mousetrap.bind("r", () => onReshuffleRandomSort()); Mousetrap.bind("r", () => onReshuffleRandomSort());
Mousetrap.bind("v g", () => { Mousetrap.bind("v g", () => {
if (props.filter.displayModeOptions.includes(DisplayMode.Grid)) { if (props.filterOptions.displayModeOptions.includes(DisplayMode.Grid)) {
onChangeDisplayMode(DisplayMode.Grid); onChangeDisplayMode(DisplayMode.Grid);
} }
}); });
Mousetrap.bind("v l", () => { Mousetrap.bind("v l", () => {
if (props.filter.displayModeOptions.includes(DisplayMode.List)) { if (props.filterOptions.displayModeOptions.includes(DisplayMode.List)) {
onChangeDisplayMode(DisplayMode.List); onChangeDisplayMode(DisplayMode.List);
} }
}); });
Mousetrap.bind("v w", () => { Mousetrap.bind("v w", () => {
if (props.filter.displayModeOptions.includes(DisplayMode.Wall)) { if (props.filterOptions.displayModeOptions.includes(DisplayMode.Wall)) {
onChangeDisplayMode(DisplayMode.Wall); onChangeDisplayMode(DisplayMode.Wall);
} }
}); });
@@ -160,9 +168,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
props.onFilterUpdate(newFilter); props.onFilterUpdate(newFilter);
} }
function onChangeSortBy(event: React.MouseEvent<HTMLAnchorElement>) { function onChangeSortBy(eventKey: string | null) {
const newFilter = _.cloneDeep(props.filter); const newFilter = _.cloneDeep(props.filter);
newFilter.sortBy = event.currentTarget.text; newFilter.sortBy = eventKey ?? undefined;
newFilter.currentPage = 1; newFilter.currentPage = 1;
props.onFilterUpdate(newFilter); props.onFilterUpdate(newFilter);
} }
@@ -180,7 +188,10 @@ export const ListFilter: React.FC<IListFilterProps> = (
props.onFilterUpdate(newFilter); props.onFilterUpdate(newFilter);
} }
function onAddCriterion(criterion: Criterion, oldId?: string) { function onAddCriterion(
criterion: Criterion<CriterionValue>,
oldId?: string
) {
const newFilter = _.cloneDeep(props.filter); const newFilter = _.cloneDeep(props.filter);
// Find if we are editing an existing criteria, then modify that. Or create a new one. // Find if we are editing an existing criteria, then modify that. Or create a new one.
@@ -208,7 +219,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
setEditingCriterion(undefined); setEditingCriterion(undefined);
} }
function onRemoveCriterion(removedCriterion: Criterion) { function onRemoveCriterion(removedCriterion: Criterion<CriterionValue>) {
const newFilter = _.cloneDeep(props.filter); const newFilter = _.cloneDeep(props.filter);
newFilter.criteria = newFilter.criteria.filter( newFilter.criteria = newFilter.criteria.filter(
(criterion) => criterion.getId() !== removedCriterion.getId() (criterion) => criterion.getId() !== removedCriterion.getId()
@@ -218,7 +229,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
} }
let removedCriterionId = ""; let removedCriterionId = "";
function onRemoveCriterionTag(criterion?: Criterion) { function onRemoveCriterionTag(criterion?: Criterion<CriterionValue>) {
if (!criterion) { if (!criterion) {
return; return;
} }
@@ -227,7 +238,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
onRemoveCriterion(criterion); onRemoveCriterion(criterion);
} }
function onClickCriterionTag(criterion?: Criterion) { function onClickCriterionTag(criterion?: Criterion<CriterionValue>) {
if (!criterion || removedCriterionId !== "") { if (!criterion || removedCriterionId !== "") {
return; return;
} }
@@ -235,15 +246,24 @@ export const ListFilter: React.FC<IListFilterProps> = (
} }
function renderSortByOptions() { function renderSortByOptions() {
return props.filter.sortByOptions.map((option) => ( return props.filterOptions.sortByOptions
<Dropdown.Item .map((o) => {
onClick={onChangeSortBy} return {
key={option} message: intl.formatMessage({ id: o.messageID }),
className="bg-secondary text-white" value: o.value,
> };
{option} })
</Dropdown.Item> .sort((a, b) => a.message.localeCompare(b.message))
)); .map((option) => (
<Dropdown.Item
onSelect={onChangeSortBy}
key={option.value}
className="bg-secondary text-white"
eventKey={option.value}
>
{option.message}
</Dropdown.Item>
));
} }
function renderDisplayModeOptions() { function renderDisplayModeOptions() {
@@ -272,7 +292,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
} }
} }
return props.filter.displayModeOptions.map((option) => ( return props.filterOptions.displayModeOptions.map((option) => (
<OverlayTrigger <OverlayTrigger
key={option} key={option}
overlay={ overlay={
@@ -298,7 +318,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
key={criterion.getId()} key={criterion.getId()}
onClick={() => onClickCriterionTag(criterion)} onClick={() => onClickCriterionTag(criterion)}
> >
{criterion.getLabel()} {criterion.getLabel(intl)}
<Button <Button
variant="secondary" variant="secondary"
onClick={() => onRemoveCriterionTag(criterion)} onClick={() => onRemoveCriterionTag(criterion)}
@@ -450,6 +470,10 @@ export const ListFilter: React.FC<IListFilterProps> = (
} }
function render() { function render() {
const currentSortBy = props.filterOptions.sortByOptions.find(
(o) => o.value === props.filter.sortBy
);
return ( return (
<> <>
<ButtonToolbar className="align-items-center justify-content-center mb-2"> <ButtonToolbar className="align-items-center justify-content-center mb-2">
@@ -465,7 +489,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
<InputGroup.Append> <InputGroup.Append>
<AddFilter <AddFilter
filter={props.filter} filterOptions={props.filterOptions}
onAddCriterion={onAddCriterion} onAddCriterion={onAddCriterion}
onCancel={onCancelAddCriterion} onCancel={onCancelAddCriterion}
editingCriterion={editingCriterion} editingCriterion={editingCriterion}
@@ -475,7 +499,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
<Dropdown as={ButtonGroup} className="mr-2"> <Dropdown as={ButtonGroup} className="mr-2">
<Dropdown.Toggle split variant="secondary" id="more-menu"> <Dropdown.Toggle split variant="secondary" id="more-menu">
{props.filter.sortBy} {intl.formatMessage({ id: currentSortBy?.messageID })}
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu className="bg-secondary text-white"> <Dropdown.Menu className="bg-secondary text-white">
{renderSortByOptions()} {renderSortByOptions()}

View File

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

View File

@@ -11,13 +11,16 @@ import {
TruncatedText, TruncatedText,
} from "src/components/Shared"; } from "src/components/Shared";
import { Button, ButtonGroup } from "react-bootstrap"; import { Button, ButtonGroup } from "react-bootstrap";
import { Criterion } from "src/models/list-filter/criteria/criterion"; import {
Criterion,
CriterionValue,
} from "src/models/list-filter/criteria/criterion";
import { PopoverCountButton } from "../Shared/PopoverCountButton"; import { PopoverCountButton } from "../Shared/PopoverCountButton";
export interface IPerformerCardExtraCriteria { export interface IPerformerCardExtraCriteria {
scenes: Criterion[]; scenes: Criterion<CriterionValue>[];
images: Criterion[]; images: Criterion<CriterionValue>[];
galleries: Criterion[]; galleries: Criterion<CriterionValue>[];
} }
interface IPerformerCardProps { interface IPerformerCardProps {

View File

@@ -22,7 +22,6 @@ import { ScenePlayer } from "src/components/ScenePlayer";
import { TextUtils, JWUtils } from "src/utils"; import { TextUtils, JWUtils } from "src/utils";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { FilterMode } from "src/models/list-filter/types";
import { SceneQueue } from "src/models/sceneQueue"; import { SceneQueue } from "src/models/sceneQueue";
import { QueueViewer } from "./QueueViewer"; import { QueueViewer } from "./QueueViewer";
import { SceneMarkersPanel } from "./SceneMarkersPanel"; import { SceneMarkersPanel } from "./SceneMarkersPanel";
@@ -220,10 +219,7 @@ export const Scene: React.FC = () => {
return; return;
} }
const filterCopy = Object.assign( const filterCopy = Object.assign(new ListFilterModel(), sceneQueue.query);
new ListFilterModel(FilterMode.Scenes),
sceneQueue.query
);
const newStart = queueStart - filterCopy.itemsPerPage; const newStart = queueStart - filterCopy.itemsPerPage;
filterCopy.currentPage = Math.ceil(newStart / filterCopy.itemsPerPage); filterCopy.currentPage = Math.ceil(newStart / filterCopy.itemsPerPage);
const query = await queryFindScenes(filterCopy); const query = await queryFindScenes(filterCopy);
@@ -244,10 +240,7 @@ export const Scene: React.FC = () => {
return; return;
} }
const filterCopy = Object.assign( const filterCopy = Object.assign(new ListFilterModel(), sceneQueue.query);
new ListFilterModel(FilterMode.Scenes),
sceneQueue.query
);
const newStart = queueStart + queueScenes.length; const newStart = queueStart + queueScenes.length;
filterCopy.currentPage = Math.ceil(newStart / filterCopy.itemsPerPage); filterCopy.currentPage = Math.ceil(newStart / filterCopy.itemsPerPage);
const query = await queryFindScenes(filterCopy); const query = await queryFindScenes(filterCopy);
@@ -284,10 +277,7 @@ export const Scene: React.FC = () => {
const pages = Math.ceil(queueTotal / query.itemsPerPage); const pages = Math.ceil(queueTotal / query.itemsPerPage);
const page = Math.floor(Math.random() * pages) + 1; const page = Math.floor(Math.random() * pages) + 1;
const index = Math.floor(Math.random() * query.itemsPerPage); const index = Math.floor(Math.random() * query.itemsPerPage);
const filterCopy = Object.assign( const filterCopy = Object.assign(new ListFilterModel(), sceneQueue.query);
new ListFilterModel(FilterMode.Scenes),
sceneQueue.query
);
filterCopy.currentPage = page; filterCopy.currentPage = page;
const queryResults = await queryFindScenes(filterCopy); const queryResults = await queryFindScenes(filterCopy);
if (queryResults.data.findScenes.scenes.length > index) { if (queryResults.data.findScenes.scenes.length > index) {

View File

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

View File

@@ -1,7 +1,10 @@
import React from "react"; import React from "react";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { TagsCriterion } from "src/models/list-filter/criteria/tags"; import {
TagsCriterion,
TagsCriterionOption,
} from "src/models/list-filter/criteria/tags";
import { SceneMarkerList } from "src/components/Scenes/SceneMarkerList"; import { SceneMarkerList } from "src/components/Scenes/SceneMarkerList";
interface ITagMarkersPanel { interface ITagMarkersPanel {
@@ -13,7 +16,7 @@ export const TagMarkersPanel: React.FC<ITagMarkersPanel> = ({ tag }) => {
const tagValue = { id: tag.id!, label: tag.name! }; const tagValue = { id: tag.id!, label: tag.name! };
// if tag is already present, then we modify it, otherwise add // if tag is already present, then we modify it, otherwise add
let tagCriterion = filter.criteria.find((c) => { let tagCriterion = filter.criteria.find((c) => {
return c.type === "tags"; return c.criterionOption.value === "tags";
}) as TagsCriterion; }) as TagsCriterion;
if ( if (
@@ -33,7 +36,7 @@ export const TagMarkersPanel: React.FC<ITagMarkersPanel> = ({ tag }) => {
tagCriterion.modifier = GQL.CriterionModifier.IncludesAll; tagCriterion.modifier = GQL.CriterionModifier.IncludesAll;
} else { } else {
// overwrite // overwrite
tagCriterion = new TagsCriterion("tags"); tagCriterion = new TagsCriterion(TagsCriterionOption);
tagCriterion.value = [tagValue]; tagCriterion.value = [tagValue];
filter.criteria.push(tagCriterion); filter.criteria.push(tagCriterion);
} }

View File

@@ -47,7 +47,7 @@ export const useFindGalleries = (filter: ListFilterModel) =>
GQL.useFindGalleriesQuery({ GQL.useFindGalleriesQuery({
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
gallery_filter: filter.makeGalleryFilter(), gallery_filter: filter.makeFilter(),
}, },
}); });
@@ -56,7 +56,7 @@ export const queryFindGalleries = (filter: ListFilterModel) =>
query: GQL.FindGalleriesDocument, query: GQL.FindGalleriesDocument,
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
gallery_filter: filter.makeImageFilter(), gallery_filter: filter.makeFilter(),
}, },
}); });
@@ -64,7 +64,7 @@ export const useFindScenes = (filter: ListFilterModel) =>
GQL.useFindScenesQuery({ GQL.useFindScenesQuery({
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
scene_filter: filter.makeSceneFilter(), scene_filter: filter.makeFilter(),
}, },
}); });
@@ -73,7 +73,7 @@ export const queryFindScenes = (filter: ListFilterModel) =>
query: GQL.FindScenesDocument, query: GQL.FindScenesDocument,
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
scene_filter: filter.makeSceneFilter(), scene_filter: filter.makeFilter(),
}, },
}); });
@@ -89,7 +89,7 @@ export const useFindSceneMarkers = (filter: ListFilterModel) =>
GQL.useFindSceneMarkersQuery({ GQL.useFindSceneMarkersQuery({
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
scene_marker_filter: filter.makeSceneMarkerFilter(), scene_marker_filter: filter.makeFilter(),
}, },
}); });
@@ -98,7 +98,7 @@ export const queryFindSceneMarkers = (filter: ListFilterModel) =>
query: GQL.FindSceneMarkersDocument, query: GQL.FindSceneMarkersDocument,
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
scene_marker_filter: filter.makeSceneMarkerFilter(), scene_marker_filter: filter.makeFilter(),
}, },
}); });
@@ -106,7 +106,7 @@ export const useFindImages = (filter: ListFilterModel) =>
GQL.useFindImagesQuery({ GQL.useFindImagesQuery({
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
image_filter: filter.makeImageFilter(), image_filter: filter.makeFilter(),
}, },
}); });
@@ -115,7 +115,7 @@ export const queryFindImages = (filter: ListFilterModel) =>
query: GQL.FindImagesDocument, query: GQL.FindImagesDocument,
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
image_filter: filter.makeImageFilter(), image_filter: filter.makeFilter(),
}, },
}); });
@@ -123,7 +123,7 @@ export const useFindStudios = (filter: ListFilterModel) =>
GQL.useFindStudiosQuery({ GQL.useFindStudiosQuery({
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
studio_filter: filter.makeStudioFilter(), studio_filter: filter.makeFilter(),
}, },
}); });
@@ -132,7 +132,7 @@ export const queryFindStudios = (filter: ListFilterModel) =>
query: GQL.FindStudiosDocument, query: GQL.FindStudiosDocument,
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
studio_filter: filter.makeStudioFilter(), studio_filter: filter.makeFilter(),
}, },
}); });
@@ -140,7 +140,7 @@ export const useFindMovies = (filter: ListFilterModel) =>
GQL.useFindMoviesQuery({ GQL.useFindMoviesQuery({
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
movie_filter: filter.makeMovieFilter(), movie_filter: filter.makeFilter(),
}, },
}); });
@@ -149,7 +149,7 @@ export const queryFindMovies = (filter: ListFilterModel) =>
query: GQL.FindMoviesDocument, query: GQL.FindMoviesDocument,
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
movie_filter: filter.makeMovieFilter(), movie_filter: filter.makeFilter(),
}, },
}); });
@@ -157,7 +157,7 @@ export const useFindPerformers = (filter: ListFilterModel) =>
GQL.useFindPerformersQuery({ GQL.useFindPerformersQuery({
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
performer_filter: filter.makePerformerFilter(), performer_filter: filter.makeFilter(),
}, },
}); });
@@ -165,7 +165,7 @@ export const useFindTags = (filter: ListFilterModel) =>
GQL.useFindTagsQuery({ GQL.useFindTagsQuery({
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
tag_filter: filter.makeTagFilter(), tag_filter: filter.makeFilter(),
}, },
}); });
@@ -174,7 +174,7 @@ export const queryFindTags = (filter: ListFilterModel) =>
query: GQL.FindTagsDocument, query: GQL.FindTagsDocument,
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
tag_filter: filter.makeTagFilter(), tag_filter: filter.makeFilter(),
}, },
}); });
@@ -183,7 +183,7 @@ export const queryFindPerformers = (filter: ListFilterModel) =>
query: GQL.FindPerformersDocument, query: GQL.FindPerformersDocument,
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
performer_filter: filter.makePerformerFilter(), performer_filter: filter.makeFilter(),
}, },
}); });

View File

@@ -9,7 +9,7 @@ export const performerFilterHook = (
const performerValue = { id: performer.id!, label: performer.name! }; const performerValue = { id: performer.id!, label: performer.name! };
// if performers is already present, then we modify it, otherwise add // if performers is already present, then we modify it, otherwise add
let performerCriterion = filter.criteria.find((c) => { let performerCriterion = filter.criteria.find((c) => {
return c.type === "performers"; return c.criterionOption.value === "performers";
}) as PerformersCriterion; }) as PerformersCriterion;
if ( if (

View File

@@ -7,7 +7,7 @@ export const studioFilterHook = (studio: Partial<GQL.StudioDataFragment>) => {
const studioValue = { id: studio.id!, label: studio.name! }; const studioValue = { id: studio.id!, label: studio.name! };
// if studio is already present, then we modify it, otherwise add // if studio is already present, then we modify it, otherwise add
let studioCriterion = filter.criteria.find((c) => { let studioCriterion = filter.criteria.find((c) => {
return c.type === "studios"; return c.criterionOption.value === "studios";
}) as StudiosCriterion; }) as StudiosCriterion;
if ( if (

View File

@@ -1,5 +1,8 @@
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { TagsCriterion } from "src/models/list-filter/criteria/tags"; import {
TagsCriterion,
TagsCriterionOption,
} from "src/models/list-filter/criteria/tags";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
export const tagFilterHook = (tag: GQL.TagDataFragment) => { export const tagFilterHook = (tag: GQL.TagDataFragment) => {
@@ -7,7 +10,7 @@ export const tagFilterHook = (tag: GQL.TagDataFragment) => {
const tagValue = { id: tag.id, label: tag.name }; const tagValue = { id: tag.id, label: tag.name };
// if tag is already present, then we modify it, otherwise add // if tag is already present, then we modify it, otherwise add
let tagCriterion = filter.criteria.find((c) => { let tagCriterion = filter.criteria.find((c) => {
return c.type === "tags"; return c.criterionOption.value === "tags";
}) as TagsCriterion; }) as TagsCriterion;
if ( if (
@@ -27,7 +30,7 @@ export const tagFilterHook = (tag: GQL.TagDataFragment) => {
tagCriterion.modifier = GQL.CriterionModifier.IncludesAll; tagCriterion.modifier = GQL.CriterionModifier.IncludesAll;
} else { } else {
// overwrite // overwrite
tagCriterion = new TagsCriterion("tags"); tagCriterion = new TagsCriterion(TagsCriterionOption);
tagCriterion.value = [tagValue]; tagCriterion.value = [tagValue];
filter.criteria.push(tagCriterion); filter.criteria.push(tagCriterion);
} }

View File

@@ -44,6 +44,8 @@ import {
} from "src/core/StashService"; } from "src/core/StashService";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { FilterMode } from "src/models/list-filter/types"; import { FilterMode } from "src/models/list-filter/types";
import { ListFilterOptions } from "src/models/list-filter/filter-options";
import { getFilterOptions } from "src/models/list-filter/factory";
const getSelectedData = <I extends IDataItem>( const getSelectedData = <I extends IDataItem>(
result: I[], result: I[],
@@ -141,6 +143,7 @@ interface IQuery<T extends IQueryResult, T2 extends IDataItem> {
interface IRenderListProps { interface IRenderListProps {
filter: ListFilterModel; filter: ListFilterModel;
filterOptions: ListFilterOptions;
onChangePage: (page: number) => void; onChangePage: (page: number) => void;
updateQueryParams: (filter: ListFilterModel) => void; updateQueryParams: (filter: ListFilterModel) => void;
} }
@@ -151,6 +154,7 @@ const RenderList = <
>({ >({
defaultZoomIndex, defaultZoomIndex,
filter, filter,
filterOptions,
onChangePage, onChangePage,
addKeybinds, addKeybinds,
useData, useData,
@@ -406,6 +410,7 @@ const RenderList = <
onEdit={renderEditDialog ? onEdit : undefined} onEdit={renderEditDialog ? onEdit : undefined}
onDelete={renderDeleteDialog ? onDelete : undefined} onDelete={renderDeleteDialog ? onDelete : undefined}
filter={filter} filter={filter}
filterOptions={filterOptions}
/> />
{isEditDialogOpen && {isEditDialogOpen &&
renderEditDialog && renderEditDialog &&
@@ -432,6 +437,8 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
options: IListHookOptions<QueryResult, QueryData> & options: IListHookOptions<QueryResult, QueryData> &
IQuery<QueryResult, QueryData> IQuery<QueryResult, QueryData>
): IListHookData => { ): IListHookData => {
const filterOptions = getFilterOptions(options.filterMode);
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const [interfaceState, setInterfaceState] = useInterfaceLocalForage(); const [interfaceState, setInterfaceState] = useInterfaceLocalForage();
@@ -445,9 +452,8 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
const [filter, setFilter] = useState<ListFilterModel>( const [filter, setFilter] = useState<ListFilterModel>(
new ListFilterModel( new ListFilterModel(
options.filterMode,
queryString.parse(location.search), queryString.parse(location.search),
options.defaultSort options.defaultSort ?? filterOptions.defaultSortBy
) )
); );
@@ -509,7 +515,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
} }
: activeFilter; : activeFilter;
const newFilter = new ListFilterModel(options.filterMode, query); const newFilter = new ListFilterModel(query);
// Compare constructed filter with current filter. // Compare constructed filter with current filter.
// If different it's the result of navigation, and we update the filter. // If different it's the result of navigation, and we update the filter.
@@ -569,6 +575,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
const { contentTemplate, onSelectChange } = RenderList({ const { contentTemplate, onSelectChange } = RenderList({
...options, ...options,
filter: renderFilter, filter: renderFilter,
filterOptions,
onChangePage, onChangePage,
updateQueryParams, updateQueryParams,
}); });

View File

@@ -0,0 +1,3 @@
Use `en-GB.json` by default. This should have _all_ message IDs in it. Only add to other json files if the value is different to `en-GB` since it will fall back to use it if the message ID is not found in the chosen language.
Try to keep message IDs in alphabetical order for ease of reference.

View File

@@ -1,20 +1,85 @@
{ {
"age": "Age",
"aliases": "Aliases",
"average_resolution": "Average Resolution",
"birth_year": "Birth Year",
"birthdate": "Birthdate",
"bitrate": "Bit Rate",
"career_length": "Career Length",
"country": "Country",
"created_at": "Created At",
"criterion_modifier": {
"equals": "is",
"not_equals": "is not",
"greater_than": "is greater than",
"less_than": "is less than",
"is_null": "is null",
"not_null": "is not null",
"includes": "includes",
"includes_all": "includes all",
"excludes": "excludes",
"matches_regex": "matches regex",
"not_matches_regex": "not matches regex"
},
"date": "Date",
"death_year": "Death Year",
"developmentVersion": "Development Version", "developmentVersion": "Development Version",
"donate": "Donate",
"duration": "Duration",
"ethnicity": "Ethnicity",
"eye_color": "Eye Colour",
"fake_tits": "Fake Tits",
"favourite": "Favourite",
"file_mod_time": "File Modification Time",
"filesize": "File Size",
"framerate": "Frame Rate",
"galleries": "Galleries",
"gallery_count": "Gallery Count",
"gender": "Gender",
"hair_color": "Hair Colour",
"hasMarkers": "Has Markers",
"height": "Height",
"image_count": "Image Count",
"images": "Images", "images": "Images",
"images-size": "Images size", "images-size": "Images size",
"galleries": "Galleries", "isMissing": "Is Missing",
"interactive": "Interactive",
"library-size": "Library size", "library-size": "Library size",
"marker_count": "Marker Count",
"markers": "Markers", "markers": "Markers",
"measurements": "Measurements",
"movie_scene_number": "Movie Scene Number",
"movies": "Movies", "movies": "Movies",
"name": "Name",
"new": "New", "new": "New",
"none": "None",
"o_counter": "O-Counter",
"organized": "Organised", "organized": "Organised",
"parent_studios": "Parent Studios",
"path": "Path",
"performerTags": "Performer Tags",
"performer_count": "Performer Count",
"performers": "Performers", "performers": "Performers",
"piercings": "Piercings",
"random": "Random",
"rating": "Rating",
"resolution": "Resolution",
"sceneTagger": "Scene Tagger",
"sceneTags": "Scene Tags",
"scene_count": "Scene Count",
"scene_id": "Scene ID",
"scenes": "Scenes", "scenes": "Scenes",
"scenes-size": "Scenes size", "scenes-size": "Scenes size",
"scenes_updated_at": "Scene Updated At",
"seconds": "Seconds",
"stash_id": "Stash ID",
"studios": "Studios", "studios": "Studios",
"tag_count": "Tag Count",
"tags": "Tags", "tags": "Tags",
"tattoos": "Tattoos",
"title": "Title",
"up-dir": "Up a directory", "up-dir": "Up a directory",
"favourite": "FAVOURITE", "updated_at": "Updated At",
"sceneTagger": "Scene Tagger", "url": "URL",
"donate": "Donate" "weight": "Weight"
} }

View File

@@ -1,20 +1,6 @@
{ {
"developmentVersion": "Development Version", "eye_color": "Eye Color",
"images": "Images", "favourite": "Favorite",
"images-size": "Images size", "hair_color": "Hair Color",
"galleries": "Galleries", "organized": "Organized"
"library-size": "Library size",
"markers": "Markers",
"movies": "Movies",
"new": "New",
"organized": "Organized",
"performers": "Performers",
"scenes": "Scenes",
"scenes-size": "Scenes size",
"studios": "Studios",
"tags": "Tags",
"up-dir": "Up a directory",
"favourite": "FAVORITE",
"sceneTagger": "Scene Tagger",
"donate": "Donate"
} }

View File

@@ -1,5 +1,7 @@
{ {
"developmentVersion": "開發版本", "developmentVersion": "開發版本",
"donate": "贊助",
"favourite": "最愛",
"images": "圖片", "images": "圖片",
"images-size": "圖片大小", "images-size": "圖片大小",
"galleries": "圖庫", "galleries": "圖庫",
@@ -11,10 +13,8 @@
"performers": "演員", "performers": "演員",
"scenes": "場景", "scenes": "場景",
"scenes-size": "場景收藏大小", "scenes-size": "場景收藏大小",
"sceneTagger": "標記場景",
"studios": "工作室", "studios": "工作室",
"tags": "標籤", "tags": "標籤",
"up-dir": "往上一層", "up-dir": "往上一層"
"favourite": "最愛",
"sceneTagger": "標記場景",
"donate": "贊助"
} }

View File

@@ -1,16 +1,9 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionOption, StringCriterion } from "./criterion";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class CountryCriterion extends Criterion { const countryCriterionOption = new CriterionOption("country", "country");
public type: CriterionType = "country";
public parameterName: string = "performers";
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: string[] = [true.toString(), false.toString()];
public value: string = "";
}
export class CountryCriterionOption implements ICriterionOption { export class CountryCriterion extends StringCriterion {
public label: string = Criterion.getLabel("performers"); constructor() {
public value: CriterionType = "country"; super(countryCriterionOption);
}
} }

View File

@@ -1,164 +1,24 @@
/* eslint-disable consistent-return */ /* eslint-disable consistent-return */
import { CriterionModifier } from "src/core/generated-graphql"; import { IntlShape } from "react-intl";
import {
CriterionModifier,
MultiCriterionInput,
} from "src/core/generated-graphql";
import DurationUtils from "src/utils/duration"; import DurationUtils from "src/utils/duration";
import { ILabeledId, ILabeledValue, IOptionType } from "../types"; import {
CriterionType,
export type CriterionType = encodeILabeledId,
| "none" ILabeledId,
| "path" ILabeledValue,
| "rating" IOptionType,
| "organized" } from "../types";
| "o_counter"
| "resolution"
| "average_resolution"
| "duration"
| "favorite"
| "hasMarkers"
| "sceneIsMissing"
| "imageIsMissing"
| "performerIsMissing"
| "galleryIsMissing"
| "tagIsMissing"
| "studioIsMissing"
| "movieIsMissing"
| "tags"
| "sceneTags"
| "performerTags"
| "tag_count"
| "performers"
| "studios"
| "movies"
| "galleries"
| "birth_year"
| "age"
| "ethnicity"
| "country"
| "hair_color"
| "eye_color"
| "height"
| "weight"
| "measurements"
| "fake_tits"
| "career_length"
| "tattoos"
| "piercings"
| "aliases"
| "gender"
| "parent_studios"
| "scene_count"
| "marker_count"
| "image_count"
| "gallery_count"
| "performer_count"
| "death_year"
| "url"
| "stash_id"
| "interactive";
type Option = string | number | IOptionType; type Option = string | number | IOptionType;
export type CriterionValue = string | number | ILabeledId[]; export type CriterionValue = string | number | ILabeledId[];
export abstract class Criterion { // V = criterion value type
public static getLabel(type: CriterionType = "none") { export abstract class Criterion<V extends CriterionValue> {
switch (type) {
case "none":
return "None";
case "path":
return "Path";
case "rating":
return "Rating";
case "organized":
return "Organized";
case "o_counter":
return "O-Counter";
case "resolution":
return "Resolution";
case "average_resolution":
return "Average Resolution";
case "duration":
return "Duration";
case "favorite":
return "Favorite";
case "hasMarkers":
return "Has Markers";
case "sceneIsMissing":
case "imageIsMissing":
case "performerIsMissing":
case "galleryIsMissing":
case "tagIsMissing":
case "studioIsMissing":
case "movieIsMissing":
return "Is Missing";
case "tags":
return "Tags";
case "sceneTags":
return "Scene Tags";
case "performerTags":
return "Performer Tags";
case "tag_count":
return "Tag Count";
case "performers":
return "Performers";
case "studios":
return "Studios";
case "movies":
return "Movies";
case "galleries":
return "Galleries";
case "birth_year":
return "Birth Year";
case "death_year":
return "Death Year";
case "age":
return "Age";
case "ethnicity":
return "Ethnicity";
case "country":
return "Country";
case "hair_color":
return "Hair Color";
case "eye_color":
return "Eye Color";
case "height":
return "Height";
case "weight":
return "Weight";
case "measurements":
return "Measurements";
case "fake_tits":
return "Fake Tits";
case "career_length":
return "Career Length";
case "tattoos":
return "Tattoos";
case "piercings":
return "Piercings";
case "aliases":
return "Aliases";
case "gender":
return "Gender";
case "parent_studios":
return "Parent Studios";
case "scene_count":
return "Scene Count";
case "marker_count":
return "Marker Count";
case "image_count":
return "Image Count";
case "gallery_count":
return "Gallery Count";
case "performer_count":
return "Performer Count";
case "url":
return "URL";
case "stash_id":
return "StashID";
case "interactive":
return "Interactive";
}
}
public static getModifierOption( public static getModifierOption(
modifier: CriterionModifier = CriterionModifier.Equals modifier: CriterionModifier = CriterionModifier.Equals
): ILabeledValue { ): ILabeledValue {
@@ -194,60 +54,62 @@ export abstract class Criterion {
} }
} }
public abstract type: CriterionType; public criterionOption: CriterionOption;
public abstract parameterName: string;
public abstract modifier: CriterionModifier; public abstract modifier: CriterionModifier;
public abstract modifierOptions: ILabeledValue[]; public abstract modifierOptions: ILabeledValue[];
public abstract options: Option[] | undefined; public abstract options: Option[] | undefined;
public abstract value: CriterionValue; public abstract value: V;
public inputType: "number" | "text" | undefined; public inputType: "number" | "text" | undefined;
public getLabelValue(): string { public abstract getLabelValue(): string;
if (typeof this.value === "string") return this.value;
if (typeof this.value === "number") return this.value.toString(); constructor(type: CriterionOption) {
return this.value.map((v) => v.label).join(", "); this.criterionOption = type;
} }
public getLabel(): string { public getLabel(intl: IntlShape): string {
let modifierString: string; let modifierMessageID: string;
switch (this.modifier) { switch (this.modifier) {
case CriterionModifier.Equals: case CriterionModifier.Equals:
modifierString = "is"; modifierMessageID = "criterion_modifier.equals";
break; break;
case CriterionModifier.NotEquals: case CriterionModifier.NotEquals:
modifierString = "is not"; modifierMessageID = "criterion_modifier.not_equals";
break; break;
case CriterionModifier.GreaterThan: case CriterionModifier.GreaterThan:
modifierString = "is greater than"; modifierMessageID = "criterion_modifier.greater_than";
break; break;
case CriterionModifier.LessThan: case CriterionModifier.LessThan:
modifierString = "is less than"; modifierMessageID = "criterion_modifier.less_than";
break; break;
case CriterionModifier.IsNull: case CriterionModifier.IsNull:
modifierString = "is null"; modifierMessageID = "criterion_modifier.is_null";
break; break;
case CriterionModifier.NotNull: case CriterionModifier.NotNull:
modifierString = "is not null"; modifierMessageID = "criterion_modifier.not_null";
break; break;
case CriterionModifier.Includes: case CriterionModifier.Includes:
modifierString = "includes"; modifierMessageID = "criterion_modifier.includes";
break; break;
case CriterionModifier.IncludesAll: case CriterionModifier.IncludesAll:
modifierString = "includes all"; modifierMessageID = "criterion_modifier.includes_all";
break; break;
case CriterionModifier.Excludes: case CriterionModifier.Excludes:
modifierString = "excludes"; modifierMessageID = "criterion_modifier.excludes";
break; break;
case CriterionModifier.MatchesRegex: case CriterionModifier.MatchesRegex:
modifierString = "matches regex"; modifierMessageID = "criterion_modifier.matches_regex";
break; break;
case CriterionModifier.NotMatchesRegex: case CriterionModifier.NotMatchesRegex:
modifierString = "not matches regex"; modifierMessageID = "criterion_modifier.not_matches_regex";
break; break;
default: default:
modifierString = ""; modifierMessageID = "";
} }
const modifierString = modifierMessageID
? intl.formatMessage({ id: modifierMessageID })
: "";
let valueString = ""; let valueString = "";
if ( if (
@@ -257,47 +119,64 @@ export abstract class Criterion {
valueString = this.getLabelValue(); valueString = this.getLabelValue();
} }
return `${Criterion.getLabel(this.type)} ${modifierString} ${valueString}`; return `${intl.formatMessage({
id: this.criterionOption.messageID,
})} ${modifierString} ${valueString}`;
} }
public getId(): string { public getId(): string {
return `${this.parameterName}-${this.modifier.toString()}`; // TODO add values? return `${this.criterionOption.parameterName}-${this.modifier.toString()}`; // TODO add values?
} }
private static replaceSpecialCharacter(str: string, c: string) { public encodeValue(): V {
return str.replaceAll(c, encodeURIComponent(c));
}
public encodeValue(): CriterionValue {
// replace certain characters
if (typeof this.value === "string") {
let ret = this.value;
ret = Criterion.replaceSpecialCharacter(ret, "&");
ret = Criterion.replaceSpecialCharacter(ret, "+");
return ret;
}
return this.value; return this.value;
} }
}
export interface ICriterionOption { public toJSON() {
label: string; const encodedCriterion = {
value: CriterionType; type: this.criterionOption.value,
} // #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.
// Instead, we'll encode the criteria values.
value: this.encodeValue(),
modifier: this.modifier,
};
return JSON.stringify(encodedCriterion);
}
export class CriterionOption implements ICriterionOption { // eslint-disable-next-line @typescript-eslint/no-explicit-any
public label: string; public apply(outputFilter: Record<string, any>) {
public value: CriterionType; // eslint-disable-next-line no-param-reassign
outputFilter[this.criterionOption.parameterName] = this.toCriterionInput();
}
constructor(label: string, value: CriterionType) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
this.label = label; protected toCriterionInput(): any {
this.value = value; return {
value: this.value,
modifier: this.modifier,
};
} }
} }
export class StringCriterion extends Criterion { export class CriterionOption {
public type: CriterionType; public readonly messageID: string;
public parameterName: string; public readonly value: CriterionType;
public readonly parameterName: string;
constructor(messageID: string, value: CriterionType, parameterName?: string) {
this.messageID = messageID;
this.value = value;
this.parameterName = parameterName ?? value;
}
}
export function createCriterionOption(value: CriterionType) {
return new CriterionOption(value, value);
}
export class StringCriterion extends Criterion<string> {
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.Equals;
public modifierOptions = [ public modifierOptions = [
StringCriterion.getModifierOption(CriterionModifier.Equals), StringCriterion.getModifierOption(CriterionModifier.Equals),
@@ -316,18 +195,23 @@ export class StringCriterion extends Criterion {
return this.value; return this.value;
} }
constructor(type: CriterionType, parameterName?: string, options?: string[]) { public encodeValue() {
super(); // replace certain characters
let ret = this.value;
ret = StringCriterion.replaceSpecialCharacter(ret, "&");
ret = StringCriterion.replaceSpecialCharacter(ret, "+");
return ret;
}
private static replaceSpecialCharacter(str: string, c: string) {
return str.replaceAll(c, encodeURIComponent(c));
}
constructor(type: CriterionOption, options?: string[]) {
super(type);
this.type = type;
this.options = options; this.options = options;
this.inputType = "text"; this.inputType = "text";
if (parameterName) {
this.parameterName = parameterName;
} else {
this.parameterName = type;
}
} }
} }
@@ -342,9 +226,20 @@ export class MandatoryStringCriterion extends StringCriterion {
]; ];
} }
export class NumberCriterion extends Criterion { export class BooleanCriterion extends StringCriterion {
public type: CriterionType; public modifier = CriterionModifier.Equals;
public parameterName: string; 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 modifier = CriterionModifier.Equals;
public modifierOptions = [ public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Equals), Criterion.getModifierOption(CriterionModifier.Equals),
@@ -361,17 +256,51 @@ export class NumberCriterion extends Criterion {
return this.value.toString(); return this.value.toString();
} }
constructor(type: CriterionType, parameterName?: string, options?: number[]) { constructor(type: CriterionOption, options?: number[]) {
super(); super(type);
this.type = type;
this.options = options; this.options = options;
this.inputType = "number"; this.inputType = "number";
}
}
if (parameterName) { export abstract class ILabeledIdCriterion extends Criterion<ILabeledId[]> {
this.parameterName = parameterName; public modifier = CriterionModifier.IncludesAll;
} else { public modifierOptions = [
this.parameterName = type; Criterion.getModifierOption(CriterionModifier.IncludesAll),
Criterion.getModifierOption(CriterionModifier.Includes),
Criterion.getModifierOption(CriterionModifier.Excludes),
];
public options: IOptionType[] = [];
public value: ILabeledId[] = [];
public getLabelValue(): string {
return this.value.map((v) => v.label).join(", ");
}
protected toCriterionInput(): MultiCriterionInput {
return {
value: this.value.map((v) => v.id),
modifier: this.modifier,
};
}
public encodeValue() {
return this.value.map((o) => {
return encodeILabeledId(o);
});
}
constructor(type: CriterionOption, includeAll: boolean) {
super(type);
if (!includeAll) {
this.modifier = CriterionModifier.Includes;
this.modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Includes),
Criterion.getModifierOption(CriterionModifier.Excludes),
];
} }
} }
} }
@@ -385,9 +314,7 @@ export class MandatoryNumberCriterion extends NumberCriterion {
]; ];
} }
export class DurationCriterion extends Criterion { export class DurationCriterion extends Criterion<number> {
public type: CriterionType;
public parameterName: string;
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.Equals;
public modifierOptions = [ public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Equals), Criterion.getModifierOption(CriterionModifier.Equals),
@@ -398,12 +325,10 @@ export class DurationCriterion extends Criterion {
public options: number[] | undefined; public options: number[] | undefined;
public value: number = 0; public value: number = 0;
constructor(type: CriterionType, parameterName?: string, options?: number[]) { constructor(type: CriterionOption, options?: number[]) {
super(); super(type);
this.type = type;
this.options = options; this.options = options;
this.parameterName = parameterName ?? type;
} }
public getLabelValue() { public getLabelValue() {

View File

@@ -1,11 +1,11 @@
/* eslint-disable consistent-return, default-case */ /* eslint-disable consistent-return, default-case */
import { import {
CriterionType,
StringCriterion, StringCriterion,
NumberCriterion, NumberCriterion,
DurationCriterion, DurationCriterion,
MandatoryStringCriterion, MandatoryStringCriterion,
MandatoryNumberCriterion, MandatoryNumberCriterion,
CriterionOption,
} from "./criterion"; } from "./criterion";
import { OrganizedCriterion } from "./organized"; import { OrganizedCriterion } from "./organized";
import { FavoriteCriterion } from "./favorite"; import { FavoriteCriterion } from "./favorite";
@@ -24,10 +24,16 @@ import { PerformersCriterion } from "./performers";
import { RatingCriterion } from "./rating"; import { RatingCriterion } from "./rating";
import { AverageResolutionCriterion, ResolutionCriterion } from "./resolution"; import { AverageResolutionCriterion, ResolutionCriterion } from "./resolution";
import { StudiosCriterion, ParentStudiosCriterion } from "./studios"; import { StudiosCriterion, ParentStudiosCriterion } from "./studios";
import { TagsCriterion } from "./tags"; import {
PerformerTagsCriterionOption,
SceneTagsCriterionOption,
TagsCriterion,
TagsCriterionOption,
} from "./tags";
import { GenderCriterion } from "./gender"; import { GenderCriterion } from "./gender";
import { MoviesCriterion } from "./movies"; import { MoviesCriterion } from "./movies";
import { GalleriesCriterion } from "./galleries"; import { GalleriesCriterion } from "./galleries";
import { CriterionType } from "../types";
import { InteractiveCriterion } from "./interactive"; import { InteractiveCriterion } from "./interactive";
export function makeCriteria(type: CriterionType = "none") { export function makeCriteria(type: CriterionType = "none") {
@@ -35,7 +41,7 @@ export function makeCriteria(type: CriterionType = "none") {
case "none": case "none":
return new NoneCriterion(); return new NoneCriterion();
case "path": case "path":
return new MandatoryStringCriterion(type, type); return new MandatoryStringCriterion(new CriterionOption(type, type));
case "rating": case "rating":
return new RatingCriterion(); return new RatingCriterion();
case "organized": case "organized":
@@ -47,13 +53,13 @@ export function makeCriteria(type: CriterionType = "none") {
case "gallery_count": case "gallery_count":
case "performer_count": case "performer_count":
case "tag_count": case "tag_count":
return new MandatoryNumberCriterion(type, type); return new MandatoryNumberCriterion(new CriterionOption(type, type));
case "resolution": case "resolution":
return new ResolutionCriterion(); return new ResolutionCriterion();
case "average_resolution": case "average_resolution":
return new AverageResolutionCriterion(); return new AverageResolutionCriterion();
case "duration": case "duration":
return new DurationCriterion(type, type); return new DurationCriterion(new CriterionOption(type, type));
case "favorite": case "favorite":
return new FavoriteCriterion(); return new FavoriteCriterion();
case "hasMarkers": case "hasMarkers":
@@ -73,11 +79,11 @@ export function makeCriteria(type: CriterionType = "none") {
case "movieIsMissing": case "movieIsMissing":
return new MovieIsMissingCriterion(); return new MovieIsMissingCriterion();
case "tags": case "tags":
return new TagsCriterion("tags"); return new TagsCriterion(TagsCriterionOption);
case "sceneTags": case "sceneTags":
return new TagsCriterion("sceneTags"); return new TagsCriterion(SceneTagsCriterionOption);
case "performerTags": case "performerTags":
return new TagsCriterion("performerTags"); return new TagsCriterion(PerformerTagsCriterionOption);
case "performers": case "performers":
return new PerformersCriterion(); return new PerformersCriterion();
case "studios": case "studios":
@@ -91,9 +97,9 @@ export function makeCriteria(type: CriterionType = "none") {
case "birth_year": case "birth_year":
case "death_year": case "death_year":
case "weight": case "weight":
return new NumberCriterion(type, type); return new NumberCriterion(new CriterionOption(type, type));
case "age": case "age":
return new MandatoryNumberCriterion(type, type); return new MandatoryNumberCriterion(new CriterionOption(type, type));
case "gender": case "gender":
return new GenderCriterion(); return new GenderCriterion();
case "ethnicity": case "ethnicity":
@@ -109,7 +115,7 @@ export function makeCriteria(type: CriterionType = "none") {
case "aliases": case "aliases":
case "url": case "url":
case "stash_id": case "stash_id":
return new StringCriterion(type, type); return new StringCriterion(new CriterionOption(type, type));
case "interactive": case "interactive":
return new InteractiveCriterion(); return new InteractiveCriterion();
} }

View File

@@ -1,16 +1,13 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { BooleanCriterion, CriterionOption } from "./criterion";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class FavoriteCriterion extends Criterion { export const FavoriteCriterionOption = new CriterionOption(
public type: CriterionType = "favorite"; "favourite",
public parameterName: string = "filter_favorites"; "favorite",
public modifier = CriterionModifier.Equals; "filter_favorites"
public modifierOptions = []; );
public options: string[] = [true.toString(), false.toString()];
public value: string = "";
}
export class FavoriteCriterionOption implements ICriterionOption { export class FavoriteCriterion extends BooleanCriterion {
public label: string = Criterion.getLabel("favorite"); constructor() {
public value: CriterionType = "favorite"; super(FavoriteCriterionOption);
}
} }

View File

@@ -1,27 +1,9 @@
import * as GQL from "src/core/generated-graphql"; import { CriterionOption, ILabeledIdCriterion } from "./criterion";
import { ILabeledId, IOptionType, encodeILabeledId } from "../types";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class GalleriesCriterion extends Criterion { const galleriesCriterionOption = new CriterionOption("galleries", "galleries");
public type: CriterionType = "galleries";
public parameterName: string = "galleries";
public modifier = GQL.CriterionModifier.IncludesAll;
public modifierOptions = [
Criterion.getModifierOption(GQL.CriterionModifier.IncludesAll),
Criterion.getModifierOption(GQL.CriterionModifier.Includes),
Criterion.getModifierOption(GQL.CriterionModifier.Excludes),
];
public options: IOptionType[] = [];
public value: ILabeledId[] = [];
public encodeValue() { export class GalleriesCriterion extends ILabeledIdCriterion {
return this.value.map((o) => { constructor() {
return encodeILabeledId(o); super(galleriesCriterionOption, true);
});
} }
} }
export class GalleriesCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("galleries");
public value: CriterionType = "galleries";
}

View File

@@ -1,17 +1,24 @@
import { CriterionModifier } from "src/core/generated-graphql"; import {
import { getGenderStrings } from "src/core/StashService"; CriterionModifier,
import { Criterion, CriterionType, ICriterionOption } from "./criterion"; GenderCriterionInput,
} from "src/core/generated-graphql";
import { getGenderStrings, stringToGender } from "src/core/StashService";
import { CriterionOption, StringCriterion } from "./criterion";
export class GenderCriterion extends Criterion { export const GenderCriterionOption = new CriterionOption("gender", "gender");
public type: CriterionType = "gender";
public parameterName: string = "gender"; export class GenderCriterion extends StringCriterion {
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.Equals;
public modifierOptions = []; public modifierOptions = [];
public options: string[] = getGenderStrings();
public value: string = "";
}
export class GenderCriterionOption implements ICriterionOption { constructor() {
public label: string = Criterion.getLabel("gender"); super(GenderCriterionOption, getGenderStrings());
public value: CriterionType = "gender"; }
protected toCriterionInput(): GenderCriterionInput {
return {
value: stringToGender(this.value),
modifier: this.modifier,
};
}
} }

View File

@@ -1,16 +1,17 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionOption, StringCriterion } from "./criterion";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class HasMarkersCriterion extends Criterion { export const HasMarkersCriterionOption = new CriterionOption(
public type: CriterionType = "hasMarkers"; "hasMarkers",
public parameterName: string = "has_markers"; "hasMarkers",
public modifier = CriterionModifier.Equals; "has_markers"
public modifierOptions = []; );
public options: string[] = [true.toString(), false.toString()];
public value: string = "";
}
export class HasMarkersCriterionOption implements ICriterionOption { export class HasMarkersCriterion extends StringCriterion {
public label: string = Criterion.getLabel("hasMarkers"); constructor() {
public value: CriterionType = "hasMarkers"; super(HasMarkersCriterionOption, [true.toString(), false.toString()]);
}
protected toCriterionInput(): string {
return this.value;
}
} }

View File

@@ -1,16 +1,12 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { BooleanCriterion, CriterionOption } from "./criterion";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class InteractiveCriterion extends Criterion { export const InteractiveCriterionOption = new CriterionOption(
public type: CriterionType = "interactive"; "organized",
public parameterName: string = "interactive"; "organized"
public modifier = CriterionModifier.Equals; );
public modifierOptions = [];
public options: string[] = [true.toString(), false.toString()];
public value: string = "";
}
export class InteractiveCriterionOption implements ICriterionOption { export class InteractiveCriterion extends BooleanCriterion {
public label: string = Criterion.getLabel("interactive"); constructor() {
public value: CriterionType = "interactive"; super(InteractiveCriterionOption);
}
} }

View File

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

View File

@@ -1,32 +1,9 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionOption, ILabeledIdCriterion } from "./criterion";
import { ILabeledId, encodeILabeledId } from "../types";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
interface IOptionType { export const MoviesCriterionOption = new CriterionOption("movies", "movies");
id: string;
name?: string;
image_path?: string;
}
export class MoviesCriterion extends Criterion { export class MoviesCriterion extends ILabeledIdCriterion {
public type: CriterionType = "movies"; constructor() {
public parameterName: string = "movies"; super(MoviesCriterionOption, false);
public modifier = CriterionModifier.Includes;
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Includes),
Criterion.getModifierOption(CriterionModifier.Excludes),
];
public options: IOptionType[] = [];
public value: ILabeledId[] = [];
public encodeValue() {
return this.value.map((o) => {
return encodeILabeledId(o);
});
} }
} }
export class MoviesCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("movies");
public value: CriterionType = "movies";
}

View File

@@ -1,16 +1,19 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { Criterion, CriterionType, ICriterionOption } from "./criterion"; import { Criterion, CriterionOption } from "./criterion";
export class NoneCriterion extends Criterion { export const NoneCriterionOption = new CriterionOption("none", "none");
public type: CriterionType = "none"; export class NoneCriterion extends Criterion<string> {
public parameterName: string = "";
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.Equals;
public modifierOptions = []; public modifierOptions = [];
public options: undefined; public options: undefined;
public value: string = "none"; public value: string = "none";
}
export class NoneCriterionOption implements ICriterionOption { constructor() {
public label: string = Criterion.getLabel("none"); super(NoneCriterionOption);
public value: CriterionType = "none"; }
// eslint-disable-next-line class-methods-use-this
public getLabelValue(): string {
return "";
}
} }

View File

@@ -1,16 +1,12 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { BooleanCriterion, CriterionOption } from "./criterion";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class OrganizedCriterion extends Criterion { export const OrganizedCriterionOption = new CriterionOption(
public type: CriterionType = "organized"; "organized",
public parameterName: string = "organized"; "organized"
public modifier = CriterionModifier.Equals; );
public modifierOptions = [];
public options: string[] = [true.toString(), false.toString()];
public value: string = "";
}
export class OrganizedCriterionOption implements ICriterionOption { export class OrganizedCriterion extends BooleanCriterion {
public label: string = Criterion.getLabel("organized"); constructor() {
public value: CriterionType = "organized"; super(OrganizedCriterionOption);
}
} }

View File

@@ -1,27 +1,12 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionOption, ILabeledIdCriterion } from "./criterion";
import { ILabeledId, IOptionType, encodeILabeledId } from "../types";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class PerformersCriterion extends Criterion { export const PerformersCriterionOption = new CriterionOption(
public type: CriterionType = "performers"; "performers",
public parameterName: string = "performers"; "performers"
public modifier = CriterionModifier.IncludesAll; );
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.IncludesAll),
Criterion.getModifierOption(CriterionModifier.Includes),
Criterion.getModifierOption(CriterionModifier.Excludes),
];
public options: IOptionType[] = [];
public value: ILabeledId[] = [];
public encodeValue() { export class PerformersCriterion extends ILabeledIdCriterion {
return this.value.map((o) => { constructor() {
return encodeILabeledId(o); super(PerformersCriterionOption, true);
});
} }
} }
export class PerformersCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("performers");
public value: CriterionType = "performers";
}

View File

@@ -1,9 +1,9 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { Criterion, CriterionType, ICriterionOption } from "./criterion"; import { Criterion, CriterionOption, NumberCriterion } from "./criterion";
export class RatingCriterion extends Criterion { export const RatingCriterionOption = new CriterionOption("rating", "rating");
public type: CriterionType = "rating";
public parameterName: string = "rating"; export class RatingCriterion extends NumberCriterion {
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.Equals;
public modifierOptions = [ public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Equals), Criterion.getModifierOption(CriterionModifier.Equals),
@@ -13,11 +13,8 @@ export class RatingCriterion extends Criterion {
Criterion.getModifierOption(CriterionModifier.IsNull), Criterion.getModifierOption(CriterionModifier.IsNull),
Criterion.getModifierOption(CriterionModifier.NotNull), Criterion.getModifierOption(CriterionModifier.NotNull),
]; ];
public options: number[] = [1, 2, 3, 4, 5];
public value: number = 0;
}
export class RatingCriterionOption implements ICriterionOption { constructor() {
public label: string = Criterion.getLabel("rating"); super(RatingCriterionOption, [1, 2, 3, 4, 5]);
public value: CriterionType = "rating"; }
} }

View File

@@ -1,39 +1,80 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier, ResolutionEnum } from "src/core/generated-graphql";
import { Criterion, CriterionType, ICriterionOption } from "./criterion"; import { CriterionOption, StringCriterion } from "./criterion";
export class ResolutionCriterion extends Criterion { abstract class AbstractResolutionCriterion extends StringCriterion {
public type: CriterionType = "resolution";
public parameterName: string = "resolution";
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.Equals;
public modifierOptions = []; public modifierOptions = [];
public options: string[] = [
"144p", constructor(type: CriterionOption) {
"240p", super(type, [
"360p", "144p",
"480p", "240p",
"540p", "360p",
"720p", "480p",
"1080p", "540p",
"1440p", "720p",
"4k", "1080p",
"5k", "1440p",
"6k", "4k",
"8k", "5k",
]; "6k",
public value: string = ""; "8k",
]);
}
protected toCriterionInput(): ResolutionEnum | undefined {
switch (this.value) {
case "144p":
return ResolutionEnum.VeryLow;
case "240p":
return ResolutionEnum.Low;
case "360p":
return ResolutionEnum.R360P;
case "480p":
return ResolutionEnum.Standard;
case "540p":
return ResolutionEnum.WebHd;
case "720p":
return ResolutionEnum.StandardHd;
case "1080p":
return ResolutionEnum.FullHd;
case "1440p":
return ResolutionEnum.QuadHd;
case "1920p":
return ResolutionEnum.VrHd;
case "4k":
return ResolutionEnum.FourK;
case "5k":
return ResolutionEnum.FiveK;
case "6k":
return ResolutionEnum.SixK;
case "8k":
return ResolutionEnum.EightK;
// no default
}
}
} }
export class ResolutionCriterionOption implements ICriterionOption { export const ResolutionCriterionOption = new CriterionOption(
public label: string = Criterion.getLabel("resolution"); "resolution",
public value: CriterionType = "resolution"; "resolution"
);
export class ResolutionCriterion extends AbstractResolutionCriterion {
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
constructor() {
super(ResolutionCriterionOption);
}
} }
export class AverageResolutionCriterion extends ResolutionCriterion { export const AverageResolutionCriterionOption = new CriterionOption(
public type: CriterionType = "average_resolution"; "average_resolution",
public parameterName: string = "average_resolution"; "average_resolution"
} );
export class AverageResolutionCriterionOption extends ResolutionCriterionOption { export class AverageResolutionCriterion extends AbstractResolutionCriterion {
public label: string = Criterion.getLabel("average_resolution"); constructor() {
public value: CriterionType = "average_resolution"; super(AverageResolutionCriterionOption);
}
} }

View File

@@ -1,36 +1,25 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionOption, ILabeledIdCriterion } from "./criterion";
import { ILabeledId, IOptionType, encodeILabeledId } from "../types";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class StudiosCriterion extends Criterion { abstract class AbstractStudiosCriterion extends ILabeledIdCriterion {
public type: CriterionType = "studios"; constructor(type: CriterionOption) {
public parameterName: string = "studios"; super(type, false);
public modifier = CriterionModifier.Includes;
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Includes),
Criterion.getModifierOption(CriterionModifier.Excludes),
];
public options: IOptionType[] = [];
public value: ILabeledId[] = [];
public encodeValue() {
return this.value.map((o) => {
return encodeILabeledId(o);
});
} }
} }
export class StudiosCriterionOption implements ICriterionOption { export const StudiosCriterionOption = new CriterionOption("studios", "studios");
public label: string = Criterion.getLabel("studios"); export class StudiosCriterion extends AbstractStudiosCriterion {
public value: CriterionType = "studios"; constructor() {
super(StudiosCriterionOption);
}
} }
export class ParentStudiosCriterion extends StudiosCriterion { export const ParentStudiosCriterionOption = new CriterionOption(
public type: CriterionType = "parent_studios"; "parent_studios",
public parameterName: string = "parents"; "parent_studios",
} "parents"
);
export class ParentStudiosCriterionOption implements ICriterionOption { export class ParentStudiosCriterion extends AbstractStudiosCriterion {
public label: string = Criterion.getLabel("parent_studios"); constructor() {
public value: CriterionType = "parent_studios"; super(ParentStudiosCriterionOption);
}
} }

View File

@@ -1,49 +1,19 @@
import * as GQL from "src/core/generated-graphql"; import { CriterionOption, ILabeledIdCriterion } from "./criterion";
import { ILabeledId, IOptionType, encodeILabeledId } from "../types";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class TagsCriterion extends Criterion { export class TagsCriterion extends ILabeledIdCriterion {
public type: CriterionType; constructor(type: CriterionOption) {
public parameterName: string; super(type, true);
public modifier = GQL.CriterionModifier.IncludesAll;
public modifierOptions = [
Criterion.getModifierOption(GQL.CriterionModifier.IncludesAll),
Criterion.getModifierOption(GQL.CriterionModifier.Includes),
Criterion.getModifierOption(GQL.CriterionModifier.Excludes),
];
public options: IOptionType[] = [];
public value: ILabeledId[] = [];
constructor(type: "tags" | "sceneTags" | "performerTags") {
super();
this.type = type;
this.parameterName = type;
if (type === "sceneTags") {
this.parameterName = "scene_tags";
}
if (type === "performerTags") {
this.parameterName = "performer_tags";
}
}
public encodeValue() {
return this.value.map((o) => {
return encodeILabeledId(o);
});
} }
} }
export class TagsCriterionOption implements ICriterionOption { export const TagsCriterionOption = new CriterionOption("tags", "tags");
public label: string = Criterion.getLabel("tags"); export const SceneTagsCriterionOption = new CriterionOption(
public value: CriterionType = "tags"; "sceneTags",
} "sceneTags",
"scene_tags"
export class SceneTagsCriterionOption implements ICriterionOption { );
public label: string = Criterion.getLabel("sceneTags"); export const PerformerTagsCriterionOption = new CriterionOption(
public value: CriterionType = "sceneTags"; "performerTags",
} "performerTags",
"performer_tags"
export class PerformerTagsCriterionOption implements ICriterionOption { );
public label: string = Criterion.getLabel("performerTags");
public value: CriterionType = "performerTags";
}

View File

@@ -0,0 +1,31 @@
import { ListFilterOptions } from "./filter-options";
import { GalleryListFilterOptions } from "./galleries";
import { ImageListFilterOptions } from "./images";
import { MovieListFilterOptions } from "./movies";
import { PerformerListFilterOptions } from "./performers";
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) {
case FilterMode.Scenes:
return SceneListFilterOptions;
case FilterMode.Performers:
return PerformerListFilterOptions;
case FilterMode.Studios:
return StudioListFilterOptions;
case FilterMode.Galleries:
return GalleryListFilterOptions;
case FilterMode.SceneMarkers:
return SceneMarkerListFilterOptions;
case FilterMode.Movies:
return MovieListFilterOptions;
case FilterMode.Tags:
return TagListFilterOptions;
case FilterMode.Images:
return ImageListFilterOptions;
}
}

View File

@@ -0,0 +1,37 @@
import { CriterionOption } from "./criteria/criterion";
import { DisplayMode } from "./types";
interface ISortByOption {
messageID: string;
value: string;
}
export class ListFilterOptions {
public readonly defaultSortBy: string = "";
public readonly sortByOptions: ISortByOption[] = [];
public readonly displayModeOptions: DisplayMode[] = [];
public readonly criterionOptions: CriterionOption[] = [];
public static createSortBy(value: string) {
return {
messageID: value,
value,
};
}
constructor(
defaultSortBy: string,
sortByOptions: ISortByOption[],
displayModeOptions: DisplayMode[],
criterionOptions: CriterionOption[]
) {
this.defaultSortBy = defaultSortBy;
this.sortByOptions = [
...sortByOptions,
ListFilterOptions.createSortBy("created_at"),
ListFilterOptions.createSortBy("updated_at"),
];
this.displayModeOptions = displayModeOptions;
this.criterionOptions = criterionOptions;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,63 @@
import { createCriterionOption } 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";
import { AverageResolutionCriterionOption } from "./criteria/resolution";
import { StudiosCriterionOption } from "./criteria/studios";
import {
PerformerTagsCriterionOption,
TagsCriterionOption,
} from "./criteria/tags";
import { ListFilterOptions } from "./filter-options";
import { DisplayMode } from "./types";
const defaultSortBy = "path";
const sortByOptions = [
"date",
"path",
"file_mod_time",
"tag_count",
"performer_count",
"title",
"random",
]
.map(ListFilterOptions.createSortBy)
.concat([
{
messageID: "image_count",
value: "images_count",
},
]);
const displayModeOptions = [
DisplayMode.Grid,
DisplayMode.List,
DisplayMode.Wall,
];
const criterionOptions = [
NoneCriterionOption,
createCriterionOption("path"),
RatingCriterionOption,
OrganizedCriterionOption,
AverageResolutionCriterionOption,
GalleryIsMissingCriterionOption,
TagsCriterionOption,
createCriterionOption("tag_count"),
PerformerTagsCriterionOption,
PerformersCriterionOption,
createCriterionOption("performer_count"),
createCriterionOption("image_count"),
StudiosCriterionOption,
createCriterionOption("url"),
];
export const GalleryListFilterOptions = new ListFilterOptions(
defaultSortBy,
sortByOptions,
displayModeOptions,
criterionOptions
);

View File

@@ -0,0 +1,51 @@
import { createCriterionOption } 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";
import { ResolutionCriterionOption } from "./criteria/resolution";
import { StudiosCriterionOption } from "./criteria/studios";
import {
PerformerTagsCriterionOption,
TagsCriterionOption,
} from "./criteria/tags";
import { ListFilterOptions } from "./filter-options";
import { DisplayMode } from "./types";
const defaultSortBy = "path";
const sortByOptions = [
"title",
"path",
"rating",
"o_counter",
"filesize",
"file_mod_time",
"tag_count",
"performer_count",
"random",
].map(ListFilterOptions.createSortBy);
const displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall];
const criterionOptions = [
NoneCriterionOption,
createCriterionOption("path"),
RatingCriterionOption,
OrganizedCriterionOption,
createCriterionOption("o_counter"),
ResolutionCriterionOption,
ImageIsMissingCriterionOption,
TagsCriterionOption,
createCriterionOption("tag_count"),
PerformerTagsCriterionOption,
PerformersCriterionOption,
createCriterionOption("performer_count"),
StudiosCriterionOption,
];
export const ImageListFilterOptions = new ListFilterOptions(
defaultSortBy,
sortByOptions,
displayModeOptions,
criterionOptions
);

View File

@@ -0,0 +1,31 @@
import { createCriterionOption } 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";
const defaultSortBy = "name";
const sortByOptions = ["name", "random"]
.map(ListFilterOptions.createSortBy)
.concat([
{
messageID: "scene_count",
value: "scenes_count",
},
]);
const displayModeOptions = [DisplayMode.Grid];
const criterionOptions = [
NoneCriterionOption,
StudiosCriterionOption,
MovieIsMissingCriterionOption,
createCriterionOption("url"),
];
export const MovieListFilterOptions = new ListFilterOptions(
defaultSortBy,
sortByOptions,
displayModeOptions,
criterionOptions
);

View File

@@ -0,0 +1,77 @@
import { createCriterionOption } 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";
import { ListFilterOptions } from "./filter-options";
import { CriterionType, DisplayMode } from "./types";
const defaultSortBy = "name";
const sortByOptions = [
"name",
"height",
"birthdate",
"tag_count",
"random",
"rating",
]
.map(ListFilterOptions.createSortBy)
.concat([
{
messageID: "scene_count",
value: "scenes_count",
},
]);
const displayModeOptions = [
DisplayMode.Grid,
DisplayMode.List,
DisplayMode.Tagger,
];
const numberCriteria: CriterionType[] = [
"birth_year",
"death_year",
"age",
"weight",
];
const stringCriteria: CriterionType[] = [
"ethnicity",
"country",
"hair_color",
"eye_color",
"height",
"measurements",
"fake_tits",
"career_length",
"tattoos",
"piercings",
"aliases",
"stash_id",
];
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)),
];
export const PerformerListFilterOptions = new ListFilterOptions(
defaultSortBy,
sortByOptions,
displayModeOptions,
criterionOptions
);

View File

@@ -0,0 +1,28 @@
import { NoneCriterionOption } from "./criteria/none";
import { PerformersCriterionOption } from "./criteria/performers";
import { SceneTagsCriterionOption, TagsCriterionOption } from "./criteria/tags";
import { ListFilterOptions } from "./filter-options";
import { DisplayMode } from "./types";
const defaultSortBy = "title";
const sortByOptions = [
"title",
"seconds",
"scene_id",
"random",
"scenes_updated_at",
].map(ListFilterOptions.createSortBy);
const displayModeOptions = [DisplayMode.Wall];
const criterionOptions = [
NoneCriterionOption,
TagsCriterionOption,
SceneTagsCriterionOption,
PerformersCriterionOption,
];
export const SceneMarkerListFilterOptions = new ListFilterOptions(
defaultSortBy,
sortByOptions,
displayModeOptions,
criterionOptions
);

View File

@@ -0,0 +1,73 @@
import { createCriterionOption } 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";
import { ResolutionCriterionOption } from "./criteria/resolution";
import { StudiosCriterionOption } from "./criteria/studios";
import { InteractiveCriterionOption } from "./criteria/interactive";
import {
PerformerTagsCriterionOption,
TagsCriterionOption,
} from "./criteria/tags";
import { ListFilterOptions } from "./filter-options";
import { DisplayMode } from "./types";
const defaultSortBy = "date";
const sortByOptions = [
"title",
"path",
"rating",
"organized",
"o_counter",
"date",
"filesize",
"file_mod_time",
"duration",
"framerate",
"bitrate",
"tag_count",
"performer_count",
"random",
"movie_scene_number",
"interactive",
].map(ListFilterOptions.createSortBy);
const displayModeOptions = [
DisplayMode.Grid,
DisplayMode.List,
DisplayMode.Wall,
DisplayMode.Tagger,
];
const criterionOptions = [
NoneCriterionOption,
createCriterionOption("path"),
RatingCriterionOption,
OrganizedCriterionOption,
createCriterionOption("o_counter"),
ResolutionCriterionOption,
createCriterionOption("duration"),
HasMarkersCriterionOption,
SceneIsMissingCriterionOption,
TagsCriterionOption,
createCriterionOption("tag_count"),
PerformerTagsCriterionOption,
PerformersCriterionOption,
createCriterionOption("performer_count"),
StudiosCriterionOption,
MoviesCriterionOption,
createCriterionOption("url"),
createCriterionOption("stash_id"),
InteractiveCriterionOption,
];
export const SceneListFilterOptions = new ListFilterOptions(
defaultSortBy,
sortByOptions,
displayModeOptions,
criterionOptions
);

View File

@@ -0,0 +1,45 @@
import { createCriterionOption } 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";
import { DisplayMode } from "./types";
const defaultSortBy = "name";
const sortByOptions = ["name", "random", "rating"]
.map(ListFilterOptions.createSortBy)
.concat([
{
messageID: "gallery_count",
value: "galleries_count",
},
{
messageID: "image_count",
value: "images_count",
},
{
messageID: "scene_count",
value: "scenes_count",
},
]);
const displayModeOptions = [DisplayMode.Grid];
const criterionOptions = [
NoneCriterionOption,
ParentStudiosCriterionOption,
StudioIsMissingCriterionOption,
RatingCriterionOption,
createCriterionOption("scene_count"),
createCriterionOption("image_count"),
createCriterionOption("gallery_count"),
createCriterionOption("url"),
createCriterionOption("stash_id"),
];
export const StudioListFilterOptions = new ListFilterOptions(
defaultSortBy,
sortByOptions,
displayModeOptions,
criterionOptions
);

View File

@@ -0,0 +1,52 @@
import { createCriterionOption } from "./criteria/criterion";
import { TagIsMissingCriterionOption } from "./criteria/is-missing";
import { NoneCriterionOption } from "./criteria/none";
import { ListFilterOptions } from "./filter-options";
import { DisplayMode } from "./types";
const defaultSortBy = "name";
// scene markers count has been disabled for now due to performance
// issues
const sortByOptions = [
"name",
"random",
/* "scene_markers_count" */
]
.map(ListFilterOptions.createSortBy)
.concat([
{
messageID: "gallery_count",
value: "galleries_count",
},
{
messageID: "image_count",
value: "images_count",
},
{
messageID: "performer_count",
value: "performers_count",
},
{
messageID: "scene_count",
value: "scenes_count",
},
]);
const displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
const criterionOptions = [
NoneCriterionOption,
TagIsMissingCriterionOption,
createCriterionOption("scene_count"),
createCriterionOption("image_count"),
createCriterionOption("gallery_count"),
createCriterionOption("performer_count"),
// marker count has been disabled for now due to performance issues
// ListFilterModel.createCriterionOption("marker_count"),
];
export const TagListFilterOptions = new ListFilterOptions(
defaultSortBy,
sortByOptions,
displayModeOptions,
criterionOptions
);

View File

@@ -1,4 +1,5 @@
// NOTE: add new enum values to the end, to ensure existing data // NOTE: add new enum values to the end, to ensure existing data
// is not impacted // is not impacted
export enum DisplayMode { export enum DisplayMode {
Grid, Grid,
@@ -39,3 +40,55 @@ export interface IOptionType {
name?: string; name?: string;
image_path?: string; image_path?: string;
} }
export type CriterionType =
| "none"
| "path"
| "rating"
| "organized"
| "o_counter"
| "resolution"
| "average_resolution"
| "duration"
| "favorite"
| "hasMarkers"
| "sceneIsMissing"
| "imageIsMissing"
| "performerIsMissing"
| "galleryIsMissing"
| "tagIsMissing"
| "studioIsMissing"
| "movieIsMissing"
| "tags"
| "sceneTags"
| "performerTags"
| "tag_count"
| "performers"
| "studios"
| "movies"
| "galleries"
| "birth_year"
| "age"
| "ethnicity"
| "country"
| "hair_color"
| "eye_color"
| "height"
| "weight"
| "measurements"
| "fake_tits"
| "career_length"
| "tattoos"
| "piercings"
| "aliases"
| "gender"
| "parent_studios"
| "scene_count"
| "marker_count"
| "image_count"
| "gallery_count"
| "performer_count"
| "death_year"
| "url"
| "stash_id"
| "interactive";

View File

@@ -1,7 +1,7 @@
import queryString from "query-string"; import queryString from "query-string";
import { RouteComponentProps } from "react-router-dom"; import { RouteComponentProps } from "react-router-dom";
import { ListFilterModel } from "./list-filter/filter"; import { ListFilterModel } from "./list-filter/filter";
import { FilterMode } from "./list-filter/types"; import { SceneListFilterOptions } from "./list-filter/scenes";
interface IQueryParameters { interface IQueryParameters {
qsort?: string; qsort?: string;
@@ -27,10 +27,7 @@ export class SceneQueue {
public static fromListFilterModel(filter: ListFilterModel) { public static fromListFilterModel(filter: ListFilterModel) {
const ret = new SceneQueue(); const ret = new SceneQueue();
const filterCopy = Object.assign( const filterCopy = Object.assign(new ListFilterModel(), filter);
new ListFilterModel(filter.filterMode),
filter
);
filterCopy.itemsPerPage = 40; filterCopy.itemsPerPage = 40;
ret.originalQueryPage = filter.currentPage; ret.originalQueryPage = filter.currentPage;
@@ -98,8 +95,8 @@ export class SceneQueue {
if (parsed.qfp) { if (parsed.qfp) {
const query = new ListFilterModel( const query = new ListFilterModel(
FilterMode.Scenes, translated as queryString.ParsedQuery,
translated as queryString.ParsedQuery SceneListFilterOptions.defaultSortBy
); );
ret.query = query; ret.query = query;
} else if (parsed.qs) { } else if (parsed.qs) {

View File

@@ -5,13 +5,21 @@ import {
StudiosCriterion, StudiosCriterion,
ParentStudiosCriterion, ParentStudiosCriterion,
} from "src/models/list-filter/criteria/studios"; } from "src/models/list-filter/criteria/studios";
import { TagsCriterion } from "src/models/list-filter/criteria/tags"; import {
TagsCriterion,
TagsCriterionOption,
} from "src/models/list-filter/criteria/tags";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { FilterMode } from "src/models/list-filter/types";
import { MoviesCriterion } from "src/models/list-filter/criteria/movies"; import { MoviesCriterion } from "src/models/list-filter/criteria/movies";
import { Criterion } from "src/models/list-filter/criteria/criterion"; import {
Criterion,
CriterionValue,
} from "src/models/list-filter/criteria/criterion";
function addExtraCriteria(dest: Criterion[], src?: Criterion[]) { function addExtraCriteria(
dest: Criterion<CriterionValue>[],
src?: Criterion<CriterionValue>[]
) {
if (src && src.length > 0) { if (src && src.length > 0) {
dest.push(...src); dest.push(...src);
} }
@@ -19,10 +27,10 @@ function addExtraCriteria(dest: Criterion[], src?: Criterion[]) {
const makePerformerScenesUrl = ( const makePerformerScenesUrl = (
performer: Partial<GQL.PerformerDataFragment>, performer: Partial<GQL.PerformerDataFragment>,
extraCriteria?: Criterion[] extraCriteria?: Criterion<CriterionValue>[]
) => { ) => {
if (!performer.id) return "#"; if (!performer.id) return "#";
const filter = new ListFilterModel(FilterMode.Scenes); const filter = new ListFilterModel();
const criterion = new PerformersCriterion(); const criterion = new PerformersCriterion();
criterion.value = [ criterion.value = [
{ id: performer.id, label: performer.name || `Performer ${performer.id}` }, { id: performer.id, label: performer.name || `Performer ${performer.id}` },
@@ -34,10 +42,10 @@ const makePerformerScenesUrl = (
const makePerformerImagesUrl = ( const makePerformerImagesUrl = (
performer: Partial<GQL.PerformerDataFragment>, performer: Partial<GQL.PerformerDataFragment>,
extraCriteria?: Criterion[] extraCriteria?: Criterion<CriterionValue>[]
) => { ) => {
if (!performer.id) return "#"; if (!performer.id) return "#";
const filter = new ListFilterModel(FilterMode.Images); const filter = new ListFilterModel();
const criterion = new PerformersCriterion(); const criterion = new PerformersCriterion();
criterion.value = [ criterion.value = [
{ id: performer.id, label: performer.name || `Performer ${performer.id}` }, { id: performer.id, label: performer.name || `Performer ${performer.id}` },
@@ -49,10 +57,10 @@ const makePerformerImagesUrl = (
const makePerformerGalleriesUrl = ( const makePerformerGalleriesUrl = (
performer: Partial<GQL.PerformerDataFragment>, performer: Partial<GQL.PerformerDataFragment>,
extraCriteria?: Criterion[] extraCriteria?: Criterion<CriterionValue>[]
) => { ) => {
if (!performer.id) return "#"; if (!performer.id) return "#";
const filter = new ListFilterModel(FilterMode.Galleries); const filter = new ListFilterModel();
const criterion = new PerformersCriterion(); const criterion = new PerformersCriterion();
criterion.value = [ criterion.value = [
{ id: performer.id, label: performer.name || `Performer ${performer.id}` }, { id: performer.id, label: performer.name || `Performer ${performer.id}` },
@@ -66,7 +74,7 @@ const makePerformersCountryUrl = (
performer: Partial<GQL.PerformerDataFragment> performer: Partial<GQL.PerformerDataFragment>
) => { ) => {
if (!performer.id) return "#"; if (!performer.id) return "#";
const filter = new ListFilterModel(FilterMode.Performers); const filter = new ListFilterModel();
const criterion = new CountryCriterion(); const criterion = new CountryCriterion();
criterion.value = `${performer.country}`; criterion.value = `${performer.country}`;
filter.criteria.push(criterion); filter.criteria.push(criterion);
@@ -75,7 +83,7 @@ const makePerformersCountryUrl = (
const makeStudioScenesUrl = (studio: Partial<GQL.StudioDataFragment>) => { const makeStudioScenesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
if (!studio.id) return "#"; if (!studio.id) return "#";
const filter = new ListFilterModel(FilterMode.Scenes); const filter = new ListFilterModel();
const criterion = new StudiosCriterion(); const criterion = new StudiosCriterion();
criterion.value = [ criterion.value = [
{ id: studio.id, label: studio.name || `Studio ${studio.id}` }, { id: studio.id, label: studio.name || `Studio ${studio.id}` },
@@ -86,7 +94,7 @@ const makeStudioScenesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
const makeStudioImagesUrl = (studio: Partial<GQL.StudioDataFragment>) => { const makeStudioImagesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
if (!studio.id) return "#"; if (!studio.id) return "#";
const filter = new ListFilterModel(FilterMode.Images); const filter = new ListFilterModel();
const criterion = new StudiosCriterion(); const criterion = new StudiosCriterion();
criterion.value = [ criterion.value = [
{ id: studio.id, label: studio.name || `Studio ${studio.id}` }, { id: studio.id, label: studio.name || `Studio ${studio.id}` },
@@ -97,7 +105,7 @@ const makeStudioImagesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
const makeStudioGalleriesUrl = (studio: Partial<GQL.StudioDataFragment>) => { const makeStudioGalleriesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
if (!studio.id) return "#"; if (!studio.id) return "#";
const filter = new ListFilterModel(FilterMode.Galleries); const filter = new ListFilterModel();
const criterion = new StudiosCriterion(); const criterion = new StudiosCriterion();
criterion.value = [ criterion.value = [
{ id: studio.id, label: studio.name || `Studio ${studio.id}` }, { id: studio.id, label: studio.name || `Studio ${studio.id}` },
@@ -108,7 +116,7 @@ const makeStudioGalleriesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
const makeChildStudiosUrl = (studio: Partial<GQL.StudioDataFragment>) => { const makeChildStudiosUrl = (studio: Partial<GQL.StudioDataFragment>) => {
if (!studio.id) return "#"; if (!studio.id) return "#";
const filter = new ListFilterModel(FilterMode.Studios); const filter = new ListFilterModel();
const criterion = new ParentStudiosCriterion(); const criterion = new ParentStudiosCriterion();
criterion.value = [ criterion.value = [
{ id: studio.id, label: studio.name || `Studio ${studio.id}` }, { id: studio.id, label: studio.name || `Studio ${studio.id}` },
@@ -119,7 +127,7 @@ const makeChildStudiosUrl = (studio: Partial<GQL.StudioDataFragment>) => {
const makeMovieScenesUrl = (movie: Partial<GQL.MovieDataFragment>) => { const makeMovieScenesUrl = (movie: Partial<GQL.MovieDataFragment>) => {
if (!movie.id) return "#"; if (!movie.id) return "#";
const filter = new ListFilterModel(FilterMode.Scenes); const filter = new ListFilterModel();
const criterion = new MoviesCriterion(); const criterion = new MoviesCriterion();
criterion.value = [ criterion.value = [
{ id: movie.id, label: movie.name || `Movie ${movie.id}` }, { id: movie.id, label: movie.name || `Movie ${movie.id}` },
@@ -130,8 +138,8 @@ const makeMovieScenesUrl = (movie: Partial<GQL.MovieDataFragment>) => {
const makeTagScenesUrl = (tag: Partial<GQL.TagDataFragment>) => { const makeTagScenesUrl = (tag: Partial<GQL.TagDataFragment>) => {
if (!tag.id) return "#"; if (!tag.id) return "#";
const filter = new ListFilterModel(FilterMode.Scenes); const filter = new ListFilterModel();
const criterion = new TagsCriterion("tags"); const criterion = new TagsCriterion(TagsCriterionOption);
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }]; criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
filter.criteria.push(criterion); filter.criteria.push(criterion);
return `/scenes?${filter.makeQueryParameters()}`; return `/scenes?${filter.makeQueryParameters()}`;
@@ -139,8 +147,8 @@ const makeTagScenesUrl = (tag: Partial<GQL.TagDataFragment>) => {
const makeTagPerformersUrl = (tag: Partial<GQL.TagDataFragment>) => { const makeTagPerformersUrl = (tag: Partial<GQL.TagDataFragment>) => {
if (!tag.id) return "#"; if (!tag.id) return "#";
const filter = new ListFilterModel(FilterMode.Performers); const filter = new ListFilterModel();
const criterion = new TagsCriterion("tags"); const criterion = new TagsCriterion(TagsCriterionOption);
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }]; criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
filter.criteria.push(criterion); filter.criteria.push(criterion);
return `/performers?${filter.makeQueryParameters()}`; return `/performers?${filter.makeQueryParameters()}`;
@@ -148,8 +156,8 @@ const makeTagPerformersUrl = (tag: Partial<GQL.TagDataFragment>) => {
const makeTagSceneMarkersUrl = (tag: Partial<GQL.TagDataFragment>) => { const makeTagSceneMarkersUrl = (tag: Partial<GQL.TagDataFragment>) => {
if (!tag.id) return "#"; if (!tag.id) return "#";
const filter = new ListFilterModel(FilterMode.SceneMarkers); const filter = new ListFilterModel();
const criterion = new TagsCriterion("tags"); const criterion = new TagsCriterion(TagsCriterionOption);
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }]; criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
filter.criteria.push(criterion); filter.criteria.push(criterion);
return `/scenes/markers?${filter.makeQueryParameters()}`; return `/scenes/markers?${filter.makeQueryParameters()}`;
@@ -157,8 +165,8 @@ const makeTagSceneMarkersUrl = (tag: Partial<GQL.TagDataFragment>) => {
const makeTagGalleriesUrl = (tag: Partial<GQL.TagDataFragment>) => { const makeTagGalleriesUrl = (tag: Partial<GQL.TagDataFragment>) => {
if (!tag.id) return "#"; if (!tag.id) return "#";
const filter = new ListFilterModel(FilterMode.Galleries); const filter = new ListFilterModel();
const criterion = new TagsCriterion("tags"); const criterion = new TagsCriterion(TagsCriterionOption);
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }]; criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
filter.criteria.push(criterion); filter.criteria.push(criterion);
return `/galleries?${filter.makeQueryParameters()}`; return `/galleries?${filter.makeQueryParameters()}`;
@@ -166,8 +174,8 @@ const makeTagGalleriesUrl = (tag: Partial<GQL.TagDataFragment>) => {
const makeTagImagesUrl = (tag: Partial<GQL.TagDataFragment>) => { const makeTagImagesUrl = (tag: Partial<GQL.TagDataFragment>) => {
if (!tag.id) return "#"; if (!tag.id) return "#";
const filter = new ListFilterModel(FilterMode.Images); const filter = new ListFilterModel();
const criterion = new TagsCriterion("tags"); const criterion = new TagsCriterion(TagsCriterionOption);
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }]; criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
filter.criteria.push(criterion); filter.criteria.push(criterion);
return `/images?${filter.makeQueryParameters()}`; return `/images?${filter.makeQueryParameters()}`;