import { Tab, Nav, Dropdown, Button, ButtonGroup } from "react-bootstrap"; import queryString from "query-string"; import React, { useEffect, useState, useMemo } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useParams, useLocation, useHistory, Link } from "react-router-dom"; import { Helmet } from "react-helmet"; import Mousetrap from "mousetrap"; import * as GQL from "src/core/generated-graphql"; import { mutateMetadataScan, useFindScene, useSceneIncrementO, useSceneDecrementO, useSceneResetO, useSceneStreams, useSceneGenerateScreenshot, useSceneUpdate, queryFindScenes, queryFindScenesByID, } from "src/core/StashService"; import { GalleryViewer } from "src/components/Galleries/GalleryViewer"; import { ErrorMessage, LoadingIndicator, Icon } from "src/components/Shared"; import { useToast } from "src/hooks"; import { ScenePlayer } from "src/components/ScenePlayer"; import { TextUtils, JWUtils } from "src/utils"; import { SubmitStashBoxDraft } from "src/components/Dialogs/SubmitDraft"; import { ListFilterModel } from "src/models/list-filter/filter"; import { SceneQueue } from "src/models/sceneQueue"; import { QueueViewer } from "./QueueViewer"; import { SceneMarkersPanel } from "./SceneMarkersPanel"; import { SceneFileInfoPanel } from "./SceneFileInfoPanel"; import { SceneEditPanel } from "./SceneEditPanel"; import { SceneDetailPanel } from "./SceneDetailPanel"; import { OCounterButton } from "./OCounterButton"; import { ExternalPlayerButton } from "./ExternalPlayerButton"; import { SceneMoviePanel } from "./SceneMoviePanel"; import { SceneGalleriesPanel } from "./SceneGalleriesPanel"; import { DeleteScenesDialog } from "../DeleteScenesDialog"; import { GenerateDialog } from "../../Dialogs/GenerateDialog"; import { SceneVideoFilterPanel } from "./SceneVideoFilterPanel"; import { OrganizedButton } from "./OrganizedButton"; interface IProps { scene: GQL.SceneDataFragment; refetch: () => void; } const ScenePage: React.FC = ({ scene, refetch }) => { const location = useLocation(); const history = useHistory(); const Toast = useToast(); const intl = useIntl(); const [updateScene] = useSceneUpdate(); const [generateScreenshot] = useSceneGenerateScreenshot(); const [timestamp, setTimestamp] = useState(getInitialTimestamp()); const [collapsed, setCollapsed] = useState(false); const [showScrubber, setShowScrubber] = useState(true); const { data } = GQL.useConfigurationQuery(); const [showDraftModal, setShowDraftModal] = useState(false); const boxes = data?.configuration?.general?.stashBoxes ?? []; const { data: sceneStreams, error: streamableError, loading: streamableLoading, } = useSceneStreams(scene.id); const [oLoading, setOLoading] = useState(false); const [incrementO] = useSceneIncrementO(scene.id); const [decrementO] = useSceneDecrementO(scene.id); const [resetO] = useSceneResetO(scene.id); const [organizedLoading, setOrganizedLoading] = useState(false); const [activeTabKey, setActiveTabKey] = useState("scene-details-panel"); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false); const [sceneQueue, setSceneQueue] = useState(new SceneQueue()); const [queueScenes, setQueueScenes] = useState( [] ); const [queueTotal, setQueueTotal] = useState(0); const [queueStart, setQueueStart] = useState(1); const [continuePlaylist, setContinuePlaylist] = useState(false); const [rerenderPlayer, setRerenderPlayer] = useState(false); const queryParams = useMemo(() => queryString.parse(location.search), [ location.search, ]); const autoplay = queryParams?.autoplay === "true"; const currentQueueIndex = queueScenes.findIndex((s) => s.id === scene.id); async function getQueueFilterScenes(filter: ListFilterModel) { const query = await queryFindScenes(filter); const { scenes, count } = query.data.findScenes; setQueueScenes(scenes); setQueueTotal(count); setQueueStart((filter.currentPage - 1) * filter.itemsPerPage + 1); } async function getQueueScenes(sceneIDs: number[]) { const query = await queryFindScenesByID(sceneIDs); const { scenes, count } = query.data.findScenes; setQueueScenes(scenes); setQueueTotal(count); setQueueStart(1); } useEffect(() => { setContinuePlaylist(queryParams?.continue === "true"); }, [queryParams]); // HACK - jwplayer doesn't handle re-rendering when scene changes, so force // a rerender by not drawing it useEffect(() => { if (rerenderPlayer) { setRerenderPlayer(false); } }, [rerenderPlayer]); useEffect(() => { setRerenderPlayer(true); }, [scene.id]); useEffect(() => { setSceneQueue(SceneQueue.fromQueryParameters(location.search)); }, [location.search]); useEffect(() => { if (sceneQueue.query) { getQueueFilterScenes(sceneQueue.query); } else if (sceneQueue.sceneIDs) { getQueueScenes(sceneQueue.sceneIDs); } }, [sceneQueue]); function getInitialTimestamp() { const params = queryString.parse(location.search); const initialTimestamp = params?.t ?? "0"; return Number.parseInt( Array.isArray(initialTimestamp) ? initialTimestamp[0] : initialTimestamp, 10 ); } const onOrganizedClick = async () => { try { setOrganizedLoading(true); await updateScene({ variables: { input: { id: scene.id, organized: !scene.organized, }, }, }); } catch (e) { Toast.error(e); } finally { setOrganizedLoading(false); } }; const onIncrementClick = async () => { try { setOLoading(true); await incrementO(); } catch (e) { Toast.error(e); } finally { setOLoading(false); } }; const onDecrementClick = async () => { try { setOLoading(true); await decrementO(); } catch (e) { Toast.error(e); } finally { setOLoading(false); } }; const onResetClick = async () => { try { setOLoading(true); await resetO(); } catch (e) { Toast.error(e); } finally { setOLoading(false); } }; function onClickMarker(marker: GQL.SceneMarkerDataFragment) { setTimestamp(marker.seconds); } async function onRescan() { await mutateMetadataScan({ paths: [scene.path], }); Toast.success({ content: intl.formatMessage( { id: "toast.rescanning_entity" }, { count: 1, singularEntity: intl .formatMessage({ id: "scene" }) .toLocaleLowerCase(), } ), }); } async function onGenerateScreenshot(at?: number) { await generateScreenshot({ variables: { id: scene.id, at, }, }); Toast.success({ content: intl.formatMessage({ id: "toast.generating_screenshot" }), }); } async function onQueueLessScenes() { if (!sceneQueue.query || queueStart <= 1) { return; } const filterCopy = sceneQueue.query.clone(); const newStart = queueStart - filterCopy.itemsPerPage; filterCopy.currentPage = Math.ceil(newStart / filterCopy.itemsPerPage); const query = await queryFindScenes(filterCopy); const { scenes } = query.data.findScenes; // prepend scenes to scene list const newScenes = scenes.concat(queueScenes); setQueueScenes(newScenes); setQueueStart(newStart); } function queueHasMoreScenes() { return queueStart + queueScenes.length - 1 < queueTotal; } async function onQueueMoreScenes() { if (!sceneQueue.query || !queueHasMoreScenes()) { return; } const filterCopy = sceneQueue.query.clone(); const newStart = queueStart + queueScenes.length; filterCopy.currentPage = Math.ceil(newStart / filterCopy.itemsPerPage); const query = await queryFindScenes(filterCopy); const { scenes } = query.data.findScenes; // append scenes to scene list const newScenes = scenes.concat(queueScenes); setQueueScenes(newScenes); // don't change queue start } function playScene(sceneID: string, page?: number) { sceneQueue.playScene(history, sceneID, { newPage: page, autoPlay: true, continue: continuePlaylist, }); } function onQueueNext() { if (currentQueueIndex >= 0 && currentQueueIndex < queueScenes.length - 1) { playScene(queueScenes[currentQueueIndex + 1].id); } } function onQueuePrevious() { if (currentQueueIndex > 0) { playScene(queueScenes[currentQueueIndex - 1].id); } } async function onQueueRandom() { if (sceneQueue.query) { const { query } = sceneQueue; const pages = Math.ceil(queueTotal / query.itemsPerPage); const page = Math.floor(Math.random() * pages) + 1; const index = Math.floor( Math.random() * Math.min(query.itemsPerPage, queueTotal) ); const filterCopy = sceneQueue.query.clone(); filterCopy.currentPage = page; const queryResults = await queryFindScenes(filterCopy); if (queryResults.data.findScenes.scenes.length > index) { const { id: sceneID } = queryResults!.data!.findScenes!.scenes[index]; // navigate to the image player page playScene(sceneID, page); } } else { const index = Math.floor(Math.random() * queueTotal); playScene(queueScenes[index].id); } } function onComplete() { // load the next scene if we're autoplaying if (continuePlaylist) { onQueueNext(); } } function onDeleteDialogClosed(deleted: boolean) { setIsDeleteAlertOpen(false); if (deleted) { history.push("/scenes"); } } function maybeRenderDeleteDialog() { if (isDeleteAlertOpen) { return ( ); } } function maybeRenderSceneGenerateDialog() { if (isGenerateDialogOpen) { return ( { setIsGenerateDialogOpen(false); }} /> ); } } const renderOperations = () => ( onRescan()} > setIsGenerateDialogOpen(true)} > onGenerateScreenshot(JWUtils.getPlayer().getPosition()) } > onGenerateScreenshot()} > {boxes.length > 0 && ( setShowDraftModal(true)} > )} setIsDeleteAlertOpen(true)} > ); const renderTabs = () => ( k && setActiveTabKey(k)} >
setContinuePlaylist(v)} onSceneClicked={(sceneID) => playScene(sceneID)} onNext={onQueueNext} onPrevious={onQueuePrevious} onRandom={onQueueRandom} start={queueStart} hasMoreScenes={queueHasMoreScenes()} onLessScenes={() => onQueueLessScenes()} onMoreScenes={() => onQueueMoreScenes()} /> {scene.galleries.length === 1 && ( )} {scene.galleries.length > 1 && ( )} setIsDeleteAlertOpen(true)} onUpdate={() => refetch()} />
); // set up hotkeys useEffect(() => { Mousetrap.bind("a", () => setActiveTabKey("scene-details-panel")); Mousetrap.bind("q", () => setActiveTabKey("scene-queue-panel")); Mousetrap.bind("e", () => setActiveTabKey("scene-edit-panel")); Mousetrap.bind("k", () => setActiveTabKey("scene-markers-panel")); Mousetrap.bind("i", () => setActiveTabKey("scene-file-info-panel")); Mousetrap.bind("o", () => onIncrementClick()); Mousetrap.bind("p n", () => onQueueNext()); Mousetrap.bind("p p", () => onQueuePrevious()); Mousetrap.bind("p r", () => onQueueRandom()); Mousetrap.bind(",", () => setCollapsed(!collapsed)); Mousetrap.bind(".", () => setShowScrubber(!showScrubber)); return () => { Mousetrap.unbind("a"); Mousetrap.unbind("q"); Mousetrap.unbind("e"); Mousetrap.unbind("k"); Mousetrap.unbind("i"); Mousetrap.unbind("o"); Mousetrap.unbind("p n"); Mousetrap.unbind("p p"); Mousetrap.unbind("p r"); Mousetrap.unbind(","); Mousetrap.unbind("."); }; }); function getCollapseButtonText() { return collapsed ? ">" : "<"; } if (streamableLoading) return ; if (streamableError) return ; return (
{scene.title ?? TextUtils.fileNameFromPath(scene.path)} {maybeRenderSceneGenerateDialog()} {maybeRenderDeleteDialog()}
{scene.studio && (

{`${scene.studio.name}

)}

{scene.title ?? TextUtils.fileNameFromPath(scene.path)}

{renderTabs()}
{!rerenderPlayer ? ( ) : undefined}
setShowDraftModal(false)} />
); }; const SceneLoader: React.FC = () => { const { id } = useParams<{ id?: string }>(); const { data, loading, error, refetch } = useFindScene(id ?? ""); if (loading) return ; if (error) return ; if (!data?.findScene) return ; return ; }; export default SceneLoader;