Upgrade javascript libraries (#516)

* Bump react-bootstrap
* Bump library versions and clean up hooks
* Bump intl libraries
* Fix image pasting
This commit is contained in:
InfiniteTF
2020-05-08 04:06:07 +02:00
committed by GitHub
parent 99f88b8d73
commit df2d2c2d09
50 changed files with 3192 additions and 2797 deletions

View File

@@ -25,79 +25,77 @@
"not op_mini all" "not op_mini all"
], ],
"dependencies": { "dependencies": {
"@apollo/react-hooks": "^3.1.3", "@apollo/react-hooks": "^3.1.5",
"@fortawesome/fontawesome-svg-core": "^1.2.26", "@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-solid-svg-icons": "^5.12.0", "@fortawesome/free-solid-svg-icons": "^5.13.0",
"@fortawesome/react-fontawesome": "^0.1.8", "@fortawesome/react-fontawesome": "^0.1.9",
"apollo-cache": "^1.3.4", "apollo-cache": "^1.3.4",
"apollo-cache-inmemory": "^1.6.5", "apollo-cache-inmemory": "^1.6.5",
"apollo-client": "^2.6.8", "apollo-client": "^2.6.8",
"apollo-link": "^1.2.13", "apollo-link": "^1.2.14",
"apollo-link-http": "^1.5.16", "apollo-link-error": "^1.1.13",
"apollo-link-ws": "^1.0.19", "apollo-link-http": "^1.5.17",
"apollo-link-ws": "^1.0.20",
"apollo-utilities": "^1.3.3", "apollo-utilities": "^1.3.3",
"axios": "0.18.1", "axios": "0.19.2",
"bootstrap": "^4.4.1", "bootstrap": "^4.4.1",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"flag-icon-css": "^3.4.6", "flag-icon-css": "^3.4.6",
"formik": "^2.1.2", "formik": "^2.1.4",
"graphql": "^14.5.8", "graphql": "^14.5.8",
"graphql-tag": "^2.10.1", "graphql-tag": "^2.10.3",
"i18n-iso-countries": "^5.2.0", "i18n-iso-countries": "^5.2.0",
"localforage": "1.7.3", "localforage": "1.7.3",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"query-string": "6.10.1", "query-string": "6.12.1",
"react": "~16.12.0", "react": "16.13.1",
"react-apollo": "^3.1.3", "react-apollo": "^3.1.5",
"react-bootstrap": "^1.0.0-beta.16", "react-bootstrap": "1.0.1",
"react-dom": "16.12.0", "react-dom": "16.13.1",
"react-images": "0.5.19", "react-images": "0.5.19",
"react-intl": "^3.12.0", "react-intl": "^4.5.1",
"react-jw-player": "1.19.0", "react-jw-player": "1.19.1",
"react-photo-gallery": "^8.0.0", "react-photo-gallery": "^8.0.0",
"react-router-bootstrap": "^0.25.0", "react-router-bootstrap": "^0.25.0",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-select": "^3.0.8", "react-select": "^3.1.0",
"subscriptions-transport-ws": "^0.9.16", "subscriptions-transport-ws": "^0.9.16",
"universal-cookie": "^4.0.3", "universal-cookie": "^4.0.3"
"video.js": "^7.6.0"
}, },
"devDependencies": { "devDependencies": {
"@graphql-codegen/add": "^1.11.2", "@graphql-codegen/add": "^1.13.5",
"@graphql-codegen/cli": "^1.11.2", "@graphql-codegen/cli": "^1.13.5",
"@graphql-codegen/time": "^1.11.2", "@graphql-codegen/time": "^1.13.5",
"@graphql-codegen/typescript": "^1.11.2", "@graphql-codegen/typescript": "^1.13.5",
"@graphql-codegen/typescript-compatibility": "^1.11.2", "@graphql-codegen/typescript-compatibility": "^1.13.5",
"@graphql-codegen/typescript-operations": "^1.11.2", "@graphql-codegen/typescript-operations": "^1.13.5",
"@graphql-codegen/typescript-react-apollo": "^1.11.2", "@graphql-codegen/typescript-react-apollo": "^1.13.5",
"@types/classnames": "^2.2.9", "@types/classnames": "^2.2.10",
"@types/jest": "24.0.13", "@types/lodash": "^4.14.150",
"@types/lodash": "^4.14.149", "@types/node": "13.13.4",
"@types/node": "13.1.8", "@types/react": "16.9.34",
"@types/react": "16.9.19", "@types/react-dom": "^16.9.7",
"@types/react-dom": "^16.9.5",
"@types/react-images": "^0.5.1", "@types/react-images": "^0.5.1",
"@types/react-router-bootstrap": "^0.24.5", "@types/react-router-bootstrap": "^0.24.5",
"@types/react-router-dom": "5.1.3", "@types/react-router-dom": "5.1.5",
"@types/react-select": "^3.0.8", "@types/react-select": "^3.0.12",
"@types/video.js": "^7.2.11", "@typescript-eslint/eslint-plugin": "^2.30.0",
"@typescript-eslint/eslint-plugin": "^2.16.0", "@typescript-eslint/parser": "^2.30.0",
"@typescript-eslint/parser": "^2.16.0", "eslint": "^6.8.0",
"eslint": "^6.7.2", "eslint-config-airbnb-typescript": "^7.2.1",
"eslint-config-airbnb-typescript": "^6.3.1", "eslint-config-prettier": "^6.11.0",
"eslint-config-prettier": "^6.10.1", "eslint-plugin-import": "^2.20.2",
"eslint-plugin-import": "^2.20.0",
"eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.18.0", "eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^1.7.0", "eslint-plugin-react-hooks": "^4.0.0",
"extract-react-intl-messages": "^2.3.5", "extract-react-intl-messages": "^4.1.1",
"node-sass": "4.13.1", "node-sass": "4.14.0",
"postcss-safe-parser": "^4.0.1", "postcss-safe-parser": "^4.0.2",
"prettier": "2.0.2", "prettier": "2.0.5",
"react-scripts": "^3.3.1", "react-scripts": "^3.4.1",
"stylelint": "^13.0.0", "stylelint": "^13.3.3",
"stylelint-config-prettier": "^8.0.1", "stylelint-config-prettier": "^8.0.1",
"stylelint-order": "^4.0.0", "stylelint-order": "^4.0.0",
"typescript": "^3.7.5" "typescript": "^3.8.3"
} }
} }

View File

