mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
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:
@@ -25,79 +25,77 @@
|
||||
"not op_mini all"
|
||||
],
|
||||
"dependencies": {
|
||||
"@apollo/react-hooks": "^3.1.3",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.26",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.12.0",
|
||||
"@fortawesome/react-fontawesome": "^0.1.8",
|
||||
"@apollo/react-hooks": "^3.1.5",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.28",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.13.0",
|
||||
"@fortawesome/react-fontawesome": "^0.1.9",
|
||||
"apollo-cache": "^1.3.4",
|
||||
"apollo-cache-inmemory": "^1.6.5",
|
||||
"apollo-client": "^2.6.8",
|
||||
"apollo-link": "^1.2.13",
|
||||
"apollo-link-http": "^1.5.16",
|
||||
"apollo-link-ws": "^1.0.19",
|
||||
"apollo-link": "^1.2.14",
|
||||
"apollo-link-error": "^1.1.13",
|
||||
"apollo-link-http": "^1.5.17",
|
||||
"apollo-link-ws": "^1.0.20",
|
||||
"apollo-utilities": "^1.3.3",
|
||||
"axios": "0.18.1",
|
||||
"axios": "0.19.2",
|
||||
"bootstrap": "^4.4.1",
|
||||
"classnames": "^2.2.6",
|
||||
"flag-icon-css": "^3.4.6",
|
||||
"formik": "^2.1.2",
|
||||
"formik": "^2.1.4",
|
||||
"graphql": "^14.5.8",
|
||||
"graphql-tag": "^2.10.1",
|
||||
"graphql-tag": "^2.10.3",
|
||||
"i18n-iso-countries": "^5.2.0",
|
||||
"localforage": "1.7.3",
|
||||
"lodash": "^4.17.15",
|
||||
"query-string": "6.10.1",
|
||||
"react": "~16.12.0",
|
||||
"react-apollo": "^3.1.3",
|
||||
"react-bootstrap": "^1.0.0-beta.16",
|
||||
"react-dom": "16.12.0",
|
||||
"query-string": "6.12.1",
|
||||
"react": "16.13.1",
|
||||
"react-apollo": "^3.1.5",
|
||||
"react-bootstrap": "1.0.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-images": "0.5.19",
|
||||
"react-intl": "^3.12.0",
|
||||
"react-jw-player": "1.19.0",
|
||||
"react-intl": "^4.5.1",
|
||||
"react-jw-player": "1.19.1",
|
||||
"react-photo-gallery": "^8.0.0",
|
||||
"react-router-bootstrap": "^0.25.0",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-select": "^3.0.8",
|
||||
"react-select": "^3.1.0",
|
||||
"subscriptions-transport-ws": "^0.9.16",
|
||||
"universal-cookie": "^4.0.3",
|
||||
"video.js": "^7.6.0"
|
||||
"universal-cookie": "^4.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/add": "^1.11.2",
|
||||
"@graphql-codegen/cli": "^1.11.2",
|
||||
"@graphql-codegen/time": "^1.11.2",
|
||||
"@graphql-codegen/typescript": "^1.11.2",
|
||||
"@graphql-codegen/typescript-compatibility": "^1.11.2",
|
||||
"@graphql-codegen/typescript-operations": "^1.11.2",
|
||||
"@graphql-codegen/typescript-react-apollo": "^1.11.2",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/jest": "24.0.13",
|
||||
"@types/lodash": "^4.14.149",
|
||||
"@types/node": "13.1.8",
|
||||
"@types/react": "16.9.19",
|
||||
"@types/react-dom": "^16.9.5",
|
||||
"@graphql-codegen/add": "^1.13.5",
|
||||
"@graphql-codegen/cli": "^1.13.5",
|
||||
"@graphql-codegen/time": "^1.13.5",
|
||||
"@graphql-codegen/typescript": "^1.13.5",
|
||||
"@graphql-codegen/typescript-compatibility": "^1.13.5",
|
||||
"@graphql-codegen/typescript-operations": "^1.13.5",
|
||||
"@graphql-codegen/typescript-react-apollo": "^1.13.5",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/lodash": "^4.14.150",
|
||||
"@types/node": "13.13.4",
|
||||
"@types/react": "16.9.34",
|
||||
"@types/react-dom": "^16.9.7",
|
||||
"@types/react-images": "^0.5.1",
|
||||
"@types/react-router-bootstrap": "^0.24.5",
|
||||
"@types/react-router-dom": "5.1.3",
|
||||
"@types/react-select": "^3.0.8",
|
||||
"@types/video.js": "^7.2.11",
|
||||
"@typescript-eslint/eslint-plugin": "^2.16.0",
|
||||
"@typescript-eslint/parser": "^2.16.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-config-airbnb-typescript": "^6.3.1",
|
||||
"eslint-config-prettier": "^6.10.1",
|
||||
"eslint-plugin-import": "^2.20.0",
|
||||
"@types/react-router-dom": "5.1.5",
|
||||
"@types/react-select": "^3.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^2.30.0",
|
||||
"@typescript-eslint/parser": "^2.30.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-airbnb-typescript": "^7.2.1",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-react": "^7.18.0",
|
||||
"eslint-plugin-react-hooks": "^1.7.0",
|
||||
"extract-react-intl-messages": "^2.3.5",
|
||||
"node-sass": "4.13.1",
|
||||
"postcss-safe-parser": "^4.0.1",
|
||||
"prettier": "2.0.2",
|
||||
"react-scripts": "^3.3.1",
|
||||
"stylelint": "^13.0.0",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"eslint-plugin-react-hooks": "^4.0.0",
|
||||
"extract-react-intl-messages": "^4.1.1",
|
||||
"node-sass": "4.14.0",
|
||||
"postcss-safe-parser": "^4.0.2",
|
||||
"prettier": "2.0.5",
|
||||
"react-scripts": "^3.4.1",
|
||||
"stylelint": "^13.3.3",
|
||||
"stylelint-config-prettier": "^8.0.1",
|
||||
"stylelint-order": "^4.0.0",
|
||||
"typescript": "^3.7.5"
|
||||
"typescript": "^3.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { fas } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import locales from "src/locale";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { useConfiguration } from "src/core/StashService";
|
||||
import { flattenMessages } from "src/utils";
|
||||
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||
import Galleries from "./components/Galleries/Galleries";
|
||||
@@ -25,7 +25,7 @@ import Movies from "./components/Movies/Movies";
|
||||
library.add(fas);
|
||||
|
||||
export const App: React.FC = () => {
|
||||
const config = StashService.useConfiguration();
|
||||
const config = useConfiguration();
|
||||
const language = config.data?.configuration?.interface?.language ?? "en-US";
|
||||
const messageLanguage = language.slice(0, 2);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from "react";
|
||||
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 { GalleryViewer } from "./GalleryViewer";
|
||||
|
||||
export const Gallery: React.FC = () => {
|
||||
const { id = "" } = useParams();
|
||||
|
||||
const { data, error, loading } = StashService.useFindGallery(id);
|
||||
const { data, error, loading } = useFindGallery(id);
|
||||
const gallery = data?.findGallery;
|
||||
|
||||
if (loading || !gallery) return <LoadingIndicator />;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { debounce } from "lodash";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { SortDirectionEnum } from "src/core/generated-graphql";
|
||||
import {
|
||||
Badge,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Form,
|
||||
OverlayTrigger,
|
||||
Tooltip,
|
||||
SafeAnchor,
|
||||
SafeAnchorProps,
|
||||
} from "react-bootstrap";
|
||||
|
||||
import { Icon } from "src/components/Shared";
|
||||
@@ -45,18 +45,15 @@ const PAGE_SIZE_OPTIONS = ["20", "40", "60", "120"];
|
||||
export const ListFilter: React.FC<IListFilterProps> = (
|
||||
props: IListFilterProps
|
||||
) => {
|
||||
const searchCallback = useCallback(
|
||||
debounce((value: string) => {
|
||||
const searchCallback = debounce((value: string) => {
|
||||
props.onChangeQuery(value);
|
||||
}, 500),
|
||||
[props.onChangeQuery]
|
||||
);
|
||||
}, 500);
|
||||
|
||||
const [editingCriterion, setEditingCriterion] = useState<
|
||||
Criterion | undefined
|
||||
>(undefined);
|
||||
|
||||
function onChangePageSize(event: React.FormEvent<HTMLSelectElement>) {
|
||||
function onChangePageSize(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const val = event.currentTarget.value;
|
||||
props.onChangePageSize(parseInt(val, 10));
|
||||
}
|
||||
@@ -73,8 +70,8 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeSortBy(event: React.MouseEvent<SafeAnchor>) {
|
||||
const target = (event.currentTarget as unknown) as HTMLAnchorElement;
|
||||
function onChangeSortBy(event: React.MouseEvent<SafeAnchorProps>) {
|
||||
const target = event.currentTarget as HTMLAnchorElement;
|
||||
props.onChangeSortBy(target.text);
|
||||
}
|
||||
|
||||
@@ -266,7 +263,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
min={0}
|
||||
max={3}
|
||||
defaultValue={1}
|
||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChangeZoom(Number.parseInt(e.currentTarget.value, 10))
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,54 +1,90 @@
|
||||
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 { IconName } from "@fortawesome/fontawesome-svg-core";
|
||||
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 { Icon } from "src/components/Shared";
|
||||
|
||||
interface IMenuItem {
|
||||
messageID: string;
|
||||
message: MessageDescriptor;
|
||||
href: string;
|
||||
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[] = [
|
||||
{
|
||||
icon: "play-circle",
|
||||
messageID: "scenes",
|
||||
message: messages.scenes,
|
||||
href: "/scenes",
|
||||
},
|
||||
{
|
||||
href: "/movies",
|
||||
icon: "film",
|
||||
messageID: "movies",
|
||||
message: messages.movies,
|
||||
},
|
||||
{
|
||||
href: "/scenes/markers",
|
||||
icon: "map-marker-alt",
|
||||
messageID: "markers",
|
||||
message: messages.markers,
|
||||
},
|
||||
{
|
||||
href: "/galleries",
|
||||
icon: "image",
|
||||
messageID: "galleries",
|
||||
message: messages.galleries,
|
||||
},
|
||||
{
|
||||
href: "/performers",
|
||||
icon: "user",
|
||||
messageID: "performers",
|
||||
message: messages.performers,
|
||||
},
|
||||
{
|
||||
href: "/studios",
|
||||
icon: "video",
|
||||
messageID: "studios",
|
||||
message: messages.studios,
|
||||
},
|
||||
{
|
||||
href: "/tags",
|
||||
icon: "tag",
|
||||
messageID: "tags",
|
||||
message: messages.tags,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -58,6 +94,7 @@ export const MainNavbar: React.FC = () => {
|
||||
// react-bootstrap typing bug
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const navbarRef = useRef<any>();
|
||||
const intl = useIntl();
|
||||
|
||||
const maybeCollapse = (event: Event) => {
|
||||
if (
|
||||
@@ -92,11 +129,11 @@ export const MainNavbar: React.FC = () => {
|
||||
path === null ? (
|
||||
""
|
||||
) : (
|
||||
<LinkContainer to={path}>
|
||||
<Link to={path}>
|
||||
<Button variant="primary">
|
||||
<FormattedMessage id="new" defaultMessage="New" />
|
||||
</Button>
|
||||
</LinkContainer>
|
||||
</Link>
|
||||
);
|
||||
|
||||
function maybeRenderLogout() {
|
||||
@@ -140,17 +177,10 @@ export const MainNavbar: React.FC = () => {
|
||||
<Nav className="mr-md-auto">
|
||||
{menuItems.map((i) => (
|
||||
<Nav.Link eventKey={i.href} as="div" key={i.href}>
|
||||
<LinkContainer
|
||||
activeClassName="active"
|
||||
exact
|
||||
to={i.href}
|
||||
key={i.href}
|
||||
>
|
||||
<LinkContainer activeClassName="active" exact to={i.href}>
|
||||
<Button className="minimal w-100">
|
||||
<Icon icon={i.icon} />
|
||||
<span>
|
||||
<FormattedMessage id={i.messageID} />
|
||||
</span>
|
||||
<span>{intl.formatMessage(i.message)}</span>
|
||||
</Button>
|
||||
</LinkContainer>
|
||||
</Nav.Link>
|
||||
@@ -159,11 +189,11 @@ export const MainNavbar: React.FC = () => {
|
||||
</Navbar.Collapse>
|
||||
<Nav className="order-2 flex-row">
|
||||
<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">
|
||||
<Icon icon="cog" />
|
||||
</Button>
|
||||
</LinkContainer>
|
||||
</NavLink>
|
||||
{maybeRenderLogout()}
|
||||
</Nav>
|
||||
</Navbar>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
/* eslint-disable react/no-this-in-sfc */
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
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 cx from "classnames";
|
||||
import {
|
||||
@@ -53,14 +58,10 @@ export const Movie: React.FC = () => {
|
||||
);
|
||||
|
||||
// Network state
|
||||
const { data, error, loading } = StashService.useFindMovie(id);
|
||||
const [updateMovie] = StashService.useMovieUpdate(
|
||||
getMovieInput() as GQL.MovieUpdateInput
|
||||
);
|
||||
const [createMovie] = StashService.useMovieCreate(
|
||||
getMovieInput() as GQL.MovieCreateInput
|
||||
);
|
||||
const [deleteMovie] = StashService.useMovieDestroy(
|
||||
const { data, error, loading } = useFindMovie(id);
|
||||
const [updateMovie] = useMovieUpdate(getMovieInput() as GQL.MovieUpdateInput);
|
||||
const [createMovie] = useMovieCreate(getMovieInput() as GQL.MovieCreateInput);
|
||||
const [deleteMovie] = useMovieDestroy(
|
||||
getMovieInput() as GQL.MovieDestroyInput
|
||||
);
|
||||
|
||||
@@ -104,8 +105,8 @@ export const Movie: React.FC = () => {
|
||||
setBackImage(this.result as string);
|
||||
}
|
||||
|
||||
ImageUtils.usePasteImage(onImageLoad);
|
||||
ImageUtils.usePasteImage(onBackImageLoad);
|
||||
ImageUtils.usePasteImage(onImageLoad, isEditing);
|
||||
ImageUtils.usePasteImage(onBackImageLoad, isEditing);
|
||||
|
||||
if (!isNew && !isEditing) {
|
||||
if (!data || !data.findMovie || loading) return <LoadingIndicator />;
|
||||
@@ -280,7 +281,7 @@ export const Movie: React.FC = () => {
|
||||
as="textarea"
|
||||
readOnly={!isEditing}
|
||||
className="movie-synopsis text-input"
|
||||
onChange={(newValue: React.FormEvent<HTMLTextAreaElement>) =>
|
||||
onChange={(newValue: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setSynopsis(newValue.currentTarget.value)
|
||||
}
|
||||
value={synopsis}
|
||||
|
||||
@@ -5,7 +5,12 @@ import { Button, Tabs, Tab } from "react-bootstrap";
|
||||
import { useParams, useHistory } from "react-router-dom";
|
||||
import cx from "classnames";
|
||||
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 { useToast } from "src/hooks";
|
||||
import { getCountryISO, TextUtils } from "src/utils";
|
||||
@@ -26,25 +31,22 @@ export const Performer: React.FC = () => {
|
||||
>({});
|
||||
const [imagePreview, setImagePreview] = useState<string>();
|
||||
const [lightboxIsOpen, setLightboxIsOpen] = useState(false);
|
||||
const activeImage = imagePreview ?? performer.image_path ?? "";
|
||||
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { data, error } = StashService.useFindPerformer(id);
|
||||
const [updatePerformer] = StashService.usePerformerUpdate();
|
||||
const [createPerformer] = StashService.usePerformerCreate();
|
||||
const [deletePerformer] = StashService.usePerformerDestroy();
|
||||
const { data, error } = useFindPerformer(id);
|
||||
const [updatePerformer] = usePerformerUpdate();
|
||||
const [createPerformer] = usePerformerCreate();
|
||||
const [deletePerformer] = usePerformerDestroy();
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(false);
|
||||
if (data?.findPerformer) setPerformer(data.findPerformer);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
setImagePreview(performer.image_path ?? undefined);
|
||||
}, [performer]);
|
||||
|
||||
function onImageChange(image: string) {
|
||||
function onImageChange(image?: string) {
|
||||
setImagePreview(image);
|
||||
}
|
||||
|
||||
@@ -64,6 +66,10 @@ export const Performer: React.FC = () => {
|
||||
const result = await updatePerformer({
|
||||
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)
|
||||
setPerformer(result.data?.performerUpdate);
|
||||
} else {
|
||||
@@ -105,9 +111,15 @@ export const Performer: React.FC = () => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
function renderTabs() {
|
||||
function renderEditPanel() {
|
||||
return (
|
||||
const renderTabs = () => (
|
||||
<Tabs defaultActiveKey="details" id="performer-details" unmountOnExit>
|
||||
<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
|
||||
performer={performer}
|
||||
isEditing
|
||||
@@ -116,55 +128,33 @@ export const Performer: React.FC = () => {
|
||||
onSave={onSave}
|
||||
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 eventKey="operations" title="Operations">
|
||||
<PerformerOperationsPanel performer={performer} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
return renderEditPanel();
|
||||
}
|
||||
|
||||
function maybeRenderAge() {
|
||||
if (performer && performer.birthdate) {
|
||||
if (performer?.birthdate) {
|
||||
// calculate the age from birthdate. In future, this should probably be
|
||||
// provided by the server
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<span className="age">{TextUtils.age(performer.birthdate)}</span>
|
||||
<span className="age-tail"> years old</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderAliases() {
|
||||
if (performer && performer.aliases) {
|
||||
if (performer?.aliases) {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<span className="alias-head">Also known as </span>
|
||||
<span className="alias">{performer.aliases}</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -231,34 +221,36 @@ export const Performer: React.FC = () => {
|
||||
);
|
||||
|
||||
function renderPerformerImage() {
|
||||
if (imagePreview) {
|
||||
return <img className="photo" src={imagePreview} alt="Performer" />;
|
||||
if (activeImage) {
|
||||
return <img className="photo" src={activeImage} alt="Performer" />;
|
||||
}
|
||||
}
|
||||
|
||||
function renderNewView() {
|
||||
if (isNew)
|
||||
return (
|
||||
<div className="row new-view">
|
||||
<div className="col-4">{renderPerformerImage()}</div>
|
||||
<div className="col-6">
|
||||
<h2>Create Performer</h2>
|
||||
{renderTabs()}
|
||||
<PerformerDetailsPanel
|
||||
performer={performer}
|
||||
isEditing
|
||||
isNew={isNew}
|
||||
onDelete={onDelete}
|
||||
onSave={onSave}
|
||||
onImageChange={onImageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const photos = [{ src: imagePreview || "", caption: "Image" }];
|
||||
|
||||
if (isNew) {
|
||||
return renderNewView();
|
||||
}
|
||||
const photos = [{ src: activeImage, caption: "Image" }];
|
||||
|
||||
return (
|
||||
<div id="performer-page" className="row">
|
||||
<div className="image-container col-sm-4 offset-sm-1 d-none d-sm-block">
|
||||
<Button variant="link" onClick={() => setLightboxIsOpen(true)}>
|
||||
<img className="performer" src={imagePreview} alt="Performer" />
|
||||
<img className="performer" src={activeImage} alt="Performer" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col col-sm-6">
|
||||
@@ -282,7 +274,7 @@ export const Performer: React.FC = () => {
|
||||
onClose={() => setLightboxIsOpen(false)}
|
||||
currentImage={0}
|
||||
isOpen={lightboxIsOpen}
|
||||
onClickImage={() => window.open(imagePreview, "_blank")}
|
||||
onClickImage={() => window.open(activeImage, "_blank")}
|
||||
width={9999}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Popover, OverlayTrigger, Table } from "react-bootstrap";
|
||||
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 {
|
||||
Icon,
|
||||
Modal,
|
||||
@@ -29,7 +36,7 @@ interface IPerformerDetails {
|
||||
| Partial<GQL.PerformerUpdateInput>
|
||||
) => void;
|
||||
onDelete?: () => void;
|
||||
onImageChange?: (image: string) => void;
|
||||
onImageChange?: (image?: string) => void;
|
||||
}
|
||||
|
||||
export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
@@ -74,9 +81,11 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const Scrapers = StashService.useListPerformerScrapers();
|
||||
const Scrapers = useListPerformerScrapers();
|
||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
||||
|
||||
ImageUtils.usePasteImage(onImageLoad, isEditing);
|
||||
|
||||
function updatePerformerEditState(
|
||||
state: Partial<GQL.PerformerDataFragment | GQL.ScrapedPerformerDataFragment>
|
||||
) {
|
||||
@@ -99,9 +108,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
setTwitter(state.twitter ?? undefined);
|
||||
setInstagram(state.instagram ?? undefined);
|
||||
setGender(
|
||||
StashService.genderToString(
|
||||
(state as GQL.PerformerDataFragment).gender ?? undefined
|
||||
)
|
||||
genderToString((state as GQL.PerformerDataFragment).gender ?? undefined)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,16 +121,16 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
|
||||
// try to translate from enum values first
|
||||
const upperGender = scrapedGender?.toUpperCase();
|
||||
const asEnum = StashService.genderToString(upperGender as GQL.GenderEnum);
|
||||
const asEnum = genderToString(upperGender as GQL.GenderEnum);
|
||||
if (asEnum) {
|
||||
retEnum = StashService.stringToGender(asEnum);
|
||||
retEnum = stringToGender(asEnum);
|
||||
} else {
|
||||
// try to match against gender strings
|
||||
const caseInsensitive = true;
|
||||
retEnum = StashService.stringToGender(scrapedGender, caseInsensitive);
|
||||
retEnum = stringToGender(scrapedGender, caseInsensitive);
|
||||
}
|
||||
|
||||
return StashService.genderToString(retEnum);
|
||||
return genderToString(retEnum);
|
||||
}
|
||||
|
||||
function updatePerformerEditStateFromScraper(
|
||||
@@ -142,10 +149,11 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
) {
|
||||
const imageStr = (state as GQL.ScrapedPerformerDataFragment).image;
|
||||
setImage(imageStr ?? undefined);
|
||||
if (onImageChange) {
|
||||
onImageChange(imageStr!);
|
||||
}
|
||||
}
|
||||
|
||||
function onImageLoad(this: FileReader) {
|
||||
setImage(this.result as string);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -153,14 +161,12 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
updatePerformerEditState(performer);
|
||||
}, [performer]);
|
||||
|
||||
function onImageLoad(this: FileReader) {
|
||||
setImage(this.result as string);
|
||||
useEffect(() => {
|
||||
if (onImageChange) {
|
||||
onImageChange(this.result as string);
|
||||
onImageChange(image);
|
||||
}
|
||||
}
|
||||
|
||||
if (isEditing) ImageUtils.usePasteImage(onImageLoad);
|
||||
return () => onImageChange?.();
|
||||
}, [image, onImageChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const newQueryableScrapers = (
|
||||
@@ -195,7 +201,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
twitter,
|
||||
instagram,
|
||||
image,
|
||||
gender: StashService.stringToGender(gender),
|
||||
gender: stringToGender(gender),
|
||||
};
|
||||
|
||||
if (!isNew) {
|
||||
@@ -225,7 +231,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
try {
|
||||
if (!scrapePerformerDetails || !isDisplayingScraperDialog) return;
|
||||
setIsLoading(true);
|
||||
const result = await StashService.queryScrapePerformer(
|
||||
const result = await queryScrapePerformer(
|
||||
isDisplayingScraperDialog.id,
|
||||
getQueryScraperPerformerInput()
|
||||
);
|
||||
@@ -242,7 +248,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
if (!url) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await StashService.queryScrapePerformerURL(url);
|
||||
const result = await queryScrapePerformerURL(url);
|
||||
if (!result.data || !result.data.scrapePerformerURL) {
|
||||
return;
|
||||
}
|
||||
@@ -442,7 +448,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
value: gender,
|
||||
isEditing: !!isEditing,
|
||||
onChange: (value: string) => setGender(value),
|
||||
selectOptions: [""].concat(StashService.getGenderStrings()),
|
||||
selectOptions: [""].concat(getGenderStrings()),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from "react-bootstrap";
|
||||
import React from "react";
|
||||
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";
|
||||
|
||||
interface IPerformerOperationsProps {
|
||||
@@ -18,7 +18,7 @@ export const PerformerOperationsPanel: React.FC<IPerformerOperationsProps> = ({
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await StashService.mutateMetadataAutoTag({ performers: [performer.id] });
|
||||
await mutateMetadataAutoTag({ performers: [performer.id] });
|
||||
Toast.success({ content: "Started auto tagging" });
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from "lodash";
|
||||
import React from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
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 { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
@@ -33,7 +33,7 @@ export const PerformerList: React.FC = () => {
|
||||
const filterCopy = _.cloneDeep(filter);
|
||||
filterCopy.itemsPerPage = 1;
|
||||
filterCopy.currentPage = index + 1;
|
||||
const singleResult = await StashService.queryFindPerformers(filterCopy);
|
||||
const singleResult = await queryFindPerformers(filterCopy);
|
||||
if (
|
||||
singleResult &&
|
||||
singleResult.data &&
|
||||
|
||||
@@ -133,7 +133,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
id="filename-pattern"
|
||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setPattern(e.currentTarget.value)
|
||||
}
|
||||
value={pattern}
|
||||
@@ -162,7 +162,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
||||
<InputGroup className="col-8">
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setIgnoreWords(e.currentTarget.value)
|
||||
}
|
||||
value={ignoreWords}
|
||||
@@ -181,7 +181,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
||||
<InputGroup className="col-8">
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setWhitespaceCharacters(e.currentTarget.value)
|
||||
}
|
||||
value={whitespaceCharacters}
|
||||
@@ -236,8 +236,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
||||
</Button>
|
||||
<Form.Control
|
||||
as="select"
|
||||
options={PAGE_SIZE_OPTIONS}
|
||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
props.onPageSizeChanged(parseInt(e.currentTarget.value, 10))
|
||||
}
|
||||
defaultValue={props.input.pageSize}
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
import React, { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { Button, Card, Form, Table } from "react-bootstrap";
|
||||
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 { LoadingIndicator } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
@@ -56,7 +59,7 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [updateScenes] = StashService.useScenesUpdate(getScenesUpdateData());
|
||||
const [updateScenes] = useScenesUpdate(getScenesUpdateData());
|
||||
|
||||
useEffect(() => {
|
||||
prevParserInputRef.current = parserInput;
|
||||
@@ -124,7 +127,7 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
capitalizeTitle: parserInput.capitalizeTitle,
|
||||
};
|
||||
|
||||
StashService.queryParseSceneFilenames(parserFilter, parserInputData)
|
||||
queryParseSceneFilenames(parserFilter, parserInputData)
|
||||
.then((response) => {
|
||||
const result = response.data.parseSceneFilenames;
|
||||
if (result) {
|
||||
|
||||
@@ -133,7 +133,7 @@ function SceneParserStringField(props: ISceneParserFieldProps<string>) {
|
||||
readOnly={!props.parserResult.isSet}
|
||||
className={props.className}
|
||||
value={props.parserResult.value || ""}
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
maybeValueChanged(event.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
@@ -177,7 +177,7 @@ function SceneParserRatingField(
|
||||
className={props.className}
|
||||
disabled={!props.parserResult.isSet}
|
||||
value={props.parserResult.value?.toString()}
|
||||
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
|
||||
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
maybeValueChanged(
|
||||
event.currentTarget.value === ""
|
||||
? undefined
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import ReactJWPlayer from "react-jw-player";
|
||||
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 { ScenePlayerScrubber } from "./ScenePlayerScrubber";
|
||||
|
||||
@@ -210,7 +210,7 @@ export class ScenePlayerImpl extends React.Component<
|
||||
export const ScenePlayer: React.FC<IScenePlayerProps> = (
|
||||
props: IScenePlayerProps
|
||||
) => {
|
||||
const config = StashService.useConfiguration();
|
||||
const config = useConfiguration();
|
||||
|
||||
return (
|
||||
<ScenePlayerImpl
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Button, ButtonGroup, Card, Form } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import cx from "classnames";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { VideoHoverHook } from "src/hooks";
|
||||
import { useConfiguration } from "src/core/StashService";
|
||||
import { useVideoHover } from "src/hooks";
|
||||
import { Icon, TagLink, HoverPopover, SweatDrops } from "src/components/Shared";
|
||||
import { TextUtils } from "src/utils";
|
||||
|
||||
@@ -19,11 +19,11 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
||||
props: ISceneCardProps
|
||||
) => {
|
||||
const [previewPath, setPreviewPath] = useState<string>();
|
||||
const videoHoverHook = VideoHoverHook.useVideoHover({
|
||||
const hoverHandler = useVideoHover({
|
||||
resetOnMouseLeave: false,
|
||||
});
|
||||
|
||||
const config = StashService.useConfiguration();
|
||||
const config = useConfiguration();
|
||||
const showStudioAsText =
|
||||
config?.data?.configuration.interface.showStudioAsText ?? false;
|
||||
|
||||
@@ -219,10 +219,10 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
||||
if (!previewPath || previewPath === "") {
|
||||
setPreviewPath(props.scene.paths.preview || "");
|
||||
}
|
||||
VideoHoverHook.onMouseEnter(videoHoverHook);
|
||||
hoverHandler.onMouseEnter();
|
||||
}
|
||||
function onMouseLeave() {
|
||||
VideoHoverHook.onMouseLeave(videoHoverHook);
|
||||
hoverHandler.onMouseLeave();
|
||||
setPreviewPath("");
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
||||
loop
|
||||
className={cx("scene-card-video", { portrait: isPortrait() })}
|
||||
poster={props.scene.paths.screenshot || ""}
|
||||
ref={videoHoverHook.videoEl}
|
||||
ref={hoverHandler.videoEl}
|
||||
>
|
||||
{previewPath ? <source src={previewPath} /> : ""}
|
||||
</video>
|
||||
|
||||
@@ -3,7 +3,12 @@ import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useLocation, useHistory } from "react-router-dom";
|
||||
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 { LoadingIndicator } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
@@ -24,11 +29,11 @@ export const Scene: React.FC = () => {
|
||||
const Toast = useToast();
|
||||
const [timestamp, setTimestamp] = useState<number>(getInitialTimestamp());
|
||||
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 [incrementO] = StashService.useSceneIncrementO(scene?.id ?? "0");
|
||||
const [decrementO] = StashService.useSceneDecrementO(scene?.id ?? "0");
|
||||
const [resetO] = StashService.useSceneResetO(scene?.id ?? "0");
|
||||
const [incrementO] = useSceneIncrementO(scene?.id ?? "0");
|
||||
const [decrementO] = useSceneDecrementO(scene?.id ?? "0");
|
||||
const [resetO] = useSceneResetO(scene?.id ?? "0");
|
||||
|
||||
const queryParams = queryString.parse(location.search);
|
||||
const autoplay = queryParams?.autoplay === "true";
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Dropdown, DropdownButton, Form, Table } from "react-bootstrap";
|
||||
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 {
|
||||
PerformerSelect,
|
||||
TagSelect,
|
||||
@@ -42,7 +48,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
const [tagIds, setTagIds] = useState<string[]>();
|
||||
const [coverImage, setCoverImage] = useState<string>();
|
||||
|
||||
const Scrapers = StashService.useListSceneScrapers();
|
||||
const Scrapers = useListSceneScrapers();
|
||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
||||
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
@@ -54,8 +60,8 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const [updateScene] = StashService.useSceneUpdate(getSceneInput());
|
||||
const [deleteScene] = StashService.useSceneDestroy(getSceneDeleteInput());
|
||||
const [updateScene] = useSceneUpdate(getSceneInput());
|
||||
const [deleteScene] = useSceneDestroy(getSceneDeleteInput());
|
||||
|
||||
useEffect(() => {
|
||||
const newQueryableScrapers = (
|
||||
@@ -127,7 +133,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
setIsLoading(false);
|
||||
}, [props.scene]);
|
||||
|
||||
ImageUtils.usePasteImage(onImageLoad);
|
||||
ImageUtils.usePasteImage(onImageLoad, true);
|
||||
|
||||
function getSceneInput(): GQL.SceneUpdateInput {
|
||||
return {
|
||||
@@ -252,10 +258,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
async function onScrapeClicked(scraper: GQL.Scraper) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await StashService.queryScrapeScene(
|
||||
scraper.id,
|
||||
getSceneInput()
|
||||
);
|
||||
const result = await queryScrapeScene(scraper.id, getSceneInput());
|
||||
if (!result.data || !result.data.scrapeScene) {
|
||||
return;
|
||||
}
|
||||
@@ -364,7 +367,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
}
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await StashService.queryScrapeSceneURL(url);
|
||||
const result = await queryScrapeSceneURL(url);
|
||||
if (!result.data || !result.data.scrapeSceneURL) {
|
||||
return;
|
||||
}
|
||||
@@ -404,7 +407,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
<td>URL</td>
|
||||
<td>
|
||||
<Form.Control
|
||||
onChange={(newValue: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(newValue: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setUrl(newValue.currentTarget.value)
|
||||
}
|
||||
value={url}
|
||||
@@ -494,7 +497,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="scene-description text-input"
|
||||
onChange={(newValue: React.FormEvent<HTMLTextAreaElement>) =>
|
||||
onChange={(newValue: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setDetails(newValue.currentTarget.value)
|
||||
}
|
||||
value={details}
|
||||
|
||||
@@ -2,7 +2,11 @@ import React from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { Field, FieldProps, Form as FormikForm, Formik } from "formik";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import {
|
||||
useSceneMarkerCreate,
|
||||
useSceneMarkerUpdate,
|
||||
useSceneMarkerDestroy,
|
||||
} from "src/core/StashService";
|
||||
import {
|
||||
DurationInput,
|
||||
TagSelect,
|
||||
@@ -29,9 +33,9 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
||||
editingMarker,
|
||||
onClose,
|
||||
}) => {
|
||||
const [sceneMarkerCreate] = StashService.useSceneMarkerCreate();
|
||||
const [sceneMarkerUpdate] = StashService.useSceneMarkerUpdate();
|
||||
const [sceneMarkerDestroy] = StashService.useSceneMarkerDestroy();
|
||||
const [sceneMarkerCreate] = useSceneMarkerCreate();
|
||||
const [sceneMarkerUpdate] = useSceneMarkerUpdate();
|
||||
const [sceneMarkerDestroy] = useSceneMarkerDestroy();
|
||||
const Toast = useToast();
|
||||
|
||||
const onSubmit = (values: IFormFields) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
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";
|
||||
|
||||
type ValidTypes = GQL.SlimMovieDataFragment;
|
||||
@@ -15,7 +15,7 @@ export interface IProps {
|
||||
export const SceneMovieTable: React.FunctionComponent<IProps> = (
|
||||
props: IProps
|
||||
) => {
|
||||
const { data } = StashService.useAllMoviesForFilter();
|
||||
const { data } = useAllMoviesForFilter();
|
||||
|
||||
const items = !!data && !!data.allMoviesSlim ? data.allMoviesSlim : [];
|
||||
let itemsFilter: ValidTypes[] = [];
|
||||
@@ -49,7 +49,7 @@ export const SceneMovieTable: React.FunctionComponent<IProps> = (
|
||||
as="select"
|
||||
className="input-control"
|
||||
value={storeIdx[index] ? storeIdx[index]?.toString() : ""}
|
||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
updateFieldChanged(
|
||||
item.id,
|
||||
Number.parseInt(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from "react-bootstrap";
|
||||
import React, { FunctionComponent } from "react";
|
||||
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 { JWUtils } from "src/utils";
|
||||
|
||||
@@ -13,7 +13,7 @@ export const SceneOperationsPanel: FunctionComponent<IOperationsPanelProps> = (
|
||||
props: IOperationsPanelProps
|
||||
) => {
|
||||
const Toast = useToast();
|
||||
const [generateScreenshot] = StashService.useSceneGenerateScreenshot();
|
||||
const [generateScreenshot] = useSceneGenerateScreenshot();
|
||||
|
||||
async function onGenerateScreenshot(at?: number) {
|
||||
await generateScreenshot({
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
FindScenesQueryResult,
|
||||
SlimSceneDataFragment,
|
||||
} from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { queryFindScenes } from "src/core/StashService";
|
||||
import { useScenesList } from "src/hooks";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
@@ -52,7 +52,7 @@ export const SceneList: React.FC<ISceneList> = ({
|
||||
const filterCopy = _.cloneDeep(filter);
|
||||
filterCopy.itemsPerPage = 1;
|
||||
filterCopy.currentPage = index + 1;
|
||||
const singleResult = await StashService.queryFindScenes(filterCopy);
|
||||
const singleResult = await queryFindScenes(filterCopy);
|
||||
if (
|
||||
singleResult &&
|
||||
singleResult.data &&
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from "lodash";
|
||||
import React from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
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 { useSceneMarkersList } from "src/hooks";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
@@ -35,7 +35,7 @@ export const SceneMarkerList: React.FC = () => {
|
||||
const filterCopy = _.cloneDeep(filter);
|
||||
filterCopy.itemsPerPage = 1;
|
||||
filterCopy.currentPage = index + 1;
|
||||
const singleResult = await StashService.queryFindSceneMarkers(filterCopy);
|
||||
const singleResult = await queryFindSceneMarkers(filterCopy);
|
||||
if (singleResult?.data?.findSceneMarkers?.scene_markers?.length === 1) {
|
||||
// navigate to the scene player page
|
||||
const url = NavUtils.makeSceneMarkerUrl(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import _ from "lodash";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { useBulkSceneUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StudioSelect, LoadingIndicator } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
@@ -27,7 +27,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
|
||||
);
|
||||
const [tagIds, setTagIds] = useState<string[]>();
|
||||
|
||||
const [updateScenes] = StashService.useBulkSceneUpdate(getSceneInput());
|
||||
const [updateScenes] = useBulkSceneUpdate(getSceneInput());
|
||||
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -318,7 +318,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
|
||||
as="select"
|
||||
className="btn-secondary"
|
||||
value={rating}
|
||||
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
|
||||
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
setRating(event.currentTarget.value)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from "react";
|
||||
import { Button, Table } from "react-bootstrap";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { useVersion, useLatestVersion } from "src/core/StashService";
|
||||
|
||||
export const SettingsAboutPanel: React.FC = () => {
|
||||
const { data, error, loading } = StashService.useVersion();
|
||||
const { data, error, loading } = useVersion();
|
||||
const {
|
||||
data: dataLatest,
|
||||
error: errorLatest,
|
||||
loading: loadingLatest,
|
||||
refetch,
|
||||
networkStatus,
|
||||
} = StashService.useLatestVersion();
|
||||
} = useLatestVersion();
|
||||
|
||||
function maybeRenderTag() {
|
||||
if (!data || !data.version || !data.version.version) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Form, InputGroup } from "react-bootstrap";
|
||||
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 { Icon, LoadingIndicator } from "src/components/Shared";
|
||||
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
|
||||
@@ -36,9 +36,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
undefined
|
||||
);
|
||||
|
||||
const { data, error, loading } = StashService.useConfiguration();
|
||||
const { data, error, loading } = useConfiguration();
|
||||
|
||||
const [updateGeneralConfig] = StashService.useConfigureGeneral({
|
||||
const [updateGeneralConfig] = useConfigureGeneral({
|
||||
stashes,
|
||||
databasePath,
|
||||
generatedPath,
|
||||
@@ -189,7 +189,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
defaultValue={databasePath}
|
||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setDatabasePath(e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
@@ -203,7 +203,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
defaultValue={generatedPath}
|
||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setGeneratedPath(e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
@@ -222,7 +222,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
value={regexp}
|
||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
excludeRegexChanged(i, e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
@@ -262,7 +262,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<Form.Control
|
||||
className="col col-sm-6 input-control"
|
||||
as="select"
|
||||
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
|
||||
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
setMaxTranscodeSize(translateQuality(event.currentTarget.value))
|
||||
}
|
||||
value={resolutionToString(maxTranscodeSize)}
|
||||
@@ -282,7 +282,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<Form.Control
|
||||
className="col col-sm-6 input-control"
|
||||
as="select"
|
||||
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
|
||||
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
setMaxStreamingTranscodeSize(
|
||||
translateQuality(event.currentTarget.value)
|
||||
)
|
||||
@@ -332,7 +332,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
defaultValue={scraperUserAgent}
|
||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setScraperUserAgent(e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
@@ -425,7 +425,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<Form.Control
|
||||
className="col col-sm-6 input-control"
|
||||
as="select"
|
||||
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
|
||||
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
setLogLevel(event.currentTarget.value)
|
||||
}
|
||||
value={logLevel}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
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";
|
||||
|
||||
export const SettingsInterfacePanel: React.FC = () => {
|
||||
const Toast = useToast();
|
||||
const { data: config, error, loading } = StashService.useConfiguration();
|
||||
const { data: config, error, loading } = useConfiguration();
|
||||
const [soundOnPreview, setSoundOnPreview] = useState<boolean>(true);
|
||||
const [wallShowTitle, setWallShowTitle] = useState<boolean>(true);
|
||||
const [maximumLoopDuration, setMaximumLoopDuration] = useState<number>(0);
|
||||
@@ -16,7 +16,7 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
const [cssEnabled, setCSSEnabled] = useState<boolean>(false);
|
||||
const [language, setLanguage] = useState<string>("en");
|
||||
|
||||
const [updateInterfaceConfig] = StashService.useConfigureInterface({
|
||||
const [updateInterfaceConfig] = useConfigureInterface({
|
||||
soundOnPreview,
|
||||
wallShowTitle,
|
||||
maximumLoopDuration,
|
||||
@@ -62,7 +62,7 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
as="select"
|
||||
className="col-4 input-control"
|
||||
value={language}
|
||||
onChange={(e: React.FormEvent<HTMLSelectElement>) =>
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
setLanguage(e.currentTarget.value)
|
||||
}
|
||||
>
|
||||
@@ -142,12 +142,12 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
value={css}
|
||||
onChange={(e: React.FormEvent<HTMLTextAreaElement>) =>
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setCSS(e.currentTarget.value)
|
||||
}
|
||||
rows={16}
|
||||
className="col col-sm-6 text-input code"
|
||||
></Form.Control>
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Page must be reloaded for changes to take effect.
|
||||
</Form.Text>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useReducer, useState } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
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 pad(val: number) {
|
||||
@@ -74,8 +74,8 @@ const logReducer = (existingEntries: LogEntry[], newEntries: LogEntry[]) => [
|
||||
];
|
||||
|
||||
export const SettingsLogsPanel: React.FC = () => {
|
||||
const { data, error } = StashService.useLoggingSubscribe();
|
||||
const { data: existingData } = StashService.useLogs();
|
||||
const { data, error } = useLoggingSubscribe();
|
||||
const { data: existingData } = useLogs();
|
||||
const [currentData, dispatchLogUpdate] = useReducer(logReducer, []);
|
||||
const [logLevel, setLogLevel] = useState<string>("Info");
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { mutateMetadataGenerate } from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
|
||||
export const GenerateButton: React.FC = () => {
|
||||
@@ -12,7 +12,7 @@ export const GenerateButton: React.FC = () => {
|
||||
|
||||
async function onGenerate() {
|
||||
try {
|
||||
await StashService.mutateMetadataGenerate({
|
||||
await mutateMetadataGenerate({
|
||||
sprites,
|
||||
previews,
|
||||
markers,
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button, Form, ProgressBar } from "react-bootstrap";
|
||||
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 { Modal } from "src/components/Shared";
|
||||
import { GenerateButton } from "./GenerateButton";
|
||||
@@ -18,8 +27,8 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
const [autoTagStudios, setAutoTagStudios] = useState<boolean>(true);
|
||||
const [autoTagTags, setAutoTagTags] = useState<boolean>(true);
|
||||
|
||||
const jobStatus = StashService.useJobStatus();
|
||||
const metadataUpdate = StashService.useMetadataUpdate();
|
||||
const jobStatus = useJobStatus();
|
||||
const metadataUpdate = useMetadataUpdate();
|
||||
|
||||
function statusToText(s: string) {
|
||||
switch (s) {
|
||||
@@ -68,7 +77,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
|
||||
function onImport() {
|
||||
setIsImportAlertOpen(false);
|
||||
StashService.mutateMetadataImport().then(() => {
|
||||
mutateMetadataImport().then(() => {
|
||||
jobStatus.refetch();
|
||||
});
|
||||
}
|
||||
@@ -91,7 +100,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
|
||||
function onClean() {
|
||||
setIsCleanAlertOpen(false);
|
||||
StashService.mutateMetadataClean().then(() => {
|
||||
mutateMetadataClean().then(() => {
|
||||
jobStatus.refetch();
|
||||
});
|
||||
}
|
||||
@@ -115,7 +124,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
|
||||
async function onScan() {
|
||||
try {
|
||||
await StashService.mutateMetadataScan({ useFileMetadata });
|
||||
await mutateMetadataScan({ useFileMetadata });
|
||||
Toast.success({ content: "Started scan" });
|
||||
jobStatus.refetch();
|
||||
} catch (e) {
|
||||
@@ -134,7 +143,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
|
||||
async function onAutoTag() {
|
||||
try {
|
||||
await StashService.mutateMetadataAutoTag(getAutoTagInput());
|
||||
await mutateMetadataAutoTag(getAutoTagInput());
|
||||
Toast.success({ content: "Started auto tagging" });
|
||||
jobStatus.refetch();
|
||||
} catch (e) {
|
||||
@@ -152,9 +161,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
<Button
|
||||
id="stop"
|
||||
variant="danger"
|
||||
onClick={() =>
|
||||
StashService.mutateStopJob().then(() => jobStatus.refetch())
|
||||
}
|
||||
onClick={() => mutateStopJob().then(() => jobStatus.refetch())}
|
||||
>
|
||||
Stop
|
||||
</Button>
|
||||
@@ -277,7 +284,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
onClick={() =>
|
||||
StashService.mutateMetadataExport().then(() => {
|
||||
mutateMetadataExport().then(() => {
|
||||
jobStatus.refetch();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
||||
className="duration-control text-input"
|
||||
disabled={props.disabled}
|
||||
value={value}
|
||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setValue(e.currentTarget.value)
|
||||
}
|
||||
onBlur={() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, InputGroup, Form, Modal } from "react-bootstrap";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { useDirectories } from "src/core/StashService";
|
||||
|
||||
interface IProps {
|
||||
directories: string[];
|
||||
@@ -12,9 +12,7 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
|
||||
const [currentDirectory, setCurrentDirectory] = useState<string>("");
|
||||
const [isDisplayingDialog, setIsDisplayingDialog] = useState<boolean>(false);
|
||||
const [selectedDirectories, setSelectedDirectories] = useState<string[]>([]);
|
||||
const { data, error, loading } = StashService.useDirectories(
|
||||
currentDirectory
|
||||
);
|
||||
const { data, error, loading } = useDirectories(currentDirectory);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedDirectories(props.directories);
|
||||
@@ -51,7 +49,7 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
|
||||
<InputGroup>
|
||||
<Form.Control
|
||||
placeholder="File path"
|
||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setCurrentDirectory(e.currentTarget.value)
|
||||
}
|
||||
defaultValue={currentDirectory}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button, Form } from "react-bootstrap";
|
||||
interface IImageInput {
|
||||
isEditing: boolean;
|
||||
text?: string;
|
||||
onImageChange: (event: React.FormEvent<HTMLInputElement>) => void;
|
||||
onImageChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
acceptSVG?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import React, { useState, useCallback, CSSProperties } from "react";
|
||||
import React, { useState, CSSProperties } from "react";
|
||||
import Select, { ValueType } from "react-select";
|
||||
import CreatableSelect from "react-select/creatable";
|
||||
import { debounce } from "lodash";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import {
|
||||
useAllTagsForFilter,
|
||||
useAllMoviesForFilter,
|
||||
useAllStudiosForFilter,
|
||||
useAllPerformersForFilter,
|
||||
useMarkerStrings,
|
||||
useScrapePerformerList,
|
||||
useValidGalleriesForScene,
|
||||
useTagCreate,
|
||||
} from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
|
||||
type ValidTypes =
|
||||
@@ -63,9 +72,7 @@ const getSelectedValues = (selectedItems: ValueType<Option>) =>
|
||||
: [];
|
||||
|
||||
export const SceneGallerySelect: React.FC<ISceneGallerySelect> = (props) => {
|
||||
const { data, loading } = StashService.useValidGalleriesForScene(
|
||||
props.sceneId
|
||||
);
|
||||
const { data, loading } = useValidGalleriesForScene(props.sceneId);
|
||||
const galleries = data?.validGalleriesForScene ?? [];
|
||||
const items = (galleries.length > 0
|
||||
? [{ path: "None", id: "0" }, ...galleries]
|
||||
@@ -103,10 +110,7 @@ export const ScrapePerformerSuggest: React.FC<IScrapePerformerSuggestProps> = (
|
||||
props
|
||||
) => {
|
||||
const [query, setQuery] = React.useState<string>("");
|
||||
const { data, loading } = StashService.useScrapePerformerList(
|
||||
props.scraperId,
|
||||
query
|
||||
);
|
||||
const { data, loading } = useScrapePerformerList(props.scraperId, query);
|
||||
|
||||
const performers = data?.scrapePerformerList ?? [];
|
||||
const items = performers.map((item) => ({
|
||||
@@ -114,12 +118,10 @@ export const ScrapePerformerSuggest: React.FC<IScrapePerformerSuggestProps> = (
|
||||
value: item.name ?? "",
|
||||
}));
|
||||
|
||||
const onInputChange = useCallback(
|
||||
debounce((input: string) => {
|
||||
const onInputChange = debounce((input: string) => {
|
||||
setQuery(input);
|
||||
}, 500),
|
||||
[]
|
||||
);
|
||||
}, 500);
|
||||
|
||||
const onChange = (selectedItems: ValueType<Option>) => {
|
||||
const name = getSelectedValues(selectedItems)[0];
|
||||
const performer = performers.find((p) => p.name === name);
|
||||
@@ -145,7 +147,7 @@ interface IMarkerSuggestProps {
|
||||
onChange: (title: string) => void;
|
||||
}
|
||||
export const MarkerTitleSuggest: React.FC<IMarkerSuggestProps> = (props) => {
|
||||
const { data, loading } = StashService.useMarkerStrings();
|
||||
const { data, loading } = useMarkerStrings();
|
||||
const suggestions = data?.markerStrings ?? [];
|
||||
|
||||
const onChange = (selectedItems: ValueType<Option>) =>
|
||||
@@ -182,7 +184,7 @@ export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) =>
|
||||
);
|
||||
|
||||
export const PerformerSelect: React.FC<IFilterProps> = (props) => {
|
||||
const { data, loading } = StashService.useAllPerformersForFilter();
|
||||
const { data, loading } = useAllPerformersForFilter();
|
||||
|
||||
const normalizedData = data?.allPerformersSlim ?? [];
|
||||
const items: Option[] = normalizedData.map((item) => ({
|
||||
@@ -215,7 +217,7 @@ export const PerformerSelect: React.FC<IFilterProps> = (props) => {
|
||||
};
|
||||
|
||||
export const StudioSelect: React.FC<IFilterProps> = (props) => {
|
||||
const { data, loading } = StashService.useAllStudiosForFilter();
|
||||
const { data, loading } = useAllStudiosForFilter();
|
||||
|
||||
const normalizedData = data?.allStudiosSlim ?? [];
|
||||
|
||||
@@ -253,7 +255,7 @@ export const StudioSelect: React.FC<IFilterProps> = (props) => {
|
||||
};
|
||||
|
||||
export const MovieSelect: React.FC<IFilterProps> = (props) => {
|
||||
const { data, loading } = StashService.useAllMoviesForFilter();
|
||||
const { data, loading } = useAllMoviesForFilter();
|
||||
|
||||
const normalizedData = data?.allMoviesSlim ?? [];
|
||||
|
||||
@@ -293,8 +295,8 @@ export const MovieSelect: React.FC<IFilterProps> = (props) => {
|
||||
export const TagSelect: React.FC<IFilterProps> = (props) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>(props.ids ?? []);
|
||||
const { data, loading: dataLoading } = StashService.useAllTagsForFilter();
|
||||
const [createTag] = StashService.useTagCreate({ name: "" });
|
||||
const { data, loading: dataLoading } = useAllTagsForFilter();
|
||||
const [createTag] = useTagCreate({ name: "" });
|
||||
const Toast = useToast();
|
||||
const placeholder = props.noSelectionString ?? "Select tags...";
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { useStats } from "src/core/StashService";
|
||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
|
||||
export const Stats: React.FC = () => {
|
||||
const { data, error, loading } = StashService.useStats();
|
||||
const { data, error, loading } = useStats();
|
||||
|
||||
if (loading || !data) return <LoadingIndicator />;
|
||||
|
||||
|
||||
@@ -6,7 +6,13 @@ import { useParams, useHistory } from "react-router-dom";
|
||||
import cx from "classnames";
|
||||
|
||||
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 {
|
||||
DetailsEditNavbar,
|
||||
@@ -35,14 +41,14 @@ export const Studio: React.FC = () => {
|
||||
const [studio, setStudio] = useState<Partial<GQL.StudioDataFragment>>({});
|
||||
const [imagePreview, setImagePreview] = useState<string>();
|
||||
|
||||
const { data, error, loading } = StashService.useFindStudio(id);
|
||||
const [updateStudio] = StashService.useStudioUpdate(
|
||||
const { data, error, loading } = useFindStudio(id);
|
||||
const [updateStudio] = useStudioUpdate(
|
||||
getStudioInput() as GQL.StudioUpdateInput
|
||||
);
|
||||
const [createStudio] = StashService.useStudioCreate(
|
||||
const [createStudio] = useStudioCreate(
|
||||
getStudioInput() as GQL.StudioCreateInput
|
||||
);
|
||||
const [deleteStudio] = StashService.useStudioDestroy(
|
||||
const [deleteStudio] = useStudioDestroy(
|
||||
getStudioInput() as GQL.StudioDestroyInput
|
||||
);
|
||||
|
||||
@@ -72,7 +78,7 @@ export const Studio: React.FC = () => {
|
||||
setImage(this.result as string);
|
||||
}
|
||||
|
||||
ImageUtils.usePasteImage(onImageLoad);
|
||||
ImageUtils.usePasteImage(onImageLoad, isEditing);
|
||||
|
||||
if (!isNew && !isEditing) {
|
||||
if (!data?.findStudio || loading) return <LoadingIndicator />;
|
||||
@@ -115,7 +121,7 @@ export const Studio: React.FC = () => {
|
||||
async function onAutoTag() {
|
||||
if (!studio.id) return;
|
||||
try {
|
||||
await StashService.mutateMetadataAutoTag({ studios: [studio.id] });
|
||||
await mutateMetadataAutoTag({ studios: [studio.id] });
|
||||
Toast.success({ content: "Started auto tagging" });
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
|
||||
@@ -2,7 +2,13 @@ import React, { useState } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
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 { Icon, Modal, LoadingIndicator } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
@@ -18,16 +24,10 @@ export const TagList: React.FC = () => {
|
||||
GQL.TagDataFragment
|
||||
> | null>(null);
|
||||
|
||||
const { data, error } = StashService.useAllTags();
|
||||
const [updateTag] = StashService.useTagUpdate(
|
||||
getTagInput() as GQL.TagUpdateInput
|
||||
);
|
||||
const [createTag] = StashService.useTagCreate(
|
||||
getTagInput() as GQL.TagCreateInput
|
||||
);
|
||||
const [deleteTag] = StashService.useTagDestroy(
|
||||
getDeleteTagInput() as GQL.TagDestroyInput
|
||||
);
|
||||
const { data, error } = useAllTags();
|
||||
const [updateTag] = useTagUpdate(getTagInput() as GQL.TagUpdateInput);
|
||||
const [createTag] = useTagCreate(getTagInput() as GQL.TagCreateInput);
|
||||
const [deleteTag] = useTagDestroy(getDeleteTagInput() as GQL.TagDestroyInput);
|
||||
|
||||
function getTagInput() {
|
||||
const tagInput: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = { name };
|
||||
@@ -62,7 +62,7 @@ export const TagList: React.FC = () => {
|
||||
async function onAutoTag(tag: GQL.TagDataFragment) {
|
||||
if (!tag) return;
|
||||
try {
|
||||
await StashService.mutateMetadataAutoTag({ tags: [tag.id] });
|
||||
await mutateMetadataAutoTag({ tags: [tag.id] });
|
||||
Toast.success({ content: "Started auto tagging" });
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
@@ -160,7 +160,7 @@ export const TagList: React.FC = () => {
|
||||
<Form.Group controlId="tag-name">
|
||||
<Form.Label>Name</Form.Label>
|
||||
<Form.Control
|
||||
onChange={(newValue: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(newValue: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setName(newValue.currentTarget.value)
|
||||
}
|
||||
defaultValue={(editingTag && editingTag.name) || ""}
|
||||
|
||||
@@ -2,8 +2,8 @@ import _ from "lodash";
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { VideoHoverHook } from "src/hooks";
|
||||
import { useConfiguration } from "src/core/StashService";
|
||||
import { useVideoHover } from "src/hooks";
|
||||
import { TextUtils, NavUtils } from "src/utils";
|
||||
|
||||
interface IWallItemProps {
|
||||
@@ -22,15 +22,15 @@ export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
|
||||
const [screenshotPath, setScreenshotPath] = useState<string>("");
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const [tags, setTags] = useState<JSX.Element[]>([]);
|
||||
const config = StashService.useConfiguration();
|
||||
const videoHoverHook = VideoHoverHook.useVideoHover({
|
||||
const config = useConfiguration();
|
||||
const hoverHandler = useVideoHover({
|
||||
resetOnMouseLeave: true,
|
||||
});
|
||||
const showTextContainer =
|
||||
config.data?.configuration.interface.wallShowTitle ?? true;
|
||||
|
||||
function onMouseEnter() {
|
||||
VideoHoverHook.onMouseEnter(videoHoverHook);
|
||||
hoverHandler.onMouseEnter();
|
||||
if (!videoPath || videoPath === "") {
|
||||
if (props.sceneMarker) {
|
||||
setVideoPath(props.sceneMarker.stream || "");
|
||||
@@ -43,7 +43,7 @@ export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
|
||||
const debouncedOnMouseEnter = useRef(_.debounce(onMouseEnter, 500));
|
||||
|
||||
function onMouseLeave() {
|
||||
VideoHoverHook.onMouseLeave(videoHoverHook);
|
||||
hoverHandler.onMouseLeave();
|
||||
setVideoPath("");
|
||||
debouncedOnMouseEnter.current.cancel();
|
||||
props.onOverlay(false);
|
||||
@@ -111,7 +111,7 @@ export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
|
||||
}
|
||||
|
||||
const className = ["scene-wall-item-container"];
|
||||
if (videoHoverHook.isHovering.current) {
|
||||
if (hoverHandler.isHovering.current) {
|
||||
className.push("double-scale");
|
||||
}
|
||||
const style: React.CSSProperties = {};
|
||||
@@ -133,10 +133,10 @@ export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
|
||||
src={videoPath}
|
||||
poster={screenshotPath}
|
||||
className="scene-wall-video"
|
||||
style={videoHoverHook.isHovering.current ? {} : { display: "none" }}
|
||||
style={hoverHandler.isHovering.current ? {} : { display: "none" }}
|
||||
autoPlay
|
||||
loop
|
||||
ref={videoHoverHook.videoEl}
|
||||
ref={hoverHandler.videoEl}
|
||||
/>
|
||||
<img
|
||||
alt={title}
|
||||
|
||||
@@ -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 * as GQL from "./generated-graphql";
|
||||
|
||||
export class StashService {
|
||||
public static client: ApolloClient<NormalizedCacheObject>;
|
||||
private static cache: InMemoryCache;
|
||||
import { createClient } from "./createClient";
|
||||
|
||||
public static getPlatformURL(ws?: boolean) {
|
||||
const platformUrl = new URL(window.location.origin);
|
||||
const { client, cache } = createClient();
|
||||
|
||||
if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
|
||||
platformUrl.port = "9999"; // TODO: Hack. Development expects port 9999
|
||||
export const getClient = () => client;
|
||||
|
||||
if (process.env.REACT_APP_HTTPS === "true") {
|
||||
platformUrl.protocol = "https:";
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
// TODO: Invalidation should happen through apollo client, rather than rewriting cache directly
|
||||
const invalidateQueries = (queries: string[]) => {
|
||||
if (cache) {
|
||||
const keyMatchers = queries.map((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) => {
|
||||
if (
|
||||
keyMatchers.some((matcher) => {
|
||||
@@ -104,269 +27,176 @@ export class StashService {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static useFindGalleries(filter: ListFilterModel) {
|
||||
return GQL.useFindGalleriesQuery({
|
||||
export const useFindGalleries = (filter: ListFilterModel) =>
|
||||
GQL.useFindGalleriesQuery({
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public static useFindScenes(filter: ListFilterModel) {
|
||||
let sceneFilter = {};
|
||||
// if (!!filter && filter.criteriaFilterOpen) {
|
||||
sceneFilter = filter.makeSceneFilter();
|
||||
// }
|
||||
// if (filter.customCriteria) {
|
||||
// filter.customCriteria.forEach(criteria => {
|
||||
// scene_filter[criteria.key] = criteria.value;
|
||||
// });
|
||||
// }
|
||||
|
||||
return GQL.useFindScenesQuery({
|
||||
export const useFindScenes = (filter: ListFilterModel) =>
|
||||
GQL.useFindScenesQuery({
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
scene_filter: sceneFilter,
|
||||
scene_filter: filter.makeSceneFilter(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public static queryFindScenes(filter: ListFilterModel) {
|
||||
let sceneFilter = {};
|
||||
sceneFilter = filter.makeSceneFilter();
|
||||
|
||||
return StashService.client.query<GQL.FindScenesQuery>({
|
||||
export const queryFindScenes = (filter: ListFilterModel) =>
|
||||
client.query<GQL.FindScenesQuery>({
|
||||
query: GQL.FindScenesDocument,
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
scene_filter: sceneFilter,
|
||||
scene_filter: filter.makeSceneFilter(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public static useFindSceneMarkers(filter: ListFilterModel) {
|
||||
let sceneMarkerFilter = {};
|
||||
// if (!!filter && filter.criteriaFilterOpen) {
|
||||
sceneMarkerFilter = filter.makeSceneMarkerFilter();
|
||||
// }
|
||||
// if (filter.customCriteria) {
|
||||
// filter.customCriteria.forEach(criteria => {
|
||||
// scene_filter[criteria.key] = criteria.value;
|
||||
// });
|
||||
// }
|
||||
|
||||
return GQL.useFindSceneMarkersQuery({
|
||||
export const useFindSceneMarkers = (filter: ListFilterModel) =>
|
||||
GQL.useFindSceneMarkersQuery({
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
scene_marker_filter: sceneMarkerFilter,
|
||||
scene_marker_filter: filter.makeSceneMarkerFilter(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public static queryFindSceneMarkers(filter: ListFilterModel) {
|
||||
let sceneMarkerFilter = {};
|
||||
sceneMarkerFilter = filter.makeSceneMarkerFilter();
|
||||
|
||||
return StashService.client.query<GQL.FindSceneMarkersQuery>({
|
||||
export const queryFindSceneMarkers = (filter: ListFilterModel) =>
|
||||
client.query<GQL.FindSceneMarkersQuery>({
|
||||
query: GQL.FindSceneMarkersDocument,
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
scene_marker_filter: sceneMarkerFilter,
|
||||
scene_marker_filter: filter.makeSceneMarkerFilter(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public static useFindStudios(filter: ListFilterModel) {
|
||||
return GQL.useFindStudiosQuery({
|
||||
export const useFindStudios = (filter: ListFilterModel) =>
|
||||
GQL.useFindStudiosQuery({
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public static useFindMovies(filter: ListFilterModel) {
|
||||
const movieFilter = filter.makeMovieFilter();
|
||||
|
||||
return GQL.useFindMoviesQuery({
|
||||
export const useFindMovies = (filter: ListFilterModel) =>
|
||||
GQL.useFindMoviesQuery({
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
movie_filter: movieFilter,
|
||||
movie_filter: filter.makeMovieFilter(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public static useFindPerformers(filter: ListFilterModel) {
|
||||
let performerFilter = {};
|
||||
// if (!!filter && filter.criteriaFilterOpen) {
|
||||
performerFilter = filter.makePerformerFilter();
|
||||
// }
|
||||
// if (filter.customCriteria) {
|
||||
// filter.customCriteria.forEach(criteria => {
|
||||
// scene_filter[criteria.key] = criteria.value;
|
||||
// });
|
||||
// }
|
||||
|
||||
return GQL.useFindPerformersQuery({
|
||||
export const useFindPerformers = (filter: ListFilterModel) =>
|
||||
GQL.useFindPerformersQuery({
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
performer_filter: performerFilter,
|
||||
performer_filter: filter.makePerformerFilter(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public static queryFindPerformers(filter: ListFilterModel) {
|
||||
let performerFilter = {};
|
||||
performerFilter = filter.makePerformerFilter();
|
||||
|
||||
return StashService.client.query<GQL.FindPerformersQuery>({
|
||||
export const queryFindPerformers = (filter: ListFilterModel) =>
|
||||
client.query<GQL.FindPerformersQuery>({
|
||||
query: GQL.FindPerformersDocument,
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
performer_filter: performerFilter,
|
||||
performer_filter: filter.makePerformerFilter(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public static useFindGallery(id: string) {
|
||||
return GQL.useFindGalleryQuery({ variables: { id } });
|
||||
}
|
||||
public static useFindScene(id: string) {
|
||||
return GQL.useFindSceneQuery({ variables: { id } });
|
||||
}
|
||||
public static useFindPerformer(id: string) {
|
||||
export const useFindGallery = (id: string) =>
|
||||
GQL.useFindGalleryQuery({ variables: { id } });
|
||||
export const useFindScene = (id: string) =>
|
||||
GQL.useFindSceneQuery({ variables: { id } });
|
||||
export const useFindPerformer = (id: string) => {
|
||||
const skip = id === "new";
|
||||
return GQL.useFindPerformerQuery({ variables: { id }, skip });
|
||||
}
|
||||
public static useFindStudio(id: string) {
|
||||
};
|
||||
export const useFindStudio = (id: string) => {
|
||||
const skip = id === "new";
|
||||
return GQL.useFindStudioQuery({ variables: { id }, skip });
|
||||
}
|
||||
public static useFindMovie(id: string) {
|
||||
};
|
||||
export const useFindMovie = (id: string) => {
|
||||
const skip = id === "new";
|
||||
return GQL.useFindMovieQuery({ variables: { id }, skip });
|
||||
}
|
||||
};
|
||||
|
||||
// TODO - scene marker manipulation functions are handled differently
|
||||
private static sceneMarkerMutationImpactedQueries = [
|
||||
// TODO - scene marker manipulation functions are handled differently
|
||||
export const sceneMarkerMutationImpactedQueries = [
|
||||
"findSceneMarkers",
|
||||
"findScenes",
|
||||
"markerStrings",
|
||||
"sceneMarkerTags",
|
||||
];
|
||||
];
|
||||
|
||||
public static useSceneMarkerCreate() {
|
||||
return GQL.useSceneMarkerCreateMutation({ refetchQueries: ["FindScene"] });
|
||||
}
|
||||
public static useSceneMarkerUpdate() {
|
||||
return GQL.useSceneMarkerUpdateMutation({ refetchQueries: ["FindScene"] });
|
||||
}
|
||||
public static useSceneMarkerDestroy() {
|
||||
return GQL.useSceneMarkerDestroyMutation({ refetchQueries: ["FindScene"] });
|
||||
}
|
||||
export const useSceneMarkerCreate = () =>
|
||||
GQL.useSceneMarkerCreateMutation({ refetchQueries: ["FindScene"] });
|
||||
export const useSceneMarkerUpdate = () =>
|
||||
GQL.useSceneMarkerUpdateMutation({ refetchQueries: ["FindScene"] });
|
||||
export const useSceneMarkerDestroy = () =>
|
||||
GQL.useSceneMarkerDestroyMutation({ refetchQueries: ["FindScene"] });
|
||||
|
||||
public static useListPerformerScrapers() {
|
||||
return GQL.useListPerformerScrapersQuery();
|
||||
}
|
||||
public static useScrapePerformerList(scraperId: string, q: string) {
|
||||
return GQL.useScrapePerformerListQuery({
|
||||
export const useListPerformerScrapers = () =>
|
||||
GQL.useListPerformerScrapersQuery();
|
||||
export const useScrapePerformerList = (scraperId: string, q: string) =>
|
||||
GQL.useScrapePerformerListQuery({
|
||||
variables: { scraper_id: scraperId, query: q },
|
||||
skip: q === "",
|
||||
});
|
||||
}
|
||||
public static useScrapePerformer(
|
||||
export const useScrapePerformer = (
|
||||
scraperId: string,
|
||||
scrapedPerformer: GQL.ScrapedPerformerInput
|
||||
) {
|
||||
return GQL.useScrapePerformerQuery({
|
||||
) =>
|
||||
GQL.useScrapePerformerQuery({
|
||||
variables: { scraper_id: scraperId, scraped_performer: scrapedPerformer },
|
||||
});
|
||||
}
|
||||
|
||||
public static useListSceneScrapers() {
|
||||
return GQL.useListSceneScrapersQuery();
|
||||
}
|
||||
export const useListSceneScrapers = () => GQL.useListSceneScrapersQuery();
|
||||
|
||||
public static useScrapeFreeonesPerformers(q: string) {
|
||||
return GQL.useScrapeFreeonesPerformersQuery({ variables: { q } });
|
||||
}
|
||||
public static useMarkerStrings() {
|
||||
return GQL.useMarkerStringsQuery();
|
||||
}
|
||||
public static useAllTags() {
|
||||
return GQL.useAllTagsQuery();
|
||||
}
|
||||
public static useAllTagsForFilter() {
|
||||
return GQL.useAllTagsForFilterQuery();
|
||||
}
|
||||
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({
|
||||
export const useScrapeFreeonesPerformers = (q: string) =>
|
||||
GQL.useScrapeFreeonesPerformersQuery({ variables: { q } });
|
||||
export const useMarkerStrings = () => GQL.useMarkerStringsQuery();
|
||||
export const useAllTags = () => GQL.useAllTagsQuery();
|
||||
export const useAllTagsForFilter = () => GQL.useAllTagsForFilterQuery();
|
||||
export const useAllPerformersForFilter = () =>
|
||||
GQL.useAllPerformersForFilterQuery();
|
||||
export const useAllStudiosForFilter = () => GQL.useAllStudiosForFilterQuery();
|
||||
export const useAllMoviesForFilter = () => GQL.useAllMoviesForFilterQuery();
|
||||
export const useValidGalleriesForScene = (sceneId: string) =>
|
||||
GQL.useValidGalleriesForSceneQuery({
|
||||
variables: { scene_id: sceneId },
|
||||
});
|
||||
}
|
||||
public static useStats() {
|
||||
return GQL.useStatsQuery();
|
||||
}
|
||||
public static useVersion() {
|
||||
return GQL.useVersionQuery();
|
||||
}
|
||||
public static useLatestVersion() {
|
||||
return GQL.useLatestVersionQuery({
|
||||
export const useStats = () => GQL.useStatsQuery();
|
||||
export const useVersion = () => GQL.useVersionQuery();
|
||||
export const useLatestVersion = () =>
|
||||
GQL.useLatestVersionQuery({
|
||||
notifyOnNetworkStatusChange: true,
|
||||
errorPolicy: "ignore",
|
||||
});
|
||||
}
|
||||
|
||||
public static useConfiguration() {
|
||||
return GQL.useConfigurationQuery();
|
||||
}
|
||||
public static useDirectories(path?: string) {
|
||||
return GQL.useDirectoriesQuery({ variables: { path } });
|
||||
}
|
||||
export const useConfiguration = () => GQL.useConfigurationQuery();
|
||||
export const useDirectories = (path?: string) =>
|
||||
GQL.useDirectoriesQuery({ variables: { path } });
|
||||
|
||||
private static performerMutationImpactedQueries = [
|
||||
export const performerMutationImpactedQueries = [
|
||||
"findPerformers",
|
||||
"findScenes",
|
||||
"findSceneMarkers",
|
||||
"allPerformers",
|
||||
];
|
||||
];
|
||||
|
||||
public static usePerformerCreate() {
|
||||
return GQL.usePerformerCreateMutation({
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.performerMutationImpactedQueries
|
||||
),
|
||||
export const usePerformerCreate = () =>
|
||||
GQL.usePerformerCreateMutation({
|
||||
update: () => invalidateQueries(performerMutationImpactedQueries),
|
||||
});
|
||||
}
|
||||
public static usePerformerUpdate() {
|
||||
return GQL.usePerformerUpdateMutation({
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.performerMutationImpactedQueries
|
||||
),
|
||||
export const usePerformerUpdate = () =>
|
||||
GQL.usePerformerUpdateMutation({
|
||||
update: () => invalidateQueries(performerMutationImpactedQueries),
|
||||
});
|
||||
}
|
||||
public static usePerformerDestroy() {
|
||||
return GQL.usePerformerDestroyMutation({
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.performerMutationImpactedQueries
|
||||
),
|
||||
export const usePerformerDestroy = () =>
|
||||
GQL.usePerformerDestroyMutation({
|
||||
update: () => invalidateQueries(performerMutationImpactedQueries),
|
||||
});
|
||||
}
|
||||
|
||||
private static sceneMutationImpactedQueries = [
|
||||
export const sceneMutationImpactedQueries = [
|
||||
"findPerformers",
|
||||
"findScenes",
|
||||
"findSceneMarkers",
|
||||
@@ -374,378 +204,301 @@ export class StashService {
|
||||
"findMovies",
|
||||
"allTags",
|
||||
// TODO - add "findTags" when it is implemented
|
||||
];
|
||||
];
|
||||
|
||||
public static useSceneUpdate(input: GQL.SceneUpdateInput) {
|
||||
return GQL.useSceneUpdateMutation({
|
||||
export const useSceneUpdate = (input: GQL.SceneUpdateInput) =>
|
||||
GQL.useSceneUpdateMutation({
|
||||
variables: input,
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.sceneMutationImpactedQueries
|
||||
),
|
||||
update: () => invalidateQueries(sceneMutationImpactedQueries),
|
||||
refetchQueries: ["AllTagsForFilter"],
|
||||
});
|
||||
}
|
||||
|
||||
// remove findScenes for bulk scene update so that we don't lose
|
||||
// existing results
|
||||
private static sceneBulkMutationImpactedQueries = [
|
||||
// remove findScenes for bulk scene update so that we don't lose
|
||||
// existing results
|
||||
export const sceneBulkMutationImpactedQueries = [
|
||||
"findPerformers",
|
||||
"findSceneMarkers",
|
||||
"findStudios",
|
||||
"findMovies",
|
||||
"allTags",
|
||||
];
|
||||
];
|
||||
|
||||
public static useBulkSceneUpdate(input: GQL.BulkSceneUpdateInput) {
|
||||
return GQL.useBulkSceneUpdateMutation({
|
||||
export const useBulkSceneUpdate = (input: GQL.BulkSceneUpdateInput) =>
|
||||
GQL.useBulkSceneUpdateMutation({
|
||||
variables: input,
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.sceneBulkMutationImpactedQueries
|
||||
),
|
||||
update: () => invalidateQueries(sceneBulkMutationImpactedQueries),
|
||||
});
|
||||
}
|
||||
|
||||
public static useScenesUpdate(input: GQL.SceneUpdateInput[]) {
|
||||
return GQL.useScenesUpdateMutation({ variables: { input } });
|
||||
}
|
||||
export const useScenesUpdate = (input: GQL.SceneUpdateInput[]) =>
|
||||
GQL.useScenesUpdateMutation({ variables: { input } });
|
||||
|
||||
public static useSceneIncrementO(id: string) {
|
||||
return GQL.useSceneIncrementOMutation({
|
||||
export const useSceneIncrementO = (id: string) =>
|
||||
GQL.useSceneIncrementOMutation({
|
||||
variables: { id },
|
||||
});
|
||||
}
|
||||
|
||||
public static useSceneDecrementO(id: string) {
|
||||
return GQL.useSceneDecrementOMutation({
|
||||
export const useSceneDecrementO = (id: string) =>
|
||||
GQL.useSceneDecrementOMutation({
|
||||
variables: { id },
|
||||
});
|
||||
}
|
||||
|
||||
public static useSceneResetO(id: string) {
|
||||
return GQL.useSceneResetOMutation({
|
||||
export const useSceneResetO = (id: string) =>
|
||||
GQL.useSceneResetOMutation({
|
||||
variables: { id },
|
||||
});
|
||||
}
|
||||
|
||||
public static useSceneDestroy(input: GQL.SceneDestroyInput) {
|
||||
return GQL.useSceneDestroyMutation({
|
||||
export const useSceneDestroy = (input: GQL.SceneDestroyInput) =>
|
||||
GQL.useSceneDestroyMutation({
|
||||
variables: input,
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.sceneMutationImpactedQueries
|
||||
),
|
||||
update: () => invalidateQueries(sceneMutationImpactedQueries),
|
||||
});
|
||||
}
|
||||
|
||||
public static useSceneGenerateScreenshot() {
|
||||
return GQL.useSceneGenerateScreenshotMutation({
|
||||
update: () => StashService.invalidateQueries(["findScenes"]),
|
||||
export const useSceneGenerateScreenshot = () =>
|
||||
GQL.useSceneGenerateScreenshotMutation({
|
||||
update: () => invalidateQueries(["findScenes"]),
|
||||
});
|
||||
}
|
||||
|
||||
private static studioMutationImpactedQueries = [
|
||||
export const studioMutationImpactedQueries = [
|
||||
"findStudios",
|
||||
"findScenes",
|
||||
"allStudios",
|
||||
];
|
||||
];
|
||||
|
||||
public static useStudioCreate(input: GQL.StudioCreateInput) {
|
||||
return GQL.useStudioCreateMutation({
|
||||
export const useStudioCreate = (input: GQL.StudioCreateInput) =>
|
||||
GQL.useStudioCreateMutation({
|
||||
variables: input,
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.studioMutationImpactedQueries
|
||||
),
|
||||
update: () => invalidateQueries(studioMutationImpactedQueries),
|
||||
});
|
||||
}
|
||||
|
||||
public static useStudioUpdate(input: GQL.StudioUpdateInput) {
|
||||
return GQL.useStudioUpdateMutation({
|
||||
export const useStudioUpdate = (input: GQL.StudioUpdateInput) =>
|
||||
GQL.useStudioUpdateMutation({
|
||||
variables: input,
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.studioMutationImpactedQueries
|
||||
),
|
||||
update: () => invalidateQueries(studioMutationImpactedQueries),
|
||||
});
|
||||
}
|
||||
|
||||
public static useStudioDestroy(input: GQL.StudioDestroyInput) {
|
||||
return GQL.useStudioDestroyMutation({
|
||||
export const useStudioDestroy = (input: GQL.StudioDestroyInput) =>
|
||||
GQL.useStudioDestroyMutation({
|
||||
variables: input,
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.studioMutationImpactedQueries
|
||||
),
|
||||
update: () => invalidateQueries(studioMutationImpactedQueries),
|
||||
});
|
||||
}
|
||||
|
||||
private static movieMutationImpactedQueries = [
|
||||
export const movieMutationImpactedQueries = [
|
||||
"findMovies",
|
||||
"findScenes",
|
||||
"allMovies",
|
||||
];
|
||||
];
|
||||
|
||||
public static useMovieCreate(input: GQL.MovieCreateInput) {
|
||||
return GQL.useMovieCreateMutation({
|
||||
export const useMovieCreate = (input: GQL.MovieCreateInput) =>
|
||||
GQL.useMovieCreateMutation({
|
||||
variables: input,
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.movieMutationImpactedQueries
|
||||
),
|
||||
update: () => invalidateQueries(movieMutationImpactedQueries),
|
||||
});
|
||||
}
|
||||
|
||||
public static useMovieUpdate(input: GQL.MovieUpdateInput) {
|
||||
return GQL.useMovieUpdateMutation({
|
||||
export const useMovieUpdate = (input: GQL.MovieUpdateInput) =>
|
||||
GQL.useMovieUpdateMutation({
|
||||
variables: input,
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.movieMutationImpactedQueries
|
||||
),
|
||||
update: () => invalidateQueries(movieMutationImpactedQueries),
|
||||
});
|
||||
}
|
||||
|
||||
public static useMovieDestroy(input: GQL.MovieDestroyInput) {
|
||||
return GQL.useMovieDestroyMutation({
|
||||
export const useMovieDestroy = (input: GQL.MovieDestroyInput) =>
|
||||
GQL.useMovieDestroyMutation({
|
||||
variables: input,
|
||||
update: () =>
|
||||
StashService.invalidateQueries(
|
||||
StashService.movieMutationImpactedQueries
|
||||
),
|
||||
update: () => invalidateQueries(movieMutationImpactedQueries),
|
||||
});
|
||||
}
|
||||
|
||||
private static tagMutationImpactedQueries = [
|
||||
export const tagMutationImpactedQueries = [
|
||||
"findScenes",
|
||||
"findSceneMarkers",
|
||||
"sceneMarkerTags",
|
||||
"allTags",
|
||||
];
|
||||
];
|
||||
|
||||
public static useTagCreate(input: GQL.TagCreateInput) {
|
||||
return GQL.useTagCreateMutation({
|
||||
export const useTagCreate = (input: GQL.TagCreateInput) =>
|
||||
GQL.useTagCreateMutation({
|
||||
variables: input,
|
||||
refetchQueries: ["AllTags", "AllTagsForFilter"],
|
||||
// update: () => StashService.invalidateQueries(StashService.tagMutationImpactedQueries)
|
||||
});
|
||||
}
|
||||
public static useTagUpdate(input: GQL.TagUpdateInput) {
|
||||
return GQL.useTagUpdateMutation({
|
||||
export const useTagUpdate = (input: GQL.TagUpdateInput) =>
|
||||
GQL.useTagUpdateMutation({
|
||||
variables: input,
|
||||
refetchQueries: ["AllTags", "AllTagsForFilter"],
|
||||
});
|
||||
}
|
||||
public static useTagDestroy(input: GQL.TagDestroyInput) {
|
||||
return GQL.useTagDestroyMutation({
|
||||
export const useTagDestroy = (input: GQL.TagDestroyInput) =>
|
||||
GQL.useTagDestroyMutation({
|
||||
variables: input,
|
||||
refetchQueries: ["AllTags", "AllTagsForFilter"],
|
||||
update: () =>
|
||||
StashService.invalidateQueries(StashService.tagMutationImpactedQueries),
|
||||
update: () => invalidateQueries(tagMutationImpactedQueries),
|
||||
});
|
||||
}
|
||||
|
||||
public static useConfigureGeneral(input: GQL.ConfigGeneralInput) {
|
||||
return GQL.useConfigureGeneralMutation({
|
||||
export const useConfigureGeneral = (input: GQL.ConfigGeneralInput) =>
|
||||
GQL.useConfigureGeneralMutation({
|
||||
variables: { input },
|
||||
refetchQueries: ["Configuration"],
|
||||
});
|
||||
}
|
||||
|
||||
public static useConfigureInterface(input: GQL.ConfigInterfaceInput) {
|
||||
return GQL.useConfigureInterfaceMutation({
|
||||
export const useConfigureInterface = (input: GQL.ConfigInterfaceInput) =>
|
||||
GQL.useConfigureInterfaceMutation({
|
||||
variables: { input },
|
||||
refetchQueries: ["Configuration"],
|
||||
});
|
||||
}
|
||||
|
||||
public static useMetadataUpdate() {
|
||||
return GQL.useMetadataUpdateSubscription();
|
||||
}
|
||||
export const useMetadataUpdate = () => GQL.useMetadataUpdateSubscription();
|
||||
|
||||
public static useLoggingSubscribe() {
|
||||
return GQL.useLoggingSubscribeSubscription();
|
||||
}
|
||||
export const useLoggingSubscribe = () => GQL.useLoggingSubscribeSubscription();
|
||||
|
||||
public static useLogs() {
|
||||
return GQL.useLogsQuery({
|
||||
export const useLogs = () =>
|
||||
GQL.useLogsQuery({
|
||||
fetchPolicy: "no-cache",
|
||||
});
|
||||
}
|
||||
|
||||
public static useJobStatus() {
|
||||
return GQL.useJobStatusQuery({
|
||||
export const useJobStatus = () =>
|
||||
GQL.useJobStatusQuery({
|
||||
fetchPolicy: "no-cache",
|
||||
});
|
||||
}
|
||||
|
||||
public static mutateStopJob() {
|
||||
return StashService.client.mutate<GQL.StopJobMutation>({
|
||||
export const mutateStopJob = () =>
|
||||
client.mutate<GQL.StopJobMutation>({
|
||||
mutation: GQL.StopJobDocument,
|
||||
});
|
||||
}
|
||||
|
||||
public static queryScrapeFreeones(performerName: string) {
|
||||
return StashService.client.query<GQL.ScrapeFreeonesQuery>({
|
||||
export const queryScrapeFreeones = (performerName: string) =>
|
||||
client.query<GQL.ScrapeFreeonesQuery>({
|
||||
query: GQL.ScrapeFreeonesDocument,
|
||||
variables: {
|
||||
performer_name: performerName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public static queryScrapePerformer(
|
||||
export const queryScrapePerformer = (
|
||||
scraperId: string,
|
||||
scrapedPerformer: GQL.ScrapedPerformerInput
|
||||
) {
|
||||
return StashService.client.query<GQL.ScrapePerformerQuery>({
|
||||
) =>
|
||||
client.query<GQL.ScrapePerformerQuery>({
|
||||
query: GQL.ScrapePerformerDocument,
|
||||
variables: {
|
||||
scraper_id: scraperId,
|
||||
scraped_performer: scrapedPerformer,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public static queryScrapePerformerURL(url: string) {
|
||||
return StashService.client.query<GQL.ScrapePerformerUrlQuery>({
|
||||
export const queryScrapePerformerURL = (url: string) =>
|
||||
client.query<GQL.ScrapePerformerUrlQuery>({
|
||||
query: GQL.ScrapePerformerUrlDocument,
|
||||
variables: {
|
||||
url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public static queryScrapeSceneURL(url: string) {
|
||||
return StashService.client.query<GQL.ScrapeSceneUrlQuery>({
|
||||
export const queryScrapeSceneURL = (url: string) =>
|
||||
client.query<GQL.ScrapeSceneUrlQuery>({
|
||||
query: GQL.ScrapeSceneUrlDocument,
|
||||
variables: {
|
||||
url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public static queryScrapeScene(
|
||||
export const queryScrapeScene = (
|
||||
scraperId: string,
|
||||
scene: GQL.SceneUpdateInput
|
||||
) {
|
||||
return StashService.client.query<GQL.ScrapeSceneQuery>({
|
||||
) =>
|
||||
client.query<GQL.ScrapeSceneQuery>({
|
||||
query: GQL.ScrapeSceneDocument,
|
||||
variables: {
|
||||
scraper_id: scraperId,
|
||||
scene,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public static mutateMetadataScan(input: GQL.ScanMetadataInput) {
|
||||
return StashService.client.mutate<GQL.MetadataScanMutation>({
|
||||
export const mutateMetadataScan = (input: GQL.ScanMetadataInput) =>
|
||||
client.mutate<GQL.MetadataScanMutation>({
|
||||
mutation: GQL.MetadataScanDocument,
|
||||
variables: { input },
|
||||
});
|
||||
}
|
||||
|
||||
public static mutateMetadataAutoTag(input: GQL.AutoTagMetadataInput) {
|
||||
return StashService.client.mutate<GQL.MetadataAutoTagMutation>({
|
||||
export const mutateMetadataAutoTag = (input: GQL.AutoTagMetadataInput) =>
|
||||
client.mutate<GQL.MetadataAutoTagMutation>({
|
||||
mutation: GQL.MetadataAutoTagDocument,
|
||||
variables: { input },
|
||||
});
|
||||
}
|
||||
|
||||
public static mutateMetadataGenerate(input: GQL.GenerateMetadataInput) {
|
||||
return StashService.client.mutate<GQL.MetadataGenerateMutation>({
|
||||
export const mutateMetadataGenerate = (input: GQL.GenerateMetadataInput) =>
|
||||
client.mutate<GQL.MetadataGenerateMutation>({
|
||||
mutation: GQL.MetadataGenerateDocument,
|
||||
variables: { input },
|
||||
});
|
||||
}
|
||||
|
||||
public static mutateMetadataClean() {
|
||||
return StashService.client.mutate<GQL.MetadataCleanMutation>({
|
||||
export const mutateMetadataClean = () =>
|
||||
client.mutate<GQL.MetadataCleanMutation>({
|
||||
mutation: GQL.MetadataCleanDocument,
|
||||
});
|
||||
}
|
||||
|
||||
public static mutateMetadataExport() {
|
||||
return StashService.client.mutate<GQL.MetadataExportMutation>({
|
||||
export const mutateMetadataExport = () =>
|
||||
client.mutate<GQL.MetadataExportMutation>({
|
||||
mutation: GQL.MetadataExportDocument,
|
||||
});
|
||||
}
|
||||
|
||||
public static mutateMetadataImport() {
|
||||
return StashService.client.mutate<GQL.MetadataImportMutation>({
|
||||
export const mutateMetadataImport = () =>
|
||||
client.mutate<GQL.MetadataImportMutation>({
|
||||
mutation: GQL.MetadataImportDocument,
|
||||
});
|
||||
}
|
||||
|
||||
public static querySceneByPathRegex(filter: GQL.FindFilterType) {
|
||||
return StashService.client.query<GQL.FindScenesByPathRegexQuery>({
|
||||
export const querySceneByPathRegex = (filter: GQL.FindFilterType) =>
|
||||
client.query<GQL.FindScenesByPathRegexQuery>({
|
||||
query: GQL.FindScenesByPathRegexDocument,
|
||||
variables: { filter },
|
||||
});
|
||||
}
|
||||
|
||||
public static queryParseSceneFilenames(
|
||||
export const queryParseSceneFilenames = (
|
||||
filter: GQL.FindFilterType,
|
||||
config: GQL.SceneParserInput
|
||||
) {
|
||||
return StashService.client.query<GQL.ParseSceneFilenamesQuery>({
|
||||
) =>
|
||||
client.query<GQL.ParseSceneFilenamesQuery>({
|
||||
query: GQL.ParseSceneFilenamesDocument,
|
||||
variables: { filter, config },
|
||||
fetchPolicy: "network-only",
|
||||
});
|
||||
}
|
||||
|
||||
private static stringGenderMap = new Map<string, GQL.GenderEnum>([
|
||||
export const stringGenderMap = new Map<string, GQL.GenderEnum>([
|
||||
["Male", GQL.GenderEnum.Male],
|
||||
["Female", GQL.GenderEnum.Female],
|
||||
["Transgender Male", GQL.GenderEnum.TransgenderMale],
|
||||
["Transgender Female", GQL.GenderEnum.TransgenderFemale],
|
||||
["Intersex", GQL.GenderEnum.Intersex],
|
||||
]);
|
||||
]);
|
||||
|
||||
public static genderToString(value?: GQL.GenderEnum) {
|
||||
export const genderToString = (value?: GQL.GenderEnum) => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const foundEntry = Array.from(StashService.stringGenderMap.entries()).find(
|
||||
(e) => {
|
||||
const foundEntry = Array.from(stringGenderMap.entries()).find((e) => {
|
||||
return e[1] === value;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (foundEntry) {
|
||||
return foundEntry[0];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static stringToGender(value?: string, caseInsensitive?: boolean) {
|
||||
export const stringToGender = (value?: string, caseInsensitive?: boolean) => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const ret = StashService.stringGenderMap.get(value);
|
||||
const ret = stringGenderMap.get(value);
|
||||
if (ret || !caseInsensitive) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
const asUpper = value.toUpperCase();
|
||||
const foundEntry = Array.from(StashService.stringGenderMap.entries()).find(
|
||||
(e) => {
|
||||
const foundEntry = Array.from(stringGenderMap.entries()).find((e) => {
|
||||
return e[0].toUpperCase() === asUpper;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (foundEntry) {
|
||||
return foundEntry[1];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static getGenderStrings() {
|
||||
return Array.from(StashService.stringGenderMap.keys());
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
private constructor() {}
|
||||
}
|
||||
export const getGenderStrings = () => Array.from(stringGenderMap.keys());
|
||||
|
||||
82
ui/v2.5/src/core/createClient.ts
Normal file
82
ui/v2.5/src/core/createClient.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -25,7 +25,14 @@ import {
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { ListFilter } from "src/components/List/ListFilter";
|
||||
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 { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DisplayMode, FilterMode } from "src/models/list-filter/types";
|
||||
@@ -418,7 +425,7 @@ export const useScenesList = (props: IListHookOptions<FindScenesQueryResult>) =>
|
||||
useList<FindScenesQueryResult, SlimSceneDataFragment>({
|
||||
...props,
|
||||
filterMode: FilterMode.Scenes,
|
||||
useData: StashService.useFindScenes,
|
||||
useData: useFindScenes,
|
||||
getData: (result: FindScenesQueryResult) =>
|
||||
result?.data?.findScenes?.scenes ?? [],
|
||||
getCount: (result: FindScenesQueryResult) =>
|
||||
@@ -431,7 +438,7 @@ export const useSceneMarkersList = (
|
||||
useList<FindSceneMarkersQueryResult, SceneMarkerDataFragment>({
|
||||
...props,
|
||||
filterMode: FilterMode.SceneMarkers,
|
||||
useData: StashService.useFindSceneMarkers,
|
||||
useData: useFindSceneMarkers,
|
||||
getData: (result: FindSceneMarkersQueryResult) =>
|
||||
result?.data?.findSceneMarkers?.scene_markers ?? [],
|
||||
getCount: (result: FindSceneMarkersQueryResult) =>
|
||||
@@ -444,7 +451,7 @@ export const useGalleriesList = (
|
||||
useList<FindGalleriesQueryResult, GalleryDataFragment>({
|
||||
...props,
|
||||
filterMode: FilterMode.Galleries,
|
||||
useData: StashService.useFindGalleries,
|
||||
useData: useFindGalleries,
|
||||
getData: (result: FindGalleriesQueryResult) =>
|
||||
result?.data?.findGalleries?.galleries ?? [],
|
||||
getCount: (result: FindGalleriesQueryResult) =>
|
||||
@@ -457,7 +464,7 @@ export const useStudiosList = (
|
||||
useList<FindStudiosQueryResult, StudioDataFragment>({
|
||||
...props,
|
||||
filterMode: FilterMode.Studios,
|
||||
useData: StashService.useFindStudios,
|
||||
useData: useFindStudios,
|
||||
getData: (result: FindStudiosQueryResult) =>
|
||||
result?.data?.findStudios?.studios ?? [],
|
||||
getCount: (result: FindStudiosQueryResult) =>
|
||||
@@ -470,7 +477,7 @@ export const usePerformersList = (
|
||||
useList<FindPerformersQueryResult, PerformerDataFragment>({
|
||||
...props,
|
||||
filterMode: FilterMode.Performers,
|
||||
useData: StashService.useFindPerformers,
|
||||
useData: useFindPerformers,
|
||||
getData: (result: FindPerformersQueryResult) =>
|
||||
result?.data?.findPerformers?.performers ?? [],
|
||||
getCount: (result: FindPerformersQueryResult) =>
|
||||
@@ -481,7 +488,7 @@ export const useMoviesList = (props: IListHookOptions<FindMoviesQueryResult>) =>
|
||||
useList<FindMoviesQueryResult, MovieDataFragment>({
|
||||
...props,
|
||||
filterMode: FilterMode.Movies,
|
||||
useData: StashService.useFindMovies,
|
||||
useData: useFindMovies,
|
||||
getData: (result: FindMoviesQueryResult) =>
|
||||
result?.data?.findMovies?.movies ?? [],
|
||||
getCount: (result: FindMoviesQueryResult) =>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/* eslint-disable no-param-reassign, no-console */
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { StashService } from "../core/StashService";
|
||||
import { useConfiguration } from "../core/StashService";
|
||||
|
||||
export interface IVideoHoverHookData {
|
||||
videoEl: React.RefObject<HTMLVideoElement>;
|
||||
@@ -14,19 +12,46 @@ export interface IVideoHoverHookOptions {
|
||||
resetOnMouseLeave: boolean;
|
||||
}
|
||||
|
||||
export class VideoHoverHook {
|
||||
public static useVideoHover(
|
||||
options: IVideoHoverHookOptions
|
||||
): IVideoHoverHookData {
|
||||
export const useVideoHover = (options: IVideoHoverHookOptions) => {
|
||||
const videoEl = useRef<HTMLVideoElement>(null);
|
||||
const isPlaying = 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 =
|
||||
!!config.data && !!config.data.configuration
|
||||
? config.data.configuration.interface.soundOnPreview
|
||||
: true;
|
||||
config?.data?.configuration?.interface?.soundOnPreview ?? true;
|
||||
|
||||
useEffect(() => {
|
||||
const videoTag = videoEl.current;
|
||||
@@ -53,37 +78,12 @@ export class VideoHoverHook {
|
||||
videoTag.volume = soundEnabled ? 0.05 : 0;
|
||||
}, [soundEnabled]);
|
||||
|
||||
return { videoEl, isPlaying, isHovering, options };
|
||||
}
|
||||
|
||||
public static onMouseEnter(data: IVideoHoverHookData) {
|
||||
data.isHovering.current = true;
|
||||
|
||||
const videoTag = data.videoEl.current;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
videoEl,
|
||||
isPlaying,
|
||||
isHovering,
|
||||
options,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export { default as useToast } from "./Toast";
|
||||
export { useInterfaceLocalForage } from "./LocalForage";
|
||||
export { VideoHoverHook } from "./VideoHover";
|
||||
export { useVideoHover } from "./VideoHover";
|
||||
export {
|
||||
useScenesList,
|
||||
useSceneMarkersList,
|
||||
|
||||
@@ -3,19 +3,16 @@ import { ApolloProvider } from "react-apollo";
|
||||
import ReactDOM from "react-dom";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { App } from "./App";
|
||||
import { StashService } from "./core/StashService";
|
||||
import { getClient } from "./core/StashService";
|
||||
import { getPlatformURL } from "./core/createClient";
|
||||
import "./index.scss";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
|
||||
ReactDOM.render(
|
||||
<>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href={`${StashService.getPlatformURL()}css`}
|
||||
/>
|
||||
<link rel="stylesheet" type="text/css" href={`${getPlatformURL()}css`} />
|
||||
<BrowserRouter>
|
||||
<ApolloProvider client={StashService.initialize()!}>
|
||||
<ApolloProvider client={getClient()}>
|
||||
<App />
|
||||
</ApolloProvider>
|
||||
</BrowserRouter>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
{
|
||||
"new": "Neu",
|
||||
"tags": "Etiketten",
|
||||
"scenes": "Szenen",
|
||||
"movies": "Filme",
|
||||
"studios": "Studios",
|
||||
"galleries": "Galerien",
|
||||
"library-size": "",
|
||||
"markers": "",
|
||||
"movies": "Filme",
|
||||
"new": "Neu",
|
||||
"performers": "Künstler",
|
||||
"markers": "Marken",
|
||||
"stats": {
|
||||
"notes": "Anmerkungen",
|
||||
"warning": "Dies ist noch eine frühe Version, einige Dinge sind noch in Arbeit."
|
||||
}
|
||||
"scenes": "Szenen",
|
||||
"studios": "Studios",
|
||||
"tags": "Etiketten"
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
{
|
||||
"new": "New",
|
||||
"tags": "Tags",
|
||||
"scenes": "Scenes",
|
||||
"movies": "Movies",
|
||||
"studios": "Studios",
|
||||
"galleries": "Galleries",
|
||||
"performers": "Performers",
|
||||
"library-size": "Library size",
|
||||
"markers": "Markers",
|
||||
"stats": {
|
||||
"notes": "Notes",
|
||||
"warning": "This is still an early version, some things are still a work in progress."
|
||||
}
|
||||
"movies": "Movies",
|
||||
"new": "New",
|
||||
"performers": "Performers",
|
||||
"scenes": "Scenes",
|
||||
"studios": "Studios",
|
||||
"tags": "Tags"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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";
|
||||
|
||||
export class GenderCriterion extends Criterion {
|
||||
@@ -7,7 +7,7 @@ export class GenderCriterion extends Criterion {
|
||||
public parameterName: string = "gender";
|
||||
public modifier = CriterionModifier.Equals;
|
||||
public modifierOptions = [];
|
||||
public options: string[] = StashService.getGenderStrings();
|
||||
public options: string[] = getGenderStrings();
|
||||
public value: string = "";
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
SortDirectionEnum,
|
||||
MovieFilterType,
|
||||
} from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { stringToGender } from "src/core/StashService";
|
||||
import {
|
||||
Criterion,
|
||||
ICriterionOption,
|
||||
@@ -512,7 +512,7 @@ export class ListFilterModel {
|
||||
case "gender": {
|
||||
const gCrit = criterion as GenderCriterion;
|
||||
result.gender = {
|
||||
value: StashService.stringToGender(gCrit.value),
|
||||
value: stringToGender(gCrit.value),
|
||||
modifier: gCrit.modifier,
|
||||
};
|
||||
break;
|
||||
|
||||
@@ -14,7 +14,7 @@ const renderTextArea = (options: {
|
||||
as="textarea"
|
||||
readOnly={!options.isEditing}
|
||||
plaintext={!options.isEditing}
|
||||
onChange={(event: React.FormEvent<HTMLTextAreaElement>) =>
|
||||
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
options.onChange(event.currentTarget.value)
|
||||
}
|
||||
value={options.value}
|
||||
@@ -32,7 +32,7 @@ const renderEditableText = (options: {
|
||||
<Form.Control
|
||||
readOnly={!options.isEditing}
|
||||
plaintext={!options.isEditing}
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
options.onChange(event.currentTarget.value)
|
||||
}
|
||||
value={
|
||||
@@ -68,7 +68,7 @@ const renderInputGroup = (options: {
|
||||
plaintext={!options.isEditing}
|
||||
value={options.value ?? ""}
|
||||
placeholder={options.placeholder ?? options.title}
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
options.onChange(event.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
@@ -150,7 +150,7 @@ const renderHtmlSelect = (options: {
|
||||
disabled={!options.isEditing}
|
||||
plaintext={!options.isEditing}
|
||||
value={options.value?.toString()}
|
||||
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
|
||||
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
options.onChange(event.currentTarget.value)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -25,13 +25,18 @@ const onImageChange = (
|
||||
if (file) readImage(file, onLoadEnd);
|
||||
};
|
||||
|
||||
const usePasteImage = (onLoadEnd: (this: FileReader) => void) => {
|
||||
const usePasteImage = (
|
||||
onLoadEnd: (this: FileReader) => void,
|
||||
isActive: boolean = true
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const paste = (event: ClipboardEvent) => pasteImage(event, onLoadEnd);
|
||||
if (isActive) {
|
||||
document.addEventListener("paste", paste);
|
||||
}
|
||||
|
||||
return () => document.removeEventListener("paste", paste);
|
||||
});
|
||||
}, [isActive, onLoadEnd]);
|
||||
};
|
||||
|
||||
const Image = {
|
||||
|
||||
3722
ui/v2.5/yarn.lock
3722
ui/v2.5/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user