From cdadb66d85ab77121e17217ffb12a5ddbc1aaa0a Mon Sep 17 00:00:00 2001 From: Infinite Date: Thu, 13 Feb 2020 19:54:37 +0100 Subject: [PATCH] Remove or exempt all uses of 'any * Refactored LocalForage * Refactored SceneFilenameParser --- ui/v2.5/.eslintrc.json | 1 + ui/v2.5/src/App.tsx | 1 + ui/v2.5/src/components/ErrorBoundary.tsx | 21 +- ui/v2.5/src/components/Galleries/Gallery.tsx | 2 +- ui/v2.5/src/components/List/AddFilter.tsx | 36 +- ui/v2.5/src/components/List/ListFilter.tsx | 28 +- .../PerformerDetails/PerformerScenesPanel.tsx | 4 +- .../SceneFilenameParser/ParserInput.tsx | 12 +- .../SceneFilenameParser.tsx | 453 +----------------- .../SceneFilenameParser/SceneParserRow.tsx | 385 +++++++++++++++ .../SceneFilenameParser/styles.scss | 16 +- .../components/ScenePlayer/ScenePlayer.tsx | 66 +-- .../ScenePlayer/ScenePlayerScrubber.tsx | 14 +- .../Scenes/SceneDetails/SceneEditPanel.tsx | 4 +- .../Scenes/SceneDetails/SceneMarkersPanel.tsx | 2 +- .../Scenes/SceneSelectedOptions.tsx | 2 +- .../Settings/SettingsAboutPanel.tsx | 2 +- .../Settings/SettingsConfigurationPanel.tsx | 8 +- .../Settings/SettingsInterfacePanel.tsx | 2 +- .../src/components/Shared/DurationInput.tsx | 2 +- .../Shared/FolderSelect/FolderSelect.tsx | 2 +- ui/v2.5/src/components/Shared/Select.tsx | 23 +- .../StudioDetails/StudioScenesPanel.tsx | 4 +- ui/v2.5/src/components/Tags/TagList.tsx | 2 +- ui/v2.5/src/core/StashService.ts | 7 +- ui/v2.5/src/hooks/ListHook.tsx | 21 +- ui/v2.5/src/hooks/LocalForage.ts | 99 ++-- ui/v2.5/src/hooks/Toast.tsx | 2 +- .../models/list-filter/criteria/criterion.ts | 50 +- .../models/list-filter/criteria/favorite.ts | 2 +- .../list-filter/criteria/has-markers.ts | 2 +- .../models/list-filter/criteria/is-missing.ts | 2 +- .../src/models/list-filter/criteria/none.ts | 6 +- .../models/list-filter/criteria/performers.ts | 10 +- .../src/models/list-filter/criteria/rating.ts | 3 +- .../models/list-filter/criteria/resolution.ts | 3 +- .../models/list-filter/criteria/studios.ts | 10 +- .../src/models/list-filter/criteria/tags.ts | 6 +- ui/v2.5/src/models/list-filter/filter.ts | 23 +- ui/v2.5/src/models/list-filter/types.ts | 6 + ui/v2.5/src/models/react-jw-player.d.ts | 2 +- ui/v2.5/src/utils/flattenMessages.ts | 1 + ui/v2.5/src/utils/jwplayer.ts | 1 + 43 files changed, 671 insertions(+), 677 deletions(-) create mode 100644 ui/v2.5/src/components/SceneFilenameParser/SceneParserRow.tsx diff --git a/ui/v2.5/.eslintrc.json b/ui/v2.5/.eslintrc.json index 650b76ff5..5718d6a31 100644 --- a/ui/v2.5/.eslintrc.json +++ b/ui/v2.5/.eslintrc.json @@ -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", diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index 7b860b88b..fb9b783b6 100755 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -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 ( diff --git a/ui/v2.5/src/components/ErrorBoundary.tsx b/ui/v2.5/src/components/ErrorBoundary.tsx index 7ca35653f..9c90c0562 100644 --- a/ui/v2.5/src/components/ErrorBoundary.tsx +++ b/ui/v2.5/src/components/ErrorBoundary.tsx @@ -1,12 +1,25 @@ import React from "react"; -export class ErrorBoundary extends React.Component { - constructor(props: any) { +interface IErrorBoundaryProps { + children?: React.ReactNode, +} + +type ErrorInfo = { + componentStack: string, +}; + +interface IErrorBoundaryState { + error?: Error; + errorInfo?: ErrorInfo; +} + +export class ErrorBoundary extends React.Component { + 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 diff --git a/ui/v2.5/src/components/Galleries/Gallery.tsx b/ui/v2.5/src/components/Galleries/Gallery.tsx index 6988057fb..887ae206b 100644 --- a/ui/v2.5/src/components/Galleries/Gallery.tsx +++ b/ui/v2.5/src/components/Galleries/Gallery.tsx @@ -15,7 +15,7 @@ export const Gallery: React.FC = () => { return (
- +
); }; diff --git a/ui/v2.5/src/components/List/AddFilter.tsx b/ui/v2.5/src/components/List/AddFilter.tsx index fa66a0433..8eb3c2c07 100644 --- a/ui/v2.5/src/components/List/AddFilter.tsx +++ b/ui/v2.5/src/components/List/AddFilter.tsx @@ -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 = ( const defaultValue = useRef(); const [isOpen, setIsOpen] = useState(false); - const [criterion, setCriterion] = useState>( + const [criterion, setCriterion] = useState( new NoneCriterion() ); - const valueStage = useRef(criterion.value); + const valueStage = useRef(criterion.value); // Configure if we are editing an existing criterion useEffect(() => { @@ -53,7 +51,7 @@ export const AddFilter: React.FC = ( event: React.ChangeEvent ) { 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 = ( 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 = ( } 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 ( { const newCriterion = _.cloneDeep(criterion); @@ -164,7 +158,7 @@ export const AddFilter: React.FC = ( })); 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 = ( {criterion.options.map(c => ( - ))} @@ -198,7 +192,7 @@ export const AddFilter: React.FC = ( type={criterion.inputType} onChange={onChangedInput} onBlur={onBlurInput} - value={criterion.value || ""} + value={criterion.value.toString()} /> ); } diff --git a/ui/v2.5/src/components/List/ListFilter.tsx b/ui/v2.5/src/components/List/ListFilter.tsx index b6c2f2177..8cd64a844 100644 --- a/ui/v2.5/src/components/List/ListFilter.tsx +++ b/ui/v2.5/src/components/List/ListFilter.tsx @@ -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 = ( 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 = ( Criterion | undefined >(undefined); - function onChangePageSize(event: SyntheticEvent) { - const val = event!.currentTarget!.value; + function onChangePageSize(event: React.FormEvent) { + const val = event.currentTarget.value; props.onChangePageSize(parseInt(val, 10)); } - function onChangeQuery(event: SyntheticEvent) { - event.persist(); - searchCallback(event); + function onChangeQuery(event: React.FormEvent) { + searchCallback(event.currentTarget.value); } function onChangeSortDirection() { @@ -72,8 +72,9 @@ export const ListFilter: React.FC = ( } } - function onChangeSortBy(event: React.MouseEvent) { - props.onChangeSortBy(event.currentTarget.text); + function onChangeSortBy(event:React.MouseEvent) { + 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 = ( onClickCriterionTag(criterion)} > {criterion.getLabel()} @@ -241,8 +243,8 @@ export const ListFilter: React.FC = ( min={0} max={3} defaultValue={1} - onChange={(event: any) => - onChangeZoom(Number.parseInt(event.target.value, 10)) + onChange={(e: React.FormEvent) => + onChangeZoom(Number.parseInt(e.currentTarget.value, 10)) } /> ); diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScenesPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScenesPanel.tsx index 1641f2ba2..513d99ba5 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScenesPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScenesPanel.tsx @@ -16,7 +16,7 @@ export const PerformerScenesPanel: React.FC = ({ // 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 = ({ ) { // add the performer if not present if ( - !performerCriterion.value.find((p: any) => { + !performerCriterion.value.find(p => { return p.id === performer.id; }) ) { diff --git a/ui/v2.5/src/components/SceneFilenameParser/ParserInput.tsx b/ui/v2.5/src/components/SceneFilenameParser/ParserInput.tsx index b2952f88e..f2e10e995 100644 --- a/ui/v2.5/src/components/SceneFilenameParser/ParserInput.tsx +++ b/ui/v2.5/src/components/SceneFilenameParser/ParserInput.tsx @@ -132,7 +132,7 @@ export const ParserInput: React.FC = ( setPattern(newValue.target.value)} + onChange={(e: React.FormEvent) => setPattern(e.currentTarget.value)} value={pattern} /> @@ -158,7 +158,7 @@ export const ParserInput: React.FC = ( Ignored words setIgnoreWords(newValue.target.value)} + onChange={(e: React.FormEvent) => setIgnoreWords(e.currentTarget.value)} value={ignoreWords} /> @@ -174,8 +174,8 @@ export const ParserInput: React.FC = ( - setWhitespaceCharacters(newValue.target.value) + onChange={(e: React.FormEvent) => + setWhitespaceCharacters(e.currentTarget.value) } value={whitespaceCharacters} /> @@ -229,8 +229,8 @@ export const ParserInput: React.FC = ( - props.onPageSizeChanged(parseInt(event.target.value, 10)) + onChange={(e: React.FormEvent) => + props.onPageSizeChanged(parseInt(e.currentTarget.value, 10)) } defaultValue={props.input.pageSize} className="col-1 filter-item" diff --git a/ui/v2.5/src/components/SceneFilenameParser/SceneFilenameParser.tsx b/ui/v2.5/src/components/SceneFilenameParser/SceneFilenameParser.tsx index d6d824f9e..5898a5ad0 100644 --- a/ui/v2.5/src/components/SceneFilenameParser/SceneFilenameParser.tsx +++ b/ui/v2.5/src/components/SceneFilenameParser/SceneFilenameParser.tsx @@ -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 { - public value: GQL.Maybe = null; - public originalValue: GQL.Maybe = null; - public set: boolean = false; - - public setOriginalValue(v: GQL.Maybe) { - this.originalValue = v; - this.value = v; - } - - public setValue(v: GQL.Maybe) { - if (v) { - this.value = v; - this.set = !_.isEqual(this.value, this.originalValue); - } - } -} - -class SceneParserResult { - public id: string; - public filename: string; - public title: ParserResult = new ParserResult(); - public date: ParserResult = new ParserResult(); - - public studio: ParserResult> = new ParserResult(); - public studioId: ParserResult = new ParserResult(); - public tags: ParserResult = new ParserResult(); - public tagIds: ParserResult = new ParserResult(); - public performers: ParserResult< - Partial[] - > = new ParserResult(); - public performerIds: ParserResult = 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 - ) { - 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; - className?: string; - fieldName: string; - onSetChanged: (set: boolean) => void; - onValueChanged: (value: any) => void; - originalParserResult?: ParserResult; - 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 ( - <> - - { - props.onSetChanged(!props.parserResult.set); - }} - /> - - - - {props.renderOriginalInputField(props)} - {props.renderNewInputField(props, value => - maybeValueChanged(value) - )} - - - - ); - } - - function renderOriginalInputGroup(props: ISceneParserFieldProps) { - const result = props.originalParserResult || props.parserResult; - - return ( - - ); - } - - interface IInputGroupWrapperProps { - parserResult: ParserResult; - onChange: (event: any) => void; - className?: string; - } - - function InputGroupWrapper(props: IInputGroupWrapperProps) { - return ( - props.onChange(event.target.value)} - /> - ); - } - - function renderNewInputGroup( - props: ISceneParserFieldProps, - onChangeHandler: (value: any) => void - ) { - return ( - { - 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 ( -
- {elements.map((name: string) => ( - - {name} - - ))} -
- ); - } - - function renderNewMultiSelect( - type: "performers" | "tags", - props: ISceneParserFieldProps, - onChangeHandler: (value: any) => void - ) { - return ( - { - 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 ( - 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, 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 ( - - - {props.scene.filename} - - - onTitleChanged(set, props.scene.title.value ?? undefined) - } - onValueChanged={value => onTitleChanged(props.scene.title.set, value)} - renderOriginalInputField={renderOriginalInputGroup} - renderNewInputField={renderNewInputGroup} - /> - - onDateChanged(set, props.scene.date.value ?? undefined) - } - onValueChanged={value => onDateChanged(props.scene.date.set, value)} - renderOriginalInputField={renderOriginalInputGroup} - renderNewInputField={renderNewInputGroup} - /> - - onPerformerIdsChanged( - set, - props.scene.performerIds.value ?? undefined - ) - } - onValueChanged={value => - onPerformerIdsChanged(props.scene.performerIds.set, value) - } - renderOriginalInputField={renderOriginalSelect} - renderNewInputField={renderNewPerformerSelect} - /> - - onTagIdsChanged(set, props.scene.tagIds.value ?? undefined) - } - onValueChanged={value => - onTagIdsChanged(props.scene.tagIds.set, value) - } - renderOriginalInputField={renderOriginalSelect} - renderNewInputField={renderNewTagSelect} - /> - - onStudioIdChanged(set, props.scene.studioId.value ?? undefined) - } - onValueChanged={value => - onStudioIdChanged(props.scene.studioId.set, value) - } - renderOriginalInputField={renderOriginalSelect} - renderNewInputField={renderNewStudioSelect} - /> - - ); - } - function onChange(scene: SceneParserResult, changedScene: SceneParserResult) { const newResult = [...parserResult]; @@ -716,7 +292,7 @@ export const SceneFilenameParser: React.FC = () => { - + {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} /> ))} diff --git a/ui/v2.5/src/components/SceneFilenameParser/SceneParserRow.tsx b/ui/v2.5/src/components/SceneFilenameParser/SceneParserRow.tsx new file mode 100644 index 000000000..0dd79f8d3 --- /dev/null +++ b/ui/v2.5/src/components/SceneFilenameParser/SceneParserRow.tsx @@ -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 { + 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 = new ParserResult(); + public date: ParserResult = new ParserResult(); + + public studio: ParserResult = new ParserResult(); + public tags: ParserResult = new ParserResult(); + public performers: ParserResult = new ParserResult(); + + 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 { + parserResult: ParserResult; + className?: string; + fieldName: string; + onSetChanged: (isSet: boolean) => void; + onValueChanged: (value: T) => void; + originalParserResult?: ParserResult; +} + +function SceneParserStringField(props: ISceneParserFieldProps) { + function maybeValueChanged(value: string) { + if (value !== props.parserResult.value) { + props.onValueChanged(value); + } + } + + const result = props.originalParserResult || props.parserResult; + + return ( + <> + + + + ); +} + +function SceneParserPerformerField(props: ISceneParserFieldProps) { + 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 ( + <> + + + + ); +} + +function SceneParserTagField(props: ISceneParserFieldProps) { + function maybeValueChanged(value: string[]) { + if (value !== props.parserResult.value) { + props.onValueChanged(value); + } + } + + const originalTags = props.originalParserResult?.originalValue ?? []; + const newTags = props.parserResult.value ?? []; + + return ( + <> + + + + ); +} + +function SceneParserStudioField(props: ISceneParserFieldProps) { + 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 ( + <> + + + + ); +} + +interface ISceneParserRowProps { + scene: SceneParserResult; + onChange: (changedScene: SceneParserResult) => void; + showFields: Map; +} + +export const SceneParserRow = (props: ISceneParserRowProps) => { + function changeParser(result: ParserResult, 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 ( + + + { props.showFields.get("Title") && ( + + onTitleChanged(isSet, props.scene.title.value ?? '') + } + onValueChanged={value => onTitleChanged(props.scene.title.isSet, value)} + /> + )} + { props.showFields.get("Date") && ( + + onDateChanged(isSet, props.scene.date.value ?? '') + } + onValueChanged={value => onDateChanged(props.scene.date.isSet, value)} + /> + )} + { props.showFields.get("Performers") && ( + + onPerformerIdsChanged( + set, + props.scene.performers.value ?? [] + ) + } + onValueChanged={value => + onPerformerIdsChanged(props.scene.performers.isSet, value) + } + /> + )} + { props.showFields.get("Tags") && ( + + onTagIdsChanged(isSet, props.scene.tags.value ?? []) + } + onValueChanged={value => + onTagIdsChanged(props.scene.tags.isSet, value) + } + /> + )} + { props.showFields.get("Studio") && ( + + onStudioIdChanged(set, props.scene.studio.value ?? '') + } + onValueChanged={value => onStudioIdChanged(props.scene.studio.isSet, value)} + /> + )} + + ); +} diff --git a/ui/v2.5/src/components/SceneFilenameParser/styles.scss b/ui/v2.5/src/components/SceneFilenameParser/styles.scss index 7fce6ae89..c00533e08 100644 --- a/ui/v2.5/src/components/SceneFilenameParser/styles.scss +++ b/ui/v2.5/src/components/SceneFilenameParser/styles.scss @@ -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; + } } diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 47817847e..4ed3fffe1 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -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; } 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) | 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 ( - - ); - } - public render() { return ( - {this.renderPlayer()} + = ( const positionIndicatorEl = useRef(null); const scrubberSliderEl = useRef(null); const mouseDown = useRef(false); - const lastMouseEvent = useRef(null); - const startMouseEvent = useRef(null); + const lastMouseEvent = useRef(null); + const startMouseEvent = useRef(null); const velocity = useRef(0); const _position = useRef(0); @@ -228,7 +228,7 @@ export const ScenePlayerScrubber: React.FC = ( } // 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 = ( 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 = ( 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 diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx index 7c10ea18e..de4408f5b 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx @@ -305,7 +305,7 @@ export const SceneEditPanel: React.FC = (props: IProps) => {
FilenameFilename + { + props.onSetChanged(!props.parserResult.isSet); + }} + /> + + + + ) => maybeValueChanged(event.currentTarget.value)} + /> + + + { + props.onSetChanged(!props.parserResult.isSet); + }} + /> + + + + { + maybeValueChanged(items.map(i => i.id)); + }} + ids={newPerformers} + /> + + + { + props.onSetChanged(!props.parserResult.isSet); + }} + /> + + + + { + maybeValueChanged(items.map(i => i.id)); + }} + ids={newTags} + /> + + + { + props.onSetChanged(!props.parserResult.isSet); + }} + /> + + + + { + maybeValueChanged(items[0].id); + }} + ids={newStudio} + /> + +
+ {props.scene.filename} +
URL setUrl(newValue.target.value)} + onChange={(newValue: React.FormEvent) => setUrl(newValue.currentTarget.value)} value={url} placeholder="URL" /> @@ -376,7 +376,7 @@ export const SceneEditPanel: React.FC = (props: IProps) => { setDetails(newValue.target.value)} + onChange={(newValue: React.FormEvent) => setDetails(newValue.currentTarget.value)} value={details} /> diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkersPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkersPanel.tsx index 9b2f936bf..4247ae9ec 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkersPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkersPanel.tsx @@ -60,7 +60,7 @@ export const SceneMarkersPanel: React.FC = ( sceneMarkers={props.scene.scene_markers} clickHandler={marker => { window.scrollTo(0, 0); - onClickMarker(marker as any); + onClickMarker(marker as GQL.SceneMarkerDataFragment); }} /> diff --git a/ui/v2.5/src/components/Scenes/SceneSelectedOptions.tsx b/ui/v2.5/src/components/Scenes/SceneSelectedOptions.tsx index 459e99a62..4a3315767 100644 --- a/ui/v2.5/src/components/Scenes/SceneSelectedOptions.tsx +++ b/ui/v2.5/src/components/Scenes/SceneSelectedOptions.tsx @@ -265,7 +265,7 @@ export const SceneSelectedOptions: React.FC = ( setRating(event.target.value)} + onChange={(event: React.FormEvent) => setRating(event.currentTarget.value)} > {["", "1", "2", "3", "4", "5"].map(opt => (