import { Tab, Nav, Dropdown, Button, ButtonGroup } from "react-bootstrap"; import queryString from "query-string"; import React, { useEffect, useState, useMemo, useContext, lazy } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useParams, useLocation, useHistory, Link } from "react-router-dom"; import { Helmet } from "react-helmet"; import * as GQL from "src/core/generated-graphql"; import { mutateMetadataScan, useFindScene, useSceneIncrementO, useSceneDecrementO, useSceneResetO, useSceneGenerateScreenshot, useSceneUpdate, queryFindScenes, queryFindScenesByID, } from "src/core/StashService"; import Icon from "src/components/Shared/Icon"; import { useToast } from "src/hooks"; import SceneQueue, { QueuedScene } from "src/models/sceneQueue"; import { ListFilterModel } from "src/models/list-filter/filter"; import Mousetrap from "mousetrap"; import { OCounterButton } from "./OCounterButton"; import { OrganizedButton } from "./OrganizedButton"; import { ConfigurationContext } from "src/hooks/Config"; import { getPlayerPosition } from "src/components/ScenePlayer/util"; import { faEllipsisV } from "@fortawesome/free-solid-svg-icons"; const SubmitStashBoxDraft = lazy( () => import("src/components/Dialogs/SubmitDraft") ); const ScenePlayer = lazy( () => import("src/components/ScenePlayer/ScenePlayer") ); const GalleryViewer = lazy( () => import("src/components/Galleries/GalleryViewer") ); const ExternalPlayerButton = lazy(() => import("./ExternalPlayerButton")); const QueueViewer = lazy(() => import("./QueueViewer")); const SceneMarkersPanel = lazy(() => import("./SceneMarkersPanel")); const SceneFileInfoPanel = lazy(() => import("./SceneFileInfoPanel")); const SceneEditPanel = lazy(() => import("./SceneEditPanel")); const SceneDetailPanel = lazy(() => import("./SceneDetailPanel")); const SceneMoviePanel = lazy(() => import("./SceneMoviePanel")); const SceneGalleriesPanel = lazy(() => import("./SceneGalleriesPanel")); const DeleteScenesDialog = lazy(() => import("../DeleteScenesDialog")); const GenerateDialog = lazy(() => import("../../Dialogs/GenerateDialog")); const SceneVideoFilterPanel = lazy(() => import("./SceneVideoFilterPanel")); import { objectPath, objectTitle } from "src/core/files"; interface IProps { scene: GQL.SceneDataFragment; refetch: () => void; setTimestamp: (num: number) => void; queueScenes: QueuedScene[]; onQueueNext: () => void; onQueuePrevious: () => void; onQueueRandom: () => void; continuePlaylist: boolean; playScene: (sceneID: string, page?: number) => void; queueHasMoreScenes: () => boolean; onQueueMoreScenes: () => void; onQueueLessScenes: () => void; queueStart: number; collapsed: boolean; setCollapsed: (state: boolean) => void; setContinuePlaylist: (value: boolean) => void; } const ScenePage: React.FC = ({ scene, refetch, setTimestamp, queueScenes, onQueueNext, onQueuePrevious, onQueueRandom, continuePlaylist, playScene, queueHasMoreScenes, onQueueMoreScenes, onQueueLessScenes, queueStart, collapsed, setCollapsed, setContinuePlaylist, }) => { const history = useHistory(); const Toast = useToast(); const intl = useIntl(); const [updateScene] = useSceneUpdate(); const [generateScreenshot] = useSceneGenerateScreenshot(); const { configuration } = useContext(ConfigurationContext); const [showDraftModal, setShowDraftModal] = useState(false); const boxes = configuration?.general?.stashBoxes ?? []; 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 onIncrementClick = async () => { try { await incrementO(); } catch (e) { Toast.error(e); } }; const onDecrementClick = async () => { try { await decrementO(); } catch (e) { Toast.error(e); } }; // 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)); 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(","); }; }); 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 onResetClick = async () => { try { await resetO(); } catch (e) { Toast.error(e); } }; function onClickMarker(marker: GQL.SceneMarkerDataFragment) { setTimestamp(marker.seconds); } async function onRescan() { await mutateMetadataScan({ paths: [objectPath(scene)], }); 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" }), }); } 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(getPlayerPosition())} > onGenerateScreenshot()} > {boxes.length > 0 && ( setShowDraftModal(true)} > )} setIsDeleteAlertOpen(true)} > ); const renderTabs = () => ( k && setActiveTabKey(k)} >
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()} />
); function getCollapseButtonText() { return collapsed ? ">" : "<"; } const title = objectTitle(scene); return ( <> {title} {maybeRenderSceneGenerateDialog()} {maybeRenderDeleteDialog()}
{scene.studio && (

{`${scene.studio.name}

)}

{title}

{renderTabs()}
setShowDraftModal(false)} /> ); }; const SceneLoader: React.FC = () => { const { id } = useParams<{ id?: string }>(); const location = useLocation(); const history = useHistory(); const { configuration } = useContext(ConfigurationContext); const { data, loading, refetch } = useFindScene(id ?? ""); const [timestamp, setTimestamp] = useState(getInitialTimestamp()); const [collapsed, setCollapsed] = useState(false); const [continuePlaylist, setContinuePlaylist] = useState(false); const [showScrubber, setShowScrubber] = useState( configuration?.interface.showScrubber ?? true ); const sceneQueue = useMemo( () => SceneQueue.fromQueryParameters(location.search), [location.search] ); const [queueScenes, setQueueScenes] = useState([]); const [queueTotal, setQueueTotal] = useState(0); const [queueStart, setQueueStart] = useState(1); const queryParams = useMemo(() => queryString.parse(location.search), [ location.search, ]); function getInitialTimestamp() { const params = queryString.parse(location.search); const initialTimestamp = params?.t ?? "0"; return Number.parseInt( Array.isArray(initialTimestamp) ? initialTimestamp[0] : initialTimestamp, 10 ); } const autoplay = queryParams?.autoplay === "true"; const autoPlayOnSelected = configuration?.interface.autostartVideoOnPlaySelected ?? false; const currentQueueIndex = queueScenes ? queueScenes.findIndex((s) => s.id === id) : -1; useEffect(() => { setContinuePlaylist(queryParams?.continue === "true"); }, [queryParams]); // set up hotkeys useEffect(() => { Mousetrap.bind(".", () => setShowScrubber(!showScrubber)); return () => { Mousetrap.unbind("."); }; }); useEffect(() => { // reset timestamp after notifying player if (timestamp !== -1) setTimestamp(-1); }, [timestamp]); 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(() => { if (sceneQueue.query) { getQueueFilterScenes(sceneQueue.query); } else if (sceneQueue.sceneIDs) { getQueueScenes(sceneQueue.sceneIDs); } }, [sceneQueue]); 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 as QueuedScene[]).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 as QueuedScene[]).concat(queueScenes); setQueueScenes(newScenes); // don't change queue start } function playScene(sceneID: string, newPage?: number) { sceneQueue.playScene(history, sceneID, { newPage, autoPlay: autoPlayOnSelected, continue: continuePlaylist, }); } function onQueueNext() { if (!queueScenes) return; if (currentQueueIndex >= 0 && currentQueueIndex < queueScenes.length - 1) { playScene(queueScenes[currentQueueIndex + 1].id); } } function onQueuePrevious() { if (!queueScenes) return; if (currentQueueIndex > 0) { playScene(queueScenes[currentQueueIndex - 1].id); } } async function onQueueRandom() { if (!queueScenes) return; 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(); } } /* if (error) return ; if (!loading && !data?.findScene) return ; */ const scene = data?.findScene; return (
{!loading && scene ? ( ) : (
)}
= 0 && currentQueueIndex < queueScenes.length - 1 ? onQueueNext : undefined } onPrevious={currentQueueIndex > 0 ? onQueuePrevious : undefined} />
); }; export default SceneLoader;