Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generalise tagger view to all scraping sources #1812

Merged
merged 14 commits into from
Oct 13, 2021
Merged
3 changes: 2 additions & 1 deletion ui/v2.5/src/components/Scenes/SceneList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { DeleteScenesDialog } from "./DeleteScenesDialog";
import { SceneGenerateDialog } from "./SceneGenerateDialog";
import { ExportDialog } from "../Shared/ExportDialog";
import { SceneCardsGrid } from "./SceneCardsGrid";
import { TaggerContext } from "../Tagger/context";

interface ISceneList {
filterHook?: (filter: ListFilterModel) => ListFilterModel;
Expand Down Expand Up @@ -253,5 +254,5 @@ export const SceneList: React.FC<ISceneList> = ({
);
}

return listData.template;
return <TaggerContext>{listData.template}</TaggerContext>;
};
4 changes: 3 additions & 1 deletion ui/v2.5/src/components/Shared/LoadingIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ interface ILoadingProps {
message?: string;
inline?: boolean;
small?: boolean;
card?: boolean;
}

const CLASSNAME = "LoadingIndicator";
Expand All @@ -15,8 +16,9 @@ const LoadingIndicator: React.FC<ILoadingProps> = ({
message,
inline = false,
small = false,
card = false,
}) => (
<div className={cx(CLASSNAME, { inline, small })}>
<div className={cx(CLASSNAME, { inline, small, "card-based": card })}>
<Spinner animation="border" role="status" size={small ? "sm" : undefined}>
<span className="sr-only">Loading...</span>
</Spinner>
Expand Down
56 changes: 56 additions & 0 deletions ui/v2.5/src/components/Shared/OperationButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { useState, useRef, useEffect } from "react";
import { Button, ButtonProps } from "react-bootstrap";
import { LoadingIndicator } from "src/components/Shared";

interface IOperationButton extends ButtonProps {
operation?: () => Promise<void>;
loading?: boolean;
hideChildrenWhenLoading?: boolean;
setLoading?: (v: boolean) => void;
}

export const OperationButton: React.FC<IOperationButton> = (props) => {
const [internalLoading, setInternalLoading] = useState(false);
const mounted = useRef(false);

const {
operation,
loading: externalLoading,
hideChildrenWhenLoading = false,
setLoading: setExternalLoading,
...withoutExtras
} = props;

useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);

const setLoading = setExternalLoading || setInternalLoading;
const loading =
externalLoading !== undefined ? externalLoading : internalLoading;

async function handleClick() {
if (operation) {
setLoading(true);
await operation();

if (mounted.current) {
setLoading(false);
}
}
}

return (
<Button onClick={handleClick} {...withoutExtras}>
{loading && (
<span className="mr-2">
<LoadingIndicator message="" inline small />
</span>
)}
{(!loading || !hideChildrenWhenLoading) && props.children}
</Button>
);
};
5 changes: 4 additions & 1 deletion ui/v2.5/src/components/Shared/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
align-items: center;
display: flex;
flex-direction: column;
height: 70vh;
justify-content: center;
width: 100%;

&:not(.card-based) {
height: 70vh;
}

