Files
stash/ui/v2.5/src/components/scenes/SceneDetails/SceneEditPanel.tsx
2020-03-01 21:04:33 +01:00

447 lines
12 KiB
TypeScript

/* eslint-disable react/no-this-in-sfc */
import React, { useEffect, useState } from "react";
import {
Collapse,
Dropdown,
DropdownButton,
Form,
Button,
Spinner
} from "react-bootstrap";
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import {
FilterSelect,
StudioSelect,
SceneGallerySelect,
Modal,
Icon
} from "src/components/Shared";
import { useToast } from "src/hooks";
import { ImageUtils } from "src/utils";
interface IProps {
scene: GQL.SceneDataFragment;
onUpdate: (scene: GQL.SceneDataFragment) => void;
onDelete: () => void;
}
export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
const Toast = useToast();
const [title, setTitle] = useState<string>();
const [details, setDetails] = useState<string>();
const [url, setUrl] = useState<string>();
const [date, setDate] = useState<string>();
const [rating, setRating] = useState<number>();
const [galleryId, setGalleryId] = useState<string>();
const [studioId, setStudioId] = useState<string>();
const [performerIds, setPerformerIds] = useState<string[]>();
const [tagIds, setTagIds] = useState<string[]>();
const [coverImage, setCoverImage] = useState<string>();
const Scrapers = StashService.useListSceneScrapers();
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
const [deleteFile, setDeleteFile] = useState<boolean>(false);
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
const [isCoverImageOpen, setIsCoverImageOpen] = useState<boolean>(false);
const [coverImagePreview, setCoverImagePreview] = useState<string>();
// Network state
const [isLoading, setIsLoading] = useState(false);
const [updateScene] = StashService.useSceneUpdate(getSceneInput());
const [deleteScene] = StashService.useSceneDestroy(getSceneDeleteInput());
useEffect(() => {
const newQueryableScrapers = (Scrapers?.data?.listSceneScrapers ?? []).filter(s => (
s.scene?.supported_scrapes.includes(GQL.ScrapeType.Fragment)
));
setQueryableScrapers(newQueryableScrapers);
}, [Scrapers]);
function updateSceneEditState(state: Partial<GQL.SceneDataFragment>) {
const perfIds = state.performers
? state.performers.map(performer => performer.id)
: undefined;
const tIds = state.tags ? state.tags.map(tag => tag.id) : undefined;
setTitle(state.title ?? undefined);
setDetails(state.details ?? undefined);
setUrl(state.url ?? undefined);
setDate(state.date ?? undefined);
setRating(state.rating === null ? NaN : state.rating);
setGalleryId(state?.gallery?.id ?? undefined);
setStudioId(state?.studio?.id ?? undefined);
setPerformerIds(perfIds);
setTagIds(tIds);
}
useEffect(() => {
updateSceneEditState(props.scene);
setCoverImagePreview(props.scene?.paths?.screenshot ?? undefined);
}, [props.scene]);
ImageUtils.usePasteImage(onImageLoad);
function getSceneInput(): GQL.SceneUpdateInput {
return {
id: props.scene.id,
title,
details,
url,
date,
rating,
gallery_id: galleryId,
studio_id: studioId,
performer_ids: performerIds,
tag_ids: tagIds,
cover_image: coverImage
};
}
async function onSave() {
setIsLoading(true);
try {
const result = await updateScene();
if(result.data?.sceneUpdate) {
props.onUpdate(result.data.sceneUpdate);
Toast.success({ content: "Updated scene" });
}
} catch (e) {
Toast.error(e);
}
setIsLoading(false);
}
function getSceneDeleteInput(): GQL.SceneDestroyInput {
return {
id: props.scene.id,
delete_file: deleteFile,
delete_generated: deleteGenerated
};
}
async function onDelete() {
setIsDeleteAlertOpen(false);
setIsLoading(true);
try {
await deleteScene();
Toast.success({ content: "Deleted scene" });
} catch (e) {
Toast.error(e);
}
setIsLoading(false);
props.onDelete();
}
function renderMultiSelect(
type: "performers" | "tags",
initialIds: string[] = []
) {
return (
<FilterSelect
type={type}
isMulti
onSelect={items => {
const ids = items.map(i => i.id);
switch (type) {
case "performers":
setPerformerIds(ids);
break;
case "tags":
setTagIds(ids);
break;
}
}}
initialIds={initialIds}
/>
);
}
function renderDeleteAlert() {
return (
<Modal
show={isDeleteAlertOpen}
icon="trash-alt"
header="Delete Scene?"
accept={{ variant: "danger", onClick: onDelete, text: "Delete" }}
cancel={{ onClick: () => setIsDeleteAlertOpen(false), text: "Cancel" }}
>
<p>
Are you sure you want to delete this scene? Unless the file is also
deleted, this scene will be re-added when scan is performed.
</p>
<Form>
<Form.Check
checked={deleteFile}
label="Delete file"
onChange={() => setDeleteFile(!deleteFile)}
/>
<Form.Check
checked={deleteGenerated}
label="Delete generated supporting files"
onChange={() => setDeleteGenerated(!deleteGenerated)}
/>
</Form>
</Modal>
);
}
function onImageLoad(this: FileReader) {
setCoverImagePreview(this.result as string);
setCoverImage(this.result as string);
}
function onCoverImageChange(event: React.FormEvent<HTMLInputElement>) {
ImageUtils.onImageChange(event, onImageLoad);
}
async function onScrapeClicked(
scraper: GQL.Scraper
) {
setIsLoading(true);
try {
const result = await StashService.queryScrapeScene(
scraper.id,
getSceneInput()
);
if (!result.data || !result.data.scrapeScene) {
return;
}
updateSceneFromScrapedScene(result.data.scrapeScene);
} catch (e) {
Toast.error(e);
} finally {
setIsLoading(false);
}
}
function renderScraperMenu() {
if (!queryableScrapers || queryableScrapers.length === 0) {
return;
}
return (
<DropdownButton id="scene-scrape" title="Scrape with...">
{queryableScrapers.map(s => (
<Dropdown.Item onClick={() => onScrapeClicked(s)}>
{s.name}
</Dropdown.Item>
))}
</DropdownButton>
);
}
function urlScrapable(scrapedUrl: string): boolean {
return (Scrapers?.data?.listSceneScrapers ?? []).some(s =>
(s?.scene?.urls ?? []).some(u => scrapedUrl.includes(u))
);
}
function updateSceneFromScrapedScene(scene: GQL.ScrapedSceneDataFragment) {
if (!title && scene.title) {
setTitle(scene.title);
}
if (!details && scene.details) {
setDetails(scene.details);
}
if (!date && scene.date) {
setDate(scene.date);
}
if (!url && scene.url) {
setUrl(scene.url);
}
if (!studioId && scene.studio && scene.studio.id) {
setStudioId(scene.studio.id);
}
if (
(!performerIds || performerIds.length === 0) &&
scene.performers &&
scene.performers.length > 0
) {
const idPerfs = scene.performers.filter(p => {
return p.id !== undefined && p.id !== null;
});
if (idPerfs.length > 0) {
const newIds = idPerfs.map(p => p.id);
setPerformerIds(newIds as string[]);
}
}
if (
(!tagIds || tagIds.length === 0) &&
scene.tags &&
scene.tags.length > 0
) {
const idTags = scene.tags.filter(p => {
return p.id !== undefined && p.id !== null;
});
if (idTags.length > 0) {
const newIds = idTags.map(p => p.id);
setTagIds(newIds as string[]);
}
}
}
async function onScrapeSceneURL() {
if (!url) {
return;
}
setIsLoading(true);
try {
const result = await StashService.queryScrapeSceneURL(url);
if (!result.data || !result.data.scrapeSceneURL) {
return;
}
updateSceneFromScrapedScene(result.data.scrapeSceneURL);
} catch (e) {
Toast.error(e);
} finally {
setIsLoading(false);
}
}
function maybeRenderScrapeButton() {
if (!url || !urlScrapable(url)) {
return undefined;
}
return (
<Button id="scrape-url-button" onClick={onScrapeSceneURL}>
<Icon icon="file-download" />
</Button>
);
}
return (
<>
{renderDeleteAlert()}
{isLoading ? <Spinner animation="border" variant="light" /> : undefined}
<div className="form-container " style={{ width: "50%" }}>
<Form.Group controlId="title">
<Form.Label>Title</Form.Label>
<Form.Control
onChange={(newValue: any) => setTitle(newValue.target.value)}
value={title}
/>
</Form.Group>
<Form.Group controlId="details">
<Form.Label>Details</Form.Label>
<Form.Control
as="textarea"
onChange={(newValue: any) => setDetails(newValue.target.value)}
value={details}
/>
</Form.Group>
<Form.Group controlId="url">
<Form.Label>URL</Form.Label>
<Form.Control
onChange={(newValue: any) => setUrl(newValue.target.value)}
value={url}
/>
{maybeRenderScrapeButton()}
</Form.Group>
<Form.Group controlId="date">
<Form.Label>Date</Form.Label>
<Form.Control
onChange={(newValue: any) => setDate(newValue.target.value)}
value={date}
/>
<div>YYYY-MM-DD</div>
</Form.Group>
<Form.Group controlId="rating">
<Form.Label>Rating</Form.Label>
<Form.Control
as="select"
onChange={(event: any) =>
setRating(parseInt(event.target.value, 10))
}
>
{["", 1, 2, 3, 4, 5].map(opt => (
<option selected={opt === rating} value={opt}>
{opt}
</option>
))}
</Form.Control>
</Form.Group>
<Form.Group controlId="gallery">
<Form.Label>Gallery</Form.Label>
<SceneGallerySelect
sceneId={props.scene.id}
initialId={galleryId}
onSelect={item => setGalleryId(item ? item.id : undefined)}
/>
</Form.Group>
<Form.Group controlId="studio">
<Form.Label>Studio</Form.Label>
<StudioSelect
onSelect={items => items.length && setStudioId(items[0]?.id)}
initialIds={studioId ? [studioId] : []}
/>
</Form.Group>
<Form.Group controlId="performers">
<Form.Label>Performers</Form.Label>
{renderMultiSelect("performers", performerIds)}
</Form.Group>
<Form.Group controlId="tags">
<Form.Label>Tags</Form.Label>
{renderMultiSelect("tags", tagIds)}
</Form.Group>
<div>
<Button
variant="link"
onClick={() => setIsCoverImageOpen(!isCoverImageOpen)}
>
<Icon icon={isCoverImageOpen ? "chevron-down" : "chevron-right"} />
<span>Cover Image</span>
</Button>
<Collapse in={isCoverImageOpen}>
<div>
<img className="scene-cover" src={coverImagePreview} alt="" />
<Form.Group className="test" controlId="cover">
<Form.Control
type="file"
onChange={onCoverImageChange}
accept=".jpg,.jpeg,.png"
/>
</Form.Group>
</div>
</Collapse>
</div>
</div>
<Button className="edit-button" variant="primary" onClick={onSave}>
Save
</Button>
<Button
className="edit-button"
variant="danger"
onClick={() => setIsDeleteAlertOpen(true)}
>
Delete
</Button>
{renderScraperMenu()}
</>
);
};