mirror of
https://github.com/stashapp/stash.git
synced 2025-12-15 12:52:38 +01:00
Remove or exempt all uses of 'any
* Refactored LocalForage * Refactored SceneFilenameParser
This commit is contained in:
parent
a60c89ceb1
commit
cdadb66d85
43 changed files with 671 additions and 677 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
})
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
385
ui/v2.5/src/components/SceneFilenameParser/SceneParserRow.tsx
Normal file
385
ui/v2.5/src/components/SceneFilenameParser/SceneParserRow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ export const SettingsAboutPanel: React.FC = () => {
|
|||
{!dataLatest || loadingLatest || networkStatus === 4 ? (
|
||||
<LoadingIndicator inline />
|
||||
) : (
|
||||
<>{renderLatestVersion()}</>
|
||||
renderLatestVersion()
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
})
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,3 +21,9 @@ export interface ILabeledValue {
|
|||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface IOptionType {
|
||||
id: string;
|
||||
name?: string;
|
||||
image_path?: string;
|
||||
}
|
||||
|
|
|
|||
2
ui/v2.5/src/models/react-jw-player.d.ts
vendored
2
ui/v2.5/src/models/react-jw-player.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const flattenMessages = (nestedMessages: any, prefix = "") => {
|
||||
if (nestedMessages === null) {
|
||||
return {};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue