mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Scrape dialog (#644)
* Fix performer page button spacing * Improve scene URL scrape button styling
This commit is contained in:
@@ -3,6 +3,7 @@ import ReactMarkdown from "react-markdown";
|
||||
|
||||
const markup = `
|
||||
### ✨ New Features
|
||||
* Add post-scrape dialog.
|
||||
* Add various keyboard shortcuts (see manual).
|
||||
* Support deleting multiple scenes.
|
||||
* Add in-app help manual.
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
EditableTextUtils,
|
||||
} from "src/utils";
|
||||
import { useToast } from "src/hooks";
|
||||
import { PerformerScrapeDialog } from "./PerformerScrapeDialog";
|
||||
|
||||
interface IPerformerDetails {
|
||||
performer: Partial<GQL.PerformerDataFragment>;
|
||||
@@ -92,6 +93,10 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
const Scrapers = useListPerformerScrapers();
|
||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
||||
|
||||
const [scrapedPerformer, setScrapedPerformer] = useState<
|
||||
GQL.ScrapedPerformer | undefined
|
||||
>();
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, isEditing);
|
||||
|
||||
function updatePerformerEditState(
|
||||
@@ -144,15 +149,63 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
function updatePerformerEditStateFromScraper(
|
||||
state: Partial<GQL.ScrapedPerformerDataFragment>
|
||||
) {
|
||||
updatePerformerEditState(state);
|
||||
if (state.name) {
|
||||
setName(state.name);
|
||||
}
|
||||
|
||||
if (state.aliases) {
|
||||
setAliases(state.aliases ?? undefined);
|
||||
}
|
||||
if (state.birthdate) {
|
||||
setBirthdate(state.birthdate ?? undefined);
|
||||
}
|
||||
if (state.ethnicity) {
|
||||
setEthnicity(state.ethnicity ?? undefined);
|
||||
}
|
||||
if (state.country) {
|
||||
setCountry(state.country ?? undefined);
|
||||
}
|
||||
if (state.eye_color) {
|
||||
setEyeColor(state.eye_color ?? undefined);
|
||||
}
|
||||
if (state.height) {
|
||||
setHeight(state.height ?? undefined);
|
||||
}
|
||||
if (state.measurements) {
|
||||
setMeasurements(state.measurements ?? undefined);
|
||||
}
|
||||
if (state.fake_tits) {
|
||||
setFakeTits(state.fake_tits ?? undefined);
|
||||
}
|
||||
if (state.career_length) {
|
||||
setCareerLength(state.career_length ?? undefined);
|
||||
}
|
||||
if (state.tattoos) {
|
||||
setTattoos(state.tattoos ?? undefined);
|
||||
}
|
||||
if (state.piercings) {
|
||||
setPiercings(state.piercings ?? undefined);
|
||||
}
|
||||
if (state.url) {
|
||||
setUrl(state.url ?? undefined);
|
||||
}
|
||||
if (state.twitter) {
|
||||
setTwitter(state.twitter ?? undefined);
|
||||
}
|
||||
if (state.instagram) {
|
||||
setInstagram(state.instagram ?? undefined);
|
||||
}
|
||||
if (state.gender) {
|
||||
// gender is a string in the scraper data
|
||||
setGender(translateScrapedGender(state.gender ?? undefined));
|
||||
}
|
||||
|
||||
// image is a base64 string
|
||||
// #404: don't overwrite image if it has been modified by the user
|
||||
// overwrite if not new since it came from a dialog
|
||||
// otherwise follow existing behaviour
|
||||
if (
|
||||
image === undefined &&
|
||||
(!isNew || image === undefined) &&
|
||||
(state as GQL.ScrapedPerformerDataFragment).image !== undefined
|
||||
) {
|
||||
const imageStr = (state as GQL.ScrapedPerformerDataFragment).image;
|
||||
@@ -286,7 +339,13 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
getQueryScraperPerformerInput()
|
||||
);
|
||||
if (!result?.data?.scrapePerformer) return;
|
||||
|
||||
// if this is a new performer, just dump the data
|
||||
if (isNew) {
|
||||
updatePerformerEditStateFromScraper(result.data.scrapePerformer);
|
||||
} else {
|
||||
setScrapedPerformer(result.data.scrapePerformer);
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
@@ -303,11 +362,12 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// leave URL as is if not set explicitly
|
||||
if (!result.data.scrapePerformerURL.url) {
|
||||
result.data.scrapePerformerURL.url = url;
|
||||
}
|
||||
// if this is a new performer, just dump the data
|
||||
if (isNew) {
|
||||
updatePerformerEditStateFromScraper(result.data.scrapePerformerURL);
|
||||
} else {
|
||||
setScrapedPerformer(result.data.scrapePerformerURL);
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
@@ -362,7 +422,9 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
|
||||
return (
|
||||
<OverlayTrigger trigger="click" placement="top" overlay={popover}>
|
||||
<Button variant="secondary">Scrape with...</Button>
|
||||
<Button variant="secondary" className="mr-2">
|
||||
Scrape with...
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
@@ -411,6 +473,49 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderScrapeDialog() {
|
||||
if (!scrapedPerformer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPerformer: Partial<GQL.PerformerDataFragment> = {
|
||||
name,
|
||||
aliases,
|
||||
birthdate,
|
||||
ethnicity,
|
||||
country,
|
||||
eye_color: eyeColor,
|
||||
height,
|
||||
measurements,
|
||||
fake_tits: fakeTits,
|
||||
career_length: careerLength,
|
||||
tattoos,
|
||||
piercings,
|
||||
url,
|
||||
twitter,
|
||||
instagram,
|
||||
gender: stringToGender(gender),
|
||||
image_path: image ?? performer.image_path,
|
||||
};
|
||||
|
||||
return (
|
||||
<PerformerScrapeDialog
|
||||
performer={currentPerformer}
|
||||
scraped={scrapedPerformer}
|
||||
onClose={(p) => {
|
||||
onScrapeDialogClosed(p);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function onScrapeDialogClosed(p?: GQL.ScrapedPerformerDataFragment) {
|
||||
if (p) {
|
||||
updatePerformerEditStateFromScraper(p);
|
||||
}
|
||||
setScrapedPerformer(undefined);
|
||||
}
|
||||
|
||||
function renderURLField() {
|
||||
return (
|
||||
<tr>
|
||||
@@ -436,7 +541,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
return (
|
||||
<div className="row">
|
||||
<Button
|
||||
className="edit-button"
|
||||
className="mr-2"
|
||||
variant="primary"
|
||||
onClick={() => onSave?.(getPerformerInput())}
|
||||
>
|
||||
@@ -444,7 +549,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
</Button>
|
||||
{!isNew ? (
|
||||
<Button
|
||||
className="edit-button"
|
||||
className="mr-2"
|
||||
variant="danger"
|
||||
onClick={() => setIsDeleteAlertOpen(true)}
|
||||
>
|
||||
@@ -528,6 +633,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
<>
|
||||
{renderDeleteAlert()}
|
||||
{renderScraperDialog()}
|
||||
{maybeRenderScrapeDialog()}
|
||||
|
||||
<Table id="performer-details" className="w-100">
|
||||
<tbody>
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
import React, { useState } from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
ScrapeDialog,
|
||||
ScrapeResult,
|
||||
ScrapedInputGroupRow,
|
||||
ScrapedImageRow,
|
||||
ScrapeDialogRow,
|
||||
} from "src/components/Shared/ScrapeDialog";
|
||||
import {
|
||||
getGenderStrings,
|
||||
genderToString,
|
||||
stringToGender,
|
||||
} from "src/core/StashService";
|
||||
import { Form } from "react-bootstrap";
|
||||
|
||||
function renderScrapedGender(
|
||||
result: ScrapeResult<string>,
|
||||
isNew?: boolean,
|
||||
onChange?: (value: string) => void
|
||||
) {
|
||||
const selectOptions = [""].concat(getGenderStrings());
|
||||
|
||||
return (
|
||||
<Form.Control
|
||||
as="select"
|
||||
className="input-control"
|
||||
disabled={!isNew}
|
||||
plaintext={!isNew}
|
||||
value={isNew ? result.newValue : result.originalValue}
|
||||
onChange={(e) => {
|
||||
if (isNew && onChange) {
|
||||
onChange(e.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectOptions.map((opt) => (
|
||||
<option value={opt} key={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
);
|
||||
}
|
||||
|
||||
function renderScrapedGenderRow(
|
||||
result: ScrapeResult<string>,
|
||||
onChange: (value: ScrapeResult<string>) => void
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title="Gender"
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedGender(result)}
|
||||
renderNewField={() =>
|
||||
renderScrapedGender(result, true, (value) =>
|
||||
onChange(result.cloneWithValue(value))
|
||||
)
|
||||
}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface IPerformerScrapeDialogProps {
|
||||
performer: Partial<GQL.PerformerDataFragment>;
|
||||
scraped: GQL.ScrapedPerformer;
|
||||
|
||||
onClose: (scrapedPerformer?: GQL.ScrapedPerformer) => void;
|
||||
}
|
||||
|
||||
export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||
props: IPerformerScrapeDialogProps
|
||||
) => {
|
||||
function translateScrapedGender(scrapedGender?: string | null) {
|
||||
if (!scrapedGender) {
|
||||
return;
|
||||
}
|
||||
|
||||
let retEnum: GQL.GenderEnum | undefined;
|
||||
|
||||
// try to translate from enum values first
|
||||
const upperGender = scrapedGender?.toUpperCase();
|
||||
const asEnum = genderToString(upperGender as GQL.GenderEnum);
|
||||
if (asEnum) {
|
||||
retEnum = stringToGender(asEnum);
|
||||
} else {
|
||||
// try to match against gender strings
|
||||
const caseInsensitive = true;
|
||||
retEnum = stringToGender(scrapedGender, caseInsensitive);
|
||||
}
|
||||
|
||||
return genderToString(retEnum);
|
||||
}
|
||||
|
||||
const [name, setName] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.name, props.scraped.name)
|
||||
);
|
||||
const [aliases, setAliases] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.aliases, props.scraped.aliases)
|
||||
);
|
||||
const [birthdate, setBirthdate] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.birthdate, props.scraped.birthdate)
|
||||
);
|
||||
const [ethnicity, setEthnicity] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.ethnicity, props.scraped.ethnicity)
|
||||
);
|
||||
const [country, setCountry] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.country, props.scraped.country)
|
||||
);
|
||||
const [eyeColor, setEyeColor] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.eye_color, props.scraped.eye_color)
|
||||
);
|
||||
const [height, setHeight] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.height, props.scraped.height)
|
||||
);
|
||||
const [measurements, setMeasurements] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(
|
||||
props.performer.measurements,
|
||||
props.scraped.measurements
|
||||
)
|
||||
);
|
||||
const [fakeTits, setFakeTits] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.fake_tits, props.scraped.fake_tits)
|
||||
);
|
||||
const [careerLength, setCareerLength] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(
|
||||
props.performer.career_length,
|
||||
props.scraped.career_length
|
||||
)
|
||||
);
|
||||
const [tattoos, setTattoos] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.tattoos, props.scraped.tattoos)
|
||||
);
|
||||
const [piercings, setPiercings] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.piercings, props.scraped.piercings)
|
||||
);
|
||||
const [url, setURL] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.url, props.scraped.url)
|
||||
);
|
||||
const [twitter, setTwitter] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.twitter, props.scraped.twitter)
|
||||
);
|
||||
const [instagram, setInstagram] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.instagram, props.scraped.instagram)
|
||||
);
|
||||
const [gender, setGender] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(
|
||||
genderToString(props.performer.gender ?? undefined),
|
||||
translateScrapedGender(props.scraped.gender)
|
||||
)
|
||||
);
|
||||
|
||||
const [image, setImage] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.performer.image_path, props.scraped.image)
|
||||
);
|
||||
|
||||
const allFields = [
|
||||
name,
|
||||
aliases,
|
||||
birthdate,
|
||||
ethnicity,
|
||||
country,
|
||||
eyeColor,
|
||||
height,
|
||||
measurements,
|
||||
fakeTits,
|
||||
careerLength,
|
||||
tattoos,
|
||||
piercings,
|
||||
url,
|
||||
twitter,
|
||||
instagram,
|
||||
gender,
|
||||
image,
|
||||
];
|
||||
// don't show the dialog if nothing was scraped
|
||||
if (allFields.every((r) => !r.scraped)) {
|
||||
props.onClose();
|
||||
return <></>;
|
||||
}
|
||||
|
||||
function makeNewScrapedItem(): GQL.ScrapedPerformer {
|
||||
return {
|
||||
name: name.getNewValue(),
|
||||
aliases: aliases.getNewValue(),
|
||||
birthdate: birthdate.getNewValue(),
|
||||
ethnicity: ethnicity.getNewValue(),
|
||||
country: country.getNewValue(),
|
||||
eye_color: eyeColor.getNewValue(),
|
||||
height: height.getNewValue(),
|
||||
measurements: measurements.getNewValue(),
|
||||
fake_tits: fakeTits.getNewValue(),
|
||||
career_length: careerLength.getNewValue(),
|
||||
tattoos: tattoos.getNewValue(),
|
||||
piercings: piercings.getNewValue(),
|
||||
url: url.getNewValue(),
|
||||
twitter: twitter.getNewValue(),
|
||||
instagram: instagram.getNewValue(),
|
||||
gender: gender.getNewValue(),
|
||||
image: image.getNewValue(),
|
||||
};
|
||||
}
|
||||
|
||||
function renderScrapeRows() {
|
||||
return (
|
||||
<>
|
||||
<ScrapedInputGroupRow
|
||||
title="Name"
|
||||
result={name}
|
||||
onChange={(value) => setName(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Aliases"
|
||||
result={aliases}
|
||||
onChange={(value) => setAliases(value)}
|
||||
/>
|
||||
{renderScrapedGenderRow(gender, (value) => setGender(value))}
|
||||
<ScrapedInputGroupRow
|
||||
title="Birthdate"
|
||||
result={birthdate}
|
||||
onChange={(value) => setBirthdate(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Ethnicity"
|
||||
result={ethnicity}
|
||||
onChange={(value) => setEthnicity(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Country"
|
||||
result={country}
|
||||
onChange={(value) => setCountry(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Eye Color"
|
||||
result={eyeColor}
|
||||
onChange={(value) => setEyeColor(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Height"
|
||||
result={height}
|
||||
onChange={(value) => setHeight(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Measurements"
|
||||
result={measurements}
|
||||
onChange={(value) => setMeasurements(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Fake Tits"
|
||||
result={fakeTits}
|
||||
onChange={(value) => setFakeTits(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Career Length"
|
||||
result={careerLength}
|
||||
onChange={(value) => setCareerLength(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Tattoos"
|
||||
result={tattoos}
|
||||
onChange={(value) => setTattoos(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Piercings"
|
||||
result={piercings}
|
||||
onChange={(value) => setPiercings(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="URL"
|
||||
result={url}
|
||||
onChange={(value) => setURL(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Twitter"
|
||||
result={twitter}
|
||||
onChange={(value) => setTwitter(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Instagram"
|
||||
result={instagram}
|
||||
onChange={(value) => setInstagram(value)}
|
||||
/>
|
||||
<ScrapedImageRow
|
||||
title="Performer Image"
|
||||
className="performer-image"
|
||||
result={image}
|
||||
onChange={(value) => setImage(value)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrapeDialog
|
||||
title="Performer Scrape Results"
|
||||
renderScrapeRows={renderScrapeRows}
|
||||
onClose={(apply) => {
|
||||
props.onClose(apply ? makeNewScrapedItem() : undefined);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -71,3 +71,10 @@
|
||||
padding: 0 0 1rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.scrape-dialog .performer-image {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import { ImageUtils, FormUtils, EditableTextUtils } from "src/utils";
|
||||
import { MovieSelect } from "src/components/Shared/Select";
|
||||
import { SceneMovieTable, MovieSceneIndexMap } from "./SceneMovieTable";
|
||||
import { RatingStars } from "./RatingStars";
|
||||
import { SceneScrapeDialog } from "./SceneScrapeDialog";
|
||||
|
||||
interface IProps {
|
||||
scene: GQL.SceneDataFragment;
|
||||
@@ -59,6 +60,8 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
const Scrapers = useListSceneScrapers();
|
||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
||||
|
||||
const [scrapedScene, setScrapedScene] = useState<GQL.ScrapedScene | null>();
|
||||
|
||||
const [coverImagePreview, setCoverImagePreview] = useState<string>();
|
||||
|
||||
// Network state
|
||||
@@ -257,7 +260,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
if (!result.data || !result.data.scrapeScene) {
|
||||
return;
|
||||
}
|
||||
updateSceneFromScrapedScene(result.data.scrapeScene);
|
||||
setScrapedScene(result.data.scrapeScene);
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
@@ -279,6 +282,34 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
function onScrapeDialogClosed(scene?: GQL.ScrapedSceneDataFragment) {
|
||||
if (scene) {
|
||||
updateSceneFromScrapedScene(scene);
|
||||
}
|
||||
setScrapedScene(undefined);
|
||||
}
|
||||
|
||||
function maybeRenderScrapeDialog() {
|
||||
if (!scrapedScene) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentScene = getSceneInput();
|
||||
if (!currentScene.cover_image) {
|
||||
currentScene.cover_image = props.scene.paths.screenshot;
|
||||
}
|
||||
|
||||
return (
|
||||
<SceneScrapeDialog
|
||||
scene={currentScene}
|
||||
scraped={scrapedScene}
|
||||
onClose={(scene) => {
|
||||
onScrapeDialogClosed(scene);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderScraperMenu() {
|
||||
return (
|
||||
<DropdownButton id="scene-scrape" title="Scrape with...">
|
||||
@@ -304,31 +335,27 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
}
|
||||
|
||||
function updateSceneFromScrapedScene(scene: GQL.ScrapedSceneDataFragment) {
|
||||
if (!title && scene.title) {
|
||||
if (scene.title) {
|
||||
setTitle(scene.title);
|
||||
}
|
||||
|
||||
if (!details && scene.details) {
|
||||
if (scene.details) {
|
||||
setDetails(scene.details);
|
||||
}
|
||||
|
||||
if (!date && scene.date) {
|
||||
if (scene.date) {
|
||||
setDate(scene.date);
|
||||
}
|
||||
|
||||
if (!url && scene.url) {
|
||||
if (scene.url) {
|
||||
setUrl(scene.url);
|
||||
}
|
||||
|
||||
if (!studioId && scene.studio && scene.studio.id) {
|
||||
if (scene.studio && scene.studio.id) {
|
||||
setStudioId(scene.studio.id);
|
||||
}
|
||||
|
||||
if (
|
||||
(!performerIds || performerIds.length === 0) &&
|
||||
scene.performers &&
|
||||
scene.performers.length > 0
|
||||
) {
|
||||
if (scene.performers && scene.performers.length > 0) {
|
||||
const idPerfs = scene.performers.filter((p) => {
|
||||
return p.id !== undefined && p.id !== null;
|
||||
});
|
||||
@@ -339,11 +366,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(!movieIds || movieIds.length === 0) &&
|
||||
scene.movies &&
|
||||
scene.movies.length > 0
|
||||
) {
|
||||
if (scene.movies && scene.movies.length > 0) {
|
||||
const idMovis = scene.movies.filter((p) => {
|
||||
return p.id !== undefined && p.id !== null;
|
||||
});
|
||||
@@ -354,7 +377,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!tagIds?.length && scene?.tags?.length) {
|
||||
if (scene?.tags?.length) {
|
||||
const idTags = scene.tags.filter((p) => {
|
||||
return p.id !== undefined && p.id !== null;
|
||||
});
|
||||
@@ -382,7 +405,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
if (!result.data || !result.data.scrapeSceneURL) {
|
||||
return;
|
||||
}
|
||||
updateSceneFromScrapedScene(result.data.scrapeSceneURL);
|
||||
setScrapedScene(result.data.scrapeSceneURL);
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
@@ -395,8 +418,12 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
<Button id="scrape-url-button" onClick={onScrapeSceneURL} title="Scrape">
|
||||
<Icon icon="file-download" />
|
||||
<Button
|
||||
className="minimal scrape-url-button"
|
||||
onClick={onScrapeSceneURL}
|
||||
title="Scrape"
|
||||
>
|
||||
<Icon className="fa-fw" icon="file-download" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -404,7 +431,8 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
if (isLoading) return <LoadingIndicator />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="scene-edit-details">
|
||||
{maybeRenderScrapeDialog()}
|
||||
<div className="form-container row px-3 pt-3">
|
||||
<div className="col edit-buttons mb-3 pl-0">
|
||||
<Button className="edit-button" variant="primary" onClick={onSave}>
|
||||
@@ -429,9 +457,12 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
isEditing: true,
|
||||
})}
|
||||
<Form.Group controlId="url" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "URL",
|
||||
})}
|
||||
<Col xs={3} className="pr-0 url-label">
|
||||
<Form.Label className="col-form-label">URL</Form.Label>
|
||||
<div className="float-right scrape-button-container">
|
||||
{maybeRenderScrapeButton()}
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={9}>
|
||||
{EditableTextUtils.renderInputGroup({
|
||||
title: "URL",
|
||||
@@ -439,7 +470,6 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
onChange: setUrl,
|
||||
isEditing: true,
|
||||
})}
|
||||
{maybeRenderScrapeButton()}
|
||||
</Col>
|
||||
</Form.Group>
|
||||
{FormUtils.renderInputGroup({
|
||||
@@ -573,6 +603,6 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
365
ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx
Normal file
365
ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx
Normal file
@@ -0,0 +1,365 @@
|
||||
import React, { useState } from "react";
|
||||
import { StudioSelect, PerformerSelect } from "src/components/Shared";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { MovieSelect, TagSelect } from "src/components/Shared/Select";
|
||||
import {
|
||||
ScrapeDialog,
|
||||
ScrapeDialogRow,
|
||||
ScrapeResult,
|
||||
ScrapedInputGroupRow,
|
||||
ScrapedTextAreaRow,
|
||||
ScrapedImageRow,
|
||||
} from "src/components/Shared/ScrapeDialog";
|
||||
import _ from "lodash";
|
||||
|
||||
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(
|
||||
result: ScrapeResult<string>,
|
||||
onChange: (value: ScrapeResult<string>) => void
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title="Studio"
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedStudio(result)}
|
||||
renderNewField={() =>
|
||||
renderScrapedStudio(result, true, (value) =>
|
||||
onChange(result.cloneWithValue(value))
|
||||
)
|
||||
}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
result: ScrapeResult<string[]>,
|
||||
onChange: (value: ScrapeResult<string[]>) => void
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title="Performers"
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedPerformers(result)}
|
||||
renderNewField={() =>
|
||||
renderScrapedPerformers(result, true, (value) =>
|
||||
onChange(result.cloneWithValue(value))
|
||||
)
|
||||
}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderScrapedMovies(
|
||||
result: ScrapeResult<string[]>,
|
||||
isNew?: boolean,
|
||||
onChange?: (value: string[]) => void
|
||||
) {
|
||||
const resultValue = isNew ? result.newValue : result.originalValue;
|
||||
const value = resultValue ?? [];
|
||||
|
||||
return (
|
||||
<MovieSelect
|
||||
isMulti
|
||||
className="form-control react-select"
|
||||
isDisabled={!isNew}
|
||||
onSelect={(items) => {
|
||||
if (onChange) {
|
||||
onChange(items.map((i) => i.id));
|
||||
}
|
||||
}}
|
||||
ids={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderScrapedMoviesRow(
|
||||
result: ScrapeResult<string[]>,
|
||||
onChange: (value: ScrapeResult<string[]>) => void
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title="Movies"
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedMovies(result)}
|
||||
renderNewField={() =>
|
||||
renderScrapedMovies(result, true, (value) =>
|
||||
onChange(result.cloneWithValue(value))
|
||||
)
|
||||
}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
result: ScrapeResult<string[]>,
|
||||
onChange: (value: ScrapeResult<string[]>) => void
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title="Tags"
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedTags(result)}
|
||||
renderNewField={() =>
|
||||
renderScrapedTags(result, true, (value) =>
|
||||
onChange(result.cloneWithValue(value))
|
||||
)
|
||||
}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface ISceneScrapeDialogProps {
|
||||
scene: Partial<GQL.SceneUpdateInput>;
|
||||
scraped: GQL.ScrapedScene;
|
||||
|
||||
onClose: (scrapedScene?: GQL.ScrapedScene) => void;
|
||||
}
|
||||
|
||||
interface IHasID {
|
||||
id?: string | null;
|
||||
}
|
||||
|
||||
export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
||||
props: ISceneScrapeDialogProps
|
||||
) => {
|
||||
const [title, setTitle] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.scene.title, props.scraped.title)
|
||||
);
|
||||
const [url, setURL] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.scene.url, props.scraped.url)
|
||||
);
|
||||
const [date, setDate] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.scene.date, props.scraped.date)
|
||||
);
|
||||
const [studio, setStudio] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.scene.studio_id, props.scraped.studio?.id)
|
||||
);
|
||||
|
||||
function mapIdObjects(scrapedObjects?: IHasID[]): string[] | undefined {
|
||||
if (!scrapedObjects) {
|
||||
return undefined;
|
||||
}
|
||||
const ret = scrapedObjects
|
||||
.map((p) => p.id)
|
||||
.filter((p) => {
|
||||
return p !== undefined && p !== null;
|
||||
}) as string[];
|
||||
|
||||
if (ret.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// sort by id numerically
|
||||
ret.sort((a, b) => {
|
||||
return parseInt(a) - parseInt(b);
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function sortIdList(idList?: string[] | null) {
|
||||
if (!idList) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ret = _.clone(idList);
|
||||
// sort by id numerically
|
||||
ret.sort((a, b) => {
|
||||
return parseInt(a) - parseInt(b);
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
const [performers, setPerformers] = useState<ScrapeResult<string[]>>(
|
||||
new ScrapeResult<string[]>(
|
||||
sortIdList(props.scene.performer_ids),
|
||||
mapIdObjects(props.scraped.performers ?? undefined)
|
||||
)
|
||||
);
|
||||
const [movies, setMovies] = useState<ScrapeResult<string[]>>(
|
||||
new ScrapeResult<string[]>(
|
||||
sortIdList(props.scene.movies?.map((p) => p.movie_id)),
|
||||
mapIdObjects(props.scraped.movies ?? undefined)
|
||||
)
|
||||
);
|
||||
const [tags, setTags] = useState<ScrapeResult<string[]>>(
|
||||
new ScrapeResult<string[]>(
|
||||
sortIdList(props.scene.tag_ids),
|
||||
mapIdObjects(props.scraped.tags ?? undefined)
|
||||
)
|
||||
);
|
||||
const [details, setDetails] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.scene.details, props.scraped.details)
|
||||
);
|
||||
const [image, setImage] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.scene.cover_image, props.scraped.image)
|
||||
);
|
||||
|
||||
// don't show the dialog if nothing was scraped
|
||||
if (
|
||||
[title, url, date, studio, performers, movies, tags, details, image].every(
|
||||
(r) => !r.scraped
|
||||
)
|
||||
) {
|
||||
props.onClose();
|
||||
return <></>;
|
||||
}
|
||||
|
||||
function makeNewScrapedItem() {
|
||||
const newStudio = studio.getNewValue();
|
||||
|
||||
return {
|
||||
title: title.getNewValue(),
|
||||
url: url.getNewValue(),
|
||||
date: date.getNewValue(),
|
||||
studio: newStudio
|
||||
? {
|
||||
id: newStudio,
|
||||
name: "",
|
||||
}
|
||||
: undefined,
|
||||
performers: performers.getNewValue()?.map((p) => {
|
||||
return {
|
||||
id: p,
|
||||
name: "",
|
||||
};
|
||||
}),
|
||||
movies: movies.getNewValue()?.map((m) => {
|
||||
return {
|
||||
id: m,
|
||||
name: "",
|
||||
};
|
||||
}),
|
||||
tags: tags.getNewValue()?.map((m) => {
|
||||
return {
|
||||
id: m,
|
||||
name: "",
|
||||
};
|
||||
}),
|
||||
details: details.getNewValue(),
|
||||
image: image.getNewValue(),
|
||||
};
|
||||
}
|
||||
|
||||
function renderScrapeRows() {
|
||||
return (
|
||||
<>
|
||||
<ScrapedInputGroupRow
|
||||
title="Title"
|
||||
result={title}
|
||||
onChange={(value) => setTitle(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="URL"
|
||||
result={url}
|
||||
onChange={(value) => setURL(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Date"
|
||||
placeholder="YYYY-MM-DD"
|
||||
result={date}
|
||||
onChange={(value) => setDate(value)}
|
||||
/>
|
||||
{renderScrapedStudioRow(studio, (value) => setStudio(value))}
|
||||
{renderScrapedPerformersRow(performers, (value) =>
|
||||
setPerformers(value)
|
||||
)}
|
||||
{renderScrapedMoviesRow(movies, (value) => setMovies(value))}
|
||||
{renderScrapedTagsRow(tags, (value) => setTags(value))}
|
||||
<ScrapedTextAreaRow
|
||||
title="Details"
|
||||
result={details}
|
||||
onChange={(value) => setDetails(value)}
|
||||
/>
|
||||
<ScrapedImageRow
|
||||
title="Cover Image"
|
||||
className="scene-cover"
|
||||
result={image}
|
||||
onChange={(value) => setImage(value)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrapeDialog
|
||||
title="Scene Scrape Results"
|
||||
renderScrapeRows={renderScrapeRows}
|
||||
onClose={(apply) => {
|
||||
props.onClose(apply ? makeNewScrapedItem() : undefined);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -90,7 +90,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
#details {
|
||||
textarea.scene-description {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
@@ -284,7 +284,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
#scene-edit-details .rating-stars {
|
||||
#scene-edit-details {
|
||||
.rating-stars {
|
||||
font-size: 1.3em;
|
||||
height: calc(1.5em + 0.75rem + 2px);
|
||||
}
|
||||
|
||||
.scrape-button-container {
|
||||
margin-right: -15px;
|
||||
}
|
||||
|
||||
.scrape-url-button {
|
||||
color: $text-color;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Button, Modal, Spinner } from "react-bootstrap";
|
||||
import { Button, Modal, Spinner, ModalProps } from "react-bootstrap";
|
||||
import { Icon } from "src/components/Shared";
|
||||
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
@@ -17,6 +17,7 @@ interface IModal {
|
||||
cancel?: IButton;
|
||||
accept?: IButton;
|
||||
isRunning?: boolean;
|
||||
modalProps?: ModalProps;
|
||||
}
|
||||
|
||||
const ModalComponent: React.FC<IModal> = ({
|
||||
@@ -28,8 +29,9 @@ const ModalComponent: React.FC<IModal> = ({
|
||||
accept,
|
||||
onHide,
|
||||
isRunning,
|
||||
modalProps,
|
||||
}) => (
|
||||
<Modal keyboard={false} onHide={onHide} show={show}>
|
||||
<Modal keyboard={false} onHide={onHide} show={show} {...modalProps}>
|
||||
<Modal.Header>
|
||||
{icon ? <Icon icon={icon} /> : ""}
|
||||
<span>{header ?? ""}</span>
|
||||
|
||||
325
ui/v2.5/src/components/Shared/ScrapeDialog.tsx
Normal file
325
ui/v2.5/src/components/Shared/ScrapeDialog.tsx
Normal file
@@ -0,0 +1,325 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Form,
|
||||
Col,
|
||||
Row,
|
||||
InputGroup,
|
||||
Button,
|
||||
FormControl,
|
||||
} from "react-bootstrap";
|
||||
import { Icon, Modal } from "src/components/Shared";
|
||||
import _ from "lodash";
|
||||
|
||||
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) {
|
||||
this.originalValue = originalValue ?? undefined;
|
||||
this.newValue = newValue ?? undefined;
|
||||
|
||||
const valuesEqual = _.isEqual(originalValue, newValue);
|
||||
this.useNewValue = !!this.newValue && !valuesEqual;
|
||||
this.scraped = this.useNewValue;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public getNewValue() {
|
||||
if (this.useNewValue) {
|
||||
return this.newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IScrapedFieldProps<T> {
|
||||
result: ScrapeResult<T>;
|
||||
}
|
||||
|
||||
interface IScrapedRowProps<T> extends IScrapedFieldProps<T> {
|
||||
title: string;
|
||||
renderOriginalField: (result: ScrapeResult<T>) => JSX.Element | undefined;
|
||||
renderNewField: (result: ScrapeResult<T>) => JSX.Element | undefined;
|
||||
onChange: (value: ScrapeResult<T>) => void;
|
||||
}
|
||||
|
||||
function renderButtonIcon(selected: boolean) {
|
||||
const className = selected ? "text-success" : "text-muted";
|
||||
|
||||
return (
|
||||
<Icon
|
||||
className={`fa-fw ${className}`}
|
||||
icon={selected ? "check" : "times"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const ScrapeDialogRow = <T,>(props: IScrapedRowProps<T>) => {
|
||||
function handleSelectClick(isNew: boolean) {
|
||||
const ret = _.clone(props.result);
|
||||
ret.useNewValue = isNew;
|
||||
props.onChange(ret);
|
||||
}
|
||||
|
||||
if (!props.result.scraped) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Row className="px-3 pt-3">
|
||||
<Form.Label column lg="3">
|
||||
{props.title}
|
||||
</Form.Label>
|
||||
|
||||
<Col lg="9">
|
||||
<Row>
|
||||
<Col xs="6">
|
||||
<InputGroup>
|
||||
<InputGroup.Prepend className="bg-secondary text-white border-secondary">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleSelectClick(false)}
|
||||
>
|
||||
{renderButtonIcon(!props.result.useNewValue)}
|
||||
</Button>
|
||||
</InputGroup.Prepend>
|
||||
{props.renderOriginalField(props.result)}
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col xs="6">
|
||||
<InputGroup>
|
||||
<InputGroup.Prepend>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleSelectClick(true)}
|
||||
>
|
||||
{renderButtonIcon(props.result.useNewValue)}
|
||||
</Button>
|
||||
</InputGroup.Prepend>
|
||||
{props.renderNewField(props.result)}
|
||||
</InputGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
interface IScrapedInputGroupProps {
|
||||
isNew?: boolean;
|
||||
placeholder?: string;
|
||||
result: ScrapeResult<string>;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
const ScrapedInputGroup: React.FC<IScrapedInputGroupProps> = (props) => {
|
||||
return (
|
||||
<FormControl
|
||||
placeholder={props.placeholder}
|
||||
value={props.isNew ? props.result.newValue : props.result.originalValue}
|
||||
readOnly={!props.isNew}
|
||||
onChange={(e) => {
|
||||
if (props.isNew && props.onChange) {
|
||||
props.onChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
className="bg-secondary text-white border-secondary"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface IScrapedInputGroupRowProps {
|
||||
title: string;
|
||||
placeholder?: string;
|
||||
result: ScrapeResult<string>;
|
||||
onChange: (value: ScrapeResult<string>) => void;
|
||||
}
|
||||
|
||||
export const ScrapedInputGroupRow: React.FC<IScrapedInputGroupRowProps> = (
|
||||
props
|
||||
) => {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title={props.title}
|
||||
result={props.result}
|
||||
renderOriginalField={() => (
|
||||
<ScrapedInputGroup
|
||||
placeholder={props.placeholder || props.title}
|
||||
result={props.result}
|
||||
/>
|
||||
)}
|
||||
renderNewField={() => (
|
||||
<ScrapedInputGroup
|
||||
placeholder={props.placeholder || props.title}
|
||||
result={props.result}
|
||||
isNew
|
||||
onChange={(value) =>
|
||||
props.onChange(props.result.cloneWithValue(value))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ScrapedTextArea: React.FC<IScrapedInputGroupProps> = (props) => {
|
||||
return (
|
||||
<FormControl
|
||||
as="textarea"
|
||||
placeholder={props.placeholder}
|
||||
value={props.isNew ? props.result.newValue : props.result.originalValue}
|
||||
readOnly={!props.isNew}
|
||||
onChange={(e) => {
|
||||
if (props.isNew && props.onChange) {
|
||||
props.onChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
className="bg-secondary text-white border-secondary scene-description"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScrapedTextAreaRow: React.FC<IScrapedInputGroupRowProps> = (
|
||||
props
|
||||
) => {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title={props.title}
|
||||
result={props.result}
|
||||
renderOriginalField={() => (
|
||||
<ScrapedTextArea
|
||||
placeholder={props.placeholder || props.title}
|
||||
result={props.result}
|
||||
/>
|
||||
)}
|
||||
renderNewField={() => (
|
||||
<ScrapedTextArea
|
||||
placeholder={props.placeholder || props.title}
|
||||
result={props.result}
|
||||
isNew
|
||||
onChange={(value) =>
|
||||
props.onChange(props.result.cloneWithValue(value))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface IScrapedImageProps {
|
||||
isNew?: boolean;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
result: ScrapeResult<string>;
|
||||
}
|
||||
|
||||
const ScrapedImage: React.FC<IScrapedImageProps> = (props) => {
|
||||
const value = props.isNew
|
||||
? props.result.newValue
|
||||
: props.result.originalValue;
|
||||
|
||||
if (!value) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<img className={props.className} src={value} alt={props.placeholder} />
|
||||
);
|
||||
};
|
||||
|
||||
interface IScrapedImageRowProps {
|
||||
title: string;
|
||||
className?: string;
|
||||
result: ScrapeResult<string>;
|
||||
onChange: (value: ScrapeResult<string>) => void;
|
||||
}
|
||||
|
||||
export const ScrapedImageRow: React.FC<IScrapedImageRowProps> = (props) => {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title={props.title}
|
||||
result={props.result}
|
||||
renderOriginalField={() => (
|
||||
<ScrapedImage
|
||||
result={props.result}
|
||||
className={props.className}
|
||||
placeholder={props.title}
|
||||
/>
|
||||
)}
|
||||
renderNewField={() => (
|
||||
<ScrapedImage
|
||||
result={props.result}
|
||||
className={props.className}
|
||||
placeholder={props.title}
|
||||
isNew
|
||||
/>
|
||||
)}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface IScrapeDialogProps {
|
||||
title: string;
|
||||
renderScrapeRows: () => JSX.Element;
|
||||
onClose: (apply?: boolean) => void;
|
||||
}
|
||||
|
||||
export const ScrapeDialog: React.FC<IScrapeDialogProps> = (
|
||||
props: IScrapeDialogProps
|
||||
) => {
|
||||
return (
|
||||
<Modal
|
||||
show
|
||||
icon="pencil-alt"
|
||||
header={props.title}
|
||||
accept={{
|
||||
onClick: () => {
|
||||
props.onClose(true);
|
||||
},
|
||||
text: "Apply",
|
||||
}}
|
||||
cancel={{
|
||||
onClick: () => props.onClose(),
|
||||
text: "Cancel",
|
||||
variant: "secondary",
|
||||
}}
|
||||
modalProps={{ size: "lg", dialogClassName: "scrape-dialog" }}
|
||||
>
|
||||
<div className="dialog-container">
|
||||
<Form>
|
||||
<Row className="px-3 pt-3">
|
||||
<Col lg={{ span: 9, offset: 3 }}>
|
||||
<Row>
|
||||
<Form.Label column xs="6">
|
||||
Existing
|
||||
</Form.Label>
|
||||
<Form.Label column xs="6">
|
||||
Scraped
|
||||
</Form.Label>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{props.renderScrapeRows()}
|
||||
</Form>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -113,3 +113,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scrape-dialog .modal-content .dialog-container {
|
||||
max-height: calc(100vh - 14rem);
|
||||
overflow-y: auto;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
@@ -211,6 +211,17 @@ div.dropdown-menu {
|
||||
}
|
||||
}
|
||||
|
||||
/* fix for react-select in input-group */
|
||||
.input-group .react-select {
|
||||
border: 0;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
|
||||
.react-select__control {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* stylelint-enable selector-class-pattern */
|
||||
|
||||
.image-thumbnail {
|
||||
|
||||
@@ -51,7 +51,7 @@ const renderInputGroup = (options: {
|
||||
value: string | undefined;
|
||||
isEditing: boolean;
|
||||
url?: string;
|
||||
onChange: (value: string) => void;
|
||||
onChange?: (value: string) => void;
|
||||
}) => {
|
||||
if (options.url && !options.isEditing) {
|
||||
return (
|
||||
@@ -68,9 +68,11 @@ const renderInputGroup = (options: {
|
||||
plaintext={!options.isEditing}
|
||||
value={options.value ?? ""}
|
||||
placeholder={options.placeholder ?? options.title}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
options.onChange(event.currentTarget.value)
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (options.onChange) {
|
||||
options.onChange(event.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user