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": {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": 2,
"lines-between-class-members": "off",
"@typescript-eslint/interface-name-prefix": [
"warn",

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
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 {
Badge,
@ -8,7 +8,8 @@ import {
Dropdown,
Form,
OverlayTrigger,
Tooltip
Tooltip,
SafeAnchor
} from "react-bootstrap";
import { Icon } from "src/components/Shared";
@ -44,8 +45,8 @@ export const ListFilter: React.FC<IListFilterProps> = (
props: IListFilterProps
) => {
const searchCallback = useCallback(
debounce((event: any) => {
props.onChangeQuery(event.target.value);
debounce((value: string) => {
props.onChangeQuery(value);
}, 500),
[props.onChangeQuery]
);
@ -54,14 +55,13 @@ export const ListFilter: React.FC<IListFilterProps> = (
Criterion | undefined
>(undefined);
function onChangePageSize(event: SyntheticEvent<HTMLSelectElement>) {
const val = event!.currentTarget!.value;
function onChangePageSize(event: React.FormEvent<HTMLSelectElement>) {
const val = event.currentTarget.value;
props.onChangePageSize(parseInt(val, 10));
}
function onChangeQuery(event: SyntheticEvent<HTMLInputElement>) {
event.persist();
searchCallback(event);
function onChangeQuery(event: React.FormEvent<HTMLInputElement>) {
searchCallback(event.currentTarget.value);
}
function onChangeSortDirection() {
@ -72,8 +72,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
}
}
function onChangeSortBy(event: React.MouseEvent<any>) {
props.onChangeSortBy(event.currentTarget.text);
function onChangeSortBy(event:React.MouseEvent<SafeAnchor>) {
const target = event.currentTarget as unknown as HTMLAnchorElement;
props.onChangeSortBy(target.text);
}
function onChangeDisplayMode(displayMode: DisplayMode) {
@ -156,6 +157,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
<Badge
className="tag-item"
variant="secondary"
key={criterion.getId()}
onClick={() => onClickCriterionTag(criterion)}
>
{criterion.getLabel()}
@ -241,8 +243,8 @@ export const ListFilter: React.FC<IListFilterProps> = (
min={0}
max={3}
defaultValue={1}
onChange={(event: any) =>
onChangeZoom(Number.parseInt(event.target.value, 10))
onChange={(e: React.FormEvent<HTMLInputElement>) =>
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
let performerCriterion = filter.criteria.find(c => {
return c.type === "performers";
});
}) as PerformersCriterion;
if (
performerCriterion &&
@ -25,7 +25,7 @@ export const PerformerScenesPanel: React.FC<IPerformerDetailsProps> = ({
) {
// add the performer if not present
if (
!performerCriterion.value.find((p: any) => {
!performerCriterion.value.find(p => {
return p.id === performer.id;
})
) {

View file

@ -132,7 +132,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
<InputGroup className="col-8">
<Form.Control
id="filename-pattern"
onChange={(newValue: any) => setPattern(newValue.target.value)}
onChange={(e: React.FormEvent<HTMLInputElement>) => setPattern(e.currentTarget.value)}
value={pattern}
/>
<InputGroup.Append>
@ -158,7 +158,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
<Form.Label className="col-2">Ignored words</Form.Label>
<InputGroup className="col-8">
<Form.Control
onChange={(newValue: any) => setIgnoreWords(newValue.target.value)}
onChange={(e: React.FormEvent<HTMLInputElement>) => setIgnoreWords(e.currentTarget.value)}
value={ignoreWords}
/>
</InputGroup>
@ -174,8 +174,8 @@ export const ParserInput: React.FC<IParserInputProps> = (
</Form.Label>
<InputGroup className="col-8">
<Form.Control
onChange={(newValue: any) =>
setWhitespaceCharacters(newValue.target.value)
onChange={(e: React.FormEvent<HTMLInputElement>) =>
setWhitespaceCharacters(e.currentTarget.value)
}
value={whitespaceCharacters}
/>
@ -229,8 +229,8 @@ export const ParserInput: React.FC<IParserInputProps> = (
<Form.Control
as="select"
options={PAGE_SIZE_OPTIONS}
onChange={(event: any) =>
props.onPageSizeChanged(parseInt(event.target.value, 10))
onChange={(e: React.FormEvent<HTMLInputElement>) =>
props.onPageSizeChanged(parseInt(e.currentTarget.value, 10))
}
defaultValue={props.input.pageSize}
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 */
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 { StashService } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql";
import {
FilterSelect,
StudioSelect,
LoadingIndicator
} from "src/components/Shared";
import { TextUtils } from "src/utils";
import { LoadingIndicator } from "src/components/Shared";
import { useToast } from "src/hooks";
import { Pagination } from "src/components/List/Pagination";
import { IParserInput, ParserInput } from "./ParserInput";
import { ParserField } from "./ParserField";
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;
}
}
import { SceneParserResult, SceneParserRow } from './SceneParserRow';
const initialParserInput = {
pattern: "{title}.{ext}",
@ -309,19 +171,19 @@ export const SceneFilenameParser: React.FC = () => {
useEffect(() => {
const newAllTitleSet = !parserResult.some(r => {
return !r.title.set;
return !r.title.isSet;
});
const newAllDateSet = !parserResult.some(r => {
return !r.date.set;
return !r.date.isSet;
});
const newAllPerformerSet = !parserResult.some(r => {
return !r.performerIds.set;
return !r.performers.isSet;
});
const newAllTagSet = !parserResult.some(r => {
return !r.tagIds.set;
return !r.tags.isSet;
});
const newAllStudioSet = !parserResult.some(r => {
return !r.studioId.set;
return !r.studio.isSet;
});
setAllTitleSet(newAllTitleSet);
@ -335,7 +197,7 @@ export const SceneFilenameParser: React.FC = () => {
const newResult = [...parserResult];
newResult.forEach(r => {
r.title.set = selected;
r.title.isSet = selected;
});
setParserResult(newResult);
@ -346,7 +208,7 @@ export const SceneFilenameParser: React.FC = () => {
const newResult = [...parserResult];
newResult.forEach(r => {
r.date.set = selected;
r.date.isSet = selected;
});
setParserResult(newResult);
@ -357,7 +219,7 @@ export const SceneFilenameParser: React.FC = () => {
const newResult = [...parserResult];
newResult.forEach(r => {
r.performerIds.set = selected;
r.performers.isSet = selected;
});
setParserResult(newResult);
@ -368,7 +230,7 @@ export const SceneFilenameParser: React.FC = () => {
const newResult = [...parserResult];
newResult.forEach(r => {
r.tagIds.set = selected;
r.tags.isSet = selected;
});
setParserResult(newResult);
@ -379,299 +241,13 @@ export const SceneFilenameParser: React.FC = () => {
const newResult = [...parserResult];
newResult.forEach(r => {
r.studioId.set = selected;
r.studio.isSet = selected;
});
setParserResult(newResult);
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) {
const newResult = [...parserResult];
@ -716,7 +292,7 @@ export const SceneFilenameParser: React.FC = () => {
<Table>
<thead>
<tr className="scene-parser-row">
<th className="w-25">Filename</th>
<th className="parser-field-filename">Filename</th>
{renderHeader("Title", allTitleSet, onSelectAllTitleSet)}
{renderHeader("Date", allDateSet, onSelectAllDateSet)}
{renderHeader(
@ -734,6 +310,7 @@ export const SceneFilenameParser: React.FC = () => {
scene={scene}
key={scene.id}
onChange={changedScene => onChange(scene, changedScene)}
showFields={showFields}
/>
))}
</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 {
margin-left: 31ch;
overflow-x: auto;
}
.scene-parser-row {
.parser-field-filename {
width: 10ch;
left: 1ch;
position: absolute;
width: 30ch;
}
.parser-field-title {
@ -16,15 +19,15 @@
}
.parser-field-performers {
width: 20ch;
width: 30ch;
}
.parser-field-tags {
width: 20ch;
width: 30ch;
}
.parser-field-studio {
width: 15ch;
width: 20ch;
}
.form-control {
@ -34,4 +37,9 @@
.form-control + .form-control {
margin-top: .5rem;
}
.badge-items {
background-color: #e9ecef;
margin-bottom: .25rem;
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -265,7 +265,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
<Form.Control
as="select"
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 => (
<option key={opt} value={opt}>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import ApolloClient from "apollo-client";
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 { split } from "apollo-link";
import { getMainDefinition } from "apollo-utilities";
@ -8,7 +8,7 @@ import { ListFilterModel } from "../models/list-filter/filter";
import * as GQL from "./generated-graphql";
export class StashService {
public static client: ApolloClient<any>;
public static client: ApolloClient<NormalizedCacheObject>;
private static cache: InMemoryCache;
public static initialize() {
@ -60,12 +60,13 @@ export class StashService {
cache: StashService.cache
});
(window as any).StashService = StashService;
return StashService.client;
}
// TODO: Invalidation should happen through apollo client, rather than rewriting cache directly
private static invalidateQueries(queries: string[]) {
if (StashService.cache) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cache = StashService.cache as any;
const keyMatchers = queries.map(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>(
options: IListHookOptions<QueryResult> & IQuery<QueryResult, QueryData>
): IListHookData => {
const [interfaceForage, setInterfaceForage] = useInterfaceLocalForage();
const [interfaceState, setInterfaceState]= useInterfaceLocalForage();
const forageInitialised = useRef(false);
const history = useHistory();
const location = useLocation();
const [filter, setFilter] = useState<ListFilterModel>(
new ListFilterModel(
options.filterMode,
options.subComponent ? "" : queryString.parse(location.search)
options.subComponent ? undefined : queryString.parse(location.search)
)
);
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);
useEffect(() => {
if (!forageInitialised.current && !interfaceForage.loading) {
if (!forageInitialised.current && !interfaceState.loading) {
forageInitialised.current = true;
// 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
if (history.location.search) return;
const queryData = interfaceForage.data?.queries[options.filterMode];
const queryData = interfaceState.data?.queries?.[options.filterMode];
if (!queryData) return;
const newFilter = new ListFilterModel(
@ -117,8 +117,8 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
history.replace(newLocation);
}
}, [
interfaceForage.data,
interfaceForage.loading,
interfaceState.data,
interfaceState.loading,
history,
options.subComponent,
options.filterMode
@ -129,15 +129,14 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
const newFilter = new ListFilterModel(
options.filterMode,
options.subComponent ? "" : queryString.parse(location.search)
options.subComponent ? undefined : queryString.parse(location.search)
);
setFilter(newFilter);
if (forageInitialised.current) {
setInterfaceForage(config => {
setInterfaceState(config => {
const data = { ...config } as IInterfaceConfig;
data.queries = {
...config?.queries,
[options.filterMode]: {
filter: location.search,
itemsPerPage: newFilter.itemsPerPage,
@ -147,7 +146,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
return data;
});
}
}, [location, options.filterMode, options.subComponent, setInterfaceForage]);
}, [location, options.filterMode, options.subComponent, setInterfaceState]);
function getFilter() {
if (!options.filterHook) {
@ -216,7 +215,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
// Remove duplicate modifiers
newFilter.criteria = newFilter.criteria.filter((obj, pos, arr) => {
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 _ from "lodash";
import React, { Dispatch, SetStateAction } from "react";
import React, { Dispatch, SetStateAction, useEffect } from "react";
interface IInterfaceWallConfig {}
export interface IInterfaceConfig {
wall: IInterfaceWallConfig;
queries: any;
interface IInterfaceQueryConfig {
filter: string;
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> {
data: T;
setData: Dispatch<SetStateAction<T>>;
data?: T;
error: Error | null;
loading: boolean;
}
function useLocalForage(item: string): ILocalForage<ValidTypes> {
const [json, setJson] = React.useState<ValidTypes>(undefined);
const [err, setErr] = React.useState(null);
const [loaded, setLoaded] = React.useState<boolean>(false);
const Loading:Record<string, boolean> = {};
const Cache:Record<string, ValidTypes> = {};
const prevJson = React.useRef<ValidTypes>(undefined);
React.useEffect(() => {
async function runAsync() {
if (typeof json !== "undefined" && !_.isEqual(json, prevJson.current)) {
await localForage.setItem(item, JSON.stringify(json));
}
prevJson.current = json;
}
runAsync();
});
function useLocalForage(key: Key): [ILocalForage<ValidTypes>, Dispatch<SetStateAction<ValidTypes>>] {
const [error, setError] = React.useState(null);
const [data, setData] = React.useState(Cache[key]);
const [loading, setLoading] = React.useState(Loading[key]);
React.useEffect(() => {
useEffect(() => {
async function runAsync() {
try {
const serialized = await localForage.getItem<any>(item);
const serialized = await localForage.getItem<string>(key);
const parsed = JSON.parse(serialized);
if (typeof json === "undefined" && !Object.is(parsed, null)) {
setErr(null);
setJson(parsed);
if (!Object.is(parsed, null)) {
setError(null);
setData(parsed);
Cache[key] = parsed;
}
} catch (error) {
setErr(error);
} catch (err) {
setError(err);
} finally {
Loading[key] = false;
setLoading(false);
}
setLoaded(true);
}
runAsync();
if(!loading && !Cache[key]) {
Loading[key] = true;
setLoading(true);
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(): [
ILocalForage<IInterfaceConfig | undefined>,
Dispatch<SetStateAction<IInterfaceConfig | undefined>>
] {
const result = useLocalForage("interface");
let returnVal = result;
if (!result.data?.queries) {
returnVal = {
...result,
data: {
wall: {},
queries: {}
}
};
}
return [returnVal, result.setData];
export function useInterfaceLocalForage():
[ILocalForage<IInterfaceConfig>,
Dispatch<SetStateAction<IInterfaceConfig>>]
{
return useLocalForage("interface");
}

View file

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

View file

@ -2,7 +2,7 @@
import { CriterionModifier } from "src/core/generated-graphql";
import DurationUtils from "src/utils/duration";
import { ILabeledId, ILabeledValue } from "../types";
import { ILabeledId, ILabeledValue, IOptionType } from "../types";
export type CriterionType =
| "none"
@ -30,7 +30,10 @@ export type CriterionType =
| "piercings"
| "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") {
switch (type) {
case "none":
@ -114,9 +117,17 @@ export abstract class Criterion<Option = any, Value = any> {
public abstract modifier: CriterionModifier;
public abstract modifierOptions: ILabeledValue[];
public abstract options: Option[] | undefined;
public abstract value: Value;
public abstract value: CriterionValue;
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 {
let modifierString: string;
switch (this.modifier) {
@ -163,27 +174,11 @@ export abstract class Criterion<Option = any, Value = any> {
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 {
return `${this.parameterName}-${this.modifier.toString()}`; // TODO add values?
}
/*
public set(modifier: CriterionModifier, value: Value) {
this.modifier = modifier;
if (Array.isArray(this.value)) {
@ -192,6 +187,7 @@ export abstract class Criterion<Option = any, Value = any> {
this.value = value;
}
}
*/
}
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 parameterName: string;
public modifier = CriterionModifier.Equals;
@ -222,6 +218,10 @@ export class StringCriterion extends Criterion<string, string> {
public options: string[] | undefined;
public value: string = "";
public getLabelValue() {
return this.value;
}
constructor(type: CriterionType, parameterName?: string, options?: string[]) {
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 parameterName: string;
public modifier = CriterionModifier.Equals;
@ -252,6 +252,10 @@ export class NumberCriterion extends Criterion<number, number> {
public options: number[] | undefined;
public value: number = 0;
public getLabelValue() {
return this.value.toString();
}
constructor(type: CriterionType, parameterName?: string, options?: number[]) {
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 parameterName: string;
public modifier = CriterionModifier.Equals;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
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;
export default ReactJSPlayer;
}

View file

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

View file

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