Blueprint removed

This commit is contained in:
Infinite
2020-01-11 23:14:20 +01:00
parent e18e67b512
commit 129dcecdef
85 changed files with 1429 additions and 1996 deletions

9
ui/v2.5/.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

File diff suppressed because one or more lines are too long

View File

@@ -3,8 +3,6 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@blueprintjs/core": "3.22.1",
"@blueprintjs/select": "3.11.2",
"@fortawesome/fontawesome-svg-core": "^1.2.26",
"@fortawesome/free-solid-svg-icons": "^5.12.0",
"@fortawesome/react-fontawesome": "^0.1.8",
@@ -12,11 +10,13 @@
"apollo-link-ws": "^1.0.19",
"axios": "0.18.1",
"bootstrap": "^4.4.1",
"classnames": "^2.2.6",
"formik": "1.5.7",
"graphql": "14.3.1",
"localforage": "1.7.3",
"lodash": "4.17.13",
"node-sass": "4.12.0",
"normalize.css": "^8.0.1",
"query-string": "6.5.0",
"react": "~16.12.0",
"react-apollo": "2.5.6",
@@ -53,6 +53,7 @@
"not op_mini all"
],
"devDependencies": {
"@types/classnames": "^2.2.9",
"@types/jest": "24.0.13",
"@types/lodash": "4.14.132",
"@types/node": "11.13.0",

View File

@@ -11,7 +11,7 @@ import { Stats } from "./components/Stats";
import Studios from "./components/Studios/Studios";
import Tags from "./components/Tags/Tags";
import { SceneFilenameParser } from "./components/scenes/SceneFilenameParser";
import { ToastProvider } from './components/Shared/Toast';
import { ToastProvider } from 'src/hooks/Toast';
import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'

View File

@@ -1,26 +1,20 @@
import React, { useEffect, useState } from "react";
import React from "react";
import { Spinner } from 'react-bootstrap';
import * as GQL from "../../core/generated-graphql";
import { StashService } from "../../core/StashService";
import { IBaseProps } from "../../models";
import { useParams } from 'react-router-dom';
import { StashService } from "src/core/StashService";
import { GalleryViewer } from "./GalleryViewer";
interface IProps extends IBaseProps {}
export const Gallery: React.FC = () => {
const { id = '' } = useParams();
export const Gallery: React.FC<IProps> = (props: IProps) => {
const [gallery, setGallery] = useState<Partial<GQL.GalleryDataFragment>>({});
const [isLoading, setIsLoading] = useState(false);
const { data, error, loading } = StashService.useFindGallery(id);
const gallery = data?.findGallery;
const { data, error, loading } = StashService.useFindGallery(props.match.params.id);
if (loading || !gallery)
return <Spinner animation="border" variant="light" />;
if (error)
return <div>{error.message}</div>;
useEffect(() => {
setIsLoading(loading);
if (!data || !data.findGallery || !!error) { return; }
setGallery(data.findGallery);
}, [data]);
if (!data || !data.findGallery || isLoading) { return <Spinner animation="border" variant="light" />; }
if (!!error) { return <>{error.message}</>; }
return (
<div style={{width: "75vw", margin: "0 auto"}}>
<GalleryViewer gallery={gallery as any} />

View File

@@ -2,11 +2,11 @@ import React from "react";
import { Table } from 'react-bootstrap';
import { QueryHookResult } from "react-apollo-hooks";
import { Link } from "react-router-dom";
import { FindGalleriesQuery, FindGalleriesVariables } from "../../core/generated-graphql";
import { ListHook } from "../../hooks/ListHook";
import { IBaseProps } from "../../models/base-props";
import { ListFilterModel } from "../../models/list-filter/filter";
import { DisplayMode, FilterMode } from "../../models/list-filter/types";
import { FindGalleriesQuery, FindGalleriesVariables } from "src/core/generated-graphql";
import { ListHook } from "src/hooks";
import { IBaseProps } from "src/models/base-props";
import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode, FilterMode } from "src/models/list-filter/types";
interface IProps extends IBaseProps {}

View File

@@ -1,17 +1,17 @@
import React, { FunctionComponent, useState } from "react";
import Lightbox from "react-images";
import Gallery from "react-photo-gallery";
import * as GQL from "../../core/generated-graphql";
import * as GQL from "src/core/generated-graphql";
interface IProps {
gallery: GQL.GalleryDataFragment;
}
export const GalleryViewer: FunctionComponent<IProps> = (props: IProps) => {
export const GalleryViewer: FunctionComponent<IProps> = ({ gallery }) => {
const [currentImage, setCurrentImage] = useState<number>(0);
const [lightboxIsOpen, setLightboxIsOpen] = useState<boolean>(false);
function openLightbox(event: any, obj: any) {
function openLightbox(_event: React.MouseEvent<Element>, obj: {index: number}) {
setCurrentImage(obj.index);
setLightboxIsOpen(true);
}
@@ -26,8 +26,8 @@ export const GalleryViewer: FunctionComponent<IProps> = (props: IProps) => {
setCurrentImage(currentImage + 1);
}
const photos = props.gallery.files.map((file) => ({src: file.path || "", caption: file.name}));
const thumbs = props.gallery.files.map((file) => ({src: `${file.path}?thumb=true` || "", width: 1, height: 1}));
const photos = gallery.files.map((file) => ({src: file.path || "", caption: file.name}));
const thumbs = gallery.files.map((file) => ({src: `${file.path}?thumb=true` || "", width: 1, height: 1}));
return (
<div>
<Gallery photos={thumbs} columns={15} onClick={openLightbox} />

View File

@@ -71,6 +71,7 @@ export const MainNavbar: React.FC = () => {
activeClassName="active"
exact={true}
to={i.href}
key={i.href}
>
<Button variant="secondary">
<FontAwesomeIcon icon={i.icon} />

View File

@@ -1,50 +1,64 @@
import {
Card,
Tab,
Tabs,
} from "@blueprintjs/core";
import React from "react";
import queryString from "query-string";
import React, { FunctionComponent, useEffect, useState } from "react";
import { IBaseProps } from "../../models";
import { Card, Tab, Nav, Row, Col } from 'react-bootstrap';
import { useHistory, useLocation } from 'react-router-dom';
import { SettingsAboutPanel } from "./SettingsAboutPanel";
import { SettingsConfigurationPanel } from "./SettingsConfigurationPanel";
import { SettingsInterfacePanel } from "./SettingsInterfacePanel";
import { SettingsLogsPanel } from "./SettingsLogsPanel";
import { SettingsTasksPanel } from "./SettingsTasksPanel/SettingsTasksPanel";
interface IProps extends IBaseProps {}
export const Settings: React.FC = () => {
const location = useLocation();
const history = useHistory();
const defaultTab = queryString.parse(location.search).tab ?? 'configuration';
type TabId = "configuration" | "tasks" | "logs" | "about";
export const Settings: FunctionComponent<IProps> = (props: IProps) => {
const [tabId, setTabId] = useState<TabId>(getTabId());
useEffect(() => {
const location = Object.assign({}, props.history.location);
location.search = queryString.stringify({tab: tabId}, {encode: false});
props.history.replace(location);
}, [tabId]);
function getTabId(): TabId {
const queryParams = queryString.parse(props.location.search);
if (!queryParams.tab || typeof queryParams.tab !== "string") { return "tasks"; }
return queryParams.tab as TabId;
}
const onSelect = ((val:string) => history.push(`?tab=${val}`));
return (
<Card id="details-container">
<Tabs
renderActiveTabPanelOnly={true}
vertical={true}
onChange={(newId) => setTabId(newId as TabId)}
defaultSelectedTabId={getTabId()}
>
<Tab id="configuration" title="Configuration" panel={<SettingsConfigurationPanel />} />
<Tab id="interface" title="Interface Configuration" panel={<SettingsInterfacePanel />} />
<Tab id="tasks" title="Tasks" panel={<SettingsTasksPanel />} />
<Tab id="logs" title="Logs" panel={<SettingsLogsPanel />} />
<Tab id="about" title="About" panel={<SettingsAboutPanel />} />
</Tabs>
<Tab.Container defaultActiveKey={defaultTab} id="configuration-tabs" onSelect={onSelect}>
<Row>
<Col sm={2}>
<Nav variant="pills" className="flex-column">
<Nav.Item>
<Nav.Link eventKey="configuration">Configuration</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="interface">Interface</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="tasks">Tasks</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="logs">Logs</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="about">About</Nav.Link>
</Nav.Item>
</Nav>
</Col>
<Col sm={10}>
<Tab.Content>
<Tab.Pane eventKey="configuration">
<SettingsConfigurationPanel />
</Tab.Pane>
<Tab.Pane eventKey="interface">
<SettingsInterfacePanel />
</Tab.Pane>
<Tab.Pane eventKey="tasks">
<SettingsTasksPanel />
</Tab.Pane>
<Tab.Pane eventKey="logs">
<SettingsLogsPanel />
</Tab.Pane>
<Tab.Pane eventKey="about">
<SettingsAboutPanel />
</Tab.Pane>
</Tab.Content>
</Col>
</Row>
</Tab.Container>
</Card>
);
};

View File

@@ -1,6 +1,6 @@
import React from "react";
import { Table, Spinner } from 'react-bootstrap';
import { StashService } from "../../core/StashService";
import { StashService } from "src/core/StashService";
export const SettingsAboutPanel: React.FC = () => {
const { data, error, loading } = StashService.useVersion();
@@ -38,8 +38,8 @@ export const SettingsAboutPanel: React.FC = () => {
return (
<>
<h4>About</h4>
{!data || loading ? <Spinner animation="border" variant="light" /> : undefined}
{!!error ? <span>error.message</span> : undefined}
{!data || loading ? <Spinner animation="border" variant="light" /> : ''}
{error ? <span>error.message</span> : ''}
{renderVersion()}
</>
);

View File

@@ -1,21 +1,13 @@
import {
AnchorButton,
Button,
Divider,
FormGroup,
InputGroup,
Spinner,
Checkbox,
HTMLSelect,
} from "@blueprintjs/core";
import React, { useEffect, useState } from "react";
import * as GQL from "../../core/generated-graphql";
import { StashService } from "../../core/StashService";
import { ErrorUtils } from "../../utils/errors";
import { ToastUtils } from "../../utils/toasts";
import { FolderSelect } from "../Shared/FolderSelect/FolderSelect";
import { Button, Form, InputGroup, Spinner } from 'react-bootstrap';
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { useToast } from 'src/hooks';
import { Icon } from 'src/components/Shared';
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
export const SettingsConfigurationPanel: React.FC = () => {
const Toast = useToast();
// Editing config state
const [stashes, setStashes] = useState<string[]>([]);
const [databasePath, setDatabasePath] = useState<string | undefined>(undefined);
@@ -48,10 +40,12 @@ export const SettingsConfigurationPanel: React.FC = () => {
});
useEffect(() => {
if (!data || !data.configuration || !!error) { return; }
const conf = StashService.nullToUndefined(data.configuration) as GQL.ConfigDataFragment;
if (!!conf.general) {
setStashes(conf.general.stashes || []);
if (!data?.configuration || error)
return;
const conf = data.configuration;
if (conf.general) {
setStashes(conf.general.stashes ?? []);
setDatabasePath(conf.general.databasePath);
setGeneratedPath(conf.general.generatedPath);
setMaxTranscodeSize(conf.general.maxTranscodeSize);
@@ -64,7 +58,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
setLogAccess(conf.general.logAccess);
setExcludes(conf.general.excludes);
}
}, [data]);
}, [data, error]);
function onStashesChanged(directories: string[]) {
setStashes(directories);
@@ -96,9 +90,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
try {
const result = await updateGeneralConfig();
console.log(result);
ToastUtils.success("Updated config");
Toast.success({ content: "Updated config" });
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
}
@@ -137,149 +131,156 @@ export const SettingsConfigurationPanel: React.FC = () => {
return GQL.StreamingResolutionEnum.Original;
}
if(error)
return <h1>{error.message}</h1>;
if(!data?.configuration || loading)
return <Spinner animation="border" variant="light" />;
return (
<>
{!!error ? <h1>{error.message}</h1> : undefined}
{(!data || !data.configuration || loading) ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}
<h4>Library</h4>
<FormGroup>
<FormGroup>
<FormGroup
label="Stashes"
helperText="Directory locations to your content"
>
<Form.Group>
<Form.Group id="stashes">
<Form.Label>Stashes</Form.Label>
<FolderSelect
directories={stashes}
onDirectoriesChanged={onStashesChanged}
/>
</FormGroup>
</FormGroup>
<Form.Text className="text-muted">Directory locations to your content</Form.Text>
</Form.Group>
<FormGroup
label="Database Path"
helperText="File location for the SQLite database (requires restart)"
>
<InputGroup value={databasePath} onChange={(e: any) => setDatabasePath(e.target.value)} />
</FormGroup>
<Form.Group id="database-path">
<Form.Label>Database Path</Form.Label>
<Form.Control defaultValue={databasePath} onChange={(e: any) => setDatabasePath(e.target.value)} />
<Form.Text className="text-muted">File location for the SQLite database (requires restart)</Form.Text>
</Form.Group>
<FormGroup
label="Generated Path"
helperText="Directory location for the generated files (scene markers, scene previews, sprites, etc)"
>
<InputGroup value={generatedPath} onChange={(e: any) => setGeneratedPath(e.target.value)} />
</FormGroup>
<Form.Group id="generated-path">
<Form.Label>Generated Path</Form.Label>
<Form.Control defaultValue={generatedPath} onChange={(e: any) => setGeneratedPath(e.target.value)} />
<Form.Text className="text-muted">Directory location for the generated files (scene markers, scene previews, sprites, etc)</Form.Text>
</Form.Group>
<FormGroup
label="Excluded Patterns"
>
{ (excludes) ? excludes.map((regexp, i) => {
return(
<InputGroup
<Form.Group>
<Form.Label>Excluded Patterns</Form.Label>
{ excludes ? excludes.map((regexp, i) => (
<InputGroup>
<Form.Control
value={regexp}
onChange={(e: any) => excludeRegexChanged(i, e.target.value)}
rightElement={<Button icon="minus" minimal={true} intent="danger" onClick={(e: any) => excludeRemoveRegex(i)} />}
/>
);
}) : null
<InputGroup.Append>
<Button variant="danger" onClick={() => excludeRemoveRegex(i)}>
<Icon icon="minus" />
</Button>
</InputGroup.Append>
</InputGroup>
)) : ''
}
<Button icon="plus" minimal={true} onClick={(e: any) => excludeAddRegex()} />
<Button variant="danger" onClick={() => excludeAddRegex()}>
<Icon icon="plus" />
</Button>
<div>
<p>
<AnchorButton
<a
href="https://github.com/stashapp/stash/wiki/Exclude-file-configuration"
rightIcon="help"
text="Regexps of files/paths to exclude from Scan and add to Clean"
minimal={true}
rel="noopener noreferrer"
target="_blank"
/>
>
<span>Regexps of files/paths to exclude from Scan and add to Clean</span>
<Icon icon="question-circle" />
</a>
</p>
</div>
</FormGroup>
</FormGroup>
</Form.Group>
</Form.Group>
<Divider />
<FormGroup>
<hr />
<Form.Group>
<h4>Video</h4>
<FormGroup
label="Maximum transcode size"
helperText="Maximum size for generated transcodes"
>
<HTMLSelect
options={transcodeQualities}
onChange={(event) => setMaxTranscodeSize(translateQuality(event.target.value))}
<Form.Group id="transcode-size">
<Form.Label>Maximum transcode size</Form.Label>
<Form.Control
as="select">
onChange={(event:React.FormEvent<HTMLSelectElement>) => setMaxTranscodeSize(translateQuality(event.currentTarget.value))}
value={resolutionToString(maxTranscodeSize)}
/>
</FormGroup>
<FormGroup
label="Maximum streaming transcode size"
helperText="Maximum size for transcoded streams"
>
<HTMLSelect
options={transcodeQualities}
onChange={(event) => setMaxStreamingTranscodeSize(translateQuality(event.target.value))}
{ transcodeQualities.map(q => (<option key={q} value={q}>{q}</option>))}
</Form.Control>
<Form.Text className="text-muted">Maximum size for generated transcodes</Form.Text>
</Form.Group>
<Form.Group id="streaming-transcode-size">
<Form.Label>Maximum streaming transcode size</Form.Label>
<Form.Control
as="select"
onChange={(event:React.FormEvent<HTMLSelectElement>) => setMaxStreamingTranscodeSize(translateQuality(event.currentTarget.value))}
value={resolutionToString(maxStreamingTranscodeSize)}
/>
</FormGroup>
</FormGroup>
<Divider />
>
{ transcodeQualities.map(q => (<option key={q} value={q}>{q}</option>))}
</Form.Control>
<Form.Text className="text-muted">Maximum size for transcoded streams</Form.Text>
</Form.Group>
</Form.Group>
<FormGroup>
<hr />
<Form.Group>
<h4>Authentication</h4>
<FormGroup
label="Username"
helperText="Username to access Stash. Leave blank to disable user authentication"
>
<InputGroup value={username} onChange={(e: any) => setUsername(e.target.value)} />
</FormGroup>
<FormGroup
label="Password"
helperText="Password to access Stash. Leave blank to disable user authentication"
>
<InputGroup type="password" value={password} onChange={(e: any) => setPassword(e.target.value)} />
</FormGroup>
</FormGroup>
<Form.Group id="username">
<Form.Label>Username</Form.Label>
<Form.Control defaultValue={username} onChange={(e: React.FormEvent<HTMLInputElement>) => setUsername(e.currentTarget.value)} />
<Form.Text className="text-muted">Username to access Stash. Leave blank to disable user authentication</Form.Text>
</Form.Group>
<Form.Group id="password">
<Form.Label>Password</Form.Label>
<Form.Control type="password" defaultValue={password} onChange={(e: React.FormEvent<HTMLInputElement>) => setPassword(e.currentTarget.value)} />
<Form.Text className="text-muted">Password to access Stash. Leave blank to disable user authentication</Form.Text>
</Form.Group>
</Form.Group>
<hr />
<Divider />
<h4>Logging</h4>
<FormGroup
label="Log file"
helperText="Path to the file to output logging to. Blank to disable file logging. Requires restart."
>
<InputGroup value={logFile} onChange={(e: any) => setLogFile(e.target.value)} />
</FormGroup>
<Form.Group id="log-file">
<Form.Label>Log file</Form.Label>
<Form.Control defaultValue={logFile} onChange={(e: React.FormEvent<HTMLInputElement>) => setLogFile(e.currentTarget.value)} />
<Form.Text className="text-muted">Path to the file to output logging to. Blank to disable file logging. Requires restart.</Form.Text>
</Form.Group>
<FormGroup
helperText="Logs to the terminal in addition to a file. Always true if file logging is disabled. Requires restart."
>
<Checkbox
<Form.Group>
<Form.Check
checked={logOut}
label="Log to terminal"
onChange={() => setLogOut(!logOut)}
/>
</FormGroup>
<Form.Text className="text-muted">Logs to the terminal in addition to a file. Always true if file logging is disabled. Requires restart.</Form.Text>
</Form.Group>
<FormGroup inline={true} label="Log Level">
<HTMLSelect
options={["Debug", "Info", "Warning", "Error"]}
onChange={(event) => setLogLevel(event.target.value)}
<Form.Group id="log-level">
<Form.Label>Log Level</Form.Label>
<Form.Control
as="select"
onChange={(event:React.FormEvent<HTMLSelectElement>) => setLogLevel(event.currentTarget.value)}
value={logLevel}
/>
</FormGroup>
<FormGroup
helperText="Logs http access to the terminal. Requires restart."
>
<Checkbox
{ ["Debug", "Info", "Warning", "Error"].map(o => (<option key={o} value={o}>{o}</option>)) }
</Form.Control>
</Form.Group>
<Form.Group>
<Form.Check
checked={logAccess}
label="Log http access"
onChange={() => setLogAccess(!logAccess)}
/>
</FormGroup>
<Form.Text className="text-muted">Logs http access to the terminal. Requires restart.</Form.Text>
</Form.Group>
<Divider />
<Button intent="primary" onClick={() => onSave()}>Save</Button>
<hr />
<Button variant="primary" onClick={() => onSave()}>Save</Button>
</>
);
};

View File

@@ -1,20 +1,10 @@
import {
Button,
Checkbox,
Divider,
FormGroup,
Spinner,
TextArea,
NumericInput
} from "@blueprintjs/core";
import React, { FunctionComponent, useEffect, useState } from "react";
import { StashService } from "../../core/StashService";
import { ErrorUtils } from "../../utils/errors";
import { ToastUtils } from "../../utils/toasts";
import React, { useEffect, useState } from "react";
import { Button, Form, Spinner } from 'react-bootstrap';
import { StashService } from "src/core/StashService";
import { useToast } from 'src/hooks';
interface IProps {}
export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
export const SettingsInterfacePanel: React.FC = () => {
const Toast = useToast();
const config = StashService.useConfiguration();
const [soundOnPreview, setSoundOnPreview] = useState<boolean>();
const [wallShowTitle, setWallShowTitle] = useState<boolean>();
@@ -35,66 +25,63 @@ export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
});
useEffect(() => {
if (!config.data || !config.data.configuration || !!config.error) { return; }
if (!!config.data.configuration.interface) {
let iCfg = config.data.configuration.interface;
setSoundOnPreview(iCfg.soundOnPreview !== undefined ? iCfg.soundOnPreview : true);
setWallShowTitle(iCfg.wallShowTitle !== undefined ? iCfg.wallShowTitle : true);
setMaximumLoopDuration(iCfg.maximumLoopDuration || 0);
setAutostartVideo(iCfg.autostartVideo !== undefined ? iCfg.autostartVideo : false);
setShowStudioAsText(iCfg.showStudioAsText !== undefined ? iCfg.showStudioAsText : false);
setCSS(config.data.configuration.interface.css || "");
setCSSEnabled(config.data.configuration.interface.cssEnabled || false);
}
}, [config.data]);
if (config.error)
return;
const iCfg = config?.data?.configuration?.interface;
setSoundOnPreview(iCfg?.soundOnPreview ?? true);
setWallShowTitle(iCfg?.wallShowTitle ?? true);
setMaximumLoopDuration(iCfg?.maximumLoopDuration ?? 0);
setAutostartVideo(iCfg?.autostartVideo ?? false);
setShowStudioAsText(iCfg?.showStudioAsText ?? false);
setCSS(iCfg?.css ?? "");
setCSSEnabled(iCfg?.cssEnabled ?? false);
}, [config]);
async function onSave() {
try {
const result = await updateInterfaceConfig();
console.log(result);
ToastUtils.success("Updated config");
Toast.success({ content: "Updated config" });
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
}
return (
<>
{!!config.error ? <h1>{config.error.message}</h1> : undefined}
{(!config.data || !config.data.configuration || config.loading) ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}
{config.error ? <h1>{config.error.message}</h1> : ''}
{(!config?.data?.configuration || config.loading) ? <Spinner animation="border" variant="light" /> : ''}
<h4>User Interface</h4>
<FormGroup
label="Scene / Marker Wall"
helperText="Configuration for wall items"
>
<Checkbox
<Form.Group>
<Form.Label>Scene / Marker Wall</Form.Label>
<Form.Check
checked={wallShowTitle}
label="Display title and tags"
onChange={() => setWallShowTitle(!wallShowTitle)}
/>
<Checkbox
<Form.Check
checked={soundOnPreview}
label="Enable sound"
onChange={() => setSoundOnPreview(!soundOnPreview)}
/>
</FormGroup>
<Form.Text className="text-muted" >Configuration for wall items</Form.Text>
</Form.Group>
<FormGroup
label="Scene List"
>
<Checkbox
<Form.Group>
<Form.Label>Scene List</Form.Label>
<Form.Check
checked={showStudioAsText}
label="Show Studios as text"
onChange={() => {
setShowStudioAsText(!showStudioAsText)
}}
/>
</FormGroup>
</Form.Group>
<FormGroup
label="Scene Player"
>
<Checkbox
<Form.Group>
<Form.Label>Scene Player</Form.Label>
<Form.Check
checked={autostartVideo}
label="Auto-start video"
onChange={() => {
@@ -102,25 +89,22 @@ export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
}}
/>
<FormGroup
label="Maximum loop duration"
helperText="Maximum scene duration - in seconds - where scene player will loop the video - 0 to disable"
>
<NumericInput
value={maximumLoopDuration}
<Form.Group id="max-loop-duration">
<Form.Label>Maximum loop duration</Form.Label>
<Form.Control
type="number"
onValueChange={(value: number) => setMaximumLoopDuration(value)}
defaultValue={maximumLoopDuration}
onChange={(event:React.FormEvent<HTMLInputElement>) => setMaximumLoopDuration(Number.parseInt(event.currentTarget.value) ?? 0)}
min={0}
minorStepSize={1}
step={1}
/>
</FormGroup>
</FormGroup>
<Form.Text className="text-muted">Maximum scene duration - in seconds - where scene player will loop the video - 0 to disable</Form.Text>
</Form.Group>
</Form.Group>
<FormGroup
label="Custom CSS"
helperText="Page must be reloaded for changes to take effect."
>
<Checkbox
<Form.Group>
<Form.Label>Custom CSS</Form.Label>
<Form.Check
checked={cssEnabled}
label="Custom CSS enabled"
onChange={() => {
@@ -128,16 +112,17 @@ export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
}}
/>
<TextArea
<Form.Control
as="textarea"
value={css}
onChange={(e: any) => setCSS(e.target.value)}
fill={true}
rows={16}>
</TextArea>
</FormGroup>
</Form.Control>
<Form.Text className="text-muted">Page must be reloaded for changes to take effect.</Form.Text>
</Form.Group>
<Divider />
<Button intent="primary" onClick={() => onSave()}>Save</Button>
<hr />
<Button variant="primary" onClick={() => onSave()}>Save</Button>
</>
);
};

View File

@@ -1,11 +1,7 @@
import {
H4, FormGroup, HTMLSelect,
} from "@blueprintjs/core";
import React, { FunctionComponent, useState, useEffect, useRef } from "react";
import * as GQL from "../../core/generated-graphql";
import { StashService } from "../../core/StashService";
interface IProps {}
import React, { useState, useEffect, useRef } from "react";
import { Form, Col } from 'react-bootstrap';
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
function convertTime(logEntry: GQL.LogEntryDataFragment) {
function pad(val : number) {
@@ -44,7 +40,7 @@ class LogEntry {
}
}
export const SettingsLogsPanel: FunctionComponent<IProps> = (props: IProps) => {
export const SettingsLogsPanel: React.FC = () => {
const { data, error } = StashService.useLoggingSubscribe();
const { data: existingData } = StashService.useLogs();
@@ -167,16 +163,21 @@ export const SettingsLogsPanel: FunctionComponent<IProps> = (props: IProps) => {
return (
<>
<H4>Logs</H4>
<div>
<FormGroup inline={true} label="Log Level">
<HTMLSelect
options={logLevels}
onChange={(event) => setLogLevel(event.target.value)}
value={logLevel}
/>
</FormGroup>
</div>
<h4>Logs</h4>
<Form.Row id="log-level">
<Col xs={1}>
<Form.Label>Log Level</Form.Label>
</Col>
<Col xs={2}>
<Form.Control
as="select"
defaultValue={logLevel}
onChange={(event) => setLogLevel(event.currentTarget.value)}
>
{ logLevels.map(level => (<option key={level} value={level}>{level}</option>)) }
</Form.Control>
</Col>
</Form.Row>
<div className="logs">
{maybeRenderError()}
{filteredLogEntries.map((logEntry) =>

View File

@@ -1,53 +1,32 @@
import {
Button,
Checkbox,
FormGroup,
} from "@blueprintjs/core";
import React, { FunctionComponent, useState } from "react";
import { StashService } from "../../../core/StashService";
import { ErrorUtils } from "../../../utils/errors";
import { ToastUtils } from "../../../utils/toasts";
import React, { useState } from "react";
import { Button, Form } from 'react-bootstrap';
import { StashService } from "src/core/StashService";
import { useToast } from 'src/hooks';
interface IProps {}
export const GenerateButton: FunctionComponent<IProps> = () => {
const [sprites, setSprites] = useState<boolean>(true);
const [previews, setPreviews] = useState<boolean>(true);
const [markers, setMarkers] = useState<boolean>(true);
const [transcodes, setTranscodes] = useState<boolean>(true);
export const GenerateButton: React.FC = () => {
const Toast = useToast();
const [sprites, setSprites] = useState(true);
const [previews, setPreviews] = useState(true);
const [markers, setMarkers] = useState(true);
const [transcodes, setTranscodes] = useState(true);
async function onGenerate() {
try {
await StashService.queryMetadataGenerate({sprites, previews, markers, transcodes});
ToastUtils.success("Started generating");
Toast.success({ content: "Started generating" });
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
}
return (
<FormGroup
helperText="Generate supporting image, sprite, video, vtt and other files."
labelFor="generate"
inline={true}
>
<Checkbox checked={sprites} label="Sprites (for the scene scrubber)" onChange={() => setSprites(!sprites)} />
<Checkbox
checked={previews}
label="Previews (video previews which play when hovering over a scene)"
onChange={() => setPreviews(!previews)}
/>
<Checkbox
checked={markers}
label="Markers (20 second videos which begin at the given timecode)"
onChange={() => setMarkers(!markers)}
/>
<Checkbox
checked={transcodes}
label="Transcodes (MP4 conversions of unsupported video formats)"
onChange={() => setTranscodes(!transcodes)}
/>
<Button id="generate" text="Generate" onClick={() => onGenerate()} />
</FormGroup>
<Form.Group>
<Form.Check id="sprite-task" checked={sprites} label="Sprites (for the scene scrubber)" onChange={() => setSprites(!sprites)} />
<Form.Check id="preview-task" checked={previews} label="Previews (video previews which play when hovering over a scene)" onChange={() => setPreviews(!previews)} />
<Form.Check id="marker-task" checked={markers} label="Markers (20 second videos which begin at the given timecode)" onChange={() => setMarkers(!markers)} />
<Form.Check id="transcode-task" checked={transcodes} label="Transcodes (MP4 conversions of unsupported video formats)" onChange={() => setTranscodes(!transcodes)} />
<Button id="generate" type="submit" onClick={() => onGenerate()}>Generate</Button>
<Form.Text className="text-muted">Generate supporting image, sprite, video, vtt and other files.</Form.Text>
</Form.Group>
);
};

View File

@@ -1,24 +1,18 @@
import {
Alert,
Button,
Checkbox,
Divider,
FormGroup,
ProgressBar,
} from "@blueprintjs/core";
import React, { useState, useEffect } from "react";
import { StashService } from "../../../core/StashService";
import { ErrorUtils } from "../../../utils/errors";
import { ToastUtils } from "../../../utils/toasts";
import { GenerateButton } from "./GenerateButton";
import { Button, Form, ProgressBar } from 'react-bootstrap';
import { Link } from "react-router-dom";
import { StashService } from "src/core/StashService";
import { useToast } from 'src/hooks';
import { Modal } from 'src/components/Shared';
import { GenerateButton } from "./GenerateButton";
export const SettingsTasksPanel: React.FC = () => {
const Toast = useToast();
const [isImportAlertOpen, setIsImportAlertOpen] = useState<boolean>(false);
const [isCleanAlertOpen, setIsCleanAlertOpen] = useState<boolean>(false);
const [useFileMetadata, setUseFileMetadata] = useState<boolean>(false);
const [status, setStatus] = useState<string>("");
const [progress, setProgress] = useState<number | undefined>(undefined);
const [progress, setProgress] = useState<number>(0);
const [autoTagPerformers, setAutoTagPerformers] = useState<boolean>(true);
const [autoTagStudios, setAutoTagStudios] = useState<boolean>(true);
@@ -53,7 +47,7 @@ export const SettingsTasksPanel: React.FC = () => {
setStatus(statusToText(jobStatus.data.jobStatus.status));
var newProgress = jobStatus.data.jobStatus.progress;
if (newProgress < 0) {
setProgress(undefined);
setProgress(0);
} else {
setProgress(newProgress);
}
@@ -65,7 +59,7 @@ export const SettingsTasksPanel: React.FC = () => {
setStatus(statusToText(metadataUpdate.data.metadataUpdate.status));
var newProgress = metadataUpdate.data.metadataUpdate.progress;
if (newProgress < 0) {
setProgress(undefined);
setProgress(0);
} else {
setProgress(newProgress);
}
@@ -79,20 +73,17 @@ export const SettingsTasksPanel: React.FC = () => {
function renderImportAlert() {
return (
<Alert
cancelButtonText="Cancel"
confirmButtonText="Import"
icon="trash"
intent="danger"
isOpen={isImportAlertOpen}
onCancel={() => setIsImportAlertOpen(false)}
onConfirm={() => onImport()}
<Modal
show={isImportAlertOpen}
icon="trash-alt"
accept={{ text: 'Import', variant: 'danger', onClick: onImport }}
cancel={{ onClick: () => setIsImportAlertOpen(false) }}
>
<p>
Are you sure you want to import? This will delete the database and re-import from
your exported metadata.
</p>
</Alert>
</Modal>
);
}
@@ -103,31 +94,28 @@ export const SettingsTasksPanel: React.FC = () => {
function renderCleanAlert() {
return (
<Alert
cancelButtonText="Cancel"
confirmButtonText="Clean"
icon="trash"
intent="danger"
isOpen={isCleanAlertOpen}
onCancel={() => setIsCleanAlertOpen(false)}
onConfirm={() => onClean()}
<Modal
show={isCleanAlertOpen}
icon="trash-alt"
accept={{ text: 'Clean', variant: 'danger', onClick: onClean }}
cancel={{ onClick: () => setIsCleanAlertOpen(false) }}
>
<p>
Are you sure you want to Clean?
This will delete db information and generated content
for all scenes that are no longer found in the filesystem.
</p>
</Alert>
</Modal>
);
}
async function onScan() {
try {
await StashService.queryMetadataScan({useFileMetadata: useFileMetadata});
ToastUtils.success("Started scan");
Toast.success({ content: "Started scan" });
jobStatus.refetch();
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
}
@@ -143,10 +131,10 @@ export const SettingsTasksPanel: React.FC = () => {
async function onAutoTag() {
try {
await StashService.queryMetadataAutoTag(getAutoTagInput());
ToastUtils.success("Started auto tagging");
Toast.success({ content: "Started auto tagging" });
jobStatus.refetch();
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
}
@@ -156,21 +144,19 @@ export const SettingsTasksPanel: React.FC = () => {
}
return (
<>
<FormGroup>
<Button id="stop" text="Stop" intent="danger" onClick={() => StashService.queryStopJob().then(() => jobStatus.refetch())} />
</FormGroup>
</>
<Form.Group>
<Button id="stop" variant="danger" onClick={() => StashService.queryStopJob().then(() => jobStatus.refetch())}>Stop</Button>
</Form.Group>
);
}
function renderJobStatus() {
return (
<>
<FormGroup>
<Form.Group>
<h5>Status: {status}</h5>
{!!status && status !== "Idle" ? <ProgressBar value={progress}/> : undefined}
</FormGroup>
{ status !== "Idle" ? <ProgressBar now={progress} label={`${progress}%`} /> : '' }
</Form.Group>
{maybeRenderStop()}
</>
);
@@ -185,83 +171,70 @@ export const SettingsTasksPanel: React.FC = () => {
{renderJobStatus()}
<Divider/>
<hr />
<h4>Library</h4>
<FormGroup
helperText="Scan for new content and add it to the database."
labelFor="scan"
inline={true}
>
<Checkbox
<Form.Group>
<Form.Check
checked={useFileMetadata}
label="Set name, date, details from metadata (if present)"
onChange={() => setUseFileMetadata(!useFileMetadata)}
/>
<Button id="scan" text="Scan" onClick={() => onScan()} />
</FormGroup>
<Button id="scan" type="submit" onClick={() => onScan()}>Scan</Button>
<Form.Text className="text-muted">Scan for new content and add it to the database.</Form.Text>
</Form.Group>
<Divider />
<hr />
<h4>Auto Tagging</h4>
<FormGroup
helperText="Auto-tag content based on filenames."
labelFor="autoTag"
inline={true}
>
<Checkbox
<Form.Group>
<Form.Check
checked={autoTagPerformers}
label="Performers"
onChange={() => setAutoTagPerformers(!autoTagPerformers)}
/>
<Checkbox
<Form.Check
checked={autoTagStudios}
label="Studios"
onChange={() => setAutoTagStudios(!autoTagStudios)}
/>
<Checkbox
<Form.Check
checked={autoTagTags}
label="Tags"
onChange={() => setAutoTagTags(!autoTagTags)}
/>
<Button id="autoTag" text="Auto Tag" onClick={() => onAutoTag()} />
</FormGroup>
<Button id="autoTag" type="submit" onClick={() => onAutoTag()}>Auto Tag</Button>
<Form.Text className="text-muted">Auto-tag content based on filenames.</Form.Text>
</Form.Group>
<FormGroup>
<Link className="bp3-button" to={"/sceneFilenameParser"}>
Scene Filename Parser
</Link>
</FormGroup>
<Divider />
<Form.Group>
<Button>
<Link to={"/sceneFilenameParser"}>Scene Filename Parser</Link>
</Button>
</Form.Group>
<hr />
<h4>Generated Content</h4>
<GenerateButton />
<FormGroup
helperText="Check for missing files and remove them from the database. This is a destructive action."
labelFor="clean"
inline={true}
>
<Button id="clean" text="Clean" intent="danger" onClick={() => setIsCleanAlertOpen(true)} />
</FormGroup>
<Divider />
<Form.Group>
<Button id="clean" variant="danger" onClick={() => setIsCleanAlertOpen(true)}>Clean</Button>
<Form.Text className="text-muted">Check for missing files and remove them from the database. This is a destructive action.</Form.Text>
</Form.Group>
<hr />
<h4>Metadata</h4>
<FormGroup
helperText="Export the database content into JSON format"
labelFor="export"
inline={true}
>
<Button id="export" text="Export" onClick={() => StashService.queryMetadataExport().then(() => { jobStatus.refetch()})} />
</FormGroup>
<Form.Group>
<Button id="export" type="submit"onClick={() => StashService.queryMetadataExport().then(() => { jobStatus.refetch()})}>Export</Button>
<Form.Text className="text-muted">Export the database content into JSON format.</Form.Text>
</Form.Group>
<FormGroup
helperText="Import from exported JSON. This is a destructive action."
labelFor="import"
inline={true}
>
<Button id="import" text="Import" intent="danger" onClick={() => setIsImportAlertOpen(true)} />
</FormGroup>
<Form.Group>
<Button id="import" variant="danger" onClick={() => setIsImportAlertOpen(true)}>Import</Button>
<Form.Text className="text-muted">Import from exported JSON. This is a destructive action.</Form.Text>
</Form.Group>
</>
);
};

View File

@@ -1,8 +1,8 @@
import { Button, Form, Modal, Nav, Navbar, OverlayTrigger, Popover } from 'react-bootstrap';
import React, { useState } from "react";
import { Link } from "react-router-dom";
import * as GQL from "../../core/generated-graphql";
import { NavigationUtils } from "../../utils/navigation";
import * as GQL from "src/core/generated-graphql";
import { NavUtils } from "src/utils";
interface IProps {
performer?: Partial<GQL.PerformerDataFragment>;
@@ -92,10 +92,10 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
function renderScenesButton() {
if (props.isEditing) { return; }
let linkSrc: string = "#";
if (!!props.performer) {
linkSrc = NavigationUtils.makePerformerScenesUrl(props.performer);
} else if (!!props.studio) {
linkSrc = NavigationUtils.makeStudioScenesUrl(props.studio);
if (props.performer) {
linkSrc = NavUtils.makePerformerScenesUrl(props.performer);
} else if (props.studio) {
linkSrc = NavUtils.makeStudioScenesUrl(props.studio);
}
return (
<Link to={linkSrc}>

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react";
import { Button, ButtonGroup, InputGroup, Form } from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { TextUtils } from "../../utils/text";
import { TextUtils } from "src/utils";
interface IProps {
disabled?: boolean
@@ -122,9 +122,7 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
onChange={(e : any) => setValue(e.target.value)}
onBlur={() => props.onValueChange(stringToSeconds(value))}
placeholder="hh:mm:ss"
>
{renderButtons()}
</Form.Control>
/>
<InputGroup.Append>
{maybeRenderReset()}
</InputGroup.Append>

View File

@@ -1,6 +1,6 @@
import { Button, InputGroup, Form, Modal, Spinner } from 'react-bootstrap';
import React, { useEffect, useState } from "react";
import { StashService } from "../../../core/StashService";
import { Button, InputGroup, Form, Modal, Spinner } from 'react-bootstrap';
import { StashService } from "src/core/StashService";
interface IProps {
directories: string[];
@@ -17,7 +17,7 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
setSelectedDirectories(props.directories);
}, [props.directories]);
const selectableDirectories:string[] = data && data.directories && !error ? StashService.nullToUndefined(data.directories) : [];
const selectableDirectories:string[] = data?.directories ?? [];
function onSelectDirectory() {
selectedDirectories.push(currentDirectory);
@@ -71,7 +71,7 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
return (
<>
{!!error ? <h1>{error.message}</h1> : undefined}
{error ? <h1>{error.message}</h1> : ''}
{renderDialog()}
<Form.Group>
{selectedDirectories.map((path) => {

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IconName } from '@fortawesome/fontawesome-svg-core';
interface IIcon {
icon: IconName;
className?: string;
color?: string;
}
const Icon: React.FC<IIcon> = ({ icon, className, color }) => (
<FontAwesomeIcon icon={icon} className={className} color={color} />
);
export default Icon;

View File

@@ -0,0 +1,44 @@
import React from "react";
import { Button, Modal } from 'react-bootstrap';
import { Icon } from 'src/components/Shared';
import { IconName } from '@fortawesome/fontawesome-svg-core';
interface IButton {
text?: string;
variant?: 'danger'|'primary';
onClick?: () => void;
}
interface IModal {
show: boolean;
onHide?: () => void;
header?: string;
icon?: IconName;
cancel?: IButton;
accept?: IButton;
}
const ModalComponent: React.FC<IModal> = ({ children, show, icon, header, cancel, accept, onHide }) => ((
<Modal
keyboard={false}
onHide={onHide}
show={show}
>
<Modal.Header>
{ icon ? <Icon icon={icon} /> : '' }
<span>{ header ?? '' }</span>
</Modal.Header>
<Modal.Body>{children}</Modal.Body>
<Modal.Footer>
<div>
{ cancel
? <Button variant={cancel.variant ?? 'primary'} onClick={cancel.onClick}>{cancel.text ?? 'Cancel'}</Button>
: ''
}
{ <Button variant={accept?.variant ?? 'primary'} onClick={accept?.onClick}>{accept?.text ?? 'Close'}</Button> }
</div>
</Modal.Footer>
</Modal>
));
export default ModalComponent;

View File

@@ -1,11 +1,11 @@
import React, { useState } from "react";
import React, { useState, useCallback } from "react";
import Select, { ValueType } from 'react-select';
import CreatableSelect from 'react-select/creatable';
import { debounce } from 'lodash';
import { ErrorUtils } from "../../utils/errors";
import * as GQL from "../../core/generated-graphql";
import { StashService } from "../../core/StashService";
import useToast from '../Shared/Toast';
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { useToast } from 'src/hooks';
type ValidTypes =
GQL.AllPerformersForFilterAllPerformers |
@@ -14,7 +14,7 @@ type ValidTypes =
type Option = { value:string, label:string };
interface ITypeProps {
type: 'performers' | 'studios' | 'tags';
type?: 'performers' | 'studios' | 'tags';
}
interface IFilterProps {
initialIds: string[];
@@ -32,8 +32,66 @@ interface ISelectProps {
isLoading: boolean;
onChange: (item: ValueType<Option>) => void;
initialIds: string[];
noSelectionString?: string;
isMulti?: boolean;
onInputChange?: (input: string) => void;
placeholder?: string;
}
interface ISceneGallerySelect {
initialId?: string;
sceneId: string;
onSelect: (item: GQL.ValidGalleriesForSceneValidGalleriesForScene | undefined) => void;
}
export const SceneGallerySelect: React.FC<ISceneGallerySelect> = (props) => {
const { data, loading } = StashService.useValidGalleriesForScene(props.sceneId);
const galleries = data?.validGalleriesForScene ?? [];
const items = (galleries.length > 0 ? [{ path: 'None', id: '0' }, ...galleries] : [])
.map(g => ({ label: g.path, value: g.id }));
const onChange = (selectedItems:ValueType<Option>) => {
const selectedItem = getSelectedValues(selectedItems)[0];
props.onSelect(galleries.find(g => g.id === selectedItem.value));
};
const initialId = props.initialId ? [props.initialId] : [];
return <SelectComponent onChange={onChange} isLoading={loading} items={items} initialIds={initialId} />
};
interface IScrapePerformerSuggestProps {
scraperId: string;
onSelectPerformer: (query: GQL.ScrapePerformerListScrapePerformerList) => void;
placeholder?: string;
}
export const ScrapePerformerSuggest: React.FC<IScrapePerformerSuggestProps> = (props) => {
const [query, setQuery] = React.useState<string>("");
const { data, loading } = StashService.useScrapePerformerList(props.scraperId, query);
const onInputChange = useCallback(debounce((input:string) => { setQuery(input)}, 500), []);
const onChange = (selectedItems:ValueType<Option>) => (
props.onSelectPerformer(getSelectedValues(selectedItems)[0])
);
const performers = data?.scrapePerformerList ?? [];
console.log(`performers: ${performers}, loading: ${loading}, query: ${query}`);
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} />
}
interface IMarkerSuggestProps {
initialMarkerTitle?: string;
onChange: (title:string) => void;
}
export const MarkerTitleSuggest: React.FC<IMarkerSuggestProps> = (props) => {
const { data, loading } = StashService.useMarkerStrings();
const suggestions = data?.markerStrings ?? [];
const onChange = (selectedItems:ValueType<Option>) => (
props.onChange(getSelectedValues(selectedItems)[0])
);
const items = suggestions.map(item => ({ label: item?.title ?? '', value: item?.title ?? '' }));
const initialIds = props.initialMarkerTitle ? [props.initialMarkerTitle] : [];
return <SelectComponent creatable onChange={onChange} isLoading={loading} items={items} initialIds={initialIds} />
}
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) => (
@@ -47,14 +105,14 @@ export const PerformerSelect: React.FC<IFilterProps> = (props) => {
const normalizedData = data?.allPerformers ?? [];
const items:Option[] = normalizedData.map(item => ({ value: item.id, label: item.name ?? '' }));
const placeholder = props.noSelectionString ?? "Select performer..."
const onChange = (selectedItems:ValueType<Option>) => {
const selectedIds = (Array.isArray(selectedItems) ? selectedItems : [selectedItems])
.map(item => item.value);
const selectedIds = getSelectedValues(selectedItems);
props.onSelect(normalizedData.filter(item => selectedIds.indexOf(item.id) !== -1));
};
return <SelectComponent {...props} onChange={onChange} type="performers" isLoading={loading} items={items} />
return <SelectComponent {...props} onChange={onChange} type="performers" isLoading={loading} items={items} placeholder={placeholder} />
}
export const StudioSelect: React.FC<IFilterProps> = (props) => {
@@ -62,14 +120,14 @@ export const StudioSelect: React.FC<IFilterProps> = (props) => {
const normalizedData = data?.allStudios ?? [];
const items:Option[] = normalizedData.map(item => ({ value: item.id, label: item.name }));
const placeholder = props.noSelectionString ?? "Select studio..."
const onChange = (selectedItems:ValueType<Option>) => {
const selectedIds = (Array.isArray(selectedItems) ? selectedItems : [selectedItems])
.map(item => item.value);
const selectedIds = getSelectedValues(selectedItems);
props.onSelect(normalizedData.filter(item => selectedIds.indexOf(item.id) !== -1));
};
return <SelectComponent {...props} onChange={onChange} type="studios" isLoading={loading} items={items} />
return <SelectComponent {...props} onChange={onChange} type="studios" isLoading={loading} items={items} placeholder={placeholder} />
}
export const TagSelect: React.FC<IFilterProps> = (props) => {
@@ -78,6 +136,7 @@ export const TagSelect: React.FC<IFilterProps> = (props) => {
const { data, loading: dataLoading } = StashService.useAllTagsForFilter();
const createTag = StashService.useTagCreate({name: ''});
const Toast = useToast();
const placeholder = props.noSelectionString ?? "Select tags..."
const tags = data?.allTags ?? [];
@@ -92,30 +151,27 @@ export const TagSelect: React.FC<IFilterProps> = (props) => {
props.onSelect([...tags, result.data.tagCreate].filter(item => selected.indexOf(item.id) !== -1));
setLoading(false);
Toast({ content: (<span>Created tag: <b>{tagName}</b></span>) });
Toast.success({ content: (<span>Created tag: <b>{tagName}</b></span>) });
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
};
const onChange = (selectedItems:ValueType<Option>) => {
debugger;
const selected = (Array.isArray(selectedItems) ? selectedItems : [selectedItems])
.map(item => item.value);
const selected = getSelectedValues(selectedItems);
setSelectedIds(selected);
props.onSelect(tags.filter(item => selected.indexOf(item.id) !== -1));
};
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 }));
return <SelectComponent {...props} onChange={onChange} creatable={true} type="tags"
return <SelectComponent {...props} onChange={onChange} creatable={true} type="tags" placeholder={placeholder}
isLoading={loading || dataLoading} items={items} onCreateOption={onCreate} selectedOptions={selected} />
}
const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
type,
initialIds,
noSelectionString,
onChange,
className,
items,
@@ -124,6 +180,8 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
onCreateOption,
creatable = false,
isMulti = false,
onInputChange,
placeholder
}) => {
const defaultValue = items.filter(item => initialIds?.indexOf(item.value) !== -1) ?? null;
@@ -135,7 +193,8 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
isMulti: isMulti,
defaultValue: defaultValue,
noOptionsMessage: () => (type !== 'tags' ? 'None' : null),
placeholder: noSelectionString ?? "(No selection)"
placeholder: placeholder,
onInputChange
}
return (
@@ -144,3 +203,9 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
: <Select {...props} isLoading={isLoading} />
);
};
const getSelectedValues = (selectedItems:ValueType<Option>) => (
(Array.isArray(selectedItems) ? selectedItems : [selectedItems])
.map(item => item.value)
);

View File

@@ -1,9 +1,8 @@
import { Badge } from 'react-bootstrap';
import React from "react";
import { Link } from "react-router-dom";
import { PerformerDataFragment, SceneMarkerDataFragment, TagDataFragment } from "../../core/generated-graphql";
import { NavigationUtils } from "../../utils/navigation";
import { TextUtils } from "../../utils/text";
import { PerformerDataFragment, SceneMarkerDataFragment, TagDataFragment } from "src/core/generated-graphql";
import { NavUtils, TextUtils } from "src/utils";
interface IProps {
tag?: Partial<TagDataFragment>;
@@ -14,14 +13,14 @@ interface IProps {
export const TagLink: React.FC<IProps> = (props: IProps) => {
let link: string = "#";
let title: string = "";
if (!!props.tag) {
link = NavigationUtils.makeTagScenesUrl(props.tag);
if (props.tag) {
link = NavUtils.makeTagScenesUrl(props.tag);
title = props.tag.name || "";
} else if (!!props.performer) {
link = NavigationUtils.makePerformerScenesUrl(props.performer);
} else if (props.performer) {
link = NavUtils.makePerformerScenesUrl(props.performer);
title = props.performer.name || "";
} else if (!!props.marker) {
link = NavigationUtils.makeSceneMarkerUrl(props.marker);
} else if (props.marker) {
link = NavUtils.makeSceneMarkerUrl(props.marker);
title = `${props.marker.title} - ${TextUtils.secondsToTimestamp(props.marker.seconds || 0)}`;
}
return (

View File

@@ -0,0 +1,15 @@
export {
SceneGallerySelect,
ScrapePerformerSuggest,
MarkerTitleSuggest,
FilterSelect,
PerformerSelect,
StudioSelect,
TagSelect
} from './Select';
export { default as Icon } from './Icon';
export { default as Modal } from './Modal';
export { DetailsEditNavbar } from './DetailsEditNavbar';
export { DurationInput } from './DurationInput';
export { TagLink } from './TagLink';

View File

@@ -1,27 +1,27 @@
import { Card } from 'react-bootstrap';
import React from "react";
import { Link } from "react-router-dom";
import * as GQL from "../../core/generated-graphql";
import * as GQL from "src/core/generated-graphql";
interface IProps {
studio: GQL.StudioDataFragment;
}
export const StudioCard: React.FC<IProps> = (props: IProps) => {
export const StudioCard: React.FC<IProps> = ({ studio }) => {
return (
<Card
className="col-4"
>
<Link
to={`/studios/${props.studio.id}`}
to={`/studios/${studio.id}`}
className="studio previewable image"
style={{backgroundImage: `url(${props.studio.image_path})`}}
style={{backgroundImage: `url(${studio.image_path})`}}
/>
<div className="card-section">
<h4 className="text-truncate">
{props.studio.name}
{studio.name}
</h4>
<span>{props.studio.scene_count} scenes.</span>
<span>{studio.scene_count} scenes.</span>
</div>
</Card>
);

View File

@@ -2,17 +2,16 @@ import { Form, Spinner, Table } from 'react-bootstrap';
import React, { useEffect, useState } from "react";
import { useParams, useHistory } from 'react-router-dom';
import * as GQL from "../../../core/generated-graphql";
import { StashService } from "../../../core/StashService";
import { ErrorUtils } from "../../../utils/errors";
import { TableUtils } from "../../../utils/table";
import { DetailsEditNavbar } from "../../Shared/DetailsEditNavbar";
import { ToastUtils } from "../../../utils/toasts";
import { ImageUtils } from "../../../utils/image";
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { ImageUtils, TableUtils } from "src/utils";
import { DetailsEditNavbar } from "src/components/Shared";
import { useToast} from "src/hooks";
export const Studio: React.FC = () => {
const { id = '' } = useParams();
const history = useHistory();
const Toast = useToast();
const { id = 'new' } = useParams();
const isNew = id === "new";
// Editing state
@@ -58,11 +57,13 @@ export const Studio: React.FC = () => {
setImage(this.result as string);
}
ImageUtils.addPasteImageHook(onImageLoad);
ImageUtils.usePasteImage(onImageLoad);
if (!isNew && !isEditing) {
if (!data || !data.findStudio || loading) { return <Spinner animation="border" variant="light" />; }
if (!!error) { return <>error...</>; }
if (!data?.findStudio || loading)
return <Spinner animation="border" variant="light" />;
if (error)
return <div>{error.message}</div>;
}
function getStudioInput() {
@@ -89,7 +90,7 @@ export const Studio: React.FC = () => {
history.push(`/studios/${result.data.studioCreate.id}`);
}
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
}
@@ -99,9 +100,9 @@ export const Studio: React.FC = () => {
}
try {
await StashService.queryMetadataAutoTag({ studios: [studio.id]});
ToastUtils.success("Started auto tagging");
Toast.success({ content: "Started auto tagging" });
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
}
@@ -109,14 +110,14 @@ export const Studio: React.FC = () => {
try {
await deleteStudio();
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
// redirect to studios page
history.push(`/studios`);
}
function onImageChange(event: React.FormEvent<HTMLInputElement>) {
function onImageChangeHandler(event: React.FormEvent<HTMLInputElement>) {
ImageUtils.onImageChange(event, onImageLoad);
}
@@ -135,7 +136,7 @@ export const Studio: React.FC = () => {
onSave={onSave}
onDelete={onDelete}
onAutoTag={onAutoTag}
onImageChange={onImageChange}
onImageChange={onImageChangeHandler}
/>
<h1>
{ !isEditing

View File

@@ -1,10 +1,10 @@
import React, { FunctionComponent } from "react";
import { QueryHookResult } from "react-apollo-hooks";
import { FindStudiosQuery, FindStudiosVariables } from "../../core/generated-graphql";
import { ListHook } from "../../hooks/ListHook";
import { IBaseProps } from "../../models/base-props";
import { ListFilterModel } from "../../models/list-filter/filter";
import { DisplayMode, FilterMode } from "../../models/list-filter/types";
import { FindStudiosQuery, FindStudiosVariables } from "src/core/generated-graphql";
import { ListHook } from "src/hooks";
import { IBaseProps } from "src/models/base-props";
import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode, FilterMode } from "src/models/list-filter/types";
import { StudioCard } from "./StudioCard";
interface IProps extends IBaseProps {}

View File

@@ -1,14 +1,14 @@
import React, { useState } from "react";
import { Button, Form, Modal, Spinner } from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Button, Form, Spinner } from 'react-bootstrap';
import { Link } from "react-router-dom";
import * as GQL from "../../core/generated-graphql";
import { StashService } from "../../core/StashService";
import { ErrorUtils } from "../../utils/errors";
import { NavigationUtils } from "../../utils/navigation";
import { ToastUtils } from "../../utils/toasts";
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { NavUtils } from "src/utils";
import { Icon, Modal } from 'src/components/Shared';
import { useToast } from 'src/hooks';
export const TagList: React.FC = () => {
const Toast = useToast();
// Editing / New state
const [name, setName] = useState('');
const [editingTag, setEditingTag] = useState<Partial<GQL.TagDataFragment> | null>(null);
@@ -21,7 +21,8 @@ export const TagList: React.FC = () => {
function getTagInput() {
const tagInput: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = { name };
if (!!editingTag) { (tagInput as Partial<GQL.TagUpdateInput>).id = editingTag.id; }
if (editingTag)
(tagInput as Partial<GQL.TagUpdateInput>).id = editingTag.id;
return tagInput;
}
@@ -37,36 +38,35 @@ export const TagList: React.FC = () => {
try {
if (editingTag && editingTag.id) {
await updateTag();
ToastUtils.success("Updated tag");
Toast.success({ content: "Updated tag" });
} else {
await createTag();
ToastUtils.success("Created tag");
Toast.success({ content: "Created tag" });
}
setEditingTag(null);
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
}
async function onAutoTag(tag : GQL.TagDataFragment) {
if (!tag) {
if (!tag)
return;
}
try {
await StashService.queryMetadataAutoTag({ tags: [tag.id]});
ToastUtils.success("Started auto tagging");
Toast.success({ content: "Started auto tagging" });
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
}
async function onDelete() {
try {
await deleteTag();
ToastUtils.success("Deleted tag");
Toast.success({ content: "Deleted tag" });
setDeletingTag(null);
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
}
@@ -74,22 +74,18 @@ export const TagList: React.FC = () => {
<Modal
onHide={() => {}}
show={!!deletingTag}
icon="trash-alt"
accept={{ onClick: onDelete, variant: 'danger', text: 'Delete' }}
cancel={{ onClick: () => setDeletingTag(null) }}
>
<Modal.Body>
<FontAwesomeIcon icon="trash-alt" color="danger" />
<span>Are you sure you want to delete {deletingTag && deletingTag.name}?</span>
</Modal.Body>
<Modal.Footer>
<div>
<Button variant="danger" onClick={onDelete}>Delete</Button>
<Button onClick={() => setDeletingTag(null)}>Cancel</Button>
</div>
</Modal.Footer>
</Modal>
);
if (!data || !data.allTags) { return <Spinner animation="border" variant="light" />; }
if (!!error) { return <>{error.message}</>; }
if (!data?.allTags)
return <Spinner animation="border" variant="light" />;
if (error)
return <div>{error.message}</div>;
const tagElements = data.allTags.map((tag) => {
return (
@@ -99,13 +95,13 @@ export const TagList: React.FC = () => {
<span onClick={() => setEditingTag(tag)}>{tag.name}</span>
<div style={{float: "right"}}>
<Button onClick={() => onAutoTag(tag)}>Auto Tag</Button>
<Link to={NavigationUtils.makeTagScenesUrl(tag)}>Scenes: {tag.scene_count}</Link>
<Link to={NavigationUtils.makeTagSceneMarkersUrl(tag)}>
<Link to={NavUtils.makeTagScenesUrl(tag)}>Scenes: {tag.scene_count}</Link>
<Link to={NavUtils.makeTagSceneMarkersUrl(tag)}>
Markers: {tag.scene_marker_count}
</Link>
<span>Total: {(tag.scene_count || 0) + (tag.scene_marker_count || 0)}</span>
<Button variant="danger" onClick={() => setDeletingTag(tag)}>
<FontAwesomeIcon icon="trash-alt" color="danger" />
<Icon icon="trash-alt" color="danger" />
</Button>
</div>
</div>
@@ -118,13 +114,11 @@ export const TagList: React.FC = () => {
<Button variant="primary" style={{marginTop: "20px"}} onClick={() => setEditingTag({})}>New Tag</Button>
<Modal
onHide={() => {setEditingTag(null)}}
show={!!editingTag}
header={editingTag && editingTag.id ? "Edit Tag" : "New Tag"}
onHide={() => setEditingTag(null)}
accept={{ onClick: onEdit, variant: 'danger', text: (editingTag?.id ? 'Update' : 'Create') }}
>
<Modal.Header>
{ editingTag && editingTag.id ? "Edit Tag" : "New Tag" }
</Modal.Header>
<Modal.Body>
<Form.Group controlId="tag-name">
<Form.Label>Name</Form.Label>
<Form.Control
@@ -132,11 +126,8 @@ export const TagList: React.FC = () => {
defaultValue={(editingTag && editingTag.name) || ''}
/>
</Form.Group>
</Modal.Body>
<Modal.Footer>
<Button onClick={() => onEdit()}>{editingTag && editingTag.id ? "Update" : "Create"}</Button>
</Modal.Footer>
</Modal>
{tagElements}
</div>
);

View File

@@ -1,11 +1,10 @@
import _ from "lodash";
import React, { FunctionComponent, useRef, useState, useEffect } from "react";
import { Link } from "react-router-dom";
import * as GQL from "../../core/generated-graphql";
import { VideoHoverHook } from "../../hooks/VideoHover";
import { TextUtils } from "../../utils/text";
import { NavigationUtils } from "../../utils/navigation";
import { StashService } from "../../core/StashService";
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { VideoHoverHook } from "src/hooks";
import { TextUtils, NavUtils } from "src/utils";
interface IWallItemProps {
scene?: GQL.SlimSceneDataFragment;
@@ -55,31 +54,31 @@ export const WallItem: FunctionComponent<IWallItemProps> = (props: IWallItemProp
}
let linkSrc: string = "#";
if (props.clickHandler === undefined) {
if (props.scene !== undefined) {
if (props.clickHandler) {
if (props.scene) {
linkSrc = `/scenes/${props.scene.id}`;
} else if (props.sceneMarker !== undefined) {
linkSrc = NavigationUtils.makeSceneMarkerUrl(props.sceneMarker);
} else if (props.sceneMarker) {
linkSrc = NavUtils.makeSceneMarkerUrl(props.sceneMarker);
}
}
function onTransitionEnd(event: React.TransitionEvent<HTMLDivElement>) {
const target = (event.target as any);
if (target.classList.contains("double-scale")) {
target.parentElement.style.zIndex = 10;
} else {
target.parentElement.style.zIndex = null;
const target = event.currentTarget;
if (target.classList.contains("double-scale") && target.parentElement) {
target.parentElement.style.zIndex = '10';
} else if(target.parentElement) {
target.parentElement.style.zIndex = '';
}
}
useEffect(() => {
if (!!props.sceneMarker) {
if (props.sceneMarker) {
setPreviewPath(props.sceneMarker.preview);
setTitle(`${props.sceneMarker!.title} - ${TextUtils.secondsToTimestamp(props.sceneMarker.seconds)}`);
const thisTags = props.sceneMarker.tags.map((tag) => (<span key={tag.id}>{tag.name}</span>));
thisTags.unshift(<span key={props.sceneMarker.primary_tag.id}>{props.sceneMarker.primary_tag.name}</span>);
setTags(thisTags);
} else if (!!props.scene) {
} else if (props.scene) {
setPreviewPath(props.scene.paths.webp || "");
setScreenshotPath(props.scene.paths.screenshot || "");
setTitle(props.scene.title || "");
@@ -123,7 +122,7 @@ export const WallItem: FunctionComponent<IWallItemProps> = (props: IWallItemProp
{title}
</div>
{tags}
</div> : undefined
</div> : ''
}
</Link>
</div>

View File

@@ -1,7 +1,7 @@
import React, { FunctionComponent, useState } from "react";
import * as GQL from "../../core/generated-graphql";
import "./Wall.scss";
import * as GQL from "src/core/generated-graphql";
import { WallItem } from "./WallItem";
import "./Wall.scss";
interface IWallPanelProps {
scenes?: GQL.SlimSceneDataFragment[];

View File

@@ -1,16 +1,16 @@
import _ from "lodash";
import React, { useEffect, useRef, useState } from "react";
import { Button, Form, Modal, OverlayTrigger, Tooltip } from 'react-bootstrap'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { CriterionModifier } from "../../core/generated-graphql";
import { Criterion, CriterionType } from "../../models/list-filter/criteria/criterion";
import { NoneCriterion } from "../../models/list-filter/criteria/none";
import { PerformersCriterion } from "../../models/list-filter/criteria/performers";
import { StudiosCriterion } from "../../models/list-filter/criteria/studios";
import { TagsCriterion } from "../../models/list-filter/criteria/tags";
import { makeCriteria } from "../../models/list-filter/criteria/utils";
import { ListFilterModel } from "../../models/list-filter/filter";
import { FilterSelect } from "../select/FilterSelect";
import { Icon } from 'src/components/Shared';
import { CriterionModifier } from "src/core/generated-graphql";
import { Criterion, CriterionType } from "src/models/list-filter/criteria/criterion";
import { NoneCriterion } from "src/models/list-filter/criteria/none";
import { PerformersCriterion } from "src/models/list-filter/criteria/performers";
import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
import { TagsCriterion } from "src/models/list-filter/criteria/tags";
import { makeCriteria } from "src/models/list-filter/criteria/utils";
import { ListFilterModel } from "src/models/list-filter/filter";
import { FilterSelect } from "src/components/Shared";
interface IAddFilterProps {
onAddCriterion: (criterion: Criterion, oldId?: string) => void;
@@ -197,7 +197,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
onClick={() => onToggle()}
active={isOpen}
>
<FontAwesomeIcon icon="filter" />
<Icon icon="filter" />
</Button>
</OverlayTrigger>

View File

@@ -1,11 +1,11 @@
import { debounce } from "lodash";
import React, { SyntheticEvent, useCallback, useState } from "react";
import { Badge, Button, ButtonGroup, Dropdown, Form, OverlayTrigger, Tooltip } from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Criterion } from "../../models/list-filter/criteria/criterion";
import { ListFilterModel } from "../../models/list-filter/filter";
import { DisplayMode } from "../../models/list-filter/types";
import { Icon } from 'src/components/Shared';
import { Criterion } from "src/models/list-filter/criteria/criterion";
import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types";
import { AddFilter } from "./AddFilter";
interface IListFilterOperation {
@@ -114,7 +114,7 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
active={props.filter.displayMode === option}
onClick={() => onChangeDisplayMode(option)}
>
<FontAwesomeIcon icon={getIcon(option)} />
<Icon icon={getIcon(option)} />
</Button>
</OverlayTrigger>
));
@@ -129,7 +129,7 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
>
{criterion.getLabel()}
<Button onClick={() => onRemoveCriterionTag(criterion)}>
<FontAwesomeIcon icon="times" />
<Icon icon="times" />
</Button>
</Badge>
));
@@ -176,7 +176,7 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
<Dropdown>
<Dropdown.Toggle variant="secondary" id="more-menu">
<Button>
<FontAwesomeIcon icon="ellipsis-h" />
<Icon icon="ellipsis-h" />
</Button>
</Dropdown.Toggle>
<Dropdown.Menu>
@@ -240,7 +240,7 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
<Tooltip id="sort-direction-tooltip">{props.filter.sortDirection === "asc" ? "Ascending" : "Descending"}</Tooltip>
}>
<Button onClick={onChangeSortDirection}>
<FontAwesomeIcon icon={props.filter.sortDirection === "asc" ? "caret-up" : "caret-down"} />
<Icon icon={props.filter.sortDirection === "asc" ? "caret-up" : "caret-down"} />
</Button>
</OverlayTrigger>
</ButtonGroup>

View File

@@ -1,9 +1,8 @@
import React from "react";
import { Card } from 'react-bootstrap';
import { Link } from "react-router-dom";
import * as GQL from "../../core/generated-graphql";
import { TextUtils } from "../../utils/text";
import { NavigationUtils } from "../../utils/navigation";
import * as GQL from "src/core/generated-graphql";
import { NavUtils, TextUtils } from "src/utils";
interface IPerformerCardProps {
performer: GQL.PerformerDataFragment;
@@ -12,7 +11,7 @@ interface IPerformerCardProps {
export const PerformerCard: React.FC<IPerformerCardProps> = (props: IPerformerCardProps) => {
const age = TextUtils.age(props.performer.birthdate, props.ageFromDate);
const ageString = `${age} years old${!!props.ageFromDate ? " in this scene." : "."}`;
const ageString = `${age} years old${props.ageFromDate ? " in this scene." : "."}`;
function maybeRenderFavoriteBanner() {
if (props.performer.favorite === false) { return; }
@@ -37,7 +36,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = (props: IPerformerCa
{props.performer.name}
</h4>
{age !== 0 ? <div>{ageString}</div> : ''}
<span>Stars in {props.performer.scene_count} <Link to={NavigationUtils.makePerformerScenesUrl(props.performer)}>scenes</Link>.
<span>Stars in {props.performer.scene_count} <Link to={NavUtils.makePerformerScenesUrl(props.performer)}>scenes</Link>.
</span>
</div>
</Card>

View File

@@ -1,22 +1,18 @@
import _ from "lodash";
import { Button, Form, Modal, Spinner, Table } from 'react-bootstrap';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useEffect, useState } from "react";
import * as GQL from "../../../core/generated-graphql";
import { StashService } from "../../../core/StashService";
import { IBaseProps } from "../../../models";
import { ErrorUtils } from "../../../utils/errors";
import { TableUtils } from "../../../utils/table";
import { ScrapePerformerSuggest } from "../../select/ScrapePerformerSuggest";
import { DetailsEditNavbar } from "../../Shared/DetailsEditNavbar";
import { ToastUtils } from "../../../utils/toasts";
import { EditableTextUtils } from "../../../utils/editabletext";
import { ImageUtils } from "../../../utils/image";
import { Button, Form, Spinner, Table } from 'react-bootstrap';
import _ from "lodash";
import { useParams, useHistory } from 'react-router-dom';
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { DetailsEditNavbar, Icon, Modal, ScrapePerformerSuggest } from "src/components/Shared";
import { ImageUtils, TableUtils } from 'src/utils'
import { useToast } from 'src/hooks';
interface IPerformerProps extends IBaseProps {}
export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) => {
const isNew = props.match.params.id === "new";
export const Performer: React.FC = () => {
const Toast = useToast();
const history = useHistory();
const { id = 'new' } = useParams();
const isNew = id === "new";
// Editing state
const [isEditing, setIsEditing] = useState<boolean>(isNew);
@@ -52,7 +48,7 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
const Scrapers = StashService.useListPerformerScrapers();
const [queryableScrapers, setQueryableScrapers] = useState<GQL.ListPerformerScrapersListPerformerScrapers[]>([]);
const { data, error, loading } = StashService.useFindPerformer(props.match.params.id);
const { data, error, loading } = StashService.useFindPerformer(id);
const updatePerformer = StashService.usePerformerUpdate(getPerformerInput() as GQL.PerformerUpdateInput);
const createPerformer = StashService.usePerformerCreate(getPerformerInput() as GQL.PerformerCreateInput);
const deletePerformer = StashService.usePerformerDestroy(getPerformerInput() as GQL.PerformerDestroyInput);
@@ -80,7 +76,8 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
useEffect(() => {
setIsLoading(loading);
if (!data || !data.findPerformer || !!error) { return; }
if (!data || !data.findPerformer || error)
return;
setPerformer(data.findPerformer);
}, [data]);
@@ -98,7 +95,7 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
setImage(this.result as string);
}
ImageUtils.addPasteImageHook(onImageLoad);
ImageUtils.usePasteImage(onImageLoad);
useEffect(() => {
var newQueryableScrapers : GQL.ListPerformerScrapersListPerformerScrapers[] = [];
@@ -113,10 +110,10 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
}, [Scrapers.data]);
if ((!isNew && !isEditing && (!data || !data.findPerformer)) || isLoading) {
if ((!isNew && !isEditing && !data?.findPerformer) || isLoading)
return <Spinner animation="border" variant="light" />;
}
if (!!error) { return <>error...</>; }
if (error)
return <div>{error.message}</div>;
function getPerformerInput() {
const performerInput: Partial<GQL.PerformerCreateInput | GQL.PerformerUpdateInput> = {
@@ -140,7 +137,7 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
};
if (!isNew) {
(performerInput as GQL.PerformerUpdateInput).id = props.match.params.id;
(performerInput as GQL.PerformerUpdateInput).id = id;
}
return performerInput;
}
@@ -154,10 +151,10 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
} else {
const result = await createPerformer();
setPerformer(result.data.performerCreate);
props.history.push(`/performers/${result.data.performerCreate.id}`);
history.push(`/performers/${result.data.performerCreate.id}`);
}
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
setIsLoading(false);
}
@@ -167,12 +164,12 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
try {
await deletePerformer();
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
setIsLoading(false);
// redirect to performers page
props.history.push(`/performers`);
history.push('/performers');
}
async function onAutoTag() {
@@ -181,13 +178,13 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
}
try {
await StashService.queryMetadataAutoTag({ performers: [performer.id]});
ToastUtils.success("Started auto tagging");
Toast.success({ content: "Started auto tagging" });
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
}
function onImageChange(event: React.FormEvent<HTMLInputElement>) {
function onImageChangeHandler(event: React.FormEvent<HTMLInputElement>) {
ImageUtils.onImageChange(event, onImageLoad);
}
@@ -196,38 +193,39 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
}
function getQueryScraperPerformerInput() {
if (!scrapePerformerDetails) {
if (!scrapePerformerDetails)
return {};
}
let ret = _.clone(scrapePerformerDetails);
delete ret.__typename;
return ret as GQL.ScrapedPerformerInput;
const { __typename, ...ret } = scrapePerformerDetails;
return ret;
}
async function onScrapePerformer() {
setIsDisplayingScraperDialog(undefined);
setIsLoading(true);
try {
if (!scrapePerformerDetails || !isDisplayingScraperDialog) { return; }
if (!scrapePerformerDetails || !isDisplayingScraperDialog)
return;
const result = await StashService.queryScrapePerformer(isDisplayingScraperDialog.id, getQueryScraperPerformerInput());
if (!result.data || !result.data.scrapePerformer) { return; }
if (!result?.data?.scrapePerformer)
return;
updatePerformerEditState(result.data.scrapePerformer);
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
setIsLoading(false);
}
async function onScrapePerformerURL() {
if (!url) { return; }
if (!url)
return;
setIsLoading(true);
try {
const result = await StashService.queryScrapePerformerURL(url);
if (!result.data || !result.data.scrapePerformerURL) { return; }
updatePerformerEditState(result.data.scrapePerformerURL);
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
} finally {
setIsLoading(false);
}
@@ -248,31 +246,24 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
<Modal
show={!!isDisplayingScraperDialog}
onHide={() => setIsDisplayingScraperDialog(undefined)}
header="Scrape"
accept={{ onClick: onScrapePerformer, text: "Scrape" }}
>
<Modal.Header>
Scrape
</Modal.Header>
<Modal.Body>
<div className="dialog-content">
<ScrapePerformerSuggest
placeholder="Performer name"
style={{width: "100%"}}
scraperId={isDisplayingScraperDialog ? isDisplayingScraperDialog.id : ""}
onSelectPerformer={(query) => setScrapePerformerDetails(query)}
/>
</div>
</Modal.Body>
<Modal.Footer>
<Button onClick={() => onScrapePerformer()}>Scrape</Button>
</Modal.Footer>
</Modal>
);
}
function urlScrapable(url: string) : boolean {
return !!url && !!Scrapers.data && Scrapers.data.listPerformerScrapers && Scrapers.data.listPerformerScrapers.some((s) => {
return !!s.performer && !!s.performer.urls && s.performer.urls.some((u) => { return url.includes(u); });
});
function urlScrapable(url: string) {
return !!url && (Scrapers?.data?.listPerformerScrapers ?? []).some(s => (
(s?.performer?.urls ?? []).some(u => url.includes(u))
));
}
function maybeRenderScrapeButton() {
@@ -283,7 +274,7 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
<Button
id="scrape-url-button"
onClick={() => onScrapePerformerURL()}>
<FontAwesomeIcon icon="file-upload" />
<Icon icon="file-upload" />
</Button>
)
}
@@ -296,9 +287,13 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
{maybeRenderScrapeButton()}
</td>
<td>
{EditableTextUtils.renderInputGroup({
value: url, isEditing, onChange: setUrl, placeholder: "URL"
})}
<Form.Control
value={url}
readOnly={!isEditing}
plaintext={!isEditing}
placeholder="URL"
onChange={(event: React.FormEvent<HTMLInputElement>) => setUrl(event.currentTarget.value) }
/>
</td>
</tr>
);
@@ -319,26 +314,31 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
onToggleEdit={() => { setIsEditing(!isEditing); updatePerformerEditState(performer); }}
onSave={onSave}
onDelete={onDelete}
onImageChange={onImageChange}
onImageChange={onImageChangeHandler}
scrapers={queryableScrapers}
onDisplayScraperDialog={onDisplayFreeOnesDialog}
onAutoTag={onAutoTag}
/>
<h1>
{ !isEditing
? <span>{name}</span>
: <Form.Control
{ <Form.Control
readOnly={!isEditing}
plaintext={!isEditing}
defaultValue={name}
placeholder="Name"
onChange={(event: any) => setName(event.target.value)} />
onChange={(event: any) => setName(event.target.value)}
/>
}
</h1>
<h6>
<Form.Group className="aliases-field" controlId="aliases">
<Form.Label>Aliases:</Form.Label>
{EditableTextUtils.renderInputGroup({
value: aliases, isEditing: isEditing, placeholder: "Aliases", onChange: setAliases
})}
<Form.Control
value={aliases}
readOnly={!isEditing}
plaintext={!isEditing}
placeholder="Aliases"
onChange={(event: React.FormEvent<HTMLInputElement>) => setAliases(event.currentTarget.value) }
/>
</Form.Group>
</h6>
<div>
@@ -348,7 +348,7 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
className={favorite ? "favorite" : undefined}
onClick={() => setFavorite(!favorite)}
>
<FontAwesomeIcon icon="heart" />
<Icon icon="heart" />
</Button>
</div>

View File

@@ -1,18 +1,18 @@
import _ from "lodash";
import React, { FunctionComponent } from "react";
import React from "react";
import { QueryHookResult } from "react-apollo-hooks";
import { FindPerformersQuery, FindPerformersVariables } from "../../core/generated-graphql";
import { ListHook } from "../../hooks/ListHook";
import { IBaseProps } from "../../models/base-props";
import { ListFilterModel } from "../../models/list-filter/filter";
import { DisplayMode, FilterMode } from "../../models/list-filter/types";
import { FindPerformersQuery, FindPerformersVariables } from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { ListHook } from "src/hooks";
import { IBaseProps } from "src/models/base-props";
import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode, FilterMode } from "src/models/list-filter/types";
import { PerformerCard } from "./PerformerCard";
import { PerformerListTable } from "./PerformerListTable";
import { StashService } from "../../core/StashService";
interface IPerformerListProps extends IBaseProps {}
export const PerformerList: FunctionComponent<IPerformerListProps> = (props: IPerformerListProps) => {
export const PerformerList: React.FC<IPerformerListProps> = (props: IPerformerListProps) => {
const otherOperations = [
{
text: "Open Random",

View File

@@ -1,9 +1,9 @@
import React from "react";
import { Button, Table } from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Link } from "react-router-dom";
import * as GQL from "../../core/generated-graphql";
import { NavigationUtils } from "../../utils/navigation";
import * as GQL from "src/core/generated-graphql";
import { Icon } from 'src/components/Shared';
import { NavUtils } from "src/utils";
interface IPerformerListTableProps {
performers: GQL.PerformerDataFragment[];
@@ -15,7 +15,7 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (props: IP
if (!performer.favorite) { return; }
return (
<Button disabled className="favorite">
<FontAwesomeIcon icon="heart" />
<Icon icon="heart" />
</Button>
);
}
@@ -59,7 +59,7 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (props: IP
{maybeRenderFavoriteHeart(performer)}
</td>
<td>
<Link to={NavigationUtils.makePerformerScenesUrl(performer)}>
<Link to={NavUtils.makePerformerScenesUrl(performer)}>
<h6>{performer.scene_count}</h6>
</Link>
</td>

View File

@@ -1,14 +1,12 @@
import { Button, ButtonGroup, Card, Form, Popover, OverlayTrigger } from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React, { useState } from "react";
import { Button, ButtonGroup, Card, Form, Popover, OverlayTrigger } from 'react-bootstrap';
import { Link } from "react-router-dom";
import * as GQL from "../../core/generated-graphql";
import { VideoHoverHook } from "../../hooks/VideoHover";
import { ColorUtils } from "../../utils/color";
import { TextUtils } from "../../utils/text";
import { TagLink } from "../Shared/TagLink";
import { ZoomUtils } from "../../utils/zoom";
import { StashService } from "../../core/StashService";
import cx from 'classnames';
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { VideoHoverHook } from "src/hooks";
import { Icon, TagLink } from 'src/components/Shared';
import { TextUtils } from "src/utils";
interface ISceneCardProps {
scene: GQL.SlimSceneDataFragment;
@@ -27,7 +25,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
function maybeRenderRatingBanner() {
if (!props.scene.rating) { return; }
return (
<div className={`rating-banner ${ColorUtils.classForRating(props.scene.rating)}`}>
<div className={`rating-banner ${props.scene.rating ? `rating-${props.scene.rating}` : '' }`}>
RATING: {props.scene.rating}
</div>
);
@@ -36,23 +34,21 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
function maybeRenderSceneSpecsOverlay() {
return (
<div className={`scene-specs-overlay`}>
{!!props.scene.file.height ? <span className={`overlay-resolution`}> {TextUtils.resolution(props.scene.file.height)}</span> : undefined}
{props.scene.file.duration !== undefined && props.scene.file.duration >= 1 ? TextUtils.secondsToTimestamp(props.scene.file.duration) : ""}
{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) : ''}
</div>
);
}
function maybeRenderSceneStudioOverlay() {
if (!props.scene.studio) {
if (!props.scene.studio)
return;
}
let style: React.CSSProperties = {
backgroundImage: `url('${props.scene.studio.image_path}')`,
};
let text = "";
if (showStudioAsText) {
style = {};
text = props.scene.studio.name;
@@ -71,7 +67,8 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
}
function maybeRenderTagPopoverButton() {
if (props.scene.tags.length <= 0) { return; }
if (props.scene.tags.length <= 0)
return;
const popover = (
<Popover id="tag-popover">
@@ -84,7 +81,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
return (
<OverlayTrigger trigger="hover" placement="bottom" overlay={popover}>
<Button>
<FontAwesomeIcon icon="tag" />
<Icon icon="tag" />
{props.scene.tags.length}
</Button>
</OverlayTrigger>
@@ -92,7 +89,8 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
}
function maybeRenderPerformerPopoverButton() {
if (props.scene.performers.length <= 0) { return; }
if (props.scene.performers.length <= 0)
return;
const popover = (
<Popover id="performer-popover">
@@ -116,7 +114,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
return (
<OverlayTrigger trigger="hover" placement="bottom" overlay={popover}>
<Button>
<FontAwesomeIcon icon="user" />
<Icon icon="user" />
{props.scene.performers.length}
</Button>
</OverlayTrigger>
@@ -140,7 +138,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
return (
<OverlayTrigger trigger="hover" placement="bottom" overlay={popover}>
<Button>
<FontAwesomeIcon icon="tag" />
<Icon icon="tag" />
{props.scene.scene_markers.length}
</Button>
</OverlayTrigger>
@@ -182,31 +180,11 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
return height > width;
}
function getLinkClassName() {
let ret = "image previewable";
if (isPortrait()) {
ret += " portrait";
}
return ret;
}
function getVideoClassName() {
let ret = "preview";
if (isPortrait()) {
ret += " portrait";
}
return ret;
}
var shiftKey = false;
return (
<Card
className={"col-4 " + ZoomUtils.classForZoom(props.zoomIndex)}
className={`col-4 zoom-${props.zoomIndex}`}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
@@ -217,13 +195,18 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
onChange={() => props.onSelectedChanged(!props.selected, shiftKey)}
onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => { shiftKey = event.shiftKey; event.stopPropagation(); } }
/>
<Link to={`/scenes/${props.scene.id}`} className={getLinkClassName()}>
<Link to={`/scenes/${props.scene.id}`} className={cx('image', 'previewable', {portrait: isPortrait()})}>
<div className="video-container">
{maybeRenderRatingBanner()}
{maybeRenderSceneSpecsOverlay()}
{maybeRenderSceneStudioOverlay()}
<video className={getVideoClassName()} loop={true} poster={props.scene.paths.screenshot || ""} ref={videoHoverHook.videoEl}>
{!!previewPath ? <source src={previewPath} /> : ""}
<video
loop
className={cx('preview', {portrait: isPortrait()})}
poster={props.scene.paths.screenshot || ""}
ref={videoHoverHook.videoEl}
>
{previewPath ? <source src={previewPath} /> : ""}
</video>
</div>
</Link>

View File

@@ -1,10 +1,10 @@
import { Card, Spinner, Tab, Tabs } from 'react-bootstrap';
import queryString from "query-string";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as GQL from "../../../core/generated-graphql";
import { StashService } from "../../../core/StashService";
import { IBaseProps } from "../../../models";
import { GalleryViewer } from "../../Galleries/GalleryViewer";
import React, { useEffect, useState } from "react";
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { IBaseProps } from "src/models";
import { GalleryViewer } from "src/components/Galleries/GalleryViewer";
import { ScenePlayer } from "../ScenePlayer/ScenePlayer";
import { SceneDetailPanel } from "./SceneDetailPanel";
import { SceneEditPanel } from "./SceneEditPanel";
@@ -14,18 +14,15 @@ import { ScenePerformerPanel } from "./ScenePerformerPanel";
interface ISceneProps extends IBaseProps {}
export const Scene: FunctionComponent<ISceneProps> = (props: ISceneProps) => {
export const Scene: React.FC<ISceneProps> = (props: ISceneProps) => {
const [timestamp, setTimestamp] = useState<number>(0);
const [autoplay, setAutoplay] = useState<boolean>(false);
const [scene, setScene] = useState<Partial<GQL.SceneDataFragment>>({});
const [isLoading, setIsLoading] = useState(false);
const { data, error, loading } = StashService.useFindScene(props.match.params.id);
useEffect(() => {
setIsLoading(loading);
if (!data || !data.findScene || !!error) { return; }
setScene(StashService.nullToUndefined(data.findScene));
}, [data]);
useEffect(() => (
setScene(data?.findScene ?? {})
), [data]);
useEffect(() => {
const queryParams = queryString.parse(props.location.search);
@@ -42,12 +39,15 @@ export const Scene: FunctionComponent<ISceneProps> = (props: ISceneProps) => {
setTimestamp(marker.seconds);
}
if (!data || !data.findScene || isLoading || Object.keys(scene).length === 0) {
if (!data?.findScene || loading || Object.keys(scene).length === 0) {
return <Spinner animation="border"/>;
}
if (error)
return <div>{error.message}</div>
const modifiedScene =
Object.assign({scene_marker_tags: data.sceneMarkerTags}, scene) as GQL.SceneDataFragment; // TODO Hack from angular
if (!!error) { return <>error...</>; }
return (
<>

View File

@@ -1,7 +1,7 @@
import React from "react";
import * as GQL from "../../../core/generated-graphql";
import { TextUtils } from "../../../utils/text";
import { TagLink } from "../../Shared/TagLink";
import * as GQL from "src/core/generated-graphql";
import { TextUtils } from "src/utils";
import { TagLink } from "src/components/Shared";
import { SceneHelpers } from "../helpers";
interface ISceneDetailProps {
@@ -38,9 +38,9 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props: ISceneDetai
<h1>
{!!props.scene.title ? props.scene.title : TextUtils.fileNameFromPath(props.scene.path)}
</h1>
{!!props.scene.date ? <h4>{props.scene.date}</h4> : undefined}
{!!props.scene.rating ? <h6>Rating: {props.scene.rating}</h6> : undefined}
{!!props.scene.file.height ? <h6>Resolution: {TextUtils.resolution(props.scene.file.height)}</h6> : undefined}
{props.scene.date ? <h4>{props.scene.date}</h4> : ''}
{props.scene.rating ? <h6>Rating: {props.scene.rating}</h6> : ''}
{props.scene.file.height ? <h6>Resolution: {TextUtils.resolution(props.scene.file.height)}</h6> : ''}
{renderDetails()}
{renderTags()}
</>

View File

@@ -1,13 +1,10 @@
import React, { useEffect, useState } from "react";
import * as GQL from "../../../core/generated-graphql";
import { StashService } from "../../../core/StashService";
import { ErrorUtils } from "../../../utils/errors";
import { ToastUtils } from "../../../utils/toasts";
import { FilterSelect, StudioSelect } from "../../select/FilterSelect";
import { ValidGalleriesSelect } from "../../select/ValidGalleriesSelect";
import { ImageUtils } from "../../../utils/image";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Collapse, Dropdown, DropdownButton, Form, Button, Modal, Spinner } from 'react-bootstrap';
import { Collapse, Dropdown, DropdownButton, Form, Button, Spinner } from 'react-bootstrap';
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { FilterSelect, StudioSelect, SceneGallerySelect, Modal, Icon } from "src/components/Shared";
import { useToast } from 'src/hooks';
import { ImageUtils } from 'src/utils';
interface IProps {
scene: GQL.SceneDataFragment;
@@ -16,6 +13,7 @@ interface IProps {
}
export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
const Toast = useToast();
const [title, setTitle] = useState<string | undefined>(undefined);
const [details, setDetails] = useState<string | undefined>(undefined);
const [url, setUrl] = useState<string | undefined>(undefined);
@@ -76,7 +74,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
setCoverImagePreview(props.scene.paths.screenshot);
}, [props.scene]);
ImageUtils.addPasteImageHook(onImageLoad);
ImageUtils.usePasteImage(onImageLoad);
function getSceneInput(): GQL.SceneUpdateInput {
return {
@@ -99,9 +97,9 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
try {
const result = await updateScene();
props.onUpdate(result.data.sceneUpdate);
ToastUtils.success("Updated scene");
Toast.success({ content: "Updated scene" });
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
setIsLoading(false);
}
@@ -119,9 +117,9 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
setIsLoading(true);
try {
await deleteScene();
ToastUtils.success("Deleted scene");
Toast.success({ content: "Deleted scene" });
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
setIsLoading(false);
@@ -148,15 +146,12 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
function renderDeleteAlert() {
return (
<Modal
keyboard={false}
onHide={() => {}}
show={isDeleteAlertOpen}
icon="trash-alt"
header="Delete Scene?"
accept={{ variant: 'danger', onClick: onDelete, text: "Delete" }}
cancel={{ onClick: () => setIsDeleteAlertOpen(false), text: "Cancel" }}
>
<Modal.Header>
<FontAwesomeIcon icon="trash-alt" />
<span>Delete Scene?</span>
</Modal.Header>
<Modal.Body>
<p>
Are you sure you want to delete this scene? Unless the file is also deleted, this scene will be re-added when scan is performed.
</p>
@@ -164,14 +159,6 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
<Form.Check checked={deleteFile} label="Delete file" onChange={() => setDeleteFile(!deleteFile)} />
<Form.Check checked={deleteGenerated} label="Delete generated supporting files" onChange={() => setDeleteGenerated(!deleteGenerated)} />
</Form>
</Modal.Body>
<Modal.Footer>
<div>
<Button variant="danger" onClick={onDelete}>Delete</Button>
<Button onClick={() => setIsDeleteAlertOpen(false)}>Cancel</Button>
</div>
</Modal.Footer>
</Modal>
);
}
@@ -192,7 +179,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
if (!result.data || !result.data.scrapeScene) { return; }
updateSceneFromScrapedScene(result.data.scrapeScene);
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
} finally {
setIsLoading(false);
}
@@ -271,7 +258,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
if (!result.data || !result.data.scrapeSceneURL) { return; }
updateSceneFromScrapedScene(result.data.scrapeSceneURL);
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
} finally {
setIsLoading(false);
}
@@ -285,7 +272,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
<Button
id="scrape-url-button"
onClick={onScrapeSceneURL}>
<FontAwesomeIcon icon="file-download" />
<Icon icon="file-download" />
</Button>
)
}
@@ -343,10 +330,10 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
<Form.Group controlId="gallery">
<Form.Label>Gallery</Form.Label>
<ValidGalleriesSelect
<SceneGallerySelect
sceneId={props.scene.id}
initialId={galleryId}
onSelectItem={(item) => setGalleryId(item ? item.id : undefined)}
onSelect={(item) => setGalleryId(item ? item.id : undefined)}
/>
</Form.Group>
@@ -370,7 +357,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
<div>
<label onClick={() => setIsCoverImageOpen(!isCoverImageOpen)}>
<FontAwesomeIcon icon={isCoverImageOpen ? "chevron-down" : "chevron-right"} />
<Icon icon={isCoverImageOpen ? "chevron-down" : "chevron-right"} />
<span>Cover Image</span>
</label>
<Collapse in={isCoverImageOpen}>

View File

@@ -1,13 +1,13 @@
import React from "react";
import { Table } from 'react-bootstrap';
import React, { FunctionComponent } from "react";
import * as GQL from "../../../core/generated-graphql";
import { TextUtils } from "../../../utils/text";
import * as GQL from "src/core/generated-graphql";
import { TextUtils } from "src/utils";
interface ISceneFileInfoPanelProps {
scene: GQL.SceneDataFragment;
}
export const SceneFileInfoPanel: FunctionComponent<ISceneFileInfoPanelProps> = (props: ISceneFileInfoPanelProps) => {
export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (props: ISceneFileInfoPanelProps) => {
function renderChecksum() {
return (
<tr>

View File

@@ -1,15 +1,13 @@
import React, { CSSProperties, useState } from "react";
import { Badge, Button, Card, Collapse, Form as BootstrapForm } from 'react-bootstrap';
import { Field, FieldProps, Form, Formik, FormikActions, FormikProps } from "formik";
import React, { CSSProperties, FunctionComponent, useState } from "react";
import * as GQL from "../../../core/generated-graphql";
import { StashService } from "../../../core/StashService";
import { TextUtils } from "../../../utils/text";
import { TagSelect } from "../../select/FilterSelect";
import { MarkerTitleSuggest } from "../../select/MarkerTitleSuggest";
import { WallPanel } from "../../Wall/WallPanel";
import { Field, FieldProps, Form, Formik, FormikActions } from "formik";
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { TextUtils } from "src/utils";
import { useToast } from 'src/hooks';
import { DurationInput, TagSelect, MarkerTitleSuggest } from "src/components/Shared";
import { WallPanel } from "src/components/Wall/WallPanel";
import { SceneHelpers } from "../helpers";
import { ErrorUtils } from "../../../utils/errors";
import { DurationInput } from "../../Shared/DurationInput";
interface ISceneMarkersPanelProps {
scene: GQL.SceneDataFragment;
@@ -23,7 +21,8 @@ interface IFormFields {
tagIds: string[];
}
export const SceneMarkersPanel: FunctionComponent<ISceneMarkersPanelProps> = (props: ISceneMarkersPanelProps) => {
export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISceneMarkersPanelProps) => {
const Toast = useToast();
const [isEditorOpen, setIsEditorOpen] = useState<boolean>(false);
const [editingMarker, setEditingMarker] = useState<GQL.SceneMarkerDataFragment | null>(null);
@@ -33,9 +32,9 @@ export const SceneMarkersPanel: FunctionComponent<ISceneMarkersPanelProps> = (pr
const jwplayer = SceneHelpers.getPlayer();
function onOpenEditor(marker: GQL.SceneMarkerDataFragment | null = null) {
function onOpenEditor(marker?: GQL.SceneMarkerDataFragment) {
setIsEditorOpen(true);
setEditingMarker(marker);
setEditingMarker(marker ?? null);
}
function onClickMarker(marker: GQL.SceneMarkerDataFragment) {
@@ -104,17 +103,17 @@ export const SceneMarkersPanel: FunctionComponent<ISceneMarkersPanelProps> = (pr
tag_ids: values.tagIds,
};
if (!isEditing) {
sceneMarkerCreate({ variables }).then((response) => {
sceneMarkerCreate({ variables }).then(() => {
setIsEditorOpen(false);
setEditingMarker(null);
}).catch((err) => ErrorUtils.handleApolloError(err));
}).catch((err) => Toast.error(err));
} else {
const updateVariables = variables as GQL.SceneMarkerUpdateVariables;
updateVariables.id = editingMarker!.id;
sceneMarkerUpdate({ variables: updateVariables }).then((response) => {
sceneMarkerUpdate({ variables: updateVariables }).then(() => {
setIsEditorOpen(false);
setEditingMarker(null);
}).catch((err) => ErrorUtils.handleApolloError(err));
}).catch((err) => Toast.error(err));
}
}
function onDelete() {
@@ -128,12 +127,8 @@ export const SceneMarkersPanel: FunctionComponent<ISceneMarkersPanelProps> = (pr
function renderTitleField(fieldProps: FieldProps<IFormFields>) {
return (
<MarkerTitleSuggest
initialMarkerString={!!editingMarker ? editingMarker.title : undefined}
placeholder="Title"
name={fieldProps.field.name}
onBlur={fieldProps.field.onBlur}
value={fieldProps.field.value}
onQueryChange={(query) => fieldProps.form.setFieldValue("title", query)}
initialMarkerTitle={editingMarker?.title}
onChange={(query:string) => fieldProps.form.setFieldValue("title", query)}
/>
);
}
@@ -163,9 +158,9 @@ export const SceneMarkersPanel: FunctionComponent<ISceneMarkersPanelProps> = (pr
/>
);
}
function renderFormFields(formikProps: FormikProps<IFormFields>) {
function renderFormFields() {
let deleteButton: JSX.Element | undefined;
if (!!editingMarker) {
if (editingMarker) {
deleteButton = (
<Button
variant="danger"
@@ -205,7 +200,7 @@ export const SceneMarkersPanel: FunctionComponent<ISceneMarkersPanelProps> = (pr
);
}
let initialValues: any;
if (!!editingMarker) {
if (editingMarker) {
initialValues = {
title: editingMarker.title,
seconds: editingMarker.seconds,

View File

@@ -1,6 +1,6 @@
import React, { FunctionComponent } from "react";
import * as GQL from "../../../core/generated-graphql";
import { PerformerCard } from "../../performers/PerformerCard";
import * as GQL from "src/core/generated-graphql";
import { PerformerCard } from "src/components/performers/PerformerCard";
interface IScenePerformerPanelProps {
scene: GQL.SceneDataFragment;

View File

@@ -1,15 +1,13 @@
import { Badge, Button, Card, Collapse, Dropdown, DropdownButton, Form, Table, Spinner } from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React, { useEffect, useState } from "react";
import { StashService } from "../../core/StashService";
import * as GQL from "../../core/generated-graphql";
import { SlimSceneDataFragment, Maybe } from "../../core/generated-graphql";
import { TextUtils } from "../../utils/text";
import { Badge, Button, Card, Collapse, Dropdown, DropdownButton, Form, Table, Spinner } from 'react-bootstrap';
import _ from "lodash";
import { ToastUtils } from "../../utils/toasts";
import { ErrorUtils } from "../../utils/errors";
import { StashService } from "src/core/StashService";
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 { TextUtils } from "src/utils";
import { useToast } from "src/hooks";
import { Pagination } from "../list/Pagination";
import { FilterSelect, StudioSelect } from "../select/FilterSelect";
class ParserResult<T> {
public value: Maybe<T>;
@@ -22,7 +20,7 @@ class ParserResult<T> {
}
public setValue(v : Maybe<T>) {
if (!!v) {
if (v) {
this.value = v;
this.set = !_.isEqual(this.value, this.originalValue);
}
@@ -258,6 +256,7 @@ const builtInRecipes = [
];
export const SceneFilenameParser: React.FC = () => {
const Toast = useToast();
const [parserResult, setParserResult] = useState<SceneParserResult[]>([]);
const [parserInput, setParserInput] = useState<IParserInput>(initialParserInput());
@@ -325,12 +324,12 @@ export const SceneFilenameParser: React.FC = () => {
const response = await StashService.queryParseSceneFilenames(getParserFilter(), getParserInput());
let result = response.data.parseSceneFilenames;
if (!!result) {
if (result) {
parseResults(result.results);
setTotalItems(result.count);
}
} catch (err) {
ErrorUtils.handle(err);
Toast.error(err);
}
setIsLoading(false);
@@ -373,9 +372,9 @@ export const SceneFilenameParser: React.FC = () => {
try {
await updateScenes();
ToastUtils.success("Updated scenes");
Toast.success({ content: "Updated scenes" });
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
setIsLoading(false);
@@ -518,7 +517,7 @@ export const SceneFilenameParser: React.FC = () => {
const fieldRows = [...props.fields.entries()].map(([label, enabled]) => (
<div key={label} onClick={() => {handleClick(label)}}>
<FontAwesomeIcon icon={enabled ? "check" : "times" } />
<Icon icon={enabled ? "check" : "times" } />
<span>{label}</span>
</div>
));
@@ -526,7 +525,7 @@ export const SceneFilenameParser: React.FC = () => {
return (
<div>
<div onClick={() => setOpen(!open)}>
<FontAwesomeIcon icon={open ? "chevron-down" : "chevron-right" } />
<Icon icon={open ? "chevron-down" : "chevron-right" } />
<span>Display fields</span>
</div>
<Collapse in={open}>

View File

@@ -1,16 +1,16 @@
import _ from "lodash";
import React from "react";
import _ from "lodash";
import { QueryHookResult } from "react-apollo-hooks";
import { FindScenesQuery, FindScenesVariables, SlimSceneDataFragment } from "../../core/generated-graphql";
import { ListHook } from "../../hooks/ListHook";
import { IBaseProps } from "../../models/base-props";
import { ListFilterModel } from "../../models/list-filter/filter";
import { DisplayMode, FilterMode } from "../../models/list-filter/types";
import { FindScenesQuery, FindScenesVariables, SlimSceneDataFragment } from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { ListHook } from "src/hooks";
import { IBaseProps } from "src/models/base-props";
import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode, FilterMode } from "src/models/list-filter/types";
import { WallPanel } from "../Wall/WallPanel";
import { SceneCard } from "./SceneCard";
import { SceneListTable } from "./SceneListTable";
import { SceneSelectedOptions } from "./SceneSelectedOptions";
import { StashService } from "../../core/StashService";
interface ISceneListProps extends IBaseProps {}
@@ -31,7 +31,7 @@ export const SceneList: React.FC<ISceneListProps> = (props: ISceneListProps) =>
renderSelectedOptions
});
async function playRandom(result: QueryHookResult<FindScenesQuery, FindScenesVariables>, filter: ListFilterModel, selectedIds: Set<string>) {
async function playRandom(result: QueryHookResult<FindScenesQuery, FindScenesVariables>, filter: ListFilterModel) {
// query for a random scene
if (result.data && result.data.findScenes) {
let count = result.data.findScenes.count;

View File

@@ -1,10 +1,8 @@
import React from "react";
import { Link } from "react-router-dom";
import * as GQL from "../../core/generated-graphql";
import { TextUtils } from "../../utils/text";
import { NavigationUtils } from "../../utils/navigation";
import { Table } from 'react-bootstrap';
import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql";
import { NavUtils, TextUtils } from "src/utils";
interface ISceneListTableProps {
scenes: GQL.SlimSceneDataFragment[];
@@ -37,7 +35,7 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneList
function renderTags(tags : GQL.SlimSceneDataTags[]) {
return tags.map((tag) => (
<Link to={NavigationUtils.makeTagScenesUrl(tag)}>
<Link to={NavUtils.makeTagScenesUrl(tag)}>
<h6>{tag.name}</h6>
</Link>
));
@@ -45,16 +43,16 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneList
function renderPerformers(performers : GQL.SlimSceneDataPerformers[]) {
return performers.map((performer) => (
<Link to={NavigationUtils.makePerformerScenesUrl(performer)}>
<Link to={NavUtils.makePerformerScenesUrl(performer)}>
<h6>{performer.name}</h6>
</Link>
));
}
function renderStudio(studio : GQL.SlimSceneDataStudio | undefined) {
if (!!studio) {
if (studio) {
return (
<Link to={NavigationUtils.makeStudioScenesUrl(studio)}>
<Link to={NavUtils.makeStudioScenesUrl(studio)}>
<h6>{studio.name}</h6>
</Link>
);
@@ -63,7 +61,6 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneList
function renderSceneRow(scene : GQL.SlimSceneDataFragment) {
return (
<>
<tr>
<td>
{renderSceneImage(scene)}
@@ -71,7 +68,7 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneList
<td style={{textAlign: "left"}}>
<Link to={`/scenes/${scene.id}`}>
<h5 className="text-truncate">
{!!scene.title ? scene.title : TextUtils.fileNameFromPath(scene.path)}
{scene.title ?? TextUtils.fileNameFromPath(scene.path)}
</h5>
</Link>
</td>
@@ -91,12 +88,10 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneList
{renderStudio(scene.studio)}
</td>
</tr>
</>
)
}
return (
<>
<div className="grid">
<Table striped bordered>
<thead>
@@ -115,7 +110,6 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneList
</tbody>
</Table>
</div>
</>
);
};

View File

@@ -1,24 +1,22 @@
import _ from "lodash";
import React, { FunctionComponent } from "react";
import React from "react";
import { QueryHookResult } from "react-apollo-hooks";
import { FindSceneMarkersQuery, FindSceneMarkersVariables } from "../../core/generated-graphql";
import { ListHook } from "../../hooks/ListHook";
import { IBaseProps } from "../../models/base-props";
import { ListFilterModel } from "../../models/list-filter/filter";
import { DisplayMode, FilterMode } from "../../models/list-filter/types";
import { FindSceneMarkersQuery, FindSceneMarkersVariables } from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { NavUtils } from "src/utils";
import { ListHook } from "src/hooks";
import { IBaseProps } from "src/models/base-props";
import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode, FilterMode } from "src/models/list-filter/types";
import { WallPanel } from "../Wall/WallPanel";
import { StashService } from "../../core/StashService";
import { NavigationUtils } from "../../utils/navigation";
interface IProps extends IBaseProps {}
export const SceneMarkerList: FunctionComponent<IProps> = (props: IProps) => {
const otherOperations = [
{
export const SceneMarkerList: React.FC<IProps> = (props: IProps) => {
const otherOperations = [{
text: "Play Random",
onClick: playRandom,
}
];
onClick: playRandom
}];
const listData = ListHook.useList({
filterMode: FilterMode.SceneMarkers,
@@ -27,7 +25,7 @@ export const SceneMarkerList: FunctionComponent<IProps> = (props: IProps) => {
renderContent,
});
async function playRandom(result: QueryHookResult<FindSceneMarkersQuery, FindSceneMarkersVariables>, filter: ListFilterModel, selectedIds: Set<string>) {
async function playRandom(result: QueryHookResult<FindSceneMarkersQuery, FindSceneMarkersVariables>, filter: ListFilterModel) {
// query for a random scene
if (result.data && result.data.findSceneMarkers) {
let count = result.data.findSceneMarkers.count;
@@ -39,7 +37,7 @@ export const SceneMarkerList: FunctionComponent<IProps> = (props: IProps) => {
const singleResult = await StashService.queryFindSceneMarkers(filterCopy);
if (singleResult && singleResult.data && singleResult.data.findSceneMarkers && singleResult.data.findSceneMarkers.scene_markers.length === 1) {
// navigate to the scene player page
let url = NavigationUtils.makeSceneMarkerUrl(singleResult!.data!.findSceneMarkers!.scene_markers[0])
let url = NavUtils.makeSceneMarkerUrl(singleResult.data.findSceneMarkers.scene_markers[0])
props.history.push(url);
}
}
@@ -49,7 +47,8 @@ export const SceneMarkerList: FunctionComponent<IProps> = (props: IProps) => {
result: QueryHookResult<FindSceneMarkersQuery, FindSceneMarkersVariables>,
filter: ListFilterModel,
) {
if (!result.data || !result.data.findSceneMarkers) { return; }
if (!result?.data?.findSceneMarkers)
return;
if (filter.displayMode === DisplayMode.Wall) {
return <WallPanel sceneMarkers={result.data.findSceneMarkers.scene_markers} />;
}

View File

@@ -1,10 +1,10 @@
import React from "react";
import ReactJWPlayer from "react-jw-player";
import { HotKeys } from "react-hotkeys";
import * as GQL from "../../../core/generated-graphql";
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { SceneHelpers } from "../helpers";
import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
import { StashService } from "../../../core/StashService";
interface IScenePlayerProps {
scene: GQL.SceneDataFragment;

View File

@@ -1,7 +1,7 @@
import axios from "axios";
import React, { CSSProperties, useEffect, useRef, useState } from "react";
import * as GQL from "../../../core/generated-graphql";
import { TextUtils } from "../../../utils/text";
import axios from "axios";
import * as GQL from "src/core/generated-graphql";
import { TextUtils } from "src/utils";
import "./ScenePlayerScrubber.scss";
interface IScenePlayerScrubberProps {

View File

@@ -1,11 +1,10 @@
import _ from "lodash";
import { Button, ButtonGroup, Form, Spinner } from 'react-bootstrap';
import React, { useEffect, useState } from "react";
import { FilterSelect, StudioSelect } from "../select/FilterSelect";
import { StashService } from "../../core/StashService";
import * as GQL from "../../core/generated-graphql";
import { ErrorUtils } from "../../utils/errors";
import { ToastUtils } from "../../utils/toasts";
import { Button, ButtonGroup, Form, Spinner } from 'react-bootstrap';
import _ from "lodash";
import { StashService } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql";
import { FilterSelect, StudioSelect } from "src/components/Shared";
import { useToast } from "src/hooks";
interface IListOperationProps {
selected: GQL.SlimSceneDataFragment[],
@@ -13,6 +12,7 @@ interface IListOperationProps {
}
export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IListOperationProps) => {
const Toast = useToast();
const [rating, setRating] = useState<string>("");
const [studioId, setStudioId] = useState<string | undefined>(undefined);
const [performerIds, setPerformerIds] = useState<string[] | undefined>(undefined);
@@ -93,9 +93,9 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
setIsLoading(true);
try {
await updateScenes();
ToastUtils.success("Updated scenes");
Toast.success({ content: "Updated scenes" });
} catch (e) {
ErrorUtils.handle(e);
Toast.error(e);
}
setIsLoading(false);
props.onScenesUpdated();

View File

@@ -1,6 +1,6 @@
import React, { } from "react";
import { Link } from "react-router-dom";
import * as GQL from "../../core/generated-graphql";
import * as GQL from "src/core/generated-graphql";
export class SceneHelpers {
public static maybeRenderStudio(

View File

@@ -1,190 +0,0 @@
import * as React from "react";
import { MenuItem } from "@blueprintjs/core";
import { IMultiSelectProps, ItemPredicate, ItemRenderer, MultiSelect } from "@blueprintjs/select";
import * as GQL from "../../core/generated-graphql";
import { StashService } from "../../core/StashService";
import { HTMLInputProps } from "../../models";
import { ErrorUtils } from "../../utils/errors";
import { ToastUtils } from "../../utils/toasts";
const InternalPerformerMultiSelect = MultiSelect.ofType<GQL.AllPerformersForFilterAllPerformers>();
const InternalTagMultiSelect = MultiSelect.ofType<GQL.AllTagsForFilterAllTags>();
const InternalStudioMultiSelect = MultiSelect.ofType<GQL.AllStudiosForFilterAllStudios>();
type ValidTypes =
GQL.AllPerformersForFilterAllPerformers |
GQL.AllTagsForFilterAllTags |
GQL.AllStudiosForFilterAllStudios;
interface IProps extends HTMLInputProps, Partial<IMultiSelectProps<ValidTypes>> {
type: "performers" | "studios" | "tags";
initialIds?: string[];
onUpdate: (items: ValidTypes[]) => void;
}
export const FilterMultiSelect: React.FunctionComponent<IProps> = (props: IProps) => {
let MultiSelectImpl = getMultiSelectImpl();
let InternalMultiSelect = MultiSelectImpl.getInternalMultiSelect();
const data = MultiSelectImpl.getData();
const [selectedItems, setSelectedItems] = React.useState<ValidTypes[]>([]);
const [items, setItems] = React.useState<ValidTypes[]>([]);
const [newTagName, setNewTagName] = React.useState<string>("");
const createTag = StashService.useTagCreate(getTagInput() as GQL.TagCreateInput);
React.useEffect(() => {
if (!!data) {
MultiSelectImpl.translateData();
}
}, [data]);
function getTagInput() {
const tagInput: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = { name: newTagName };
return tagInput;
}
async function onCreateNewObject(item: ValidTypes) {
var created : any;
if (props.type === "tags") {
try {
created = await createTag();
items.push(created.data.tagCreate);
setItems(items.slice());
addSelectedItem(created.data.tagCreate);
ToastUtils.success("Created tag");
} catch (e) {
ErrorUtils.handle(e);
}
}
}
function createNewTag(query : string) {
setNewTagName(query);
return {
name : query
};
}
function createNewRenderer(query: string, active: boolean, handleClick: React.MouseEventHandler<HTMLElement>) {
// if tag already exists with that name, then don't return anything
if (items.find((item) => {
return item.name === query;
})) {
return undefined;
}
return (
<MenuItem
icon="add"
text={`Create "${query}"`}
active={active}
onClick={handleClick}
/>
);
}
React.useEffect(() => {
if (!!props.initialIds && !!items) {
const initialItems = items.filter((item) => props.initialIds!.includes(item.id));
setSelectedItems(initialItems);
}
}, [props.initialIds, items]);
function getMultiSelectImpl() {
let getInternalMultiSelect: () => new (props: IMultiSelectProps<any>) => MultiSelect<any>;
let getData: () => GQL.AllPerformersForFilterQuery | GQL.AllStudiosForFilterQuery | GQL.AllTagsForFilterQuery | undefined;
let translateData: () => void;
let createNewObject: ((query : string) => void) | undefined = undefined;
switch (props.type) {
case "performers": {
getInternalMultiSelect = () => { return InternalPerformerMultiSelect; };
getData = () => { const { data } = StashService.useAllPerformersForFilter(); return data; }
translateData = () => { let perfData = data as GQL.AllPerformersForFilterQuery; setItems(!!perfData && !!perfData.allPerformers ? perfData.allPerformers : []); };
break;
}
case "studios": {
getInternalMultiSelect = () => { return InternalStudioMultiSelect; };
getData = () => { const { data } = StashService.useAllStudiosForFilter(); return data; }
translateData = () => { let studioData = data as GQL.AllStudiosForFilterQuery; setItems(!!studioData && !!studioData.allStudios ? studioData.allStudios : []); };
break;
}
case "tags": {
getInternalMultiSelect = () => { return InternalTagMultiSelect; };
getData = () => { const { data } = StashService.useAllTagsForFilter(); return data; }
translateData = () => { let tagData = data as GQL.AllTagsForFilterQuery; setItems(!!tagData && !!tagData.allTags ? tagData.allTags : []); };
createNewObject = createNewTag;
break;
}
default: {
throw "Unhandled case in FilterMultiSelect";
}
}
return {
getInternalMultiSelect: getInternalMultiSelect,
getData: getData,
translateData: translateData,
createNewObject: createNewObject
};
}
const renderItem: ItemRenderer<ValidTypes> = (item, itemProps) => {
if (!itemProps.modifiers.matchesPredicate) { return null; }
return (
<MenuItem
active={itemProps.modifiers.active}
disabled={itemProps.modifiers.disabled}
key={item.id}
onClick={itemProps.handleClick}
text={item.name}
/>
);
};
const filter: ItemPredicate<ValidTypes> = (query, item) => {
if (selectedItems.includes(item)) { return false; }
return item.name!.toLowerCase().indexOf(query.toLowerCase()) >= 0;
};
function addSelectedItem(item: ValidTypes) {
selectedItems.push(item);
setSelectedItems(selectedItems);
props.onUpdate(selectedItems);
}
function onItemSelect(item: ValidTypes) {
if (item.id === undefined) {
// create the new item, if applicable
onCreateNewObject(item);
} else {
addSelectedItem(item);
}
}
function onItemRemove(value: string, index: number) {
const newSelectedItems = selectedItems.filter((_, i) => i !== index);
setSelectedItems(newSelectedItems);
props.onUpdate(newSelectedItems);
}
return (
<InternalMultiSelect
items={items}
selectedItems={selectedItems}
itemRenderer={renderItem}
itemPredicate={filter}
tagRenderer={(tag) => tag.name}
tagInputProps={{ onRemove: onItemRemove }}
onItemSelect={onItemSelect}
resetOnSelect={true}
popoverProps={{position: "bottom"}}
createNewItemFromQuery={MultiSelectImpl.createNewObject}
createNewItemRenderer={createNewRenderer}
{...props}
/>
);
};

View File

@@ -1,61 +0,0 @@
import * as React from "react";
import { MenuItem } from "@blueprintjs/core";
import { ItemPredicate, ItemRenderer, Suggest } from "@blueprintjs/select";
import * as GQL from "../../core/generated-graphql";
import { StashService } from "../../core/StashService";
import { HTMLInputProps } from "../../models";
const InternalSuggest = Suggest.ofType<GQL.MarkerStringsMarkerStrings>();
interface IProps extends HTMLInputProps {
initialMarkerString?: string;
onQueryChange: (query: string) => void;
}
export const MarkerTitleSuggest: React.FunctionComponent<IProps> = (props: IProps) => {
const { data } = StashService.useMarkerStrings();
const markerStrings = !!data && !!data.markerStrings ? data.markerStrings : [];
const [selectedItem, setSelectedItem] = React.useState<GQL.MarkerStringsMarkerStrings | null>(null);
if (!!props.initialMarkerString && !selectedItem) {
const initialItem = markerStrings.find((item) => {
return props.initialMarkerString!.toLowerCase() === item!.title.toLowerCase();
});
if (!!initialItem) { setSelectedItem(initialItem); }
}
const renderInputValue = (markerString: GQL.MarkerStringsMarkerStrings) => markerString.title;
const renderItem: ItemRenderer<GQL.MarkerStringsMarkerStrings> = (markerString, itemProps) => {
if (!itemProps.modifiers.matchesPredicate) { return null; }
return (
<MenuItem
active={itemProps.modifiers.active}
disabled={itemProps.modifiers.disabled}
label={markerString.count.toString()}
key={markerString.id}
onClick={itemProps.handleClick}
text={markerString.title}
/>
);
};
const filter: ItemPredicate<GQL.MarkerStringsMarkerStrings> = (query, item) => {
return item.title.toLowerCase().indexOf(query.toLowerCase()) >= 0;
};
return (
<InternalSuggest
inputValueRenderer={renderInputValue}
items={markerStrings as any}
itemRenderer={renderItem}
itemPredicate={filter}
onItemSelect={(item) => { props.onQueryChange(item.title); setSelectedItem(item); }}
onQueryChange={(query) => { props.onQueryChange(query); setSelectedItem(null); }}
activeItem={null}
selectedItem={selectedItem}
popoverProps={{position: "bottom"}}
/>
);
};

View File

@@ -1,74 +0,0 @@
import * as React from "react";
import { MenuItem } from "@blueprintjs/core";
import { ItemRenderer, Suggest } from "@blueprintjs/select";
import * as GQL from "../../core/generated-graphql";
import { StashService } from "../../core/StashService";
import { HTMLInputProps } from "../../models";
const InternalSuggest = Suggest.ofType<GQL.ScrapePerformerListScrapePerformerList>();
interface IProps extends HTMLInputProps {
scraperId: string;
onSelectPerformer: (query: GQL.ScrapePerformerListScrapePerformerList) => void;
}
export const ScrapePerformerSuggest: React.FunctionComponent<IProps> = (props: IProps) => {
const [query, setQuery] = React.useState<string>("");
const [selectedItem, setSelectedItem] = React.useState<GQL.ScrapePerformerListScrapePerformerList | undefined>();
const [debouncedQuery, setDebouncedQuery] = React.useState<string>("");
const { data, error, loading } = StashService.useScrapePerformerList(props.scraperId, debouncedQuery);
React.useEffect(() => {
const handler = setTimeout(() => {
setDebouncedQuery(query);
}, 500);
return () => {
clearTimeout(handler);
};
}, [query])
const performerNames = !!data && !!data.scrapePerformerList ? data.scrapePerformerList : [];
const renderInputValue = (performer: GQL.ScrapePerformerListScrapePerformerList) => performer.name || "";
const renderItem: ItemRenderer<GQL.ScrapePerformerListScrapePerformerList> = (performer, itemProps) => {
if (!itemProps.modifiers.matchesPredicate) { return null; }
return (
<MenuItem
active={itemProps.modifiers.active}
disabled={itemProps.modifiers.disabled}
key={performer.name}
onClick={itemProps.handleClick}
text={performer.name}
/>
);
};
function renderLoadingError() {
if (error) {
return (<MenuItem disabled={true} text={error.toString()} />);
}
if (loading) {
return (<MenuItem disabled={true} text="Loading..." />);
}
if (debouncedQuery && data && !!data.scrapePerformerList && data.scrapePerformerList.length === 0) {
return (<MenuItem disabled={true} text="No results" />);
}
}
return (
<InternalSuggest
inputValueRenderer={renderInputValue}
items={performerNames}
itemRenderer={renderItem}
onItemSelect={(item) => { props.onSelectPerformer(item); setSelectedItem(item); }}
onQueryChange={(newQuery) => { setQuery(newQuery); }}
activeItem={null}
selectedItem={selectedItem}
noResults={renderLoadingError()}
popoverProps={{position: "bottom"}}
/>
);
};

View File

@@ -1,75 +0,0 @@
import * as React from "react";
import { Button, MenuItem } from "@blueprintjs/core";
import { ItemPredicate, ItemRenderer, Select } from "@blueprintjs/select";
import * as GQL from "../../core/generated-graphql";
import { StashService } from "../../core/StashService";
import { HTMLInputProps } from "../../models";
const InternalSelect = Select.ofType<GQL.ValidGalleriesForSceneValidGalleriesForScene>();
interface IProps extends HTMLInputProps {
initialId?: string;
sceneId: string;
onSelectItem: (item: GQL.ValidGalleriesForSceneValidGalleriesForScene | undefined) => void;
}
export const ValidGalleriesSelect: React.FunctionComponent<IProps> = (props: IProps) => {
const { data } = StashService.useValidGalleriesForScene(props.sceneId);
const items = !!data && !!data.validGalleriesForScene ? data.validGalleriesForScene : [];
// Add a none option to clear the gallery
if (!items.find((item) => item.id === "0")) { items.unshift({id: "0", path: "None"}); }
const [selectedItem, setSelectedItem] = React.useState<GQL.ValidGalleriesForSceneValidGalleriesForScene | undefined>(undefined);
const [isInitialized, setIsInitialized] = React.useState<boolean>(false);
if (!!props.initialId && !selectedItem && !isInitialized) {
const initialItem = items.find((item) => props.initialId === item.id);
if (!!initialItem) {
setSelectedItem(initialItem);
setIsInitialized(true);
}
}
const renderItem: ItemRenderer<GQL.ValidGalleriesForSceneValidGalleriesForScene> = (item, itemProps) => {
if (!itemProps.modifiers.matchesPredicate) { return null; }
return (
<MenuItem
active={itemProps.modifiers.active}
disabled={itemProps.modifiers.disabled}
key={item.id}
onClick={itemProps.handleClick}
text={item.path}
shouldDismissPopover={false}
/>
);
};
const filter: ItemPredicate<GQL.ValidGalleriesForSceneValidGalleriesForScene> = (query, item) => {
return item.path!.toLowerCase().indexOf(query.toLowerCase()) >= 0;
};
function onItemSelect(item: GQL.ValidGalleriesForSceneValidGalleriesForScene | undefined) {
if (item && item.id === "0") {
item = undefined;
}
props.onSelectItem(item);
setSelectedItem(item);
}
const buttonText = selectedItem ? selectedItem.path : "(No selection)";
return (
<InternalSelect
items={items}
itemRenderer={renderItem}
itemPredicate={filter}
noResults={<MenuItem disabled={true} text="No results." />}
onItemSelect={onItemSelect}
popoverProps={{position: "bottom"}}
{...props}
>
<Button fill={true} text={buttonText} />
</InternalSelect>
);
};

View File

@@ -2,10 +2,9 @@ import ApolloClient from "apollo-client";
import { WebSocketLink } from 'apollo-link-ws';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink, split } from "apollo-boost";
import _ from "lodash";
import { getMainDefinition } from "apollo-utilities";
import { ListFilterModel } from "../models/list-filter/filter";
import * as GQL from "./generated-graphql";
import { getMainDefinition } from "apollo-utilities";
export class StashService {
public static client: ApolloClient<any>;
@@ -228,7 +227,7 @@ export class StashService {
return GQL.useListPerformerScrapers();
}
public static useScrapePerformerList(scraperId: string, q : string) {
return GQL.useScrapePerformerList({ variables: { scraper_id: scraperId, query: q }});
return GQL.useScrapePerformerList({ variables: { scraper_id: scraperId, query: q }, skip: q === ''});
}
public static useScrapePerformer(scraperId: string, scrapedPerformer : GQL.ScrapedPerformerInput) {
return GQL.useScrapePerformer({ variables: { scraper_id: scraperId, scraped_performer: scrapedPerformer }});
@@ -520,18 +519,5 @@ export class StashService {
});
}
public static nullToUndefined(value: any): any {
if (_.isPlainObject(value)) {
return _.mapValues(value, StashService.nullToUndefined);
}
if (Array.isArray(value)) {
return value.map(StashService.nullToUndefined);
}
if (value === null) {
return undefined;
}
return value;
}
private constructor() {}
}

View File

@@ -1,7 +1,7 @@
import { Spinner } from "@blueprintjs/core";
import _ from "lodash";
import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { Spinner } from 'react-bootstrap';
import { QueryHookResult } from "react-apollo-hooks";
import { ListFilter } from "../components/list/ListFilter";
import { Pagination } from "../components/list/Pagination";
@@ -293,7 +293,7 @@ export class ListHook {
filter={filter}
/>
{options.renderSelectedOptions && selectedIds.size > 0 ? options.renderSelectedOptions(result, selectedIds) : undefined}
{result.loading ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}
{result.loading ? <Spinner animation="border" variant="light" /> : undefined}
{result.error ? <h1>{result.error.message}</h1> : undefined}
{options.renderContent(result, filter, selectedIds, zoomIndex)}
<Pagination

View File

@@ -5,6 +5,7 @@ interface IToast {
header?: string;
content: JSX.Element|string;
delay?: number;
variant?: 'success'|'danger'|'warning'|'info';
}
interface IActiveToast extends IToast {
id: number;
@@ -21,7 +22,13 @@ export const ToastProvider: React.FC = ({children}) => {
);
const toastItems = toasts.map(toast => (
<Toast key={toast.id} onClose={() => removeToast(toast.id)} autohide delay={toast.delay ?? 5000}>
<Toast
autohide
key={toast.id}
onClose={() => removeToast(toast.id)}
className={toast.variant ?? 'success'}
delay={toast.delay ?? 5000}
>
<Toast.Header>
<span className="mr-auto">
{ toast.header ?? 'Stash' }
@@ -47,7 +54,17 @@ export const ToastProvider: React.FC = ({children}) => {
const useToasts = () => {
const setToast = useContext(ToastContext);
return setToast;
return {
success: setToast,
error: (error: Error) => {
console.error(error.message);
setToast({
variant: 'danger',
header: 'Error',
content: error.message ?? error.toString()
});
}
};
}
export default useToasts;

View File

@@ -0,0 +1,4 @@
export { default as useToast } from './Toast';
export { useInterfaceLocalForage } from './LocalForage';
export { VideoHoverHook } from './VideoHover';
export { ListHook } from './ListHook';

View File

@@ -4,7 +4,6 @@
@import "styles/shared/details";
@import "styles/blueprint-overrides";
@import "styles/scrollbars";
@import "styles/variables";

View File

@@ -1,4 +1,4 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import { CriterionModifier } from "src/core/generated-graphql";
import { ILabeledId, ILabeledValue } from "../types";
export type CriterionType =

View File

@@ -1,4 +1,4 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import { CriterionModifier } from "src/core/generated-graphql";
import {
Criterion,
CriterionType,

View File

@@ -1,4 +1,4 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import { CriterionModifier } from "src/core/generated-graphql";
import {
Criterion,
CriterionType,

View File

@@ -1,4 +1,4 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import { CriterionModifier } from "src/core/generated-graphql";
import {
Criterion,
CriterionType,

View File

@@ -1,4 +1,4 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import { CriterionModifier } from "src/core/generated-graphql";
import {
Criterion,
CriterionType,

View File

@@ -1,4 +1,4 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import { CriterionModifier } from "src/core/generated-graphql";
import { ILabeledId } from "../types";
import {
Criterion,

View File

@@ -1,4 +1,4 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import { CriterionModifier } from "src/core/generated-graphql";
import {
Criterion,
CriterionType,

View File

@@ -1,4 +1,4 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import { CriterionModifier } from "src/core/generated-graphql";
import {
Criterion,
CriterionType,

View File

@@ -1,4 +1,4 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import { CriterionModifier } from "src/core/generated-graphql";
import { ILabeledId } from "../types";
import {
Criterion,

View File

@@ -1,5 +1,5 @@
import * as GQL from "../../../core/generated-graphql";
import { CriterionModifier } from "../../../core/generated-graphql";
import * as GQL from "src/core/generated-graphql";
import { CriterionModifier } from "src/core/generated-graphql";
import { ILabeledId } from "../types";
import {
Criterion,

View File

@@ -1,6 +1,6 @@
import {
CriterionModifier,
} from "../../../core/generated-graphql";
} from "src/core/generated-graphql";
import { Criterion, CriterionType, StringCriterion, NumberCriterion } from "./criterion";
import { FavoriteCriterion } from "./favorite";
import { HasMarkersCriterion } from "./has-markers";

View File

@@ -6,7 +6,7 @@ import {
SceneFilterType,
SceneMarkerFilterType,
SortDirectionEnum,
} from "../../core/generated-graphql";
} from "src/core/generated-graphql";
import { Criterion, ICriterionOption, CriterionType, CriterionOption, NumberCriterion, StringCriterion } from "./criteria/criterion";
import { FavoriteCriterion, FavoriteCriterionOption } from "./criteria/favorite";
import { HasMarkersCriterion, HasMarkersCriterionOption } from "./criteria/has-markers";

View File

@@ -1,30 +0,0 @@
.bp3-popover-content {
padding: 10px;
max-width: 33vw;
min-width: 200px;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.bp3-select-popover .bp3-menu {
max-width: 400px;
max-height: 300px;
overflow: auto;
padding: 0;
}
.bp3-multi-select-popover .bp3-menu {
max-width: 400px;
max-height: 300px;
overflow: auto;
padding: 0;
}
.filter-container .bp3-portal {
position: relative;
}
.card-section.centered .bp3-popover-wrapper {
flex: unset !important;
}

View File

@@ -1,12 +0,0 @@
export class ColorUtils {
public static classForRating(rating: number): string {
switch (rating) {
case 5: return "rating-5";
case 4: return "rating-4";
case 3: return "rating-3";
case 2: return "rating-2";
case 1: return "rating-1";
default: return "";
}
}
}

View File

@@ -1,71 +0,0 @@
import { HTMLSelect, InputGroup, IOptionProps, TextArea, Label } from "@blueprintjs/core";
import React from "react";
export class EditableTextUtils {
public static renderTextArea(options: {
value: string | undefined,
isEditing: boolean,
onChange: ((value: string) => void)
}) {
let element: JSX.Element;
if (options.isEditing) {
element = (
<TextArea
fill={true}
onChange={(newValue) => options.onChange(newValue.target.value)}
value={options.value}
/>
);
} else {
element = <p className="pre">{options.value}</p>;
}
return element;
}
public static renderInputGroup(options: {
value: string | undefined,
isEditing: boolean,
placeholder?: string,
onChange: ((value: string) => void),
}) {
let element: JSX.Element;
if (options.isEditing) {
element = (
<InputGroup
onChange={(newValue: any) => options.onChange(newValue.target.value)}
value={options.value}
placeholder={options.placeholder}
/>
);
} else {
element = <Label>{options.value}</Label>;
}
return element;
}
public static renderHtmlSelect(options: {
value: string | number | undefined,
isEditing: boolean,
onChange: ((value: string) => void),
selectOptions: Array<string | number | IOptionProps>,
}) {
let stringValue = options.value;
if (typeof stringValue === "number") {
stringValue = stringValue.toString();
}
let element: JSX.Element;
if (options.isEditing) {
element = (
<HTMLSelect
options={options.selectOptions}
onChange={(event) => options.onChange(event.target.value)}
value={stringValue}
/>
);
} else {
element = <span>{options.value}</span>;
}
return element;
}
}

View File

@@ -1,24 +0,0 @@
import { Intent, Position, Toaster } from "@blueprintjs/core";
import { ApolloError } from "apollo-boost";
const toaster = Toaster.create({
position: Position.TOP,
});
export class ErrorUtils {
public static handle(error: any) {
console.error(error);
toaster.show({
message: error.toString(),
intent: Intent.DANGER,
});
}
public static handleApolloError(error: ApolloError) {
console.error(error);
toaster.show({
message: error.message,
intent: Intent.DANGER,
});
}
}

View File

@@ -1,34 +1,37 @@
import React, { useEffect } from "react";
export class ImageUtils {
private static readImage(file: File, onLoadEnd: (this: FileReader) => any) {
const readImage = (file: File, onLoadEnd: (this: FileReader) => void) => {
const reader: FileReader = new FileReader();
reader.onloadend = onLoadEnd;
reader.readAsDataURL(file);
}
public static onImageChange(event: React.FormEvent<HTMLInputElement>, onLoadEnd: (this: FileReader) => any) {
const file: File = (event.target as any).files[0];
ImageUtils.readImage(file, onLoadEnd);
}
public static pasteImage(e : any, onLoadEnd: (this: FileReader) => any) {
if (e.clipboardData.files.length === 0) {
const pasteImage = (event: ClipboardEvent, onLoadEnd: (this: FileReader) => void) => {
const files = event?.clipboardData?.files;
if(!files?.length)
return;
const file = files[0];
readImage(file, onLoadEnd);
}
const file: File = e.clipboardData.files[0];
ImageUtils.readImage(file, onLoadEnd);
const onImageChange = (event: React.FormEvent<HTMLInputElement>, onLoadEnd: (this: FileReader) => void) => {
const file = event?.currentTarget?.files?.[0];
if(file)
readImage(file, onLoadEnd);
}
public static addPasteImageHook(onLoadEnd: (this: FileReader) => any) {
const usePasteImage = (onLoadEnd: (this: FileReader) => void) => {
useEffect(() => {
const pasteImage = (e: any) => { ImageUtils.pasteImage(e, onLoadEnd) }
window.addEventListener("paste", pasteImage);
const paste = (event: ClipboardEvent) => ( pasteImage(event, onLoadEnd) );
document.addEventListener("paste", paste);
return () => window.removeEventListener("paste", pasteImage);
return () => document.removeEventListener("paste", paste);
});
}
const Image = {
onImageChange,
usePasteImage
}
export default Image;

View File

@@ -0,0 +1,4 @@
export { default as ImageUtils } from './image';
export { default as NavUtils } from './navigation';
export { default as TableUtils } from './table';
export { default as TextUtils } from './text';

View File

@@ -5,9 +5,9 @@ import { TagsCriterion } from "../models/list-filter/criteria/tags";
import { ListFilterModel } from "../models/list-filter/filter";
import { FilterMode } from "../models/list-filter/types";
export class NavigationUtils {
public static makePerformerScenesUrl(performer: Partial<GQL.PerformerDataFragment>): string {
if (performer.id === undefined) { return "#"; }
const makePerformerScenesUrl = (performer: Partial<GQL.PerformerDataFragment>) => {
if (!performer.id)
return "#";
const filter = new ListFilterModel(FilterMode.Scenes);
const criterion = new PerformersCriterion();
criterion.value = [{ id: performer.id, label: performer.name || `Performer ${performer.id}` }];
@@ -15,8 +15,9 @@ export class NavigationUtils {
return `/scenes?${filter.makeQueryParameters()}`;
}
public static makeStudioScenesUrl(studio: Partial<GQL.StudioDataFragment>): string {
if (studio.id === undefined) { return "#"; }
const makeStudioScenesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
if (!studio.id)
return "#";
const filter = new ListFilterModel(FilterMode.Scenes);
const criterion = new StudiosCriterion();
criterion.value = [{ id: studio.id, label: studio.name || `Studio ${studio.id}` }];
@@ -24,8 +25,9 @@ export class NavigationUtils {
return `/scenes?${filter.makeQueryParameters()}`;
}
public static makeTagScenesUrl(tag: Partial<GQL.TagDataFragment>): string {
if (tag.id === undefined) { return "#"; }
const makeTagScenesUrl = (tag: Partial<GQL.TagDataFragment>) => {
if (!tag.id)
return "#";
const filter = new ListFilterModel(FilterMode.Scenes);
const criterion = new TagsCriterion("tags");
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
@@ -33,8 +35,9 @@ export class NavigationUtils {
return `/scenes?${filter.makeQueryParameters()}`;
}
public static makeTagSceneMarkersUrl(tag: Partial<GQL.TagDataFragment>): string {
if (tag.id === undefined) { return "#"; }
const makeTagSceneMarkersUrl = (tag: Partial<GQL.TagDataFragment>) => {
if (!tag.id)
return "#";
const filter = new ListFilterModel(FilterMode.SceneMarkers);
const criterion = new TagsCriterion("tags");
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
@@ -42,8 +45,17 @@ export class NavigationUtils {
return `/scenes/markers?${filter.makeQueryParameters()}`;
}
public static makeSceneMarkerUrl(sceneMarker: Partial<GQL.SceneMarkerDataFragment>): string {
if (sceneMarker.id === undefined || sceneMarker.scene === undefined) { return "#"; }
const makeSceneMarkerUrl = (sceneMarker: Partial<GQL.SceneMarkerDataFragment>) => {
if (!sceneMarker.id || !sceneMarker.scene)
return "#";
return `/scenes/${sceneMarker.scene.id}?t=${sceneMarker.seconds}`;
}
const Nav = {
makePerformerScenesUrl,
makeStudioScenesUrl,
makeTagSceneMarkersUrl,
makeTagScenesUrl,
makeSceneMarkerUrl
}
export default Nav;

View File

@@ -1,105 +1,95 @@
import { EditableText, IOptionProps } from "@blueprintjs/core";
import { Form } from 'react-bootstrap';
import React from "react";
import { EditableTextUtils } from "./editabletext";
import { FilterSelect } from "../components/select/FilterSelect";
import _ from "lodash";
import { Form } from 'react-bootstrap';
import { FilterSelect } from "src/components/Shared";
export class TableUtils {
public static renderEditableTextTableRow(options: {
const renderEditableTextTableRow = (options: {
title: string;
value: string | number | undefined;
value?: string | number;
isEditing: boolean;
onChange: ((value: string) => void);
}) {
let stringValue = options.value;
if (typeof stringValue === "number") {
stringValue = stringValue.toString();
}
return (
}) => (
<tr>
<td>{options.title}</td>
<td>
<EditableText
disabled={!options.isEditing}
value={stringValue}
<Form.Control
readOnly={!options.isEditing}
plaintext={!options.isEditing}
onChange={(event: React.FormEvent<HTMLInputElement>) => ( options.onChange(event.currentTarget.value) )}
value={typeof options.value === 'number' ? options.value.toString() : options.value}
placeholder={options.title}
multiline={true}
onChange={(newValue) => options.onChange(newValue)}
/>
</td>
</tr>
);
}
)
public static renderTextArea(options: {
const renderTextArea = (options: {
title: string,
value: string | undefined,
isEditing: boolean,
onChange: ((value: string) => void),
}) {
return (
}) => (
<tr>
<td>{options.title}</td>
<td>
{EditableTextUtils.renderTextArea(options)}
<Form.Control
as="textarea"
readOnly={!options.isEditing}
plaintext={!options.isEditing}
onChange={(event: React.FormEvent<HTMLTextAreaElement>) => ( options.onChange(event.currentTarget.value) )}
value={options.value}
/>
</td>
</tr>
);
}
)
public static renderInputGroup(options: {
const renderInputGroup = (options: {
title: string,
placeholder?: string,
value: string | undefined,
isEditing: boolean,
onChange: ((value: string) => void),
}) {
let optionsCopy = _.clone(options);
optionsCopy.placeholder = options.placeholder || options.title;
return (
}) => (
<tr>
<td>{options.title}</td>
<td>
{ !options.isEditing
? <h4>{optionsCopy.value}</h4>
: <Form.Control
<Form.Control
readOnly={!options.isEditing}
plaintext={!options.isEditing}
defaultValue={options.value}
placeholder={optionsCopy.placeholder}
onChange={ (event:any) => options.onChange(event.target.value) }
placeholder={options.placeholder ?? options.title}
onChange={(event: React.FormEvent<HTMLInputElement>) => ( options.onChange(event.currentTarget.value) )}
/>
}
</td>
</tr>
);
}
)
public static renderHtmlSelect(options: {
const renderHtmlSelect = (options: {
title: string,
value: string | number | undefined,
value?: string | number,
isEditing: boolean,
onChange: ((value: string) => void),
selectOptions: Array<string | number | IOptionProps>,
}) {
return (
selectOptions: Array<string | number>,
}) => (
<tr>
<td>{options.title}</td>
<td>
{EditableTextUtils.renderHtmlSelect(options)}
<Form.Control
as="select"
readOnly={!options.isEditing}
plaintext={!options.isEditing}
onChange={(event: React.FormEvent<HTMLSelectElement>) => ( options.onChange(event.currentTarget.value) )}
/>
</td>
</tr>
);
}
)
// TODO: isediting
public static renderFilterSelect(options: {
const renderFilterSelect = (options: {
title: string,
type: "performers" | "studios" | "tags",
initialId: string | undefined,
onChange: ((id: string | undefined) => void),
}) {
return (
}) => (
<tr>
<td>{options.title}</td>
<td>
@@ -110,17 +100,15 @@ export class TableUtils {
/>
</td>
</tr>
);
}
)
// TODO: isediting
public static renderMultiSelect(options: {
const renderMultiSelect = (options: {
title: string,
type: "performers" | "studios" | "tags",
initialIds: string[] | undefined,
onChange: ((ids: string[]) => void),
}) {
return (
}) => (
<tr>
<td>{options.title}</td>
<td>
@@ -132,6 +120,14 @@ export class TableUtils {
/>
</td>
</tr>
);
}
)
const Table = {
renderEditableTextTableRow,
renderTextArea,
renderInputGroup,
renderHtmlSelect,
renderFilterSelect,
renderMultiSelect
}
export default Table;

View File

@@ -1,12 +1,23 @@
export class TextUtils {
const Units = [
"bytes",
"kB",
"MB",
"GB",
"TB",
"PB",
];
public static truncate(value?: string, limit: number = 100, tail: string = "..."): string {
if (!value) { return ""; }
return value.length > limit ? value.substring(0, limit) + tail : value;
const truncate = (value?: string, limit: number = 100, tail: string = "...") => {
if (!value)
return "";
return value.length > limit
? value.substring(0, limit) + tail
: value;
}
public static fileSize(bytes: number = 0, precision: number = 2): string {
if (isNaN(parseFloat(String(bytes))) || !isFinite(bytes)) { return "?"; }
const fileSize = (bytes: number = 0, precision: number = 2) => {
if (Number.isNaN(parseFloat(String(bytes))) || !isFinite(bytes))
return "?";
let unit = 0;
while ( bytes >= 1024 ) {
@@ -14,10 +25,10 @@ export class TextUtils {
unit++;
}
return bytes.toFixed(+precision) + " " + this.units[unit];
return `${bytes.toFixed(+precision)} ${Units[unit]}`;
}
public static secondsToTimestamp(seconds: number): string {
const secondsToTimestamp = (seconds: number) => {
let ret = new Date(seconds * 1000).toISOString().substr(11, 8);
if (ret.startsWith("00")) {
@@ -31,16 +42,18 @@ export class TextUtils {
return ret;
}
public static fileNameFromPath(path: string): string {
if (!!path === false) { return "No File Name"; }
return path.replace(/^.*[\\\/]/, "");
const fileNameFromPath = (path: string) => {
if (!!path === false)
return "No File Name";
return path.replace(/^.*[\\/]/, "");
}
public static age(dateString?: string, fromDateString?: string): number {
if (!dateString) { return 0; }
const age = (dateString?: string, fromDateString?: string) => {
if (!dateString)
return 0;
const birthdate = new Date(dateString);
const fromDate = !!fromDateString ? new Date(fromDateString) : new Date();
const fromDate = fromDateString ? new Date(fromDateString) : new Date();
let age = fromDate.getFullYear() - birthdate.getFullYear();
if (birthdate.getMonth() > fromDate.getMonth() ||
@@ -51,12 +64,12 @@ export class TextUtils {
return age;
}
public static bitRate(bitrate: number) {
const bitRate = (bitrate: number) => {
const megabits = bitrate / 1000000;
return `${megabits.toFixed(2)} megabits per second`;
}
public static resolution(height: number) {
const resolution = (height: number) => {
if (height >= 240 && height < 480) {
return "240p";
} else if (height >= 480 && height < 720) {
@@ -72,12 +85,14 @@ export class TextUtils {
}
}
private static units = [
"bytes",
"kB",
"MB",
"GB",
"TB",
"PB",
];
const TextUtils = {
truncate,
fileSize,
secondsToTimestamp,
fileNameFromPath,
age,
bitRate,
resolution
}
export default TextUtils;

View File

@@ -1,14 +0,0 @@
import { Intent, Position, Toaster } from "@blueprintjs/core";
const toaster = Toaster.create({
position: Position.TOP,
});
export class ToastUtils {
public static success(message: string) {
toaster.show({
message,
intent: Intent.SUCCESS,
});
}
}

View File

@@ -1,6 +0,0 @@
export class ZoomUtils {
public static classForZoom(zoomIndex: number): string {
return "zoom-" + zoomIndex;
}
}

View File

@@ -18,11 +18,11 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"downlevelIteration": true,
"experimentalDecorators": true
"experimentalDecorators": true,
"baseUrl": "."
},
"include": [
"src"
"src/**/*"
]
}

View File

@@ -953,57 +953,6 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@blueprintjs/core@3.22.1":
version "3.22.1"
resolved "https://registry.yarnpkg.com/@blueprintjs/core/-/core-3.22.1.tgz#2fd1e985c46440f16dbe7136f563c4e833e2f34d"
integrity sha512-egM1jyE+O4X/HvgCDZ96DDvwIlC/z9J1vBVYgnZmeGHPhjCOnmc8bRqlifmPMTBwO6xl/1H67gCp/GEhyvu0EA==
dependencies:
"@blueprintjs/icons" "^3.12.0"
"@types/dom4" "^2.0.1"
classnames "^2.2"
dom4 "^2.1.5"
normalize.css "^8.0.1"
popper.js "^1.15.0"
react-lifecycles-compat "^3.0.4"
react-popper "^1.3.3"
react-transition-group "^2.9.0"
resize-observer-polyfill "^1.5.1"
tslib "~1.9.0"
"@blueprintjs/core@^3.20.0":
version "3.22.3"
resolved "https://registry.yarnpkg.com/@blueprintjs/core/-/core-3.22.3.tgz#57dc2c072a17db0e52cc5679d8bbc016082b27e7"
integrity sha512-IaxkvJyF+4VCvAjMvyHtJ4qUiQNwgPu4zIxLRo1cBsu30gHjYzwe+xDdssBR7yRnXARguM6bkI523w+yJOerCA==
dependencies:
"@blueprintjs/icons" "^3.12.0"
"@types/dom4" "^2.0.1"
classnames "^2.2"
dom4 "^2.1.5"
normalize.css "^8.0.1"
popper.js "^1.15.0"
react-lifecycles-compat "^3.0.4"
react-popper "^1.3.7"
react-transition-group "^2.9.0"
resize-observer-polyfill "^1.5.1"
tslib "~1.9.0"
"@blueprintjs/icons@^3.12.0":
version "3.13.0"
resolved "https://registry.yarnpkg.com/@blueprintjs/icons/-/icons-3.13.0.tgz#bff93a9ea5ced03afd2b3a65ddf4bb90bda485e4"
integrity sha512-fvXGsAJ66SSjeHv3OeXjLEdKdPJ3wVztjhJQCAd51uebhj3FJ16EDDvO7BqBw5FyVkLkU11KAxSoCFZt7TC9GA==
dependencies:
classnames "^2.2"
tslib "~1.9.0"
"@blueprintjs/select@3.11.2":
version "3.11.2"
resolved "https://registry.yarnpkg.com/@blueprintjs/select/-/select-3.11.2.tgz#3324db0de44a9f386b957aac1ba3ec774b3eb1e7"
integrity sha512-fU0Km6QI/ayWhzYeu9N1gTj0+L0XUO4KB3u2LfJXgj648UGY8F4HX2ETdJ+XPdtsu6TesrIL7ghMQhtLcvafBg==
dependencies:
"@blueprintjs/core" "^3.20.0"
classnames "^2.2"
tslib "~1.9.0"
"@cnakazawa/watch@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
@@ -1483,10 +1432,10 @@
dependencies:
"@types/babel-types" "*"
"@types/dom4@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/dom4/-/dom4-2.0.1.tgz#506d5781b9bcab81bd9a878b198aec7dee2a6033"
integrity sha512-kSkVAvWmMZiCYtvqjqQEwOmvKwcH+V4uiv3qPQ8pAh1Xl39xggGEo8gHUqV4waYGHezdFw0rKBR8Jt0CrQSDZA==
"@types/classnames@^2.2.9":
version "2.2.9"
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.9.tgz#d868b6febb02666330410fe7f58f3c4b8258be7b"
integrity sha512-MNl+rT5UmZeilaPxAVs6YaPC2m6aA8rofviZbhbxpPpl61uKodfdQVsBtgJGTqGizEf02oW3tsVe7FYB8kK14A==
"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
@@ -3209,7 +3158,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classnames@^2.2, classnames@^2.2.6:
classnames@^2.2.6:
version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
@@ -3647,14 +3596,6 @@ create-react-context@^0.2.2:
fbjs "^0.8.0"
gud "^1.0.0"
create-react-context@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.3.0.tgz#546dede9dc422def0d3fc2fe03afe0bc0f4f7d8c"
integrity sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==
dependencies:
gud "^1.0.0"
warning "^4.0.3"
cross-fetch@2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-2.2.2.tgz#a47ff4f7fc712daba8f6a695a11c948440d45723"
@@ -4001,7 +3942,7 @@ decode-uri-component@^0.2.0:
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
deep-equal@^1.0.1, deep-equal@^1.1.1:
deep-equal@^1.0.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==
@@ -4242,11 +4183,6 @@ dom-walk@^0.1.0:
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
integrity sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=
dom4@^2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/dom4/-/dom4-2.1.5.tgz#f98a94eb67b340f0fa5b42b0ee9c38cda035428e"
integrity sha512-gJbnVGq5zaBUY0lUh0LUEVGYrtN75Ks8ZwpwOYvnVFrKy/qzXK4R/1WuLIFExWj/tBxbRAkTzZUGJHXmqsBNjQ==
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
@@ -9006,7 +8942,7 @@ pnp-webpack-plugin@1.5.0:
dependencies:
ts-pnp "^1.1.2"
popper.js@^1.14.4, popper.js@^1.15.0, popper.js@^1.16.0:
popper.js@^1.15.0, popper.js@^1.16.0:
version "1.16.0"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.0.tgz#2e1816bcbbaa518ea6c2e15a466f4cb9c6e2fbb3"
integrity sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw==
@@ -10126,19 +10062,6 @@ react-photo-gallery@7.0.2:
prop-types "~15.7.2"
resize-observer-polyfill "^1.5.0"
react-popper@^1.3.3, react-popper@^1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324"
integrity sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==
dependencies:
"@babel/runtime" "^7.1.2"
create-react-context "^0.3.0"
deep-equal "^1.1.1"
popper.js "^1.14.4"
prop-types "^15.6.1"
typed-styles "^0.0.7"
warning "^4.0.2"
react-prop-toggle@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/react-prop-toggle/-/react-prop-toggle-1.0.2.tgz#8b0b7e74653606b1427cfcf6c4eaa9198330568e"
@@ -10262,7 +10185,7 @@ react-select@^3.0.8:
react-input-autosize "^2.2.2"
react-transition-group "^2.2.1"
react-transition-group@2, react-transition-group@^2.2.1, react-transition-group@^2.9.0:
react-transition-group@2, react-transition-group@^2.2.1:
version "2.9.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
@@ -10589,7 +10512,7 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
resize-observer-polyfill@^1.5.0, resize-observer-polyfill@^1.5.1:
resize-observer-polyfill@^1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
@@ -11866,11 +11789,6 @@ tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
tslib@~1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
tsutils@^3.17.1:
version "3.17.1"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
@@ -11925,11 +11843,6 @@ type@^2.0.0:
resolved "https://registry.yarnpkg.com/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3"
integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==
typed-styles@^0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9"
integrity sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==
typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@@ -12260,7 +12173,7 @@ warning@^3.0.0:
dependencies:
loose-envify "^1.0.0"
warning@^4.0.2, warning@^4.0.3:
warning@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==