mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Blueprint removed
This commit is contained in:
9
ui/v2.5/.editorconfig
Normal file
9
ui/v2.5/.editorconfig
Normal 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
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": [
|
"extends": [
|
||||||
"react-app"
|
"react-app"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"jsx-a11y/anchor-is-valid": "off"
|
"jsx-a11y/anchor-is-valid": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blueprintjs/core": "3.22.1",
|
|
||||||
"@blueprintjs/select": "3.11.2",
|
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.26",
|
"@fortawesome/fontawesome-svg-core": "^1.2.26",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.12.0",
|
"@fortawesome/free-solid-svg-icons": "^5.12.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.8",
|
"@fortawesome/react-fontawesome": "^0.1.8",
|
||||||
@@ -12,11 +10,13 @@
|
|||||||
"apollo-link-ws": "^1.0.19",
|
"apollo-link-ws": "^1.0.19",
|
||||||
"axios": "0.18.1",
|
"axios": "0.18.1",
|
||||||
"bootstrap": "^4.4.1",
|
"bootstrap": "^4.4.1",
|
||||||
|
"classnames": "^2.2.6",
|
||||||
"formik": "1.5.7",
|
"formik": "1.5.7",
|
||||||
"graphql": "14.3.1",
|
"graphql": "14.3.1",
|
||||||
"localforage": "1.7.3",
|
"localforage": "1.7.3",
|
||||||
"lodash": "4.17.13",
|
"lodash": "4.17.13",
|
||||||
"node-sass": "4.12.0",
|
"node-sass": "4.12.0",
|
||||||
|
"normalize.css": "^8.0.1",
|
||||||
"query-string": "6.5.0",
|
"query-string": "6.5.0",
|
||||||
"react": "~16.12.0",
|
"react": "~16.12.0",
|
||||||
"react-apollo": "2.5.6",
|
"react-apollo": "2.5.6",
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
"not op_mini all"
|
"not op_mini all"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/classnames": "^2.2.9",
|
||||||
"@types/jest": "24.0.13",
|
"@types/jest": "24.0.13",
|
||||||
"@types/lodash": "4.14.132",
|
"@types/lodash": "4.14.132",
|
||||||
"@types/node": "11.13.0",
|
"@types/node": "11.13.0",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Stats } from "./components/Stats";
|
|||||||
import Studios from "./components/Studios/Studios";
|
import Studios from "./components/Studios/Studios";
|
||||||
import Tags from "./components/Tags/Tags";
|
import Tags from "./components/Tags/Tags";
|
||||||
import { SceneFilenameParser } from "./components/scenes/SceneFilenameParser";
|
import { SceneFilenameParser } from "./components/scenes/SceneFilenameParser";
|
||||||
import { ToastProvider } from './components/Shared/Toast';
|
import { ToastProvider } from 'src/hooks/Toast';
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { fas } from '@fortawesome/free-solid-svg-icons'
|
import { fas } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|||||||
@@ -1,26 +1,20 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React from "react";
|
||||||
import { Spinner } from 'react-bootstrap';
|
import { Spinner } from 'react-bootstrap';
|
||||||
import * as GQL from "../../core/generated-graphql";
|
import { useParams } from 'react-router-dom';
|
||||||
import { StashService } from "../../core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
import { IBaseProps } from "../../models";
|
|
||||||
import { GalleryViewer } from "./GalleryViewer";
|
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 { data, error, loading } = StashService.useFindGallery(id);
|
||||||
const [gallery, setGallery] = useState<Partial<GQL.GalleryDataFragment>>({});
|
const gallery = data?.findGallery;
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div style={{width: "75vw", margin: "0 auto"}}>
|
<div style={{width: "75vw", margin: "0 auto"}}>
|
||||||
<GalleryViewer gallery={gallery as any} />
|
<GalleryViewer gallery={gallery as any} />
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import React from "react";
|
|||||||
import { Table } from 'react-bootstrap';
|
import { Table } from 'react-bootstrap';
|
||||||
import { QueryHookResult } from "react-apollo-hooks";
|
import { QueryHookResult } from "react-apollo-hooks";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { FindGalleriesQuery, FindGalleriesVariables } from "../../core/generated-graphql";
|
import { FindGalleriesQuery, FindGalleriesVariables } from "src/core/generated-graphql";
|
||||||
import { ListHook } from "../../hooks/ListHook";
|
import { ListHook } from "src/hooks";
|
||||||
import { IBaseProps } from "../../models/base-props";
|
import { IBaseProps } from "src/models/base-props";
|
||||||
import { ListFilterModel } from "../../models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { DisplayMode, FilterMode } from "../../models/list-filter/types";
|
import { DisplayMode, FilterMode } from "src/models/list-filter/types";
|
||||||
|
|
||||||
interface IProps extends IBaseProps {}
|
interface IProps extends IBaseProps {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import React, { FunctionComponent, useState } from "react";
|
import React, { FunctionComponent, useState } from "react";
|
||||||
import Lightbox from "react-images";
|
import Lightbox from "react-images";
|
||||||
import Gallery from "react-photo-gallery";
|
import Gallery from "react-photo-gallery";
|
||||||
import * as GQL from "../../core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
gallery: GQL.GalleryDataFragment;
|
gallery: GQL.GalleryDataFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GalleryViewer: FunctionComponent<IProps> = (props: IProps) => {
|
export const GalleryViewer: FunctionComponent<IProps> = ({ gallery }) => {
|
||||||
const [currentImage, setCurrentImage] = useState<number>(0);
|
const [currentImage, setCurrentImage] = useState<number>(0);
|
||||||
const [lightboxIsOpen, setLightboxIsOpen] = useState<boolean>(false);
|
const [lightboxIsOpen, setLightboxIsOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
function openLightbox(event: any, obj: any) {
|
function openLightbox(_event: React.MouseEvent<Element>, obj: {index: number}) {
|
||||||
setCurrentImage(obj.index);
|
setCurrentImage(obj.index);
|
||||||
setLightboxIsOpen(true);
|
setLightboxIsOpen(true);
|
||||||
}
|
}
|
||||||
@@ -26,8 +26,8 @@ export const GalleryViewer: FunctionComponent<IProps> = (props: IProps) => {
|
|||||||
setCurrentImage(currentImage + 1);
|
setCurrentImage(currentImage + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const photos = props.gallery.files.map((file) => ({src: file.path || "", caption: file.name}));
|
const photos = 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 thumbs = gallery.files.map((file) => ({src: `${file.path}?thumb=true` || "", width: 1, height: 1}));
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Gallery photos={thumbs} columns={15} onClick={openLightbox} />
|
<Gallery photos={thumbs} columns={15} onClick={openLightbox} />
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export const MainNavbar: React.FC = () => {
|
|||||||
activeClassName="active"
|
activeClassName="active"
|
||||||
exact={true}
|
exact={true}
|
||||||
to={i.href}
|
to={i.href}
|
||||||
|
key={i.href}
|
||||||
>
|
>
|
||||||
<Button variant="secondary">
|
<Button variant="secondary">
|
||||||
<FontAwesomeIcon icon={i.icon} />
|
<FontAwesomeIcon icon={i.icon} />
|
||||||
@@ -82,7 +83,7 @@ export const MainNavbar: React.FC = () => {
|
|||||||
<Nav>
|
<Nav>
|
||||||
{newButton}
|
{newButton}
|
||||||
<LinkContainer
|
<LinkContainer
|
||||||
exact={true}
|
exact={true}
|
||||||
to="/settings">
|
to="/settings">
|
||||||
<Button variant="secondary">
|
<Button variant="secondary">
|
||||||
<FontAwesomeIcon icon="cog" />
|
<FontAwesomeIcon icon="cog" />
|
||||||
|
|||||||
@@ -1,50 +1,64 @@
|
|||||||
import {
|
import React from "react";
|
||||||
Card,
|
|
||||||
Tab,
|
|
||||||
Tabs,
|
|
||||||
} from "@blueprintjs/core";
|
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import { Card, Tab, Nav, Row, Col } from 'react-bootstrap';
|
||||||
import { IBaseProps } from "../../models";
|
import { useHistory, useLocation } from 'react-router-dom';
|
||||||
import { SettingsAboutPanel } from "./SettingsAboutPanel";
|
import { SettingsAboutPanel } from "./SettingsAboutPanel";
|
||||||
import { SettingsConfigurationPanel } from "./SettingsConfigurationPanel";
|
import { SettingsConfigurationPanel } from "./SettingsConfigurationPanel";
|
||||||
import { SettingsInterfacePanel } from "./SettingsInterfacePanel";
|
import { SettingsInterfacePanel } from "./SettingsInterfacePanel";
|
||||||
import { SettingsLogsPanel } from "./SettingsLogsPanel";
|
import { SettingsLogsPanel } from "./SettingsLogsPanel";
|
||||||
import { SettingsTasksPanel } from "./SettingsTasksPanel/SettingsTasksPanel";
|
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";
|
const onSelect = ((val:string) => history.push(`?tab=${val}`));
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card id="details-container">
|
<Card id="details-container">
|
||||||
<Tabs
|
<Tab.Container defaultActiveKey={defaultTab} id="configuration-tabs" onSelect={onSelect}>
|
||||||
renderActiveTabPanelOnly={true}
|
<Row>
|
||||||
vertical={true}
|
<Col sm={2}>
|
||||||
onChange={(newId) => setTabId(newId as TabId)}
|
<Nav variant="pills" className="flex-column">
|
||||||
defaultSelectedTabId={getTabId()}
|
<Nav.Item>
|
||||||
>
|
<Nav.Link eventKey="configuration">Configuration</Nav.Link>
|
||||||
<Tab id="configuration" title="Configuration" panel={<SettingsConfigurationPanel />} />
|
</Nav.Item>
|
||||||
<Tab id="interface" title="Interface Configuration" panel={<SettingsInterfacePanel />} />
|
<Nav.Item>
|
||||||
<Tab id="tasks" title="Tasks" panel={<SettingsTasksPanel />} />
|
<Nav.Link eventKey="interface">Interface</Nav.Link>
|
||||||
<Tab id="logs" title="Logs" panel={<SettingsLogsPanel />} />
|
</Nav.Item>
|
||||||
<Tab id="about" title="About" panel={<SettingsAboutPanel />} />
|
<Nav.Item>
|
||||||
</Tabs>
|
<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>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Table, Spinner } from 'react-bootstrap';
|
import { Table, Spinner } from 'react-bootstrap';
|
||||||
import { StashService } from "../../core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
|
|
||||||
export const SettingsAboutPanel: React.FC = () => {
|
export const SettingsAboutPanel: React.FC = () => {
|
||||||
const { data, error, loading } = StashService.useVersion();
|
const { data, error, loading } = StashService.useVersion();
|
||||||
@@ -30,7 +30,7 @@ export const SettingsAboutPanel: React.FC = () => {
|
|||||||
<td>Build time:</td>
|
<td>Build time:</td>
|
||||||
<td>{data.version.build_time}</td>
|
<td>{data.version.build_time}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -38,8 +38,8 @@ export const SettingsAboutPanel: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4>About</h4>
|
<h4>About</h4>
|
||||||
{!data || loading ? <Spinner animation="border" variant="light" /> : undefined}
|
{!data || loading ? <Spinner animation="border" variant="light" /> : ''}
|
||||||
{!!error ? <span>error.message</span> : undefined}
|
{error ? <span>error.message</span> : ''}
|
||||||
{renderVersion()}
|
{renderVersion()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
import {
|
|
||||||
AnchorButton,
|
|
||||||
Button,
|
|
||||||
Divider,
|
|
||||||
FormGroup,
|
|
||||||
InputGroup,
|
|
||||||
Spinner,
|
|
||||||
Checkbox,
|
|
||||||
HTMLSelect,
|
|
||||||
} from "@blueprintjs/core";
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import * as GQL from "../../core/generated-graphql";
|
import { Button, Form, InputGroup, Spinner } from 'react-bootstrap';
|
||||||
import { StashService } from "../../core/StashService";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { ErrorUtils } from "../../utils/errors";
|
import { StashService } from "src/core/StashService";
|
||||||
import { ToastUtils } from "../../utils/toasts";
|
import { useToast } from 'src/hooks';
|
||||||
import { FolderSelect } from "../Shared/FolderSelect/FolderSelect";
|
import { Icon } from 'src/components/Shared';
|
||||||
|
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
|
||||||
|
|
||||||
export const SettingsConfigurationPanel: React.FC = () => {
|
export const SettingsConfigurationPanel: React.FC = () => {
|
||||||
|
const Toast = useToast();
|
||||||
// Editing config state
|
// Editing config state
|
||||||
const [stashes, setStashes] = useState<string[]>([]);
|
const [stashes, setStashes] = useState<string[]>([]);
|
||||||
const [databasePath, setDatabasePath] = useState<string | undefined>(undefined);
|
const [databasePath, setDatabasePath] = useState<string | undefined>(undefined);
|
||||||
@@ -48,10 +40,12 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data || !data.configuration || !!error) { return; }
|
if (!data?.configuration || error)
|
||||||
const conf = StashService.nullToUndefined(data.configuration) as GQL.ConfigDataFragment;
|
return;
|
||||||
if (!!conf.general) {
|
|
||||||
setStashes(conf.general.stashes || []);
|
const conf = data.configuration;
|
||||||
|
if (conf.general) {
|
||||||
|
setStashes(conf.general.stashes ?? []);
|
||||||
setDatabasePath(conf.general.databasePath);
|
setDatabasePath(conf.general.databasePath);
|
||||||
setGeneratedPath(conf.general.generatedPath);
|
setGeneratedPath(conf.general.generatedPath);
|
||||||
setMaxTranscodeSize(conf.general.maxTranscodeSize);
|
setMaxTranscodeSize(conf.general.maxTranscodeSize);
|
||||||
@@ -64,7 +58,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
setLogAccess(conf.general.logAccess);
|
setLogAccess(conf.general.logAccess);
|
||||||
setExcludes(conf.general.excludes);
|
setExcludes(conf.general.excludes);
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data, error]);
|
||||||
|
|
||||||
function onStashesChanged(directories: string[]) {
|
function onStashesChanged(directories: string[]) {
|
||||||
setStashes(directories);
|
setStashes(directories);
|
||||||
@@ -96,9 +90,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const result = await updateGeneralConfig();
|
const result = await updateGeneralConfig();
|
||||||
console.log(result);
|
console.log(result);
|
||||||
ToastUtils.success("Updated config");
|
Toast.success({ content: "Updated config" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,149 +131,156 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
return GQL.StreamingResolutionEnum.Original;
|
return GQL.StreamingResolutionEnum.Original;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(error)
|
||||||
|
return <h1>{error.message}</h1>;
|
||||||
|
if(!data?.configuration || loading)
|
||||||
|
return <Spinner animation="border" variant="light" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!!error ? <h1>{error.message}</h1> : undefined}
|
|
||||||
{(!data || !data.configuration || loading) ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}
|
|
||||||
<h4>Library</h4>
|
<h4>Library</h4>
|
||||||
<FormGroup>
|
<Form.Group>
|
||||||
<FormGroup>
|
<Form.Group id="stashes">
|
||||||
<FormGroup
|
<Form.Label>Stashes</Form.Label>
|
||||||
label="Stashes"
|
<FolderSelect
|
||||||
helperText="Directory locations to your content"
|
directories={stashes}
|
||||||
>
|
onDirectoriesChanged={onStashesChanged}
|
||||||
<FolderSelect
|
/>
|
||||||
directories={stashes}
|
<Form.Text className="text-muted">Directory locations to your content</Form.Text>
|
||||||
onDirectoriesChanged={onStashesChanged}
|
</Form.Group>
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
label="Database Path"
|
|
||||||
helperText="File location for the SQLite database (requires restart)"
|
|
||||||
>
|
|
||||||
<InputGroup value={databasePath} onChange={(e: any) => setDatabasePath(e.target.value)} />
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
<Form.Group id="database-path">
|
||||||
label="Generated Path"
|
<Form.Label>Database Path</Form.Label>
|
||||||
helperText="Directory location for the generated files (scene markers, scene previews, sprites, etc)"
|
<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>
|
||||||
<InputGroup value={generatedPath} onChange={(e: any) => setGeneratedPath(e.target.value)} />
|
</Form.Group>
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
<Form.Group id="generated-path">
|
||||||
label="Excluded Patterns"
|
<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>
|
||||||
|
|
||||||
{ (excludes) ? excludes.map((regexp, i) => {
|
<Form.Group>
|
||||||
return(
|
<Form.Label>Excluded Patterns</Form.Label>
|
||||||
<InputGroup
|
{ excludes ? excludes.map((regexp, i) => (
|
||||||
value={regexp}
|
<InputGroup>
|
||||||
onChange={(e: any) => excludeRegexChanged(i, e.target.value)}
|
<Form.Control
|
||||||
rightElement={<Button icon="minus" minimal={true} intent="danger" onClick={(e: any) => excludeRemoveRegex(i)} />}
|
value={regexp}
|
||||||
/>
|
onChange={(e: any) => excludeRegexChanged(i, e.target.value)}
|
||||||
);
|
/>
|
||||||
}) : 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>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
<AnchorButton
|
<a
|
||||||
href="https://github.com/stashapp/stash/wiki/Exclude-file-configuration"
|
href="https://github.com/stashapp/stash/wiki/Exclude-file-configuration"
|
||||||
rightIcon="help"
|
rel="noopener noreferrer"
|
||||||
text="Regexps of files/paths to exclude from Scan and add to Clean"
|
target="_blank"
|
||||||
minimal={true}
|
>
|
||||||
target="_blank"
|
<span>Regexps of files/paths to exclude from Scan and add to Clean</span>
|
||||||
/>
|
<Icon icon="question-circle" />
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</FormGroup>
|
</Form.Group>
|
||||||
</FormGroup>
|
</Form.Group>
|
||||||
|
|
||||||
<Divider />
|
|
||||||
<FormGroup>
|
|
||||||
<h4>Video</h4>
|
|
||||||
<FormGroup
|
|
||||||
label="Maximum transcode size"
|
|
||||||
helperText="Maximum size for generated transcodes"
|
|
||||||
>
|
|
||||||
<HTMLSelect
|
|
||||||
options={transcodeQualities}
|
|
||||||
onChange={(event) => setMaxTranscodeSize(translateQuality(event.target.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))}
|
|
||||||
value={resolutionToString(maxStreamingTranscodeSize)}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</FormGroup>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<FormGroup>
|
<hr />
|
||||||
|
|
||||||
|
<Form.Group>
|
||||||
|
<h4>Video</h4>
|
||||||
|
<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)}
|
||||||
|
>
|
||||||
|
{ 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)}
|
||||||
|
>
|
||||||
|
{ 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>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<Form.Group>
|
||||||
<h4>Authentication</h4>
|
<h4>Authentication</h4>
|
||||||
<FormGroup
|
<Form.Group id="username">
|
||||||
label="Username"
|
<Form.Label>Username</Form.Label>
|
||||||
helperText="Username to access Stash. Leave blank to disable user authentication"
|
<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>
|
||||||
<InputGroup value={username} onChange={(e: any) => setUsername(e.target.value)} />
|
</Form.Group>
|
||||||
</FormGroup>
|
<Form.Group id="password">
|
||||||
<FormGroup
|
<Form.Label>Password</Form.Label>
|
||||||
label="Password"
|
<Form.Control type="password" defaultValue={password} onChange={(e: React.FormEvent<HTMLInputElement>) => setPassword(e.currentTarget.value)} />
|
||||||
helperText="Password to access Stash. Leave blank to disable user authentication"
|
<Form.Text className="text-muted">Password to access Stash. Leave blank to disable user authentication</Form.Text>
|
||||||
>
|
</Form.Group>
|
||||||
<InputGroup type="password" value={password} onChange={(e: any) => setPassword(e.target.value)} />
|
</Form.Group>
|
||||||
</FormGroup>
|
|
||||||
</FormGroup>
|
<hr />
|
||||||
|
|
||||||
<Divider />
|
|
||||||
<h4>Logging</h4>
|
<h4>Logging</h4>
|
||||||
<FormGroup
|
<Form.Group id="log-file">
|
||||||
label="Log file"
|
<Form.Label>Log file</Form.Label>
|
||||||
helperText="Path to the file to output logging to. Blank to disable file logging. Requires restart."
|
<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>
|
||||||
<InputGroup value={logFile} onChange={(e: any) => setLogFile(e.target.value)} />
|
</Form.Group>
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
<Form.Group>
|
||||||
helperText="Logs to the terminal in addition to a file. Always true if file logging is disabled. Requires restart."
|
<Form.Check
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={logOut}
|
checked={logOut}
|
||||||
label="Log to terminal"
|
label="Log to terminal"
|
||||||
onChange={() => setLogOut(!logOut)}
|
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">
|
<Form.Group id="log-level">
|
||||||
<HTMLSelect
|
<Form.Label>Log Level</Form.Label>
|
||||||
options={["Debug", "Info", "Warning", "Error"]}
|
<Form.Control
|
||||||
onChange={(event) => setLogLevel(event.target.value)}
|
as="select"
|
||||||
|
onChange={(event:React.FormEvent<HTMLSelectElement>) => setLogLevel(event.currentTarget.value)}
|
||||||
value={logLevel}
|
value={logLevel}
|
||||||
/>
|
>
|
||||||
</FormGroup>
|
{ ["Debug", "Info", "Warning", "Error"].map(o => (<option key={o} value={o}>{o}</option>)) }
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
<FormGroup
|
<Form.Group>
|
||||||
helperText="Logs http access to the terminal. Requires restart."
|
<Form.Check
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={logAccess}
|
checked={logAccess}
|
||||||
label="Log http access"
|
label="Log http access"
|
||||||
onChange={() => setLogAccess(!logAccess)}
|
onChange={() => setLogAccess(!logAccess)}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
<Form.Text className="text-muted">Logs http access to the terminal. Requires restart.</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
<Divider />
|
<hr />
|
||||||
<Button intent="primary" onClick={() => onSave()}>Save</Button>
|
|
||||||
|
<Button variant="primary" onClick={() => onSave()}>Save</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,10 @@
|
|||||||
import {
|
import React, { useEffect, useState } from "react";
|
||||||
Button,
|
import { Button, Form, Spinner } from 'react-bootstrap';
|
||||||
Checkbox,
|
import { StashService } from "src/core/StashService";
|
||||||
Divider,
|
import { useToast } from 'src/hooks';
|
||||||
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";
|
|
||||||
|
|
||||||
interface IProps {}
|
export const SettingsInterfacePanel: React.FC = () => {
|
||||||
|
const Toast = useToast();
|
||||||
export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
|
|
||||||
const config = StashService.useConfiguration();
|
const config = StashService.useConfiguration();
|
||||||
const [soundOnPreview, setSoundOnPreview] = useState<boolean>();
|
const [soundOnPreview, setSoundOnPreview] = useState<boolean>();
|
||||||
const [wallShowTitle, setWallShowTitle] = useState<boolean>();
|
const [wallShowTitle, setWallShowTitle] = useState<boolean>();
|
||||||
@@ -35,66 +25,63 @@ export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!config.data || !config.data.configuration || !!config.error) { return; }
|
if (config.error)
|
||||||
if (!!config.data.configuration.interface) {
|
return;
|
||||||
let iCfg = config.data.configuration.interface;
|
|
||||||
setSoundOnPreview(iCfg.soundOnPreview !== undefined ? iCfg.soundOnPreview : true);
|
const iCfg = config?.data?.configuration?.interface;
|
||||||
setWallShowTitle(iCfg.wallShowTitle !== undefined ? iCfg.wallShowTitle : true);
|
setSoundOnPreview(iCfg?.soundOnPreview ?? true);
|
||||||
setMaximumLoopDuration(iCfg.maximumLoopDuration || 0);
|
setWallShowTitle(iCfg?.wallShowTitle ?? true);
|
||||||
setAutostartVideo(iCfg.autostartVideo !== undefined ? iCfg.autostartVideo : false);
|
setMaximumLoopDuration(iCfg?.maximumLoopDuration ?? 0);
|
||||||
setShowStudioAsText(iCfg.showStudioAsText !== undefined ? iCfg.showStudioAsText : false);
|
setAutostartVideo(iCfg?.autostartVideo ?? false);
|
||||||
setCSS(config.data.configuration.interface.css || "");
|
setShowStudioAsText(iCfg?.showStudioAsText ?? false);
|
||||||
setCSSEnabled(config.data.configuration.interface.cssEnabled || false);
|
setCSS(iCfg?.css ?? "");
|
||||||
}
|
setCSSEnabled(iCfg?.cssEnabled ?? false);
|
||||||
}, [config.data]);
|
}, [config]);
|
||||||
|
|
||||||
async function onSave() {
|
async function onSave() {
|
||||||
try {
|
try {
|
||||||
const result = await updateInterfaceConfig();
|
const result = await updateInterfaceConfig();
|
||||||
console.log(result);
|
console.log(result);
|
||||||
ToastUtils.success("Updated config");
|
Toast.success({ content: "Updated config" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!!config.error ? <h1>{config.error.message}</h1> : undefined}
|
{config.error ? <h1>{config.error.message}</h1> : ''}
|
||||||
{(!config.data || !config.data.configuration || config.loading) ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}
|
{(!config?.data?.configuration || config.loading) ? <Spinner animation="border" variant="light" /> : ''}
|
||||||
<h4>User Interface</h4>
|
<h4>User Interface</h4>
|
||||||
<FormGroup
|
<Form.Group>
|
||||||
label="Scene / Marker Wall"
|
<Form.Label>Scene / Marker Wall</Form.Label>
|
||||||
helperText="Configuration for wall items"
|
<Form.Check
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={wallShowTitle}
|
checked={wallShowTitle}
|
||||||
label="Display title and tags"
|
label="Display title and tags"
|
||||||
onChange={() => setWallShowTitle(!wallShowTitle)}
|
onChange={() => setWallShowTitle(!wallShowTitle)}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Form.Check
|
||||||
checked={soundOnPreview}
|
checked={soundOnPreview}
|
||||||
label="Enable sound"
|
label="Enable sound"
|
||||||
onChange={() => setSoundOnPreview(!soundOnPreview)}
|
onChange={() => setSoundOnPreview(!soundOnPreview)}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
<Form.Text className="text-muted" >Configuration for wall items</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
<FormGroup
|
<Form.Group>
|
||||||
label="Scene List"
|
<Form.Label>Scene List</Form.Label>
|
||||||
>
|
<Form.Check
|
||||||
<Checkbox
|
|
||||||
checked={showStudioAsText}
|
checked={showStudioAsText}
|
||||||
label="Show Studios as text"
|
label="Show Studios as text"
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setShowStudioAsText(!showStudioAsText)
|
setShowStudioAsText(!showStudioAsText)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</Form.Group>
|
||||||
|
|
||||||
<FormGroup
|
<Form.Group>
|
||||||
label="Scene Player"
|
<Form.Label>Scene Player</Form.Label>
|
||||||
>
|
<Form.Check
|
||||||
<Checkbox
|
|
||||||
checked={autostartVideo}
|
checked={autostartVideo}
|
||||||
label="Auto-start video"
|
label="Auto-start video"
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
@@ -102,25 +89,22 @@ export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormGroup
|
<Form.Group id="max-loop-duration">
|
||||||
label="Maximum loop duration"
|
<Form.Label>Maximum loop duration</Form.Label>
|
||||||
helperText="Maximum scene duration - in seconds - where scene player will loop the video - 0 to disable"
|
<Form.Control
|
||||||
>
|
|
||||||
<NumericInput
|
|
||||||
value={maximumLoopDuration}
|
|
||||||
type="number"
|
type="number"
|
||||||
onValueChange={(value: number) => setMaximumLoopDuration(value)}
|
defaultValue={maximumLoopDuration}
|
||||||
|
onChange={(event:React.FormEvent<HTMLInputElement>) => setMaximumLoopDuration(Number.parseInt(event.currentTarget.value) ?? 0)}
|
||||||
min={0}
|
min={0}
|
||||||
minorStepSize={1}
|
step={1}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
<Form.Text className="text-muted">Maximum scene duration - in seconds - where scene player will loop the video - 0 to disable</Form.Text>
|
||||||
</FormGroup>
|
</Form.Group>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
<FormGroup
|
<Form.Group>
|
||||||
label="Custom CSS"
|
<Form.Label>Custom CSS</Form.Label>
|
||||||
helperText="Page must be reloaded for changes to take effect."
|
<Form.Check
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={cssEnabled}
|
checked={cssEnabled}
|
||||||
label="Custom CSS enabled"
|
label="Custom CSS enabled"
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
@@ -128,16 +112,17 @@ export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextArea
|
<Form.Control
|
||||||
value={css}
|
as="textarea"
|
||||||
|
value={css}
|
||||||
onChange={(e: any) => setCSS(e.target.value)}
|
onChange={(e: any) => setCSS(e.target.value)}
|
||||||
fill={true}
|
|
||||||
rows={16}>
|
rows={16}>
|
||||||
</TextArea>
|
</Form.Control>
|
||||||
</FormGroup>
|
<Form.Text className="text-muted">Page must be reloaded for changes to take effect.</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
<Divider />
|
<hr />
|
||||||
<Button intent="primary" onClick={() => onSave()}>Save</Button>
|
<Button variant="primary" onClick={() => onSave()}>Save</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import {
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
H4, FormGroup, HTMLSelect,
|
import { Form, Col } from 'react-bootstrap';
|
||||||
} from "@blueprintjs/core";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import React, { FunctionComponent, useState, useEffect, useRef } from "react";
|
import { StashService } from "src/core/StashService";
|
||||||
import * as GQL from "../../core/generated-graphql";
|
|
||||||
import { StashService } from "../../core/StashService";
|
|
||||||
|
|
||||||
interface IProps {}
|
function convertTime(logEntry: GQL.LogEntryDataFragment) {
|
||||||
|
|
||||||
function convertTime(logEntry : GQL.LogEntryDataFragment) {
|
|
||||||
function pad(val : number) {
|
function pad(val : number) {
|
||||||
var ret = val.toString();
|
var ret = val.toString();
|
||||||
if (val <= 9) {
|
if (val <= 9) {
|
||||||
@@ -44,17 +40,17 @@ class LogEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingsLogsPanel: FunctionComponent<IProps> = (props: IProps) => {
|
export const SettingsLogsPanel: React.FC = () => {
|
||||||
const { data, error } = StashService.useLoggingSubscribe();
|
const { data, error } = StashService.useLoggingSubscribe();
|
||||||
const { data: existingData } = StashService.useLogs();
|
const { data: existingData } = StashService.useLogs();
|
||||||
|
|
||||||
const logEntries = useRef<LogEntry[]>([]);
|
const logEntries = useRef<LogEntry[]>([]);
|
||||||
const [logLevel, setLogLevel] = useState<string>("Info");
|
const [logLevel, setLogLevel] = useState<string>("Info");
|
||||||
const [filteredLogEntries, setFilteredLogEntries] = useState<LogEntry[]>([]);
|
const [filteredLogEntries, setFilteredLogEntries] = useState<LogEntry[]>([]);
|
||||||
const lastUpdate = useRef<number>(0);
|
const lastUpdate = useRef<number>(0);
|
||||||
const updateTimeout = useRef<NodeJS.Timeout>();
|
const updateTimeout = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
// maximum number of log entries to display. Subsequent entries will truncate
|
// maximum number of log entries to display. Subsequent entries will truncate
|
||||||
// the list, dropping off the oldest entries first.
|
// the list, dropping off the oldest entries first.
|
||||||
const MAX_LOG_ENTRIES = 200;
|
const MAX_LOG_ENTRIES = 200;
|
||||||
|
|
||||||
@@ -83,7 +79,7 @@ export const SettingsLogsPanel: FunctionComponent<IProps> = (props: IProps) => {
|
|||||||
// filter subscribed data as it comes in, otherwise we'll end up
|
// filter subscribed data as it comes in, otherwise we'll end up
|
||||||
// truncating stuff that wasn't filtered out
|
// truncating stuff that wasn't filtered out
|
||||||
convertedData = convertedData.filter(filterByLogLevel)
|
convertedData = convertedData.filter(filterByLogLevel)
|
||||||
|
|
||||||
// put newest entries at the top
|
// put newest entries at the top
|
||||||
convertedData.reverse();
|
convertedData.reverse();
|
||||||
prependLogEntries(convertedData);
|
prependLogEntries(convertedData);
|
||||||
@@ -167,16 +163,21 @@ export const SettingsLogsPanel: FunctionComponent<IProps> = (props: IProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<H4>Logs</H4>
|
<h4>Logs</h4>
|
||||||
<div>
|
<Form.Row id="log-level">
|
||||||
<FormGroup inline={true} label="Log Level">
|
<Col xs={1}>
|
||||||
<HTMLSelect
|
<Form.Label>Log Level</Form.Label>
|
||||||
options={logLevels}
|
</Col>
|
||||||
onChange={(event) => setLogLevel(event.target.value)}
|
<Col xs={2}>
|
||||||
value={logLevel}
|
<Form.Control
|
||||||
/>
|
as="select"
|
||||||
</FormGroup>
|
defaultValue={logLevel}
|
||||||
</div>
|
onChange={(event) => setLogLevel(event.currentTarget.value)}
|
||||||
|
>
|
||||||
|
{ logLevels.map(level => (<option key={level} value={level}>{level}</option>)) }
|
||||||
|
</Form.Control>
|
||||||
|
</Col>
|
||||||
|
</Form.Row>
|
||||||
<div className="logs">
|
<div className="logs">
|
||||||
{maybeRenderError()}
|
{maybeRenderError()}
|
||||||
{filteredLogEntries.map((logEntry) =>
|
{filteredLogEntries.map((logEntry) =>
|
||||||
|
|||||||
@@ -1,53 +1,32 @@
|
|||||||
import {
|
import React, { useState } from "react";
|
||||||
Button,
|
import { Button, Form } from 'react-bootstrap';
|
||||||
Checkbox,
|
import { StashService } from "src/core/StashService";
|
||||||
FormGroup,
|
import { useToast } from 'src/hooks';
|
||||||
} from "@blueprintjs/core";
|
|
||||||
import React, { FunctionComponent, useState } from "react";
|
|
||||||
import { StashService } from "../../../core/StashService";
|
|
||||||
import { ErrorUtils } from "../../../utils/errors";
|
|
||||||
import { ToastUtils } from "../../../utils/toasts";
|
|
||||||
|
|
||||||
interface IProps {}
|
export const GenerateButton: React.FC = () => {
|
||||||
|
const Toast = useToast();
|
||||||
export const GenerateButton: FunctionComponent<IProps> = () => {
|
const [sprites, setSprites] = useState(true);
|
||||||
const [sprites, setSprites] = useState<boolean>(true);
|
const [previews, setPreviews] = useState(true);
|
||||||
const [previews, setPreviews] = useState<boolean>(true);
|
const [markers, setMarkers] = useState(true);
|
||||||
const [markers, setMarkers] = useState<boolean>(true);
|
const [transcodes, setTranscodes] = useState(true);
|
||||||
const [transcodes, setTranscodes] = useState<boolean>(true);
|
|
||||||
|
|
||||||
async function onGenerate() {
|
async function onGenerate() {
|
||||||
try {
|
try {
|
||||||
await StashService.queryMetadataGenerate({sprites, previews, markers, transcodes});
|
await StashService.queryMetadataGenerate({sprites, previews, markers, transcodes});
|
||||||
ToastUtils.success("Started generating");
|
Toast.success({ content: "Started generating" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup
|
<Form.Group>
|
||||||
helperText="Generate supporting image, sprite, video, vtt and other files."
|
<Form.Check id="sprite-task" checked={sprites} label="Sprites (for the scene scrubber)" onChange={() => setSprites(!sprites)} />
|
||||||
labelFor="generate"
|
<Form.Check id="preview-task" checked={previews} label="Previews (video previews which play when hovering over a scene)" onChange={() => setPreviews(!previews)} />
|
||||||
inline={true}
|
<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)} />
|
||||||
<Checkbox checked={sprites} label="Sprites (for the scene scrubber)" onChange={() => setSprites(!sprites)} />
|
<Button id="generate" type="submit" onClick={() => onGenerate()}>Generate</Button>
|
||||||
<Checkbox
|
<Form.Text className="text-muted">Generate supporting image, sprite, video, vtt and other files.</Form.Text>
|
||||||
checked={previews}
|
</Form.Group>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
import {
|
|
||||||
Alert,
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Divider,
|
|
||||||
FormGroup,
|
|
||||||
ProgressBar,
|
|
||||||
} from "@blueprintjs/core";
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { StashService } from "../../../core/StashService";
|
import { Button, Form, ProgressBar } from 'react-bootstrap';
|
||||||
import { ErrorUtils } from "../../../utils/errors";
|
|
||||||
import { ToastUtils } from "../../../utils/toasts";
|
|
||||||
import { GenerateButton } from "./GenerateButton";
|
|
||||||
import { Link } from "react-router-dom";
|
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 = () => {
|
export const SettingsTasksPanel: React.FC = () => {
|
||||||
|
const Toast = useToast();
|
||||||
const [isImportAlertOpen, setIsImportAlertOpen] = useState<boolean>(false);
|
const [isImportAlertOpen, setIsImportAlertOpen] = useState<boolean>(false);
|
||||||
const [isCleanAlertOpen, setIsCleanAlertOpen] = useState<boolean>(false);
|
const [isCleanAlertOpen, setIsCleanAlertOpen] = useState<boolean>(false);
|
||||||
const [useFileMetadata, setUseFileMetadata] = useState<boolean>(false);
|
const [useFileMetadata, setUseFileMetadata] = useState<boolean>(false);
|
||||||
const [status, setStatus] = useState<string>("");
|
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 [autoTagPerformers, setAutoTagPerformers] = useState<boolean>(true);
|
||||||
const [autoTagStudios, setAutoTagStudios] = useState<boolean>(true);
|
const [autoTagStudios, setAutoTagStudios] = useState<boolean>(true);
|
||||||
@@ -53,7 +47,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
setStatus(statusToText(jobStatus.data.jobStatus.status));
|
setStatus(statusToText(jobStatus.data.jobStatus.status));
|
||||||
var newProgress = jobStatus.data.jobStatus.progress;
|
var newProgress = jobStatus.data.jobStatus.progress;
|
||||||
if (newProgress < 0) {
|
if (newProgress < 0) {
|
||||||
setProgress(undefined);
|
setProgress(0);
|
||||||
} else {
|
} else {
|
||||||
setProgress(newProgress);
|
setProgress(newProgress);
|
||||||
}
|
}
|
||||||
@@ -65,7 +59,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
setStatus(statusToText(metadataUpdate.data.metadataUpdate.status));
|
setStatus(statusToText(metadataUpdate.data.metadataUpdate.status));
|
||||||
var newProgress = metadataUpdate.data.metadataUpdate.progress;
|
var newProgress = metadataUpdate.data.metadataUpdate.progress;
|
||||||
if (newProgress < 0) {
|
if (newProgress < 0) {
|
||||||
setProgress(undefined);
|
setProgress(0);
|
||||||
} else {
|
} else {
|
||||||
setProgress(newProgress);
|
setProgress(newProgress);
|
||||||
}
|
}
|
||||||
@@ -79,20 +73,17 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
|
|
||||||
function renderImportAlert() {
|
function renderImportAlert() {
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Modal
|
||||||
cancelButtonText="Cancel"
|
show={isImportAlertOpen}
|
||||||
confirmButtonText="Import"
|
icon="trash-alt"
|
||||||
icon="trash"
|
accept={{ text: 'Import', variant: 'danger', onClick: onImport }}
|
||||||
intent="danger"
|
cancel={{ onClick: () => setIsImportAlertOpen(false) }}
|
||||||
isOpen={isImportAlertOpen}
|
|
||||||
onCancel={() => setIsImportAlertOpen(false)}
|
|
||||||
onConfirm={() => onImport()}
|
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
Are you sure you want to import? This will delete the database and re-import from
|
Are you sure you want to import? This will delete the database and re-import from
|
||||||
your exported metadata.
|
your exported metadata.
|
||||||
</p>
|
</p>
|
||||||
</Alert>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,31 +94,28 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
|
|
||||||
function renderCleanAlert() {
|
function renderCleanAlert() {
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Modal
|
||||||
cancelButtonText="Cancel"
|
show={isCleanAlertOpen}
|
||||||
confirmButtonText="Clean"
|
icon="trash-alt"
|
||||||
icon="trash"
|
accept={{ text: 'Clean', variant: 'danger', onClick: onClean }}
|
||||||
intent="danger"
|
cancel={{ onClick: () => setIsCleanAlertOpen(false) }}
|
||||||
isOpen={isCleanAlertOpen}
|
|
||||||
onCancel={() => setIsCleanAlertOpen(false)}
|
|
||||||
onConfirm={() => onClean()}
|
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
Are you sure you want to Clean?
|
Are you sure you want to Clean?
|
||||||
This will delete db information and generated content
|
This will delete db information and generated content
|
||||||
for all scenes that are no longer found in the filesystem.
|
for all scenes that are no longer found in the filesystem.
|
||||||
</p>
|
</p>
|
||||||
</Alert>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onScan() {
|
async function onScan() {
|
||||||
try {
|
try {
|
||||||
await StashService.queryMetadataScan({useFileMetadata: useFileMetadata});
|
await StashService.queryMetadataScan({useFileMetadata: useFileMetadata});
|
||||||
ToastUtils.success("Started scan");
|
Toast.success({ content: "Started scan" });
|
||||||
jobStatus.refetch();
|
jobStatus.refetch();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,10 +131,10 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
async function onAutoTag() {
|
async function onAutoTag() {
|
||||||
try {
|
try {
|
||||||
await StashService.queryMetadataAutoTag(getAutoTagInput());
|
await StashService.queryMetadataAutoTag(getAutoTagInput());
|
||||||
ToastUtils.success("Started auto tagging");
|
Toast.success({ content: "Started auto tagging" });
|
||||||
jobStatus.refetch();
|
jobStatus.refetch();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,21 +144,19 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Form.Group>
|
||||||
<FormGroup>
|
<Button id="stop" variant="danger" onClick={() => StashService.queryStopJob().then(() => jobStatus.refetch())}>Stop</Button>
|
||||||
<Button id="stop" text="Stop" intent="danger" onClick={() => StashService.queryStopJob().then(() => jobStatus.refetch())} />
|
</Form.Group>
|
||||||
</FormGroup>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderJobStatus() {
|
function renderJobStatus() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormGroup>
|
<Form.Group>
|
||||||
<h5>Status: {status}</h5>
|
<h5>Status: {status}</h5>
|
||||||
{!!status && status !== "Idle" ? <ProgressBar value={progress}/> : undefined}
|
{ status !== "Idle" ? <ProgressBar now={progress} label={`${progress}%`} /> : '' }
|
||||||
</FormGroup>
|
</Form.Group>
|
||||||
{maybeRenderStop()}
|
{maybeRenderStop()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -185,83 +171,70 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
|
|
||||||
{renderJobStatus()}
|
{renderJobStatus()}
|
||||||
|
|
||||||
<Divider/>
|
<hr />
|
||||||
|
|
||||||
<h4>Library</h4>
|
<h4>Library</h4>
|
||||||
<FormGroup
|
<Form.Group>
|
||||||
helperText="Scan for new content and add it to the database."
|
<Form.Check
|
||||||
labelFor="scan"
|
|
||||||
inline={true}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={useFileMetadata}
|
checked={useFileMetadata}
|
||||||
label="Set name, date, details from metadata (if present)"
|
label="Set name, date, details from metadata (if present)"
|
||||||
onChange={() => setUseFileMetadata(!useFileMetadata)}
|
onChange={() => setUseFileMetadata(!useFileMetadata)}
|
||||||
/>
|
/>
|
||||||
<Button id="scan" text="Scan" onClick={() => onScan()} />
|
<Button id="scan" type="submit" onClick={() => onScan()}>Scan</Button>
|
||||||
</FormGroup>
|
<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>
|
<h4>Auto Tagging</h4>
|
||||||
|
|
||||||
<FormGroup
|
<Form.Group>
|
||||||
helperText="Auto-tag content based on filenames."
|
<Form.Check
|
||||||
labelFor="autoTag"
|
|
||||||
inline={true}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={autoTagPerformers}
|
checked={autoTagPerformers}
|
||||||
label="Performers"
|
label="Performers"
|
||||||
onChange={() => setAutoTagPerformers(!autoTagPerformers)}
|
onChange={() => setAutoTagPerformers(!autoTagPerformers)}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Form.Check
|
||||||
checked={autoTagStudios}
|
checked={autoTagStudios}
|
||||||
label="Studios"
|
label="Studios"
|
||||||
onChange={() => setAutoTagStudios(!autoTagStudios)}
|
onChange={() => setAutoTagStudios(!autoTagStudios)}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Form.Check
|
||||||
checked={autoTagTags}
|
checked={autoTagTags}
|
||||||
label="Tags"
|
label="Tags"
|
||||||
onChange={() => setAutoTagTags(!autoTagTags)}
|
onChange={() => setAutoTagTags(!autoTagTags)}
|
||||||
/>
|
/>
|
||||||
<Button id="autoTag" text="Auto Tag" onClick={() => onAutoTag()} />
|
<Button id="autoTag" type="submit" onClick={() => onAutoTag()}>Auto Tag</Button>
|
||||||
</FormGroup>
|
<Form.Text className="text-muted">Auto-tag content based on filenames.</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
<FormGroup>
|
<Form.Group>
|
||||||
<Link className="bp3-button" to={"/sceneFilenameParser"}>
|
<Button>
|
||||||
Scene Filename Parser
|
<Link to={"/sceneFilenameParser"}>Scene Filename Parser</Link>
|
||||||
</Link>
|
</Button>
|
||||||
</FormGroup>
|
</Form.Group>
|
||||||
<Divider />
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
<h4>Generated Content</h4>
|
<h4>Generated Content</h4>
|
||||||
<GenerateButton />
|
<GenerateButton />
|
||||||
<FormGroup
|
<Form.Group>
|
||||||
helperText="Check for missing files and remove them from the database. This is a destructive action."
|
<Button id="clean" variant="danger" onClick={() => setIsCleanAlertOpen(true)}>Clean</Button>
|
||||||
labelFor="clean"
|
<Form.Text className="text-muted">Check for missing files and remove them from the database. This is a destructive action.</Form.Text>
|
||||||
inline={true}
|
</Form.Group>
|
||||||
>
|
|
||||||
<Button id="clean" text="Clean" intent="danger" onClick={() => setIsCleanAlertOpen(true)} />
|
<hr />
|
||||||
</FormGroup>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<h4>Metadata</h4>
|
<h4>Metadata</h4>
|
||||||
<FormGroup
|
<Form.Group>
|
||||||
helperText="Export the database content into JSON format"
|
<Button id="export" type="submit"onClick={() => StashService.queryMetadataExport().then(() => { jobStatus.refetch()})}>Export</Button>
|
||||||
labelFor="export"
|
<Form.Text className="text-muted">Export the database content into JSON format.</Form.Text>
|
||||||
inline={true}
|
</Form.Group>
|
||||||
>
|
|
||||||
<Button id="export" text="Export" onClick={() => StashService.queryMetadataExport().then(() => { jobStatus.refetch()})} />
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
<Form.Group>
|
||||||
helperText="Import from exported JSON. This is a destructive action."
|
<Button id="import" variant="danger" onClick={() => setIsImportAlertOpen(true)}>Import</Button>
|
||||||
labelFor="import"
|
<Form.Text className="text-muted">Import from exported JSON. This is a destructive action.</Form.Text>
|
||||||
inline={true}
|
</Form.Group>
|
||||||
>
|
|
||||||
<Button id="import" text="Import" intent="danger" onClick={() => setIsImportAlertOpen(true)} />
|
|
||||||
</FormGroup>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Button, Form, Modal, Nav, Navbar, OverlayTrigger, Popover } from 'react-bootstrap';
|
import { Button, Form, Modal, Nav, Navbar, OverlayTrigger, Popover } from 'react-bootstrap';
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "../../core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { NavigationUtils } from "../../utils/navigation";
|
import { NavUtils } from "src/utils";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
performer?: Partial<GQL.PerformerDataFragment>;
|
performer?: Partial<GQL.PerformerDataFragment>;
|
||||||
@@ -92,10 +92,10 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
|||||||
function renderScenesButton() {
|
function renderScenesButton() {
|
||||||
if (props.isEditing) { return; }
|
if (props.isEditing) { return; }
|
||||||
let linkSrc: string = "#";
|
let linkSrc: string = "#";
|
||||||
if (!!props.performer) {
|
if (props.performer) {
|
||||||
linkSrc = NavigationUtils.makePerformerScenesUrl(props.performer);
|
linkSrc = NavUtils.makePerformerScenesUrl(props.performer);
|
||||||
} else if (!!props.studio) {
|
} else if (props.studio) {
|
||||||
linkSrc = NavigationUtils.makeStudioScenesUrl(props.studio);
|
linkSrc = NavUtils.makeStudioScenesUrl(props.studio);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Link to={linkSrc}>
|
<Link to={linkSrc}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Button, ButtonGroup, InputGroup, Form } from 'react-bootstrap';
|
import { Button, ButtonGroup, InputGroup, Form } from 'react-bootstrap';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { TextUtils } from "../../utils/text";
|
import { TextUtils } from "src/utils";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
@@ -35,7 +35,7 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
|||||||
if (!v) {
|
if (!v) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let splits = v.split(":");
|
let splits = v.split(":");
|
||||||
|
|
||||||
if (splits.length > 3) {
|
if (splits.length > 3) {
|
||||||
@@ -122,9 +122,7 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
|||||||
onChange={(e : any) => setValue(e.target.value)}
|
onChange={(e : any) => setValue(e.target.value)}
|
||||||
onBlur={() => props.onValueChange(stringToSeconds(value))}
|
onBlur={() => props.onValueChange(stringToSeconds(value))}
|
||||||
placeholder="hh:mm:ss"
|
placeholder="hh:mm:ss"
|
||||||
>
|
/>
|
||||||
{renderButtons()}
|
|
||||||
</Form.Control>
|
|
||||||
<InputGroup.Append>
|
<InputGroup.Append>
|
||||||
{maybeRenderReset()}
|
{maybeRenderReset()}
|
||||||
</InputGroup.Append>
|
</InputGroup.Append>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Button, InputGroup, Form, Modal, Spinner } from 'react-bootstrap';
|
|
||||||
import React, { useEffect, useState } from "react";
|
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 {
|
interface IProps {
|
||||||
directories: string[];
|
directories: string[];
|
||||||
@@ -17,7 +17,7 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
|
|||||||
setSelectedDirectories(props.directories);
|
setSelectedDirectories(props.directories);
|
||||||
}, [props.directories]);
|
}, [props.directories]);
|
||||||
|
|
||||||
const selectableDirectories:string[] = data && data.directories && !error ? StashService.nullToUndefined(data.directories) : [];
|
const selectableDirectories:string[] = data?.directories ?? [];
|
||||||
|
|
||||||
function onSelectDirectory() {
|
function onSelectDirectory() {
|
||||||
selectedDirectories.push(currentDirectory);
|
selectedDirectories.push(currentDirectory);
|
||||||
@@ -55,7 +55,7 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
|
|||||||
{(!data || !data.directories || loading) ? <Spinner animation="border" variant="light" /> : undefined}
|
{(!data || !data.directories || loading) ? <Spinner animation="border" variant="light" /> : undefined}
|
||||||
</InputGroup.Append>
|
</InputGroup.Append>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
/>
|
/>
|
||||||
{selectableDirectories.map((path) => {
|
{selectableDirectories.map((path) => {
|
||||||
return <div key={path} onClick={() => setCurrentDirectory(path)}>{path}</div>;
|
return <div key={path} onClick={() => setCurrentDirectory(path)}>{path}</div>;
|
||||||
@@ -71,14 +71,14 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!!error ? <h1>{error.message}</h1> : undefined}
|
{error ? <h1>{error.message}</h1> : ''}
|
||||||
{renderDialog()}
|
{renderDialog()}
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
{selectedDirectories.map((path) => {
|
{selectedDirectories.map((path) => {
|
||||||
return <div key={path}>{path} <a onClick={() => onRemoveDirectory(path)}>Remove</a></div>;
|
return <div key={path}>{path} <a onClick={() => onRemoveDirectory(path)}>Remove</a></div>;
|
||||||
})}
|
})}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Button onClick={() => setIsDisplayingDialog(true)}>Add Directory</Button>
|
<Button onClick={() => setIsDisplayingDialog(true)}>Add Directory</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
15
ui/v2.5/src/components/Shared/Icon.tsx
Normal file
15
ui/v2.5/src/components/Shared/Icon.tsx
Normal 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;
|
||||||
44
ui/v2.5/src/components/Shared/Modal.tsx
Normal file
44
ui/v2.5/src/components/Shared/Modal.tsx
Normal 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;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useCallback } from "react";
|
||||||
import Select, { ValueType } from 'react-select';
|
import Select, { ValueType } from 'react-select';
|
||||||
import CreatableSelect from 'react-select/creatable';
|
import CreatableSelect from 'react-select/creatable';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
import { ErrorUtils } from "../../utils/errors";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import * as GQL from "../../core/generated-graphql";
|
import { StashService } from "src/core/StashService";
|
||||||
import { StashService } from "../../core/StashService";
|
import { useToast } from 'src/hooks';
|
||||||
import useToast from '../Shared/Toast';
|
|
||||||
|
|
||||||
type ValidTypes =
|
type ValidTypes =
|
||||||
GQL.AllPerformersForFilterAllPerformers |
|
GQL.AllPerformersForFilterAllPerformers |
|
||||||
@@ -14,7 +14,7 @@ type ValidTypes =
|
|||||||
type Option = { value:string, label:string };
|
type Option = { value:string, label:string };
|
||||||
|
|
||||||
interface ITypeProps {
|
interface ITypeProps {
|
||||||
type: 'performers' | 'studios' | 'tags';
|
type?: 'performers' | 'studios' | 'tags';
|
||||||
}
|
}
|
||||||
interface IFilterProps {
|
interface IFilterProps {
|
||||||
initialIds: string[];
|
initialIds: string[];
|
||||||
@@ -32,8 +32,66 @@ interface ISelectProps {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
onChange: (item: ValueType<Option>) => void;
|
onChange: (item: ValueType<Option>) => void;
|
||||||
initialIds: string[];
|
initialIds: string[];
|
||||||
noSelectionString?: string;
|
|
||||||
isMulti?: boolean;
|
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) => (
|
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) => (
|
||||||
@@ -44,17 +102,17 @@ export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) => (
|
|||||||
|
|
||||||
export const PerformerSelect: React.FC<IFilterProps> = (props) => {
|
export const PerformerSelect: React.FC<IFilterProps> = (props) => {
|
||||||
const { data, loading } = StashService.useAllPerformersForFilter();
|
const { data, loading } = StashService.useAllPerformersForFilter();
|
||||||
|
|
||||||
const normalizedData = data?.allPerformers ?? [];
|
const normalizedData = data?.allPerformers ?? [];
|
||||||
const items:Option[] = normalizedData.map(item => ({ value: item.id, label: item.name ?? '' }));
|
const items:Option[] = normalizedData.map(item => ({ value: item.id, label: item.name ?? '' }));
|
||||||
|
const placeholder = props.noSelectionString ?? "Select performer..."
|
||||||
|
|
||||||
const onChange = (selectedItems:ValueType<Option>) => {
|
const onChange = (selectedItems:ValueType<Option>) => {
|
||||||
const selectedIds = (Array.isArray(selectedItems) ? selectedItems : [selectedItems])
|
const selectedIds = getSelectedValues(selectedItems);
|
||||||
.map(item => item.value);
|
|
||||||
props.onSelect(normalizedData.filter(item => selectedIds.indexOf(item.id) !== -1));
|
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) => {
|
export const StudioSelect: React.FC<IFilterProps> = (props) => {
|
||||||
@@ -62,14 +120,14 @@ export const StudioSelect: React.FC<IFilterProps> = (props) => {
|
|||||||
|
|
||||||
const normalizedData = data?.allStudios ?? [];
|
const normalizedData = data?.allStudios ?? [];
|
||||||
const items:Option[] = normalizedData.map(item => ({ value: item.id, label: item.name }));
|
const items:Option[] = normalizedData.map(item => ({ value: item.id, label: item.name }));
|
||||||
|
const placeholder = props.noSelectionString ?? "Select studio..."
|
||||||
|
|
||||||
const onChange = (selectedItems:ValueType<Option>) => {
|
const onChange = (selectedItems:ValueType<Option>) => {
|
||||||
const selectedIds = (Array.isArray(selectedItems) ? selectedItems : [selectedItems])
|
const selectedIds = getSelectedValues(selectedItems);
|
||||||
.map(item => item.value);
|
|
||||||
props.onSelect(normalizedData.filter(item => selectedIds.indexOf(item.id) !== -1));
|
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) => {
|
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 { data, loading: dataLoading } = StashService.useAllTagsForFilter();
|
||||||
const createTag = StashService.useTagCreate({name: ''});
|
const createTag = StashService.useTagCreate({name: ''});
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
const placeholder = props.noSelectionString ?? "Select tags..."
|
||||||
|
|
||||||
const tags = data?.allTags ?? [];
|
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));
|
props.onSelect([...tags, result.data.tagCreate].filter(item => selected.indexOf(item.id) !== -1));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
Toast({ content: (<span>Created tag: <b>{tagName}</b></span>) });
|
Toast.success({ content: (<span>Created tag: <b>{tagName}</b></span>) });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChange = (selectedItems:ValueType<Option>) => {
|
const onChange = (selectedItems:ValueType<Option>) => {
|
||||||
debugger;
|
const selected = getSelectedValues(selectedItems);
|
||||||
const selected = (Array.isArray(selectedItems) ? selectedItems : [selectedItems])
|
|
||||||
.map(item => item.value);
|
|
||||||
setSelectedIds(selected);
|
setSelectedIds(selected);
|
||||||
props.onSelect(tags.filter(item => selected.indexOf(item.id) !== -1));
|
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 selected = tags.filter(tag => selectedIds.indexOf(tag.id) !== -1).map(tag => ({value: tag.id, label: tag.name}));
|
||||||
const items:Option[] = tags.map(item => ({ value: item.id, label: item.name }));
|
const 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} />
|
isLoading={loading || dataLoading} items={items} onCreateOption={onCreate} selectedOptions={selected} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
|
const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
|
||||||
type,
|
type,
|
||||||
initialIds,
|
initialIds,
|
||||||
noSelectionString,
|
|
||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
items,
|
items,
|
||||||
@@ -124,6 +180,8 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
|
|||||||
onCreateOption,
|
onCreateOption,
|
||||||
creatable = false,
|
creatable = false,
|
||||||
isMulti = false,
|
isMulti = false,
|
||||||
|
onInputChange,
|
||||||
|
placeholder
|
||||||
}) => {
|
}) => {
|
||||||
const defaultValue = items.filter(item => initialIds?.indexOf(item.value) !== -1) ?? null;
|
const defaultValue = items.filter(item => initialIds?.indexOf(item.value) !== -1) ?? null;
|
||||||
|
|
||||||
@@ -135,12 +193,19 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
|
|||||||
isMulti: isMulti,
|
isMulti: isMulti,
|
||||||
defaultValue: defaultValue,
|
defaultValue: defaultValue,
|
||||||
noOptionsMessage: () => (type !== 'tags' ? 'None' : null),
|
noOptionsMessage: () => (type !== 'tags' ? 'None' : null),
|
||||||
placeholder: noSelectionString ?? "(No selection)"
|
placeholder: placeholder,
|
||||||
|
onInputChange
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
creatable
|
creatable
|
||||||
? <CreatableSelect {...props} isLoading={isLoading} isDisabled={isLoading} onCreateOption={onCreateOption} />
|
? <CreatableSelect {...props} isLoading={isLoading} isDisabled={isLoading} onCreateOption={onCreateOption} />
|
||||||
: <Select {...props} isLoading={isLoading} />
|
: <Select {...props} isLoading={isLoading} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const getSelectedValues = (selectedItems:ValueType<Option>) => (
|
||||||
|
(Array.isArray(selectedItems) ? selectedItems : [selectedItems])
|
||||||
|
.map(item => item.value)
|
||||||
|
);
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Badge } from 'react-bootstrap';
|
import { Badge } from 'react-bootstrap';
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { PerformerDataFragment, SceneMarkerDataFragment, TagDataFragment } from "../../core/generated-graphql";
|
import { PerformerDataFragment, SceneMarkerDataFragment, TagDataFragment } from "src/core/generated-graphql";
|
||||||
import { NavigationUtils } from "../../utils/navigation";
|
import { NavUtils, TextUtils } from "src/utils";
|
||||||
import { TextUtils } from "../../utils/text";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
tag?: Partial<TagDataFragment>;
|
tag?: Partial<TagDataFragment>;
|
||||||
@@ -14,18 +13,18 @@ interface IProps {
|
|||||||
export const TagLink: React.FC<IProps> = (props: IProps) => {
|
export const TagLink: React.FC<IProps> = (props: IProps) => {
|
||||||
let link: string = "#";
|
let link: string = "#";
|
||||||
let title: string = "";
|
let title: string = "";
|
||||||
if (!!props.tag) {
|
if (props.tag) {
|
||||||
link = NavigationUtils.makeTagScenesUrl(props.tag);
|
link = NavUtils.makeTagScenesUrl(props.tag);
|
||||||
title = props.tag.name || "";
|
title = props.tag.name || "";
|
||||||
} else if (!!props.performer) {
|
} else if (props.performer) {
|
||||||
link = NavigationUtils.makePerformerScenesUrl(props.performer);
|
link = NavUtils.makePerformerScenesUrl(props.performer);
|
||||||
title = props.performer.name || "";
|
title = props.performer.name || "";
|
||||||
} else if (!!props.marker) {
|
} else if (props.marker) {
|
||||||
link = NavigationUtils.makeSceneMarkerUrl(props.marker);
|
link = NavUtils.makeSceneMarkerUrl(props.marker);
|
||||||
title = `${props.marker.title} - ${TextUtils.secondsToTimestamp(props.marker.seconds || 0)}`;
|
title = `${props.marker.title} - ${TextUtils.secondsToTimestamp(props.marker.seconds || 0)}`;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
className="tag-item"
|
className="tag-item"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
>
|
>
|
||||||
|
|||||||
15
ui/v2.5/src/components/Shared/index.ts
Normal file
15
ui/v2.5/src/components/Shared/index.ts
Normal 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';
|
||||||
@@ -1,27 +1,27 @@
|
|||||||
import { Card } from 'react-bootstrap';
|
import { Card } from 'react-bootstrap';
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "../../core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
studio: GQL.StudioDataFragment;
|
studio: GQL.StudioDataFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StudioCard: React.FC<IProps> = (props: IProps) => {
|
export const StudioCard: React.FC<IProps> = ({ studio }) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="col-4"
|
className="col-4"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
to={`/studios/${props.studio.id}`}
|
to={`/studios/${studio.id}`}
|
||||||
className="studio previewable image"
|
className="studio previewable image"
|
||||||
style={{backgroundImage: `url(${props.studio.image_path})`}}
|
style={{backgroundImage: `url(${studio.image_path})`}}
|
||||||
/>
|
/>
|
||||||
<div className="card-section">
|
<div className="card-section">
|
||||||
<h4 className="text-truncate">
|
<h4 className="text-truncate">
|
||||||
{props.studio.name}
|
{studio.name}
|
||||||
</h4>
|
</h4>
|
||||||
<span>{props.studio.scene_count} scenes.</span>
|
<span>{studio.scene_count} scenes.</span>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,17 +2,16 @@ import { Form, Spinner, Table } from 'react-bootstrap';
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useParams, useHistory } from 'react-router-dom';
|
import { useParams, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import * as GQL from "../../../core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { StashService } from "../../../core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
import { ErrorUtils } from "../../../utils/errors";
|
import { ImageUtils, TableUtils } from "src/utils";
|
||||||
import { TableUtils } from "../../../utils/table";
|
import { DetailsEditNavbar } from "src/components/Shared";
|
||||||
import { DetailsEditNavbar } from "../../Shared/DetailsEditNavbar";
|
import { useToast} from "src/hooks";
|
||||||
import { ToastUtils } from "../../../utils/toasts";
|
|
||||||
import { ImageUtils } from "../../../utils/image";
|
|
||||||
|
|
||||||
export const Studio: React.FC = () => {
|
export const Studio: React.FC = () => {
|
||||||
const { id = '' } = useParams();
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const Toast = useToast();
|
||||||
|
const { id = 'new' } = useParams();
|
||||||
const isNew = id === "new";
|
const isNew = id === "new";
|
||||||
|
|
||||||
// Editing state
|
// Editing state
|
||||||
@@ -58,11 +57,13 @@ export const Studio: React.FC = () => {
|
|||||||
setImage(this.result as string);
|
setImage(this.result as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageUtils.addPasteImageHook(onImageLoad);
|
ImageUtils.usePasteImage(onImageLoad);
|
||||||
|
|
||||||
if (!isNew && !isEditing) {
|
if (!isNew && !isEditing) {
|
||||||
if (!data || !data.findStudio || loading) { return <Spinner animation="border" variant="light" />; }
|
if (!data?.findStudio || loading)
|
||||||
if (!!error) { return <>error...</>; }
|
return <Spinner animation="border" variant="light" />;
|
||||||
|
if (error)
|
||||||
|
return <div>{error.message}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStudioInput() {
|
function getStudioInput() {
|
||||||
@@ -89,7 +90,7 @@ export const Studio: React.FC = () => {
|
|||||||
history.push(`/studios/${result.data.studioCreate.id}`);
|
history.push(`/studios/${result.data.studioCreate.id}`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,9 +100,9 @@ export const Studio: React.FC = () => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await StashService.queryMetadataAutoTag({ studios: [studio.id]});
|
await StashService.queryMetadataAutoTag({ studios: [studio.id]});
|
||||||
ToastUtils.success("Started auto tagging");
|
Toast.success({ content: "Started auto tagging" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,14 +110,14 @@ export const Studio: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
await deleteStudio();
|
await deleteStudio();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// redirect to studios page
|
// redirect to studios page
|
||||||
history.push(`/studios`);
|
history.push(`/studios`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onImageChange(event: React.FormEvent<HTMLInputElement>) {
|
function onImageChangeHandler(event: React.FormEvent<HTMLInputElement>) {
|
||||||
ImageUtils.onImageChange(event, onImageLoad);
|
ImageUtils.onImageChange(event, onImageLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +136,7 @@ export const Studio: React.FC = () => {
|
|||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onAutoTag={onAutoTag}
|
onAutoTag={onAutoTag}
|
||||||
onImageChange={onImageChange}
|
onImageChange={onImageChangeHandler}
|
||||||
/>
|
/>
|
||||||
<h1>
|
<h1>
|
||||||
{ !isEditing
|
{ !isEditing
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { FunctionComponent } from "react";
|
import React, { FunctionComponent } from "react";
|
||||||
import { QueryHookResult } from "react-apollo-hooks";
|
import { QueryHookResult } from "react-apollo-hooks";
|
||||||
import { FindStudiosQuery, FindStudiosVariables } from "../../core/generated-graphql";
|
import { FindStudiosQuery, FindStudiosVariables } from "src/core/generated-graphql";
|
||||||
import { ListHook } from "../../hooks/ListHook";
|
import { ListHook } from "src/hooks";
|
||||||
import { IBaseProps } from "../../models/base-props";
|
import { IBaseProps } from "src/models/base-props";
|
||||||
import { ListFilterModel } from "../../models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { DisplayMode, FilterMode } from "../../models/list-filter/types";
|
import { DisplayMode, FilterMode } from "src/models/list-filter/types";
|
||||||
import { StudioCard } from "./StudioCard";
|
import { StudioCard } from "./StudioCard";
|
||||||
|
|
||||||
interface IProps extends IBaseProps {}
|
interface IProps extends IBaseProps {}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Button, Form, Modal, Spinner } from 'react-bootstrap';
|
import { Button, Form, Spinner } from 'react-bootstrap';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "../../core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { StashService } from "../../core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
import { ErrorUtils } from "../../utils/errors";
|
import { NavUtils } from "src/utils";
|
||||||
import { NavigationUtils } from "../../utils/navigation";
|
import { Icon, Modal } from 'src/components/Shared';
|
||||||
import { ToastUtils } from "../../utils/toasts";
|
import { useToast } from 'src/hooks';
|
||||||
|
|
||||||
export const TagList: React.FC = () => {
|
export const TagList: React.FC = () => {
|
||||||
|
const Toast = useToast();
|
||||||
// Editing / New state
|
// Editing / New state
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [editingTag, setEditingTag] = useState<Partial<GQL.TagDataFragment> | null>(null);
|
const [editingTag, setEditingTag] = useState<Partial<GQL.TagDataFragment> | null>(null);
|
||||||
@@ -21,7 +21,8 @@ export const TagList: React.FC = () => {
|
|||||||
|
|
||||||
function getTagInput() {
|
function getTagInput() {
|
||||||
const tagInput: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = { name };
|
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;
|
return tagInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,59 +38,54 @@ export const TagList: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
if (editingTag && editingTag.id) {
|
if (editingTag && editingTag.id) {
|
||||||
await updateTag();
|
await updateTag();
|
||||||
ToastUtils.success("Updated tag");
|
Toast.success({ content: "Updated tag" });
|
||||||
} else {
|
} else {
|
||||||
await createTag();
|
await createTag();
|
||||||
ToastUtils.success("Created tag");
|
Toast.success({ content: "Created tag" });
|
||||||
}
|
}
|
||||||
setEditingTag(null);
|
setEditingTag(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onAutoTag(tag : GQL.TagDataFragment) {
|
async function onAutoTag(tag : GQL.TagDataFragment) {
|
||||||
if (!tag) {
|
if (!tag)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await StashService.queryMetadataAutoTag({ tags: [tag.id]});
|
await StashService.queryMetadataAutoTag({ tags: [tag.id]});
|
||||||
ToastUtils.success("Started auto tagging");
|
Toast.success({ content: "Started auto tagging" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDelete() {
|
async function onDelete() {
|
||||||
try {
|
try {
|
||||||
await deleteTag();
|
await deleteTag();
|
||||||
ToastUtils.success("Deleted tag");
|
Toast.success({ content: "Deleted tag" });
|
||||||
setDeletingTag(null);
|
setDeletingTag(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteAlert = (
|
const deleteAlert = (
|
||||||
<Modal
|
<Modal
|
||||||
onHide={() => {}}
|
onHide={() => {}}
|
||||||
show={!!deletingTag}
|
show={!!deletingTag}
|
||||||
|
icon="trash-alt"
|
||||||
|
accept={{ onClick: onDelete, variant: 'danger', text: 'Delete' }}
|
||||||
|
cancel={{ onClick: () => setDeletingTag(null) }}
|
||||||
>
|
>
|
||||||
<Modal.Body>
|
<span>Are you sure you want to delete {deletingTag && deletingTag.name}?</span>
|
||||||
<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>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!data || !data.allTags) { return <Spinner animation="border" variant="light" />; }
|
if (!data?.allTags)
|
||||||
if (!!error) { return <>{error.message}</>; }
|
return <Spinner animation="border" variant="light" />;
|
||||||
|
if (error)
|
||||||
|
return <div>{error.message}</div>;
|
||||||
|
|
||||||
const tagElements = data.allTags.map((tag) => {
|
const tagElements = data.allTags.map((tag) => {
|
||||||
return (
|
return (
|
||||||
@@ -99,13 +95,13 @@ export const TagList: React.FC = () => {
|
|||||||
<span onClick={() => setEditingTag(tag)}>{tag.name}</span>
|
<span onClick={() => setEditingTag(tag)}>{tag.name}</span>
|
||||||
<div style={{float: "right"}}>
|
<div style={{float: "right"}}>
|
||||||
<Button onClick={() => onAutoTag(tag)}>Auto Tag</Button>
|
<Button onClick={() => onAutoTag(tag)}>Auto Tag</Button>
|
||||||
<Link to={NavigationUtils.makeTagScenesUrl(tag)}>Scenes: {tag.scene_count}</Link>
|
<Link to={NavUtils.makeTagScenesUrl(tag)}>Scenes: {tag.scene_count}</Link>
|
||||||
<Link to={NavigationUtils.makeTagSceneMarkersUrl(tag)}>
|
<Link to={NavUtils.makeTagSceneMarkersUrl(tag)}>
|
||||||
Markers: {tag.scene_marker_count}
|
Markers: {tag.scene_marker_count}
|
||||||
</Link>
|
</Link>
|
||||||
<span>Total: {(tag.scene_count || 0) + (tag.scene_marker_count || 0)}</span>
|
<span>Total: {(tag.scene_count || 0) + (tag.scene_marker_count || 0)}</span>
|
||||||
<Button variant="danger" onClick={() => setDeletingTag(tag)}>
|
<Button variant="danger" onClick={() => setDeletingTag(tag)}>
|
||||||
<FontAwesomeIcon icon="trash-alt" color="danger" />
|
<Icon icon="trash-alt" color="danger" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,25 +114,20 @@ export const TagList: React.FC = () => {
|
|||||||
<Button variant="primary" style={{marginTop: "20px"}} onClick={() => setEditingTag({})}>New Tag</Button>
|
<Button variant="primary" style={{marginTop: "20px"}} onClick={() => setEditingTag({})}>New Tag</Button>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
onHide={() => {setEditingTag(null)}}
|
show={!!editingTag}
|
||||||
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>
|
<Form.Group controlId="tag-name">
|
||||||
{ editingTag && editingTag.id ? "Edit Tag" : "New Tag" }
|
<Form.Label>Name</Form.Label>
|
||||||
</Modal.Header>
|
<Form.Control
|
||||||
<Modal.Body>
|
onChange={(newValue: any) => setName(newValue.target.value)}
|
||||||
<Form.Group controlId="tag-name">
|
defaultValue={(editingTag && editingTag.name) || ''}
|
||||||
<Form.Label>Name</Form.Label>
|
/>
|
||||||
<Form.Control
|
</Form.Group>
|
||||||
onChange={(newValue: any) => setName(newValue.target.value)}
|
|
||||||
defaultValue={(editingTag && editingTag.name) || ''}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
</Modal.Body>
|
|
||||||
<Modal.Footer>
|
|
||||||
<Button onClick={() => onEdit()}>{editingTag && editingTag.id ? "Update" : "Create"}</Button>
|
|
||||||
</Modal.Footer>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{tagElements}
|
{tagElements}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { FunctionComponent, useRef, useState, useEffect } from "react";
|
import React, { FunctionComponent, useRef, useState, useEffect } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "../../core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { VideoHoverHook } from "../../hooks/VideoHover";
|
import { StashService } from "src/core/StashService";
|
||||||
import { TextUtils } from "../../utils/text";
|
import { VideoHoverHook } from "src/hooks";
|
||||||
import { NavigationUtils } from "../../utils/navigation";
|
import { TextUtils, NavUtils } from "src/utils";
|
||||||
import { StashService } from "../../core/StashService";
|
|
||||||
|
|
||||||
interface IWallItemProps {
|
interface IWallItemProps {
|
||||||
scene?: GQL.SlimSceneDataFragment;
|
scene?: GQL.SlimSceneDataFragment;
|
||||||
@@ -55,31 +54,31 @@ export const WallItem: FunctionComponent<IWallItemProps> = (props: IWallItemProp
|
|||||||
}
|
}
|
||||||
|
|
||||||
let linkSrc: string = "#";
|
let linkSrc: string = "#";
|
||||||
if (props.clickHandler === undefined) {
|
if (props.clickHandler) {
|
||||||
if (props.scene !== undefined) {
|
if (props.scene) {
|
||||||
linkSrc = `/scenes/${props.scene.id}`;
|
linkSrc = `/scenes/${props.scene.id}`;
|
||||||
} else if (props.sceneMarker !== undefined) {
|
} else if (props.sceneMarker) {
|
||||||
linkSrc = NavigationUtils.makeSceneMarkerUrl(props.sceneMarker);
|
linkSrc = NavUtils.makeSceneMarkerUrl(props.sceneMarker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTransitionEnd(event: React.TransitionEvent<HTMLDivElement>) {
|
function onTransitionEnd(event: React.TransitionEvent<HTMLDivElement>) {
|
||||||
const target = (event.target as any);
|
const target = event.currentTarget;
|
||||||
if (target.classList.contains("double-scale")) {
|
if (target.classList.contains("double-scale") && target.parentElement) {
|
||||||
target.parentElement.style.zIndex = 10;
|
target.parentElement.style.zIndex = '10';
|
||||||
} else {
|
} else if(target.parentElement) {
|
||||||
target.parentElement.style.zIndex = null;
|
target.parentElement.style.zIndex = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!!props.sceneMarker) {
|
if (props.sceneMarker) {
|
||||||
setPreviewPath(props.sceneMarker.preview);
|
setPreviewPath(props.sceneMarker.preview);
|
||||||
setTitle(`${props.sceneMarker!.title} - ${TextUtils.secondsToTimestamp(props.sceneMarker.seconds)}`);
|
setTitle(`${props.sceneMarker!.title} - ${TextUtils.secondsToTimestamp(props.sceneMarker.seconds)}`);
|
||||||
const thisTags = props.sceneMarker.tags.map((tag) => (<span key={tag.id}>{tag.name}</span>));
|
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>);
|
thisTags.unshift(<span key={props.sceneMarker.primary_tag.id}>{props.sceneMarker.primary_tag.name}</span>);
|
||||||
setTags(thisTags);
|
setTags(thisTags);
|
||||||
} else if (!!props.scene) {
|
} else if (props.scene) {
|
||||||
setPreviewPath(props.scene.paths.webp || "");
|
setPreviewPath(props.scene.paths.webp || "");
|
||||||
setScreenshotPath(props.scene.paths.screenshot || "");
|
setScreenshotPath(props.scene.paths.screenshot || "");
|
||||||
setTitle(props.scene.title || "");
|
setTitle(props.scene.title || "");
|
||||||
@@ -123,7 +122,7 @@ export const WallItem: FunctionComponent<IWallItemProps> = (props: IWallItemProp
|
|||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
{tags}
|
{tags}
|
||||||
</div> : undefined
|
</div> : ''
|
||||||
}
|
}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { FunctionComponent, useState } from "react";
|
import React, { FunctionComponent, useState } from "react";
|
||||||
import * as GQL from "../../core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import "./Wall.scss";
|
|
||||||
import { WallItem } from "./WallItem";
|
import { WallItem } from "./WallItem";
|
||||||
|
import "./Wall.scss";
|
||||||
|
|
||||||
interface IWallPanelProps {
|
interface IWallPanelProps {
|
||||||
scenes?: GQL.SlimSceneDataFragment[];
|
scenes?: GQL.SlimSceneDataFragment[];
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { Button, Form, Modal, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
import { Button, Form, Modal, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { Icon } from 'src/components/Shared';
|
||||||
import { CriterionModifier } from "../../core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import { Criterion, CriterionType } from "../../models/list-filter/criteria/criterion";
|
import { Criterion, CriterionType } from "src/models/list-filter/criteria/criterion";
|
||||||
import { NoneCriterion } from "../../models/list-filter/criteria/none";
|
import { NoneCriterion } from "src/models/list-filter/criteria/none";
|
||||||
import { PerformersCriterion } from "../../models/list-filter/criteria/performers";
|
import { PerformersCriterion } from "src/models/list-filter/criteria/performers";
|
||||||
import { StudiosCriterion } from "../../models/list-filter/criteria/studios";
|
import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
|
||||||
import { TagsCriterion } from "../../models/list-filter/criteria/tags";
|
import { TagsCriterion } from "src/models/list-filter/criteria/tags";
|
||||||
import { makeCriteria } from "../../models/list-filter/criteria/utils";
|
import { makeCriteria } from "src/models/list-filter/criteria/utils";
|
||||||
import { ListFilterModel } from "../../models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { FilterSelect } from "../select/FilterSelect";
|
import { FilterSelect } from "src/components/Shared";
|
||||||
|
|
||||||
interface IAddFilterProps {
|
interface IAddFilterProps {
|
||||||
onAddCriterion: (criterion: Criterion, oldId?: string) => void;
|
onAddCriterion: (criterion: Criterion, oldId?: string) => void;
|
||||||
@@ -65,8 +65,8 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
|
|||||||
function onAddFilter() {
|
function onAddFilter() {
|
||||||
if (!Array.isArray(criterion.value) && defaultValue.current) {
|
if (!Array.isArray(criterion.value) && defaultValue.current) {
|
||||||
const value = defaultValue.current;
|
const value = defaultValue.current;
|
||||||
if (criterion.options && (value === undefined || value === "" || typeof value === "number")) {
|
if (criterion.options && (value === undefined || value === "" || typeof value === "number")) {
|
||||||
criterion.value = criterion.options[0];
|
criterion.value = criterion.options[0];
|
||||||
} else if (typeof value === "number" && value === undefined) {
|
} else if (typeof value === "number" && value === undefined) {
|
||||||
criterion.value = 0;
|
criterion.value = 0;
|
||||||
} else if (value === undefined) {
|
} else if (value === undefined) {
|
||||||
@@ -174,7 +174,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
|
|||||||
return (
|
return (
|
||||||
<Form.Group controlId="filter">
|
<Form.Group controlId="filter">
|
||||||
<Form.Label>Filter</Form.Label>
|
<Form.Label>Filter</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="select"
|
as="select"
|
||||||
onChange={onChangedCriteriaType}
|
onChange={onChangedCriteriaType}
|
||||||
value={criterion.type}>
|
value={criterion.type}>
|
||||||
@@ -193,14 +193,14 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
|
|||||||
placement="top"
|
placement="top"
|
||||||
overlay={<Tooltip id="filter-tooltip">Filter</Tooltip>}
|
overlay={<Tooltip id="filter-tooltip">Filter</Tooltip>}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onToggle()}
|
onClick={() => onToggle()}
|
||||||
active={isOpen}
|
active={isOpen}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon="filter" />
|
<Icon icon="filter" />
|
||||||
</Button>
|
</Button>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
show={isOpen}
|
show={isOpen}
|
||||||
onHide={() => onToggle()}>
|
onHide={() => onToggle()}>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import React, { SyntheticEvent, useCallback, useState } from "react";
|
import React, { SyntheticEvent, useCallback, useState } from "react";
|
||||||
import { Badge, Button, ButtonGroup, Dropdown, Form, OverlayTrigger, Tooltip } from 'react-bootstrap';
|
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 { Icon } from 'src/components/Shared';
|
||||||
import { ListFilterModel } from "../../models/list-filter/filter";
|
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||||
import { DisplayMode } from "../../models/list-filter/types";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
import { AddFilter } from "./AddFilter";
|
import { AddFilter } from "./AddFilter";
|
||||||
|
|
||||||
interface IListFilterOperation {
|
interface IListFilterOperation {
|
||||||
@@ -114,7 +114,7 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
|
|||||||
active={props.filter.displayMode === option}
|
active={props.filter.displayMode === option}
|
||||||
onClick={() => onChangeDisplayMode(option)}
|
onClick={() => onChangeDisplayMode(option)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={getIcon(option)} />
|
<Icon icon={getIcon(option)} />
|
||||||
</Button>
|
</Button>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
));
|
));
|
||||||
@@ -129,7 +129,7 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
|
|||||||
>
|
>
|
||||||
{criterion.getLabel()}
|
{criterion.getLabel()}
|
||||||
<Button onClick={() => onRemoveCriterionTag(criterion)}>
|
<Button onClick={() => onRemoveCriterionTag(criterion)}>
|
||||||
<FontAwesomeIcon icon="times" />
|
<Icon icon="times" />
|
||||||
</Button>
|
</Button>
|
||||||
</Badge>
|
</Badge>
|
||||||
));
|
));
|
||||||
@@ -176,7 +176,7 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
|
|||||||
<Dropdown>
|
<Dropdown>
|
||||||
<Dropdown.Toggle variant="secondary" id="more-menu">
|
<Dropdown.Toggle variant="secondary" id="more-menu">
|
||||||
<Button>
|
<Button>
|
||||||
<FontAwesomeIcon icon="ellipsis-h" />
|
<Icon icon="ellipsis-h" />
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown.Toggle>
|
</Dropdown.Toggle>
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
@@ -191,7 +191,7 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
|
|||||||
if (props.onChangeZoom) {
|
if (props.onChangeZoom) {
|
||||||
props.onChangeZoom(v);
|
props.onChangeZoom(v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderZoom() {
|
function maybeRenderZoom() {
|
||||||
if (props.onChangeZoom) {
|
if (props.onChangeZoom) {
|
||||||
@@ -240,7 +240,7 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
|
|||||||
<Tooltip id="sort-direction-tooltip">{props.filter.sortDirection === "asc" ? "Ascending" : "Descending"}</Tooltip>
|
<Tooltip id="sort-direction-tooltip">{props.filter.sortDirection === "asc" ? "Ascending" : "Descending"}</Tooltip>
|
||||||
}>
|
}>
|
||||||
<Button onClick={onChangeSortDirection}>
|
<Button onClick={onChangeSortDirection}>
|
||||||
<FontAwesomeIcon icon={props.filter.sortDirection === "asc" ? "caret-up" : "caret-down"} />
|
<Icon icon={props.filter.sortDirection === "asc" ? "caret-up" : "caret-down"} />
|
||||||
</Button>
|
</Button>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Card } from 'react-bootstrap';
|
import { Card } from 'react-bootstrap';
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "../../core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { TextUtils } from "../../utils/text";
|
import { NavUtils, TextUtils } from "src/utils";
|
||||||
import { NavigationUtils } from "../../utils/navigation";
|
|
||||||
|
|
||||||
interface IPerformerCardProps {
|
interface IPerformerCardProps {
|
||||||
performer: GQL.PerformerDataFragment;
|
performer: GQL.PerformerDataFragment;
|
||||||
@@ -12,7 +11,7 @@ interface IPerformerCardProps {
|
|||||||
|
|
||||||
export const PerformerCard: React.FC<IPerformerCardProps> = (props: IPerformerCardProps) => {
|
export const PerformerCard: React.FC<IPerformerCardProps> = (props: IPerformerCardProps) => {
|
||||||
const age = TextUtils.age(props.performer.birthdate, props.ageFromDate);
|
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() {
|
function maybeRenderFavoriteBanner() {
|
||||||
if (props.performer.favorite === false) { return; }
|
if (props.performer.favorite === false) { return; }
|
||||||
@@ -37,7 +36,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = (props: IPerformerCa
|
|||||||
{props.performer.name}
|
{props.performer.name}
|
||||||
</h4>
|
</h4>
|
||||||
{age !== 0 ? <div>{ageString}</div> : ''}
|
{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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -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 React, { useEffect, useState } from "react";
|
||||||
import * as GQL from "../../../core/generated-graphql";
|
import { Button, Form, Spinner, Table } from 'react-bootstrap';
|
||||||
import { StashService } from "../../../core/StashService";
|
import _ from "lodash";
|
||||||
import { IBaseProps } from "../../../models";
|
import { useParams, useHistory } from 'react-router-dom';
|
||||||
import { ErrorUtils } from "../../../utils/errors";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { TableUtils } from "../../../utils/table";
|
import { StashService } from "src/core/StashService";
|
||||||
import { ScrapePerformerSuggest } from "../../select/ScrapePerformerSuggest";
|
import { DetailsEditNavbar, Icon, Modal, ScrapePerformerSuggest } from "src/components/Shared";
|
||||||
import { DetailsEditNavbar } from "../../Shared/DetailsEditNavbar";
|
import { ImageUtils, TableUtils } from 'src/utils'
|
||||||
import { ToastUtils } from "../../../utils/toasts";
|
import { useToast } from 'src/hooks';
|
||||||
import { EditableTextUtils } from "../../../utils/editabletext";
|
|
||||||
import { ImageUtils } from "../../../utils/image";
|
|
||||||
|
|
||||||
interface IPerformerProps extends IBaseProps {}
|
export const Performer: React.FC = () => {
|
||||||
|
const Toast = useToast();
|
||||||
export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) => {
|
const history = useHistory();
|
||||||
const isNew = props.match.params.id === "new";
|
const { id = 'new' } = useParams();
|
||||||
|
const isNew = id === "new";
|
||||||
|
|
||||||
// Editing state
|
// Editing state
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
||||||
@@ -52,7 +48,7 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
|
|||||||
const Scrapers = StashService.useListPerformerScrapers();
|
const Scrapers = StashService.useListPerformerScrapers();
|
||||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.ListPerformerScrapersListPerformerScrapers[]>([]);
|
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 updatePerformer = StashService.usePerformerUpdate(getPerformerInput() as GQL.PerformerUpdateInput);
|
||||||
const createPerformer = StashService.usePerformerCreate(getPerformerInput() as GQL.PerformerCreateInput);
|
const createPerformer = StashService.usePerformerCreate(getPerformerInput() as GQL.PerformerCreateInput);
|
||||||
const deletePerformer = StashService.usePerformerDestroy(getPerformerInput() as GQL.PerformerDestroyInput);
|
const deletePerformer = StashService.usePerformerDestroy(getPerformerInput() as GQL.PerformerDestroyInput);
|
||||||
@@ -80,7 +76,8 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(loading);
|
setIsLoading(loading);
|
||||||
if (!data || !data.findPerformer || !!error) { return; }
|
if (!data || !data.findPerformer || error)
|
||||||
|
return;
|
||||||
setPerformer(data.findPerformer);
|
setPerformer(data.findPerformer);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
@@ -98,8 +95,8 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
|
|||||||
setImage(this.result as string);
|
setImage(this.result as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageUtils.addPasteImageHook(onImageLoad);
|
ImageUtils.usePasteImage(onImageLoad);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
var newQueryableScrapers : GQL.ListPerformerScrapersListPerformerScrapers[] = [];
|
var newQueryableScrapers : GQL.ListPerformerScrapersListPerformerScrapers[] = [];
|
||||||
|
|
||||||
@@ -113,10 +110,10 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
|
|||||||
|
|
||||||
}, [Scrapers.data]);
|
}, [Scrapers.data]);
|
||||||
|
|
||||||
if ((!isNew && !isEditing && (!data || !data.findPerformer)) || isLoading) {
|
if ((!isNew && !isEditing && !data?.findPerformer) || isLoading)
|
||||||
return <Spinner animation="border" variant="light" />;
|
return <Spinner animation="border" variant="light" />;
|
||||||
}
|
if (error)
|
||||||
if (!!error) { return <>error...</>; }
|
return <div>{error.message}</div>;
|
||||||
|
|
||||||
function getPerformerInput() {
|
function getPerformerInput() {
|
||||||
const performerInput: Partial<GQL.PerformerCreateInput | GQL.PerformerUpdateInput> = {
|
const performerInput: Partial<GQL.PerformerCreateInput | GQL.PerformerUpdateInput> = {
|
||||||
@@ -140,7 +137,7 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!isNew) {
|
if (!isNew) {
|
||||||
(performerInput as GQL.PerformerUpdateInput).id = props.match.params.id;
|
(performerInput as GQL.PerformerUpdateInput).id = id;
|
||||||
}
|
}
|
||||||
return performerInput;
|
return performerInput;
|
||||||
}
|
}
|
||||||
@@ -154,10 +151,10 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
|
|||||||
} else {
|
} else {
|
||||||
const result = await createPerformer();
|
const result = await createPerformer();
|
||||||
setPerformer(result.data.performerCreate);
|
setPerformer(result.data.performerCreate);
|
||||||
props.history.push(`/performers/${result.data.performerCreate.id}`);
|
history.push(`/performers/${result.data.performerCreate.id}`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -167,12 +164,12 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
|
|||||||
try {
|
try {
|
||||||
await deletePerformer();
|
await deletePerformer();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
||||||
// redirect to performers page
|
// redirect to performers page
|
||||||
props.history.push(`/performers`);
|
history.push('/performers');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onAutoTag() {
|
async function onAutoTag() {
|
||||||
@@ -181,13 +178,13 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await StashService.queryMetadataAutoTag({ performers: [performer.id]});
|
await StashService.queryMetadataAutoTag({ performers: [performer.id]});
|
||||||
ToastUtils.success("Started auto tagging");
|
Toast.success({ content: "Started auto tagging" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onImageChange(event: React.FormEvent<HTMLInputElement>) {
|
function onImageChangeHandler(event: React.FormEvent<HTMLInputElement>) {
|
||||||
ImageUtils.onImageChange(event, onImageLoad);
|
ImageUtils.onImageChange(event, onImageLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,38 +193,39 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getQueryScraperPerformerInput() {
|
function getQueryScraperPerformerInput() {
|
||||||
if (!scrapePerformerDetails) {
|
if (!scrapePerformerDetails)
|
||||||
return {};
|
return {};
|
||||||
}
|
|
||||||
|
|
||||||
let ret = _.clone(scrapePerformerDetails);
|
const { __typename, ...ret } = scrapePerformerDetails;
|
||||||
delete ret.__typename;
|
return ret;
|
||||||
return ret as GQL.ScrapedPerformerInput;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onScrapePerformer() {
|
async function onScrapePerformer() {
|
||||||
setIsDisplayingScraperDialog(undefined);
|
setIsDisplayingScraperDialog(undefined);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
if (!scrapePerformerDetails || !isDisplayingScraperDialog) { return; }
|
if (!scrapePerformerDetails || !isDisplayingScraperDialog)
|
||||||
|
return;
|
||||||
const result = await StashService.queryScrapePerformer(isDisplayingScraperDialog.id, getQueryScraperPerformerInput());
|
const result = await StashService.queryScrapePerformer(isDisplayingScraperDialog.id, getQueryScraperPerformerInput());
|
||||||
if (!result.data || !result.data.scrapePerformer) { return; }
|
if (!result?.data?.scrapePerformer)
|
||||||
|
return;
|
||||||
updatePerformerEditState(result.data.scrapePerformer);
|
updatePerformerEditState(result.data.scrapePerformer);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onScrapePerformerURL() {
|
async function onScrapePerformerURL() {
|
||||||
if (!url) { return; }
|
if (!url)
|
||||||
|
return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await StashService.queryScrapePerformerURL(url);
|
const result = await StashService.queryScrapePerformerURL(url);
|
||||||
if (!result.data || !result.data.scrapePerformerURL) { return; }
|
if (!result.data || !result.data.scrapePerformerURL) { return; }
|
||||||
updatePerformerEditState(result.data.scrapePerformerURL);
|
updatePerformerEditState(result.data.scrapePerformerURL);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -248,31 +246,24 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
|
|||||||
<Modal
|
<Modal
|
||||||
show={!!isDisplayingScraperDialog}
|
show={!!isDisplayingScraperDialog}
|
||||||
onHide={() => setIsDisplayingScraperDialog(undefined)}
|
onHide={() => setIsDisplayingScraperDialog(undefined)}
|
||||||
|
header="Scrape"
|
||||||
|
accept={{ onClick: onScrapePerformer, text: "Scrape" }}
|
||||||
>
|
>
|
||||||
<Modal.Header>
|
<div className="dialog-content">
|
||||||
Scrape
|
<ScrapePerformerSuggest
|
||||||
</Modal.Header>
|
placeholder="Performer name"
|
||||||
<Modal.Body>
|
scraperId={isDisplayingScraperDialog ? isDisplayingScraperDialog.id : ""}
|
||||||
<div className="dialog-content">
|
onSelectPerformer={(query) => setScrapePerformerDetails(query)}
|
||||||
<ScrapePerformerSuggest
|
/>
|
||||||
placeholder="Performer name"
|
</div>
|
||||||
style={{width: "100%"}}
|
|
||||||
scraperId={isDisplayingScraperDialog ? isDisplayingScraperDialog.id : ""}
|
|
||||||
onSelectPerformer={(query) => setScrapePerformerDetails(query)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Modal.Body>
|
|
||||||
<Modal.Footer>
|
|
||||||
<Button onClick={() => onScrapePerformer()}>Scrape</Button>
|
|
||||||
</Modal.Footer>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function urlScrapable(url: string) : boolean {
|
function urlScrapable(url: string) {
|
||||||
return !!url && !!Scrapers.data && Scrapers.data.listPerformerScrapers && Scrapers.data.listPerformerScrapers.some((s) => {
|
return !!url && (Scrapers?.data?.listPerformerScrapers ?? []).some(s => (
|
||||||
return !!s.performer && !!s.performer.urls && s.performer.urls.some((u) => { return url.includes(u); });
|
(s?.performer?.urls ?? []).some(u => url.includes(u))
|
||||||
});
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderScrapeButton() {
|
function maybeRenderScrapeButton() {
|
||||||
@@ -280,10 +271,10 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
id="scrape-url-button"
|
id="scrape-url-button"
|
||||||
onClick={() => onScrapePerformerURL()}>
|
onClick={() => onScrapePerformerURL()}>
|
||||||
<FontAwesomeIcon icon="file-upload" />
|
<Icon icon="file-upload" />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -292,13 +283,17 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
|
|||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td id="url-field">
|
<td id="url-field">
|
||||||
URL
|
URL
|
||||||
{maybeRenderScrapeButton()}
|
{maybeRenderScrapeButton()}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{EditableTextUtils.renderInputGroup({
|
<Form.Control
|
||||||
value: url, isEditing, onChange: setUrl, placeholder: "URL"
|
value={url}
|
||||||
})}
|
readOnly={!isEditing}
|
||||||
|
plaintext={!isEditing}
|
||||||
|
placeholder="URL"
|
||||||
|
onChange={(event: React.FormEvent<HTMLInputElement>) => setUrl(event.currentTarget.value) }
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -319,26 +314,31 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
|
|||||||
onToggleEdit={() => { setIsEditing(!isEditing); updatePerformerEditState(performer); }}
|
onToggleEdit={() => { setIsEditing(!isEditing); updatePerformerEditState(performer); }}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onImageChange={onImageChange}
|
onImageChange={onImageChangeHandler}
|
||||||
scrapers={queryableScrapers}
|
scrapers={queryableScrapers}
|
||||||
onDisplayScraperDialog={onDisplayFreeOnesDialog}
|
onDisplayScraperDialog={onDisplayFreeOnesDialog}
|
||||||
onAutoTag={onAutoTag}
|
onAutoTag={onAutoTag}
|
||||||
/>
|
/>
|
||||||
<h1>
|
<h1>
|
||||||
{ !isEditing
|
{ <Form.Control
|
||||||
? <span>{name}</span>
|
readOnly={!isEditing}
|
||||||
: <Form.Control
|
plaintext={!isEditing}
|
||||||
defaultValue={name}
|
defaultValue={name}
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
onChange={(event: any) => setName(event.target.value)} />
|
onChange={(event: any) => setName(event.target.value)}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
</h1>
|
</h1>
|
||||||
<h6>
|
<h6>
|
||||||
<Form.Group className="aliases-field" controlId="aliases">
|
<Form.Group className="aliases-field" controlId="aliases">
|
||||||
<Form.Label>Aliases:</Form.Label>
|
<Form.Label>Aliases:</Form.Label>
|
||||||
{EditableTextUtils.renderInputGroup({
|
<Form.Control
|
||||||
value: aliases, isEditing: isEditing, placeholder: "Aliases", onChange: setAliases
|
value={aliases}
|
||||||
})}
|
readOnly={!isEditing}
|
||||||
|
plaintext={!isEditing}
|
||||||
|
placeholder="Aliases"
|
||||||
|
onChange={(event: React.FormEvent<HTMLInputElement>) => setAliases(event.currentTarget.value) }
|
||||||
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</h6>
|
</h6>
|
||||||
<div>
|
<div>
|
||||||
@@ -348,7 +348,7 @@ export const Performer: React.FC<IPerformerProps> = (props: IPerformerProps) =>
|
|||||||
className={favorite ? "favorite" : undefined}
|
className={favorite ? "favorite" : undefined}
|
||||||
onClick={() => setFavorite(!favorite)}
|
onClick={() => setFavorite(!favorite)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon="heart" />
|
<Icon icon="heart" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { FunctionComponent } from "react";
|
import React from "react";
|
||||||
import { QueryHookResult } from "react-apollo-hooks";
|
import { QueryHookResult } from "react-apollo-hooks";
|
||||||
import { FindPerformersQuery, FindPerformersVariables } from "../../core/generated-graphql";
|
import { FindPerformersQuery, FindPerformersVariables } from "src/core/generated-graphql";
|
||||||
import { ListHook } from "../../hooks/ListHook";
|
import { StashService } from "src/core/StashService";
|
||||||
import { IBaseProps } from "../../models/base-props";
|
import { ListHook } from "src/hooks";
|
||||||
import { ListFilterModel } from "../../models/list-filter/filter";
|
import { IBaseProps } from "src/models/base-props";
|
||||||
import { DisplayMode, FilterMode } from "../../models/list-filter/types";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
import { DisplayMode, FilterMode } from "src/models/list-filter/types";
|
||||||
import { PerformerCard } from "./PerformerCard";
|
import { PerformerCard } from "./PerformerCard";
|
||||||
import { PerformerListTable } from "./PerformerListTable";
|
import { PerformerListTable } from "./PerformerListTable";
|
||||||
import { StashService } from "../../core/StashService";
|
|
||||||
|
|
||||||
interface IPerformerListProps extends IBaseProps {}
|
interface IPerformerListProps extends IBaseProps {}
|
||||||
|
|
||||||
export const PerformerList: FunctionComponent<IPerformerListProps> = (props: IPerformerListProps) => {
|
export const PerformerList: React.FC<IPerformerListProps> = (props: IPerformerListProps) => {
|
||||||
const otherOperations = [
|
const otherOperations = [
|
||||||
{
|
{
|
||||||
text: "Open Random",
|
text: "Open Random",
|
||||||
onClick: getRandom,
|
onClick: getRandom,
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const listData = ListHook.useList({
|
const listData = ListHook.useList({
|
||||||
filterMode: FilterMode.Performers,
|
filterMode: FilterMode.Performers,
|
||||||
props,
|
props,
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button, Table } from 'react-bootstrap';
|
import { Button, Table } from 'react-bootstrap';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "../../core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { NavigationUtils } from "../../utils/navigation";
|
import { Icon } from 'src/components/Shared';
|
||||||
|
import { NavUtils } from "src/utils";
|
||||||
|
|
||||||
interface IPerformerListTableProps {
|
interface IPerformerListTableProps {
|
||||||
performers: GQL.PerformerDataFragment[];
|
performers: GQL.PerformerDataFragment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PerformerListTable: React.FC<IPerformerListTableProps> = (props: IPerformerListTableProps) => {
|
export const PerformerListTable: React.FC<IPerformerListTableProps> = (props: IPerformerListTableProps) => {
|
||||||
|
|
||||||
function maybeRenderFavoriteHeart(performer : GQL.PerformerDataFragment) {
|
function maybeRenderFavoriteHeart(performer : GQL.PerformerDataFragment) {
|
||||||
if (!performer.favorite) { return; }
|
if (!performer.favorite) { return; }
|
||||||
return (
|
return (
|
||||||
<Button disabled className="favorite">
|
<Button disabled className="favorite">
|
||||||
<FontAwesomeIcon icon="heart" />
|
<Icon icon="heart" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (props: IP
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className="performer-list-thumbnail"
|
className="performer-list-thumbnail"
|
||||||
to={`/performers/${performer.id}`}
|
to={`/performers/${performer.id}`}
|
||||||
style={style}/>
|
style={style}/>
|
||||||
@@ -59,7 +59,7 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (props: IP
|
|||||||
{maybeRenderFavoriteHeart(performer)}
|
{maybeRenderFavoriteHeart(performer)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Link to={NavigationUtils.makePerformerScenesUrl(performer)}>
|
<Link to={NavUtils.makePerformerScenesUrl(performer)}>
|
||||||
<h6>{performer.scene_count}</h6>
|
<h6>{performer.scene_count}</h6>
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
@@ -97,4 +97,4 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (props: IP
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 React, { useState } from "react";
|
||||||
|
import { Button, ButtonGroup, Card, Form, Popover, OverlayTrigger } from 'react-bootstrap';
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "../../core/generated-graphql";
|
import cx from 'classnames';
|
||||||
import { VideoHoverHook } from "../../hooks/VideoHover";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { ColorUtils } from "../../utils/color";
|
import { StashService } from "src/core/StashService";
|
||||||
import { TextUtils } from "../../utils/text";
|
import { VideoHoverHook } from "src/hooks";
|
||||||
import { TagLink } from "../Shared/TagLink";
|
import { Icon, TagLink } from 'src/components/Shared';
|
||||||
import { ZoomUtils } from "../../utils/zoom";
|
import { TextUtils } from "src/utils";
|
||||||
import { StashService } from "../../core/StashService";
|
|
||||||
|
|
||||||
interface ISceneCardProps {
|
interface ISceneCardProps {
|
||||||
scene: GQL.SlimSceneDataFragment;
|
scene: GQL.SlimSceneDataFragment;
|
||||||
@@ -20,14 +18,14 @@ interface ISceneCardProps {
|
|||||||
export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) => {
|
export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) => {
|
||||||
const [previewPath, setPreviewPath] = useState<string | undefined>(undefined);
|
const [previewPath, setPreviewPath] = useState<string | undefined>(undefined);
|
||||||
const videoHoverHook = VideoHoverHook.useVideoHover({resetOnMouseLeave: false});
|
const videoHoverHook = VideoHoverHook.useVideoHover({resetOnMouseLeave: false});
|
||||||
|
|
||||||
const config = StashService.useConfiguration();
|
const config = StashService.useConfiguration();
|
||||||
const showStudioAsText = !!config.data && !!config.data.configuration ? config.data.configuration.interface.showStudioAsText : false;
|
const showStudioAsText = !!config.data && !!config.data.configuration ? config.data.configuration.interface.showStudioAsText : false;
|
||||||
|
|
||||||
function maybeRenderRatingBanner() {
|
function maybeRenderRatingBanner() {
|
||||||
if (!props.scene.rating) { return; }
|
if (!props.scene.rating) { return; }
|
||||||
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}
|
RATING: {props.scene.rating}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -36,23 +34,21 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
|||||||
function maybeRenderSceneSpecsOverlay() {
|
function maybeRenderSceneSpecsOverlay() {
|
||||||
return (
|
return (
|
||||||
<div className={`scene-specs-overlay`}>
|
<div className={`scene-specs-overlay`}>
|
||||||
{!!props.scene.file.height ? <span className={`overlay-resolution`}> {TextUtils.resolution(props.scene.file.height)}</span> : undefined}
|
{props.scene.file.height ? <span className={`overlay-resolution`}> {TextUtils.resolution(props.scene.file.height)}</span> : ''}
|
||||||
{props.scene.file.duration !== undefined && props.scene.file.duration >= 1 ? TextUtils.secondsToTimestamp(props.scene.file.duration) : ""}
|
{props.scene.file.duration !== undefined && props.scene.file.duration >= 1 ? TextUtils.secondsToTimestamp(props.scene.file.duration) : ''}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderSceneStudioOverlay() {
|
function maybeRenderSceneStudioOverlay() {
|
||||||
if (!props.scene.studio) {
|
if (!props.scene.studio)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
let style: React.CSSProperties = {
|
let style: React.CSSProperties = {
|
||||||
backgroundImage: `url('${props.scene.studio.image_path}')`,
|
backgroundImage: `url('${props.scene.studio.image_path}')`,
|
||||||
};
|
};
|
||||||
|
|
||||||
let text = "";
|
let text = "";
|
||||||
|
|
||||||
if (showStudioAsText) {
|
if (showStudioAsText) {
|
||||||
style = {};
|
style = {};
|
||||||
text = props.scene.studio.name;
|
text = props.scene.studio.name;
|
||||||
@@ -71,7 +67,8 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderTagPopoverButton() {
|
function maybeRenderTagPopoverButton() {
|
||||||
if (props.scene.tags.length <= 0) { return; }
|
if (props.scene.tags.length <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
const popover = (
|
const popover = (
|
||||||
<Popover id="tag-popover">
|
<Popover id="tag-popover">
|
||||||
@@ -84,7 +81,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
|||||||
return (
|
return (
|
||||||
<OverlayTrigger trigger="hover" placement="bottom" overlay={popover}>
|
<OverlayTrigger trigger="hover" placement="bottom" overlay={popover}>
|
||||||
<Button>
|
<Button>
|
||||||
<FontAwesomeIcon icon="tag" />
|
<Icon icon="tag" />
|
||||||
{props.scene.tags.length}
|
{props.scene.tags.length}
|
||||||
</Button>
|
</Button>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
@@ -92,7 +89,8 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderPerformerPopoverButton() {
|
function maybeRenderPerformerPopoverButton() {
|
||||||
if (props.scene.performers.length <= 0) { return; }
|
if (props.scene.performers.length <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
const popover = (
|
const popover = (
|
||||||
<Popover id="performer-popover">
|
<Popover id="performer-popover">
|
||||||
@@ -116,7 +114,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
|||||||
return (
|
return (
|
||||||
<OverlayTrigger trigger="hover" placement="bottom" overlay={popover}>
|
<OverlayTrigger trigger="hover" placement="bottom" overlay={popover}>
|
||||||
<Button>
|
<Button>
|
||||||
<FontAwesomeIcon icon="user" />
|
<Icon icon="user" />
|
||||||
{props.scene.performers.length}
|
{props.scene.performers.length}
|
||||||
</Button>
|
</Button>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
@@ -140,7 +138,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
|||||||
return (
|
return (
|
||||||
<OverlayTrigger trigger="hover" placement="bottom" overlay={popover}>
|
<OverlayTrigger trigger="hover" placement="bottom" overlay={popover}>
|
||||||
<Button>
|
<Button>
|
||||||
<FontAwesomeIcon icon="tag" />
|
<Icon icon="tag" />
|
||||||
{props.scene.scene_markers.length}
|
{props.scene.scene_markers.length}
|
||||||
</Button>
|
</Button>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
@@ -182,31 +180,11 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
|||||||
return height > width;
|
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;
|
var shiftKey = false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={"col-4 " + ZoomUtils.classForZoom(props.zoomIndex)}
|
className={`col-4 zoom-${props.zoomIndex}`}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
>
|
>
|
||||||
@@ -217,13 +195,18 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
|||||||
onChange={() => props.onSelectedChanged(!props.selected, shiftKey)}
|
onChange={() => props.onSelectedChanged(!props.selected, shiftKey)}
|
||||||
onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => { shiftKey = event.shiftKey; event.stopPropagation(); } }
|
onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => { 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">
|
<div className="video-container">
|
||||||
{maybeRenderRatingBanner()}
|
{maybeRenderRatingBanner()}
|
||||||
{maybeRenderSceneSpecsOverlay()}
|
{maybeRenderSceneSpecsOverlay()}
|
||||||
{maybeRenderSceneStudioOverlay()}
|
{maybeRenderSceneStudioOverlay()}
|
||||||
<video className={getVideoClassName()} loop={true} poster={props.scene.paths.screenshot || ""} ref={videoHoverHook.videoEl}>
|
<video
|
||||||
{!!previewPath ? <source src={previewPath} /> : ""}
|
loop
|
||||||
|
className={cx('preview', {portrait: isPortrait()})}
|
||||||
|
poster={props.scene.paths.screenshot || ""}
|
||||||
|
ref={videoHoverHook.videoEl}
|
||||||
|
>
|
||||||
|
{previewPath ? <source src={previewPath} /> : ""}
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Card, Spinner, Tab, Tabs } from 'react-bootstrap';
|
import { Card, Spinner, Tab, Tabs } from 'react-bootstrap';
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import * as GQL from "../../../core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { StashService } from "../../../core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
import { IBaseProps } from "../../../models";
|
import { IBaseProps } from "src/models";
|
||||||
import { GalleryViewer } from "../../Galleries/GalleryViewer";
|
import { GalleryViewer } from "src/components/Galleries/GalleryViewer";
|
||||||
import { ScenePlayer } from "../ScenePlayer/ScenePlayer";
|
import { ScenePlayer } from "../ScenePlayer/ScenePlayer";
|
||||||
import { SceneDetailPanel } from "./SceneDetailPanel";
|
import { SceneDetailPanel } from "./SceneDetailPanel";
|
||||||
import { SceneEditPanel } from "./SceneEditPanel";
|
import { SceneEditPanel } from "./SceneEditPanel";
|
||||||
@@ -14,18 +14,15 @@ import { ScenePerformerPanel } from "./ScenePerformerPanel";
|
|||||||
|
|
||||||
interface ISceneProps extends IBaseProps {}
|
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 [timestamp, setTimestamp] = useState<number>(0);
|
||||||
const [autoplay, setAutoplay] = useState<boolean>(false);
|
const [autoplay, setAutoplay] = useState<boolean>(false);
|
||||||
const [scene, setScene] = useState<Partial<GQL.SceneDataFragment>>({});
|
const [scene, setScene] = useState<Partial<GQL.SceneDataFragment>>({});
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const { data, error, loading } = StashService.useFindScene(props.match.params.id);
|
const { data, error, loading } = StashService.useFindScene(props.match.params.id);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => (
|
||||||
setIsLoading(loading);
|
setScene(data?.findScene ?? {})
|
||||||
if (!data || !data.findScene || !!error) { return; }
|
), [data]);
|
||||||
setScene(StashService.nullToUndefined(data.findScene));
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const queryParams = queryString.parse(props.location.search);
|
const queryParams = queryString.parse(props.location.search);
|
||||||
@@ -42,12 +39,15 @@ export const Scene: FunctionComponent<ISceneProps> = (props: ISceneProps) => {
|
|||||||
setTimestamp(marker.seconds);
|
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"/>;
|
return <Spinner animation="border"/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error)
|
||||||
|
return <div>{error.message}</div>
|
||||||
|
|
||||||
const modifiedScene =
|
const modifiedScene =
|
||||||
Object.assign({scene_marker_tags: data.sceneMarkerTags}, scene) as GQL.SceneDataFragment; // TODO Hack from angular
|
Object.assign({scene_marker_tags: data.sceneMarkerTags}, scene) as GQL.SceneDataFragment; // TODO Hack from angular
|
||||||
if (!!error) { return <>error...</>; }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -82,9 +82,9 @@ export const Scene: FunctionComponent<ISceneProps> = (props: ISceneProps) => {
|
|||||||
<Tab
|
<Tab
|
||||||
eventKey="scene-edit-panel"
|
eventKey="scene-edit-panel"
|
||||||
title="Edit">
|
title="Edit">
|
||||||
<SceneEditPanel
|
<SceneEditPanel
|
||||||
scene={modifiedScene}
|
scene={modifiedScene}
|
||||||
onUpdate={(newScene) => setScene(newScene)}
|
onUpdate={(newScene) => setScene(newScene)}
|
||||||
onDelete={() => props.history.push("/scenes")}
|
onDelete={() => props.history.push("/scenes")}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import * as GQL from "../../../core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { TextUtils } from "../../../utils/text";
|
import { TextUtils } from "src/utils";
|
||||||
import { TagLink } from "../../Shared/TagLink";
|
import { TagLink } from "src/components/Shared";
|
||||||
import { SceneHelpers } from "../helpers";
|
import { SceneHelpers } from "../helpers";
|
||||||
|
|
||||||
interface ISceneDetailProps {
|
interface ISceneDetailProps {
|
||||||
@@ -38,9 +38,9 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props: ISceneDetai
|
|||||||
<h1>
|
<h1>
|
||||||
{!!props.scene.title ? props.scene.title : TextUtils.fileNameFromPath(props.scene.path)}
|
{!!props.scene.title ? props.scene.title : TextUtils.fileNameFromPath(props.scene.path)}
|
||||||
</h1>
|
</h1>
|
||||||
{!!props.scene.date ? <h4>{props.scene.date}</h4> : undefined}
|
{props.scene.date ? <h4>{props.scene.date}</h4> : ''}
|
||||||
{!!props.scene.rating ? <h6>Rating: {props.scene.rating}</h6> : undefined}
|
{props.scene.rating ? <h6>Rating: {props.scene.rating}</h6> : ''}
|
||||||
{!!props.scene.file.height ? <h6>Resolution: {TextUtils.resolution(props.scene.file.height)}</h6> : undefined}
|
{props.scene.file.height ? <h6>Resolution: {TextUtils.resolution(props.scene.file.height)}</h6> : ''}
|
||||||
{renderDetails()}
|
{renderDetails()}
|
||||||
{renderTags()}
|
{renderTags()}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import * as GQL from "../../../core/generated-graphql";
|
import { Collapse, Dropdown, DropdownButton, Form, Button, Spinner } from 'react-bootstrap';
|
||||||
import { StashService } from "../../../core/StashService";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { ErrorUtils } from "../../../utils/errors";
|
import { StashService } from "src/core/StashService";
|
||||||
import { ToastUtils } from "../../../utils/toasts";
|
import { FilterSelect, StudioSelect, SceneGallerySelect, Modal, Icon } from "src/components/Shared";
|
||||||
import { FilterSelect, StudioSelect } from "../../select/FilterSelect";
|
import { useToast } from 'src/hooks';
|
||||||
import { ValidGalleriesSelect } from "../../select/ValidGalleriesSelect";
|
import { ImageUtils } from 'src/utils';
|
||||||
import { ImageUtils } from "../../../utils/image";
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
||||||
import { Collapse, Dropdown, DropdownButton, Form, Button, Modal, Spinner } from 'react-bootstrap';
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
scene: GQL.SceneDataFragment;
|
scene: GQL.SceneDataFragment;
|
||||||
@@ -16,6 +13,7 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||||
|
const Toast = useToast();
|
||||||
const [title, setTitle] = useState<string | undefined>(undefined);
|
const [title, setTitle] = useState<string | undefined>(undefined);
|
||||||
const [details, setDetails] = useState<string | undefined>(undefined);
|
const [details, setDetails] = useState<string | undefined>(undefined);
|
||||||
const [url, setUrl] = 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);
|
setCoverImagePreview(props.scene.paths.screenshot);
|
||||||
}, [props.scene]);
|
}, [props.scene]);
|
||||||
|
|
||||||
ImageUtils.addPasteImageHook(onImageLoad);
|
ImageUtils.usePasteImage(onImageLoad);
|
||||||
|
|
||||||
function getSceneInput(): GQL.SceneUpdateInput {
|
function getSceneInput(): GQL.SceneUpdateInput {
|
||||||
return {
|
return {
|
||||||
@@ -99,9 +97,9 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
try {
|
try {
|
||||||
const result = await updateScene();
|
const result = await updateScene();
|
||||||
props.onUpdate(result.data.sceneUpdate);
|
props.onUpdate(result.data.sceneUpdate);
|
||||||
ToastUtils.success("Updated scene");
|
Toast.success({ content: "Updated scene" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -119,9 +117,9 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await deleteScene();
|
await deleteScene();
|
||||||
ToastUtils.success("Deleted scene");
|
Toast.success({ content: "Deleted scene" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
||||||
@@ -148,30 +146,19 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
function renderDeleteAlert() {
|
function renderDeleteAlert() {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
keyboard={false}
|
|
||||||
onHide={() => {}}
|
|
||||||
show={isDeleteAlertOpen}
|
show={isDeleteAlertOpen}
|
||||||
|
icon="trash-alt"
|
||||||
|
header="Delete Scene?"
|
||||||
|
accept={{ variant: 'danger', onClick: onDelete, text: "Delete" }}
|
||||||
|
cancel={{ onClick: () => setIsDeleteAlertOpen(false), text: "Cancel" }}
|
||||||
>
|
>
|
||||||
<Modal.Header>
|
<p>
|
||||||
<FontAwesomeIcon icon="trash-alt" />
|
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.
|
||||||
<span>Delete Scene?</span>
|
</p>
|
||||||
</Modal.Header>
|
<Form>
|
||||||
<Modal.Body>
|
<Form.Check checked={deleteFile} label="Delete file" onChange={() => setDeleteFile(!deleteFile)} />
|
||||||
<p>
|
<Form.Check checked={deleteGenerated} label="Delete generated supporting files" onChange={() => setDeleteGenerated(!deleteGenerated)} />
|
||||||
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>
|
|
||||||
<Form>
|
|
||||||
<Form.Check checked={deleteFile} label="Delete file" onChange={() => setDeleteFile(!deleteFile)} />
|
|
||||||
<Form.Check checked={deleteGenerated} label="Delete generated supporting files" onChange={() => setDeleteGenerated(!deleteGenerated)} />
|
|
||||||
</Form>
|
</Form>
|
||||||
</Modal.Body>
|
|
||||||
|
|
||||||
<Modal.Footer>
|
|
||||||
<div>
|
|
||||||
<Button variant="danger" onClick={onDelete}>Delete</Button>
|
|
||||||
<Button onClick={() => setIsDeleteAlertOpen(false)}>Cancel</Button>
|
|
||||||
</div>
|
|
||||||
</Modal.Footer>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -184,7 +171,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
function onCoverImageChange(event: React.FormEvent<HTMLInputElement>) {
|
function onCoverImageChange(event: React.FormEvent<HTMLInputElement>) {
|
||||||
ImageUtils.onImageChange(event, onImageLoad);
|
ImageUtils.onImageChange(event, onImageLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onScrapeClicked(scraper : GQL.ListSceneScrapersListSceneScrapers) {
|
async function onScrapeClicked(scraper : GQL.ListSceneScrapersListSceneScrapers) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -192,7 +179,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
if (!result.data || !result.data.scrapeScene) { return; }
|
if (!result.data || !result.data.scrapeScene) { return; }
|
||||||
updateSceneFromScrapedScene(result.data.scrapeScene);
|
updateSceneFromScrapedScene(result.data.scrapeScene);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -227,7 +214,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
if (!details && scene.details) {
|
if (!details && scene.details) {
|
||||||
setDetails(scene.details);
|
setDetails(scene.details);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!date && scene.date) {
|
if (!date && scene.date) {
|
||||||
setDate(scene.date);
|
setDate(scene.date);
|
||||||
}
|
}
|
||||||
@@ -235,7 +222,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
if (!url && scene.url) {
|
if (!url && scene.url) {
|
||||||
setUrl(scene.url);
|
setUrl(scene.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!studioId && scene.studio && scene.studio.id) {
|
if (!studioId && scene.studio && scene.studio.id) {
|
||||||
setStudioId(scene.studio.id);
|
setStudioId(scene.studio.id);
|
||||||
}
|
}
|
||||||
@@ -271,7 +258,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
if (!result.data || !result.data.scrapeSceneURL) { return; }
|
if (!result.data || !result.data.scrapeSceneURL) { return; }
|
||||||
updateSceneFromScrapedScene(result.data.scrapeSceneURL);
|
updateSceneFromScrapedScene(result.data.scrapeSceneURL);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -282,10 +269,10 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
id="scrape-url-button"
|
id="scrape-url-button"
|
||||||
onClick={onScrapeSceneURL}>
|
onClick={onScrapeSceneURL}>
|
||||||
<FontAwesomeIcon icon="file-download" />
|
<Icon icon="file-download" />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -343,10 +330,10 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
|
|
||||||
<Form.Group controlId="gallery">
|
<Form.Group controlId="gallery">
|
||||||
<Form.Label>Gallery</Form.Label>
|
<Form.Label>Gallery</Form.Label>
|
||||||
<ValidGalleriesSelect
|
<SceneGallerySelect
|
||||||
sceneId={props.scene.id}
|
sceneId={props.scene.id}
|
||||||
initialId={galleryId}
|
initialId={galleryId}
|
||||||
onSelectItem={(item) => setGalleryId(item ? item.id : undefined)}
|
onSelect={(item) => setGalleryId(item ? item.id : undefined)}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
@@ -370,7 +357,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label onClick={() => setIsCoverImageOpen(!isCoverImageOpen)}>
|
<label onClick={() => setIsCoverImageOpen(!isCoverImageOpen)}>
|
||||||
<FontAwesomeIcon icon={isCoverImageOpen ? "chevron-down" : "chevron-right"} />
|
<Icon icon={isCoverImageOpen ? "chevron-down" : "chevron-right"} />
|
||||||
<span>Cover Image</span>
|
<span>Cover Image</span>
|
||||||
</label>
|
</label>
|
||||||
<Collapse in={isCoverImageOpen}>
|
<Collapse in={isCoverImageOpen}>
|
||||||
@@ -382,7 +369,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<Button className="edit-button" variant="primary" onClick={onSave}>Save</Button>
|
<Button className="edit-button" variant="primary" onClick={onSave}>Save</Button>
|
||||||
<Button className="edit-button" variant="danger" onClick={() => setIsDeleteAlertOpen(true)}>Delete</Button>
|
<Button className="edit-button" variant="danger" onClick={() => setIsDeleteAlertOpen(true)}>Delete</Button>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
import { Table } from 'react-bootstrap';
|
import { Table } from 'react-bootstrap';
|
||||||
import React, { FunctionComponent } from "react";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import * as GQL from "../../../core/generated-graphql";
|
import { TextUtils } from "src/utils";
|
||||||
import { TextUtils } from "../../../utils/text";
|
|
||||||
|
|
||||||
interface ISceneFileInfoPanelProps {
|
interface ISceneFileInfoPanelProps {
|
||||||
scene: GQL.SceneDataFragment;
|
scene: GQL.SceneDataFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SceneFileInfoPanel: FunctionComponent<ISceneFileInfoPanelProps> = (props: ISceneFileInfoPanelProps) => {
|
export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (props: ISceneFileInfoPanelProps) => {
|
||||||
function renderChecksum() {
|
function renderChecksum() {
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
@@ -25,7 +25,7 @@ export const SceneFileInfoPanel: FunctionComponent<ISceneFileInfoPanelProps> = (
|
|||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStream() {
|
function renderStream() {
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
|
import React, { CSSProperties, useState } from "react";
|
||||||
import { Badge, Button, Card, Collapse, Form as BootstrapForm } from 'react-bootstrap';
|
import { Badge, Button, Card, Collapse, Form as BootstrapForm } from 'react-bootstrap';
|
||||||
import { Field, FieldProps, Form, Formik, FormikActions, FormikProps } from "formik";
|
import { Field, FieldProps, Form, Formik, FormikActions } from "formik";
|
||||||
import React, { CSSProperties, FunctionComponent, useState } from "react";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import * as GQL from "../../../core/generated-graphql";
|
import { StashService } from "src/core/StashService";
|
||||||
import { StashService } from "../../../core/StashService";
|
import { TextUtils } from "src/utils";
|
||||||
import { TextUtils } from "../../../utils/text";
|
import { useToast } from 'src/hooks';
|
||||||
import { TagSelect } from "../../select/FilterSelect";
|
import { DurationInput, TagSelect, MarkerTitleSuggest } from "src/components/Shared";
|
||||||
import { MarkerTitleSuggest } from "../../select/MarkerTitleSuggest";
|
import { WallPanel } from "src/components/Wall/WallPanel";
|
||||||
import { WallPanel } from "../../Wall/WallPanel";
|
|
||||||
import { SceneHelpers } from "../helpers";
|
import { SceneHelpers } from "../helpers";
|
||||||
import { ErrorUtils } from "../../../utils/errors";
|
|
||||||
import { DurationInput } from "../../Shared/DurationInput";
|
|
||||||
|
|
||||||
interface ISceneMarkersPanelProps {
|
interface ISceneMarkersPanelProps {
|
||||||
scene: GQL.SceneDataFragment;
|
scene: GQL.SceneDataFragment;
|
||||||
@@ -23,7 +21,8 @@ interface IFormFields {
|
|||||||
tagIds: string[];
|
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 [isEditorOpen, setIsEditorOpen] = useState<boolean>(false);
|
||||||
const [editingMarker, setEditingMarker] = useState<GQL.SceneMarkerDataFragment | null>(null);
|
const [editingMarker, setEditingMarker] = useState<GQL.SceneMarkerDataFragment | null>(null);
|
||||||
|
|
||||||
@@ -33,9 +32,9 @@ export const SceneMarkersPanel: FunctionComponent<ISceneMarkersPanelProps> = (pr
|
|||||||
|
|
||||||
const jwplayer = SceneHelpers.getPlayer();
|
const jwplayer = SceneHelpers.getPlayer();
|
||||||
|
|
||||||
function onOpenEditor(marker: GQL.SceneMarkerDataFragment | null = null) {
|
function onOpenEditor(marker?: GQL.SceneMarkerDataFragment) {
|
||||||
setIsEditorOpen(true);
|
setIsEditorOpen(true);
|
||||||
setEditingMarker(marker);
|
setEditingMarker(marker ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClickMarker(marker: GQL.SceneMarkerDataFragment) {
|
function onClickMarker(marker: GQL.SceneMarkerDataFragment) {
|
||||||
@@ -104,17 +103,17 @@ export const SceneMarkersPanel: FunctionComponent<ISceneMarkersPanelProps> = (pr
|
|||||||
tag_ids: values.tagIds,
|
tag_ids: values.tagIds,
|
||||||
};
|
};
|
||||||
if (!isEditing) {
|
if (!isEditing) {
|
||||||
sceneMarkerCreate({ variables }).then((response) => {
|
sceneMarkerCreate({ variables }).then(() => {
|
||||||
setIsEditorOpen(false);
|
setIsEditorOpen(false);
|
||||||
setEditingMarker(null);
|
setEditingMarker(null);
|
||||||
}).catch((err) => ErrorUtils.handleApolloError(err));
|
}).catch((err) => Toast.error(err));
|
||||||
} else {
|
} else {
|
||||||
const updateVariables = variables as GQL.SceneMarkerUpdateVariables;
|
const updateVariables = variables as GQL.SceneMarkerUpdateVariables;
|
||||||
updateVariables.id = editingMarker!.id;
|
updateVariables.id = editingMarker!.id;
|
||||||
sceneMarkerUpdate({ variables: updateVariables }).then((response) => {
|
sceneMarkerUpdate({ variables: updateVariables }).then(() => {
|
||||||
setIsEditorOpen(false);
|
setIsEditorOpen(false);
|
||||||
setEditingMarker(null);
|
setEditingMarker(null);
|
||||||
}).catch((err) => ErrorUtils.handleApolloError(err));
|
}).catch((err) => Toast.error(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onDelete() {
|
function onDelete() {
|
||||||
@@ -128,12 +127,8 @@ export const SceneMarkersPanel: FunctionComponent<ISceneMarkersPanelProps> = (pr
|
|||||||
function renderTitleField(fieldProps: FieldProps<IFormFields>) {
|
function renderTitleField(fieldProps: FieldProps<IFormFields>) {
|
||||||
return (
|
return (
|
||||||
<MarkerTitleSuggest
|
<MarkerTitleSuggest
|
||||||
initialMarkerString={!!editingMarker ? editingMarker.title : undefined}
|
initialMarkerTitle={editingMarker?.title}
|
||||||
placeholder="Title"
|
onChange={(query:string) => fieldProps.form.setFieldValue("title", query)}
|
||||||
name={fieldProps.field.name}
|
|
||||||
onBlur={fieldProps.field.onBlur}
|
|
||||||
value={fieldProps.field.value}
|
|
||||||
onQueryChange={(query) => 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;
|
let deleteButton: JSX.Element | undefined;
|
||||||
if (!!editingMarker) {
|
if (editingMarker) {
|
||||||
deleteButton = (
|
deleteButton = (
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -205,7 +200,7 @@ export const SceneMarkersPanel: FunctionComponent<ISceneMarkersPanelProps> = (pr
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let initialValues: any;
|
let initialValues: any;
|
||||||
if (!!editingMarker) {
|
if (editingMarker) {
|
||||||
initialValues = {
|
initialValues = {
|
||||||
title: editingMarker.title,
|
title: editingMarker.title,
|
||||||
seconds: editingMarker.seconds,
|
seconds: editingMarker.seconds,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { FunctionComponent } from "react";
|
import React, { FunctionComponent } from "react";
|
||||||
import * as GQL from "../../../core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { PerformerCard } from "../../performers/PerformerCard";
|
import { PerformerCard } from "src/components/performers/PerformerCard";
|
||||||
|
|
||||||
interface IScenePerformerPanelProps {
|
interface IScenePerformerPanelProps {
|
||||||
scene: GQL.SceneDataFragment;
|
scene: GQL.SceneDataFragment;
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
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 React, { useEffect, useState } from "react";
|
||||||
import { StashService } from "../../core/StashService";
|
import { Badge, Button, Card, Collapse, Dropdown, DropdownButton, Form, Table, Spinner } from 'react-bootstrap';
|
||||||
import * as GQL from "../../core/generated-graphql";
|
|
||||||
import { SlimSceneDataFragment, Maybe } from "../../core/generated-graphql";
|
|
||||||
import { TextUtils } from "../../utils/text";
|
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { ToastUtils } from "../../utils/toasts";
|
import { StashService } from "src/core/StashService";
|
||||||
import { ErrorUtils } from "../../utils/errors";
|
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 { Pagination } from "../list/Pagination";
|
||||||
import { FilterSelect, StudioSelect } from "../select/FilterSelect";
|
|
||||||
|
|
||||||
class ParserResult<T> {
|
class ParserResult<T> {
|
||||||
public value: Maybe<T>;
|
public value: Maybe<T>;
|
||||||
public originalValue: Maybe<T>;
|
public originalValue: Maybe<T>;
|
||||||
@@ -22,7 +20,7 @@ class ParserResult<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setValue(v : Maybe<T>) {
|
public setValue(v : Maybe<T>) {
|
||||||
if (!!v) {
|
if (v) {
|
||||||
this.value = v;
|
this.value = v;
|
||||||
this.set = !_.isEqual(this.value, this.originalValue);
|
this.set = !_.isEqual(this.value, this.originalValue);
|
||||||
}
|
}
|
||||||
@@ -258,6 +256,7 @@ const builtInRecipes = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const SceneFilenameParser: React.FC = () => {
|
export const SceneFilenameParser: React.FC = () => {
|
||||||
|
const Toast = useToast();
|
||||||
const [parserResult, setParserResult] = useState<SceneParserResult[]>([]);
|
const [parserResult, setParserResult] = useState<SceneParserResult[]>([]);
|
||||||
const [parserInput, setParserInput] = useState<IParserInput>(initialParserInput());
|
const [parserInput, setParserInput] = useState<IParserInput>(initialParserInput());
|
||||||
|
|
||||||
@@ -268,7 +267,7 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
const [allStudioSet, setAllStudioSet] = useState<boolean>(false);
|
const [allStudioSet, setAllStudioSet] = useState<boolean>(false);
|
||||||
|
|
||||||
const [showFields, setShowFields] = useState<Map<string, boolean>>(initialShowFieldsState());
|
const [showFields, setShowFields] = useState<Map<string, boolean>>(initialShowFieldsState());
|
||||||
|
|
||||||
const [totalItems, setTotalItems] = useState<number>(0);
|
const [totalItems, setTotalItems] = useState<number>(0);
|
||||||
|
|
||||||
// Network state
|
// Network state
|
||||||
@@ -320,17 +319,17 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
setParserResult([]);
|
setParserResult([]);
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await StashService.queryParseSceneFilenames(getParserFilter(), getParserInput());
|
const response = await StashService.queryParseSceneFilenames(getParserFilter(), getParserInput());
|
||||||
|
|
||||||
let result = response.data.parseSceneFilenames;
|
let result = response.data.parseSceneFilenames;
|
||||||
if (!!result) {
|
if (result) {
|
||||||
parseResults(result.results);
|
parseResults(result.results);
|
||||||
setTotalItems(result.count);
|
setTotalItems(result.count);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ErrorUtils.handle(err);
|
Toast.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -373,9 +372,9 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await updateScenes();
|
await updateScenes();
|
||||||
ToastUtils.success("Updated scenes");
|
Toast.success({ content: "Updated scenes" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -395,7 +394,7 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
function determineFieldsToHide() {
|
function determineFieldsToHide() {
|
||||||
var pattern = parserInput.pattern;
|
var pattern = parserInput.pattern;
|
||||||
var titleSet = pattern.includes("{title}");
|
var titleSet = pattern.includes("{title}");
|
||||||
var dateSet = pattern.includes("{date}") ||
|
var dateSet = pattern.includes("{date}") ||
|
||||||
pattern.includes("{dd}") || // don't worry about other partial date fields since this should be implied
|
pattern.includes("{dd}") || // don't worry about other partial date fields since this should be implied
|
||||||
ParserField.fullDateFields.some((f) => {
|
ParserField.fullDateFields.some((f) => {
|
||||||
return pattern.includes("{" + f.field + "}");
|
return pattern.includes("{" + f.field + "}");
|
||||||
@@ -518,7 +517,7 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
|
|
||||||
const fieldRows = [...props.fields.entries()].map(([label, enabled]) => (
|
const fieldRows = [...props.fields.entries()].map(([label, enabled]) => (
|
||||||
<div key={label} onClick={() => {handleClick(label)}}>
|
<div key={label} onClick={() => {handleClick(label)}}>
|
||||||
<FontAwesomeIcon icon={enabled ? "check" : "times" } />
|
<Icon icon={enabled ? "check" : "times" } />
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
@@ -526,7 +525,7 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div onClick={() => setOpen(!open)}>
|
<div onClick={() => setOpen(!open)}>
|
||||||
<FontAwesomeIcon icon={open ? "chevron-down" : "chevron-right" } />
|
<Icon icon={open ? "chevron-down" : "chevron-right" } />
|
||||||
<span>Display fields</span>
|
<span>Display fields</span>
|
||||||
</div>
|
</div>
|
||||||
<Collapse in={open}>
|
<Collapse in={open}>
|
||||||
@@ -567,9 +566,9 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
setWhitespaceCharacters(recipe.whitespaceCharacters);
|
setWhitespaceCharacters(recipe.whitespaceCharacters);
|
||||||
setCapitalizeTitle(recipe.capitalizeTitle);
|
setCapitalizeTitle(recipe.capitalizeTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
const validFields = [new ParserField("", "Wildcard")].concat(ParserField.validFields);
|
const validFields = [new ParserField("", "Wildcard")].concat(ParserField.validFields);
|
||||||
|
|
||||||
function addParserField(field: ParserField) {
|
function addParserField(field: ParserField) {
|
||||||
setPattern(pattern + field.getFieldPattern());
|
setPattern(pattern + field.getFieldPattern());
|
||||||
}
|
}
|
||||||
@@ -601,7 +600,7 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<div>Matches with {"{i}"}</div>
|
<div>Matches with {"{i}"}</div>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h5>Title</h5>
|
<h5>Title</h5>
|
||||||
<Form.Label>Whitespace characters:</Form.Label>
|
<Form.Label>Whitespace characters:</Form.Label>
|
||||||
@@ -619,7 +618,7 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
<div>These characters will be replaced with whitespace in the title</div>
|
<div>These characters will be replaced with whitespace in the title</div>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
{/* TODO - mapping stuff will go here */}
|
{/* TODO - mapping stuff will go here */}
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
@@ -726,7 +725,7 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNewInputGroup(props : ISceneParserFieldProps, onChange : (value : any) => void) {
|
function renderNewInputGroup(props : ISceneParserFieldProps, onChange : (value : any) => void) {
|
||||||
return (
|
return (
|
||||||
<InputGroupWrapper
|
<InputGroupWrapper
|
||||||
@@ -840,17 +839,17 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
<td style={{textAlign: "left"}}>
|
<td style={{textAlign: "left"}}>
|
||||||
{props.scene.filename}
|
{props.scene.filename}
|
||||||
</td>
|
</td>
|
||||||
<SceneParserField
|
<SceneParserField
|
||||||
key="title"
|
key="title"
|
||||||
fieldName="Title"
|
fieldName="Title"
|
||||||
className="parser-field-title"
|
className="parser-field-title"
|
||||||
parserResult={props.scene.title}
|
parserResult={props.scene.title}
|
||||||
onSetChanged={(set) => onTitleChanged(set, props.scene.title.value)}
|
onSetChanged={(set) => onTitleChanged(set, props.scene.title.value)}
|
||||||
onValueChanged={(value) => onTitleChanged(props.scene.title.set, value)}
|
onValueChanged={(value) => onTitleChanged(props.scene.title.set, value)}
|
||||||
renderOriginalInputField={renderOriginalInputGroup}
|
renderOriginalInputField={renderOriginalInputGroup}
|
||||||
renderNewInputField={renderNewInputGroup}
|
renderNewInputField={renderNewInputGroup}
|
||||||
/>
|
/>
|
||||||
<SceneParserField
|
<SceneParserField
|
||||||
key="date"
|
key="date"
|
||||||
fieldName="Date"
|
fieldName="Date"
|
||||||
className="parser-field-date"
|
className="parser-field-date"
|
||||||
@@ -860,7 +859,7 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
renderOriginalInputField={renderOriginalInputGroup}
|
renderOriginalInputField={renderOriginalInputGroup}
|
||||||
renderNewInputField={renderNewInputGroup}
|
renderNewInputField={renderNewInputGroup}
|
||||||
/>
|
/>
|
||||||
<SceneParserField
|
<SceneParserField
|
||||||
key="performers"
|
key="performers"
|
||||||
fieldName="Performers"
|
fieldName="Performers"
|
||||||
className="parser-field-performers"
|
className="parser-field-performers"
|
||||||
@@ -871,7 +870,7 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
renderOriginalInputField={renderOriginalSelect}
|
renderOriginalInputField={renderOriginalSelect}
|
||||||
renderNewInputField={renderNewPerformerSelect}
|
renderNewInputField={renderNewPerformerSelect}
|
||||||
/>
|
/>
|
||||||
<SceneParserField
|
<SceneParserField
|
||||||
key="tags"
|
key="tags"
|
||||||
fieldName="Tags"
|
fieldName="Tags"
|
||||||
className="parser-field-tags"
|
className="parser-field-tags"
|
||||||
@@ -882,7 +881,7 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
renderOriginalInputField={renderOriginalSelect}
|
renderOriginalInputField={renderOriginalSelect}
|
||||||
renderNewInputField={renderNewTagSelect}
|
renderNewInputField={renderNewTagSelect}
|
||||||
/>
|
/>
|
||||||
<SceneParserField
|
<SceneParserField
|
||||||
key="studio"
|
key="studio"
|
||||||
fieldName="Studio"
|
fieldName="Studio"
|
||||||
className="parser-field-studio"
|
className="parser-field-studio"
|
||||||
@@ -944,9 +943,9 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{parserResult.map((scene) =>
|
{parserResult.map((scene) =>
|
||||||
<SceneParserRow
|
<SceneParserRow
|
||||||
scene={scene}
|
scene={scene}
|
||||||
key={scene.id}
|
key={scene.id}
|
||||||
onChange={(changedScene) => onChange(scene, changedScene)}/>
|
onChange={(changedScene) => onChange(scene, changedScene)}/>
|
||||||
)}
|
)}
|
||||||
@@ -978,4 +977,4 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import _ from "lodash";
|
||||||
import { QueryHookResult } from "react-apollo-hooks";
|
import { QueryHookResult } from "react-apollo-hooks";
|
||||||
import { FindScenesQuery, FindScenesVariables, SlimSceneDataFragment } from "../../core/generated-graphql";
|
import { FindScenesQuery, FindScenesVariables, SlimSceneDataFragment } from "src/core/generated-graphql";
|
||||||
import { ListHook } from "../../hooks/ListHook";
|
import { StashService } from "src/core/StashService";
|
||||||
import { IBaseProps } from "../../models/base-props";
|
import { ListHook } from "src/hooks";
|
||||||
import { ListFilterModel } from "../../models/list-filter/filter";
|
import { IBaseProps } from "src/models/base-props";
|
||||||
import { DisplayMode, FilterMode } from "../../models/list-filter/types";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
import { DisplayMode, FilterMode } from "src/models/list-filter/types";
|
||||||
import { WallPanel } from "../Wall/WallPanel";
|
import { WallPanel } from "../Wall/WallPanel";
|
||||||
import { SceneCard } from "./SceneCard";
|
import { SceneCard } from "./SceneCard";
|
||||||
import { SceneListTable } from "./SceneListTable";
|
import { SceneListTable } from "./SceneListTable";
|
||||||
import { SceneSelectedOptions } from "./SceneSelectedOptions";
|
import { SceneSelectedOptions } from "./SceneSelectedOptions";
|
||||||
import { StashService } from "../../core/StashService";
|
|
||||||
|
|
||||||
interface ISceneListProps extends IBaseProps {}
|
interface ISceneListProps extends IBaseProps {}
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ export const SceneList: React.FC<ISceneListProps> = (props: ISceneListProps) =>
|
|||||||
onClick: playRandom,
|
onClick: playRandom,
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const listData = ListHook.useList({
|
const listData = ListHook.useList({
|
||||||
filterMode: FilterMode.Scenes,
|
filterMode: FilterMode.Scenes,
|
||||||
props,
|
props,
|
||||||
@@ -31,7 +31,7 @@ export const SceneList: React.FC<ISceneListProps> = (props: ISceneListProps) =>
|
|||||||
renderSelectedOptions
|
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
|
// query for a random scene
|
||||||
if (result.data && result.data.findScenes) {
|
if (result.data && result.data.findScenes) {
|
||||||
let count = result.data.findScenes.count;
|
let count = result.data.findScenes.count;
|
||||||
@@ -54,7 +54,7 @@ export const SceneList: React.FC<ISceneListProps> = (props: ISceneListProps) =>
|
|||||||
if (!result.data || !result.data.findScenes) { return undefined; }
|
if (!result.data || !result.data.findScenes) { return undefined; }
|
||||||
|
|
||||||
var scenes = result.data.findScenes.scenes;
|
var scenes = result.data.findScenes.scenes;
|
||||||
|
|
||||||
var selectedScenes : SlimSceneDataFragment[] = [];
|
var selectedScenes : SlimSceneDataFragment[] = [];
|
||||||
selectedIds.forEach((id) => {
|
selectedIds.forEach((id) => {
|
||||||
var scene = scenes.find((scene) => {
|
var scene = scenes.find((scene) => {
|
||||||
@@ -75,9 +75,9 @@ export const SceneList: React.FC<ISceneListProps> = (props: ISceneListProps) =>
|
|||||||
|
|
||||||
function renderSceneCard(scene : SlimSceneDataFragment, selectedIds: Set<string>, zoomIndex: number) {
|
function renderSceneCard(scene : SlimSceneDataFragment, selectedIds: Set<string>, zoomIndex: number) {
|
||||||
return (
|
return (
|
||||||
<SceneCard
|
<SceneCard
|
||||||
key={scene.id}
|
key={scene.id}
|
||||||
scene={scene}
|
scene={scene}
|
||||||
zoomIndex={zoomIndex}
|
zoomIndex={zoomIndex}
|
||||||
selected={selectedIds.has(scene.id)}
|
selected={selectedIds.has(scene.id)}
|
||||||
onSelectedChanged={(selected: boolean, shiftKey: boolean) => listData.onSelectChange(scene.id, selected, shiftKey)}
|
onSelectedChanged={(selected: boolean, shiftKey: boolean) => listData.onSelectChange(scene.id, selected, shiftKey)}
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import React from "react";
|
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 { 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 {
|
interface ISceneListTableProps {
|
||||||
scenes: GQL.SlimSceneDataFragment[];
|
scenes: GQL.SlimSceneDataFragment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneListTableProps) => {
|
export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneListTableProps) => {
|
||||||
|
|
||||||
function renderSceneImage(scene : GQL.SlimSceneDataFragment) {
|
function renderSceneImage(scene : GQL.SlimSceneDataFragment) {
|
||||||
const style: React.CSSProperties = {
|
const style: React.CSSProperties = {
|
||||||
backgroundImage: `url('${scene.paths.screenshot}')`,
|
backgroundImage: `url('${scene.paths.screenshot}')`,
|
||||||
@@ -23,7 +21,7 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneList
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className="scene-list-thumbnail"
|
className="scene-list-thumbnail"
|
||||||
to={`/scenes/${scene.id}`}
|
to={`/scenes/${scene.id}`}
|
||||||
style={style}/>
|
style={style}/>
|
||||||
@@ -37,7 +35,7 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneList
|
|||||||
|
|
||||||
function renderTags(tags : GQL.SlimSceneDataTags[]) {
|
function renderTags(tags : GQL.SlimSceneDataTags[]) {
|
||||||
return tags.map((tag) => (
|
return tags.map((tag) => (
|
||||||
<Link to={NavigationUtils.makeTagScenesUrl(tag)}>
|
<Link to={NavUtils.makeTagScenesUrl(tag)}>
|
||||||
<h6>{tag.name}</h6>
|
<h6>{tag.name}</h6>
|
||||||
</Link>
|
</Link>
|
||||||
));
|
));
|
||||||
@@ -45,16 +43,16 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneList
|
|||||||
|
|
||||||
function renderPerformers(performers : GQL.SlimSceneDataPerformers[]) {
|
function renderPerformers(performers : GQL.SlimSceneDataPerformers[]) {
|
||||||
return performers.map((performer) => (
|
return performers.map((performer) => (
|
||||||
<Link to={NavigationUtils.makePerformerScenesUrl(performer)}>
|
<Link to={NavUtils.makePerformerScenesUrl(performer)}>
|
||||||
<h6>{performer.name}</h6>
|
<h6>{performer.name}</h6>
|
||||||
</Link>
|
</Link>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStudio(studio : GQL.SlimSceneDataStudio | undefined) {
|
function renderStudio(studio : GQL.SlimSceneDataStudio | undefined) {
|
||||||
if (!!studio) {
|
if (studio) {
|
||||||
return (
|
return (
|
||||||
<Link to={NavigationUtils.makeStudioScenesUrl(studio)}>
|
<Link to={NavUtils.makeStudioScenesUrl(studio)}>
|
||||||
<h6>{studio.name}</h6>
|
<h6>{studio.name}</h6>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -63,7 +61,6 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneList
|
|||||||
|
|
||||||
function renderSceneRow(scene : GQL.SlimSceneDataFragment) {
|
function renderSceneRow(scene : GQL.SlimSceneDataFragment) {
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{renderSceneImage(scene)}
|
{renderSceneImage(scene)}
|
||||||
@@ -71,7 +68,7 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneList
|
|||||||
<td style={{textAlign: "left"}}>
|
<td style={{textAlign: "left"}}>
|
||||||
<Link to={`/scenes/${scene.id}`}>
|
<Link to={`/scenes/${scene.id}`}>
|
||||||
<h5 className="text-truncate">
|
<h5 className="text-truncate">
|
||||||
{!!scene.title ? scene.title : TextUtils.fileNameFromPath(scene.path)}
|
{scene.title ?? TextUtils.fileNameFromPath(scene.path)}
|
||||||
</h5>
|
</h5>
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
@@ -91,12 +88,10 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneList
|
|||||||
{renderStudio(scene.studio)}
|
{renderStudio(scene.studio)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<div className="grid">
|
<div className="grid">
|
||||||
<Table striped bordered>
|
<Table striped bordered>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -115,7 +110,6 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneList
|
|||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { FunctionComponent } from "react";
|
import React from "react";
|
||||||
import { QueryHookResult } from "react-apollo-hooks";
|
import { QueryHookResult } from "react-apollo-hooks";
|
||||||
import { FindSceneMarkersQuery, FindSceneMarkersVariables } from "../../core/generated-graphql";
|
import { FindSceneMarkersQuery, FindSceneMarkersVariables } from "src/core/generated-graphql";
|
||||||
import { ListHook } from "../../hooks/ListHook";
|
import { StashService } from "src/core/StashService";
|
||||||
import { IBaseProps } from "../../models/base-props";
|
import { NavUtils } from "src/utils";
|
||||||
import { ListFilterModel } from "../../models/list-filter/filter";
|
import { ListHook } from "src/hooks";
|
||||||
import { DisplayMode, FilterMode } from "../../models/list-filter/types";
|
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 { WallPanel } from "../Wall/WallPanel";
|
||||||
import { StashService } from "../../core/StashService";
|
|
||||||
import { NavigationUtils } from "../../utils/navigation";
|
|
||||||
|
|
||||||
interface IProps extends IBaseProps {}
|
interface IProps extends IBaseProps {}
|
||||||
|
|
||||||
export const SceneMarkerList: FunctionComponent<IProps> = (props: IProps) => {
|
export const SceneMarkerList: React.FC<IProps> = (props: IProps) => {
|
||||||
const otherOperations = [
|
const otherOperations = [{
|
||||||
{
|
text: "Play Random",
|
||||||
text: "Play Random",
|
onClick: playRandom
|
||||||
onClick: playRandom,
|
}];
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const listData = ListHook.useList({
|
const listData = ListHook.useList({
|
||||||
filterMode: FilterMode.SceneMarkers,
|
filterMode: FilterMode.SceneMarkers,
|
||||||
@@ -27,7 +25,7 @@ export const SceneMarkerList: FunctionComponent<IProps> = (props: IProps) => {
|
|||||||
renderContent,
|
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
|
// query for a random scene
|
||||||
if (result.data && result.data.findSceneMarkers) {
|
if (result.data && result.data.findSceneMarkers) {
|
||||||
let count = result.data.findSceneMarkers.count;
|
let count = result.data.findSceneMarkers.count;
|
||||||
@@ -39,7 +37,7 @@ export const SceneMarkerList: FunctionComponent<IProps> = (props: IProps) => {
|
|||||||
const singleResult = await StashService.queryFindSceneMarkers(filterCopy);
|
const singleResult = await StashService.queryFindSceneMarkers(filterCopy);
|
||||||
if (singleResult && singleResult.data && singleResult.data.findSceneMarkers && singleResult.data.findSceneMarkers.scene_markers.length === 1) {
|
if (singleResult && singleResult.data && singleResult.data.findSceneMarkers && singleResult.data.findSceneMarkers.scene_markers.length === 1) {
|
||||||
// navigate to the scene player page
|
// navigate to the scene player page
|
||||||
let url = NavigationUtils.makeSceneMarkerUrl(singleResult!.data!.findSceneMarkers!.scene_markers[0])
|
let url = NavUtils.makeSceneMarkerUrl(singleResult.data.findSceneMarkers.scene_markers[0])
|
||||||
props.history.push(url);
|
props.history.push(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,7 +47,8 @@ export const SceneMarkerList: FunctionComponent<IProps> = (props: IProps) => {
|
|||||||
result: QueryHookResult<FindSceneMarkersQuery, FindSceneMarkersVariables>,
|
result: QueryHookResult<FindSceneMarkersQuery, FindSceneMarkersVariables>,
|
||||||
filter: ListFilterModel,
|
filter: ListFilterModel,
|
||||||
) {
|
) {
|
||||||
if (!result.data || !result.data.findSceneMarkers) { return; }
|
if (!result?.data?.findSceneMarkers)
|
||||||
|
return;
|
||||||
if (filter.displayMode === DisplayMode.Wall) {
|
if (filter.displayMode === DisplayMode.Wall) {
|
||||||
return <WallPanel sceneMarkers={result.data.findSceneMarkers.scene_markers} />;
|
return <WallPanel sceneMarkers={result.data.findSceneMarkers.scene_markers} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactJWPlayer from "react-jw-player";
|
import ReactJWPlayer from "react-jw-player";
|
||||||
import { HotKeys } from "react-hotkeys";
|
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 { SceneHelpers } from "../helpers";
|
||||||
import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
|
import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
|
||||||
import { StashService } from "../../../core/StashService";
|
|
||||||
|
|
||||||
interface IScenePlayerProps {
|
interface IScenePlayerProps {
|
||||||
scene: GQL.SceneDataFragment;
|
scene: GQL.SceneDataFragment;
|
||||||
@@ -110,8 +110,8 @@ export class ScenePlayerImpl extends React.Component<IScenePlayerProps, IScenePl
|
|||||||
let getCurrentTimeHook: ((_videoTag: any) => number) | undefined = undefined;
|
let getCurrentTimeHook: ((_videoTag: any) => number) | undefined = undefined;
|
||||||
|
|
||||||
if (!this.props.scene.is_streamable) {
|
if (!this.props.scene.is_streamable) {
|
||||||
getDurationHook = () => {
|
getDurationHook = () => {
|
||||||
return this.props.scene.file.duration;
|
return this.props.scene.file.duration;
|
||||||
};
|
};
|
||||||
|
|
||||||
seekHook = (seekToPosition: number, _videoTag: any) => {
|
seekHook = (seekToPosition: number, _videoTag: any) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios from "axios";
|
|
||||||
import React, { CSSProperties, useEffect, useRef, useState } from "react";
|
import React, { CSSProperties, useEffect, useRef, useState } from "react";
|
||||||
import * as GQL from "../../../core/generated-graphql";
|
import axios from "axios";
|
||||||
import { TextUtils } from "../../../utils/text";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { TextUtils } from "src/utils";
|
||||||
import "./ScenePlayerScrubber.scss";
|
import "./ScenePlayerScrubber.scss";
|
||||||
|
|
||||||
interface IScenePlayerScrubberProps {
|
interface IScenePlayerScrubberProps {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import { Button, ButtonGroup, Form, Spinner } from 'react-bootstrap';
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { FilterSelect, StudioSelect } from "../select/FilterSelect";
|
import { Button, ButtonGroup, Form, Spinner } from 'react-bootstrap';
|
||||||
import { StashService } from "../../core/StashService";
|
import _ from "lodash";
|
||||||
import * as GQL from "../../core/generated-graphql";
|
import { StashService } from "src/core/StashService";
|
||||||
import { ErrorUtils } from "../../utils/errors";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { ToastUtils } from "../../utils/toasts";
|
import { FilterSelect, StudioSelect } from "src/components/Shared";
|
||||||
|
import { useToast } from "src/hooks";
|
||||||
|
|
||||||
interface IListOperationProps {
|
interface IListOperationProps {
|
||||||
selected: GQL.SlimSceneDataFragment[],
|
selected: GQL.SlimSceneDataFragment[],
|
||||||
@@ -13,6 +12,7 @@ interface IListOperationProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IListOperationProps) => {
|
export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IListOperationProps) => {
|
||||||
|
const Toast = useToast();
|
||||||
const [rating, setRating] = useState<string>("");
|
const [rating, setRating] = useState<string>("");
|
||||||
const [studioId, setStudioId] = useState<string | undefined>(undefined);
|
const [studioId, setStudioId] = useState<string | undefined>(undefined);
|
||||||
const [performerIds, setPerformerIds] = useState<string[] | undefined>(undefined);
|
const [performerIds, setPerformerIds] = useState<string[] | undefined>(undefined);
|
||||||
@@ -36,7 +36,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// if rating is undefined
|
// if rating is undefined
|
||||||
if (rating === "") {
|
if (rating === "") {
|
||||||
// and all scenes have the same rating, then we are unsetting the rating.
|
// and all scenes have the same rating, then we are unsetting the rating.
|
||||||
if(aggregateRating) {
|
if(aggregateRating) {
|
||||||
@@ -48,8 +48,8 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
|||||||
// if rating is set, then we are setting the rating for all
|
// if rating is set, then we are setting the rating for all
|
||||||
sceneInput.rating = Number.parseInt(rating);
|
sceneInput.rating = Number.parseInt(rating);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if studioId is undefined
|
// if studioId is undefined
|
||||||
if (studioId === undefined) {
|
if (studioId === undefined) {
|
||||||
// and all scenes have the same studioId,
|
// and all scenes have the same studioId,
|
||||||
// then unset the studioId, otherwise ignoring studioId
|
// then unset the studioId, otherwise ignoring studioId
|
||||||
@@ -61,7 +61,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
|||||||
// if studioId is set, then we are setting it
|
// if studioId is set, then we are setting it
|
||||||
sceneInput.studio_id = studioId;
|
sceneInput.studio_id = studioId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if performerIds are empty
|
// if performerIds are empty
|
||||||
if (!performerIds || performerIds.length === 0) {
|
if (!performerIds || performerIds.length === 0) {
|
||||||
// and all scenes have the same ids,
|
// and all scenes have the same ids,
|
||||||
@@ -73,7 +73,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
|||||||
// if performerIds non-empty, then we are setting them
|
// if performerIds non-empty, then we are setting them
|
||||||
sceneInput.performer_ids = performerIds;
|
sceneInput.performer_ids = performerIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if tagIds non-empty, then we are setting them
|
// if tagIds non-empty, then we are setting them
|
||||||
if (!tagIds || tagIds.length === 0) {
|
if (!tagIds || tagIds.length === 0) {
|
||||||
// and all scenes have the same ids,
|
// and all scenes have the same ids,
|
||||||
@@ -93,9 +93,9 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await updateScenes();
|
await updateScenes();
|
||||||
ToastUtils.success("Updated scenes");
|
Toast.success({ content: "Updated scenes" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorUtils.handle(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
props.onScenesUpdated();
|
props.onScenesUpdated();
|
||||||
@@ -152,7 +152,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
|||||||
first = false;
|
first = false;
|
||||||
} else {
|
} else {
|
||||||
const perfIds = !!scene.performers ? scene.performers.map(toId).sort() : [];
|
const perfIds = !!scene.performers ? scene.performers.map(toId).sort() : [];
|
||||||
|
|
||||||
if (!_.isEqual(ret, perfIds)) {
|
if (!_.isEqual(ret, perfIds)) {
|
||||||
ret = [];
|
ret = [];
|
||||||
}
|
}
|
||||||
@@ -172,7 +172,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
|||||||
first = false;
|
first = false;
|
||||||
} else {
|
} else {
|
||||||
const tIds = !!scene.tags ? scene.tags.map(toId).sort() : [];
|
const tIds = !!scene.tags ? scene.tags.map(toId).sort() : [];
|
||||||
|
|
||||||
if (!_.isEqual(ret, tIds)) {
|
if (!_.isEqual(ret, tIds)) {
|
||||||
ret = [];
|
ret = [];
|
||||||
}
|
}
|
||||||
@@ -212,7 +212,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
|||||||
}
|
}
|
||||||
const perfIds = !!scene.performers ? scene.performers.map(toId).sort() : [];
|
const perfIds = !!scene.performers ? scene.performers.map(toId).sort() : [];
|
||||||
const tIds = !!scene.tags ? scene.tags.map(toId).sort() : [];
|
const tIds = !!scene.tags ? scene.tags.map(toId).sort() : [];
|
||||||
|
|
||||||
if (!_.isEqual(performerIds, perfIds)) {
|
if (!_.isEqual(performerIds, perfIds)) {
|
||||||
performerIds = [];
|
performerIds = [];
|
||||||
}
|
}
|
||||||
@@ -222,7 +222,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setRating(rating);
|
setRating(rating);
|
||||||
setStudioId(studioId);
|
setStudioId(studioId);
|
||||||
setPerformerIds(performerIds);
|
setPerformerIds(performerIds);
|
||||||
@@ -249,7 +249,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -283,9 +283,9 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
|||||||
<Form.Label>Performers</Form.Label>
|
<Form.Label>Performers</Form.Label>
|
||||||
{renderMultiSelect("tags", tagIds)}
|
{renderMultiSelect("tags", tagIds)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<ButtonGroup className="operation-item">
|
<ButtonGroup className="operation-item">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={onSave}>
|
onClick={onSave}>
|
||||||
Apply
|
Apply
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { } from "react";
|
import React, { } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "../../core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
|
||||||
export class SceneHelpers {
|
export class SceneHelpers {
|
||||||
public static maybeRenderStudio(
|
public static maybeRenderStudio(
|
||||||
|
|||||||
@@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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"}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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"}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -2,10 +2,9 @@ import ApolloClient from "apollo-client";
|
|||||||
import { WebSocketLink } from 'apollo-link-ws';
|
import { WebSocketLink } from 'apollo-link-ws';
|
||||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||||
import { HttpLink, split } from "apollo-boost";
|
import { HttpLink, split } from "apollo-boost";
|
||||||
import _ from "lodash";
|
import { getMainDefinition } from "apollo-utilities";
|
||||||
import { ListFilterModel } from "../models/list-filter/filter";
|
import { ListFilterModel } from "../models/list-filter/filter";
|
||||||
import * as GQL from "./generated-graphql";
|
import * as GQL from "./generated-graphql";
|
||||||
import { getMainDefinition } from "apollo-utilities";
|
|
||||||
|
|
||||||
export class StashService {
|
export class StashService {
|
||||||
public static client: ApolloClient<any>;
|
public static client: ApolloClient<any>;
|
||||||
@@ -41,7 +40,7 @@ export class StashService {
|
|||||||
reconnect: true
|
reconnect: true
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const link = split(
|
const link = split(
|
||||||
({ query }) => {
|
({ query }) => {
|
||||||
const { kind, operation } = getMainDefinition(query);
|
const { kind, operation } = getMainDefinition(query);
|
||||||
@@ -215,27 +214,27 @@ export class StashService {
|
|||||||
];
|
];
|
||||||
|
|
||||||
public static useSceneMarkerCreate() {
|
public static useSceneMarkerCreate() {
|
||||||
return GQL.useSceneMarkerCreate({ refetchQueries: ["FindScene"] });
|
return GQL.useSceneMarkerCreate({ refetchQueries: ["FindScene"] });
|
||||||
}
|
}
|
||||||
public static useSceneMarkerUpdate() {
|
public static useSceneMarkerUpdate() {
|
||||||
return GQL.useSceneMarkerUpdate({ refetchQueries: ["FindScene"] });
|
return GQL.useSceneMarkerUpdate({ refetchQueries: ["FindScene"] });
|
||||||
}
|
}
|
||||||
public static useSceneMarkerDestroy() {
|
public static useSceneMarkerDestroy() {
|
||||||
return GQL.useSceneMarkerDestroy({ refetchQueries: ["FindScene"] });
|
return GQL.useSceneMarkerDestroy({ refetchQueries: ["FindScene"] });
|
||||||
}
|
}
|
||||||
|
|
||||||
public static useListPerformerScrapers() {
|
public static useListPerformerScrapers() {
|
||||||
return GQL.useListPerformerScrapers();
|
return GQL.useListPerformerScrapers();
|
||||||
}
|
}
|
||||||
public static useScrapePerformerList(scraperId: string, q : string) {
|
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) {
|
public static useScrapePerformer(scraperId: string, scrapedPerformer : GQL.ScrapedPerformerInput) {
|
||||||
return GQL.useScrapePerformer({ variables: { scraper_id: scraperId, scraped_performer: scrapedPerformer }});
|
return GQL.useScrapePerformer({ variables: { scraper_id: scraperId, scraped_performer: scrapedPerformer }});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static useListSceneScrapers() {
|
public static useListSceneScrapers() {
|
||||||
return GQL.useListSceneScrapers();
|
return GQL.useListSceneScrapers();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static useScrapeFreeonesPerformers(q: string) { return GQL.useScrapeFreeonesPerformers({ variables: { q } }); }
|
public static useScrapeFreeonesPerformers(q: string) { return GQL.useScrapeFreeonesPerformers({ variables: { q } }); }
|
||||||
@@ -261,13 +260,13 @@ export class StashService {
|
|||||||
];
|
];
|
||||||
|
|
||||||
public static usePerformerCreate(input: GQL.PerformerCreateInput) {
|
public static usePerformerCreate(input: GQL.PerformerCreateInput) {
|
||||||
return GQL.usePerformerCreate({
|
return GQL.usePerformerCreate({
|
||||||
variables: input,
|
variables: input,
|
||||||
update: () => StashService.invalidateQueries(StashService.performerMutationImpactedQueries)
|
update: () => StashService.invalidateQueries(StashService.performerMutationImpactedQueries)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public static usePerformerUpdate(input: GQL.PerformerUpdateInput) {
|
public static usePerformerUpdate(input: GQL.PerformerUpdateInput) {
|
||||||
return GQL.usePerformerUpdate({
|
return GQL.usePerformerUpdate({
|
||||||
variables: input,
|
variables: input,
|
||||||
update: () => StashService.invalidateQueries(StashService.performerMutationImpactedQueries)
|
update: () => StashService.invalidateQueries(StashService.performerMutationImpactedQueries)
|
||||||
});
|
});
|
||||||
@@ -289,7 +288,7 @@ export class StashService {
|
|||||||
];
|
];
|
||||||
|
|
||||||
public static useSceneUpdate(input: GQL.SceneUpdateInput) {
|
public static useSceneUpdate(input: GQL.SceneUpdateInput) {
|
||||||
return GQL.useSceneUpdate({
|
return GQL.useSceneUpdate({
|
||||||
variables: input,
|
variables: input,
|
||||||
update: () => StashService.invalidateQueries(StashService.sceneMutationImpactedQueries),
|
update: () => StashService.invalidateQueries(StashService.sceneMutationImpactedQueries),
|
||||||
refetchQueries: ["AllTagsForFilter"]
|
refetchQueries: ["AllTagsForFilter"]
|
||||||
@@ -306,18 +305,18 @@ export class StashService {
|
|||||||
];
|
];
|
||||||
|
|
||||||
public static useBulkSceneUpdate(input: GQL.BulkSceneUpdateInput) {
|
public static useBulkSceneUpdate(input: GQL.BulkSceneUpdateInput) {
|
||||||
return GQL.useBulkSceneUpdate({
|
return GQL.useBulkSceneUpdate({
|
||||||
variables: input,
|
variables: input,
|
||||||
update: () => StashService.invalidateQueries(StashService.sceneBulkMutationImpactedQueries)
|
update: () => StashService.invalidateQueries(StashService.sceneBulkMutationImpactedQueries)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static useScenesUpdate(input: GQL.SceneUpdateInput[]) {
|
public static useScenesUpdate(input: GQL.SceneUpdateInput[]) {
|
||||||
return GQL.useScenesUpdate({ variables: { input : input }});
|
return GQL.useScenesUpdate({ variables: { input : input }});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static useSceneDestroy(input: GQL.SceneDestroyInput) {
|
public static useSceneDestroy(input: GQL.SceneDestroyInput) {
|
||||||
return GQL.useSceneDestroy({
|
return GQL.useSceneDestroy({
|
||||||
variables: input,
|
variables: input,
|
||||||
update: () => StashService.invalidateQueries(StashService.sceneMutationImpactedQueries)
|
update: () => StashService.invalidateQueries(StashService.sceneMutationImpactedQueries)
|
||||||
});
|
});
|
||||||
@@ -330,22 +329,22 @@ export class StashService {
|
|||||||
];
|
];
|
||||||
|
|
||||||
public static useStudioCreate(input: GQL.StudioCreateInput) {
|
public static useStudioCreate(input: GQL.StudioCreateInput) {
|
||||||
return GQL.useStudioCreate({
|
return GQL.useStudioCreate({
|
||||||
variables: input,
|
variables: input,
|
||||||
update: () => StashService.invalidateQueries(StashService.studioMutationImpactedQueries)
|
update: () => StashService.invalidateQueries(StashService.studioMutationImpactedQueries)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static useStudioUpdate(input: GQL.StudioUpdateInput) {
|
public static useStudioUpdate(input: GQL.StudioUpdateInput) {
|
||||||
return GQL.useStudioUpdate({
|
return GQL.useStudioUpdate({
|
||||||
variables: input,
|
variables: input,
|
||||||
update: () => StashService.invalidateQueries(StashService.studioMutationImpactedQueries)
|
update: () => StashService.invalidateQueries(StashService.studioMutationImpactedQueries)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static useStudioDestroy(input: GQL.StudioDestroyInput) {
|
public static useStudioDestroy(input: GQL.StudioDestroyInput) {
|
||||||
return GQL.useStudioDestroy({
|
return GQL.useStudioDestroy({
|
||||||
variables: input,
|
variables: input,
|
||||||
update: () => StashService.invalidateQueries(StashService.studioMutationImpactedQueries)
|
update: () => StashService.invalidateQueries(StashService.studioMutationImpactedQueries)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -358,21 +357,21 @@ export class StashService {
|
|||||||
];
|
];
|
||||||
|
|
||||||
public static useTagCreate(input: GQL.TagCreateInput) {
|
public static useTagCreate(input: GQL.TagCreateInput) {
|
||||||
return GQL.useTagCreate({
|
return GQL.useTagCreate({
|
||||||
variables: input,
|
variables: input,
|
||||||
refetchQueries: ["AllTags", "AllTagsForFilter"],
|
refetchQueries: ["AllTags", "AllTagsForFilter"],
|
||||||
//update: () => StashService.invalidateQueries(StashService.tagMutationImpactedQueries)
|
//update: () => StashService.invalidateQueries(StashService.tagMutationImpactedQueries)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public static useTagUpdate(input: GQL.TagUpdateInput) {
|
public static useTagUpdate(input: GQL.TagUpdateInput) {
|
||||||
return GQL.useTagUpdate({
|
return GQL.useTagUpdate({
|
||||||
variables: input,
|
variables: input,
|
||||||
refetchQueries: ["AllTags", "AllTagsForFilter"],
|
refetchQueries: ["AllTags", "AllTagsForFilter"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public static useTagDestroy(input: GQL.TagDestroyInput) {
|
public static useTagDestroy(input: GQL.TagDestroyInput) {
|
||||||
return GQL.useTagDestroy({
|
return GQL.useTagDestroy({
|
||||||
variables: input,
|
variables: input,
|
||||||
refetchQueries: ["AllTags", "AllTagsForFilter"],
|
refetchQueries: ["AllTags", "AllTagsForFilter"],
|
||||||
update: () => StashService.invalidateQueries(StashService.tagMutationImpactedQueries)
|
update: () => StashService.invalidateQueries(StashService.tagMutationImpactedQueries)
|
||||||
});
|
});
|
||||||
@@ -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() {}
|
private constructor() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Spinner } from "@blueprintjs/core";
|
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Spinner } from 'react-bootstrap';
|
||||||
import { QueryHookResult } from "react-apollo-hooks";
|
import { QueryHookResult } from "react-apollo-hooks";
|
||||||
import { ListFilter } from "../components/list/ListFilter";
|
import { ListFilter } from "../components/list/ListFilter";
|
||||||
import { Pagination } from "../components/list/Pagination";
|
import { Pagination } from "../components/list/Pagination";
|
||||||
@@ -201,7 +201,7 @@ export class ListHook {
|
|||||||
|
|
||||||
function singleSelect(id: string, selected: boolean) {
|
function singleSelect(id: string, selected: boolean) {
|
||||||
setLastClickedId(id);
|
setLastClickedId(id);
|
||||||
|
|
||||||
const newSelectedIds = _.clone(selectedIds);
|
const newSelectedIds = _.clone(selectedIds);
|
||||||
if (selected) {
|
if (selected) {
|
||||||
newSelectedIds.add(id);
|
newSelectedIds.add(id);
|
||||||
@@ -215,7 +215,7 @@ export class ListHook {
|
|||||||
function multiSelect(id: string, selected : boolean) {
|
function multiSelect(id: string, selected : boolean) {
|
||||||
let startIndex = 0;
|
let startIndex = 0;
|
||||||
let thisIndex = -1;
|
let thisIndex = -1;
|
||||||
|
|
||||||
if (!!lastClickedId) {
|
if (!!lastClickedId) {
|
||||||
startIndex = getItems().findIndex((item) => {
|
startIndex = getItems().findIndex((item) => {
|
||||||
return item.id === lastClickedId;
|
return item.id === lastClickedId;
|
||||||
@@ -228,14 +228,14 @@ export class ListHook {
|
|||||||
|
|
||||||
selectRange(startIndex, thisIndex);
|
selectRange(startIndex, thisIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectRange(startIndex : number, endIndex : number) {
|
function selectRange(startIndex : number, endIndex : number) {
|
||||||
if (startIndex > endIndex) {
|
if (startIndex > endIndex) {
|
||||||
let tmp = startIndex;
|
let tmp = startIndex;
|
||||||
startIndex = endIndex;
|
startIndex = endIndex;
|
||||||
endIndex = tmp;
|
endIndex = tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
const subset = getItems().slice(startIndex, endIndex + 1);
|
const subset = getItems().slice(startIndex, endIndex + 1);
|
||||||
const newSelectedIds : Set<string> = new Set();
|
const newSelectedIds : Set<string> = new Set();
|
||||||
|
|
||||||
@@ -293,7 +293,7 @@ export class ListHook {
|
|||||||
filter={filter}
|
filter={filter}
|
||||||
/>
|
/>
|
||||||
{options.renderSelectedOptions && selectedIds.size > 0 ? options.renderSelectedOptions(result, selectedIds) : undefined}
|
{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}
|
{result.error ? <h1>{result.error.message}</h1> : undefined}
|
||||||
{options.renderContent(result, filter, selectedIds, zoomIndex)}
|
{options.renderContent(result, filter, selectedIds, zoomIndex)}
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ interface IToast {
|
|||||||
header?: string;
|
header?: string;
|
||||||
content: JSX.Element|string;
|
content: JSX.Element|string;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
|
variant?: 'success'|'danger'|'warning'|'info';
|
||||||
}
|
}
|
||||||
interface IActiveToast extends IToast {
|
interface IActiveToast extends IToast {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -21,7 +22,13 @@ export const ToastProvider: React.FC = ({children}) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const toastItems = toasts.map(toast => (
|
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>
|
<Toast.Header>
|
||||||
<span className="mr-auto">
|
<span className="mr-auto">
|
||||||
{ toast.header ?? 'Stash' }
|
{ toast.header ?? 'Stash' }
|
||||||
@@ -30,7 +37,7 @@ export const ToastProvider: React.FC = ({children}) => {
|
|||||||
<Toast.Body>{toast.content}</Toast.Body>
|
<Toast.Body>{toast.content}</Toast.Body>
|
||||||
</Toast>
|
</Toast>
|
||||||
));
|
));
|
||||||
|
|
||||||
const addToast = (toast:IToast) => (
|
const addToast = (toast:IToast) => (
|
||||||
setToasts([...toasts, { ...toast, id: toastID++ }])
|
setToasts([...toasts, { ...toast, id: toastID++ }])
|
||||||
);
|
);
|
||||||
@@ -47,7 +54,17 @@ export const ToastProvider: React.FC = ({children}) => {
|
|||||||
|
|
||||||
const useToasts = () => {
|
const useToasts = () => {
|
||||||
const setToast = useContext(ToastContext);
|
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;
|
export default useToasts;
|
||||||
4
ui/v2.5/src/hooks/index.ts
Normal file
4
ui/v2.5/src/hooks/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { default as useToast } from './Toast';
|
||||||
|
export { useInterfaceLocalForage } from './LocalForage';
|
||||||
|
export { VideoHoverHook } from './VideoHover';
|
||||||
|
export { ListHook } from './ListHook';
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
@import "styles/shared/details";
|
@import "styles/shared/details";
|
||||||
|
|
||||||
@import "styles/blueprint-overrides";
|
|
||||||
@import "styles/scrollbars";
|
@import "styles/scrollbars";
|
||||||
@import "styles/variables";
|
@import "styles/variables";
|
||||||
|
|
||||||
@@ -392,7 +391,7 @@ span.block {
|
|||||||
& .inputs label {
|
& .inputs label {
|
||||||
width: 12em;
|
width: 12em;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .inputs .bp3-input-group {
|
& .inputs .bp3-input-group {
|
||||||
width: 80ch;
|
width: 80ch;
|
||||||
}
|
}
|
||||||
@@ -400,11 +399,11 @@ span.block {
|
|||||||
& .scene-parser-results {
|
& .scene-parser-results {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .scene-parser-row .bp3-checkbox {
|
& .scene-parser-row .bp3-checkbox {
|
||||||
margin: 0px -20px 0px 0px;
|
margin: 0px -20px 0px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .scene-parser-row .parser-field-title input {
|
& .scene-parser-row .parser-field-title input {
|
||||||
width: 50ch;
|
width: 50ch;
|
||||||
}
|
}
|
||||||
@@ -424,15 +423,15 @@ span.block {
|
|||||||
& .scene-parser-row .parser-field-studio input {
|
& .scene-parser-row .parser-field-studio input {
|
||||||
width: 15ch;
|
width: 15ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .scene-parser-row input {
|
& .scene-parser-row input {
|
||||||
min-width: 10ch;
|
min-width: 10ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .scene-parser-row .bp3-form-group {
|
& .scene-parser-row .bp3-form-group {
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .scene-parser-row div:first-child > input {
|
& .scene-parser-row div:first-child > input {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import { ILabeledId, ILabeledValue } from "../types";
|
import { ILabeledId, ILabeledValue } from "../types";
|
||||||
|
|
||||||
export type CriterionType =
|
export type CriterionType =
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
Criterion,
|
Criterion,
|
||||||
CriterionType,
|
CriterionType,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
Criterion,
|
Criterion,
|
||||||
CriterionType,
|
CriterionType,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
Criterion,
|
Criterion,
|
||||||
CriterionType,
|
CriterionType,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
Criterion,
|
Criterion,
|
||||||
CriterionType,
|
CriterionType,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import { ILabeledId } from "../types";
|
import { ILabeledId } from "../types";
|
||||||
import {
|
import {
|
||||||
Criterion,
|
Criterion,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
Criterion,
|
Criterion,
|
||||||
CriterionType,
|
CriterionType,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
Criterion,
|
Criterion,
|
||||||
CriterionType,
|
CriterionType,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import { ILabeledId } from "../types";
|
import { ILabeledId } from "../types";
|
||||||
import {
|
import {
|
||||||
Criterion,
|
Criterion,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as GQL from "../../../core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import { ILabeledId } from "../types";
|
import { ILabeledId } from "../types";
|
||||||
import {
|
import {
|
||||||
Criterion,
|
Criterion,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
CriterionModifier,
|
CriterionModifier,
|
||||||
} from "../../../core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import { Criterion, CriterionType, StringCriterion, NumberCriterion } from "./criterion";
|
import { Criterion, CriterionType, StringCriterion, NumberCriterion } from "./criterion";
|
||||||
import { FavoriteCriterion } from "./favorite";
|
import { FavoriteCriterion } from "./favorite";
|
||||||
import { HasMarkersCriterion } from "./has-markers";
|
import { HasMarkersCriterion } from "./has-markers";
|
||||||
@@ -24,7 +24,7 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
case "sceneTags": return new TagsCriterion("sceneTags");
|
case "sceneTags": return new TagsCriterion("sceneTags");
|
||||||
case "performers": return new PerformersCriterion();
|
case "performers": return new PerformersCriterion();
|
||||||
case "studios": return new StudiosCriterion();
|
case "studios": return new StudiosCriterion();
|
||||||
|
|
||||||
case "birth_year":
|
case "birth_year":
|
||||||
case "age":
|
case "age":
|
||||||
var ret = new NumberCriterion(type, type);
|
var ret = new NumberCriterion(type, type);
|
||||||
@@ -36,7 +36,7 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
Criterion.getModifierOption(CriterionModifier.LessThan)
|
Criterion.getModifierOption(CriterionModifier.LessThan)
|
||||||
];
|
];
|
||||||
return ret;
|
return ret;
|
||||||
case "ethnicity":
|
case "ethnicity":
|
||||||
case "country":
|
case "country":
|
||||||
case "eye_color":
|
case "eye_color":
|
||||||
case "height":
|
case "height":
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
SceneFilterType,
|
SceneFilterType,
|
||||||
SceneMarkerFilterType,
|
SceneMarkerFilterType,
|
||||||
SortDirectionEnum,
|
SortDirectionEnum,
|
||||||
} from "../../core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import { Criterion, ICriterionOption, CriterionType, CriterionOption, NumberCriterion, StringCriterion } from "./criteria/criterion";
|
import { Criterion, ICriterionOption, CriterionType, CriterionOption, NumberCriterion, StringCriterion } from "./criteria/criterion";
|
||||||
import { FavoriteCriterion, FavoriteCriterionOption } from "./criteria/favorite";
|
import { FavoriteCriterion, FavoriteCriterionOption } from "./criteria/favorite";
|
||||||
import { HasMarkersCriterion, HasMarkersCriterionOption } from "./criteria/has-markers";
|
import { HasMarkersCriterion, HasMarkersCriterionOption } from "./criteria/has-markers";
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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 "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +1,37 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
export class ImageUtils {
|
const readImage = (file: File, onLoadEnd: (this: FileReader) => void) => {
|
||||||
|
const reader: FileReader = new FileReader();
|
||||||
private static readImage(file: File, onLoadEnd: (this: FileReader) => any) {
|
reader.onloadend = onLoadEnd;
|
||||||
const reader: FileReader = new FileReader();
|
reader.readAsDataURL(file);
|
||||||
|
|
||||||
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) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const file: File = e.clipboardData.files[0];
|
|
||||||
ImageUtils.readImage(file, onLoadEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static addPasteImageHook(onLoadEnd: (this: FileReader) => any) {
|
|
||||||
useEffect(() => {
|
|
||||||
const pasteImage = (e: any) => { ImageUtils.pasteImage(e, onLoadEnd) }
|
|
||||||
window.addEventListener("paste", pasteImage);
|
|
||||||
|
|
||||||
return () => window.removeEventListener("paste", pasteImage);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 onImageChange = (event: React.FormEvent<HTMLInputElement>, onLoadEnd: (this: FileReader) => void) => {
|
||||||
|
const file = event?.currentTarget?.files?.[0];
|
||||||
|
if(file)
|
||||||
|
readImage(file, onLoadEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
const usePasteImage = (onLoadEnd: (this: FileReader) => void) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const paste = (event: ClipboardEvent) => ( pasteImage(event, onLoadEnd) );
|
||||||
|
document.addEventListener("paste", paste);
|
||||||
|
|
||||||
|
return () => document.removeEventListener("paste", paste);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const Image = {
|
||||||
|
onImageChange,
|
||||||
|
usePasteImage
|
||||||
|
}
|
||||||
|
export default Image;
|
||||||
|
|||||||
4
ui/v2.5/src/utils/index.ts
Normal file
4
ui/v2.5/src/utils/index.ts
Normal 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';
|
||||||
@@ -5,45 +5,57 @@ import { TagsCriterion } from "../models/list-filter/criteria/tags";
|
|||||||
import { ListFilterModel } from "../models/list-filter/filter";
|
import { ListFilterModel } from "../models/list-filter/filter";
|
||||||
import { FilterMode } from "../models/list-filter/types";
|
import { FilterMode } from "../models/list-filter/types";
|
||||||
|
|
||||||
export class NavigationUtils {
|
const makePerformerScenesUrl = (performer: Partial<GQL.PerformerDataFragment>) => {
|
||||||
public static makePerformerScenesUrl(performer: Partial<GQL.PerformerDataFragment>): string {
|
if (!performer.id)
|
||||||
if (performer.id === undefined) { return "#"; }
|
return "#";
|
||||||
const filter = new ListFilterModel(FilterMode.Scenes);
|
const filter = new ListFilterModel(FilterMode.Scenes);
|
||||||
const criterion = new PerformersCriterion();
|
const criterion = new PerformersCriterion();
|
||||||
criterion.value = [{ id: performer.id, label: performer.name || `Performer ${performer.id}` }];
|
criterion.value = [{ id: performer.id, label: performer.name || `Performer ${performer.id}` }];
|
||||||
filter.criteria.push(criterion);
|
filter.criteria.push(criterion);
|
||||||
return `/scenes?${filter.makeQueryParameters()}`;
|
return `/scenes?${filter.makeQueryParameters()}`;
|
||||||
}
|
|
||||||
|
|
||||||
public static makeStudioScenesUrl(studio: Partial<GQL.StudioDataFragment>): string {
|
|
||||||
if (studio.id === undefined) { return "#"; }
|
|
||||||
const filter = new ListFilterModel(FilterMode.Scenes);
|
|
||||||
const criterion = new StudiosCriterion();
|
|
||||||
criterion.value = [{ id: studio.id, label: studio.name || `Studio ${studio.id}` }];
|
|
||||||
filter.criteria.push(criterion);
|
|
||||||
return `/scenes?${filter.makeQueryParameters()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static makeTagScenesUrl(tag: Partial<GQL.TagDataFragment>): string {
|
|
||||||
if (tag.id === undefined) { return "#"; }
|
|
||||||
const filter = new ListFilterModel(FilterMode.Scenes);
|
|
||||||
const criterion = new TagsCriterion("tags");
|
|
||||||
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
|
||||||
filter.criteria.push(criterion);
|
|
||||||
return `/scenes?${filter.makeQueryParameters()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static makeTagSceneMarkersUrl(tag: Partial<GQL.TagDataFragment>): string {
|
|
||||||
if (tag.id === undefined) { return "#"; }
|
|
||||||
const filter = new ListFilterModel(FilterMode.SceneMarkers);
|
|
||||||
const criterion = new TagsCriterion("tags");
|
|
||||||
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
|
||||||
filter.criteria.push(criterion);
|
|
||||||
return `/scenes/markers?${filter.makeQueryParameters()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static makeSceneMarkerUrl(sceneMarker: Partial<GQL.SceneMarkerDataFragment>): string {
|
|
||||||
if (sceneMarker.id === undefined || sceneMarker.scene === undefined) { return "#"; }
|
|
||||||
return `/scenes/${sceneMarker.scene.id}?t=${sceneMarker.seconds}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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}` }];
|
||||||
|
filter.criteria.push(criterion);
|
||||||
|
return `/scenes?${filter.makeQueryParameters()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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}` }];
|
||||||
|
filter.criteria.push(criterion);
|
||||||
|
return `/scenes?${filter.makeQueryParameters()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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}` }];
|
||||||
|
filter.criteria.push(criterion);
|
||||||
|
return `/scenes/markers?${filter.makeQueryParameters()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -1,137 +1,133 @@
|
|||||||
import { EditableText, IOptionProps } from "@blueprintjs/core";
|
|
||||||
import { Form } from 'react-bootstrap';
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { EditableTextUtils } from "./editabletext";
|
import { Form } from 'react-bootstrap';
|
||||||
import { FilterSelect } from "../components/select/FilterSelect";
|
import { FilterSelect } from "src/components/Shared";
|
||||||
import _ from "lodash";
|
|
||||||
|
|
||||||
export class TableUtils {
|
const renderEditableTextTableRow = (options: {
|
||||||
public static renderEditableTextTableRow(options: {
|
title: string;
|
||||||
title: string;
|
value?: string | number;
|
||||||
value: string | number | undefined;
|
isEditing: boolean;
|
||||||
isEditing: boolean;
|
onChange: ((value: string) => void);
|
||||||
onChange: ((value: string) => void);
|
}) => (
|
||||||
}) {
|
<tr>
|
||||||
let stringValue = options.value;
|
<td>{options.title}</td>
|
||||||
if (typeof stringValue === "number") {
|
<td>
|
||||||
stringValue = stringValue.toString();
|
<Form.Control
|
||||||
}
|
readOnly={!options.isEditing}
|
||||||
return (
|
plaintext={!options.isEditing}
|
||||||
<tr>
|
onChange={(event: React.FormEvent<HTMLInputElement>) => ( options.onChange(event.currentTarget.value) )}
|
||||||
<td>{options.title}</td>
|
value={typeof options.value === 'number' ? options.value.toString() : options.value}
|
||||||
<td>
|
placeholder={options.title}
|
||||||
<EditableText
|
/>
|
||||||
disabled={!options.isEditing}
|
</td>
|
||||||
value={stringValue}
|
</tr>
|
||||||
placeholder={options.title}
|
)
|
||||||
multiline={true}
|
|
||||||
onChange={(newValue) => options.onChange(newValue)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static renderTextArea(options: {
|
const renderTextArea = (options: {
|
||||||
title: string,
|
title: string,
|
||||||
value: string | undefined,
|
value: string | undefined,
|
||||||
isEditing: boolean,
|
isEditing: boolean,
|
||||||
onChange: ((value: string) => void),
|
onChange: ((value: string) => void),
|
||||||
}) {
|
}) => (
|
||||||
|
<tr>
|
||||||
return (
|
<td>{options.title}</td>
|
||||||
<tr>
|
<td>
|
||||||
<td>{options.title}</td>
|
<Form.Control
|
||||||
<td>
|
as="textarea"
|
||||||
{EditableTextUtils.renderTextArea(options)}
|
readOnly={!options.isEditing}
|
||||||
</td>
|
plaintext={!options.isEditing}
|
||||||
</tr>
|
onChange={(event: React.FormEvent<HTMLTextAreaElement>) => ( options.onChange(event.currentTarget.value) )}
|
||||||
);
|
value={options.value}
|
||||||
}
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
|
||||||
public static renderInputGroup(options: {
|
const renderInputGroup = (options: {
|
||||||
title: string,
|
title: string,
|
||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
value: string | undefined,
|
value: string | undefined,
|
||||||
isEditing: boolean,
|
isEditing: boolean,
|
||||||
onChange: ((value: string) => void),
|
onChange: ((value: string) => void),
|
||||||
}) {
|
}) => (
|
||||||
let optionsCopy = _.clone(options);
|
<tr>
|
||||||
optionsCopy.placeholder = options.placeholder || options.title;
|
<td>{options.title}</td>
|
||||||
return (
|
<td>
|
||||||
<tr>
|
<Form.Control
|
||||||
<td>{options.title}</td>
|
readOnly={!options.isEditing}
|
||||||
<td>
|
plaintext={!options.isEditing}
|
||||||
{ !options.isEditing
|
defaultValue={options.value}
|
||||||
? <h4>{optionsCopy.value}</h4>
|
placeholder={options.placeholder ?? options.title}
|
||||||
: <Form.Control
|
onChange={(event: React.FormEvent<HTMLInputElement>) => ( options.onChange(event.currentTarget.value) )}
|
||||||
defaultValue={options.value}
|
/>
|
||||||
placeholder={optionsCopy.placeholder}
|
</td>
|
||||||
onChange={ (event:any) => options.onChange(event.target.value) }
|
</tr>
|
||||||
/>
|
)
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static renderHtmlSelect(options: {
|
const renderHtmlSelect = (options: {
|
||||||
title: string,
|
title: string,
|
||||||
value: string | number | undefined,
|
value?: string | number,
|
||||||
isEditing: boolean,
|
isEditing: boolean,
|
||||||
onChange: ((value: string) => void),
|
onChange: ((value: string) => void),
|
||||||
selectOptions: Array<string | number | IOptionProps>,
|
selectOptions: Array<string | number>,
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<tr>
|
||||||
<tr>
|
<td>{options.title}</td>
|
||||||
<td>{options.title}</td>
|
<td>
|
||||||
<td>
|
<Form.Control
|
||||||
{EditableTextUtils.renderHtmlSelect(options)}
|
as="select"
|
||||||
</td>
|
readOnly={!options.isEditing}
|
||||||
</tr>
|
plaintext={!options.isEditing}
|
||||||
);
|
onChange={(event: React.FormEvent<HTMLSelectElement>) => ( options.onChange(event.currentTarget.value) )}
|
||||||
}
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
|
||||||
// TODO: isediting
|
// TODO: isediting
|
||||||
public static renderFilterSelect(options: {
|
const renderFilterSelect = (options: {
|
||||||
title: string,
|
title: string,
|
||||||
type: "performers" | "studios" | "tags",
|
type: "performers" | "studios" | "tags",
|
||||||
initialId: string | undefined,
|
initialId: string | undefined,
|
||||||
onChange: ((id: string | undefined) => void),
|
onChange: ((id: string | undefined) => void),
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<tr>
|
||||||
<tr>
|
<td>{options.title}</td>
|
||||||
<td>{options.title}</td>
|
<td>
|
||||||
<td>
|
<FilterSelect
|
||||||
<FilterSelect
|
type={options.type}
|
||||||
type={options.type}
|
onSelect={(items) => options.onChange(items[0]?.id)}
|
||||||
onSelect={(items) => options.onChange(items[0]?.id)}
|
initialIds={options.initialId ? [options.initialId] : []}
|
||||||
initialIds={options.initialId ? [options.initialId] : []}
|
/>
|
||||||
/>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
)
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: isediting
|
// TODO: isediting
|
||||||
public static renderMultiSelect(options: {
|
const renderMultiSelect = (options: {
|
||||||
title: string,
|
title: string,
|
||||||
type: "performers" | "studios" | "tags",
|
type: "performers" | "studios" | "tags",
|
||||||
initialIds: string[] | undefined,
|
initialIds: string[] | undefined,
|
||||||
onChange: ((ids: string[]) => void),
|
onChange: ((ids: string[]) => void),
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<tr>
|
||||||
<tr>
|
<td>{options.title}</td>
|
||||||
<td>{options.title}</td>
|
<td>
|
||||||
<td>
|
<FilterSelect
|
||||||
<FilterSelect
|
type={options.type}
|
||||||
type={options.type}
|
isMulti={true}
|
||||||
isMulti={true}
|
onSelect={(items) => options.onChange(items.map((i) => i.id))}
|
||||||
onSelect={(items) => options.onChange(items.map((i) => i.id))}
|
initialIds={options.initialIds ?? []}
|
||||||
initialIds={options.initialIds ?? []}
|
/>
|
||||||
/>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
)
|
||||||
);
|
|
||||||
}
|
const Table = {
|
||||||
|
renderEditableTextTableRow,
|
||||||
|
renderTextArea,
|
||||||
|
renderInputGroup,
|
||||||
|
renderHtmlSelect,
|
||||||
|
renderFilterSelect,
|
||||||
|
renderMultiSelect
|
||||||
}
|
}
|
||||||
|
export default Table;
|
||||||
|
|||||||
@@ -1,83 +1,98 @@
|
|||||||
export class TextUtils {
|
const Units = [
|
||||||
|
"bytes",
|
||||||
|
"kB",
|
||||||
|
"MB",
|
||||||
|
"GB",
|
||||||
|
"TB",
|
||||||
|
"PB",
|
||||||
|
];
|
||||||
|
|
||||||
public static truncate(value?: string, limit: number = 100, tail: string = "..."): string {
|
const truncate = (value?: string, limit: number = 100, tail: string = "...") => {
|
||||||
if (!value) { return ""; }
|
if (!value)
|
||||||
return value.length > limit ? value.substring(0, limit) + tail : value;
|
return "";
|
||||||
}
|
return value.length > limit
|
||||||
|
? value.substring(0, limit) + tail
|
||||||
public static fileSize(bytes: number = 0, precision: number = 2): string {
|
: value;
|
||||||
if (isNaN(parseFloat(String(bytes))) || !isFinite(bytes)) { return "?"; }
|
|
||||||
|
|
||||||
let unit = 0;
|
|
||||||
while ( bytes >= 1024 ) {
|
|
||||||
bytes /= 1024;
|
|
||||||
unit++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bytes.toFixed(+precision) + " " + this.units[unit];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static secondsToTimestamp(seconds: number): string {
|
|
||||||
let ret = new Date(seconds * 1000).toISOString().substr(11, 8);
|
|
||||||
|
|
||||||
if (ret.startsWith("00")) {
|
|
||||||
// strip hours if under one hour
|
|
||||||
ret = ret.substr(3);
|
|
||||||
}
|
|
||||||
if (ret.startsWith("0")) {
|
|
||||||
// for duration under a minute, leave one leading zero
|
|
||||||
ret = ret.substr(1);
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static fileNameFromPath(path: string): string {
|
|
||||||
if (!!path === false) { return "No File Name"; }
|
|
||||||
return path.replace(/^.*[\\\/]/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static age(dateString?: string, fromDateString?: string): number {
|
|
||||||
if (!dateString) { return 0; }
|
|
||||||
|
|
||||||
const birthdate = new Date(dateString);
|
|
||||||
const fromDate = !!fromDateString ? new Date(fromDateString) : new Date();
|
|
||||||
|
|
||||||
let age = fromDate.getFullYear() - birthdate.getFullYear();
|
|
||||||
if (birthdate.getMonth() > fromDate.getMonth() ||
|
|
||||||
(birthdate.getMonth() >= fromDate.getMonth() && birthdate.getDay() > fromDate.getDay())) {
|
|
||||||
age -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return age;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bitRate(bitrate: number) {
|
|
||||||
const megabits = bitrate / 1000000;
|
|
||||||
return `${megabits.toFixed(2)} megabits per second`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static resolution(height: number) {
|
|
||||||
if (height >= 240 && height < 480) {
|
|
||||||
return "240p";
|
|
||||||
} else if (height >= 480 && height < 720) {
|
|
||||||
return "480p";
|
|
||||||
} else if (height >= 720 && height < 1080) {
|
|
||||||
return "720p";
|
|
||||||
} else if (height >= 1080 && height < 2160) {
|
|
||||||
return "1080p";
|
|
||||||
} else if (height >= 2160) {
|
|
||||||
return "4K";
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static units = [
|
|
||||||
"bytes",
|
|
||||||
"kB",
|
|
||||||
"MB",
|
|
||||||
"GB",
|
|
||||||
"TB",
|
|
||||||
"PB",
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fileSize = (bytes: number = 0, precision: number = 2) => {
|
||||||
|
if (Number.isNaN(parseFloat(String(bytes))) || !isFinite(bytes))
|
||||||
|
return "?";
|
||||||
|
|
||||||
|
let unit = 0;
|
||||||
|
while ( bytes >= 1024 ) {
|
||||||
|
bytes /= 1024;
|
||||||
|
unit++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${bytes.toFixed(+precision)} ${Units[unit]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secondsToTimestamp = (seconds: number) => {
|
||||||
|
let ret = new Date(seconds * 1000).toISOString().substr(11, 8);
|
||||||
|
|
||||||
|
if (ret.startsWith("00")) {
|
||||||
|
// strip hours if under one hour
|
||||||
|
ret = ret.substr(3);
|
||||||
|
}
|
||||||
|
if (ret.startsWith("0")) {
|
||||||
|
// for duration under a minute, leave one leading zero
|
||||||
|
ret = ret.substr(1);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileNameFromPath = (path: string) => {
|
||||||
|
if (!!path === false)
|
||||||
|
return "No File Name";
|
||||||
|
return path.replace(/^.*[\\/]/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const age = (dateString?: string, fromDateString?: string) => {
|
||||||
|
if (!dateString)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
const birthdate = new Date(dateString);
|
||||||
|
const fromDate = fromDateString ? new Date(fromDateString) : new Date();
|
||||||
|
|
||||||
|
let age = fromDate.getFullYear() - birthdate.getFullYear();
|
||||||
|
if (birthdate.getMonth() > fromDate.getMonth() ||
|
||||||
|
(birthdate.getMonth() >= fromDate.getMonth() && birthdate.getDay() > fromDate.getDay())) {
|
||||||
|
age -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return age;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bitRate = (bitrate: number) => {
|
||||||
|
const megabits = bitrate / 1000000;
|
||||||
|
return `${megabits.toFixed(2)} megabits per second`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolution = (height: number) => {
|
||||||
|
if (height >= 240 && height < 480) {
|
||||||
|
return "240p";
|
||||||
|
} else if (height >= 480 && height < 720) {
|
||||||
|
return "480p";
|
||||||
|
} else if (height >= 720 && height < 1080) {
|
||||||
|
return "720p";
|
||||||
|
} else if (height >= 1080 && height < 2160) {
|
||||||
|
return "1080p";
|
||||||
|
} else if (height >= 2160) {
|
||||||
|
return "4K";
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextUtils = {
|
||||||
|
truncate,
|
||||||
|
fileSize,
|
||||||
|
secondsToTimestamp,
|
||||||
|
fileNameFromPath,
|
||||||
|
age,
|
||||||
|
bitRate,
|
||||||
|
resolution
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextUtils;
|
||||||
|
|||||||
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export class ZoomUtils {
|
|
||||||
public static classForZoom(zoomIndex: number): string {
|
|
||||||
return "zoom-" + zoomIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -18,11 +18,11 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
|
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"experimentalDecorators": true
|
"experimentalDecorators": true,
|
||||||
|
"baseUrl": "."
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -953,57 +953,6 @@
|
|||||||
lodash "^4.17.13"
|
lodash "^4.17.13"
|
||||||
to-fast-properties "^2.0.0"
|
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":
|
"@cnakazawa/watch@^1.0.3":
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
|
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
|
||||||
@@ -1483,10 +1432,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/babel-types" "*"
|
"@types/babel-types" "*"
|
||||||
|
|
||||||
"@types/dom4@^2.0.1":
|
"@types/classnames@^2.2.9":
|
||||||
version "2.0.1"
|
version "2.2.9"
|
||||||
resolved "https://registry.yarnpkg.com/@types/dom4/-/dom4-2.0.1.tgz#506d5781b9bcab81bd9a878b198aec7dee2a6033"
|
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.9.tgz#d868b6febb02666330410fe7f58f3c4b8258be7b"
|
||||||
integrity sha512-kSkVAvWmMZiCYtvqjqQEwOmvKwcH+V4uiv3qPQ8pAh1Xl39xggGEo8gHUqV4waYGHezdFw0rKBR8Jt0CrQSDZA==
|
integrity sha512-MNl+rT5UmZeilaPxAVs6YaPC2m6aA8rofviZbhbxpPpl61uKodfdQVsBtgJGTqGizEf02oW3tsVe7FYB8kK14A==
|
||||||
|
|
||||||
"@types/eslint-visitor-keys@^1.0.0":
|
"@types/eslint-visitor-keys@^1.0.0":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
@@ -3209,7 +3158,7 @@ class-utils@^0.3.5:
|
|||||||
isobject "^3.0.0"
|
isobject "^3.0.0"
|
||||||
static-extend "^0.1.1"
|
static-extend "^0.1.1"
|
||||||
|
|
||||||
classnames@^2.2, classnames@^2.2.6:
|
classnames@^2.2.6:
|
||||||
version "2.2.6"
|
version "2.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
|
||||||
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
|
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
|
||||||
@@ -3647,14 +3596,6 @@ create-react-context@^0.2.2:
|
|||||||
fbjs "^0.8.0"
|
fbjs "^0.8.0"
|
||||||
gud "^1.0.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:
|
cross-fetch@2.2.2:
|
||||||
version "2.2.2"
|
version "2.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-2.2.2.tgz#a47ff4f7fc712daba8f6a695a11c948440d45723"
|
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"
|
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
||||||
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
|
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
|
||||||
|
|
||||||
deep-equal@^1.0.1, deep-equal@^1.1.1:
|
deep-equal@^1.0.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
|
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
|
||||||
integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==
|
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"
|
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
|
||||||
integrity sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=
|
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:
|
domain-browser@^1.1.1:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
|
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:
|
dependencies:
|
||||||
ts-pnp "^1.1.2"
|
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"
|
version "1.16.0"
|
||||||
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.0.tgz#2e1816bcbbaa518ea6c2e15a466f4cb9c6e2fbb3"
|
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.0.tgz#2e1816bcbbaa518ea6c2e15a466f4cb9c6e2fbb3"
|
||||||
integrity sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw==
|
integrity sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw==
|
||||||
@@ -10126,19 +10062,6 @@ react-photo-gallery@7.0.2:
|
|||||||
prop-types "~15.7.2"
|
prop-types "~15.7.2"
|
||||||
resize-observer-polyfill "^1.5.0"
|
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:
|
react-prop-toggle@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-prop-toggle/-/react-prop-toggle-1.0.2.tgz#8b0b7e74653606b1427cfcf6c4eaa9198330568e"
|
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-input-autosize "^2.2.2"
|
||||||
react-transition-group "^2.2.1"
|
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"
|
version "2.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
|
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
|
||||||
integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
|
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"
|
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||||
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
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"
|
version "1.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
||||||
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
|
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"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
|
||||||
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
|
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:
|
tsutils@^3.17.1:
|
||||||
version "3.17.1"
|
version "3.17.1"
|
||||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
|
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"
|
resolved "https://registry.yarnpkg.com/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3"
|
||||||
integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==
|
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:
|
typedarray@^0.0.6:
|
||||||
version "0.0.6"
|
version "0.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||||
@@ -12260,7 +12173,7 @@ warning@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.0.0"
|
loose-envify "^1.0.0"
|
||||||
|
|
||||||
warning@^4.0.2, warning@^4.0.3:
|
warning@^4.0.3:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
||||||
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
||||||
|
|||||||
Reference in New Issue
Block a user