This commit is contained in:
Infinite
2020-01-24 16:30:07 +01:00
parent e1a1914d16
commit c2544fee98
37 changed files with 5473 additions and 3182 deletions

View File

@@ -1,9 +1,7 @@
import React from "react"; import React from "react";
import { Table } from "react-bootstrap"; import { Table } from "react-bootstrap";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { import { FindGalleriesQueryResult } from "src/core/generated-graphql";
FindGalleriesQueryResult,
} from "src/core/generated-graphql";
import { useGalleriesList } from "src/hooks"; import { useGalleriesList } from "src/hooks";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types"; import { DisplayMode } from "src/models/list-filter/types";
@@ -38,7 +36,10 @@ export const GalleryList: React.FC = () => {
<td> <td>
<Link to={`/galleries/${gallery.id}`}> <Link to={`/galleries/${gallery.id}`}>
{gallery.files.length > 0 ? ( {gallery.files.length > 0 ? (
<img alt={gallery.title ?? ''} src={`${gallery.files[0].path}?thumb=true`} /> <img
alt={gallery.title ?? ""}
src={`${gallery.files[0].path}?thumb=true`}
/>
) : ( ) : (
undefined undefined
)} )}

View File

@@ -1,5 +1,5 @@
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import Lightbox from 'react-images'; import Lightbox from "react-images";
import Gallery from "react-photo-gallery"; import Gallery from "react-photo-gallery";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
@@ -30,8 +30,8 @@ export const GalleryViewer: FunctionComponent<IProps> = ({ gallery }) => {
} }
const photos = gallery.files.map(file => ({ const photos = gallery.files.map(file => ({
src: file.path ?? '', src: file.path ?? "",
caption: file.name ?? '' caption: file.name ?? ""
})); }));
const thumbs = gallery.files.map(file => ({ const thumbs = gallery.files.map(file => ({
src: `${file.path}?thumb=true` || "", src: `${file.path}?thumb=true` || "",
@@ -48,7 +48,9 @@ export const GalleryViewer: FunctionComponent<IProps> = ({ gallery }) => {
onClickPrev={gotoPrevious} onClickPrev={gotoPrevious}
onClickNext={gotoNext} onClickNext={gotoNext}
currentImage={currentImage} currentImage={currentImage}
onClickImage={() => window.open(photos[currentImage].src ?? '', "_blank")} onClickImage={() =>
window.open(photos[currentImage].src ?? "", "_blank")
}
isOpen={lightboxIsOpen} isOpen={lightboxIsOpen}
width={9999} width={9999}
/> />

View File

@@ -4,7 +4,13 @@ import { StashService } from "src/core/StashService";
export const SettingsAboutPanel: React.FC = () => { export const SettingsAboutPanel: React.FC = () => {
const { data, error, loading } = StashService.useVersion(); const { data, error, loading } = StashService.useVersion();
const { data: dataLatest, error: errorLatest, loading: loadingLatest, refetch, networkStatus } = StashService.useLatestVersion(); const {
data: dataLatest,
error: errorLatest,
loading: loadingLatest,
refetch,
networkStatus
} = StashService.useLatestVersion();
function maybeRenderTag() { function maybeRenderTag() {
if (!data || !data.version || !data.version.version) { if (!data || !data.version || !data.version.version) {
@@ -19,28 +25,34 @@ export const SettingsAboutPanel: React.FC = () => {
} }
function maybeRenderLatestVersion() { function maybeRenderLatestVersion() {
if (!dataLatest || !dataLatest.latestversion || !dataLatest.latestversion.shorthash || !dataLatest.latestversion.url) { return; } if (
!dataLatest ||
!dataLatest.latestversion ||
!dataLatest.latestversion.shorthash ||
!dataLatest.latestversion.url
) {
return;
}
if (!data || !data.version || !data.version.hash) { if (!data || !data.version || !data.version.hash) {
return ( return <>{dataLatest.latestversion.shorthash}</>;
<>{dataLatest.latestversion.shorthash}</>
);
} }
if (data.version.hash !== dataLatest.latestversion.shorthash) { if (data.version.hash !== dataLatest.latestversion.shorthash) {
return ( return (
<> <>
<strong>{dataLatest.latestversion.shorthash} [NEW] </strong><a href={dataLatest.latestversion.url}>Download</a> <strong>{dataLatest.latestversion.shorthash} [NEW] </strong>
<a href={dataLatest.latestversion.url}>Download</a>
</> </>
); );
} }
return ( return <>{dataLatest.latestversion.shorthash}</>;
<>{dataLatest.latestversion.shorthash}</>
);
} }
function renderLatestVersion() { function renderLatestVersion() {
if (!data || !data.version || !data.version.version) { return; } // if there is no "version" latest version check is obviously not supported if (!data || !data.version || !data.version.version) {
return;
} // if there is no "version" latest version check is obviously not supported
return ( return (
<Table> <Table>
<tbody> <tbody>
@@ -49,7 +61,9 @@ export const SettingsAboutPanel: React.FC = () => {
<td>{maybeRenderLatestVersion()} </td> <td>{maybeRenderLatestVersion()} </td>
</tr> </tr>
<tr> <tr>
<td><Button onClick={() => refetch()}>Check for new version</Button></td> <td>
<Button onClick={() => refetch()}>Check for new version</Button>
</td>
</tr> </tr>
</tbody> </tbody>
</Table> </Table>
@@ -84,16 +98,54 @@ export const SettingsAboutPanel: React.FC = () => {
<Table> <Table>
<tbody> <tbody>
<tr> <tr>
<td>Stash home at <a href="https://github.com/stashapp/stash" rel="noopener noreferrer" target="_blank">Github</a></td> <td>
Stash home at{" "}
<a
href="https://github.com/stashapp/stash"
rel="noopener noreferrer"
target="_blank"
>
Github
</a>
</td>
</tr> </tr>
<tr> <tr>
<td>Stash <a href="https://github.com/stashapp/stash/wiki" rel="noopener noreferrer" target="_blank">Wiki</a> page</td> <td>
Stash{" "}
<a
href="https://github.com/stashapp/stash/wiki"
rel="noopener noreferrer"
target="_blank"
>
Wiki
</a>{" "}
page
</td>
</tr> </tr>
<tr> <tr>
<td>Join our <a href="https://discord.gg/2TsNFKt" rel="noopener noreferrer" target="_blank">Discord</a> channel</td> <td>
Join our{" "}
<a
href="https://discord.gg/2TsNFKt"
rel="noopener noreferrer"
target="_blank"
>
Discord
</a>{" "}
channel
</td>
</tr> </tr>
<tr> <tr>
<td>Support us through <a href="https://opencollective.com/stashapp" rel="noopener noreferrer" target="_blank">Open Collective</a></td> <td>
Support us through{" "}
<a
href="https://opencollective.com/stashapp"
rel="noopener noreferrer"
target="_blank"
>
Open Collective
</a>
</td>
</tr> </tr>
</tbody> </tbody>
</Table> </Table>
@@ -101,7 +153,11 @@ export const SettingsAboutPanel: React.FC = () => {
{error && <span>{error.message}</span>} {error && <span>{error.message}</span>}
{errorLatest && <span>{errorLatest.message}</span>} {errorLatest && <span>{errorLatest.message}</span>}
{renderVersion()} {renderVersion()}
{!dataLatest || loadingLatest || networkStatus === 4 ? <Spinner animation="border" variant="light" /> : <>{renderLatestVersion()}</>} {!dataLatest || loadingLatest || networkStatus === 4 ? (
<Spinner animation="border" variant="light" />
) : (
<>{renderLatestVersion()}</>
)}
</> </>
); );
}; };

View File

@@ -56,7 +56,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
setDatabasePath(conf.general.databasePath); setDatabasePath(conf.general.databasePath);
setGeneratedPath(conf.general.generatedPath); setGeneratedPath(conf.general.generatedPath);
setMaxTranscodeSize(conf.general.maxTranscodeSize ?? undefined); setMaxTranscodeSize(conf.general.maxTranscodeSize ?? undefined);
setMaxStreamingTranscodeSize(conf.general.maxStreamingTranscodeSize ?? undefined); setMaxStreamingTranscodeSize(
conf.general.maxStreamingTranscodeSize ?? undefined
);
setUsername(conf.general.username); setUsername(conf.general.username);
setPassword(conf.general.password); setPassword(conf.general.password);
setLogFile(conf.general.logFile ?? undefined); setLogFile(conf.general.logFile ?? undefined);

View File

@@ -24,10 +24,8 @@ interface IProps {
onImageChange: (event: React.FormEvent<HTMLInputElement>) => void; onImageChange: (event: React.FormEvent<HTMLInputElement>) => void;
// TODO: only for performers. make generic // TODO: only for performers. make generic
scrapers?: Pick<GQL.Scraper, 'id' | 'name'>[]; scrapers?: Pick<GQL.Scraper, "id" | "name">[];
onDisplayScraperDialog?: ( onDisplayScraperDialog?: (scraper: Pick<GQL.Scraper, "id" | "name">) => void;
scraper: Pick<GQL.Scraper, 'id' | 'name'>
) => void;
} }
export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => { export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
@@ -95,9 +93,7 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
? props.scrapers.map(s => ( ? props.scrapers.map(s => (
<Button <Button
variant="link" variant="link"
onClick={() => onClick={() => props.onDisplayScraperDialog?.(s)}
props.onDisplayScraperDialog?.(s)
}
> >
{s.name} {s.name}
</Button> </Button>

View File

@@ -11,13 +11,14 @@ interface IProps {
} }
export const DurationInput: React.FC<IProps> = (props: IProps) => { export const DurationInput: React.FC<IProps> = (props: IProps) => {
const [value, setValue] = useState<string>(DurationUtils.secondsToString(props.numericValue)); const [value, setValue] = useState<string>(
DurationUtils.secondsToString(props.numericValue)
);
useEffect(() => { useEffect(() => {
setValue(DurationUtils.secondsToString(props.numericValue)); setValue(DurationUtils.secondsToString(props.numericValue));
}, [props.numericValue]); }, [props.numericValue]);
function increment() { function increment() {
let seconds = DurationUtils.stringToSeconds(value); let seconds = DurationUtils.stringToSeconds(value);
seconds += 1; seconds += 1;
@@ -66,7 +67,9 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
disabled={props.disabled} disabled={props.disabled}
value={value} value={value}
onChange={(e: any) => setValue(e.target.value)} onChange={(e: any) => setValue(e.target.value)}
onBlur={() => props.onValueChange(DurationUtils.stringToSeconds(value))} onBlur={() =>
props.onValueChange(DurationUtils.stringToSeconds(value))
}
placeholder="hh:mm:ss" placeholder="hh:mm:ss"
/> />
<InputGroup.Append> <InputGroup.Append>

View File

@@ -1,11 +1,11 @@
import React from 'react'; import React from "react";
import { Spinner } from 'react-bootstrap'; import { Spinner } from "react-bootstrap";
interface ILoadingProps { interface ILoadingProps {
message: string; message: string;
} }
const CLASSNAME = 'LoadingIndicator'; const CLASSNAME = "LoadingIndicator";
const CLASSNAME_MESSAGE = `${CLASSNAME}-message`; const CLASSNAME_MESSAGE = `${CLASSNAME}-message`;
const LoadingIndicator: React.FC<ILoadingProps> = ({ message }) => ( const LoadingIndicator: React.FC<ILoadingProps> = ({ message }) => (
@@ -13,9 +13,7 @@ const LoadingIndicator: React.FC<ILoadingProps> = ({ message }) => (
<Spinner animation="border" role="status"> <Spinner animation="border" role="status">
<span className="sr-only">Loading...</span> <span className="sr-only">Loading...</span>
</Spinner> </Spinner>
<h4 className={CLASSNAME_MESSAGE}> <h4 className={CLASSNAME_MESSAGE}>{message}</h4>
{ message }
</h4>
</div> </div>
); );

View File

@@ -41,7 +41,9 @@ interface ISceneGallerySelect {
initialId?: string; initialId?: string;
sceneId: string; sceneId: string;
onSelect: ( onSelect: (
item: GQL.ValidGalleriesForSceneQuery["validGalleriesForScene"][0] | undefined item:
| GQL.ValidGalleriesForSceneQuery["validGalleriesForScene"][0]
| undefined
) => void; ) => void;
} }
@@ -78,9 +80,7 @@ export const SceneGallerySelect: React.FC<ISceneGallerySelect> = props => {
interface IScrapePerformerSuggestProps { interface IScrapePerformerSuggestProps {
scraperId: string; scraperId: string;
onSelectPerformer: ( onSelectPerformer: (performer: GQL.ScrapedPerformerDataFragment) => void;
performer: GQL.ScrapedPerformerDataFragment
) => void;
placeholder?: string; placeholder?: string;
} }
export const ScrapePerformerSuggest: React.FC<IScrapePerformerSuggestProps> = props => { export const ScrapePerformerSuggest: React.FC<IScrapePerformerSuggestProps> = props => {
@@ -105,8 +105,7 @@ export const ScrapePerformerSuggest: React.FC<IScrapePerformerSuggestProps> = pr
const onChange = (selectedItems: ValueType<Option>) => { const onChange = (selectedItems: ValueType<Option>) => {
const name = getSelectedValues(selectedItems)[0]; const name = getSelectedValues(selectedItems)[0];
const performer = performers.find(p => p.name === name); const performer = performers.find(p => p.name === name);
if(performer) if (performer) props.onSelectPerformer(performer);
props.onSelectPerformer(performer)
}; };
return ( return (
@@ -239,7 +238,7 @@ export const TagSelect: React.FC<IFilterProps> = props => {
variables: { name: tagName } variables: { name: tagName }
}); });
if(result?.data?.tagCreate) { if (result?.data?.tagCreate) {
setSelectedIds([...selectedIds, result.data.tagCreate.id]); setSelectedIds([...selectedIds, result.data.tagCreate.id]);
props.onSelect( props.onSelect(
[...tags, result.data.tagCreate].filter( [...tags, result.data.tagCreate].filter(

View File

@@ -14,4 +14,4 @@ export { DetailsEditNavbar } from "./DetailsEditNavbar";
export { DurationInput } from "./DurationInput"; export { DurationInput } from "./DurationInput";
export { TagLink } from "./TagLink"; export { TagLink } from "./TagLink";
export { HoverPopover } from "./HoverPopover"; export { HoverPopover } from "./HoverPopover";
export { default as LoadingIndicator } from './LoadingIndicator'; export { default as LoadingIndicator } from "./LoadingIndicator";

View File

@@ -5,11 +5,9 @@ import { LoadingIndicator } from "src/components/Shared";
export const Stats: React.FC = () => { export const Stats: React.FC = () => {
const { data, error, loading } = StashService.useStats(); const { data, error, loading } = StashService.useStats();
if (loading || !data) if (loading || !data) return <LoadingIndicator message="Loading..." />;
return <LoadingIndicator message="Loading..." />
if (error) if (error) return <span>error.message</span>;
return <span>error.message</span> ;
return ( return (
<div className="w-75 m-auto"> <div className="w-75 m-auto">

View File

@@ -98,7 +98,7 @@ export const Studio: React.FC = () => {
} }
} else { } else {
const result = await createStudio(); const result = await createStudio();
if(result.data?.studioCreate?.id) if (result.data?.studioCreate?.id)
history.push(`/studios/${result.data.studioCreate.id}`); history.push(`/studios/${result.data.studioCreate.id}`);
} }
} catch (e) { } catch (e) {

View File

@@ -1,7 +1,5 @@
import React from "react"; import React from "react";
import { import { FindStudiosQueryResult } from "src/core/generated-graphql";
FindStudiosQueryResult
} from "src/core/generated-graphql";
import { useStudiosList } from "src/hooks"; import { useStudiosList } from "src/hooks";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types"; import { DisplayMode } from "src/models/list-filter/types";

View File

@@ -183,7 +183,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
numericValue={criterion.value ? criterion.value : 0} numericValue={criterion.value ? criterion.value : 0}
onValueChange={onChangedDuration} onValueChange={onChangedDuration}
/> />
) );
} }
return ( return (
<Form.Control <Form.Control

View File

@@ -249,7 +249,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
value={props.filter.searchTerm} value={props.filter.searchTerm}
onChange={onChangeQuery} onChange={onChangeQuery}
className="filter-item" className="filter-item"
style={{ width: 'inherit' }} style={{ width: "inherit" }}
/> />
<Form.Control <Form.Control
as="select" as="select"

View File

@@ -36,7 +36,8 @@ export const PerformerCard: React.FC<IPerformerCardProps> = (
<div className="card-section"> <div className="card-section">
<h5 className="text-truncate">{props.performer.name}</h5> <h5 className="text-truncate">{props.performer.name}</h5>
{age !== 0 ? <div className="text-muted">{ageString}</div> : ""} {age !== 0 ? <div className="text-muted">{ageString}</div> : ""}
<div className="text-muted">Stars in {props.performer.scene_count}{" "} <div className="text-muted">
Stars in {props.performer.scene_count}{" "}
<Link to={NavUtils.makePerformerScenesUrl(props.performer)}> <Link to={NavUtils.makePerformerScenesUrl(props.performer)}>
scenes scenes
</Link> </Link>

View File

@@ -5,16 +5,14 @@ import { Button, Spinner, Tabs, Tab } from "react-bootstrap";
import { useParams, useHistory } from "react-router-dom"; import { useParams, useHistory } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import { import { Icon } from "src/components/Shared";
Icon, import { useToast } from "src/hooks";
} from "src/components/Shared";
import { useToast } from 'src/hooks';
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
import Lightbox from 'react-images'; import Lightbox from "react-images";
import { IconName } from "@fortawesome/fontawesome-svg-core"; import { IconName } from "@fortawesome/fontawesome-svg-core";
import { PerformerDetailsPanel } from './PerformerDetailsPanel'; import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
import { PerformerOperationsPanel } from './PerformerOperationsPanel'; import { PerformerOperationsPanel } from "./PerformerOperationsPanel";
import { PerformerScenesPanel } from './PerformerScenesPanel'; import { PerformerScenesPanel } from "./PerformerScenesPanel";
export const Performer: React.FC = () => { export const Performer: React.FC = () => {
const Toast = useToast(); const Toast = useToast();
@@ -23,7 +21,9 @@ export const Performer: React.FC = () => {
const isNew = id === "new"; const isNew = id === "new";
// Performer state // Performer state
const [performer, setPerformer] = useState<Partial<GQL.PerformerDataFragment>>({}); const [performer, setPerformer] = useState<
Partial<GQL.PerformerDataFragment>
>({});
const [imagePreview, setImagePreview] = useState<string>(); const [imagePreview, setImagePreview] = useState<string>();
const [lightboxIsOpen, setLightboxIsOpen] = useState(false); const [lightboxIsOpen, setLightboxIsOpen] = useState(false);
@@ -35,7 +35,6 @@ export const Performer: React.FC = () => {
const [createPerformer] = StashService.usePerformerCreate(); const [createPerformer] = StashService.usePerformerCreate();
const [deletePerformer] = StashService.usePerformerDestroy(); const [deletePerformer] = StashService.usePerformerDestroy();
useEffect(() => { useEffect(() => {
setIsLoading(false); setIsLoading(false);
if (data?.findPerformer) setPerformer(data.findPerformer); if (data?.findPerformer) setPerformer(data.findPerformer);
@@ -54,16 +53,24 @@ export const Performer: React.FC = () => {
if (error) return <div>{error.message}</div>; if (error) return <div>{error.message}</div>;
async function onSave(performerInput: Partial<GQL.PerformerCreateInput> | Partial<GQL.PerformerUpdateInput>) { async function onSave(
performerInput:
| Partial<GQL.PerformerCreateInput>
| Partial<GQL.PerformerUpdateInput>
) {
setIsLoading(true); setIsLoading(true);
try { try {
if (!isNew) { if (!isNew) {
const result = await updatePerformer({variables: performerInput as GQL.PerformerUpdateInput}); const result = await updatePerformer({
if(result.data?.performerUpdate) variables: performerInput as GQL.PerformerUpdateInput
});
if (result.data?.performerUpdate)
setPerformer(result.data?.performerUpdate); setPerformer(result.data?.performerUpdate);
} else { } else {
const result = await createPerformer({variables: performerInput as GQL.PerformerCreateInput}); const result = await createPerformer({
if(result.data?.performerCreate) { variables: performerInput as GQL.PerformerCreateInput
});
if (result.data?.performerCreate) {
setPerformer(result.data.performerCreate); setPerformer(result.data.performerCreate);
history.push(`/performers/${result.data.performerCreate.id}`); history.push(`/performers/${result.data.performerCreate.id}`);
} }
@@ -77,7 +84,7 @@ export const Performer: React.FC = () => {
async function onDelete() { async function onDelete() {
setIsLoading(true); setIsLoading(true);
try { try {
await deletePerformer({variables: { id }}); await deletePerformer({ variables: { id } });
} catch (e) { } catch (e) {
Toast.error(e); Toast.error(e);
} }
@@ -112,7 +119,7 @@ export const Performer: React.FC = () => {
<PerformerScenesPanel performer={performer} /> <PerformerScenesPanel performer={performer} />
</Tab> </Tab>
<Tab eventKey="edit" title="Edit"> <Tab eventKey="edit" title="Edit">
{ renderEditPanel() } {renderEditPanel()}
</Tab> </Tab>
<Tab eventKey="operations" title="Operations"> <Tab eventKey="operations" title="Operations">
<PerformerOperationsPanel performer={performer} /> <PerformerOperationsPanel performer={performer} />
@@ -151,7 +158,7 @@ export const Performer: React.FC = () => {
} }
} }
function setFavorite(v : boolean) { function setFavorite(v: boolean) {
performer.favorite = v; performer.favorite = v;
onSave(performer); onSave(performer);
} }
@@ -165,7 +172,7 @@ export const Performer: React.FC = () => {
<Icon icon={icon} /> <Icon icon={icon} />
</a> </a>
</Button> </Button>
) );
} }
} }
@@ -175,7 +182,8 @@ export const Performer: React.FC = () => {
<Button <Button
className={performer.favorite ? "favorite" : "not-favorite"} className={performer.favorite ? "favorite" : "not-favorite"}
onClick={() => setFavorite(!performer.favorite)} onClick={() => setFavorite(!performer.favorite)}
><Icon icon="heart" /> >
<Icon icon="heart" />
</Button> </Button>
{maybeRenderURL(performer.url ?? undefined)} {maybeRenderURL(performer.url ?? undefined)}
{/* TODO - render instagram and twitter links with icons */} {/* TODO - render instagram and twitter links with icons */}
@@ -188,7 +196,7 @@ export const Performer: React.FC = () => {
return ( return (
<div className="columns is-multiline no-spacing"> <div className="columns is-multiline no-spacing">
<div className="column is-half details-image-container"> <div className="column is-half details-image-container">
<img className="performer" src={imagePreview} alt='Performer' /> <img className="performer" src={imagePreview} alt="Performer" />
</div> </div>
<div className="column is-half details-detail-container"> <div className="column is-half details-detail-container">
{renderTabs()} {renderTabs()}
@@ -197,7 +205,7 @@ export const Performer: React.FC = () => {
); );
} }
const photos = [{src: imagePreview || "", caption: "Image"}]; const photos = [{ src: imagePreview || "", caption: "Image" }];
function openLightbox() { function openLightbox() {
setLightboxIsOpen(true); setLightboxIsOpen(true);
@@ -216,7 +224,7 @@ export const Performer: React.FC = () => {
<div id="performer-page"> <div id="performer-page">
<div className="details-image-container"> <div className="details-image-container">
<Button variant="link" onClick={openLightbox}> <Button variant="link" onClick={openLightbox}>
<img className="performer" src={imagePreview} alt='Performer' /> <img className="performer" src={imagePreview} alt="Performer" />
</Button> </Button>
</div> </div>
<div className="performer-head"> <div className="performer-head">
@@ -229,9 +237,7 @@ export const Performer: React.FC = () => {
</div> </div>
<div className="performer-body"> <div className="performer-body">
<div className="details-detail-container"> <div className="details-detail-container">{renderTabs()}</div>
{renderTabs()}
</div>
</div> </div>
</div> </div>
<Lightbox <Lightbox

View File

@@ -1,14 +1,17 @@
/* eslint-disable react/no-this-in-sfc */ /* eslint-disable react/no-this-in-sfc */
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Button, Form, Popover, OverlayTrigger, Spinner, Table } from "react-bootstrap"; import {
Button,
Form,
Popover,
OverlayTrigger,
Spinner,
Table
} from "react-bootstrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import { import { Icon, Modal, ScrapePerformerSuggest } from "src/components/Shared";
Icon,
Modal,
ScrapePerformerSuggest
} from "src/components/Shared";
import { ImageUtils, TableUtils } from "src/utils"; import { ImageUtils, TableUtils } from "src/utils";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
@@ -16,20 +19,34 @@ interface IPerformerDetails {
performer: Partial<GQL.PerformerDataFragment>; performer: Partial<GQL.PerformerDataFragment>;
isNew?: boolean; isNew?: boolean;
isEditing?: boolean; isEditing?: boolean;
onSave?: (performer: Partial<GQL.PerformerCreateInput> | Partial<GQL.PerformerUpdateInput>) => void; onSave?: (
performer:
| Partial<GQL.PerformerCreateInput>
| Partial<GQL.PerformerUpdateInput>
) => void;
onDelete?: () => void; onDelete?: () => void;
onImageChange?: (image: string) => void; onImageChange?: (image: string) => void;
} }
export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({ performer, isNew, isEditing, onSave, onDelete, onImageChange }) => { export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
performer,
isNew,
isEditing,
onSave,
onDelete,
onImageChange
}) => {
const Toast = useToast(); const Toast = useToast();
// Editing state // Editing state
const [isDisplayingScraperDialog, setIsDisplayingScraperDialog] = useState<GQL.Scraper>(); const [isDisplayingScraperDialog, setIsDisplayingScraperDialog] = useState<
const [scrapePerformerDetails, setScrapePerformerDetails] = useState<GQL.ScrapedPerformerDataFragment>(); GQL.Scraper
>();
const [scrapePerformerDetails, setScrapePerformerDetails] = useState<
GQL.ScrapedPerformerDataFragment
>();
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
// Editing performer state // Editing performer state
const [image, setImage] = useState<string>(); const [image, setImage] = useState<string>();
const [name, setName] = useState<string>(); const [name, setName] = useState<string>();
@@ -55,7 +72,6 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({ performer,
const Scrapers = StashService.useListPerformerScrapers(); const Scrapers = StashService.useListPerformerScrapers();
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]); const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
function updatePerformerEditState( function updatePerformerEditState(
state: Partial<GQL.PerformerDataFragment | GQL.ScrapedPerformer> state: Partial<GQL.PerformerDataFragment | GQL.ScrapedPerformer>
) { ) {
@@ -91,19 +107,17 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({ performer,
} }
} }
if (isEditing) if (isEditing) ImageUtils.usePasteImage(onImageLoad);
ImageUtils.usePasteImage(onImageLoad);
useEffect(() => { useEffect(() => {
const newQueryableScrapers = (Scrapers?.data?.listPerformerScrapers ?? []).filter(s => ( const newQueryableScrapers = (
s.performer?.supported_scrapes.includes(GQL.ScrapeType.Name) Scrapers?.data?.listPerformerScrapers ?? []
)); ).filter(s => s.performer?.supported_scrapes.includes(GQL.ScrapeType.Name));
setQueryableScrapers(newQueryableScrapers); setQueryableScrapers(newQueryableScrapers);
}, [Scrapers]); }, [Scrapers]);
if (isLoading) if (isLoading) return <Spinner animation="border" variant="light" />;
return <Spinner animation="border" variant="light" />;
function getPerformerInput() { function getPerformerInput() {
const performerInput: Partial< const performerInput: Partial<
@@ -138,9 +152,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({ performer,
ImageUtils.onImageChange(event, onImageLoad); ImageUtils.onImageChange(event, onImageLoad);
} }
function onDisplayFreeOnesDialog( function onDisplayFreeOnesDialog(scraper: GQL.Scraper) {
scraper: GQL.Scraper
) {
setIsDisplayingScraperDialog(scraper); setIsDisplayingScraperDialog(scraper);
} }
@@ -212,9 +224,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({ performer,
? queryableScrapers.map(s => ( ? queryableScrapers.map(s => (
<Button <Button
variant="link" variant="link"
onClick={() => onClick={() => onDisplayFreeOnesDialog(s)}
onDisplayFreeOnesDialog(s)
}
> >
{s.name} {s.name}
</Button> </Button>
@@ -299,15 +309,30 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({ performer,
if (isEditing) { if (isEditing) {
return ( return (
<> <>
<Button className="edit-button" variant="primary" onClick={() => onSave?.(getPerformerInput())}>Save</Button> <Button
{!isNew ? <Button className="edit-button" variant="danger" onClick={() => setIsDeleteAlertOpen(true)}>Delete</Button> : ''} className="edit-button"
variant="primary"
onClick={() => onSave?.(getPerformerInput())}
>
Save
</Button>
{!isNew ? (
<Button
className="edit-button"
variant="danger"
onClick={() => setIsDeleteAlertOpen(true)}
>
Delete
</Button>
) : (
""
)}
{renderScraperMenu()} {renderScraperMenu()}
</> </>
); );
} }
} }
function renderDeleteAlert() { function renderDeleteAlert() {
return ( return (
<Modal <Modal
@@ -316,34 +341,50 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({ performer,
accept={{ text: "Delete", variant: "danger", onClick: onDelete }} accept={{ text: "Delete", variant: "danger", onClick: onDelete }}
cancel={{ onClick: () => setIsDeleteAlertOpen(false) }} cancel={{ onClick: () => setIsDeleteAlertOpen(false) }}
> >
<p> <p>Are you sure you want to delete {name}?</p>
Are you sure you want to delete {name}?
</p>
</Modal> </Modal>
); );
} }
function renderImageInput() { function renderImageInput() {
if (!isEditing) { return; } if (!isEditing) {
return;
}
return ( return (
<tr> <tr>
<td>Image</td> <td>Image</td>
<td><Form.Control type="file" onChange={onImageChangeHandler} accept=".jpg,.jpeg" /></td> <td>
<Form.Control
type="file"
onChange={onImageChangeHandler}
accept=".jpg,.jpeg"
/>
</td>
</tr> </tr>
) );
} }
function maybeRenderName() { function maybeRenderName() {
if (isEditing) { if (isEditing) {
return TableUtils.renderInputGroup( return TableUtils.renderInputGroup({
{title: "Name", value: name, isEditing: !!isEditing, placeholder: "Name", onChange: setName}); title: "Name",
value: name,
isEditing: !!isEditing,
placeholder: "Name",
onChange: setName
});
} }
} }
function maybeRenderAliases() { function maybeRenderAliases() {
if (isEditing) { if (isEditing) {
return TableUtils.renderInputGroup( return TableUtils.renderInputGroup({
{title: "Aliases", value: aliases, isEditing: !!isEditing, placeholder: "Aliases", onChange: setAliases}); title: "Aliases",
value: aliases,
isEditing: !!isEditing,
placeholder: "Aliases",
onChange: setAliases
});
} }
} }

View File

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

View File

@@ -5,25 +5,30 @@ import { ListFilterModel } from "src/models/list-filter/filter";
import { SceneList } from "../../scenes/SceneList"; import { SceneList } from "../../scenes/SceneList";
interface IPerformerDetailsProps { interface IPerformerDetailsProps {
performer: Partial<GQL.PerformerDataFragment> performer: Partial<GQL.PerformerDataFragment>;
} }
export const PerformerScenesPanel: React.FC<IPerformerDetailsProps> = ({ performer }) => { export const PerformerScenesPanel: React.FC<IPerformerDetailsProps> = ({
performer
}) => {
function filterHook(filter: ListFilterModel) { function filterHook(filter: ListFilterModel) {
const performerValue = {id: performer.id!, label: performer.name!}; const performerValue = { id: performer.id!, label: performer.name! };
// if performers is already present, then we modify it, otherwise add // if performers is already present, then we modify it, otherwise add
let performerCriterion = filter.criteria.find((c) => { let performerCriterion = filter.criteria.find(c => {
return c.type === "performers"; return c.type === "performers";
}); });
if (performerCriterion && if (
performerCriterion &&
(performerCriterion.modifier === GQL.CriterionModifier.IncludesAll || (performerCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
performerCriterion.modifier === GQL.CriterionModifier.Includes)) { performerCriterion.modifier === GQL.CriterionModifier.Includes)
) {
// add the performer if not present // add the performer if not present
if (!performerCriterion.value.find((p : any) => { if (
!performerCriterion.value.find((p: any) => {
return p.id === performer.id; return p.id === performer.id;
})) { })
) {
performerCriterion.value.push(performerValue); performerCriterion.value.push(performerValue);
} }
@@ -38,10 +43,5 @@ export const PerformerScenesPanel: React.FC<IPerformerDetailsProps> = ({ perform
return filter; return filter;
} }
return ( return <SceneList subComponent filterHook={filterHook} />;
<SceneList };
subComponent
filterHook={filterHook}
/>
);
}

View File

@@ -1,9 +1,7 @@
import _ from "lodash"; import _ from "lodash";
import React from "react"; import React from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { import { FindPerformersQueryResult } from "src/core/generated-graphql";
FindPerformersQueryResult,
} from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import { usePerformersList } from "src/hooks"; import { usePerformersList } from "src/hooks";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";

View File

@@ -24,7 +24,8 @@ export const SceneCard: React.FC<ISceneCardProps> = (
}); });
const config = StashService.useConfiguration(); const config = StashService.useConfiguration();
const showStudioAsText = config?.data?.configuration.interface.showStudioAsText ?? false; const showStudioAsText =
config?.data?.configuration.interface.showStudioAsText ?? false;
function maybeRenderRatingBanner() { function maybeRenderRatingBanner() {
if (!props.scene.rating) { if (!props.scene.rating) {
@@ -52,7 +53,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
) : ( ) : (
"" ""
)} )}
{ (props.scene.file.duration ?? 0) >= 1 {(props.scene.file.duration ?? 0) >= 1
? TextUtils.secondsToTimestamp(props.scene.file.duration ?? 0) ? TextUtils.secondsToTimestamp(props.scene.file.duration ?? 0)
: ""} : ""}
</div> </div>
@@ -221,7 +222,13 @@ export const SceneCard: React.FC<ISceneCardProps> = (
: TextUtils.fileNameFromPath(props.scene.path)} : TextUtils.fileNameFromPath(props.scene.path)}
</h4> </h4>
<span>{props.scene.date}</span> <span>{props.scene.date}</span>
<p>{TextUtils.truncate(props.scene.details ?? "", 100, "... (continued)")}</p> <p>
{TextUtils.truncate(
props.scene.details ?? "",
100,
"... (continued)"
)}
</p>
</div> </div>
{maybeRenderPopoverButtonGroup()} {maybeRenderPopoverButtonGroup()}

View File

@@ -57,9 +57,9 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
const [deleteScene] = StashService.useSceneDestroy(getSceneDeleteInput()); const [deleteScene] = StashService.useSceneDestroy(getSceneDeleteInput());
useEffect(() => { useEffect(() => {
const newQueryableScrapers = (Scrapers?.data?.listSceneScrapers ?? []).filter(s => ( const newQueryableScrapers = (
s.scene?.supported_scrapes.includes(GQL.ScrapeType.Fragment) Scrapers?.data?.listSceneScrapers ?? []
)); ).filter(s => s.scene?.supported_scrapes.includes(GQL.ScrapeType.Fragment));
setQueryableScrapers(newQueryableScrapers); setQueryableScrapers(newQueryableScrapers);
}, [Scrapers]); }, [Scrapers]);
@@ -108,7 +108,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
setIsLoading(true); setIsLoading(true);
try { try {
const result = await updateScene(); const result = await updateScene();
if(result.data?.sceneUpdate) { if (result.data?.sceneUpdate) {
props.onUpdate(result.data.sceneUpdate); props.onUpdate(result.data.sceneUpdate);
Toast.success({ content: "Updated scene" }); Toast.success({ content: "Updated scene" });
} }
@@ -202,9 +202,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
ImageUtils.onImageChange(event, onImageLoad); ImageUtils.onImageChange(event, onImageLoad);
} }
async function onScrapeClicked( async function onScrapeClicked(scraper: GQL.Scraper) {
scraper: GQL.Scraper
) {
setIsLoading(true); setIsLoading(true);
try { try {
const result = await StashService.queryScrapeScene( const result = await StashService.queryScrapeScene(
@@ -418,7 +416,11 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
</Button> </Button>
<Collapse in={isCoverImageOpen}> <Collapse in={isCoverImageOpen}>
<div> <div>
<img className="scene-cover" src={coverImagePreview} alt="Scene cover" /> <img
className="scene-cover"
src={coverImagePreview}
alt="Scene cover"
/>
<Form.Group className="test" controlId="cover"> <Form.Group className="test" controlId="cover">
<Form.Control <Form.Control
type="file" type="file"

View File

@@ -38,7 +38,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
<tr> <tr>
<td>Stream</td> <td>Stream</td>
<td> <td>
<a href={props.scene.paths.stream ?? ''}>{props.scene.paths.stream}</a>{" "} <a href={props.scene.paths.stream ?? ""}>
{props.scene.paths.stream}
</a>{" "}
</td> </td>
</tr> </tr>
); );
@@ -51,7 +53,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return ( return (
<tr> <tr>
<td>File Size</td> <td>File Size</td>
<td>{TextUtils.fileSize(parseInt(props.scene.file.size ?? '0', 10))}</td> <td>
{TextUtils.fileSize(parseInt(props.scene.file.size ?? "0", 10))}
</td>
</tr> </tr>
); );
} }

View File

@@ -114,12 +114,16 @@ class SceneParserResult {
public studioId: ParserResult<string> = new ParserResult(); public studioId: ParserResult<string> = new ParserResult();
public tags: ParserResult<GQL.Tag[]> = new ParserResult(); public tags: ParserResult<GQL.Tag[]> = new ParserResult();
public tagIds: ParserResult<string[]> = new ParserResult(); public tagIds: ParserResult<string[]> = new ParserResult();
public performers: ParserResult<Partial<GQL.Performer>[]> = new ParserResult(); public performers: ParserResult<
Partial<GQL.Performer>[]
> = new ParserResult();
public performerIds: ParserResult<string[]> = new ParserResult(); public performerIds: ParserResult<string[]> = new ParserResult();
public scene: GQL.SlimSceneDataFragment; public scene: GQL.SlimSceneDataFragment;
constructor(result: GQL.ParseSceneFilenamesQuery["parseSceneFilenames"]["results"][0]) { constructor(
result: GQL.ParseSceneFilenamesQuery["parseSceneFilenames"]["results"][0]
) {
this.scene = result.scene; this.scene = result.scene;
this.id = this.scene.id; this.id = this.scene.id;
@@ -141,12 +145,15 @@ class SceneParserResult {
if (result.performer_ids) { if (result.performer_ids) {
this.performers.setValue( this.performers.setValue(
(result.performer_ids ?? []).map(p => ({ (result.performer_ids ?? []).map(
p =>
({
id: p, id: p,
name: "", name: "",
favorite: false, favorite: false,
image_path: "" image_path: ""
} as GQL.Performer)) } as GQL.Performer)
)
); );
} }
@@ -343,7 +350,9 @@ export const SceneFilenameParser: React.FC = () => {
}, [parserInput]); }, [parserInput]);
const parseResults = useCallback( const parseResults = useCallback(
(results: GQL.ParseSceneFilenamesQuery["parseSceneFilenames"]["results"]) => { (
results: GQL.ParseSceneFilenamesQuery["parseSceneFilenames"]["results"]
) => {
if (results) { if (results) {
const result = results const result = results
.map(r => { .map(r => {
@@ -897,7 +906,9 @@ export const SceneFilenameParser: React.FC = () => {
fieldName="Title" fieldName="Title"
className="parser-field-title" className="parser-field-title"
parserResult={props.scene.title} parserResult={props.scene.title}
onSetChanged={set => onTitleChanged(set, props.scene.title.value ?? undefined)} onSetChanged={set =>
onTitleChanged(set, props.scene.title.value ?? undefined)
}
onValueChanged={value => onTitleChanged(props.scene.title.set, value)} onValueChanged={value => onTitleChanged(props.scene.title.set, value)}
renderOriginalInputField={renderOriginalInputGroup} renderOriginalInputField={renderOriginalInputGroup}
renderNewInputField={renderNewInputGroup} renderNewInputField={renderNewInputGroup}
@@ -907,7 +918,9 @@ export const SceneFilenameParser: React.FC = () => {
fieldName="Date" fieldName="Date"
className="parser-field-date" className="parser-field-date"
parserResult={props.scene.date} parserResult={props.scene.date}
onSetChanged={set => onDateChanged(set, props.scene.date.value ?? undefined)} onSetChanged={set =>
onDateChanged(set, props.scene.date.value ?? undefined)
}
onValueChanged={value => onDateChanged(props.scene.date.set, value)} onValueChanged={value => onDateChanged(props.scene.date.set, value)}
renderOriginalInputField={renderOriginalInputGroup} renderOriginalInputField={renderOriginalInputGroup}
renderNewInputField={renderNewInputGroup} renderNewInputField={renderNewInputGroup}
@@ -919,7 +932,10 @@ export const SceneFilenameParser: React.FC = () => {
parserResult={props.scene.performerIds} parserResult={props.scene.performerIds}
originalParserResult={props.scene.performers} originalParserResult={props.scene.performers}
onSetChanged={set => onSetChanged={set =>
onPerformerIdsChanged(set, props.scene.performerIds.value ?? undefined) onPerformerIdsChanged(
set,
props.scene.performerIds.value ?? undefined
)
} }
onValueChanged={value => onValueChanged={value =>
onPerformerIdsChanged(props.scene.performerIds.set, value) onPerformerIdsChanged(props.scene.performerIds.set, value)
@@ -933,7 +949,9 @@ export const SceneFilenameParser: React.FC = () => {
className="parser-field-tags" className="parser-field-tags"
parserResult={props.scene.tagIds} parserResult={props.scene.tagIds}
originalParserResult={props.scene.tags} originalParserResult={props.scene.tags}
onSetChanged={set => onTagIdsChanged(set, props.scene.tagIds.value ?? undefined)} onSetChanged={set =>
onTagIdsChanged(set, props.scene.tagIds.value ?? undefined)
}
onValueChanged={value => onValueChanged={value =>
onTagIdsChanged(props.scene.tagIds.set, value) onTagIdsChanged(props.scene.tagIds.set, value)
} }

View File

@@ -19,7 +19,10 @@ interface ISceneList {
filterHook?: (filter: ListFilterModel) => ListFilterModel; filterHook?: (filter: ListFilterModel) => ListFilterModel;
} }
export const SceneList: React.FC<ISceneList> = ({ subComponent, filterHook }) => { export const SceneList: React.FC<ISceneList> = ({
subComponent,
filterHook
}) => {
const history = useHistory(); const history = useHistory();
const otherOperations = [ const otherOperations = [
{ {

View File

@@ -1,9 +1,7 @@
import _ from "lodash"; import _ from "lodash";
import React from "react"; import React from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { import { FindSceneMarkersQueryResult } from "src/core/generated-graphql";
FindSceneMarkersQueryResult
} from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import { NavUtils } from "src/utils"; import { NavUtils } from "src/utils";
import { useSceneMarkersList } from "src/hooks"; import { useSceneMarkersList } from "src/hooks";

View File

@@ -1,8 +1,8 @@
import ApolloClient from "apollo-client"; import ApolloClient from "apollo-client";
import { WebSocketLink } from "apollo-link-ws"; import { WebSocketLink } from "apollo-link-ws";
import { InMemoryCache } from "apollo-cache-inmemory"; import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from 'apollo-link-http'; import { HttpLink } from "apollo-link-http";
import { split } from 'apollo-link'; import { split } from "apollo-link";
import { getMainDefinition } from "apollo-utilities"; import { getMainDefinition } from "apollo-utilities";
import { ListFilterModel } from "../models/list-filter/filter"; import { ListFilterModel } from "../models/list-filter/filter";
import * as GQL from "./generated-graphql"; import * as GQL from "./generated-graphql";
@@ -45,7 +45,10 @@ export class StashService {
const link = split( const link = split(
({ query }) => { ({ query }) => {
const definition = getMainDefinition(query); const definition = getMainDefinition(query);
return definition.kind === "OperationDefinition" && definition.operation === "subscription"; return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
}, },
wsLink, wsLink,
httpLink httpLink
@@ -267,7 +270,9 @@ export class StashService {
return GQL.useAllStudiosForFilterQuery(); return GQL.useAllStudiosForFilterQuery();
} }
public static useValidGalleriesForScene(sceneId: string) { public static useValidGalleriesForScene(sceneId: string) {
return GQL.useValidGalleriesForSceneQuery({ variables: { scene_id: sceneId } }); return GQL.useValidGalleriesForSceneQuery({
variables: { scene_id: sceneId }
});
} }
public static useStats() { public static useStats() {
return GQL.useStatsQuery(); return GQL.useStatsQuery();
@@ -275,8 +280,12 @@ export class StashService {
public static useVersion() { public static useVersion() {
return GQL.useVersionQuery(); return GQL.useVersionQuery();
} }
public static useLatestVersion() { return GQL.useLatestVersionQuery({ notifyOnNetworkStatusChange: true, errorPolicy: 'ignore' }); } public static useLatestVersion() {
return GQL.useLatestVersionQuery({
notifyOnNetworkStatusChange: true,
errorPolicy: "ignore"
});
}
public static useConfiguration() { public static useConfiguration() {
return GQL.useConfigurationQuery(); return GQL.useConfigurationQuery();

File diff suppressed because it is too large Load Diff

View File

@@ -77,7 +77,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
const [filter, setFilter] = useState<ListFilterModel>( const [filter, setFilter] = useState<ListFilterModel>(
new ListFilterModel( new ListFilterModel(
options.filterMode, options.filterMode,
options.subComponent ? '' : queryString.parse(history.location.search) options.subComponent ? "" : queryString.parse(history.location.search)
) )
); );
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set()); const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
@@ -318,8 +318,10 @@ export const useScenesList = (props: IListHookOptions<FindScenesQueryResult>) =>
...props, ...props,
filterMode: FilterMode.Scenes, filterMode: FilterMode.Scenes,
useData: StashService.useFindScenes, useData: StashService.useFindScenes,
getData: (result: FindScenesQueryResult) => result?.data?.findScenes?.scenes ?? [], getData: (result: FindScenesQueryResult) =>
getCount: (result: FindScenesQueryResult) => result?.data?.findScenes?.count ?? 0 result?.data?.findScenes?.scenes ?? [],
getCount: (result: FindScenesQueryResult) =>
result?.data?.findScenes?.count ?? 0
}); });
export const useSceneMarkersList = ( export const useSceneMarkersList = (
@@ -335,7 +337,9 @@ export const useSceneMarkersList = (
result?.data?.findSceneMarkers?.count ?? 0 result?.data?.findSceneMarkers?.count ?? 0
}); });
export const useGalleriesList = (props: IListHookOptions<FindGalleriesQueryResult>) => export const useGalleriesList = (
props: IListHookOptions<FindGalleriesQueryResult>
) =>
useList<FindGalleriesQueryResult, GalleryDataFragment>({ useList<FindGalleriesQueryResult, GalleryDataFragment>({
...props, ...props,
filterMode: FilterMode.Galleries, filterMode: FilterMode.Galleries,
@@ -346,16 +350,22 @@ export const useGalleriesList = (props: IListHookOptions<FindGalleriesQueryResul
result?.data?.findGalleries?.count ?? 0 result?.data?.findGalleries?.count ?? 0
}); });
export const useStudiosList = (props: IListHookOptions<FindStudiosQueryResult>) => export const useStudiosList = (
props: IListHookOptions<FindStudiosQueryResult>
) =>
useList<FindStudiosQueryResult, StudioDataFragment>({ useList<FindStudiosQueryResult, StudioDataFragment>({
...props, ...props,
filterMode: FilterMode.Studios, filterMode: FilterMode.Studios,
useData: StashService.useFindStudios, useData: StashService.useFindStudios,
getData: (result: FindStudiosQueryResult) => result?.data?.findStudios?.studios ?? [], getData: (result: FindStudiosQueryResult) =>
getCount: (result: FindStudiosQueryResult) => result?.data?.findStudios?.count ?? 0 result?.data?.findStudios?.studios ?? [],
getCount: (result: FindStudiosQueryResult) =>
result?.data?.findStudios?.count ?? 0
}); });
export const usePerformersList = (props: IListHookOptions<FindPerformersQueryResult>) => export const usePerformersList = (
props: IListHookOptions<FindPerformersQueryResult>
) =>
useList<FindPerformersQueryResult, PerformerDataFragment>({ useList<FindPerformersQueryResult, PerformerDataFragment>({
...props, ...props,
filterMode: FilterMode.Performers, filterMode: FilterMode.Performers,

View File

@@ -1,7 +1,7 @@
/* eslint-disable consistent-return */ /* eslint-disable consistent-return */
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { DurationUtils } from 'src/utils'; import { DurationUtils } from "src/utils";
import { ILabeledId, ILabeledValue } from "../types"; import { ILabeledId, ILabeledValue } from "../types";
export type CriterionType = export type CriterionType =
@@ -38,7 +38,8 @@ export abstract class Criterion<Option = any, Value = any> {
return "Rating"; return "Rating";
case "resolution": case "resolution":
return "Resolution"; return "Resolution";
case "duration": return "Duration"; case "duration":
return "Duration";
case "favorite": case "favorite":
return "Favorite"; return "Favorite";
case "hasMarkers": case "hasMarkers":
@@ -149,7 +150,10 @@ export abstract class Criterion<Option = any, Value = any> {
let valueString = ""; let valueString = "";
if (this.modifier !== CriterionModifier.IsNull && this.modifier !== CriterionModifier.NotNull) { if (
this.modifier !== CriterionModifier.IsNull &&
this.modifier !== CriterionModifier.NotNull
) {
valueString = this.getLabelValue(); valueString = this.getLabelValue();
} }
@@ -268,12 +272,12 @@ export class DurationCriterion extends Criterion<number, number> {
Criterion.getModifierOption(CriterionModifier.Equals), Criterion.getModifierOption(CriterionModifier.Equals),
Criterion.getModifierOption(CriterionModifier.NotEquals), Criterion.getModifierOption(CriterionModifier.NotEquals),
Criterion.getModifierOption(CriterionModifier.GreaterThan), Criterion.getModifierOption(CriterionModifier.GreaterThan),
Criterion.getModifierOption(CriterionModifier.LessThan), Criterion.getModifierOption(CriterionModifier.LessThan)
]; ];
public options: number[] | undefined; public options: number[] | undefined;
public value: number = 0; public value: number = 0;
constructor(type : CriterionType, parameterName?: string, options? : number[]) { constructor(type: CriterionType, parameterName?: string, options?: number[]) {
super(); super();
this.type = type; this.type = type;

View File

@@ -2,10 +2,7 @@ import * as GQL from "src/core/generated-graphql";
import { ILabeledId } from "../types"; import { ILabeledId } from "../types";
import { Criterion, CriterionType, ICriterionOption } from "./criterion"; import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class TagsCriterion extends Criterion< export class TagsCriterion extends Criterion<GQL.Tag, ILabeledId[]> {
GQL.Tag,
ILabeledId[]
> {
public type: CriterionType; public type: CriterionType;
public parameterName: string; public parameterName: string;
public modifier = GQL.CriterionModifier.IncludesAll; public modifier = GQL.CriterionModifier.IncludesAll;

View File

@@ -25,7 +25,8 @@ export function makeCriteria(type: CriterionType = "none") {
return new RatingCriterion(); return new RatingCriterion();
case "resolution": case "resolution":
return new ResolutionCriterion(); return new ResolutionCriterion();
case "duration": return new DurationCriterion(type, type); case "duration":
return new DurationCriterion(type, type);
case "favorite": case "favorite":
return new FavoriteCriterion(); return new FavoriteCriterion();
case "hasMarkers": case "hasMarkers":
@@ -41,7 +42,8 @@ export function makeCriteria(type: CriterionType = "none") {
case "studios": case "studios":
return new StudiosCriterion(); return new StudiosCriterion();
case "birth_year": return new NumberCriterion(type, type); case "birth_year":
return new NumberCriterion(type, type);
case "age": { case "age": {
const ret = new NumberCriterion(type, type); const ret = new NumberCriterion(type, type);
// null/not null doesn't make sense for these criteria // null/not null doesn't make sense for these criteria

View File

@@ -299,7 +299,10 @@ export class ListFilterModel {
} }
case "duration": { case "duration": {
const durationCrit = criterion as DurationCriterion; const durationCrit = criterion as DurationCriterion;
result.duration = { value: durationCrit.value, modifier: durationCrit.modifier } result.duration = {
value: durationCrit.value,
modifier: durationCrit.modifier
};
break; break;
} }
case "hasMarkers": case "hasMarkers":

View File

@@ -1,6 +1,6 @@
import TextUtils from "./text"; import TextUtils from "./text";
const secondsToString = (seconds : number) => { const secondsToString = (seconds: number) => {
let ret = TextUtils.secondsToTimestamp(seconds); let ret = TextUtils.secondsToTimestamp(seconds);
if (ret.startsWith("00:")) { if (ret.startsWith("00:")) {
@@ -12,9 +12,9 @@ const secondsToString = (seconds : number) => {
} }
return ret; return ret;
} };
const stringToSeconds = (v : string) => { const stringToSeconds = (v: string) => {
if (!v) { if (!v) {
return 0; return 0;
} }
@@ -27,7 +27,7 @@ const stringToSeconds = (v : string) => {
let seconds = 0; let seconds = 0;
let factor = 1; let factor = 1;
while(splits.length > 0) { while (splits.length > 0) {
const thisSplit = splits.pop(); const thisSplit = splits.pop();
if (thisSplit === undefined) { if (thisSplit === undefined) {
return 0; return 0;
@@ -43,7 +43,7 @@ const stringToSeconds = (v : string) => {
} }
return seconds; return seconds;
} };
export default { export default {
secondsToString, secondsToString,

View File

@@ -2,4 +2,4 @@ export { default as ImageUtils } from "./image";
export { default as NavUtils } from "./navigation"; export { default as NavUtils } from "./navigation";
export { default as TableUtils } from "./table"; export { default as TableUtils } from "./table";
export { default as TextUtils } from "./text"; export { default as TextUtils } from "./text";
export { default as DurationUtils } from './duration'; export { default as DurationUtils } from "./duration";

View File

@@ -55,8 +55,8 @@ const renderInputGroup = (options: {
placeholder?: string; placeholder?: string;
value: string | undefined; value: string | undefined;
isEditing: boolean; isEditing: boolean;
asURL?: boolean, asURL?: boolean;
urlPrefix?: string, urlPrefix?: string;
onChange: (value: string) => void; onChange: (value: string) => void;
}) => ( }) => (
<tr> <tr>

View File

@@ -42,7 +42,7 @@ const fileNameFromPath = (path: string) => {
return path.replace(/^.*[\\/]/, ""); return path.replace(/^.*[\\/]/, "");
}; };
const getAge = (dateString?: string|null, fromDateString?: string) => { const getAge = (dateString?: string | null, fromDateString?: string) => {
if (!dateString) return 0; if (!dateString) return 0;
const birthdate = new Date(dateString); const birthdate = new Date(dateString);