&-message {
margin-top: 1rem;
}
Expand Down
46 changes: 5 additions & 41 deletions ui/v2.5/src/components/Tagger/Config.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Dispatch, useRef } from "react";
import React, { useRef, useContext } from "react";
import {
Badge,
Button,
Expand All @@ -9,29 +9,18 @@ import {
} from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl";
import { Icon } from "src/components/Shared";
import { useConfiguration } from "src/core/StashService";

import { ITaggerConfig, ParseMode, TagOperation } from "./constants";
import { ParseMode, TagOperation } from "./constants";
import { TaggerStateContext } from "./context";

interface IConfigProps {
show: boolean;
config: ITaggerConfig;
setConfig: Dispatch<ITaggerConfig>;
}

const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
const Config: React.FC<IConfigProps> = ({ show }) => {
const { config, setConfig } = useContext(TaggerStateContext);
const intl = useIntl();
const stashConfig = useConfiguration();
const blacklistRef = useRef<HTMLInputElement | null>(null);

const handleInstanceSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedEndpoint = e.currentTarget.value;
setConfig({
...config,
selectedEndpoint,
});
};

const removeBlacklist = (index: number) => {
setConfig({
...config,
Expand All @@ -55,8 +44,6 @@ const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
blacklistRef.current.value = "";
};

const stashBoxes = stashConfig.data?.configuration.general.stashBoxes ?? [];

return (
<Collapse in={show}>
<Card>
Expand Down Expand Up @@ -221,29 +208,6 @@ const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
</Button>
</Badge>
))}

<Form.Group
controlId="stash-box-endpoint"
className="align-items-center row no-gutters mt-4"
>
<Form.Label className="mr-4">
<FormattedMessage id="component_tagger.config.active_instance" />
</Form.Label>
<Form.Control
as="select"
value={config.selectedEndpoint}
className="col-md-4 col-6 input-control"
disabled={!stashBoxes.length}
onChange={handleInstanceSelect}
>
{!stashBoxes.length && <option>No instances found</option>}
{stashConfig.data?.configuration.general.stashBoxes.map((i) => (
<option value={i.endpoint} key={i.endpoint}>
{i.endpoint}
</option>
))}
</Form.Control>
</Form.Group>
</div>
</div>
</Card>
Expand Down
17 changes: 11 additions & 6 deletions ui/v2.5/src/components/Tagger/IncludeButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const IncludeExcludeButton: React.FC<IIncludeExcludeButton> = ({

interface IOptionalField {
exclude: boolean;
title?: string;
disabled?: boolean;
setExclude: (v: boolean) => void;
}
Expand All @@ -35,9 +36,13 @@ export const OptionalField: React.FC<IOptionalField> = ({
exclude,
setExclude,
children,
}) => (
<div className={`optional-field ${!exclude ? "included" : "excluded"}`}>
<IncludeExcludeButton exclude={exclude} setExclude={setExclude} />
{children}
</div>
);
title,
}) => {
return (
<div className={`optional-field ${!exclude ? "included" : "excluded"}`}>
<IncludeExcludeButton exclude={exclude} setExclude={setExclude} />
{title && <span className="optional-field-title">{title}</span>}
<div className="optional-field-content">{children}</div>
</div>
);
};
103 changes: 85 additions & 18 deletions ui/v2.5/src/components/Tagger/PerformerModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { Button } from "react-bootstrap";
import { useIntl } from "react-intl";
import { FormattedMessage, useIntl } from "react-intl";
import cx from "classnames";
import { IconName } from "@fortawesome/fontawesome-svg-core";

Expand All @@ -11,26 +11,24 @@ import {
TruncatedText,
} from "src/components/Shared";
import * as GQL from "src/core/generated-graphql";
import { TextUtils } from "src/utils";
import { genderToString } from "src/utils/gender";
import { IStashBoxPerformer } from "./utils";
import { genderToString, stringToGender } from "src/utils/gender";

interface IPerformerModalProps {
performer: IStashBoxPerformer;
performer: GQL.ScrapedScenePerformerDataFragment;
modalVisible: boolean;
closeModal: () => void;
handlePerformerCreate: (imageIndex: number, excludedFields: string[]) => void;
onSave: (input: GQL.PerformerCreateInput) => void;
excludedPerformerFields?: string[];
header: string;
icon: IconName;
create?: boolean;
endpoint: string;
endpoint?: string;
}

const PerformerModal: React.FC<IPerformerModalProps> = ({
modalVisible,
performer,
handlePerformerCreate,
onSave,
closeModal,
excludedPerformerFields = [],
header,
Expand All @@ -39,6 +37,7 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
endpoint,
}) => {
const intl = useIntl();

const [imageIndex, setImageIndex] = useState(0);
const [imageState, setImageState] = useState<
"loading" | "error" | "loaded" | "empty"
Expand All @@ -51,7 +50,7 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
)
);

