mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Prettier
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"airbnb-typescript",
|
||||
"prettier",
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint --cache src/**/*.{ts,tsx}",
|
||||
"lint:fix": "eslint --fix src/**/*.{ts,tsx}",
|
||||
"format": "prettier \"src/**/*.{js,jsx,ts,tsx}\"",
|
||||
"gqlgen": "gql-gen --config codegen.yml"
|
||||
},
|
||||
"browserslist": [
|
||||
@@ -68,14 +69,13 @@
|
||||
"eslint-config-airbnb-typescript": "^6.3.1",
|
||||
"eslint-config-prettier": "^6.9.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-prettier": "^3.1.2",
|
||||
"graphql-code-generator": "0.18.2",
|
||||
"graphql-codegen-add": "0.18.2",
|
||||
"graphql-codegen-time": "0.18.2",
|
||||
"graphql-codegen-typescript-client": "0.18.2",
|
||||
"graphql-codegen-typescript-common": "0.18.2",
|
||||
"graphql-codegen-typescript-react-apollo": "0.18.2",
|
||||
"prettier": "^1.19.1",
|
||||
"prettier": "1.19.1",
|
||||
"typescript": "~3.7.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { ToastProvider } from 'src/hooks/Toast';
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { fas } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ToastProvider } from "src/hooks/Toast";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { fas } from "@fortawesome/free-solid-svg-icons";
|
||||
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||
import Galleries from "./components/Galleries/Galleries";
|
||||
import { MainNavbar } from "./components/MainNavbar";
|
||||
@@ -15,8 +15,7 @@ import Studios from "./components/Studios/Studios";
|
||||
import Tags from "./components/Tags/Tags";
|
||||
import { SceneFilenameParser } from "./components/scenes/SceneFilenameParser";
|
||||
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
|
||||
library.add(fas);
|
||||
|
||||
@@ -35,7 +34,10 @@ export const App: React.FC = () => (
|
||||
<Route path="/tags" component={Tags} />
|
||||
<Route path="/studios" component={Studios} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
<Route path="/sceneFilenameParser" component={SceneFilenameParser} />
|
||||
<Route
|
||||
path="/sceneFilenameParser"
|
||||
component={SceneFilenameParser}
|
||||
/>
|
||||
<Route component={PageNotFound} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ export class ErrorBoundary extends React.Component<any, any> {
|
||||
public componentDidCatch(error: any, errorInfo: any) {
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo,
|
||||
errorInfo
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import React from "react";
|
||||
import { Spinner } from 'react-bootstrap';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Spinner } from "react-bootstrap";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { GalleryViewer } from "./GalleryViewer";
|
||||
|
||||
export const Gallery: React.FC = () => {
|
||||
const { id = '' } = useParams();
|
||||
const { id = "" } = useParams();
|
||||
|
||||
const { data, error, loading } = StashService.useFindGallery(id);
|
||||
const gallery = data?.findGallery;
|
||||
|
||||
if (loading || !gallery)
|
||||
return <Spinner animation="border" variant="light" />;
|
||||
if (error)
|
||||
return <div>{error.message}</div>;
|
||||
if (error) return <div>{error.message}</div>;
|
||||
|
||||
return (
|
||||
<div style={{width: "75vw", margin: "0 auto"}}>
|
||||
<div style={{ width: "75vw", margin: "0 auto" }}>
|
||||
<GalleryViewer gallery={gallery as any} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
import React from "react";
|
||||
import { Table } from 'react-bootstrap';
|
||||
import { Table } from "react-bootstrap";
|
||||
import { QueryHookResult } from "react-apollo-hooks";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FindGalleriesQuery, FindGalleriesVariables } from "src/core/generated-graphql";
|
||||
import {
|
||||
FindGalleriesQuery,
|
||||
FindGalleriesVariables
|
||||
} from "src/core/generated-graphql";
|
||||
import { useGalleriesList } from "src/hooks";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
|
||||
export const GalleryList: React.FC = () => {
|
||||
const listData = useGalleriesList({
|
||||
renderContent,
|
||||
renderContent
|
||||
});
|
||||
|
||||
function renderContent(result: QueryHookResult<FindGalleriesQuery, FindGalleriesVariables>, filter: ListFilterModel) {
|
||||
if (!result.data || !result.data.findGalleries) { return; }
|
||||
function renderContent(
|
||||
result: QueryHookResult<FindGalleriesQuery, FindGalleriesVariables>,
|
||||
filter: ListFilterModel
|
||||
) {
|
||||
if (!result.data || !result.data.findGalleries) {
|
||||
return;
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.Grid) {
|
||||
return <h1>TODO</h1>;
|
||||
} if (filter.displayMode === DisplayMode.List) {
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.List) {
|
||||
return (
|
||||
<Table style={{margin: "0 auto"}}>
|
||||
<Table style={{ margin: "0 auto" }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Preview</th>
|
||||
@@ -26,20 +35,27 @@ export const GalleryList: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{result.data.findGalleries.galleries.map((gallery) => (
|
||||
{result.data.findGalleries.galleries.map(gallery => (
|
||||
<tr key={gallery.id}>
|
||||
<td>
|
||||
<Link to={`/galleries/${gallery.id}`}>
|
||||
{gallery.files.length > 0 ? <img alt="" src={`${gallery.files[0].path}?thumb=true`} /> : undefined}
|
||||
{gallery.files.length > 0 ? (
|
||||
<img alt="" src={`${gallery.files[0].path}?thumb=true`} />
|
||||
) : (
|
||||
undefined
|
||||
)}
|
||||
</Link>
|
||||
</td>
|
||||
<td><Link to={`/galleries/${gallery.id}`}>{gallery.path}</Link></td>
|
||||
<td>
|
||||
<Link to={`/galleries/${gallery.id}`}>{gallery.path}</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
);
|
||||
} if (filter.displayMode === DisplayMode.Wall) {
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.Wall) {
|
||||
return <h1>TODO</h1>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@ export const GalleryViewer: FunctionComponent<IProps> = ({ gallery }) => {
|
||||
const [currentImage, setCurrentImage] = useState<number>(0);
|
||||
const [lightboxIsOpen, setLightboxIsOpen] = useState<boolean>(false);
|
||||
|
||||
function openLightbox(_event: React.MouseEvent<Element>, obj: {index: number}) {
|
||||
function openLightbox(
|
||||
_event: React.MouseEvent<Element>,
|
||||
obj: { index: number }
|
||||
) {
|
||||
setCurrentImage(obj.index);
|
||||
setLightboxIsOpen(true);
|
||||
}
|
||||
@@ -26,8 +29,15 @@ export const GalleryViewer: FunctionComponent<IProps> = ({ gallery }) => {
|
||||
setCurrentImage(currentImage + 1);
|
||||
}
|
||||
|
||||
const photos = gallery.files.map((file) => ({src: file.path || "", caption: file.name}));
|
||||
const thumbs = gallery.files.map((file) => ({src: `${file.path}?thumb=true` || "", width: 1, height: 1}));
|
||||
const photos = gallery.files.map(file => ({
|
||||
src: file.path || "",
|
||||
caption: file.name
|
||||
}));
|
||||
const thumbs = gallery.files.map(file => ({
|
||||
src: `${file.path}?thumb=true` || "",
|
||||
width: 1,
|
||||
height: 1
|
||||
}));
|
||||
return (
|
||||
<div>
|
||||
<Gallery photos={thumbs} columns={15} onClick={openLightbox} />
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import { Nav, Navbar, Button } from "react-bootstrap";
|
||||
import { IconName } from '@fortawesome/fontawesome-svg-core';
|
||||
import { LinkContainer } from 'react-router-bootstrap';
|
||||
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
||||
import { LinkContainer } from "react-router-bootstrap";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
|
||||
import { Icon } from 'src/components/Shared'
|
||||
import { Icon } from "src/components/Shared";
|
||||
|
||||
interface IMenuItem {
|
||||
text: string;
|
||||
@@ -12,47 +12,52 @@ interface IMenuItem {
|
||||
icon: IconName;
|
||||
}
|
||||
|
||||
const menuItems:IMenuItem[] = [
|
||||
{
|
||||
const menuItems: IMenuItem[] = [
|
||||
{
|
||||
icon: "play-circle",
|
||||
text: "Scenes",
|
||||
href: "/scenes"
|
||||
},
|
||||
{
|
||||
},
|
||||
{
|
||||
href: "/scenes/markers",
|
||||
icon: "map-marker-alt",
|
||||
text: "Markers"
|
||||
},
|
||||
{
|
||||
},
|
||||
{
|
||||
href: "/galleries",
|
||||
icon: "image",
|
||||
text: "Galleries"
|
||||
},
|
||||
{
|
||||
},
|
||||
{
|
||||
href: "/performers",
|
||||
icon: "user",
|
||||
text: "Performers"
|
||||
},
|
||||
{
|
||||
},
|
||||
{
|
||||
href: "/studios",
|
||||
icon: "video",
|
||||
text: "Studios"
|
||||
},
|
||||
{
|
||||
},
|
||||
{
|
||||
href: "/tags",
|
||||
icon: "tag",
|
||||
text: "Tags"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export const MainNavbar: React.FC = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const path = location.pathname === '/performers'
|
||||
? '/performers/new'
|
||||
: location.pathname === '/studios'
|
||||
? '/studios/new' : null;
|
||||
const newButton = path === null ? '' : (
|
||||
const path =
|
||||
location.pathname === "/performers"
|
||||
? "/performers/new"
|
||||
: location.pathname === "/studios"
|
||||
? "/studios/new"
|
||||
: null;
|
||||
const newButton =
|
||||
path === null ? (
|
||||
""
|
||||
) : (
|
||||
<LinkContainer to={path}>
|
||||
<Button variant="primary">New</Button>
|
||||
</LinkContainer>
|
||||
@@ -66,7 +71,7 @@ export const MainNavbar: React.FC = () => {
|
||||
</Link>
|
||||
</Navbar.Brand>
|
||||
<Nav className="mr-auto">
|
||||
{menuItems.map((i) => (
|
||||
{menuItems.map(i => (
|
||||
<LinkContainer
|
||||
activeClassName="active"
|
||||
exact
|
||||
@@ -82,9 +87,7 @@ export const MainNavbar: React.FC = () => {
|
||||
</Nav>
|
||||
<Nav>
|
||||
{newButton}
|
||||
<LinkContainer
|
||||
exact
|
||||
to="/settings">
|
||||
<LinkContainer exact to="/settings">
|
||||
<Button variant="secondary">
|
||||
<Icon icon="cog" />
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
export const PageNotFound: FunctionComponent = () => {
|
||||
return (
|
||||
<h1>Page not found.</h1>
|
||||
);
|
||||
return <h1>Page not found.</h1>;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import queryString from "query-string";
|
||||
import { Card, Tab, Nav, Row, Col } from 'react-bootstrap';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Card, Tab, Nav, Row, Col } from "react-bootstrap";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { SettingsAboutPanel } from "./SettingsAboutPanel";
|
||||
import { SettingsConfigurationPanel } from "./SettingsConfigurationPanel";
|
||||
import { SettingsInterfacePanel } from "./SettingsInterfacePanel";
|
||||
@@ -11,13 +11,17 @@ import { SettingsTasksPanel } from "./SettingsTasksPanel/SettingsTasksPanel";
|
||||
export const Settings: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const defaultTab = queryString.parse(location.search).tab ?? 'configuration';
|
||||
const defaultTab = queryString.parse(location.search).tab ?? "configuration";
|
||||
|
||||
const onSelect = ((val:string) => history.push(`?tab=${val}`));
|
||||
const onSelect = (val: string) => history.push(`?tab=${val}`);
|
||||
|
||||
return (
|
||||
<Card id="details-container">
|
||||
<Tab.Container defaultActiveKey={defaultTab} id="configuration-tabs" onSelect={onSelect}>
|
||||
<Tab.Container
|
||||
defaultActiveKey={defaultTab}
|
||||
id="configuration-tabs"
|
||||
onSelect={onSelect}
|
||||
>
|
||||
<Row>
|
||||
<Col sm={2}>
|
||||
<Nav variant="pills" className="flex-column">
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React from "react";
|
||||
import { Table, Spinner } from 'react-bootstrap';
|
||||
import { Table, Spinner } from "react-bootstrap";
|
||||
import { StashService } from "src/core/StashService";
|
||||
|
||||
export const SettingsAboutPanel: React.FC = () => {
|
||||
const { data, error, loading } = StashService.useVersion();
|
||||
|
||||
function maybeRenderTag() {
|
||||
if (!data || !data.version || !data.version.version) { return; }
|
||||
if (!data || !data.version || !data.version.version) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<tr>
|
||||
<td>Version:</td>
|
||||
@@ -16,7 +18,9 @@ export const SettingsAboutPanel: React.FC = () => {
|
||||
}
|
||||
|
||||
function renderVersion() {
|
||||
if (!data || !data.version) { return; }
|
||||
if (!data || !data.version) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Table>
|
||||
@@ -38,8 +42,8 @@ export const SettingsAboutPanel: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<h4>About</h4>
|
||||
{!data || loading ? <Spinner animation="border" variant="light" /> : ''}
|
||||
{error ? <span>error.message</span> : ''}
|
||||
{!data || loading ? <Spinner animation="border" variant="light" /> : ""}
|
||||
{error ? <span>error.message</span> : ""}
|
||||
{renderVersion()}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Form, InputGroup, Spinner } from 'react-bootstrap';
|
||||
import { Button, Form, InputGroup, Spinner } from "react-bootstrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { useToast } from 'src/hooks';
|
||||
import { Icon } from 'src/components/Shared';
|
||||
import { useToast } from "src/hooks";
|
||||
import { Icon } from "src/components/Shared";
|
||||
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
|
||||
|
||||
export const SettingsConfigurationPanel: React.FC = () => {
|
||||
const Toast = useToast();
|
||||
// Editing config state
|
||||
const [stashes, setStashes] = useState<string[]>([]);
|
||||
const [databasePath, setDatabasePath] = useState<string | undefined>(undefined);
|
||||
const [generatedPath, setGeneratedPath] = useState<string | undefined>(undefined);
|
||||
const [maxTranscodeSize, setMaxTranscodeSize] = useState<GQL.StreamingResolutionEnum | undefined>(undefined);
|
||||
const [maxStreamingTranscodeSize, setMaxStreamingTranscodeSize] = useState<GQL.StreamingResolutionEnum | undefined>(undefined);
|
||||
const [databasePath, setDatabasePath] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [generatedPath, setGeneratedPath] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [maxTranscodeSize, setMaxTranscodeSize] = useState<
|
||||
GQL.StreamingResolutionEnum | undefined
|
||||
>(undefined);
|
||||
const [maxStreamingTranscodeSize, setMaxStreamingTranscodeSize] = useState<
|
||||
GQL.StreamingResolutionEnum | undefined
|
||||
>(undefined);
|
||||
const [username, setUsername] = useState<string | undefined>(undefined);
|
||||
const [password, setPassword] = useState<string | undefined>(undefined);
|
||||
const [logFile, setLogFile] = useState<string | undefined>();
|
||||
const [logOut, setLogOut] = useState<boolean>(true);
|
||||
const [logLevel, setLogLevel] = useState<string>("Info");
|
||||
const [logAccess, setLogAccess] = useState<boolean>(true);
|
||||
const [excludes, setExcludes] = useState<(string)[]>([]);
|
||||
const [excludes, setExcludes] = useState<string[]>([]);
|
||||
|
||||
const { data, error, loading } = StashService.useConfiguration();
|
||||
|
||||
@@ -36,12 +44,11 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
logOut,
|
||||
logLevel,
|
||||
logAccess,
|
||||
excludes,
|
||||
excludes
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!data?.configuration || error)
|
||||
return;
|
||||
if (!data?.configuration || error) return;
|
||||
|
||||
const conf = data.configuration;
|
||||
if (conf.general) {
|
||||
@@ -65,27 +72,26 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
|
||||
function excludeRegexChanged(idx: number, value: string) {
|
||||
const newExcludes = excludes.map((regex, i)=> {
|
||||
const ret = ( idx !== i ) ? regex : value ;
|
||||
return ret
|
||||
})
|
||||
const newExcludes = excludes.map((regex, i) => {
|
||||
const ret = idx !== i ? regex : value;
|
||||
return ret;
|
||||
});
|
||||
setExcludes(newExcludes);
|
||||
}
|
||||
|
||||
function excludeRemoveRegex(idx: number) {
|
||||
const newExcludes = excludes.filter((_regex, i) => i !== idx );
|
||||
const newExcludes = excludes.filter((_regex, i) => i !== idx);
|
||||
|
||||
setExcludes(newExcludes);
|
||||
}
|
||||
|
||||
function excludeAddRegex() {
|
||||
const demo = "sample\\.mp4$"
|
||||
const demo = "sample\\.mp4$";
|
||||
const newExcludes = excludes.concat(demo);
|
||||
|
||||
setExcludes(newExcludes);
|
||||
}
|
||||
|
||||
|
||||
async function onSave() {
|
||||
try {
|
||||
const result = await updateGeneralConfig();
|
||||
@@ -106,35 +112,46 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
GQL.StreamingResolutionEnum.Original
|
||||
].map(resolutionToString);
|
||||
|
||||
function resolutionToString(r : GQL.StreamingResolutionEnum | undefined) {
|
||||
function resolutionToString(r: GQL.StreamingResolutionEnum | undefined) {
|
||||
switch (r) {
|
||||
case GQL.StreamingResolutionEnum.Low: return "240p";
|
||||
case GQL.StreamingResolutionEnum.Standard: return "480p";
|
||||
case GQL.StreamingResolutionEnum.StandardHd: return "720p";
|
||||
case GQL.StreamingResolutionEnum.FullHd: return "1080p";
|
||||
case GQL.StreamingResolutionEnum.FourK: return "4k";
|
||||
case GQL.StreamingResolutionEnum.Original: return "Original";
|
||||
case GQL.StreamingResolutionEnum.Low:
|
||||
return "240p";
|
||||
case GQL.StreamingResolutionEnum.Standard:
|
||||
return "480p";
|
||||
case GQL.StreamingResolutionEnum.StandardHd:
|
||||
return "720p";
|
||||
case GQL.StreamingResolutionEnum.FullHd:
|
||||
return "1080p";
|
||||
case GQL.StreamingResolutionEnum.FourK:
|
||||
return "4k";
|
||||
case GQL.StreamingResolutionEnum.Original:
|
||||
return "Original";
|
||||
}
|
||||
|
||||
return "Original";
|
||||
}
|
||||
|
||||
function translateQuality(quality : string) {
|
||||
function translateQuality(quality: string) {
|
||||
switch (quality) {
|
||||
case "240p": return GQL.StreamingResolutionEnum.Low;
|
||||
case "480p": return GQL.StreamingResolutionEnum.Standard;
|
||||
case "720p": return GQL.StreamingResolutionEnum.StandardHd;
|
||||
case "1080p": return GQL.StreamingResolutionEnum.FullHd;
|
||||
case "4k": return GQL.StreamingResolutionEnum.FourK;
|
||||
case "Original": return GQL.StreamingResolutionEnum.Original;
|
||||
case "240p":
|
||||
return GQL.StreamingResolutionEnum.Low;
|
||||
case "480p":
|
||||
return GQL.StreamingResolutionEnum.Standard;
|
||||
case "720p":
|
||||
return GQL.StreamingResolutionEnum.StandardHd;
|
||||
case "1080p":
|
||||
return GQL.StreamingResolutionEnum.FullHd;
|
||||
case "4k":
|
||||
return GQL.StreamingResolutionEnum.FourK;
|
||||
case "Original":
|
||||
return GQL.StreamingResolutionEnum.Original;
|
||||
}
|
||||
|
||||
return GQL.StreamingResolutionEnum.Original;
|
||||
}
|
||||
|
||||
if(error)
|
||||
return <h1>{error.message}</h1>;
|
||||
if(!data?.configuration || loading)
|
||||
if (error) return <h1>{error.message}</h1>;
|
||||
if (!data?.configuration || loading)
|
||||
return <Spinner animation="border" variant="light" />;
|
||||
|
||||
return (
|
||||
@@ -147,37 +164,56 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
directories={stashes}
|
||||
onDirectoriesChanged={onStashesChanged}
|
||||
/>
|
||||
<Form.Text className="text-muted">Directory locations to your content</Form.Text>
|
||||
<Form.Text className="text-muted">
|
||||
Directory locations to your content
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="database-path">
|
||||
<Form.Label>Database Path</Form.Label>
|
||||
<Form.Control defaultValue={databasePath} onChange={(e: any) => setDatabasePath(e.target.value)} />
|
||||
<Form.Text className="text-muted">File location for the SQLite database (requires restart)</Form.Text>
|
||||
<Form.Control
|
||||
defaultValue={databasePath}
|
||||
onChange={(e: any) => setDatabasePath(e.target.value)}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
File location for the SQLite database (requires restart)
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="generated-path">
|
||||
<Form.Label>Generated Path</Form.Label>
|
||||
<Form.Control defaultValue={generatedPath} onChange={(e: any) => setGeneratedPath(e.target.value)} />
|
||||
<Form.Text className="text-muted">Directory location for the generated files (scene markers, scene previews, sprites, etc)</Form.Text>
|
||||
<Form.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>
|
||||
|
||||
<Form.Group>
|
||||
<Form.Label>Excluded Patterns</Form.Label>
|
||||
{ excludes ? excludes.map((regexp, i) => (
|
||||
{excludes
|
||||
? excludes.map((regexp, i) => (
|
||||
<InputGroup>
|
||||
<Form.Control
|
||||
value={regexp}
|
||||
onChange={(e: any) => excludeRegexChanged(i, e.target.value)}
|
||||
onChange={(e: any) =>
|
||||
excludeRegexChanged(i, e.target.value)
|
||||
}
|
||||
/>
|
||||
<InputGroup.Append>
|
||||
<Button variant="danger" onClick={() => excludeRemoveRegex(i)}>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => excludeRemoveRegex(i)}
|
||||
>
|
||||
<Icon icon="minus" />
|
||||
</Button>
|
||||
</InputGroup.Append>
|
||||
</InputGroup>
|
||||
)) : ''
|
||||
}
|
||||
))
|
||||
: ""}
|
||||
|
||||
<Button variant="danger" onClick={() => excludeAddRegex()}>
|
||||
<Icon icon="plus" />
|
||||
@@ -189,7 +225,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span>Regexps of files/paths to exclude from Scan and add to Clean</span>
|
||||
<span>
|
||||
Regexps of files/paths to exclude from Scan and add to Clean
|
||||
</span>
|
||||
<Icon icon="question-circle" />
|
||||
</a>
|
||||
</p>
|
||||
@@ -205,23 +243,41 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<Form.Label>Maximum transcode size</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
onChange={(event:React.FormEvent<HTMLSelectElement>) => setMaxTranscodeSize(translateQuality(event.currentTarget.value))}
|
||||
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
|
||||
setMaxTranscodeSize(translateQuality(event.currentTarget.value))
|
||||
}
|
||||
value={resolutionToString(maxTranscodeSize)}
|
||||
>
|
||||
{ transcodeQualities.map(q => (<option key={q} value={q}>{q}</option>))}
|
||||
{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.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))}
|
||||
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
|
||||
setMaxStreamingTranscodeSize(
|
||||
translateQuality(event.currentTarget.value)
|
||||
)
|
||||
}
|
||||
value={resolutionToString(maxStreamingTranscodeSize)}
|
||||
>
|
||||
{ transcodeQualities.map(q => (<option key={q} value={q}>{q}</option>))}
|
||||
{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.Text className="text-muted">
|
||||
Maximum size for transcoded streams
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
|
||||
@@ -231,13 +287,28 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<h4>Authentication</h4>
|
||||
<Form.Group id="username">
|
||||
<Form.Label>Username</Form.Label>
|
||||
<Form.Control defaultValue={username} onChange={(e: React.FormEvent<HTMLInputElement>) => setUsername(e.currentTarget.value)} />
|
||||
<Form.Text className="text-muted">Username to access Stash. Leave blank to disable user authentication</Form.Text>
|
||||
<Form.Control
|
||||
defaultValue={username}
|
||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
setUsername(e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Username to access Stash. Leave blank to disable user authentication
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<Form.Group id="password">
|
||||
<Form.Label>Password</Form.Label>
|
||||
<Form.Control type="password" defaultValue={password} onChange={(e: React.FormEvent<HTMLInputElement>) => setPassword(e.currentTarget.value)} />
|
||||
<Form.Text className="text-muted">Password to access Stash. Leave blank to disable user authentication</Form.Text>
|
||||
<Form.Control
|
||||
type="password"
|
||||
defaultValue={password}
|
||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
setPassword(e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Password to access Stash. Leave blank to disable user authentication
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
|
||||
@@ -246,8 +317,16 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<h4>Logging</h4>
|
||||
<Form.Group id="log-file">
|
||||
<Form.Label>Log file</Form.Label>
|
||||
<Form.Control defaultValue={logFile} onChange={(e: React.FormEvent<HTMLInputElement>) => setLogFile(e.currentTarget.value)} />
|
||||
<Form.Text className="text-muted">Path to the file to output logging to. Blank to disable file logging. Requires restart.</Form.Text>
|
||||
<Form.Control
|
||||
defaultValue={logFile}
|
||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
setLogFile(e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Path to the file to output logging to. Blank to disable file logging.
|
||||
Requires restart.
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
@@ -256,17 +335,26 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
label="Log to terminal"
|
||||
onChange={() => setLogOut(!logOut)}
|
||||
/>
|
||||
<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.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>
|
||||
|
||||
<Form.Group id="log-level">
|
||||
<Form.Label>Log Level</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
onChange={(event:React.FormEvent<HTMLSelectElement>) => setLogLevel(event.currentTarget.value)}
|
||||
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
|
||||
setLogLevel(event.currentTarget.value)
|
||||
}
|
||||
value={logLevel}
|
||||
>
|
||||
{ ["Debug", "Info", "Warning", "Error"].map(o => (<option key={o} value={o}>{o}</option>)) }
|
||||
{["Debug", "Info", "Warning", "Error"].map(o => (
|
||||
<option key={o} value={o}>
|
||||
{o}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
|
||||
@@ -276,12 +364,16 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
label="Log http access"
|
||||
onChange={() => setLogAccess(!logAccess)}
|
||||
/>
|
||||
<Form.Text className="text-muted">Logs http access to the terminal. Requires restart.</Form.Text>
|
||||
<Form.Text className="text-muted">
|
||||
Logs http access to the terminal. Requires restart.
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
|
||||
<Button variant="primary" onClick={() => onSave()}>Save</Button>
|
||||
<Button variant="primary" onClick={() => onSave()}>
|
||||
Save
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Form, Spinner } from 'react-bootstrap';
|
||||
import { Button, Form, Spinner } from "react-bootstrap";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { useToast } from 'src/hooks';
|
||||
import { useToast } from "src/hooks";
|
||||
|
||||
export const SettingsInterfacePanel: React.FC = () => {
|
||||
const Toast = useToast();
|
||||
@@ -25,8 +25,7 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (config.error)
|
||||
return;
|
||||
if (config.error) return;
|
||||
|
||||
const iCfg = config?.data?.configuration?.interface;
|
||||
setSoundOnPreview(iCfg?.soundOnPreview ?? true);
|
||||
@@ -51,8 +50,12 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{config.error ? <h1>{config.error.message}</h1> : ''}
|
||||
{(!config?.data?.configuration || config.loading) ? <Spinner animation="border" variant="light" /> : ''}
|
||||
{config.error ? <h1>{config.error.message}</h1> : ""}
|
||||
{!config?.data?.configuration || config.loading ? (
|
||||
<Spinner animation="border" variant="light" />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<h4>User Interface</h4>
|
||||
<Form.Group>
|
||||
<Form.Label>Scene / Marker Wall</Form.Label>
|
||||
@@ -66,7 +69,9 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
label="Enable sound"
|
||||
onChange={() => setSoundOnPreview(!soundOnPreview)}
|
||||
/>
|
||||
<Form.Text className="text-muted" >Configuration for wall items</Form.Text>
|
||||
<Form.Text className="text-muted">
|
||||
Configuration for wall items
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
@@ -75,7 +80,7 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
checked={showStudioAsText}
|
||||
label="Show Studios as text"
|
||||
onChange={() => {
|
||||
setShowStudioAsText(!showStudioAsText)
|
||||
setShowStudioAsText(!showStudioAsText);
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
@@ -86,7 +91,7 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
checked={autostartVideo}
|
||||
label="Auto-start video"
|
||||
onChange={() => {
|
||||
setAutostartVideo(!autostartVideo)
|
||||
setAutostartVideo(!autostartVideo);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -95,11 +100,18 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
<Form.Control
|
||||
type="number"
|
||||
defaultValue={maximumLoopDuration}
|
||||
onChange={(event:React.FormEvent<HTMLInputElement>) => setMaximumLoopDuration(Number.parseInt(event.currentTarget.value, 10) ?? 0)}
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
||||
setMaximumLoopDuration(
|
||||
Number.parseInt(event.currentTarget.value, 10) ?? 0
|
||||
)
|
||||
}
|
||||
min={0}
|
||||
step={1}
|
||||
/>
|
||||
<Form.Text className="text-muted">Maximum scene duration - in seconds - where scene player will loop the video - 0 to disable</Form.Text>
|
||||
<Form.Text className="text-muted">
|
||||
Maximum scene duration - in seconds - where scene player will loop
|
||||
the video - 0 to disable
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
|
||||
@@ -109,7 +121,7 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
checked={cssEnabled}
|
||||
label="Custom CSS enabled"
|
||||
onChange={() => {
|
||||
setCSSEnabled(!cssEnabled)
|
||||
setCSSEnabled(!cssEnabled);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -117,13 +129,17 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
as="textarea"
|
||||
value={css}
|
||||
onChange={(e: any) => setCSS(e.target.value)}
|
||||
rows={16}>
|
||||
</Form.Control>
|
||||
<Form.Text className="text-muted">Page must be reloaded for changes to take effect.</Form.Text>
|
||||
rows={16}
|
||||
></Form.Control>
|
||||
<Form.Text className="text-muted">
|
||||
Page must be reloaded for changes to take effect.
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
<Button variant="primary" onClick={() => onSave()}>Save</Button>
|
||||
<Button variant="primary" onClick={() => onSave()}>
|
||||
Save
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useState } from "react";
|
||||
import { Form, Col } from 'react-bootstrap';
|
||||
import { Form, Col } from "react-bootstrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
|
||||
function convertTime(logEntry: GQL.LogEntryDataFragment) {
|
||||
function pad(val : number) {
|
||||
function pad(val: number) {
|
||||
let ret = val.toString();
|
||||
if (val <= 9) {
|
||||
ret = `0${ ret}`;
|
||||
ret = `0${ret}`;
|
||||
}
|
||||
|
||||
return ret;
|
||||
@@ -16,18 +16,20 @@ function convertTime(logEntry: GQL.LogEntryDataFragment) {
|
||||
const date = new Date(logEntry.time);
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
let dateStr = `${date.getFullYear() }-${ pad(month) }-${ pad(day)}`;
|
||||
dateStr += ` ${ pad(date.getHours()) }:${ pad(date.getMinutes()) }:${ pad(date.getSeconds())}`;
|
||||
let dateStr = `${date.getFullYear()}-${pad(month)}-${pad(day)}`;
|
||||
dateStr += ` ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(
|
||||
date.getSeconds()
|
||||
)}`;
|
||||
|
||||
return dateStr;
|
||||
}
|
||||
|
||||
function levelClass(level : string) {
|
||||
function levelClass(level: string) {
|
||||
return level.toLowerCase().trim();
|
||||
}
|
||||
|
||||
interface ILogElementProps {
|
||||
logEntry : LogEntry
|
||||
logEntry: LogEntry;
|
||||
}
|
||||
|
||||
const LogElement: React.FC<ILogElementProps> = ({ logEntry }) => {
|
||||
@@ -39,11 +41,10 @@ const LogElement: React.FC<ILogElementProps> = ({ logEntry }) => {
|
||||
<span>{logEntry.time}</span>
|
||||
<span className={levelClass(logEntry.level)}>{level}</span>
|
||||
<span>{logEntry.message}</span>
|
||||
<br/>
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
class LogEntry {
|
||||
public time: string;
|
||||
@@ -77,15 +78,17 @@ export const SettingsLogsPanel: React.FC = () => {
|
||||
const newData = (data?.loggingSubscribe ?? []).map(e => new LogEntry(e));
|
||||
|
||||
const filteredLogEntries = [...newData.reverse(), ...oldData]
|
||||
.filter(filterByLogLevel).slice(0, MAX_LOG_ENTRIES);
|
||||
.filter(filterByLogLevel)
|
||||
.slice(0, MAX_LOG_ENTRIES);
|
||||
|
||||
const maybeRenderError = error
|
||||
? <div className="error">Error connecting to log server: {error.message}</div>
|
||||
: '';
|
||||
const maybeRenderError = error ? (
|
||||
<div className="error">Error connecting to log server: {error.message}</div>
|
||||
) : (
|
||||
""
|
||||
);
|
||||
|
||||
function filterByLogLevel(logEntry : LogEntry) {
|
||||
if (logLevel === "Debug")
|
||||
return true;
|
||||
function filterByLogLevel(logEntry: LogEntry) {
|
||||
if (logLevel === "Debug") return true;
|
||||
|
||||
const logLevelIndex = logLevels.indexOf(logLevel);
|
||||
const levelIndex = logLevels.indexOf(logEntry.level);
|
||||
@@ -104,17 +107,21 @@ export const SettingsLogsPanel: React.FC = () => {
|
||||
<Form.Control
|
||||
as="select"
|
||||
defaultValue={logLevel}
|
||||
onChange={(event) => setLogLevel(event.currentTarget.value)}
|
||||
onChange={event => setLogLevel(event.currentTarget.value)}
|
||||
>
|
||||
{ logLevels.map(level => (<option key={level} value={level}>{level}</option>)) }
|
||||
{logLevels.map(level => (
|
||||
<option key={level} value={level}>
|
||||
{level}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Col>
|
||||
</Form.Row>
|
||||
<div className="logs">
|
||||
{maybeRenderError}
|
||||
{filteredLogEntries.map((logEntry) =>
|
||||
<LogElement logEntry={logEntry} key={logEntry.id}/>
|
||||
)}
|
||||
{filteredLogEntries.map(logEntry => (
|
||||
<LogElement logEntry={logEntry} key={logEntry.id} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, Form } from 'react-bootstrap';
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { useToast } from 'src/hooks';
|
||||
import { useToast } from "src/hooks";
|
||||
|
||||
export const GenerateButton: React.FC = () => {
|
||||
const Toast = useToast();
|
||||
@@ -12,7 +12,12 @@ export const GenerateButton: React.FC = () => {
|
||||
|
||||
async function onGenerate() {
|
||||
try {
|
||||
await StashService.queryMetadataGenerate({sprites, previews, markers, transcodes});
|
||||
await StashService.queryMetadataGenerate({
|
||||
sprites,
|
||||
previews,
|
||||
markers,
|
||||
transcodes
|
||||
});
|
||||
Toast.success({ content: "Started generating" });
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
@@ -21,12 +26,36 @@ export const GenerateButton: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Check id="sprite-task" checked={sprites} label="Sprites (for the scene scrubber)" onChange={() => setSprites(!sprites)} />
|
||||
<Form.Check id="preview-task" checked={previews} label="Previews (video previews which play when hovering over a scene)" onChange={() => setPreviews(!previews)} />
|
||||
<Form.Check id="marker-task" checked={markers} label="Markers (20 second videos which begin at the given timecode)" onChange={() => setMarkers(!markers)} />
|
||||
<Form.Check id="transcode-task" checked={transcodes} label="Transcodes (MP4 conversions of unsupported video formats)" onChange={() => setTranscodes(!transcodes)} />
|
||||
<Button id="generate" type="submit" onClick={() => onGenerate()}>Generate</Button>
|
||||
<Form.Text className="text-muted">Generate supporting image, sprite, video, vtt and other files.</Form.Text>
|
||||
<Form.Check
|
||||
id="sprite-task"
|
||||
checked={sprites}
|
||||
label="Sprites (for the scene scrubber)"
|
||||
onChange={() => setSprites(!sprites)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="preview-task"
|
||||
checked={previews}
|
||||
label="Previews (video previews which play when hovering over a scene)"
|
||||
onChange={() => setPreviews(!previews)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="marker-task"
|
||||
checked={markers}
|
||||
label="Markers (20 second videos which begin at the given timecode)"
|
||||
onChange={() => setMarkers(!markers)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="transcode-task"
|
||||
checked={transcodes}
|
||||
label="Transcodes (MP4 conversions of unsupported video formats)"
|
||||
onChange={() => setTranscodes(!transcodes)}
|
||||
/>
|
||||
<Button id="generate" type="submit" onClick={() => onGenerate()}>
|
||||
Generate
|
||||
</Button>
|
||||
<Form.Text className="text-muted">
|
||||
Generate supporting image, sprite, video, vtt and other files.
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button, Form, ProgressBar } from 'react-bootstrap';
|
||||
import { Button, Form, ProgressBar } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { useToast } from 'src/hooks';
|
||||
import { Modal } from 'src/components/Shared';
|
||||
import { useToast } from "src/hooks";
|
||||
import { Modal } from "src/components/Shared";
|
||||
import { GenerateButton } from "./GenerateButton";
|
||||
|
||||
export const SettingsTasksPanel: React.FC = () => {
|
||||
@@ -22,7 +22,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
const metadataUpdate = StashService.useMetadataUpdate();
|
||||
|
||||
function statusToText(s: string) {
|
||||
switch(s) {
|
||||
switch (s) {
|
||||
case "Idle":
|
||||
return "Idle";
|
||||
case "Scan":
|
||||
@@ -38,7 +38,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
case "Auto Tag":
|
||||
return "Auto tagging scenes";
|
||||
default:
|
||||
return "Idle"
|
||||
return "Idle";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,9 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
|
||||
function onImport() {
|
||||
setIsImportAlertOpen(false);
|
||||
StashService.queryMetadataImport().then(() => { jobStatus.refetch()});
|
||||
StashService.queryMetadataImport().then(() => {
|
||||
jobStatus.refetch();
|
||||
});
|
||||
}
|
||||
|
||||
function renderImportAlert() {
|
||||
@@ -76,12 +78,12 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
<Modal
|
||||
show={isImportAlertOpen}
|
||||
icon="trash-alt"
|
||||
accept={{ text: 'Import', variant: 'danger', onClick: onImport }}
|
||||
accept={{ text: "Import", variant: "danger", onClick: onImport }}
|
||||
cancel={{ onClick: () => setIsImportAlertOpen(false) }}
|
||||
>
|
||||
<p>
|
||||
Are you sure you want to import? This will delete the database and re-import from
|
||||
your exported metadata.
|
||||
Are you sure you want to import? This will delete the database and
|
||||
re-import from your exported metadata.
|
||||
</p>
|
||||
</Modal>
|
||||
);
|
||||
@@ -89,7 +91,9 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
|
||||
function onClean() {
|
||||
setIsCleanAlertOpen(false);
|
||||
StashService.queryMetadataClean().then(() => { jobStatus.refetch()});
|
||||
StashService.queryMetadataClean().then(() => {
|
||||
jobStatus.refetch();
|
||||
});
|
||||
}
|
||||
|
||||
function renderCleanAlert() {
|
||||
@@ -97,13 +101,13 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
<Modal
|
||||
show={isCleanAlertOpen}
|
||||
icon="trash-alt"
|
||||
accept={{ text: 'Clean', variant: 'danger', onClick: onClean }}
|
||||
accept={{ text: "Clean", variant: "danger", onClick: onClean }}
|
||||
cancel={{ onClick: () => setIsCleanAlertOpen(false) }}
|
||||
>
|
||||
<p>
|
||||
Are you sure you want to Clean?
|
||||
This will delete db information and generated content
|
||||
for all scenes that are no longer found in the filesystem.
|
||||
Are you sure you want to Clean? This will delete db information and
|
||||
generated content for all scenes that are no longer found in the
|
||||
filesystem.
|
||||
</p>
|
||||
</Modal>
|
||||
);
|
||||
@@ -111,7 +115,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
|
||||
async function onScan() {
|
||||
try {
|
||||
await StashService.queryMetadataScan({useFileMetadata});
|
||||
await StashService.queryMetadataScan({ useFileMetadata });
|
||||
Toast.success({ content: "Started scan" });
|
||||
jobStatus.refetch();
|
||||
} catch (e) {
|
||||
@@ -125,7 +129,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
performers: autoTagPerformers ? wildcard : [],
|
||||
studios: autoTagStudios ? wildcard : [],
|
||||
tags: autoTagTags ? wildcard : []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function onAutoTag() {
|
||||
@@ -145,7 +149,15 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Button id="stop" variant="danger" onClick={() => StashService.queryStopJob().then(() => jobStatus.refetch())}>Stop</Button>
|
||||
<Button
|
||||
id="stop"
|
||||
variant="danger"
|
||||
onClick={() =>
|
||||
StashService.queryStopJob().then(() => jobStatus.refetch())
|
||||
}
|
||||
>
|
||||
Stop
|
||||
</Button>
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
@@ -155,7 +167,11 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
<>
|
||||
<Form.Group>
|
||||
<h5>Status: {status}</h5>
|
||||
{ status !== "Idle" ? <ProgressBar now={progress} label={`${progress}%`} /> : '' }
|
||||
{status !== "Idle" ? (
|
||||
<ProgressBar now={progress} label={`${progress}%`} />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Form.Group>
|
||||
{maybeRenderStop()}
|
||||
</>
|
||||
@@ -180,8 +196,12 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
label="Set name, date, details from metadata (if present)"
|
||||
onChange={() => setUseFileMetadata(!useFileMetadata)}
|
||||
/>
|
||||
<Button id="scan" type="submit" onClick={() => onScan()}>Scan</Button>
|
||||
<Form.Text className="text-muted">Scan for new content and add it to the database.</Form.Text>
|
||||
<Button id="scan" type="submit" onClick={() => onScan()}>
|
||||
Scan
|
||||
</Button>
|
||||
<Form.Text className="text-muted">
|
||||
Scan for new content and add it to the database.
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
@@ -204,8 +224,12 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
label="Tags"
|
||||
onChange={() => setAutoTagTags(!autoTagTags)}
|
||||
/>
|
||||
<Button id="autoTag" type="submit" onClick={() => onAutoTag()}>Auto Tag</Button>
|
||||
<Form.Text className="text-muted">Auto-tag content based on filenames.</Form.Text>
|
||||
<Button id="autoTag" type="submit" onClick={() => onAutoTag()}>
|
||||
Auto Tag
|
||||
</Button>
|
||||
<Form.Text className="text-muted">
|
||||
Auto-tag content based on filenames.
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
@@ -219,21 +243,50 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
<h4>Generated Content</h4>
|
||||
<GenerateButton />
|
||||
<Form.Group>
|
||||
<Button id="clean" variant="danger" onClick={() => setIsCleanAlertOpen(true)}>Clean</Button>
|
||||
<Form.Text className="text-muted">Check for missing files and remove them from the database. This is a destructive action.</Form.Text>
|
||||
<Button
|
||||
id="clean"
|
||||
variant="danger"
|
||||
onClick={() => setIsCleanAlertOpen(true)}
|
||||
>
|
||||
Clean
|
||||
</Button>
|
||||
<Form.Text className="text-muted">
|
||||
Check for missing files and remove them from the database. This is a
|
||||
destructive action.
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
|
||||
<h4>Metadata</h4>
|
||||
<Form.Group>
|
||||
<Button id="export" type="submit"onClick={() => StashService.queryMetadataExport().then(() => { jobStatus.refetch()})}>Export</Button>
|
||||
<Form.Text className="text-muted">Export the database content into JSON format.</Form.Text>
|
||||
<Button
|
||||
id="export"
|
||||
type="submit"
|
||||
onClick={() =>
|
||||
StashService.queryMetadataExport().then(() => {
|
||||
jobStatus.refetch();
|
||||
})
|
||||
}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
<Form.Text className="text-muted">
|
||||
Export the database content into JSON format.
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<Button id="import" variant="danger" onClick={() => setIsImportAlertOpen(true)}>Import</Button>
|
||||
<Form.Text className="text-muted">Import from exported JSON. This is a destructive action.</Form.Text>
|
||||
<Button
|
||||
id="import"
|
||||
variant="danger"
|
||||
onClick={() => setIsImportAlertOpen(true)}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
<Form.Text className="text-muted">
|
||||
Import from exported JSON. This is a destructive action.
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
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 { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
@@ -17,56 +25,85 @@ interface IProps {
|
||||
|
||||
// TODO: only for performers. make generic
|
||||
scrapers?: GQL.ListPerformerScrapersListPerformerScrapers[];
|
||||
onDisplayScraperDialog?: (scraper: GQL.ListPerformerScrapersListPerformerScrapers) => void;
|
||||
onDisplayScraperDialog?: (
|
||||
scraper: GQL.ListPerformerScrapersListPerformerScrapers
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
|
||||
function renderEditButton() {
|
||||
if (props.isNew) { return; }
|
||||
if (props.isNew) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => props.onToggleEdit()}
|
||||
>
|
||||
{ props.isEditing ? "Cancel" : "Edit"}
|
||||
<Button variant="primary" onClick={() => props.onToggleEdit()}>
|
||||
{props.isEditing ? "Cancel" : "Edit"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSaveButton() {
|
||||
if (!props.isEditing) { return; }
|
||||
return <Button variant="success" onClick={() => props.onSave()}>Save</Button>;
|
||||
if (!props.isEditing) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<Button variant="success" onClick={() => props.onSave()}>
|
||||
Save
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDeleteButton() {
|
||||
if (props.isNew || props.isEditing) { return; }
|
||||
return <Button variant="danger" onClick={() => setIsDeleteAlertOpen(true)}>Delete</Button>;
|
||||
if (props.isNew || props.isEditing) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<Button variant="danger" onClick={() => setIsDeleteAlertOpen(true)}>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function renderImageInput() {
|
||||
if (!props.isEditing) { return; }
|
||||
if (!props.isEditing) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<Form.Group controlId="cover-file">
|
||||
<Form.Label>Choose image...</Form.Label>
|
||||
<Form.Control type="file" accept=".jpg,.jpeg,.png" onChange={props.onImageChange} />
|
||||
<Form.Control
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png"
|
||||
onChange={props.onImageChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function renderScraperMenu() {
|
||||
if (!props.performer || !props.isEditing) { return; }
|
||||
if (!props.performer || !props.isEditing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const popover = (
|
||||
<Popover id="scraper-popover">
|
||||
<Popover.Content>
|
||||
<div>
|
||||
{ props.scrapers ? props.scrapers.map((s) => (
|
||||
<Button variant="link" onClick={() => props.onDisplayScraperDialog && props.onDisplayScraperDialog(s) }>
|
||||
{props.scrapers
|
||||
? props.scrapers.map(s => (
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() =>
|
||||
props.onDisplayScraperDialog &&
|
||||
props.onDisplayScraperDialog(s)
|
||||
}
|
||||
>
|
||||
{s.name}
|
||||
</Button>
|
||||
)) : ''}
|
||||
))
|
||||
: ""}
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
@@ -80,48 +117,58 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
||||
}
|
||||
|
||||
function renderAutoTagButton() {
|
||||
if (props.isNew || props.isEditing) { return; }
|
||||
if (props.isNew || props.isEditing) {
|
||||
return;
|
||||
}
|
||||
if (props.onAutoTag) {
|
||||
return (<Button onClick={() => {
|
||||
if (props.onAutoTag) { props.onAutoTag() }
|
||||
}}>Auto Tag</Button>)
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (props.onAutoTag) {
|
||||
props.onAutoTag();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Auto Tag
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderScenesButton() {
|
||||
if (props.isEditing) { return; }
|
||||
if (props.isEditing) {
|
||||
return;
|
||||
}
|
||||
let linkSrc: string = "#";
|
||||
if (props.performer) {
|
||||
linkSrc = NavUtils.makePerformerScenesUrl(props.performer);
|
||||
} else if (props.studio) {
|
||||
linkSrc = NavUtils.makeStudioScenesUrl(props.studio);
|
||||
}
|
||||
return (
|
||||
<Link to={linkSrc}>
|
||||
Scenes
|
||||
</Link>
|
||||
);
|
||||
return <Link to={linkSrc}>Scenes</Link>;
|
||||
}
|
||||
|
||||
function renderDeleteAlert() {
|
||||
const name = props?.studio?.name ?? props?.performer?.name;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={isDeleteAlertOpen}
|
||||
>
|
||||
<Modal.Body>
|
||||
Are you sure you want to delete {name}?
|
||||
</Modal.Body>
|
||||
<Modal show={isDeleteAlertOpen}>
|
||||
<Modal.Body>Are you sure you want to delete {name}?</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="danger" onClick={props.onDelete}>Delete</Button>
|
||||
<Button variant="secondary" onClick={() => setIsDeleteAlertOpen(false)}>Cancel</Button>
|
||||
<Button variant="danger" onClick={props.onDelete}>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setIsDeleteAlertOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderDeleteAlert()}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button, ButtonGroup, InputGroup, Form } from 'react-bootstrap';
|
||||
import { Icon } from 'src/components/Shared'
|
||||
import { Button, ButtonGroup, InputGroup, Form } from "react-bootstrap";
|
||||
import { Icon } from "src/components/Shared";
|
||||
import { TextUtils } from "src/utils";
|
||||
|
||||
interface IProps {
|
||||
disabled?: boolean
|
||||
numericValue: number
|
||||
onValueChange(valueAsNumber: number): void
|
||||
onReset?(): void
|
||||
disabled?: boolean;
|
||||
numericValue: number;
|
||||
onValueChange(valueAsNumber: number): void;
|
||||
onReset?(): void;
|
||||
}
|
||||
|
||||
export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
||||
const [value, setValue] = useState<string>(secondsToString(props.numericValue));
|
||||
const [value, setValue] = useState<string>(
|
||||
secondsToString(props.numericValue)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(secondsToString(props.numericValue));
|
||||
}, [props.numericValue]);
|
||||
|
||||
function secondsToString(seconds : number) {
|
||||
function secondsToString(seconds: number) {
|
||||
let ret = TextUtils.secondsToTimestamp(seconds);
|
||||
|
||||
if (ret.startsWith("00:")) {
|
||||
@@ -31,7 +33,7 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
||||
return ret;
|
||||
}
|
||||
|
||||
function stringToSeconds(v : string) {
|
||||
function stringToSeconds(v: string) {
|
||||
if (!v) {
|
||||
return 0;
|
||||
}
|
||||
@@ -44,7 +46,7 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
||||
|
||||
let seconds = 0;
|
||||
let factor = 1;
|
||||
while(splits.length > 0) {
|
||||
while (splits.length > 0) {
|
||||
const thisSplit = splits.pop();
|
||||
if (thisSplit === undefined) {
|
||||
return 0;
|
||||
@@ -76,23 +78,15 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
||||
|
||||
function renderButtons() {
|
||||
return (
|
||||
<ButtonGroup
|
||||
vertical
|
||||
>
|
||||
<Button
|
||||
disabled={props.disabled}
|
||||
onClick={() => increment()}
|
||||
>
|
||||
<ButtonGroup vertical>
|
||||
<Button disabled={props.disabled} onClick={() => increment()}>
|
||||
<Icon icon="chevron-up" />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={props.disabled}
|
||||
onClick={() => decrement()}
|
||||
>
|
||||
<Button disabled={props.disabled} onClick={() => decrement()}>
|
||||
<Icon icon="chevron-down" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function onReset() {
|
||||
@@ -104,12 +98,10 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
||||
function maybeRenderReset() {
|
||||
if (props.onReset) {
|
||||
return (
|
||||
<Button
|
||||
onClick={() => onReset()}
|
||||
>
|
||||
<Button onClick={() => onReset()}>
|
||||
<Icon icon="clock" />
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,15 +111,15 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
||||
<Form.Control
|
||||
disabled={props.disabled}
|
||||
value={value}
|
||||
onChange={(e : any) => setValue(e.target.value)}
|
||||
onChange={(e: any) => setValue(e.target.value)}
|
||||
onBlur={() => props.onValueChange(stringToSeconds(value))}
|
||||
placeholder="hh:mm:ss"
|
||||
/>
|
||||
<InputGroup.Append>
|
||||
{ maybeRenderReset() }
|
||||
{ renderButtons() }
|
||||
{maybeRenderReset()}
|
||||
{renderButtons()}
|
||||
</InputGroup.Append>
|
||||
</InputGroup>
|
||||
</Form.Group>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, InputGroup, Form, Modal, Spinner } from 'react-bootstrap';
|
||||
import { Button, InputGroup, Form, Modal, Spinner } from "react-bootstrap";
|
||||
import { StashService } from "src/core/StashService";
|
||||
|
||||
interface IProps {
|
||||
@@ -11,13 +11,15 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
|
||||
const [currentDirectory, setCurrentDirectory] = useState<string>("");
|
||||
const [isDisplayingDialog, setIsDisplayingDialog] = useState<boolean>(false);
|
||||
const [selectedDirectories, setSelectedDirectories] = useState<string[]>([]);
|
||||
const { data, error, loading } = StashService.useDirectories(currentDirectory);
|
||||
const { data, error, loading } = StashService.useDirectories(
|
||||
currentDirectory
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedDirectories(props.directories);
|
||||
}, [props.directories]);
|
||||
|
||||
const selectableDirectories:string[] = data?.directories ?? [];
|
||||
const selectableDirectories: string[] = data?.directories ?? [];
|
||||
|
||||
function onSelectDirectory() {
|
||||
selectedDirectories.push(currentDirectory);
|
||||
@@ -28,7 +30,9 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
|
||||
}
|
||||
|
||||
function onRemoveDirectory(directory: string) {
|
||||
const newSelectedDirectories = selectedDirectories.filter((dir) => dir !== directory);
|
||||
const newSelectedDirectories = selectedDirectories.filter(
|
||||
dir => dir !== directory
|
||||
);
|
||||
setSelectedDirectories(newSelectedDirectories);
|
||||
props.onDirectoriesChanged(newSelectedDirectories);
|
||||
}
|
||||
@@ -40,9 +44,7 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
|
||||
onHide={() => setIsDisplayingDialog(false)}
|
||||
title=""
|
||||
>
|
||||
<Modal.Header>
|
||||
Select Directory
|
||||
</Modal.Header>
|
||||
<Modal.Header>Select Directory</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className="dialog-content">
|
||||
<InputGroup>
|
||||
@@ -52,11 +54,23 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
|
||||
defaultValue={currentDirectory}
|
||||
/>
|
||||
<InputGroup.Append>
|
||||
{(!data || !data.directories || loading) ? <Spinner animation="border" variant="light" /> : ''}
|
||||
{!data || !data.directories || loading ? (
|
||||
<Spinner animation="border" variant="light" />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</InputGroup.Append>
|
||||
</InputGroup>
|
||||
{selectableDirectories.map((path) => {
|
||||
return <Button variant="link" key={path} onClick={() => setCurrentDirectory(path)}>{path}</Button>;
|
||||
{selectableDirectories.map(path => {
|
||||
return (
|
||||
<Button
|
||||
variant="link"
|
||||
key={path}
|
||||
onClick={() => setCurrentDirectory(path)}
|
||||
>
|
||||
{path}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Modal.Body>
|
||||
@@ -69,11 +83,18 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{error ? <h1>{error.message}</h1> : ''}
|
||||
{error ? <h1>{error.message}</h1> : ""}
|
||||
{renderDialog()}
|
||||
<Form.Group>
|
||||
{selectedDirectories.map((path) => {
|
||||
return <div key={path}>{path} <Button variant="link" onClick={() => onRemoveDirectory(path)}>Remove</Button></div>;
|
||||
{selectedDirectories.map(path => {
|
||||
return (
|
||||
<div key={path}>
|
||||
{path}{" "}
|
||||
<Button variant="link" onClick={() => onRemoveDirectory(path)}>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Form.Group>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react'
|
||||
import { Overlay, Popover, OverlayProps } from 'react-bootstrap'
|
||||
import React, { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { Overlay, Popover, OverlayProps } from "react-bootstrap";
|
||||
|
||||
interface IHoverPopover {
|
||||
enterDelay?: number;
|
||||
@@ -9,7 +9,14 @@ interface IHoverPopover {
|
||||
placement?: OverlayProps["placement"];
|
||||
}
|
||||
|
||||
export const HoverPopover: React.FC<IHoverPopover> = ({ enterDelay = 0, leaveDelay = 400, content, children, className, placement = 'top' }) => {
|
||||
export const HoverPopover: React.FC<IHoverPopover> = ({
|
||||
enterDelay = 0,
|
||||
leaveDelay = 400,
|
||||
content,
|
||||
children,
|
||||
className,
|
||||
placement = "top"
|
||||
}) => {
|
||||
const [show, setShow] = useState(false);
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
const enterTimer = useRef<number>();
|
||||
@@ -25,33 +32,35 @@ export const HoverPopover: React.FC<IHoverPopover> = ({ enterDelay = 0, leaveDel
|
||||
leaveTimer.current = window.setTimeout(() => setShow(false), leaveDelay);
|
||||
}, [leaveDelay]);
|
||||
|
||||
useEffect(() => (
|
||||
() => {
|
||||
window.clearTimeout(enterTimer.current)
|
||||
window.clearTimeout(leaveTimer.current)
|
||||
}
|
||||
), []);
|
||||
useEffect(
|
||||
() => () => {
|
||||
window.clearTimeout(enterTimer.current);
|
||||
window.clearTimeout(leaveTimer.current);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={className} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} ref={triggerRef}>
|
||||
<div
|
||||
className={className}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
ref={triggerRef}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{ triggerRef.current &&
|
||||
<Overlay
|
||||
show={show}
|
||||
placement={placement}
|
||||
target={triggerRef.current}
|
||||
>
|
||||
{triggerRef.current && (
|
||||
<Overlay show={show} placement={placement} target={triggerRef.current}>
|
||||
<Popover
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
id='popover'
|
||||
id="popover"
|
||||
>
|
||||
{content}
|
||||
</Popover>
|
||||
</Overlay>
|
||||
}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { IconName } from '@fortawesome/fontawesome-svg-core';
|
||||
import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
interface IIcon {
|
||||
icon: IconName;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from "react";
|
||||
import { Button, Modal } from 'react-bootstrap';
|
||||
import { Icon } from 'src/components/Shared';
|
||||
import { IconName } from '@fortawesome/fontawesome-svg-core';
|
||||
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';
|
||||
variant?: "danger" | "primary";
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
@@ -18,27 +18,42 @@ interface IModal {
|
||||
accept?: IButton;
|
||||
}
|
||||
|
||||
const ModalComponent: React.FC<IModal> = ({ children, show, icon, header, cancel, accept, onHide }) => ((
|
||||
<Modal
|
||||
keyboard={false}
|
||||
onHide={onHide}
|
||||
show={show}
|
||||
>
|
||||
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>
|
||||
{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>
|
||||
{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,20 +1,20 @@
|
||||
import React, { useState, useCallback } from "react";
|
||||
import Select, { ValueType } from 'react-select';
|
||||
import CreatableSelect from 'react-select/creatable';
|
||||
import { debounce } from 'lodash';
|
||||
import Select, { ValueType } from "react-select";
|
||||
import CreatableSelect from "react-select/creatable";
|
||||
import { debounce } from "lodash";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { useToast } from 'src/hooks';
|
||||
import { useToast } from "src/hooks";
|
||||
|
||||
type ValidTypes =
|
||||
GQL.AllPerformersForFilterAllPerformers |
|
||||
GQL.AllTagsForFilterAllTags |
|
||||
GQL.AllStudiosForFilterAllStudios;
|
||||
type Option = { value:string, label:string };
|
||||
| GQL.AllPerformersForFilterAllPerformers
|
||||
| GQL.AllTagsForFilterAllTags
|
||||
| GQL.AllStudiosForFilterAllStudios;
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
interface ITypeProps {
|
||||
type?: 'performers' | 'studios' | 'tags';
|
||||
type?: "performers" | "studios" | "tags";
|
||||
}
|
||||
interface IFilterProps {
|
||||
initialIds: string[];
|
||||
@@ -40,131 +40,221 @@ interface ISelectProps {
|
||||
interface ISceneGallerySelect {
|
||||
initialId?: string;
|
||||
sceneId: string;
|
||||
onSelect: (item: GQL.ValidGalleriesForSceneValidGalleriesForScene | undefined) => void;
|
||||
onSelect: (
|
||||
item: GQL.ValidGalleriesForSceneValidGalleriesForScene | undefined
|
||||
) => void;
|
||||
}
|
||||
|
||||
const getSelectedValues = (selectedItems:ValueType<Option>) => (
|
||||
(Array.isArray(selectedItems) ? selectedItems : [selectedItems])
|
||||
.map(item => item.value)
|
||||
);
|
||||
const getSelectedValues = (selectedItems: ValueType<Option>) =>
|
||||
(Array.isArray(selectedItems) ? selectedItems : [selectedItems]).map(
|
||||
item => item.value
|
||||
);
|
||||
|
||||
export const SceneGallerySelect: React.FC<ISceneGallerySelect> = (props) => {
|
||||
const { data, loading } = StashService.useValidGalleriesForScene(props.sceneId);
|
||||
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 items = (galleries.length > 0
|
||||
? [{ path: "None", id: "0" }, ...galleries]
|
||||
: []
|
||||
).map(g => ({ label: g.path, value: g.id }));
|
||||
|
||||
const onChange = (selectedItems:ValueType<Option>) => {
|
||||
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} />
|
||||
return (
|
||||
<SelectComponent
|
||||
onChange={onChange}
|
||||
isLoading={loading}
|
||||
items={items}
|
||||
initialIds={initialId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface IScrapePerformerSuggestProps {
|
||||
scraperId: string;
|
||||
onSelectPerformer: (query: GQL.ScrapePerformerListScrapePerformerList) => void;
|
||||
onSelectPerformer: (
|
||||
query: GQL.ScrapePerformerListScrapePerformerList
|
||||
) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
export const ScrapePerformerSuggest: React.FC<IScrapePerformerSuggestProps> = (props) => {
|
||||
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 { 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 ?? [];
|
||||
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} />
|
||||
}
|
||||
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;
|
||||
onChange: (title: string) => void;
|
||||
}
|
||||
export const MarkerTitleSuggest: React.FC<IMarkerSuggestProps> = (props) => {
|
||||
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 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 =>
|
||||
props.type === "performers" ? (
|
||||
<PerformerSelect {...(props as IFilterProps)} />
|
||||
) : props.type === "studios" ? (
|
||||
<StudioSelect {...(props as IFilterProps)} />
|
||||
) : (
|
||||
<TagSelect {...(props as IFilterProps)} />
|
||||
);
|
||||
|
||||
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) => (
|
||||
props.type === 'performers' ? <PerformerSelect {...props as IFilterProps} />
|
||||
: props.type === 'studios' ? <StudioSelect {...props as IFilterProps} />
|
||||
: <TagSelect {...props as IFilterProps} />
|
||||
);
|
||||
|
||||
export const PerformerSelect: React.FC<IFilterProps> = (props) => {
|
||||
export const PerformerSelect: React.FC<IFilterProps> = props => {
|
||||
const { data, loading } = StashService.useAllPerformersForFilter();
|
||||
|
||||
const normalizedData = data?.allPerformers ?? [];
|
||||
const items:Option[] = normalizedData.map(item => ({ value: item.id, label: item.name ?? '' }));
|
||||
const placeholder = props.noSelectionString ?? "Select performer..."
|
||||
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 = getSelectedValues(selectedItems);
|
||||
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} placeholder={placeholder} />
|
||||
}
|
||||
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 => {
|
||||
const { data, loading } = StashService.useAllStudiosForFilter();
|
||||
|
||||
const normalizedData = data?.allStudios ?? [];
|
||||
const items:Option[] = normalizedData.map(item => ({ value: item.id, label: item.name }));
|
||||
const placeholder = props.noSelectionString ?? "Select studio..."
|
||||
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 = getSelectedValues(selectedItems);
|
||||
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} placeholder={placeholder} />
|
||||
}
|
||||
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 => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
const { data, loading: dataLoading } = StashService.useAllTagsForFilter();
|
||||
const createTag = StashService.useTagCreate({name: ''});
|
||||
const createTag = StashService.useTagCreate({ name: "" });
|
||||
const Toast = useToast();
|
||||
const placeholder = props.noSelectionString ?? "Select tags..."
|
||||
const placeholder = props.noSelectionString ?? "Select tags...";
|
||||
|
||||
const tags = data?.allTags ?? [];
|
||||
const selected = tags.filter(tag => selectedIds.indexOf(tag.id) !== -1).map(tag => ({value: tag.id, label: tag.name}));
|
||||
const items:Option[] = tags.map(item => ({ value: item.id, label: item.name }));
|
||||
const selected = tags
|
||||
.filter(tag => selectedIds.indexOf(tag.id) !== -1)
|
||||
.map(tag => ({ value: tag.id, label: tag.name }));
|
||||
const items: Option[] = tags.map(item => ({
|
||||
value: item.id,
|
||||
label: item.name
|
||||
}));
|
||||
|
||||
const onCreate = async (tagName: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await createTag({
|
||||
variables: { name: tagName },
|
||||
variables: { name: tagName }
|
||||
});
|
||||
|
||||
setSelectedIds([...selectedIds, result.data.tagCreate.id]);
|
||||
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);
|
||||
|
||||
Toast.success({ content: (<span>Created tag: <b>{tagName}</b></span>) });
|
||||
Toast.success({
|
||||
content: (
|
||||
<span>
|
||||
Created tag: <b>{tagName}</b>
|
||||
</span>
|
||||
)
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (selectedItems:ValueType<Option>) => {
|
||||
const onChange = (selectedItems: ValueType<Option>) => {
|
||||
const selectedValues = getSelectedValues(selectedItems);
|
||||
setSelectedIds(selectedValues);
|
||||
props.onSelect(tags.filter(item => selectedValues.indexOf(item.id) !== -1));
|
||||
@@ -183,7 +273,7 @@ export const TagSelect: React.FC<IFilterProps> = (props) => {
|
||||
selectedOptions={selected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
|
||||
type,
|
||||
@@ -199,7 +289,8 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
|
||||
onInputChange,
|
||||
placeholder
|
||||
}) => {
|
||||
const defaultValue = items.filter(item => initialIds?.indexOf(item.value) !== -1) ?? null;
|
||||
const defaultValue =
|
||||
items.filter(item => initialIds?.indexOf(item.value) !== -1) ?? null;
|
||||
|
||||
const props = {
|
||||
className,
|
||||
@@ -208,14 +299,19 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
|
||||
onChange,
|
||||
isMulti,
|
||||
defaultValue,
|
||||
noOptionsMessage: () => (type !== 'tags' ? 'None' : null),
|
||||
noOptionsMessage: () => (type !== "tags" ? "None" : null),
|
||||
placeholder,
|
||||
onInputChange
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
creatable
|
||||
? <CreatableSelect {...props} isLoading={isLoading} isDisabled={isLoading} onCreateOption={onCreateOption} />
|
||||
: <Select {...props} isLoading={isLoading} />
|
||||
return creatable ? (
|
||||
<CreatableSelect
|
||||
{...props}
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
onCreateOption={onCreateOption}
|
||||
/>
|
||||
) : (
|
||||
<Select {...props} isLoading={isLoading} />
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Badge } from 'react-bootstrap';
|
||||
import { Badge } from "react-bootstrap";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { PerformerDataFragment, SceneMarkerDataFragment, TagDataFragment } from "src/core/generated-graphql";
|
||||
import {
|
||||
PerformerDataFragment,
|
||||
SceneMarkerDataFragment,
|
||||
TagDataFragment
|
||||
} from "src/core/generated-graphql";
|
||||
import { NavUtils, TextUtils } from "src/utils";
|
||||
|
||||
interface IProps {
|
||||
@@ -21,13 +25,12 @@ export const TagLink: React.FC<IProps> = (props: IProps) => {
|
||||
title = props.performer.name || "";
|
||||
} else if (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 (
|
||||
<Badge
|
||||
className="tag-item"
|
||||
variant="secondary"
|
||||
>
|
||||
<Badge className="tag-item" variant="secondary">
|
||||
<Link to={link}>{title}</Link>
|
||||
</Badge>
|
||||
);
|
||||
|
||||
@@ -6,11 +6,11 @@ export {
|
||||
PerformerSelect,
|
||||
StudioSelect,
|
||||
TagSelect
|
||||
} from './Select';
|
||||
} 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';
|
||||
export { HoverPopover } from './HoverPopover';
|
||||
export { default as Icon } from "./Icon";
|
||||
export { default as Modal } from "./Modal";
|
||||
export { DetailsEditNavbar } from "./DetailsEditNavbar";
|
||||
export { DurationInput } from "./DurationInput";
|
||||
export { TagLink } from "./TagLink";
|
||||
export { HoverPopover } from "./HoverPopover";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Spinner } from 'react-bootstrap';
|
||||
import { Spinner } from "react-bootstrap";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { StashService } from "../core/StashService";
|
||||
|
||||
@@ -6,7 +6,9 @@ export const Stats: FunctionComponent = () => {
|
||||
const { data, error, loading } = StashService.useStats();
|
||||
|
||||
function renderStats() {
|
||||
if (!data || !data.stats) { return; }
|
||||
if (!data || !data.stats) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<nav id="details-container" className="level">
|
||||
<div className="level-item has-text-centered">
|
||||
@@ -45,10 +47,13 @@ export const Stats: FunctionComponent = () => {
|
||||
|
||||
return (
|
||||
<div id="details-container">
|
||||
{!data || loading ?
|
||||
{!data || loading ? (
|
||||
<Spinner animation="border" role="status" size="sm">
|
||||
<span className="sr-only">Loading...</span>
|
||||
</Spinner> : undefined}
|
||||
</Spinner>
|
||||
) : (
|
||||
undefined
|
||||
)}
|
||||
{error ? <span>error.message</span> : undefined}
|
||||
{renderStats()}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Card } from 'react-bootstrap';
|
||||
import { Card } from "react-bootstrap";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
@@ -9,18 +9,14 @@ interface IProps {
|
||||
|
||||
export const StudioCard: React.FC<IProps> = ({ studio }) => {
|
||||
return (
|
||||
<Card
|
||||
className="col-4"
|
||||
>
|
||||
<Card className="col-4">
|
||||
<Link
|
||||
to={`/studios/${studio.id}`}
|
||||
className="studio previewable image"
|
||||
style={{backgroundImage: `url(${studio.image_path})`}}
|
||||
style={{ backgroundImage: `url(${studio.image_path})` }}
|
||||
/>
|
||||
<div className="card-section">
|
||||
<h4 className="text-truncate">
|
||||
{studio.name}
|
||||
</h4>
|
||||
<h4 className="text-truncate">{studio.name}</h4>
|
||||
<span>{studio.scene_count} scenes.</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,37 +1,45 @@
|
||||
/* eslint-disable react/no-this-in-sfc */
|
||||
|
||||
import { Form, Spinner, Table } from 'react-bootstrap';
|
||||
import { Form, Spinner, Table } from "react-bootstrap";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import { useParams, useHistory } from "react-router-dom";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { ImageUtils, TableUtils } from "src/utils";
|
||||
import { DetailsEditNavbar } from "src/components/Shared";
|
||||
import { useToast} from "src/hooks";
|
||||
import { useToast } from "src/hooks";
|
||||
|
||||
export const Studio: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
const { id = 'new' } = useParams();
|
||||
const { id = "new" } = useParams();
|
||||
const isNew = id === "new";
|
||||
|
||||
// Editing state
|
||||
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
||||
|
||||
// Editing studio state
|
||||
const [image, setImage] = useState<string|undefined>(undefined);
|
||||
const [name, setName] = useState<string|undefined>(undefined);
|
||||
const [url, setUrl] = useState<string|undefined>(undefined);
|
||||
const [image, setImage] = useState<string | undefined>(undefined);
|
||||
const [name, setName] = useState<string | undefined>(undefined);
|
||||
const [url, setUrl] = useState<string | undefined>(undefined);
|
||||
|
||||
// Studio state
|
||||
const [studio, setStudio] = useState<Partial<GQL.StudioDataFragment>>({});
|
||||
const [imagePreview, setImagePreview] = useState<string | undefined>(undefined);
|
||||
const [imagePreview, setImagePreview] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const { data, error, loading } = StashService.useFindStudio(id);
|
||||
const updateStudio = StashService.useStudioUpdate(getStudioInput() as GQL.StudioUpdateInput);
|
||||
const createStudio = StashService.useStudioCreate(getStudioInput() as GQL.StudioCreateInput);
|
||||
const deleteStudio = StashService.useStudioDestroy(getStudioInput() as GQL.StudioDestroyInput);
|
||||
const updateStudio = StashService.useStudioUpdate(
|
||||
getStudioInput() as GQL.StudioUpdateInput
|
||||
);
|
||||
const createStudio = StashService.useStudioCreate(
|
||||
getStudioInput() as GQL.StudioCreateInput
|
||||
);
|
||||
const deleteStudio = StashService.useStudioDestroy(
|
||||
getStudioInput() as GQL.StudioDestroyInput
|
||||
);
|
||||
|
||||
function updateStudioEditState(state: Partial<GQL.StudioDataFragment>) {
|
||||
setName(state.name);
|
||||
@@ -64,15 +72,14 @@ export const Studio: React.FC = () => {
|
||||
if (!isNew && !isEditing) {
|
||||
if (!data?.findStudio || loading)
|
||||
return <Spinner animation="border" variant="light" />;
|
||||
if (error)
|
||||
return <div>{error.message}</div>;
|
||||
if (error) return <div>{error.message}</div>;
|
||||
}
|
||||
|
||||
function getStudioInput() {
|
||||
const input: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput> = {
|
||||
name,
|
||||
url,
|
||||
image,
|
||||
image
|
||||
};
|
||||
|
||||
if (!isNew) {
|
||||
@@ -85,7 +92,7 @@ export const Studio: React.FC = () => {
|
||||
try {
|
||||
if (!isNew) {
|
||||
const result = await updateStudio();
|
||||
updateStudioData(result.data.studioUpdate)
|
||||
updateStudioData(result.data.studioUpdate);
|
||||
setIsEditing(false);
|
||||
} else {
|
||||
const result = await createStudio();
|
||||
@@ -101,7 +108,7 @@ export const Studio: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await StashService.queryMetadataAutoTag({ studios: [studio.id]});
|
||||
await StashService.queryMetadataAutoTag({ studios: [studio.id] });
|
||||
Toast.success({ content: "Started auto tagging" });
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
@@ -134,29 +141,38 @@ export const Studio: React.FC = () => {
|
||||
studio={studio}
|
||||
isNew={isNew}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={() => { setIsEditing(!isEditing); updateStudioEditState(studio); }}
|
||||
onToggleEdit={() => {
|
||||
setIsEditing(!isEditing);
|
||||
updateStudioEditState(studio);
|
||||
}}
|
||||
onSave={onSave}
|
||||
onDelete={onDelete}
|
||||
onAutoTag={onAutoTag}
|
||||
onImageChange={onImageChangeHandler}
|
||||
/>
|
||||
<h1>
|
||||
{ !isEditing
|
||||
? <span>{studio.name}</span>
|
||||
: <Form.Group controlId="studio-name">
|
||||
{!isEditing ? (
|
||||
<span>{studio.name}</span>
|
||||
) : (
|
||||
<Form.Group controlId="studio-name">
|
||||
<Form.Label>Name</Form.Label>
|
||||
<Form.Control
|
||||
defaultValue={studio.name || ''}
|
||||
defaultValue={studio.name || ""}
|
||||
placeholder="Name"
|
||||
onChange={(event:any) => setName(event.target.value)}
|
||||
onChange={(event: any) => setName(event.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
}
|
||||
)}
|
||||
</h1>
|
||||
|
||||
<Table style={{width: "100%"}}>
|
||||
<Table style={{ width: "100%" }}>
|
||||
<tbody>
|
||||
{TableUtils.renderInputGroup({title: "URL", value: studio.url, isEditing, onChange: (val:string) => setUrl(val)})}
|
||||
{TableUtils.renderInputGroup({
|
||||
title: "URL",
|
||||
value: studio.url,
|
||||
isEditing,
|
||||
onChange: (val: string) => setUrl(val)
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import React from "react";
|
||||
import { QueryHookResult } from "react-apollo-hooks";
|
||||
import { FindStudiosQuery, FindStudiosVariables } from "src/core/generated-graphql";
|
||||
import {
|
||||
FindStudiosQuery,
|
||||
FindStudiosVariables
|
||||
} from "src/core/generated-graphql";
|
||||
import { useStudiosList } from "src/hooks";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
@@ -8,20 +11,29 @@ import { StudioCard } from "./StudioCard";
|
||||
|
||||
export const StudioList: React.FC = () => {
|
||||
const listData = useStudiosList({
|
||||
renderContent,
|
||||
renderContent
|
||||
});
|
||||
|
||||
function renderContent(result: QueryHookResult<FindStudiosQuery, FindStudiosVariables>, filter: ListFilterModel) {
|
||||
if (!result.data || !result.data.findStudios) { return; }
|
||||
function renderContent(
|
||||
result: QueryHookResult<FindStudiosQuery, FindStudiosVariables>,
|
||||
filter: ListFilterModel
|
||||
) {
|
||||
if (!result.data || !result.data.findStudios) {
|
||||
return;
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.Grid) {
|
||||
return (
|
||||
<div className="grid">
|
||||
{result.data.findStudios.studios.map((studio) => (<StudioCard key={studio.id} studio={studio} />))}
|
||||
{result.data.findStudios.studios.map(studio => (
|
||||
<StudioCard key={studio.id} studio={studio} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} if (filter.displayMode === DisplayMode.List) {
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.List) {
|
||||
return <h1>TODO</h1>;
|
||||
} if (filter.displayMode === DisplayMode.Wall) {
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.Wall) {
|
||||
return <h1>TODO</h1>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, Form, Spinner } from 'react-bootstrap';
|
||||
import { Button, Form, Spinner } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { NavUtils } from "src/utils";
|
||||
import { Icon, Modal } from 'src/components/Shared';
|
||||
import { useToast } from 'src/hooks';
|
||||
import { Icon, Modal } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
|
||||
export const TagList: React.FC = () => {
|
||||
const Toast = useToast();
|
||||
// Editing / New state
|
||||
const [name, setName] = useState('');
|
||||
const [editingTag, setEditingTag] = useState<Partial<GQL.TagDataFragment> | null>(null);
|
||||
const [deletingTag, setDeletingTag] = useState<Partial<GQL.TagDataFragment> | null>(null);
|
||||
const [name, setName] = useState("");
|
||||
const [editingTag, setEditingTag] = useState<Partial<
|
||||
GQL.TagDataFragment
|
||||
> | null>(null);
|
||||
const [deletingTag, setDeletingTag] = useState<Partial<
|
||||
GQL.TagDataFragment
|
||||
> | null>(null);
|
||||
|
||||
const { data, error } = StashService.useAllTags();
|
||||
const updateTag = StashService.useTagUpdate(getTagInput() as GQL.TagUpdateInput);
|
||||
const createTag = StashService.useTagCreate(getTagInput() as GQL.TagCreateInput);
|
||||
const deleteTag = StashService.useTagDestroy(getDeleteTagInput() as GQL.TagDestroyInput);
|
||||
const updateTag = StashService.useTagUpdate(
|
||||
getTagInput() as GQL.TagUpdateInput
|
||||
);
|
||||
const createTag = StashService.useTagCreate(
|
||||
getTagInput() as GQL.TagCreateInput
|
||||
);
|
||||
const deleteTag = StashService.useTagDestroy(
|
||||
getDeleteTagInput() as GQL.TagDestroyInput
|
||||
);
|
||||
|
||||
function getTagInput() {
|
||||
const tagInput: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = { name };
|
||||
@@ -49,11 +59,10 @@ export const TagList: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function onAutoTag(tag : GQL.TagDataFragment) {
|
||||
if (!tag)
|
||||
return;
|
||||
async function onAutoTag(tag: GQL.TagDataFragment) {
|
||||
if (!tag) return;
|
||||
try {
|
||||
await StashService.queryMetadataAutoTag({ tags: [tag.id]});
|
||||
await StashService.queryMetadataAutoTag({ tags: [tag.id] });
|
||||
Toast.success({ content: "Started auto tagging" });
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
@@ -75,31 +84,37 @@ export const TagList: React.FC = () => {
|
||||
onHide={() => {}}
|
||||
show={!!deletingTag}
|
||||
icon="trash-alt"
|
||||
accept={{ onClick: onDelete, variant: 'danger', text: 'Delete' }}
|
||||
accept={{ onClick: onDelete, variant: "danger", text: "Delete" }}
|
||||
cancel={{ onClick: () => setDeletingTag(null) }}
|
||||
>
|
||||
<span>Are you sure you want to delete {deletingTag && deletingTag.name}?</span>
|
||||
<span>
|
||||
Are you sure you want to delete {deletingTag && deletingTag.name}?
|
||||
</span>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
if (!data?.allTags)
|
||||
return <Spinner animation="border" variant="light" />;
|
||||
if (error)
|
||||
return <div>{error.message}</div>;
|
||||
if (!data?.allTags) 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 (
|
||||
<>
|
||||
{deleteAlert}
|
||||
<div key={tag.id} className="tag-list-row">
|
||||
<Button variant="link" onClick={() => setEditingTag(tag)}>{tag.name}</Button>
|
||||
<div style={{float: "right"}}>
|
||||
<Button variant="link" onClick={() => setEditingTag(tag)}>
|
||||
{tag.name}
|
||||
</Button>
|
||||
<div style={{ float: "right" }}>
|
||||
<Button onClick={() => onAutoTag(tag)}>Auto Tag</Button>
|
||||
<Link to={NavUtils.makeTagScenesUrl(tag)}>Scenes: {tag.scene_count}</Link>
|
||||
<Link to={NavUtils.makeTagScenesUrl(tag)}>
|
||||
Scenes: {tag.scene_count}
|
||||
</Link>
|
||||
<Link to={NavUtils.makeTagSceneMarkersUrl(tag)}>
|
||||
Markers: {tag.scene_marker_count}
|
||||
</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)}>
|
||||
<Icon icon="trash-alt" color="danger" />
|
||||
</Button>
|
||||
@@ -111,19 +126,29 @@ export const TagList: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div id="tag-list-container">
|
||||
<Button variant="primary" style={{marginTop: "20px"}} onClick={() => setEditingTag({})}>New Tag</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
style={{ marginTop: "20px" }}
|
||||
onClick={() => setEditingTag({})}
|
||||
>
|
||||
New Tag
|
||||
</Button>
|
||||
|
||||
<Modal
|
||||
show={!!editingTag}
|
||||
header={editingTag && editingTag.id ? "Edit Tag" : "New Tag"}
|
||||
onHide={() => setEditingTag(null)}
|
||||
accept={{ onClick: onEdit, variant: 'danger', text: (editingTag?.id ? 'Update' : 'Create') }}
|
||||
accept={{
|
||||
onClick: onEdit,
|
||||
variant: "danger",
|
||||
text: editingTag?.id ? "Update" : "Create"
|
||||
}}
|
||||
>
|
||||
<Form.Group controlId="tag-name">
|
||||
<Form.Label>Name</Form.Label>
|
||||
<Form.Control
|
||||
onChange={(newValue: any) => setName(newValue.target.value)}
|
||||
defaultValue={(editingTag && editingTag.name) || ''}
|
||||
defaultValue={(editingTag && editingTag.name) || ""}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Modal>
|
||||
|
||||
@@ -11,18 +11,27 @@ interface IWallItemProps {
|
||||
sceneMarker?: GQL.SceneMarkerDataFragment;
|
||||
origin?: string;
|
||||
onOverlay: (show: boolean) => void;
|
||||
clickHandler?: (item: GQL.SlimSceneDataFragment | GQL.SceneMarkerDataFragment) => void;
|
||||
clickHandler?: (
|
||||
item: GQL.SlimSceneDataFragment | GQL.SceneMarkerDataFragment
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const WallItem: FunctionComponent<IWallItemProps> = (props: IWallItemProps) => {
|
||||
export const WallItem: FunctionComponent<IWallItemProps> = (
|
||||
props: IWallItemProps
|
||||
) => {
|
||||
const [videoPath, setVideoPath] = useState<string | undefined>(undefined);
|
||||
const [previewPath, setPreviewPath] = useState<string>("");
|
||||
const [screenshotPath, setScreenshotPath] = useState<string>("");
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const [tags, setTags] = useState<JSX.Element[]>([]);
|
||||
const config = StashService.useConfiguration();
|
||||
const videoHoverHook = VideoHoverHook.useVideoHover({resetOnMouseLeave: true});
|
||||
const showTextContainer = !!config.data && !!config.data.configuration ? config.data.configuration.interface.wallShowTitle : true;
|
||||
const videoHoverHook = VideoHoverHook.useVideoHover({
|
||||
resetOnMouseLeave: true
|
||||
});
|
||||
const showTextContainer =
|
||||
!!config.data && !!config.data.configuration
|
||||
? config.data.configuration.interface.wallShowTitle
|
||||
: true;
|
||||
|
||||
function onMouseEnter() {
|
||||
VideoHoverHook.onMouseEnter(videoHoverHook);
|
||||
@@ -45,7 +54,9 @@ export const WallItem: FunctionComponent<IWallItemProps> = (props: IWallItemProp
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
if (props.clickHandler === undefined) { return; }
|
||||
if (props.clickHandler === undefined) {
|
||||
return;
|
||||
}
|
||||
if (props.scene !== undefined) {
|
||||
props.clickHandler(props.scene);
|
||||
} else if (props.sceneMarker !== undefined) {
|
||||
@@ -65,18 +76,28 @@ export const WallItem: FunctionComponent<IWallItemProps> = (props: IWallItemProp
|
||||
function onTransitionEnd(event: React.TransitionEvent<HTMLDivElement>) {
|
||||
const target = event.currentTarget;
|
||||
if (target.classList.contains("double-scale") && target.parentElement) {
|
||||
target.parentElement.style.zIndex = '10';
|
||||
} else if(target.parentElement) {
|
||||
target.parentElement.style.zIndex = '';
|
||||
target.parentElement.style.zIndex = "10";
|
||||
} else if (target.parentElement) {
|
||||
target.parentElement.style.zIndex = "";
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (props.sceneMarker) {
|
||||
setPreviewPath(props.sceneMarker.preview);
|
||||
setTitle(`${props.sceneMarker!.title} - ${TextUtils.secondsToTimestamp(props.sceneMarker.seconds)}`);
|
||||
const thisTags = props.sceneMarker.tags.map((tag) => (<span key={tag.id}>{tag.name}</span>));
|
||||
thisTags.unshift(<span key={props.sceneMarker.primary_tag.id}>{props.sceneMarker.primary_tag.name}</span>);
|
||||
setTitle(
|
||||
`${props.sceneMarker!.title} - ${TextUtils.secondsToTimestamp(
|
||||
props.sceneMarker.seconds
|
||||
)}`
|
||||
);
|
||||
const thisTags = props.sceneMarker.tags.map(tag => (
|
||||
<span key={tag.id}>{tag.name}</span>
|
||||
));
|
||||
thisTags.unshift(
|
||||
<span key={props.sceneMarker.primary_tag.id}>
|
||||
{props.sceneMarker.primary_tag.name}
|
||||
</span>
|
||||
);
|
||||
setTags(thisTags);
|
||||
} else if (props.scene) {
|
||||
setPreviewPath(props.scene.paths.webp || "");
|
||||
@@ -93,9 +114,13 @@ export const WallItem: FunctionComponent<IWallItemProps> = (props: IWallItemProp
|
||||
}
|
||||
|
||||
const className = ["scene-wall-item-container"];
|
||||
if (videoHoverHook.isHovering.current) { className.push("double-scale"); }
|
||||
if (videoHoverHook.isHovering.current) {
|
||||
className.push("double-scale");
|
||||
}
|
||||
const style: React.CSSProperties = {};
|
||||
if (props.origin) { style.transformOrigin = props.origin; }
|
||||
if (props.origin) {
|
||||
style.transformOrigin = props.origin;
|
||||
}
|
||||
return (
|
||||
<div className="wall grid-item">
|
||||
<div
|
||||
@@ -110,20 +135,24 @@ export const WallItem: FunctionComponent<IWallItemProps> = (props: IWallItemProp
|
||||
<video
|
||||
src={videoPath}
|
||||
poster={screenshotPath}
|
||||
style={videoHoverHook.isHovering.current ? {} : {display: "none"}}
|
||||
style={videoHoverHook.isHovering.current ? {} : { display: "none" }}
|
||||
autoPlay
|
||||
loop
|
||||
ref={videoHoverHook.videoEl}
|
||||
/>
|
||||
<img alt="Preview" src={previewPath || screenshotPath} onError={() => previewNotFound()} />
|
||||
{showTextContainer ?
|
||||
<img
|
||||
alt="Preview"
|
||||
src={previewPath || screenshotPath}
|
||||
onError={() => previewNotFound()}
|
||||
/>
|
||||
{showTextContainer ? (
|
||||
<div className="scene-wall-item-text-container">
|
||||
<div style={{lineHeight: 1}}>
|
||||
{title}
|
||||
</div>
|
||||
<div style={{ lineHeight: 1 }}>{title}</div>
|
||||
{tags}
|
||||
</div> : ''
|
||||
}
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,10 +6,14 @@ import "./Wall.scss";
|
||||
interface IWallPanelProps {
|
||||
scenes?: GQL.SlimSceneDataFragment[];
|
||||
sceneMarkers?: GQL.SceneMarkerDataFragment[];
|
||||
clickHandler?: (item: GQL.SlimSceneDataFragment | GQL.SceneMarkerDataFragment) => void;
|
||||
clickHandler?: (
|
||||
item: GQL.SlimSceneDataFragment | GQL.SceneMarkerDataFragment
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const WallPanel: FunctionComponent<IWallPanelProps> = (props: IWallPanelProps) => {
|
||||
export const WallPanel: FunctionComponent<IWallPanelProps> = (
|
||||
props: IWallPanelProps
|
||||
) => {
|
||||
const [showOverlay, setShowOverlay] = useState<boolean>(false);
|
||||
|
||||
function onOverlay(show: boolean) {
|
||||
@@ -22,25 +26,47 @@ export const WallPanel: FunctionComponent<IWallPanelProps> = (props: IWallPanelP
|
||||
const endRemaining = total % rowSize;
|
||||
|
||||
// First row
|
||||
if (total === 1) { return "top"; }
|
||||
if (index === 0) { return "top left"; }
|
||||
if (index === rowSize - 1 || (total < rowSize && index === total - 1)) { return "top right"; }
|
||||
if (index < rowSize) { return "top"; }
|
||||
if (total === 1) {
|
||||
return "top";
|
||||
}
|
||||
if (index === 0) {
|
||||
return "top left";
|
||||
}
|
||||
if (index === rowSize - 1 || (total < rowSize && index === total - 1)) {
|
||||
return "top right";
|
||||
}
|
||||
if (index < rowSize) {
|
||||
return "top";
|
||||
}
|
||||
|
||||
// Bottom row
|
||||
if (isAtEnd && index === total - 1) { return "bottom right"; }
|
||||
if (isAtStart && index === total - rowSize) { return "bottom left"; }
|
||||
if (endRemaining !== 0 && index >= total - endRemaining) { return "bottom"; }
|
||||
if (endRemaining === 0 && index >= total - rowSize) { return "bottom"; }
|
||||
if (isAtEnd && index === total - 1) {
|
||||
return "bottom right";
|
||||
}
|
||||
if (isAtStart && index === total - rowSize) {
|
||||
return "bottom left";
|
||||
}
|
||||
if (endRemaining !== 0 && index >= total - endRemaining) {
|
||||
return "bottom";
|
||||
}
|
||||
if (endRemaining === 0 && index >= total - rowSize) {
|
||||
return "bottom";
|
||||
}
|
||||
|
||||
// Everything else
|
||||
if (isAtStart) { return "center left"; }
|
||||
if (isAtEnd) { return "center right"; }
|
||||
if (isAtStart) {
|
||||
return "center left";
|
||||
}
|
||||
if (isAtEnd) {
|
||||
return "center right";
|
||||
}
|
||||
return "center";
|
||||
}
|
||||
|
||||
function maybeRenderScenes() {
|
||||
if (props.scenes === undefined) { return; }
|
||||
if (props.scenes === undefined) {
|
||||
return;
|
||||
}
|
||||
return props.scenes.map((scene, index) => {
|
||||
const origin = getOrigin(index, 5, props.scenes!.length);
|
||||
return (
|
||||
@@ -56,7 +82,9 @@ export const WallPanel: FunctionComponent<IWallPanelProps> = (props: IWallPanelP
|
||||
}
|
||||
|
||||
function maybeRenderSceneMarkers() {
|
||||
if (props.sceneMarkers === undefined) { return; }
|
||||
if (props.sceneMarkers === undefined) {
|
||||
return;
|
||||
}
|
||||
return props.sceneMarkers.map((marker, index) => {
|
||||
const origin = getOrigin(index, 5, props.sceneMarkers!.length);
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import _ from "lodash";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Button, Form, Modal, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||
import { Icon , FilterSelect } from 'src/components/Shared';
|
||||
import { Button, Form, Modal, OverlayTrigger, Tooltip } from "react-bootstrap";
|
||||
import { Icon, FilterSelect } from "src/components/Shared";
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { Criterion, CriterionType } from "src/models/list-filter/criteria/criterion";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionType
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
import { NoneCriterion } from "src/models/list-filter/criteria/none";
|
||||
import { PerformersCriterion } from "src/models/list-filter/criteria/performers";
|
||||
import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
|
||||
@@ -11,7 +14,6 @@ import { TagsCriterion } from "src/models/list-filter/criteria/tags";
|
||||
import { makeCriteria } from "src/models/list-filter/criteria/utils";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
|
||||
|
||||
interface IAddFilterProps {
|
||||
onAddCriterion: (criterion: Criterion, oldId?: string) => void;
|
||||
onCancel: () => void;
|
||||
@@ -19,17 +21,23 @@ interface IAddFilterProps {
|
||||
editingCriterion?: Criterion;
|
||||
}
|
||||
|
||||
export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) => {
|
||||
const defaultValue= useRef<string|number|undefined>();
|
||||
export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
props: IAddFilterProps
|
||||
) => {
|
||||
const defaultValue = useRef<string | number | undefined>();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [criterion, setCriterion] = useState<Criterion<any, any>>(new NoneCriterion());
|
||||
const [criterion, setCriterion] = useState<Criterion<any, any>>(
|
||||
new NoneCriterion()
|
||||
);
|
||||
|
||||
const valueStage = useRef<any>(criterion.value);
|
||||
|
||||
// Configure if we are editing an existing criterion
|
||||
useEffect(() => {
|
||||
if (!props.editingCriterion) { return; }
|
||||
if (!props.editingCriterion) {
|
||||
return;
|
||||
}
|
||||
setIsOpen(true);
|
||||
setCriterion(props.editingCriterion);
|
||||
}, [props.editingCriterion]);
|
||||
@@ -40,7 +48,9 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
|
||||
setCriterion(newCriterion);
|
||||
}
|
||||
|
||||
function onChangedModifierSelect(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
function onChangedModifierSelect(
|
||||
event: React.ChangeEvent<HTMLSelectElement>
|
||||
) {
|
||||
const newCriterion = _.cloneDeep(criterion);
|
||||
newCriterion.modifier = event.target.value as any;
|
||||
setCriterion(newCriterion);
|
||||
@@ -65,7 +75,10 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
|
||||
function onAddFilter() {
|
||||
if (!Array.isArray(criterion.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];
|
||||
} else if (typeof value === "number" && value === undefined) {
|
||||
criterion.value = 0;
|
||||
@@ -73,7 +86,9 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
|
||||
criterion.value = "";
|
||||
}
|
||||
}
|
||||
const oldId = props.editingCriterion ? props.editingCriterion.getId() : undefined;
|
||||
const oldId = props.editingCriterion
|
||||
? props.editingCriterion.getId()
|
||||
: undefined;
|
||||
props.onAddCriterion(criterion, oldId);
|
||||
onToggle();
|
||||
}
|
||||
@@ -87,10 +102,14 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
|
||||
}
|
||||
|
||||
const maybeRenderFilterPopoverContents = () => {
|
||||
if (criterion.type === "none") { return; }
|
||||
if (criterion.type === "none") {
|
||||
return;
|
||||
}
|
||||
|
||||
function renderModifier() {
|
||||
if (criterion.modifierOptions.length === 0) { return; }
|
||||
if (criterion.modifierOptions.length === 0) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Form.Control
|
||||
@@ -98,7 +117,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
|
||||
onChange={onChangedModifierSelect}
|
||||
value={criterion.modifier}
|
||||
>
|
||||
{ criterion.modifierOptions.map(c => (
|
||||
{criterion.modifierOptions.map(c => (
|
||||
<option value={c.value}>{c.label}</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
@@ -108,7 +127,10 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
|
||||
|
||||
function renderSelect() {
|
||||
// Hide the value select if the modifier is "IsNull" or "NotNull"
|
||||
if (criterion.modifier === CriterionModifier.IsNull || criterion.modifier === CriterionModifier.NotNull) {
|
||||
if (
|
||||
criterion.modifier === CriterionModifier.IsNull ||
|
||||
criterion.modifier === CriterionModifier.NotNull
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -127,9 +149,9 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
|
||||
return (
|
||||
<FilterSelect
|
||||
type={type}
|
||||
onSelect={(items) => {
|
||||
criterion.value = items.map(i => ({id: i.id, label: i.name!})) }
|
||||
}
|
||||
onSelect={items => {
|
||||
criterion.value = items.map(i => ({ id: i.id, label: i.name! }));
|
||||
}}
|
||||
initialIds={criterion.value.map((labeled: any) => labeled.id)}
|
||||
/>
|
||||
);
|
||||
@@ -142,7 +164,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
|
||||
onChange={onChangedSingleSelect}
|
||||
value={criterion.value}
|
||||
>
|
||||
{ criterion.options.map(c => (
|
||||
{criterion.options.map(c => (
|
||||
<option value={c}>{c}</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
@@ -155,32 +177,29 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
|
||||
onBlur={onBlurInput}
|
||||
value={criterion.value || ""}
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Form.Group>
|
||||
{renderModifier()}
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
{renderSelect()}
|
||||
</Form.Group>
|
||||
<Form.Group>{renderModifier()}</Form.Group>
|
||||
<Form.Group>{renderSelect()}</Form.Group>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function maybeRenderFilterSelect() {
|
||||
if (props.editingCriterion) { return; }
|
||||
if (props.editingCriterion) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<Form.Group controlId="filter">
|
||||
<Form.Label>Filter</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
onChange={onChangedCriteriaType}
|
||||
value={criterion.type}>
|
||||
{ props.filter.criterionOptions.map(c => (
|
||||
value={criterion.type}
|
||||
>
|
||||
{props.filter.criterionOptions.map(c => (
|
||||
<option value={c.value}>{c.label}</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
@@ -195,17 +214,12 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
|
||||
placement="top"
|
||||
overlay={<Tooltip id="filter-tooltip">Filter</Tooltip>}
|
||||
>
|
||||
<Button
|
||||
onClick={() => onToggle()}
|
||||
active={isOpen}
|
||||
>
|
||||
<Button onClick={() => onToggle()} active={isOpen}>
|
||||
<Icon icon="filter" />
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
|
||||
<Modal
|
||||
show={isOpen}
|
||||
onHide={() => onToggle()}>
|
||||
<Modal show={isOpen} onHide={() => onToggle()}>
|
||||
<Modal.Header>{title}</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className="dialog-content">
|
||||
@@ -214,7 +228,9 @@ export const AddFilter: React.FC<IAddFilterProps> = (props: IAddFilterProps) =>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={onAddFilter} disabled={criterion.type === "none"}>{title}</Button>
|
||||
<Button onClick={onAddFilter} disabled={criterion.type === "none"}>
|
||||
{title}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { debounce } from "lodash";
|
||||
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 { Icon } from 'src/components/Shared';
|
||||
import { Icon } from "src/components/Shared";
|
||||
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
@@ -31,14 +39,19 @@ interface IListFilterProps {
|
||||
|
||||
const PAGE_SIZE_OPTIONS = ["20", "40", "60", "120"];
|
||||
|
||||
export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps) => {
|
||||
export const ListFilter: React.FC<IListFilterProps> = (
|
||||
props: IListFilterProps
|
||||
) => {
|
||||
const searchCallback = useCallback(
|
||||
debounce((event: any) => {
|
||||
props.onChangeQuery(event.target.value);
|
||||
}, 500), [props.onChangeQuery]
|
||||
}, 500),
|
||||
[props.onChangeQuery]
|
||||
);
|
||||
|
||||
const [editingCriterion, setEditingCriterion] = useState<Criterion | undefined>(undefined);
|
||||
const [editingCriterion, setEditingCriterion] = useState<
|
||||
Criterion | undefined
|
||||
>(undefined);
|
||||
|
||||
function onChangePageSize(event: SyntheticEvent<HTMLSelectElement>) {
|
||||
const val = event!.currentTarget!.value;
|
||||
@@ -76,39 +89,55 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
|
||||
|
||||
let removedCriterionId = "";
|
||||
function onRemoveCriterionTag(criterion?: Criterion) {
|
||||
if (!criterion) { return; }
|
||||
if (!criterion) {
|
||||
return;
|
||||
}
|
||||
setEditingCriterion(undefined);
|
||||
removedCriterionId = criterion.getId();
|
||||
props.onRemoveCriterion(criterion);
|
||||
}
|
||||
function onClickCriterionTag(criterion?: Criterion) {
|
||||
if (!criterion || removedCriterionId !== "") { return; }
|
||||
if (!criterion || removedCriterionId !== "") {
|
||||
return;
|
||||
}
|
||||
setEditingCriterion(criterion);
|
||||
}
|
||||
|
||||
function renderSortByOptions() {
|
||||
return props.filter.sortByOptions.map((option) => (
|
||||
<Dropdown.Item onClick={onChangeSortBy} key={option}>{option}</Dropdown.Item>
|
||||
return props.filter.sortByOptions.map(option => (
|
||||
<Dropdown.Item onClick={onChangeSortBy} key={option}>
|
||||
{option}
|
||||
</Dropdown.Item>
|
||||
));
|
||||
}
|
||||
|
||||
function renderDisplayModeOptions() {
|
||||
function getIcon(option: DisplayMode) {
|
||||
switch (option) {
|
||||
case DisplayMode.Grid: return "th-large";
|
||||
case DisplayMode.List: return "list";
|
||||
case DisplayMode.Wall: return "square";
|
||||
case DisplayMode.Grid:
|
||||
return "th-large";
|
||||
case DisplayMode.List:
|
||||
return "list";
|
||||
case DisplayMode.Wall:
|
||||
return "square";
|
||||
}
|
||||
}
|
||||
function getLabel(option: DisplayMode) {
|
||||
switch (option) {
|
||||
case DisplayMode.Grid: return "Grid";
|
||||
case DisplayMode.List: return "List";
|
||||
case DisplayMode.Wall: return "Wall";
|
||||
case DisplayMode.Grid:
|
||||
return "Grid";
|
||||
case DisplayMode.List:
|
||||
return "List";
|
||||
case DisplayMode.Wall:
|
||||
return "Wall";
|
||||
}
|
||||
}
|
||||
return props.filter.displayModeOptions.map((option) => (
|
||||
<OverlayTrigger overlay={<Tooltip id="display-mode-tooltip">{getLabel(option)}</Tooltip>}>
|
||||
return props.filter.displayModeOptions.map(option => (
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
<Tooltip id="display-mode-tooltip">{getLabel(option)}</Tooltip>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
key={option}
|
||||
active={props.filter.displayMode === option}
|
||||
@@ -121,7 +150,7 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
|
||||
}
|
||||
|
||||
function renderFilterTags() {
|
||||
return props.filter.criteria.map((criterion) => (
|
||||
return props.filter.criteria.map(criterion => (
|
||||
<Badge
|
||||
className="tag-item"
|
||||
variant="secondary"
|
||||
@@ -149,25 +178,30 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
|
||||
|
||||
function renderSelectAll() {
|
||||
if (props.onSelectAll) {
|
||||
return <Dropdown.Item onClick={() => onSelectAll()}>Select All</Dropdown.Item>;
|
||||
return (
|
||||
<Dropdown.Item onClick={() => onSelectAll()}>Select All</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSelectNone() {
|
||||
if (props.onSelectNone) {
|
||||
return <Dropdown.Item onClick={() => onSelectNone()}>Select None</Dropdown.Item>;
|
||||
return (
|
||||
<Dropdown.Item onClick={() => onSelectNone()}>
|
||||
Select None
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderMore() {
|
||||
const options = [
|
||||
renderSelectAll(),
|
||||
renderSelectNone()
|
||||
];
|
||||
const options = [renderSelectAll(), renderSelectNone()];
|
||||
|
||||
if (props.otherOperations) {
|
||||
props.otherOperations.forEach((o) => {
|
||||
options.push(<Dropdown.Item onClick={o.onClick}>{o.text}</Dropdown.Item>);
|
||||
props.otherOperations.forEach(o => {
|
||||
options.push(
|
||||
<Dropdown.Item onClick={o.onClick}>{o.text}</Dropdown.Item>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -179,15 +213,13 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
|
||||
<Icon icon="ellipsis-h" />
|
||||
</Button>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{options}
|
||||
</Dropdown.Menu>
|
||||
<Dropdown.Menu>{options}</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeZoom(v : number) {
|
||||
function onChangeZoom(v: number) {
|
||||
if (props.onChangeZoom) {
|
||||
props.onChangeZoom(v);
|
||||
}
|
||||
@@ -201,7 +233,9 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
|
||||
type="range"
|
||||
min={0}
|
||||
max={3}
|
||||
onChange={(event: any) => onChangeZoom(Number.parseInt(event.target.value, 10))}
|
||||
onChange={(event: any) =>
|
||||
onChangeZoom(Number.parseInt(event.target.value, 10))
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
@@ -224,23 +258,35 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
|
||||
value={props.filter.itemsPerPage.toString()}
|
||||
className="filter-item"
|
||||
>
|
||||
{ PAGE_SIZE_OPTIONS.map(s => <option value={s}>{s}</option>) }
|
||||
{PAGE_SIZE_OPTIONS.map(s => (
|
||||
<option value={s}>{s}</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
<ButtonGroup className="filter-item">
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle variant="secondary" id="more-menu">
|
||||
<Button>{props.filter.sortBy}</Button>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{renderSortByOptions()}
|
||||
</Dropdown.Menu>
|
||||
<Dropdown.Menu>{renderSortByOptions()}</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
|
||||
<OverlayTrigger overlay={
|
||||
<Tooltip id="sort-direction-tooltip">{props.filter.sortDirection === "asc" ? "Ascending" : "Descending"}</Tooltip>
|
||||
}>
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
<Tooltip id="sort-direction-tooltip">
|
||||
{props.filter.sortDirection === "asc"
|
||||
? "Ascending"
|
||||
: "Descending"}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Button onClick={onChangeSortDirection}>
|
||||
<Icon icon={props.filter.sortDirection === "asc" ? "caret-up" : "caret-down"} />
|
||||
<Icon
|
||||
icon={
|
||||
props.filter.sortDirection === "asc"
|
||||
? "caret-up"
|
||||
: "caret-down"
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
</ButtonGroup>
|
||||
@@ -258,11 +304,15 @@ export const ListFilter: React.FC<IListFilterProps> = (props: IListFilterProps)
|
||||
|
||||
{maybeRenderZoom()}
|
||||
|
||||
<ButtonGroup className="filter-item">
|
||||
{renderMore()}
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="filter-item">{renderMore()}</ButtonGroup>
|
||||
</div>
|
||||
<div style={{display: "flex", justifyContent: "center", margin: "10px auto"}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
margin: "10px auto"
|
||||
}}
|
||||
>
|
||||
{renderFilterTags()}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Button, ButtonGroup } from 'react-bootstrap';
|
||||
import { Button, ButtonGroup } from "react-bootstrap";
|
||||
|
||||
interface IPaginationProps {
|
||||
itemsPerPage: number;
|
||||
@@ -8,7 +8,12 @@ interface IPaginationProps {
|
||||
onChangePage: (page: number) => void;
|
||||
}
|
||||
|
||||
export const Pagination: React.FC<IPaginationProps> = ({ itemsPerPage, currentPage, totalItems, onChangePage }) => {
|
||||
export const Pagination: React.FC<IPaginationProps> = ({
|
||||
itemsPerPage,
|
||||
currentPage,
|
||||
totalItems,
|
||||
onChangePage
|
||||
}) => {
|
||||
const totalPages = Math.ceil(totalItems / itemsPerPage);
|
||||
|
||||
let startPage: number;
|
||||
@@ -28,35 +33,44 @@ export const Pagination: React.FC<IPaginationProps> = ({ itemsPerPage, currentPa
|
||||
endPage = currentPage + 4;
|
||||
}
|
||||
|
||||
const pages = [...Array((endPage + 1) - startPage).keys()].map((i) => startPage + i);
|
||||
const pages = [...Array(endPage + 1 - startPage).keys()].map(
|
||||
i => startPage + i
|
||||
);
|
||||
|
||||
const pageButtons = pages.map((page: number) => (
|
||||
<Button
|
||||
key={page}
|
||||
active={currentPage === page}
|
||||
onClick={() => onChangePage(page)}
|
||||
>{page}</Button>
|
||||
>
|
||||
{page}
|
||||
</Button>
|
||||
));
|
||||
|
||||
return (
|
||||
<ButtonGroup className="filter-container">
|
||||
<Button
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => onChangePage(1)}
|
||||
>First</Button>
|
||||
<Button disabled={currentPage === 1} onClick={() => onChangePage(1)}>
|
||||
First
|
||||
</Button>
|
||||
<Button
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => onChangePage(currentPage - 1)}
|
||||
>Previous</Button>
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
{pageButtons}
|
||||
<Button
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() => onChangePage(currentPage + 1)}
|
||||
>Next</Button>
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
<Button
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() => onChangePage(totalPages)}
|
||||
>Last</Button>
|
||||
>
|
||||
Last
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Card } from 'react-bootstrap';
|
||||
import { Card } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { NavUtils, TextUtils } from "src/utils";
|
||||
@@ -9,17 +9,19 @@ interface IPerformerCardProps {
|
||||
ageFromDate?: string;
|
||||
}
|
||||
|
||||
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 ageString = `${age} years old${props.ageFromDate ? " in this scene." : "."}`;
|
||||
const ageString = `${age} years old${
|
||||
props.ageFromDate ? " in this scene." : "."
|
||||
}`;
|
||||
|
||||
function maybeRenderFavoriteBanner() {
|
||||
if (props.performer.favorite === false) { return; }
|
||||
return (
|
||||
<div className="rating-banner rating-5">
|
||||
FAVORITE
|
||||
</div>
|
||||
);
|
||||
if (props.performer.favorite === false) {
|
||||
return;
|
||||
}
|
||||
return <div className="rating-banner rating-5">FAVORITE</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -27,16 +29,19 @@ export const PerformerCard: React.FC<IPerformerCardProps> = (props: IPerformerCa
|
||||
<Link
|
||||
to={`/performers/${props.performer.id}`}
|
||||
className="performer previewable image"
|
||||
style={{backgroundImage: `url(${props.performer.image_path})`}}
|
||||
style={{ backgroundImage: `url(${props.performer.image_path})` }}
|
||||
>
|
||||
{maybeRenderFavoriteBanner()}
|
||||
</Link>
|
||||
<div className="card-section">
|
||||
<h4 className="text-truncate">
|
||||
{props.performer.name}
|
||||
</h4>
|
||||
{age !== 0 ? <div>{ageString}</div> : ''}
|
||||
<span>Stars in {props.performer.scene_count} <Link to={NavUtils.makePerformerScenesUrl(props.performer)}>scenes</Link>.
|
||||
<h4 className="text-truncate">{props.performer.name}</h4>
|
||||
{age !== 0 ? <div>{ageString}</div> : ""}
|
||||
<span>
|
||||
Stars in {props.performer.scene_count}{" "}
|
||||
<Link to={NavUtils.makePerformerScenesUrl(props.performer)}>
|
||||
scenes
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
/* eslint-disable react/no-this-in-sfc */
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Form, Spinner, Table } from 'react-bootstrap';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import { Button, Form, Spinner, Table } from "react-bootstrap";
|
||||
import { useParams, useHistory } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { DetailsEditNavbar, Icon, Modal, ScrapePerformerSuggest } from "src/components/Shared";
|
||||
import { ImageUtils, TableUtils } from 'src/utils'
|
||||
import { useToast } from 'src/hooks';
|
||||
import {
|
||||
DetailsEditNavbar,
|
||||
Icon,
|
||||
Modal,
|
||||
ScrapePerformerSuggest
|
||||
} from "src/components/Shared";
|
||||
import { ImageUtils, TableUtils } from "src/utils";
|
||||
import { useToast } from "src/hooks";
|
||||
|
||||
export const Performer: React.FC = () => {
|
||||
const Toast = useToast();
|
||||
const history = useHistory();
|
||||
const { id = 'new' } = useParams();
|
||||
const { id = "new" } = useParams();
|
||||
const isNew = id === "new";
|
||||
|
||||
// Editing state
|
||||
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
||||
const [isDisplayingScraperDialog, setIsDisplayingScraperDialog] = useState<GQL.ListPerformerScrapersListPerformerScrapers | undefined>(undefined);
|
||||
const [scrapePerformerDetails, setScrapePerformerDetails] = useState<GQL.ScrapePerformerListScrapePerformerList | undefined>(undefined);
|
||||
const [isDisplayingScraperDialog, setIsDisplayingScraperDialog] = useState<
|
||||
GQL.ListPerformerScrapersListPerformerScrapers | undefined
|
||||
>(undefined);
|
||||
const [scrapePerformerDetails, setScrapePerformerDetails] = useState<
|
||||
GQL.ScrapePerformerListScrapePerformerList | undefined
|
||||
>(undefined);
|
||||
|
||||
// Editing performer state
|
||||
const [image, setImage] = useState<string | undefined>(undefined);
|
||||
@@ -30,9 +39,13 @@ export const Performer: React.FC = () => {
|
||||
const [country, setCountry] = useState<string | undefined>(undefined);
|
||||
const [eyeColor, setEyeColor] = useState<string | undefined>(undefined);
|
||||
const [height, setHeight] = useState<string | undefined>(undefined);
|
||||
const [measurements, setMeasurements] = useState<string | undefined>(undefined);
|
||||
const [measurements, setMeasurements] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [fakeTits, setFakeTits] = useState<string | undefined>(undefined);
|
||||
const [careerLength, setCareerLength] = useState<string | undefined>(undefined);
|
||||
const [careerLength, setCareerLength] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [tattoos, setTattoos] = useState<string | undefined>(undefined);
|
||||
const [piercings, setPiercings] = useState<string | undefined>(undefined);
|
||||
const [url, setUrl] = useState<string | undefined>(undefined);
|
||||
@@ -40,21 +53,35 @@ export const Performer: React.FC = () => {
|
||||
const [instagram, setInstagram] = useState<string | undefined>(undefined);
|
||||
|
||||
// Performer state
|
||||
const [performer, setPerformer] = useState<Partial<GQL.PerformerDataFragment>>({});
|
||||
const [imagePreview, setImagePreview] = useState<string | undefined>(undefined);
|
||||
const [performer, setPerformer] = useState<
|
||||
Partial<GQL.PerformerDataFragment>
|
||||
>({});
|
||||
const [imagePreview, setImagePreview] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const Scrapers = StashService.useListPerformerScrapers();
|
||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.ListPerformerScrapersListPerformerScrapers[]>([]);
|
||||
const [queryableScrapers, setQueryableScrapers] = useState<
|
||||
GQL.ListPerformerScrapersListPerformerScrapers[]
|
||||
>([]);
|
||||
|
||||
const { data, error } = StashService.useFindPerformer(id);
|
||||
const updatePerformer = StashService.usePerformerUpdate(getPerformerInput() as GQL.PerformerUpdateInput);
|
||||
const createPerformer = StashService.usePerformerCreate(getPerformerInput() as GQL.PerformerCreateInput);
|
||||
const deletePerformer = StashService.usePerformerDestroy(getPerformerInput() as GQL.PerformerDestroyInput);
|
||||
const updatePerformer = StashService.usePerformerUpdate(
|
||||
getPerformerInput() as GQL.PerformerUpdateInput
|
||||
);
|
||||
const createPerformer = StashService.usePerformerCreate(
|
||||
getPerformerInput() as GQL.PerformerCreateInput
|
||||
);
|
||||
const deletePerformer = StashService.usePerformerDestroy(
|
||||
getPerformerInput() as GQL.PerformerDestroyInput
|
||||
);
|
||||
|
||||
function updatePerformerEditState(state: Partial<GQL.PerformerDataFragment | GQL.ScrapeFreeonesScrapeFreeones>) {
|
||||
function updatePerformerEditState(
|
||||
state: Partial<GQL.PerformerDataFragment | GQL.ScrapeFreeonesScrapeFreeones>
|
||||
) {
|
||||
if ((state as GQL.PerformerDataFragment).favorite !== undefined) {
|
||||
setFavorite((state as GQL.PerformerDataFragment).favorite);
|
||||
}
|
||||
@@ -77,8 +104,7 @@ export const Performer: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(false);
|
||||
if(data?.findPerformer)
|
||||
setPerformer(data.findPerformer);
|
||||
if (data?.findPerformer) setPerformer(data.findPerformer);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -96,25 +122,28 @@ export const Performer: React.FC = () => {
|
||||
ImageUtils.usePasteImage(onImageLoad);
|
||||
|
||||
useEffect(() => {
|
||||
let newQueryableScrapers : GQL.ListPerformerScrapersListPerformerScrapers[] = [];
|
||||
let newQueryableScrapers: GQL.ListPerformerScrapersListPerformerScrapers[] = [];
|
||||
|
||||
if (!!Scrapers.data && Scrapers.data.listPerformerScrapers) {
|
||||
newQueryableScrapers = Scrapers.data.listPerformerScrapers.filter((s) => {
|
||||
return s.performer && s.performer.supported_scrapes.includes(GQL.ScrapeType.Name);
|
||||
newQueryableScrapers = Scrapers.data.listPerformerScrapers.filter(s => {
|
||||
return (
|
||||
s.performer &&
|
||||
s.performer.supported_scrapes.includes(GQL.ScrapeType.Name)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
setQueryableScrapers(newQueryableScrapers);
|
||||
|
||||
}, [Scrapers.data]);
|
||||
|
||||
if ((!isNew && !isEditing && !data?.findPerformer) || isLoading)
|
||||
return <Spinner animation="border" variant="light" />;
|
||||
if (error)
|
||||
return <div>{error.message}</div>;
|
||||
if (error) return <div>{error.message}</div>;
|
||||
|
||||
function getPerformerInput() {
|
||||
const performerInput: Partial<GQL.PerformerCreateInput | GQL.PerformerUpdateInput> = {
|
||||
const performerInput: Partial<
|
||||
GQL.PerformerCreateInput | GQL.PerformerUpdateInput
|
||||
> = {
|
||||
name,
|
||||
aliases,
|
||||
favorite,
|
||||
@@ -131,7 +160,7 @@ export const Performer: React.FC = () => {
|
||||
url,
|
||||
twitter,
|
||||
instagram,
|
||||
image,
|
||||
image
|
||||
};
|
||||
|
||||
if (!isNew) {
|
||||
@@ -167,7 +196,7 @@ export const Performer: React.FC = () => {
|
||||
setIsLoading(false);
|
||||
|
||||
// redirect to performers page
|
||||
history.push('/performers');
|
||||
history.push("/performers");
|
||||
}
|
||||
|
||||
async function onAutoTag() {
|
||||
@@ -175,7 +204,7 @@ export const Performer: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await StashService.queryMetadataAutoTag({ performers: [performer.id]});
|
||||
await StashService.queryMetadataAutoTag({ performers: [performer.id] });
|
||||
Toast.success({ content: "Started auto tagging" });
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
@@ -186,13 +215,14 @@ export const Performer: React.FC = () => {
|
||||
ImageUtils.onImageChange(event, onImageLoad);
|
||||
}
|
||||
|
||||
function onDisplayFreeOnesDialog(scraper: GQL.ListPerformerScrapersListPerformerScrapers) {
|
||||
function onDisplayFreeOnesDialog(
|
||||
scraper: GQL.ListPerformerScrapersListPerformerScrapers
|
||||
) {
|
||||
setIsDisplayingScraperDialog(scraper);
|
||||
}
|
||||
|
||||
function getQueryScraperPerformerInput() {
|
||||
if (!scrapePerformerDetails)
|
||||
return {};
|
||||
if (!scrapePerformerDetails) return {};
|
||||
|
||||
const { __typename, ...ret } = scrapePerformerDetails;
|
||||
return ret;
|
||||
@@ -202,11 +232,12 @@ export const Performer: React.FC = () => {
|
||||
setIsDisplayingScraperDialog(undefined);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (!scrapePerformerDetails || !isDisplayingScraperDialog)
|
||||
return;
|
||||
const result = await StashService.queryScrapePerformer(isDisplayingScraperDialog.id, getQueryScraperPerformerInput());
|
||||
if (!result?.data?.scrapePerformer)
|
||||
return;
|
||||
if (!scrapePerformerDetails || !isDisplayingScraperDialog) return;
|
||||
const result = await StashService.queryScrapePerformer(
|
||||
isDisplayingScraperDialog.id,
|
||||
getQueryScraperPerformerInput()
|
||||
);
|
||||
if (!result?.data?.scrapePerformer) return;
|
||||
updatePerformerEditState(result.data.scrapePerformer);
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
@@ -215,12 +246,13 @@ export const Performer: React.FC = () => {
|
||||
}
|
||||
|
||||
async function onScrapePerformerURL() {
|
||||
if (!url)
|
||||
return;
|
||||
if (!url) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await StashService.queryScrapePerformerURL(url);
|
||||
if (!result.data || !result.data.scrapePerformerURL) { return; }
|
||||
if (!result.data || !result.data.scrapePerformerURL) {
|
||||
return;
|
||||
}
|
||||
updatePerformerEditState(result.data.scrapePerformerURL);
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
@@ -235,7 +267,7 @@ export const Performer: React.FC = () => {
|
||||
value: ethnicity,
|
||||
isEditing,
|
||||
onChange: (value: string) => setEthnicity(value),
|
||||
selectOptions: ["white", "black", "asian", "hispanic"],
|
||||
selectOptions: ["white", "black", "asian", "hispanic"]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -250,8 +282,10 @@ export const Performer: React.FC = () => {
|
||||
<div className="dialog-content">
|
||||
<ScrapePerformerSuggest
|
||||
placeholder="Performer name"
|
||||
scraperId={isDisplayingScraperDialog ? isDisplayingScraperDialog.id : ""}
|
||||
onSelectPerformer={(query) => setScrapePerformerDetails(query)}
|
||||
scraperId={
|
||||
isDisplayingScraperDialog ? isDisplayingScraperDialog.id : ""
|
||||
}
|
||||
onSelectPerformer={query => setScrapePerformerDetails(query)}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -259,9 +293,12 @@ export const Performer: React.FC = () => {
|
||||
}
|
||||
|
||||
function urlScrapable(scrapedUrl: string) {
|
||||
return !!scrapedUrl && (Scrapers?.data?.listPerformerScrapers ?? []).some(s => (
|
||||
return (
|
||||
!!scrapedUrl &&
|
||||
(Scrapers?.data?.listPerformerScrapers ?? []).some(s =>
|
||||
(s?.performer?.urls ?? []).some(u => scrapedUrl.includes(u))
|
||||
));
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderScrapeButton() {
|
||||
@@ -269,12 +306,10 @@ export const Performer: React.FC = () => {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
id="scrape-url-button"
|
||||
onClick={() => onScrapePerformerURL()}>
|
||||
<Button id="scrape-url-button" onClick={() => onScrapePerformerURL()}>
|
||||
<Icon icon="file-upload" />
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function renderURLField() {
|
||||
@@ -290,7 +325,9 @@ export const Performer: React.FC = () => {
|
||||
readOnly={!isEditing}
|
||||
plaintext={!isEditing}
|
||||
placeholder="URL"
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) => setUrl(event.currentTarget.value) }
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
||||
setUrl(event.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -309,7 +346,10 @@ export const Performer: React.FC = () => {
|
||||
performer={performer}
|
||||
isNew={isNew}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={() => { setIsEditing(!isEditing); updatePerformerEditState(performer); }}
|
||||
onToggleEdit={() => {
|
||||
setIsEditing(!isEditing);
|
||||
updatePerformerEditState(performer);
|
||||
}}
|
||||
onSave={onSave}
|
||||
onDelete={onDelete}
|
||||
onImageChange={onImageChangeHandler}
|
||||
@@ -334,12 +374,14 @@ export const Performer: React.FC = () => {
|
||||
readOnly={!isEditing}
|
||||
plaintext={!isEditing}
|
||||
placeholder="Aliases"
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) => setAliases(event.currentTarget.value) }
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
||||
setAliases(event.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
</Form.Group>
|
||||
</h6>
|
||||
<div>
|
||||
<span style={{fontWeight: 300}}>Favorite:</span>
|
||||
<span style={{ fontWeight: 300 }}>Favorite:</span>
|
||||
<Button
|
||||
disabled={!isEditing}
|
||||
className={favorite ? "favorite" : undefined}
|
||||
@@ -349,32 +391,76 @@ export const Performer: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table id="performer-details" style={{width: "100%"}}>
|
||||
<Table id="performer-details" style={{ width: "100%" }}>
|
||||
<tbody>
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Birthdate (YYYY-MM-DD)", value: birthdate, isEditing, onChange: setBirthdate})}
|
||||
{TableUtils.renderInputGroup({
|
||||
title: "Birthdate (YYYY-MM-DD)",
|
||||
value: birthdate,
|
||||
isEditing,
|
||||
onChange: setBirthdate
|
||||
})}
|
||||
{renderEthnicity()}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Eye Color", value: eyeColor, isEditing, onChange: setEyeColor})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Country", value: country, isEditing, onChange: setCountry})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Height (CM)", value: height, isEditing, onChange: setHeight})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Measurements", value: measurements, isEditing, onChange: setMeasurements})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Fake Tits", value: fakeTits, isEditing, onChange: setFakeTits})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Career Length", value: careerLength, isEditing, onChange: setCareerLength})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Tattoos", value: tattoos, isEditing, onChange: setTattoos})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Piercings", value: piercings, isEditing, onChange: setPiercings})}
|
||||
{TableUtils.renderInputGroup({
|
||||
title: "Eye Color",
|
||||
value: eyeColor,
|
||||
isEditing,
|
||||
onChange: setEyeColor
|
||||
})}
|
||||
{TableUtils.renderInputGroup({
|
||||
title: "Country",
|
||||
value: country,
|
||||
isEditing,
|
||||
onChange: setCountry
|
||||
})}
|
||||
{TableUtils.renderInputGroup({
|
||||
title: "Height (CM)",
|
||||
value: height,
|
||||
isEditing,
|
||||
onChange: setHeight
|
||||
})}
|
||||
{TableUtils.renderInputGroup({
|
||||
title: "Measurements",
|
||||
value: measurements,
|
||||
isEditing,
|
||||
onChange: setMeasurements
|
||||
})}
|
||||
{TableUtils.renderInputGroup({
|
||||
title: "Fake Tits",
|
||||
value: fakeTits,
|
||||
isEditing,
|
||||
onChange: setFakeTits
|
||||
})}
|
||||
{TableUtils.renderInputGroup({
|
||||
title: "Career Length",
|
||||
value: careerLength,
|
||||
isEditing,
|
||||
onChange: setCareerLength
|
||||
})}
|
||||
{TableUtils.renderInputGroup({
|
||||
title: "Tattoos",
|
||||
value: tattoos,
|
||||
isEditing,
|
||||
onChange: setTattoos
|
||||
})}
|
||||
{TableUtils.renderInputGroup({
|
||||
title: "Piercings",
|
||||
value: piercings,
|
||||
isEditing,
|
||||
onChange: setPiercings
|
||||
})}
|
||||
{renderURLField()}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Twitter", value: twitter, isEditing, onChange: setTwitter})}
|
||||
{TableUtils.renderInputGroup(
|
||||
{title: "Instagram", value: instagram, isEditing, onChange: setInstagram})}
|
||||
{TableUtils.renderInputGroup({
|
||||
title: "Twitter",
|
||||
value: twitter,
|
||||
isEditing,
|
||||
onChange: setTwitter
|
||||
})}
|
||||
{TableUtils.renderInputGroup({
|
||||
title: "Instagram",
|
||||
value: instagram,
|
||||
isEditing,
|
||||
onChange: setInstagram
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import { QueryHookResult } from "react-apollo-hooks";
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { FindPerformersQuery, FindPerformersVariables } from "src/core/generated-graphql";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import {
|
||||
FindPerformersQuery,
|
||||
FindPerformersVariables
|
||||
} from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { usePerformersList } from "src/hooks";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
@@ -15,41 +18,60 @@ export const PerformerList: React.FC = () => {
|
||||
const otherOperations = [
|
||||
{
|
||||
text: "Open Random",
|
||||
onClick: getRandom,
|
||||
onClick: getRandom
|
||||
}
|
||||
];
|
||||
|
||||
const listData = usePerformersList({
|
||||
otherOperations,
|
||||
renderContent,
|
||||
renderContent
|
||||
});
|
||||
|
||||
async function getRandom(result: QueryHookResult<FindPerformersQuery, FindPerformersVariables>, filter: ListFilterModel) {
|
||||
async function getRandom(
|
||||
result: QueryHookResult<FindPerformersQuery, FindPerformersVariables>,
|
||||
filter: ListFilterModel
|
||||
) {
|
||||
if (result.data && result.data.findPerformers) {
|
||||
const {count} = result.data.findPerformers;
|
||||
const { count } = result.data.findPerformers;
|
||||
const index = Math.floor(Math.random() * count);
|
||||
const filterCopy = _.cloneDeep(filter);
|
||||
filterCopy.itemsPerPage = 1;
|
||||
filterCopy.currentPage = index + 1;
|
||||
const singleResult = await StashService.queryFindPerformers(filterCopy);
|
||||
if (singleResult && singleResult.data && singleResult.data.findPerformers && singleResult.data.findPerformers.performers.length === 1) {
|
||||
const {id} = singleResult!.data!.findPerformers!.performers[0]!;
|
||||
history.push(`/performers/${ id}`);
|
||||
if (
|
||||
singleResult &&
|
||||
singleResult.data &&
|
||||
singleResult.data.findPerformers &&
|
||||
singleResult.data.findPerformers.performers.length === 1
|
||||
) {
|
||||
const { id } = singleResult!.data!.findPerformers!.performers[0]!;
|
||||
history.push(`/performers/${id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderContent(
|
||||
result: QueryHookResult<FindPerformersQuery, FindPerformersVariables>, filter: ListFilterModel) {
|
||||
if (!result.data || !result.data.findPerformers) { return; }
|
||||
result: QueryHookResult<FindPerformersQuery, FindPerformersVariables>,
|
||||
filter: ListFilterModel
|
||||
) {
|
||||
if (!result.data || !result.data.findPerformers) {
|
||||
return;
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.Grid) {
|
||||
return (
|
||||
<div className="grid">
|
||||
{result.data.findPerformers.performers.map((p) => (<PerformerCard key={p.id} performer={p} />))}
|
||||
{result.data.findPerformers.performers.map(p => (
|
||||
<PerformerCard key={p.id} performer={p} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} if (filter.displayMode === DisplayMode.List) {
|
||||
return <PerformerListTable performers={result.data.findPerformers.performers}/>;
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.List) {
|
||||
return (
|
||||
<PerformerListTable
|
||||
performers={result.data.findPerformers.performers}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
/* eslint-disable jsx-a11y/control-has-associated-label */
|
||||
|
||||
import React from "react";
|
||||
import { Button, Table } from 'react-bootstrap';
|
||||
import { Button, Table } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Icon } from 'src/components/Shared';
|
||||
import { Icon } from "src/components/Shared";
|
||||
import { NavUtils } from "src/utils";
|
||||
|
||||
interface IPerformerListTableProps {
|
||||
performers: GQL.PerformerDataFragment[];
|
||||
}
|
||||
|
||||
export const PerformerListTable: React.FC<IPerformerListTableProps> = (props: IPerformerListTableProps) => {
|
||||
|
||||
function maybeRenderFavoriteHeart(performer : GQL.PerformerDataFragment) {
|
||||
if (!performer.favorite) { return; }
|
||||
export const PerformerListTable: React.FC<IPerformerListTableProps> = (
|
||||
props: IPerformerListTableProps
|
||||
) => {
|
||||
function maybeRenderFavoriteHeart(performer: GQL.PerformerDataFragment) {
|
||||
if (!performer.favorite) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<Button disabled className="favorite">
|
||||
<Icon icon="heart" />
|
||||
@@ -22,58 +25,47 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (props: IP
|
||||
);
|
||||
}
|
||||
|
||||
function renderPerformerImage(performer : GQL.PerformerDataFragment) {
|
||||
function renderPerformerImage(performer: GQL.PerformerDataFragment) {
|
||||
const style: React.CSSProperties = {
|
||||
backgroundImage: `url('${performer.image_path}')`,
|
||||
lineHeight: 5,
|
||||
backgroundSize: "contain",
|
||||
display: "inline-block",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundRepeat: "no-repeat"
|
||||
};
|
||||
|
||||
return (
|
||||
<Link
|
||||
className="performer-list-thumbnail"
|
||||
to={`/performers/${performer.id}`}
|
||||
style={style}/>
|
||||
)
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderPerformerRow(performer : GQL.PerformerDataFragment) {
|
||||
function renderPerformerRow(performer: GQL.PerformerDataFragment) {
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<td>
|
||||
{renderPerformerImage(performer)}
|
||||
</td>
|
||||
<td style={{textAlign: "left"}}>
|
||||
<td>{renderPerformerImage(performer)}</td>
|
||||
<td style={{ textAlign: "left" }}>
|
||||
<Link to={`/performers/${performer.id}`}>
|
||||
<h5 className="text-truncate">
|
||||
{performer.name}
|
||||
</h5>
|
||||
<h5 className="text-truncate">{performer.name}</h5>
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
{performer.aliases ? performer.aliases : ''}
|
||||
</td>
|
||||
<td>
|
||||
{maybeRenderFavoriteHeart(performer)}
|
||||
</td>
|
||||
<td>{performer.aliases ? performer.aliases : ""}</td>
|
||||
<td>{maybeRenderFavoriteHeart(performer)}</td>
|
||||
<td>
|
||||
<Link to={NavUtils.makePerformerScenesUrl(performer)}>
|
||||
<h6>{performer.scene_count}</h6>
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
{performer.birthdate}
|
||||
</td>
|
||||
<td>
|
||||
{performer.height}
|
||||
</td>
|
||||
<td>{performer.birthdate}</td>
|
||||
<td>{performer.height}</td>
|
||||
</tr>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -91,12 +83,9 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (props: IP
|
||||
<th>Height</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{props.performers.map(renderPerformerRow)}
|
||||
</tbody>
|
||||
<tbody>{props.performers.map(renderPerformerRow)}</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,31 +1,44 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, ButtonGroup, Card, Form } from 'react-bootstrap';
|
||||
import { Button, ButtonGroup, Card, Form } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import cx from 'classnames';
|
||||
import cx from "classnames";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { VideoHoverHook } from "src/hooks";
|
||||
import { Icon, TagLink, HoverPopover } from 'src/components/Shared';
|
||||
import { Icon, TagLink, HoverPopover } from "src/components/Shared";
|
||||
import { TextUtils } from "src/utils";
|
||||
|
||||
interface ISceneCardProps {
|
||||
scene: GQL.SlimSceneDataFragment;
|
||||
selected: boolean | undefined;
|
||||
zoomIndex: number;
|
||||
onSelectedChanged: (selected : boolean, shiftKey : boolean) => void;
|
||||
onSelectedChanged: (selected: boolean, shiftKey: boolean) => void;
|
||||
}
|
||||
|
||||
export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) => {
|
||||
export const SceneCard: React.FC<ISceneCardProps> = (
|
||||
props: ISceneCardProps
|
||||
) => {
|
||||
const [previewPath, setPreviewPath] = useState<string | undefined>(undefined);
|
||||
const videoHoverHook = VideoHoverHook.useVideoHover({resetOnMouseLeave: false});
|
||||
const videoHoverHook = VideoHoverHook.useVideoHover({
|
||||
resetOnMouseLeave: false
|
||||
});
|
||||
|
||||
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() {
|
||||
if (!props.scene.rating) { return; }
|
||||
if (!props.scene.rating) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div className={`rating-banner ${props.scene.rating ? `rating-${props.scene.rating}` : '' }`}>
|
||||
<div
|
||||
className={`rating-banner ${
|
||||
props.scene.rating ? `rating-${props.scene.rating}` : ""
|
||||
}`}
|
||||
>
|
||||
RATING: {props.scene.rating}
|
||||
</div>
|
||||
);
|
||||
@@ -34,18 +47,27 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
||||
function maybeRenderSceneSpecsOverlay() {
|
||||
return (
|
||||
<div className="scene-specs-overlay">
|
||||
{props.scene.file.height ? <span className="overlay-resolution"> {TextUtils.resolution(props.scene.file.height)}</span> : ''}
|
||||
{props.scene.file.duration !== undefined && props.scene.file.duration >= 1 ? TextUtils.secondsToTimestamp(props.scene.file.duration) : ''}
|
||||
{props.scene.file.height ? (
|
||||
<span className="overlay-resolution">
|
||||
{" "}
|
||||
{TextUtils.resolution(props.scene.file.height)}
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{props.scene.file.duration !== undefined &&
|
||||
props.scene.file.duration >= 1
|
||||
? TextUtils.secondsToTimestamp(props.scene.file.duration)
|
||||
: ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderSceneStudioOverlay() {
|
||||
if (!props.scene.studio)
|
||||
return;
|
||||
if (!props.scene.studio) return;
|
||||
|
||||
let style: React.CSSProperties = {
|
||||
backgroundImage: `url('${props.scene.studio.image_path}')`,
|
||||
backgroundImage: `url('${props.scene.studio.image_path}')`
|
||||
};
|
||||
|
||||
let text = "";
|
||||
@@ -56,10 +78,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
||||
|
||||
return (
|
||||
<div className="scene-studio-overlay">
|
||||
<Link
|
||||
to={`/studios/${props.scene.studio.id}`}
|
||||
style={style}
|
||||
>
|
||||
<Link to={`/studios/${props.scene.studio.id}`} style={style}>
|
||||
{text}
|
||||
</Link>
|
||||
</div>
|
||||
@@ -67,18 +86,14 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
||||
}
|
||||
|
||||
function maybeRenderTagPopoverButton() {
|
||||
if (props.scene.tags.length <= 0)
|
||||
return;
|
||||
if (props.scene.tags.length <= 0) return;
|
||||
|
||||
const popoverContent = props.scene.tags.map((tag) => (
|
||||
const popoverContent = props.scene.tags.map(tag => (
|
||||
<TagLink key={tag.id} tag={tag} />
|
||||
));
|
||||
|
||||
return (
|
||||
<HoverPopover
|
||||
placement="bottom"
|
||||
content={popoverContent}
|
||||
>
|
||||
<HoverPopover placement="bottom" content={popoverContent}>
|
||||
<Button>
|
||||
<Icon icon="tag" />
|
||||
{props.scene.tags.length}
|
||||
@@ -88,25 +103,21 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
||||
}
|
||||
|
||||
function maybeRenderPerformerPopoverButton() {
|
||||
if (props.scene.performers.length <= 0)
|
||||
return;
|
||||
if (props.scene.performers.length <= 0) return;
|
||||
|
||||
const popoverContent = props.scene.performers.map((performer) => (
|
||||
const popoverContent = props.scene.performers.map(performer => (
|
||||
<div className="performer-tag-container">
|
||||
<Link
|
||||
to={`/performers/${performer.id}`}
|
||||
className="performer-tag previewable image"
|
||||
style={{backgroundImage: `url(${performer.image_path})`}}
|
||||
style={{ backgroundImage: `url(${performer.image_path})` }}
|
||||
/>
|
||||
<TagLink key={performer.id} performer={performer} />
|
||||
</div>
|
||||
));
|
||||
|
||||
return (
|
||||
<HoverPopover
|
||||
placement="bottom"
|
||||
content={popoverContent}
|
||||
>
|
||||
<HoverPopover placement="bottom" content={popoverContent}>
|
||||
<Button>
|
||||
<Icon icon="user" />
|
||||
{props.scene.performers.length}
|
||||
@@ -116,8 +127,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
||||
}
|
||||
|
||||
function maybeRenderSceneMarkerPopoverButton() {
|
||||
if (props.scene.scene_markers.length <= 0)
|
||||
return;
|
||||
if (props.scene.scene_markers.length <= 0) return;
|
||||
|
||||
const popoverContent = props.scene.scene_markers.map(marker => {
|
||||
const markerPopover = { ...marker, scene: { id: props.scene.id } };
|
||||
@@ -125,10 +135,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
||||
});
|
||||
|
||||
return (
|
||||
<HoverPopover
|
||||
placement="bottom"
|
||||
content={popoverContent}
|
||||
>
|
||||
<HoverPopover placement="bottom" content={popoverContent}>
|
||||
<Button>
|
||||
<Icon icon="tag" />
|
||||
{props.scene.scene_markers.length}
|
||||
@@ -138,9 +145,11 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
||||
}
|
||||
|
||||
function maybeRenderPopoverButtonGroup() {
|
||||
if (props.scene.tags.length > 0 ||
|
||||
if (
|
||||
props.scene.tags.length > 0 ||
|
||||
props.scene.performers.length > 0 ||
|
||||
props.scene.scene_markers.length > 0) {
|
||||
props.scene.scene_markers.length > 0
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
@@ -166,7 +175,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
||||
}
|
||||
|
||||
function isPortrait() {
|
||||
const {file} = props.scene;
|
||||
const { file } = props.scene;
|
||||
const width = file.width ? file.width : 0;
|
||||
const height = file.height ? file.height : 0;
|
||||
return height > width;
|
||||
@@ -191,14 +200,17 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
||||
event.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
<Link to={`/scenes/${props.scene.id}`} className={cx('image', 'previewable', {portrait: isPortrait()})}>
|
||||
<Link
|
||||
to={`/scenes/${props.scene.id}`}
|
||||
className={cx("image", "previewable", { portrait: isPortrait() })}
|
||||
>
|
||||
<div className="video-container">
|
||||
{maybeRenderRatingBanner()}
|
||||
{maybeRenderSceneSpecsOverlay()}
|
||||
{maybeRenderSceneStudioOverlay()}
|
||||
<video
|
||||
loop
|
||||
className={cx('preview', {portrait: isPortrait()})}
|
||||
className={cx("preview", { portrait: isPortrait() })}
|
||||
poster={props.scene.paths.screenshot || ""}
|
||||
ref={videoHoverHook.videoEl}
|
||||
>
|
||||
@@ -208,7 +220,9 @@ export const SceneCard: React.FC<ISceneCardProps> = (props: ISceneCardProps) =>
|
||||
</Link>
|
||||
<div className="card-section">
|
||||
<h4 className="text-truncate">
|
||||
{props.scene.title ? props.scene.title : TextUtils.fileNameFromPath(props.scene.path)}
|
||||
{props.scene.title
|
||||
? props.scene.title
|
||||
: TextUtils.fileNameFromPath(props.scene.path)}
|
||||
</h4>
|
||||
<span>{props.scene.date}</span>
|
||||
<p>{TextUtils.truncate(props.scene.details, 100, "... (continued)")}</p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Card, Spinner, Tab, Tabs } from 'react-bootstrap';
|
||||
import { Card, Spinner, Tab, Tabs } from "react-bootstrap";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useLocation, useHistory } from 'react-router-dom';
|
||||
import { useParams, useLocation, useHistory } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { GalleryViewer } from "src/components/Galleries/GalleryViewer";
|
||||
@@ -13,7 +13,7 @@ import { SceneMarkersPanel } from "./SceneMarkersPanel";
|
||||
import { ScenePerformerPanel } from "./ScenePerformerPanel";
|
||||
|
||||
export const Scene: React.FC = () => {
|
||||
const { id = 'new' } = useParams();
|
||||
const { id = "new" } = useParams();
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const [timestamp, setTimestamp] = useState<number>(getInitialTimestamp());
|
||||
@@ -21,16 +21,17 @@ export const Scene: React.FC = () => {
|
||||
const { data, error, loading } = StashService.useFindScene(id);
|
||||
|
||||
const queryParams = queryString.parse(location.search);
|
||||
const autoplay = queryParams?.autoplay === 'true';
|
||||
const autoplay = queryParams?.autoplay === "true";
|
||||
|
||||
useEffect(() => (
|
||||
setScene(data?.findScene ?? {})
|
||||
), [data]);
|
||||
useEffect(() => setScene(data?.findScene ?? {}), [data]);
|
||||
|
||||
function getInitialTimestamp() {
|
||||
const params = queryString.parse(location.search);
|
||||
const initialTimestamp = params?.t ?? '0';
|
||||
return Number.parseInt(Array.isArray(initialTimestamp) ? initialTimestamp[0] : initialTimestamp, 10);
|
||||
const initialTimestamp = params?.t ?? "0";
|
||||
return Number.parseInt(
|
||||
Array.isArray(initialTimestamp) ? initialTimestamp[0] : initialTimestamp,
|
||||
10
|
||||
);
|
||||
}
|
||||
|
||||
function onClickMarker(marker: GQL.SceneMarkerDataFragment) {
|
||||
@@ -38,51 +39,55 @@ export const Scene: React.FC = () => {
|
||||
}
|
||||
|
||||
if (!data?.findScene || loading || Object.keys(scene).length === 0) {
|
||||
return <Spinner animation="border"/>;
|
||||
return <Spinner animation="border" />;
|
||||
}
|
||||
|
||||
if (error)
|
||||
return <div>{error.message}</div>
|
||||
if (error) return <div>{error.message}</div>;
|
||||
|
||||
const modifiedScene =
|
||||
({scene_marker_tags: data.sceneMarkerTags, ...scene}) as GQL.SceneDataFragment; // TODO Hack from angular
|
||||
const modifiedScene = {
|
||||
scene_marker_tags: data.sceneMarkerTags,
|
||||
...scene
|
||||
} as GQL.SceneDataFragment; // TODO Hack from angular
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScenePlayer scene={modifiedScene} timestamp={timestamp} autoplay={autoplay}/>
|
||||
<ScenePlayer
|
||||
scene={modifiedScene}
|
||||
timestamp={timestamp}
|
||||
autoplay={autoplay}
|
||||
/>
|
||||
<Card id="details-container">
|
||||
<Tabs id="scene-tabs" mountOnEnter>
|
||||
<Tab eventKey="scene-details-panel" title="Details">
|
||||
<SceneDetailPanel scene={modifiedScene} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="scene-markers-panel"
|
||||
title="Markers">
|
||||
<SceneMarkersPanel scene={modifiedScene} onClickMarker={onClickMarker} />
|
||||
<Tab eventKey="scene-markers-panel" title="Markers">
|
||||
<SceneMarkersPanel
|
||||
scene={modifiedScene}
|
||||
onClickMarker={onClickMarker}
|
||||
/>
|
||||
</Tab>
|
||||
{modifiedScene.performers.length > 0 ?
|
||||
<Tab
|
||||
eventKey="scene-performer-panel"
|
||||
title="Performers">
|
||||
{modifiedScene.performers.length > 0 ? (
|
||||
<Tab eventKey="scene-performer-panel" title="Performers">
|
||||
<ScenePerformerPanel scene={modifiedScene} />
|
||||
</Tab> : ''
|
||||
}
|
||||
{modifiedScene.gallery ?
|
||||
<Tab
|
||||
eventKey="scene-gallery-panel"
|
||||
title="Gallery">
|
||||
</Tab>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{modifiedScene.gallery ? (
|
||||
<Tab eventKey="scene-gallery-panel" title="Gallery">
|
||||
<GalleryViewer gallery={modifiedScene.gallery} />
|
||||
</Tab> : ''
|
||||
}
|
||||
</Tab>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Tab eventKey="scene-file-info-panel" title="File Info">
|
||||
<SceneFileInfoPanel scene={modifiedScene} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="scene-edit-panel"
|
||||
title="Edit">
|
||||
<Tab eventKey="scene-edit-panel" title="Edit">
|
||||
<SceneEditPanel
|
||||
scene={modifiedScene}
|
||||
onUpdate={(newScene) => setScene(newScene)}
|
||||
onUpdate={newScene => setScene(newScene)}
|
||||
onDelete={() => history.push("/scenes")}
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
@@ -8,9 +8,11 @@ interface ISceneDetailProps {
|
||||
scene: GQL.SceneDataFragment;
|
||||
}
|
||||
|
||||
export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
|
||||
export const SceneDetailPanel: React.FC<ISceneDetailProps> = props => {
|
||||
function renderDetails() {
|
||||
if (!props.scene.details || props.scene.details === "") { return; }
|
||||
if (!props.scene.details || props.scene.details === "") {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<h6>Details</h6>
|
||||
@@ -20,8 +22,10 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
|
||||
}
|
||||
|
||||
function renderTags() {
|
||||
if (props.scene.tags.length === 0) { return; }
|
||||
const tags = props.scene.tags.map((tag) => (
|
||||
if (props.scene.tags.length === 0) {
|
||||
return;
|
||||
}
|
||||
const tags = props.scene.tags.map(tag => (
|
||||
<TagLink key={tag.id} tag={tag} />
|
||||
));
|
||||
return (
|
||||
@@ -36,11 +40,17 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
|
||||
<>
|
||||
{SceneHelpers.maybeRenderStudio(props.scene, 70)}
|
||||
<h1>
|
||||
{props.scene.title ? props.scene.title : TextUtils.fileNameFromPath(props.scene.path)}
|
||||
{props.scene.title
|
||||
? props.scene.title
|
||||
: TextUtils.fileNameFromPath(props.scene.path)}
|
||||
</h1>
|
||||
{props.scene.date ? <h4>{props.scene.date}</h4> : ''}
|
||||
{props.scene.rating ? <h6>Rating: {props.scene.rating}</h6> : ''}
|
||||
{props.scene.file.height ? <h6>Resolution: {TextUtils.resolution(props.scene.file.height)}</h6> : ''}
|
||||
{props.scene.date ? <h4>{props.scene.date}</h4> : ""}
|
||||
{props.scene.rating ? <h6>Rating: {props.scene.rating}</h6> : ""}
|
||||
{props.scene.file.height ? (
|
||||
<h6>Resolution: {TextUtils.resolution(props.scene.file.height)}</h6>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{renderDetails()}
|
||||
{renderTags()}
|
||||
</>
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
/* eslint-disable react/no-this-in-sfc */
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Collapse, Dropdown, DropdownButton, Form, Button, Spinner } from 'react-bootstrap';
|
||||
import {
|
||||
Collapse,
|
||||
Dropdown,
|
||||
DropdownButton,
|
||||
Form,
|
||||
Button,
|
||||
Spinner
|
||||
} from "react-bootstrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { FilterSelect, StudioSelect, SceneGallerySelect, Modal, Icon } from "src/components/Shared";
|
||||
import { useToast } from 'src/hooks';
|
||||
import { ImageUtils } from 'src/utils';
|
||||
import {
|
||||
FilterSelect,
|
||||
StudioSelect,
|
||||
SceneGallerySelect,
|
||||
Modal,
|
||||
Icon
|
||||
} from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { ImageUtils } from "src/utils";
|
||||
|
||||
interface IProps {
|
||||
scene: GQL.SceneDataFragment;
|
||||
@@ -23,19 +36,25 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
const [rating, setRating] = useState<number | undefined>(undefined);
|
||||
const [galleryId, setGalleryId] = 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
|
||||
);
|
||||
const [tagIds, setTagIds] = useState<string[] | undefined>(undefined);
|
||||
const [coverImage, setCoverImage] = useState<string | undefined>(undefined);
|
||||
|
||||
const Scrapers = StashService.useListSceneScrapers();
|
||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.ListSceneScrapersListSceneScrapers[]>([]);
|
||||
const [queryableScrapers, setQueryableScrapers] = useState<
|
||||
GQL.ListSceneScrapersListSceneScrapers[]
|
||||
>([]);
|
||||
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
const [deleteFile, setDeleteFile] = useState<boolean>(false);
|
||||
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
|
||||
|
||||
const [isCoverImageOpen, setIsCoverImageOpen] = useState<boolean>(false);
|
||||
const [coverImagePreview, setCoverImagePreview] = useState<string | undefined>(undefined);
|
||||
const [coverImagePreview, setCoverImagePreview] = useState<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -44,21 +63,24 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
const deleteScene = StashService.useSceneDestroy(getSceneDeleteInput());
|
||||
|
||||
useEffect(() => {
|
||||
let newQueryableScrapers : GQL.ListSceneScrapersListSceneScrapers[] = [];
|
||||
let newQueryableScrapers: GQL.ListSceneScrapersListSceneScrapers[] = [];
|
||||
|
||||
if (!!Scrapers.data && Scrapers.data.listSceneScrapers) {
|
||||
newQueryableScrapers = Scrapers.data.listSceneScrapers.filter((s) => {
|
||||
return s.scene && s.scene.supported_scrapes.includes(GQL.ScrapeType.Fragment);
|
||||
newQueryableScrapers = Scrapers.data.listSceneScrapers.filter(s => {
|
||||
return (
|
||||
s.scene && s.scene.supported_scrapes.includes(GQL.ScrapeType.Fragment)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
setQueryableScrapers(newQueryableScrapers);
|
||||
|
||||
}, [Scrapers.data])
|
||||
}, [Scrapers.data]);
|
||||
|
||||
function updateSceneEditState(state: Partial<GQL.SceneDataFragment>) {
|
||||
const perfIds = state.performers ? state.performers.map((performer) => performer.id) : undefined;
|
||||
const tIds = state.tags ? state.tags.map((tag) => tag.id) : undefined;
|
||||
const perfIds = state.performers
|
||||
? state.performers.map(performer => performer.id)
|
||||
: undefined;
|
||||
const tIds = state.tags ? state.tags.map(tag => tag.id) : undefined;
|
||||
|
||||
setTitle(state.title);
|
||||
setDetails(state.details);
|
||||
@@ -90,7 +112,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
studio_id: studioId,
|
||||
performer_ids: performerIds,
|
||||
tag_ids: tagIds,
|
||||
cover_image: coverImage,
|
||||
cover_image: coverImage
|
||||
};
|
||||
}
|
||||
|
||||
@@ -128,16 +150,23 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
props.onDelete();
|
||||
}
|
||||
|
||||
function renderMultiSelect(type: "performers" | "tags", initialIds: string[] = []) {
|
||||
function renderMultiSelect(
|
||||
type: "performers" | "tags",
|
||||
initialIds: string[] = []
|
||||
) {
|
||||
return (
|
||||
<FilterSelect
|
||||
type={type}
|
||||
isMulti
|
||||
onSelect={(items) => {
|
||||
const ids = items.map((i) => i.id);
|
||||
onSelect={items => {
|
||||
const ids = items.map(i => i.id);
|
||||
switch (type) {
|
||||
case "performers": setPerformerIds(ids); break;
|
||||
case "tags": setTagIds(ids); break;
|
||||
case "performers":
|
||||
setPerformerIds(ids);
|
||||
break;
|
||||
case "tags":
|
||||
setTagIds(ids);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
initialIds={initialIds}
|
||||
@@ -151,15 +180,24 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
show={isDeleteAlertOpen}
|
||||
icon="trash-alt"
|
||||
header="Delete Scene?"
|
||||
accept={{ variant: 'danger', onClick: onDelete, text: "Delete" }}
|
||||
accept={{ variant: "danger", onClick: onDelete, text: "Delete" }}
|
||||
cancel={{ onClick: () => setIsDeleteAlertOpen(false), text: "Cancel" }}
|
||||
>
|
||||
<p>
|
||||
Are you sure you want to delete this scene? Unless the file is also deleted, this scene will be re-added when scan is performed.
|
||||
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.Check
|
||||
checked={deleteFile}
|
||||
label="Delete file"
|
||||
onChange={() => setDeleteFile(!deleteFile)}
|
||||
/>
|
||||
<Form.Check
|
||||
checked={deleteGenerated}
|
||||
label="Delete generated supporting files"
|
||||
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
@@ -174,11 +212,18 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
ImageUtils.onImageChange(event, onImageLoad);
|
||||
}
|
||||
|
||||
async function onScrapeClicked(scraper : GQL.ListSceneScrapersListSceneScrapers) {
|
||||
async function onScrapeClicked(
|
||||
scraper: GQL.ListSceneScrapersListSceneScrapers
|
||||
) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await StashService.queryScrapeScene(scraper.id, getSceneInput());
|
||||
if (!result.data || !result.data.scrapeScene) { return; }
|
||||
const result = await StashService.queryScrapeScene(
|
||||
scraper.id,
|
||||
getSceneInput()
|
||||
);
|
||||
if (!result.data || !result.data.scrapeScene) {
|
||||
return;
|
||||
}
|
||||
updateSceneFromScrapedScene(result.data.scrapeScene);
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
@@ -194,21 +239,22 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
|
||||
return (
|
||||
<DropdownButton id="scene-scrape" title="Scrape with...">
|
||||
{ queryableScrapers.map(s => (
|
||||
<Dropdown.Item onClick={() => onScrapeClicked(s)}>{s.name}</Dropdown.Item>
|
||||
))
|
||||
}
|
||||
{queryableScrapers.map(s => (
|
||||
<Dropdown.Item onClick={() => onScrapeClicked(s)}>
|
||||
{s.name}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</DropdownButton>
|
||||
);
|
||||
}
|
||||
|
||||
function urlScrapable(scrapedUrl: string) : boolean {
|
||||
return (Scrapers?.data?.listSceneScrapers ?? []).some(s => (
|
||||
function urlScrapable(scrapedUrl: string): boolean {
|
||||
return (Scrapers?.data?.listSceneScrapers ?? []).some(s =>
|
||||
(s?.scene?.urls ?? []).some(u => scrapedUrl.includes(u))
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
function updateSceneFromScrapedScene(scene : GQL.ScrapedSceneDataFragment) {
|
||||
function updateSceneFromScrapedScene(scene: GQL.ScrapedSceneDataFragment) {
|
||||
if (!title && scene.title) {
|
||||
setTitle(scene.title);
|
||||
}
|
||||
@@ -229,35 +275,47 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
setStudioId(scene.studio.id);
|
||||
}
|
||||
|
||||
if ((!performerIds || performerIds.length === 0) && scene.performers && scene.performers.length > 0) {
|
||||
const idPerfs = scene.performers.filter((p) => {
|
||||
if (
|
||||
(!performerIds || performerIds.length === 0) &&
|
||||
scene.performers &&
|
||||
scene.performers.length > 0
|
||||
) {
|
||||
const idPerfs = scene.performers.filter(p => {
|
||||
return p.id !== undefined && p.id !== null;
|
||||
});
|
||||
|
||||
if (idPerfs.length > 0) {
|
||||
const newIds = idPerfs.map((p) => p.id);
|
||||
const newIds = idPerfs.map(p => p.id);
|
||||
setPerformerIds(newIds as string[]);
|
||||
}
|
||||
}
|
||||
|
||||
if ((!tagIds || tagIds.length === 0) && scene.tags && scene.tags.length > 0) {
|
||||
const idTags = scene.tags.filter((p) => {
|
||||
if (
|
||||
(!tagIds || tagIds.length === 0) &&
|
||||
scene.tags &&
|
||||
scene.tags.length > 0
|
||||
) {
|
||||
const idTags = scene.tags.filter(p => {
|
||||
return p.id !== undefined && p.id !== null;
|
||||
});
|
||||
|
||||
if (idTags.length > 0) {
|
||||
const newIds = idTags.map((p) => p.id);
|
||||
const newIds = idTags.map(p => p.id);
|
||||
setTagIds(newIds as string[]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function onScrapeSceneURL() {
|
||||
if (!url) { return; }
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await StashService.queryScrapeSceneURL(url);
|
||||
if (!result.data || !result.data.scrapeSceneURL) { return; }
|
||||
if (!result.data || !result.data.scrapeSceneURL) {
|
||||
return;
|
||||
}
|
||||
updateSceneFromScrapedScene(result.data.scrapeSceneURL);
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
@@ -271,19 +329,17 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
id="scrape-url-button"
|
||||
onClick={onScrapeSceneURL}>
|
||||
<Button id="scrape-url-button" onClick={onScrapeSceneURL}>
|
||||
<Icon icon="file-download" />
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderDeleteAlert()}
|
||||
{isLoading ? <Spinner animation="border" variant="light" /> : undefined}
|
||||
<div className="form-container " style={{width: "50%"}}>
|
||||
<div className="form-container " style={{ width: "50%" }}>
|
||||
<Form.Group controlId="title">
|
||||
<Form.Label>Title</Form.Label>
|
||||
<Form.Control
|
||||
@@ -323,10 +379,15 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
<Form.Label>Rating</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
onChange={(event: any) => setRating(parseInt(event.target.value, 10))}>
|
||||
{ ["", 1, 2, 3, 4, 5].map(opt => (
|
||||
<option selected={opt === rating} value={opt}>{opt}</option>
|
||||
)) }
|
||||
onChange={(event: any) =>
|
||||
setRating(parseInt(event.target.value, 10))
|
||||
}
|
||||
>
|
||||
{["", 1, 2, 3, 4, 5].map(opt => (
|
||||
<option selected={opt === rating} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
|
||||
@@ -335,14 +396,14 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
<SceneGallerySelect
|
||||
sceneId={props.scene.id}
|
||||
initialId={galleryId}
|
||||
onSelect={(item) => setGalleryId(item ? item.id : undefined)}
|
||||
onSelect={item => setGalleryId(item ? item.id : undefined)}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="studio">
|
||||
<Form.Label>Studio</Form.Label>
|
||||
<StudioSelect
|
||||
onSelect={(items) => items.length && setStudioId(items[0]?.id)}
|
||||
onSelect={items => items.length && setStudioId(items[0]?.id)}
|
||||
initialIds={studioId ? [studioId] : []}
|
||||
/>
|
||||
</Form.Group>
|
||||
@@ -358,7 +419,10 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
</Form.Group>
|
||||
|
||||
<div>
|
||||
<Button variant="link" onClick={() => setIsCoverImageOpen(!isCoverImageOpen)}>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => setIsCoverImageOpen(!isCoverImageOpen)}
|
||||
>
|
||||
<Icon icon={isCoverImageOpen ? "chevron-down" : "chevron-right"} />
|
||||
<span>Cover Image</span>
|
||||
</Button>
|
||||
@@ -366,15 +430,26 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
<div>
|
||||
<img className="scene-cover" src={coverImagePreview} alt="" />
|
||||
<Form.Group className="test" controlId="cover">
|
||||
<Form.Control type="file" onChange={onCoverImageChange} accept=".jpg,.jpeg,.png" />
|
||||
<Form.Control
|
||||
type="file"
|
||||
onChange={onCoverImageChange}
|
||||
accept=".jpg,.jpeg,.png"
|
||||
/>
|
||||
</Form.Group>
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<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="primary" onClick={onSave}>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
className="edit-button"
|
||||
variant="danger"
|
||||
onClick={() => setIsDeleteAlertOpen(true)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
{renderScraperMenu()}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Table } from 'react-bootstrap';
|
||||
import { Table } from "react-bootstrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { TextUtils } from "src/utils";
|
||||
|
||||
@@ -7,7 +7,9 @@ interface ISceneFileInfoPanelProps {
|
||||
scene: GQL.SceneDataFragment;
|
||||
}
|
||||
|
||||
export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (props: ISceneFileInfoPanelProps) => {
|
||||
export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
||||
props: ISceneFileInfoPanelProps
|
||||
) => {
|
||||
function renderChecksum() {
|
||||
return (
|
||||
<tr>
|
||||
@@ -18,11 +20,15 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (props: IS
|
||||
}
|
||||
|
||||
function renderPath() {
|
||||
const { scene: { path } } = props;
|
||||
const {
|
||||
scene: { path }
|
||||
} = props;
|
||||
return (
|
||||
<tr>
|
||||
<td>Path</td>
|
||||
<td><a href={`file://${path}`}>{`file://${props.scene.path}`}</a> </td>
|
||||
<td>
|
||||
<a href={`file://${path}`}>{`file://${props.scene.path}`}</a>{" "}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
@@ -31,13 +37,17 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (props: IS
|
||||
return (
|
||||
<tr>
|
||||
<td>Stream</td>
|
||||
<td><a href={props.scene.paths.stream}>{props.scene.paths.stream}</a> </td>
|
||||
<td>
|
||||
<a href={props.scene.paths.stream}>{props.scene.paths.stream}</a>{" "}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function renderFileSize() {
|
||||
if (props.scene.file.size === undefined) { return; }
|
||||
if (props.scene.file.size === undefined) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<tr>
|
||||
<td>File Size</td>
|
||||
@@ -47,7 +57,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (props: IS
|
||||
}
|
||||
|
||||
function renderDuration() {
|
||||
if (props.scene.file.duration === undefined) { return; }
|
||||
if (props.scene.file.duration === undefined) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<tr>
|
||||
<td>Duration</td>
|
||||
@@ -57,17 +69,23 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (props: IS
|
||||
}
|
||||
|
||||
function renderDimensions() {
|
||||
if (props.scene.file.duration === undefined) { return; }
|
||||
if (props.scene.file.duration === undefined) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<tr>
|
||||
<td>Dimensions</td>
|
||||
<td>{props.scene.file.width} x {props.scene.file.height}</td>
|
||||
<td>
|
||||
{props.scene.file.width} x {props.scene.file.height}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function renderFrameRate() {
|
||||
if (props.scene.file.framerate === undefined) { return; }
|
||||
if (props.scene.file.framerate === undefined) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<tr>
|
||||
<td>Frame Rate</td>
|
||||
@@ -77,7 +95,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (props: IS
|
||||
}
|
||||
|
||||
function renderBitRate() {
|
||||
if (props.scene.file.bitrate === undefined) { return; }
|
||||
if (props.scene.file.bitrate === undefined) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<tr>
|
||||
<td>Bit Rate</td>
|
||||
@@ -87,7 +107,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (props: IS
|
||||
}
|
||||
|
||||
function renderVideoCodec() {
|
||||
if (props.scene.file.video_codec === undefined) { return; }
|
||||
if (props.scene.file.video_codec === undefined) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<tr>
|
||||
<td>Video Codec</td>
|
||||
@@ -97,7 +119,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (props: IS
|
||||
}
|
||||
|
||||
function renderAudioCodec() {
|
||||
if (props.scene.file.audio_codec === undefined) { return; }
|
||||
if (props.scene.file.audio_codec === undefined) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<tr>
|
||||
<td>Audio Codec</td>
|
||||
@@ -107,7 +131,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (props: IS
|
||||
}
|
||||
|
||||
function renderUrl() {
|
||||
if (!props.scene.url || props.scene.url === "") { return; }
|
||||
if (!props.scene.url || props.scene.url === "") {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<tr>
|
||||
<td>Downloaded From</td>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
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 } from "formik";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { useToast } from 'src/hooks';
|
||||
import { DurationInput, TagSelect, MarkerTitleSuggest } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import {
|
||||
DurationInput,
|
||||
TagSelect,
|
||||
MarkerTitleSuggest
|
||||
} from "src/components/Shared";
|
||||
import { WallPanel } from "src/components/Wall/WallPanel";
|
||||
import { SceneHelpers } from "../helpers";
|
||||
|
||||
@@ -21,10 +31,15 @@ interface IFormFields {
|
||||
tagIds: string[];
|
||||
}
|
||||
|
||||
export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISceneMarkersPanelProps) => {
|
||||
export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
|
||||
props: ISceneMarkersPanelProps
|
||||
) => {
|
||||
const Toast = useToast();
|
||||
const [isEditorOpen, setIsEditorOpen] = useState<boolean>(false);
|
||||
const [editingMarker, setEditingMarker] = useState<GQL.SceneMarkerDataFragment | null>(null);
|
||||
const [
|
||||
editingMarker,
|
||||
setEditingMarker
|
||||
] = useState<GQL.SceneMarkerDataFragment | null>(null);
|
||||
|
||||
const sceneMarkerCreate = StashService.useSceneMarkerCreate();
|
||||
const sceneMarkerUpdate = StashService.useSceneMarkerUpdate();
|
||||
@@ -43,24 +58,34 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISce
|
||||
|
||||
function renderTags() {
|
||||
function renderMarkers(primaryTag: GQL.FindSceneSceneMarkerTags) {
|
||||
const markers = primaryTag.scene_markers.map((marker) => {
|
||||
const markerTags = marker.tags.map((tag) => (
|
||||
<Badge key={tag.id} variant="secondary" className="tag-item">{tag.name}</Badge>
|
||||
const markers = primaryTag.scene_markers.map(marker => {
|
||||
const markerTags = marker.tags.map(tag => (
|
||||
<Badge key={tag.id} variant="secondary" className="tag-item">
|
||||
{tag.name}
|
||||
</Badge>
|
||||
));
|
||||
|
||||
return (
|
||||
<div key={marker.id}>
|
||||
<hr />
|
||||
<div>
|
||||
<Button variant="link" onClick={() => onClickMarker(marker)}>{marker.title}</Button>
|
||||
{!isEditorOpen ? <Button variant="link" style={{float: "right"}} onClick={() => onOpenEditor(marker)}>Edit</Button> : ''}
|
||||
</div>
|
||||
<div>
|
||||
{TextUtils.secondsToTimestamp(marker.seconds)}
|
||||
</div>
|
||||
<div className="card-section centered">
|
||||
{markerTags}
|
||||
<Button variant="link" onClick={() => onClickMarker(marker)}>
|
||||
{marker.title}
|
||||
</Button>
|
||||
{!isEditorOpen ? (
|
||||
<Button
|
||||
variant="link"
|
||||
style={{ float: "right" }}
|
||||
onClick={() => onOpenEditor(marker)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
<div>{TextUtils.secondsToTimestamp(marker.seconds)}</div>
|
||||
<div className="card-section centered">{markerTags}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -74,51 +99,60 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISce
|
||||
display: "inline-block",
|
||||
margin: "5px",
|
||||
width: "300px",
|
||||
flex: "0 0 auto",
|
||||
flex: "0 0 auto"
|
||||
};
|
||||
const tags = (props.scene as any).scene_marker_tags.map((primaryTag: GQL.FindSceneSceneMarkerTags) => {
|
||||
|
||||
const tags = (props.scene as any).scene_marker_tags.map(
|
||||
(primaryTag: GQL.FindSceneSceneMarkerTags) => {
|
||||
return (
|
||||
<div key={primaryTag.tag.id} style={{padding: "1px"}}>
|
||||
<div key={primaryTag.tag.id} style={{ padding: "1px" }}>
|
||||
<Card style={style}>
|
||||
<div className="content" style={{whiteSpace: "normal"}}>
|
||||
<div className="content" style={{ whiteSpace: "normal" }}>
|
||||
<h3>{primaryTag.tag.name}</h3>
|
||||
{renderMarkers(primaryTag)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
return tags;
|
||||
}
|
||||
|
||||
function renderForm() {
|
||||
function onSubmit(values: IFormFields) {
|
||||
const isEditing = !!editingMarker;
|
||||
const variables: GQL.SceneMarkerCreateVariables | GQL.SceneMarkerUpdateVariables = {
|
||||
const variables:
|
||||
| GQL.SceneMarkerCreateVariables
|
||||
| GQL.SceneMarkerUpdateVariables = {
|
||||
title: values.title,
|
||||
seconds: parseFloat(values.seconds),
|
||||
scene_id: props.scene.id,
|
||||
primary_tag_id: values.primaryTagId,
|
||||
tag_ids: values.tagIds,
|
||||
tag_ids: values.tagIds
|
||||
};
|
||||
if (!isEditing) {
|
||||
sceneMarkerCreate({ variables }).then(() => {
|
||||
sceneMarkerCreate({ variables })
|
||||
.then(() => {
|
||||
setIsEditorOpen(false);
|
||||
setEditingMarker(null);
|
||||
}).catch((err) => Toast.error(err));
|
||||
})
|
||||
.catch(err => Toast.error(err));
|
||||
} else {
|
||||
const updateVariables = variables as GQL.SceneMarkerUpdateVariables;
|
||||
updateVariables.id = editingMarker!.id;
|
||||
sceneMarkerUpdate({ variables: updateVariables }).then(() => {
|
||||
sceneMarkerUpdate({ variables: updateVariables })
|
||||
.then(() => {
|
||||
setIsEditorOpen(false);
|
||||
setEditingMarker(null);
|
||||
}).catch((err) => Toast.error(err));
|
||||
})
|
||||
.catch(err => Toast.error(err));
|
||||
}
|
||||
}
|
||||
function onDelete() {
|
||||
if (!editingMarker) { return; }
|
||||
sceneMarkerDestroy({variables: {id: editingMarker.id}})
|
||||
if (!editingMarker) {
|
||||
return;
|
||||
}
|
||||
sceneMarkerDestroy({ variables: { id: editingMarker.id } })
|
||||
// eslint-disable-next-line no-console
|
||||
.catch(err => console.error(err));
|
||||
setIsEditorOpen(false);
|
||||
@@ -128,15 +162,22 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISce
|
||||
return (
|
||||
<MarkerTitleSuggest
|
||||
initialMarkerTitle={editingMarker?.title}
|
||||
onChange={(query:string) => fieldProps.form.setFieldValue("title", query)}
|
||||
onChange={(query: string) =>
|
||||
fieldProps.form.setFieldValue("title", query)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
function renderSecondsField(fieldProps: FieldProps<IFormFields>) {
|
||||
return (
|
||||
<DurationInput
|
||||
onValueChange={(s) => fieldProps.form.setFieldValue("seconds", s)}
|
||||
onReset={() => fieldProps.form.setFieldValue("seconds", Math.round(jwplayer.getPosition()))}
|
||||
onValueChange={s => fieldProps.form.setFieldValue("seconds", s)}
|
||||
onReset={() =>
|
||||
fieldProps.form.setFieldValue(
|
||||
"seconds",
|
||||
Math.round(jwplayer.getPosition())
|
||||
)
|
||||
}
|
||||
numericValue={fieldProps.field.value}
|
||||
/>
|
||||
);
|
||||
@@ -144,7 +185,9 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISce
|
||||
function renderPrimaryTagField(fieldProps: FieldProps<IFormFields>) {
|
||||
return (
|
||||
<TagSelect
|
||||
onSelect={(tags) => fieldProps.form.setFieldValue("primaryTagId", tags[0]?.id)}
|
||||
onSelect={tags =>
|
||||
fieldProps.form.setFieldValue("primaryTagId", tags[0]?.id)
|
||||
}
|
||||
initialIds={editingMarker ? [editingMarker.primary_tag.id] : []}
|
||||
/>
|
||||
);
|
||||
@@ -153,7 +196,12 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISce
|
||||
return (
|
||||
<TagSelect
|
||||
isMulti
|
||||
onSelect={(tags) => fieldProps.form.setFieldValue("tagIds", tags.map((tag) => tag.id))}
|
||||
onSelect={tags =>
|
||||
fieldProps.form.setFieldValue(
|
||||
"tagIds",
|
||||
tags.map(tag => tag.id)
|
||||
)
|
||||
}
|
||||
initialIds={editingMarker ? fieldProps.form.values.tagIds : []}
|
||||
/>
|
||||
);
|
||||
@@ -164,7 +212,7 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISce
|
||||
deleteButton = (
|
||||
<Button
|
||||
variant="danger"
|
||||
style={{float: "right", marginRight: "10px"}}
|
||||
style={{ float: "right", marginRight: "10px" }}
|
||||
onClick={() => onDelete()}
|
||||
>
|
||||
Delete
|
||||
@@ -172,10 +220,12 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISce
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form style={{marginTop: "10px"}}>
|
||||
<Form style={{ marginTop: "10px" }}>
|
||||
<div className="columns is-multiline is-gapless">
|
||||
<BootstrapForm.Group>
|
||||
<BootstrapForm.Label htmlFor="title">Scene Marker Title</BootstrapForm.Label>
|
||||
<BootstrapForm.Label htmlFor="title">
|
||||
Scene Marker Title
|
||||
</BootstrapForm.Label>
|
||||
<Field name="title" render={renderTitleField} />
|
||||
</BootstrapForm.Group>
|
||||
<BootstrapForm.Group>
|
||||
@@ -183,7 +233,9 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISce
|
||||
<Field name="seconds" render={renderSecondsField} />
|
||||
</BootstrapForm.Group>
|
||||
<BootstrapForm.Group>
|
||||
<BootstrapForm.Label htmlFor="primaryTagId">Primary Tag</BootstrapForm.Label>
|
||||
<BootstrapForm.Label htmlFor="primaryTagId">
|
||||
Primary Tag
|
||||
</BootstrapForm.Label>
|
||||
<Field name="primaryTagId" render={renderPrimaryTagField} />
|
||||
</BootstrapForm.Group>
|
||||
<BootstrapForm.Group>
|
||||
@@ -192,8 +244,12 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISce
|
||||
</BootstrapForm.Group>
|
||||
</div>
|
||||
<div className="buttons-container">
|
||||
<Button variant="primary" type="submit">Submit</Button>
|
||||
<Button type="button" onClick={() => setIsEditorOpen(false)}>Cancel</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
<Button type="button" onClick={() => setIsEditorOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
{deleteButton}
|
||||
</div>
|
||||
</Form>
|
||||
@@ -205,10 +261,15 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISce
|
||||
title: editingMarker.title,
|
||||
seconds: editingMarker.seconds,
|
||||
primaryTagId: editingMarker.primary_tag.id,
|
||||
tagIds: editingMarker.tags.map((tag) => tag.id),
|
||||
tagIds: editingMarker.tags.map(tag => tag.id)
|
||||
};
|
||||
} else {
|
||||
initialValues = {title: "", seconds: Math.round(jwplayer.getPosition()), primaryTagId: "", tagIds: []};
|
||||
initialValues = {
|
||||
title: "",
|
||||
seconds: Math.round(jwplayer.getPosition()),
|
||||
primaryTagId: "",
|
||||
tagIds: []
|
||||
};
|
||||
}
|
||||
return (
|
||||
<Collapse in={isEditorOpen}>
|
||||
@@ -225,7 +286,7 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISce
|
||||
|
||||
function render() {
|
||||
const newMarkerForm = (
|
||||
<div style={{margin: "5px"}}>
|
||||
<div style={{ margin: "5px" }}>
|
||||
<Button onClick={() => onOpenEditor()}>Create</Button>
|
||||
{renderForm()}
|
||||
</div>
|
||||
@@ -240,17 +301,18 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISce
|
||||
whiteSpace: "nowrap",
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
marginBottom: "20px",
|
||||
marginBottom: "20px"
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{newMarkerForm}
|
||||
<div style={containerStyle}>
|
||||
{renderTags()}
|
||||
</div>
|
||||
<div style={containerStyle}>{renderTags()}</div>
|
||||
<WallPanel
|
||||
sceneMarkers={props.scene.scene_markers}
|
||||
clickHandler={(marker) => { window.scrollTo(0, 0); onClickMarker(marker as any); }}
|
||||
clickHandler={marker => {
|
||||
window.scrollTo(0, 0);
|
||||
onClickMarker(marker as any);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,16 +6,20 @@ interface IScenePerformerPanelProps {
|
||||
scene: GQL.SceneDataFragment;
|
||||
}
|
||||
|
||||
export const ScenePerformerPanel: FunctionComponent<IScenePerformerPanelProps> = (props: IScenePerformerPanelProps) => {
|
||||
const cards = props.scene.performers.map((performer) => (
|
||||
<PerformerCard key={performer.id} performer={performer} ageFromDate={props.scene.date} />
|
||||
export const ScenePerformerPanel: FunctionComponent<IScenePerformerPanelProps> = (
|
||||
props: IScenePerformerPanelProps
|
||||
) => {
|
||||
const cards = props.scene.performers.map(performer => (
|
||||
<PerformerCard
|
||||
key={performer.id}
|
||||
performer={performer}
|
||||
ageFromDate={props.scene.date}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid">
|
||||
{cards}
|
||||
</div>
|
||||
<div className="grid">{cards}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
/* eslint-disable no-param-reassign, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
|
||||
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import { Badge, Button, Card, Collapse, Dropdown, DropdownButton, Form, Table, Spinner } from 'react-bootstrap';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Collapse,
|
||||
Dropdown,
|
||||
DropdownButton,
|
||||
Form,
|
||||
Table,
|
||||
Spinner
|
||||
} from "react-bootstrap";
|
||||
import _ from "lodash";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
@@ -15,12 +25,12 @@ class ParserResult<T> {
|
||||
public originalValue: GQL.Maybe<T>;
|
||||
public set: boolean = false;
|
||||
|
||||
public setOriginalValue(v : GQL.Maybe<T>) {
|
||||
public setOriginalValue(v: GQL.Maybe<T>) {
|
||||
this.originalValue = v;
|
||||
this.value = v;
|
||||
}
|
||||
|
||||
public setValue(v : GQL.Maybe<T>) {
|
||||
public setValue(v: GQL.Maybe<T>) {
|
||||
if (v) {
|
||||
this.value = v;
|
||||
this.set = !_.isEqual(this.value, this.originalValue);
|
||||
@@ -29,8 +39,8 @@ class ParserResult<T> {
|
||||
}
|
||||
|
||||
class ParserField {
|
||||
public field : string;
|
||||
public helperText? : string;
|
||||
public field: string;
|
||||
public helperText?: string;
|
||||
|
||||
constructor(field: string, helperText?: string) {
|
||||
this.field = field;
|
||||
@@ -38,7 +48,7 @@ class ParserField {
|
||||
}
|
||||
|
||||
public getFieldPattern() {
|
||||
return `{${ this.field }}`;
|
||||
return `{${this.field}}`;
|
||||
}
|
||||
|
||||
static Title = new ParserField("title");
|
||||
@@ -83,7 +93,7 @@ class ParserField {
|
||||
ParserField.DDMMYY,
|
||||
ParserField.MMDDYYYY,
|
||||
ParserField.MMDDYY
|
||||
]
|
||||
];
|
||||
|
||||
static fullDateFields = [
|
||||
ParserField.YYYYMMDD,
|
||||
@@ -104,23 +114,27 @@ class SceneParserResult {
|
||||
public studioId: ParserResult<string> = new ParserResult();
|
||||
public tags: ParserResult<GQL.SlimSceneDataTags[]> = new ParserResult();
|
||||
public tagIds: ParserResult<string[]> = new ParserResult();
|
||||
public performers: ParserResult<GQL.SlimSceneDataPerformers[]> = new ParserResult();
|
||||
public performers: ParserResult<
|
||||
GQL.SlimSceneDataPerformers[]
|
||||
> = new ParserResult();
|
||||
public performerIds: ParserResult<string[]> = new ParserResult();
|
||||
|
||||
public scene : GQL.SlimSceneDataFragment;
|
||||
public scene: GQL.SlimSceneDataFragment;
|
||||
|
||||
constructor(result : GQL.ParseSceneFilenamesResults) {
|
||||
constructor(result: GQL.ParseSceneFilenamesResults) {
|
||||
this.scene = result.scene;
|
||||
|
||||
this.id = this.scene.id;
|
||||
this.filename = TextUtils.fileNameFromPath(this.scene.path);
|
||||
this.title.setOriginalValue(this.scene.title);
|
||||
this.date.setOriginalValue(this.scene.date);
|
||||
this.performerIds.setOriginalValue(this.scene.performers.map((p) => p.id));
|
||||
this.performerIds.setOriginalValue(this.scene.performers.map(p => p.id));
|
||||
this.performers.setOriginalValue(this.scene.performers);
|
||||
this.tagIds.setOriginalValue(this.scene.tags.map((t) => t.id));
|
||||
this.tagIds.setOriginalValue(this.scene.tags.map(t => t.id));
|
||||
this.tags.setOriginalValue(this.scene.tags);
|
||||
this.studioId.setOriginalValue(this.scene.studio ? this.scene.studio.id : undefined);
|
||||
this.studioId.setOriginalValue(
|
||||
this.scene.studio ? this.scene.studio.id : undefined
|
||||
);
|
||||
this.studio.setOriginalValue(this.scene.studio);
|
||||
|
||||
this.title.setValue(result.title);
|
||||
@@ -130,23 +144,27 @@ class SceneParserResult {
|
||||
this.studioId.setValue(result.studio_id);
|
||||
|
||||
if (result.performer_ids) {
|
||||
this.performers.setValue(result.performer_ids.map((p) => {
|
||||
this.performers.setValue(
|
||||
result.performer_ids.map(p => {
|
||||
return {
|
||||
id: p,
|
||||
name: "",
|
||||
favorite: false,
|
||||
image_path: ""
|
||||
};
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (result.tag_ids) {
|
||||
this.tags.setValue(result.tag_ids.map((t) => {
|
||||
this.tags.setValue(
|
||||
result.tag_ids.map(t => {
|
||||
return {
|
||||
id: t,
|
||||
name: "",
|
||||
name: ""
|
||||
};
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (result.studio_id) {
|
||||
@@ -158,7 +176,11 @@ class SceneParserResult {
|
||||
}
|
||||
}
|
||||
|
||||
private static setInput(obj: any, key: string, parserResult : ParserResult<any>) {
|
||||
private static setInput(
|
||||
obj: any,
|
||||
key: string,
|
||||
parserResult: ParserResult<any>
|
||||
) {
|
||||
if (parserResult.set) {
|
||||
obj[key] = parserResult.value;
|
||||
}
|
||||
@@ -166,7 +188,13 @@ class SceneParserResult {
|
||||
|
||||
// returns true if any of its fields have set == true
|
||||
public isChanged() {
|
||||
return this.title.set || this.date.set || this.performerIds.set || this.studioId.set || this.tagIds.set;
|
||||
return (
|
||||
this.title.set ||
|
||||
this.date.set ||
|
||||
this.performerIds.set ||
|
||||
this.studioId.set ||
|
||||
this.tagIds.set
|
||||
);
|
||||
}
|
||||
|
||||
public toSceneUpdateInput() {
|
||||
@@ -179,8 +207,8 @@ class SceneParserResult {
|
||||
rating: this.scene.rating,
|
||||
gallery_id: this.scene.gallery ? this.scene.gallery.id : undefined,
|
||||
studio_id: this.scene.studio ? this.scene.studio.id : undefined,
|
||||
performer_ids: this.scene.performers.map((performer) => performer.id),
|
||||
tag_ids: this.scene.tags.map((tag) => tag.id)
|
||||
performer_ids: this.scene.performers.map(performer => performer.id),
|
||||
tag_ids: this.scene.tags.map(tag => tag.id)
|
||||
};
|
||||
|
||||
SceneParserResult.setInput(ret, "title", this.title);
|
||||
@@ -191,24 +219,24 @@ class SceneParserResult {
|
||||
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface IParserInput {
|
||||
pattern: string,
|
||||
ignoreWords: string[],
|
||||
whitespaceCharacters: string,
|
||||
capitalizeTitle: boolean,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
findClicked: boolean
|
||||
pattern: string;
|
||||
ignoreWords: string[];
|
||||
whitespaceCharacters: string;
|
||||
capitalizeTitle: boolean;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
findClicked: boolean;
|
||||
}
|
||||
|
||||
interface IParserRecipe {
|
||||
pattern: string,
|
||||
ignoreWords: string[],
|
||||
whitespaceCharacters: string,
|
||||
capitalizeTitle: boolean,
|
||||
description: string
|
||||
pattern: string;
|
||||
ignoreWords: string[];
|
||||
whitespaceCharacters: string;
|
||||
capitalizeTitle: boolean;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const builtInRecipes = [
|
||||
@@ -277,7 +305,9 @@ const initialShowFieldsState = new Map<string, boolean>([
|
||||
export const SceneFilenameParser: React.FC = () => {
|
||||
const Toast = useToast();
|
||||
const [parserResult, setParserResult] = useState<SceneParserResult[]>([]);
|
||||
const [parserInput, setParserInput] = useState<IParserInput>(initialParserInput);
|
||||
const [parserInput, setParserInput] = useState<IParserInput>(
|
||||
initialParserInput
|
||||
);
|
||||
|
||||
const [allTitleSet, setAllTitleSet] = useState<boolean>(false);
|
||||
const [allDateSet, setAllDateSet] = useState<boolean>(false);
|
||||
@@ -285,7 +315,9 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
const [allTagSet, setAllTagSet] = 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);
|
||||
|
||||
@@ -295,12 +327,13 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
const updateScenes = StashService.useScenesUpdate(getScenesUpdateData());
|
||||
|
||||
const determineFieldsToHide = useCallback(() => {
|
||||
const {pattern} = parserInput;
|
||||
const { pattern } = parserInput;
|
||||
const titleSet = pattern.includes("{title}");
|
||||
const dateSet = pattern.includes("{date}") ||
|
||||
const dateSet =
|
||||
pattern.includes("{date}") ||
|
||||
pattern.includes("{dd}") || // don't worry about other partial date fields since this should be implied
|
||||
ParserField.fullDateFields.some((f) => {
|
||||
return pattern.includes(`{${ f.field }}`);
|
||||
ParserField.fullDateFields.some(f => {
|
||||
return pattern.includes(`{${f.field}}`);
|
||||
});
|
||||
const performerSet = pattern.includes("{performer}");
|
||||
const tagSet = pattern.includes("{tag}");
|
||||
@@ -317,19 +350,24 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
setShowFields(newShowFields);
|
||||
}, [parserInput]);
|
||||
|
||||
const parseResults = useCallback((results : GQL.ParseSceneFilenamesResults[]) => {
|
||||
const parseResults = useCallback(
|
||||
(results: GQL.ParseSceneFilenamesResults[]) => {
|
||||
if (results) {
|
||||
const result = results.map((r) => {
|
||||
const result = results
|
||||
.map(r => {
|
||||
return new SceneParserResult(r);
|
||||
}).filter((r) => !!r) as SceneParserResult[];
|
||||
})
|
||||
.filter(r => !!r) as SceneParserResult[];
|
||||
|
||||
setParserResult(result);
|
||||
determineFieldsToHide();
|
||||
}
|
||||
}, [determineFieldsToHide]);
|
||||
},
|
||||
[determineFieldsToHide]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if(parserInput.findClicked) {
|
||||
if (parserInput.findClicked) {
|
||||
setParserResult([]);
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -338,7 +376,7 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
page: parserInput.page,
|
||||
per_page: parserInput.pageSize,
|
||||
sort: "path",
|
||||
direction: GQL.SortDirectionEnum.Asc,
|
||||
direction: GQL.SortDirectionEnum.Asc
|
||||
};
|
||||
|
||||
const parserInputData = {
|
||||
@@ -348,30 +386,26 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
};
|
||||
|
||||
StashService.queryParseSceneFilenames(parserFilter, parserInputData)
|
||||
.then((response) => {
|
||||
.then(response => {
|
||||
const result = response.data.parseSceneFilenames;
|
||||
if (result) {
|
||||
parseResults(result.results);
|
||||
setTotalItems(result.count);
|
||||
}
|
||||
})
|
||||
.catch((err) => (
|
||||
Toast.error(err)
|
||||
))
|
||||
.finally(() => (
|
||||
setIsLoading(false)
|
||||
));
|
||||
.catch(err => Toast.error(err))
|
||||
.finally(() => setIsLoading(false));
|
||||
}
|
||||
}, [parserInput, parseResults, Toast]);
|
||||
|
||||
function onPageSizeChanged(newSize : number) {
|
||||
function onPageSizeChanged(newSize: number) {
|
||||
const newInput = _.clone(parserInput);
|
||||
newInput.page = 1;
|
||||
newInput.pageSize = newSize;
|
||||
setParserInput(newInput);
|
||||
}
|
||||
|
||||
function onPageChanged(newPage : number) {
|
||||
function onPageChanged(newPage: number) {
|
||||
if (newPage !== parserInput.page) {
|
||||
const newInput = _.clone(parserInput);
|
||||
newInput.page = newPage;
|
||||
@@ -379,7 +413,7 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
function onFindClicked(input : IParserInput) {
|
||||
function onFindClicked(input: IParserInput) {
|
||||
input.page = 1;
|
||||
input.findClicked = true;
|
||||
setParserInput(input);
|
||||
@@ -387,7 +421,9 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
}
|
||||
|
||||
function getScenesUpdateData() {
|
||||
return parserResult.filter((result) => result.isChanged()).map((result) => result.toSceneUpdateInput());
|
||||
return parserResult
|
||||
.filter(result => result.isChanged())
|
||||
.map(result => result.toSceneUpdateInput());
|
||||
}
|
||||
|
||||
async function onApply() {
|
||||
@@ -404,19 +440,19 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const newAllTitleSet = !parserResult.some((r) => {
|
||||
const newAllTitleSet = !parserResult.some(r => {
|
||||
return !r.title.set;
|
||||
});
|
||||
const newAllDateSet = !parserResult.some((r) => {
|
||||
const newAllDateSet = !parserResult.some(r => {
|
||||
return !r.date.set;
|
||||
});
|
||||
const newAllPerformerSet = !parserResult.some((r) => {
|
||||
const newAllPerformerSet = !parserResult.some(r => {
|
||||
return !r.performerIds.set;
|
||||
});
|
||||
const newAllTagSet = !parserResult.some((r) => {
|
||||
const newAllTagSet = !parserResult.some(r => {
|
||||
return !r.tagIds.set;
|
||||
});
|
||||
const newAllStudioSet = !parserResult.some((r) => {
|
||||
const newAllStudioSet = !parserResult.some(r => {
|
||||
return !r.studioId.set;
|
||||
});
|
||||
|
||||
@@ -427,10 +463,10 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
setAllStudioSet(newAllStudioSet);
|
||||
}, [parserResult]);
|
||||
|
||||
function onSelectAllTitleSet(selected : boolean) {
|
||||
function onSelectAllTitleSet(selected: boolean) {
|
||||
const newResult = [...parserResult];
|
||||
|
||||
newResult.forEach((r) => {
|
||||
newResult.forEach(r => {
|
||||
r.title.set = selected;
|
||||
});
|
||||
|
||||
@@ -438,10 +474,10 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
setAllTitleSet(selected);
|
||||
}
|
||||
|
||||
function onSelectAllDateSet(selected : boolean) {
|
||||
function onSelectAllDateSet(selected: boolean) {
|
||||
const newResult = [...parserResult];
|
||||
|
||||
newResult.forEach((r) => {
|
||||
newResult.forEach(r => {
|
||||
r.date.set = selected;
|
||||
});
|
||||
|
||||
@@ -449,10 +485,10 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
setAllDateSet(selected);
|
||||
}
|
||||
|
||||
function onSelectAllPerformerSet(selected : boolean) {
|
||||
function onSelectAllPerformerSet(selected: boolean) {
|
||||
const newResult = [...parserResult];
|
||||
|
||||
newResult.forEach((r) => {
|
||||
newResult.forEach(r => {
|
||||
r.performerIds.set = selected;
|
||||
});
|
||||
|
||||
@@ -460,10 +496,10 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
setAllPerformerSet(selected);
|
||||
}
|
||||
|
||||
function onSelectAllTagSet(selected : boolean) {
|
||||
function onSelectAllTagSet(selected: boolean) {
|
||||
const newResult = [...parserResult];
|
||||
|
||||
newResult.forEach((r) => {
|
||||
newResult.forEach(r => {
|
||||
r.tagIds.set = selected;
|
||||
});
|
||||
|
||||
@@ -471,10 +507,10 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
setAllTagSet(selected);
|
||||
}
|
||||
|
||||
function onSelectAllStudioSet(selected : boolean) {
|
||||
function onSelectAllStudioSet(selected: boolean) {
|
||||
const newResult = [...parserResult];
|
||||
|
||||
newResult.forEach((r) => {
|
||||
newResult.forEach(r => {
|
||||
r.studioId.set = selected;
|
||||
});
|
||||
|
||||
@@ -483,8 +519,8 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
}
|
||||
|
||||
interface IShowFieldsProps {
|
||||
fields: Map<string, boolean>
|
||||
onShowFieldsChanged: (fields : Map<string, boolean>) => void
|
||||
fields: Map<string, boolean>;
|
||||
onShowFieldsChanged: (fields: Map<string, boolean>) => void;
|
||||
}
|
||||
|
||||
function ShowFields(props: IShowFieldsProps) {
|
||||
@@ -497,8 +533,13 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
}
|
||||
|
||||
const fieldRows = [...props.fields.entries()].map(([label, enabled]) => (
|
||||
<div key={label} onClick={() => {handleClick(label)}}>
|
||||
<Icon icon={enabled ? "check" : "times" } />
|
||||
<div
|
||||
key={label}
|
||||
onClick={() => {
|
||||
handleClick(label);
|
||||
}}
|
||||
>
|
||||
<Icon icon={enabled ? "check" : "times"} />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
));
|
||||
@@ -506,28 +547,32 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<div onClick={() => setOpen(!open)}>
|
||||
<Icon icon={open ? "chevron-down" : "chevron-right" } />
|
||||
<Icon icon={open ? "chevron-down" : "chevron-right"} />
|
||||
<span>Display fields</span>
|
||||
</div>
|
||||
<Collapse in={open}>
|
||||
<div>
|
||||
{fieldRows}
|
||||
</div>
|
||||
<div>{fieldRows}</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface IParserInputProps {
|
||||
input: IParserInput,
|
||||
onFind: (input : IParserInput) => void
|
||||
input: IParserInput;
|
||||
onFind: (input: IParserInput) => void;
|
||||
}
|
||||
|
||||
function ParserInput(props : IParserInputProps) {
|
||||
function ParserInput(props: IParserInputProps) {
|
||||
const [pattern, setPattern] = useState<string>(props.input.pattern);
|
||||
const [ignoreWords, setIgnoreWords] = useState<string>(props.input.ignoreWords.join(" "));
|
||||
const [whitespaceCharacters, setWhitespaceCharacters] = useState<string>(props.input.whitespaceCharacters);
|
||||
const [capitalizeTitle, setCapitalizeTitle] = useState<boolean>(props.input.capitalizeTitle);
|
||||
const [ignoreWords, setIgnoreWords] = useState<string>(
|
||||
props.input.ignoreWords.join(" ")
|
||||
);
|
||||
const [whitespaceCharacters, setWhitespaceCharacters] = useState<string>(
|
||||
props.input.whitespaceCharacters
|
||||
);
|
||||
const [capitalizeTitle, setCapitalizeTitle] = useState<boolean>(
|
||||
props.input.capitalizeTitle
|
||||
);
|
||||
|
||||
function onFind() {
|
||||
props.onFind({
|
||||
@@ -548,7 +593,9 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
setCapitalizeTitle(recipe.capitalizeTitle);
|
||||
}
|
||||
|
||||
const validFields = [new ParserField("", "Wildcard")].concat(ParserField.validFields);
|
||||
const validFields = [new ParserField("", "Wildcard")].concat(
|
||||
ParserField.validFields
|
||||
);
|
||||
|
||||
function addParserField(field: ParserField) {
|
||||
setPattern(pattern + field.getFieldPattern());
|
||||
@@ -564,9 +611,10 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
value={pattern}
|
||||
/>
|
||||
<DropdownButton id="parser-field-select" title="Add Field">
|
||||
{ validFields.map(item => (
|
||||
{validFields.map(item => (
|
||||
<Dropdown.Item onSelect={() => addParserField(item)}>
|
||||
<span>{item.field}</span><span className="ml-auto">{item.helperText}</span>
|
||||
<span>{item.field}</span>
|
||||
<span className="ml-auto">{item.helperText}</span>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</DropdownButton>
|
||||
@@ -586,7 +634,9 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
<h5>Title</h5>
|
||||
<Form.Label>Whitespace characters:</Form.Label>
|
||||
<Form.Control
|
||||
onChange={(newValue: any) => setWhitespaceCharacters(newValue.target.value)}
|
||||
onChange={(newValue: any) =>
|
||||
setWhitespaceCharacters(newValue.target.value)
|
||||
}
|
||||
value={whitespaceCharacters}
|
||||
/>
|
||||
<Form.Group>
|
||||
@@ -597,16 +647,19 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
onChange={() => setCapitalizeTitle(!capitalizeTitle)}
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* TODO - mapping stuff will go here */}
|
||||
|
||||
<Form.Group>
|
||||
<DropdownButton id="recipe-select" title="Select Parser Recipe">
|
||||
{ builtInRecipes.map(item => (
|
||||
{builtInRecipes.map(item => (
|
||||
<Dropdown.Item onSelect={() => setParserRecipe(item)}>
|
||||
<span>{item.pattern}</span><span className="mr-auto">{item.description}</span>
|
||||
<span>{item.pattern}</span>
|
||||
<span className="mr-auto">{item.description}</span>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</DropdownButton>
|
||||
@@ -615,7 +668,7 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
<Form.Group>
|
||||
<ShowFields
|
||||
fields={showFields}
|
||||
onShowFieldsChanged={(fields) => setShowFields(fields)}
|
||||
onShowFieldsChanged={fields => setShowFields(fields)}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
@@ -623,13 +676,17 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
<Button onClick={onFind}>Find</Button>
|
||||
<Form.Control
|
||||
as="select"
|
||||
style={{flexBasis: "min-content"}}
|
||||
style={{ flexBasis: "min-content" }}
|
||||
options={PAGE_SIZE_OPTIONS}
|
||||
onChange={(event: any) => onPageSizeChanged(parseInt(event.target.value, 10))}
|
||||
onChange={(event: any) =>
|
||||
onPageSizeChanged(parseInt(event.target.value, 10))
|
||||
}
|
||||
defaultValue={props.input.pageSize}
|
||||
className="filter-item"
|
||||
>
|
||||
{ PAGE_SIZE_OPTIONS.map(val => <option value="val">{val}</option>) }
|
||||
{PAGE_SIZE_OPTIONS.map(val => (
|
||||
<option value="val">{val}</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
@@ -637,19 +694,21 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
}
|
||||
|
||||
interface ISceneParserFieldProps {
|
||||
parserResult : ParserResult<any>
|
||||
className? : string
|
||||
fieldName : string
|
||||
onSetChanged : (set : boolean) => void
|
||||
onValueChanged : (value : any) => void
|
||||
originalParserResult? : ParserResult<any>
|
||||
renderOriginalInputField: (props : ISceneParserFieldProps) => JSX.Element
|
||||
renderNewInputField: (props : ISceneParserFieldProps, onChange : (event : any) => void) => JSX.Element
|
||||
parserResult: ParserResult<any>;
|
||||
className?: string;
|
||||
fieldName: string;
|
||||
onSetChanged: (set: boolean) => void;
|
||||
onValueChanged: (value: any) => void;
|
||||
originalParserResult?: ParserResult<any>;
|
||||
renderOriginalInputField: (props: ISceneParserFieldProps) => JSX.Element;
|
||||
renderNewInputField: (
|
||||
props: ISceneParserFieldProps,
|
||||
onChange: (event: any) => void
|
||||
) => JSX.Element;
|
||||
}
|
||||
|
||||
function SceneParserField(props : ISceneParserFieldProps) {
|
||||
|
||||
function maybeValueChanged(value : any) {
|
||||
function SceneParserField(props: ISceneParserFieldProps) {
|
||||
function maybeValueChanged(value: any) {
|
||||
if (value !== props.parserResult.value) {
|
||||
props.onValueChanged(value);
|
||||
}
|
||||
@@ -665,13 +724,17 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
<Form.Control
|
||||
type="checkbox"
|
||||
checked={props.parserResult.set}
|
||||
onChange={() => {props.onSetChanged(!props.parserResult.set)}}
|
||||
onChange={() => {
|
||||
props.onSetChanged(!props.parserResult.set);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Group>
|
||||
{props.renderOriginalInputField(props)}
|
||||
{props.renderNewInputField(props, (value) => maybeValueChanged(value))}
|
||||
{props.renderNewInputField(props, value =>
|
||||
maybeValueChanged(value)
|
||||
)}
|
||||
</Form.Group>
|
||||
</td>
|
||||
</>
|
||||
@@ -691,9 +754,9 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
}
|
||||
|
||||
interface IInputGroupWrapperProps {
|
||||
parserResult: ParserResult<any>
|
||||
onChange : (event : any) => void
|
||||
className? : string
|
||||
parserResult: ParserResult<any>;
|
||||
onChange: (event: any) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function InputGroupWrapper(props: IInputGroupWrapperProps) {
|
||||
@@ -707,44 +770,55 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
function renderNewInputGroup(props : ISceneParserFieldProps, onChangeHandler : (value : any) => void) {
|
||||
function renderNewInputGroup(
|
||||
props: ISceneParserFieldProps,
|
||||
onChangeHandler: (value: any) => void
|
||||
) {
|
||||
return (
|
||||
<InputGroupWrapper
|
||||
className={props.className}
|
||||
onChange={(value : any) => {onChangeHandler(value)}}
|
||||
onChange={(value: any) => {
|
||||
onChangeHandler(value);
|
||||
}}
|
||||
parserResult={props.parserResult}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface IHasName {
|
||||
name: string
|
||||
name: string;
|
||||
}
|
||||
|
||||
function renderOriginalSelect(props : ISceneParserFieldProps) {
|
||||
function renderOriginalSelect(props: ISceneParserFieldProps) {
|
||||
const result = props.originalParserResult || props.parserResult;
|
||||
|
||||
const elements = result.originalValue
|
||||
? Array.isArray(result.originalValue)
|
||||
? result.originalValue.map((el:IHasName) => el.name)
|
||||
? result.originalValue.map((el: IHasName) => el.name)
|
||||
: [result.originalValue.name]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ elements.map((name:string) => <Badge variant="secondary">{name}</Badge>) }
|
||||
{elements.map((name: string) => (
|
||||
<Badge variant="secondary">{name}</Badge>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderNewMultiSelect(type: "performers" | "tags", props : ISceneParserFieldProps, onChangeHandler : (value : any) => void) {
|
||||
function renderNewMultiSelect(
|
||||
type: "performers" | "tags",
|
||||
props: ISceneParserFieldProps,
|
||||
onChangeHandler: (value: any) => void
|
||||
) {
|
||||
return (
|
||||
<FilterSelect
|
||||
className={props.className}
|
||||
type={type}
|
||||
isMulti
|
||||
onSelect={(items) => {
|
||||
const ids = items.map((i) => i.id);
|
||||
onSelect={items => {
|
||||
const ids = items.map(i => i.id);
|
||||
onChangeHandler(ids);
|
||||
}}
|
||||
initialIds={props.parserResult.value}
|
||||
@@ -752,64 +826,72 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
function renderNewPerformerSelect(props : ISceneParserFieldProps, onChangeHandler : (value : any) => void) {
|
||||
function renderNewPerformerSelect(
|
||||
props: ISceneParserFieldProps,
|
||||
onChangeHandler: (value: any) => void
|
||||
) {
|
||||
return renderNewMultiSelect("performers", props, onChangeHandler);
|
||||
}
|
||||
|
||||
function renderNewTagSelect(props : ISceneParserFieldProps, onChangeHandler : (value : any) => void) {
|
||||
function renderNewTagSelect(
|
||||
props: ISceneParserFieldProps,
|
||||
onChangeHandler: (value: any) => void
|
||||
) {
|
||||
return renderNewMultiSelect("tags", props, onChangeHandler);
|
||||
}
|
||||
|
||||
function renderNewStudioSelect(props : ISceneParserFieldProps, onChangeHandler : (value : any) => void) {
|
||||
function renderNewStudioSelect(
|
||||
props: ISceneParserFieldProps,
|
||||
onChangeHandler: (value: any) => void
|
||||
) {
|
||||
return (
|
||||
<StudioSelect
|
||||
noSelectionString=""
|
||||
className={props.className}
|
||||
onSelect={(items) => onChangeHandler(items[0]?.id)}
|
||||
onSelect={items => onChangeHandler(items[0]?.id)}
|
||||
initialIds={props.parserResult.value ? [props.parserResult.value] : []}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface ISceneParserRowProps {
|
||||
scene : SceneParserResult,
|
||||
onChange: (changedScene : SceneParserResult) => void
|
||||
scene: SceneParserResult;
|
||||
onChange: (changedScene: SceneParserResult) => void;
|
||||
}
|
||||
|
||||
function SceneParserRow(props : ISceneParserRowProps) {
|
||||
|
||||
function changeParser(result : ParserResult<any>, set : boolean, value : any) {
|
||||
function SceneParserRow(props: ISceneParserRowProps) {
|
||||
function changeParser(result: ParserResult<any>, set: boolean, value: any) {
|
||||
const newParser = _.clone(result);
|
||||
newParser.set = set;
|
||||
newParser.value = value;
|
||||
return newParser;
|
||||
}
|
||||
|
||||
function onTitleChanged(set : boolean, value: string | undefined) {
|
||||
function onTitleChanged(set: boolean, value: string | undefined) {
|
||||
const newResult = _.clone(props.scene);
|
||||
newResult.title = changeParser(newResult.title, set, value);
|
||||
props.onChange(newResult);
|
||||
}
|
||||
|
||||
function onDateChanged(set : boolean, value: string | undefined) {
|
||||
function onDateChanged(set: boolean, value: string | undefined) {
|
||||
const newResult = _.clone(props.scene);
|
||||
newResult.date = changeParser(newResult.date, set, value);
|
||||
props.onChange(newResult);
|
||||
}
|
||||
|
||||
function onPerformerIdsChanged(set : boolean, value: string[] | undefined) {
|
||||
function onPerformerIdsChanged(set: boolean, value: string[] | undefined) {
|
||||
const newResult = _.clone(props.scene);
|
||||
newResult.performerIds = changeParser(newResult.performerIds, set, value);
|
||||
props.onChange(newResult);
|
||||
}
|
||||
|
||||
function onTagIdsChanged(set : boolean, value: string[] | undefined) {
|
||||
function onTagIdsChanged(set: boolean, value: string[] | undefined) {
|
||||
const newResult = _.clone(props.scene);
|
||||
newResult.tagIds = changeParser(newResult.tagIds, set, value);
|
||||
props.onChange(newResult);
|
||||
}
|
||||
|
||||
function onStudioIdChanged(set : boolean, value: string | undefined) {
|
||||
function onStudioIdChanged(set: boolean, value: string | undefined) {
|
||||
const newResult = _.clone(props.scene);
|
||||
newResult.studioId = changeParser(newResult.studioId, set, value);
|
||||
props.onChange(newResult);
|
||||
@@ -817,16 +899,14 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
|
||||
return (
|
||||
<tr className="scene-parser-row">
|
||||
<td style={{textAlign: "left"}}>
|
||||
{props.scene.filename}
|
||||
</td>
|
||||
<td style={{ textAlign: "left" }}>{props.scene.filename}</td>
|
||||
<SceneParserField
|
||||
key="title"
|
||||
fieldName="Title"
|
||||
className="parser-field-title"
|
||||
parserResult={props.scene.title}
|
||||
onSetChanged={(set) => onTitleChanged(set, props.scene.title.value)}
|
||||
onValueChanged={(value) => onTitleChanged(props.scene.title.set, value)}
|
||||
onSetChanged={set => onTitleChanged(set, props.scene.title.value)}
|
||||
onValueChanged={value => onTitleChanged(props.scene.title.set, value)}
|
||||
renderOriginalInputField={renderOriginalInputGroup}
|
||||
renderNewInputField={renderNewInputGroup}
|
||||
/>
|
||||
@@ -835,8 +915,8 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
fieldName="Date"
|
||||
className="parser-field-date"
|
||||
parserResult={props.scene.date}
|
||||
onSetChanged={(set) => onDateChanged(set, props.scene.date.value)}
|
||||
onValueChanged={(value) => onDateChanged(props.scene.date.set, value)}
|
||||
onSetChanged={set => onDateChanged(set, props.scene.date.value)}
|
||||
onValueChanged={value => onDateChanged(props.scene.date.set, value)}
|
||||
renderOriginalInputField={renderOriginalInputGroup}
|
||||
renderNewInputField={renderNewInputGroup}
|
||||
/>
|
||||
@@ -846,8 +926,12 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
className="parser-field-performers"
|
||||
parserResult={props.scene.performerIds}
|
||||
originalParserResult={props.scene.performers}
|
||||
onSetChanged={(set) => onPerformerIdsChanged(set, props.scene.performerIds.value)}
|
||||
onValueChanged={(value) => onPerformerIdsChanged(props.scene.performerIds.set, value)}
|
||||
onSetChanged={set =>
|
||||
onPerformerIdsChanged(set, props.scene.performerIds.value)
|
||||
}
|
||||
onValueChanged={value =>
|
||||
onPerformerIdsChanged(props.scene.performerIds.set, value)
|
||||
}
|
||||
renderOriginalInputField={renderOriginalSelect}
|
||||
renderNewInputField={renderNewPerformerSelect}
|
||||
/>
|
||||
@@ -857,8 +941,10 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
className="parser-field-tags"
|
||||
parserResult={props.scene.tagIds}
|
||||
originalParserResult={props.scene.tags}
|
||||
onSetChanged={(set) => onTagIdsChanged(set, props.scene.tagIds.value)}
|
||||
onValueChanged={(value) => onTagIdsChanged(props.scene.tagIds.set, value)}
|
||||
onSetChanged={set => onTagIdsChanged(set, props.scene.tagIds.value)}
|
||||
onValueChanged={value =>
|
||||
onTagIdsChanged(props.scene.tagIds.set, value)
|
||||
}
|
||||
renderOriginalInputField={renderOriginalSelect}
|
||||
renderNewInputField={renderNewTagSelect}
|
||||
/>
|
||||
@@ -868,16 +954,20 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
className="parser-field-studio"
|
||||
parserResult={props.scene.studioId}
|
||||
originalParserResult={props.scene.studio}
|
||||
onSetChanged={(set) => onStudioIdChanged(set, props.scene.studioId.value)}
|
||||
onValueChanged={(value) => onStudioIdChanged(props.scene.studioId.set, value)}
|
||||
onSetChanged={set =>
|
||||
onStudioIdChanged(set, props.scene.studioId.value)
|
||||
}
|
||||
onValueChanged={value =>
|
||||
onStudioIdChanged(props.scene.studioId.set, value)
|
||||
}
|
||||
renderOriginalInputField={renderOriginalSelect}
|
||||
renderNewInputField={renderNewStudioSelect}
|
||||
/>
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function onChange(scene : SceneParserResult, changedScene : SceneParserResult) {
|
||||
function onChange(scene: SceneParserResult, changedScene: SceneParserResult) {
|
||||
const newResult = [...parserResult];
|
||||
|
||||
const index = newResult.indexOf(scene);
|
||||
@@ -886,7 +976,11 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
setParserResult(newResult);
|
||||
}
|
||||
|
||||
function renderHeader(fieldName: string, allSet: boolean, onAllSet: (set: boolean) => void) {
|
||||
function renderHeader(
|
||||
fieldName: string,
|
||||
allSet: boolean,
|
||||
onAllSet: (set: boolean) => void
|
||||
) {
|
||||
if (!showFields.get(fieldName)) {
|
||||
return null;
|
||||
}
|
||||
@@ -897,16 +991,20 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
<Form.Control
|
||||
type="checkbox"
|
||||
checked={allSet}
|
||||
onChange={() => {onAllSet(!allSet)}}
|
||||
onChange={() => {
|
||||
onAllSet(!allSet);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<th>{fieldName}</th>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
if (parserResult.length === 0) { return undefined; }
|
||||
if (parserResult.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -918,18 +1016,23 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
<th>Filename</th>
|
||||
{renderHeader("Title", allTitleSet, onSelectAllTitleSet)}
|
||||
{renderHeader("Date", allDateSet, onSelectAllDateSet)}
|
||||
{renderHeader("Performers", allPerformerSet, onSelectAllPerformerSet)}
|
||||
{renderHeader(
|
||||
"Performers",
|
||||
allPerformerSet,
|
||||
onSelectAllPerformerSet
|
||||
)}
|
||||
{renderHeader("Tags", allTagSet, onSelectAllTagSet)}
|
||||
{renderHeader("Studio", allStudioSet, onSelectAllStudioSet)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{parserResult.map((scene) =>
|
||||
{parserResult.map(scene => (
|
||||
<SceneParserRow
|
||||
scene={scene}
|
||||
key={scene.id}
|
||||
onChange={(changedScene) => onChange(scene, changedScene)}/>
|
||||
)}
|
||||
onChange={changedScene => onChange(scene, changedScene)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
@@ -937,25 +1040,23 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
currentPage={parserInput.page}
|
||||
itemsPerPage={parserInput.pageSize}
|
||||
totalItems={totalItems}
|
||||
onChangePage={(page) => onPageChanged(page)}
|
||||
onChangePage={page => onPageChanged(page)}
|
||||
/>
|
||||
<Button variant="primary" onClick={onApply}>Apply</Button>
|
||||
<Button variant="primary" onClick={onApply}>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card id="parser-container">
|
||||
<h4>Scene Filename Parser</h4>
|
||||
<ParserInput
|
||||
input={parserInput}
|
||||
onFind={(input) => onFindClicked(input)}
|
||||
/>
|
||||
<ParserInput input={parserInput} onFind={input => onFindClicked(input)} />
|
||||
|
||||
{isLoading ? <Spinner animation="border" variant="light" /> : undefined}
|
||||
{renderTable()}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import { QueryHookResult } from "react-apollo-hooks";
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { FindScenesQuery, FindScenesVariables, SlimSceneDataFragment } from "src/core/generated-graphql";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import {
|
||||
FindScenesQuery,
|
||||
FindScenesVariables,
|
||||
SlimSceneDataFragment
|
||||
} from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { useScenesList } from "src/hooks";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
@@ -17,7 +21,7 @@ export const SceneList: React.FC = () => {
|
||||
const otherOperations = [
|
||||
{
|
||||
text: "Play Random",
|
||||
onClick: playRandom,
|
||||
onClick: playRandom
|
||||
}
|
||||
];
|
||||
|
||||
@@ -28,32 +32,45 @@ export const SceneList: React.FC = () => {
|
||||
renderSelectedOptions
|
||||
});
|
||||
|
||||
async function playRandom(result: QueryHookResult<FindScenesQuery, FindScenesVariables>, filter: ListFilterModel) {
|
||||
async function playRandom(
|
||||
result: QueryHookResult<FindScenesQuery, FindScenesVariables>,
|
||||
filter: ListFilterModel
|
||||
) {
|
||||
// query for a random scene
|
||||
if (result.data && result.data.findScenes) {
|
||||
const {count} = result.data.findScenes;
|
||||
const { count } = result.data.findScenes;
|
||||
|
||||
const index = Math.floor(Math.random() * count);
|
||||
const filterCopy = _.cloneDeep(filter);
|
||||
filterCopy.itemsPerPage = 1;
|
||||
filterCopy.currentPage = index + 1;
|
||||
const singleResult = await StashService.queryFindScenes(filterCopy);
|
||||
if (singleResult && singleResult.data && singleResult.data.findScenes && singleResult.data.findScenes.scenes.length === 1) {
|
||||
const {id} = singleResult!.data!.findScenes!.scenes[0];
|
||||
if (
|
||||
singleResult &&
|
||||
singleResult.data &&
|
||||
singleResult.data.findScenes &&
|
||||
singleResult.data.findScenes.scenes.length === 1
|
||||
) {
|
||||
const { id } = singleResult!.data!.findScenes!.scenes[0];
|
||||
// navigate to the scene player page
|
||||
history.push(`/scenes/${ id }?autoplay=true`);
|
||||
history.push(`/scenes/${id}?autoplay=true`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderSelectedOptions(result: QueryHookResult<FindScenesQuery, FindScenesVariables>, selectedIds: Set<string>) {
|
||||
function renderSelectedOptions(
|
||||
result: QueryHookResult<FindScenesQuery, FindScenesVariables>,
|
||||
selectedIds: Set<string>
|
||||
) {
|
||||
// find the selected items from the ids
|
||||
if (!result.data || !result.data.findScenes) { return undefined; }
|
||||
if (!result.data || !result.data.findScenes) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const {scenes} = result.data.findScenes;
|
||||
const { scenes } = result.data.findScenes;
|
||||
|
||||
const selectedScenes : SlimSceneDataFragment[] = [];
|
||||
selectedIds.forEach((id) => {
|
||||
const selectedScenes: SlimSceneDataFragment[] = [];
|
||||
selectedIds.forEach(id => {
|
||||
const scene = scenes.find(s => s.id === id);
|
||||
|
||||
if (scene) {
|
||||
@@ -63,34 +80,54 @@ export const SceneList: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SceneSelectedOptions selected={selectedScenes} onScenesUpdated={() => { }}/>
|
||||
<SceneSelectedOptions
|
||||
selected={selectedScenes}
|
||||
onScenesUpdated={() => {}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSceneCard(scene : SlimSceneDataFragment, selectedIds: Set<string>, zoomIndex: number) {
|
||||
function renderSceneCard(
|
||||
scene: SlimSceneDataFragment,
|
||||
selectedIds: Set<string>,
|
||||
zoomIndex: number
|
||||
) {
|
||||
return (
|
||||
<SceneCard
|
||||
key={scene.id}
|
||||
scene={scene}
|
||||
zoomIndex={zoomIndex}
|
||||
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)
|
||||
}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function renderContent(result: QueryHookResult<FindScenesQuery, FindScenesVariables>, filter: ListFilterModel, selectedIds: Set<string>, zoomIndex: number) {
|
||||
if (!result.data || !result.data.findScenes) { return; }
|
||||
function renderContent(
|
||||
result: QueryHookResult<FindScenesQuery, FindScenesVariables>,
|
||||
filter: ListFilterModel,
|
||||
selectedIds: Set<string>,
|
||||
zoomIndex: number
|
||||
) {
|
||||
if (!result.data || !result.data.findScenes) {
|
||||
return;
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.Grid) {
|
||||
return (
|
||||
<div className="grid">
|
||||
{result.data.findScenes.scenes.map((scene) => renderSceneCard(scene, selectedIds, zoomIndex))}
|
||||
{result.data.findScenes.scenes.map(scene =>
|
||||
renderSceneCard(scene, selectedIds, zoomIndex)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} if (filter.displayMode === DisplayMode.List) {
|
||||
return <SceneListTable scenes={result.data.findScenes.scenes}/>;
|
||||
} if (filter.displayMode === DisplayMode.Wall) {
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.List) {
|
||||
return <SceneListTable scenes={result.data.findScenes.scenes} />;
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.Wall) {
|
||||
return <WallPanel scenes={result.data.findScenes.scenes} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
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";
|
||||
@@ -8,48 +8,52 @@ interface ISceneListTableProps {
|
||||
scenes: GQL.SlimSceneDataFragment[];
|
||||
}
|
||||
|
||||
export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneListTableProps) => {
|
||||
|
||||
function renderSceneImage(scene : GQL.SlimSceneDataFragment) {
|
||||
export const SceneListTable: React.FC<ISceneListTableProps> = (
|
||||
props: ISceneListTableProps
|
||||
) => {
|
||||
function renderSceneImage(scene: GQL.SlimSceneDataFragment) {
|
||||
const style: React.CSSProperties = {
|
||||
backgroundImage: `url('${scene.paths.screenshot}')`,
|
||||
lineHeight: 5,
|
||||
backgroundSize: "contain",
|
||||
display: "inline-block",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundRepeat: "no-repeat"
|
||||
};
|
||||
|
||||
return (
|
||||
<Link
|
||||
className="scene-list-thumbnail"
|
||||
to={`/scenes/${scene.id}`}
|
||||
style={style}/>
|
||||
)
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDuration(scene : GQL.SlimSceneDataFragment) {
|
||||
if (scene.file.duration === undefined) { return; }
|
||||
function renderDuration(scene: GQL.SlimSceneDataFragment) {
|
||||
if (scene.file.duration === undefined) {
|
||||
return;
|
||||
}
|
||||
return TextUtils.secondsToTimestamp(scene.file.duration);
|
||||
}
|
||||
|
||||
function renderTags(tags : GQL.SlimSceneDataTags[]) {
|
||||
return tags.map((tag) => (
|
||||
function renderTags(tags: GQL.SlimSceneDataTags[]) {
|
||||
return tags.map(tag => (
|
||||
<Link to={NavUtils.makeTagScenesUrl(tag)}>
|
||||
<h6>{tag.name}</h6>
|
||||
</Link>
|
||||
));
|
||||
}
|
||||
|
||||
function renderPerformers(performers : GQL.SlimSceneDataPerformers[]) {
|
||||
return performers.map((performer) => (
|
||||
function renderPerformers(performers: GQL.SlimSceneDataPerformers[]) {
|
||||
return performers.map(performer => (
|
||||
<Link to={NavUtils.makePerformerScenesUrl(performer)}>
|
||||
<h6>{performer.name}</h6>
|
||||
</Link>
|
||||
));
|
||||
}
|
||||
|
||||
function renderStudio(studio : GQL.SlimSceneDataStudio | undefined) {
|
||||
function renderStudio(studio: GQL.SlimSceneDataStudio | undefined) {
|
||||
if (studio) {
|
||||
return (
|
||||
<Link to={NavUtils.makeStudioScenesUrl(studio)}>
|
||||
@@ -59,36 +63,24 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneList
|
||||
}
|
||||
}
|
||||
|
||||
function renderSceneRow(scene : GQL.SlimSceneDataFragment) {
|
||||
function renderSceneRow(scene: GQL.SlimSceneDataFragment) {
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
{renderSceneImage(scene)}
|
||||
</td>
|
||||
<td style={{textAlign: "left"}}>
|
||||
<td>{renderSceneImage(scene)}</td>
|
||||
<td style={{ textAlign: "left" }}>
|
||||
<Link to={`/scenes/${scene.id}`}>
|
||||
<h5 className="text-truncate">
|
||||
{scene.title ?? TextUtils.fileNameFromPath(scene.path)}
|
||||
</h5>
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
{scene.rating ? scene.rating : ''}
|
||||
</td>
|
||||
<td>
|
||||
{renderDuration(scene)}
|
||||
</td>
|
||||
<td>
|
||||
{renderTags(scene.tags)}
|
||||
</td>
|
||||
<td>
|
||||
{renderPerformers(scene.performers)}
|
||||
</td>
|
||||
<td>
|
||||
{renderStudio(scene.studio)}
|
||||
</td>
|
||||
<td>{scene.rating ? scene.rating : ""}</td>
|
||||
<td>{renderDuration(scene)}</td>
|
||||
<td>{renderTags(scene.tags)}</td>
|
||||
<td>{renderPerformers(scene.performers)}</td>
|
||||
<td>{renderStudio(scene.studio)}</td>
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -104,11 +96,8 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (props: ISceneList
|
||||
<th>Studio</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{props.scenes.map(renderSceneRow)}
|
||||
</tbody>
|
||||
<tbody>{props.scenes.map(renderSceneRow)}</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import { QueryHookResult } from "react-apollo-hooks";
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { FindSceneMarkersQuery, FindSceneMarkersVariables } from "src/core/generated-graphql";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import {
|
||||
FindSceneMarkersQuery,
|
||||
FindSceneMarkersVariables
|
||||
} from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { NavUtils } from "src/utils";
|
||||
import { useSceneMarkersList } from "src/hooks";
|
||||
@@ -12,29 +15,41 @@ import { WallPanel } from "../Wall/WallPanel";
|
||||
|
||||
export const SceneMarkerList: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const otherOperations = [{
|
||||
const otherOperations = [
|
||||
{
|
||||
text: "Play Random",
|
||||
onClick: playRandom
|
||||
}];
|
||||
}
|
||||
];
|
||||
|
||||
const listData = useSceneMarkersList({
|
||||
otherOperations,
|
||||
renderContent,
|
||||
renderContent
|
||||
});
|
||||
|
||||
async function playRandom(result: QueryHookResult<FindSceneMarkersQuery, FindSceneMarkersVariables>, filter: ListFilterModel) {
|
||||
async function playRandom(
|
||||
result: QueryHookResult<FindSceneMarkersQuery, FindSceneMarkersVariables>,
|
||||
filter: ListFilterModel
|
||||
) {
|
||||
// query for a random scene
|
||||
if (result.data && result.data.findSceneMarkers) {
|
||||
const {count} = result.data.findSceneMarkers;
|
||||
const { count } = result.data.findSceneMarkers;
|
||||
|
||||
const index = Math.floor(Math.random() * count);
|
||||
const filterCopy = _.cloneDeep(filter);
|
||||
filterCopy.itemsPerPage = 1;
|
||||
filterCopy.currentPage = index + 1;
|
||||
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
|
||||
const url = NavUtils.makeSceneMarkerUrl(singleResult.data.findSceneMarkers.scene_markers[0])
|
||||
const url = NavUtils.makeSceneMarkerUrl(
|
||||
singleResult.data.findSceneMarkers.scene_markers[0]
|
||||
);
|
||||
history.push(url);
|
||||
}
|
||||
}
|
||||
@@ -42,12 +57,13 @@ export const SceneMarkerList: React.FC = () => {
|
||||
|
||||
function renderContent(
|
||||
result: QueryHookResult<FindSceneMarkersQuery, FindSceneMarkersVariables>,
|
||||
filter: ListFilterModel,
|
||||
filter: ListFilterModel
|
||||
) {
|
||||
if (!result?.data?.findSceneMarkers)
|
||||
return;
|
||||
if (!result?.data?.findSceneMarkers) return;
|
||||
if (filter.displayMode === DisplayMode.Wall) {
|
||||
return <WallPanel sceneMarkers={result.data.findSceneMarkers.scene_markers} />;
|
||||
return (
|
||||
<WallPanel sceneMarkers={result.data.findSceneMarkers.scene_markers} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,18 +24,29 @@ const KeyMap = {
|
||||
NUM1: "1",
|
||||
NUM2: "2",
|
||||
SPACE: " "
|
||||
}
|
||||
};
|
||||
|
||||
export class ScenePlayerImpl extends React.Component<IScenePlayerProps, IScenePlayerState> {
|
||||
export class ScenePlayerImpl extends React.Component<
|
||||
IScenePlayerProps,
|
||||
IScenePlayerState
|
||||
> {
|
||||
private player: any;
|
||||
private lastTime = 0;
|
||||
|
||||
private KeyHandlers = {
|
||||
NUM0: () => {this.onReset()},
|
||||
NUM1: () => {this.onDecrease()},
|
||||
NUM2: () => {this.onIncrease()},
|
||||
SPACE: () => {this.onPause()}
|
||||
NUM0: () => {
|
||||
this.onReset();
|
||||
},
|
||||
NUM1: () => {
|
||||
this.onDecrease();
|
||||
},
|
||||
NUM2: () => {
|
||||
this.onIncrease();
|
||||
},
|
||||
SPACE: () => {
|
||||
this.onPause();
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props: IScenePlayerProps) {
|
||||
super(props);
|
||||
@@ -46,7 +57,7 @@ export class ScenePlayerImpl extends React.Component<IScenePlayerProps, IScenePl
|
||||
this.onScrubberSeek = this.onScrubberSeek.bind(this);
|
||||
this.onScrubberScrolled = this.onScrubberScrolled.bind(this);
|
||||
|
||||
this.state = {scrubberPosition: 0};
|
||||
this.state = { scrubberPosition: 0 };
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IScenePlayerProps) {
|
||||
@@ -58,19 +69,19 @@ export class ScenePlayerImpl extends React.Component<IScenePlayerProps, IScenePl
|
||||
onIncrease() {
|
||||
const currentPlaybackRate = this.player ? this.player.getPlaybackRate() : 1;
|
||||
this.player.setPlaybackRate(currentPlaybackRate + 0.5);
|
||||
};
|
||||
}
|
||||
onDecrease() {
|
||||
const currentPlaybackRate = this.player ? this.player.getPlaybackRate() : 1;
|
||||
this.player.setPlaybackRate(currentPlaybackRate - 0.5);
|
||||
};
|
||||
}
|
||||
|
||||
onReset() { this.player.setPlaybackRate(1); };
|
||||
onReset() {
|
||||
this.player.setPlaybackRate(1);
|
||||
}
|
||||
onPause() {
|
||||
if (this.player.getState().paused)
|
||||
this.player.play();
|
||||
else
|
||||
this.player.pause();
|
||||
};
|
||||
if (this.player.getState().paused) this.player.play();
|
||||
else this.player.pause();
|
||||
}
|
||||
|
||||
private onReady() {
|
||||
this.player = SceneHelpers.getPlayer();
|
||||
@@ -81,7 +92,7 @@ export class ScenePlayerImpl extends React.Component<IScenePlayerProps, IScenePl
|
||||
|
||||
private onSeeked() {
|
||||
const position = this.player.getPosition();
|
||||
this.setState({scrubberPosition: position});
|
||||
this.setState({ scrubberPosition: position });
|
||||
this.player.play();
|
||||
}
|
||||
|
||||
@@ -90,7 +101,7 @@ export class ScenePlayerImpl extends React.Component<IScenePlayerProps, IScenePl
|
||||
const difference = Math.abs(position - this.lastTime);
|
||||
if (difference > 1) {
|
||||
this.lastTime = position;
|
||||
this.setState({scrubberPosition: position});
|
||||
this.setState({ scrubberPosition: position });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,16 +114,26 @@ export class ScenePlayerImpl extends React.Component<IScenePlayerProps, IScenePl
|
||||
}
|
||||
|
||||
private shouldRepeat(scene: GQL.SceneDataFragment) {
|
||||
const maxLoopDuration = this.props.config ? this.props.config.maximumLoopDuration : 0;
|
||||
return !!scene.file.duration && !!maxLoopDuration && scene.file.duration < maxLoopDuration;
|
||||
const maxLoopDuration = this.props.config
|
||||
? this.props.config.maximumLoopDuration
|
||||
: 0;
|
||||
return (
|
||||
!!scene.file.duration &&
|
||||
!!maxLoopDuration &&
|
||||
scene.file.duration < maxLoopDuration
|
||||
);
|
||||
}
|
||||
|
||||
private makeJWPlayerConfig(scene: GQL.SceneDataFragment) {
|
||||
if (!scene.paths.stream) { return {}; }
|
||||
if (!scene.paths.stream) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const repeat = this.shouldRepeat(scene);
|
||||
let getDurationHook: (() => GQL.Maybe<number>) | undefined;
|
||||
let seekHook: ((seekToPosition: number, _videoTag: any) => void) | undefined;
|
||||
let seekHook:
|
||||
| ((seekToPosition: number, _videoTag: any) => void)
|
||||
| undefined;
|
||||
let getCurrentTimeHook: ((_videoTag: any) => number) | undefined;
|
||||
|
||||
if (!this.props.scene.is_streamable) {
|
||||
@@ -124,14 +145,14 @@ export class ScenePlayerImpl extends React.Component<IScenePlayerProps, IScenePl
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
_videoTag.start = seekToPosition;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
_videoTag.src = (`${this.props.scene.paths.stream }?start=${ seekToPosition}`);
|
||||
_videoTag.src = `${this.props.scene.paths.stream}?start=${seekToPosition}`;
|
||||
_videoTag.play();
|
||||
};
|
||||
|
||||
getCurrentTimeHook = (_videoTag: any) => {
|
||||
const start = _videoTag.start || 0;
|
||||
return _videoTag.currentTime + start;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const ret = {
|
||||
@@ -140,21 +161,23 @@ export class ScenePlayerImpl extends React.Component<IScenePlayerProps, IScenePl
|
||||
tracks: [
|
||||
{
|
||||
file: scene.paths.vtt,
|
||||
kind: "thumbnails",
|
||||
kind: "thumbnails"
|
||||
},
|
||||
{
|
||||
file: scene.paths.chapters_vtt,
|
||||
kind: "chapters",
|
||||
},
|
||||
kind: "chapters"
|
||||
}
|
||||
],
|
||||
aspectratio: "16:9",
|
||||
width: "100%",
|
||||
floating: {
|
||||
dismissible: true,
|
||||
dismissible: true
|
||||
},
|
||||
cast: {},
|
||||
primary: "html5",
|
||||
autostart: this.props.autoplay || (this.props.config ? this.props.config.autostartVideo : false),
|
||||
autostart:
|
||||
this.props.autoplay ||
|
||||
(this.props.config ? this.props.config.autostartVideo : false),
|
||||
repeat,
|
||||
playbackRateControls: true,
|
||||
playbackRates: [0.75, 1, 1.5, 2, 3, 4],
|
||||
@@ -197,8 +220,19 @@ export class ScenePlayerImpl extends React.Component<IScenePlayerProps, IScenePl
|
||||
}
|
||||
}
|
||||
|
||||
export const ScenePlayer: React.FC<IScenePlayerProps> = (props: IScenePlayerProps) => {
|
||||
export const ScenePlayer: React.FC<IScenePlayerProps> = (
|
||||
props: IScenePlayerProps
|
||||
) => {
|
||||
const config = StashService.useConfiguration();
|
||||
|
||||
return <ScenePlayerImpl {...props} config={config.data && config.data.configuration ? config.data.configuration.interface : undefined}/>
|
||||
}
|
||||
return (
|
||||
<ScenePlayerImpl
|
||||
{...props}
|
||||
config={
|
||||
config.data && config.data.configuration
|
||||
? config.data.configuration.interface
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
|
||||
import React, { CSSProperties, useEffect, useRef, useState, useCallback } from "react";
|
||||
import { Button } from 'react-bootstrap';
|
||||
import React, {
|
||||
CSSProperties,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback
|
||||
} from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import axios from "axios";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { TextUtils } from "src/utils";
|
||||
@@ -23,36 +29,42 @@ interface ISceneSpriteItem {
|
||||
h: number;
|
||||
}
|
||||
|
||||
|
||||
async function fetchSpriteInfo(vttPath: string) {
|
||||
const response = await axios.get<string>(vttPath, {responseType: "text"});
|
||||
const response = await axios.get<string>(vttPath, { responseType: "text" });
|
||||
|
||||
// TODO: This is gnarly
|
||||
const lines = response.data.split("\n");
|
||||
if (lines.shift() !== "WEBVTT") { return; }
|
||||
if (lines.shift() !== "") { return; }
|
||||
let item: ISceneSpriteItem = {start: 0, end: 0, x: 0, y: 0, w: 0, h: 0};
|
||||
if (lines.shift() !== "WEBVTT") {
|
||||
return;
|
||||
}
|
||||
if (lines.shift() !== "") {
|
||||
return;
|
||||
}
|
||||
let item: ISceneSpriteItem = { start: 0, end: 0, x: 0, y: 0, w: 0, h: 0 };
|
||||
const newSpriteItems: ISceneSpriteItem[] = [];
|
||||
while (lines.length) {
|
||||
const line = lines.shift();
|
||||
if (line !== undefined) {
|
||||
if (line.includes("#") && line.includes("=") && line.includes(",")) {
|
||||
const size = line.split("#")[1].split("=")[1].split(",");
|
||||
const size = line
|
||||
.split("#")[1]
|
||||
.split("=")[1]
|
||||
.split(",");
|
||||
item.x = Number(size[0]);
|
||||
item.y = Number(size[1]);
|
||||
item.w = Number(size[2]);
|
||||
item.h = Number(size[3]);
|
||||
|
||||
newSpriteItems.push(item);
|
||||
item = {start: 0, end: 0, x: 0, y: 0, w: 0, h: 0};
|
||||
item = { start: 0, end: 0, x: 0, y: 0, w: 0, h: 0 };
|
||||
} else if (line.includes(" --> ")) {
|
||||
const times = line.split(" --> ");
|
||||
|
||||
const start = times[0].split(":");
|
||||
item.start = (+start[0]) * 60 * 60 + (+start[1]) * 60 + (+start[2]);
|
||||
item.start = +start[0] * 60 * 60 + +start[1] * 60 + +start[2];
|
||||
|
||||
const end = times[1].split(":");
|
||||
item.end = (+end[0]) * 60 * 60 + (+end[1]) * 60 + (+end[2]);
|
||||
item.end = +end[0] * 60 * 60 + +end[1] * 60 + +end[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,8 +72,9 @@ async function fetchSpriteInfo(vttPath: string) {
|
||||
return newSpriteItems;
|
||||
}
|
||||
|
||||
|
||||
export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props: IScenePlayerScrubberProps) => {
|
||||
export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (
|
||||
props: IScenePlayerScrubberProps
|
||||
) => {
|
||||
const contentEl = useRef<HTMLDivElement>(null);
|
||||
const positionIndicatorEl = useRef<HTMLDivElement>(null);
|
||||
const scrubberSliderEl = useRef<HTMLDivElement>(null);
|
||||
@@ -72,9 +85,14 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
|
||||
|
||||
const _position = useRef(0);
|
||||
const getPosition = useCallback(() => _position.current, []);
|
||||
const setPosition = useCallback((newPostion: number, shouldEmit: boolean = true) => {
|
||||
if (!scrubberSliderEl.current || !positionIndicatorEl.current) { return; }
|
||||
if (shouldEmit) { props.onScrolled(); }
|
||||
const setPosition = useCallback(
|
||||
(newPostion: number, shouldEmit: boolean = true) => {
|
||||
if (!scrubberSliderEl.current || !positionIndicatorEl.current) {
|
||||
return;
|
||||
}
|
||||
if (shouldEmit) {
|
||||
props.onScrolled();
|
||||
}
|
||||
|
||||
const midpointOffset = scrubberSliderEl.current.clientWidth / 2;
|
||||
|
||||
@@ -89,35 +107,41 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
|
||||
|
||||
scrubberSliderEl.current.style.transform = `translateX(${_position.current}px)`;
|
||||
|
||||
const indicatorPosition = (
|
||||
(newPostion - midpointOffset) / (bounds - (midpointOffset * 2)) * scrubberSliderEl.current.clientWidth
|
||||
);
|
||||
const indicatorPosition =
|
||||
((newPostion - midpointOffset) / (bounds - midpointOffset * 2)) *
|
||||
scrubberSliderEl.current.clientWidth;
|
||||
positionIndicatorEl.current.style.transform = `translateX(${indicatorPosition}px)`;
|
||||
}, [props]);
|
||||
},
|
||||
[props]
|
||||
);
|
||||
|
||||
const [spriteItems, setSpriteItems] = useState<ISceneSpriteItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrubberSliderEl.current) { return; }
|
||||
scrubberSliderEl.current.style.transform = `translateX(${scrubberSliderEl.current.clientWidth / 2}px)`;
|
||||
if (!scrubberSliderEl.current) {
|
||||
return;
|
||||
}
|
||||
scrubberSliderEl.current.style.transform = `translateX(${scrubberSliderEl
|
||||
.current.clientWidth / 2}px)`;
|
||||
}, [scrubberSliderEl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.scene.paths.vtt)
|
||||
return;
|
||||
fetchSpriteInfo(props.scene.paths.vtt).then((sprites) => {
|
||||
if(sprites)
|
||||
setSpriteItems(sprites);
|
||||
if (!props.scene.paths.vtt) return;
|
||||
fetchSpriteInfo(props.scene.paths.vtt).then(sprites => {
|
||||
if (sprites) setSpriteItems(sprites);
|
||||
});
|
||||
}, [props.scene]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrubberSliderEl.current) { return; }
|
||||
if (!scrubberSliderEl.current) {
|
||||
return;
|
||||
}
|
||||
const duration = Number(props.scene.file.duration);
|
||||
const percentage = props.position / duration;
|
||||
const position = (
|
||||
(scrubberSliderEl.current.scrollWidth * percentage) - (scrubberSliderEl.current.clientWidth / 2)
|
||||
) * -1;
|
||||
const position =
|
||||
(scrubberSliderEl.current.scrollWidth * percentage -
|
||||
scrubberSliderEl.current.clientWidth / 2) *
|
||||
-1;
|
||||
setPosition(position, false);
|
||||
}, [props.position, props.scene.file.duration, setPosition]);
|
||||
|
||||
@@ -129,37 +153,48 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!contentEl.current) { return; }
|
||||
if (!contentEl.current) {
|
||||
return;
|
||||
}
|
||||
const el = contentEl.current;
|
||||
el.addEventListener("mousedown", onMouseDown, false);
|
||||
return () => {
|
||||
if (!el) { return; }
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
el.removeEventListener("mousedown", onMouseDown);
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!contentEl.current) { return; }
|
||||
if (!contentEl.current) {
|
||||
return;
|
||||
}
|
||||
const el = contentEl.current;
|
||||
el.addEventListener("mousemove", onMouseMove, false);
|
||||
return () => {
|
||||
if (!el) { return; }
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
el.removeEventListener("mousemove", onMouseMove);
|
||||
};
|
||||
});
|
||||
|
||||
function onMouseUp(this: Window, event: MouseEvent) {
|
||||
if (!startMouseEvent.current || !scrubberSliderEl.current) { return; }
|
||||
if (!startMouseEvent.current || !scrubberSliderEl.current) {
|
||||
return;
|
||||
}
|
||||
mouseDown.current = false;
|
||||
const delta = Math.abs(event.clientX - startMouseEvent.current.clientX);
|
||||
if (delta < 1 && event.target instanceof HTMLDivElement) {
|
||||
const {target} = event;
|
||||
const { target } = event;
|
||||
let seekSeconds: number | undefined;
|
||||
|
||||
const spriteIdString = target.getAttribute("data-sprite-item-id");
|
||||
if (spriteIdString != null) {
|
||||
const spritePercentage = event.offsetX / target.clientWidth;
|
||||
const offset = target.offsetLeft + (target.clientWidth * spritePercentage);
|
||||
const offset =
|
||||
target.offsetLeft + target.clientWidth * spritePercentage;
|
||||
const percentage = offset / scrubberSliderEl.current.scrollWidth;
|
||||
seekSeconds = percentage * (props.scene.file.duration || 0);
|
||||
}
|
||||
@@ -170,9 +205,11 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
|
||||
seekSeconds = marker.seconds;
|
||||
}
|
||||
|
||||
if (seekSeconds) { props.onSeek(seekSeconds); }
|
||||
if (seekSeconds) {
|
||||
props.onSeek(seekSeconds);
|
||||
}
|
||||
} else if (Math.abs(velocity.current) > 25) {
|
||||
const newPosition = getPosition() + (velocity.current * 10);
|
||||
const newPosition = getPosition() + velocity.current * 10;
|
||||
setPosition(newPosition);
|
||||
velocity.current = 0;
|
||||
}
|
||||
@@ -187,7 +224,9 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
|
||||
}
|
||||
|
||||
function onMouseMove(this: HTMLDivElement, event: MouseEvent) {
|
||||
if (!mouseDown.current) { return; }
|
||||
if (!mouseDown.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// negative dragging right (past), positive left (future)
|
||||
const delta = event.clientX - lastMouseEvent.current.clientX;
|
||||
@@ -201,30 +240,45 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
|
||||
}
|
||||
|
||||
function getBounds(): number {
|
||||
if (!scrubberSliderEl.current || !positionIndicatorEl.current) { return 0; }
|
||||
return scrubberSliderEl.current.scrollWidth - scrubberSliderEl.current.clientWidth;
|
||||
if (!scrubberSliderEl.current || !positionIndicatorEl.current) {
|
||||
return 0;
|
||||
}
|
||||
return (
|
||||
scrubberSliderEl.current.scrollWidth -
|
||||
scrubberSliderEl.current.clientWidth
|
||||
);
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (!scrubberSliderEl.current) { return; }
|
||||
if (!scrubberSliderEl.current) {
|
||||
return;
|
||||
}
|
||||
const newPosition = getPosition() + scrubberSliderEl.current.clientWidth;
|
||||
setPosition(newPosition);
|
||||
}
|
||||
|
||||
function goForward() {
|
||||
if (!scrubberSliderEl.current) { return; }
|
||||
if (!scrubberSliderEl.current) {
|
||||
return;
|
||||
}
|
||||
const newPosition = getPosition() - scrubberSliderEl.current.clientWidth;
|
||||
setPosition(newPosition);
|
||||
}
|
||||
|
||||
function renderTags() {
|
||||
function getTagStyle(i: number): CSSProperties {
|
||||
if (!scrubberSliderEl.current ||
|
||||
if (
|
||||
!scrubberSliderEl.current ||
|
||||
spriteItems.length === 0 ||
|
||||
getBounds() === 0) { return {}; }
|
||||
getBounds() === 0
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const tags = window.document.getElementsByClassName("scrubber-tag");
|
||||
if (tags.length === 0) { return {}; }
|
||||
if (tags.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let tag: any;
|
||||
for (let index = 0; index < tags.length; index++) {
|
||||
@@ -239,16 +293,17 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
|
||||
const duration = Number(props.scene.file.duration);
|
||||
const percentage = marker.seconds / duration;
|
||||
|
||||
const left = (scrubberSliderEl.current.scrollWidth * percentage) - (tag.clientWidth / 2);
|
||||
const left =
|
||||
scrubberSliderEl.current.scrollWidth * percentage - tag.clientWidth / 2;
|
||||
return {
|
||||
left: `${left}px`,
|
||||
height: 20,
|
||||
height: 20
|
||||
};
|
||||
}
|
||||
|
||||
return props.scene.scene_markers.map((marker, index) => {
|
||||
const dataAttrs = {
|
||||
"data-marker-id": index,
|
||||
"data-marker-id": index
|
||||
};
|
||||
return (
|
||||
<div
|
||||
@@ -265,7 +320,9 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
|
||||
|
||||
function renderSprites() {
|
||||
function getStyleForSprite(index: number): CSSProperties {
|
||||
if (!props.scene.paths.vtt) { return {}; }
|
||||
if (!props.scene.paths.vtt) {
|
||||
return {};
|
||||
}
|
||||
const sprite = spriteItems[index];
|
||||
const left = sprite.w * index;
|
||||
const path = props.scene.paths.vtt.replace("_thumbs.vtt", "_sprite.jpg"); // TODO: Gnarly
|
||||
@@ -273,15 +330,15 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
|
||||
width: `${sprite.w}px`,
|
||||
height: `${sprite.h}px`,
|
||||
margin: "0px auto",
|
||||
backgroundPosition: `${-sprite.x }px ${ -sprite.y }px`,
|
||||
backgroundPosition: `${-sprite.x}px ${-sprite.y}px`,
|
||||
backgroundImage: `url(${path})`,
|
||||
left: `${left}px`,
|
||||
left: `${left}px`
|
||||
};
|
||||
}
|
||||
|
||||
return spriteItems.map((spriteItem, index) => {
|
||||
const dataAttrs = {
|
||||
"data-sprite-item-id": index,
|
||||
"data-sprite-item-id": index
|
||||
};
|
||||
return (
|
||||
<div
|
||||
@@ -290,7 +347,10 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
|
||||
style={getStyleForSprite(index)}
|
||||
{...dataAttrs}
|
||||
>
|
||||
<span>{TextUtils.secondsToTimestamp(spriteItem.start)} - {TextUtils.secondsToTimestamp(spriteItem.end)}</span>
|
||||
<span>
|
||||
{TextUtils.secondsToTimestamp(spriteItem.start)} -{" "}
|
||||
{TextUtils.secondsToTimestamp(spriteItem.end)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -298,21 +358,32 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
|
||||
|
||||
return (
|
||||
<div className="scrubber-wrapper">
|
||||
<Button variant="link" className="scrubber-button" id="scrubber-back" onClick={() => goBack()}><</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
className="scrubber-button"
|
||||
id="scrubber-back"
|
||||
onClick={() => goBack()}
|
||||
>
|
||||
<
|
||||
</Button>
|
||||
<div ref={contentEl} className="scrubber-content">
|
||||
<div className="scrubber-tags-background" />
|
||||
<div ref={positionIndicatorEl} id="scrubber-position-indicator" />
|
||||
<div id="scrubber-current-position" />
|
||||
<div className="scrubber-viewport">
|
||||
<div ref={scrubberSliderEl} className="scrubber-slider">
|
||||
<div className="scrubber-tags">
|
||||
{renderTags()}
|
||||
</div>
|
||||
<div className="scrubber-tags">{renderTags()}</div>
|
||||
{renderSprites()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="scrubber-button" id="scrubber-forward" onClick={() => goForward()}>></Button>
|
||||
<Button
|
||||
className="scrubber-button"
|
||||
id="scrubber-forward"
|
||||
onClick={() => goForward()}
|
||||
>
|
||||
>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, ButtonGroup, Form, Spinner } from 'react-bootstrap';
|
||||
import { Button, ButtonGroup, Form, Spinner } from "react-bootstrap";
|
||||
import _ from "lodash";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
@@ -7,15 +7,19 @@ import { FilterSelect, StudioSelect } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
|
||||
interface IListOperationProps {
|
||||
selected: GQL.SlimSceneDataFragment[],
|
||||
selected: GQL.SlimSceneDataFragment[];
|
||||
onScenesUpdated: () => void;
|
||||
}
|
||||
|
||||
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 [studioId, setStudioId] = useState<string | undefined>(undefined);
|
||||
const [performerIds, setPerformerIds] = useState<string[] | undefined>(undefined);
|
||||
const [performerIds, setPerformerIds] = useState<string[] | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [tagIds, setTagIds] = useState<string[] | undefined>(undefined);
|
||||
|
||||
const updateScenes = StashService.useBulkSceneUpdate(getSceneInput());
|
||||
@@ -23,15 +27,15 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
function getSceneInput() : GQL.BulkSceneUpdateInput {
|
||||
function getSceneInput(): GQL.BulkSceneUpdateInput {
|
||||
// need to determine what we are actually setting on each scene
|
||||
const aggregateRating = getRating(props.selected);
|
||||
const aggregateStudioId = getStudioId(props.selected);
|
||||
const aggregatePerformerIds = getPerformerIds(props.selected);
|
||||
const aggregateTagIds = getTagIds(props.selected);
|
||||
|
||||
const sceneInput : GQL.BulkSceneUpdateInput = {
|
||||
ids: props.selected.map((scene) => {
|
||||
const sceneInput: GQL.BulkSceneUpdateInput = {
|
||||
ids: props.selected.map(scene => {
|
||||
return scene.id;
|
||||
})
|
||||
};
|
||||
@@ -39,7 +43,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
||||
// if rating is undefined
|
||||
if (rating === "") {
|
||||
// and all scenes have the same rating, then we are unsetting the rating.
|
||||
if(aggregateRating) {
|
||||
if (aggregateRating) {
|
||||
// an undefined rating is ignored in the server, so set it to 0 instead
|
||||
sceneInput.rating = 0;
|
||||
}
|
||||
@@ -102,10 +106,10 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
||||
}
|
||||
|
||||
function getRating(state: GQL.SlimSceneDataFragment[]) {
|
||||
let ret : number | undefined;
|
||||
let ret: number | undefined;
|
||||
let first = true;
|
||||
|
||||
state.forEach((scene : GQL.SlimSceneDataFragment) => {
|
||||
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
||||
if (first) {
|
||||
ret = scene.rating;
|
||||
first = false;
|
||||
@@ -118,10 +122,10 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
||||
}
|
||||
|
||||
function getStudioId(state: GQL.SlimSceneDataFragment[]) {
|
||||
let ret : string | undefined;
|
||||
let ret: string | undefined;
|
||||
let first = true;
|
||||
|
||||
state.forEach((scene : GQL.SlimSceneDataFragment) => {
|
||||
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
||||
if (first) {
|
||||
ret = scene?.studio?.id;
|
||||
first = false;
|
||||
@@ -137,15 +141,17 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
||||
}
|
||||
|
||||
function getPerformerIds(state: GQL.SlimSceneDataFragment[]) {
|
||||
let ret : string[] = [];
|
||||
let ret: string[] = [];
|
||||
let first = true;
|
||||
|
||||
state.forEach((scene : GQL.SlimSceneDataFragment) => {
|
||||
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
||||
if (first) {
|
||||
ret = scene.performers ? scene.performers.map(p => p.id).sort() : [];
|
||||
first = false;
|
||||
} else {
|
||||
const perfIds = scene.performers ? scene.performers.map(p => p.id).sort() : [];
|
||||
const perfIds = scene.performers
|
||||
? scene.performers.map(p => p.id).sort()
|
||||
: [];
|
||||
|
||||
if (!_.isEqual(ret, perfIds)) {
|
||||
ret = [];
|
||||
@@ -157,10 +163,10 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
||||
}
|
||||
|
||||
function getTagIds(state: GQL.SlimSceneDataFragment[]) {
|
||||
let ret : string[] = [];
|
||||
let ret: string[] = [];
|
||||
let first = true;
|
||||
|
||||
state.forEach((scene : GQL.SlimSceneDataFragment) => {
|
||||
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
||||
if (first) {
|
||||
ret = scene.tags ? scene.tags.map(t => t.id).sort() : [];
|
||||
first = false;
|
||||
@@ -178,19 +184,21 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
||||
|
||||
function updateScenesEditState(state: GQL.SlimSceneDataFragment[]) {
|
||||
let updateRating = "";
|
||||
let updateStudioId : string | undefined;
|
||||
let updatePerformerIds : string[] = [];
|
||||
let updateTagIds : string[] = [];
|
||||
let updateStudioId: string | undefined;
|
||||
let updatePerformerIds: string[] = [];
|
||||
let updateTagIds: string[] = [];
|
||||
let first = true;
|
||||
|
||||
state.forEach((scene : GQL.SlimSceneDataFragment) => {
|
||||
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
||||
const thisRating = scene.rating ? scene.rating.toString() : "";
|
||||
const thisStudio = scene.studio ? scene.studio.id : undefined;
|
||||
|
||||
if (first) {
|
||||
updateRating = thisRating;
|
||||
updateStudioId = thisStudio;
|
||||
updatePerformerIds = scene.performers ? scene.performers.map(p => p.id).sort() : [];
|
||||
updatePerformerIds = scene.performers
|
||||
? scene.performers.map(p => p.id).sort()
|
||||
: [];
|
||||
updateTagIds = scene.tags ? scene.tags.map(p => p.id).sort() : [];
|
||||
first = false;
|
||||
} else {
|
||||
@@ -200,7 +208,9 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
||||
if (studioId !== thisStudio) {
|
||||
updateStudioId = undefined;
|
||||
}
|
||||
const perfIds = scene.performers ? scene.performers.map(p => p.id).sort() : [];
|
||||
const perfIds = scene.performers
|
||||
? scene.performers.map(p => p.id).sort()
|
||||
: [];
|
||||
const tIds = scene.tags ? scene.tags.map(t => t.id).sort() : [];
|
||||
|
||||
if (!_.isEqual(performerIds, perfIds)) {
|
||||
@@ -223,16 +233,23 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
||||
updateScenesEditState(props.selected);
|
||||
}, [props.selected]);
|
||||
|
||||
function renderMultiSelect(type: "performers" | "tags", initialIds: string[] | undefined) {
|
||||
function renderMultiSelect(
|
||||
type: "performers" | "tags",
|
||||
initialIds: string[] | undefined
|
||||
) {
|
||||
return (
|
||||
<FilterSelect
|
||||
type={type}
|
||||
isMulti
|
||||
onSelect={(items) => {
|
||||
const ids = items.map((i) => i.id);
|
||||
onSelect={items => {
|
||||
const ids = items.map(i => i.id);
|
||||
switch (type) {
|
||||
case "performers": setPerformerIds(ids); break;
|
||||
case "tags": setTagIds(ids); break;
|
||||
case "performers":
|
||||
setPerformerIds(ids);
|
||||
break;
|
||||
case "tags":
|
||||
setTagIds(ids);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
initialIds={initialIds ?? []}
|
||||
@@ -249,17 +266,20 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
||||
<Form.Label>Rating</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
onChange={(event: any) => setRating(event.target.value)}>
|
||||
{ ["", 1, 2, 3, 4, 5].map(opt => (
|
||||
<option selected={opt === rating} value={opt}>{opt}</option>
|
||||
)) }
|
||||
onChange={(event: any) => setRating(event.target.value)}
|
||||
>
|
||||
{["", 1, 2, 3, 4, 5].map(opt => (
|
||||
<option selected={opt === rating} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="studio" className="operation-item">
|
||||
<Form.Label>Studio</Form.Label>
|
||||
<StudioSelect
|
||||
onSelect={(items) => setStudioId(items[0]?.id)}
|
||||
onSelect={items => setStudioId(items[0]?.id)}
|
||||
initialIds={studioId ? [studioId] : []}
|
||||
/>
|
||||
</Form.Group>
|
||||
@@ -275,9 +295,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (props: IList
|
||||
</Form.Group>
|
||||
|
||||
<ButtonGroup className="operation-item">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onSave}>
|
||||
<Button variant="primary" onClick={onSave}>
|
||||
Apply
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { } from "react";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
|
||||
@@ -7,8 +7,7 @@ export class SceneHelpers {
|
||||
scene: GQL.SceneDataFragment | GQL.SlimSceneDataFragment,
|
||||
height: number
|
||||
) {
|
||||
if (!scene.studio)
|
||||
return;
|
||||
if (!scene.studio) return;
|
||||
const style: React.CSSProperties = {
|
||||
backgroundImage: `url('${scene.studio.image_path}')`,
|
||||
width: "100%",
|
||||
@@ -17,17 +16,14 @@ export class SceneHelpers {
|
||||
backgroundSize: "contain",
|
||||
display: "inline-block",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundRepeat: "no-repeat"
|
||||
};
|
||||
return (
|
||||
<Link
|
||||
to={`/studios/${scene.studio.id}`}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
return <Link to={`/studios/${scene.studio.id}`} style={style} />;
|
||||
}
|
||||
|
||||
public static getJWPlayerId(): string { return "main-jwplayer"; }
|
||||
public static getJWPlayerId(): string {
|
||||
return "main-jwplayer";
|
||||
}
|
||||
public static getPlayer(): any {
|
||||
return (window as any).jwplayer("main-jwplayer");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ApolloClient from "apollo-client";
|
||||
import { WebSocketLink } from 'apollo-link-ws';
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||
import { WebSocketLink } from "apollo-link-ws";
|
||||
import { InMemoryCache } from "apollo-cache-inmemory";
|
||||
import { HttpLink, split } from "apollo-boost";
|
||||
import { getMainDefinition } from "apollo-utilities";
|
||||
import { ListFilterModel } from "../models/list-filter/filter";
|
||||
@@ -27,27 +27,27 @@ export class StashService {
|
||||
if (platformUrl.protocol === "https:") {
|
||||
wsPlatformUrl.protocol = "wss:";
|
||||
}
|
||||
const url = `${platformUrl.toString().slice(0, -1) }/graphql`;
|
||||
const wsUrl = `${wsPlatformUrl.toString().slice(0, -1) }/graphql`;
|
||||
const url = `${platformUrl.toString().slice(0, -1)}/graphql`;
|
||||
const wsUrl = `${wsPlatformUrl.toString().slice(0, -1)}/graphql`;
|
||||
|
||||
const httpLink = new HttpLink({
|
||||
uri: url,
|
||||
uri: url
|
||||
});
|
||||
|
||||
const wsLink = new WebSocketLink({
|
||||
uri: wsUrl,
|
||||
options: {
|
||||
reconnect: true
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
const link = split(
|
||||
({ query }) => {
|
||||
const { kind, operation } = getMainDefinition(query);
|
||||
return kind === 'OperationDefinition' && operation === 'subscription';
|
||||
return kind === "OperationDefinition" && operation === "subscription";
|
||||
},
|
||||
wsLink,
|
||||
httpLink,
|
||||
httpLink
|
||||
);
|
||||
|
||||
StashService.cache = new InMemoryCache();
|
||||
@@ -64,18 +64,20 @@ export class StashService {
|
||||
StashService.client.resetStore();
|
||||
}
|
||||
|
||||
private static invalidateQueries(queries : string[]) {
|
||||
private static invalidateQueries(queries: string[]) {
|
||||
if (StashService.cache) {
|
||||
const cache = StashService.cache as any;
|
||||
const keyMatchers = queries.map(query => {
|
||||
return new RegExp(`^${ query}`);
|
||||
return new RegExp(`^${query}`);
|
||||
});
|
||||
|
||||
const rootQuery = cache.data.data.ROOT_QUERY;
|
||||
Object.keys(rootQuery).forEach(key => {
|
||||
if (keyMatchers.some(matcher => {
|
||||
if (
|
||||
keyMatchers.some(matcher => {
|
||||
return !!key.match(matcher);
|
||||
})) {
|
||||
})
|
||||
) {
|
||||
delete rootQuery[key];
|
||||
}
|
||||
});
|
||||
@@ -85,8 +87,8 @@ export class StashService {
|
||||
public static useFindGalleries(filter: ListFilterModel) {
|
||||
return GQL.useFindGalleries({
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
},
|
||||
filter: filter.makeFindFilter()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -104,8 +106,8 @@ export class StashService {
|
||||
return GQL.useFindScenes({
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
scene_filter: sceneFilter,
|
||||
},
|
||||
scene_filter: sceneFilter
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,7 +119,7 @@ export class StashService {
|
||||
query: GQL.FindScenesDocument,
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
scene_filter: sceneFilter,
|
||||
scene_filter: sceneFilter
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -136,8 +138,8 @@ export class StashService {
|
||||
return GQL.useFindSceneMarkers({
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
scene_marker_filter: sceneMarkerFilter,
|
||||
},
|
||||
scene_marker_filter: sceneMarkerFilter
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -149,7 +151,7 @@ export class StashService {
|
||||
query: GQL.FindSceneMarkersDocument,
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
scene_marker_filter: sceneMarkerFilter,
|
||||
scene_marker_filter: sceneMarkerFilter
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -157,8 +159,8 @@ export class StashService {
|
||||
public static useFindStudios(filter: ListFilterModel) {
|
||||
return GQL.useFindStudios({
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
},
|
||||
filter: filter.makeFindFilter()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -176,8 +178,8 @@ export class StashService {
|
||||
return GQL.useFindPerformers({
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
performer_filter: performerFilter,
|
||||
},
|
||||
performer_filter: performerFilter
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -189,20 +191,24 @@ export class StashService {
|
||||
query: GQL.FindPerformersDocument,
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
performer_filter: performerFilter,
|
||||
performer_filter: performerFilter
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static useFindGallery(id: string) { return GQL.useFindGallery({variables: {id}}); }
|
||||
public static useFindScene(id: string) { return GQL.useFindScene({variables: {id}}); }
|
||||
public static useFindGallery(id: string) {
|
||||
return GQL.useFindGallery({ variables: { id } });
|
||||
}
|
||||
public static useFindScene(id: string) {
|
||||
return GQL.useFindScene({ variables: { id } });
|
||||
}
|
||||
public static useFindPerformer(id: string) {
|
||||
const skip = id === "new";
|
||||
return GQL.useFindPerformer({variables: {id}, skip});
|
||||
return GQL.useFindPerformer({ variables: { id }, skip });
|
||||
}
|
||||
public static useFindStudio(id: string) {
|
||||
const skip = id === "new";
|
||||
return GQL.useFindStudio({variables: {id}, skip});
|
||||
return GQL.useFindStudio({ variables: { id }, skip });
|
||||
}
|
||||
|
||||
// TODO - scene marker manipulation functions are handled differently
|
||||
@@ -226,31 +232,59 @@ export class StashService {
|
||||
public static useListPerformerScrapers() {
|
||||
return GQL.useListPerformerScrapers();
|
||||
}
|
||||
public static useScrapePerformerList(scraperId: string, q : string) {
|
||||
return GQL.useScrapePerformerList({ variables: { scraper_id: scraperId, query: q }, skip: q === ''});
|
||||
public static useScrapePerformerList(scraperId: string, q: string) {
|
||||
return GQL.useScrapePerformerList({
|
||||
variables: { scraper_id: scraperId, query: q },
|
||||
skip: q === ""
|
||||
});
|
||||
}
|
||||
public static useScrapePerformer(scraperId: string, scrapedPerformer : GQL.ScrapedPerformerInput) {
|
||||
return GQL.useScrapePerformer({ variables: { scraper_id: scraperId, scraped_performer: scrapedPerformer }});
|
||||
public static useScrapePerformer(
|
||||
scraperId: string,
|
||||
scrapedPerformer: GQL.ScrapedPerformerInput
|
||||
) {
|
||||
return GQL.useScrapePerformer({
|
||||
variables: { scraper_id: scraperId, scraped_performer: scrapedPerformer }
|
||||
});
|
||||
}
|
||||
|
||||
public static useListSceneScrapers() {
|
||||
return GQL.useListSceneScrapers();
|
||||
}
|
||||
|
||||
public static useScrapeFreeonesPerformers(q: string) { return GQL.useScrapeFreeonesPerformers({ variables: { q } }); }
|
||||
public static useMarkerStrings() { return GQL.useMarkerStrings(); }
|
||||
public static useAllTags() { return GQL.useAllTags(); }
|
||||
public static useAllTagsForFilter() { return GQL.useAllTagsForFilter(); }
|
||||
public static useAllPerformersForFilter() { return GQL.useAllPerformersForFilter(); }
|
||||
public static useAllStudiosForFilter() { return GQL.useAllStudiosForFilter(); }
|
||||
public static useValidGalleriesForScene(sceneId: string) {
|
||||
return GQL.useValidGalleriesForScene({variables: {scene_id: sceneId}});
|
||||
public static useScrapeFreeonesPerformers(q: string) {
|
||||
return GQL.useScrapeFreeonesPerformers({ variables: { q } });
|
||||
}
|
||||
public static useMarkerStrings() {
|
||||
return GQL.useMarkerStrings();
|
||||
}
|
||||
public static useAllTags() {
|
||||
return GQL.useAllTags();
|
||||
}
|
||||
public static useAllTagsForFilter() {
|
||||
return GQL.useAllTagsForFilter();
|
||||
}
|
||||
public static useAllPerformersForFilter() {
|
||||
return GQL.useAllPerformersForFilter();
|
||||
}
|
||||
public static useAllStudiosForFilter() {
|
||||
return GQL.useAllStudiosForFilter();
|
||||
}
|
||||
public static useValidGalleriesForScene(sceneId: string) {
|
||||
return GQL.useValidGalleriesForScene({ variables: { scene_id: sceneId } });
|
||||
}
|
||||
public static useStats() {
|
||||
return GQL.useStats();
|
||||
}
|
||||
public static useVersion() {
|
||||
return GQL.useVersion();
|
||||
}
|
||||
public static useStats() { return GQL.useStats(); }
|
||||
public static useVersion() { return GQL.useVersion(); }
|
||||
|
||||
public static useConfiguration() { return GQL.useConfiguration(); }
|
||||
public static useDirectories(path?: string) { return GQL.useDirectories({ variables: { path }}); }
|
||||
public static useConfiguration() {
|
||||
return GQL.useConfiguration();
|
||||
}
|
||||
public static useDirectories(path?: string) {
|
||||
return GQL.useDirectories({ variables: { path } });
|
||||
}
|
||||
|
||||
private static performerMutationImpactedQueries = [
|
||||
"findPerformers",
|
||||
@@ -262,19 +296,28 @@ export class StashService {
|
||||
public static usePerformerCreate(input: GQL.PerformerCreateInput) {
|
||||
return GQL.usePerformerCreate({
|
||||
variables: input,
|
||||
update: () => StashService.invalidateQueries(StashService.performerMutationImpactedQueries)
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.performerMutationImpactedQueries
|
||||
)
|
||||
});
|
||||
}
|
||||
public static usePerformerUpdate(input: GQL.PerformerUpdateInput) {
|
||||
return GQL.usePerformerUpdate({
|
||||
variables: input,
|
||||
update: () => StashService.invalidateQueries(StashService.performerMutationImpactedQueries)
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.performerMutationImpactedQueries
|
||||
)
|
||||
});
|
||||
}
|
||||
public static usePerformerDestroy(input: GQL.PerformerDestroyInput) {
|
||||
return GQL.usePerformerDestroy({
|
||||
variables: input,
|
||||
update: () => StashService.invalidateQueries(StashService.performerMutationImpactedQueries)
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.performerMutationImpactedQueries
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -290,7 +333,10 @@ export class StashService {
|
||||
public static useSceneUpdate(input: GQL.SceneUpdateInput) {
|
||||
return GQL.useSceneUpdate({
|
||||
variables: input,
|
||||
update: () => StashService.invalidateQueries(StashService.sceneMutationImpactedQueries),
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.sceneMutationImpactedQueries
|
||||
),
|
||||
refetchQueries: ["AllTagsForFilter"]
|
||||
});
|
||||
}
|
||||
@@ -307,18 +353,24 @@ export class StashService {
|
||||
public static useBulkSceneUpdate(input: GQL.BulkSceneUpdateInput) {
|
||||
return GQL.useBulkSceneUpdate({
|
||||
variables: input,
|
||||
update: () => StashService.invalidateQueries(StashService.sceneBulkMutationImpactedQueries)
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.sceneBulkMutationImpactedQueries
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
public static useScenesUpdate(input: GQL.SceneUpdateInput[]) {
|
||||
return GQL.useScenesUpdate({ variables: { input }});
|
||||
return GQL.useScenesUpdate({ variables: { input } });
|
||||
}
|
||||
|
||||
public static useSceneDestroy(input: GQL.SceneDestroyInput) {
|
||||
return GQL.useSceneDestroy({
|
||||
variables: input,
|
||||
update: () => StashService.invalidateQueries(StashService.sceneMutationImpactedQueries)
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.sceneMutationImpactedQueries
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -331,21 +383,30 @@ export class StashService {
|
||||
public static useStudioCreate(input: GQL.StudioCreateInput) {
|
||||
return GQL.useStudioCreate({
|
||||
variables: input,
|
||||
update: () => StashService.invalidateQueries(StashService.studioMutationImpactedQueries)
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.studioMutationImpactedQueries
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
public static useStudioUpdate(input: GQL.StudioUpdateInput) {
|
||||
return GQL.useStudioUpdate({
|
||||
variables: input,
|
||||
update: () => StashService.invalidateQueries(StashService.studioMutationImpactedQueries)
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.studioMutationImpactedQueries
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
public static useStudioDestroy(input: GQL.StudioDestroyInput) {
|
||||
return GQL.useStudioDestroy({
|
||||
variables: input,
|
||||
update: () => StashService.invalidateQueries(StashService.studioMutationImpactedQueries)
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.studioMutationImpactedQueries
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -359,30 +420,37 @@ export class StashService {
|
||||
public static useTagCreate(input: GQL.TagCreateInput) {
|
||||
return GQL.useTagCreate({
|
||||
variables: input,
|
||||
refetchQueries: ["AllTags", "AllTagsForFilter"],
|
||||
refetchQueries: ["AllTags", "AllTagsForFilter"]
|
||||
// update: () => StashService.invalidateQueries(StashService.tagMutationImpactedQueries)
|
||||
});
|
||||
}
|
||||
public static useTagUpdate(input: GQL.TagUpdateInput) {
|
||||
return GQL.useTagUpdate({
|
||||
variables: input,
|
||||
refetchQueries: ["AllTags", "AllTagsForFilter"],
|
||||
refetchQueries: ["AllTags", "AllTagsForFilter"]
|
||||
});
|
||||
}
|
||||
public static useTagDestroy(input: GQL.TagDestroyInput) {
|
||||
return GQL.useTagDestroy({
|
||||
variables: input,
|
||||
refetchQueries: ["AllTags", "AllTagsForFilter"],
|
||||
update: () => StashService.invalidateQueries(StashService.tagMutationImpactedQueries)
|
||||
update: () =>
|
||||
StashService.invalidateQueries(StashService.tagMutationImpactedQueries)
|
||||
});
|
||||
}
|
||||
|
||||
public static useConfigureGeneral(input: GQL.ConfigGeneralInput) {
|
||||
return GQL.useConfigureGeneral({ variables: { input }, refetchQueries: ["Configuration"] });
|
||||
return GQL.useConfigureGeneral({
|
||||
variables: { input },
|
||||
refetchQueries: ["Configuration"]
|
||||
});
|
||||
}
|
||||
|
||||
public static useConfigureInterface(input: GQL.ConfigInterfaceInput) {
|
||||
return GQL.useConfigureInterface({ variables: { input }, refetchQueries: ["Configuration"] });
|
||||
return GQL.useConfigureInterface({
|
||||
variables: { input },
|
||||
refetchQueries: ["Configuration"]
|
||||
});
|
||||
}
|
||||
|
||||
public static useMetadataUpdate() {
|
||||
@@ -395,20 +463,20 @@ export class StashService {
|
||||
|
||||
public static useLogs() {
|
||||
return GQL.useLogs({
|
||||
fetchPolicy: 'no-cache'
|
||||
fetchPolicy: "no-cache"
|
||||
});
|
||||
}
|
||||
|
||||
public static useJobStatus() {
|
||||
return GQL.useJobStatus({
|
||||
fetchPolicy: 'no-cache'
|
||||
fetchPolicy: "no-cache"
|
||||
});
|
||||
}
|
||||
|
||||
public static queryStopJob() {
|
||||
return StashService.client.query<GQL.StopJobQuery>({
|
||||
query: GQL.StopJobDocument,
|
||||
fetchPolicy: "network-only",
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -416,18 +484,21 @@ export class StashService {
|
||||
return StashService.client.query<GQL.ScrapeFreeonesQuery>({
|
||||
query: GQL.ScrapeFreeonesDocument,
|
||||
variables: {
|
||||
performer_name: performerName,
|
||||
},
|
||||
performer_name: performerName
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static queryScrapePerformer(scraperId: string, scrapedPerformer: GQL.ScrapedPerformerInput) {
|
||||
public static queryScrapePerformer(
|
||||
scraperId: string,
|
||||
scrapedPerformer: GQL.ScrapedPerformerInput
|
||||
) {
|
||||
return StashService.client.query<GQL.ScrapePerformerQuery>({
|
||||
query: GQL.ScrapePerformerDocument,
|
||||
variables: {
|
||||
scraper_id: scraperId,
|
||||
scraped_performer: scrapedPerformer,
|
||||
},
|
||||
scraped_performer: scrapedPerformer
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -435,8 +506,8 @@ export class StashService {
|
||||
return StashService.client.query<GQL.ScrapePerformerUrlQuery>({
|
||||
query: GQL.ScrapePerformerUrlDocument,
|
||||
variables: {
|
||||
url,
|
||||
},
|
||||
url
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -444,18 +515,21 @@ export class StashService {
|
||||
return StashService.client.query<GQL.ScrapeSceneUrlQuery>({
|
||||
query: GQL.ScrapeSceneUrlDocument,
|
||||
variables: {
|
||||
url,
|
||||
},
|
||||
url
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static queryScrapeScene(scraperId: string, scene: GQL.SceneUpdateInput) {
|
||||
public static queryScrapeScene(
|
||||
scraperId: string,
|
||||
scene: GQL.SceneUpdateInput
|
||||
) {
|
||||
return StashService.client.query<GQL.ScrapeSceneQuery>({
|
||||
query: GQL.ScrapeSceneDocument,
|
||||
variables: {
|
||||
scraper_id: scraperId,
|
||||
scene,
|
||||
},
|
||||
scene
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -463,7 +537,7 @@ export class StashService {
|
||||
return StashService.client.query<GQL.MetadataScanQuery>({
|
||||
query: GQL.MetadataScanDocument,
|
||||
variables: { input },
|
||||
fetchPolicy: "network-only",
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -471,7 +545,7 @@ export class StashService {
|
||||
return StashService.client.query<GQL.MetadataAutoTagQuery>({
|
||||
query: GQL.MetadataAutoTagDocument,
|
||||
variables: { input },
|
||||
fetchPolicy: "network-only",
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -479,43 +553,46 @@ export class StashService {
|
||||
return StashService.client.query<GQL.MetadataGenerateQuery>({
|
||||
query: GQL.MetadataGenerateDocument,
|
||||
variables: { input },
|
||||
fetchPolicy: "network-only",
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
}
|
||||
|
||||
public static queryMetadataClean() {
|
||||
return StashService.client.query<GQL.MetadataCleanQuery>({
|
||||
query: GQL.MetadataCleanDocument,
|
||||
fetchPolicy: "network-only",
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
}
|
||||
|
||||
public static queryMetadataExport() {
|
||||
return StashService.client.query<GQL.MetadataExportQuery>({
|
||||
query: GQL.MetadataExportDocument,
|
||||
fetchPolicy: "network-only",
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
}
|
||||
|
||||
public static queryMetadataImport() {
|
||||
return StashService.client.query<GQL.MetadataImportQuery>({
|
||||
query: GQL.MetadataImportDocument,
|
||||
fetchPolicy: "network-only",
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
}
|
||||
|
||||
public static querySceneByPathRegex(filter: GQL.FindFilterType) {
|
||||
return StashService.client.query<GQL.FindScenesByPathRegexQuery>({
|
||||
query: GQL.FindScenesByPathRegexDocument,
|
||||
variables: {filter},
|
||||
variables: { filter }
|
||||
});
|
||||
}
|
||||
|
||||
public static queryParseSceneFilenames(filter: GQL.FindFilterType, config: GQL.SceneParserInput) {
|
||||
public static queryParseSceneFilenames(
|
||||
filter: GQL.FindFilterType,
|
||||
config: GQL.SceneParserInput
|
||||
) {
|
||||
return StashService.client.query<GQL.ParseSceneFilenamesQuery>({
|
||||
query: GQL.ParseSceneFilenamesDocument,
|
||||
variables: {filter, config},
|
||||
fetchPolicy: "network-only",
|
||||
variables: { filter, config },
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -634,7 +634,7 @@ export type ScenesUpdateVariables = {
|
||||
export type ScenesUpdateMutation = {
|
||||
__typename?: "Mutation";
|
||||
|
||||
scenesUpdate: Maybe<(Maybe<ScenesUpdateScenesUpdate>)[]>;
|
||||
scenesUpdate: Maybe<Maybe<ScenesUpdateScenesUpdate>[]>;
|
||||
};
|
||||
|
||||
export type ScenesUpdateScenesUpdate = SceneDataFragment;
|
||||
@@ -801,7 +801,7 @@ export type MarkerStringsVariables = {
|
||||
export type MarkerStringsQuery = {
|
||||
__typename?: "Query";
|
||||
|
||||
markerStrings: (Maybe<MarkerStringsMarkerStrings>)[];
|
||||
markerStrings: Maybe<MarkerStringsMarkerStrings>[];
|
||||
};
|
||||
|
||||
export type MarkerStringsMarkerStrings = {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import _ from "lodash";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { Spinner } from 'react-bootstrap';
|
||||
import { Spinner } from "react-bootstrap";
|
||||
import { QueryHookResult } from "react-apollo-hooks";
|
||||
import { ApolloError } from 'apollo-client';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { ApolloError } from "apollo-client";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import {
|
||||
FindScenesQuery,
|
||||
FindScenesVariables,
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
FindPerformersQuery,
|
||||
FindPerformersVariables,
|
||||
PerformerDataFragment
|
||||
} from 'src/core/generated-graphql';
|
||||
} from "src/core/generated-graphql";
|
||||
import { ListFilter } from "../components/list/ListFilter";
|
||||
import { Pagination } from "../components/list/Pagination";
|
||||
import { StashService } from "../core/StashService";
|
||||
@@ -32,19 +32,31 @@ import { DisplayMode, FilterMode } from "../models/list-filter/types";
|
||||
interface IListHookData {
|
||||
filter: ListFilterModel;
|
||||
template: JSX.Element;
|
||||
onSelectChange: (id: string, selected : boolean, shiftKey: boolean) => void;
|
||||
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
||||
}
|
||||
|
||||
interface IListHookOperation<T> {
|
||||
text: string;
|
||||
onClick: (result: T, filter: ListFilterModel, selectedIds: Set<string>) => void;
|
||||
onClick: (
|
||||
result: T,
|
||||
filter: ListFilterModel,
|
||||
selectedIds: Set<string>
|
||||
) => void;
|
||||
}
|
||||
|
||||
interface IListHookOptions<T> {
|
||||
zoomable?: boolean;
|
||||
otherOperations?: IListHookOperation<T>[];
|
||||
renderContent: (result: T, filter: ListFilterModel, selectedIds: Set<string>, zoomIndex: number) => JSX.Element | undefined;
|
||||
renderSelectedOptions?: (result: T, selectedIds: Set<string>) => JSX.Element | undefined;
|
||||
renderContent: (
|
||||
result: T,
|
||||
filter: ListFilterModel,
|
||||
selectedIds: Set<string>,
|
||||
zoomIndex: number
|
||||
) => JSX.Element | undefined;
|
||||
renderSelectedOptions?: (
|
||||
result: T,
|
||||
selectedIds: Set<string>
|
||||
) => JSX.Element | undefined;
|
||||
}
|
||||
|
||||
interface IDataItem {
|
||||
@@ -63,10 +75,15 @@ interface IQuery<T extends IQueryResult, T2 extends IDataItem> {
|
||||
}
|
||||
|
||||
const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
options: (IListHookOptions<QueryResult> & IQuery<QueryResult, QueryData>)
|
||||
options: IListHookOptions<QueryResult> & IQuery<QueryResult, QueryData>
|
||||
): IListHookData => {
|
||||
const history = useHistory();
|
||||
const [filter, setFilter] = useState<ListFilterModel>(new ListFilterModel(options.filterMode, queryString.parse(history.location.search)));
|
||||
const [filter, setFilter] = useState<ListFilterModel>(
|
||||
new ListFilterModel(
|
||||
options.filterMode,
|
||||
queryString.parse(history.location.search)
|
||||
)
|
||||
);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [lastClickedId, setLastClickedId] = useState<string | undefined>();
|
||||
const [zoomIndex, setZoomIndex] = useState<number>(1);
|
||||
@@ -75,8 +92,8 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
const totalCount = options.getCount(result);
|
||||
const items = options.getData(result);
|
||||
|
||||
function updateQueryParams(listfilter:ListFilterModel) {
|
||||
const newLocation = { ...history.location};
|
||||
function updateQueryParams(listfilter: ListFilterModel) {
|
||||
const newLocation = { ...history.location };
|
||||
newLocation.search = listfilter.makeQueryParameters();
|
||||
history.replace(newLocation);
|
||||
}
|
||||
@@ -123,7 +140,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
const newFilter = _.cloneDeep(filter);
|
||||
|
||||
// Find if we are editing an existing criteria, then modify that. Or create a new one.
|
||||
const existingIndex = newFilter.criteria.findIndex((c) => {
|
||||
const existingIndex = newFilter.criteria.findIndex(c => {
|
||||
// If we modified an existing criterion, then look for the old id.
|
||||
const id = oldId || criterion.getId();
|
||||
return c.getId() === id;
|
||||
@@ -136,7 +153,9 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
|
||||
// Remove duplicate modifiers
|
||||
newFilter.criteria = newFilter.criteria.filter((obj, pos, arr) => {
|
||||
return arr.map((mapObj: any) => mapObj.getId()).indexOf(obj.getId()) === pos;
|
||||
return (
|
||||
arr.map((mapObj: any) => mapObj.getId()).indexOf(obj.getId()) === pos
|
||||
);
|
||||
});
|
||||
|
||||
newFilter.currentPage = 1;
|
||||
@@ -146,7 +165,9 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
|
||||
function onRemoveCriterion(removedCriterion: Criterion) {
|
||||
const newFilter = _.cloneDeep(filter);
|
||||
newFilter.criteria = newFilter.criteria.filter((criterion) => criterion.getId() !== removedCriterion.getId());
|
||||
newFilter.criteria = newFilter.criteria.filter(
|
||||
criterion => criterion.getId() !== removedCriterion.getId()
|
||||
);
|
||||
newFilter.currentPage = 1;
|
||||
setFilter(newFilter);
|
||||
updateQueryParams(newFilter);
|
||||
@@ -172,7 +193,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
setSelectedIds(newSelectedIds);
|
||||
}
|
||||
|
||||
function selectRange(startIndex : number, endIndex : number) {
|
||||
function selectRange(startIndex: number, endIndex: number) {
|
||||
let start = startIndex;
|
||||
let end = endIndex;
|
||||
if (start > end) {
|
||||
@@ -182,9 +203,9 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
}
|
||||
|
||||
const subset = items.slice(start, end + 1);
|
||||
const newSelectedIds : Set<string> = new Set();
|
||||
const newSelectedIds: Set<string> = new Set();
|
||||
|
||||
subset.forEach((item) => {
|
||||
subset.forEach(item => {
|
||||
newSelectedIds.add(item.id);
|
||||
});
|
||||
|
||||
@@ -196,19 +217,19 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
let thisIndex = -1;
|
||||
|
||||
if (lastClickedId) {
|
||||
startIndex = items.findIndex((item) => {
|
||||
startIndex = items.findIndex(item => {
|
||||
return item.id === lastClickedId;
|
||||
});
|
||||
}
|
||||
|
||||
thisIndex = items.findIndex((item) => {
|
||||
thisIndex = items.findIndex(item => {
|
||||
return item.id === id;
|
||||
});
|
||||
|
||||
selectRange(startIndex, thisIndex);
|
||||
}
|
||||
|
||||
function onSelectChange(id: string, selected : boolean, shiftKey: boolean) {
|
||||
function onSelectChange(id: string, selected: boolean, shiftKey: boolean) {
|
||||
if (shiftKey) {
|
||||
multiSelect(id);
|
||||
} else {
|
||||
@@ -217,8 +238,8 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
}
|
||||
|
||||
function onSelectAll() {
|
||||
const newSelectedIds : Set<string> = new Set();
|
||||
items.forEach((item) => {
|
||||
const newSelectedIds: Set<string> = new Set();
|
||||
items.forEach(item => {
|
||||
newSelectedIds.add(item.id);
|
||||
});
|
||||
|
||||
@@ -227,23 +248,25 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
}
|
||||
|
||||
function onSelectNone() {
|
||||
const newSelectedIds : Set<string> = new Set();
|
||||
const newSelectedIds: Set<string> = new Set();
|
||||
setSelectedIds(newSelectedIds);
|
||||
setLastClickedId(undefined);
|
||||
}
|
||||
|
||||
function onChangeZoom(newZoomIndex : number) {
|
||||
function onChangeZoom(newZoomIndex: number) {
|
||||
setZoomIndex(newZoomIndex);
|
||||
}
|
||||
|
||||
const otherOperations = options.otherOperations ? options.otherOperations.map((o) => {
|
||||
const otherOperations = options.otherOperations
|
||||
? options.otherOperations.map(o => {
|
||||
return {
|
||||
text: o.text,
|
||||
onClick: () => {
|
||||
o.onClick(result, filter, selectedIds);
|
||||
}
|
||||
}
|
||||
}) : undefined;
|
||||
};
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const template = (
|
||||
<div>
|
||||
@@ -262,8 +285,14 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
otherOperations={otherOperations}
|
||||
filter={filter}
|
||||
/>
|
||||
{options.renderSelectedOptions && selectedIds.size > 0 ? options.renderSelectedOptions(result, selectedIds) : undefined}
|
||||
{result.loading ? <Spinner animation="border" variant="light" /> : undefined}
|
||||
{options.renderSelectedOptions && selectedIds.size > 0
|
||||
? options.renderSelectedOptions(result, selectedIds)
|
||||
: undefined}
|
||||
{result.loading ? (
|
||||
<Spinner animation="border" variant="light" />
|
||||
) : (
|
||||
undefined
|
||||
)}
|
||||
{result.error ? <h1>{result.error.message}</h1> : undefined}
|
||||
{options.renderContent(result, filter, selectedIds, zoomIndex)}
|
||||
<Pagination
|
||||
@@ -276,61 +305,71 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
);
|
||||
|
||||
return { filter, template, onSelectChange };
|
||||
}
|
||||
};
|
||||
|
||||
type ScenesQuery = QueryHookResult<FindScenesQuery, FindScenesVariables>;
|
||||
export const useScenesList = (props:IListHookOptions<ScenesQuery>) => (
|
||||
export const useScenesList = (props: IListHookOptions<ScenesQuery>) =>
|
||||
useList<ScenesQuery, SlimSceneDataFragment>({
|
||||
...props,
|
||||
filterMode: FilterMode.Scenes,
|
||||
useData: StashService.useFindScenes,
|
||||
getData: (result:ScenesQuery) => (result?.data?.findScenes?.scenes ?? []),
|
||||
getCount: (result:ScenesQuery) => (result?.data?.findScenes?.count ?? 0)
|
||||
})
|
||||
)
|
||||
getData: (result: ScenesQuery) => result?.data?.findScenes?.scenes ?? [],
|
||||
getCount: (result: ScenesQuery) => result?.data?.findScenes?.count ?? 0
|
||||
});
|
||||
|
||||
type SceneMarkersQuery = QueryHookResult<FindSceneMarkersQuery, FindSceneMarkersVariables>;
|
||||
export const useSceneMarkersList = (props:IListHookOptions<SceneMarkersQuery>) => (
|
||||
type SceneMarkersQuery = QueryHookResult<
|
||||
FindSceneMarkersQuery,
|
||||
FindSceneMarkersVariables
|
||||
>;
|
||||
export const useSceneMarkersList = (
|
||||
props: IListHookOptions<SceneMarkersQuery>
|
||||
) =>
|
||||
useList<SceneMarkersQuery, FindSceneMarkersSceneMarkers>({
|
||||
...props,
|
||||
filterMode: FilterMode.SceneMarkers,
|
||||
useData: StashService.useFindSceneMarkers,
|
||||
getData: (result:SceneMarkersQuery) => (result?.data?.findSceneMarkers?.scene_markers?? []),
|
||||
getCount: (result:SceneMarkersQuery) => (result?.data?.findSceneMarkers?.count ?? 0)
|
||||
})
|
||||
)
|
||||
getData: (result: SceneMarkersQuery) =>
|
||||
result?.data?.findSceneMarkers?.scene_markers ?? [],
|
||||
getCount: (result: SceneMarkersQuery) =>
|
||||
result?.data?.findSceneMarkers?.count ?? 0
|
||||
});
|
||||
|
||||
type GalleriesQuery = QueryHookResult<FindGalleriesQuery, FindGalleriesVariables>;
|
||||
export const useGalleriesList = (props:IListHookOptions<GalleriesQuery>) => (
|
||||
type GalleriesQuery = QueryHookResult<
|
||||
FindGalleriesQuery,
|
||||
FindGalleriesVariables
|
||||
>;
|
||||
export const useGalleriesList = (props: IListHookOptions<GalleriesQuery>) =>
|
||||
useList<GalleriesQuery, GalleryDataFragment>({
|
||||
...props,
|
||||
filterMode: FilterMode.Galleries,
|
||||
useData: StashService.useFindGalleries,
|
||||
getData: (result:GalleriesQuery) => (result?.data?.findGalleries?.galleries ?? []),
|
||||
getCount: (result:GalleriesQuery) => (result?.data?.findGalleries?.count ?? 0)
|
||||
})
|
||||
)
|
||||
getData: (result: GalleriesQuery) =>
|
||||
result?.data?.findGalleries?.galleries ?? [],
|
||||
getCount: (result: GalleriesQuery) =>
|
||||
result?.data?.findGalleries?.count ?? 0
|
||||
});
|
||||
|
||||
type StudiosQuery = QueryHookResult<FindStudiosQuery, FindStudiosVariables>;
|
||||
export const useStudiosList = (props:IListHookOptions<StudiosQuery>) => (
|
||||
export const useStudiosList = (props: IListHookOptions<StudiosQuery>) =>
|
||||
useList<StudiosQuery, StudioDataFragment>({
|
||||
...props,
|
||||
filterMode: FilterMode.Studios,
|
||||
useData: StashService.useFindStudios,
|
||||
getData: (result:StudiosQuery) => (result?.data?.findStudios?.studios ?? []),
|
||||
getCount: (result:StudiosQuery) => (result?.data?.findStudios?.count ?? 0)
|
||||
})
|
||||
)
|
||||
getData: (result: StudiosQuery) => result?.data?.findStudios?.studios ?? [],
|
||||
getCount: (result: StudiosQuery) => result?.data?.findStudios?.count ?? 0
|
||||
});
|
||||
|
||||
type PerformersQuery = QueryHookResult<FindPerformersQuery, FindPerformersVariables>;
|
||||
export const usePerformersList = (props:IListHookOptions<PerformersQuery>) => (
|
||||
type PerformersQuery = QueryHookResult<
|
||||
FindPerformersQuery,
|
||||
FindPerformersVariables
|
||||
>;
|
||||
export const usePerformersList = (props: IListHookOptions<PerformersQuery>) =>
|
||||
useList<PerformersQuery, PerformerDataFragment>({
|
||||
...props,
|
||||
filterMode: FilterMode.Performers,
|
||||
useData: StashService.useFindPerformers,
|
||||
getData: (result:PerformersQuery) => (result?.data?.findPerformers?.performers ?? []),
|
||||
getCount: (result:PerformersQuery) => (result?.data?.findPerformers?.count ?? 0)
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
getData: (result: PerformersQuery) =>
|
||||
result?.data?.findPerformers?.performers ?? [],
|
||||
getCount: (result: PerformersQuery) =>
|
||||
result?.data?.findPerformers?.count ?? 0
|
||||
});
|
||||
|
||||
@@ -2,8 +2,7 @@ import localForage from "localforage";
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
|
||||
interface IInterfaceWallConfig {
|
||||
}
|
||||
interface IInterfaceWallConfig {}
|
||||
export interface IInterfaceConfig {
|
||||
wall: IInterfaceWallConfig;
|
||||
}
|
||||
@@ -47,10 +46,12 @@ function useLocalForage(item: string): ILocalForage<ValidTypes> {
|
||||
runAsync();
|
||||
});
|
||||
|
||||
return {data: json, setData: setJson, error: err};
|
||||
return { data: json, setData: setJson, error: err };
|
||||
}
|
||||
|
||||
export function useInterfaceLocalForage(): ILocalForage<IInterfaceConfig | undefined> {
|
||||
export function useInterfaceLocalForage(): ILocalForage<
|
||||
IInterfaceConfig | undefined
|
||||
> {
|
||||
const result = useLocalForage("interface");
|
||||
// Set defaults
|
||||
React.useEffect(() => {
|
||||
@@ -58,7 +59,7 @@ export function useInterfaceLocalForage(): ILocalForage<IInterfaceConfig | undef
|
||||
result.setData({
|
||||
wall: {
|
||||
// nothing here currently
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,80 +1,72 @@
|
||||
import React, { useEffect, useState, useContext, createContext } from 'react';
|
||||
import { Toast } from 'react-bootstrap';
|
||||
import React, { useEffect, useState, useContext, createContext } from "react";
|
||||
import { Toast } from "react-bootstrap";
|
||||
|
||||
interface IToast {
|
||||
header?: string;
|
||||
content: JSX.Element|string;
|
||||
content: JSX.Element | string;
|
||||
delay?: number;
|
||||
variant?: 'success'|'danger'|'warning'|'info';
|
||||
variant?: "success" | "danger" | "warning" | "info";
|
||||
}
|
||||
interface IActiveToast extends IToast {
|
||||
id: number;
|
||||
}
|
||||
|
||||
let toastID = 0;
|
||||
const ToastContext = createContext<(item:IToast) => void>(() => {});
|
||||
const ToastContext = createContext<(item: IToast) => void>(() => {});
|
||||
|
||||
export const ToastProvider: React.FC = ({children}) => {
|
||||
export const ToastProvider: React.FC = ({ children }) => {
|
||||
const [toasts, setToasts] = useState<IActiveToast[]>([]);
|
||||
|
||||
const removeToast = (id:number) => (
|
||||
setToasts(toasts.filter(item => item.id !== id))
|
||||
);
|
||||
const removeToast = (id: number) =>
|
||||
setToasts(toasts.filter(item => item.id !== id));
|
||||
|
||||
const toastItems = toasts.map(toast => (
|
||||
<Toast
|
||||
autohide
|
||||
key={toast.id}
|
||||
onClose={() => removeToast(toast.id)}
|
||||
className={toast.variant ?? 'success'}
|
||||
className={toast.variant ?? "success"}
|
||||
delay={toast.delay ?? 5000}
|
||||
>
|
||||
<Toast.Header>
|
||||
<span className="mr-auto">
|
||||
{ toast.header ?? 'Stash' }
|
||||
</span>
|
||||
<span className="mr-auto">{toast.header ?? "Stash"}</span>
|
||||
</Toast.Header>
|
||||
<Toast.Body>{toast.content}</Toast.Body>
|
||||
</Toast>
|
||||
));
|
||||
|
||||
const addToast = (toast:IToast) => (
|
||||
setToasts([...toasts, { ...toast, id: toastID++ }])
|
||||
);
|
||||
const addToast = (toast: IToast) =>
|
||||
setToasts([...toasts, { ...toast, id: toastID++ }]);
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={addToast}>
|
||||
{children}
|
||||
<div className="toast-container row">
|
||||
{ toastItems }
|
||||
</div>
|
||||
<div className="toast-container row">{toastItems}</div>
|
||||
</ToastContext.Provider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function createHookObject(toastFunc: (toast:IToast) => void) {
|
||||
function createHookObject(toastFunc: (toast: IToast) => void) {
|
||||
return {
|
||||
success: toastFunc,
|
||||
error: (error: Error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error.message);
|
||||
toastFunc({
|
||||
variant: 'danger',
|
||||
header: 'Error',
|
||||
variant: "danger",
|
||||
header: "Error",
|
||||
content: error.message ?? error.toString()
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const useToasts = () => {
|
||||
const setToast = useContext(ToastContext);
|
||||
const [hookObject, setHookObject] = useState(createHookObject(setToast));
|
||||
useEffect(() => (
|
||||
setHookObject(createHookObject(setToast))
|
||||
), [setToast]);
|
||||
useEffect(() => setHookObject(createHookObject(setToast)), [setToast]);
|
||||
|
||||
return hookObject;
|
||||
}
|
||||
};
|
||||
|
||||
export default useToasts;
|
||||
|
||||
@@ -15,17 +15,24 @@ export interface IVideoHoverHookOptions {
|
||||
}
|
||||
|
||||
export class VideoHoverHook {
|
||||
public static useVideoHover(options: IVideoHoverHookOptions): IVideoHoverHookData {
|
||||
public static useVideoHover(
|
||||
options: IVideoHoverHookOptions
|
||||
): IVideoHoverHookData {
|
||||
const videoEl = useRef<HTMLVideoElement>(null);
|
||||
const isPlaying = useRef<boolean>(false);
|
||||
const isHovering = useRef<boolean>(false);
|
||||
|
||||
const config = StashService.useConfiguration();
|
||||
const soundEnabled = !!config.data && !!config.data.configuration ? config.data.configuration.interface.soundOnPreview : true;
|
||||
const soundEnabled =
|
||||
!!config.data && !!config.data.configuration
|
||||
? config.data.configuration.interface.soundOnPreview
|
||||
: true;
|
||||
|
||||
useEffect(() => {
|
||||
const videoTag = videoEl.current;
|
||||
if (!videoTag) { return; }
|
||||
if (!videoTag) {
|
||||
return;
|
||||
}
|
||||
videoTag.onplaying = () => {
|
||||
if (isHovering.current === true) {
|
||||
isPlaying.current = true;
|
||||
@@ -33,25 +40,31 @@ export class VideoHoverHook {
|
||||
videoTag.pause();
|
||||
}
|
||||
};
|
||||
videoTag.onpause = () => { isPlaying.current = false };
|
||||
videoTag.onpause = () => {
|
||||
isPlaying.current = false;
|
||||
};
|
||||
}, [videoEl]);
|
||||
|
||||
useEffect(() => {
|
||||
const videoTag = videoEl.current;
|
||||
if (!videoTag) { return; }
|
||||
if (!videoTag) {
|
||||
return;
|
||||
}
|
||||
videoTag.volume = soundEnabled ? 0.05 : 0;
|
||||
}, [soundEnabled]);
|
||||
|
||||
return {videoEl, isPlaying, isHovering, options};
|
||||
return { videoEl, isPlaying, isHovering, options };
|
||||
}
|
||||
|
||||
public static onMouseEnter(data: IVideoHoverHookData) {
|
||||
data.isHovering.current = true;
|
||||
|
||||
const videoTag = data.videoEl.current;
|
||||
if (!videoTag) { return; }
|
||||
if (!videoTag) {
|
||||
return;
|
||||
}
|
||||
if (videoTag.paused && !data.isPlaying.current) {
|
||||
videoTag.play().catch((error) => {
|
||||
videoTag.play().catch(error => {
|
||||
console.log(error.message);
|
||||
});
|
||||
}
|
||||
@@ -61,7 +74,9 @@ export class VideoHoverHook {
|
||||
data.isHovering.current = false;
|
||||
|
||||
const videoTag = data.videoEl.current;
|
||||
if (!videoTag) { return; }
|
||||
if (!videoTag) {
|
||||
return;
|
||||
}
|
||||
if (!videoTag.paused && data.isPlaying) {
|
||||
videoTag.pause();
|
||||
if (data.options.resetOnMouseLeave) {
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
export { default as useToast } from './Toast';
|
||||
export { useInterfaceLocalForage } from './LocalForage';
|
||||
export { VideoHoverHook } from './VideoHover';
|
||||
export { useScenesList, useSceneMarkersList, useGalleriesList, useStudiosList, usePerformersList } from './ListHook';
|
||||
export { default as useToast } from "./Toast";
|
||||
export { useInterfaceLocalForage } from "./LocalForage";
|
||||
export { VideoHoverHook } from "./VideoHover";
|
||||
export {
|
||||
useScenesList,
|
||||
useSceneMarkersList,
|
||||
useGalleriesList,
|
||||
useStudiosList,
|
||||
usePerformersList
|
||||
} from "./ListHook";
|
||||
|
||||
@@ -7,13 +7,14 @@ import { StashService } from "./core/StashService";
|
||||
import "./index.scss";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
|
||||
ReactDOM.render((
|
||||
ReactDOM.render(
|
||||
<BrowserRouter>
|
||||
<ApolloProvider client={StashService.initialize()!}>
|
||||
<App />
|
||||
</ApolloProvider>
|
||||
</BrowserRouter>
|
||||
), document.getElementById("root"));
|
||||
</BrowserRouter>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
|
||||
@@ -4,68 +4,101 @@ import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { ILabeledId, ILabeledValue } from "../types";
|
||||
|
||||
export type CriterionType =
|
||||
"none" |
|
||||
"rating" |
|
||||
"resolution" |
|
||||
"favorite" |
|
||||
"hasMarkers" |
|
||||
"isMissing" |
|
||||
"tags" |
|
||||
"sceneTags" |
|
||||
"performers" |
|
||||
"studios" |
|
||||
"birth_year" |
|
||||
"age" |
|
||||
"ethnicity" |
|
||||
"country" |
|
||||
"eye_color" |
|
||||
"height" |
|
||||
"measurements" |
|
||||
"fake_tits" |
|
||||
"career_length" |
|
||||
"tattoos" |
|
||||
"piercings" |
|
||||
"aliases";
|
||||
| "none"
|
||||
| "rating"
|
||||
| "resolution"
|
||||
| "favorite"
|
||||
| "hasMarkers"
|
||||
| "isMissing"
|
||||
| "tags"
|
||||
| "sceneTags"
|
||||
| "performers"
|
||||
| "studios"
|
||||
| "birth_year"
|
||||
| "age"
|
||||
| "ethnicity"
|
||||
| "country"
|
||||
| "eye_color"
|
||||
| "height"
|
||||
| "measurements"
|
||||
| "fake_tits"
|
||||
| "career_length"
|
||||
| "tattoos"
|
||||
| "piercings"
|
||||
| "aliases";
|
||||
|
||||
export abstract class Criterion<Option = any, Value = any> {
|
||||
public static getLabel(type: CriterionType = "none") {
|
||||
switch (type) {
|
||||
case "none": return "None";
|
||||
case "rating": return "Rating";
|
||||
case "resolution": return "Resolution";
|
||||
case "favorite": return "Favorite";
|
||||
case "hasMarkers": return "Has Markers";
|
||||
case "isMissing": return "Is Missing";
|
||||
case "tags": return "Tags";
|
||||
case "sceneTags": return "Scene Tags";
|
||||
case "performers": return "Performers";
|
||||
case "studios": return "Studios";
|
||||
case "birth_year": return "Birth Year";
|
||||
case "age": return "Age";
|
||||
case "ethnicity": return "Ethnicity";
|
||||
case "country": return "Country";
|
||||
case "eye_color": return "Eye Color";
|
||||
case "height": return "Height";
|
||||
case "measurements": return "Measurements";
|
||||
case "fake_tits": return "Fake Tits";
|
||||
case "career_length": return "Career Length";
|
||||
case "tattoos": return "Tattoos";
|
||||
case "piercings": return "Piercings";
|
||||
case "aliases": return "Aliases";
|
||||
case "none":
|
||||
return "None";
|
||||
case "rating":
|
||||
return "Rating";
|
||||
case "resolution":
|
||||
return "Resolution";
|
||||
case "favorite":
|
||||
return "Favorite";
|
||||
case "hasMarkers":
|
||||
return "Has Markers";
|
||||
case "isMissing":
|
||||
return "Is Missing";
|
||||
case "tags":
|
||||
return "Tags";
|
||||
case "sceneTags":
|
||||
return "Scene Tags";
|
||||
case "performers":
|
||||
return "Performers";
|
||||
case "studios":
|
||||
return "Studios";
|
||||
case "birth_year":
|
||||
return "Birth Year";
|
||||
case "age":
|
||||
return "Age";
|
||||
case "ethnicity":
|
||||
return "Ethnicity";
|
||||
case "country":
|
||||
return "Country";
|
||||
case "eye_color":
|
||||
return "Eye Color";
|
||||
case "height":
|
||||
return "Height";
|
||||
case "measurements":
|
||||
return "Measurements";
|
||||
case "fake_tits":
|
||||
return "Fake Tits";
|
||||
case "career_length":
|
||||
return "Career Length";
|
||||
case "tattoos":
|
||||
return "Tattoos";
|
||||
case "piercings":
|
||||
return "Piercings";
|
||||
case "aliases":
|
||||
return "Aliases";
|
||||
}
|
||||
}
|
||||
|
||||
public static getModifierOption(modifier: CriterionModifier = CriterionModifier.Equals): ILabeledValue {
|
||||
public static getModifierOption(
|
||||
modifier: CriterionModifier = CriterionModifier.Equals
|
||||
): ILabeledValue {
|
||||
switch (modifier) {
|
||||
case CriterionModifier.Equals: return {value: CriterionModifier.Equals, label: "Equals"};
|
||||
case CriterionModifier.NotEquals: return {value: CriterionModifier.NotEquals, label: "Not Equals"};
|
||||
case CriterionModifier.GreaterThan: return {value: CriterionModifier.GreaterThan, label: "Greater Than"};
|
||||
case CriterionModifier.LessThan: return {value: CriterionModifier.LessThan, label: "Less Than"};
|
||||
case CriterionModifier.IsNull: return {value: CriterionModifier.IsNull, label: "Is NULL"};
|
||||
case CriterionModifier.NotNull: return {value: CriterionModifier.NotNull, label: "Not NULL"};
|
||||
case CriterionModifier.IncludesAll: return {value: CriterionModifier.IncludesAll, label: "Includes All"};
|
||||
case CriterionModifier.Includes: return {value: CriterionModifier.Includes, label: "Includes"};
|
||||
case CriterionModifier.Excludes: return {value: CriterionModifier.Excludes, label: "Excludes"};
|
||||
case CriterionModifier.Equals:
|
||||
return { value: CriterionModifier.Equals, label: "Equals" };
|
||||
case CriterionModifier.NotEquals:
|
||||
return { value: CriterionModifier.NotEquals, label: "Not Equals" };
|
||||
case CriterionModifier.GreaterThan:
|
||||
return { value: CriterionModifier.GreaterThan, label: "Greater Than" };
|
||||
case CriterionModifier.LessThan:
|
||||
return { value: CriterionModifier.LessThan, label: "Less Than" };
|
||||
case CriterionModifier.IsNull:
|
||||
return { value: CriterionModifier.IsNull, label: "Is NULL" };
|
||||
case CriterionModifier.NotNull:
|
||||
return { value: CriterionModifier.NotNull, label: "Not NULL" };
|
||||
case CriterionModifier.IncludesAll:
|
||||
return { value: CriterionModifier.IncludesAll, label: "Includes All" };
|
||||
case CriterionModifier.Includes:
|
||||
return { value: CriterionModifier.Includes, label: "Includes" };
|
||||
case CriterionModifier.Excludes:
|
||||
return { value: CriterionModifier.Excludes, label: "Excludes" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,25 +113,47 @@ export abstract class Criterion<Option = any, Value = any> {
|
||||
public getLabel(): string {
|
||||
let modifierString: string;
|
||||
switch (this.modifier) {
|
||||
case CriterionModifier.Equals: modifierString = "is"; break;
|
||||
case CriterionModifier.NotEquals: modifierString = "is not"; break;
|
||||
case CriterionModifier.GreaterThan: modifierString = "is greater than"; break;
|
||||
case CriterionModifier.LessThan: modifierString = "is less than"; break;
|
||||
case CriterionModifier.IsNull: modifierString = "is null"; break;
|
||||
case CriterionModifier.NotNull: modifierString = "is not null"; break;
|
||||
case CriterionModifier.Includes: modifierString = "includes"; break;
|
||||
case CriterionModifier.IncludesAll: modifierString = "includes all"; break;
|
||||
case CriterionModifier.Excludes: modifierString = "excludes"; break;
|
||||
default: modifierString = "";
|
||||
case CriterionModifier.Equals:
|
||||
modifierString = "is";
|
||||
break;
|
||||
case CriterionModifier.NotEquals:
|
||||
modifierString = "is not";
|
||||
break;
|
||||
case CriterionModifier.GreaterThan:
|
||||
modifierString = "is greater than";
|
||||
break;
|
||||
case CriterionModifier.LessThan:
|
||||
modifierString = "is less than";
|
||||
break;
|
||||
case CriterionModifier.IsNull:
|
||||
modifierString = "is null";
|
||||
break;
|
||||
case CriterionModifier.NotNull:
|
||||
modifierString = "is not null";
|
||||
break;
|
||||
case CriterionModifier.Includes:
|
||||
modifierString = "includes";
|
||||
break;
|
||||
case CriterionModifier.IncludesAll:
|
||||
modifierString = "includes all";
|
||||
break;
|
||||
case CriterionModifier.Excludes:
|
||||
modifierString = "excludes";
|
||||
break;
|
||||
default:
|
||||
modifierString = "";
|
||||
}
|
||||
|
||||
let valueString: string;
|
||||
if (this.modifier === CriterionModifier.IsNull || this.modifier === CriterionModifier.NotNull) {
|
||||
if (
|
||||
this.modifier === CriterionModifier.IsNull ||
|
||||
this.modifier === CriterionModifier.NotNull
|
||||
) {
|
||||
valueString = "";
|
||||
} else if (Array.isArray(this.value) && this.value.length > 0) {
|
||||
let items = this.value;
|
||||
if ((this.value as ILabeledId[])[0].label) {
|
||||
items = this.value.map((item) => item.label) as any;
|
||||
items = this.value.map(item => item.label) as any;
|
||||
}
|
||||
valueString = items.join(", ");
|
||||
} else if (typeof this.value === "string") {
|
||||
@@ -133,7 +188,7 @@ export class CriterionOption implements ICriterionOption {
|
||||
public label: string;
|
||||
public value: CriterionType;
|
||||
|
||||
constructor(label : string, value : CriterionType) {
|
||||
constructor(label: string, value: CriterionType) {
|
||||
this.label = label;
|
||||
this.value = value;
|
||||
}
|
||||
@@ -147,12 +202,12 @@ export class StringCriterion extends Criterion<string, string> {
|
||||
Criterion.getModifierOption(CriterionModifier.Equals),
|
||||
Criterion.getModifierOption(CriterionModifier.NotEquals),
|
||||
Criterion.getModifierOption(CriterionModifier.IsNull),
|
||||
Criterion.getModifierOption(CriterionModifier.NotNull),
|
||||
Criterion.getModifierOption(CriterionModifier.NotNull)
|
||||
];
|
||||
public options: string[] | undefined;
|
||||
public value: string = "";
|
||||
|
||||
constructor(type : CriterionType, parameterName?: string, options? : string[]) {
|
||||
constructor(type: CriterionType, parameterName?: string, options?: string[]) {
|
||||
super();
|
||||
|
||||
this.type = type;
|
||||
@@ -177,12 +232,12 @@ export class NumberCriterion extends Criterion<number, number> {
|
||||
Criterion.getModifierOption(CriterionModifier.GreaterThan),
|
||||
Criterion.getModifierOption(CriterionModifier.LessThan),
|
||||
Criterion.getModifierOption(CriterionModifier.IsNull),
|
||||
Criterion.getModifierOption(CriterionModifier.NotNull),
|
||||
Criterion.getModifierOption(CriterionModifier.NotNull)
|
||||
];
|
||||
public options: number[] | undefined;
|
||||
public value: number = 0;
|
||||
|
||||
constructor(type : CriterionType, parameterName?: string, options? : number[]) {
|
||||
constructor(type: CriterionType, parameterName?: string, options?: number[]) {
|
||||
super();
|
||||
|
||||
this.type = type;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionType,
|
||||
ICriterionOption,
|
||||
} from "./criterion";
|
||||
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
|
||||
|
||||
export class FavoriteCriterion extends Criterion<string, string> {
|
||||
public type: CriterionType = "favorite";
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionType,
|
||||
ICriterionOption,
|
||||
} from "./criterion";
|
||||
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
|
||||
|
||||
export class HasMarkersCriterion extends Criterion<string, string> {
|
||||
public type: CriterionType = "hasMarkers";
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionType,
|
||||
ICriterionOption,
|
||||
} from "./criterion";
|
||||
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
|
||||
|
||||
export class IsMissingCriterion extends Criterion<string, string> {
|
||||
public type: CriterionType = "isMissing";
|
||||
public parameterName: string = "is_missing";
|
||||
public modifier = CriterionModifier.Equals;
|
||||
public modifierOptions = [];
|
||||
public options: string[] = ["title", "url", "date", "gallery", "studio", "performers"];
|
||||
public options: string[] = [
|
||||
"title",
|
||||
"url",
|
||||
"date",
|
||||
"gallery",
|
||||
"studio",
|
||||
"performers"
|
||||
];
|
||||
public value: string = "";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionType,
|
||||
ICriterionOption,
|
||||
} from "./criterion";
|
||||
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
|
||||
|
||||
export class NoneCriterion extends Criterion<any, any> {
|
||||
public type: CriterionType = "none";
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { ILabeledId } from "../types";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionType,
|
||||
ICriterionOption,
|
||||
} from "./criterion";
|
||||
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
|
||||
|
||||
interface IOptionType {
|
||||
id: string;
|
||||
@@ -19,7 +15,7 @@ export class PerformersCriterion extends Criterion<IOptionType, ILabeledId[]> {
|
||||
public modifierOptions = [
|
||||
Criterion.getModifierOption(CriterionModifier.IncludesAll),
|
||||
Criterion.getModifierOption(CriterionModifier.Includes),
|
||||
Criterion.getModifierOption(CriterionModifier.Excludes),
|
||||
Criterion.getModifierOption(CriterionModifier.Excludes)
|
||||
];
|
||||
public options: IOptionType[] = [];
|
||||
public value: ILabeledId[] = [];
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionType,
|
||||
ICriterionOption,
|
||||
} from "./criterion";
|
||||
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
|
||||
|
||||
export class RatingCriterion extends Criterion<number, number> { // TODO <number, number[]>
|
||||
export class RatingCriterion extends Criterion<number, number> {
|
||||
// TODO <number, number[]>
|
||||
public type: CriterionType = "rating";
|
||||
public parameterName: string = "rating";
|
||||
public modifier = CriterionModifier.Equals;
|
||||
@@ -15,7 +12,7 @@ export class RatingCriterion extends Criterion<number, number> { // TODO <number
|
||||
Criterion.getModifierOption(CriterionModifier.GreaterThan),
|
||||
Criterion.getModifierOption(CriterionModifier.LessThan),
|
||||
Criterion.getModifierOption(CriterionModifier.IsNull),
|
||||
Criterion.getModifierOption(CriterionModifier.NotNull),
|
||||
Criterion.getModifierOption(CriterionModifier.NotNull)
|
||||
];
|
||||
public options: number[] = [1, 2, 3, 4, 5];
|
||||
public value: number = 0;
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionType,
|
||||
ICriterionOption,
|
||||
} from "./criterion";
|
||||
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
|
||||
|
||||
export class ResolutionCriterion extends Criterion<string, string> { // TODO <string, string[]>
|
||||
export class ResolutionCriterion extends Criterion<string, string> {
|
||||
// TODO <string, string[]>
|
||||
public type: CriterionType = "resolution";
|
||||
public parameterName: string = "resolution";
|
||||
public modifier = CriterionModifier.Equals;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { ILabeledId } from "../types";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionType,
|
||||
ICriterionOption,
|
||||
} from "./criterion";
|
||||
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
|
||||
|
||||
interface IOptionType {
|
||||
id: string;
|
||||
@@ -18,7 +14,7 @@ export class StudiosCriterion extends Criterion<IOptionType, ILabeledId[]> {
|
||||
public modifier = CriterionModifier.Includes;
|
||||
public modifierOptions = [
|
||||
Criterion.getModifierOption(CriterionModifier.Includes),
|
||||
Criterion.getModifierOption(CriterionModifier.Excludes),
|
||||
Criterion.getModifierOption(CriterionModifier.Excludes)
|
||||
];
|
||||
public options: IOptionType[] = [];
|
||||
public value: ILabeledId[] = [];
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { ILabeledId } from "../types";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionType,
|
||||
ICriterionOption,
|
||||
} from "./criterion";
|
||||
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
|
||||
|
||||
export class TagsCriterion extends Criterion<GQL.AllTagsForFilterAllTags, ILabeledId[]> {
|
||||
export class TagsCriterion extends Criterion<
|
||||
GQL.AllTagsForFilterAllTags,
|
||||
ILabeledId[]
|
||||
> {
|
||||
public type: CriterionType;
|
||||
public parameterName: string;
|
||||
public modifier = GQL.CriterionModifier.IncludesAll;
|
||||
public modifierOptions = [
|
||||
Criterion.getModifierOption(GQL.CriterionModifier.IncludesAll),
|
||||
Criterion.getModifierOption(GQL.CriterionModifier.Includes),
|
||||
Criterion.getModifierOption(GQL.CriterionModifier.Excludes),
|
||||
Criterion.getModifierOption(GQL.CriterionModifier.Excludes)
|
||||
];
|
||||
public options: GQL.AllTagsForFilterAllTags[] = [];
|
||||
public value: ILabeledId[] = [];
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
/* eslint-disable consistent-return, default-case */
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import {
|
||||
CriterionModifier,
|
||||
} from "src/core/generated-graphql";
|
||||
import { Criterion, CriterionType, StringCriterion, NumberCriterion } from "./criterion";
|
||||
Criterion,
|
||||
CriterionType,
|
||||
StringCriterion,
|
||||
NumberCriterion
|
||||
} from "./criterion";
|
||||
import { FavoriteCriterion } from "./favorite";
|
||||
import { HasMarkersCriterion } from "./has-markers";
|
||||
import { IsMissingCriterion } from "./is-missing";
|
||||
@@ -15,16 +18,26 @@ import { TagsCriterion } from "./tags";
|
||||
|
||||
export function makeCriteria(type: CriterionType = "none") {
|
||||
switch (type) {
|
||||
case "none": return new NoneCriterion();
|
||||
case "rating": return new RatingCriterion();
|
||||
case "resolution": return new ResolutionCriterion();
|
||||
case "favorite": return new FavoriteCriterion();
|
||||
case "hasMarkers": return new HasMarkersCriterion();
|
||||
case "isMissing": return new IsMissingCriterion();
|
||||
case "tags": return new TagsCriterion("tags");
|
||||
case "sceneTags": return new TagsCriterion("sceneTags");
|
||||
case "performers": return new PerformersCriterion();
|
||||
case "studios": return new StudiosCriterion();
|
||||
case "none":
|
||||
return new NoneCriterion();
|
||||
case "rating":
|
||||
return new RatingCriterion();
|
||||
case "resolution":
|
||||
return new ResolutionCriterion();
|
||||
case "favorite":
|
||||
return new FavoriteCriterion();
|
||||
case "hasMarkers":
|
||||
return new HasMarkersCriterion();
|
||||
case "isMissing":
|
||||
return new IsMissingCriterion();
|
||||
case "tags":
|
||||
return new TagsCriterion("tags");
|
||||
case "sceneTags":
|
||||
return new TagsCriterion("sceneTags");
|
||||
case "performers":
|
||||
return new PerformersCriterion();
|
||||
case "studios":
|
||||
return new StudiosCriterion();
|
||||
|
||||
case "birth_year":
|
||||
case "age": {
|
||||
|
||||
@@ -5,23 +5,46 @@ import {
|
||||
ResolutionEnum,
|
||||
SceneFilterType,
|
||||
SceneMarkerFilterType,
|
||||
SortDirectionEnum,
|
||||
SortDirectionEnum
|
||||
} from "src/core/generated-graphql";
|
||||
import { Criterion, ICriterionOption, CriterionType, CriterionOption, NumberCriterion, StringCriterion } from "./criteria/criterion";
|
||||
import { FavoriteCriterion, FavoriteCriterionOption } from "./criteria/favorite";
|
||||
import { HasMarkersCriterion, HasMarkersCriterionOption } from "./criteria/has-markers";
|
||||
import { IsMissingCriterion, IsMissingCriterionOption } from "./criteria/is-missing";
|
||||
import { NoneCriterionOption } from "./criteria/none";
|
||||
import { PerformersCriterion, PerformersCriterionOption } from "./criteria/performers";
|
||||
import { RatingCriterion, RatingCriterionOption } from "./criteria/rating";
|
||||
import { ResolutionCriterion, ResolutionCriterionOption } from "./criteria/resolution";
|
||||
import { StudiosCriterion, StudiosCriterionOption } from "./criteria/studios";
|
||||
import { SceneTagsCriterionOption, TagsCriterion, TagsCriterionOption } from "./criteria/tags";
|
||||
import { makeCriteria } from "./criteria/utils";
|
||||
import {
|
||||
DisplayMode,
|
||||
FilterMode,
|
||||
} from "./types";
|
||||
Criterion,
|
||||
ICriterionOption,
|
||||
CriterionType,
|
||||
CriterionOption,
|
||||
NumberCriterion,
|
||||
StringCriterion
|
||||
} from "./criteria/criterion";
|
||||
import {
|
||||
FavoriteCriterion,
|
||||
FavoriteCriterionOption
|
||||
} from "./criteria/favorite";
|
||||
import {
|
||||
HasMarkersCriterion,
|
||||
HasMarkersCriterionOption
|
||||
} from "./criteria/has-markers";
|
||||
import {
|
||||
IsMissingCriterion,
|
||||
IsMissingCriterionOption
|
||||
} from "./criteria/is-missing";
|
||||
import { NoneCriterionOption } from "./criteria/none";
|
||||
import {
|
||||
PerformersCriterion,
|
||||
PerformersCriterionOption
|
||||
} from "./criteria/performers";
|
||||
import { RatingCriterion, RatingCriterionOption } from "./criteria/rating";
|
||||
import {
|
||||
ResolutionCriterion,
|
||||
ResolutionCriterionOption
|
||||
} from "./criteria/resolution";
|
||||
import { StudiosCriterion, StudiosCriterionOption } from "./criteria/studios";
|
||||
import {
|
||||
SceneTagsCriterionOption,
|
||||
TagsCriterion,
|
||||
TagsCriterionOption
|
||||
} from "./criteria/tags";
|
||||
import { makeCriteria } from "./criteria/utils";
|
||||
import { DisplayMode, FilterMode } from "./types";
|
||||
|
||||
interface IQueryParameters {
|
||||
sortby?: string;
|
||||
@@ -49,12 +72,24 @@ export class ListFilterModel {
|
||||
public constructor(filterMode: FilterMode, rawParms?: any) {
|
||||
switch (filterMode) {
|
||||
case FilterMode.Scenes:
|
||||
if (!!this.sortBy === false) { this.sortBy = "date"; }
|
||||
this.sortByOptions = ["title", "path", "rating", "date", "filesize", "duration", "framerate", "bitrate", "random"];
|
||||
if (!!this.sortBy === false) {
|
||||
this.sortBy = "date";
|
||||
}
|
||||
this.sortByOptions = [
|
||||
"title",
|
||||
"path",
|
||||
"rating",
|
||||
"date",
|
||||
"filesize",
|
||||
"duration",
|
||||
"framerate",
|
||||
"bitrate",
|
||||
"random"
|
||||
];
|
||||
this.displayModeOptions = [
|
||||
DisplayMode.Grid,
|
||||
DisplayMode.List,
|
||||
DisplayMode.Wall,
|
||||
DisplayMode.Wall
|
||||
];
|
||||
this.criterionOptions = [
|
||||
new NoneCriterionOption(),
|
||||
@@ -64,19 +99,18 @@ export class ListFilterModel {
|
||||
new IsMissingCriterionOption(),
|
||||
new TagsCriterionOption(),
|
||||
new PerformersCriterionOption(),
|
||||
new StudiosCriterionOption(),
|
||||
new StudiosCriterionOption()
|
||||
];
|
||||
break;
|
||||
case FilterMode.Performers: {
|
||||
if (!!this.sortBy === false) { this.sortBy = "name"; }
|
||||
if (!!this.sortBy === false) {
|
||||
this.sortBy = "name";
|
||||
}
|
||||
this.sortByOptions = ["name", "height", "birthdate", "scenes_count"];
|
||||
this.displayModeOptions = [
|
||||
DisplayMode.Grid,
|
||||
DisplayMode.List,
|
||||
];
|
||||
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
|
||||
|
||||
const numberCriteria : CriterionType[] = ["birth_year", "age"];
|
||||
const stringCriteria : CriterionType[] = [
|
||||
const numberCriteria: CriterionType[] = ["birth_year", "age"];
|
||||
const stringCriteria: CriterionType[] = [
|
||||
"ethnicity",
|
||||
"country",
|
||||
"eye_color",
|
||||
@@ -94,56 +128,59 @@ export class ListFilterModel {
|
||||
new FavoriteCriterionOption()
|
||||
];
|
||||
|
||||
this.criterionOptions = this.criterionOptions.concat(numberCriteria.concat(stringCriteria).map((c) => {
|
||||
this.criterionOptions = this.criterionOptions.concat(
|
||||
numberCriteria.concat(stringCriteria).map(c => {
|
||||
return new CriterionOption(Criterion.getLabel(c), c);
|
||||
}));
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
case FilterMode.Studios:
|
||||
if (!!this.sortBy === false) { this.sortBy = "name"; }
|
||||
if (!!this.sortBy === false) {
|
||||
this.sortBy = "name";
|
||||
}
|
||||
this.sortByOptions = ["name", "scenes_count"];
|
||||
this.displayModeOptions = [
|
||||
DisplayMode.Grid,
|
||||
];
|
||||
this.criterionOptions = [
|
||||
new NoneCriterionOption(),
|
||||
];
|
||||
this.displayModeOptions = [DisplayMode.Grid];
|
||||
this.criterionOptions = [new NoneCriterionOption()];
|
||||
break;
|
||||
case FilterMode.Galleries:
|
||||
if (!!this.sortBy === false) { this.sortBy = "path"; }
|
||||
if (!!this.sortBy === false) {
|
||||
this.sortBy = "path";
|
||||
}
|
||||
this.sortByOptions = ["path"];
|
||||
this.displayModeOptions = [
|
||||
DisplayMode.List,
|
||||
];
|
||||
this.criterionOptions = [
|
||||
new NoneCriterionOption(),
|
||||
];
|
||||
this.displayModeOptions = [DisplayMode.List];
|
||||
this.criterionOptions = [new NoneCriterionOption()];
|
||||
break;
|
||||
case FilterMode.SceneMarkers:
|
||||
if (!!this.sortBy === false) { this.sortBy = "title"; }
|
||||
this.sortByOptions = ["title", "seconds", "scene_id", "random", "scenes_updated_at"];
|
||||
this.displayModeOptions = [
|
||||
DisplayMode.Wall,
|
||||
if (!!this.sortBy === false) {
|
||||
this.sortBy = "title";
|
||||
}
|
||||
this.sortByOptions = [
|
||||
"title",
|
||||
"seconds",
|
||||
"scene_id",
|
||||
"random",
|
||||
"scenes_updated_at"
|
||||
];
|
||||
this.displayModeOptions = [DisplayMode.Wall];
|
||||
this.criterionOptions = [
|
||||
new NoneCriterionOption(),
|
||||
new TagsCriterionOption(),
|
||||
new SceneTagsCriterionOption(),
|
||||
new PerformersCriterionOption(),
|
||||
new PerformersCriterionOption()
|
||||
];
|
||||
break;
|
||||
default:
|
||||
this.sortByOptions = [];
|
||||
this.displayModeOptions = [];
|
||||
this.criterionOptions = [
|
||||
new NoneCriterionOption(),
|
||||
];
|
||||
this.criterionOptions = [new NoneCriterionOption()];
|
||||
break;
|
||||
}
|
||||
if (!!this.displayMode === false) { this.displayMode = this.displayModeOptions[0]; }
|
||||
if (!!this.displayMode === false) {
|
||||
this.displayMode = this.displayModeOptions[0];
|
||||
}
|
||||
this.sortByOptions = [...this.sortByOptions, "created_at", "updated_at"];
|
||||
if(rawParms)
|
||||
this.configureFromQueryParameters(rawParms);
|
||||
if (rawParms) this.configureFromQueryParameters(rawParms);
|
||||
}
|
||||
|
||||
public configureFromQueryParameters(rawParms: any) {
|
||||
@@ -186,7 +223,7 @@ export class ListFilterModel {
|
||||
|
||||
public makeQueryParameters(): string {
|
||||
const encodedCriteria: string[] = [];
|
||||
this.criteria.forEach((criterion) => {
|
||||
this.criteria.forEach(criterion => {
|
||||
const encodedCriterion: any = {};
|
||||
encodedCriterion.type = criterion.type;
|
||||
encodedCriterion.value = criterion.value;
|
||||
@@ -201,9 +238,9 @@ export class ListFilterModel {
|
||||
disp: this.displayMode,
|
||||
q: this.searchTerm,
|
||||
p: this.currentPage,
|
||||
c: encodedCriteria,
|
||||
c: encodedCriteria
|
||||
};
|
||||
return queryString.stringify(result, {encode: false});
|
||||
return queryString.stringify(result, { encode: false });
|
||||
}
|
||||
|
||||
// TODO: These don't support multiple of the same criteria, only the last one set is used.
|
||||
@@ -214,26 +251,42 @@ export class ListFilterModel {
|
||||
page: this.currentPage,
|
||||
per_page: this.itemsPerPage,
|
||||
sort: this.sortBy,
|
||||
direction: this.sortDirection === "asc" ? SortDirectionEnum.Asc : SortDirectionEnum.Desc,
|
||||
direction:
|
||||
this.sortDirection === "asc"
|
||||
? SortDirectionEnum.Asc
|
||||
: SortDirectionEnum.Desc
|
||||
};
|
||||
}
|
||||
|
||||
public makeSceneFilter(): SceneFilterType {
|
||||
const result: SceneFilterType = {};
|
||||
this.criteria.forEach((criterion) => {
|
||||
this.criteria.forEach(criterion => {
|
||||
switch (criterion.type) {
|
||||
case "rating": {
|
||||
const ratingCrit = criterion as RatingCriterion;
|
||||
result.rating = { value: ratingCrit.value, modifier: ratingCrit.modifier };
|
||||
result.rating = {
|
||||
value: ratingCrit.value,
|
||||
modifier: ratingCrit.modifier
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "resolution": {
|
||||
switch ((criterion as ResolutionCriterion).value) {
|
||||
case "240p": result.resolution = ResolutionEnum.Low; break;
|
||||
case "480p": result.resolution = ResolutionEnum.Standard; break;
|
||||
case "720p": result.resolution = ResolutionEnum.StandardHd; break;
|
||||
case "1080p": result.resolution = ResolutionEnum.FullHd; break;
|
||||
case "4k": result.resolution = ResolutionEnum.FourK; break;
|
||||
case "240p":
|
||||
result.resolution = ResolutionEnum.Low;
|
||||
break;
|
||||
case "480p":
|
||||
result.resolution = ResolutionEnum.Standard;
|
||||
break;
|
||||
case "720p":
|
||||
result.resolution = ResolutionEnum.StandardHd;
|
||||
break;
|
||||
case "1080p":
|
||||
result.resolution = ResolutionEnum.FullHd;
|
||||
break;
|
||||
case "4k":
|
||||
result.resolution = ResolutionEnum.FourK;
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
break;
|
||||
@@ -246,17 +299,26 @@ export class ListFilterModel {
|
||||
break;
|
||||
case "tags": {
|
||||
const tagsCrit = criterion as TagsCriterion;
|
||||
result.tags = { value: tagsCrit.value.map((tag) => tag.id), modifier: tagsCrit.modifier };
|
||||
result.tags = {
|
||||
value: tagsCrit.value.map(tag => tag.id),
|
||||
modifier: tagsCrit.modifier
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "performers": {
|
||||
const perfCrit = criterion as PerformersCriterion;
|
||||
result.performers = { value: perfCrit.value.map((perf) => perf.id), modifier: perfCrit.modifier };
|
||||
result.performers = {
|
||||
value: perfCrit.value.map(perf => perf.id),
|
||||
modifier: perfCrit.modifier
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "studios": {
|
||||
const studCrit = criterion as StudiosCriterion;
|
||||
result.studios = { value: studCrit.value.map((studio) => studio.id), modifier: studCrit.modifier };
|
||||
result.studios = {
|
||||
value: studCrit.value.map(studio => studio.id),
|
||||
modifier: studCrit.modifier
|
||||
};
|
||||
break;
|
||||
}
|
||||
// no default
|
||||
@@ -267,14 +329,18 @@ export class ListFilterModel {
|
||||
|
||||
public makePerformerFilter(): PerformerFilterType {
|
||||
const result: PerformerFilterType = {};
|
||||
this.criteria.forEach((criterion) => {
|
||||
this.criteria.forEach(criterion => {
|
||||
switch (criterion.type) {
|
||||
case "favorite":
|
||||
result.filter_favorites = (criterion as FavoriteCriterion).value === "true";
|
||||
result.filter_favorites =
|
||||
(criterion as FavoriteCriterion).value === "true";
|
||||
break;
|
||||
case "birth_year": {
|
||||
const byCrit = criterion as NumberCriterion;
|
||||
result.birth_year = { value: byCrit.value, modifier: byCrit.modifier };
|
||||
result.birth_year = {
|
||||
value: byCrit.value,
|
||||
modifier: byCrit.modifier
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "age": {
|
||||
@@ -284,12 +350,18 @@ export class ListFilterModel {
|
||||
}
|
||||
case "ethnicity": {
|
||||
const ethCrit = criterion as StringCriterion;
|
||||
result.ethnicity = { value: ethCrit.value, modifier: ethCrit.modifier };
|
||||
result.ethnicity = {
|
||||
value: ethCrit.value,
|
||||
modifier: ethCrit.modifier
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "country": {
|
||||
const cntryCrit = criterion as StringCriterion;
|
||||
result.country = { value: cntryCrit.value, modifier: cntryCrit.modifier };
|
||||
result.country = {
|
||||
value: cntryCrit.value,
|
||||
modifier: cntryCrit.modifier
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "eye_color": {
|
||||
@@ -304,7 +376,10 @@ export class ListFilterModel {
|
||||
}
|
||||
case "measurements": {
|
||||
const mCrit = criterion as StringCriterion;
|
||||
result.measurements = { value: mCrit.value, modifier: mCrit.modifier };
|
||||
result.measurements = {
|
||||
value: mCrit.value,
|
||||
modifier: mCrit.modifier
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "fake_tits": {
|
||||
@@ -314,7 +389,10 @@ export class ListFilterModel {
|
||||
}
|
||||
case "career_length": {
|
||||
const clCrit = criterion as StringCriterion;
|
||||
result.career_length = { value: clCrit.value, modifier: clCrit.modifier };
|
||||
result.career_length = {
|
||||
value: clCrit.value,
|
||||
modifier: clCrit.modifier
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "tattoos": {
|
||||
@@ -340,21 +418,30 @@ export class ListFilterModel {
|
||||
|
||||
public makeSceneMarkerFilter(): SceneMarkerFilterType {
|
||||
const result: SceneMarkerFilterType = {};
|
||||
this.criteria.forEach((criterion) => {
|
||||
this.criteria.forEach(criterion => {
|
||||
switch (criterion.type) {
|
||||
case "tags": {
|
||||
const tagsCrit = criterion as TagsCriterion;
|
||||
result.tags = { value: tagsCrit.value.map((tag) => tag.id), modifier: tagsCrit.modifier };
|
||||
result.tags = {
|
||||
value: tagsCrit.value.map(tag => tag.id),
|
||||
modifier: tagsCrit.modifier
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "sceneTags": {
|
||||
const sceneTagsCrit = criterion as TagsCriterion;
|
||||
result.scene_tags = { value: sceneTagsCrit.value.map((tag) => tag.id), modifier: sceneTagsCrit.modifier };
|
||||
result.scene_tags = {
|
||||
value: sceneTagsCrit.value.map(tag => tag.id),
|
||||
modifier: sceneTagsCrit.modifier
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "performers": {
|
||||
const performersCrit = criterion as PerformersCriterion;
|
||||
result.performers = { value: performersCrit.value.map((performer) => performer.id), modifier: performersCrit.modifier };
|
||||
result.performers = {
|
||||
value: performersCrit.value.map(performer => performer.id),
|
||||
modifier: performersCrit.modifier
|
||||
};
|
||||
break;
|
||||
}
|
||||
// no default
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export enum DisplayMode {
|
||||
Grid,
|
||||
List,
|
||||
Wall,
|
||||
Wall
|
||||
}
|
||||
|
||||
export enum FilterMode {
|
||||
@@ -9,7 +9,7 @@ export enum FilterMode {
|
||||
Performers,
|
||||
Studios,
|
||||
Galleries,
|
||||
SceneMarkers,
|
||||
SceneMarkers
|
||||
}
|
||||
|
||||
export interface ILabeledId {
|
||||
|
||||
@@ -18,8 +18,8 @@ const isLocalhost = Boolean(
|
||||
window.location.hostname === "[::1]" ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
|
||||
),
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
interface IConfig {
|
||||
@@ -30,7 +30,7 @@ interface IConfig {
|
||||
function registerValidSW(swUrl: string, config?: IConfig) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
.then(registration => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
@@ -45,7 +45,7 @@ function registerValidSW(swUrl: string, config?: IConfig) {
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
"New content is available and will be used when all " +
|
||||
"tabs for this page are closed. See http://bit.ly/CRA-PWA.",
|
||||
"tabs for this page are closed. See http://bit.ly/CRA-PWA."
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
@@ -67,7 +67,7 @@ function registerValidSW(swUrl: string, config?: IConfig) {
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error("Error during service worker registration:", error);
|
||||
});
|
||||
}
|
||||
@@ -75,7 +75,7 @@ function registerValidSW(swUrl: string, config?: IConfig) {
|
||||
function checkValidServiceWorker(swUrl: string, config?: IConfig) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then((response) => {
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (
|
||||
@@ -83,7 +83,7 @@ function checkValidServiceWorker(swUrl: string, config?: IConfig) {
|
||||
(contentType != null && contentType.indexOf("javascript") === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
@@ -95,7 +95,7 @@ function checkValidServiceWorker(swUrl: string, config?: IConfig) {
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
"No internet connection found. App is running in offline mode.",
|
||||
"No internet connection found. App is running in offline mode."
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export function register(config?: IConfig) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(
|
||||
(process as { env: { [key: string]: string } }).env.PUBLIC_URL,
|
||||
window.location.href,
|
||||
window.location.href
|
||||
);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
@@ -126,7 +126,7 @@ export function register(config?: IConfig) {
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
"This web app is being served cache-first by a service " +
|
||||
"worker. To learn more, visit http://bit.ly/CRA-PWA",
|
||||
"worker. To learn more, visit http://bit.ly/CRA-PWA"
|
||||
);
|
||||
});
|
||||
} else {
|
||||
@@ -137,10 +137,9 @@ export function register(config?: IConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function unregister() {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,34 +4,38 @@ const readImage = (file: File, onLoadEnd: (this: FileReader) => void) => {
|
||||
const reader: FileReader = new FileReader();
|
||||
reader.onloadend = onLoadEnd;
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const pasteImage = (event: ClipboardEvent, onLoadEnd: (this: FileReader) => void) => {
|
||||
const pasteImage = (
|
||||
event: ClipboardEvent,
|
||||
onLoadEnd: (this: FileReader) => void
|
||||
) => {
|
||||
const files = event?.clipboardData?.files;
|
||||
if(!files?.length)
|
||||
return;
|
||||
if (!files?.length) return;
|
||||
|
||||
const file = files[0];
|
||||
readImage(file, onLoadEnd);
|
||||
}
|
||||
};
|
||||
|
||||
const onImageChange = (event: React.FormEvent<HTMLInputElement>, onLoadEnd: (this: FileReader) => void) => {
|
||||
const onImageChange = (
|
||||
event: React.FormEvent<HTMLInputElement>,
|
||||
onLoadEnd: (this: FileReader) => void
|
||||
) => {
|
||||
const file = event?.currentTarget?.files?.[0];
|
||||
if(file)
|
||||
readImage(file, onLoadEnd);
|
||||
}
|
||||
if (file) readImage(file, onLoadEnd);
|
||||
};
|
||||
|
||||
const usePasteImage = (onLoadEnd: (this: FileReader) => void) => {
|
||||
useEffect(() => {
|
||||
const paste = (event: ClipboardEvent) => ( pasteImage(event, onLoadEnd) );
|
||||
const paste = (event: ClipboardEvent) => pasteImage(event, onLoadEnd);
|
||||
document.addEventListener("paste", paste);
|
||||
|
||||
return () => document.removeEventListener("paste", paste);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const Image = {
|
||||
onImageChange,
|
||||
usePasteImage
|
||||
}
|
||||
};
|
||||
export default Image;
|
||||
|
||||
@@ -1,4 +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';
|
||||
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,51 +5,54 @@ import { TagsCriterion } from "../models/list-filter/criteria/tags";
|
||||
import { ListFilterModel } from "../models/list-filter/filter";
|
||||
import { FilterMode } from "../models/list-filter/types";
|
||||
|
||||
const makePerformerScenesUrl = (performer: Partial<GQL.PerformerDataFragment>) => {
|
||||
if (!performer.id)
|
||||
return "#";
|
||||
const makePerformerScenesUrl = (
|
||||
performer: Partial<GQL.PerformerDataFragment>
|
||||
) => {
|
||||
if (!performer.id) return "#";
|
||||
const filter = new ListFilterModel(FilterMode.Scenes);
|
||||
const criterion = new PerformersCriterion();
|
||||
criterion.value = [{ id: performer.id, label: performer.name || `Performer ${performer.id}` }];
|
||||
criterion.value = [
|
||||
{ id: performer.id, label: performer.name || `Performer ${performer.id}` }
|
||||
];
|
||||
filter.criteria.push(criterion);
|
||||
return `/scenes?${filter.makeQueryParameters()}`;
|
||||
}
|
||||
};
|
||||
|
||||
const makeStudioScenesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
||||
if (!studio.id)
|
||||
return "#";
|
||||
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}` }];
|
||||
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 "#";
|
||||
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 "#";
|
||||
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 "#";
|
||||
const makeSceneMarkerUrl = (
|
||||
sceneMarker: Partial<GQL.SceneMarkerDataFragment>
|
||||
) => {
|
||||
if (!sceneMarker.id || !sceneMarker.scene) return "#";
|
||||
return `/scenes/${sceneMarker.scene.id}?t=${sceneMarker.seconds}`;
|
||||
}
|
||||
};
|
||||
|
||||
const Nav = {
|
||||
makePerformerScenesUrl,
|
||||
@@ -57,5 +60,5 @@ const Nav = {
|
||||
makeTagSceneMarkersUrl,
|
||||
makeTagScenesUrl,
|
||||
makeSceneMarkerUrl
|
||||
}
|
||||
};
|
||||
export default Nav;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from "react";
|
||||
import { Form } from 'react-bootstrap';
|
||||
import { Form } from "react-bootstrap";
|
||||
import { FilterSelect } from "src/components/Shared";
|
||||
|
||||
const renderEditableTextTableRow = (options: {
|
||||
title: string;
|
||||
value?: string | number;
|
||||
isEditing: boolean;
|
||||
onChange: ((value: string) => void);
|
||||
onChange: (value: string) => void;
|
||||
}) => (
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
@@ -14,19 +14,25 @@ const renderEditableTextTableRow = (options: {
|
||||
<Form.Control
|
||||
readOnly={!options.isEditing}
|
||||
plaintext={!options.isEditing}
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) => ( options.onChange(event.currentTarget.value) )}
|
||||
value={typeof options.value === 'number' ? options.value.toString() : options.value}
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
||||
options.onChange(event.currentTarget.value)
|
||||
}
|
||||
value={
|
||||
typeof options.value === "number"
|
||||
? options.value.toString()
|
||||
: options.value
|
||||
}
|
||||
placeholder={options.title}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
|
||||
const renderTextArea = (options: {
|
||||
title: string,
|
||||
value: string | undefined,
|
||||
isEditing: boolean,
|
||||
onChange: ((value: string) => void),
|
||||
title: string;
|
||||
value: string | undefined;
|
||||
isEditing: boolean;
|
||||
onChange: (value: string) => void;
|
||||
}) => (
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
@@ -35,19 +41,21 @@ const renderTextArea = (options: {
|
||||
as="textarea"
|
||||
readOnly={!options.isEditing}
|
||||
plaintext={!options.isEditing}
|
||||
onChange={(event: React.FormEvent<HTMLTextAreaElement>) => ( options.onChange(event.currentTarget.value) )}
|
||||
onChange={(event: React.FormEvent<HTMLTextAreaElement>) =>
|
||||
options.onChange(event.currentTarget.value)
|
||||
}
|
||||
value={options.value}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
|
||||
const renderInputGroup = (options: {
|
||||
title: string,
|
||||
placeholder?: string,
|
||||
value: string | undefined,
|
||||
isEditing: boolean,
|
||||
onChange: ((value: string) => void),
|
||||
title: string;
|
||||
placeholder?: string;
|
||||
value: string | undefined;
|
||||
isEditing: boolean;
|
||||
onChange: (value: string) => void;
|
||||
}) => (
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
@@ -57,18 +65,20 @@ const renderInputGroup = (options: {
|
||||
plaintext={!options.isEditing}
|
||||
defaultValue={options.value}
|
||||
placeholder={options.placeholder ?? options.title}
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) => ( options.onChange(event.currentTarget.value) )}
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
||||
options.onChange(event.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
|
||||
const renderHtmlSelect = (options: {
|
||||
title: string,
|
||||
value?: string | number,
|
||||
isEditing: boolean,
|
||||
onChange: ((value: string) => void),
|
||||
selectOptions: Array<string | number>,
|
||||
title: string;
|
||||
value?: string | number;
|
||||
isEditing: boolean;
|
||||
onChange: (value: string) => void;
|
||||
selectOptions: Array<string | number>;
|
||||
}) => (
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
@@ -77,37 +87,39 @@ const renderHtmlSelect = (options: {
|
||||
as="select"
|
||||
readOnly={!options.isEditing}
|
||||
plaintext={!options.isEditing}
|
||||
onChange={(event: React.FormEvent<HTMLSelectElement>) => ( options.onChange(event.currentTarget.value) )}
|
||||
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
|
||||
options.onChange(event.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
|
||||
// TODO: isediting
|
||||
const renderFilterSelect = (options: {
|
||||
title: string,
|
||||
type: "performers" | "studios" | "tags",
|
||||
initialId: string | undefined,
|
||||
onChange: ((id: string | undefined) => void),
|
||||
title: string;
|
||||
type: "performers" | "studios" | "tags";
|
||||
initialId: string | undefined;
|
||||
onChange: (id: string | undefined) => void;
|
||||
}) => (
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
<td>
|
||||
<FilterSelect
|
||||
type={options.type}
|
||||
onSelect={(items) => options.onChange(items[0]?.id)}
|
||||
onSelect={items => options.onChange(items[0]?.id)}
|
||||
initialIds={options.initialId ? [options.initialId] : []}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
|
||||
// TODO: isediting
|
||||
const renderMultiSelect = (options: {
|
||||
title: string,
|
||||
type: "performers" | "studios" | "tags",
|
||||
initialIds: string[] | undefined,
|
||||
onChange: ((ids: string[]) => void),
|
||||
title: string;
|
||||
type: "performers" | "studios" | "tags";
|
||||
initialIds: string[] | undefined;
|
||||
onChange: (ids: string[]) => void;
|
||||
}) => (
|
||||
<tr>
|
||||
<td>{options.title}</td>
|
||||
@@ -115,12 +127,12 @@ const renderMultiSelect = (options: {
|
||||
<FilterSelect
|
||||
type={options.type}
|
||||
isMulti
|
||||
onSelect={(items) => options.onChange(items.map((i) => i.id))}
|
||||
onSelect={items => options.onChange(items.map(i => i.id))}
|
||||
initialIds={options.initialIds ?? []}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
|
||||
const Table = {
|
||||
renderEditableTextTableRow,
|
||||
@@ -129,5 +141,5 @@ const Table = {
|
||||
renderHtmlSelect,
|
||||
renderFilterSelect,
|
||||
renderMultiSelect
|
||||
}
|
||||
};
|
||||
export default Table;
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
const Units = [
|
||||
"bytes",
|
||||
"kB",
|
||||
"MB",
|
||||
"GB",
|
||||
"TB",
|
||||
"PB",
|
||||
];
|
||||
const Units = ["bytes", "kB", "MB", "GB", "TB", "PB"];
|
||||
|
||||
const truncate = (value?: string, limit: number = 100, tail: string = "...") => {
|
||||
if (!value)
|
||||
return "";
|
||||
return value.length > limit
|
||||
? value.substring(0, limit) + tail
|
||||
: value;
|
||||
}
|
||||
const truncate = (
|
||||
value?: string,
|
||||
limit: number = 100,
|
||||
tail: string = "..."
|
||||
) => {
|
||||
if (!value) return "";
|
||||
return value.length > limit ? value.substring(0, limit) + tail : value;
|
||||
};
|
||||
|
||||
const fileSize = (bytes: number = 0, precision: number = 2) => {
|
||||
if (Number.isNaN(parseFloat(String(bytes))) || !Number.isFinite(bytes))
|
||||
@@ -21,13 +15,13 @@ const fileSize = (bytes: number = 0, precision: number = 2) => {
|
||||
|
||||
let unit = 0;
|
||||
let count = bytes;
|
||||
while ( count >= 1024 ) {
|
||||
while (count >= 1024) {
|
||||
count /= 1024;
|
||||
unit++;
|
||||
}
|
||||
|
||||
return `${bytes.toFixed(+precision)} ${Units[unit]}`;
|
||||
}
|
||||
};
|
||||
|
||||
const secondsToTimestamp = (seconds: number) => {
|
||||
let ret = new Date(seconds * 1000).toISOString().substr(11, 8);
|
||||
@@ -41,34 +35,35 @@ const secondsToTimestamp = (seconds: number) => {
|
||||
ret = ret.substr(1);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
const fileNameFromPath = (path: string) => {
|
||||
if (!!path === false)
|
||||
return "No File Name";
|
||||
if (!!path === false) return "No File Name";
|
||||
return path.replace(/^.*[\\/]/, "");
|
||||
}
|
||||
};
|
||||
|
||||
const getAge = (dateString?: string, fromDateString?: string) => {
|
||||
if (!dateString)
|
||||
return 0;
|
||||
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())) {
|
||||
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) {
|
||||
@@ -86,7 +81,7 @@ const resolution = (height: number) => {
|
||||
if (height >= 2160) {
|
||||
return "4K";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const TextUtils = {
|
||||
truncate,
|
||||
@@ -96,6 +91,6 @@ const TextUtils = {
|
||||
age: getAge,
|
||||
bitRate,
|
||||
resolution
|
||||
}
|
||||
};
|
||||
|
||||
export default TextUtils;
|
||||
|
||||
@@ -4613,13 +4613,6 @@ eslint-plugin-jsx-a11y@6.2.3, eslint-plugin-jsx-a11y@^6.2.3:
|
||||
has "^1.0.3"
|
||||
jsx-ast-utils "^2.2.1"
|
||||
|
||||
eslint-plugin-prettier@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz#432e5a667666ab84ce72f945c72f77d996a5c9ba"
|
||||
integrity sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==
|
||||
dependencies:
|
||||
prettier-linter-helpers "^1.0.0"
|
||||
|
||||
eslint-plugin-react-hooks@^1.6.1:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz#6210b6d5a37205f0b92858f895a4e827020a7d04"
|
||||
@@ -4943,11 +4936,6 @@ fast-deep-equal@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
|
||||
integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
|
||||
|
||||
fast-diff@^1.1.2:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
|
||||
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
|
||||
|
||||
fast-glob@^2.0.2:
|
||||
version "2.2.7"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
|
||||
@@ -9734,19 +9722,12 @@ prepend-http@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
||||
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
|
||||
|
||||
prettier-linter-helpers@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
|
||||
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
|
||||
dependencies:
|
||||
fast-diff "^1.1.2"
|
||||
|
||||
prettier@1.16.4:
|
||||
version "1.16.4"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.4.tgz#73e37e73e018ad2db9c76742e2647e21790c9717"
|
||||
integrity sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g==
|
||||
|
||||
prettier@^1.19.1:
|
||||
prettier@1.19.1:
|
||||
version "1.19.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
||||
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
||||
|
||||
Reference in New Issue
Block a user