mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Refactor scrape dialog (#4069)
* Fix performer select showing blank values after scrape * Move and separate scrape dialog * Separate row components from scene scrape dialog * Refactor object creation * Refactor gallery scrape dialog
This commit is contained in:
@@ -255,6 +255,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||
return (
|
||||
<GalleryScrapeDialog
|
||||
gallery={currentGallery}
|
||||
galleryPerformers={performers}
|
||||
scraped={scrapedGallery}
|
||||
onClose={(data) => {
|
||||
onScrapeDialogClosed(data);
|
||||
|
||||
@@ -1,180 +1,29 @@
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import {
|
||||
StudioSelect,
|
||||
PerformerSelect,
|
||||
TagSelect,
|
||||
} from "src/components/Shared/Select";
|
||||
import { useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
ScrapeDialog,
|
||||
ScrapeDialogRow,
|
||||
ScrapeResult,
|
||||
ScrapedInputGroupRow,
|
||||
ScrapedTextAreaRow,
|
||||
} from "src/components/Shared/ScrapeDialog";
|
||||
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||
import clone from "lodash-es/clone";
|
||||
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
|
||||
import {
|
||||
useStudioCreate,
|
||||
usePerformerCreate,
|
||||
useTagCreate,
|
||||
} from "src/core/StashService";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { scrapedPerformerToCreateInput } from "src/core/performers";
|
||||
|
||||
function renderScrapedStudio(
|
||||
result: ScrapeResult<string>,
|
||||
isNew?: boolean,
|
||||
onChange?: (value: string) => void
|
||||
) {
|
||||
const resultValue = isNew ? result.newValue : result.originalValue;
|
||||
const value = resultValue ? [resultValue] : [];
|
||||
|
||||
return (
|
||||
<StudioSelect
|
||||
className="form-control react-select"
|
||||
isDisabled={!isNew}
|
||||
onSelect={(items) => {
|
||||
if (onChange) {
|
||||
onChange(items[0]?.id);
|
||||
}
|
||||
}}
|
||||
ids={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderScrapedStudioRow(
|
||||
title: string,
|
||||
result: ScrapeResult<string>,
|
||||
onChange: (value: ScrapeResult<string>) => void,
|
||||
newStudio?: GQL.ScrapedStudio,
|
||||
onCreateNew?: (value: GQL.ScrapedStudio) => void
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedStudio(result)}
|
||||
renderNewField={() =>
|
||||
renderScrapedStudio(result, true, (value) =>
|
||||
onChange(result.cloneWithValue(value))
|
||||
)
|
||||
}
|
||||
onChange={onChange}
|
||||
newValues={newStudio ? [newStudio] : undefined}
|
||||
onCreateNew={() => {
|
||||
if (onCreateNew && newStudio) onCreateNew(newStudio);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderScrapedPerformers(
|
||||
result: ScrapeResult<string[]>,
|
||||
isNew?: boolean,
|
||||
onChange?: (value: string[]) => void
|
||||
) {
|
||||
const resultValue = isNew ? result.newValue : result.originalValue;
|
||||
const value = resultValue ?? [];
|
||||
|
||||
return (
|
||||
<PerformerSelect
|
||||
isMulti
|
||||
className="form-control react-select"
|
||||
isDisabled={!isNew}
|
||||
onSelect={(items) => {
|
||||
if (onChange) {
|
||||
onChange(items.map((i) => i.id));
|
||||
}
|
||||
}}
|
||||
ids={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderScrapedPerformersRow(
|
||||
title: string,
|
||||
result: ScrapeResult<string[]>,
|
||||
onChange: (value: ScrapeResult<string[]>) => void,
|
||||
newPerformers: GQL.ScrapedPerformer[],
|
||||
onCreateNew?: (value: GQL.ScrapedPerformer) => void
|
||||
) {
|
||||
const performersCopy = newPerformers.map((p) => {
|
||||
const name: string = p.name ?? "";
|
||||
return { ...p, name };
|
||||
});
|
||||
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedPerformers(result)}
|
||||
renderNewField={() =>
|
||||
renderScrapedPerformers(result, true, (value) =>
|
||||
onChange(result.cloneWithValue(value))
|
||||
)
|
||||
}
|
||||
onChange={onChange}
|
||||
newValues={performersCopy}
|
||||
onCreateNew={(i) => {
|
||||
if (onCreateNew) onCreateNew(newPerformers[i]);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderScrapedTags(
|
||||
result: ScrapeResult<string[]>,
|
||||
isNew?: boolean,
|
||||
onChange?: (value: string[]) => void
|
||||
) {
|
||||
const resultValue = isNew ? result.newValue : result.originalValue;
|
||||
const value = resultValue ?? [];
|
||||
|
||||
return (
|
||||
<TagSelect
|
||||
isMulti
|
||||
className="form-control react-select"
|
||||
isDisabled={!isNew}
|
||||
onSelect={(items) => {
|
||||
if (onChange) {
|
||||
onChange(items.map((i) => i.id));
|
||||
}
|
||||
}}
|
||||
ids={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderScrapedTagsRow(
|
||||
title: string,
|
||||
result: ScrapeResult<string[]>,
|
||||
onChange: (value: ScrapeResult<string[]>) => void,
|
||||
newTags: GQL.ScrapedTag[],
|
||||
onCreateNew?: (value: GQL.ScrapedTag) => void
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedTags(result)}
|
||||
renderNewField={() =>
|
||||
renderScrapedTags(result, true, (value) =>
|
||||
onChange(result.cloneWithValue(value))
|
||||
)
|
||||
}
|
||||
newValues={newTags}
|
||||
onChange={onChange}
|
||||
onCreateNew={(i) => {
|
||||
if (onCreateNew) onCreateNew(newTags[i]);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
ScrapedPerformersRow,
|
||||
ScrapedStudioRow,
|
||||
ScrapedTagsRow,
|
||||
} from "src/components/Shared/ScrapeDialog/ScrapedObjectsRow";
|
||||
import { sortStoredIdObjects } from "src/utils/data";
|
||||
import { Performer } from "src/components/Performers/PerformerSelect";
|
||||
import {
|
||||
useCreateScrapedPerformer,
|
||||
useCreateScrapedStudio,
|
||||
useCreateScrapedTag,
|
||||
} from "src/components/Shared/ScrapeDialog/createObjects";
|
||||
|
||||
interface IGalleryScrapeDialogProps {
|
||||
gallery: Partial<GQL.GalleryUpdateInput>;
|
||||
galleryPerformers: Performer[];
|
||||
scraped: GQL.ScrapedGallery;
|
||||
|
||||
onClose: (scrapedGallery?: GQL.ScrapedGallery) => void;
|
||||
@@ -247,10 +96,17 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||
return ret;
|
||||
}
|
||||
|
||||
const [performers, setPerformers] = useState<ScrapeResult<string[]>>(
|
||||
new ScrapeResult<string[]>(
|
||||
sortIdList(props.gallery.performer_ids),
|
||||
mapStoredIdObjects(props.scraped.performers ?? undefined)
|
||||
const [performers, setPerformers] = useState<
|
||||
ScrapeResult<GQL.ScrapedPerformer[]>
|
||||
>(
|
||||
new ScrapeResult<GQL.ScrapedPerformer[]>(
|
||||
sortStoredIdObjects(
|
||||
props.galleryPerformers.map((p) => ({
|
||||
stored_id: p.id,
|
||||
name: p.name,
|
||||
}))
|
||||
),
|
||||
sortStoredIdObjects(props.scraped.performers ?? undefined)
|
||||
)
|
||||
);
|
||||
const [newPerformers, setNewPerformers] = useState<GQL.ScrapedPerformer[]>(
|
||||
@@ -271,11 +127,25 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||
new ScrapeResult<string>(props.gallery.details, props.scraped.details)
|
||||
);
|
||||
|
||||
const [createStudio] = useStudioCreate();
|
||||
const [createPerformer] = usePerformerCreate();
|
||||
const [createTag] = useTagCreate();
|
||||
const createNewStudio = useCreateScrapedStudio({
|
||||
scrapeResult: studio,
|
||||
setScrapeResult: setStudio,
|
||||
setNewObject: setNewStudio,
|
||||
});
|
||||
|
||||
const Toast = useToast();
|
||||
const createNewPerformer = useCreateScrapedPerformer({
|
||||
scrapeResult: performers,
|
||||
setScrapeResult: setPerformers,
|
||||
newObjects: newPerformers,
|
||||
setNewObjects: setNewPerformers,
|
||||
});
|
||||
|
||||
const createNewTag = useCreateScrapedTag({
|
||||
scrapeResult: tags,
|
||||
setScrapeResult: setTags,
|
||||
newObjects: newTags,
|
||||
setNewObjects: setNewTags,
|
||||
});
|
||||
|
||||
// don't show the dialog if nothing was scraped
|
||||
if (
|
||||
@@ -290,122 +160,6 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||
return <></>;
|
||||
}
|
||||
|
||||
async function createNewStudio(toCreate: GQL.ScrapedStudio) {
|
||||
try {
|
||||
const result = await createStudio({
|
||||
variables: {
|
||||
input: {
|
||||
name: toCreate.name,
|
||||
url: toCreate.url,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// set the new studio as the value
|
||||
setStudio(studio.cloneWithValue(result.data!.studioCreate!.id));
|
||||
setNewStudio(undefined);
|
||||
|
||||
Toast.success({
|
||||
content: (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="actions.created_entity"
|
||||
values={{
|
||||
entity_type: intl.formatMessage({ id: "studio" }),
|
||||
entity_name: <b>{toCreate.name}</b>,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewPerformer(toCreate: GQL.ScrapedPerformer) {
|
||||
const input = scrapedPerformerToCreateInput(toCreate);
|
||||
|
||||
try {
|
||||
const result = await createPerformer({
|
||||
variables: { input },
|
||||
});
|
||||
|
||||
// add the new performer to the new performers value
|
||||
const performerClone = performers.cloneWithValue(performers.newValue);
|
||||
if (!performerClone.newValue) {
|
||||
performerClone.newValue = [];
|
||||
}
|
||||
performerClone.newValue.push(result.data!.performerCreate!.id);
|
||||
setPerformers(performerClone);
|
||||
|
||||
// remove the performer from the list
|
||||
const newPerformersClone = newPerformers.concat();
|
||||
const pIndex = newPerformersClone.indexOf(toCreate);
|
||||
newPerformersClone.splice(pIndex, 1);
|
||||
|
||||
setNewPerformers(newPerformersClone);
|
||||
|
||||
Toast.success({
|
||||
content: (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="actions.created_entity"
|
||||
values={{
|
||||
entity_type: intl.formatMessage({ id: "performer" }),
|
||||
entity_name: <b>{toCreate.name}</b>,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewTag(toCreate: GQL.ScrapedTag) {
|
||||
const tagInput: GQL.TagCreateInput = { name: toCreate.name ?? "" };
|
||||
try {
|
||||
const result = await createTag({
|
||||
variables: {
|
||||
input: tagInput,
|
||||
},
|
||||
});
|
||||
|
||||
// add the new tag to the new tags value
|
||||
const tagClone = tags.cloneWithValue(tags.newValue);
|
||||
if (!tagClone.newValue) {
|
||||
tagClone.newValue = [];
|
||||
}
|
||||
tagClone.newValue.push(result.data!.tagCreate!.id);
|
||||
setTags(tagClone);
|
||||
|
||||
// remove the tag from the list
|
||||
const newTagsClone = newTags.concat();
|
||||
const pIndex = newTagsClone.indexOf(toCreate);
|
||||
newTagsClone.splice(pIndex, 1);
|
||||
|
||||
setNewTags(newTagsClone);
|
||||
|
||||
Toast.success({
|
||||
content: (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="actions.created_entity"
|
||||
values={{
|
||||
entity_type: intl.formatMessage({ id: "tag" }),
|
||||
entity_name: <b>{toCreate.name}</b>,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function makeNewScrapedItem(): GQL.ScrapedGalleryDataFragment {
|
||||
const newStudioValue = studio.getNewValue();
|
||||
|
||||
@@ -419,12 +173,7 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||
name: "",
|
||||
}
|
||||
: undefined,
|
||||
performers: performers.getNewValue()?.map((p) => {
|
||||
return {
|
||||
stored_id: p,
|
||||
name: "",
|
||||
};
|
||||
}),
|
||||
performers: performers.getNewValue(),
|
||||
tags: tags.getNewValue()?.map((m) => {
|
||||
return {
|
||||
stored_id: m,
|
||||
@@ -454,27 +203,27 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||
result={date}
|
||||
onChange={(value) => setDate(value)}
|
||||
/>
|
||||
{renderScrapedStudioRow(
|
||||
intl.formatMessage({ id: "studios" }),
|
||||
studio,
|
||||
(value) => setStudio(value),
|
||||
newStudio,
|
||||
createNewStudio
|
||||
)}
|
||||
{renderScrapedPerformersRow(
|
||||
intl.formatMessage({ id: "performers" }),
|
||||
performers,
|
||||
(value) => setPerformers(value),
|
||||
newPerformers,
|
||||
createNewPerformer
|
||||
)}
|
||||
{renderScrapedTagsRow(
|
||||
intl.formatMessage({ id: "tags" }),
|
||||
tags,
|
||||
(value) => setTags(value),
|
||||
newTags,
|
||||
createNewTag
|
||||
)}
|
||||
<ScrapedStudioRow
|
||||
title={intl.formatMessage({ id: "studios" })}
|
||||
result={studio}
|
||||
onChange={(value) => setStudio(value)}
|
||||
newStudio={newStudio}
|
||||
onCreateNew={createNewStudio}
|
||||
/>
|
||||
<ScrapedPerformersRow
|
||||
title={intl.formatMessage({ id: "performers" })}
|
||||
result={performers}
|
||||
onChange={(value) => setPerformers(value)}
|
||||
newObjects={newPerformers}
|
||||
onCreateNew={createNewPerformer}
|
||||
/>
|
||||
<ScrapedTagsRow
|
||||
title={intl.formatMessage({ id: "tags" })}
|
||||
result={tags}
|
||||
onChange={(value) => setTags(value)}
|
||||
newObjects={newTags}
|
||||
onCreateNew={createNewTag}
|
||||
/>
|
||||
<ScrapedTextAreaRow
|
||||
title={intl.formatMessage({ id: "details" })}
|
||||
result={details}
|
||||
|
||||
@@ -3,16 +3,16 @@ import { useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
ScrapeDialog,
|
||||
ScrapeResult,
|
||||
ScrapedInputGroupRow,
|
||||
ScrapedImageRow,
|
||||
ScrapeDialogRow,
|
||||
ScrapedTextAreaRow,
|
||||
} from "src/components/Shared/ScrapeDialog";
|
||||
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||
import { StudioSelect } from "src/components/Shared/Select";
|
||||
import DurationUtils from "src/utils/duration";
|
||||
import { useStudioCreate } from "src/core/StashService";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
|
||||
|
||||
function renderScrapedStudio(
|
||||
result: ScrapeResult<string>,
|
||||
|
||||
@@ -3,13 +3,12 @@ import { useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
ScrapeDialog,
|
||||
ScrapeResult,
|
||||
ScrapedInputGroupRow,
|
||||
ScrapedImagesRow,
|
||||
ScrapeDialogRow,
|
||||
ScrapedTextAreaRow,
|
||||
ScrapedCountryRow,
|
||||
} from "src/components/Shared/ScrapeDialog";
|
||||
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||
import { useTagCreate } from "src/core/StashService";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { TagSelect } from "src/components/Shared/Select";
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
stringToCircumcised,
|
||||
} from "src/utils/circumcised";
|
||||
import { IStashBox } from "./PerformerStashBoxModal";
|
||||
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
|
||||
|
||||
function renderScrapedGender(
|
||||
result: ScrapeResult<string>,
|
||||
|
||||
@@ -430,6 +430,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
return (
|
||||
<SceneScrapeDialog
|
||||
scene={currentScene}
|
||||
scenePerformers={performers}
|
||||
scraped={scrapedScene}
|
||||
endpoint={endpoint}
|
||||
onClose={(s) => onScrapeDialogClosed(s)}
|
||||
|
||||
@@ -1,292 +1,43 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
MovieSelect,
|
||||
TagSelect,
|
||||
StudioSelect,
|
||||
PerformerSelect,
|
||||
} from "src/components/Shared/Select";
|
||||
import {
|
||||
ScrapeDialog,
|
||||
ScrapeDialogRow,
|
||||
ScrapeResult,
|
||||
ScrapedInputGroupRow,
|
||||
ScrapedTextAreaRow,
|
||||
ScrapedImageRow,
|
||||
IHasName,
|
||||
ScrapedStringListRow,
|
||||
} from "src/components/Shared/ScrapeDialog";
|
||||
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||
import clone from "lodash-es/clone";
|
||||
import {
|
||||
useStudioCreate,
|
||||
usePerformerCreate,
|
||||
useMovieCreate,
|
||||
useTagCreate,
|
||||
} from "src/core/StashService";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { useIntl } from "react-intl";
|
||||
import { uniq } from "lodash-es";
|
||||
import { scrapedPerformerToCreateInput } from "src/core/performers";
|
||||
import { scrapedMovieToCreateInput } from "src/core/movies";
|
||||
|
||||
interface IScrapedStudioRow {
|
||||
title: string;
|
||||
result: ScrapeResult<string>;
|
||||
onChange: (value: ScrapeResult<string>) => void;
|
||||
newStudio?: GQL.ScrapedStudio;
|
||||
onCreateNew?: (value: GQL.ScrapedStudio) => void;
|
||||
}
|
||||
|
||||
export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
|
||||
title,
|
||||
result,
|
||||
onChange,
|
||||
newStudio,
|
||||
onCreateNew,
|
||||
}) => {
|
||||
function renderScrapedStudio(
|
||||
scrapeResult: ScrapeResult<string>,
|
||||
isNew?: boolean,
|
||||
onChangeFn?: (value: string) => void
|
||||
) {
|
||||
const resultValue = isNew
|
||||
? scrapeResult.newValue
|
||||
: scrapeResult.originalValue;
|
||||
const value = resultValue ? [resultValue] : [];
|
||||
|
||||
return (
|
||||
<StudioSelect
|
||||
className="form-control react-select"
|
||||
isDisabled={!isNew}
|
||||
onSelect={(items) => {
|
||||
if (onChangeFn) {
|
||||
onChangeFn(items[0]?.id);
|
||||
}
|
||||
}}
|
||||
ids={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedStudio(result)}
|
||||
renderNewField={() =>
|
||||
renderScrapedStudio(result, true, (value) =>
|
||||
onChange(result.cloneWithValue(value))
|
||||
)
|
||||
}
|
||||
onChange={onChange}
|
||||
newValues={newStudio ? [newStudio] : undefined}
|
||||
onCreateNew={() => {
|
||||
if (onCreateNew && newStudio) onCreateNew(newStudio);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface IScrapedObjectsRow<T> {
|
||||
title: string;
|
||||
result: ScrapeResult<string[]>;
|
||||
onChange: (value: ScrapeResult<string[]>) => void;
|
||||
newObjects?: T[];
|
||||
onCreateNew?: (value: T) => void;
|
||||
renderObjects: (
|
||||
result: ScrapeResult<string[]>,
|
||||
isNew?: boolean,
|
||||
onChange?: (value: string[]) => void
|
||||
) => JSX.Element;
|
||||
}
|
||||
|
||||
export const ScrapedObjectsRow = <T extends IHasName>(
|
||||
props: IScrapedObjectsRow<T>
|
||||
) => {
|
||||
const { title, result, onChange, newObjects, onCreateNew, renderObjects } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderObjects(result)}
|
||||
renderNewField={() =>
|
||||
renderObjects(result, true, (value) =>
|
||||
onChange(result.cloneWithValue(value))
|
||||
)
|
||||
}
|
||||
onChange={onChange}
|
||||
newValues={newObjects}
|
||||
onCreateNew={(i) => {
|
||||
if (onCreateNew) onCreateNew(newObjects![i]);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type IScrapedObjectRowImpl<T> = Omit<IScrapedObjectsRow<T>, "renderObjects">;
|
||||
|
||||
export const ScrapedPerformersRow: React.FC<
|
||||
IScrapedObjectRowImpl<GQL.ScrapedPerformer>
|
||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||
const performersCopy = useMemo(() => {
|
||||
return (
|
||||
newObjects?.map((p) => {
|
||||
const name: string = p.name ?? "";
|
||||
return { ...p, name };
|
||||
}) ?? []
|
||||
);
|
||||
}, [newObjects]);
|
||||
|
||||
type PerformerType = GQL.ScrapedPerformer & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
function renderScrapedPerformers(
|
||||
scrapeResult: ScrapeResult<string[]>,
|
||||
isNew?: boolean,
|
||||
onChangeFn?: (value: string[]) => void
|
||||
) {
|
||||
const resultValue = isNew
|
||||
? scrapeResult.newValue
|
||||
: scrapeResult.originalValue;
|
||||
const value = resultValue ?? [];
|
||||
|
||||
return (
|
||||
<PerformerSelect
|
||||
isMulti
|
||||
className="form-control react-select"
|
||||
isDisabled={!isNew}
|
||||
onSelect={(items) => {
|
||||
if (onChangeFn) {
|
||||
onChangeFn(items.map((i) => i.id));
|
||||
}
|
||||
}}
|
||||
ids={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrapedObjectsRow<PerformerType>
|
||||
title={title}
|
||||
result={result}
|
||||
renderObjects={renderScrapedPerformers}
|
||||
onChange={onChange}
|
||||
newObjects={performersCopy}
|
||||
onCreateNew={onCreateNew}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScrapedMoviesRow: React.FC<
|
||||
IScrapedObjectRowImpl<GQL.ScrapedMovie>
|
||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||
const moviesCopy = useMemo(() => {
|
||||
return (
|
||||
newObjects?.map((p) => {
|
||||
const name: string = p.name ?? "";
|
||||
return { ...p, name };
|
||||
}) ?? []
|
||||
);
|
||||
}, [newObjects]);
|
||||
|
||||
type MovieType = GQL.ScrapedMovie & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
function renderScrapedMovies(
|
||||
scrapeResult: ScrapeResult<string[]>,
|
||||
isNew?: boolean,
|
||||
onChangeFn?: (value: string[]) => void
|
||||
) {
|
||||
const resultValue = isNew
|
||||
? scrapeResult.newValue
|
||||
: scrapeResult.originalValue;
|
||||
const value = resultValue ?? [];
|
||||
|
||||
return (
|
||||
<MovieSelect
|
||||
isMulti
|
||||
className="form-control react-select"
|
||||
isDisabled={!isNew}
|
||||
onSelect={(items) => {
|
||||
if (onChangeFn) {
|
||||
onChangeFn(items.map((i) => i.id));
|
||||
}
|
||||
}}
|
||||
ids={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrapedObjectsRow<MovieType>
|
||||
title={title}
|
||||
result={result}
|
||||
renderObjects={renderScrapedMovies}
|
||||
onChange={onChange}
|
||||
newObjects={moviesCopy}
|
||||
onCreateNew={onCreateNew}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScrapedTagsRow: React.FC<
|
||||
IScrapedObjectRowImpl<GQL.ScrapedTag>
|
||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||
function renderScrapedTags(
|
||||
scrapeResult: ScrapeResult<string[]>,
|
||||
isNew?: boolean,
|
||||
onChangeFn?: (value: string[]) => void
|
||||
) {
|
||||
const resultValue = isNew
|
||||
? scrapeResult.newValue
|
||||
: scrapeResult.originalValue;
|
||||
const value = resultValue ?? [];
|
||||
|
||||
return (
|
||||
<TagSelect
|
||||
isMulti
|
||||
className="form-control react-select"
|
||||
isDisabled={!isNew}
|
||||
onSelect={(items) => {
|
||||
if (onChangeFn) {
|
||||
onChangeFn(items.map((i) => i.id));
|
||||
}
|
||||
}}
|
||||
ids={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrapedObjectsRow<GQL.ScrapedTag>
|
||||
title={title}
|
||||
result={result}
|
||||
renderObjects={renderScrapedTags}
|
||||
onChange={onChange}
|
||||
newObjects={newObjects}
|
||||
onCreateNew={onCreateNew}
|
||||
/>
|
||||
);
|
||||
};
|
||||
import { Performer } from "src/components/Performers/PerformerSelect";
|
||||
import { IHasStoredID, sortStoredIdObjects } from "src/utils/data";
|
||||
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
|
||||
import {
|
||||
ScrapedMoviesRow,
|
||||
ScrapedPerformersRow,
|
||||
ScrapedStudioRow,
|
||||
ScrapedTagsRow,
|
||||
} from "src/components/Shared/ScrapeDialog/ScrapedObjectsRow";
|
||||
import {
|
||||
useCreateScrapedMovie,
|
||||
useCreateScrapedPerformer,
|
||||
useCreateScrapedStudio,
|
||||
useCreateScrapedTag,
|
||||
} from "src/components/Shared/ScrapeDialog/createObjects";
|
||||
|
||||
interface ISceneScrapeDialogProps {
|
||||
scene: Partial<GQL.SceneUpdateInput>;
|
||||
scenePerformers: Performer[];
|
||||
scraped: GQL.ScrapedScene;
|
||||
endpoint?: string;
|
||||
|
||||
onClose: (scrapedScene?: GQL.ScrapedScene) => void;
|
||||
}
|
||||
|
||||
interface IHasStoredID {
|
||||
stored_id?: string | null;
|
||||
}
|
||||
|
||||
export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
||||
scene,
|
||||
scenePerformers,
|
||||
scraped,
|
||||
onClose,
|
||||
endpoint,
|
||||
@@ -365,10 +116,17 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
||||
return ret;
|
||||
}
|
||||
|
||||
const [performers, setPerformers] = useState<ScrapeResult<string[]>>(
|
||||
new ScrapeResult<string[]>(
|
||||
sortIdList(scene.performer_ids),
|
||||
mapStoredIdObjects(scraped.performers ?? undefined)
|
||||
const [performers, setPerformers] = useState<
|
||||
ScrapeResult<GQL.ScrapedPerformer[]>
|
||||
>(
|
||||
new ScrapeResult<GQL.ScrapedPerformer[]>(
|
||||
sortStoredIdObjects(
|
||||
scenePerformers.map((p) => ({
|
||||
stored_id: p.id,
|
||||
name: p.name,
|
||||
}))
|
||||
),
|
||||
sortStoredIdObjects(scraped.performers ?? undefined)
|
||||
)
|
||||
);
|
||||
const [newPerformers, setNewPerformers] = useState<GQL.ScrapedPerformer[]>(
|
||||
@@ -403,13 +161,34 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
||||
new ScrapeResult<string>(scene.cover_image, scraped.image)
|
||||
);
|
||||
|
||||
const [createStudio] = useStudioCreate();
|
||||
const [createPerformer] = usePerformerCreate();
|
||||
const [createMovie] = useMovieCreate();
|
||||
const [createTag] = useTagCreate();
|
||||
const createNewStudio = useCreateScrapedStudio({
|
||||
scrapeResult: studio,
|
||||
setScrapeResult: setStudio,
|
||||
setNewObject: setNewStudio,
|
||||
});
|
||||
|
||||
const createNewPerformer = useCreateScrapedPerformer({
|
||||
scrapeResult: performers,
|
||||
setScrapeResult: setPerformers,
|
||||
newObjects: newPerformers,
|
||||
setNewObjects: setNewPerformers,
|
||||
});
|
||||
|
||||
const createNewMovie = useCreateScrapedMovie({
|
||||
scrapeResult: movies,
|
||||
setScrapeResult: setMovies,
|
||||
newObjects: newMovies,
|
||||
setNewObjects: setNewMovies,
|
||||
});
|
||||
|
||||
const createNewTag = useCreateScrapedTag({
|
||||
scrapeResult: tags,
|
||||
setScrapeResult: setTags,
|
||||
newObjects: newTags,
|
||||
setNewObjects: setNewTags,
|
||||
});
|
||||
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
// don't show the dialog if nothing was scraped
|
||||
if (
|
||||
@@ -436,143 +215,6 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
||||
return <></>;
|
||||
}
|
||||
|
||||
async function createNewStudio(toCreate: GQL.ScrapedStudio) {
|
||||
try {
|
||||
const result = await createStudio({
|
||||
variables: {
|
||||
input: {
|
||||
name: toCreate.name,
|
||||
url: toCreate.url,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// set the new studio as the value
|
||||
setStudio(studio.cloneWithValue(result.data!.studioCreate!.id));
|
||||
setNewStudio(undefined);
|
||||
|
||||
Toast.success({
|
||||
content: (
|
||||
<span>
|
||||
Created studio: <b>{toCreate.name}</b>
|
||||
</span>
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewPerformer(toCreate: GQL.ScrapedPerformer) {
|
||||
const input = scrapedPerformerToCreateInput(toCreate);
|
||||
|
||||
try {
|
||||
const result = await createPerformer({
|
||||
variables: { input },
|
||||
});
|
||||
|
||||
const newValue = [...(performers.newValue ?? [])];
|
||||
if (result.data?.performerCreate)
|
||||
newValue.push(result.data.performerCreate.id);
|
||||
|
||||
// add the new performer to the new performers value
|
||||
const performerClone = performers.cloneWithValue(newValue);
|
||||
setPerformers(performerClone);
|
||||
|
||||
// remove the performer from the list
|
||||
const newPerformersClone = newPerformers.concat();
|
||||
const pIndex = newPerformersClone.findIndex(
|
||||
(p) => p.name === toCreate.name
|
||||
);
|
||||
if (pIndex === -1) throw new Error("Could not find performer to remove");
|
||||
|
||||
newPerformersClone.splice(pIndex, 1);
|
||||
|
||||
setNewPerformers(newPerformersClone);
|
||||
|
||||
Toast.success({
|
||||
content: (
|
||||
<span>
|
||||
Created performer: <b>{toCreate.name}</b>
|
||||
</span>
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewMovie(toCreate: GQL.ScrapedMovie) {
|
||||
const movieInput = scrapedMovieToCreateInput(toCreate);
|
||||
try {
|
||||
const result = await createMovie({
|
||||
variables: { input: movieInput },
|
||||
});
|
||||
|
||||
// add the new movie to the new movies value
|
||||
const movieClone = movies.cloneWithValue(movies.newValue);
|
||||
if (!movieClone.newValue) {
|
||||
movieClone.newValue = [];
|
||||
}
|
||||
movieClone.newValue.push(result.data!.movieCreate!.id);
|
||||
setMovies(movieClone);
|
||||
|
||||
// remove the movie from the list
|
||||
const newMoviesClone = newMovies.concat();
|
||||
const pIndex = newMoviesClone.findIndex((p) => p.name === toCreate.name);
|
||||
if (pIndex === -1) throw new Error("Could not find movie to remove");
|
||||
newMoviesClone.splice(pIndex, 1);
|
||||
|
||||
setNewMovies(newMoviesClone);
|
||||
|
||||
Toast.success({
|
||||
content: (
|
||||
<span>
|
||||
Created movie: <b>{toCreate.name}</b>
|
||||
</span>
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewTag(toCreate: GQL.ScrapedTag) {
|
||||
const tagInput: GQL.TagCreateInput = { name: toCreate.name ?? "" };
|
||||
try {
|
||||
const result = await createTag({
|
||||
variables: {
|
||||
input: tagInput,
|
||||
},
|
||||
});
|
||||
|
||||
const newValue = [...(tags.newValue ?? [])];
|
||||
if (result.data?.tagCreate) newValue.push(result.data.tagCreate.id);
|
||||
|
||||
// add the new tag to the new tags value
|
||||
const tagClone = tags.cloneWithValue(newValue);
|
||||
setTags(tagClone);
|
||||
|
||||
// remove the tag from the list
|
||||
const newTagsClone = newTags.concat();
|
||||
const pIndex = newTagsClone.indexOf(toCreate);
|
||||
if (pIndex === -1) throw new Error("Could not find tag to remove");
|
||||
newTagsClone.splice(pIndex, 1);
|
||||
|
||||
setNewTags(newTagsClone);
|
||||
|
||||
Toast.success({
|
||||
content: (
|
||||
<span>
|
||||
Created tag: <b>{toCreate.name}</b>
|
||||
</span>
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function makeNewScrapedItem(): GQL.ScrapedSceneDataFragment {
|
||||
const newStudioValue = studio.getNewValue();
|
||||
|
||||
@@ -588,12 +230,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
||||
name: "",
|
||||
}
|
||||
: undefined,
|
||||
performers: performers.getNewValue()?.map((p) => {
|
||||
return {
|
||||
stored_id: p,
|
||||
name: "",
|
||||
};
|
||||
}),
|
||||
performers: performers.getNewValue(),
|
||||
movies: movies.getNewValue()?.map((m) => {
|
||||
return {
|
||||
stored_id: m,
|
||||
|
||||
@@ -12,26 +12,29 @@ import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { faExchangeAlt, faSignInAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
hasScrapedValues,
|
||||
ScrapeDialog,
|
||||
ScrapeDialogRow,
|
||||
ScrapedImageRow,
|
||||
ScrapedInputGroupRow,
|
||||
ScrapedStringListRow,
|
||||
ScrapedTextAreaRow,
|
||||
} from "../Shared/ScrapeDialog/ScrapeDialog";
|
||||
import { clone, uniq } from "lodash-es";
|
||||
import { galleryTitle } from "src/core/galleries";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { ModalComponent } from "../Shared/Modal";
|
||||
import { IHasStoredID, sortStoredIdObjects } from "src/utils/data";
|
||||
import {
|
||||
ScrapeResult,
|
||||
ZeroableScrapeResult,
|
||||
} from "../Shared/ScrapeDialog";
|
||||
import { clone, uniq } from "lodash-es";
|
||||
hasScrapedValues,
|
||||
} from "../Shared/ScrapeDialog/scrapeResult";
|
||||
import {
|
||||
ScrapedMoviesRow,
|
||||
ScrapedPerformersRow,
|
||||
ScrapedStudioRow,
|
||||
ScrapedTagsRow,
|
||||
} from "./SceneDetails/SceneScrapeDialog";
|
||||
import { galleryTitle } from "src/core/galleries";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { ModalComponent } from "../Shared/Modal";
|
||||
} from "../Shared/ScrapeDialog/ScrapedObjectsRow";
|
||||
|
||||
interface IStashIDsField {
|
||||
values: GQL.StashId[];
|
||||
@@ -101,8 +104,25 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
||||
return ret;
|
||||
}
|
||||
|
||||
const [performers, setPerformers] = useState<ScrapeResult<string[]>>(
|
||||
new ScrapeResult<string[]>(sortIdList(dest.performers.map((p) => p.id)))
|
||||
function idToStoredID(o: { id: string; name: string }) {
|
||||
return {
|
||||
stored_id: o.id,
|
||||
name: o.name,
|
||||
};
|
||||
}
|
||||
|
||||
function uniqIDStoredIDs(objs: IHasStoredID[]) {
|
||||
return objs.filter((o, i) => {
|
||||
return objs.findIndex((oo) => oo.stored_id === o.stored_id) === i;
|
||||
});
|
||||
}
|
||||
|
||||
const [performers, setPerformers] = useState<
|
||||
ScrapeResult<GQL.ScrapedPerformer[]>
|
||||
>(
|
||||
new ScrapeResult<GQL.ScrapedPerformer[]>(
|
||||
sortStoredIdObjects(dest.performers.map(idToStoredID))
|
||||
)
|
||||
);
|
||||
|
||||
const [movies, setMovies] = useState<ScrapeResult<string[]>>(
|
||||
@@ -184,8 +204,8 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
||||
|
||||
setPerformers(
|
||||
new ScrapeResult(
|
||||
dest.performers.map((p) => p.id),
|
||||
uniq(all.map((s) => s.performers.map((p) => p.id)).flat())
|
||||
dest.performers.map(idToStoredID),
|
||||
uniqIDStoredIDs(all.map((s) => s.performers.map(idToStoredID)).flat())
|
||||
)
|
||||
);
|
||||
setTags(
|
||||
@@ -559,7 +579,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
||||
play_duration: playDuration.getNewValue(),
|
||||
gallery_ids: galleries.getNewValue(),
|
||||
studio_id: studio.getNewValue(),
|
||||
performer_ids: performers.getNewValue(),
|
||||
performer_ids: performers.getNewValue()?.map((p) => p.stored_id!),
|
||||
movies: movies.getNewValue()?.map((m) => {
|
||||
// find the equivalent movie in the original scenes
|
||||
const found = all
|
||||
|
||||
@@ -8,10 +8,9 @@ import {
|
||||
FormControl,
|
||||
Badge,
|
||||
} from "react-bootstrap";
|
||||
import { CollapseButton } from "./CollapseButton";
|
||||
import { Icon } from "./Icon";
|
||||
import { ModalComponent } from "./Modal";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { CollapseButton } from "../CollapseButton";
|
||||
import { Icon } from "../Icon";
|
||||
import { ModalComponent } from "../Modal";
|
||||
import clone from "lodash-es/clone";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import {
|
||||
@@ -21,78 +20,10 @@ import {
|
||||
faTimes,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { getCountryByISO } from "src/utils/country";
|
||||
import { CountrySelect } from "./CountrySelect";
|
||||
import { StringListInput } from "./StringListInput";
|
||||
import { ImageSelector } from "./ImageSelector";
|
||||
|
||||
export class ScrapeResult<T> {
|
||||
public newValue?: T;
|
||||
public originalValue?: T;
|
||||
public scraped: boolean = false;
|
||||
public useNewValue: boolean = false;
|
||||
|
||||
public constructor(
|
||||
originalValue?: T | null,
|
||||
newValue?: T | null,
|
||||
useNewValue?: boolean
|
||||
) {
|
||||
this.originalValue = originalValue ?? undefined;
|
||||
this.newValue = newValue ?? undefined;
|
||||
// NOTE: this means that zero values are treated as null
|
||||
// this is incorrect for numbers and booleans, but correct for strings
|
||||
const hasNewValue = !!this.newValue;
|
||||
|
||||
const valuesEqual = isEqual(originalValue, newValue);
|
||||
this.useNewValue = useNewValue ?? (hasNewValue && !valuesEqual);
|
||||
this.scraped = hasNewValue && !valuesEqual;
|
||||
}
|
||||
|
||||
public setOriginalValue(value?: T) {
|
||||
this.originalValue = value;
|
||||
this.newValue = value;
|
||||
}
|
||||
|
||||
public cloneWithValue(value?: T) {
|
||||
const ret = clone(this);
|
||||
|
||||
ret.newValue = value;
|
||||
ret.useNewValue = !isEqual(ret.newValue, ret.originalValue);
|
||||
|
||||
// #2691 - if we're setting the value, assume it should be treated as
|
||||
// scraped
|
||||
ret.scraped = true;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public getNewValue() {
|
||||
if (this.useNewValue) {
|
||||
return this.newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for types where !!value is a valid value (boolean and number)
|
||||
export class ZeroableScrapeResult<T> extends ScrapeResult<T> {
|
||||
public constructor(
|
||||
originalValue?: T | null,
|
||||
newValue?: T | null,
|
||||
useNewValue?: boolean
|
||||
) {
|
||||
super(originalValue, newValue, useNewValue);
|
||||
|
||||
const hasNewValue = this.newValue !== undefined;
|
||||
|
||||
const valuesEqual = isEqual(originalValue, newValue);
|
||||
this.useNewValue = useNewValue ?? (hasNewValue && !valuesEqual);
|
||||
this.scraped = hasNewValue && !valuesEqual;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function hasScrapedValues(values: ScrapeResult<any>[]) {
|
||||
return values.some((r) => r.scraped);
|
||||
}
|
||||
import { CountrySelect } from "../CountrySelect";
|
||||
import { StringListInput } from "../StringListInput";
|
||||
import { ImageSelector } from "../ImageSelector";
|
||||
import { ScrapeResult } from "./scrapeResult";
|
||||
|
||||
export interface IHasName {
|
||||
name: string | undefined;
|
||||
269
ui/v2.5/src/components/Shared/ScrapeDialog/ScrapedObjectsRow.tsx
Normal file
269
ui/v2.5/src/components/Shared/ScrapeDialog/ScrapedObjectsRow.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import React, { useMemo } from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
MovieSelect,
|
||||
TagSelect,
|
||||
StudioSelect,
|
||||
} from "src/components/Shared/Select";
|
||||
import {
|
||||
ScrapeDialogRow,
|
||||
IHasName,
|
||||
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||
import { PerformerSelect } from "src/components/Performers/PerformerSelect";
|
||||
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
|
||||
|
||||
interface IScrapedStudioRow {
|
||||
title: string;
|
||||
result: ScrapeResult<string>;
|
||||
onChange: (value: ScrapeResult<string>) => void;
|
||||
newStudio?: GQL.ScrapedStudio;
|
||||
onCreateNew?: (value: GQL.ScrapedStudio) => void;
|
||||
}
|
||||
|
||||
export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
|
||||
title,
|
||||
result,
|
||||
onChange,
|
||||
newStudio,
|
||||
onCreateNew,
|
||||
}) => {
|
||||
function renderScrapedStudio(
|
||||
scrapeResult: ScrapeResult<string>,
|
||||
isNew?: boolean,
|
||||
onChangeFn?: (value: string) => void
|
||||
) {
|
||||
const resultValue = isNew
|
||||
? scrapeResult.newValue
|
||||
: scrapeResult.originalValue;
|
||||
const value = resultValue ? [resultValue] : [];
|
||||
|
||||
return (
|
||||
<StudioSelect
|
||||
className="form-control react-select"
|
||||
isDisabled={!isNew}
|
||||
onSelect={(items) => {
|
||||
if (onChangeFn) {
|
||||
onChangeFn(items[0]?.id);
|
||||
}
|
||||
}}
|
||||
ids={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedStudio(result)}
|
||||
renderNewField={() =>
|
||||
renderScrapedStudio(result, true, (value) =>
|
||||
onChange(result.cloneWithValue(value))
|
||||
)
|
||||
}
|
||||
onChange={onChange}
|
||||
newValues={newStudio ? [newStudio] : undefined}
|
||||
onCreateNew={() => {
|
||||
if (onCreateNew && newStudio) onCreateNew(newStudio);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface IScrapedObjectsRow<T, R> {
|
||||
title: string;
|
||||
result: ScrapeResult<R[]>;
|
||||
onChange: (value: ScrapeResult<R[]>) => void;
|
||||
newObjects?: T[];
|
||||
onCreateNew?: (value: T) => void;
|
||||
renderObjects: (
|
||||
result: ScrapeResult<R[]>,
|
||||
isNew?: boolean,
|
||||
onChange?: (value: R[]) => void
|
||||
) => JSX.Element;
|
||||
}
|
||||
|
||||
export const ScrapedObjectsRow = <T extends IHasName, R>(
|
||||
props: IScrapedObjectsRow<T, R>
|
||||
) => {
|
||||
const { title, result, onChange, newObjects, onCreateNew, renderObjects } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderObjects(result)}
|
||||
renderNewField={() =>
|
||||
renderObjects(result, true, (value) =>
|
||||
onChange(result.cloneWithValue(value))
|
||||
)
|
||||
}
|
||||
onChange={onChange}
|
||||
newValues={newObjects}
|
||||
onCreateNew={(i) => {
|
||||
if (onCreateNew) onCreateNew(newObjects![i]);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type IScrapedObjectRowImpl<T, R> = Omit<
|
||||
IScrapedObjectsRow<T, R>,
|
||||
"renderObjects"
|
||||
>;
|
||||
|
||||
export const ScrapedPerformersRow: React.FC<
|
||||
IScrapedObjectRowImpl<GQL.ScrapedPerformer, GQL.ScrapedPerformer>
|
||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||
const performersCopy = useMemo(() => {
|
||||
return (
|
||||
newObjects?.map((p) => {
|
||||
const name: string = p.name ?? "";
|
||||
return { ...p, name };
|
||||
}) ?? []
|
||||
);
|
||||
}, [newObjects]);
|
||||
|
||||
function renderScrapedPerformers(
|
||||
scrapeResult: ScrapeResult<GQL.ScrapedPerformer[]>,
|
||||
isNew?: boolean,
|
||||
onChangeFn?: (value: GQL.ScrapedPerformer[]) => void
|
||||
) {
|
||||
const resultValue = isNew
|
||||
? scrapeResult.newValue
|
||||
: scrapeResult.originalValue;
|
||||
const value = resultValue ?? [];
|
||||
|
||||
const selectValue = value.map((p) => {
|
||||
const alias_list: string[] = [];
|
||||
return {
|
||||
id: p.stored_id ?? "",
|
||||
name: p.name ?? "",
|
||||
alias_list,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<PerformerSelect
|
||||
isMulti
|
||||
className="form-control react-select"
|
||||
isDisabled={!isNew}
|
||||
onSelect={(items) => {
|
||||
if (onChangeFn) {
|
||||
onChangeFn(items);
|
||||
}
|
||||
}}
|
||||
values={selectValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type PerformerType = GQL.ScrapedPerformer & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrapedObjectsRow<PerformerType, GQL.ScrapedPerformer>
|
||||
title={title}
|
||||
result={result}
|
||||
renderObjects={renderScrapedPerformers}
|
||||
onChange={onChange}
|
||||
newObjects={performersCopy}
|
||||
onCreateNew={onCreateNew}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScrapedMoviesRow: React.FC<
|
||||
IScrapedObjectRowImpl<GQL.ScrapedMovie, string>
|
||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||
const moviesCopy = useMemo(() => {
|
||||
return (
|
||||
newObjects?.map((p) => {
|
||||
const name: string = p.name ?? "";
|
||||
return { ...p, name };
|
||||
}) ?? []
|
||||
);
|
||||
}, [newObjects]);
|
||||
|
||||
type MovieType = GQL.ScrapedMovie & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
function renderScrapedMovies(
|
||||
scrapeResult: ScrapeResult<string[]>,
|
||||
isNew?: boolean,
|
||||
onChangeFn?: (value: string[]) => void
|
||||
) {
|
||||
const resultValue = isNew
|
||||
? scrapeResult.newValue
|
||||
: scrapeResult.originalValue;
|
||||
const value = resultValue ?? [];
|
||||
|
||||
return (
|
||||
<MovieSelect
|
||||
isMulti
|
||||
className="form-control react-select"
|
||||
isDisabled={!isNew}
|
||||
onSelect={(items) => {
|
||||
if (onChangeFn) {
|
||||
onChangeFn(items.map((i) => i.id));
|
||||
}
|
||||
}}
|
||||
ids={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrapedObjectsRow<MovieType, string>
|
||||
title={title}
|
||||
result={result}
|
||||
renderObjects={renderScrapedMovies}
|
||||
onChange={onChange}
|
||||
newObjects={moviesCopy}
|
||||
onCreateNew={onCreateNew}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScrapedTagsRow: React.FC<
|
||||
IScrapedObjectRowImpl<GQL.ScrapedTag, string>
|
||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||
function renderScrapedTags(
|
||||
scrapeResult: ScrapeResult<string[]>,
|
||||
isNew?: boolean,
|
||||
onChangeFn?: (value: string[]) => void
|
||||
) {
|
||||
const resultValue = isNew
|
||||
? scrapeResult.newValue
|
||||
: scrapeResult.originalValue;
|
||||
const value = resultValue ?? [];
|
||||
|
||||
return (
|
||||
<TagSelect
|
||||
isMulti
|
||||
className="form-control react-select"
|
||||
isDisabled={!isNew}
|
||||
onSelect={(items) => {
|
||||
if (onChangeFn) {
|
||||
onChangeFn(items.map((i) => i.id));
|
||||
}
|
||||
}}
|
||||
ids={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrapedObjectsRow<GQL.ScrapedTag, string>
|
||||
title={title}
|
||||
result={result}
|
||||
renderObjects={renderScrapedTags}
|
||||
onChange={onChange}
|
||||
newObjects={newObjects}
|
||||
onCreateNew={onCreateNew}
|
||||
/>
|
||||
);
|
||||
};
|
||||
192
ui/v2.5/src/components/Shared/ScrapeDialog/createObjects.ts
Normal file
192
ui/v2.5/src/components/Shared/ScrapeDialog/createObjects.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
useMovieCreate,
|
||||
usePerformerCreate,
|
||||
useStudioCreate,
|
||||
useTagCreate,
|
||||
} from "src/core/StashService";
|
||||
import { ScrapeResult } from "./scrapeResult";
|
||||
import { useIntl } from "react-intl";
|
||||
import { scrapedPerformerToCreateInput } from "src/core/performers";
|
||||
import { scrapedMovieToCreateInput } from "src/core/movies";
|
||||
|
||||
function useCreateObject<T>(
|
||||
entityTypeID: string,
|
||||
createFunc: (o: T) => Promise<void>
|
||||
) {
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
|
||||
async function createNewObject(o: T) {
|
||||
try {
|
||||
await createFunc(o);
|
||||
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.created_entity" },
|
||||
{
|
||||
entity: intl
|
||||
.formatMessage({ id: entityTypeID })
|
||||
.toLocaleLowerCase(),
|
||||
}
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return createNewObject;
|
||||
}
|
||||
|
||||
interface IUseCreateNewStudioProps {
|
||||
scrapeResult: ScrapeResult<string>;
|
||||
setScrapeResult: (scrapeResult: ScrapeResult<string>) => void;
|
||||
setNewObject: (newObject: GQL.ScrapedStudio | undefined) => void;
|
||||
}
|
||||
|
||||
export function useCreateScrapedStudio(props: IUseCreateNewStudioProps) {
|
||||
const [createStudio] = useStudioCreate();
|
||||
|
||||
const { scrapeResult, setScrapeResult, setNewObject } = props;
|
||||
|
||||
async function createNewStudio(toCreate: GQL.ScrapedStudio) {
|
||||
const result = await createStudio({
|
||||
variables: {
|
||||
input: {
|
||||
name: toCreate.name,
|
||||
url: toCreate.url,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// set the new studio as the value
|
||||
setScrapeResult(scrapeResult.cloneWithValue(result.data!.studioCreate!.id));
|
||||
setNewObject(undefined);
|
||||
}
|
||||
|
||||
return useCreateObject("studio", createNewStudio);
|
||||
}
|
||||
|
||||
interface IUseCreateNewPerformerProps {
|
||||
scrapeResult: ScrapeResult<GQL.ScrapedPerformer[]>;
|
||||
setScrapeResult: (scrapeResult: ScrapeResult<GQL.ScrapedPerformer[]>) => void;
|
||||
newObjects: GQL.ScrapedPerformer[];
|
||||
setNewObjects: (newObject: GQL.ScrapedPerformer[]) => void;
|
||||
}
|
||||
|
||||
export function useCreateScrapedPerformer(props: IUseCreateNewPerformerProps) {
|
||||
const [createPerformer] = usePerformerCreate();
|
||||
|
||||
const { scrapeResult, setScrapeResult, newObjects, setNewObjects } = props;
|
||||
|
||||
async function createNewPerformer(toCreate: GQL.ScrapedPerformer) {
|
||||
const input = scrapedPerformerToCreateInput(toCreate);
|
||||
|
||||
const result = await createPerformer({
|
||||
variables: { input },
|
||||
});
|
||||
|
||||
const newValue = [...(scrapeResult.newValue ?? [])];
|
||||
if (result.data?.performerCreate)
|
||||
newValue.push({
|
||||
stored_id: result.data.performerCreate.id,
|
||||
name: result.data.performerCreate.name,
|
||||
});
|
||||
|
||||
// add the new performer to the new performers value
|
||||
const performerClone = scrapeResult.cloneWithValue(newValue);
|
||||
setScrapeResult(performerClone);
|
||||
|
||||
// remove the performer from the list
|
||||
const newPerformersClone = newObjects.concat();
|
||||
const pIndex = newPerformersClone.findIndex(
|
||||
(p) => p.name === toCreate.name
|
||||
);
|
||||
if (pIndex === -1) throw new Error("Could not find performer to remove");
|
||||
|
||||
newPerformersClone.splice(pIndex, 1);
|
||||
|
||||
setNewObjects(newPerformersClone);
|
||||
}
|
||||
|
||||
return useCreateObject("performer", createNewPerformer);
|
||||
}
|
||||
|
||||
interface IUseCreateNewObjectIDListProps<
|
||||
T extends { name?: string | undefined | null }
|
||||
> {
|
||||
scrapeResult: ScrapeResult<string[]>;
|
||||
setScrapeResult: (scrapeResult: ScrapeResult<string[]>) => void;
|
||||
newObjects: T[];
|
||||
setNewObjects: (newObject: T[]) => void;
|
||||
}
|
||||
|
||||
function useCreateNewObjectIDList<
|
||||
T extends { name?: string | undefined | null }
|
||||
>(
|
||||
entityTypeID: string,
|
||||
props: IUseCreateNewObjectIDListProps<T>,
|
||||
createObject: (toCreate: T) => Promise<string>
|
||||
) {
|
||||
const { scrapeResult, setScrapeResult, newObjects, setNewObjects } = props;
|
||||
|
||||
async function createNewObject(toCreate: T) {
|
||||
const newID = await createObject(toCreate);
|
||||
|
||||
// add the new object to the new objects value
|
||||
const newResult = scrapeResult.cloneWithValue(scrapeResult.newValue);
|
||||
if (!newResult.newValue) {
|
||||
newResult.newValue = [];
|
||||
}
|
||||
newResult.newValue.push(newID);
|
||||
setScrapeResult(newResult);
|
||||
|
||||
// remove the object from the list
|
||||
const newObjectsClone = newObjects.concat();
|
||||
const pIndex = newObjectsClone.findIndex((p) => p.name === toCreate.name);
|
||||
if (pIndex === -1) throw new Error("Could not find object to remove");
|
||||
newObjectsClone.splice(pIndex, 1);
|
||||
|
||||
setNewObjects(newObjectsClone);
|
||||
}
|
||||
|
||||
return useCreateObject(entityTypeID, createNewObject);
|
||||
}
|
||||
|
||||
export function useCreateScrapedMovie(
|
||||
props: IUseCreateNewObjectIDListProps<GQL.ScrapedMovie>
|
||||
) {
|
||||
const [createMovie] = useMovieCreate();
|
||||
|
||||
async function createNewMovie(toCreate: GQL.ScrapedMovie) {
|
||||
const movieInput = scrapedMovieToCreateInput(toCreate);
|
||||
const result = await createMovie({
|
||||
variables: { input: movieInput },
|
||||
});
|
||||
|
||||
return result.data?.movieCreate?.id ?? "";
|
||||
}
|
||||
|
||||
return useCreateNewObjectIDList("movie", props, createNewMovie);
|
||||
}
|
||||
|
||||
export function useCreateScrapedTag(
|
||||
props: IUseCreateNewObjectIDListProps<GQL.ScrapedTag>
|
||||
) {
|
||||
const [createTag] = useTagCreate();
|
||||
|
||||
async function createNewTag(toCreate: GQL.ScrapedTag) {
|
||||
const tagInput: GQL.TagCreateInput = { name: toCreate.name ?? "" };
|
||||
const result = await createTag({
|
||||
variables: {
|
||||
input: tagInput,
|
||||
},
|
||||
});
|
||||
|
||||
return result.data?.tagCreate?.id ?? "";
|
||||
}
|
||||
|
||||
return useCreateNewObjectIDList("tag", props, createNewTag);
|
||||
}
|
||||
71
ui/v2.5/src/components/Shared/ScrapeDialog/scrapeResult.ts
Normal file
71
ui/v2.5/src/components/Shared/ScrapeDialog/scrapeResult.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import clone from "lodash-es/clone";
|
||||
|
||||
export class ScrapeResult<T> {
|
||||
public newValue?: T;
|
||||
public originalValue?: T;
|
||||
public scraped: boolean = false;
|
||||
public useNewValue: boolean = false;
|
||||
|
||||
public constructor(
|
||||
originalValue?: T | null,
|
||||
newValue?: T | null,
|
||||
useNewValue?: boolean
|
||||
) {
|
||||
this.originalValue = originalValue ?? undefined;
|
||||
this.newValue = newValue ?? undefined;
|
||||
// NOTE: this means that zero values are treated as null
|
||||
// this is incorrect for numbers and booleans, but correct for strings
|
||||
const hasNewValue = !!this.newValue;
|
||||
|
||||
const valuesEqual = isEqual(originalValue, newValue);
|
||||
this.useNewValue = useNewValue ?? (hasNewValue && !valuesEqual);
|
||||
this.scraped = hasNewValue && !valuesEqual;
|
||||
}
|
||||
|
||||
public setOriginalValue(value?: T) {
|
||||
this.originalValue = value;
|
||||
this.newValue = value;
|
||||
}
|
||||
|
||||
public cloneWithValue(value?: T) {
|
||||
const ret = clone(this);
|
||||
|
||||
ret.newValue = value;
|
||||
ret.useNewValue = !isEqual(ret.newValue, ret.originalValue);
|
||||
|
||||
// #2691 - if we're setting the value, assume it should be treated as
|
||||
// scraped
|
||||
ret.scraped = true;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public getNewValue() {
|
||||
if (this.useNewValue) {
|
||||
return this.newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for types where !!value is a valid value (boolean and number)
|
||||
export class ZeroableScrapeResult<T> extends ScrapeResult<T> {
|
||||
public constructor(
|
||||
originalValue?: T | null,
|
||||
newValue?: T | null,
|
||||
useNewValue?: boolean
|
||||
) {
|
||||
super(originalValue, newValue, useNewValue);
|
||||
|
||||
const hasNewValue = this.newValue !== undefined;
|
||||
|
||||
const valuesEqual = isEqual(originalValue, newValue);
|
||||
this.useNewValue = useNewValue ?? (hasNewValue && !valuesEqual);
|
||||
this.scraped = hasNewValue && !valuesEqual;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function hasScrapedValues(values: ScrapeResult<any>[]) {
|
||||
return values.some((r) => r.scraped);
|
||||
}
|
||||
@@ -42,3 +42,27 @@ export function excludeFields(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export interface IHasStoredID {
|
||||
stored_id?: string | null;
|
||||
}
|
||||
|
||||
export function sortStoredIdObjects(
|
||||
scrapedObjects?: IHasStoredID[]
|
||||
): IHasStoredID[] | undefined {
|
||||
if (!scrapedObjects) {
|
||||
return undefined;
|
||||
}
|
||||
const ret = scrapedObjects.filter((p) => !!p.stored_id);
|
||||
|
||||
if (ret.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// sort by id numerically
|
||||
ret.sort((a, b) => {
|
||||
return parseInt(a.stored_id!, 10) - parseInt(b.stored_id!, 10);
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user