import { Tab, Nav, Dropdown, Button, ButtonGroup } from "react-bootstrap"; import queryString from "query-string"; import React, { useEffect, useState, useMemo, useContext, lazy, useRef, } 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 { ErrorMessage, LoadingIndicator, Icon, Counter, } from "src/components/Shared"; 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; setTimestamp: (num: number) => void; queueScenes: QueuedScene[]; onQueueNext: () => void; onQueuePrevious: () => void; onQueueRandom: () => void; onDelete: () => void; continuePlaylist: boolean; loadScene: (sceneID: string) => void; queueHasMoreScenes: () => boolean; onQueueMoreScenes: () => void; onQueueLessScenes: () => void; queueStart: number; collapsed: boolean; setCollapsed: (state: boolean) => void; setContinuePlaylist: (value: boolean) => void; } const ScenePage: React.FC = ({ scene, setTimestamp, queueScenes, onQueueNext, onQueuePrevious, onQueueRandom, onDelete, continuePlaylist, loadScene, queueHasMoreScenes, onQueueMoreScenes, onQueueLessScenes, queueStart, collapsed, setCollapsed, setContinuePlaylist, }) => { 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) { onDelete(); } } function maybeRenderDeleteDialog() { if (isDeleteAlertOpen) { return ( ); } } function maybeRenderSceneGenerateDialog() { if (isGenerateDialogOpen) { return ( { setIsGenerateDialogOpen(false); }} /> ); } } const renderOperations = () => ( {!!scene.files.length && ( onRescan()} > )} setIsGenerateDialogOpen(true)} > onGenerateScreenshot(getPlayerPosition())} > onGenerateScreenshot()} > {boxes.length > 0 && ( setShowDraftModal(true)} > )} setIsDeleteAlertOpen(true)} > ); const renderTabs = () => ( k && setActiveTabKey(k)} >
{scene.galleries.length === 1 && ( )} {scene.galleries.length > 1 && ( )} setIsDeleteAlertOpen(true)} />
); 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, error } = useFindScene(id ?? ""); const queryParams = useMemo( () => queryString.parse(location.search, { decode: false }), [location.search] ); const sceneQueue = useMemo( () => SceneQueue.fromQueryParameters(queryParams), [queryParams] ); const queryContinue = useMemo(() => { let cont = queryParams.continue; if (cont !== undefined) { return cont === "true"; } else { return !!configuration?.interface.continuePlaylistDefault; } }, [configuration?.interface.continuePlaylistDefault, queryParams.continue]); const [queueScenes, setQueueScenes] = useState([]); const [collapsed, setCollapsed] = useState(false); const [continuePlaylist, setContinuePlaylist] = useState(queryContinue); const [hideScrubber, setHideScrubber] = useState( !(configuration?.interface.showScrubber ?? true) ); const _setTimestamp = useRef<(value: number) => void>(); const initialTimestamp = useMemo(() => { const t = Array.isArray(queryParams.t) ? queryParams.t[0] : queryParams.t; return Number.parseInt(t ?? "0", 10); }, [queryParams]); const [queueTotal, setQueueTotal] = useState(0); const [queueStart, setQueueStart] = useState(1); const autoplay = queryParams.autoplay === "true"; const currentQueueIndex = queueScenes ? queueScenes.findIndex((s) => s.id === id) : -1; function getSetTimestamp(fn: (value: number) => void) { _setTimestamp.current = fn; } function setTimestamp(value: number) { if (_setTimestamp.current) { _setTimestamp.current(value); } } // set up hotkeys useEffect(() => { Mousetrap.bind(".", () => setHideScrubber((value) => !value)); return () => { Mousetrap.unbind("."); }; }, []); 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 loadScene(sceneID: string, autoPlay?: boolean, newPage?: number) { const sceneLink = sceneQueue.makeLink(sceneID, { newPage, autoPlay, continue: continuePlaylist, }); history.replace(sceneLink); } function onDelete() { if ( continuePlaylist && queueScenes && currentQueueIndex >= 0 && currentQueueIndex < queueScenes.length - 1 ) { loadScene(queueScenes[currentQueueIndex + 1].id); } else { history.push("/scenes"); } } function onQueueNext() { if (!queueScenes) return; if (currentQueueIndex >= 0 && currentQueueIndex < queueScenes.length - 1) { loadScene(queueScenes[currentQueueIndex + 1].id); } } function onQueuePrevious() { if (!queueScenes) return; if (currentQueueIndex > 0) { loadScene(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 loadScene(sceneID, undefined, page); } } else { const index = Math.floor(Math.random() * queueTotal); loadScene(queueScenes[index].id); } } function onComplete() { if (!queueScenes) return; // load the next scene if we're continuing if (continuePlaylist) { if ( currentQueueIndex >= 0 && currentQueueIndex < queueScenes.length - 1 ) { loadScene(queueScenes[currentQueueIndex + 1].id, true); } } } function onNext() { if (!queueScenes) return; if (currentQueueIndex >= 0 && currentQueueIndex < queueScenes.length - 1) { loadScene(queueScenes[currentQueueIndex + 1].id, true); } } function onPrevious() { if (!queueScenes) return; if (currentQueueIndex > 0) { loadScene(queueScenes[currentQueueIndex - 1].id, true); } } if (loading) return ; if (error) return ; const scene = data?.findScene; if (!scene) return ; return (
); }; export default SceneLoader;