@@ -6,7 +6,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import { fas } from "@fortawesome/free-solid-svg-icons"; import { fas } from "@fortawesome/free-solid-svg-icons";
import locales from "src/locale"; import locales from "src/locale";
import { StashService } from "src/core/StashService"; import { useConfiguration } from "src/core/StashService";
import { flattenMessages } from "src/utils"; import { flattenMessages } from "src/utils";
import { ErrorBoundary } from "./components/ErrorBoundary"; import { ErrorBoundary } from "./components/ErrorBoundary";
import Galleries from "./components/Galleries/Galleries"; import Galleries from "./components/Galleries/Galleries";
@@ -25,7 +25,7 @@ import Movies from "./components/Movies/Movies";
library.add(fas); library.add(fas);
export const App: React.FC = () => { export const App: React.FC = () => {
const config = StashService.useConfiguration(); const config = useConfiguration();
const language = config.data?.configuration?.interface?.language ?? "en-US"; const language = config.data?.configuration?.interface?.language ?? "en-US";
const messageLanguage = language.slice(0, 2); const messageLanguage = language.slice(0, 2);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -1,13 +1,13 @@
import React from "react"; import React from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { StashService } from "src/core/StashService"; import { useFindGallery } from "src/core/StashService";
import { LoadingIndicator } from "src/components/Shared"; import { LoadingIndicator } from "src/components/Shared";
import { GalleryViewer } from "./GalleryViewer"; import { GalleryViewer } from "./GalleryViewer";
export const Gallery: React.FC = () => { export const Gallery: React.FC = () => {
const { id = "" } = useParams(); const { id = "" } = useParams();
const { data, error, loading } = StashService.useFindGallery(id); const { data, error, loading } = useFindGallery(id);
const gallery = data?.findGallery; const gallery = data?.findGallery;
if (loading || !gallery) return <LoadingIndicator />; if (loading || !gallery) return <LoadingIndicator />;

View File

@@ -1,5 +1,5 @@
import { debounce } from "lodash"; import { debounce } from "lodash";
import React, { useCallback, useState } from "react"; import React, { useState } from "react";
import { SortDirectionEnum } from "src/core/generated-graphql"; import { SortDirectionEnum } from "src/core/generated-graphql";
import { import {
Badge, Badge,
@@ -9,7 +9,7 @@ import {
Form, Form,
OverlayTrigger, OverlayTrigger,
Tooltip, Tooltip,
SafeAnchor, SafeAnchorProps,
} from "react-bootstrap"; } from "react-bootstrap";
import { Icon } from "src/components/Shared"; import { Icon } from "src/components/Shared";
@@ -45,18 +45,15 @@ const PAGE_SIZE_OPTIONS = ["20", "40", "60", "120"];
export const ListFilter: React.FC<IListFilterProps> = ( export const ListFilter: React.FC<IListFilterProps> = (
props: IListFilterProps props: IListFilterProps
) => { ) => {
const searchCallback = useCallback( const searchCallback = debounce((value: string) => {
debounce((value: string) => {
props.onChangeQuery(value); props.onChangeQuery(value);
}, 500), }, 500);
[props.onChangeQuery]
);
const [editingCriterion, setEditingCriterion] = useState< const [editingCriterion, setEditingCriterion] = useState<
Criterion | undefined Criterion | undefined
>(undefined); >(undefined);
function onChangePageSize(event: React.FormEvent<HTMLSelectElement>) { function onChangePageSize(event: React.ChangeEvent<HTMLSelectElement>) {
const val = event.currentTarget.value; const val = event.currentTarget.value;
props.onChangePageSize(parseInt(val, 10)); props.onChangePageSize(parseInt(val, 10));
} }
@@ -73,8 +70,8 @@ export const ListFilter: React.FC<IListFilterProps> = (
} }
} }
function onChangeSortBy(event: React.MouseEvent<SafeAnchor>) { function onChangeSortBy(event: React.MouseEvent<SafeAnchorProps>) {
const target = (event.currentTarget as unknown) as HTMLAnchorElement; const target = event.currentTarget as HTMLAnchorElement;
props.onChangeSortBy(target.text); props.onChangeSortBy(target.text);
} }
@@ -266,7 +263,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
min={0} min={0}
max={3} max={3}
defaultValue={1} defaultValue={1}
onChange={(e: React.FormEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChangeZoom(Number.parseInt(e.currentTarget.value, 10)) onChangeZoom(Number.parseInt(e.currentTarget.value, 10))
} }
/> />

View File

@@ -1,54 +1,90 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { FormattedMessage } from "react-intl"; import {
defineMessages,
FormattedMessage,
MessageDescriptor,
useIntl,
} from "react-intl";
import { Nav, Navbar, Button } from "react-bootstrap"; import { Nav, Navbar, Button } from "react-bootstrap";
import { IconName } from "@fortawesome/fontawesome-svg-core"; import { IconName } from "@fortawesome/fontawesome-svg-core";
import { LinkContainer } from "react-router-bootstrap"; import { LinkContainer } from "react-router-bootstrap";
import { Link, useLocation } from "react-router-dom"; import { Link, NavLink, useLocation } from "react-router-dom";
import { SessionUtils } from "src/utils"; import { SessionUtils } from "src/utils";
import { Icon } from "src/components/Shared"; import { Icon } from "src/components/Shared";
interface IMenuItem { interface IMenuItem {
messageID: string; message: MessageDescriptor;
href: string; href: string;
icon: IconName; icon: IconName;
} }
const messages = defineMessages({
scenes: {
id: "scenes",
defaultMessage: "Scenes",
},
movies: {
id: "movies",
defaultMessage: "Movies",
},
markers: {
id: "markers",
defaultMessage: "Markers",
},
performers: {
id: "performers",
defaultMessage: "Performers",
},
studios: {
id: "studios",
defaultMessage: "Studios",
},
tags: {
id: "tags",
defaultMessage: "Tags",
},
galleries: {
id: "galleries",
defaultMessage: "Galleries",
},
});
const menuItems: IMenuItem[] = [ const menuItems: IMenuItem[] = [
{ {
icon: "play-circle", icon: "play-circle",
messageID: "scenes", message: messages.scenes,
href: "/scenes", href: "/scenes",
}, },
{ {
href: "/movies", href: "/movies",
icon: "film", icon: "film",
messageID: "movies", message: messages.movies,
}, },
{ {
href: "/scenes/markers", href: "/scenes/markers",
icon: "map-marker-alt", icon: "map-marker-alt",
messageID: "markers", message: messages.markers,
}, },
{ {
href: "/galleries", href: "/galleries",
icon: "image", icon: "image",
messageID: "galleries", message: messages.galleries,
}, },
{ {
href: "/performers", href: "/performers",
icon: "user", icon: "user",
messageID: "performers", message: messages.performers,
}, },
{ {
href: "/studios", href: "/studios",
icon: "video", icon: "video",
messageID: "studios", message: messages.studios,
}, },
{ {
href: "/tags", href: "/tags",
icon: "tag", icon: "tag",
messageID: "tags", message: messages.tags,
}, },
]; ];
@@ -58,6 +94,7 @@ export const MainNavbar: React.FC = () => {
// react-bootstrap typing bug // react-bootstrap typing bug
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const navbarRef = useRef<any>(); const navbarRef = useRef<any>();
const intl = useIntl();
const maybeCollapse = (event: Event) => { const maybeCollapse = (event: Event) => {
if ( if (
@@ -92,11 +129,11 @@ export const MainNavbar: React.FC = () => {
path === null ? ( path === null ? (
"" ""
) : ( ) : (
<LinkContainer to={path}> <Link to={path}>
<Button variant="primary"> <Button variant="primary">
<FormattedMessage id="new" defaultMessage="New" /> <FormattedMessage id="new" defaultMessage="New" />
</Button> </Button>
</LinkContainer> </Link>
); );
function maybeRenderLogout() { function maybeRenderLogout() {
@@ -140,17 +177,10 @@ export const MainNavbar: React.FC = () => {
<Nav className="mr-md-auto"> <Nav className="mr-md-auto">
{menuItems.map((i) => ( {menuItems.map((i) => (
<Nav.Link eventKey={i.href} as="div" key={i.href}> <Nav.Link eventKey={i.href} as="div" key={i.href}>
<LinkContainer <LinkContainer activeClassName="active" exact to={i.href}>
activeClassName="active"
exact
to={i.href}
key={i.href}
>
<Button className="minimal w-100"> <Button className="minimal w-100">
<Icon icon={i.icon} /> <Icon icon={i.icon} />
<span> <span>{intl.formatMessage(i.message)}</span>
<FormattedMessage id={i.messageID} />
</span>
</Button> </Button>
</LinkContainer> </LinkContainer>
</Nav.Link> </Nav.Link>
@@ -159,11 +189,11 @@ export const MainNavbar: React.FC = () => {
</Navbar.Collapse> </Navbar.Collapse>
<Nav className="order-2 flex-row"> <Nav className="order-2 flex-row">
<div className="d-none d-sm-block">{newButton}</div> <div className="d-none d-sm-block">{newButton}</div>
<LinkContainer exact to="/settings" onClick={() => setExpanded(false)}> <NavLink exact to="/settings" onClick={() => setExpanded(false)}>
<Button className="minimal settings-button"> <Button className="minimal settings-button">
<Icon icon="cog" /> <Icon icon="cog" />
</Button> </Button>
</LinkContainer> </NavLink>
{maybeRenderLogout()} {maybeRenderLogout()}
</Nav> </Nav>
</Navbar> </Navbar>

View File

@@ -1,7 +1,12 @@
/* eslint-disable react/no-this-in-sfc */ /* eslint-disable react/no-this-in-sfc */
import React, { useEffect, useState, useCallback } from "react"; import React, { useEffect, useState, useCallback } from "react";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import {
useFindMovie,
useMovieUpdate,
useMovieCreate,
useMovieDestroy,
} from "src/core/StashService";
import { useParams, useHistory } from "react-router-dom"; import { useParams, useHistory } from "react-router-dom";
import cx from "classnames"; import cx from "classnames";
import { import {
@@ -53,14 +58,10 @@ export const Movie: React.FC = () => {
); );
// Network state // Network state
const { data, error, loading } = StashService.useFindMovie(id); const { data, error, loading } = useFindMovie(id);
const [updateMovie] = StashService.useMovieUpdate( const [updateMovie] = useMovieUpdate(getMovieInput() as GQL.MovieUpdateInput);
getMovieInput() as GQL.MovieUpdateInput const [createMovie] = useMovieCreate(getMovieInput() as GQL.MovieCreateInput);
); const [deleteMovie] = useMovieDestroy(
const [createMovie] = StashService.useMovieCreate(
getMovieInput() as GQL.MovieCreateInput
);
const [deleteMovie] = StashService.useMovieDestroy(
getMovieInput() as GQL.MovieDestroyInput getMovieInput() as GQL.MovieDestroyInput
); );
@@ -104,8 +105,8 @@ export const Movie: React.FC = () => {
setBackImage(this.result as string); setBackImage(this.result as string);
} }
ImageUtils.usePasteImage(onImageLoad); ImageUtils.usePasteImage(onImageLoad, isEditing);
ImageUtils.usePasteImage(onBackImageLoad); ImageUtils.usePasteImage(onBackImageLoad, isEditing);
if (!isNew && !isEditing) { if (!isNew && !isEditing) {
if (!data || !data.findMovie || loading) return <LoadingIndicator />; if (!data || !data.findMovie || loading) return <LoadingIndicator />;
@@ -280,7 +281,7 @@ export const Movie: React.FC = () => {
as="textarea" as="textarea"
readOnly={!isEditing} readOnly={!isEditing}
className="movie-synopsis text-input" className="movie-synopsis text-input"
onChange={(newValue: React.FormEvent<HTMLTextAreaElement>) => onChange={(newValue: React.ChangeEvent<HTMLTextAreaElement>) =>
setSynopsis(newValue.currentTarget.value) setSynopsis(newValue.currentTarget.value)
} }
value={synopsis} value={synopsis}

View File

@@ -5,7 +5,12 @@ import { Button, Tabs, Tab } from "react-bootstrap";
import { useParams, useHistory } from "react-router-dom"; import { useParams, useHistory } from "react-router-dom";
import cx from "classnames"; import cx from "classnames";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import {
useFindPerformer,
usePerformerUpdate,
usePerformerCreate,
usePerformerDestroy,
} from "src/core/StashService";
import { Icon, LoadingIndicator } from "src/components/Shared"; import { Icon, LoadingIndicator } from "src/components/Shared";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { getCountryISO, TextUtils } from "src/utils"; import { getCountryISO, TextUtils } from "src/utils";
@@ -26,25 +31,22 @@ export const Performer: React.FC = () => {
>({}); >({});
const [imagePreview, setImagePreview] = useState<string>(); const [imagePreview, setImagePreview] = useState<string>();
const [lightboxIsOpen, setLightboxIsOpen] = useState(false); const [lightboxIsOpen, setLightboxIsOpen] = useState(false);
const activeImage = imagePreview ?? performer.image_path ?? "";
// Network state // Network state
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { data, error } = StashService.useFindPerformer(id); const { data, error } = useFindPerformer(id);
const [updatePerformer] = StashService.usePerformerUpdate(); const [updatePerformer] = usePerformerUpdate();
const [createPerformer] = StashService.usePerformerCreate(); const [createPerformer] = usePerformerCreate();
const [deletePerformer] = StashService.usePerformerDestroy(); const [deletePerformer] = usePerformerDestroy();
useEffect(() => { useEffect(() => {
setIsLoading(false); setIsLoading(false);
if (data?.findPerformer) setPerformer(data.findPerformer); if (data?.findPerformer) setPerformer(data.findPerformer);
}, [data]); }, [data]);
useEffect(() => { function onImageChange(image?: string) {
setImagePreview(performer.image_path ?? undefined);
}, [performer]);
function onImageChange(image: string) {
setImagePreview(image); setImagePreview(image);
} }
@@ -64,6 +66,10 @@ export const Performer: React.FC = () => {
const result = await updatePerformer({ const result = await updatePerformer({
variables: performerInput as GQL.PerformerUpdateInput, variables: performerInput as GQL.PerformerUpdateInput,
}); });
if (performerInput.image) {
// Refetch image to bust browser cache
await fetch(`/performer/${performer.id}/image`, { cache: "reload" });
}
if (result.data?.performerUpdate) if (result.data?.performerUpdate)
setPerformer(result.data?.performerUpdate); setPerformer(result.data?.performerUpdate);
} else { } else {
@@ -105,9 +111,15 @@ export const Performer: React.FC = () => {
return undefined; return undefined;
}; };
function renderTabs() { const renderTabs = () => (
function renderEditPanel() { <Tabs defaultActiveKey="details" id="performer-details" unmountOnExit>
return ( <Tab eventKey="details" title="Details">
<PerformerDetailsPanel performer={performer} isEditing={false} />
</Tab>
<Tab eventKey="scenes" title="Scenes">
<PerformerScenesPanel performer={performer} />
</Tab>
<Tab eventKey="edit" title="Edit">
<PerformerDetailsPanel <PerformerDetailsPanel
performer={performer} performer={performer}
isEditing isEditing
@@ -116,55 +128,33 @@ export const Performer: React.FC = () => {
onSave={onSave} onSave={onSave}
onImageChange={onImageChange} onImageChange={onImageChange}
/> />
);
}
// render tabs if not new
if (!isNew) {
return (
<Tabs defaultActiveKey="details" id="performer-details">
<Tab eventKey="details" title="Details">
<PerformerDetailsPanel performer={performer} isEditing={false} />
</Tab>
<Tab eventKey="scenes" title="Scenes">
<PerformerScenesPanel performer={performer} />
</Tab>
<Tab eventKey="edit" title="Edit">
{renderEditPanel()}
</Tab> </Tab>
<Tab eventKey="operations" title="Operations"> <Tab eventKey="operations" title="Operations">
<PerformerOperationsPanel performer={performer} /> <PerformerOperationsPanel performer={performer} />
</Tab> </Tab>
</Tabs> </Tabs>
); );
}
return renderEditPanel();
}
function maybeRenderAge() { function maybeRenderAge() {
if (performer && performer.birthdate) { if (performer?.birthdate) {
// calculate the age from birthdate. In future, this should probably be // calculate the age from birthdate. In future, this should probably be
// provided by the server // provided by the server
return ( return (
<>
<div> <div>
<span className="age">{TextUtils.age(performer.birthdate)}</span> <span className="age">{TextUtils.age(performer.birthdate)}</span>
<span className="age-tail"> years old</span> <span className="age-tail"> years old</span>
</div> </div>
</>
); );
} }
} }
function maybeRenderAliases() { function maybeRenderAliases() {
if (performer && performer.aliases) { if (performer?.aliases) {
return ( return (
<>
<div> <div>
<span className="alias-head">Also known as </span> <span className="alias-head">Also known as </span>
<span className="alias">{performer.aliases}</span> <span className="alias">{performer.aliases}</span>
</div> </div>
</>
); );
} }
} }
@@ -231,34 +221,36 @@ export const Performer: React.FC = () => {
); );
function renderPerformerImage() { function renderPerformerImage() {
if (imagePreview) { if (activeImage) {
return <img className="photo" src={imagePreview} alt="Performer" />; return <img className="photo" src={activeImage} alt="Performer" />;
} }
} }
function renderNewView() { if (isNew)
return ( return (
<div className="row new-view"> <div className="row new-view">
<div className="col-4">{renderPerformerImage()}</div> <div className="col-4">{renderPerformerImage()}</div>
<div className="col-6"> <div className="col-6">
<h2>Create Performer</h2> <h2>Create Performer</h2>
{renderTabs()} <PerformerDetailsPanel
performer={performer}
isEditing
isNew={isNew}
onDelete={onDelete}
onSave={onSave}
onImageChange={onImageChange}
/>
</div> </div>
</div> </div>
); );
}
const photos = [{ src: imagePreview || "", caption: "Image" }]; const photos = [{ src: activeImage, caption: "Image" }];
if (isNew) {
return renderNewView();
}
return ( return (
<div id="performer-page" className="row"> <div id="performer-page" className="row">
<div className="image-container col-sm-4 offset-sm-1 d-none d-sm-block"> <div className="image-container col-sm-4 offset-sm-1 d-none d-sm-block">
<Button variant="link" onClick={() => setLightboxIsOpen(true)}> <Button variant="link" onClick={() => setLightboxIsOpen(true)}>
<img className="performer" src={imagePreview} alt="Performer" /> <img className="performer" src={activeImage} alt="Performer" />
</Button> </Button>
</div> </div>
<div className="col col-sm-6"> <div className="col col-sm-6">
@@ -282,7 +274,7 @@ export const Performer: React.FC = () => {
onClose={() => setLightboxIsOpen(false)} onClose={() => setLightboxIsOpen(false)}
currentImage={0} currentImage={0}
isOpen={lightboxIsOpen} isOpen={lightboxIsOpen}
onClickImage={() => window.open(imagePreview, "_blank")} onClickImage={() => window.open(activeImage, "_blank")}
width={9999} width={9999}
/> />
</div> </div>

View File

@@ -3,7 +3,14 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Button, Popover, OverlayTrigger, Table } from "react-bootstrap"; import { Button, Popover, OverlayTrigger, Table } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import {
getGenderStrings,
useListPerformerScrapers,
genderToString,
stringToGender,
queryScrapePerformer,
queryScrapePerformerURL,
} from "src/core/StashService";
import { import {
Icon, Icon,
Modal, Modal,
@@ -29,7 +36,7 @@ interface IPerformerDetails {
| Partial<GQL.PerformerUpdateInput> | Partial<GQL.PerformerUpdateInput>
) => void; ) => void;
onDelete?: () => void; onDelete?: () => void;
onImageChange?: (image: string) => void; onImageChange?: (image?: string) => void;
} }
export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
@@ -74,9 +81,11 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
// Network state // Network state
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const Scrapers = StashService.useListPerformerScrapers(); const Scrapers = useListPerformerScrapers();
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]); const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
ImageUtils.usePasteImage(onImageLoad, isEditing);
function updatePerformerEditState( function updatePerformerEditState(
state: Partial<GQL.PerformerDataFragment | GQL.ScrapedPerformerDataFragment> state: Partial<GQL.PerformerDataFragment | GQL.ScrapedPerformerDataFragment>
) { ) {
@@ -99,9 +108,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
setTwitter(state.twitter ?? undefined); setTwitter(state.twitter ?? undefined);
setInstagram(state.instagram ?? undefined); setInstagram(state.instagram ?? undefined);
setGender( setGender(
StashService.genderToString( genderToString((state as GQL.PerformerDataFragment).gender ?? undefined)
(state as GQL.PerformerDataFragment).gender ?? undefined
)
); );
} }
@@ -114,16 +121,16 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
// try to translate from enum values first // try to translate from enum values first
const upperGender = scrapedGender?.toUpperCase(); const upperGender = scrapedGender?.toUpperCase();
const asEnum = StashService.genderToString(upperGender as GQL.GenderEnum); const asEnum = genderToString(upperGender as GQL.GenderEnum);
if (asEnum) { if (asEnum) {
retEnum = StashService.stringToGender(asEnum); retEnum = stringToGender(asEnum);
} else { } else {
// try to match against gender strings // try to match against gender strings
const caseInsensitive = true; const caseInsensitive = true;
retEnum = StashService.stringToGender(scrapedGender, caseInsensitive); retEnum = stringToGender(scrapedGender, caseInsensitive);
} }
return StashService.genderToString(retEnum); return genderToString(retEnum);
} }
function updatePerformerEditStateFromScraper( function updatePerformerEditStateFromScraper(
@@ -142,10 +149,11 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
) { ) {
const imageStr = (state as GQL.ScrapedPerformerDataFragment).image; const imageStr = (state as GQL.ScrapedPerformerDataFragment).image;
setImage(imageStr ?? undefined); setImage(imageStr ?? undefined);
if (onImageChange) {
onImageChange(imageStr!);
} }
} }
function onImageLoad(this: FileReader) {
setImage(this.result as string);
} }
useEffect(() => { useEffect(() => {
@@ -153,14 +161,12 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
updatePerformerEditState(performer); updatePerformerEditState(performer);
}, [performer]); }, [performer]);
function onImageLoad(this: FileReader) { useEffect(() => {
setImage(this.result as string);
if (onImageChange) { if (onImageChange) {
onImageChange(this.result as string); onImageChange(image);
} }
} return () => onImageChange?.();
}, [image, onImageChange]);
if (isEditing) ImageUtils.usePasteImage(onImageLoad);
useEffect(() => { useEffect(() => {
const newQueryableScrapers = ( const newQueryableScrapers = (
@@ -195,7 +201,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
twitter, twitter,
instagram, instagram,
image, image,
gender: StashService.stringToGender(gender), gender: stringToGender(gender),
}; };
if (!isNew) { if (!isNew) {
@@ -225,7 +231,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
try { try {
if (!scrapePerformerDetails || !isDisplayingScraperDialog) return; if (!scrapePerformerDetails || !isDisplayingScraperDialog) return;
setIsLoading(true); setIsLoading(true);
const result = await StashService.queryScrapePerformer( const result = await queryScrapePerformer(
isDisplayingScraperDialog.id, isDisplayingScraperDialog.id,
getQueryScraperPerformerInput() getQueryScraperPerformerInput()
); );
@@ -242,7 +248,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
if (!url) return; if (!url) return;
setIsLoading(true); setIsLoading(true);
try { try {
const result = await StashService.queryScrapePerformerURL(url); const result = await queryScrapePerformerURL(url);
if (!result.data || !result.data.scrapePerformerURL) { if (!result.data || !result.data.scrapePerformerURL) {
return; return;
} }
@@ -442,7 +448,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
value: gender, value: gender,
isEditing: !!isEditing, isEditing: !!isEditing,
onChange: (value: string) => setGender(value), onChange: (value: string) => setGender(value),
selectOptions: [""].concat(StashService.getGenderStrings()), selectOptions: [""].concat(getGenderStrings()),
}); });
} }

View File

@@ -1,7 +1,7 @@
import { Button } from "react-bootstrap"; import { Button } from "react-bootstrap";
import React from "react"; import React from "react";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { mutateMetadataAutoTag } from "src/core/StashService";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
interface IPerformerOperationsProps { interface IPerformerOperationsProps {
@@ -18,7 +18,7 @@ export const PerformerOperationsPanel: React.FC<IPerformerOperationsProps> = ({
return; return;
} }
try { try {
await StashService.mutateMetadataAutoTag({ performers: [performer.id] }); await mutateMetadataAutoTag({ performers: [performer.id] });
Toast.success({ content: "Started auto tagging" }); Toast.success({ content: "Started auto tagging" });
} catch (e) { } catch (e) {
Toast.error(e); Toast.error(e);

View File

@@ -2,7 +2,7 @@ import _ from "lodash";
import React from "react"; import React from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { FindPerformersQueryResult } from "src/core/generated-graphql"; import { FindPerformersQueryResult } from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { queryFindPerformers } from "src/core/StashService";
import { usePerformersList } from "src/hooks"; import { usePerformersList } from "src/hooks";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types"; import { DisplayMode } from "src/models/list-filter/types";
@@ -33,7 +33,7 @@ export const PerformerList: React.FC = () => {
const filterCopy = _.cloneDeep(filter); const filterCopy = _.cloneDeep(filter);
filterCopy.itemsPerPage = 1; filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1; filterCopy.currentPage = index + 1;
const singleResult = await StashService.queryFindPerformers(filterCopy); const singleResult = await queryFindPerformers(filterCopy);
if ( if (
singleResult && singleResult &&
singleResult.data && singleResult.data &&

View File

@@ -133,7 +133,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
<Form.Control <Form.Control
className="text-input" className="text-input"
id="filename-pattern" id="filename-pattern"
onChange={(e: React.FormEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setPattern(e.currentTarget.value) setPattern(e.currentTarget.value)
} }
value={pattern} value={pattern}
@@ -162,7 +162,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
<InputGroup className="col-8"> <InputGroup className="col-8">
<Form.Control <Form.Control
className="text-input" className="text-input"
onChange={(e: React.FormEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setIgnoreWords(e.currentTarget.value) setIgnoreWords(e.currentTarget.value)
} }
value={ignoreWords} value={ignoreWords}
@@ -181,7 +181,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
<InputGroup className="col-8"> <InputGroup className="col-8">
<Form.Control <Form.Control
className="text-input" className="text-input"
onChange={(e: React.FormEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setWhitespaceCharacters(e.currentTarget.value) setWhitespaceCharacters(e.currentTarget.value)
} }
value={whitespaceCharacters} value={whitespaceCharacters}
@@ -236,8 +236,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
</Button> </Button>
<Form.Control <Form.Control
as="select" as="select"
options={PAGE_SIZE_OPTIONS} onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange={(e: React.FormEvent<HTMLInputElement>) =>
props.onPageSizeChanged(parseInt(e.currentTarget.value, 10)) props.onPageSizeChanged(parseInt(e.currentTarget.value, 10))
} }
defaultValue={props.input.pageSize} defaultValue={props.input.pageSize}

View File

@@ -3,7 +3,10 @@
import React, { useEffect, useState, useCallback, useRef } from "react"; import React, { useEffect, useState, useCallback, useRef } from "react";
import { Button, Card, Form, Table } from "react-bootstrap"; import { Button, Card, Form, Table } from "react-bootstrap";
import _ from "lodash"; import _ from "lodash";
import { StashService } from "src/core/StashService"; import {
queryParseSceneFilenames,
useScenesUpdate,
} from "src/core/StashService";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { LoadingIndicator } from "src/components/Shared"; import { LoadingIndicator } from "src/components/Shared";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
@@ -56,7 +59,7 @@ export const SceneFilenameParser: React.FC = () => {
// Network state // Network state
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [updateScenes] = StashService.useScenesUpdate(getScenesUpdateData()); const [updateScenes] = useScenesUpdate(getScenesUpdateData());
useEffect(() => { useEffect(() => {
prevParserInputRef.current = parserInput; prevParserInputRef.current = parserInput;
@@ -124,7 +127,7 @@ export const SceneFilenameParser: React.FC = () => {
capitalizeTitle: parserInput.capitalizeTitle, capitalizeTitle: parserInput.capitalizeTitle,
}; };
StashService.queryParseSceneFilenames(parserFilter, parserInputData) queryParseSceneFilenames(parserFilter, parserInputData)
.then((response) => { .then((response) => {
const result = response.data.parseSceneFilenames; const result = response.data.parseSceneFilenames;
if (result) { if (result) {

View File

@@ -133,7 +133,7 @@ function SceneParserStringField(props: ISceneParserFieldProps<string>) {
readOnly={!props.parserResult.isSet} readOnly={!props.parserResult.isSet}
className={props.className} className={props.className}
value={props.parserResult.value || ""} value={props.parserResult.value || ""}
onChange={(event: React.FormEvent<HTMLInputElement>) => onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
maybeValueChanged(event.currentTarget.value) maybeValueChanged(event.currentTarget.value)
} }
/> />
@@ -177,7 +177,7 @@ function SceneParserRatingField(
className={props.className} className={props.className}
disabled={!props.parserResult.isSet} disabled={!props.parserResult.isSet}
value={props.parserResult.value?.toString()} value={props.parserResult.value?.toString()}
onChange={(event: React.FormEvent<HTMLSelectElement>) => onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
maybeValueChanged( maybeValueChanged(
event.currentTarget.value === "" event.currentTarget.value === ""
? undefined ? undefined

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import ReactJWPlayer from "react-jw-player"; import ReactJWPlayer from "react-jw-player";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { useConfiguration } from "src/core/StashService";
import { JWUtils } from "src/utils"; import { JWUtils } from "src/utils";
import { ScenePlayerScrubber } from "./ScenePlayerScrubber"; import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
@@ -210,7 +210,7 @@ export class ScenePlayerImpl extends React.Component<
export const ScenePlayer: React.FC<IScenePlayerProps> = ( export const ScenePlayer: React.FC<IScenePlayerProps> = (
props: IScenePlayerProps props: IScenePlayerProps
) => { ) => {
const config = StashService.useConfiguration(); const config = useConfiguration();
return ( return (
<ScenePlayerImpl <ScenePlayerImpl

View File

@@ -3,8 +3,8 @@ import { Button, ButtonGroup, Card, Form } from "react-bootstrap";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import cx from "classnames"; import cx from "classnames";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { useConfiguration } from "src/core/StashService";
import { VideoHoverHook } from "src/hooks"; import { useVideoHover } from "src/hooks";
import { Icon, TagLink, HoverPopover, SweatDrops } from "src/components/Shared"; import { Icon, TagLink, HoverPopover, SweatDrops } from "src/components/Shared";
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
@@ -19,11 +19,11 @@ export const SceneCard: React.FC<ISceneCardProps> = (
props: ISceneCardProps props: ISceneCardProps
) => { ) => {
const [previewPath, setPreviewPath] = useState<string>(); const [previewPath, setPreviewPath] = useState<string>();
const videoHoverHook = VideoHoverHook.useVideoHover({ const hoverHandler = useVideoHover({
resetOnMouseLeave: false, resetOnMouseLeave: false,
}); });
const config = StashService.useConfiguration(); const config = useConfiguration();
const showStudioAsText = const showStudioAsText =
config?.data?.configuration.interface.showStudioAsText ?? false; config?.data?.configuration.interface.showStudioAsText ?? false;
@@ -219,10 +219,10 @@ export const SceneCard: React.FC<ISceneCardProps> = (
if (!previewPath || previewPath === "") { if (!previewPath || previewPath === "") {
setPreviewPath(props.scene.paths.preview || ""); setPreviewPath(props.scene.paths.preview || "");
} }
VideoHoverHook.onMouseEnter(videoHoverHook); hoverHandler.onMouseEnter();
} }
function onMouseLeave() { function onMouseLeave() {
VideoHoverHook.onMouseLeave(videoHoverHook); hoverHandler.onMouseLeave();
setPreviewPath(""); setPreviewPath("");
} }
@@ -260,7 +260,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
loop loop
className={cx("scene-card-video", { portrait: isPortrait() })} className={cx("scene-card-video", { portrait: isPortrait() })}
poster={props.scene.paths.screenshot || ""} poster={props.scene.paths.screenshot || ""}
ref={videoHoverHook.videoEl} ref={hoverHandler.videoEl}
> >
{previewPath ? <source src={previewPath} /> : ""} {previewPath ? <source src={previewPath} /> : ""}
</video> </video>

View File

@@ -3,7 +3,12 @@ import queryString from "query-string";
import React, { useEffect, useState } from "react"; 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 * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import {
useFindScene,
useSceneIncrementO,
useSceneDecrementO,
useSceneResetO,
} from "src/core/StashService";
import { GalleryViewer } from "src/components/Galleries/GalleryViewer"; import { GalleryViewer } from "src/components/Galleries/GalleryViewer";
import { LoadingIndicator } from "src/components/Shared"; import { LoadingIndicator } from "src/components/Shared";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
@@ -24,11 +29,11 @@ export const Scene: React.FC = () => {
const Toast = useToast(); const Toast = useToast();
const [timestamp, setTimestamp] = useState<number>(getInitialTimestamp()); const [timestamp, setTimestamp] = useState<number>(getInitialTimestamp());
const [scene, setScene] = useState<GQL.SceneDataFragment | undefined>(); const [scene, setScene] = useState<GQL.SceneDataFragment | undefined>();
const { data, error, loading } = StashService.useFindScene(id); const { data, error, loading } = useFindScene(id);
const [oLoading, setOLoading] = useState(false); const [oLoading, setOLoading] = useState(false);
const [incrementO] = StashService.useSceneIncrementO(scene?.id ?? "0"); const [incrementO] = useSceneIncrementO(scene?.id ?? "0");
const [decrementO] = StashService.useSceneDecrementO(scene?.id ?? "0"); const [decrementO] = useSceneDecrementO(scene?.id ?? "0");
const [resetO] = StashService.useSceneResetO(scene?.id ?? "0"); const [resetO] = useSceneResetO(scene?.id ?? "0");
const queryParams = queryString.parse(location.search); const queryParams = queryString.parse(location.search);
const autoplay = queryParams?.autoplay === "true"; const autoplay = queryParams?.autoplay === "true";

View File

@@ -3,7 +3,13 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Button, Dropdown, DropdownButton, Form, Table } from "react-bootstrap"; import { Button, Dropdown, DropdownButton, Form, Table } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import {
queryScrapeScene,
queryScrapeSceneURL,
useListSceneScrapers,
useSceneUpdate,
useSceneDestroy,
} from "src/core/StashService";
import { import {
PerformerSelect, PerformerSelect,
TagSelect, TagSelect,
@@ -42,7 +48,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
const [tagIds, setTagIds] = useState<string[]>(); const [tagIds, setTagIds] = useState<string[]>();
const [coverImage, setCoverImage] = useState<string>(); const [coverImage, setCoverImage] = useState<string>();
const Scrapers = StashService.useListSceneScrapers(); const Scrapers = useListSceneScrapers();
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]); const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
@@ -54,8 +60,8 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
// Network state // Network state
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [updateScene] = StashService.useSceneUpdate(getSceneInput()); const [updateScene] = useSceneUpdate(getSceneInput());
const [deleteScene] = StashService.useSceneDestroy(getSceneDeleteInput()); const [deleteScene] = useSceneDestroy(getSceneDeleteInput());
useEffect(() => { useEffect(() => {
const newQueryableScrapers = ( const newQueryableScrapers = (
@@ -127,7 +133,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
setIsLoading(false); setIsLoading(false);
}, [props.scene]); }, [props.scene]);
ImageUtils.usePasteImage(onImageLoad); ImageUtils.usePasteImage(onImageLoad, true);
function getSceneInput(): GQL.SceneUpdateInput { function getSceneInput(): GQL.SceneUpdateInput {
return { return {
@@ -252,10 +258,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
async function onScrapeClicked(scraper: GQL.Scraper) { async function onScrapeClicked(scraper: GQL.Scraper) {
setIsLoading(true); setIsLoading(true);
try { try {
const result = await StashService.queryScrapeScene( const result = await queryScrapeScene(scraper.id, getSceneInput());
scraper.id,
getSceneInput()
);
if (!result.data || !result.data.scrapeScene) { if (!result.data || !result.data.scrapeScene) {
return; return;
} }
@@ -364,7 +367,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
} }
setIsLoading(true); setIsLoading(true);
try { try {
const result = await StashService.queryScrapeSceneURL(url); const result = await queryScrapeSceneURL(url);
if (!result.data || !result.data.scrapeSceneURL) { if (!result.data || !result.data.scrapeSceneURL) {
return; return;
} }
@@ -404,7 +407,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
<td>URL</td> <td>URL</td>
<td> <td>
<Form.Control <Form.Control
onChange={(newValue: React.FormEvent<HTMLInputElement>) => onChange={(newValue: React.ChangeEvent<HTMLInputElement>) =>
setUrl(newValue.currentTarget.value) setUrl(newValue.currentTarget.value)
} }
value={url} value={url}
@@ -494,7 +497,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
<Form.Control <Form.Control
as="textarea" as="textarea"
className="scene-description text-input" className="scene-description text-input"
onChange={(newValue: React.FormEvent<HTMLTextAreaElement>) => onChange={(newValue: React.ChangeEvent<HTMLTextAreaElement>) =>
setDetails(newValue.currentTarget.value) setDetails(newValue.currentTarget.value)
} }
value={details} value={details}

View File

@@ -2,7 +2,11 @@ import React from "react";
import { Button, Form } from "react-bootstrap"; import { Button, Form } from "react-bootstrap";
import { Field, FieldProps, Form as FormikForm, Formik } from "formik"; import { Field, FieldProps, Form as FormikForm, Formik } from "formik";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import {
useSceneMarkerCreate,
useSceneMarkerUpdate,
useSceneMarkerDestroy,
} from "src/core/StashService";
import { import {
DurationInput, DurationInput,
TagSelect, TagSelect,
@@ -29,9 +33,9 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
editingMarker, editingMarker,
onClose, onClose,
}) => { }) => {
const [sceneMarkerCreate] = StashService.useSceneMarkerCreate(); const [sceneMarkerCreate] = useSceneMarkerCreate();
const [sceneMarkerUpdate] = StashService.useSceneMarkerUpdate(); const [sceneMarkerUpdate] = useSceneMarkerUpdate();
const [sceneMarkerDestroy] = StashService.useSceneMarkerDestroy(); const [sceneMarkerDestroy] = useSceneMarkerDestroy();
const Toast = useToast(); const Toast = useToast();
const onSubmit = (values: IFormFields) => { const onSubmit = (values: IFormFields) => {

View File

@@ -1,6 +1,6 @@
import * as React from "react"; import * as React from "react";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { useAllMoviesForFilter } from "src/core/StashService";
import { Form } from "react-bootstrap"; import { Form } from "react-bootstrap";
type ValidTypes = GQL.SlimMovieDataFragment; type ValidTypes = GQL.SlimMovieDataFragment;
@@ -15,7 +15,7 @@ export interface IProps {
export const SceneMovieTable: React.FunctionComponent<IProps> = ( export const SceneMovieTable: React.FunctionComponent<IProps> = (
props: IProps props: IProps
) => { ) => {
const { data } = StashService.useAllMoviesForFilter(); const { data } = useAllMoviesForFilter();
const items = !!data && !!data.allMoviesSlim ? data.allMoviesSlim : []; const items = !!data && !!data.allMoviesSlim ? data.allMoviesSlim : [];
let itemsFilter: ValidTypes[] = []; let itemsFilter: ValidTypes[] = [];
@@ -49,7 +49,7 @@ export const SceneMovieTable: React.FunctionComponent<IProps> = (
as="select" as="select"
className="input-control" className="input-control"
value={storeIdx[index] ? storeIdx[index]?.toString() : ""} value={storeIdx[index] ? storeIdx[index]?.toString() : ""}
onChange={(e: React.FormEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
updateFieldChanged( updateFieldChanged(
item.id, item.id,
Number.parseInt( Number.parseInt(

View File

@@ -1,7 +1,7 @@
import { Button } from "react-bootstrap"; import { Button } from "react-bootstrap";
import React, { FunctionComponent } from "react"; import React, { FunctionComponent } from "react";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { useSceneGenerateScreenshot } from "src/core/StashService";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { JWUtils } from "src/utils"; import { JWUtils } from "src/utils";
@@ -13,7 +13,7 @@ export const SceneOperationsPanel: FunctionComponent<IOperationsPanelProps> = (
props: IOperationsPanelProps props: IOperationsPanelProps
) => { ) => {
const Toast = useToast(); const Toast = useToast();
const [generateScreenshot] = StashService.useSceneGenerateScreenshot(); const [generateScreenshot] = useSceneGenerateScreenshot();
async function onGenerateScreenshot(at?: number) { async function onGenerateScreenshot(at?: number) {
await generateScreenshot({ await generateScreenshot({

View File

@@ -5,7 +5,7 @@ import {
FindScenesQueryResult, FindScenesQueryResult,
SlimSceneDataFragment, SlimSceneDataFragment,
} from "src/core/generated-graphql"; } from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { queryFindScenes } from "src/core/StashService";
import { useScenesList } from "src/hooks"; import { useScenesList } from "src/hooks";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types"; import { DisplayMode } from "src/models/list-filter/types";
@@ -52,7 +52,7 @@ export const SceneList: React.FC<ISceneList> = ({
const filterCopy = _.cloneDeep(filter); const filterCopy = _.cloneDeep(filter);
filterCopy.itemsPerPage = 1; filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1; filterCopy.currentPage = index + 1;
const singleResult = await StashService.queryFindScenes(filterCopy); const singleResult = await queryFindScenes(filterCopy);
if ( if (
singleResult && singleResult &&
singleResult.data && singleResult.data &&

View File

@@ -2,7 +2,7 @@ import _ from "lodash";
import React from "react"; import React from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { FindSceneMarkersQueryResult } from "src/core/generated-graphql"; import { FindSceneMarkersQueryResult } from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { queryFindSceneMarkers } from "src/core/StashService";
import { NavUtils } from "src/utils"; import { NavUtils } from "src/utils";
import { useSceneMarkersList } from "src/hooks"; import { useSceneMarkersList } from "src/hooks";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
@@ -35,7 +35,7 @@ export const SceneMarkerList: React.FC = () => {
const filterCopy = _.cloneDeep(filter); const filterCopy = _.cloneDeep(filter);
filterCopy.itemsPerPage = 1; filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1; filterCopy.currentPage = index + 1;
const singleResult = await StashService.queryFindSceneMarkers(filterCopy); const singleResult = await queryFindSceneMarkers(filterCopy);
if (singleResult?.data?.findSceneMarkers?.scene_markers?.length === 1) { if (singleResult?.data?.findSceneMarkers?.scene_markers?.length === 1) {
// navigate to the scene player page // navigate to the scene player page
const url = NavUtils.makeSceneMarkerUrl( const url = NavUtils.makeSceneMarkerUrl(

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Button, Form } from "react-bootstrap"; import { Button, Form } from "react-bootstrap";
import _ from "lodash"; import _ from "lodash";
import { StashService } from "src/core/StashService"; import { useBulkSceneUpdate } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StudioSelect, LoadingIndicator } from "src/components/Shared"; import { StudioSelect, LoadingIndicator } from "src/components/Shared";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
@@ -27,7 +27,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
); );
const [tagIds, setTagIds] = useState<string[]>(); const [tagIds, setTagIds] = useState<string[]>();
const [updateScenes] = StashService.useBulkSceneUpdate(getSceneInput()); const [updateScenes] = useBulkSceneUpdate(getSceneInput());
// Network state // Network state
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@@ -318,7 +318,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
as="select" as="select"
className="btn-secondary" className="btn-secondary"
value={rating} value={rating}
onChange={(event: React.FormEvent<HTMLSelectElement>) => onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
setRating(event.currentTarget.value) setRating(event.currentTarget.value)
} }
> >

View File

@@ -1,17 +1,17 @@
import React from "react"; import React from "react";
import { Button, Table } from "react-bootstrap"; import { Button, Table } from "react-bootstrap";
import { LoadingIndicator } from "src/components/Shared"; import { LoadingIndicator } from "src/components/Shared";
import { StashService } from "src/core/StashService"; import { useVersion, useLatestVersion } from "src/core/StashService";
export const SettingsAboutPanel: React.FC = () => { export const SettingsAboutPanel: React.FC = () => {
const { data, error, loading } = StashService.useVersion(); const { data, error, loading } = useVersion();
const { const {
data: dataLatest, data: dataLatest,
error: errorLatest, error: errorLatest,
loading: loadingLatest, loading: loadingLatest,
refetch, refetch,
networkStatus, networkStatus,
} = StashService.useLatestVersion(); } = useLatestVersion();
function maybeRenderTag() { function maybeRenderTag() {
if (!data || !data.version || !data.version.version) { if (!data || !data.version || !data.version.version) {

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Button, Form, InputGroup } from "react-bootstrap"; import { Button, Form, InputGroup } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { useConfiguration, useConfigureGeneral } from "src/core/StashService";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { Icon, LoadingIndicator } from "src/components/Shared"; import { Icon, LoadingIndicator } from "src/components/Shared";
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect"; import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
@@ -36,9 +36,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
undefined undefined
); );
const { data, error, loading } = StashService.useConfiguration(); const { data, error, loading } = useConfiguration();
const [updateGeneralConfig] = StashService.useConfigureGeneral({ const [updateGeneralConfig] = useConfigureGeneral({
stashes, stashes,
databasePath, databasePath,
generatedPath, generatedPath,
@@ -189,7 +189,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Control <Form.Control
className="col col-sm-6 text-input" className="col col-sm-6 text-input"
defaultValue={databasePath} defaultValue={databasePath}
onChange={(e: React.FormEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setDatabasePath(e.currentTarget.value) setDatabasePath(e.currentTarget.value)
} }
/> />
@@ -203,7 +203,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Control <Form.Control
className="col col-sm-6 text-input" className="col col-sm-6 text-input"
defaultValue={generatedPath} defaultValue={generatedPath}
onChange={(e: React.FormEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setGeneratedPath(e.currentTarget.value) setGeneratedPath(e.currentTarget.value)
} }
/> />
@@ -222,7 +222,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Control <Form.Control
className="col col-sm-6 text-input" className="col col-sm-6 text-input"
value={regexp} value={regexp}
onChange={(e: React.FormEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
excludeRegexChanged(i, e.currentTarget.value) excludeRegexChanged(i, e.currentTarget.value)
} }
/> />
@@ -262,7 +262,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Control <Form.Control
className="col col-sm-6 input-control" className="col col-sm-6 input-control"
as="select" as="select"
onChange={(event: React.FormEvent<HTMLSelectElement>) => onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
setMaxTranscodeSize(translateQuality(event.currentTarget.value)) setMaxTranscodeSize(translateQuality(event.currentTarget.value))
} }
value={resolutionToString(maxTranscodeSize)} value={resolutionToString(maxTranscodeSize)}
@@ -282,7 +282,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Control <Form.Control
className="col col-sm-6 input-control" className="col col-sm-6 input-control"
as="select" as="select"
onChange={(event: React.FormEvent<HTMLSelectElement>) => onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
setMaxStreamingTranscodeSize( setMaxStreamingTranscodeSize(
translateQuality(event.currentTarget.value) translateQuality(event.currentTarget.value)
) )
@@ -332,7 +332,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Control <Form.Control
className="col col-sm-6 text-input" className="col col-sm-6 text-input"
defaultValue={scraperUserAgent} defaultValue={scraperUserAgent}
onChange={(e: React.FormEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setScraperUserAgent(e.currentTarget.value) setScraperUserAgent(e.currentTarget.value)
} }
/> />
@@ -425,7 +425,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
<Form.Control <Form.Control
className="col col-sm-6 input-control" className="col col-sm-6 input-control"
as="select" as="select"
onChange={(event: React.FormEvent<HTMLSelectElement>) => onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
setLogLevel(event.currentTarget.value) setLogLevel(event.currentTarget.value)
} }
value={logLevel} value={logLevel}

View File

@@ -1,12 +1,12 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Button, Form } from "react-bootstrap"; import { Button, Form } from "react-bootstrap";
import { DurationInput, LoadingIndicator } from "src/components/Shared"; import { DurationInput, LoadingIndicator } from "src/components/Shared";
import { StashService } from "src/core/StashService"; import { useConfiguration, useConfigureInterface } from "src/core/StashService";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
export const SettingsInterfacePanel: React.FC = () => { export const SettingsInterfacePanel: React.FC = () => {
const Toast = useToast(); const Toast = useToast();
const { data: config, error, loading } = StashService.useConfiguration(); const { data: config, error, loading } = useConfiguration();
const [soundOnPreview, setSoundOnPreview] = useState<boolean>(true); const [soundOnPreview, setSoundOnPreview] = useState<boolean>(true);
const [wallShowTitle, setWallShowTitle] = useState<boolean>(true); const [wallShowTitle, setWallShowTitle] = useState<boolean>(true);
const [maximumLoopDuration, setMaximumLoopDuration] = useState<number>(0); const [maximumLoopDuration, setMaximumLoopDuration] = useState<number>(0);
@@ -16,7 +16,7 @@ export const SettingsInterfacePanel: React.FC = () => {
const [cssEnabled, setCSSEnabled] = useState<boolean>(false); const [cssEnabled, setCSSEnabled] = useState<boolean>(false);
const [language, setLanguage] = useState<string>("en"); const [language, setLanguage] = useState<string>("en");
const [updateInterfaceConfig] = StashService.useConfigureInterface({ const [updateInterfaceConfig] = useConfigureInterface({
soundOnPreview, soundOnPreview,
wallShowTitle, wallShowTitle,
maximumLoopDuration, maximumLoopDuration,
@@ -62,7 +62,7 @@ export const SettingsInterfacePanel: React.FC = () => {
as="select" as="select"
className="col-4 input-control" className="col-4 input-control"
value={language} value={language}
onChange={(e: React.FormEvent<HTMLSelectElement>) => onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
setLanguage(e.currentTarget.value) setLanguage(e.currentTarget.value)
} }
> >
@@ -142,12 +142,12 @@ export const SettingsInterfacePanel: React.FC = () => {
<Form.Control <Form.Control
as="textarea" as="textarea"
value={css} value={css}
onChange={(e: React.FormEvent<HTMLTextAreaElement>) => onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
setCSS(e.currentTarget.value) setCSS(e.currentTarget.value)
} }
rows={16} rows={16}
className="col col-sm-6 text-input code" className="col col-sm-6 text-input code"
></Form.Control> />
<Form.Text className="text-muted"> <Form.Text className="text-muted">
Page must be reloaded for changes to take effect. Page must be reloaded for changes to take effect.
</Form.Text> </Form.Text>

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useReducer, useState } from "react"; import React, { useEffect, useReducer, useState } from "react";
import { Form } from "react-bootstrap"; import { Form } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { useLogs, useLoggingSubscribe } from "src/core/StashService";
function convertTime(logEntry: GQL.LogEntryDataFragment) { function convertTime(logEntry: GQL.LogEntryDataFragment) {
function pad(val: number) { function pad(val: number) {
@@ -74,8 +74,8 @@ const logReducer = (existingEntries: LogEntry[], newEntries: LogEntry[]) => [
]; ];
export const SettingsLogsPanel: React.FC = () => { export const SettingsLogsPanel: React.FC = () => {
const { data, error } = StashService.useLoggingSubscribe(); const { data, error } = useLoggingSubscribe();
const { data: existingData } = StashService.useLogs(); const { data: existingData } = useLogs();
const [currentData, dispatchLogUpdate] = useReducer(logReducer, []); const [currentData, dispatchLogUpdate] = useReducer(logReducer, []);
const [logLevel, setLogLevel] = useState<string>("Info"); const [logLevel, setLogLevel] = useState<string>("Info");

View File

@@ -1,6 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Button, Form } from "react-bootstrap"; import { Button, Form } from "react-bootstrap";
import { StashService } from "src/core/StashService"; import { mutateMetadataGenerate } from "src/core/StashService";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
export const GenerateButton: React.FC = () => { export const GenerateButton: React.FC = () => {
@@ -12,7 +12,7 @@ export const GenerateButton: React.FC = () => {
async function onGenerate() { async function onGenerate() {
try { try {
await StashService.mutateMetadataGenerate({ await mutateMetadataGenerate({
sprites, sprites,
previews, previews,
markers, markers,

View File

@@ -1,7 +1,16 @@
import React, { useState, useEffect } from "react"; 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 { Link } from "react-router-dom";
import { StashService } from "src/core/StashService"; import {
useJobStatus,
useMetadataUpdate,
mutateMetadataImport,
mutateMetadataClean,
mutateMetadataScan,
mutateMetadataAutoTag,
mutateMetadataExport,
mutateStopJob,
} from "src/core/StashService";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { Modal } from "src/components/Shared"; import { Modal } from "src/components/Shared";
import { GenerateButton } from "./GenerateButton"; import { GenerateButton } from "./GenerateButton";
@@ -18,8 +27,8 @@ export const SettingsTasksPanel: React.FC = () => {
const [autoTagStudios, setAutoTagStudios] = useState<boolean>(true); const [autoTagStudios, setAutoTagStudios] = useState<boolean>(true);
const [autoTagTags, setAutoTagTags] = useState<boolean>(true); const [autoTagTags, setAutoTagTags] = useState<boolean>(true);
const jobStatus = StashService.useJobStatus(); const jobStatus = useJobStatus();
const metadataUpdate = StashService.useMetadataUpdate(); const metadataUpdate = useMetadataUpdate();
function statusToText(s: string) { function statusToText(s: string) {
switch (s) { switch (s) {
@@ -68,7 +77,7 @@ export const SettingsTasksPanel: React.FC = () => {
function onImport() { function onImport() {
setIsImportAlertOpen(false); setIsImportAlertOpen(false);
StashService.mutateMetadataImport().then(() => { mutateMetadataImport().then(() => {
jobStatus.refetch(); jobStatus.refetch();
}); });
} }
@@ -91,7 +100,7 @@ export const SettingsTasksPanel: React.FC = () => {
function onClean() { function onClean() {
setIsCleanAlertOpen(false); setIsCleanAlertOpen(false);
StashService.mutateMetadataClean().then(() => { mutateMetadataClean().then(() => {
jobStatus.refetch(); jobStatus.refetch();
}); });
} }
@@ -115,7 +124,7 @@ export const SettingsTasksPanel: React.FC = () => {
async function onScan() { async function onScan() {
try { try {
await StashService.mutateMetadataScan({ useFileMetadata }); await mutateMetadataScan({ useFileMetadata });
Toast.success({ content: "Started scan" }); Toast.success({ content: "Started scan" });
jobStatus.refetch(); jobStatus.refetch();
} catch (e) { } catch (e) {
@@ -134,7 +143,7 @@ export const SettingsTasksPanel: React.FC = () => {
async function onAutoTag() { async function onAutoTag() {
try { try {
await StashService.mutateMetadataAutoTag(getAutoTagInput()); await mutateMetadataAutoTag(getAutoTagInput());
Toast.success({ content: "Started auto tagging" }); Toast.success({ content: "Started auto tagging" });
jobStatus.refetch(); jobStatus.refetch();
} catch (e) { } catch (e) {
@@ -152,9 +161,7 @@ export const SettingsTasksPanel: React.FC = () => {
<Button <Button
id="stop" id="stop"
variant="danger" variant="danger"
onClick={() => onClick={() => mutateStopJob().then(() => jobStatus.refetch())}
StashService.mutateStopJob().then(() => jobStatus.refetch())
}
> >
Stop Stop
</Button> </Button>
@@ -277,7 +284,7 @@ export const SettingsTasksPanel: React.FC = () => {
variant="secondary" variant="secondary"
type="submit" type="submit"
onClick={() => onClick={() =>
StashService.mutateMetadataExport().then(() => { mutateMetadataExport().then(() => {
jobStatus.refetch(); jobStatus.refetch();
}) })
} }

View File

@@ -98,7 +98,7 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
className="duration-control text-input" className="duration-control text-input"
disabled={props.disabled} disabled={props.disabled}
value={value} value={value}
onChange={(e: React.FormEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setValue(e.currentTarget.value) setValue(e.currentTarget.value)
} }
onBlur={() => { onBlur={() => {

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Button, InputGroup, Form, Modal } from "react-bootstrap"; import { Button, InputGroup, Form, Modal } from "react-bootstrap";
import { LoadingIndicator } from "src/components/Shared"; import { LoadingIndicator } from "src/components/Shared";
import { StashService } from "src/core/StashService"; import { useDirectories } from "src/core/StashService";
interface IProps { interface IProps {
directories: string[]; directories: string[];
@@ -12,9 +12,7 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
const [currentDirectory, setCurrentDirectory] = useState<string>(""); const [currentDirectory, setCurrentDirectory] = useState<string>("");
const [isDisplayingDialog, setIsDisplayingDialog] = useState<boolean>(false); const [isDisplayingDialog, setIsDisplayingDialog] = useState<boolean>(false);
const [selectedDirectories, setSelectedDirectories] = useState<string[]>([]); const [selectedDirectories, setSelectedDirectories] = useState<string[]>([]);
const { data, error, loading } = StashService.useDirectories( const { data, error, loading } = useDirectories(currentDirectory);
currentDirectory
);
useEffect(() => { useEffect(() => {
setSelectedDirectories(props.directories); setSelectedDirectories(props.directories);
@@ -51,7 +49,7 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
<InputGroup> <InputGroup>
<Form.Control <Form.Control
placeholder="File path" placeholder="File path"
onChange={(e: React.FormEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setCurrentDirectory(e.currentTarget.value) setCurrentDirectory(e.currentTarget.value)
} }
defaultValue={currentDirectory} defaultValue={currentDirectory}

View File

@@ -4,7 +4,7 @@ import { Button, Form } from "react-bootstrap";
interface IImageInput { interface IImageInput {
isEditing: boolean; isEditing: boolean;
text?: string; text?: string;
onImageChange: (event: React.FormEvent<HTMLInputElement>) => void; onImageChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
acceptSVG?: boolean; acceptSVG?: boolean;
} }

View File

@@ -1,10 +1,19 @@
import React, { useState, useCallback, CSSProperties } from "react"; import React, { useState, CSSProperties } from "react";
import Select, { ValueType } from "react-select"; import Select, { ValueType } from "react-select";
import CreatableSelect from "react-select/creatable"; import CreatableSelect from "react-select/creatable";
import { debounce } from "lodash"; import { debounce } from "lodash";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import {
useAllTagsForFilter,
useAllMoviesForFilter,
useAllStudiosForFilter,
useAllPerformersForFilter,
useMarkerStrings,
useScrapePerformerList,
useValidGalleriesForScene,
useTagCreate,
} from "src/core/StashService";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
type ValidTypes = type ValidTypes =
@@ -63,9 +72,7 @@ const getSelectedValues = (selectedItems: ValueType<Option>) =>
: []; : [];
export const SceneGallerySelect: React.FC<ISceneGallerySelect> = (props) => { export const SceneGallerySelect: React.FC<ISceneGallerySelect> = (props) => {
const { data, loading } = StashService.useValidGalleriesForScene( const { data, loading } = useValidGalleriesForScene(props.sceneId);
props.sceneId
);
const galleries = data?.validGalleriesForScene ?? []; const galleries = data?.validGalleriesForScene ?? [];
const items = (galleries.length > 0 const items = (galleries.length > 0
? [{ path: "None", id: "0" }, ...galleries] ? [{ path: "None", id: "0" }, ...galleries]
@@ -103,10 +110,7 @@ export const ScrapePerformerSuggest: React.FC<IScrapePerformerSuggestProps> = (
props props
) => { ) => {
const [query, setQuery] = React.useState<string>(""); const [query, setQuery] = React.useState<string>("");
const { data, loading } = StashService.useScrapePerformerList( const { data, loading } = useScrapePerformerList(props.scraperId, query);
props.scraperId,
query
);
const performers = data?.scrapePerformerList ?? []; const performers = data?.scrapePerformerList ?? [];
const items = performers.map((item) => ({ const items = performers.map((item) => ({
@@ -114,12 +118,10 @@ export const ScrapePerformerSuggest: React.FC<IScrapePerformerSuggestProps> = (
value: item.name ?? "", value: item.name ?? "",
})); }));
const onInputChange = useCallback( const onInputChange = debounce((input: string) => {
debounce((input: string) => {
setQuery(input); setQuery(input);
}, 500), }, 500);
[]
);
const onChange = (selectedItems: ValueType<Option>) => { const onChange = (selectedItems: ValueType<Option>) => {
const name = getSelectedValues(selectedItems)[0]; const name = getSelectedValues(selectedItems)[0];
const performer = performers.find((p) => p.name === name); const performer = performers.find((p) => p.name === name);
@@ -145,7 +147,7 @@ interface IMarkerSuggestProps {
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 { data, loading } = useMarkerStrings();
const suggestions = data?.markerStrings ?? []; const suggestions = data?.markerStrings ?? [];
const onChange = (selectedItems: ValueType<Option>) => const onChange = (selectedItems: ValueType<Option>) =>
@@ -182,7 +184,7 @@ export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) =>
); );
export const PerformerSelect: React.FC<IFilterProps> = (props) => { export const PerformerSelect: React.FC<IFilterProps> = (props) => {
const { data, loading } = StashService.useAllPerformersForFilter(); const { data, loading } = useAllPerformersForFilter();
const normalizedData = data?.allPerformersSlim ?? []; const normalizedData = data?.allPerformersSlim ?? [];
const items: Option[] = normalizedData.map((item) => ({ const items: Option[] = normalizedData.map((item) => ({
@@ -215,7 +217,7 @@ export const PerformerSelect: React.FC<IFilterProps> = (props) => {
}; };
export const StudioSelect: React.FC<IFilterProps> = (props) => { export const StudioSelect: React.FC<IFilterProps> = (props) => {
const { data, loading } = StashService.useAllStudiosForFilter(); const { data, loading } = useAllStudiosForFilter();
const normalizedData = data?.allStudiosSlim ?? []; const normalizedData = data?.allStudiosSlim ?? [];
@@ -253,7 +255,7 @@ export const StudioSelect: React.FC<IFilterProps> = (props) => {
}; };
export const MovieSelect: React.FC<IFilterProps> = (props) => { export const MovieSelect: React.FC<IFilterProps> = (props) => {
const { data, loading } = StashService.useAllMoviesForFilter(); const { data, loading } = useAllMoviesForFilter();
const normalizedData = data?.allMoviesSlim ?? []; const normalizedData = data?.allMoviesSlim ?? [];
@@ -293,8 +295,8 @@ export const MovieSelect: React.FC<IFilterProps> = (props) => {
export const TagSelect: React.FC<IFilterProps> = (props) => { export const TagSelect: React.FC<IFilterProps> = (props) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [selectedIds, setSelectedIds] = useState<string[]>(props.ids ?? []); const [selectedIds, setSelectedIds] = useState<string[]>(props.ids ?? []);
const { data, loading: dataLoading } = StashService.useAllTagsForFilter(); const { data, loading: dataLoading } = useAllTagsForFilter();
const [createTag] = StashService.useTagCreate({ name: "" }); const [createTag] = useTagCreate({ name: "" });
const Toast = useToast(); const Toast = useToast();
const placeholder = props.noSelectionString ?? "Select tags..."; const placeholder = props.noSelectionString ?? "Select tags...";

View File

@@ -1,10 +1,10 @@
import React from "react"; import React from "react";
import { StashService } from "src/core/StashService"; import { useStats } from "src/core/StashService";
import { FormattedMessage, FormattedNumber } from "react-intl"; import { FormattedMessage, FormattedNumber } from "react-intl";
import { LoadingIndicator } from "src/components/Shared"; import { LoadingIndicator } from "src/components/Shared";
export const Stats: React.FC = () => { export const Stats: React.FC = () => {
const { data, error, loading } = StashService.useStats(); const { data, error, loading } = useStats();
if (loading || !data) return <LoadingIndicator />; if (loading || !data) return <LoadingIndicator />;

View File

@@ -6,7 +6,13 @@ import { useParams, useHistory } from "react-router-dom";
import cx from "classnames"; import cx from "classnames";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import {
useFindStudio,
useStudioUpdate,
useStudioCreate,
useStudioDestroy,
mutateMetadataAutoTag,
} from "src/core/StashService";
import { ImageUtils, TableUtils } from "src/utils"; import { ImageUtils, TableUtils } from "src/utils";
import { import {
DetailsEditNavbar, DetailsEditNavbar,
@@ -35,14 +41,14 @@ export const Studio: React.FC = () => {
const [studio, setStudio] = useState<Partial<GQL.StudioDataFragment>>({}); const [studio, setStudio] = useState<Partial<GQL.StudioDataFragment>>({});
const [imagePreview, setImagePreview] = useState<string>(); const [imagePreview, setImagePreview] = useState<string>();
const { data, error, loading } = StashService.useFindStudio(id); const { data, error, loading } = useFindStudio(id);
const [updateStudio] = StashService.useStudioUpdate( const [updateStudio] = useStudioUpdate(
getStudioInput() as GQL.StudioUpdateInput getStudioInput() as GQL.StudioUpdateInput
); );
const [createStudio] = StashService.useStudioCreate( const [createStudio] = useStudioCreate(
getStudioInput() as GQL.StudioCreateInput getStudioInput() as GQL.StudioCreateInput
); );
const [deleteStudio] = StashService.useStudioDestroy( const [deleteStudio] = useStudioDestroy(
getStudioInput() as GQL.StudioDestroyInput getStudioInput() as GQL.StudioDestroyInput
); );
@@ -72,7 +78,7 @@ export const Studio: React.FC = () => {
setImage(this.result as string); setImage(this.result as string);
} }
ImageUtils.usePasteImage(onImageLoad); ImageUtils.usePasteImage(onImageLoad, isEditing);
if (!isNew && !isEditing) { if (!isNew && !isEditing) {
if (!data?.findStudio || loading) return <LoadingIndicator />; if (!data?.findStudio || loading) return <LoadingIndicator />;
@@ -115,7 +121,7 @@ export const Studio: React.FC = () => {
async function onAutoTag() { async function onAutoTag() {
if (!studio.id) return; if (!studio.id) return;
try { try {
await StashService.mutateMetadataAutoTag({ studios: [studio.id] }); await mutateMetadataAutoTag({ studios: [studio.id] });
Toast.success({ content: "Started auto tagging" }); Toast.success({ content: "Started auto tagging" });
} catch (e) { } catch (e) {
Toast.error(e); Toast.error(e);

View File

@@ -2,7 +2,13 @@ import React, { useState } from "react";
import { Button, Form } from "react-bootstrap"; import { Button, Form } from "react-bootstrap";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import {
mutateMetadataAutoTag,
useAllTags,
useTagUpdate,
useTagCreate,
useTagDestroy,
} from "src/core/StashService";
import { NavUtils } from "src/utils"; import { NavUtils } from "src/utils";
import { Icon, Modal, LoadingIndicator } from "src/components/Shared"; import { Icon, Modal, LoadingIndicator } from "src/components/Shared";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
@@ -18,16 +24,10 @@ export const TagList: React.FC = () => {
GQL.TagDataFragment GQL.TagDataFragment
> | null>(null); > | null>(null);
const { data, error } = StashService.useAllTags(); const { data, error } = useAllTags();
const [updateTag] = StashService.useTagUpdate( const [updateTag] = useTagUpdate(getTagInput() as GQL.TagUpdateInput);
getTagInput() as GQL.TagUpdateInput const [createTag] = useTagCreate(getTagInput() as GQL.TagCreateInput);
); const [deleteTag] = useTagDestroy(getDeleteTagInput() as GQL.TagDestroyInput);
const [createTag] = StashService.useTagCreate(
getTagInput() as GQL.TagCreateInput
);
const [deleteTag] = StashService.useTagDestroy(
getDeleteTagInput() as GQL.TagDestroyInput
);
function getTagInput() { function getTagInput() {
const tagInput: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = { name }; const tagInput: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = { name };
@@ -62,7 +62,7 @@ export const TagList: React.FC = () => {
async function onAutoTag(tag: GQL.TagDataFragment) { async function onAutoTag(tag: GQL.TagDataFragment) {
if (!tag) return; if (!tag) return;
try { try {
await StashService.mutateMetadataAutoTag({ tags: [tag.id] }); await mutateMetadataAutoTag({ tags: [tag.id] });
Toast.success({ content: "Started auto tagging" }); Toast.success({ content: "Started auto tagging" });
} catch (e) { } catch (e) {
Toast.error(e); Toast.error(e);
@@ -160,7 +160,7 @@ export const TagList: React.FC = () => {
<Form.Group controlId="tag-name"> <Form.Group controlId="tag-name">
<Form.Label>Name</Form.Label> <Form.Label>Name</Form.Label>
<Form.Control <Form.Control
onChange={(newValue: React.FormEvent<HTMLInputElement>) => onChange={(newValue: React.ChangeEvent<HTMLInputElement>) =>
setName(newValue.currentTarget.value) setName(newValue.currentTarget.value)
} }
defaultValue={(editingTag && editingTag.name) || ""} defaultValue={(editingTag && editingTag.name) || ""}

View File

@@ -2,8 +2,8 @@ import _ from "lodash";
import React, { useRef, useState, useEffect } from "react"; import React, { useRef, useState, useEffect } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { useConfiguration } from "src/core/StashService";
import { VideoHoverHook } from "src/hooks"; import { useVideoHover } from "src/hooks";
import { TextUtils, NavUtils } from "src/utils"; import { TextUtils, NavUtils } from "src/utils";
interface IWallItemProps { interface IWallItemProps {
@@ -22,15 +22,15 @@ export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
const [screenshotPath, setScreenshotPath] = useState<string>(""); const [screenshotPath, setScreenshotPath] = useState<string>("");
const [title, setTitle] = useState<string>(""); const [title, setTitle] = useState<string>("");
const [tags, setTags] = useState<JSX.Element[]>([]); const [tags, setTags] = useState<JSX.Element[]>([]);
const config = StashService.useConfiguration(); const config = useConfiguration();
const videoHoverHook = VideoHoverHook.useVideoHover({ const hoverHandler = useVideoHover({
resetOnMouseLeave: true, resetOnMouseLeave: true,
}); });
const showTextContainer = const showTextContainer =
config.data?.configuration.interface.wallShowTitle ?? true; config.data?.configuration.interface.wallShowTitle ?? true;
function onMouseEnter() { function onMouseEnter() {
VideoHoverHook.onMouseEnter(videoHoverHook); hoverHandler.onMouseEnter();
if (!videoPath || videoPath === "") { if (!videoPath || videoPath === "") {
if (props.sceneMarker) { if (props.sceneMarker) {
setVideoPath(props.sceneMarker.stream || ""); setVideoPath(props.sceneMarker.stream || "");
@@ -43,7 +43,7 @@ export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
const debouncedOnMouseEnter = useRef(_.debounce(onMouseEnter, 500)); const debouncedOnMouseEnter = useRef(_.debounce(onMouseEnter, 500));
function onMouseLeave() { function onMouseLeave() {
VideoHoverHook.onMouseLeave(videoHoverHook); hoverHandler.onMouseLeave();
setVideoPath(""); setVideoPath("");
debouncedOnMouseEnter.current.cancel(); debouncedOnMouseEnter.current.cancel();
props.onOverlay(false); props.onOverlay(false);
@@ -111,7 +111,7 @@ export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
} }
const className = ["scene-wall-item-container"]; const className = ["scene-wall-item-container"];
if (videoHoverHook.isHovering.current) { if (hoverHandler.isHovering.current) {
className.push("double-scale"); className.push("double-scale");
} }
const style: React.CSSProperties = {}; const style: React.CSSProperties = {};
@@ -133,10 +133,10 @@ export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
src={videoPath} src={videoPath}
poster={screenshotPath} poster={screenshotPath}
className="scene-wall-video" className="scene-wall-video"
style={videoHoverHook.isHovering.current ? {} : { display: "none" }} style={hoverHandler.isHovering.current ? {} : { display: "none" }}
autoPlay autoPlay
loop loop
ref={videoHoverHook.videoEl} ref={hoverHandler.videoEl}
/> />
<img <img
alt={title} alt={title}

View File

@@ -1,99 +1,22 @@
import ApolloClient from "apollo-client";
import { WebSocketLink } from "apollo-link-ws";
import { InMemoryCache, NormalizedCacheObject } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";
import { onError } from "apollo-link-error";
import { ServerError } from "apollo-link-http-common";
import { split, from } from "apollo-link";
import { getMainDefinition } from "apollo-utilities";
import { ListFilterModel } from "../models/list-filter/filter"; import { ListFilterModel } from "../models/list-filter/filter";
import * as GQL from "./generated-graphql"; import * as GQL from "./generated-graphql";
export class StashService { import { createClient } from "./createClient";
public static client: ApolloClient<NormalizedCacheObject>;
private static cache: InMemoryCache;
public static getPlatformURL(ws?: boolean) { const { client, cache } = createClient();
const platformUrl = new URL(window.location.origin);
if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") { export const getClient = () => client;
platformUrl.port = "9999"; // TODO: Hack. Development expects port 9999
if (process.env.REACT_APP_HTTPS === "true") { // TODO: Invalidation should happen through apollo client, rather than rewriting cache directly
platformUrl.protocol = "https:"; const invalidateQueries = (queries: string[]) => {
} if (cache) {
}
if (ws) {
platformUrl.protocol = "ws:";
}
return platformUrl;
}
public static initialize() {
const platformUrl = StashService.getPlatformURL();
const wsPlatformUrl = StashService.getPlatformURL(true);
if (platformUrl.protocol === "https:") {
wsPlatformUrl.protocol = "wss:";
}
const url = `${platformUrl.toString().slice(0, -1)}/graphql`;
const wsUrl = `${wsPlatformUrl.toString().slice(0, -1)}/graphql`;
const httpLink = new HttpLink({
uri: url,
});
const wsLink = new WebSocketLink({
uri: wsUrl,
options: {
reconnect: true,
},
});
const errorLink = onError(({ networkError }) => {
// handle unauthorized error by redirecting to the login page
if (networkError && (networkError as ServerError).statusCode === 401) {
// redirect to login page
window.location.href = "/login";
}
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsLink,
httpLink
);
const link = from([errorLink, splitLink]);
StashService.cache = new InMemoryCache();
StashService.client = new ApolloClient({
link,
cache: StashService.cache,
});
return StashService.client;
}
// TODO: Invalidation should happen through apollo client, rather than rewriting cache directly
private static invalidateQueries(queries: string[]) {
if (StashService.cache) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cache = StashService.cache as any;
const keyMatchers = queries.map((query) => { const keyMatchers = queries.map((query) => {
return new RegExp(`^${query}`); return new RegExp(`^${query}`);
}); });
const rootQuery = cache.data.data.ROOT_QUERY; // TODO: Hack to invalidate, manipulating private data
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rootQuery = (cache as any).data.data.ROOT_QUERY;
Object.keys(rootQuery).forEach((key) => { Object.keys(rootQuery).forEach((key) => {
if ( if (
keyMatchers.some((matcher) => { keyMatchers.some((matcher) => {
@@ -104,269 +27,176 @@ export class StashService {
} }
}); });
} }
} };
public static useFindGalleries(filter: ListFilterModel) { export const useFindGalleries = (filter: ListFilterModel) =>
return GQL.useFindGalleriesQuery({ GQL.useFindGalleriesQuery({
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
}, },
}); });
}
public static useFindScenes(filter: ListFilterModel) { export const useFindScenes = (filter: ListFilterModel) =>
let sceneFilter = {}; GQL.useFindScenesQuery({
// if (!!filter && filter.criteriaFilterOpen) {
sceneFilter = filter.makeSceneFilter();
// }
// if (filter.customCriteria) {
// filter.customCriteria.forEach(criteria => {
// scene_filter[criteria.key] = criteria.value;
// });
// }
return GQL.useFindScenesQuery({
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
scene_filter: sceneFilter, scene_filter: filter.makeSceneFilter(),
}, },
}); });
}
public static queryFindScenes(filter: ListFilterModel) { export const queryFindScenes = (filter: ListFilterModel) =>
let sceneFilter = {}; client.query<GQL.FindScenesQuery>({
sceneFilter = filter.makeSceneFilter();
return StashService.client.query<GQL.FindScenesQuery>({
query: GQL.FindScenesDocument, query: GQL.FindScenesDocument,
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
scene_filter: sceneFilter, scene_filter: filter.makeSceneFilter(),
}, },
}); });
}
public static useFindSceneMarkers(filter: ListFilterModel) { export const useFindSceneMarkers = (filter: ListFilterModel) =>
let sceneMarkerFilter = {}; GQL.useFindSceneMarkersQuery({
// if (!!filter && filter.criteriaFilterOpen) {
sceneMarkerFilter = filter.makeSceneMarkerFilter();
// }
// if (filter.customCriteria) {
// filter.customCriteria.forEach(criteria => {
// scene_filter[criteria.key] = criteria.value;
// });
// }
return GQL.useFindSceneMarkersQuery({
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
scene_marker_filter: sceneMarkerFilter, scene_marker_filter: filter.makeSceneMarkerFilter(),
}, },
}); });
}
public static queryFindSceneMarkers(filter: ListFilterModel) { export const queryFindSceneMarkers = (filter: ListFilterModel) =>
let sceneMarkerFilter = {}; client.query<GQL.FindSceneMarkersQuery>({
sceneMarkerFilter = filter.makeSceneMarkerFilter();
return StashService.client.query<GQL.FindSceneMarkersQuery>({
query: GQL.FindSceneMarkersDocument, query: GQL.FindSceneMarkersDocument,
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
scene_marker_filter: sceneMarkerFilter, scene_marker_filter: filter.makeSceneMarkerFilter(),
}, },
}); });
}
public static useFindStudios(filter: ListFilterModel) { export const useFindStudios = (filter: ListFilterModel) =>
return GQL.useFindStudiosQuery({ GQL.useFindStudiosQuery({
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
}, },
}); });
}
public static useFindMovies(filter: ListFilterModel) { export const useFindMovies = (filter: ListFilterModel) =>
const movieFilter = filter.makeMovieFilter(); GQL.useFindMoviesQuery({
return GQL.useFindMoviesQuery({
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
movie_filter: movieFilter, movie_filter: filter.makeMovieFilter(),
}, },
}); });
}
public static useFindPerformers(filter: ListFilterModel) { export const useFindPerformers = (filter: ListFilterModel) =>
let performerFilter = {}; GQL.useFindPerformersQuery({
// if (!!filter && filter.criteriaFilterOpen) {
performerFilter = filter.makePerformerFilter();
// }
// if (filter.customCriteria) {
// filter.customCriteria.forEach(criteria => {
// scene_filter[criteria.key] = criteria.value;
// });
// }
return GQL.useFindPerformersQuery({
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
performer_filter: performerFilter, performer_filter: filter.makePerformerFilter(),
}, },
}); });
}
public static queryFindPerformers(filter: ListFilterModel) { export const queryFindPerformers = (filter: ListFilterModel) =>
let performerFilter = {}; client.query<GQL.FindPerformersQuery>({
performerFilter = filter.makePerformerFilter();
return StashService.client.query<GQL.FindPerformersQuery>({
query: GQL.FindPerformersDocument, query: GQL.FindPerformersDocument,
variables: { variables: {
filter: filter.makeFindFilter(), filter: filter.makeFindFilter(),
performer_filter: performerFilter, performer_filter: filter.makePerformerFilter(),
}, },
}); });
}
public static useFindGallery(id: string) { export const useFindGallery = (id: string) =>
return GQL.useFindGalleryQuery({ variables: { id } }); GQL.useFindGalleryQuery({ variables: { id } });
} export const useFindScene = (id: string) =>
public static useFindScene(id: string) { GQL.useFindSceneQuery({ variables: { id } });
return GQL.useFindSceneQuery({ variables: { id } }); export const useFindPerformer = (id: string) => {
}
public static useFindPerformer(id: string) {
const skip = id === "new"; const skip = id === "new";
return GQL.useFindPerformerQuery({ variables: { id }, skip }); return GQL.useFindPerformerQuery({ variables: { id }, skip });
} };
public static useFindStudio(id: string) { export const useFindStudio = (id: string) => {
const skip = id === "new"; const skip = id === "new";
return GQL.useFindStudioQuery({ variables: { id }, skip }); return GQL.useFindStudioQuery({ variables: { id }, skip });
} };
public static useFindMovie(id: string) { export const useFindMovie = (id: string) => {
const skip = id === "new"; const skip = id === "new";
return GQL.useFindMovieQuery({ variables: { id }, skip }); return GQL.useFindMovieQuery({ variables: { id }, skip });
} };
// TODO - scene marker manipulation functions are handled differently // TODO - scene marker manipulation functions are handled differently
private static sceneMarkerMutationImpactedQueries = [ export const sceneMarkerMutationImpactedQueries = [
"findSceneMarkers", "findSceneMarkers",
"findScenes", "findScenes",
"markerStrings", "markerStrings",
"sceneMarkerTags", "sceneMarkerTags",
]; ];
public static useSceneMarkerCreate() { export const useSceneMarkerCreate = () =>
return GQL.useSceneMarkerCreateMutation({ refetchQueries: ["FindScene"] }); GQL.useSceneMarkerCreateMutation({ refetchQueries: ["FindScene"] });
} export const useSceneMarkerUpdate = () =>
public static useSceneMarkerUpdate() { GQL.useSceneMarkerUpdateMutation({ refetchQueries: ["FindScene"] });
return GQL.useSceneMarkerUpdateMutation({ refetchQueries: ["FindScene"] }); export const useSceneMarkerDestroy = () =>
} GQL.useSceneMarkerDestroyMutation({ refetchQueries: ["FindScene"] });
public static useSceneMarkerDestroy() {
return GQL.useSceneMarkerDestroyMutation({ refetchQueries: ["FindScene"] });
}
public static useListPerformerScrapers() { export const useListPerformerScrapers = () =>
return GQL.useListPerformerScrapersQuery(); GQL.useListPerformerScrapersQuery();
} export const useScrapePerformerList = (scraperId: string, q: string) =>
public static useScrapePerformerList(scraperId: string, q: string) { GQL.useScrapePerformerListQuery({
return GQL.useScrapePerformerListQuery({
variables: { scraper_id: scraperId, query: q }, variables: { scraper_id: scraperId, query: q },
skip: q === "", skip: q === "",
}); });
} export const useScrapePerformer = (
public static useScrapePerformer(
scraperId: string, scraperId: string,
scrapedPerformer: GQL.ScrapedPerformerInput scrapedPerformer: GQL.ScrapedPerformerInput
) { ) =>
return GQL.useScrapePerformerQuery({ GQL.useScrapePerformerQuery({
variables: { scraper_id: scraperId, scraped_performer: scrapedPerformer }, variables: { scraper_id: scraperId, scraped_performer: scrapedPerformer },
}); });
}
public static useListSceneScrapers() { export const useListSceneScrapers = () => GQL.useListSceneScrapersQuery();
return GQL.useListSceneScrapersQuery();
}
public static useScrapeFreeonesPerformers(q: string) { export const useScrapeFreeonesPerformers = (q: string) =>
return GQL.useScrapeFreeonesPerformersQuery({ variables: { q } }); GQL.useScrapeFreeonesPerformersQuery({ variables: { q } });
} export const useMarkerStrings = () => GQL.useMarkerStringsQuery();
public static useMarkerStrings() { export const useAllTags = () => GQL.useAllTagsQuery();
return GQL.useMarkerStringsQuery(); export const useAllTagsForFilter = () => GQL.useAllTagsForFilterQuery();
} export const useAllPerformersForFilter = () =>
public static useAllTags() { GQL.useAllPerformersForFilterQuery();
return GQL.useAllTagsQuery(); export const useAllStudiosForFilter = () => GQL.useAllStudiosForFilterQuery();
} export const useAllMoviesForFilter = () => GQL.useAllMoviesForFilterQuery();
public static useAllTagsForFilter() { export const useValidGalleriesForScene = (sceneId: string) =>
return GQL.useAllTagsForFilterQuery(); GQL.useValidGalleriesForSceneQuery({
}
public static useAllPerformersForFilter() {
return GQL.useAllPerformersForFilterQuery();
}
public static useAllStudiosForFilter() {
return GQL.useAllStudiosForFilterQuery();
}
public static useAllMoviesForFilter() {
return GQL.useAllMoviesForFilterQuery();
}
public static useValidGalleriesForScene(sceneId: string) {
return GQL.useValidGalleriesForSceneQuery({
variables: { scene_id: sceneId }, variables: { scene_id: sceneId },
}); });
} export const useStats = () => GQL.useStatsQuery();
public static useStats() { export const useVersion = () => GQL.useVersionQuery();
return GQL.useStatsQuery(); export const useLatestVersion = () =>
} GQL.useLatestVersionQuery({
public static useVersion() {
return GQL.useVersionQuery();
}
public static useLatestVersion() {
return GQL.useLatestVersionQuery({
notifyOnNetworkStatusChange: true, notifyOnNetworkStatusChange: true,
errorPolicy: "ignore", errorPolicy: "ignore",
}); });
}
public static useConfiguration() { export const useConfiguration = () => GQL.useConfigurationQuery();
return GQL.useConfigurationQuery(); export const useDirectories = (path?: string) =>
} GQL.useDirectoriesQuery({ variables: { path } });
public static useDirectories(path?: string) {
return GQL.useDirectoriesQuery({ variables: { path } });
}
private static performerMutationImpactedQueries = [ export const performerMutationImpactedQueries = [
"findPerformers", "findPerformers",
"findScenes", "findScenes",
"findSceneMarkers", "findSceneMarkers",
"allPerformers", "allPerformers",
]; ];
public static usePerformerCreate() { export const usePerformerCreate = () =>
return GQL.usePerformerCreateMutation({ GQL.usePerformerCreateMutation({
update: () => update: () => invalidateQueries(performerMutationImpactedQueries),
StashService.invalidateQueries(
StashService.performerMutationImpactedQueries
),
}); });
} export const usePerformerUpdate = () =>
public static usePerformerUpdate() { GQL.usePerformerUpdateMutation({
return GQL.usePerformerUpdateMutation({ update: () => invalidateQueries(performerMutationImpactedQueries),
update: () =>
StashService.invalidateQueries(
StashService.performerMutationImpactedQueries
),
}); });
} export const usePerformerDestroy = () =>
public static usePerformerDestroy() { GQL.usePerformerDestroyMutation({
return GQL.usePerformerDestroyMutation({ update: () => invalidateQueries(performerMutationImpactedQueries),
update: () =>
StashService.invalidateQueries(
StashService.performerMutationImpactedQueries
),
}); });
}
private static sceneMutationImpactedQueries = [ export const sceneMutationImpactedQueries = [
"findPerformers", "findPerformers",
"findScenes", "findScenes",
"findSceneMarkers", "findSceneMarkers",
@@ -374,378 +204,301 @@ export class StashService {
"findMovies", "findMovies",
"allTags", "allTags",
// TODO - add "findTags" when it is implemented // TODO - add "findTags" when it is implemented
]; ];
public static useSceneUpdate(input: GQL.SceneUpdateInput) { export const useSceneUpdate = (input: GQL.SceneUpdateInput) =>
return GQL.useSceneUpdateMutation({ GQL.useSceneUpdateMutation({
variables: input, variables: input,
update: () => update: () => invalidateQueries(sceneMutationImpactedQueries),
StashService.invalidateQueries(
StashService.sceneMutationImpactedQueries
),
refetchQueries: ["AllTagsForFilter"], refetchQueries: ["AllTagsForFilter"],
}); });
}
// remove findScenes for bulk scene update so that we don't lose // remove findScenes for bulk scene update so that we don't lose
// existing results // existing results
private static sceneBulkMutationImpactedQueries = [ export const sceneBulkMutationImpactedQueries = [
"findPerformers", "findPerformers",
"findSceneMarkers", "findSceneMarkers",
"findStudios", "findStudios",
"findMovies", "findMovies",
"allTags", "allTags",
]; ];
public static useBulkSceneUpdate(input: GQL.BulkSceneUpdateInput) { export const useBulkSceneUpdate = (input: GQL.BulkSceneUpdateInput) =>
return GQL.useBulkSceneUpdateMutation({ GQL.useBulkSceneUpdateMutation({
variables: input, variables: input,
update: () => update: () => invalidateQueries(sceneBulkMutationImpactedQueries),
StashService.invalidateQueries(
StashService.sceneBulkMutationImpactedQueries
),
}); });
}
public static useScenesUpdate(input: GQL.SceneUpdateInput[]) { export const useScenesUpdate = (input: GQL.SceneUpdateInput[]) =>
return GQL.useScenesUpdateMutation({ variables: { input } }); GQL.useScenesUpdateMutation({ variables: { input } });
}
public static useSceneIncrementO(id: string) { export const useSceneIncrementO = (id: string) =>
return GQL.useSceneIncrementOMutation({ GQL.useSceneIncrementOMutation({
variables: { id }, variables: { id },
}); });
}
public static useSceneDecrementO(id: string) { export const useSceneDecrementO = (id: string) =>
return GQL.useSceneDecrementOMutation({ GQL.useSceneDecrementOMutation({
variables: { id }, variables: { id },
}); });
}
public static useSceneResetO(id: string) { export const useSceneResetO = (id: string) =>
return GQL.useSceneResetOMutation({ GQL.useSceneResetOMutation({
variables: { id }, variables: { id },
}); });
}
public static useSceneDestroy(input: GQL.SceneDestroyInput) { export const useSceneDestroy = (input: GQL.SceneDestroyInput) =>
return GQL.useSceneDestroyMutation({ GQL.useSceneDestroyMutation({
variables: input, variables: input,
update: () => update: () => invalidateQueries(sceneMutationImpactedQueries),
StashService.invalidateQueries(
StashService.sceneMutationImpactedQueries
),
}); });
}
public static useSceneGenerateScreenshot() { export const useSceneGenerateScreenshot = () =>
return GQL.useSceneGenerateScreenshotMutation({ GQL.useSceneGenerateScreenshotMutation({
update: () => StashService.invalidateQueries(["findScenes"]), update: () => invalidateQueries(["findScenes"]),
}); });
}
private static studioMutationImpactedQueries = [ export const studioMutationImpactedQueries = [
"findStudios", "findStudios",
"findScenes", "findScenes",
"allStudios", "allStudios",
]; ];
public static useStudioCreate(input: GQL.StudioCreateInput) { export const useStudioCreate = (input: GQL.StudioCreateInput) =>
return GQL.useStudioCreateMutation({ GQL.useStudioCreateMutation({
variables: input, variables: input,
update: () => update: () => invalidateQueries(studioMutationImpactedQueries),
StashService.invalidateQueries(
StashService.studioMutationImpactedQueries
),
}); });
}
public static useStudioUpdate(input: GQL.StudioUpdateInput) { export const useStudioUpdate = (input: GQL.StudioUpdateInput) =>
return GQL.useStudioUpdateMutation({ GQL.useStudioUpdateMutation({
variables: input, variables: input,
update: () => update: () => invalidateQueries(studioMutationImpactedQueries),
StashService.invalidateQueries(
StashService.studioMutationImpactedQueries
),
}); });
}
public static useStudioDestroy(input: GQL.StudioDestroyInput) { export const useStudioDestroy = (input: GQL.StudioDestroyInput) =>
return GQL.useStudioDestroyMutation({ GQL.useStudioDestroyMutation({
variables: input, variables: input,
update: () => update: () => invalidateQueries(studioMutationImpactedQueries),
StashService.invalidateQueries(
StashService.studioMutationImpactedQueries
),
}); });
}
private static movieMutationImpactedQueries = [ export const movieMutationImpactedQueries = [
"findMovies", "findMovies",
"findScenes", "findScenes",
"allMovies", "allMovies",
]; ];
public static useMovieCreate(input: GQL.MovieCreateInput) { export const useMovieCreate = (input: GQL.MovieCreateInput) =>
return GQL.useMovieCreateMutation({ GQL.useMovieCreateMutation({
variables: input, variables: input,
update: () => update: () => invalidateQueries(movieMutationImpactedQueries),
StashService.invalidateQueries(
StashService.movieMutationImpactedQueries
),
}); });
}
public static useMovieUpdate(input: GQL.MovieUpdateInput) { export const useMovieUpdate = (input: GQL.MovieUpdateInput) =>
return GQL.useMovieUpdateMutation({ GQL.useMovieUpdateMutation({
variables: input, variables: input,
update: () => update: () => invalidateQueries(movieMutationImpactedQueries),
StashService.invalidateQueries(
StashService.movieMutationImpactedQueries
),
}); });
}
public static useMovieDestroy(input: GQL.MovieDestroyInput) { export const useMovieDestroy = (input: GQL.MovieDestroyInput) =>
return GQL.useMovieDestroyMutation({ GQL.useMovieDestroyMutation({
variables: input, variables: input,
update: () => update: () => invalidateQueries(movieMutationImpactedQueries),
StashService.invalidateQueries(
StashService.movieMutationImpactedQueries
),
}); });
}
private static tagMutationImpactedQueries = [ export const tagMutationImpactedQueries = [
"findScenes", "findScenes",
"findSceneMarkers", "findSceneMarkers",
"sceneMarkerTags", "sceneMarkerTags",
"allTags", "allTags",
]; ];
public static useTagCreate(input: GQL.TagCreateInput) { export const useTagCreate = (input: GQL.TagCreateInput) =>
return GQL.useTagCreateMutation({ GQL.useTagCreateMutation({
variables: input, variables: input,
refetchQueries: ["AllTags", "AllTagsForFilter"], refetchQueries: ["AllTags", "AllTagsForFilter"],
// update: () => StashService.invalidateQueries(StashService.tagMutationImpactedQueries) // update: () => StashService.invalidateQueries(StashService.tagMutationImpactedQueries)
}); });
} export const useTagUpdate = (input: GQL.TagUpdateInput) =>
public static useTagUpdate(input: GQL.TagUpdateInput) { GQL.useTagUpdateMutation({
return GQL.useTagUpdateMutation({
variables: input, variables: input,
refetchQueries: ["AllTags", "AllTagsForFilter"], refetchQueries: ["AllTags", "AllTagsForFilter"],
}); });
} export const useTagDestroy = (input: GQL.TagDestroyInput) =>
public static useTagDestroy(input: GQL.TagDestroyInput) { GQL.useTagDestroyMutation({
return GQL.useTagDestroyMutation({
variables: input, variables: input,
refetchQueries: ["AllTags", "AllTagsForFilter"], refetchQueries: ["AllTags", "AllTagsForFilter"],
update: () => update: () => invalidateQueries(tagMutationImpactedQueries),
StashService.invalidateQueries(StashService.tagMutationImpactedQueries),
}); });
}
public static useConfigureGeneral(input: GQL.ConfigGeneralInput) { export const useConfigureGeneral = (input: GQL.ConfigGeneralInput) =>
return GQL.useConfigureGeneralMutation({ GQL.useConfigureGeneralMutation({
variables: { input }, variables: { input },
refetchQueries: ["Configuration"], refetchQueries: ["Configuration"],
}); });
}
public static useConfigureInterface(input: GQL.ConfigInterfaceInput) { export const useConfigureInterface = (input: GQL.ConfigInterfaceInput) =>
return GQL.useConfigureInterfaceMutation({ GQL.useConfigureInterfaceMutation({
variables: { input }, variables: { input },
refetchQueries: ["Configuration"], refetchQueries: ["Configuration"],
}); });
}
public static useMetadataUpdate() { export const useMetadataUpdate = () => GQL.useMetadataUpdateSubscription();
return GQL.useMetadataUpdateSubscription();
}
public static useLoggingSubscribe() { export const useLoggingSubscribe = () => GQL.useLoggingSubscribeSubscription();
return GQL.useLoggingSubscribeSubscription();
}
public static useLogs() { export const useLogs = () =>
return GQL.useLogsQuery({ GQL.useLogsQuery({
fetchPolicy: "no-cache", fetchPolicy: "no-cache",
}); });
}
public static useJobStatus() { export const useJobStatus = () =>
return GQL.useJobStatusQuery({ GQL.useJobStatusQuery({
fetchPolicy: "no-cache", fetchPolicy: "no-cache",
}); });
}
public static mutateStopJob() { export const mutateStopJob = () =>
return StashService.client.mutate<GQL.StopJobMutation>({ client.mutate<GQL.StopJobMutation>({
mutation: GQL.StopJobDocument, mutation: GQL.StopJobDocument,
}); });
}
public static queryScrapeFreeones(performerName: string) { export const queryScrapeFreeones = (performerName: string) =>
return StashService.client.query<GQL.ScrapeFreeonesQuery>({ client.query<GQL.ScrapeFreeonesQuery>({
query: GQL.ScrapeFreeonesDocument, query: GQL.ScrapeFreeonesDocument,
variables: { variables: {
performer_name: performerName, performer_name: performerName,
}, },
}); });
}
public static queryScrapePerformer( export const queryScrapePerformer = (
scraperId: string, scraperId: string,
scrapedPerformer: GQL.ScrapedPerformerInput scrapedPerformer: GQL.ScrapedPerformerInput
) { ) =>
return StashService.client.query<GQL.ScrapePerformerQuery>({ client.query<GQL.ScrapePerformerQuery>({
query: GQL.ScrapePerformerDocument, query: GQL.ScrapePerformerDocument,
variables: { variables: {
scraper_id: scraperId, scraper_id: scraperId,
scraped_performer: scrapedPerformer, scraped_performer: scrapedPerformer,
}, },
}); });
}
public static queryScrapePerformerURL(url: string) { export const queryScrapePerformerURL = (url: string) =>
return StashService.client.query<GQL.ScrapePerformerUrlQuery>({ client.query<GQL.ScrapePerformerUrlQuery>({
query: GQL.ScrapePerformerUrlDocument, query: GQL.ScrapePerformerUrlDocument,
variables: { variables: {
url, url,
}, },
}); });
}
public static queryScrapeSceneURL(url: string) { export const queryScrapeSceneURL = (url: string) =>
return StashService.client.query<GQL.ScrapeSceneUrlQuery>({ client.query<GQL.ScrapeSceneUrlQuery>({
query: GQL.ScrapeSceneUrlDocument, query: GQL.ScrapeSceneUrlDocument,
variables: { variables: {
url, url,
}, },
}); });
}
public static queryScrapeScene( export const queryScrapeScene = (
scraperId: string, scraperId: string,
scene: GQL.SceneUpdateInput scene: GQL.SceneUpdateInput
) { ) =>
return StashService.client.query<GQL.ScrapeSceneQuery>({ client.query<GQL.ScrapeSceneQuery>({
query: GQL.ScrapeSceneDocument, query: GQL.ScrapeSceneDocument,
variables: { variables: {
scraper_id: scraperId, scraper_id: scraperId,
scene, scene,
}, },
}); });
}
public static mutateMetadataScan(input: GQL.ScanMetadataInput) { export const mutateMetadataScan = (input: GQL.ScanMetadataInput) =>
return StashService.client.mutate<GQL.MetadataScanMutation>({ client.mutate<GQL.MetadataScanMutation>({
mutation: GQL.MetadataScanDocument, mutation: GQL.MetadataScanDocument,
variables: { input }, variables: { input },
}); });
}
public static mutateMetadataAutoTag(input: GQL.AutoTagMetadataInput) { export const mutateMetadataAutoTag = (input: GQL.AutoTagMetadataInput) =>
return StashService.client.mutate<GQL.MetadataAutoTagMutation>({ client.mutate<GQL.MetadataAutoTagMutation>({
mutation: GQL.MetadataAutoTagDocument, mutation: GQL.MetadataAutoTagDocument,
variables: { input }, variables: { input },
}); });
}
public static mutateMetadataGenerate(input: GQL.GenerateMetadataInput) { export const mutateMetadataGenerate = (input: GQL.GenerateMetadataInput) =>
return StashService.client.mutate<GQL.MetadataGenerateMutation>({ client.mutate<GQL.MetadataGenerateMutation>({
mutation: GQL.MetadataGenerateDocument, mutation: GQL.MetadataGenerateDocument,
variables: { input }, variables: { input },
}); });
}
public static mutateMetadataClean() { export const mutateMetadataClean = () =>
return StashService.client.mutate<GQL.MetadataCleanMutation>({ client.mutate<GQL.MetadataCleanMutation>({
mutation: GQL.MetadataCleanDocument, mutation: GQL.MetadataCleanDocument,
}); });
}
public static mutateMetadataExport() { export const mutateMetadataExport = () =>
return StashService.client.mutate<GQL.MetadataExportMutation>({ client.mutate<GQL.MetadataExportMutation>({
mutation: GQL.MetadataExportDocument, mutation: GQL.MetadataExportDocument,
}); });
}
public static mutateMetadataImport() { export const mutateMetadataImport = () =>
return StashService.client.mutate<GQL.MetadataImportMutation>({ client.mutate<GQL.MetadataImportMutation>({
mutation: GQL.MetadataImportDocument, mutation: GQL.MetadataImportDocument,
}); });
}
public static querySceneByPathRegex(filter: GQL.FindFilterType) { export const querySceneByPathRegex = (filter: GQL.FindFilterType) =>
return StashService.client.query<GQL.FindScenesByPathRegexQuery>({ client.query<GQL.FindScenesByPathRegexQuery>({
query: GQL.FindScenesByPathRegexDocument, query: GQL.FindScenesByPathRegexDocument,
variables: { filter }, variables: { filter },
}); });
}
public static queryParseSceneFilenames( export const queryParseSceneFilenames = (
filter: GQL.FindFilterType, filter: GQL.FindFilterType,
config: GQL.SceneParserInput config: GQL.SceneParserInput
) { ) =>
return StashService.client.query<GQL.ParseSceneFilenamesQuery>({ client.query<GQL.ParseSceneFilenamesQuery>({
query: GQL.ParseSceneFilenamesDocument, query: GQL.ParseSceneFilenamesDocument,
variables: { filter, config }, variables: { filter, config },
fetchPolicy: "network-only", fetchPolicy: "network-only",
}); });
}
private static stringGenderMap = new Map<string, GQL.GenderEnum>([ export const stringGenderMap = new Map<string, GQL.GenderEnum>([
["Male", GQL.GenderEnum.Male], ["Male", GQL.GenderEnum.Male],
["Female", GQL.GenderEnum.Female], ["Female", GQL.GenderEnum.Female],
["Transgender Male", GQL.GenderEnum.TransgenderMale], ["Transgender Male", GQL.GenderEnum.TransgenderMale],
["Transgender Female", GQL.GenderEnum.TransgenderFemale], ["Transgender Female", GQL.GenderEnum.TransgenderFemale],
["Intersex", GQL.GenderEnum.Intersex], ["Intersex", GQL.GenderEnum.Intersex],
]); ]);
public static genderToString(value?: GQL.GenderEnum) { export const genderToString = (value?: GQL.GenderEnum) => {
if (!value) { if (!value) {
return undefined; return undefined;
} }
const foundEntry = Array.from(StashService.stringGenderMap.entries()).find( const foundEntry = Array.from(stringGenderMap.entries()).find((e) => {
(e) => {
return e[1] === value; return e[1] === value;
} });
);
if (foundEntry) { if (foundEntry) {
return foundEntry[0]; return foundEntry[0];
} }
} };
public static stringToGender(value?: string, caseInsensitive?: boolean) { export const stringToGender = (value?: string, caseInsensitive?: boolean) => {
if (!value) { if (!value) {
return undefined; return undefined;
} }
const ret = StashService.stringGenderMap.get(value); const ret = stringGenderMap.get(value);
if (ret || !caseInsensitive) { if (ret || !caseInsensitive) {
return ret; return ret;
} }
const asUpper = value.toUpperCase(); const asUpper = value.toUpperCase();
const foundEntry = Array.from(StashService.stringGenderMap.entries()).find( const foundEntry = Array.from(stringGenderMap.entries()).find((e) => {
(e) => {
return e[0].toUpperCase() === asUpper; return e[0].toUpperCase() === asUpper;
} });
);
if (foundEntry) { if (foundEntry) {
return foundEntry[1]; return foundEntry[1];
} }
} };
public static getGenderStrings() { export const getGenderStrings = () => Array.from(stringGenderMap.keys());
return Array.from(StashService.stringGenderMap.keys());
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}
}

View File

@@ -0,0 +1,82 @@
import ApolloClient from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { WebSocketLink } from "apollo-link-ws";
import { HttpLink } from "apollo-link-http";
import { onError } from "apollo-link-error";
import { ServerError } from "apollo-link-http-common";
import { split, from } from "apollo-link";
import { getMainDefinition } from "apollo-utilities";
export const getPlatformURL = (ws?: boolean) => {
const platformUrl = new URL(window.location.origin);
if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
platformUrl.port = "9999"; // TODO: Hack. Development expects port 9999
if (process.env.REACT_APP_HTTPS === "true") {
platformUrl.protocol = "https:";
}
}
if (ws) {
platformUrl.protocol = "ws:";
}
return platformUrl;
};
export const createClient = () => {
const platformUrl = getPlatformURL();
const wsPlatformUrl = getPlatformURL(true);
if (platformUrl.protocol === "https:") {
wsPlatformUrl.protocol = "wss:";
}
const url = `${platformUrl.toString().slice(0, -1)}/graphql`;
const wsUrl = `${wsPlatformUrl.toString().slice(0, -1)}/graphql`;
const httpLink = new HttpLink({
uri: url,
});
const wsLink = new WebSocketLink({
uri: wsUrl,
options: {
reconnect: true,
},
});
const errorLink = onError(({ networkError }) => {
// handle unauthorized error by redirecting to the login page
if (networkError && (networkError as ServerError).statusCode === 401) {
// redirect to login page
window.location.href = "/login";
}
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsLink,
httpLink
);
const link = from([errorLink, splitLink]);
const cache = new InMemoryCache();
const client = new ApolloClient({
link,
cache,
});
return {
cache,
client,
};
};

View File

@@ -25,7 +25,14 @@ import {
import { LoadingIndicator } from "src/components/Shared"; import { LoadingIndicator } from "src/components/Shared";
import { ListFilter } from "src/components/List/ListFilter"; import { ListFilter } from "src/components/List/ListFilter";
import { Pagination, PaginationIndex } from "src/components/List/Pagination"; import { Pagination, PaginationIndex } from "src/components/List/Pagination";
import { StashService } from "src/core/StashService"; import {
useFindScenes,
useFindSceneMarkers,
useFindMovies,
useFindStudios,
useFindGalleries,
useFindPerformers,
} from "src/core/StashService";
import { Criterion } from "src/models/list-filter/criteria/criterion"; import { Criterion } from "src/models/list-filter/criteria/criterion";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode, FilterMode } from "src/models/list-filter/types"; import { DisplayMode, FilterMode } from "src/models/list-filter/types";
@@ -418,7 +425,7 @@ export const useScenesList = (props: IListHookOptions<FindScenesQueryResult>) =>
useList<FindScenesQueryResult, SlimSceneDataFragment>({ useList<FindScenesQueryResult, SlimSceneDataFragment>({
...props, ...props,
filterMode: FilterMode.Scenes, filterMode: FilterMode.Scenes,
useData: StashService.useFindScenes, useData: useFindScenes,
getData: (result: FindScenesQueryResult) => getData: (result: FindScenesQueryResult) =>
result?.data?.findScenes?.scenes ?? [], result?.data?.findScenes?.scenes ?? [],
getCount: (result: FindScenesQueryResult) => getCount: (result: FindScenesQueryResult) =>
@@ -431,7 +438,7 @@ export const useSceneMarkersList = (
useList<FindSceneMarkersQueryResult, SceneMarkerDataFragment>({ useList<FindSceneMarkersQueryResult, SceneMarkerDataFragment>({
...props, ...props,
filterMode: FilterMode.SceneMarkers, filterMode: FilterMode.SceneMarkers,
useData: StashService.useFindSceneMarkers, useData: useFindSceneMarkers,
getData: (result: FindSceneMarkersQueryResult) => getData: (result: FindSceneMarkersQueryResult) =>
result?.data?.findSceneMarkers?.scene_markers ?? [], result?.data?.findSceneMarkers?.scene_markers ?? [],
getCount: (result: FindSceneMarkersQueryResult) => getCount: (result: FindSceneMarkersQueryResult) =>
@@ -444,7 +451,7 @@ export const useGalleriesList = (
useList<FindGalleriesQueryResult, GalleryDataFragment>({ useList<FindGalleriesQueryResult, GalleryDataFragment>({
...props, ...props,
filterMode: FilterMode.Galleries, filterMode: FilterMode.Galleries,
useData: StashService.useFindGalleries, useData: useFindGalleries,
getData: (result: FindGalleriesQueryResult) => getData: (result: FindGalleriesQueryResult) =>
result?.data?.findGalleries?.galleries ?? [], result?.data?.findGalleries?.galleries ?? [],
getCount: (result: FindGalleriesQueryResult) => getCount: (result: FindGalleriesQueryResult) =>
@@ -457,7 +464,7 @@ export const useStudiosList = (
useList<FindStudiosQueryResult, StudioDataFragment>({ useList<FindStudiosQueryResult, StudioDataFragment>({
...props, ...props,
filterMode: FilterMode.Studios, filterMode: FilterMode.Studios,
useData: StashService.useFindStudios, useData: useFindStudios,
getData: (result: FindStudiosQueryResult) => getData: (result: FindStudiosQueryResult) =>
result?.data?.findStudios?.studios ?? [], result?.data?.findStudios?.studios ?? [],
getCount: (result: FindStudiosQueryResult) => getCount: (result: FindStudiosQueryResult) =>
@@ -470,7 +477,7 @@ export const usePerformersList = (
useList<FindPerformersQueryResult, PerformerDataFragment>({ useList<FindPerformersQueryResult, PerformerDataFragment>({
...props, ...props,
filterMode: FilterMode.Performers, filterMode: FilterMode.Performers,
useData: StashService.useFindPerformers, useData: useFindPerformers,
getData: (result: FindPerformersQueryResult) => getData: (result: FindPerformersQueryResult) =>
result?.data?.findPerformers?.performers ?? [], result?.data?.findPerformers?.performers ?? [],
getCount: (result: FindPerformersQueryResult) => getCount: (result: FindPerformersQueryResult) =>
@@ -481,7 +488,7 @@ export const useMoviesList = (props: IListHookOptions<FindMoviesQueryResult>) =>
useList<FindMoviesQueryResult, MovieDataFragment>({ useList<FindMoviesQueryResult, MovieDataFragment>({
...props, ...props,
filterMode: FilterMode.Movies, filterMode: FilterMode.Movies,
useData: StashService.useFindMovies, useData: useFindMovies,
getData: (result: FindMoviesQueryResult) => getData: (result: FindMoviesQueryResult) =>
result?.data?.findMovies?.movies ?? [], result?.data?.findMovies?.movies ?? [],
getCount: (result: FindMoviesQueryResult) => getCount: (result: FindMoviesQueryResult) =>

View File

@@ -1,7 +1,5 @@
/* eslint-disable no-param-reassign, no-console */
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { StashService } from "../core/StashService"; import { useConfiguration } from "../core/StashService";
export interface IVideoHoverHookData { export interface IVideoHoverHookData {
videoEl: React.RefObject<HTMLVideoElement>; videoEl: React.RefObject<HTMLVideoElement>;
@@ -14,19 +12,46 @@ export interface IVideoHoverHookOptions {
resetOnMouseLeave: boolean; resetOnMouseLeave: boolean;
} }
export class VideoHoverHook { export const useVideoHover = (options: IVideoHoverHookOptions) => {
public static useVideoHover(
options: IVideoHoverHookOptions
): IVideoHoverHookData {
const videoEl = useRef<HTMLVideoElement>(null); const videoEl = useRef<HTMLVideoElement>(null);
const isPlaying = useRef<boolean>(false); const isPlaying = useRef<boolean>(false);
const isHovering = useRef<boolean>(false); const isHovering = useRef<boolean>(false);
const config = useConfiguration();
const onMouseEnter = () => {
isHovering.current = true;
const videoTag = videoEl.current;
if (!videoTag) {
return;
}
if (videoTag.paused && !isPlaying.current) {
videoTag.play().catch((error) => {
// eslint-disable-next-line no-console
console.log(error.message);
});
}
};
const onMouseLeave = () => {
isHovering.current = false;
const videoTag = videoEl.current;
if (!videoTag) {
return;
}
if (!videoTag.paused && isPlaying) {
videoTag.pause();
if (options.resetOnMouseLeave) {
videoTag.removeAttribute("src");
videoTag.load();
isPlaying.current = false;
}
}
};
const config = StashService.useConfiguration();
const soundEnabled = const soundEnabled =
!!config.data && !!config.data.configuration config?.data?.configuration?.interface?.soundOnPreview ?? true;
? config.data.configuration.interface.soundOnPreview
: true;
useEffect(() => { useEffect(() => {
const videoTag = videoEl.current; const videoTag = videoEl.current;
@@ -53,37 +78,12 @@ export class VideoHoverHook {
videoTag.volume = soundEnabled ? 0.05 : 0; videoTag.volume = soundEnabled ? 0.05 : 0;
}, [soundEnabled]); }, [soundEnabled]);
return { videoEl, isPlaying, isHovering, options }; return {
} videoEl,
isPlaying,
public static onMouseEnter(data: IVideoHoverHookData) { isHovering,
data.isHovering.current = true; options,
onMouseEnter,
const videoTag = data.videoEl.current; onMouseLeave,
if (!videoTag) { };
return; };
}
if (videoTag.paused && !data.isPlaying.current) {
videoTag.play().catch((error) => {
console.log(error.message);
});
}
}
public static onMouseLeave(data: IVideoHoverHookData) {
data.isHovering.current = false;
const videoTag = data.videoEl.current;
if (!videoTag) {
return;
}
if (!videoTag.paused && data.isPlaying) {
videoTag.pause();
if (data.options.resetOnMouseLeave) {
videoTag.removeAttribute("src");
videoTag.load();
data.isPlaying.current = false;
}
}
}
}

View File

@@ -1,6 +1,6 @@
export { default as useToast } from "./Toast"; export { default as useToast } from "./Toast";
export { useInterfaceLocalForage } from "./LocalForage"; export { useInterfaceLocalForage } from "./LocalForage";
export { VideoHoverHook } from "./VideoHover"; export { useVideoHover } from "./VideoHover";
export { export {
useScenesList, useScenesList,
useSceneMarkersList, useSceneMarkersList,

View File

@@ -3,19 +3,16 @@ import { ApolloProvider } from "react-apollo";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import { App } from "./App"; import { App } from "./App";
import { StashService } from "./core/StashService"; import { getClient } from "./core/StashService";
import { getPlatformURL } from "./core/createClient";
import "./index.scss"; import "./index.scss";
import * as serviceWorker from "./serviceWorker"; import * as serviceWorker from "./serviceWorker";
ReactDOM.render( ReactDOM.render(
<> <>
<link <link rel="stylesheet" type="text/css" href={`${getPlatformURL()}css`} />
rel="stylesheet"
type="text/css"
href={`${StashService.getPlatformURL()}css`}
/>
<BrowserRouter> <BrowserRouter>
<ApolloProvider client={StashService.initialize()!}> <ApolloProvider client={getClient()}>
<App /> <App />
</ApolloProvider> </ApolloProvider>
</BrowserRouter> </BrowserRouter>

View File

@@ -1,14 +1,11 @@
{ {
"new": "Neu",
"tags": "Etiketten",
"scenes": "Szenen",
"movies": "Filme",
"studios": "Studios",
"galleries": "Galerien", "galleries": "Galerien",
"library-size": "",
"markers": "",
"movies": "Filme",
"new": "Neu",
"performers": "Künstler", "performers": "Künstler",
"markers": "Marken", "scenes": "Szenen",
"stats": { "studios": "Studios",
"notes": "Anmerkungen", "tags": "Etiketten"
"warning": "Dies ist noch eine frühe Version, einige Dinge sind noch in Arbeit."
}
} }

View File

@@ -1,14 +1,11 @@
{ {
"new": "New",
"tags": "Tags",
"scenes": "Scenes",
"movies": "Movies",
"studios": "Studios",
"galleries": "Galleries", "galleries": "Galleries",
"performers": "Performers", "library-size": "Library size",
"markers": "Markers", "markers": "Markers",
"stats": { "movies": "Movies",
"notes": "Notes", "new": "New",
"warning": "This is still an early version, some things are still a work in progress." "performers": "Performers",
} "scenes": "Scenes",
"studios": "Studios",
"tags": "Tags"
} }

View File

@@ -1,5 +1,5 @@
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { getGenderStrings } from "src/core/StashService";
import { Criterion, CriterionType, ICriterionOption } from "./criterion"; import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class GenderCriterion extends Criterion { export class GenderCriterion extends Criterion {
@@ -7,7 +7,7 @@ export class GenderCriterion extends Criterion {
public parameterName: string = "gender"; public parameterName: string = "gender";
public modifier = CriterionModifier.Equals; public modifier = CriterionModifier.Equals;
public modifierOptions = []; public modifierOptions = [];
public options: string[] = StashService.getGenderStrings(); public options: string[] = getGenderStrings();
public value: string = ""; public value: string = "";
} }

View File

@@ -8,7 +8,7 @@ import {
SortDirectionEnum, SortDirectionEnum,
MovieFilterType, MovieFilterType,
} from "src/core/generated-graphql"; } from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { stringToGender } from "src/core/StashService";
import { import {
Criterion, Criterion,
ICriterionOption, ICriterionOption,
@@ -512,7 +512,7 @@ export class ListFilterModel {
case "gender": { case "gender": {
const gCrit = criterion as GenderCriterion; const gCrit = criterion as GenderCriterion;
result.gender = { result.gender = {
value: StashService.stringToGender(gCrit.value), value: stringToGender(gCrit.value),
modifier: gCrit.modifier, modifier: gCrit.modifier,
}; };
break; break;

View File

@@ -14,7 +14,7 @@ const renderTextArea = (options: {
as="textarea" as="textarea"
readOnly={!options.isEditing} readOnly={!options.isEditing}
plaintext={!options.isEditing} plaintext={!options.isEditing}
onChange={(event: React.FormEvent<HTMLTextAreaElement>) => onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) =>
options.onChange(event.currentTarget.value) options.onChange(event.currentTarget.value)
} }
value={options.value} value={options.value}
@@ -32,7 +32,7 @@ const renderEditableText = (options: {
<Form.Control <Form.Control
readOnly={!options.isEditing} readOnly={!options.isEditing}
plaintext={!options.isEditing} plaintext={!options.isEditing}
onChange={(event: React.FormEvent<HTMLInputElement>) => onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
options.onChange(event.currentTarget.value) options.onChange(event.currentTarget.value)
} }
value={ value={
@@ -68,7 +68,7 @@ const renderInputGroup = (options: {
plaintext={!options.isEditing} plaintext={!options.isEditing}
value={options.value ?? ""} value={options.value ?? ""}
placeholder={options.placeholder ?? options.title} placeholder={options.placeholder ?? options.title}
onChange={(event: React.FormEvent<HTMLInputElement>) => onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
options.onChange(event.currentTarget.value) options.onChange(event.currentTarget.value)
} }
/> />
@@ -150,7 +150,7 @@ const renderHtmlSelect = (options: {
disabled={!options.isEditing} disabled={!options.isEditing}
plaintext={!options.isEditing} plaintext={!options.isEditing}
value={options.value?.toString()} value={options.value?.toString()}
onChange={(event: React.FormEvent<HTMLSelectElement>) => onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
options.onChange(event.currentTarget.value) options.onChange(event.currentTarget.value)
} }
> >

View File

@@ -25,13 +25,18 @@ const onImageChange = (
if (file) readImage(file, onLoadEnd); if (file) readImage(file, onLoadEnd);
}; };
const usePasteImage = (onLoadEnd: (this: FileReader) => void) => { const usePasteImage = (
onLoadEnd: (this: FileReader) => void,
isActive: boolean = true
) => {
useEffect(() => { useEffect(() => {
const paste = (event: ClipboardEvent) => pasteImage(event, onLoadEnd); const paste = (event: ClipboardEvent) => pasteImage(event, onLoadEnd);
if (isActive) {
document.addEventListener("paste", paste); document.addEventListener("paste", paste);
}
return () => document.removeEventListener("paste", paste); return () => document.removeEventListener("paste", paste);
}); }, [isActive, onLoadEnd]);
}; };
const Image = { const Image = {

File diff suppressed because it is too large Load Diff