/* eslint-disable react/no-this-in-sfc */ import React, { useEffect, useState } from "react"; import { Button, Dropdown, DropdownButton, Form, Table } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; import { queryScrapeScene, queryScrapeSceneURL, useListSceneScrapers, useSceneUpdate, useSceneDestroy, mutateReloadScrapers, } from "src/core/StashService"; import { PerformerSelect, TagSelect, StudioSelect, SceneGallerySelect, Modal, Icon, LoadingIndicator, ImageInput, } from "src/components/Shared"; import { useToast } from "src/hooks"; import { ImageUtils, TableUtils } from "src/utils"; import { MovieSelect } from "src/components/Shared/Select"; import { SceneMovieTable, MovieSceneIndexMap } from "./SceneMovieTable"; interface IProps { scene: GQL.SceneDataFragment; onUpdate: (scene: GQL.SceneDataFragment) => void; onDelete: () => void; } export const SceneEditPanel: React.FC = (props: IProps) => { const Toast = useToast(); const [title, setTitle] = useState(); const [details, setDetails] = useState(); const [url, setUrl] = useState(); const [date, setDate] = useState(); const [rating, setRating] = useState(); const [galleryId, setGalleryId] = useState(); const [studioId, setStudioId] = useState(); const [performerIds, setPerformerIds] = useState(); const [movieIds, setMovieIds] = useState(undefined); const [movieSceneIndexes, setMovieSceneIndexes] = useState< MovieSceneIndexMap >(new Map()); const [tagIds, setTagIds] = useState(); const [coverImage, setCoverImage] = useState(); const Scrapers = useListSceneScrapers(); const [queryableScrapers, setQueryableScrapers] = useState([]); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); const [deleteFile, setDeleteFile] = useState(false); const [deleteGenerated, setDeleteGenerated] = useState(true); const [coverImagePreview, setCoverImagePreview] = useState(); // Network state const [isLoading, setIsLoading] = useState(true); const [updateScene] = useSceneUpdate(getSceneInput()); const [deleteScene] = useSceneDestroy(getSceneDeleteInput()); useEffect(() => { const newQueryableScrapers = ( Scrapers?.data?.listSceneScrapers ?? [] ).filter((s) => s.scene?.supported_scrapes.includes(GQL.ScrapeType.Fragment) ); setQueryableScrapers(newQueryableScrapers); }, [Scrapers]); useEffect(() => { let changed = false; const newMap: MovieSceneIndexMap = new Map(); if (movieIds) { movieIds.forEach((id) => { if (!movieSceneIndexes.has(id)) { changed = true; newMap.set(id, undefined); } else { newMap.set(id, movieSceneIndexes.get(id)); } }); if (!changed) { movieSceneIndexes.forEach((v, id) => { if (!newMap.has(id)) { // id was removed changed = true; } }); } if (changed) { setMovieSceneIndexes(newMap); } } }, [movieIds, movieSceneIndexes]); function updateSceneEditState(state: Partial) { const perfIds = state.performers?.map((performer) => performer.id); const tIds = state.tags ? state.tags.map((tag) => tag.id) : undefined; const moviIds = state.movies ? state.movies.map((sceneMovie) => sceneMovie.movie.id) : undefined; const movieSceneIdx: MovieSceneIndexMap = new Map(); if (state.movies) { state.movies.forEach((m) => { movieSceneIdx.set(m.movie.id, m.scene_index ?? 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); setMovieIds(moviIds); setMovieSceneIndexes(movieSceneIdx); setPerformerIds(perfIds); setTagIds(tIds); } useEffect(() => { updateSceneEditState(props.scene); setCoverImagePreview(props.scene?.paths?.screenshot ?? undefined); setIsLoading(false); }, [props.scene]); const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true); function getSceneInput(): GQL.SceneUpdateInput { return { id: props.scene.id, title, details, url, date, rating, gallery_id: galleryId, studio_id: studioId, performer_ids: performerIds, movies: makeMovieInputs(), tag_ids: tagIds, cover_image: coverImage, }; } function makeMovieInputs(): GQL.SceneMovieInput[] | undefined { if (!movieIds) { return undefined; } let ret = movieIds.map((id) => { const r: GQL.SceneMovieInput = { movie_id: id, }; return r; }); ret = ret.map((r) => { return { scene_index: movieSceneIndexes.get(r.movie_id), ...r }; }); return ret; } 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 renderTableMovies() { return ( { setMovieSceneIndexes(items); }} /> ); } function renderDeleteAlert() { return ( setIsDeleteAlertOpen(false), text: "Cancel" }} >

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.

setDeleteFile(!deleteFile)} /> setDeleteGenerated(!deleteGenerated)} />
); } function onImageLoad(imageData: string) { setCoverImagePreview(imageData); setCoverImage(imageData); } function onCoverImageChange(event: React.FormEvent) { ImageUtils.onImageChange(event, onImageLoad); } async function onScrapeClicked(scraper: GQL.Scraper) { setIsLoading(true); try { const result = await queryScrapeScene(scraper.id, getSceneInput()); if (!result.data || !result.data.scrapeScene) { return; } updateSceneFromScrapedScene(result.data.scrapeScene); } catch (e) { Toast.error(e); } finally { setIsLoading(false); } } async function onReloadScrapers() { setIsLoading(true); try { await mutateReloadScrapers(); // reload the performer scrapers await Scrapers.refetch(); } catch (e) { Toast.error(e); } finally { setIsLoading(false); } } function renderScraperMenu() { return ( {queryableScrapers.map((s) => ( onScrapeClicked(s)}> {s.name} ))} onReloadScrapers()}> Reload scrapers ); } 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 ( (!movieIds || movieIds.length === 0) && scene.movies && scene.movies.length > 0 ) { const idMovis = scene.movies.filter((p) => { return p.id !== undefined && p.id !== null; }); if (idMovis.length > 0) { const newIds = idMovis.map((p) => p.id); setMovieIds(newIds as string[]); } } if (!tagIds?.length && scene?.tags?.length) { 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[]); } } if (scene.image) { // image is a base64 string setCoverImage(scene.image); setCoverImagePreview(scene.image); } } async function onScrapeSceneURL() { if (!url) { return; } setIsLoading(true); try { const result = await 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 ( ); } if (isLoading) return ; return (
{TableUtils.renderInputGroup({ title: "Title", value: title, onChange: setTitle, isEditing: true, })} {TableUtils.renderInputGroup({ title: "Date", value: date, isEditing: true, onChange: setDate, placeholder: "YYYY-MM-DD", })} {TableUtils.renderHtmlSelect({ title: "Rating", value: rating, isEditing: true, onChange: (value: string) => setRating(Number.parseInt(value, 10)), selectOptions: ["", 1, 2, 3, 4, 5], })}
URL ) => setUrl(newValue.currentTarget.value) } value={url} placeholder="URL" className="text-input" /> {maybeRenderScrapeButton()}
Gallery setGalleryId(item ? item.id : undefined)} />
Studio setStudioId(items.length > 0 ? items[0]?.id : undefined) } ids={studioId ? [studioId] : []} />
Performers setPerformerIds(items.map((item) => item.id)) } ids={performerIds} />
Movies/Scenes setMovieIds(items.map((item) => item.id)) } ids={movieIds} /> {renderTableMovies()}
Tags setTagIds(items.map((item) => item.id))} ids={tagIds} />
Details ) => setDetails(newValue.currentTarget.value) } value={details} />
Cover Image {imageEncoding ? ( ) : ( Scene cover )}
{renderScraperMenu()} {renderDeleteAlert()}
); };