Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ui/v2.5/src/components/Shared/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,12 @@
top: 0;
}
}

.studio-image {
max-height: 200px;
max-width: 100%;
object-fit: contain;
}
}

button.collapse-button.btn-primary:not(:disabled):not(.disabled):hover,
Expand Down
2 changes: 1 addition & 1 deletion ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -492,10 +492,10 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
)}
{isEditing ? (
<StudioEditPanel
key={studio.id}
studio={studio}
onSubmit={onSave}
onCancel={() => toggleEditing()}
onDelete={onDelete}
setImage={setImage}
setEncodingImage={setEncodingImage}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ const StudioCreate: React.FC = () => {
studio={studio}
onSubmit={onSave}
onCancel={() => history.push("/studios")}
onDelete={() => {}}
setImage={setImage}
setEncodingImage={setEncodingImage}
/>
Expand Down
226 changes: 203 additions & 23 deletions ui/v2.5/src/components/Studios/StudioDetails/StudioEditPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React, { useEffect, useState } from "react";
import { useIntl } from "react-intl";
import { FormattedMessage, useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql";
import * as yup from "yup";
import Mousetrap from "mousetrap";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
import { Button, Form } from "react-bootstrap";
import { ImageInput } from "src/components/Shared/ImageInput";
import cx from "classnames";
import { Button, Dropdown, Form } from "react-bootstrap";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import ImageUtils from "src/utils/image";
import { addUpdateStashID, getStashIDs } from "src/utils/stashIds";
import { stashboxDisplayName } from "src/utils/stashbox";
import { useFormik } from "formik";
import { Prompt } from "react-router-dom";
import isEqual from "lodash-es/isEqual";
Expand All @@ -21,12 +23,13 @@ import { Studio, StudioSelect } from "../StudioSelect";
import { useTagsEdit } from "src/hooks/tagsEdit";
import { Icon } from "src/components/Shared/Icon";
import StashBoxIDSearchModal from "src/components/Shared/StashBoxIDSearchModal";
import StudioStashBoxModal, { IStashBox } from "./StudioStashBoxModal";
import { StudioScrapeDialog } from "./StudioScrapeDialog";

interface IStudioEditPanel {
studio: Partial<GQL.StudioDataFragment>;
onSubmit: (studio: GQL.StudioCreateInput) => Promise<void>;
onCancel: () => void;
onDelete: () => void;
setImage: (image?: string | null) => void;
setEncodingImage: (loading: boolean) => void;
}
Expand All @@ -35,7 +38,6 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
studio,
onSubmit,
onCancel,
onDelete,
setImage,
setEncodingImage,
}) => {
Expand All @@ -45,7 +47,10 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({

const isNew = studio.id === undefined;

// Editing state
// Editing/scraper state
const [scraper, setScraper] = useState<IStashBox>();
const [isScraperModalOpen, setIsScraperModalOpen] = useState(false);
const [scrapedStudio, setScrapedStudio] = useState<GQL.ScrapedStudio>();
const [isStashIDSearchOpen, setIsStashIDSearchOpen] = useState(false);

// Network state
Expand Down Expand Up @@ -86,8 +91,9 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
onSubmit: (values) => onSave(schema.cast(values)),
});

const { tagsControl } = useTagsEdit(studio.tags, (ids) =>
formik.setFieldValue("tag_ids", ids)
const { tags, updateTagsStateFromScraper, tagsControl } = useTagsEdit(
studio.tags,
(ids) => formik.setFieldValue("tag_ids", ids)
);

function onSetParentStudio(item: Studio | null) {
Expand Down Expand Up @@ -159,6 +165,189 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
);
}

function updateStashIDs(remoteSiteID: string | null | undefined) {
if (remoteSiteID && scraper?.endpoint) {
const newIDs =
formik.values.stash_ids?.filter(
(s) => s.endpoint !== scraper.endpoint
) ?? [];
newIDs.push({
endpoint: scraper.endpoint,
stash_id: remoteSiteID,
updated_at: new Date().toISOString(),
});
formik.setFieldValue("stash_ids", newIDs);
}
}

function updateStudioEditStateFromScraper(
state: Partial<GQL.ScrapedStudioDataFragment>
) {
if (state.name) {
formik.setFieldValue("name", state.name);
}
if (state.urls) {
formik.setFieldValue("urls", state.urls);
}
if (state.details) {
formik.setFieldValue("details", state.details);
}
if (state.aliases) {
formik.setFieldValue(
"aliases",
state.aliases.split(",").map((a) => a.trim())
);
}
updateTagsStateFromScraper(state.tags ?? undefined);

// image is a base64 string
// overwrite if not new since it came from a dialog
// overwrite if image is unset
if ((!isNew || !formik.values.image) && state.image) {
formik.setFieldValue("image", state.image);
}

updateStashIDs(state.remote_site_id);
}

function onScrapeStashBox(studioResult: GQL.ScrapedStudio) {
setIsScraperModalOpen(false);

const result: GQL.ScrapedStudioDataFragment = {
...studioResult,
__typename: "ScrapedStudio",
};

// if this is a new studio, just dump the data
if (isNew) {
updateStudioEditStateFromScraper(result);
setScraper(undefined);
} else {
setScrapedStudio(result);
}
}

function onScraperSelected(s: IStashBox) {
setScraper(s);
setIsScraperModalOpen(true);
}

function renderScraperMenu() {
if (!studio) {
return;
}
const stashBoxes = stashConfig?.general.stashBoxes ?? [];

if (stashBoxes.length === 0) {
return;
}

const popover = (
<Dropdown.Menu id="studio-scraper-popover">
{stashBoxes.map((s, index) => (
<Dropdown.Item
as={Button}
key={s.endpoint}
className="minimal"
onClick={() => onScraperSelected({ ...s, index })}
>
{stashboxDisplayName(s.name, index)}
</Dropdown.Item>
))}
</Dropdown.Menu>
);

return (
<Dropdown className="d-inline-block">
<Dropdown.Toggle variant="secondary" className="mr-2">
<FormattedMessage id="actions.scrape_with" />
</Dropdown.Toggle>
{popover}
</Dropdown>
);
}

function renderButtons(classNames: string) {
return (
<div className={cx("details-edit", "col-xl-9", classNames)}>
{!isNew && (
<Button className="mr-2" variant="primary" onClick={onCancel}>
<FormattedMessage id="actions.cancel" />
</Button>
)}
{renderScraperMenu()}
<ImageInput
isEditing
onImageChange={onImageChange}
onImageURL={onImageLoad}
acceptSVG
/>
<div>
<Button
className="mr-2"
variant="danger"
onClick={() => onImageLoad(null)}
>
<FormattedMessage id="actions.clear_image" />
</Button>
</div>
<Button
variant="success"
disabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
onClick={() => formik.submitForm()}
>
<FormattedMessage id="actions.save" />
</Button>
</div>
);
}

function maybeRenderScrapeDialog() {
if (!scrapedStudio || !scraper) {
return;
}

const currentStudio = {
...formik.values,
image: formik.values.image ?? studio.image_path,
};

return (
<StudioScrapeDialog
studio={currentStudio}
studioTags={tags}
scraped={scrapedStudio}
scraper={scraper}
onClose={(s) => {
onScrapeDialogClosed(s);
}}
/>
);
}

function onScrapeDialogClosed(s?: GQL.ScrapedStudioDataFragment) {
if (s) {
updateStudioEditStateFromScraper(s);
}
setScrapedStudio(undefined);
setScraper(undefined);
}

function renderScrapeModal() {
if (!isScraperModalOpen || !scraper) {
return;
}

return (
<StudioStashBoxModal
instance={scraper}
onHide={() => setScraper(undefined)}
onSelectStudio={onScrapeStashBox}
name={formik.values.name || ""}
/>
);
}

const {
renderField,
renderInputField,
Expand Down Expand Up @@ -189,6 +378,8 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({

return (
<>
{renderScrapeModal()}
{maybeRenderScrapeDialog()}
{isStashIDSearchOpen && (
<StashBoxIDSearchModal
entityType="studio"
Expand All @@ -215,6 +406,8 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
}}
/>

{renderButtons("mb-3")}

<Form noValidate onSubmit={formik.handleSubmit} id="studio-edit">
{renderInputField("name")}
{renderStringListField("aliases")}
Expand All @@ -239,22 +432,9 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
)}
<hr />
{renderInputField("ignore_auto_tag", "checkbox")}
</Form>

<DetailsEditNavbar
objectName={studio?.name ?? intl.formatMessage({ id: "studio" })}
classNames="col-xl-9 mt-3"
isNew={isNew}
isEditing
onToggleEdit={onCancel}
onSave={formik.handleSubmit}
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
onImageChange={onImageChange}
onImageChangeURL={onImageLoad}
onClearImage={() => onImageLoad(null)}
onDelete={onDelete}
acceptSVG
/>
{renderButtons("mt-3")}
</Form>
</>
);
};
Loading