Upgrade javascript libraries (#516)

* Bump react-bootstrap
* Bump library versions and clean up hooks
* Bump intl libraries
* Fix image pasting
This commit is contained in:
InfiniteTF 2020-05-08 04:06:07 +02:00 committed by GitHub
parent 99f88b8d73
commit df2d2c2d09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 3192 additions and 2797 deletions

View file

@ -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"
}
}

View file

@ -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

View file

@ -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 />;

View file

@ -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) => {
props.onChangeQuery(value);
}, 500),
[props.onChangeQuery]
);
const searchCallback = debounce((value: string) => {
props.onChangeQuery(value);
}, 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))
}
/>

View file

@ -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>

View file

@ -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}

View file

@ -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();
}
</Tab>
<Tab eventKey="operations" title="Operations">
<PerformerOperationsPanel performer={performer} />
</Tab>
</Tabs>
);
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>
</>
<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>
</>
<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>

View file

@ -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,25 +149,24 @@ 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(() => {
setImage(undefined);
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()),
});
}

View file

@ -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);

View file

@ -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 &&

View file

@ -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}

View file

@ -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) {

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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";

View file

@ -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}

View file

@ -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) => {

View file

@ -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(

View file

@ -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({

View file

@ -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 &&

View file

@ -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(

View file

@ -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)
}
>

View file

@ -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) {

View file

@ -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}

View file

@ -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>

View file

@ -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");

View file

@ -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,

View file

@ -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();
})
}

View file

@ -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={() => {

View file

@ -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}

View file

@ -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;
}

View file

@ -1,10 +1,19 @@
import React, { useState, useCallback, CSSProperties } from "react";
import React, { useState, CSSProperties } from "react";
import Select, { ValueType } from "react-select";
import 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) => {
setQuery(input);
}, 500),
[]
);
const onInputChange = debounce((input: string) => {
setQuery(input);
}, 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...";

View file

@ -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 />;

View file

@ -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);

View file

@ -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) || ""}

View file

@ -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}

File diff suppressed because it is too large Load diff

View file

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

View file

@ -25,7 +25,14 @@ import {
import { LoadingIndicator } from "src/components/Shared";
import { 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) =>

View file

@ -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,76 +12,78 @@ export interface IVideoHoverHookOptions {
resetOnMouseLeave: boolean;
}
export class VideoHoverHook {
public static useVideoHover(
options: IVideoHoverHookOptions
): IVideoHoverHookData {
const videoEl = useRef<HTMLVideoElement>(null);
const isPlaying = useRef<boolean>(false);
const isHovering = useRef<boolean>(false);
export const useVideoHover = (options: IVideoHoverHookOptions) => {
const videoEl = useRef<HTMLVideoElement>(null);
const isPlaying = useRef<boolean>(false);
const isHovering = useRef<boolean>(false);
const config = useConfiguration();
const config = StashService.useConfiguration();
const soundEnabled =
!!config.data && !!config.data.configuration
? config.data.configuration.interface.soundOnPreview
: true;
const onMouseEnter = () => {
isHovering.current = true;
useEffect(() => {
const videoTag = videoEl.current;
if (!videoTag) {
return;
}
videoTag.onplaying = () => {
if (isHovering.current === true) {
isPlaying.current = true;
} else {
videoTag.pause();
}
};
videoTag.onpause = () => {
isPlaying.current = false;
};
}, [videoEl]);
useEffect(() => {
const videoTag = videoEl.current;
if (!videoTag) {
return;
}
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;
const videoTag = videoEl.current;
if (!videoTag) {
return;
}
if (videoTag.paused && !data.isPlaying.current) {
if (videoTag.paused && !isPlaying.current) {
videoTag.play().catch((error) => {
// eslint-disable-next-line no-console
console.log(error.message);
});
}
}
};
public static onMouseLeave(data: IVideoHoverHookData) {
data.isHovering.current = false;
const onMouseLeave = () => {
isHovering.current = false;
const videoTag = data.videoEl.current;
const videoTag = videoEl.current;
if (!videoTag) {
return;
}
if (!videoTag.paused && data.isPlaying) {
if (!videoTag.paused && isPlaying) {
videoTag.pause();
if (data.options.resetOnMouseLeave) {
if (options.resetOnMouseLeave) {
videoTag.removeAttribute("src");
videoTag.load();
data.isPlaying.current = false;
isPlaying.current = false;
}
}
}
}
};
const soundEnabled =
config?.data?.configuration?.interface?.soundOnPreview ?? true;
useEffect(() => {
const videoTag = videoEl.current;
if (!videoTag) {
return;
}
videoTag.onplaying = () => {
if (isHovering.current === true) {
isPlaying.current = true;
} else {
videoTag.pause();
}
};
videoTag.onpause = () => {
isPlaying.current = false;
};
}, [videoEl]);
useEffect(() => {
const videoTag = videoEl.current;
if (!videoTag) {
return;
}
videoTag.volume = soundEnabled ? 0.05 : 0;
}, [soundEnabled]);
return {
videoEl,
isPlaying,
isHovering,
options,
onMouseEnter,
onMouseLeave,
};
};

View file

@ -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,

View file

@ -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>

View file

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

View file

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

View file

@ -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 = "";
}

View file

@ -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;

View file

@ -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)
}
>

View file

@ -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);
document.addEventListener("paste", paste);
if (isActive) {
document.addEventListener("paste", paste);
}
return () => document.removeEventListener("paste", paste);
});
}, [isActive, onLoadEnd]);
};
const Image = {

File diff suppressed because it is too large Load diff