mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
Rebuild Studio page by splitting view and edit (#1629)
* Rebuild Studio page by splitting view and edit * Fix parent studio id, open studio link in same tab Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Button, Table, Tabs, Tab } from "react-bootstrap";
|
||||
import { Tabs, Tab } from "react-bootstrap";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useHistory, Link } from "react-router-dom";
|
||||
import { useParams, useHistory } from "react-router-dom";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import cx from "classnames";
|
||||
import Mousetrap from "mousetrap";
|
||||
@@ -13,21 +13,21 @@ import {
|
||||
useStudioDestroy,
|
||||
mutateMetadataAutoTag,
|
||||
} from "src/core/StashService";
|
||||
import { ImageUtils, TableUtils } from "src/utils";
|
||||
import { ImageUtils } from "src/utils";
|
||||
import {
|
||||
Icon,
|
||||
DetailsEditNavbar,
|
||||
Modal,
|
||||
LoadingIndicator,
|
||||
StudioSelect,
|
||||
ErrorMessage,
|
||||
} from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
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 { StudioDetailsPanel } from "./StudioDetailsPanel";
|
||||
|
||||
interface IStudioParams {
|
||||
id?: string;
|
||||
@@ -45,83 +45,23 @@ export const Studio: React.FC = () => {
|
||||
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
|
||||
// Editing studio state
|
||||
const [image, setImage] = useState<string | null>();
|
||||
const [name, setName] = useState<string>();
|
||||
const [url, setUrl] = useState<string>();
|
||||
const [parentStudioId, setParentStudioId] = useState<string>();
|
||||
const [rating, setRating] = useState<number | undefined>(undefined);
|
||||
const [details, setDetails] = useState<string>();
|
||||
const [stashIDs, setStashIDs] = useState<GQL.StashIdInput[]>([]);
|
||||
|
||||
// Studio state
|
||||
const [studio, setStudio] = useState<Partial<GQL.StudioDataFragment>>({});
|
||||
const [imagePreview, setImagePreview] = useState<string | null>();
|
||||
const [image, setImage] = useState<string | null>();
|
||||
|
||||
const { data, error, loading } = useFindStudio(id);
|
||||
const { data, error } = useFindStudio(id);
|
||||
const studio = data?.findStudio;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [updateStudio] = useStudioUpdate();
|
||||
const [createStudio] = useStudioCreate(
|
||||
getStudioInput() as GQL.StudioCreateInput
|
||||
);
|
||||
const [deleteStudio] = useStudioDestroy(
|
||||
getStudioInput() as GQL.StudioDestroyInput
|
||||
);
|
||||
|
||||
function updateStudioEditState(state: Partial<GQL.StudioDataFragment>) {
|
||||
setName(state.name);
|
||||
setUrl(state.url ?? undefined);
|
||||
setParentStudioId(state?.parent_studio?.id ?? undefined);
|
||||
setRating(state.rating ?? undefined);
|
||||
setDetails(state.details ?? undefined);
|
||||
setStashIDs(state.stash_ids ?? []);
|
||||
}
|
||||
|
||||
function updateStudioData(studioData: Partial<GQL.StudioDataFragment>) {
|
||||
setImage(undefined);
|
||||
updateStudioEditState(studioData);
|
||||
setImagePreview(studioData.image_path ?? undefined);
|
||||
setStudio(studioData);
|
||||
setRating(studioData.rating ?? undefined);
|
||||
}
|
||||
const [createStudio] = useStudioCreate();
|
||||
const [deleteStudio] = useStudioDestroy({ id });
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
Mousetrap.bind("s s", () => onSave());
|
||||
}
|
||||
|
||||
Mousetrap.bind("e", () => setIsEditing(true));
|
||||
Mousetrap.bind("d d", () => onDelete());
|
||||
|
||||
// numeric keypresses get caught by jwplayer, so blur the element
|
||||
// if the rating sequence is started
|
||||
Mousetrap.bind("r", () => {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
Mousetrap.bind("0", () => setRating(NaN));
|
||||
Mousetrap.bind("1", () => setRating(1));
|
||||
Mousetrap.bind("2", () => setRating(2));
|
||||
Mousetrap.bind("3", () => setRating(3));
|
||||
Mousetrap.bind("4", () => setRating(4));
|
||||
Mousetrap.bind("5", () => setRating(5));
|
||||
|
||||
setTimeout(() => {
|
||||
Mousetrap.unbind("0");
|
||||
Mousetrap.unbind("1");
|
||||
Mousetrap.unbind("2");
|
||||
Mousetrap.unbind("3");
|
||||
Mousetrap.unbind("4");
|
||||
Mousetrap.unbind("5");
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (isEditing) {
|
||||
Mousetrap.unbind("s s");
|
||||
}
|
||||
|
||||
Mousetrap.unbind("e");
|
||||
Mousetrap.unbind("d d");
|
||||
};
|
||||
@@ -130,58 +70,36 @@ export const Studio: React.FC = () => {
|
||||
useEffect(() => {
|
||||
if (data && data.findStudio) {
|
||||
setImage(undefined);
|
||||
updateStudioEditState(data.findStudio);
|
||||
setImagePreview(data.findStudio.image_path ?? undefined);
|
||||
setStudio(data.findStudio);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
function onImageLoad(imageData: string) {
|
||||
setImagePreview(imageData);
|
||||
setImage(imageData);
|
||||
}
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, isEditing);
|
||||
|
||||
if (!isNew && !isEditing) {
|
||||
if (!data?.findStudio || loading || !studio.id) return <LoadingIndicator />;
|
||||
if (error) return <div>{error.message}</div>;
|
||||
}
|
||||
|
||||
function getStudioInput() {
|
||||
const input: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput> = {
|
||||
name,
|
||||
url,
|
||||
image,
|
||||
details,
|
||||
parent_id: parentStudioId ?? null,
|
||||
rating: rating ?? null,
|
||||
stash_ids: stashIDs.map((s) => ({
|
||||
stash_id: s.stash_id,
|
||||
endpoint: s.endpoint,
|
||||
})),
|
||||
};
|
||||
|
||||
if (!isNew) {
|
||||
(input as GQL.StudioUpdateInput).id = id;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
async function onSave(
|
||||
input: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput>
|
||||
) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!isNew) {
|
||||
const result = await updateStudio({
|
||||
variables: {
|
||||
input: getStudioInput() as GQL.StudioUpdateInput,
|
||||
input: input as GQL.StudioUpdateInput,
|
||||
},
|
||||
});
|
||||
if (result.data?.studioUpdate) {
|
||||
updateStudioData(result.data.studioUpdate);
|
||||
setIsEditing(false);
|
||||
}
|
||||
} else {
|
||||
const result = await createStudio();
|
||||
const result = await createStudio({
|
||||
variables: {
|
||||
input: input as GQL.StudioCreateInput,
|
||||
},
|
||||
});
|
||||
if (result.data?.studioCreate?.id) {
|
||||
history.push(`/studios/${result.data.studioCreate.id}`);
|
||||
setIsEditing(false);
|
||||
@@ -189,11 +107,13 @@ export const Studio: React.FC = () => {
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onAutoTag() {
|
||||
if (!studio.id) return;
|
||||
if (!studio?.id) return;
|
||||
try {
|
||||
await mutateMetadataAutoTag({ studios: [studio.id] });
|
||||
Toast.success({
|
||||
@@ -215,19 +135,6 @@ export const Studio: React.FC = () => {
|
||||
history.push(`/studios`);
|
||||
}
|
||||
|
||||
const removeStashID = (stashID: GQL.StashIdInput) => {
|
||||
setStashIDs(
|
||||
stashIDs.filter(
|
||||
(s) =>
|
||||
!(s.endpoint === stashID.endpoint && s.stash_id === stashID.stash_id)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
function onImageChangeHandler(event: React.FormEvent<HTMLInputElement>) {
|
||||
ImageUtils.onImageChange(event, onImageLoad);
|
||||
}
|
||||
|
||||
function renderDeleteAlert() {
|
||||
return (
|
||||
<Modal
|
||||
@@ -245,7 +152,7 @@ export const Studio: React.FC = () => {
|
||||
id="dialogs.delete_confirm"
|
||||
values={{
|
||||
entityName:
|
||||
name ??
|
||||
studio?.name ??
|
||||
intl.formatMessage({ id: "studio" }).toLocaleLowerCase(),
|
||||
}}
|
||||
/>
|
||||
@@ -254,64 +161,25 @@ export const Studio: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
function renderStashIDs() {
|
||||
if (!studio.stash_ids?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>StashIDs</td>
|
||||
<td>
|
||||
<ul className="pl-0">
|
||||
{stashIDs.map((stashID) => {
|
||||
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||
const link = base ? (
|
||||
<a
|
||||
href={`${base}studios/${stashID.stash_id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{stashID.stash_id}
|
||||
</a>
|
||||
) : (
|
||||
stashID.stash_id
|
||||
);
|
||||
return (
|
||||
<li key={stashID.stash_id} className="row no-gutters">
|
||||
{isEditing && (
|
||||
<Button
|
||||
variant="danger"
|
||||
className="mr-2 py-0"
|
||||
title={intl.formatMessage(
|
||||
{ id: "actions.delete_entity" },
|
||||
{ entityType: intl.formatMessage({ id: "stash_id" }) }
|
||||
)}
|
||||
onClick={() => removeStashID(stashID)}
|
||||
>
|
||||
<Icon icon="trash-alt" />
|
||||
</Button>
|
||||
)}
|
||||
{link}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function onToggleEdit() {
|
||||
setIsEditing(!isEditing);
|
||||
updateStudioData(studio);
|
||||
}
|
||||
|
||||
function onClearImage() {
|
||||
setImage(null);
|
||||
setImagePreview(
|
||||
studio.image_path ? `${studio.image_path}&default=true` : undefined
|
||||
);
|
||||
function renderImage() {
|
||||
let studioImage = studio?.image_path;
|
||||
if (isEditing) {
|
||||
if (image === null) {
|
||||
studioImage = `${studioImage}&default=true`;
|
||||
} else if (image) {
|
||||
studioImage = image;
|
||||
}
|
||||
}
|
||||
|
||||
if (studioImage) {
|
||||
return (
|
||||
<img className="logo" alt={studio?.name ?? ""} src={studioImage} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const activeTabKey =
|
||||
@@ -328,28 +196,10 @@ export const Studio: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
function renderStudio() {
|
||||
if (isEditing || !parentStudioId) {
|
||||
return (
|
||||
<StudioSelect
|
||||
onSelect={(items) =>
|
||||
setParentStudioId(items.length > 0 ? items[0]?.id : undefined)
|
||||
}
|
||||
ids={parentStudioId ? [parentStudioId] : []}
|
||||
isDisabled={!isEditing}
|
||||
excludeIds={studio.id ? [studio.id] : []}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (studio.parent_studio) {
|
||||
return (
|
||||
<Link to={`/studios/${studio.parent_studio.id}`}>
|
||||
{studio.parent_studio.name}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isLoading) return <LoadingIndicator />;
|
||||
if (error) return <ErrorMessage error={error.message} />;
|
||||
if (!studio?.id && !isNew)
|
||||
return <ErrorMessage error={`No studio found with id ${id}.`} />;
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
@@ -370,66 +220,36 @@ export const Studio: React.FC = () => {
|
||||
<div className="text-center">
|
||||
{imageEncoding ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
) : imagePreview ? (
|
||||
<img className="logo" alt={name} src={imagePreview} />
|
||||
) : (
|
||||
""
|
||||
renderImage()
|
||||
)}
|
||||
</div>
|
||||
<Table>
|
||||
<tbody>
|
||||
{TableUtils.renderInputGroup({
|
||||
title: intl.formatMessage({ id: "name" }),
|
||||
value: name ?? "",
|
||||
isEditing: !!isEditing,
|
||||
onChange: setName,
|
||||
})}
|
||||
{TableUtils.renderInputGroup({
|
||||
title: intl.formatMessage({ id: "url" }),
|
||||
value: url,
|
||||
isEditing: !!isEditing,
|
||||
onChange: setUrl,
|
||||
})}
|
||||
{TableUtils.renderTextArea({
|
||||
title: intl.formatMessage({ id: "details" }),
|
||||
value: details,
|
||||
isEditing: !!isEditing,
|
||||
onChange: setDetails,
|
||||
})}
|
||||
<tr>
|
||||
<td>{intl.formatMessage({ id: "parent_studios" })}</td>
|
||||
<td>{renderStudio()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{intl.formatMessage({ id: "rating" })}:</td>
|
||||
<td>
|
||||
<RatingStars
|
||||
value={rating}
|
||||
disabled={!isEditing}
|
||||
onSetRating={(value) => setRating(value ?? NaN)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{renderStashIDs()}
|
||||
</tbody>
|
||||
</Table>
|
||||
<DetailsEditNavbar
|
||||
objectName={name ?? "studio"}
|
||||
isNew={isNew}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={onToggleEdit}
|
||||
onSave={onSave}
|
||||
onImageChange={onImageChangeHandler}
|
||||
onImageChangeURL={onImageLoad}
|
||||
onClearImage={() => {
|
||||
onClearImage();
|
||||
}}
|
||||
onAutoTag={onAutoTag}
|
||||
onDelete={onDelete}
|
||||
acceptSVG
|
||||
/>
|
||||
{!isEditing && !isNew && studio ? (
|
||||
<>
|
||||
<StudioDetailsPanel studio={studio} />
|
||||
<DetailsEditNavbar
|
||||
objectName={studio.name ?? intl.formatMessage({ id: "studio" })}
|
||||
isNew={isNew}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={onToggleEdit}
|
||||
onSave={() => {}}
|
||||
onImageChange={() => {}}
|
||||
onClearImage={() => {}}
|
||||
onAutoTag={onAutoTag}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<StudioEditPanel
|
||||
studio={studio ?? ({} as Partial<GQL.Studio>)}
|
||||
onSubmit={onSave}
|
||||
onCancel={onToggleEdit}
|
||||
onDelete={onDelete}
|
||||
onImageChange={setImage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!isNew && (
|
||||
{studio?.id && (
|
||||
<div className="col col-md-8">
|
||||
<Tabs
|
||||
id="studio-tabs"
|
||||
|
||||
Reference in New Issue
Block a user