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 (
|
return (
|
||||||
<GalleryScrapeDialog
|
<GalleryScrapeDialog
|
||||||
gallery={currentGallery}
|
gallery={currentGallery}
|
||||||
|
galleryPerformers={performers}
|
||||||
scraped={scrapedGallery}
|
scraped={scrapedGallery}
|
||||||
onClose={(data) => {
|
onClose={(data) => {
|
||||||
onScrapeDialogClosed(data);
|
onScrapeDialogClosed(data);
|
||||||
|
|||||||
@@ -1,180 +1,29 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import {
|
|
||||||
StudioSelect,
|
|
||||||
PerformerSelect,
|
|
||||||
TagSelect,
|
|
||||||
} from "src/components/Shared/Select";
|
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
ScrapeDialog,
|
ScrapeDialog,
|
||||||
ScrapeDialogRow,
|
|
||||||
ScrapeResult,
|
|
||||||
ScrapedInputGroupRow,
|
ScrapedInputGroupRow,
|
||||||
ScrapedTextAreaRow,
|
ScrapedTextAreaRow,
|
||||||
} from "src/components/Shared/ScrapeDialog";
|
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||||
import clone from "lodash-es/clone";
|
import clone from "lodash-es/clone";
|
||||||
|
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
|
||||||
import {
|
import {
|
||||||
useStudioCreate,
|
ScrapedPerformersRow,
|
||||||
usePerformerCreate,
|
ScrapedStudioRow,
|
||||||
useTagCreate,
|
ScrapedTagsRow,
|
||||||
} from "src/core/StashService";
|
} from "src/components/Shared/ScrapeDialog/ScrapedObjectsRow";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { sortStoredIdObjects } from "src/utils/data";
|
||||||
import { scrapedPerformerToCreateInput } from "src/core/performers";
|
import { Performer } from "src/components/Performers/PerformerSelect";
|
||||||
|
import {
|
||||||
function renderScrapedStudio(
|
useCreateScrapedPerformer,
|
||||||
result: ScrapeResult<string>,
|
useCreateScrapedStudio,
|
||||||
isNew?: boolean,
|
useCreateScrapedTag,
|
||||||
onChange?: (value: string) => void
|
} from "src/components/Shared/ScrapeDialog/createObjects";
|
||||||
) {
|
|
||||||
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]);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IGalleryScrapeDialogProps {
|
interface IGalleryScrapeDialogProps {
|
||||||
gallery: Partial<GQL.GalleryUpdateInput>;
|
gallery: Partial<GQL.GalleryUpdateInput>;
|
||||||
|
galleryPerformers: Performer[];
|
||||||
scraped: GQL.ScrapedGallery;
|
scraped: GQL.ScrapedGallery;
|
||||||
|
|
||||||
onClose: (scrapedGallery?: GQL.ScrapedGallery) => void;
|
onClose: (scrapedGallery?: GQL.ScrapedGallery) => void;
|
||||||
@@ -247,10 +96,17 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [performers, setPerformers] = useState<ScrapeResult<string[]>>(
|
const [performers, setPerformers] = useState<
|
||||||
new ScrapeResult<string[]>(
|
ScrapeResult<GQL.ScrapedPerformer[]>
|
||||||
sortIdList(props.gallery.performer_ids),
|
>(
|
||||||
mapStoredIdObjects(props.scraped.performers ?? undefined)
|
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[]>(
|
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)
|
new ScrapeResult<string>(props.gallery.details, props.scraped.details)
|
||||||
);
|
);
|
||||||
|
|
||||||
const [createStudio] = useStudioCreate();
|
const createNewStudio = useCreateScrapedStudio({
|
||||||
const [createPerformer] = usePerformerCreate();
|
scrapeResult: studio,
|
||||||
const [createTag] = useTagCreate();
|
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
|
// don't show the dialog if nothing was scraped
|
||||||
if (
|
if (
|
||||||
@@ -290,122 +160,6 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
|||||||
return <></>;
|
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 {
|
function makeNewScrapedItem(): GQL.ScrapedGalleryDataFragment {
|
||||||
const newStudioValue = studio.getNewValue();
|
const newStudioValue = studio.getNewValue();
|
||||||
|
|
||||||
@@ -419,12 +173,7 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
|||||||
name: "",
|
name: "",
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
performers: performers.getNewValue()?.map((p) => {
|
performers: performers.getNewValue(),
|
||||||
return {
|
|
||||||
stored_id: p,
|
|
||||||
name: "",
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
tags: tags.getNewValue()?.map((m) => {
|
tags: tags.getNewValue()?.map((m) => {
|
||||||
return {
|
return {
|
||||||
stored_id: m,
|
stored_id: m,
|
||||||
@@ -454,27 +203,27 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
|||||||
result={date}
|
result={date}
|
||||||
onChange={(value) => setDate(value)}
|
onChange={(value) => setDate(value)}
|
||||||
/>
|
/>
|
||||||
{renderScrapedStudioRow(
|
<ScrapedStudioRow
|
||||||
intl.formatMessage({ id: "studios" }),
|
title={intl.formatMessage({ id: "studios" })}
|
||||||
studio,
|
result={studio}
|
||||||
(value) => setStudio(value),
|
onChange={(value) => setStudio(value)}
|
||||||
newStudio,
|
newStudio={newStudio}
|
||||||
createNewStudio
|
onCreateNew={createNewStudio}
|
||||||
)}
|
/>
|
||||||
{renderScrapedPerformersRow(
|
<ScrapedPerformersRow
|
||||||
intl.formatMessage({ id: "performers" }),
|
title={intl.formatMessage({ id: "performers" })}
|
||||||
performers,
|
result={performers}
|
||||||
(value) => setPerformers(value),
|
onChange={(value) => setPerformers(value)}
|
||||||
newPerformers,
|
newObjects={newPerformers}
|
||||||
createNewPerformer
|
onCreateNew={createNewPerformer}
|
||||||
)}
|
/>
|
||||||
{renderScrapedTagsRow(
|
<ScrapedTagsRow
|
||||||
intl.formatMessage({ id: "tags" }),
|
title={intl.formatMessage({ id: "tags" })}
|
||||||
tags,
|
result={tags}
|
||||||
(value) => setTags(value),
|
onChange={(value) => setTags(value)}
|
||||||
newTags,
|
newObjects={newTags}
|
||||||
createNewTag
|
onCreateNew={createNewTag}
|
||||||
)}
|
/>
|
||||||
<ScrapedTextAreaRow
|
<ScrapedTextAreaRow
|
||||||
title={intl.formatMessage({ id: "details" })}
|
title={intl.formatMessage({ id: "details" })}
|
||||||
result={details}
|
result={details}
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ import { useIntl } from "react-intl";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
ScrapeDialog,
|
ScrapeDialog,
|
||||||
ScrapeResult,
|
|
||||||
ScrapedInputGroupRow,
|
ScrapedInputGroupRow,
|
||||||
ScrapedImageRow,
|
ScrapedImageRow,
|
||||||
ScrapeDialogRow,
|
ScrapeDialogRow,
|
||||||
ScrapedTextAreaRow,
|
ScrapedTextAreaRow,
|
||||||
} from "src/components/Shared/ScrapeDialog";
|
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||||
import { StudioSelect } from "src/components/Shared/Select";
|
import { StudioSelect } from "src/components/Shared/Select";
|
||||||
import DurationUtils from "src/utils/duration";
|
import DurationUtils from "src/utils/duration";
|
||||||
import { useStudioCreate } from "src/core/StashService";
|
import { useStudioCreate } from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
|
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
|
||||||
|
|
||||||
function renderScrapedStudio(
|
function renderScrapedStudio(
|
||||||
result: ScrapeResult<string>,
|
result: ScrapeResult<string>,
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ import { useIntl } from "react-intl";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
ScrapeDialog,
|
ScrapeDialog,
|
||||||
ScrapeResult,
|
|
||||||
ScrapedInputGroupRow,
|
ScrapedInputGroupRow,
|
||||||
ScrapedImagesRow,
|
ScrapedImagesRow,
|
||||||
ScrapeDialogRow,
|
ScrapeDialogRow,
|
||||||
ScrapedTextAreaRow,
|
ScrapedTextAreaRow,
|
||||||
ScrapedCountryRow,
|
ScrapedCountryRow,
|
||||||
} from "src/components/Shared/ScrapeDialog";
|
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||||
import { useTagCreate } from "src/core/StashService";
|
import { useTagCreate } from "src/core/StashService";
|
||||||
import { Form } from "react-bootstrap";
|
import { Form } from "react-bootstrap";
|
||||||
import { TagSelect } from "src/components/Shared/Select";
|
import { TagSelect } from "src/components/Shared/Select";
|
||||||
@@ -26,6 +25,7 @@ import {
|
|||||||
stringToCircumcised,
|
stringToCircumcised,
|
||||||
} from "src/utils/circumcised";
|
} from "src/utils/circumcised";
|
||||||
import { IStashBox } from "./PerformerStashBoxModal";
|
import { IStashBox } from "./PerformerStashBoxModal";
|
||||||
|
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
|
||||||
|
|
||||||
function renderScrapedGender(
|
function renderScrapedGender(
|
||||||
result: ScrapeResult<string>,
|
result: ScrapeResult<string>,
|
||||||
|
|||||||
@@ -430,6 +430,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
return (
|
return (
|
||||||
<SceneScrapeDialog
|
<SceneScrapeDialog
|
||||||
scene={currentScene}
|
scene={currentScene}
|
||||||
|
scenePerformers={performers}
|
||||||
scraped={scrapedScene}
|
scraped={scrapedScene}
|
||||||
endpoint={endpoint}
|
endpoint={endpoint}
|
||||||
onClose={(s) => onScrapeDialogClosed(s)}
|
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 * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
|
||||||
MovieSelect,
|
|
||||||
TagSelect,
|
|
||||||
StudioSelect,
|
|
||||||
PerformerSelect,
|
|
||||||
} from "src/components/Shared/Select";
|
|
||||||
import {
|
import {
|
||||||
ScrapeDialog,
|
ScrapeDialog,
|
||||||
ScrapeDialogRow,
|
|
||||||
ScrapeResult,
|
|
||||||
ScrapedInputGroupRow,
|
ScrapedInputGroupRow,
|
||||||
ScrapedTextAreaRow,
|
ScrapedTextAreaRow,
|
||||||
ScrapedImageRow,
|
ScrapedImageRow,
|
||||||
IHasName,
|
|
||||||
ScrapedStringListRow,
|
ScrapedStringListRow,
|
||||||
} from "src/components/Shared/ScrapeDialog";
|
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||||
import clone from "lodash-es/clone";
|
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 { useIntl } from "react-intl";
|
||||||
import { uniq } from "lodash-es";
|
import { uniq } from "lodash-es";
|
||||||
import { scrapedPerformerToCreateInput } from "src/core/performers";
|
import { Performer } from "src/components/Performers/PerformerSelect";
|
||||||
import { scrapedMovieToCreateInput } from "src/core/movies";
|
import { IHasStoredID, sortStoredIdObjects } from "src/utils/data";
|
||||||
|
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
|
||||||
interface IScrapedStudioRow {
|
import {
|
||||||
title: string;
|
ScrapedMoviesRow,
|
||||||
result: ScrapeResult<string>;
|
ScrapedPerformersRow,
|
||||||
onChange: (value: ScrapeResult<string>) => void;
|
ScrapedStudioRow,
|
||||||
newStudio?: GQL.ScrapedStudio;
|
ScrapedTagsRow,
|
||||||
onCreateNew?: (value: GQL.ScrapedStudio) => void;
|
} from "src/components/Shared/ScrapeDialog/ScrapedObjectsRow";
|
||||||
}
|
import {
|
||||||
|
useCreateScrapedMovie,
|
||||||
export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
|
useCreateScrapedPerformer,
|
||||||
title,
|
useCreateScrapedStudio,
|
||||||
result,
|
useCreateScrapedTag,
|
||||||
onChange,
|
} from "src/components/Shared/ScrapeDialog/createObjects";
|
||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ISceneScrapeDialogProps {
|
interface ISceneScrapeDialogProps {
|
||||||
scene: Partial<GQL.SceneUpdateInput>;
|
scene: Partial<GQL.SceneUpdateInput>;
|
||||||
|
scenePerformers: Performer[];
|
||||||
scraped: GQL.ScrapedScene;
|
scraped: GQL.ScrapedScene;
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
|
|
||||||
onClose: (scrapedScene?: GQL.ScrapedScene) => void;
|
onClose: (scrapedScene?: GQL.ScrapedScene) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IHasStoredID {
|
|
||||||
stored_id?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
||||||
scene,
|
scene,
|
||||||
|
scenePerformers,
|
||||||
scraped,
|
scraped,
|
||||||
onClose,
|
onClose,
|
||||||
endpoint,
|
endpoint,
|
||||||
@@ -365,10 +116,17 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [performers, setPerformers] = useState<ScrapeResult<string[]>>(
|
const [performers, setPerformers] = useState<
|
||||||
new ScrapeResult<string[]>(
|
ScrapeResult<GQL.ScrapedPerformer[]>
|
||||||
sortIdList(scene.performer_ids),
|
>(
|
||||||
mapStoredIdObjects(scraped.performers ?? undefined)
|
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[]>(
|
const [newPerformers, setNewPerformers] = useState<GQL.ScrapedPerformer[]>(
|
||||||
@@ -403,13 +161,34 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
|||||||
new ScrapeResult<string>(scene.cover_image, scraped.image)
|
new ScrapeResult<string>(scene.cover_image, scraped.image)
|
||||||
);
|
);
|
||||||
|
|
||||||
const [createStudio] = useStudioCreate();
|
const createNewStudio = useCreateScrapedStudio({
|
||||||
const [createPerformer] = usePerformerCreate();
|
scrapeResult: studio,
|
||||||
const [createMovie] = useMovieCreate();
|
setScrapeResult: setStudio,
|
||||||
const [createTag] = useTagCreate();
|
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 intl = useIntl();
|
||||||
const Toast = useToast();
|
|
||||||
|
|
||||||
// don't show the dialog if nothing was scraped
|
// don't show the dialog if nothing was scraped
|
||||||
if (
|
if (
|
||||||
@@ -436,143 +215,6 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
|||||||
return <></>;
|
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 {
|
function makeNewScrapedItem(): GQL.ScrapedSceneDataFragment {
|
||||||
const newStudioValue = studio.getNewValue();
|
const newStudioValue = studio.getNewValue();
|
||||||
|
|
||||||
@@ -588,12 +230,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
|||||||
name: "",
|
name: "",
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
performers: performers.getNewValue()?.map((p) => {
|
performers: performers.getNewValue(),
|
||||||
return {
|
|
||||||
stored_id: p,
|
|
||||||
name: "",
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
movies: movies.getNewValue()?.map((m) => {
|
movies: movies.getNewValue()?.map((m) => {
|
||||||
return {
|
return {
|
||||||
stored_id: m,
|
stored_id: m,
|
||||||
|
|||||||
@@ -12,26 +12,29 @@ import { FormattedMessage, useIntl } from "react-intl";
|
|||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { faExchangeAlt, faSignInAlt } from "@fortawesome/free-solid-svg-icons";
|
import { faExchangeAlt, faSignInAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
import {
|
import {
|
||||||
hasScrapedValues,
|
|
||||||
ScrapeDialog,
|
ScrapeDialog,
|
||||||
ScrapeDialogRow,
|
ScrapeDialogRow,
|
||||||
ScrapedImageRow,
|
ScrapedImageRow,
|
||||||
ScrapedInputGroupRow,
|
ScrapedInputGroupRow,
|
||||||
ScrapedStringListRow,
|
ScrapedStringListRow,
|
||||||
ScrapedTextAreaRow,
|
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,
|
ScrapeResult,
|
||||||
ZeroableScrapeResult,
|
ZeroableScrapeResult,
|
||||||
} from "../Shared/ScrapeDialog";
|
hasScrapedValues,
|
||||||
import { clone, uniq } from "lodash-es";
|
} from "../Shared/ScrapeDialog/scrapeResult";
|
||||||
import {
|
import {
|
||||||
ScrapedMoviesRow,
|
ScrapedMoviesRow,
|
||||||
ScrapedPerformersRow,
|
ScrapedPerformersRow,
|
||||||
ScrapedStudioRow,
|
ScrapedStudioRow,
|
||||||
ScrapedTagsRow,
|
ScrapedTagsRow,
|
||||||
} from "./SceneDetails/SceneScrapeDialog";
|
} from "../Shared/ScrapeDialog/ScrapedObjectsRow";
|
||||||
import { galleryTitle } from "src/core/galleries";
|
|
||||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
|
||||||
import { ModalComponent } from "../Shared/Modal";
|
|
||||||
|
|
||||||
interface IStashIDsField {
|
interface IStashIDsField {
|
||||||
values: GQL.StashId[];
|
values: GQL.StashId[];
|
||||||
@@ -101,8 +104,25 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [performers, setPerformers] = useState<ScrapeResult<string[]>>(
|
function idToStoredID(o: { id: string; name: string }) {
|
||||||
new ScrapeResult<string[]>(sortIdList(dest.performers.map((p) => p.id)))
|
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[]>>(
|
const [movies, setMovies] = useState<ScrapeResult<string[]>>(
|
||||||
@@ -184,8 +204,8 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
|
|
||||||
setPerformers(
|
setPerformers(
|
||||||
new ScrapeResult(
|
new ScrapeResult(
|
||||||
dest.performers.map((p) => p.id),
|
dest.performers.map(idToStoredID),
|
||||||
uniq(all.map((s) => s.performers.map((p) => p.id)).flat())
|
uniqIDStoredIDs(all.map((s) => s.performers.map(idToStoredID)).flat())
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
setTags(
|
setTags(
|
||||||
@@ -559,7 +579,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
play_duration: playDuration.getNewValue(),
|
play_duration: playDuration.getNewValue(),
|
||||||
gallery_ids: galleries.getNewValue(),
|
gallery_ids: galleries.getNewValue(),
|
||||||
studio_id: studio.getNewValue(),
|
studio_id: studio.getNewValue(),
|
||||||
performer_ids: performers.getNewValue(),
|
performer_ids: performers.getNewValue()?.map((p) => p.stored_id!),
|
||||||
movies: movies.getNewValue()?.map((m) => {
|
movies: movies.getNewValue()?.map((m) => {
|
||||||
// find the equivalent movie in the original scenes
|
// find the equivalent movie in the original scenes
|
||||||
const found = all
|
const found = all
|
||||||
|
|||||||
@@ -8,10 +8,9 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
Badge,
|
Badge,
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
import { CollapseButton } from "./CollapseButton";
|
import { CollapseButton } from "../CollapseButton";
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "../Icon";
|
||||||
import { ModalComponent } from "./Modal";
|
import { ModalComponent } from "../Modal";
|
||||||
import isEqual from "lodash-es/isEqual";
|
|
||||||
import clone from "lodash-es/clone";
|
import clone from "lodash-es/clone";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import {
|
import {
|
||||||
@@ -21,78 +20,10 @@ import {
|
|||||||
faTimes,
|
faTimes,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { getCountryByISO } from "src/utils/country";
|
import { getCountryByISO } from "src/utils/country";
|
||||||
import { CountrySelect } from "./CountrySelect";
|
import { CountrySelect } from "../CountrySelect";
|
||||||
import { StringListInput } from "./StringListInput";
|
import { StringListInput } from "../StringListInput";
|
||||||
import { ImageSelector } from "./ImageSelector";
|
import { ImageSelector } from "../ImageSelector";
|
||||||
|
import { ScrapeResult } from "./scrapeResult";
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IHasName {
|
export interface IHasName {
|
||||||
name: string | undefined;
|
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