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 = () => {
const config = useConfiguration();
const { data: systemStatusData } = useSystemStatus();
const language = config.data?.configuration?.interface?.language ?? "en-GB";
const messageLanguage = language.replace(/-/, "");
const defaultLocale = "en-GB";
const language =
config.data?.configuration?.interface?.language ?? defaultLocale;
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
const messages = flattenMessages((locales as any)[messageLanguage]);
...(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"]);

View File

@@ -7,6 +7,7 @@
* Added [DLNA server](/settings?tab=dlna). ([#1364](https://github.com/stashapp/stash/pull/1364))
### 🎨 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 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))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,13 +11,16 @@ import {
TruncatedText,
} from "src/components/Shared";
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";
export interface IPerformerCardExtraCriteria {
scenes: Criterion[];
images: Criterion[];
galleries: Criterion[];
scenes: Criterion<CriterionValue>[];
images: Criterion<CriterionValue>[];
galleries: Criterion<CriterionValue>[];
}
interface IPerformerCardProps {

View File

@@ -22,7 +22,6 @@ import { ScenePlayer } from "src/components/ScenePlayer";
import { TextUtils, JWUtils } from "src/utils";
import Mousetrap from "mousetrap";
import { ListFilterModel } from "src/models/list-filter/filter";
import { FilterMode } from "src/models/list-filter/types";
import { SceneQueue } from "src/models/sceneQueue";
import { QueueViewer } from "./QueueViewer";
import { SceneMarkersPanel } from "./SceneMarkersPanel";
@@ -220,10 +219,7 @@ export const Scene: React.FC = () => {
return;
}
const filterCopy = Object.assign(
new ListFilterModel(FilterMode.Scenes),
sceneQueue.query
);
const filterCopy = Object.assign(new ListFilterModel(), sceneQueue.query);
const newStart = queueStart - filterCopy.itemsPerPage;
filterCopy.currentPage = Math.ceil(newStart / filterCopy.itemsPerPage);
const query = await queryFindScenes(filterCopy);
@@ -244,10 +240,7 @@ export const Scene: React.FC = () => {
return;
}
const filterCopy = Object.assign(
new ListFilterModel(FilterMode.Scenes),
sceneQueue.query
);
const filterCopy = Object.assign(new ListFilterModel(), sceneQueue.query);
const newStart = queueStart + queueScenes.length;
filterCopy.currentPage = Math.ceil(newStart / filterCopy.itemsPerPage);
const query = await queryFindScenes(filterCopy);
@@ -284,10 +277,7 @@ export const Scene: React.FC = () => {
const pages = Math.ceil(queueTotal / query.itemsPerPage);
const page = Math.floor(Math.random() * pages) + 1;
const index = Math.floor(Math.random() * query.itemsPerPage);
const filterCopy = Object.assign(
new ListFilterModel(FilterMode.Scenes),
sceneQueue.query
);
const filterCopy = Object.assign(new ListFilterModel(), sceneQueue.query);
filterCopy.currentPage = page;
const queryResults = await queryFindScenes(filterCopy);
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! };
// if studio is already present, then we modify it, otherwise add
let parentStudioCriterion = filter.criteria.find((c) => {
return c.type === "parent_studios";
return c.criterionOption.value === "parent_studios";
}) as ParentStudiosCriterion;
if (

View File

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

View File

@@ -47,7 +47,7 @@ export const useFindGalleries = (filter: ListFilterModel) =>
GQL.useFindGalleriesQuery({
variables: {
filter: filter.makeFindFilter(),
gallery_filter: filter.makeGalleryFilter(),
gallery_filter: filter.makeFilter(),
},
});
@@ -56,7 +56,7 @@ export const queryFindGalleries = (filter: ListFilterModel) =>
query: GQL.FindGalleriesDocument,
variables: {
filter: filter.makeFindFilter(),
gallery_filter: filter.makeImageFilter(),
gallery_filter: filter.makeFilter(),
},
});
@@ -64,7 +64,7 @@ export const useFindScenes = (filter: ListFilterModel) =>
GQL.useFindScenesQuery({
variables: {
filter: filter.makeFindFilter(),
scene_filter: filter.makeSceneFilter(),
scene_filter: filter.makeFilter(),
},
});
@@ -73,7 +73,7 @@ export const queryFindScenes = (filter: ListFilterModel) =>
query: GQL.FindScenesDocument,
variables: {
filter: filter.makeFindFilter(),
scene_filter: filter.makeSceneFilter(),
scene_filter: filter.makeFilter(),
},
});
@@ -89,7 +89,7 @@ export const useFindSceneMarkers = (filter: ListFilterModel) =>
GQL.useFindSceneMarkersQuery({
variables: {
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,
variables: {
filter: filter.makeFindFilter(),
scene_marker_filter: filter.makeSceneMarkerFilter(),
scene_marker_filter: filter.makeFilter(),
},
});
@@ -106,7 +106,7 @@ export const useFindImages = (filter: ListFilterModel) =>
GQL.useFindImagesQuery({
variables: {
filter: filter.makeFindFilter(),
image_filter: filter.makeImageFilter(),
image_filter: filter.makeFilter(),
},
});
@@ -115,7 +115,7 @@ export const queryFindImages = (filter: ListFilterModel) =>
query: GQL.FindImagesDocument,
variables: {
filter: filter.makeFindFilter(),
image_filter: filter.makeImageFilter(),
image_filter: filter.makeFilter(),
},
});
@@ -123,7 +123,7 @@ export const useFindStudios = (filter: ListFilterModel) =>
GQL.useFindStudiosQuery({
variables: {
filter: filter.makeFindFilter(),
studio_filter: filter.makeStudioFilter(),
studio_filter: filter.makeFilter(),
},
});
@@ -132,7 +132,7 @@ export const queryFindStudios = (filter: ListFilterModel) =>
query: GQL.FindStudiosDocument,
variables: {
filter: filter.makeFindFilter(),
studio_filter: filter.makeStudioFilter(),
studio_filter: filter.makeFilter(),
},
});
@@ -140,7 +140,7 @@ export const useFindMovies = (filter: ListFilterModel) =>
GQL.useFindMoviesQuery({
variables: {
filter: filter.makeFindFilter(),
movie_filter: filter.makeMovieFilter(),
movie_filter: filter.makeFilter(),
},
});
@@ -149,7 +149,7 @@ export const queryFindMovies = (filter: ListFilterModel) =>
query: GQL.FindMoviesDocument,
variables: {
filter: filter.makeFindFilter(),
movie_filter: filter.makeMovieFilter(),
movie_filter: filter.makeFilter(),
},
});
@@ -157,7 +157,7 @@ export const useFindPerformers = (filter: ListFilterModel) =>
GQL.useFindPerformersQuery({
variables: {
filter: filter.makeFindFilter(),
performer_filter: filter.makePerformerFilter(),
performer_filter: filter.makeFilter(),
},
});
@@ -165,7 +165,7 @@ export const useFindTags = (filter: ListFilterModel) =>
GQL.useFindTagsQuery({
variables: {
filter: filter.makeFindFilter(),
tag_filter: filter.makeTagFilter(),
tag_filter: filter.makeFilter(),
},
});
@@ -174,7 +174,7 @@ export const queryFindTags = (filter: ListFilterModel) =>
query: GQL.FindTagsDocument,
variables: {
filter: filter.makeFindFilter(),
tag_filter: filter.makeTagFilter(),
tag_filter: filter.makeFilter(),
},
});
@@ -183,7 +183,7 @@ export const queryFindPerformers = (filter: ListFilterModel) =>
query: GQL.FindPerformersDocument,
variables: {
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! };
// if performers is already present, then we modify it, otherwise add
let performerCriterion = filter.criteria.find((c) => {
return c.type === "performers";
return c.criterionOption.value === "performers";
}) as PerformersCriterion;
if (

View File

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

View File

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

View File

@@ -44,6 +44,8 @@ import {
} from "src/core/StashService";
import { ListFilterModel } from "src/models/list-filter/filter";
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>(
result: I[],
@@ -141,6 +143,7 @@ interface IQuery<T extends IQueryResult, T2 extends IDataItem> {
interface IRenderListProps {
filter: ListFilterModel;
filterOptions: ListFilterOptions;
onChangePage: (page: number) => void;
updateQueryParams: (filter: ListFilterModel) => void;
}
@@ -151,6 +154,7 @@ const RenderList = <
>({
defaultZoomIndex,
filter,
filterOptions,
onChangePage,
addKeybinds,
useData,
@@ -406,6 +410,7 @@ const RenderList = <
onEdit={renderEditDialog ? onEdit : undefined}
onDelete={renderDeleteDialog ? onDelete : undefined}
filter={filter}
filterOptions={filterOptions}
/>
{isEditDialogOpen &&
renderEditDialog &&
@@ -432,6 +437,8 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
options: IListHookOptions<QueryResult, QueryData> &
IQuery<QueryResult, QueryData>
): IListHookData => {
const filterOptions = getFilterOptions(options.filterMode);
const history = useHistory();
const location = useLocation();
const [interfaceState, setInterfaceState] = useInterfaceLocalForage();
@@ -445,9 +452,8 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
const [filter, setFilter] = useState<ListFilterModel>(
new ListFilterModel(
options.filterMode,
queryString.parse(location.search),
options.defaultSort
options.defaultSort ?? filterOptions.defaultSortBy
)
);
@@ -509,7 +515,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
}
: activeFilter;
const newFilter = new ListFilterModel(options.filterMode, query);
const newFilter = new ListFilterModel(query);
// Compare constructed filter with current 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({
...options,
filter: renderFilter,
filterOptions,
onChangePage,
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",
"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-size": "Images size",
"galleries": "Galleries",
"isMissing": "Is Missing",
"interactive": "Interactive",
"library-size": "Library size",
"marker_count": "Marker Count",
"markers": "Markers",
"measurements": "Measurements",
"movie_scene_number": "Movie Scene Number",
"movies": "Movies",
"name": "Name",
"new": "New",
"none": "None",
"o_counter": "O-Counter",
"organized": "Organised",
"parent_studios": "Parent Studios",
"path": "Path",
"performerTags": "Performer Tags",
"performer_count": "Performer Count",
"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-size": "Scenes size",
"scenes_updated_at": "Scene Updated At",
"seconds": "Seconds",
"stash_id": "Stash ID",
"studios": "Studios",
"tag_count": "Tag Count",
"tags": "Tags",
"tattoos": "Tattoos",
"title": "Title",
"up-dir": "Up a directory",
"favourite": "FAVOURITE",
"sceneTagger": "Scene Tagger",
"donate": "Donate"
"updated_at": "Updated At",
"url": "URL",
"weight": "Weight"
}

View File

@@ -1,20 +1,6 @@
{
"developmentVersion": "Development Version",
"images": "Images",
"images-size": "Images size",
"galleries": "Galleries",
"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"
"eye_color": "Eye Color",
"favourite": "Favorite",
"hair_color": "Hair Color",
"organized": "Organized"
}

View File

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

View File

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

View File

@@ -1,164 +1,24 @@
/* 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 { ILabeledId, ILabeledValue, IOptionType } from "../types";
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";
import {
CriterionType,
encodeILabeledId,
ILabeledId,
ILabeledValue,
IOptionType,
} from "../types";
type Option = string | number | IOptionType;
export type CriterionValue = string | number | ILabeledId[];
export abstract class Criterion {
public static getLabel(type: CriterionType = "none") {
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";
}
}
// V = criterion value type
export abstract class Criterion<V extends CriterionValue> {
public static getModifierOption(
modifier: CriterionModifier = CriterionModifier.Equals
): ILabeledValue {
@@ -194,60 +54,62 @@ export abstract class Criterion {
}
}
public abstract type: CriterionType;
public abstract parameterName: string;
public criterionOption: CriterionOption;
public abstract modifier: CriterionModifier;
public abstract modifierOptions: ILabeledValue[];
public abstract options: Option[] | undefined;
public abstract value: CriterionValue;
public abstract value: V;
public inputType: "number" | "text" | undefined;
public getLabelValue(): string {
if (typeof this.value === "string") return this.value;
if (typeof this.value === "number") return this.value.toString();
return this.value.map((v) => v.label).join(", ");
public abstract getLabelValue(): string;
constructor(type: CriterionOption) {
this.criterionOption = type;
}
public getLabel(): string {
let modifierString: string;
public getLabel(intl: IntlShape): string {
let modifierMessageID: string;
switch (this.modifier) {
case CriterionModifier.Equals:
modifierString = "is";
modifierMessageID = "criterion_modifier.equals";
break;
case CriterionModifier.NotEquals:
modifierString = "is not";
modifierMessageID = "criterion_modifier.not_equals";
break;
case CriterionModifier.GreaterThan:
modifierString = "is greater than";
modifierMessageID = "criterion_modifier.greater_than";
break;
case CriterionModifier.LessThan:
modifierString = "is less than";
modifierMessageID = "criterion_modifier.less_than";
break;
case CriterionModifier.IsNull:
modifierString = "is null";
modifierMessageID = "criterion_modifier.is_null";
break;
case CriterionModifier.NotNull:
modifierString = "is not null";
modifierMessageID = "criterion_modifier.not_null";
break;
case CriterionModifier.Includes:
modifierString = "includes";
modifierMessageID = "criterion_modifier.includes";
break;
case CriterionModifier.IncludesAll:
modifierString = "includes all";
modifierMessageID = "criterion_modifier.includes_all";
break;
case CriterionModifier.Excludes:
modifierString = "excludes";
modifierMessageID = "criterion_modifier.excludes";
break;
case CriterionModifier.MatchesRegex:
modifierString = "matches regex";
modifierMessageID = "criterion_modifier.matches_regex";
break;
case CriterionModifier.NotMatchesRegex:
modifierString = "not matches regex";
modifierMessageID = "criterion_modifier.not_matches_regex";
break;
default:
modifierString = "";
modifierMessageID = "";
}
const modifierString = modifierMessageID
? intl.formatMessage({ id: modifierMessageID })
: "";
let valueString = "";
if (
@@ -257,47 +119,64 @@ export abstract class Criterion {
valueString = this.getLabelValue();
}
return `${Criterion.getLabel(this.type)} ${modifierString} ${valueString}`;
return `${intl.formatMessage({
id: this.criterionOption.messageID,
})} ${modifierString} ${valueString}`;
}
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) {
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;
}
public encodeValue(): V {
return this.value;
}
public toJSON() {
const encodedCriterion = {
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 interface ICriterionOption {
label: string;
value: CriterionType;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public apply(outputFilter: Record<string, any>) {
// eslint-disable-next-line no-param-reassign
outputFilter[this.criterionOption.parameterName] = this.toCriterionInput();
}
export class CriterionOption implements ICriterionOption {
public label: string;
public value: CriterionType;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected toCriterionInput(): any {
return {
value: this.value,
modifier: this.modifier,
};
}
}
constructor(label: string, value: CriterionType) {
this.label = label;
export class CriterionOption {
public readonly messageID: 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 class StringCriterion extends Criterion {
public type: CriterionType;
public parameterName: string;
export function createCriterionOption(value: CriterionType) {
return new CriterionOption(value, value);
}
export class StringCriterion extends Criterion<string> {
public modifier = CriterionModifier.Equals;
public modifierOptions = [
StringCriterion.getModifierOption(CriterionModifier.Equals),
@@ -316,18 +195,23 @@ export class StringCriterion extends Criterion {
return this.value;
}
constructor(type: CriterionType, parameterName?: string, options?: string[]) {
super();
public encodeValue() {
// 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.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 {
public type: CriterionType;
public parameterName: string;
export class BooleanCriterion extends StringCriterion {
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
constructor(type: CriterionOption) {
super(type, [true.toString(), false.toString()]);
}
protected toCriterionInput(): boolean {
return this.value === "true";
}
}
export class NumberCriterion extends Criterion<number> {
public modifier = CriterionModifier.Equals;
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Equals),
@@ -361,17 +256,51 @@ export class NumberCriterion extends Criterion {
return this.value.toString();
}
constructor(type: CriterionType, parameterName?: string, options?: number[]) {
super();
constructor(type: CriterionOption, options?: number[]) {
super(type);
this.type = type;
this.options = options;
this.inputType = "number";
}
}
if (parameterName) {
this.parameterName = parameterName;
} else {
this.parameterName = type;
export abstract class ILabeledIdCriterion extends Criterion<ILabeledId[]> {
public modifier = CriterionModifier.IncludesAll;
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.IncludesAll),
Criterion.getModifierOption(CriterionModifier.Includes),
Criterion.getModifierOption(CriterionModifier.Excludes),
];
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 {
public type: CriterionType;
public parameterName: string;
export class DurationCriterion extends Criterion<number> {
public modifier = CriterionModifier.Equals;
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Equals),
@@ -398,12 +325,10 @@ export class DurationCriterion extends Criterion {
public options: number[] | undefined;
public value: number = 0;
constructor(type: CriterionType, parameterName?: string, options?: number[]) {
super();
constructor(type: CriterionOption, options?: number[]) {
super(type);
this.type = type;
this.options = options;
this.parameterName = parameterName ?? type;
}
public getLabelValue() {

View File

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

View File

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

View File

@@ -1,27 +1,9 @@
import * as GQL from "src/core/generated-graphql";
import { ILabeledId, IOptionType, encodeILabeledId } from "../types";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
import { CriterionOption, ILabeledIdCriterion } from "./criterion";
export class GalleriesCriterion extends Criterion {
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[] = [];
const galleriesCriterionOption = new CriterionOption("galleries", "galleries");
public encodeValue() {
return this.value.map((o) => {
return encodeILabeledId(o);
});
export class GalleriesCriterion extends ILabeledIdCriterion {
constructor() {
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 { getGenderStrings } from "src/core/StashService";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
import {
CriterionModifier,
GenderCriterionInput,
} from "src/core/generated-graphql";
import { getGenderStrings, stringToGender } from "src/core/StashService";
import { CriterionOption, StringCriterion } from "./criterion";
export class GenderCriterion extends Criterion {
public type: CriterionType = "gender";
public parameterName: string = "gender";
export const GenderCriterionOption = new CriterionOption("gender", "gender");
export class GenderCriterion extends StringCriterion {
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: string[] = getGenderStrings();
public value: string = "";
constructor() {
super(GenderCriterionOption, getGenderStrings());
}
export class GenderCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("gender");
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 { Criterion, CriterionType, ICriterionOption } from "./criterion";
import { CriterionOption, StringCriterion } from "./criterion";
export class HasMarkersCriterion extends Criterion {
public type: CriterionType = "hasMarkers";
public parameterName: string = "has_markers";
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: string[] = [true.toString(), false.toString()];
public value: string = "";
export const HasMarkersCriterionOption = new CriterionOption(
"hasMarkers",
"hasMarkers",
"has_markers"
);
export class HasMarkersCriterion extends StringCriterion {
constructor() {
super(HasMarkersCriterionOption, [true.toString(), false.toString()]);
}
export class HasMarkersCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("hasMarkers");
public value: CriterionType = "hasMarkers";
protected toCriterionInput(): string {
return this.value;
}
}

View File

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

View File

@@ -1,16 +1,24 @@
import { CriterionModifier } from "src/core/generated-graphql";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
import { CriterionOption, StringCriterion } from "./criterion";
export abstract class IsMissingCriterion extends Criterion {
public parameterName: string = "is_missing";
export abstract class IsMissingCriterion extends StringCriterion {
public modifierOptions = [];
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 {
public type: CriterionType = "sceneIsMissing";
public options: string[] = [
constructor() {
super(SceneIsMissingCriterionOption, [
"title",
"details",
"url",
@@ -20,33 +28,38 @@ export class SceneIsMissingCriterion extends IsMissingCriterion {
"movie",
"performers",
"tags",
];
"stash_id",
]);
}
}
export class SceneIsMissingCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("sceneIsMissing");
public value: CriterionType = "sceneIsMissing";
}
export const ImageIsMissingCriterionOption = new CriterionOption(
"isMissing",
"imageIsMissing",
"is_missing"
);
export class ImageIsMissingCriterion extends IsMissingCriterion {
public type: CriterionType = "imageIsMissing";
public options: string[] = [
constructor() {
super(ImageIsMissingCriterionOption, [
"title",
"galleries",
"studio",
"performers",
"tags",
];
]);
}
}
export class ImageIsMissingCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("imageIsMissing");
public value: CriterionType = "imageIsMissing";
}
export const PerformerIsMissingCriterionOption = new CriterionOption(
"isMissing",
"performerIsMissing",
"is_missing"
);
export class PerformerIsMissingCriterion extends IsMissingCriterion {
public type: CriterionType = "performerIsMissing";
public options: string[] = [
constructor() {
super(PerformerIsMissingCriterionOption, [
"url",
"twitter",
"instagram",
@@ -65,17 +78,20 @@ export class PerformerIsMissingCriterion extends IsMissingCriterion {
"gender",
"image",
"details",
];
"stash_id",
]);
}
}
export class PerformerIsMissingCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("performerIsMissing");
public value: CriterionType = "performerIsMissing";
}
export const GalleryIsMissingCriterionOption = new CriterionOption(
"isMissing",
"galleryIsMissing",
"is_missing"
);
export class GalleryIsMissingCriterion extends IsMissingCriterion {
public type: CriterionType = "galleryIsMissing";
public options: string[] = [
constructor() {
super(GalleryIsMissingCriterionOption, [
"title",
"details",
"url",
@@ -84,40 +100,46 @@ export class GalleryIsMissingCriterion extends IsMissingCriterion {
"performers",
"tags",
"scenes",
];
]);
}
}
export class GalleryIsMissingCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("galleryIsMissing");
public value: CriterionType = "galleryIsMissing";
}
export const TagIsMissingCriterionOption = new CriterionOption(
"isMissing",
"tagIsMissing",
"is_missing"
);
export class TagIsMissingCriterion extends IsMissingCriterion {
public type: CriterionType = "tagIsMissing";
public options: string[] = ["image"];
constructor() {
super(TagIsMissingCriterionOption, ["image"]);
}
}
export class TagIsMissingCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("tagIsMissing");
public value: CriterionType = "tagIsMissing";
}
export const StudioIsMissingCriterionOption = new CriterionOption(
"isMissing",
"studioIsMissing",
"is_missing"
);
export class StudioIsMissingCriterion extends IsMissingCriterion {
public type: CriterionType = "studioIsMissing";
public options: string[] = ["image", "details"];
constructor() {
super(StudioIsMissingCriterionOption, ["image", "stash_id", "details"]);
}
}
export class StudioIsMissingCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("studioIsMissing");
public value: CriterionType = "studioIsMissing";
}
export const MovieIsMissingCriterionOption = new CriterionOption(
"isMissing",
"movieIsMissing",
"is_missing"
);
export class MovieIsMissingCriterion extends IsMissingCriterion {
public type: CriterionType = "movieIsMissing";
public options: string[] = ["front_image", "back_image", "scenes"];
constructor() {
super(MovieIsMissingCriterionOption, [
"front_image",
"back_image",
"scenes",
]);
}
export class MovieIsMissingCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("movieIsMissing");
public value: CriterionType = "movieIsMissing";
}

View File

@@ -1,32 +1,9 @@
import { CriterionModifier } from "src/core/generated-graphql";
import { ILabeledId, encodeILabeledId } from "../types";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
import { CriterionOption, ILabeledIdCriterion } from "./criterion";
interface IOptionType {
id: string;
name?: string;
image_path?: string;
}
export const MoviesCriterionOption = new CriterionOption("movies", "movies");
export class MoviesCriterion extends Criterion {
public type: CriterionType = "movies";
public parameterName: string = "movies";
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 MoviesCriterion extends ILabeledIdCriterion {
constructor() {
super(MoviesCriterionOption, false);
}
}
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 { Criterion, CriterionType, ICriterionOption } from "./criterion";
import { Criterion, CriterionOption } from "./criterion";
export class NoneCriterion extends Criterion {
public type: CriterionType = "none";
public parameterName: string = "";
export const NoneCriterionOption = new CriterionOption("none", "none");
export class NoneCriterion extends Criterion<string> {
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: undefined;
public value: string = "none";
constructor() {
super(NoneCriterionOption);
}
export class NoneCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("none");
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 { Criterion, CriterionType, ICriterionOption } from "./criterion";
import { BooleanCriterion, CriterionOption } from "./criterion";
export class OrganizedCriterion extends Criterion {
public type: CriterionType = "organized";
public parameterName: string = "organized";
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: string[] = [true.toString(), false.toString()];
public value: string = "";
}
export const OrganizedCriterionOption = new CriterionOption(
"organized",
"organized"
);
export class OrganizedCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("organized");
public value: CriterionType = "organized";
export class OrganizedCriterion extends BooleanCriterion {
constructor() {
super(OrganizedCriterionOption);
}
}

View File

@@ -1,27 +1,12 @@
import { CriterionModifier } from "src/core/generated-graphql";
import { ILabeledId, IOptionType, encodeILabeledId } from "../types";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
import { CriterionOption, ILabeledIdCriterion } from "./criterion";
export class PerformersCriterion extends Criterion {
public type: CriterionType = "performers";
public parameterName: string = "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[] = [];
export const PerformersCriterionOption = new CriterionOption(
"performers",
"performers"
);
public encodeValue() {
return this.value.map((o) => {
return encodeILabeledId(o);
});
export class PerformersCriterion extends ILabeledIdCriterion {
constructor() {
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 { Criterion, CriterionType, ICriterionOption } from "./criterion";
import { Criterion, CriterionOption, NumberCriterion } from "./criterion";
export class RatingCriterion extends Criterion {
public type: CriterionType = "rating";
public parameterName: string = "rating";
export const RatingCriterionOption = new CriterionOption("rating", "rating");
export class RatingCriterion extends NumberCriterion {
public modifier = CriterionModifier.Equals;
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Equals),
@@ -13,11 +13,8 @@ export class RatingCriterion extends Criterion {
Criterion.getModifierOption(CriterionModifier.IsNull),
Criterion.getModifierOption(CriterionModifier.NotNull),
];
public options: number[] = [1, 2, 3, 4, 5];
public value: number = 0;
}
export class RatingCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("rating");
public value: CriterionType = "rating";
constructor() {
super(RatingCriterionOption, [1, 2, 3, 4, 5]);
}
}

View File

@@ -1,12 +1,12 @@
import { CriterionModifier } from "src/core/generated-graphql";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
import { CriterionModifier, ResolutionEnum } from "src/core/generated-graphql";
import { CriterionOption, StringCriterion } from "./criterion";
export class ResolutionCriterion extends Criterion {
public type: CriterionType = "resolution";
public parameterName: string = "resolution";
abstract class AbstractResolutionCriterion extends StringCriterion {
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: string[] = [
constructor(type: CriterionOption) {
super(type, [
"144p",
"240p",
"360p",
@@ -19,21 +19,62 @@ export class ResolutionCriterion extends Criterion {
"5k",
"6k",
"8k",
];
public value: string = "";
]);
}
export class ResolutionCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("resolution");
public value: CriterionType = "resolution";
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 AverageResolutionCriterion extends ResolutionCriterion {
public type: CriterionType = "average_resolution";
public parameterName: string = "average_resolution";
export const ResolutionCriterionOption = new CriterionOption(
"resolution",
"resolution"
);
export class ResolutionCriterion extends AbstractResolutionCriterion {
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
constructor() {
super(ResolutionCriterionOption);
}
}
export class AverageResolutionCriterionOption extends ResolutionCriterionOption {
public label: string = Criterion.getLabel("average_resolution");
public value: CriterionType = "average_resolution";
export const AverageResolutionCriterionOption = new CriterionOption(
"average_resolution",
"average_resolution"
);
export class AverageResolutionCriterion extends AbstractResolutionCriterion {
constructor() {
super(AverageResolutionCriterionOption);
}
}

View File

@@ -1,36 +1,25 @@
import { CriterionModifier } from "src/core/generated-graphql";
import { ILabeledId, IOptionType, encodeILabeledId } from "../types";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
import { CriterionOption, ILabeledIdCriterion } from "./criterion";
export class StudiosCriterion extends Criterion {
public type: CriterionType = "studios";
public parameterName: string = "studios";
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);
});
abstract class AbstractStudiosCriterion extends ILabeledIdCriterion {
constructor(type: CriterionOption) {
super(type, false);
}
}
export class StudiosCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("studios");
public value: CriterionType = "studios";
export const StudiosCriterionOption = new CriterionOption("studios", "studios");
export class StudiosCriterion extends AbstractStudiosCriterion {
constructor() {
super(StudiosCriterionOption);
}
}
export class ParentStudiosCriterion extends StudiosCriterion {
public type: CriterionType = "parent_studios";
public parameterName: string = "parents";
export const ParentStudiosCriterionOption = new CriterionOption(
"parent_studios",
"parent_studios",
"parents"
);
export class ParentStudiosCriterion extends AbstractStudiosCriterion {
constructor() {
super(ParentStudiosCriterionOption);
}
export class ParentStudiosCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("parent_studios");
public value: CriterionType = "parent_studios";
}

View File

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

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
// is not impacted
export enum DisplayMode {
Grid,
@@ -39,3 +40,55 @@ export interface IOptionType {
name?: 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 { RouteComponentProps } from "react-router-dom";
import { ListFilterModel } from "./list-filter/filter";
import { FilterMode } from "./list-filter/types";
import { SceneListFilterOptions } from "./list-filter/scenes";
interface IQueryParameters {
qsort?: string;
@@ -27,10 +27,7 @@ export class SceneQueue {
public static fromListFilterModel(filter: ListFilterModel) {
const ret = new SceneQueue();
const filterCopy = Object.assign(
new ListFilterModel(filter.filterMode),
filter
);
const filterCopy = Object.assign(new ListFilterModel(), filter);
filterCopy.itemsPerPage = 40;
ret.originalQueryPage = filter.currentPage;
@@ -98,8 +95,8 @@ export class SceneQueue {
if (parsed.qfp) {
const query = new ListFilterModel(
FilterMode.Scenes,
translated as queryString.ParsedQuery
translated as queryString.ParsedQuery,
SceneListFilterOptions.defaultSortBy
);
ret.query = query;
} else if (parsed.qs) {

View File

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