import { Tabs, Tab, Form } from "react-bootstrap"; import React, { useEffect, useMemo, useState } from "react"; import { useHistory, Redirect, RouteComponentProps } from "react-router-dom"; import { FormattedMessage, useIntl } from "react-intl"; import { Helmet } from "react-helmet"; import cx from "classnames"; import Mousetrap from "mousetrap"; import * as GQL from "src/core/generated-graphql"; import { useFindStudio, useStudioUpdate, useStudioDestroy, mutateMetadataAutoTag, } from "src/core/StashService"; import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; import { ModalComponent } from "src/components/Shared/Modal"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { ErrorMessage } from "src/components/Shared/ErrorMessage"; import { useToast } from "src/hooks/Toast"; import { ConfigurationContext } from "src/hooks/Config"; import { StudioScenesPanel } from "./StudioScenesPanel"; import { StudioGalleriesPanel } from "./StudioGalleriesPanel"; import { StudioImagesPanel } from "./StudioImagesPanel"; import { StudioChildrenPanel } from "./StudioChildrenPanel"; import { StudioPerformersPanel } from "./StudioPerformersPanel"; import { StudioEditPanel } from "./StudioEditPanel"; import { CompressedStudioDetailsPanel, StudioDetailsPanel, } from "./StudioDetailsPanel"; import { StudioGroupsPanel } from "./StudioGroupsPanel"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { DetailImage } from "src/components/Shared/DetailImage"; import { useRatingKeybinds } from "src/hooks/keybinds"; import { useLoadStickyHeader } from "src/hooks/detailsPanel"; import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { BackgroundImage } from "src/components/Shared/DetailsPage/BackgroundImage"; import { TabTitleCounter, useTabKey, } from "src/components/Shared/DetailsPage/Tabs"; import { DetailTitle } from "src/components/Shared/DetailsPage/DetailTitle"; import { ExpandCollapseButton } from "src/components/Shared/CollapseButton"; import { FavoriteIcon } from "src/components/Shared/FavoriteIcon"; import { ExternalLinkButtons } from "src/components/Shared/ExternalLinksButton"; import { AliasList } from "src/components/Shared/DetailsPage/AliasList"; import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage"; import { goBackOrReplace } from "src/utils/history"; interface IProps { studio: GQL.StudioDataFragment; tabKey?: TabKey; } interface IStudioParams { id: string; tab?: string; } const validTabs = [ "default", "scenes", "galleries", "images", "performers", "groups", "childstudios", ] as const; type TabKey = (typeof validTabs)[number]; function isTabKey(tab: string): tab is TabKey { return validTabs.includes(tab as TabKey); } const StudioTabs: React.FC<{ tabKey?: TabKey; studio: GQL.StudioDataFragment; abbreviateCounter: boolean; showAllCounts?: boolean; }> = ({ tabKey, studio, abbreviateCounter, showAllCounts = false }) => { const [showAllDetails, setShowAllDetails] = useState( showAllCounts && studio.child_studios.length > 0 ); const sceneCount = (showAllDetails ? studio.scene_count_all : studio.scene_count) ?? 0; const galleryCount = (showAllDetails ? studio.gallery_count_all : studio.gallery_count) ?? 0; const imageCount = (showAllDetails ? studio.image_count_all : studio.image_count) ?? 0; const performerCount = (showAllDetails ? studio.performer_count_all : studio.performer_count) ?? 0; const groupCount = (showAllDetails ? studio.group_count_all : studio.group_count) ?? 0; const populatedDefaultTab = useMemo(() => { let ret: TabKey = "scenes"; if (sceneCount == 0) { if (galleryCount != 0) { ret = "galleries"; } else if (imageCount != 0) { ret = "images"; } else if (performerCount != 0) { ret = "performers"; } else if (groupCount != 0) { ret = "groups"; } else if (studio.child_studios.length != 0) { ret = "childstudios"; } } return ret; }, [ sceneCount, galleryCount, imageCount, performerCount, groupCount, studio, ]); const { setTabKey } = useTabKey({ tabKey, validTabs, defaultTabKey: populatedDefaultTab, baseURL: `/studios/${studio.id}`, }); const contentSwitch = useMemo(() => { if (!studio.child_studios.length) { return null; } return (
setShowAllDetails(!showAllDetails)} type="switch" label={} />
); }, [showAllDetails, studio.child_studios.length]); return ( } > {contentSwitch} } > {contentSwitch} } > {contentSwitch} } > {contentSwitch} } > {contentSwitch} } > ); }; const StudioPage: React.FC = ({ studio, tabKey }) => { const history = useHistory(); const Toast = useToast(); const intl = useIntl(); // Configuration settings const { configuration } = React.useContext(ConfigurationContext); const uiConfig = configuration?.ui; const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; const enableBackgroundImage = uiConfig?.enableStudioBackgroundImage ?? false; const showAllDetails = uiConfig?.showAllDetails ?? true; const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false; const [collapsed, setCollapsed] = useState(!showAllDetails); const loadStickyHeader = useLoadStickyHeader(); // Editing state const [isEditing, setIsEditing] = useState(false); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); // Editing studio state const [image, setImage] = useState(); const [encodingImage, setEncodingImage] = useState(false); const [updateStudio] = useStudioUpdate(); const [deleteStudio] = useStudioDestroy({ id: studio.id }); const showAllCounts = uiConfig?.showChildStudioContent; const studioImage = useMemo(() => { const existingPath = studio.image_path; if (isEditing) { if (image === null && existingPath) { const studioImageURL = new URL(existingPath); studioImageURL.searchParams.set("default", "true"); return studioImageURL.toString(); } else if (image) { return image; } } return existingPath; }, [isEditing, image, studio.image_path]); function setFavorite(v: boolean) { if (studio.id) { updateStudio({ variables: { input: { id: studio.id, favorite: v, }, }, }); } } // set up hotkeys useEffect(() => { Mousetrap.bind("e", () => toggleEditing()); Mousetrap.bind("d d", () => { setIsDeleteAlertOpen(true); }); Mousetrap.bind(",", () => setCollapsed(!collapsed)); Mousetrap.bind("f", () => setFavorite(!studio.favorite)); return () => { Mousetrap.unbind("e"); Mousetrap.unbind("d d"); Mousetrap.unbind(","); Mousetrap.unbind("f"); }; }); useRatingKeybinds( true, configuration?.ui.ratingSystemOptions?.type, setRating ); async function onSave(input: GQL.StudioCreateInput) { await updateStudio({ variables: { input: { id: studio.id, ...input, }, }, }); toggleEditing(false); Toast.success( intl.formatMessage( { id: "toast.updated_entity" }, { entity: intl.formatMessage({ id: "studio" }).toLocaleLowerCase() } ) ); } async function onAutoTag() { if (!studio.id) return; try { await mutateMetadataAutoTag({ studios: [studio.id] }); Toast.success(intl.formatMessage({ id: "toast.started_auto_tagging" })); } catch (e) { Toast.error(e); } } async function onDelete() { try { await deleteStudio(); } catch (e) { Toast.error(e); return; } goBackOrReplace(history, "/studios"); } function renderDeleteAlert() { return ( setIsDeleteAlertOpen(false) }} >

); } function toggleEditing(value?: boolean) { if (value !== undefined) { setIsEditing(value); } else { setIsEditing((e) => !e); } setImage(undefined); } function setRating(v: number | null) { if (studio.id) { updateStudio({ variables: { input: { id: studio.id, rating100: v, }, }, }); } } const headerClassName = cx("detail-header", { edit: isEditing, collapsed, "full-width": !collapsed && !compactExpandedDetails, }); return (
{studio.name ?? intl.formatMessage({ id: "studio" })}
{studioImage && ( )}
{!isEditing && ( setCollapsed(v)} /> )} setFavorite(v)} /> setRating(value)} clickToRate withoutContext /> {!isEditing && ( )} {isEditing ? ( toggleEditing()} onDelete={onDelete} setImage={setImage} setEncodingImage={setEncodingImage} /> ) : ( toggleEditing()} onSave={() => {}} onImageChange={() => {}} onClearImage={() => {}} onAutoTag={onAutoTag} autoTagDisabled={studio.ignore_auto_tag} onDelete={onDelete} /> )}
{!isEditing && loadStickyHeader && ( )}
{!isEditing && ( )}
{renderDeleteAlert()}
); }; const StudioLoader: React.FC> = ({ location, match, }) => { const { id, tab } = match.params; const { data, loading, error } = useFindStudio(id); useScrollToTopOnMount(); if (loading) return ; if (error) return ; if (!data?.findStudio) return ; if (tab && !isTabKey(tab)) { return ( ); } return ( ); }; export default StudioLoader;