Linting update

This commit is contained in:
Infinite
2020-01-20 21:25:47 +01:00
parent c83e0898f9
commit 9827647122
58 changed files with 789 additions and 737 deletions

View File

@@ -36,6 +36,14 @@
"spaced-comment": ["error", "always", { "spaced-comment": ["error", "always", {
"markers": ["/"] "markers": ["/"]
}], }],
"max-classes-per-file": "off" "max-classes-per-file": "off",
"no-plusplus": "off",
"prefer-destructuring": ["error", {"object": true, "array": false}],
"default-case": "off",
"consistent-return": "off",
"@typescript-eslint/no-use-before-define": ["error", { "functions": false, "classes": true }],
"no-underscore-dangle": "off",
"no-nested-ternary": "off",
"jsx-a11y/media-has-caption": "off"
} }
} }

View File

@@ -1,5 +1,8 @@
import React from "react"; import React from "react";
import { Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import { ToastProvider } from 'src/hooks/Toast';
import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'
import { ErrorBoundary } from "./components/ErrorBoundary"; import { ErrorBoundary } from "./components/ErrorBoundary";
import Galleries from "./components/Galleries/Galleries"; import Galleries from "./components/Galleries/Galleries";
import { MainNavbar } from "./components/MainNavbar"; import { MainNavbar } from "./components/MainNavbar";
@@ -11,10 +14,7 @@ import { Stats } from "./components/Stats";
import Studios from "./components/Studios/Studios"; import Studios from "./components/Studios/Studios";
import Tags from "./components/Tags/Tags"; import Tags from "./components/Tags/Tags";
import { SceneFilenameParser } from "./components/scenes/SceneFilenameParser"; import { SceneFilenameParser } from "./components/scenes/SceneFilenameParser";
import { ToastProvider } from 'src/hooks/Toast';
import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'
import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/css/bootstrap.min.css';
@@ -27,7 +27,7 @@ export const App: React.FC = () => (
<ToastProvider> <ToastProvider>
<div className="main"> <div className="main">
<Switch> <Switch>
<Route exact={true} path="/" component={Stats} /> <Route exact path="/" component={Stats} />
<Route path="/scenes" component={Scenes} /> <Route path="/scenes" component={Scenes} />
{/* <Route path="/scenes/:id" component={Scene} /> */} {/* <Route path="/scenes/:id" component={Scene} /> */}
<Route path="/galleries" component={Galleries} /> <Route path="/galleries" component={Galleries} />

View File

@@ -5,7 +5,7 @@ import { GalleryList } from "./GalleryList";
const Galleries = () => ( const Galleries = () => (
<Switch> <Switch>
<Route exact={true} path="/galleries" component={GalleryList} /> <Route exact path="/galleries" component={GalleryList} />
<Route path="/galleries/:id" component={Gallery} /> <Route path="/galleries/:id" component={Gallery} />
</Switch> </Switch>
); );

View File

@@ -16,7 +16,7 @@ export const GalleryList: React.FC = () => {
if (!result.data || !result.data.findGalleries) { return; } if (!result.data || !result.data.findGalleries) { return; }
if (filter.displayMode === DisplayMode.Grid) { if (filter.displayMode === DisplayMode.Grid) {
return <h1>TODO</h1>; return <h1>TODO</h1>;
} else if (filter.displayMode === DisplayMode.List) { } if (filter.displayMode === DisplayMode.List) {
return ( return (
<Table style={{margin: "0 auto"}}> <Table style={{margin: "0 auto"}}>
<thead> <thead>
@@ -39,7 +39,7 @@ export const GalleryList: React.FC = () => {
</tbody> </tbody>
</Table> </Table>
); );
} else if (filter.displayMode === DisplayMode.Wall) { } if (filter.displayMode === DisplayMode.Wall) {
return <h1>TODO</h1>; return <h1>TODO</h1>;
} }
} }

View File

@@ -69,7 +69,7 @@ export const MainNavbar: React.FC = () => {
{menuItems.map((i) => ( {menuItems.map((i) => (
<LinkContainer <LinkContainer
activeClassName="active" activeClassName="active"
exact={true} exact
to={i.href} to={i.href}
key={i.href} key={i.href}
> >
@@ -83,7 +83,7 @@ export const MainNavbar: React.FC = () => {
<Nav> <Nav>
{newButton} {newButton}
<LinkContainer <LinkContainer
exact={true} exact
to="/settings"> to="/settings">
<Button variant="secondary"> <Button variant="secondary">
<Icon icon="cog" /> <Icon icon="cog" />

View File

@@ -89,6 +89,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
async function onSave() { async function onSave() {
try { try {
const result = await updateGeneralConfig(); const result = await updateGeneralConfig();
// eslint-disable-next-line no-console
console.log(result); console.log(result);
Toast.success({ content: "Updated config" }); Toast.success({ content: "Updated config" });
} catch (e) { } catch (e) {
@@ -203,7 +204,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Group id="transcode-size"> <Form.Group id="transcode-size">
<Form.Label>Maximum transcode size</Form.Label> <Form.Label>Maximum transcode size</Form.Label>
<Form.Control <Form.Control
as="select"> as="select"
onChange={(event:React.FormEvent<HTMLSelectElement>) => setMaxTranscodeSize(translateQuality(event.currentTarget.value))} onChange={(event:React.FormEvent<HTMLSelectElement>) => setMaxTranscodeSize(translateQuality(event.currentTarget.value))}
value={resolutionToString(maxTranscodeSize)} value={resolutionToString(maxTranscodeSize)}
> >

View File

@@ -41,6 +41,7 @@ export const SettingsInterfacePanel: React.FC = () => {
async function onSave() { async function onSave() {
try { try {
const result = await updateInterfaceConfig(); const result = await updateInterfaceConfig();
// eslint-disable-next-line no-console
console.log(result); console.log(result);
Toast.success({ content: "Updated config" }); Toast.success({ content: "Updated config" });
} catch (e) { } catch (e) {
@@ -94,7 +95,7 @@ export const SettingsInterfacePanel: React.FC = () => {
<Form.Control <Form.Control
type="number" type="number"
defaultValue={maximumLoopDuration} defaultValue={maximumLoopDuration}
onChange={(event:React.FormEvent<HTMLInputElement>) => setMaximumLoopDuration(Number.parseInt(event.currentTarget.value) ?? 0)} onChange={(event:React.FormEvent<HTMLInputElement>) => setMaximumLoopDuration(Number.parseInt(event.currentTarget.value, 10) ?? 0)}
min={0} min={0}
step={1} step={1}
/> />

View File

@@ -5,19 +5,19 @@ import { StashService } from "src/core/StashService";
function convertTime(logEntry: GQL.LogEntryDataFragment) { function convertTime(logEntry: GQL.LogEntryDataFragment) {
function pad(val : number) { function pad(val : number) {
var ret = val.toString(); let ret = val.toString();
if (val <= 9) { if (val <= 9) {
ret = "0" + ret; ret = `0${ ret}`;
} }
return ret; return ret;
} }
var date = new Date(logEntry.time); const date = new Date(logEntry.time);
var month = date.getMonth() + 1; const month = date.getMonth() + 1;
var day = date.getDate(); const day = date.getDate();
var dateStr = date.getFullYear() + "-" + pad(month) + "-" + pad(day); let dateStr = `${date.getFullYear() }-${ pad(month) }-${ pad(day)}`;
dateStr += " " + pad(date.getHours()) + ":" + pad(date.getMinutes()) + ":" + pad(date.getSeconds()); dateStr += ` ${ pad(date.getHours()) }:${ pad(date.getMinutes()) }:${ pad(date.getSeconds())}`;
return dateStr; return dateStr;
} }
@@ -32,7 +32,7 @@ interface ILogElementProps {
const LogElement: React.FC<ILogElementProps> = ({ logEntry }) => { const LogElement: React.FC<ILogElementProps> = ({ logEntry }) => {
// pad to maximum length of level enum // pad to maximum length of level enum
var level = logEntry.level.padEnd(GQL.LogLevel.Progress.length); const level = logEntry.level.padEnd(GQL.LogLevel.Progress.length);
return ( return (
<> <>
@@ -58,7 +58,7 @@ class LogEntry {
this.level = logEntry.level; this.level = logEntry.level;
this.message = logEntry.message; this.message = logEntry.message;
var id = LogEntry.nextId++; const id = LogEntry.nextId++;
this.id = id.toString(); this.id = id.toString();
} }
} }
@@ -80,15 +80,15 @@ export const SettingsLogsPanel: React.FC = () => {
.filter(filterByLogLevel).slice(0, MAX_LOG_ENTRIES); .filter(filterByLogLevel).slice(0, MAX_LOG_ENTRIES);
const maybeRenderError = error const maybeRenderError = error
? <div className={"error"}>Error connecting to log server: {error.message}</div> ? <div className="error">Error connecting to log server: {error.message}</div>
: ''; : '';
function filterByLogLevel(logEntry : LogEntry) { function filterByLogLevel(logEntry : LogEntry) {
if (logLevel === "Debug") if (logLevel === "Debug")
return true; return true;
var logLevelIndex = logLevels.indexOf(logLevel); const logLevelIndex = logLevels.indexOf(logLevel);
var levelIndex = logLevels.indexOf(logEntry.level); const levelIndex = logLevels.indexOf(logEntry.level);
return levelIndex >= logLevelIndex; return levelIndex >= logLevelIndex;
} }

View File

@@ -21,8 +21,8 @@ export const SettingsTasksPanel: React.FC = () => {
const jobStatus = StashService.useJobStatus(); const jobStatus = StashService.useJobStatus();
const metadataUpdate = StashService.useMetadataUpdate(); const metadataUpdate = StashService.useMetadataUpdate();
function statusToText(status : string) { function statusToText(s: string) {
switch(status) { switch(s) {
case "Idle": case "Idle":
return "Idle"; return "Idle";
case "Scan": case "Scan":
@@ -37,15 +37,15 @@ export const SettingsTasksPanel: React.FC = () => {
return "Importing from JSON"; return "Importing from JSON";
case "Auto Tag": case "Auto Tag":
return "Auto tagging scenes"; return "Auto tagging scenes";
default:
return "Idle"
} }
return "Idle";
} }
useEffect(() => { useEffect(() => {
if (!!jobStatus.data && !!jobStatus.data.jobStatus) { if (jobStatus?.data?.jobStatus) {
setStatus(statusToText(jobStatus.data.jobStatus.status)); setStatus(statusToText(jobStatus.data.jobStatus.status));
var newProgress = jobStatus.data.jobStatus.progress; const newProgress = jobStatus.data.jobStatus.progress;
if (newProgress < 0) { if (newProgress < 0) {
setProgress(0); setProgress(0);
} else { } else {
@@ -55,9 +55,9 @@ export const SettingsTasksPanel: React.FC = () => {
}, [jobStatus.data]); }, [jobStatus.data]);
useEffect(() => { useEffect(() => {
if (!!metadataUpdate.data && !!metadataUpdate.data.metadataUpdate) { if (metadataUpdate?.data?.metadataUpdate) {
setStatus(statusToText(metadataUpdate.data.metadataUpdate.status)); setStatus(statusToText(metadataUpdate.data.metadataUpdate.status));
var newProgress = metadataUpdate.data.metadataUpdate.progress; const newProgress = metadataUpdate.data.metadataUpdate.progress;
if (newProgress < 0) { if (newProgress < 0) {
setProgress(0); setProgress(0);
} else { } else {
@@ -111,7 +111,7 @@ export const SettingsTasksPanel: React.FC = () => {
async function onScan() { async function onScan() {
try { try {
await StashService.queryMetadataScan({useFileMetadata: useFileMetadata}); await StashService.queryMetadataScan({useFileMetadata});
Toast.success({ content: "Started scan" }); Toast.success({ content: "Started scan" });
jobStatus.refetch(); jobStatus.refetch();
} catch (e) { } catch (e) {
@@ -120,7 +120,7 @@ export const SettingsTasksPanel: React.FC = () => {
} }
function getAutoTagInput() { function getAutoTagInput() {
var wildcard = ["*"]; const wildcard = ["*"];
return { return {
performers: autoTagPerformers ? wildcard : [], performers: autoTagPerformers ? wildcard : [],
studios: autoTagStudios ? wildcard : [], studios: autoTagStudios ? wildcard : [],
@@ -210,7 +210,7 @@ export const SettingsTasksPanel: React.FC = () => {
<Form.Group> <Form.Group>
<Button> <Button>
<Link to={"/sceneFilenameParser"}>Scene Filename Parser</Link> <Link to="/sceneFilenameParser">Scene Filename Parser</Link>
</Button> </Button>
</Form.Group> </Form.Group>

View File

@@ -56,17 +56,16 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
} }
function renderScraperMenu() { function renderScraperMenu() {
if (!props.performer) { return; } if (!props.performer || !props.isEditing) { return; }
if (!props.isEditing) { return; }
const popover = ( const popover = (
<Popover id="scraper-popover"> <Popover id="scraper-popover">
<Popover.Content> <Popover.Content>
<div> <div>
{ props.scrapers ? props.scrapers.map((s) => ( { props.scrapers ? props.scrapers.map((s) => (
<div onClick={() => props.onDisplayScraperDialog && props.onDisplayScraperDialog(s) }> <Button variant="link" onClick={() => props.onDisplayScraperDialog && props.onDisplayScraperDialog(s) }>
{s.name} {s.name}
</div> </Button>
)) : ''} )) : ''}
</div> </div>
</Popover.Content> </Popover.Content>
@@ -82,7 +81,7 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
function renderAutoTagButton() { function renderAutoTagButton() {
if (props.isNew || props.isEditing) { return; } if (props.isNew || props.isEditing) { return; }
if (!!props.onAutoTag) { if (props.onAutoTag) {
return (<Button onClick={() => { return (<Button onClick={() => {
if (props.onAutoTag) { props.onAutoTag() } if (props.onAutoTag) { props.onAutoTag() }
}}>Auto Tag</Button>) }}>Auto Tag</Button>)
@@ -105,14 +104,7 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
} }
function renderDeleteAlert() { function renderDeleteAlert() {
var name; const name = props?.studio?.name ?? props?.performer?.name;
if (props.performer) {
name = props.performer.name;
}
if (props.studio) {
name = props.studio.name;
}
return ( return (
<Modal <Modal

View File

@@ -36,7 +36,7 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
return 0; return 0;
} }
let splits = v.split(":"); const splits = v.split(":");
if (splits.length > 3) { if (splits.length > 3) {
return 0; return 0;
@@ -45,13 +45,13 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
let seconds = 0; let seconds = 0;
let factor = 1; let factor = 1;
while(splits.length > 0) { while(splits.length > 0) {
let thisSplit = splits.pop(); const thisSplit = splits.pop();
if (thisSplit === undefined) { if (thisSplit === undefined) {
return 0; return 0;
} }
let thisInt = parseInt(thisSplit, 10); const thisInt = parseInt(thisSplit, 10);
if (isNaN(thisInt)) { if (Number.isNaN(thisInt)) {
return 0; return 0;
} }
@@ -77,7 +77,7 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
function renderButtons() { function renderButtons() {
return ( return (
<ButtonGroup <ButtonGroup
vertical={true} vertical
> >
<Button <Button
disabled={props.disabled} disabled={props.disabled}

View File

@@ -55,10 +55,8 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
{(!data || !data.directories || loading) ? <Spinner animation="border" variant="light" /> : ''} {(!data || !data.directories || loading) ? <Spinner animation="border" variant="light" /> : ''}
</InputGroup.Append> </InputGroup.Append>
</InputGroup> </InputGroup>
/>
{selectableDirectories.map((path) => { {selectableDirectories.map((path) => {
return <div key={path} onClick={() => setCurrentDirectory(path)}>{path}</div>; return <Button variant="link" key={path} onClick={() => setCurrentDirectory(path)}>{path}</Button>;
})} })}
</div> </div>
</Modal.Body> </Modal.Body>

View File

@@ -0,0 +1,57 @@
import React, { useState, useCallback, useEffect, useRef } from 'react'
import { Overlay, Popover, OverlayProps } from 'react-bootstrap'
interface IHoverPopover {
enterDelay?: number;
leaveDelay?: number;
content: JSX.Element[] | JSX.Element | string;
className?: string;
placement?: OverlayProps["placement"];
}
export const HoverPopover: React.FC<IHoverPopover> = ({ enterDelay = 0, leaveDelay = 400, content, children, className, placement = 'top' }) => {
const [show, setShow] = useState(false);
const triggerRef = useRef<HTMLDivElement>(null);
const enterTimer = useRef<number>();
const leaveTimer = useRef<number>();
const handleMouseEnter = useCallback(() => {
window.clearTimeout(leaveTimer.current);
enterTimer.current = window.setTimeout(() => setShow(true), enterDelay);
}, [enterDelay]);
const handleMouseLeave = useCallback(() => {
window.clearTimeout(enterTimer.current);
leaveTimer.current = window.setTimeout(() => setShow(false), leaveDelay);
}, [leaveDelay]);
useEffect(() => (
() => {
window.clearTimeout(enterTimer.current)
window.clearTimeout(leaveTimer.current)
}
), []);
return (
<>
<div className={className} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} ref={triggerRef}>
{children}
</div>
{ triggerRef.current &&
<Overlay
show={show}
placement={placement}
target={triggerRef.current}
>
<Popover
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
id='popover'
>
{content}
</Popover>
</Overlay>
}
</>
);
}

View File

@@ -35,7 +35,7 @@ const ModalComponent: React.FC<IModal> = ({ children, show, icon, header, cancel
? <Button variant={cancel.variant ?? 'primary'} onClick={cancel.onClick}>{cancel.text ?? 'Cancel'}</Button> ? <Button variant={cancel.variant ?? 'primary'} onClick={cancel.onClick}>{cancel.text ?? 'Cancel'}</Button>
: '' : ''
} }
{ <Button variant={accept?.variant ?? 'primary'} onClick={accept?.onClick}>{accept?.text ?? 'Close'}</Button> } <Button variant={accept?.variant ?? 'primary'} onClick={accept?.onClick}>{accept?.text ?? 'Close'}</Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
</Modal> </Modal>

View File

@@ -42,6 +42,12 @@ interface ISceneGallerySelect {
sceneId: string; sceneId: string;
onSelect: (item: GQL.ValidGalleriesForSceneValidGalleriesForScene | undefined) => void; onSelect: (item: GQL.ValidGalleriesForSceneValidGalleriesForScene | undefined) => void;
} }
const getSelectedValues = (selectedItems:ValueType<Option>) => (
(Array.isArray(selectedItems) ? selectedItems : [selectedItems])
.map(item => item.value)
);
export const SceneGallerySelect: React.FC<ISceneGallerySelect> = (props) => { export const SceneGallerySelect: React.FC<ISceneGallerySelect> = (props) => {
const { data, loading } = StashService.useValidGalleriesForScene(props.sceneId); const { data, loading } = StashService.useValidGalleriesForScene(props.sceneId);
const galleries = data?.validGalleriesForScene ?? []; const galleries = data?.validGalleriesForScene ?? [];
@@ -72,7 +78,6 @@ export const ScrapePerformerSuggest: React.FC<IScrapePerformerSuggestProps> = (p
); );
const performers = data?.scrapePerformerList ?? []; const performers = data?.scrapePerformerList ?? [];
console.log(`performers: ${performers}, loading: ${loading}, query: ${query}`);
const items = performers.map(item => ({ label: item.name ?? '', value: item.name ?? '' })); const items = performers.map(item => ({ label: item.name ?? '', value: item.name ?? '' }));
return <SelectComponent onChange={onChange} onInputChange={onInputChange} isLoading={loading} items={items} initialIds={[]} placeholder={props.placeholder} /> return <SelectComponent onChange={onChange} onInputChange={onInputChange} isLoading={loading} items={items} initialIds={[]} placeholder={props.placeholder} />
} }
@@ -139,6 +144,8 @@ export const TagSelect: React.FC<IFilterProps> = (props) => {
const placeholder = props.noSelectionString ?? "Select tags..." const placeholder = props.noSelectionString ?? "Select tags..."
const tags = data?.allTags ?? []; const tags = data?.allTags ?? [];
const selected = tags.filter(tag => selectedIds.indexOf(tag.id) !== -1).map(tag => ({value: tag.id, label: tag.name}));
const items:Option[] = tags.map(item => ({ value: item.id, label: item.name }));
const onCreate = async (tagName: string) => { const onCreate = async (tagName: string) => {
try { try {
@@ -158,15 +165,24 @@ export const TagSelect: React.FC<IFilterProps> = (props) => {
}; };
const onChange = (selectedItems:ValueType<Option>) => { const onChange = (selectedItems:ValueType<Option>) => {
const selected = getSelectedValues(selectedItems); const selectedValues = getSelectedValues(selectedItems);
setSelectedIds(selected); setSelectedIds(selectedValues);
props.onSelect(tags.filter(item => selected.indexOf(item.id) !== -1)); props.onSelect(tags.filter(item => selectedValues.indexOf(item.id) !== -1));
}; };
const selected = tags.filter(tag => selectedIds.indexOf(tag.id) !== -1).map(tag => ({value: tag.id, label: tag.name})); return (
const items:Option[] = tags.map(item => ({ value: item.id, label: item.name })); <SelectComponent
return <SelectComponent {...props} onChange={onChange} creatable={true} type="tags" placeholder={placeholder} {...props}
isLoading={loading || dataLoading} items={items} onCreateOption={onCreate} selectedOptions={selected} /> onChange={onChange}
creatable
type="tags"
placeholder={placeholder}
isLoading={loading || dataLoading}
items={items}
onCreateOption={onCreate}
selectedOptions={selected}
/>
);
} }
const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
@@ -186,14 +202,14 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
const defaultValue = items.filter(item => initialIds?.indexOf(item.value) !== -1) ?? null; const defaultValue = items.filter(item => initialIds?.indexOf(item.value) !== -1) ?? null;
const props = { const props = {
className: className, className,
options: items, options: items,
value: selectedOptions, value: selectedOptions,
onChange: onChange, onChange,
isMulti: isMulti, isMulti,
defaultValue: defaultValue, defaultValue,
noOptionsMessage: () => (type !== 'tags' ? 'None' : null), noOptionsMessage: () => (type !== 'tags' ? 'None' : null),
placeholder: placeholder, placeholder,
onInputChange onInputChange
} }
@@ -203,9 +219,3 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
: <Select {...props} isLoading={isLoading} /> : <Select {...props} isLoading={isLoading} />
); );
}; };
const getSelectedValues = (selectedItems:ValueType<Option>) => (
(Array.isArray(selectedItems) ? selectedItems : [selectedItems])
.map(item => item.value)
);

View File

@@ -13,3 +13,4 @@ export { default as Modal } from './Modal';
export { DetailsEditNavbar } from './DetailsEditNavbar'; export { DetailsEditNavbar } from './DetailsEditNavbar';
export { DurationInput } from './DurationInput'; export { DurationInput } from './DurationInput';
export { TagLink } from './TagLink'; export { TagLink } from './TagLink';
export { HoverPopover } from './HoverPopover';

View File

@@ -49,7 +49,7 @@ export const Stats: FunctionComponent = () => {
<Spinner animation="border" role="status" size="sm"> <Spinner animation="border" role="status" size="sm">
<span className="sr-only">Loading...</span> <span className="sr-only">Loading...</span>
</Spinner> : undefined} </Spinner> : undefined}
{!!error ? <span>error.message</span> : undefined} {error ? <span>error.message</span> : undefined}
{renderStats()} {renderStats()}
<h3>Notes</h3> <h3>Notes</h3>

View File

@@ -1,3 +1,5 @@
/* eslint-disable react/no-this-in-sfc */
import { Form, Spinner, Table } from 'react-bootstrap'; import { Form, Spinner, Table } from 'react-bootstrap';
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useParams, useHistory } from 'react-router-dom'; import { useParams, useHistory } from 'react-router-dom';
@@ -36,11 +38,11 @@ export const Studio: React.FC = () => {
setUrl(state.url); setUrl(state.url);
} }
function updateStudioData(studio:Partial<GQL.StudioDataFragment>) { function updateStudioData(studioData: Partial<GQL.StudioDataFragment>) {
setImage(undefined); setImage(undefined);
updateStudioEditState(studio); updateStudioEditState(studioData);
setImagePreview(studio.image_path); setImagePreview(studioData.image_path);
setStudio(studio); setStudio(studioData);
} }
useEffect(() => { useEffect(() => {

View File

@@ -19,9 +19,9 @@ export const StudioList: React.FC = () => {
{result.data.findStudios.studios.map((studio) => (<StudioCard key={studio.id} studio={studio} />))} {result.data.findStudios.studios.map((studio) => (<StudioCard key={studio.id} studio={studio} />))}
</div> </div>
); );
} else if (filter.displayMode === DisplayMode.List) { } if (filter.displayMode === DisplayMode.List) {
return <h1>TODO</h1>; return <h1>TODO</h1>;
} else if (filter.displayMode === DisplayMode.Wall) { } if (filter.displayMode === DisplayMode.Wall) {
return <h1>TODO</h1>; return <h1>TODO</h1>;
} }
} }

View File

@@ -5,7 +5,7 @@ import { StudioList } from "./StudioList";
const Studios = () => ( const Studios = () => (
<Switch> <Switch>
<Route exact={true} path="/studios" component={StudioList} /> <Route exact path="/studios" component={StudioList} />
<Route path="/studios/:id" component={Studio} /> <Route path="/studios/:id" component={Studio} />
</Switch> </Switch>
); );

View File

@@ -92,7 +92,7 @@ export const TagList: React.FC = () => {
<> <>
{deleteAlert} {deleteAlert}
<div key={tag.id} className="tag-list-row"> <div key={tag.id} className="tag-list-row">
<span onClick={() => setEditingTag(tag)}>{tag.name}</span> <Button variant="link" onClick={() => setEditingTag(tag)}>{tag.name}</Button>
<div style={{float: "right"}}> <div style={{float: "right"}}>
<Button onClick={() => onAutoTag(tag)}>Auto Tag</Button> <Button onClick={() => onAutoTag(tag)}>Auto Tag</Button>
<Link to={NavUtils.makeTagScenesUrl(tag)}>Scenes: {tag.scene_count}</Link> <Link to={NavUtils.makeTagScenesUrl(tag)}>Scenes: {tag.scene_count}</Link>

View File

@@ -4,7 +4,7 @@ import { TagList } from "./TagList";
const Tags = () => ( const Tags = () => (
<Switch> <Switch>
<Route exact={true} path="/tags" component={TagList} /> <Route exact path="/tags" component={TagList} />
</Switch> </Switch>
); );

View File

@@ -27,9 +27,9 @@ export const WallItem: FunctionComponent<IWallItemProps> = (props: IWallItemProp
function onMouseEnter() { function onMouseEnter() {
VideoHoverHook.onMouseEnter(videoHoverHook); VideoHoverHook.onMouseEnter(videoHoverHook);
if (!videoPath || videoPath === "") { if (!videoPath || videoPath === "") {
if (!!props.sceneMarker) { if (props.sceneMarker) {
setVideoPath(props.sceneMarker.stream || ""); setVideoPath(props.sceneMarker.stream || "");
} else if (!!props.scene) { } else if (props.scene) {
setVideoPath(props.scene.paths.preview || ""); setVideoPath(props.scene.paths.preview || "");
} }
} }
@@ -95,7 +95,7 @@ export const WallItem: FunctionComponent<IWallItemProps> = (props: IWallItemProp
const className = ["scene-wall-item-container"]; const className = ["scene-wall-item-container"];
if (videoHoverHook.isHovering.current) { className.push("double-scale"); } if (videoHoverHook.isHovering.current) { className.push("double-scale"); }
const style: React.CSSProperties = {}; const style: React.CSSProperties = {};
if (!!props.origin) { style.transformOrigin = props.origin; } if (props.origin) { style.transformOrigin = props.origin; }
return ( return (
<div className="wall grid-item"> <div className="wall grid-item">
<div <div
@@ -111,8 +111,8 @@ export const WallItem: FunctionComponent<IWallItemProps> = (props: IWallItemProp
src={videoPath} src={videoPath}
poster={screenshotPath} poster={screenshotPath}
style={videoHoverHook.isHovering.current ? {} : {display: "none"}} style={videoHoverHook.isHovering.current ? {} : {display: "none"}}
autoPlay={true} autoPlay
loop={true} loop
ref={videoHoverHook.videoEl} ref={videoHoverHook.videoEl}
/> />
<img alt="Preview" src={previewPath || screenshotPath} onError={() => previewNotFound()} /> <img alt="Preview" src={previewPath || screenshotPath} onError={() => previewNotFound()} />

View File

@@ -1,7 +1,7 @@
import _ from "lodash"; import _ from "lodash";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { Button, Form, Modal, OverlayTrigger, Tooltip } from 'react-bootstrap' import { Button, Form, Modal, OverlayTrigger, Tooltip } from 'react-bootstrap'
import { Icon } from 'src/components/Shared'; import { Icon , FilterSelect } from 'src/components/Shared';
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { Criterion, CriterionType } from "src/models/list-filter/criteria/criterion"; import { Criterion, CriterionType } from "src/models/list-filter/criteria/criterion";
import { NoneCriterion } from "src/models/list-filter/criteria/none"; import { NoneCriterion } from "src/models/list-filter/criteria/none";
@@ -10,7 +10,7 @@ import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
import { TagsCriterion } from "src/models/list-filter/criteria/tags"; import { TagsCriterion } from "src/models/list-filter/criteria/tags";
import { makeCriteria } from "src/models/list-filter/criteria/utils"; import { makeCriteria } from "src/models/list-filter/criteria/utils";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { FilterSelect } from "src/components/Shared";
interface IAddFilterProps { interface IAddFilterProps {
onAddCriterion: (criterion: Criterion, oldId?: string) => void; onAddCriterion: (criterion: Criterion, oldId?: string) => void;
@@ -73,7 +73,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
criterion.value = ""; criterion.value = "";
} }
} }
const oldId = !!props.editingCriterion ? props.editingCriterion.getId() : undefined; const oldId = props.editingCriterion ? props.editingCriterion.getId() : undefined;
props.onAddCriterion(criterion, oldId); props.onAddCriterion(criterion, oldId);
onToggle(); onToggle();
} }
@@ -127,11 +127,13 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
return ( return (
<FilterSelect <FilterSelect
type={type} type={type}
onSelect={(items) => criterion.value = items.map((i) => ({id: i.id, label: i.name!}))} onSelect={(items) => {
criterion.value = items.map(i => ({id: i.id, label: i.name!})) }
}
initialIds={criterion.value.map((labeled: any) => labeled.id)} initialIds={criterion.value.map((labeled: any) => labeled.id)}
/> />
); );
} else { }
if (criterion.options) { if (criterion.options) {
defaultValue.current = criterion.value; defaultValue.current = criterion.value;
return ( return (
@@ -145,7 +147,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
))} ))}
</Form.Control> </Form.Control>
); );
} else { }
return ( return (
<Form.Control <Form.Control
type={criterion.inputType} type={criterion.inputType}
@@ -154,8 +156,8 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
value={criterion.value || ""} value={criterion.value || ""}
/> />
) )
}
}
} }
return ( return (
<> <>

View File

@@ -50,7 +50,7 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
searchCallback(event); searchCallback(event);
} }
function onChangeSortDirection(_: any) { function onChangeSortDirection() {
if (props.filter.sortDirection === "asc") { if (props.filter.sortDirection === "asc") {
props.onChangeSortDirection("desc"); props.onChangeSortDirection("desc");
} else { } else {
@@ -160,7 +160,7 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
} }
function renderMore() { function renderMore() {
let options = [ const options = [
renderSelectAll(), renderSelectAll(),
renderSelectNone() renderSelectNone()
]; ];
@@ -201,7 +201,7 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
type="range" type="range"
min={0} min={0}
max={3} max={3}
onChange={(event: any) => onChangeZoom(Number.parseInt(event.target.value))} onChange={(event: any) => onChangeZoom(Number.parseInt(event.target.value, 10))}
/> />
</span> </span>
); );

View File

@@ -6,112 +6,57 @@ interface IPaginationProps {
currentPage: number; currentPage: number;
totalItems: number; totalItems: number;
onChangePage: (page: number) => void; onChangePage: (page: number) => void;
loading?: boolean;
} }
interface IPaginationState { export const Pagination: React.FC<IPaginationProps> = ({ itemsPerPage, currentPage, totalItems, onChangePage }) => {
pages: number[]; const totalPages = Math.ceil(totalItems / itemsPerPage);
totalPages: number;
}
export class Pagination extends React.Component<IPaginationProps, IPaginationState> { let startPage: number;
constructor(props: IPaginationProps) { let endPage: number;
super(props); if (totalPages <= 10) {
this.state = { // less than 10 total pages so show all
pages: [], startPage = 1;
totalPages: Number.MAX_SAFE_INTEGER, endPage = totalPages;
}; } else if (currentPage <= 6) {
startPage = 1;
endPage = 10;
} else if (currentPage + 4 >= totalPages) {
startPage = totalPages - 9;
endPage = totalPages;
} else {
startPage = currentPage - 5;
endPage = currentPage + 4;
} }
public componentWillMount() { const pages = [...Array((endPage + 1) - startPage).keys()].map((i) => startPage + i);
this.setPage(this.props.currentPage, false);
}
public componentDidUpdate(prevProps: IPaginationProps) { const pageButtons = pages.map((page: number) => (
if (this.props.loading) <Button
return; key={page}
if (this.props.totalItems !== prevProps.totalItems || this.props.itemsPerPage !== prevProps.itemsPerPage) { active={currentPage === page}
this.setPage(this.props.currentPage); onClick={() => onChangePage(page)}
} >{page}</Button>
} ));
public render() { return (
if (!this.state || !this.state.pages || this.state.pages.length <= 1) { return null; } <ButtonGroup className="filter-container">
return (
<ButtonGroup className="filter-container">
<Button
disabled={this.props.currentPage === 1}
onClick={() => this.setPage(1)}
>First</Button>
<Button
disabled={this.props.currentPage === 1}
onClick={() => this.setPage(this.props.currentPage - 1)}
>Previous</Button>
{this.renderPageButtons()}
<Button
disabled={this.props.currentPage === this.state.totalPages}
onClick={() => this.setPage(this.props.currentPage + 1)}
>Next</Button>
<Button
disabled={this.props.currentPage === this.state.totalPages}
onClick={() => this.setPage(this.state.totalPages)}
>Last</Button>
</ButtonGroup>
);
}
private renderPageButtons() {
return this.state.pages.map((page: number, index: number) => (
<Button <Button
key={index} disabled={currentPage === 1}
active={this.props.currentPage === page} onClick={() => onChangePage(1)}
onClick={() => this.setPage(page)} >First</Button>
>{page}</Button> <Button
)); disabled={currentPage === 1}
} onClick={() => onChangePage(currentPage - 1)}
>Previous</Button>
private setPage(page?: number, propagate: boolean = true) { {pageButtons}
if (page === undefined) { return; } <Button
disabled={currentPage === totalPages}
const pagerState = this.getPagerState(this.props.totalItems, page, this.props.itemsPerPage); onClick={() => onChangePage(currentPage + 1)}
>Next</Button>
if (page < 1) { page = 1; } <Button
if (page > pagerState.totalPages) { page = pagerState.totalPages; } disabled={currentPage === totalPages}
onClick={() => onChangePage(totalPages)}
this.setState(pagerState); >Last</Button>
if (propagate) { this.props.onChangePage(page); } </ButtonGroup>
} );
private getPagerState(totalItems: number, currentPage: number, pageSize: number) {
const totalPages = Math.max(Math.ceil(totalItems / pageSize), 1);
let startPage: number;
let endPage: number;
if (totalPages <= 10) {
// less than 10 total pages so show all
startPage = 1;
endPage = totalPages;
} else {
// more than 10 total pages so calculate start and end pages
if (currentPage <= 6) {
startPage = 1;
endPage = 10;
} else if (currentPage + 4 >= totalPages) {
startPage = totalPages - 9;
endPage = totalPages;
} else {
startPage = currentPage - 5;
endPage = currentPage + 4;
}
}
// create an array of pages numbers
const pages = [...Array((endPage + 1) - startPage).keys()].map((i) => startPage + i);
return {
pages,
totalPages,
};
}
} }

View File

@@ -16,7 +16,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = (props: IPerformerCa
function maybeRenderFavoriteBanner() { function maybeRenderFavoriteBanner() {
if (props.performer.favorite === false) { return; } if (props.performer.favorite === false) { return; }
return ( return (
<div className={`rating-banner rating-5`}> <div className="rating-banner rating-5">
FAVORITE FAVORITE
</div> </div>
); );

View File

@@ -1,3 +1,5 @@
/* eslint-disable react/no-this-in-sfc */
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Button, Form, Spinner, Table } from 'react-bootstrap'; import { Button, Form, Spinner, Table } from 'react-bootstrap';
import { useParams, useHistory } from 'react-router-dom'; import { useParams, useHistory } from 'react-router-dom';
@@ -94,7 +96,7 @@ export const Performer: React.FC = () => {
ImageUtils.usePasteImage(onImageLoad); ImageUtils.usePasteImage(onImageLoad);
useEffect(() => { useEffect(() => {
var newQueryableScrapers : GQL.ListPerformerScrapersListPerformerScrapers[] = []; let newQueryableScrapers : GQL.ListPerformerScrapersListPerformerScrapers[] = [];
if (!!Scrapers.data && Scrapers.data.listPerformerScrapers) { if (!!Scrapers.data && Scrapers.data.listPerformerScrapers) {
newQueryableScrapers = Scrapers.data.listPerformerScrapers.filter((s) => { newQueryableScrapers = Scrapers.data.listPerformerScrapers.filter((s) => {
@@ -256,9 +258,9 @@ export const Performer: React.FC = () => {
); );
} }
function urlScrapable(url: string) { function urlScrapable(scrapedUrl: string) {
return !!url && (Scrapers?.data?.listPerformerScrapers ?? []).some(s => ( return !!scrapedUrl && (Scrapers?.data?.listPerformerScrapers ?? []).some(s => (
(s?.performer?.urls ?? []).some(u => url.includes(u)) (s?.performer?.urls ?? []).some(u => scrapedUrl.includes(u))
)); ));
} }
@@ -316,14 +318,13 @@ export const Performer: React.FC = () => {
onAutoTag={onAutoTag} onAutoTag={onAutoTag}
/> />
<h1> <h1>
{ <Form.Control <Form.Control
readOnly={!isEditing} readOnly={!isEditing}
plaintext={!isEditing} plaintext={!isEditing}
defaultValue={name} defaultValue={name}
placeholder="Name" placeholder="Name"
onChange={(event: any) => setName(event.target.value)} onChange={(event: any) => setName(event.target.value)}
/> />
}
</h1> </h1>
<h6> <h6>
<Form.Group className="aliases-field" controlId="aliases"> <Form.Group className="aliases-field" controlId="aliases">

View File

@@ -20,21 +20,21 @@ export const PerformerList: React.FC = () => {
]; ];
const listData = usePerformersList({ const listData = usePerformersList({
otherOperations: otherOperations, otherOperations,
renderContent, renderContent,
}); });
async function getRandom(result: QueryHookResult<FindPerformersQuery, FindPerformersVariables>, filter: ListFilterModel) { async function getRandom(result: QueryHookResult<FindPerformersQuery, FindPerformersVariables>, filter: ListFilterModel) {
if (result.data && result.data.findPerformers) { if (result.data && result.data.findPerformers) {
let count = result.data.findPerformers.count; const {count} = result.data.findPerformers;
let index = Math.floor(Math.random() * count); const index = Math.floor(Math.random() * count);
let filterCopy = _.cloneDeep(filter); const filterCopy = _.cloneDeep(filter);
filterCopy.itemsPerPage = 1; filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1; filterCopy.currentPage = index + 1;
const singleResult = await StashService.queryFindPerformers(filterCopy); const singleResult = await StashService.queryFindPerformers(filterCopy);
if (singleResult && singleResult.data && singleResult.data.findPerformers && singleResult.data.findPerformers.performers.length === 1) { if (singleResult && singleResult.data && singleResult.data.findPerformers && singleResult.data.findPerformers.performers.length === 1) {
let id = singleResult!.data!.findPerformers!.performers[0]!.id; const {id} = singleResult!.data!.findPerformers!.performers[0]!;
history.push("/performers/" + id); history.push(`/performers/${ id}`);
} }
} }
} }
@@ -48,10 +48,8 @@ export const PerformerList: React.FC = () => {
{result.data.findPerformers.performers.map((p) => (<PerformerCard key={p.id} performer={p} />))} {result.data.findPerformers.performers.map((p) => (<PerformerCard key={p.id} performer={p} />))}
</div> </div>
); );
} else if (filter.displayMode === DisplayMode.List) { } if (filter.displayMode === DisplayMode.List) {
return <PerformerListTable performers={result.data.findPerformers.performers}/>; return <PerformerListTable performers={result.data.findPerformers.performers}/>;
} else if (filter.displayMode === DisplayMode.Wall) {
return;
} }
} }

View File

@@ -1,3 +1,5 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from "react"; import React from "react";
import { Button, Table } from 'react-bootstrap'; import { Button, Table } from 'react-bootstrap';
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@@ -80,7 +82,7 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (props: IP
<Table bordered striped> <Table bordered striped>
<thead> <thead>
<tr> <tr>
<th></th> <th />
<th>Name</th> <th>Name</th>
<th>Aliases</th> <th>Aliases</th>
<th>Favourite</th> <th>Favourite</th>

View File

@@ -5,7 +5,7 @@ import { PerformerList } from "./PerformerList";
const Performers = () => ( const Performers = () => (
<Switch> <Switch>
<Route exact={true} path="/performers" component={PerformerList} /> <Route exact path="/performers" component={PerformerList} />
<Route path="/performers/:id" component={Performer} /> <Route path="/performers/:id" component={Performer} />
</Switch> </Switch>
); );

View File

@@ -1,11 +1,11 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Button, ButtonGroup, Card, Form, Popover, OverlayTrigger } from 'react-bootstrap'; import { Button, ButtonGroup, Card, Form } from 'react-bootstrap';
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import cx from 'classnames'; import cx from 'classnames';
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import { VideoHoverHook } from "src/hooks"; import { VideoHoverHook } from "src/hooks";
import { Icon, TagLink } from 'src/components/Shared'; import { Icon, TagLink, HoverPopover } from 'src/components/Shared';
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
interface ISceneCardProps { interface ISceneCardProps {
@@ -33,8 +33,8 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
function maybeRenderSceneSpecsOverlay() { function maybeRenderSceneSpecsOverlay() {
return ( return (
<div className={`scene-specs-overlay`}> <div className="scene-specs-overlay">
{props.scene.file.height ? <span className={`overlay-resolution`}> {TextUtils.resolution(props.scene.file.height)}</span> : ''} {props.scene.file.height ? <span className="overlay-resolution"> {TextUtils.resolution(props.scene.file.height)}</span> : ''}
{props.scene.file.duration !== undefined && props.scene.file.duration >= 1 ? TextUtils.secondsToTimestamp(props.scene.file.duration) : ''} {props.scene.file.duration !== undefined && props.scene.file.duration >= 1 ? TextUtils.secondsToTimestamp(props.scene.file.duration) : ''}
</div> </div>
); );
@@ -55,7 +55,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
} }
return ( return (
<div className={`scene-studio-overlay`}> <div className="scene-studio-overlay">
<Link <Link
to={`/studios/${props.scene.studio.id}`} to={`/studios/${props.scene.studio.id}`}
style={style} style={style}
@@ -70,21 +70,20 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
if (props.scene.tags.length <= 0) if (props.scene.tags.length <= 0)
return; return;
const popover = ( const popoverContent = props.scene.tags.map((tag) => (
<Popover id="tag-popover"> <TagLink key={tag.id} tag={tag} />
{ props.scene.tags.map((tag) => ( ));
<TagLink key={tag.id} tag={tag} />
)) }
</Popover>
);
return ( return (
<OverlayTrigger trigger="hover" placement="bottom" overlay={popover}> <HoverPopover
placement="bottom"
content={popoverContent}
>
<Button> <Button>
<Icon icon="tag" /> <Icon icon="tag" />
{props.scene.tags.length} {props.scene.tags.length}
</Button> </Button>
</OverlayTrigger> </HoverPopover>
); );
} }
@@ -92,56 +91,49 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
if (props.scene.performers.length <= 0) if (props.scene.performers.length <= 0)
return; return;
const popover = ( const popoverContent = props.scene.performers.map((performer) => (
<Popover id="performer-popover"> <div className="performer-tag-container">
{ <Link
props.scene.performers.map((performer) => { to={`/performers/${performer.id}`}
return ( className="performer-tag previewable image"
<div className="performer-tag-container"> style={{backgroundImage: `url(${performer.image_path})`}}
<Link />
to={`/performers/${performer.id}`} <TagLink key={performer.id} performer={performer} />
className="performer-tag previewable image" </div>
style={{backgroundImage: `url(${performer.image_path})`}} ));
></Link>
<TagLink key={performer.id} performer={performer} />
</div>
);
})
}
</Popover>
);
return ( return (
<OverlayTrigger trigger="hover" placement="bottom" overlay={popover}> <HoverPopover
placement="bottom"
content={popoverContent}
>
<Button> <Button>
<Icon icon="user" /> <Icon icon="user" />
{props.scene.performers.length} {props.scene.performers.length}
</Button> </Button>
</OverlayTrigger> </HoverPopover>
); );
} }
function maybeRenderSceneMarkerPopoverButton() { function maybeRenderSceneMarkerPopoverButton() {
if (props.scene.scene_markers.length <= 0) if (props.scene.scene_markers.length <= 0)
return; return;
const popover = ( const popoverContent = props.scene.scene_markers.map(marker => {
<Popover id="marker-popover"> const markerPopover = { ...marker, scene: { id: props.scene.id } };
{ props.scene.scene_markers.map((marker) => { return <TagLink key={marker.id} marker={markerPopover} />;
(marker as any).scene = {}; });
(marker as any).scene.id = props.scene.id;
return <TagLink key={marker.id} marker={marker} />;
}) }
</Popover>
);
return ( return (
<OverlayTrigger trigger="hover" placement="bottom" overlay={popover}> <HoverPopover
placement="bottom"
content={popoverContent}
>
<Button> <Button>
<Icon icon="tag" /> <Icon icon="tag" />
{props.scene.scene_markers.length} {props.scene.scene_markers.length}
</Button> </Button>
</OverlayTrigger> </HoverPopover>
); );
} }
@@ -174,13 +166,13 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
} }
function isPortrait() { function isPortrait() {
let file = props.scene.file; const {file} = props.scene;
let width = file.width ? file.width : 0; const width = file.width ? file.width : 0;
let height = file.height ? file.height : 0; const height = file.height ? file.height : 0;
return height > width; return height > width;
} }
var shiftKey = false; let shiftKey = false;
return ( return (
<Card <Card
@@ -193,7 +185,11 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
className="card-select" className="card-select"
checked={props.selected} checked={props.selected}
onChange={() => props.onSelectedChanged(!props.selected, shiftKey)} onChange={() => props.onSelectedChanged(!props.selected, shiftKey)}
onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => { shiftKey = event.shiftKey; event.stopPropagation(); } } onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
// eslint-disable-next-line prefer-destructuring
shiftKey = event.shiftKey;
event.stopPropagation();
}}
/> />
<Link to={`/scenes/${props.scene.id}`} className={cx('image', 'previewable', {portrait: isPortrait()})}> <Link to={`/scenes/${props.scene.id}`} className={cx('image', 'previewable', {portrait: isPortrait()})}>
<div className="video-container"> <div className="video-container">
@@ -212,7 +208,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
</Link> </Link>
<div className="card-section"> <div className="card-section">
<h4 className="text-truncate"> <h4 className="text-truncate">
{!!props.scene.title ? props.scene.title : TextUtils.fileNameFromPath(props.scene.path)} {props.scene.title ? props.scene.title : TextUtils.fileNameFromPath(props.scene.path)}
</h4> </h4>
<span>{props.scene.date}</span> <span>{props.scene.date}</span>
<p>{TextUtils.truncate(props.scene.details, 100, "... (continued)")}</p> <p>{TextUtils.truncate(props.scene.details, 100, "... (continued)")}</p>

View File

@@ -29,8 +29,8 @@ export const Scene: React.FC = () => {
function getInitialTimestamp() { function getInitialTimestamp() {
const params = queryString.parse(location.search); const params = queryString.parse(location.search);
const timestamp = params?.t; const initialTimestamp = params?.t ?? '0';
return Number.parseInt(Array.isArray(timestamp) ? timestamp[0] : timestamp ?? '0', 10); return Number.parseInt(Array.isArray(initialTimestamp) ? initialTimestamp[0] : initialTimestamp, 10);
} }
function onClickMarker(marker: GQL.SceneMarkerDataFragment) { function onClickMarker(marker: GQL.SceneMarkerDataFragment) {
@@ -45,13 +45,13 @@ export const Scene: React.FC = () => {
return <div>{error.message}</div> return <div>{error.message}</div>
const modifiedScene = const modifiedScene =
Object.assign({scene_marker_tags: data.sceneMarkerTags}, scene) as GQL.SceneDataFragment; // TODO Hack from angular ({scene_marker_tags: data.sceneMarkerTags, ...scene}) as GQL.SceneDataFragment; // TODO Hack from angular
return ( return (
<> <>
<ScenePlayer scene={modifiedScene} timestamp={timestamp} autoplay={autoplay}/> <ScenePlayer scene={modifiedScene} timestamp={timestamp} autoplay={autoplay}/>
<Card id="details-container"> <Card id="details-container">
<Tabs id="scene-tabs" mountOnEnter={true}> <Tabs id="scene-tabs" mountOnEnter>
<Tab eventKey="scene-details-panel" title="Details"> <Tab eventKey="scene-details-panel" title="Details">
<SceneDetailPanel scene={modifiedScene} /> <SceneDetailPanel scene={modifiedScene} />
</Tab> </Tab>
@@ -67,7 +67,7 @@ export const Scene: React.FC = () => {
<ScenePerformerPanel scene={modifiedScene} /> <ScenePerformerPanel scene={modifiedScene} />
</Tab> : '' </Tab> : ''
} }
{!!modifiedScene.gallery ? {modifiedScene.gallery ?
<Tab <Tab
eventKey="scene-gallery-panel" eventKey="scene-gallery-panel"
title="Gallery"> title="Gallery">

View File

@@ -8,7 +8,7 @@ interface ISceneDetailProps {
scene: GQL.SceneDataFragment; scene: GQL.SceneDataFragment;
} }
export default SceneDetailPanel: React.FC<ISceneDetailProps> = (props: ISceneDetailProps) => { export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
function renderDetails() { function renderDetails() {
if (!props.scene.details || props.scene.details === "") { return; } if (!props.scene.details || props.scene.details === "") { return; }
return ( return (
@@ -36,7 +36,7 @@ export default SceneDetailPanel: React.FC<ISceneDetailProps> = (props: ISceneDet
<> <>
{SceneHelpers.maybeRenderStudio(props.scene, 70)} {SceneHelpers.maybeRenderStudio(props.scene, 70)}
<h1> <h1>
{!!props.scene.title ? props.scene.title : TextUtils.fileNameFromPath(props.scene.path)} {props.scene.title ? props.scene.title : TextUtils.fileNameFromPath(props.scene.path)}
</h1> </h1>
{props.scene.date ? <h4>{props.scene.date}</h4> : ''} {props.scene.date ? <h4>{props.scene.date}</h4> : ''}
{props.scene.rating ? <h6>Rating: {props.scene.rating}</h6> : ''} {props.scene.rating ? <h6>Rating: {props.scene.rating}</h6> : ''}

View File

@@ -1,3 +1,5 @@
/* eslint-disable react/no-this-in-sfc */
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Collapse, Dropdown, DropdownButton, Form, Button, Spinner } from 'react-bootstrap'; import { Collapse, Dropdown, DropdownButton, Form, Button, Spinner } from 'react-bootstrap';
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
@@ -42,7 +44,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
const deleteScene = StashService.useSceneDestroy(getSceneDeleteInput()); const deleteScene = StashService.useSceneDestroy(getSceneDeleteInput());
useEffect(() => { useEffect(() => {
var newQueryableScrapers : GQL.ListSceneScrapersListSceneScrapers[] = []; let newQueryableScrapers : GQL.ListSceneScrapersListSceneScrapers[] = [];
if (!!Scrapers.data && Scrapers.data.listSceneScrapers) { if (!!Scrapers.data && Scrapers.data.listSceneScrapers) {
newQueryableScrapers = Scrapers.data.listSceneScrapers.filter((s) => { newQueryableScrapers = Scrapers.data.listSceneScrapers.filter((s) => {
@@ -55,8 +57,8 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
}, [Scrapers.data]) }, [Scrapers.data])
function updateSceneEditState(state: Partial<GQL.SceneDataFragment>) { function updateSceneEditState(state: Partial<GQL.SceneDataFragment>) {
const perfIds = !!state.performers ? state.performers.map((performer) => performer.id) : undefined; const perfIds = state.performers ? state.performers.map((performer) => performer.id) : undefined;
const tIds = !!state.tags ? state.tags.map((tag) => tag.id) : undefined; const tIds = state.tags ? state.tags.map((tag) => tag.id) : undefined;
setTitle(state.title); setTitle(state.title);
setDetails(state.details); setDetails(state.details);
@@ -130,7 +132,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
return ( return (
<FilterSelect <FilterSelect
type={type} type={type}
isMulti={true} isMulti
onSelect={(items) => { onSelect={(items) => {
const ids = items.map((i) => i.id); const ids = items.map((i) => i.id);
switch (type) { switch (type) {
@@ -200,10 +202,10 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
); );
} }
function urlScrapable(url: string) : boolean { function urlScrapable(scrapedUrl: string) : boolean {
return !!url && !!Scrapers.data && Scrapers.data.listSceneScrapers && Scrapers.data.listSceneScrapers.some((s) => { return (Scrapers?.data?.listSceneScrapers ?? []).some(s => (
return !!s.scene && !!s.scene.urls && s.scene.urls.some((u) => { return url.includes(u); }); (s?.scene?.urls ?? []).some(u => scrapedUrl.includes(u))
}); ));
} }
function updateSceneFromScrapedScene(scene : GQL.ScrapedSceneDataFragment) { function updateSceneFromScrapedScene(scene : GQL.ScrapedSceneDataFragment) {
@@ -228,23 +230,23 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
} }
if ((!performerIds || performerIds.length === 0) && scene.performers && scene.performers.length > 0) { if ((!performerIds || performerIds.length === 0) && scene.performers && scene.performers.length > 0) {
let idPerfs = scene.performers.filter((p) => { const idPerfs = scene.performers.filter((p) => {
return p.id !== undefined && p.id !== null; return p.id !== undefined && p.id !== null;
}); });
if (idPerfs.length > 0) { if (idPerfs.length > 0) {
let newIds = idPerfs.map((p) => p.id); const newIds = idPerfs.map((p) => p.id);
setPerformerIds(newIds as string[]); setPerformerIds(newIds as string[]);
} }
} }
if ((!tagIds || tagIds.length === 0) && scene.tags && scene.tags.length > 0) { if ((!tagIds || tagIds.length === 0) && scene.tags && scene.tags.length > 0) {
let idTags = scene.tags.filter((p) => { const idTags = scene.tags.filter((p) => {
return p.id !== undefined && p.id !== null; return p.id !== undefined && p.id !== null;
}); });
if (idTags.length > 0) { if (idTags.length > 0) {
let newIds = idTags.map((p) => p.id); const newIds = idTags.map((p) => p.id);
setTagIds(newIds as string[]); setTagIds(newIds as string[]);
} }
} }
@@ -356,10 +358,10 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
</Form.Group> </Form.Group>
<div> <div>
<label onClick={() => setIsCoverImageOpen(!isCoverImageOpen)}> <Button variant="link" onClick={() => setIsCoverImageOpen(!isCoverImageOpen)}>
<Icon icon={isCoverImageOpen ? "chevron-down" : "chevron-right"} /> <Icon icon={isCoverImageOpen ? "chevron-down" : "chevron-right"} />
<span>Cover Image</span> <span>Cover Image</span>
</label> </Button>
<Collapse in={isCoverImageOpen}> <Collapse in={isCoverImageOpen}>
<div> <div>
<img className="scene-cover" src={coverImagePreview} alt="" /> <img className="scene-cover" src={coverImagePreview} alt="" />

View File

@@ -22,7 +22,7 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (props: IS
return ( return (
<tr> <tr>
<td>Path</td> <td>Path</td>
<td><a href={`file://${path}`}>{"file://"+props.scene.path}</a> </td> <td><a href={`file://${path}`}>{`file://${props.scene.path}`}</a> </td>
</tr> </tr>
); );
} }

View File

@@ -1,6 +1,6 @@
import React, { CSSProperties, useState } from "react"; import React, { CSSProperties, useState } from "react";
import { Badge, Button, Card, Collapse, Form as BootstrapForm } from 'react-bootstrap'; import { Badge, Button, Card, Collapse, Form as BootstrapForm } from 'react-bootstrap';
import { Field, FieldProps, Form, Formik, FormikActions } from "formik"; import { Field, FieldProps, Form, Formik } from "formik";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
@@ -93,7 +93,7 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISce
} }
function renderForm() { function renderForm() {
function onSubmit(values: IFormFields, _: FormikActions<IFormFields>) { function onSubmit(values: IFormFields) {
const isEditing = !!editingMarker; const isEditing = !!editingMarker;
const variables: GQL.SceneMarkerCreateVariables | GQL.SceneMarkerUpdateVariables = { const variables: GQL.SceneMarkerCreateVariables | GQL.SceneMarkerUpdateVariables = {
title: values.title, title: values.title,
@@ -118,9 +118,9 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISce
} }
function onDelete() { function onDelete() {
if (!editingMarker) { return; } if (!editingMarker) { return; }
sceneMarkerDestroy({variables: {id: editingMarker.id}}).then((response) => { sceneMarkerDestroy({variables: {id: editingMarker.id}})
console.log(response); // eslint-disable-next-line no-console
}).catch((err) => console.error(err)); .catch(err => console.error(err));
setIsEditorOpen(false); setIsEditorOpen(false);
setEditingMarker(null); setEditingMarker(null);
} }
@@ -152,7 +152,7 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISce
function renderTagsField(fieldProps: FieldProps<IFormFields>) { function renderTagsField(fieldProps: FieldProps<IFormFields>) {
return ( return (
<TagSelect <TagSelect
isMulti={true} isMulti
onSelect={(tags) => fieldProps.form.setFieldValue("tagIds", tags.map((tag) => tag.id))} onSelect={(tags) => fieldProps.form.setFieldValue("tagIds", tags.map((tag) => tag.id))}
initialIds={editingMarker ? fieldProps.form.values.tagIds : []} initialIds={editingMarker ? fieldProps.form.values.tagIds : []}
/> />

View File

@@ -1,25 +1,26 @@
/* eslint-disable no-param-reassign, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
import React, { useEffect, useState, useCallback } from "react"; import React, { useEffect, useState, useCallback } from "react";
import { Badge, Button, Card, Collapse, Dropdown, DropdownButton, Form, Table, Spinner } from 'react-bootstrap'; import { Badge, Button, Card, Collapse, Dropdown, DropdownButton, Form, Table, Spinner } from 'react-bootstrap';
import _ from "lodash"; import _ from "lodash";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { SlimSceneDataFragment, Maybe } from "src/core/generated-graphql";
import { FilterSelect, Icon, StudioSelect } from "src/components/Shared"; import { FilterSelect, Icon, StudioSelect } from "src/components/Shared";
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { Pagination } from "../list/Pagination"; import { Pagination } from "../list/Pagination";
class ParserResult<T> { class ParserResult<T> {
public value: Maybe<T>; public value: GQL.Maybe<T>;
public originalValue: Maybe<T>; public originalValue: GQL.Maybe<T>;
public set: boolean = false; public set: boolean = false;
public setOriginalValue(v : Maybe<T>) { public setOriginalValue(v : GQL.Maybe<T>) {
this.originalValue = v; this.originalValue = v;
this.value = v; this.value = v;
} }
public setValue(v : Maybe<T>) { public setValue(v : GQL.Maybe<T>) {
if (v) { if (v) {
this.value = v; this.value = v;
this.set = !_.isEqual(this.value, this.originalValue); this.set = !_.isEqual(this.value, this.originalValue);
@@ -37,7 +38,7 @@ class ParserField {
} }
public getFieldPattern() { public getFieldPattern() {
return "{" + this.field + "}"; return `{${ this.field }}`;
} }
static Title = new ParserField("title"); static Title = new ParserField("title");
@@ -106,7 +107,7 @@ class SceneParserResult {
public performers: ParserResult<GQL.SlimSceneDataPerformers[]> = new ParserResult(); public performers: ParserResult<GQL.SlimSceneDataPerformers[]> = new ParserResult();
public performerIds: ParserResult<string[]> = new ParserResult(); public performerIds: ParserResult<string[]> = new ParserResult();
public scene : SlimSceneDataFragment; public scene : GQL.SlimSceneDataFragment;
constructor(result : GQL.ParseSceneFilenamesResults) { constructor(result : GQL.ParseSceneFilenamesResults) {
this.scene = result.scene; this.scene = result.scene;
@@ -157,9 +158,9 @@ class SceneParserResult {
} }
} }
private static setInput(object: any, key: string, parserResult : ParserResult<any>) { private static setInput(obj: any, key: string, parserResult : ParserResult<any>) {
if (parserResult.set) { if (parserResult.set) {
object[key] = parserResult.value; obj[key] = parserResult.value;
} }
} }
@@ -169,7 +170,7 @@ class SceneParserResult {
} }
public toSceneUpdateInput() { public toSceneUpdateInput() {
var ret = { const ret = {
id: this.id, id: this.id,
title: this.scene.title, title: this.scene.title,
details: this.scene.details, details: this.scene.details,
@@ -294,16 +295,16 @@ export const SceneFilenameParser: React.FC = () => {
const updateScenes = StashService.useScenesUpdate(getScenesUpdateData()); const updateScenes = StashService.useScenesUpdate(getScenesUpdateData());
const determineFieldsToHide = useCallback(() => { const determineFieldsToHide = useCallback(() => {
var pattern = parserInput.pattern; const {pattern} = parserInput;
var titleSet = pattern.includes("{title}"); const titleSet = pattern.includes("{title}");
var dateSet = pattern.includes("{date}") || const dateSet = pattern.includes("{date}") ||
pattern.includes("{dd}") || // don't worry about other partial date fields since this should be implied pattern.includes("{dd}") || // don't worry about other partial date fields since this should be implied
ParserField.fullDateFields.some((f) => { ParserField.fullDateFields.some((f) => {
return pattern.includes("{" + f.field + "}"); return pattern.includes(`{${ f.field }}`);
}); });
var performerSet = pattern.includes("{performer}"); const performerSet = pattern.includes("{performer}");
var tagSet = pattern.includes("{tag}"); const tagSet = pattern.includes("{tag}");
var studioSet = pattern.includes("{studio}"); const studioSet = pattern.includes("{studio}");
const newShowFields = new Map<string, boolean>([ const newShowFields = new Map<string, boolean>([
["Title", titleSet], ["Title", titleSet],
@@ -318,7 +319,7 @@ export const SceneFilenameParser: React.FC = () => {
const parseResults = useCallback((results : GQL.ParseSceneFilenamesResults[]) => { const parseResults = useCallback((results : GQL.ParseSceneFilenamesResults[]) => {
if (results) { if (results) {
var result = results.map((r) => { const result = results.map((r) => {
return new SceneParserResult(r); return new SceneParserResult(r);
}).filter((r) => !!r) as SceneParserResult[]; }).filter((r) => !!r) as SceneParserResult[];
@@ -348,7 +349,7 @@ export const SceneFilenameParser: React.FC = () => {
StashService.queryParseSceneFilenames(parserFilter, parserInputData) StashService.queryParseSceneFilenames(parserFilter, parserInputData)
.then((response) => { .then((response) => {
let result = response.data.parseSceneFilenames; const result = response.data.parseSceneFilenames;
if (result) { if (result) {
parseResults(result.results); parseResults(result.results);
setTotalItems(result.count); setTotalItems(result.count);
@@ -364,7 +365,7 @@ export const SceneFilenameParser: React.FC = () => {
}, [parserInput, parseResults, Toast]); }, [parserInput, parseResults, Toast]);
function onPageSizeChanged(newSize : number) { function onPageSizeChanged(newSize : number) {
var newInput = _.clone(parserInput); const newInput = _.clone(parserInput);
newInput.page = 1; newInput.page = 1;
newInput.pageSize = newSize; newInput.pageSize = newSize;
setParserInput(newInput); setParserInput(newInput);
@@ -372,7 +373,7 @@ export const SceneFilenameParser: React.FC = () => {
function onPageChanged(newPage : number) { function onPageChanged(newPage : number) {
if (newPage !== parserInput.page) { if (newPage !== parserInput.page) {
var newInput = _.clone(parserInput); const newInput = _.clone(parserInput);
newInput.page = newPage; newInput.page = newPage;
setParserInput(newInput); setParserInput(newInput);
} }
@@ -403,19 +404,19 @@ export const SceneFilenameParser: React.FC = () => {
} }
useEffect(() => { useEffect(() => {
var newAllTitleSet = !parserResult.some((r) => { const newAllTitleSet = !parserResult.some((r) => {
return !r.title.set; return !r.title.set;
}); });
var newAllDateSet = !parserResult.some((r) => { const newAllDateSet = !parserResult.some((r) => {
return !r.date.set; return !r.date.set;
}); });
var newAllPerformerSet = !parserResult.some((r) => { const newAllPerformerSet = !parserResult.some((r) => {
return !r.performerIds.set; return !r.performerIds.set;
}); });
var newAllTagSet = !parserResult.some((r) => { const newAllTagSet = !parserResult.some((r) => {
return !r.tagIds.set; return !r.tagIds.set;
}); });
var newAllStudioSet = !parserResult.some((r) => { const newAllStudioSet = !parserResult.some((r) => {
return !r.studioId.set; return !r.studioId.set;
}); });
@@ -427,7 +428,7 @@ export const SceneFilenameParser: React.FC = () => {
}, [parserResult]); }, [parserResult]);
function onSelectAllTitleSet(selected : boolean) { function onSelectAllTitleSet(selected : boolean) {
var newResult = [...parserResult]; const newResult = [...parserResult];
newResult.forEach((r) => { newResult.forEach((r) => {
r.title.set = selected; r.title.set = selected;
@@ -438,7 +439,7 @@ export const SceneFilenameParser: React.FC = () => {
} }
function onSelectAllDateSet(selected : boolean) { function onSelectAllDateSet(selected : boolean) {
var newResult = [...parserResult]; const newResult = [...parserResult];
newResult.forEach((r) => { newResult.forEach((r) => {
r.date.set = selected; r.date.set = selected;
@@ -449,7 +450,7 @@ export const SceneFilenameParser: React.FC = () => {
} }
function onSelectAllPerformerSet(selected : boolean) { function onSelectAllPerformerSet(selected : boolean) {
var newResult = [...parserResult]; const newResult = [...parserResult];
newResult.forEach((r) => { newResult.forEach((r) => {
r.performerIds.set = selected; r.performerIds.set = selected;
@@ -460,7 +461,7 @@ export const SceneFilenameParser: React.FC = () => {
} }
function onSelectAllTagSet(selected : boolean) { function onSelectAllTagSet(selected : boolean) {
var newResult = [...parserResult]; const newResult = [...parserResult];
newResult.forEach((r) => { newResult.forEach((r) => {
r.tagIds.set = selected; r.tagIds.set = selected;
@@ -471,7 +472,7 @@ export const SceneFilenameParser: React.FC = () => {
} }
function onSelectAllStudioSet(selected : boolean) { function onSelectAllStudioSet(selected : boolean) {
var newResult = [...parserResult]; const newResult = [...parserResult];
newResult.forEach((r) => { newResult.forEach((r) => {
r.studioId.set = selected; r.studioId.set = selected;
@@ -530,10 +531,10 @@ export const SceneFilenameParser: React.FC = () => {
function onFind() { function onFind() {
props.onFind({ props.onFind({
pattern: pattern, pattern,
ignoreWords: ignoreWords.split(" "), ignoreWords: ignoreWords.split(" "),
whitespaceCharacters: whitespaceCharacters, whitespaceCharacters,
capitalizeTitle: capitalizeTitle, capitalizeTitle,
page: 1, page: 1,
pageSize: props.input.pageSize, pageSize: props.input.pageSize,
findClicked: props.input.findClicked findClicked: props.input.findClicked
@@ -569,7 +570,7 @@ export const SceneFilenameParser: React.FC = () => {
</Dropdown.Item> </Dropdown.Item>
))} ))}
</DropdownButton> </DropdownButton>
<div>Use '\\' to escape literal {} characters</div> <div>Use &apos;\\&apos; to escape literal {} characters</div>
</Form.Group> </Form.Group>
<Form.Group> <Form.Group>
@@ -624,7 +625,7 @@ export const SceneFilenameParser: React.FC = () => {
as="select" as="select"
style={{flexBasis: "min-content"}} style={{flexBasis: "min-content"}}
options={PAGE_SIZE_OPTIONS} options={PAGE_SIZE_OPTIONS}
onChange={(event: any) => onPageSizeChanged(parseInt(event.target.value))} onChange={(event: any) => onPageSizeChanged(parseInt(event.target.value, 10))}
defaultValue={props.input.pageSize} defaultValue={props.input.pageSize}
className="filter-item" className="filter-item"
> >
@@ -678,13 +679,13 @@ export const SceneFilenameParser: React.FC = () => {
} }
function renderOriginalInputGroup(props: ISceneParserFieldProps) { function renderOriginalInputGroup(props: ISceneParserFieldProps) {
var parserResult = props.originalParserResult || props.parserResult; const result = props.originalParserResult || props.parserResult;
return ( return (
<Form.Control <Form.Control
disabled disabled
className={props.className} className={props.className}
defaultValue={parserResult.originalValue || ""} defaultValue={result.originalValue || ""}
/> />
); );
} }
@@ -706,27 +707,27 @@ export const SceneFilenameParser: React.FC = () => {
); );
} }
function renderNewInputGroup(props : ISceneParserFieldProps, onChange : (value : any) => void) { function renderNewInputGroup(props : ISceneParserFieldProps, onChangeHandler : (value : any) => void) {
return ( return (
<InputGroupWrapper <InputGroupWrapper
className={props.className} className={props.className}
onChange={(value : any) => {onChange(value)}} onChange={(value : any) => {onChangeHandler(value)}}
parserResult={props.parserResult} parserResult={props.parserResult}
/> />
); );
} }
interface HasName { interface IHasName {
name: string name: string
} }
function renderOriginalSelect(props : ISceneParserFieldProps) { function renderOriginalSelect(props : ISceneParserFieldProps) {
const parserResult = props.originalParserResult || props.parserResult; const result = props.originalParserResult || props.parserResult;
const elements = parserResult.originalValue const elements = result.originalValue
? Array.isArray(parserResult.originalValue) ? Array.isArray(result.originalValue)
? parserResult.originalValue.map((el:HasName) => el.name) ? result.originalValue.map((el:IHasName) => el.name)
: [parserResult.originalValue.name] : [result.originalValue.name]
: []; : [];
return ( return (
@@ -736,35 +737,35 @@ export const SceneFilenameParser: React.FC = () => {
); );
} }
function renderNewMultiSelect(type: "performers" | "tags", props : ISceneParserFieldProps, onChange : (value : any) => void) { function renderNewMultiSelect(type: "performers" | "tags", props : ISceneParserFieldProps, onChangeHandler : (value : any) => void) {
return ( return (
<FilterSelect <FilterSelect
className={props.className} className={props.className}
type={type} type={type}
isMulti={true} isMulti
onSelect={(items) => { onSelect={(items) => {
const ids = items.map((i) => i.id); const ids = items.map((i) => i.id);
onChange(ids); onChangeHandler(ids);
}} }}
initialIds={props.parserResult.value} initialIds={props.parserResult.value}
/> />
); );
} }
function renderNewPerformerSelect(props : ISceneParserFieldProps, onChange : (value : any) => void) { function renderNewPerformerSelect(props : ISceneParserFieldProps, onChangeHandler : (value : any) => void) {
return renderNewMultiSelect("performers", props, onChange); return renderNewMultiSelect("performers", props, onChangeHandler);
} }
function renderNewTagSelect(props : ISceneParserFieldProps, onChange : (value : any) => void) { function renderNewTagSelect(props : ISceneParserFieldProps, onChangeHandler : (value : any) => void) {
return renderNewMultiSelect("tags", props, onChange); return renderNewMultiSelect("tags", props, onChangeHandler);
} }
function renderNewStudioSelect(props : ISceneParserFieldProps, onChange : (value : any) => void) { function renderNewStudioSelect(props : ISceneParserFieldProps, onChangeHandler : (value : any) => void) {
return ( return (
<StudioSelect <StudioSelect
noSelectionString="" noSelectionString=""
className={props.className} className={props.className}
onSelect={(items) => onChange(items[0]?.id)} onSelect={(items) => onChangeHandler(items[0]?.id)}
initialIds={props.parserResult.value ? [props.parserResult.value] : []} initialIds={props.parserResult.value ? [props.parserResult.value] : []}
/> />
); );
@@ -778,38 +779,38 @@ export const SceneFilenameParser: React.FC = () => {
function SceneParserRow(props : ISceneParserRowProps) { function SceneParserRow(props : ISceneParserRowProps) {
function changeParser(result : ParserResult<any>, set : boolean, value : any) { function changeParser(result : ParserResult<any>, set : boolean, value : any) {
var newParser = _.clone(result); const newParser = _.clone(result);
newParser.set = set; newParser.set = set;
newParser.value = value; newParser.value = value;
return newParser; return newParser;
} }
function onTitleChanged(set : boolean, value: string | undefined) { function onTitleChanged(set : boolean, value: string | undefined) {
var newResult = _.clone(props.scene); const newResult = _.clone(props.scene);
newResult.title = changeParser(newResult.title, set, value); newResult.title = changeParser(newResult.title, set, value);
props.onChange(newResult); props.onChange(newResult);
} }
function onDateChanged(set : boolean, value: string | undefined) { function onDateChanged(set : boolean, value: string | undefined) {
var newResult = _.clone(props.scene); const newResult = _.clone(props.scene);
newResult.date = changeParser(newResult.date, set, value); newResult.date = changeParser(newResult.date, set, value);
props.onChange(newResult); props.onChange(newResult);
} }
function onPerformerIdsChanged(set : boolean, value: string[] | undefined) { function onPerformerIdsChanged(set : boolean, value: string[] | undefined) {
var newResult = _.clone(props.scene); const newResult = _.clone(props.scene);
newResult.performerIds = changeParser(newResult.performerIds, set, value); newResult.performerIds = changeParser(newResult.performerIds, set, value);
props.onChange(newResult); props.onChange(newResult);
} }
function onTagIdsChanged(set : boolean, value: string[] | undefined) { function onTagIdsChanged(set : boolean, value: string[] | undefined) {
var newResult = _.clone(props.scene); const newResult = _.clone(props.scene);
newResult.tagIds = changeParser(newResult.tagIds, set, value); newResult.tagIds = changeParser(newResult.tagIds, set, value);
props.onChange(newResult); props.onChange(newResult);
} }
function onStudioIdChanged(set : boolean, value: string | undefined) { function onStudioIdChanged(set : boolean, value: string | undefined) {
var newResult = _.clone(props.scene); const newResult = _.clone(props.scene);
newResult.studioId = changeParser(newResult.studioId, set, value); newResult.studioId = changeParser(newResult.studioId, set, value);
props.onChange(newResult); props.onChange(newResult);
} }
@@ -877,9 +878,9 @@ export const SceneFilenameParser: React.FC = () => {
} }
function onChange(scene : SceneParserResult, changedScene : SceneParserResult) { function onChange(scene : SceneParserResult, changedScene : SceneParserResult) {
var newResult = [...parserResult]; const newResult = [...parserResult];
var index = newResult.indexOf(scene); const index = newResult.indexOf(scene);
newResult[index] = changedScene; newResult[index] = changedScene;
setParserResult(newResult); setParserResult(newResult);

View File

@@ -23,7 +23,7 @@ export const SceneList: React.FC = () => {
const listData = useScenesList({ const listData = useScenesList({
zoomable: true, zoomable: true,
otherOperations: otherOperations, otherOperations,
renderContent, renderContent,
renderSelectedOptions renderSelectedOptions
}); });
@@ -31,17 +31,17 @@ export const SceneList: React.FC = () => {
async function playRandom(result: QueryHookResult<FindScenesQuery, FindScenesVariables>, filter: ListFilterModel) { async function playRandom(result: QueryHookResult<FindScenesQuery, FindScenesVariables>, filter: ListFilterModel) {
// query for a random scene // query for a random scene
if (result.data && result.data.findScenes) { if (result.data && result.data.findScenes) {
let count = result.data.findScenes.count; const {count} = result.data.findScenes;
let index = Math.floor(Math.random() * count); const index = Math.floor(Math.random() * count);
let filterCopy = _.cloneDeep(filter); const filterCopy = _.cloneDeep(filter);
filterCopy.itemsPerPage = 1; filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1; filterCopy.currentPage = index + 1;
const singleResult = await StashService.queryFindScenes(filterCopy); const singleResult = await StashService.queryFindScenes(filterCopy);
if (singleResult && singleResult.data && singleResult.data.findScenes && singleResult.data.findScenes.scenes.length === 1) { if (singleResult && singleResult.data && singleResult.data.findScenes && singleResult.data.findScenes.scenes.length === 1) {
let id = singleResult!.data!.findScenes!.scenes[0].id; const {id} = singleResult!.data!.findScenes!.scenes[0];
// navigate to the scene player page // navigate to the scene player page
history.push("/scenes/" + id + "?autoplay=true"); history.push(`/scenes/${ id }?autoplay=true`);
} }
} }
} }
@@ -50,13 +50,11 @@ export const SceneList: React.FC = () => {
// find the selected items from the ids // find the selected items from the ids
if (!result.data || !result.data.findScenes) { return undefined; } if (!result.data || !result.data.findScenes) { return undefined; }
var scenes = result.data.findScenes.scenes; const {scenes} = result.data.findScenes;
var selectedScenes : SlimSceneDataFragment[] = []; const selectedScenes : SlimSceneDataFragment[] = [];
selectedIds.forEach((id) => { selectedIds.forEach((id) => {
var scene = scenes.find((scene) => { const scene = scenes.find(s => s.id === id);
return scene.id === id;
});
if (scene) { if (scene) {
selectedScenes.push(scene); selectedScenes.push(scene);
@@ -65,7 +63,7 @@ export const SceneList: React.FC = () => {
return ( return (
<> <>
<SceneSelectedOptions selected={selectedScenes} onScenesUpdated={() => { return; }}/> <SceneSelectedOptions selected={selectedScenes} onScenesUpdated={() => { }}/>
</> </>
); );
} }
@@ -90,9 +88,9 @@ export const SceneList: React.FC = () => {
{result.data.findScenes.scenes.map((scene) => renderSceneCard(scene, selectedIds, zoomIndex))} {result.data.findScenes.scenes.map((scene) => renderSceneCard(scene, selectedIds, zoomIndex))}
</div> </div>
); );
} else if (filter.displayMode === DisplayMode.List) { } if (filter.displayMode === DisplayMode.List) {
return <SceneListTable scenes={result.data.findScenes.scenes}/>; return <SceneListTable scenes={result.data.findScenes.scenes}/>;
} else if (filter.displayMode === DisplayMode.Wall) { } if (filter.displayMode === DisplayMode.Wall) {
return <WallPanel scenes={result.data.findScenes.scenes} />; return <WallPanel scenes={result.data.findScenes.scenes} />;
} }
} }

View File

@@ -96,8 +96,7 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneList
<Table striped bordered> <Table striped bordered>
<thead> <thead>
<tr> <tr>
<th></th> <th colSpan={2}>Title</th>
<th>Title</th>
<th>Rating</th> <th>Rating</th>
<th>Duration</th> <th>Duration</th>
<th>Tags</th> <th>Tags</th>

View File

@@ -18,23 +18,23 @@ export const SceneMarkerList: React.FC = () => {
}]; }];
const listData = useSceneMarkersList({ const listData = useSceneMarkersList({
otherOperations: otherOperations, otherOperations,
renderContent, renderContent,
}); });
async function playRandom(result: QueryHookResult<FindSceneMarkersQuery, FindSceneMarkersVariables>, filter: ListFilterModel) { async function playRandom(result: QueryHookResult<FindSceneMarkersQuery, FindSceneMarkersVariables>, filter: ListFilterModel) {
// query for a random scene // query for a random scene
if (result.data && result.data.findSceneMarkers) { if (result.data && result.data.findSceneMarkers) {
let count = result.data.findSceneMarkers.count; const {count} = result.data.findSceneMarkers;
let index = Math.floor(Math.random() * count); const index = Math.floor(Math.random() * count);
let filterCopy = _.cloneDeep(filter); const filterCopy = _.cloneDeep(filter);
filterCopy.itemsPerPage = 1; filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1; filterCopy.currentPage = index + 1;
const singleResult = await StashService.queryFindSceneMarkers(filterCopy); const singleResult = await StashService.queryFindSceneMarkers(filterCopy);
if (singleResult && singleResult.data && singleResult.data.findSceneMarkers && singleResult.data.findSceneMarkers.scene_markers.length === 1) { if (singleResult && singleResult.data && singleResult.data.findSceneMarkers && singleResult.data.findSceneMarkers.scene_markers.length === 1) {
// navigate to the scene player page // navigate to the scene player page
let url = NavUtils.makeSceneMarkerUrl(singleResult.data.findSceneMarkers.scene_markers[0]) const url = NavUtils.makeSceneMarkerUrl(singleResult.data.findSceneMarkers.scene_markers[0])
history.push(url); history.push(url);
} }
} }

View File

@@ -30,6 +30,13 @@ export class ScenePlayerImpl extends React.Component<IScenePlayerProps, IScenePl
private player: any; private player: any;
private lastTime = 0; private lastTime = 0;
private KeyHandlers = {
NUM0: () => {this.onReset()},
NUM1: () => {this.onDecrease()},
NUM2: () => {this.onIncrease()},
SPACE: () => {this.onPause()}
}
constructor(props: IScenePlayerProps) { constructor(props: IScenePlayerProps) {
super(props); super(props);
this.onReady = this.onReady.bind(this); this.onReady = this.onReady.bind(this);
@@ -48,66 +55,65 @@ export class ScenePlayerImpl extends React.Component<IScenePlayerProps, IScenePl
} }
} }
renderPlayer() {
const config = this.makeJWPlayerConfig(this.props.scene);
return (
<ReactJWPlayer
playerId={SceneHelpers.getJWPlayerId()}
playerScript="/jwplayer/jwplayer.js"
customProps={config}
onReady={this.onReady}
onSeeked={this.onSeeked}
onTime={this.onTime}
/>
);
}
onIncrease() { onIncrease() {
const currentPlaybackRate = !!this.player ? this.player.getPlaybackRate() : 1; const currentPlaybackRate = this.player ? this.player.getPlaybackRate() : 1;
this.player.setPlaybackRate(currentPlaybackRate + 0.5); this.player.setPlaybackRate(currentPlaybackRate + 0.5);
}; };
onDecrease() { onDecrease() {
const currentPlaybackRate = !!this.player ? this.player.getPlaybackRate() : 1; const currentPlaybackRate = this.player ? this.player.getPlaybackRate() : 1;
this.player.setPlaybackRate(currentPlaybackRate - 0.5); this.player.setPlaybackRate(currentPlaybackRate - 0.5);
}; };
onReset() { this.player.setPlaybackRate(1); };
onPause() { this.player.getState().paused ? this.player.play() : this.player.pause(); };
private KeyHandlers = { onReset() { this.player.setPlaybackRate(1); };
NUM0: () => {this.onReset()}, onPause() {
NUM1: () => {this.onDecrease()}, if (this.player.getState().paused)
NUM2: () => {this.onIncrease()}, this.player.play();
SPACE: () => {this.onPause()} else
this.player.pause();
};
private onReady() {
this.player = SceneHelpers.getPlayer();
if (this.props.timestamp > 0) {
this.player.seek(this.props.timestamp);
}
} }
public render() { private onSeeked() {
return ( const position = this.player.getPosition();
<HotKeys keyMap={KeyMap} handlers={this.KeyHandlers}> this.setState({scrubberPosition: position});
<div id="jwplayer-container"> this.player.play();
{this.renderPlayer()} }
<ScenePlayerScrubber
scene={this.props.scene} private onTime() {
position={this.state.scrubberPosition} const position = this.player.getPosition();
onSeek={this.onScrubberSeek} const difference = Math.abs(position - this.lastTime);
onScrolled={this.onScrubberScrolled} if (difference > 1) {
/> this.lastTime = position;
</div> this.setState({scrubberPosition: position});
</HotKeys> }
); }
private onScrubberSeek(seconds: number) {
this.player.seek(seconds);
}
private onScrubberScrolled() {
this.player.pause();
} }
private shouldRepeat(scene: GQL.SceneDataFragment) { private shouldRepeat(scene: GQL.SceneDataFragment) {
let maxLoopDuration = this.props.config ? this.props.config.maximumLoopDuration : 0; const maxLoopDuration = this.props.config ? this.props.config.maximumLoopDuration : 0;
return !!scene.file.duration && !!maxLoopDuration && scene.file.duration < maxLoopDuration; return !!scene.file.duration && !!maxLoopDuration && scene.file.duration < maxLoopDuration;
} }
private makeJWPlayerConfig(scene: GQL.SceneDataFragment) { private makeJWPlayerConfig(scene: GQL.SceneDataFragment) {
if (!scene.paths.stream) { return {}; } if (!scene.paths.stream) { return {}; }
let repeat = this.shouldRepeat(scene); const repeat = this.shouldRepeat(scene);
let getDurationHook: (() => GQL.Maybe<number>) | undefined = undefined; let getDurationHook: (() => GQL.Maybe<number>) | undefined;
let seekHook: ((seekToPosition: number, _videoTag: any) => void) | undefined = undefined; let seekHook: ((seekToPosition: number, _videoTag: any) => void) | undefined;
let getCurrentTimeHook: ((_videoTag: any) => number) | undefined = undefined; let getCurrentTimeHook: ((_videoTag: any) => number) | undefined;
if (!this.props.scene.is_streamable) { if (!this.props.scene.is_streamable) {
getDurationHook = () => { getDurationHook = () => {
@@ -115,18 +121,20 @@ export class ScenePlayerImpl extends React.Component<IScenePlayerProps, IScenePl
}; };
seekHook = (seekToPosition: number, _videoTag: any) => { seekHook = (seekToPosition: number, _videoTag: any) => {
// eslint-disable-next-line no-param-reassign
_videoTag.start = seekToPosition; _videoTag.start = seekToPosition;
_videoTag.src = (this.props.scene.paths.stream + "?start=" + seekToPosition); // eslint-disable-next-line no-param-reassign
_videoTag.src = (`${this.props.scene.paths.stream }?start=${ seekToPosition}`);
_videoTag.play(); _videoTag.play();
}; };
getCurrentTimeHook = (_videoTag: any) => { getCurrentTimeHook = (_videoTag: any) => {
let start = _videoTag.start || 0; const start = _videoTag.start || 0;
return _videoTag.currentTime + start; return _videoTag.currentTime + start;
} }
} }
let ret = { const ret = {
file: scene.paths.stream, file: scene.paths.stream,
image: scene.paths.screenshot, image: scene.paths.screenshot,
tracks: [ tracks: [
@@ -147,45 +155,45 @@ export class ScenePlayerImpl extends React.Component<IScenePlayerProps, IScenePl
cast: {}, cast: {},
primary: "html5", primary: "html5",
autostart: this.props.autoplay || (this.props.config ? this.props.config.autostartVideo : false), autostart: this.props.autoplay || (this.props.config ? this.props.config.autostartVideo : false),
repeat: repeat, repeat,
playbackRateControls: true, playbackRateControls: true,
playbackRates: [0.75, 1, 1.5, 2, 3, 4], playbackRates: [0.75, 1, 1.5, 2, 3, 4],
getDurationHook: getDurationHook, getDurationHook,
seekHook: seekHook, seekHook,
getCurrentTimeHook: getCurrentTimeHook getCurrentTimeHook
}; };
return ret; return ret;
} }
private onReady() { renderPlayer() {
this.player = SceneHelpers.getPlayer(); const config = this.makeJWPlayerConfig(this.props.scene);
if (this.props.timestamp > 0) { return (
this.player.seek(this.props.timestamp); <ReactJWPlayer
} playerId={SceneHelpers.getJWPlayerId()}
playerScript="/jwplayer/jwplayer.js"
customProps={config}
onReady={this.onReady}
onSeeked={this.onSeeked}
onTime={this.onTime}
/>
);
} }
private onSeeked() { public render() {
const position = this.player.getPosition(); return (
this.setState({scrubberPosition: position}); <HotKeys keyMap={KeyMap} handlers={this.KeyHandlers}>
this.player.play(); <div id="jwplayer-container">
} {this.renderPlayer()}
<ScenePlayerScrubber
private onTime(data: any) { scene={this.props.scene}
const position = this.player.getPosition(); position={this.state.scrubberPosition}
const difference = Math.abs(position - this.lastTime); onSeek={this.onScrubberSeek}
if (difference > 1) { onScrolled={this.onScrubberScrolled}
this.lastTime = position; />
this.setState({scrubberPosition: position}); </div>
} </HotKeys>
} );
private onScrubberSeek(seconds: number) {
this.player.seek(seconds);
}
private onScrubberScrolled() {
this.player.pause();
} }
} }

View File

@@ -1,3 +1,5 @@
/* eslint-disable react/no-array-index-key */
import React, { CSSProperties, useEffect, useRef, useState, useCallback } from "react"; import React, { CSSProperties, useEffect, useRef, useState, useCallback } from "react";
import { Button } from 'react-bootstrap'; import { Button } from 'react-bootstrap';
import axios from "axios"; import axios from "axios";
@@ -24,9 +26,6 @@ interface ISceneSpriteItem {
async function fetchSpriteInfo(vttPath: string) { async function fetchSpriteInfo(vttPath: string) {
const response = await axios.get<string>(vttPath, {responseType: "text"}); const response = await axios.get<string>(vttPath, {responseType: "text"});
if (response.status !== 200) {
console.log(response.statusText);
}
// TODO: This is gnarly // TODO: This is gnarly
const lines = response.data.split("\n"); const lines = response.data.split("\n");
@@ -36,25 +35,25 @@ async function fetchSpriteInfo(vttPath: string) {
const newSpriteItems: ISceneSpriteItem[] = []; const newSpriteItems: ISceneSpriteItem[] = [];
while (lines.length) { while (lines.length) {
const line = lines.shift(); const line = lines.shift();
if (line === undefined) { continue; } if (line !== undefined) {
if (line.includes("#") && line.includes("=") && line.includes(",")) {
const size = line.split("#")[1].split("=")[1].split(",");
item.x = Number(size[0]);
item.y = Number(size[1]);
item.w = Number(size[2]);
item.h = Number(size[3]);
if (line.includes("#") && line.includes("=") && line.includes(",")) { newSpriteItems.push(item);
const size = line.split("#")[1].split("=")[1].split(","); item = {start: 0, end: 0, x: 0, y: 0, w: 0, h: 0};
item.x = Number(size[0]); } else if (line.includes(" --> ")) {
item.y = Number(size[1]); const times = line.split(" --> ");
item.w = Number(size[2]);
item.h = Number(size[3]);
newSpriteItems.push(item); const start = times[0].split(":");
item = {start: 0, end: 0, x: 0, y: 0, w: 0, h: 0}; item.start = (+start[0]) * 60 * 60 + (+start[1]) * 60 + (+start[2]);
} else if (line.includes(" --> ")) {
const times = line.split(" --> ");
const start = times[0].split(":"); const end = times[1].split(":");
item.start = (+start[0]) * 60 * 60 + (+start[1]) * 60 + (+start[2]); item.end = (+end[0]) * 60 * 60 + (+end[1]) * 60 + (+end[2]);
}
const end = times[1].split(":");
item.end = (+end[0]) * 60 * 60 + (+end[1]) * 60 + (+end[2]);
} }
} }
@@ -154,7 +153,7 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
mouseDown.current = false; mouseDown.current = false;
const delta = Math.abs(event.clientX - startMouseEvent.current.clientX); const delta = Math.abs(event.clientX - startMouseEvent.current.clientX);
if (delta < 1 && event.target instanceof HTMLDivElement) { if (delta < 1 && event.target instanceof HTMLDivElement) {
const target: HTMLDivElement = event.target; const {target} = event;
let seekSeconds: number | undefined; let seekSeconds: number | undefined;
const spriteIdString = target.getAttribute("data-sprite-item-id"); const spriteIdString = target.getAttribute("data-sprite-item-id");
@@ -171,7 +170,7 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
seekSeconds = marker.seconds; seekSeconds = marker.seconds;
} }
if (!!seekSeconds) { props.onSeek(seekSeconds); } if (seekSeconds) { props.onSeek(seekSeconds); }
} else if (Math.abs(velocity.current) > 25) { } else if (Math.abs(velocity.current) > 25) {
const newPosition = getPosition() + (velocity.current * 10); const newPosition = getPosition() + (velocity.current * 10);
setPosition(newPosition); setPosition(newPosition);
@@ -274,7 +273,7 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
width: `${sprite.w}px`, width: `${sprite.w}px`,
height: `${sprite.h}px`, height: `${sprite.h}px`,
margin: "0px auto", margin: "0px auto",
backgroundPosition: -sprite.x + "px " + -sprite.y + "px", backgroundPosition: `${-sprite.x }px ${ -sprite.y }px`,
backgroundImage: `url(${path})`, backgroundImage: `url(${path})`,
left: `${left}px`, left: `${left}px`,
}; };

View File

@@ -25,12 +25,12 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
function getSceneInput() : GQL.BulkSceneUpdateInput { function getSceneInput() : GQL.BulkSceneUpdateInput {
// need to determine what we are actually setting on each scene // need to determine what we are actually setting on each scene
var aggregateRating = getRating(props.selected); const aggregateRating = getRating(props.selected);
var aggregateStudioId = getStudioId(props.selected); const aggregateStudioId = getStudioId(props.selected);
var aggregatePerformerIds = getPerformerIds(props.selected); const aggregatePerformerIds = getPerformerIds(props.selected);
var aggregateTagIds = getTagIds(props.selected); const aggregateTagIds = getTagIds(props.selected);
var sceneInput : GQL.BulkSceneUpdateInput = { const sceneInput : GQL.BulkSceneUpdateInput = {
ids: props.selected.map((scene) => { ids: props.selected.map((scene) => {
return scene.id; return scene.id;
}) })
@@ -46,7 +46,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
// otherwise not setting the rating // otherwise not setting the rating
} else { } else {
// if rating is set, then we are setting the rating for all // if rating is set, then we are setting the rating for all
sceneInput.rating = Number.parseInt(rating); sceneInput.rating = Number.parseInt(rating, 10);
} }
// if studioId is undefined // if studioId is undefined
@@ -102,34 +102,32 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
} }
function getRating(state: GQL.SlimSceneDataFragment[]) { function getRating(state: GQL.SlimSceneDataFragment[]) {
var ret : number | undefined; let ret : number | undefined;
var first = true; let first = true;
state.forEach((scene : GQL.SlimSceneDataFragment) => { state.forEach((scene : GQL.SlimSceneDataFragment) => {
if (first) { if (first) {
ret = scene.rating; ret = scene.rating;
first = false; first = false;
} else { } else if (ret !== scene.rating) {
if (ret !== scene.rating) {
ret = undefined; ret = undefined;
} }
}
}); });
return ret; return ret;
} }
function getStudioId(state: GQL.SlimSceneDataFragment[]) { function getStudioId(state: GQL.SlimSceneDataFragment[]) {
var ret : string | undefined; let ret : string | undefined;
var first = true; let first = true;
state.forEach((scene : GQL.SlimSceneDataFragment) => { state.forEach((scene : GQL.SlimSceneDataFragment) => {
if (first) { if (first) {
ret = scene.studio ? scene.studio.id : undefined; ret = scene?.studio?.id;
first = false; first = false;
} else { } else {
var studioId = scene.studio ? scene.studio.id : undefined; const studio = scene?.studio?.id;
if (ret !== studioId) { if (ret !== studio) {
ret = undefined; ret = undefined;
} }
} }
@@ -138,20 +136,16 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
return ret; return ret;
} }
function toId(object : any) {
return object.id;
}
function getPerformerIds(state: GQL.SlimSceneDataFragment[]) { function getPerformerIds(state: GQL.SlimSceneDataFragment[]) {
var ret : string[] = []; let ret : string[] = [];
var first = true; let first = true;
state.forEach((scene : GQL.SlimSceneDataFragment) => { state.forEach((scene : GQL.SlimSceneDataFragment) => {
if (first) { if (first) {
ret = !!scene.performers ? scene.performers.map(toId).sort() : []; ret = scene.performers ? scene.performers.map(p => p.id).sort() : [];
first = false; first = false;
} else { } else {
const perfIds = !!scene.performers ? scene.performers.map(toId).sort() : []; const perfIds = scene.performers ? scene.performers.map(p => p.id).sort() : [];
if (!_.isEqual(ret, perfIds)) { if (!_.isEqual(ret, perfIds)) {
ret = []; ret = [];
@@ -163,15 +157,15 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
} }
function getTagIds(state: GQL.SlimSceneDataFragment[]) { function getTagIds(state: GQL.SlimSceneDataFragment[]) {
var ret : string[] = []; let ret : string[] = [];
var first = true; let first = true;
state.forEach((scene : GQL.SlimSceneDataFragment) => { state.forEach((scene : GQL.SlimSceneDataFragment) => {
if (first) { if (first) {
ret = !!scene.tags ? scene.tags.map(toId).sort() : []; ret = scene.tags ? scene.tags.map(t => t.id).sort() : [];
first = false; first = false;
} else { } else {
const tIds = !!scene.tags ? scene.tags.map(toId).sort() : []; const tIds = scene.tags ? scene.tags.map(t => t.id).sort() : [];
if (!_.isEqual(ret, tIds)) { if (!_.isEqual(ret, tIds)) {
ret = []; ret = [];
@@ -183,50 +177,46 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
} }
function updateScenesEditState(state: GQL.SlimSceneDataFragment[]) { function updateScenesEditState(state: GQL.SlimSceneDataFragment[]) {
function toId(object : any) { let updateRating = "";
return object.id; let updateStudioId : string | undefined;
} let updatePerformerIds : string[] = [];
let updateTagIds : string[] = [];
var rating : string = ""; let first = true;
var studioId : string | undefined;
var performerIds : string[] = [];
var tagIds : string[] = [];
var first = true;
state.forEach((scene : GQL.SlimSceneDataFragment) => { state.forEach((scene : GQL.SlimSceneDataFragment) => {
var thisRating = scene.rating ? scene.rating.toString() : ""; const thisRating = scene.rating ? scene.rating.toString() : "";
var thisStudio = scene.studio ? scene.studio.id : undefined; const thisStudio = scene.studio ? scene.studio.id : undefined;
if (first) { if (first) {
rating = thisRating; updateRating = thisRating;
studioId = thisStudio; updateStudioId = thisStudio;
performerIds = !!scene.performers ? scene.performers.map(toId).sort() : []; updatePerformerIds = scene.performers ? scene.performers.map(p => p.id).sort() : [];
tagIds = !!scene.tags ? scene.tags.map(toId).sort() : []; updateTagIds = scene.tags ? scene.tags.map(p => p.id).sort() : [];
first = false; first = false;
} else { } else {
if (rating !== thisRating) { if (rating !== thisRating) {
rating = ""; updateRating = "";
} }
if (studioId !== thisStudio) { if (studioId !== thisStudio) {
studioId = undefined; updateStudioId = undefined;
} }
const perfIds = !!scene.performers ? scene.performers.map(toId).sort() : []; const perfIds = scene.performers ? scene.performers.map(p => p.id).sort() : [];
const tIds = !!scene.tags ? scene.tags.map(toId).sort() : []; const tIds = scene.tags ? scene.tags.map(t => t.id).sort() : [];
if (!_.isEqual(performerIds, perfIds)) { if (!_.isEqual(performerIds, perfIds)) {
performerIds = []; updatePerformerIds = [];
} }
if (!_.isEqual(tagIds, tIds)) { if (!_.isEqual(tagIds, tIds)) {
tagIds = []; updateTagIds = [];
} }
} }
}); });
setRating(rating); setRating(updateRating);
setStudioId(studioId); setStudioId(updateStudioId);
setPerformerIds(performerIds); setPerformerIds(updatePerformerIds);
setTagIds(tagIds); setTagIds(updateTagIds);
} }
useEffect(() => { useEffect(() => {
@@ -237,7 +227,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
return ( return (
<FilterSelect <FilterSelect
type={type} type={type}
isMulti={true} isMulti
onSelect={(items) => { onSelect={(items) => {
const ids = items.map((i) => i.id); const ids = items.map((i) => i.id);
switch (type) { switch (type) {

View File

@@ -6,8 +6,8 @@ import { SceneMarkerList } from "./SceneMarkerList";
const Scenes = () => ( const Scenes = () => (
<Switch> <Switch>
<Route exact={true} path="/scenes" component={SceneList} /> <Route exact path="/scenes" component={SceneList} />
<Route exact={true} path="/scenes/markers" component={SceneMarkerList} /> <Route exact path="/scenes/markers" component={SceneMarkerList} />
<Route path="/scenes/:id" component={Scene} /> <Route path="/scenes/:id" component={Scene} />
</Switch> </Switch>
); );

View File

@@ -27,8 +27,8 @@ export class StashService {
if (platformUrl.protocol === "https:") { if (platformUrl.protocol === "https:") {
wsPlatformUrl.protocol = "wss:"; wsPlatformUrl.protocol = "wss:";
} }
const url = platformUrl.toString().slice(0, -1) + "/graphql"; const url = `${platformUrl.toString().slice(0, -1) }/graphql`;
const wsUrl = wsPlatformUrl.toString().slice(0, -1) + "/graphql"; const wsUrl = `${wsPlatformUrl.toString().slice(0, -1) }/graphql`;
const httpLink = new HttpLink({ const httpLink = new HttpLink({
uri: url, uri: url,
@@ -52,7 +52,7 @@ export class StashService {
StashService.cache = new InMemoryCache(); StashService.cache = new InMemoryCache();
StashService.client = new ApolloClient({ StashService.client = new ApolloClient({
link: link, link,
cache: StashService.cache cache: StashService.cache
}); });
@@ -65,10 +65,10 @@ export class StashService {
} }
private static invalidateQueries(queries : string[]) { private static invalidateQueries(queries : string[]) {
if (!!StashService.cache) { if (StashService.cache) {
const cache = StashService.cache as any; const cache = StashService.cache as any;
const keyMatchers = queries.map(query => { const keyMatchers = queries.map(query => {
return new RegExp("^" + query); return new RegExp(`^${ query}`);
}); });
const rootQuery = cache.data.data.ROOT_QUERY; const rootQuery = cache.data.data.ROOT_QUERY;
@@ -197,11 +197,11 @@ export class StashService {
public static useFindGallery(id: string) { return GQL.useFindGallery({variables: {id}}); } public static useFindGallery(id: string) { return GQL.useFindGallery({variables: {id}}); }
public static useFindScene(id: string) { return GQL.useFindScene({variables: {id}}); } public static useFindScene(id: string) { return GQL.useFindScene({variables: {id}}); }
public static useFindPerformer(id: string) { public static useFindPerformer(id: string) {
const skip = id === "new" ? true : false; const skip = id === "new";
return GQL.useFindPerformer({variables: {id}, skip}); return GQL.useFindPerformer({variables: {id}, skip});
} }
public static useFindStudio(id: string) { public static useFindStudio(id: string) {
const skip = id === "new" ? true : false; const skip = id === "new";
return GQL.useFindStudio({variables: {id}, skip}); return GQL.useFindStudio({variables: {id}, skip});
} }
@@ -312,7 +312,7 @@ export class StashService {
} }
public static useScenesUpdate(input: GQL.SceneUpdateInput[]) { public static useScenesUpdate(input: GQL.SceneUpdateInput[]) {
return GQL.useScenesUpdate({ variables: { input : input }}); return GQL.useScenesUpdate({ variables: { input }});
} }
public static useSceneDestroy(input: GQL.SceneDestroyInput) { public static useSceneDestroy(input: GQL.SceneDestroyInput) {
@@ -360,7 +360,7 @@ export class StashService {
return GQL.useTagCreate({ return GQL.useTagCreate({
variables: input, variables: input,
refetchQueries: ["AllTags", "AllTagsForFilter"], refetchQueries: ["AllTags", "AllTagsForFilter"],
//update: () => StashService.invalidateQueries(StashService.tagMutationImpactedQueries) // update: () => StashService.invalidateQueries(StashService.tagMutationImpactedQueries)
}); });
} }
public static useTagUpdate(input: GQL.TagUpdateInput) { public static useTagUpdate(input: GQL.TagUpdateInput) {
@@ -435,7 +435,7 @@ export class StashService {
return StashService.client.query<GQL.ScrapePerformerUrlQuery>({ return StashService.client.query<GQL.ScrapePerformerUrlQuery>({
query: GQL.ScrapePerformerUrlDocument, query: GQL.ScrapePerformerUrlDocument,
variables: { variables: {
url: url, url,
}, },
}); });
} }
@@ -444,7 +444,7 @@ export class StashService {
return StashService.client.query<GQL.ScrapeSceneUrlQuery>({ return StashService.client.query<GQL.ScrapeSceneUrlQuery>({
query: GQL.ScrapeSceneUrlDocument, query: GQL.ScrapeSceneUrlDocument,
variables: { variables: {
url: url, url,
}, },
}); });
} }
@@ -454,7 +454,7 @@ export class StashService {
query: GQL.ScrapeSceneDocument, query: GQL.ScrapeSceneDocument,
variables: { variables: {
scraper_id: scraperId, scraper_id: scraperId,
scene: scene, scene,
}, },
}); });
} }
@@ -507,17 +507,18 @@ export class StashService {
public static querySceneByPathRegex(filter: GQL.FindFilterType) { public static querySceneByPathRegex(filter: GQL.FindFilterType) {
return StashService.client.query<GQL.FindScenesByPathRegexQuery>({ return StashService.client.query<GQL.FindScenesByPathRegexQuery>({
query: GQL.FindScenesByPathRegexDocument, query: GQL.FindScenesByPathRegexDocument,
variables: {filter: filter}, variables: {filter},
}); });
} }
public static queryParseSceneFilenames(filter: GQL.FindFilterType, config: GQL.SceneParserInput) { public static queryParseSceneFilenames(filter: GQL.FindFilterType, config: GQL.SceneParserInput) {
return StashService.client.query<GQL.ParseSceneFilenamesQuery>({ return StashService.client.query<GQL.ParseSceneFilenamesQuery>({
query: GQL.ParseSceneFilenamesDocument, query: GQL.ParseSceneFilenamesDocument,
variables: {filter: filter, config: config}, variables: {filter, config},
fetchPolicy: "network-only", fetchPolicy: "network-only",
}); });
} }
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {} private constructor() {}
} }

View File

@@ -62,61 +62,6 @@ interface IQuery<T extends IQueryResult, T2 extends IDataItem> {
getCount: (data: T) => number; getCount: (data: T) => number;
} }
type ScenesQuery = QueryHookResult<FindScenesQuery, FindScenesVariables>;
export const useScenesList = (props:IListHookOptions<ScenesQuery>) => (
useList<ScenesQuery, SlimSceneDataFragment>({
...props,
filterMode: FilterMode.Scenes,
useData: StashService.useFindScenes,
getData: (result:ScenesQuery) => (result?.data?.findScenes?.scenes ?? []),
getCount: (result:ScenesQuery) => (result?.data?.findScenes?.count ?? 0)
})
)
type SceneMarkersQuery = QueryHookResult<FindSceneMarkersQuery, FindSceneMarkersVariables>;
export const useSceneMarkersList = (props:IListHookOptions<SceneMarkersQuery>) => (
useList<SceneMarkersQuery, FindSceneMarkersSceneMarkers>({
...props,
filterMode: FilterMode.SceneMarkers,
useData: StashService.useFindSceneMarkers,
getData: (result:SceneMarkersQuery) => (result?.data?.findSceneMarkers?.scene_markers?? []),
getCount: (result:SceneMarkersQuery) => (result?.data?.findSceneMarkers?.count ?? 0)
})
)
type GalleriesQuery = QueryHookResult<FindGalleriesQuery, FindGalleriesVariables>;
export const useGalleriesList = (props:IListHookOptions<GalleriesQuery>) => (
useList<GalleriesQuery, GalleryDataFragment>({
...props,
filterMode: FilterMode.Galleries,
useData: StashService.useFindGalleries,
getData: (result:GalleriesQuery) => (result?.data?.findGalleries?.galleries ?? []),
getCount: (result:GalleriesQuery) => (result?.data?.findGalleries?.count ?? 0)
})
)
type StudiosQuery = QueryHookResult<FindStudiosQuery, FindStudiosVariables>;
export const useStudiosList = (props:IListHookOptions<StudiosQuery>) => (
useList<StudiosQuery, StudioDataFragment>({
...props,
filterMode: FilterMode.Studios,
useData: StashService.useFindStudios,
getData: (result:StudiosQuery) => (result?.data?.findStudios?.studios ?? []),
getCount: (result:StudiosQuery) => (result?.data?.findStudios?.count ?? 0)
})
)
type PerformersQuery = QueryHookResult<FindPerformersQuery, FindPerformersVariables>;
export const usePerformersList = (props:IListHookOptions<PerformersQuery>) => (
useList<PerformersQuery, PerformerDataFragment>({
...props,
filterMode: FilterMode.Performers,
useData: StashService.useFindPerformers,
getData: (result:PerformersQuery) => (result?.data?.findPerformers?.performers ?? []),
getCount: (result:PerformersQuery) => (result?.data?.findPerformers?.count ?? 0)
})
)
const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>( const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
options: (IListHookOptions<QueryResult> & IQuery<QueryResult, QueryData>) options: (IListHookOptions<QueryResult> & IQuery<QueryResult, QueryData>)
): IListHookData => { ): IListHookData => {
@@ -130,9 +75,9 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
const totalCount = options.getCount(result); const totalCount = options.getCount(result);
const items = options.getData(result); const items = options.getData(result);
function updateQueryParams(filter:ListFilterModel) { function updateQueryParams(listfilter:ListFilterModel) {
const newLocation = Object.assign({}, history.location); const newLocation = { ...history.location};
newLocation.search = filter.makeQueryParameters(); newLocation.search = listfilter.makeQueryParameters();
history.replace(newLocation); history.replace(newLocation);
} }
@@ -180,7 +125,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
// Find if we are editing an existing criteria, then modify that. Or create a new one. // Find if we are editing an existing criteria, then modify that. Or create a new one.
const existingIndex = newFilter.criteria.findIndex((c) => { const existingIndex = newFilter.criteria.findIndex((c) => {
// If we modified an existing criterion, then look for the old id. // If we modified an existing criterion, then look for the old id.
const id = !!oldId ? oldId : criterion.getId(); const id = oldId || criterion.getId();
return c.getId() === id; return c.getId() === id;
}); });
if (existingIndex === -1) { if (existingIndex === -1) {
@@ -214,14 +159,6 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
updateQueryParams(newFilter); updateQueryParams(newFilter);
} }
function onSelectChange(id: string, selected : boolean, shiftKey: boolean) {
if (shiftKey) {
multiSelect(id);
} else {
singleSelect(id, selected);
}
}
function singleSelect(id: string, selected: boolean) { function singleSelect(id: string, selected: boolean) {
setLastClickedId(id); setLastClickedId(id);
@@ -235,6 +172,25 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
setSelectedIds(newSelectedIds); setSelectedIds(newSelectedIds);
} }
function selectRange(startIndex : number, endIndex : number) {
let start = startIndex;
let end = endIndex;
if (start > end) {
const tmp = start;
start = end;
end = tmp;
}
const subset = items.slice(start, end + 1);
const newSelectedIds : Set<string> = new Set();
subset.forEach((item) => {
newSelectedIds.add(item.id);
});
setSelectedIds(newSelectedIds);
}
function multiSelect(id: string) { function multiSelect(id: string) {
let startIndex = 0; let startIndex = 0;
let thisIndex = -1; let thisIndex = -1;
@@ -252,21 +208,12 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
selectRange(startIndex, thisIndex); selectRange(startIndex, thisIndex);
} }
function selectRange(startIndex : number, endIndex : number) { function onSelectChange(id: string, selected : boolean, shiftKey: boolean) {
if (startIndex > endIndex) { if (shiftKey) {
let tmp = startIndex; multiSelect(id);
startIndex = endIndex; } else {
endIndex = tmp; singleSelect(id, selected);
} }
const subset = items.slice(startIndex, endIndex + 1);
const newSelectedIds : Set<string> = new Set();
subset.forEach((item) => {
newSelectedIds.add(item.id);
});
setSelectedIds(newSelectedIds);
} }
function onSelectAll() { function onSelectAll() {
@@ -324,10 +271,66 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
currentPage={filter.currentPage} currentPage={filter.currentPage}
totalItems={totalCount} totalItems={totalCount}
onChangePage={onChangePage} onChangePage={onChangePage}
loading={result.loading}
/> />
</div> </div>
); );
return { filter, template, onSelectChange }; return { filter, template, onSelectChange };
} }
type ScenesQuery = QueryHookResult<FindScenesQuery, FindScenesVariables>;
export const useScenesList = (props:IListHookOptions<ScenesQuery>) => (
useList<ScenesQuery, SlimSceneDataFragment>({
...props,
filterMode: FilterMode.Scenes,
useData: StashService.useFindScenes,
getData: (result:ScenesQuery) => (result?.data?.findScenes?.scenes ?? []),
getCount: (result:ScenesQuery) => (result?.data?.findScenes?.count ?? 0)
})
)
type SceneMarkersQuery = QueryHookResult<FindSceneMarkersQuery, FindSceneMarkersVariables>;
export const useSceneMarkersList = (props:IListHookOptions<SceneMarkersQuery>) => (
useList<SceneMarkersQuery, FindSceneMarkersSceneMarkers>({
...props,
filterMode: FilterMode.SceneMarkers,
useData: StashService.useFindSceneMarkers,
getData: (result:SceneMarkersQuery) => (result?.data?.findSceneMarkers?.scene_markers?? []),
getCount: (result:SceneMarkersQuery) => (result?.data?.findSceneMarkers?.count ?? 0)
})
)
type GalleriesQuery = QueryHookResult<FindGalleriesQuery, FindGalleriesVariables>;
export const useGalleriesList = (props:IListHookOptions<GalleriesQuery>) => (
useList<GalleriesQuery, GalleryDataFragment>({
...props,
filterMode: FilterMode.Galleries,
useData: StashService.useFindGalleries,
getData: (result:GalleriesQuery) => (result?.data?.findGalleries?.galleries ?? []),
getCount: (result:GalleriesQuery) => (result?.data?.findGalleries?.count ?? 0)
})
)
type StudiosQuery = QueryHookResult<FindStudiosQuery, FindStudiosVariables>;
export const useStudiosList = (props:IListHookOptions<StudiosQuery>) => (
useList<StudiosQuery, StudioDataFragment>({
...props,
filterMode: FilterMode.Studios,
useData: StashService.useFindStudios,
getData: (result:StudiosQuery) => (result?.data?.findStudios?.studios ?? []),
getCount: (result:StudiosQuery) => (result?.data?.findStudios?.count ?? 0)
})
)
type PerformersQuery = QueryHookResult<FindPerformersQuery, FindPerformersVariables>;
export const usePerformersList = (props:IListHookOptions<PerformersQuery>) => (
useList<PerformersQuery, PerformerDataFragment>({
...props,
filterMode: FilterMode.Performers,
useData: StashService.useFindPerformers,
getData: (result:PerformersQuery) => (result?.data?.findPerformers?.performers ?? []),
getCount: (result:PerformersQuery) => (result?.data?.findPerformers?.count ?? 0)
})
)

View File

@@ -16,21 +16,6 @@ interface ILocalForage<T> {
error: Error | null; error: Error | null;
} }
export function useInterfaceLocalForage(): ILocalForage<IInterfaceConfig | undefined> {
const result = useLocalForage("interface");
// Set defaults
React.useEffect(() => {
if (result.data === undefined) {
result.setData({
wall: {
// nothing here currently
},
});
}
});
return result;
}
function useLocalForage(item: string): ILocalForage<ValidTypes> { function useLocalForage(item: string): ILocalForage<ValidTypes> {
const [json, setJson] = React.useState<ValidTypes>(undefined); const [json, setJson] = React.useState<ValidTypes>(undefined);
@@ -64,3 +49,18 @@ function useLocalForage(item: string): ILocalForage<ValidTypes> {
return {data: json, setData: setJson, error: err}; return {data: json, setData: setJson, error: err};
} }
export function useInterfaceLocalForage(): ILocalForage<IInterfaceConfig | undefined> {
const result = useLocalForage("interface");
// Set defaults
React.useEffect(() => {
if (result.data === undefined) {
result.setData({
wall: {
// nothing here currently
},
});
}
});
return result;
}

View File

@@ -56,6 +56,7 @@ function createHookObject(toastFunc: (toast:IToast) => void) {
return { return {
success: toastFunc, success: toastFunc,
error: (error: Error) => { error: (error: Error) => {
// eslint-disable-next-line no-console
console.error(error.message); console.error(error.message);
toastFunc({ toastFunc({
variant: 'danger', variant: 'danger',

View File

@@ -1,3 +1,5 @@
/* eslint-disable no-param-reassign, no-console */
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { StashService } from "../core/StashService"; import { StashService } from "../core/StashService";
@@ -31,7 +33,7 @@ export class VideoHoverHook {
videoTag.pause(); videoTag.pause();
} }
}; };
videoTag.onpause = () => isPlaying.current = false; videoTag.onpause = () => { isPlaying.current = false };
}, [videoEl]); }, [videoEl]);
useEffect(() => { useEffect(() => {

View File

@@ -1,3 +1,5 @@
/* eslint-disable consistent-return */
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { ILabeledId, ILabeledValue } from "../types"; import { ILabeledId, ILabeledValue } from "../types";
@@ -26,7 +28,7 @@ export type CriterionType =
"aliases"; "aliases";
export abstract class Criterion<Option = any, Value = any> { export abstract class Criterion<Option = any, Value = any> {
public static getLabel(type: CriterionType = "none"): string { public static getLabel(type: CriterionType = "none") {
switch (type) { switch (type) {
case "none": return "None"; case "none": return "None";
case "rating": return "Rating"; case "rating": return "Rating";
@@ -157,7 +159,7 @@ export class StringCriterion extends Criterion<string, string> {
this.options = options; this.options = options;
this.inputType = "text"; this.inputType = "text";
if (!!parameterName) { if (parameterName) {
this.parameterName = parameterName; this.parameterName = parameterName;
} else { } else {
this.parameterName = type; this.parameterName = type;
@@ -187,7 +189,7 @@ export class NumberCriterion extends Criterion<number, number> {
this.options = options; this.options = options;
this.inputType = "number"; this.inputType = "number";
if (!!parameterName) { if (parameterName) {
this.parameterName = parameterName; this.parameterName = parameterName;
} else { } else {
this.parameterName = type; this.parameterName = type;

View File

@@ -1,5 +1,4 @@
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { CriterionModifier } from "src/core/generated-graphql";
import { ILabeledId } from "../types"; import { ILabeledId } from "../types";
import { import {
Criterion, Criterion,
@@ -10,11 +9,11 @@ import {
export class TagsCriterion extends Criterion<GQL.AllTagsForFilterAllTags, ILabeledId[]> { export class TagsCriterion extends Criterion<GQL.AllTagsForFilterAllTags, ILabeledId[]> {
public type: CriterionType; public type: CriterionType;
public parameterName: string; public parameterName: string;
public modifier = CriterionModifier.IncludesAll; public modifier = GQL.CriterionModifier.IncludesAll;
public modifierOptions = [ public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.IncludesAll), Criterion.getModifierOption(GQL.CriterionModifier.IncludesAll),
Criterion.getModifierOption(CriterionModifier.Includes), Criterion.getModifierOption(GQL.CriterionModifier.Includes),
Criterion.getModifierOption(CriterionModifier.Excludes), Criterion.getModifierOption(GQL.CriterionModifier.Excludes),
]; ];
public options: GQL.AllTagsForFilterAllTags[] = []; public options: GQL.AllTagsForFilterAllTags[] = [];
public value: ILabeledId[] = []; public value: ILabeledId[] = [];

View File

@@ -1,3 +1,4 @@
/* eslint-disable consistent-return, default-case */
import { import {
CriterionModifier, CriterionModifier,
} from "src/core/generated-graphql"; } from "src/core/generated-graphql";
@@ -26,8 +27,8 @@ export function makeCriteria(type: CriterionType = "none") {
case "studios": return new StudiosCriterion(); case "studios": return new StudiosCriterion();
case "birth_year": case "birth_year":
case "age": case "age": {
var ret = new NumberCriterion(type, type); const ret = new NumberCriterion(type, type);
// null/not null doesn't make sense for these criteria // null/not null doesn't make sense for these criteria
ret.modifierOptions = [ ret.modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Equals), Criterion.getModifierOption(CriterionModifier.Equals),
@@ -36,6 +37,7 @@ export function makeCriteria(type: CriterionType = "none") {
Criterion.getModifierOption(CriterionModifier.LessThan) Criterion.getModifierOption(CriterionModifier.LessThan)
]; ];
return ret; return ret;
}
case "ethnicity": case "ethnicity":
case "country": case "country":
case "eye_color": case "eye_color":

View File

@@ -67,7 +67,7 @@ export class ListFilterModel {
new StudiosCriterionOption(), new StudiosCriterionOption(),
]; ];
break; break;
case FilterMode.Performers: case FilterMode.Performers: {
if (!!this.sortBy === false) { this.sortBy = "name"; } if (!!this.sortBy === false) { this.sortBy = "name"; }
this.sortByOptions = ["name", "height", "birthdate", "scenes_count"]; this.sortByOptions = ["name", "height", "birthdate", "scenes_count"];
this.displayModeOptions = [ this.displayModeOptions = [
@@ -75,8 +75,8 @@ export class ListFilterModel {
DisplayMode.List, DisplayMode.List,
]; ];
var numberCriteria : CriterionType[] = ["birth_year", "age"]; const numberCriteria : CriterionType[] = ["birth_year", "age"];
var stringCriteria : CriterionType[] = [ const stringCriteria : CriterionType[] = [
"ethnicity", "ethnicity",
"country", "country",
"eye_color", "eye_color",
@@ -98,6 +98,7 @@ export class ListFilterModel {
return new CriterionOption(Criterion.getLabel(c), c); return new CriterionOption(Criterion.getLabel(c), c);
})); }));
break; break;
}
case FilterMode.Studios: case FilterMode.Studios:
if (!!this.sortBy === false) { this.sortBy = "name"; } if (!!this.sortBy === false) { this.sortBy = "name"; }
this.sortByOptions = ["name", "scenes_count"]; this.sortByOptions = ["name", "scenes_count"];
@@ -173,13 +174,13 @@ export class ListFilterModel {
jsonParameters = [params.c]; jsonParameters = [params.c];
} }
for (const jsonString of jsonParameters) { jsonParameters.forEach(jsonString => {
const encodedCriterion = JSON.parse(jsonString); const encodedCriterion = JSON.parse(jsonString);
const criterion = makeCriteria(encodedCriterion.type); const criterion = makeCriteria(encodedCriterion.type);
criterion.value = encodedCriterion.value; criterion.value = encodedCriterion.value;
criterion.modifier = encodedCriterion.modifier; criterion.modifier = encodedCriterion.modifier;
this.criteria.push(criterion); this.criteria.push(criterion);
} });
} }
} }
@@ -221,10 +222,11 @@ export class ListFilterModel {
const result: SceneFilterType = {}; const result: SceneFilterType = {};
this.criteria.forEach((criterion) => { this.criteria.forEach((criterion) => {
switch (criterion.type) { switch (criterion.type) {
case "rating": case "rating": {
const ratingCrit = criterion as RatingCriterion; const ratingCrit = criterion as RatingCriterion;
result.rating = { value: ratingCrit.value, modifier: ratingCrit.modifier }; result.rating = { value: ratingCrit.value, modifier: ratingCrit.modifier };
break; break;
}
case "resolution": { case "resolution": {
switch ((criterion as ResolutionCriterion).value) { switch ((criterion as ResolutionCriterion).value) {
case "240p": result.resolution = ResolutionEnum.Low; break; case "240p": result.resolution = ResolutionEnum.Low; break;
@@ -232,6 +234,7 @@ export class ListFilterModel {
case "720p": result.resolution = ResolutionEnum.StandardHd; break; case "720p": result.resolution = ResolutionEnum.StandardHd; break;
case "1080p": result.resolution = ResolutionEnum.FullHd; break; case "1080p": result.resolution = ResolutionEnum.FullHd; break;
case "4k": result.resolution = ResolutionEnum.FourK; break; case "4k": result.resolution = ResolutionEnum.FourK; break;
// no default
} }
break; break;
} }
@@ -241,18 +244,22 @@ export class ListFilterModel {
case "isMissing": case "isMissing":
result.is_missing = (criterion as IsMissingCriterion).value; result.is_missing = (criterion as IsMissingCriterion).value;
break; break;
case "tags": case "tags": {
const tagsCrit = criterion as TagsCriterion; const tagsCrit = criterion as TagsCriterion;
result.tags = { value: tagsCrit.value.map((tag) => tag.id), modifier: tagsCrit.modifier }; result.tags = { value: tagsCrit.value.map((tag) => tag.id), modifier: tagsCrit.modifier };
break; break;
case "performers": }
case "performers": {
const perfCrit = criterion as PerformersCriterion; const perfCrit = criterion as PerformersCriterion;
result.performers = { value: perfCrit.value.map((perf) => perf.id), modifier: perfCrit.modifier }; result.performers = { value: perfCrit.value.map((perf) => perf.id), modifier: perfCrit.modifier };
break; break;
case "studios": }
case "studios": {
const studCrit = criterion as StudiosCriterion; const studCrit = criterion as StudiosCriterion;
result.studios = { value: studCrit.value.map((studio) => studio.id), modifier: studCrit.modifier }; result.studios = { value: studCrit.value.map((studio) => studio.id), modifier: studCrit.modifier };
break; break;
}
// no default
} }
}); });
return result; return result;
@@ -265,54 +272,67 @@ export class ListFilterModel {
case "favorite": case "favorite":
result.filter_favorites = (criterion as FavoriteCriterion).value === "true"; result.filter_favorites = (criterion as FavoriteCriterion).value === "true";
break; break;
case "birth_year": case "birth_year": {
const byCrit = criterion as NumberCriterion; const byCrit = criterion as NumberCriterion;
result.birth_year = { value: byCrit.value, modifier: byCrit.modifier }; result.birth_year = { value: byCrit.value, modifier: byCrit.modifier };
break; break;
case "age": }
case "age": {
const ageCrit = criterion as NumberCriterion; const ageCrit = criterion as NumberCriterion;
result.age = { value: ageCrit.value, modifier: ageCrit.modifier }; result.age = { value: ageCrit.value, modifier: ageCrit.modifier };
break; break;
case "ethnicity": }
case "ethnicity": {
const ethCrit = criterion as StringCriterion; const ethCrit = criterion as StringCriterion;
result.ethnicity = { value: ethCrit.value, modifier: ethCrit.modifier }; result.ethnicity = { value: ethCrit.value, modifier: ethCrit.modifier };
break; break;
case "country": }
case "country": {
const cntryCrit = criterion as StringCriterion; const cntryCrit = criterion as StringCriterion;
result.country = { value: cntryCrit.value, modifier: cntryCrit.modifier }; result.country = { value: cntryCrit.value, modifier: cntryCrit.modifier };
break; break;
case "eye_color": }
case "eye_color": {
const ecCrit = criterion as StringCriterion; const ecCrit = criterion as StringCriterion;
result.eye_color = { value: ecCrit.value, modifier: ecCrit.modifier }; result.eye_color = { value: ecCrit.value, modifier: ecCrit.modifier };
break; break;
case "height": }
case "height": {
const hCrit = criterion as StringCriterion; const hCrit = criterion as StringCriterion;
result.height = { value: hCrit.value, modifier: hCrit.modifier }; result.height = { value: hCrit.value, modifier: hCrit.modifier };
break; break;
case "measurements": }
case "measurements": {
const mCrit = criterion as StringCriterion; const mCrit = criterion as StringCriterion;
result.measurements = { value: mCrit.value, modifier: mCrit.modifier }; result.measurements = { value: mCrit.value, modifier: mCrit.modifier };
break; break;
case "fake_tits": }
case "fake_tits": {
const ftCrit = criterion as StringCriterion; const ftCrit = criterion as StringCriterion;
result.fake_tits = { value: ftCrit.value, modifier: ftCrit.modifier }; result.fake_tits = { value: ftCrit.value, modifier: ftCrit.modifier };
break; break;
case "career_length": }
case "career_length": {
const clCrit = criterion as StringCriterion; const clCrit = criterion as StringCriterion;
result.career_length = { value: clCrit.value, modifier: clCrit.modifier }; result.career_length = { value: clCrit.value, modifier: clCrit.modifier };
break; break;
case "tattoos": }
case "tattoos": {
const tCrit = criterion as StringCriterion; const tCrit = criterion as StringCriterion;
result.tattoos = { value: tCrit.value, modifier: tCrit.modifier }; result.tattoos = { value: tCrit.value, modifier: tCrit.modifier };
break; break;
case "piercings": }
case "piercings": {
const pCrit = criterion as StringCriterion; const pCrit = criterion as StringCriterion;
result.piercings = { value: pCrit.value, modifier: pCrit.modifier }; result.piercings = { value: pCrit.value, modifier: pCrit.modifier };
break; break;
case "aliases": }
case "aliases": {
const aCrit = criterion as StringCriterion; const aCrit = criterion as StringCriterion;
result.aliases = { value: aCrit.value, modifier: aCrit.modifier }; result.aliases = { value: aCrit.value, modifier: aCrit.modifier };
break; break;
}
// no default
} }
}); });
return result; return result;
@@ -322,18 +342,22 @@ export class ListFilterModel {
const result: SceneMarkerFilterType = {}; const result: SceneMarkerFilterType = {};
this.criteria.forEach((criterion) => { this.criteria.forEach((criterion) => {
switch (criterion.type) { switch (criterion.type) {
case "tags": case "tags": {
const tagsCrit = criterion as TagsCriterion; const tagsCrit = criterion as TagsCriterion;
result.tags = { value: tagsCrit.value.map((tag) => tag.id), modifier: tagsCrit.modifier }; result.tags = { value: tagsCrit.value.map((tag) => tag.id), modifier: tagsCrit.modifier };
break; break;
case "sceneTags": }
case "sceneTags": {
const sceneTagsCrit = criterion as TagsCriterion; const sceneTagsCrit = criterion as TagsCriterion;
result.scene_tags = { value: sceneTagsCrit.value.map((tag) => tag.id), modifier: sceneTagsCrit.modifier }; result.scene_tags = { value: sceneTagsCrit.value.map((tag) => tag.id), modifier: sceneTagsCrit.modifier };
break; break;
case "performers": }
case "performers": {
const performersCrit = criterion as PerformersCriterion; const performersCrit = criterion as PerformersCriterion;
result.performers = { value: performersCrit.value.map((performer) => performer.id), modifier: performersCrit.modifier }; result.performers = { value: performersCrit.value.map((performer) => performer.id), modifier: performersCrit.modifier };
break; break;
}
// no default
} }
}); });
return result; return result;

View File

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

View File

@@ -1,3 +1,5 @@
/* eslint-disable no-console */
// This optional code is used to register a service worker. // This optional code is used to register a service worker.
// register() is not called by default. // register() is not called by default.
@@ -20,52 +22,16 @@ const isLocalhost = Boolean(
), ),
); );
interface Config { interface IConfig {
onSuccess?: (registration: ServiceWorkerRegistration) => void; onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void; onUpdate?: (registration: ServiceWorkerRegistration) => void;
} }
export function register(config?: Config) { function registerValidSW(swUrl: string, config?: IConfig) {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
(process as { env: { [key: string]: string } }).env.PUBLIC_URL,
window.location.href,
);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
"This web app is being served cache-first by a service " +
"worker. To learn more, visit http://bit.ly/CRA-PWA",
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker navigator.serviceWorker
.register(swUrl) .register(swUrl)
.then((registration) => { .then((registration) => {
// eslint-disable-next-line no-param-reassign
registration.onupdatefound = () => { registration.onupdatefound = () => {
const installingWorker = registration.installing; const installingWorker = registration.installing;
if (installingWorker == null) { if (installingWorker == null) {
@@ -106,7 +72,7 @@ function registerValidSW(swUrl: string, config?: Config) {
}); });
} }
function checkValidServiceWorker(swUrl: string, config?: Config) { function checkValidServiceWorker(swUrl: string, config?: IConfig) {
// Check if the service worker can be found. If it can't reload the page. // Check if the service worker can be found. If it can't reload the page.
fetch(swUrl) fetch(swUrl)
.then((response) => { .then((response) => {
@@ -134,6 +100,44 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
}); });
} }
export function register(config?: IConfig) {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
(process as { env: { [key: string]: string } }).env.PUBLIC_URL,
window.location.href,
);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
"This web app is being served cache-first by a service " +
"worker. To learn more, visit http://bit.ly/CRA-PWA",
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
export function unregister() { export function unregister() {
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready.then((registration) => { navigator.serviceWorker.ready.then((registration) => {

View File

@@ -114,7 +114,7 @@ const renderMultiSelect = (options: {
<td> <td>
<FilterSelect <FilterSelect
type={options.type} type={options.type}
isMulti={true} isMulti
onSelect={(items) => options.onChange(items.map((i) => i.id))} onSelect={(items) => options.onChange(items.map((i) => i.id))}
initialIds={options.initialIds ?? []} initialIds={options.initialIds ?? []}
/> />

View File

@@ -16,12 +16,13 @@ const truncate = (value?: string, limit: number = 100, tail: string = "...") =>
} }
const fileSize = (bytes: number = 0, precision: number = 2) => { const fileSize = (bytes: number = 0, precision: number = 2) => {
if (Number.isNaN(parseFloat(String(bytes))) || !isFinite(bytes)) if (Number.isNaN(parseFloat(String(bytes))) || !Number.isFinite(bytes))
return "?"; return "?";
let unit = 0; let unit = 0;
while ( bytes >= 1024 ) { let count = bytes;
bytes /= 1024; while ( count >= 1024 ) {
count /= 1024;
unit++; unit++;
} }
@@ -48,7 +49,7 @@ const fileNameFromPath = (path: string) => {
return path.replace(/^.*[\\/]/, ""); return path.replace(/^.*[\\/]/, "");
} }
const age = (dateString?: string, fromDateString?: string) => { const getAge = (dateString?: string, fromDateString?: string) => {
if (!dateString) if (!dateString)
return 0; return 0;
@@ -72,16 +73,18 @@ const bitRate = (bitrate: number) => {
const resolution = (height: number) => { const resolution = (height: number) => {
if (height >= 240 && height < 480) { if (height >= 240 && height < 480) {
return "240p"; return "240p";
} else if (height >= 480 && height < 720) { }
if (height >= 480 && height < 720) {
return "480p"; return "480p";
} else if (height >= 720 && height < 1080) { }
if (height >= 720 && height < 1080) {
return "720p"; return "720p";
} else if (height >= 1080 && height < 2160) { }
if (height >= 1080 && height < 2160) {
return "1080p"; return "1080p";
} else if (height >= 2160) { }
if (height >= 2160) {
return "4K"; return "4K";
} else {
return undefined;
} }
} }
@@ -90,7 +93,7 @@ const TextUtils = {
fileSize, fileSize,
secondsToTimestamp, secondsToTimestamp,
fileNameFromPath, fileNameFromPath,
age, age: getAge,
bitRate, bitRate,
resolution resolution
} }