Remove or exempt all uses of 'any

* Refactored LocalForage
* Refactored SceneFilenameParser
This commit is contained in:
Infinite
2020-02-13 19:54:37 +01:00
parent a60c89ceb1
commit cdadb66d85
43 changed files with 671 additions and 677 deletions

View File

@@ -15,6 +15,7 @@
], ],
"rules": { "rules": {
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": 2,
"lines-between-class-members": "off", "lines-between-class-members": "off",
"@typescript-eslint/interface-name-prefix": [ "@typescript-eslint/interface-name-prefix": [
"warn", "warn",

View File

@@ -27,6 +27,7 @@ export const App: React.FC = () => {
const config = StashService.useConfiguration(); const config = StashService.useConfiguration();
const language = config.data?.configuration?.interface?.language ?? "en-US"; const language = config.data?.configuration?.interface?.language ?? "en-US";
const messageLanguage = language.slice(0, 2); const messageLanguage = language.slice(0, 2);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const messages = flattenMessages((locales as any)[messageLanguage]); const messages = flattenMessages((locales as any)[messageLanguage]);
return ( return (

View File

@@ -1,12 +1,25 @@
import React from "react"; import React from "react";
export class ErrorBoundary extends React.Component<any, any> { interface IErrorBoundaryProps {
constructor(props: any) { children?: React.ReactNode,
super(props);
this.state = { error: null, errorInfo: null };
} }
public componentDidCatch(error: any, errorInfo: any) { type ErrorInfo = {
componentStack: string,
};
interface IErrorBoundaryState {
error?: Error;
errorInfo?: ErrorInfo;
}
export class ErrorBoundary extends React.Component<IErrorBoundaryProps, IErrorBoundaryState> {
constructor(props: IErrorBoundaryProps) {
super(props);
this.state = {};
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.setState({ this.setState({
error, error,
errorInfo errorInfo

View File

@@ -15,7 +15,7 @@ export const Gallery: React.FC = () => {
return ( return (
<div className="col-9 m-auto"> <div className="col-9 m-auto">
<GalleryViewer gallery={gallery as any} /> <GalleryViewer gallery={gallery} />
</div> </div>
); );
}; };

View File

@@ -6,12 +6,10 @@ import { CriterionModifier } from "src/core/generated-graphql";
import { import {
Criterion, Criterion,
CriterionType, CriterionType,
DurationCriterion DurationCriterion,
CriterionValue
} 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 { PerformersCriterion } from "src/models/list-filter/criteria/performers";
import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
import { TagsCriterion } from "src/models/list-filter/criteria/tags";
import { makeCriteria } from "src/models/list-filter/criteria/utils"; import { makeCriteria } from "src/models/list-filter/criteria/utils";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
@@ -28,11 +26,11 @@ 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<any, any>>( const [criterion, setCriterion] = useState<Criterion>(
new NoneCriterion() new NoneCriterion()
); );
const valueStage = useRef<any>(criterion.value); const valueStage = useRef<CriterionValue>(criterion.value);
// Configure if we are editing an existing criterion // Configure if we are editing an existing criterion
useEffect(() => { useEffect(() => {
@@ -53,7 +51,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
event: React.ChangeEvent<HTMLSelectElement> event: React.ChangeEvent<HTMLSelectElement>
) { ) {
const newCriterion = _.cloneDeep(criterion); const newCriterion = _.cloneDeep(criterion);
newCriterion.modifier = event.target.value as any; newCriterion.modifier = event.target.value as CriterionModifier;
setCriterion(newCriterion); setCriterion(newCriterion);
} }
@@ -83,6 +81,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
const value = defaultValue.current; const value = defaultValue.current;
if ( if (
criterion.options && criterion.options &&
!Array.isArray(criterion.options) &&
(value === undefined || value === "" || typeof value === "number") (value === undefined || value === "" || typeof value === "number")
) { ) {
criterion.value = criterion.options[0]; criterion.value = criterion.options[0];
@@ -141,20 +140,15 @@ export const AddFilter: React.FC<IAddFilterProps> = (
} }
if (Array.isArray(criterion.value)) { if (Array.isArray(criterion.value)) {
let type: "performers" | "studios" | "tags"; if(
if (criterion instanceof PerformersCriterion) { criterion.type !== "performers" &&
type = "performers"; criterion.type !== "studios" &&
} else if (criterion instanceof StudiosCriterion) { criterion.type !== "tags")
type = "studios";
} else if (criterion instanceof TagsCriterion) {
type = "tags";
} else {
return; return;
}
return ( return (
<FilterSelect <FilterSelect
type={type} type={criterion.type}
isMulti isMulti
onSelect={items => { onSelect={items => {
const newCriterion = _.cloneDeep(criterion); const newCriterion = _.cloneDeep(criterion);
@@ -164,7 +158,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
})); }));
setCriterion(newCriterion); setCriterion(newCriterion);
}} }}
ids={criterion.value.map((labeled: any) => labeled.id)} ids={criterion.value.map(labeled => labeled.id)}
/> />
); );
} }
@@ -174,10 +168,10 @@ export const AddFilter: React.FC<IAddFilterProps> = (
<Form.Control <Form.Control
as="select" as="select"
onChange={onChangedSingleSelect} onChange={onChangedSingleSelect}
value={criterion.value} value={criterion.value.toString()}
> >
{criterion.options.map(c => ( {criterion.options.map(c => (
<option key={c} value={c}> <option key={c.toString()} value={c.toString()}>
{c} {c}
</option> </option>
))} ))}
@@ -198,7 +192,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
type={criterion.inputType} type={criterion.inputType}
onChange={onChangedInput} onChange={onChangedInput}
onBlur={onBlurInput} onBlur={onBlurInput}
value={criterion.value || ""} value={criterion.value.toString()}
/> />
); );
} }

View File

@@ -1,5 +1,5 @@
import { debounce } from "lodash"; import { debounce } from "lodash";
import React, { SyntheticEvent, useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import { SortDirectionEnum } from "src/core/generated-graphql"; import { SortDirectionEnum } from "src/core/generated-graphql";
import { import {
Badge, Badge,
@@ -8,7 +8,8 @@ import {
Dropdown, Dropdown,
Form, Form,
OverlayTrigger, OverlayTrigger,
Tooltip Tooltip,
SafeAnchor
} from "react-bootstrap"; } from "react-bootstrap";
import { Icon } from "src/components/Shared"; import { Icon } from "src/components/Shared";
@@ -44,8 +45,8 @@ export const ListFilter: React.FC<IListFilterProps> = (
props: IListFilterProps props: IListFilterProps
) => { ) => {
const searchCallback = useCallback( const searchCallback = useCallback(
debounce((event: any) => { debounce((value: string) => {
props.onChangeQuery(event.target.value); props.onChangeQuery(value);
}, 500), }, 500),
[props.onChangeQuery] [props.onChangeQuery]
); );
@@ -54,14 +55,13 @@ export const ListFilter: React.FC<IListFilterProps> = (
Criterion | undefined Criterion | undefined
>(undefined); >(undefined);
function onChangePageSize(event: SyntheticEvent<HTMLSelectElement>) { function onChangePageSize(event: React.FormEvent<HTMLSelectElement>) {
const val = event!.currentTarget!.value; const val = event.currentTarget.value;
props.onChangePageSize(parseInt(val, 10)); props.onChangePageSize(parseInt(val, 10));
} }
function onChangeQuery(event: SyntheticEvent<HTMLInputElement>) { function onChangeQuery(event: React.FormEvent<HTMLInputElement>) {
event.persist(); searchCallback(event.currentTarget.value);
searchCallback(event);
} }
function onChangeSortDirection() { function onChangeSortDirection() {
@@ -72,8 +72,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
} }
} }
function onChangeSortBy(event: React.MouseEvent<any>) { function onChangeSortBy(event:React.MouseEvent<SafeAnchor>) {
props.onChangeSortBy(event.currentTarget.text); const target = event.currentTarget as unknown as HTMLAnchorElement;
props.onChangeSortBy(target.text);
} }
function onChangeDisplayMode(displayMode: DisplayMode) { function onChangeDisplayMode(displayMode: DisplayMode) {
@@ -156,6 +157,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
<Badge <Badge
className="tag-item" className="tag-item"
variant="secondary" variant="secondary"
key={criterion.getId()}
onClick={() => onClickCriterionTag(criterion)} onClick={() => onClickCriterionTag(criterion)}
> >
{criterion.getLabel()} {criterion.getLabel()}
@@ -241,8 +243,8 @@ export const ListFilter: React.FC<IListFilterProps> = (
min={0} min={0}
max={3} max={3}
defaultValue={1} defaultValue={1}
onChange={(event: any) => onChange={(e: React.FormEvent<HTMLInputElement>) =>
onChangeZoom(Number.parseInt(event.target.value, 10)) onChangeZoom(Number.parseInt(e.currentTarget.value, 10))
} }
/> />
); );

View File

@@ -16,7 +16,7 @@ export const PerformerScenesPanel: React.FC<IPerformerDetailsProps> = ({
// 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.type === "performers";
}); }) as PerformersCriterion;
if ( if (
performerCriterion && performerCriterion &&
@@ -25,7 +25,7 @@ export const PerformerScenesPanel: React.FC<IPerformerDetailsProps> = ({
) { ) {
// add the performer if not present // add the performer if not present
if ( if (
!performerCriterion.value.find((p: any) => { !performerCriterion.value.find(p => {
return p.id === performer.id; return p.id === performer.id;
}) })
) { ) {

View File

@@ -132,7 +132,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
<InputGroup className="col-8"> <InputGroup className="col-8">
<Form.Control <Form.Control
id="filename-pattern" id="filename-pattern"
onChange={(newValue: any) => setPattern(newValue.target.value)} onChange={(e: React.FormEvent<HTMLInputElement>) => setPattern(e.currentTarget.value)}
value={pattern} value={pattern}
/> />
<InputGroup.Append> <InputGroup.Append>
@@ -158,7 +158,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
<Form.Label className="col-2">Ignored words</Form.Label> <Form.Label className="col-2">Ignored words</Form.Label>
<InputGroup className="col-8"> <InputGroup className="col-8">
<Form.Control <Form.Control
onChange={(newValue: any) => setIgnoreWords(newValue.target.value)} onChange={(e: React.FormEvent<HTMLInputElement>) => setIgnoreWords(e.currentTarget.value)}
value={ignoreWords} value={ignoreWords}
/> />
</InputGroup> </InputGroup>
@@ -174,8 +174,8 @@ export const ParserInput: React.FC<IParserInputProps> = (
</Form.Label> </Form.Label>
<InputGroup className="col-8"> <InputGroup className="col-8">
<Form.Control <Form.Control
onChange={(newValue: any) => onChange={(e: React.FormEvent<HTMLInputElement>) =>
setWhitespaceCharacters(newValue.target.value) setWhitespaceCharacters(e.currentTarget.value)
} }
value={whitespaceCharacters} value={whitespaceCharacters}
/> />
@@ -229,8 +229,8 @@ export const ParserInput: React.FC<IParserInputProps> = (
<Form.Control <Form.Control
as="select" as="select"
options={PAGE_SIZE_OPTIONS} options={PAGE_SIZE_OPTIONS}
onChange={(event: any) => onChange={(e: React.FormEvent<HTMLInputElement>) =>
props.onPageSizeChanged(parseInt(event.target.value, 10)) props.onPageSizeChanged(parseInt(e.currentTarget.value, 10))
} }
defaultValue={props.input.pageSize} defaultValue={props.input.pageSize}
className="col-1 filter-item" className="col-1 filter-item"

View File

@@ -1,154 +1,16 @@
/* eslint-disable no-param-reassign, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ /* eslint-disable no-param-reassign, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
import React, { useEffect, useState, useCallback } from "react"; import React, { useEffect, useState, useCallback } from "react";
import { Badge, Button, Card, Form, Table } from "react-bootstrap"; import { Button, Card, Form, Table } from "react-bootstrap";
import _ from "lodash"; import _ from "lodash";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { import { LoadingIndicator } from "src/components/Shared";
FilterSelect,
StudioSelect,
LoadingIndicator
} from "src/components/Shared";
import { TextUtils } from "src/utils";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { Pagination } from "src/components/List/Pagination"; import { Pagination } from "src/components/List/Pagination";
import { IParserInput, ParserInput } from "./ParserInput"; import { IParserInput, ParserInput } from "./ParserInput";
import { ParserField } from "./ParserField"; import { ParserField } from "./ParserField";
import { SceneParserResult, SceneParserRow } from './SceneParserRow';
class ParserResult<T> {
public value: GQL.Maybe<T> = null;
public originalValue: GQL.Maybe<T> = null;
public set: boolean = false;
public setOriginalValue(v: GQL.Maybe<T>) {
this.originalValue = v;
this.value = v;
}
public setValue(v: GQL.Maybe<T>) {
if (v) {
this.value = v;
this.set = !_.isEqual(this.value, this.originalValue);
}
}
}
class SceneParserResult {
public id: string;
public filename: string;
public title: ParserResult<string> = new ParserResult();
public date: ParserResult<string> = new ParserResult();
public studio: ParserResult<Partial<GQL.Studio>> = new ParserResult();
public studioId: ParserResult<string> = new ParserResult();
public tags: ParserResult<GQL.Tag[]> = new ParserResult();
public tagIds: ParserResult<string[]> = new ParserResult();
public performers: ParserResult<
Partial<GQL.Performer>[]
> = new ParserResult();
public performerIds: ParserResult<string[]> = new ParserResult();
public scene: GQL.SlimSceneDataFragment;
constructor(
result: GQL.ParseSceneFilenamesQuery["parseSceneFilenames"]["results"][0]
) {
this.scene = result.scene;
this.id = this.scene.id;
this.filename = TextUtils.fileNameFromPath(this.scene.path);
this.title.setOriginalValue(this.scene.title ?? null);
this.date.setOriginalValue(this.scene.date ?? null);
this.performerIds.setOriginalValue(this.scene.performers.map(p => p.id));
this.performers.setOriginalValue(this.scene.performers);
this.tagIds.setOriginalValue(this.scene.tags.map(t => t.id));
this.tags.setOriginalValue(this.scene.tags);
this.studioId.setOriginalValue(this.scene.studio?.id ?? null);
this.studio.setOriginalValue(this.scene.studio ?? null);
this.title.setValue(result.title ?? null);
this.date.setValue(result.date ?? null);
this.performerIds.setValue(result.performer_ids ?? []);
this.tagIds.setValue(result.tag_ids ?? []);
this.studioId.setValue(result.studio_id ?? null);
if (result.performer_ids) {
this.performers.setValue(
(result.performer_ids ?? []).map(
p =>
({
id: p,
name: "",
favorite: false,
image_path: ""
} as GQL.Performer)
)
);
}
if (result.tag_ids) {
this.tags.setValue(
result.tag_ids.map(t => ({
id: t,
name: ""
}))
);
}
if (result.studio_id) {
this.studio.setValue({
id: result.studio_id,
name: "",
image_path: ""
} as GQL.Studio);
}
}
private static setInput(
obj: any,
key: string,
parserResult: ParserResult<any>
) {
if (parserResult.set) {
obj[key] = parserResult.value;
}
}
// returns true if any of its fields have set == true
public isChanged() {
return (
this.title.set ||
this.date.set ||
this.performerIds.set ||
this.studioId.set ||
this.tagIds.set
);
}
public toSceneUpdateInput() {
const ret = {
id: this.id,
title: this.scene.title,
details: this.scene.details,
url: this.scene.url,
date: this.scene.date,
rating: this.scene.rating,
gallery_id: this.scene.gallery ? this.scene.gallery.id : undefined,
studio_id: this.scene.studio ? this.scene.studio.id : undefined,
performer_ids: this.scene.performers.map(performer => performer.id),
tag_ids: this.scene.tags.map(tag => tag.id)
};
SceneParserResult.setInput(ret, "title", this.title);
SceneParserResult.setInput(ret, "date", this.date);
SceneParserResult.setInput(ret, "performer_ids", this.performerIds);
SceneParserResult.setInput(ret, "studio_id", this.studioId);
SceneParserResult.setInput(ret, "tag_ids", this.tagIds);
return ret;
}
}
const initialParserInput = { const initialParserInput = {
pattern: "{title}.{ext}", pattern: "{title}.{ext}",
@@ -309,19 +171,19 @@ export const SceneFilenameParser: React.FC = () => {
useEffect(() => { useEffect(() => {
const newAllTitleSet = !parserResult.some(r => { const newAllTitleSet = !parserResult.some(r => {
return !r.title.set; return !r.title.isSet;
}); });
const newAllDateSet = !parserResult.some(r => { const newAllDateSet = !parserResult.some(r => {
return !r.date.set; return !r.date.isSet;
}); });
const newAllPerformerSet = !parserResult.some(r => { const newAllPerformerSet = !parserResult.some(r => {
return !r.performerIds.set; return !r.performers.isSet;
}); });
const newAllTagSet = !parserResult.some(r => { const newAllTagSet = !parserResult.some(r => {
return !r.tagIds.set; return !r.tags.isSet;
}); });
const newAllStudioSet = !parserResult.some(r => { const newAllStudioSet = !parserResult.some(r => {
return !r.studioId.set; return !r.studio.isSet;
}); });
setAllTitleSet(newAllTitleSet); setAllTitleSet(newAllTitleSet);
@@ -335,7 +197,7 @@ export const SceneFilenameParser: React.FC = () => {
const newResult = [...parserResult]; const newResult = [...parserResult];
newResult.forEach(r => { newResult.forEach(r => {
r.title.set = selected; r.title.isSet = selected;
}); });
setParserResult(newResult); setParserResult(newResult);
@@ -346,7 +208,7 @@ export const SceneFilenameParser: React.FC = () => {
const newResult = [...parserResult]; const newResult = [...parserResult];
newResult.forEach(r => { newResult.forEach(r => {
r.date.set = selected; r.date.isSet = selected;
}); });
setParserResult(newResult); setParserResult(newResult);
@@ -357,7 +219,7 @@ export const SceneFilenameParser: React.FC = () => {
const newResult = [...parserResult]; const newResult = [...parserResult];
newResult.forEach(r => { newResult.forEach(r => {
r.performerIds.set = selected; r.performers.isSet = selected;
}); });
setParserResult(newResult); setParserResult(newResult);
@@ -368,7 +230,7 @@ export const SceneFilenameParser: React.FC = () => {
const newResult = [...parserResult]; const newResult = [...parserResult];
newResult.forEach(r => { newResult.forEach(r => {
r.tagIds.set = selected; r.tags.isSet = selected;
}); });
setParserResult(newResult); setParserResult(newResult);
@@ -379,299 +241,13 @@ export const SceneFilenameParser: React.FC = () => {
const newResult = [...parserResult]; const newResult = [...parserResult];
newResult.forEach(r => { newResult.forEach(r => {
r.studioId.set = selected; r.studio.isSet = selected;
}); });
setParserResult(newResult); setParserResult(newResult);
setAllStudioSet(selected); setAllStudioSet(selected);
} }
interface ISceneParserFieldProps {
parserResult: ParserResult<any>;
className?: string;
fieldName: string;
onSetChanged: (set: boolean) => void;
onValueChanged: (value: any) => void;
originalParserResult?: ParserResult<any>;
renderOriginalInputField: (props: ISceneParserFieldProps) => JSX.Element;
renderNewInputField: (
props: ISceneParserFieldProps,
onChange: (event: any) => void
) => JSX.Element;
}
function SceneParserField(props: ISceneParserFieldProps) {
function maybeValueChanged(value: any) {
if (value !== props.parserResult.value) {
props.onValueChanged(value);
}
}
if (!showFields.get(props.fieldName)) {
return null;
}
return (
<>
<td>
<Form.Check
checked={props.parserResult.set}
onChange={() => {
props.onSetChanged(!props.parserResult.set);
}}
/>
</td>
<td>
<Form.Group>
{props.renderOriginalInputField(props)}
{props.renderNewInputField(props, value =>
maybeValueChanged(value)
)}
</Form.Group>
</td>
</>
);
}
function renderOriginalInputGroup(props: ISceneParserFieldProps) {
const result = props.originalParserResult || props.parserResult;
return (
<Form.Control
disabled
className={props.className}
defaultValue={result.originalValue || ""}
/>
);
}
interface IInputGroupWrapperProps {
parserResult: ParserResult<any>;
onChange: (event: any) => void;
className?: string;
}
function InputGroupWrapper(props: IInputGroupWrapperProps) {
return (
<Form.Control
disabled={!props.parserResult.set}
className={props.className}
value={props.parserResult.value || ""}
onChange={(event: any) => props.onChange(event.target.value)}
/>
);
}
function renderNewInputGroup(
props: ISceneParserFieldProps,
onChangeHandler: (value: any) => void
) {
return (
<InputGroupWrapper
className={props.className}
onChange={(value: any) => {
onChangeHandler(value);
}}
parserResult={props.parserResult}
/>
);
}
interface IHasName {
name: string;
}
function renderOriginalSelect(props: ISceneParserFieldProps) {
const result = props.originalParserResult || props.parserResult;
const elements = result.originalValue
? Array.isArray(result.originalValue)
? result.originalValue.map((el: IHasName) => el.name)
: [result.originalValue.name]
: [];
return (
<div>
{elements.map((name: string) => (
<Badge key={name} variant="secondary">
{name}
</Badge>
))}
</div>
);
}
function renderNewMultiSelect(
type: "performers" | "tags",
props: ISceneParserFieldProps,
onChangeHandler: (value: any) => void
) {
return (
<FilterSelect
className={props.className}
type={type}
isMulti
onSelect={items => {
const ids = items.map(i => i.id);
onChangeHandler(ids);
}}
ids={props.parserResult.value}
/>
);
}
function renderNewPerformerSelect(
props: ISceneParserFieldProps,
onChangeHandler: (value: any) => void
) {
return renderNewMultiSelect("performers", props, onChangeHandler);
}
function renderNewTagSelect(
props: ISceneParserFieldProps,
onChangeHandler: (value: any) => void
) {
return renderNewMultiSelect("tags", props, onChangeHandler);
}
function renderNewStudioSelect(
props: ISceneParserFieldProps,
onChangeHandler: (value: any) => void
) {
return (
<StudioSelect
noSelectionString=""
className={props.className}
onSelect={items => onChangeHandler(items[0]?.id)}
initialIds={props.parserResult.value ? [props.parserResult.value] : []}
/>
);
}
interface ISceneParserRowProps {
scene: SceneParserResult;
onChange: (changedScene: SceneParserResult) => void;
}
function SceneParserRow(props: ISceneParserRowProps) {
function changeParser(result: ParserResult<any>, set: boolean, value: any) {
const newParser = _.clone(result);
newParser.set = set;
newParser.value = value;
return newParser;
}
function onTitleChanged(set: boolean, value: string | undefined) {
const newResult = _.clone(props.scene);
newResult.title = changeParser(newResult.title, set, value);
props.onChange(newResult);
}
function onDateChanged(set: boolean, value: string | undefined) {
const newResult = _.clone(props.scene);
newResult.date = changeParser(newResult.date, set, value);
props.onChange(newResult);
}
function onPerformerIdsChanged(set: boolean, value: string[] | undefined) {
const newResult = _.clone(props.scene);
newResult.performerIds = changeParser(newResult.performerIds, set, value);
props.onChange(newResult);
}
function onTagIdsChanged(set: boolean, value: string[] | undefined) {
const newResult = _.clone(props.scene);
newResult.tagIds = changeParser(newResult.tagIds, set, value);
props.onChange(newResult);
}
function onStudioIdChanged(set: boolean, value: string | undefined) {
const newResult = _.clone(props.scene);
newResult.studioId = changeParser(newResult.studioId, set, value);
props.onChange(newResult);
}
return (
<tr className="scene-parser-row">
<td className="text-left parser-field-filename">
{props.scene.filename}
</td>
<SceneParserField
key="title"
fieldName="Title"
className="parser-field-title"
parserResult={props.scene.title}
onSetChanged={set =>
onTitleChanged(set, props.scene.title.value ?? undefined)
}
onValueChanged={value => onTitleChanged(props.scene.title.set, value)}
renderOriginalInputField={renderOriginalInputGroup}
renderNewInputField={renderNewInputGroup}
/>
<SceneParserField
key="date"
fieldName="Date"
className="parser-field-date"
parserResult={props.scene.date}
onSetChanged={set =>
onDateChanged(set, props.scene.date.value ?? undefined)
}
onValueChanged={value => onDateChanged(props.scene.date.set, value)}
renderOriginalInputField={renderOriginalInputGroup}
renderNewInputField={renderNewInputGroup}
/>
<SceneParserField
key="performers"
fieldName="Performers"
className="parser-field-performers"
parserResult={props.scene.performerIds}
originalParserResult={props.scene.performers}
onSetChanged={set =>
onPerformerIdsChanged(
set,
props.scene.performerIds.value ?? undefined
)
}
onValueChanged={value =>
onPerformerIdsChanged(props.scene.performerIds.set, value)
}
renderOriginalInputField={renderOriginalSelect}
renderNewInputField={renderNewPerformerSelect}
/>
<SceneParserField
key="tags"
fieldName="Tags"
className="parser-field-tags"
parserResult={props.scene.tagIds}
originalParserResult={props.scene.tags}
onSetChanged={set =>
onTagIdsChanged(set, props.scene.tagIds.value ?? undefined)
}
onValueChanged={value =>
onTagIdsChanged(props.scene.tagIds.set, value)
}
renderOriginalInputField={renderOriginalSelect}
renderNewInputField={renderNewTagSelect}
/>
<SceneParserField
key="studio"
fieldName="Studio"
className="parser-field-studio"
parserResult={props.scene.studioId}
originalParserResult={props.scene.studio}
onSetChanged={set =>
onStudioIdChanged(set, props.scene.studioId.value ?? undefined)
}
onValueChanged={value =>
onStudioIdChanged(props.scene.studioId.set, value)
}
renderOriginalInputField={renderOriginalSelect}
renderNewInputField={renderNewStudioSelect}
/>
</tr>
);
}
function onChange(scene: SceneParserResult, changedScene: SceneParserResult) { function onChange(scene: SceneParserResult, changedScene: SceneParserResult) {
const newResult = [...parserResult]; const newResult = [...parserResult];
@@ -716,7 +292,7 @@ export const SceneFilenameParser: React.FC = () => {
<Table> <Table>
<thead> <thead>
<tr className="scene-parser-row"> <tr className="scene-parser-row">
<th className="w-25">Filename</th> <th className="parser-field-filename">Filename</th>
{renderHeader("Title", allTitleSet, onSelectAllTitleSet)} {renderHeader("Title", allTitleSet, onSelectAllTitleSet)}
{renderHeader("Date", allDateSet, onSelectAllDateSet)} {renderHeader("Date", allDateSet, onSelectAllDateSet)}
{renderHeader( {renderHeader(
@@ -734,6 +310,7 @@ export const SceneFilenameParser: React.FC = () => {
scene={scene} scene={scene}
key={scene.id} key={scene.id}
onChange={changedScene => onChange(scene, changedScene)} onChange={changedScene => onChange(scene, changedScene)}
showFields={showFields}
/> />
))} ))}
</tbody> </tbody>

View File

@@ -0,0 +1,385 @@
import React from "react";
import _ from "lodash";
import { Form } from 'react-bootstrap';
import {
ParseSceneFilenamesQuery,
SlimSceneDataFragment,
} from "src/core/generated-graphql";
import {
PerformerSelect,
TagSelect,
StudioSelect
} from "src/components/Shared";
import { TextUtils } from "src/utils";
class ParserResult<T> {
public value?: T;
public originalValue?: T;
public isSet: boolean = false;
public setOriginalValue(value?: T) {
this.originalValue = value;
this.value = value;
}
public setValue(value?: T) {
if (value) {
this.value = value;
this.isSet = !_.isEqual(this.value, this.originalValue);
}
}
}
export class SceneParserResult {
public id: string;
public filename: string;
public title: ParserResult<string> = new ParserResult<string>();
public date: ParserResult<string> = new ParserResult<string>();
public studio: ParserResult<string> = new ParserResult<string>();
public tags: ParserResult<string[]> = new ParserResult<string[]>();
public performers: ParserResult<string[]> = new ParserResult<string[]>();
public scene: SlimSceneDataFragment;
constructor(
result: ParseSceneFilenamesQuery["parseSceneFilenames"]["results"][0]
) {
this.scene = result.scene;
this.id = this.scene.id;
this.filename = TextUtils.fileNameFromPath(this.scene.path);
this.title.setOriginalValue(this.scene.title ?? undefined);
this.date.setOriginalValue(this.scene.date ?? undefined);
this.performers.setOriginalValue(this.scene.performers.map(p => p.id));
this.tags.setOriginalValue(this.scene.tags.map(t => t.id));
this.studio.setOriginalValue(this.scene.studio?.id);
this.title.setValue(result.title ?? undefined);
this.date.setValue(result.date ?? undefined);
}
// returns true if any of its fields have set == true
public isChanged() {
return (
this.title.isSet ||
this.date.isSet ||
this.performers.isSet ||
this.studio.isSet ||
this.tags.isSet
);
}
public toSceneUpdateInput() {
return {
id: this.id,
details: this.scene.details,
url: this.scene.url,
rating: this.scene.rating,
gallery_id: this.scene.gallery?.id,
title: this.title.isSet
? this.title.value
: this.scene.title,
date: this.date.isSet
? this.date.value
: this.scene.date,
studio_id: this.studio.isSet
? this.studio.value
: this.scene.studio?.id,
performer_ids: this.performers.isSet
? this.performers.value
: this.scene.performers.map(performer => performer.id),
tag_ids: this.tags.isSet
? this.tags.value
: this.scene.tags.map(tag => tag.id)
};
}
}
interface ISceneParserFieldProps<T> {
parserResult: ParserResult<T>;
className?: string;
fieldName: string;
onSetChanged: (isSet: boolean) => void;
onValueChanged: (value: T) => void;
originalParserResult?: ParserResult<T>;
}
function SceneParserStringField(props: ISceneParserFieldProps<string>) {
function maybeValueChanged(value: string) {
if (value !== props.parserResult.value) {
props.onValueChanged(value);
}
}
const result = props.originalParserResult || props.parserResult;
return (
<>
<td>
<Form.Check
checked={props.parserResult.isSet}
onChange={() => {
props.onSetChanged(!props.parserResult.isSet);
}}
/>
</td>
<td>
<Form.Group>
<Form.Control
disabled
className={props.className}
defaultValue={result.originalValue || ""}
/>
<Form.Control
disabled={!props.parserResult.isSet}
className={props.className}
value={props.parserResult.value || ""}
onChange={(event: React.FormEvent<HTMLInputElement>) => maybeValueChanged(event.currentTarget.value)}
/>
</Form.Group>
</td>
</>
);
}
function SceneParserPerformerField(props: ISceneParserFieldProps<string[]>) {
function maybeValueChanged(value: string[]) {
if (value !== props.parserResult.value) {
props.onValueChanged(value);
}
}
const originalPerformers = (props.originalParserResult?.originalValue ?? []) as string[];
const newPerformers = props.parserResult.value ?? [];
return (
<>
<td>
<Form.Check
checked={props.parserResult.isSet}
onChange={() => {
props.onSetChanged(!props.parserResult.isSet);
}}
/>
</td>
<td>
<Form.Group className={props.className}>
<PerformerSelect
isDisabled
isMulti
ids={originalPerformers}
/>
<PerformerSelect
isMulti
onSelect={items => {
maybeValueChanged(items.map(i => i.id));
}}
ids={newPerformers}
/>
</Form.Group>
</td>
</>
);
}
function SceneParserTagField(props: ISceneParserFieldProps<string[]>) {
function maybeValueChanged(value: string[]) {
if (value !== props.parserResult.value) {
props.onValueChanged(value);
}
}
const originalTags = props.originalParserResult?.originalValue ?? [];
const newTags = props.parserResult.value ?? [];
return (
<>
<td>
<Form.Check
checked={props.parserResult.isSet}
onChange={() => {
props.onSetChanged(!props.parserResult.isSet);
}}
/>
</td>
<td>
<Form.Group className={props.className}>
<TagSelect
isDisabled
isMulti
ids={originalTags}
/>
<TagSelect
isMulti
onSelect={items => {
maybeValueChanged(items.map(i => i.id));
}}
ids={newTags}
/>
</Form.Group>
</td>
</>
);
}
function SceneParserStudioField(props: ISceneParserFieldProps<string>) {
function maybeValueChanged(value: string) {
if (value !== props.parserResult.value) {
props.onValueChanged(value);
}
}
const originalStudio = props.originalParserResult?.originalValue ? [props.originalParserResult?.originalValue] : [];
const newStudio = props.parserResult.value ? [props.parserResult.value] : [];
return (
<>
<td>
<Form.Check
checked={props.parserResult.isSet}
onChange={() => {
props.onSetChanged(!props.parserResult.isSet);
}}
/>
</td>
<td>
<Form.Group className={props.className}>
<StudioSelect
isDisabled
ids={originalStudio}
/>
<StudioSelect
onSelect={items => {
maybeValueChanged(items[0].id);
}}
ids={newStudio}
/>
</Form.Group>
</td>
</>
);
}
interface ISceneParserRowProps {
scene: SceneParserResult;
onChange: (changedScene: SceneParserResult) => void;
showFields: Map<string, boolean>;
}
export const SceneParserRow = (props: ISceneParserRowProps) => {
function changeParser<T>(result: ParserResult<T>, isSet: boolean, value: T) {
const newParser = _.clone(result);
newParser.isSet = isSet;
newParser.value = value;
return newParser;
}
function onTitleChanged(set: boolean, value: string) {
const newResult = _.clone(props.scene);
newResult.title = changeParser(newResult.title, set, value);
props.onChange(newResult);
}
function onDateChanged(set: boolean, value: string) {
const newResult = _.clone(props.scene);
newResult.date = changeParser(newResult.date, set, value);
props.onChange(newResult);
}
function onPerformerIdsChanged(set: boolean, value: string[]) {
const newResult = _.clone(props.scene);
newResult.performers = changeParser(newResult.performers, set, value);
props.onChange(newResult);
}
function onTagIdsChanged(set: boolean, value: string[]) {
const newResult = _.clone(props.scene);
newResult.tags= changeParser(newResult.tags, set, value);
props.onChange(newResult);
}
function onStudioIdChanged(set: boolean, value: string) {
const newResult = _.clone(props.scene);
newResult.studio = changeParser(newResult.studio, set, value);
props.onChange(newResult);
}
return (
<tr className="scene-parser-row">
<td className="text-left parser-field-filename">
{props.scene.filename}
</td>
{ props.showFields.get("Title") && (
<SceneParserStringField
key="title"
fieldName="Title"
className="parser-field-title"
parserResult={props.scene.title}
onSetChanged={isSet =>
onTitleChanged(isSet, props.scene.title.value ?? '')
}
onValueChanged={value => onTitleChanged(props.scene.title.isSet, value)}
/>
)}
{ props.showFields.get("Date") && (
<SceneParserStringField
key="date"
fieldName="Date"
className="parser-field-date"
parserResult={props.scene.date}
onSetChanged={isSet =>
onDateChanged(isSet, props.scene.date.value ?? '')
}
onValueChanged={value => onDateChanged(props.scene.date.isSet, value)}
/>
)}
{ props.showFields.get("Performers") && (
<SceneParserPerformerField
key="performers"
fieldName="Performers"
className="parser-field-performers"
parserResult={props.scene.performers}
originalParserResult={props.scene.performers}
onSetChanged={set =>
onPerformerIdsChanged(
set,
props.scene.performers.value ?? []
)
}
onValueChanged={value =>
onPerformerIdsChanged(props.scene.performers.isSet, value)
}
/>
)}
{ props.showFields.get("Tags") && (
<SceneParserTagField
key="tags"
fieldName="Tags"
className="parser-field-tags"
parserResult={props.scene.tags}
originalParserResult={props.scene.tags}
onSetChanged={isSet =>
onTagIdsChanged(isSet, props.scene.tags.value ?? [])
}
onValueChanged={value =>
onTagIdsChanged(props.scene.tags.isSet, value)
}
/>
)}
{ props.showFields.get("Studio") && (
<SceneParserStudioField
key="studio"
fieldName="Studio"
className="parser-field-studio"
parserResult={props.scene.studio}
originalParserResult={props.scene.studio}
onSetChanged={set =>
onStudioIdChanged(set, props.scene.studio.value ?? '')
}
onValueChanged={value => onStudioIdChanged(props.scene.studio.isSet, value)}
/>
)}
</tr>
);
}

View File

@@ -1,10 +1,13 @@
.scene-parser-results { .scene-parser-results {
margin-left: 31ch;
overflow-x: auto; overflow-x: auto;
} }
.scene-parser-row { .scene-parser-row {
.parser-field-filename { .parser-field-filename {
width: 10ch; left: 1ch;
position: absolute;
width: 30ch;
} }
.parser-field-title { .parser-field-title {
@@ -16,15 +19,15 @@
} }
.parser-field-performers { .parser-field-performers {
width: 20ch; width: 30ch;
} }
.parser-field-tags { .parser-field-tags {
width: 20ch; width: 30ch;
} }
.parser-field-studio { .parser-field-studio {
width: 15ch; width: 20ch;
} }
.form-control { .form-control {
@@ -34,4 +37,9 @@
.form-control + .form-control { .form-control + .form-control {
margin-top: .5rem; margin-top: .5rem;
} }
.badge-items {
background-color: #e9ecef;
margin-bottom: .25rem;
}
} }

View File

@@ -10,13 +10,15 @@ interface IScenePlayerProps {
scene: GQL.SceneDataFragment; scene: GQL.SceneDataFragment;
timestamp: number; timestamp: number;
autoplay?: boolean; autoplay?: boolean;
onReady?: any; onReady?: () => void;
onSeeked?: any; onSeeked?: () => void;
onTime?: any; onTime?: () => void;
config?: GQL.ConfigInterfaceDataFragment; config?: GQL.ConfigInterfaceDataFragment;
} }
interface IScenePlayerState { interface IScenePlayerState {
scrubberPosition: number; scrubberPosition: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
config: Record<string, any>;
} }
const KeyMap = { const KeyMap = {
@@ -30,6 +32,8 @@ export class ScenePlayerImpl extends React.Component<
IScenePlayerProps, IScenePlayerProps,
IScenePlayerState IScenePlayerState
> { > {
// Typings for jwplayer are, unfortunately, very lacking
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private player: any; private player: any;
private lastTime = 0; private lastTime = 0;
@@ -57,7 +61,18 @@ export class ScenePlayerImpl extends React.Component<
this.onScrubberSeek = this.onScrubberSeek.bind(this); this.onScrubberSeek = this.onScrubberSeek.bind(this);
this.onScrubberScrolled = this.onScrubberScrolled.bind(this); this.onScrubberScrolled = this.onScrubberScrolled.bind(this);
this.state = { scrubberPosition: 0 }; this.state = {
scrubberPosition: 0,
config: this.makeJWPlayerConfig(props.scene)
};
}
public UNSAFE_componentWillReceiveProps(props: IScenePlayerProps) {
if(props.scene !== this.props.scene) {
this.setState( state => (
{ ...state, config: this.makeJWPlayerConfig(this.props.scene) }
));
}
} }
public componentDidUpdate(prevProps: IScenePlayerProps) { public componentDidUpdate(prevProps: IScenePlayerProps) {
@@ -114,9 +129,7 @@ export class ScenePlayerImpl extends React.Component<
} }
private shouldRepeat(scene: GQL.SceneDataFragment) { private shouldRepeat(scene: GQL.SceneDataFragment) {
const maxLoopDuration = this.props.config const maxLoopDuration = this.state?.config.maximumLoopDuration ?? 0;
? this.props.config.maximumLoopDuration
: 0;
return ( return (
!!scene.file.duration && !!scene.file.duration &&
!!maxLoopDuration && !!maxLoopDuration &&
@@ -132,25 +145,25 @@ export class ScenePlayerImpl extends React.Component<
const repeat = this.shouldRepeat(scene); const repeat = this.shouldRepeat(scene);
let getDurationHook: (() => GQL.Maybe<number>) | undefined; let getDurationHook: (() => GQL.Maybe<number>) | undefined;
let seekHook: let seekHook:
| ((seekToPosition: number, _videoTag: any) => void) | ((seekToPosition: number, _videoTag: HTMLVideoElement) => void)
| undefined; | undefined;
let getCurrentTimeHook: ((_videoTag: any) => number) | undefined; let getCurrentTimeHook: ((_videoTag: HTMLVideoElement) => number) | undefined;
if (!this.props.scene.is_streamable) { if (!this.props.scene.is_streamable) {
getDurationHook = () => { getDurationHook = () => {
return this.props.scene.file.duration ?? null; return this.props.scene.file.duration ?? null;
}; };
seekHook = (seekToPosition: number, _videoTag: any) => { seekHook = (seekToPosition: number, _videoTag: HTMLVideoElement) => {
// eslint-disable-next-line no-param-reassign /* eslint-disable no-param-reassign */
_videoTag.start = seekToPosition; _videoTag.dataset.start = seekToPosition.toString();
// eslint-disable-next-line no-param-reassign
_videoTag.src = `${this.props.scene.paths.stream}?start=${seekToPosition}`; _videoTag.src = `${this.props.scene.paths.stream}?start=${seekToPosition}`;
/* eslint-enable no-param-reassign */
_videoTag.play(); _videoTag.play();
}; };
getCurrentTimeHook = (_videoTag: any) => { getCurrentTimeHook = (_videoTag: HTMLVideoElement) => {
const start = _videoTag.start || 0; const start = Number.parseInt(_videoTag.dataset?.start ?? '0', 10);
return _videoTag.currentTime + start; return _videoTag.currentTime + start;
}; };
} }
@@ -189,20 +202,6 @@ export class ScenePlayerImpl extends React.Component<
return ret; return ret;
} }
renderPlayer() {
const config = this.makeJWPlayerConfig(this.props.scene);
return (
<ReactJWPlayer
playerId={JWUtils.playerID}
playerScript="/jwplayer/jwplayer.js"
customProps={config}
onReady={this.onReady}
onSeeked={this.onSeeked}
onTime={this.onTime}
/>
);
}
public render() { public render() {
return ( return (
<HotKeys <HotKeys
@@ -214,7 +213,14 @@ export class ScenePlayerImpl extends React.Component<
id="jwplayer-container" id="jwplayer-container"
className="w-100 col-sm-9 m-sm-auto no-gutter" className="w-100 col-sm-9 m-sm-auto no-gutter"
> >
{this.renderPlayer()} <ReactJWPlayer
playerId={JWUtils.playerID}
playerScript="/jwplayer/jwplayer.js"
customProps={this.state.config}
onReady={this.onReady}
onSeeked={this.onSeeked}
onTime={this.onTime}
/>
<ScenePlayerScrubber <ScenePlayerScrubber
scene={this.props.scene} scene={this.props.scene}
position={this.state.scrubberPosition} position={this.state.scrubberPosition}

View File

@@ -78,8 +78,8 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (
const positionIndicatorEl = useRef<HTMLDivElement>(null); const positionIndicatorEl = useRef<HTMLDivElement>(null);
const scrubberSliderEl = useRef<HTMLDivElement>(null); const scrubberSliderEl = useRef<HTMLDivElement>(null);
const mouseDown = useRef(false); const mouseDown = useRef(false);
const lastMouseEvent = useRef<any>(null); const lastMouseEvent = useRef<MouseEvent|null>(null);
const startMouseEvent = useRef<any>(null); const startMouseEvent = useRef<MouseEvent|null>(null);
const velocity = useRef(0); const velocity = useRef(0);
const _position = useRef(0); const _position = useRef(0);
@@ -228,7 +228,7 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (
} }
// negative dragging right (past), positive left (future) // negative dragging right (past), positive left (future)
const delta = event.clientX - lastMouseEvent.current.clientX; const delta = event.clientX - (lastMouseEvent.current?.clientX ?? 0);
const movement = event.movementX; const movement = event.movementX;
velocity.current = movement; velocity.current = movement;
@@ -279,10 +279,10 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (
return {}; return {};
} }
let tag: any; let tag: Element|null;
for (let index = 0; index < tags.length; index++) { for (let index = 0; index < tags.length; index++) {
tag = tags.item(index) as any; tag = tags.item(index);
const id = tag.getAttribute("data-marker-id"); const id = tag?.getAttribute("data-marker-id") ?? null;
if (id === i.toString()) { if (id === i.toString()) {
break; break;
} }
@@ -293,7 +293,7 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (
const percentage = marker.seconds / duration; const percentage = marker.seconds / duration;
const left = const left =
scrubberSliderEl.current.scrollWidth * percentage - tag.clientWidth / 2; scrubberSliderEl.current.scrollWidth * percentage - tag!.clientWidth / 2;
return { return {
left: `${left}px`, left: `${left}px`,
height: 20 height: 20

View File

@@ -305,7 +305,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
<td>URL</td> <td>URL</td>
<td> <td>
<Form.Control <Form.Control
onChange={(newValue: any) => setUrl(newValue.target.value)} onChange={(newValue: React.FormEvent<HTMLInputElement>) => setUrl(newValue.currentTarget.value)}
value={url} value={url}
placeholder="URL" placeholder="URL"
/> />
@@ -376,7 +376,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
<Form.Control <Form.Control
as="textarea" as="textarea"
className="scene-description" className="scene-description"
onChange={(newValue: any) => setDetails(newValue.target.value)} onChange={(newValue: React.FormEvent<HTMLTextAreaElement>) => setDetails(newValue.currentTarget.value)}
value={details} value={details}
/> />
</Form.Group> </Form.Group>

View File

@@ -60,7 +60,7 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
sceneMarkers={props.scene.scene_markers} sceneMarkers={props.scene.scene_markers}
clickHandler={marker => { clickHandler={marker => {
window.scrollTo(0, 0); window.scrollTo(0, 0);
onClickMarker(marker as any); onClickMarker(marker as GQL.SceneMarkerDataFragment);
}} }}
/> />
</div> </div>

View File

@@ -265,7 +265,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
<Form.Control <Form.Control
as="select" as="select"
value={rating} value={rating}
onChange={(event: any) => setRating(event.target.value)} onChange={(event: React.FormEvent<HTMLSelectElement>) => setRating(event.currentTarget.value)}
> >
{["", "1", "2", "3", "4", "5"].map(opt => ( {["", "1", "2", "3", "4", "5"].map(opt => (
<option key={opt} value={opt}> <option key={opt} value={opt}>

View File

@@ -155,7 +155,7 @@ export const SettingsAboutPanel: React.FC = () => {
{!dataLatest || loadingLatest || networkStatus === 4 ? ( {!dataLatest || loadingLatest || networkStatus === 4 ? (
<LoadingIndicator inline /> <LoadingIndicator inline />
) : ( ) : (
<>{renderLatestVersion()}</> renderLatestVersion()
)} )}
</> </>
); );

View File

@@ -175,7 +175,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Control <Form.Control
className="col col-sm-6" className="col col-sm-6"
defaultValue={databasePath} defaultValue={databasePath}
onChange={(e: any) => setDatabasePath(e.target.value)} onChange={(e: React.FormEvent<HTMLInputElement>) => setDatabasePath(e.currentTarget.value)}
/> />
<Form.Text className="text-muted"> <Form.Text className="text-muted">
File location for the SQLite database (requires restart) File location for the SQLite database (requires restart)
@@ -187,7 +187,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Control <Form.Control
className="col col-sm-6" className="col col-sm-6"
defaultValue={generatedPath} defaultValue={generatedPath}
onChange={(e: any) => setGeneratedPath(e.target.value)} onChange={(e: React.FormEvent<HTMLInputElement>) => setGeneratedPath(e.currentTarget.value)}
/> />
<Form.Text className="text-muted"> <Form.Text className="text-muted">
Directory location for the generated files (scene markers, scene Directory location for the generated files (scene markers, scene
@@ -204,8 +204,8 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Control <Form.Control
className="col col-sm-6" className="col col-sm-6"
value={regexp} value={regexp}
onChange={(e: any) => onChange={(e: React.FormEvent<HTMLInputElement>) =>
excludeRegexChanged(i, e.target.value) excludeRegexChanged(i, e.currentTarget.value)
} }
/> />
<InputGroup.Append> <InputGroup.Append>

View File

@@ -141,7 +141,7 @@ export const SettingsInterfacePanel: React.FC = () => {
<Form.Control <Form.Control
as="textarea" as="textarea"
value={css} value={css}
onChange={(e: any) => setCSS(e.target.value)} onChange={(e: React.FormEvent<HTMLTextAreaElement>) => setCSS(e.currentTarget.value)}
rows={16} rows={16}
className="col col-sm-6" className="col col-sm-6"
></Form.Control> ></Form.Control>

View File

@@ -78,7 +78,7 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
className="duration-control" className="duration-control"
disabled={props.disabled} disabled={props.disabled}
value={value} value={value}
onChange={(e: any) => setValue(e.target.value)} onChange={(e: React.FormEvent<HTMLInputElement>) => setValue(e.currentTarget.value)}
onBlur={() => onBlur={() =>
props.onValueChange(DurationUtils.stringToSeconds(value)) props.onValueChange(DurationUtils.stringToSeconds(value))
} }

View File

@@ -51,7 +51,7 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
<InputGroup> <InputGroup>
<Form.Control <Form.Control
placeholder="File path" placeholder="File path"
onChange={(e: any) => setCurrentDirectory(e.target.value)} onChange={(e: React.FormEvent<HTMLInputElement>) => setCurrentDirectory(e.currentTarget.value)}
defaultValue={currentDirectory} defaultValue={currentDirectory}
/> />
<InputGroup.Append> <InputGroup.Append>

View File

@@ -19,11 +19,12 @@ interface ITypeProps {
interface IFilterProps { interface IFilterProps {
ids?: string[]; ids?: string[];
initialIds?: string[]; initialIds?: string[];
onSelect: (item: ValidTypes[]) => void; onSelect?: (item: ValidTypes[]) => void;
noSelectionString?: string; noSelectionString?: string;
className?: string; className?: string;
isMulti?: boolean; isMulti?: boolean;
isClearable?: boolean; isClearable?: boolean;
isDisabled?: boolean;
} }
interface ISelectProps { interface ISelectProps {
className?: string; className?: string;
@@ -32,6 +33,7 @@ interface ISelectProps {
creatable?: boolean; creatable?: boolean;
onCreateOption?: (value: string) => void; onCreateOption?: (value: string) => void;
isLoading: boolean; isLoading: boolean;
isDisabled?: boolean;
onChange: (item: ValueType<Option>) => void; onChange: (item: ValueType<Option>) => void;
initialIds?: string[]; initialIds?: string[];
isMulti?: boolean; isMulti?: boolean;
@@ -183,7 +185,7 @@ export const PerformerSelect: React.FC<IFilterProps> = props => {
const onChange = (selectedItems: ValueType<Option>) => { const onChange = (selectedItems: ValueType<Option>) => {
const selectedIds = getSelectedValues(selectedItems); const selectedIds = getSelectedValues(selectedItems);
props.onSelect( props.onSelect?.(
normalizedData.filter(item => selectedIds.indexOf(item.id) !== -1) normalizedData.filter(item => selectedIds.indexOf(item.id) !== -1)
); );
}; };
@@ -216,7 +218,7 @@ export const StudioSelect: React.FC<IFilterProps> = props => {
const onChange = (selectedItems: ValueType<Option>) => { const onChange = (selectedItems: ValueType<Option>) => {
const selectedIds = getSelectedValues(selectedItems); const selectedIds = getSelectedValues(selectedItems);
props.onSelect( props.onSelect?.(
normalizedData.filter(item => selectedIds.indexOf(item.id) !== -1) normalizedData.filter(item => selectedIds.indexOf(item.id) !== -1)
); );
}; };
@@ -262,7 +264,7 @@ export const TagSelect: React.FC<IFilterProps> = props => {
if (result?.data?.tagCreate) { if (result?.data?.tagCreate) {
setSelectedIds([...selectedIds, result.data.tagCreate.id]); setSelectedIds([...selectedIds, result.data.tagCreate.id]);
props.onSelect( props.onSelect?.(
[...tags, result.data.tagCreate].filter( [...tags, result.data.tagCreate].filter(
item => selectedIds.indexOf(item.id) !== -1 item => selectedIds.indexOf(item.id) !== -1
) )
@@ -285,7 +287,7 @@ export const TagSelect: React.FC<IFilterProps> = props => {
const onChange = (selectedItems: ValueType<Option>) => { const onChange = (selectedItems: ValueType<Option>) => {
const selectedValues = getSelectedValues(selectedItems); const selectedValues = getSelectedValues(selectedItems);
setSelectedIds(selectedValues); setSelectedIds(selectedValues);
props.onSelect(tags.filter(item => selectedValues.indexOf(item.id) !== -1)); props.onSelect?.(tags.filter(item => selectedValues.indexOf(item.id) !== -1));
}; };
return ( return (
@@ -311,6 +313,7 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
items, items,
selectedOptions, selectedOptions,
isLoading, isLoading,
isDisabled = false,
onCreateOption, onCreateOption,
isClearable = true, isClearable = true,
creatable = false, creatable = false,
@@ -337,10 +340,12 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
...base, ...base,
color: "#000" color: "#000"
}), }),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
container: (base: CSSProperties, state: any) => ({ container: (base: CSSProperties, state: any) => ({
...base, ...base,
zIndex: state.isFocused ? 10 : base.zIndex zIndex: state.isFocused ? 10 : base.zIndex
}), }),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
multiValueRemove: (base: CSSProperties, state: any) => ({ multiValueRemove: (base: CSSProperties, state: any) => ({
...base, ...base,
color: state.isFocused ? base.color : "#333333" color: state.isFocused ? base.color : "#333333"
@@ -356,20 +361,22 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
isClearable, isClearable,
defaultValue, defaultValue,
noOptionsMessage: () => (type !== "tags" ? "None" : null), noOptionsMessage: () => (type !== "tags" ? "None" : null),
placeholder, placeholder: isDisabled ? '' : placeholder,
onInputChange, onInputChange,
isDisabled,
isLoading, isLoading,
styles, styles,
components: { components: {
IndicatorSeparator: () => null, IndicatorSeparator: () => null,
...(!showDropdown && { DropdownIndicator: () => null }) ...((!showDropdown || isDisabled) && { DropdownIndicator: () => null }),
...(isDisabled && { MultiValueRemove: () => null })
} }
}; };
return creatable ? ( return creatable ? (
<CreatableSelect <CreatableSelect
{...props} {...props}
isDisabled={isLoading} isDisabled={isLoading || isDisabled}
onCreateOption={onCreateOption} onCreateOption={onCreateOption}
/> />
) : ( ) : (

View File

@@ -14,7 +14,7 @@ export const StudioScenesPanel: React.FC<IStudioScenesPanel> = ({ studio }) => {
// 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.type === "studios";
}); }) as StudiosCriterion;
if ( if (
studioCriterion && studioCriterion &&
@@ -23,7 +23,7 @@ export const StudioScenesPanel: React.FC<IStudioScenesPanel> = ({ studio }) => {
) { ) {
// add the studio if not present // add the studio if not present
if ( if (
!studioCriterion.value.find((p: any) => { !studioCriterion.value.find(p => {
return p.id === studio.id; return p.id === studio.id;
}) })
) { ) {

View File

@@ -160,7 +160,7 @@ export const TagList: React.FC = () => {
<Form.Group controlId="tag-name"> <Form.Group controlId="tag-name">
<Form.Label>Name</Form.Label> <Form.Label>Name</Form.Label>
<Form.Control <Form.Control
onChange={(newValue: any) => setName(newValue.target.value)} onChange={(newValue:React.FormEvent<HTMLInputElement>) => setName(newValue.currentTarget.value)}
defaultValue={(editingTag && editingTag.name) || ""} defaultValue={(editingTag && editingTag.name) || ""}
/> />
</Form.Group> </Form.Group>

View File

@@ -1,6 +1,6 @@
import ApolloClient from "apollo-client"; import ApolloClient from "apollo-client";
import { WebSocketLink } from "apollo-link-ws"; import { WebSocketLink } from "apollo-link-ws";
import { InMemoryCache } from "apollo-cache-inmemory"; import { InMemoryCache, NormalizedCacheObject } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http"; import { HttpLink } from "apollo-link-http";
import { split } from "apollo-link"; import { split } from "apollo-link";
import { getMainDefinition } from "apollo-utilities"; import { getMainDefinition } from "apollo-utilities";
@@ -8,7 +8,7 @@ import { ListFilterModel } from "../models/list-filter/filter";
import * as GQL from "./generated-graphql"; import * as GQL from "./generated-graphql";
export class StashService { export class StashService {
public static client: ApolloClient<any>; public static client: ApolloClient<NormalizedCacheObject>;
private static cache: InMemoryCache; private static cache: InMemoryCache;
public static initialize() { public static initialize() {
@@ -60,12 +60,13 @@ export class StashService {
cache: StashService.cache cache: StashService.cache
}); });
(window as any).StashService = StashService;
return StashService.client; return StashService.client;
} }
// TODO: Invalidation should happen through apollo client, rather than rewriting cache directly
private static invalidateQueries(queries: string[]) { private static invalidateQueries(queries: string[]) {
if (StashService.cache) { if (StashService.cache) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cache = StashService.cache as any; const cache = StashService.cache as any;
const keyMatchers = queries.map(query => { const keyMatchers = queries.map(query => {
return new RegExp(`^${query}`); return new RegExp(`^${query}`);

View File

@@ -75,14 +75,14 @@ interface IQuery<T extends IQueryResult, T2 extends IDataItem> {
const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>( const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
options: IListHookOptions<QueryResult> & IQuery<QueryResult, QueryData> options: IListHookOptions<QueryResult> & IQuery<QueryResult, QueryData>
): IListHookData => { ): IListHookData => {
const [interfaceForage, setInterfaceForage] = useInterfaceLocalForage(); const [interfaceState, setInterfaceState]= useInterfaceLocalForage();
const forageInitialised = useRef(false); const forageInitialised = useRef(false);
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const [filter, setFilter] = useState<ListFilterModel>( const [filter, setFilter] = useState<ListFilterModel>(
new ListFilterModel( new ListFilterModel(
options.filterMode, options.filterMode,
options.subComponent ? "" : queryString.parse(location.search) options.subComponent ? undefined : queryString.parse(location.search)
) )
); );
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set()); const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
@@ -94,7 +94,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
const items = options.getData(result); const items = options.getData(result);
useEffect(() => { useEffect(() => {
if (!forageInitialised.current && !interfaceForage.loading) { if (!forageInitialised.current && !interfaceState.loading) {
forageInitialised.current = true; forageInitialised.current = true;
// Don't use query parameters for sub-components // Don't use query parameters for sub-components
@@ -102,7 +102,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
// Don't read localForage if page already had query parameters // Don't read localForage if page already had query parameters
if (history.location.search) return; if (history.location.search) return;
const queryData = interfaceForage.data?.queries[options.filterMode]; const queryData = interfaceState.data?.queries?.[options.filterMode];
if (!queryData) return; if (!queryData) return;
const newFilter = new ListFilterModel( const newFilter = new ListFilterModel(
@@ -117,8 +117,8 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
history.replace(newLocation); history.replace(newLocation);
} }
}, [ }, [
interfaceForage.data, interfaceState.data,
interfaceForage.loading, interfaceState.loading,
history, history,
options.subComponent, options.subComponent,
options.filterMode options.filterMode
@@ -129,15 +129,14 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
const newFilter = new ListFilterModel( const newFilter = new ListFilterModel(
options.filterMode, options.filterMode,
options.subComponent ? "" : queryString.parse(location.search) options.subComponent ? undefined : queryString.parse(location.search)
); );
setFilter(newFilter); setFilter(newFilter);
if (forageInitialised.current) { if (forageInitialised.current) {
setInterfaceForage(config => { setInterfaceState(config => {
const data = { ...config } as IInterfaceConfig; const data = { ...config } as IInterfaceConfig;
data.queries = { data.queries = {
...config?.queries,
[options.filterMode]: { [options.filterMode]: {
filter: location.search, filter: location.search,
itemsPerPage: newFilter.itemsPerPage, itemsPerPage: newFilter.itemsPerPage,
@@ -147,7 +146,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
return data; return data;
}); });
} }
}, [location, options.filterMode, options.subComponent, setInterfaceForage]); }, [location, options.filterMode, options.subComponent, setInterfaceState]);
function getFilter() { function getFilter() {
if (!options.filterHook) { if (!options.filterHook) {
@@ -216,7 +215,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
// Remove duplicate modifiers // Remove duplicate modifiers
newFilter.criteria = newFilter.criteria.filter((obj, pos, arr) => { newFilter.criteria = newFilter.criteria.filter((obj, pos, arr) => {
return ( return (
arr.map((mapObj: any) => mapObj.getId()).indexOf(obj.getId()) === pos arr.map(mapObj => mapObj.getId()).indexOf(obj.getId()) === pos
); );
}); });

View File

@@ -1,74 +1,75 @@
import localForage from "localforage"; import localForage from "localforage";
import _ from "lodash"; import _ from "lodash";
import React, { Dispatch, SetStateAction } from "react"; import React, { Dispatch, SetStateAction, useEffect } from "react";
interface IInterfaceWallConfig {} interface IInterfaceWallConfig {}
export interface IInterfaceConfig { interface IInterfaceQueryConfig {
wall: IInterfaceWallConfig; filter: string;
queries: any; itemsPerPage: number;
currentPage: number;
} }
type ValidTypes = IInterfaceConfig | undefined; export interface IInterfaceConfig {
wall?: IInterfaceWallConfig;
queries?: Record<string, IInterfaceQueryConfig>;
}
type ValidTypes = IInterfaceConfig;
type Key = "interface";
interface ILocalForage<T> { interface ILocalForage<T> {
data: T; data?: T;
setData: Dispatch<SetStateAction<T>>;
error: Error | null; error: Error | null;
loading: boolean; loading: boolean;
} }
function useLocalForage(item: string): ILocalForage<ValidTypes> { const Loading:Record<string, boolean> = {};
const [json, setJson] = React.useState<ValidTypes>(undefined); const Cache:Record<string, ValidTypes> = {};
const [err, setErr] = React.useState(null);
const [loaded, setLoaded] = React.useState<boolean>(false);
const prevJson = React.useRef<ValidTypes>(undefined); function useLocalForage(key: Key): [ILocalForage<ValidTypes>, Dispatch<SetStateAction<ValidTypes>>] {
React.useEffect(() => { const [error, setError] = React.useState(null);
async function runAsync() { const [data, setData] = React.useState(Cache[key]);
if (typeof json !== "undefined" && !_.isEqual(json, prevJson.current)) { const [loading, setLoading] = React.useState(Loading[key]);
await localForage.setItem(item, JSON.stringify(json));
}
prevJson.current = json;
}
runAsync();
});
React.useEffect(() => { useEffect(() => {
async function runAsync() { async function runAsync() {
try { try {
const serialized = await localForage.getItem<any>(item); const serialized = await localForage.getItem<string>(key);
const parsed = JSON.parse(serialized); const parsed = JSON.parse(serialized);
if (typeof json === "undefined" && !Object.is(parsed, null)) { if (!Object.is(parsed, null)) {
setErr(null); setError(null);
setJson(parsed); setData(parsed);
Cache[key] = parsed;
} }
} catch (error) { } catch (err) {
setErr(error); setError(err);
} finally {
Loading[key] = false;
setLoading(false);
} }
setLoaded(true);
} }
if(!loading && !Cache[key]) {
Loading[key] = true;
setLoading(true);
runAsync(); runAsync();
}
}, [loading, data, key]);
useEffect(() => {
if (!_.isEqual(Cache[key], data)) {
Cache[key] = _.merge(Cache[key], data);
localForage.setItem(key, JSON.stringify(Cache[key]));
}
}); });
return { data: json, setData: setJson, error: err, loading: !loaded }; const isLoading = loading || loading === undefined;
return [{ data, error, loading: isLoading }, setData];
} }
export function useInterfaceLocalForage(): [ export function useInterfaceLocalForage():
ILocalForage<IInterfaceConfig | undefined>, [ILocalForage<IInterfaceConfig>,
Dispatch<SetStateAction<IInterfaceConfig | undefined>> Dispatch<SetStateAction<IInterfaceConfig>>]
] { {
const result = useLocalForage("interface"); return useLocalForage("interface");
let returnVal = result;
if (!result.data?.queries) {
returnVal = {
...result,
data: {
wall: {},
queries: {}
}
};
}
return [returnVal, result.setData];
} }

View File

@@ -26,7 +26,7 @@ export const ToastProvider: React.FC = ({ children }) => {
key={toast.id} key={toast.id}
onClose={() => removeToast(toast.id)} onClose={() => removeToast(toast.id)}
className={toast.variant ?? "success"} className={toast.variant ?? "success"}
delay={toast.delay ?? 5000} delay={toast.delay ?? 3000}
> >
<Toast.Header> <Toast.Header>
<span className="mr-auto">{toast.header ?? "Stash"}</span> <span className="mr-auto">{toast.header ?? "Stash"}</span>

View File

@@ -2,7 +2,7 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import DurationUtils from "src/utils/duration"; import DurationUtils from "src/utils/duration";
import { ILabeledId, ILabeledValue } from "../types"; import { ILabeledId, ILabeledValue, IOptionType } from "../types";
export type CriterionType = export type CriterionType =
| "none" | "none"
@@ -30,7 +30,10 @@ export type CriterionType =
| "piercings" | "piercings"
| "aliases"; | "aliases";
export abstract class Criterion<Option = any, Value = any> { type Option = string | number | IOptionType;
export type CriterionValue = string | number | ILabeledId[];
export abstract class Criterion {
public static getLabel(type: CriterionType = "none") { public static getLabel(type: CriterionType = "none") {
switch (type) { switch (type) {
case "none": case "none":
@@ -114,9 +117,17 @@ export abstract class Criterion<Option = any, Value = any> {
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: Value; public abstract value: CriterionValue;
public inputType: "number" | "text" | undefined; 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 getLabel(): string { public getLabel(): string {
let modifierString: string; let modifierString: string;
switch (this.modifier) { switch (this.modifier) {
@@ -163,27 +174,11 @@ export abstract class Criterion<Option = any, Value = any> {
return `${Criterion.getLabel(this.type)} ${modifierString} ${valueString}`; return `${Criterion.getLabel(this.type)} ${modifierString} ${valueString}`;
} }
public getLabelValue() {
let valueString: string;
if (Array.isArray(this.value) && this.value.length > 0) {
let items = this.value;
if ((this.value as ILabeledId[])[0].label) {
items = this.value.map(item => item.label) as any;
}
valueString = items.join(", ");
} else if (typeof this.value === "string") {
valueString = this.value;
} else {
valueString = (this.value as any).toString();
}
return valueString;
}
public getId(): string { public getId(): string {
return `${this.parameterName}-${this.modifier.toString()}`; // TODO add values? return `${this.parameterName}-${this.modifier.toString()}`; // TODO add values?
} }
/*
public set(modifier: CriterionModifier, value: Value) { public set(modifier: CriterionModifier, value: Value) {
this.modifier = modifier; this.modifier = modifier;
if (Array.isArray(this.value)) { if (Array.isArray(this.value)) {
@@ -192,6 +187,7 @@ export abstract class Criterion<Option = any, Value = any> {
this.value = value; this.value = value;
} }
} }
*/
} }
export interface ICriterionOption { export interface ICriterionOption {
@@ -209,7 +205,7 @@ export class CriterionOption implements ICriterionOption {
} }
} }
export class StringCriterion extends Criterion<string, string> { export class StringCriterion extends Criterion {
public type: CriterionType; public type: CriterionType;
public parameterName: string; public parameterName: string;
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.Equals;
@@ -222,6 +218,10 @@ export class StringCriterion extends Criterion<string, string> {
public options: string[] | undefined; public options: string[] | undefined;
public value: string = ""; public value: string = "";
public getLabelValue() {
return this.value;
}
constructor(type: CriterionType, parameterName?: string, options?: string[]) { constructor(type: CriterionType, parameterName?: string, options?: string[]) {
super(); super();
@@ -237,7 +237,7 @@ export class StringCriterion extends Criterion<string, string> {
} }
} }
export class NumberCriterion extends Criterion<number, number> { export class NumberCriterion extends Criterion {
public type: CriterionType; public type: CriterionType;
public parameterName: string; public parameterName: string;
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.Equals;
@@ -252,6 +252,10 @@ export class NumberCriterion extends Criterion<number, number> {
public options: number[] | undefined; public options: number[] | undefined;
public value: number = 0; public value: number = 0;
public getLabelValue() {
return this.value.toString();
}
constructor(type: CriterionType, parameterName?: string, options?: number[]) { constructor(type: CriterionType, parameterName?: string, options?: number[]) {
super(); super();
@@ -267,7 +271,7 @@ export class NumberCriterion extends Criterion<number, number> {
} }
} }
export class DurationCriterion extends Criterion<number, number> { export class DurationCriterion extends Criterion {
public type: CriterionType; public type: CriterionType;
public parameterName: string; public parameterName: string;
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.Equals;

View File

@@ -1,7 +1,7 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { Criterion, CriterionType, ICriterionOption } from "./criterion"; import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class FavoriteCriterion extends Criterion<string, string> { export class FavoriteCriterion extends Criterion {
public type: CriterionType = "favorite"; public type: CriterionType = "favorite";
public parameterName: string = "filter_favorites"; public parameterName: string = "filter_favorites";
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.Equals;

View File

@@ -1,7 +1,7 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { Criterion, CriterionType, ICriterionOption } from "./criterion"; import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class HasMarkersCriterion extends Criterion<string, string> { export class HasMarkersCriterion extends Criterion {
public type: CriterionType = "hasMarkers"; public type: CriterionType = "hasMarkers";
public parameterName: string = "has_markers"; public parameterName: string = "has_markers";
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.Equals;

View File

@@ -1,7 +1,7 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { Criterion, CriterionType, ICriterionOption } from "./criterion"; import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class IsMissingCriterion extends Criterion<string, string> { export class IsMissingCriterion extends Criterion {
public type: CriterionType = "isMissing"; public type: CriterionType = "isMissing";
public parameterName: string = "is_missing"; public parameterName: string = "is_missing";
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.Equals;

View File

@@ -1,13 +1,13 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { Criterion, CriterionType, ICriterionOption } from "./criterion"; import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class NoneCriterion extends Criterion<any, any> { export class NoneCriterion extends Criterion {
public type: CriterionType = "none"; public type: CriterionType = "none";
public parameterName: string = ""; public parameterName: string = "";
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.Equals;
public modifierOptions = []; public modifierOptions = [];
public options: any; public options: undefined;
public value: any; public value: string = "none";
} }
export class NoneCriterionOption implements ICriterionOption { export class NoneCriterionOption implements ICriterionOption {

View File

@@ -1,14 +1,8 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { ILabeledId } from "../types"; import { ILabeledId, IOptionType } from "../types";
import { Criterion, CriterionType, ICriterionOption } from "./criterion"; import { Criterion, CriterionType, ICriterionOption } from "./criterion";
interface IOptionType { export class PerformersCriterion extends Criterion {
id: string;
name?: string;
image_path?: string;
}
export class PerformersCriterion extends Criterion<IOptionType, ILabeledId[]> {
public type: CriterionType = "performers"; public type: CriterionType = "performers";
public parameterName: string = "performers"; public parameterName: string = "performers";
public modifier = CriterionModifier.IncludesAll; public modifier = CriterionModifier.IncludesAll;

View File

@@ -1,8 +1,7 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { Criterion, CriterionType, ICriterionOption } from "./criterion"; import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class RatingCriterion extends Criterion<number, number> { export class RatingCriterion extends Criterion {
// TODO <number, number[]>
public type: CriterionType = "rating"; public type: CriterionType = "rating";
public parameterName: string = "rating"; public parameterName: string = "rating";
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.Equals;

View File

@@ -1,8 +1,7 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { Criterion, CriterionType, ICriterionOption } from "./criterion"; import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class ResolutionCriterion extends Criterion<string, string> { export class ResolutionCriterion extends Criterion {
// TODO <string, string[]>
public type: CriterionType = "resolution"; public type: CriterionType = "resolution";
public parameterName: string = "resolution"; public parameterName: string = "resolution";
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.Equals;

View File

@@ -1,14 +1,8 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { ILabeledId } from "../types"; import { ILabeledId, IOptionType } from "../types";
import { Criterion, CriterionType, ICriterionOption } from "./criterion"; import { Criterion, CriterionType, ICriterionOption } from "./criterion";
interface IOptionType { export class StudiosCriterion extends Criterion {
id: string;
name?: string;
image_path?: string;
}
export class StudiosCriterion extends Criterion<IOptionType, ILabeledId[]> {
public type: CriterionType = "studios"; public type: CriterionType = "studios";
public parameterName: string = "studios"; public parameterName: string = "studios";
public modifier = CriterionModifier.Includes; public modifier = CriterionModifier.Includes;

View File

@@ -1,8 +1,8 @@
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { ILabeledId } from "../types"; import { ILabeledId, IOptionType } from "../types";
import { Criterion, CriterionType, ICriterionOption } from "./criterion"; import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class TagsCriterion extends Criterion<GQL.Tag, ILabeledId[]> { export class TagsCriterion extends Criterion {
public type: CriterionType; public type: CriterionType;
public parameterName: string; public parameterName: string;
public modifier = GQL.CriterionModifier.IncludesAll; public modifier = GQL.CriterionModifier.IncludesAll;
@@ -11,7 +11,7 @@ export class TagsCriterion extends Criterion<GQL.Tag, ILabeledId[]> {
Criterion.getModifierOption(GQL.CriterionModifier.Includes), Criterion.getModifierOption(GQL.CriterionModifier.Includes),
Criterion.getModifierOption(GQL.CriterionModifier.Excludes) Criterion.getModifierOption(GQL.CriterionModifier.Excludes)
]; ];
public options: GQL.Tag[] = []; public options: IOptionType[] = [];
public value: ILabeledId[] = []; public value: ILabeledId[] = [];
constructor(type: "tags" | "sceneTags") { constructor(type: "tags" | "sceneTags") {

View File

@@ -1,4 +1,4 @@
import queryString from "query-string"; import queryString, { ParsedQuery } from "query-string";
import { import {
FindFilterType, FindFilterType,
PerformerFilterType, PerformerFilterType,
@@ -76,14 +76,15 @@ export class ListFilterModel {
public displayMode: DisplayMode = DEFAULT_PARAMS.displayMode; public displayMode: DisplayMode = DEFAULT_PARAMS.displayMode;
public displayModeOptions: DisplayMode[] = []; public displayModeOptions: DisplayMode[] = [];
public criterionOptions: ICriterionOption[] = []; public criterionOptions: ICriterionOption[] = [];
public criteria: Array<Criterion<any, any>> = []; public criteria: Array<Criterion> = [];
public randomSeed = -1; public randomSeed = -1;
private static createCriterionOption(criterion: CriterionType) { private static createCriterionOption(criterion: CriterionType) {
return new CriterionOption(Criterion.getLabel(criterion), criterion); return new CriterionOption(Criterion.getLabel(criterion), criterion);
} }
public constructor(filterMode: FilterMode, rawParms?: any) { public constructor(filterMode: FilterMode, rawParms?: ParsedQuery<string>) {
const params = rawParms as IQueryParameters;
switch (filterMode) { switch (filterMode) {
case FilterMode.Scenes: case FilterMode.Scenes:
this.sortBy = "date"; this.sortBy = "date";
@@ -187,11 +188,10 @@ export class ListFilterModel {
this.displayMode = this.displayModeOptions[0]; this.displayMode = this.displayModeOptions[0];
} }
this.sortByOptions = [...this.sortByOptions, "created_at", "updated_at"]; this.sortByOptions = [...this.sortByOptions, "created_at", "updated_at"];
if (rawParms) this.configureFromQueryParameters(rawParms); if (params) this.configureFromQueryParameters(params);
} }
public configureFromQueryParameters(rawParms: any) { public configureFromQueryParameters(params: IQueryParameters) {
const params = rawParms as IQueryParameters;
if (params.sortby !== undefined) { if (params.sortby !== undefined) {
this.sortBy = params.sortby; this.sortBy = params.sortby;
@@ -226,7 +226,7 @@ export class ListFilterModel {
if (params.c !== undefined) { if (params.c !== undefined) {
this.criteria = []; this.criteria = [];
let jsonParameters: any[]; let jsonParameters: string[];
if (params.c instanceof Array) { if (params.c instanceof Array) {
jsonParameters = params.c; jsonParameters = params.c;
} else { } else {
@@ -268,10 +268,11 @@ export class ListFilterModel {
public makeQueryParameters(): string { public makeQueryParameters(): string {
const encodedCriteria: string[] = []; const encodedCriteria: string[] = [];
this.criteria.forEach(criterion => { this.criteria.forEach(criterion => {
const encodedCriterion: any = {}; const encodedCriterion:Partial<Criterion> = {
encodedCriterion.type = criterion.type; type: criterion.type,
encodedCriterion.value = criterion.value; value: criterion.value,
encodedCriterion.modifier = criterion.modifier; modifier: criterion.modifier,
};
const jsonCriterion = JSON.stringify(encodedCriterion); const jsonCriterion = JSON.stringify(encodedCriterion);
encodedCriteria.push(jsonCriterion); encodedCriteria.push(jsonCriterion);
}); });

View File

@@ -21,3 +21,9 @@ export interface ILabeledValue {
label: string; label: string;
value: string; value: string;
} }
export interface IOptionType {
id: string;
name?: string;
image_path?: string;
}

View File

@@ -1,5 +1,5 @@
declare module "react-jw-player" { declare module "react-jw-player" {
// typing module default export as `any` will allow you to access its members without compiler warning // eslint-disable-next-line @typescript-eslint/no-explicit-any
const ReactJSPlayer: any; const ReactJSPlayer: any;
export default ReactJSPlayer; export default ReactJSPlayer;
} }

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const flattenMessages = (nestedMessages: any, prefix = "") => { const flattenMessages = (nestedMessages: any, prefix = "") => {
if (nestedMessages === null) { if (nestedMessages === null) {
return {}; return {};

View File

@@ -1,4 +1,5 @@
const playerID = "main-jwplayer"; const playerID = "main-jwplayer";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getPlayer = () => (window as any).jwplayer(playerID); const getPlayer = () => (window as any).jwplayer(playerID);
export default { export default {