const { images } = performer;
const images = performer.images ?? [];

const changeImage = (index: number) => {
setImageIndex(index);
Expand Down Expand Up @@ -94,7 +93,9 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
<Icon icon={excluded[name] ? "times" : "check"} />
</Button>
)}
<strong>{TextUtils.capitalize(name)}:</strong>
<strong>
<FormattedMessage id={name} />:
</strong>
</div>
{truncate ? (
<TruncatedText className="col-7" text={text} />
Expand All @@ -104,19 +105,77 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
</div>
);

const base = endpoint.match(/https?:\/\/.*?\//)?.[0];
const link = base ? `${base}performers/${performer.stash_id}` : undefined;
const base = endpoint?.match(/https?:\/\/.*?\//)?.[0];
const link = base
? `${base}performers/${performer.remote_site_id}`
: undefined;

function onSaveClicked() {
if (!performer.name) {
throw new Error("performer name must set");
}

const performerData: GQL.PerformerCreateInput = {
name: performer.name ?? "",
aliases: performer.aliases,
gender: stringToGender(performer.gender ?? undefined),
birthdate: performer.birthdate,
ethnicity: performer.ethnicity,
eye_color: performer.eye_color,
country: performer.country,
height: performer.height,
measurements: performer.measurements,
fake_tits: performer.fake_tits,
career_length: performer.career_length,
tattoos: performer.tattoos,
piercings: performer.piercings,
url: performer.url,
twitter: performer.twitter,
instagram: performer.instagram,
image: images.length > imageIndex ? images[imageIndex] : undefined,
details: performer.details,
death_date: performer.death_date,
hair_color: performer.hair_color,
weight: Number.parseFloat(performer.weight ?? "") ?? undefined,
};

if (Number.isNaN(performerData.weight ?? 0)) {
performerData.weight = undefined;
}

if (performer.tags) {
performerData.tag_ids = performer.tags
.map((t) => t.stored_id)
.filter((t) => t) as string[];
}

// stashid handling code
const remoteSiteID = performer.remote_site_id;
if (remoteSiteID && endpoint) {
performerData.stash_ids = [
{
endpoint,
stash_id: remoteSiteID,
},
];
}

// handle exclusions
Object.keys(performerData).forEach((k) => {
if (excludedPerformerFields.includes(k) || excluded[k]) {
(performerData as Record<string, unknown>)[k] = undefined;
}
});

onSave(performerData);
}

return (
<Modal
show={modalVisible}
accept={{
text: intl.formatMessage({ id: "actions.save" }),
onClick: () =>
handlePerformerCreate(
imageIndex,
create ? [] : Object.keys(excluded).filter((key) => excluded[key])
),
onClick: onSaveClicked,
}}
cancel={{ onClick: () => closeModal(), variant: "secondary" }}
onHide={() => closeModal()}
Expand All @@ -127,7 +186,10 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
<div className="row">
<div className="col-7">
{renderField("name", performer.name)}
{renderField("gender", genderToString(performer.gender))}
{renderField(
"gender",
performer.gender ? genderToString(performer.gender) : ""
)}
{renderField("birthdate", performer.birthdate)}
{renderField("death_date", performer.death_date)}
{renderField("ethnicity", performer.ethnicity)}
Expand All @@ -142,6 +204,11 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
{renderField("career_length", performer.career_length)}
{renderField("tattoos", performer.tattoos, false)}
{renderField("piercings", performer.piercings, false)}
{renderField("weight", performer.weight, false)}
{renderField("details", performer.details)}
{renderField("url", performer.url)}
{renderField("twitter", performer.twitter)}
{renderField("instagram", performer.instagram)}
{link && (
<h6 className="mt-2">
<a href={link} target="_blank" rel="noopener noreferrer">
Expand Down
Loading