mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Manually Search Stash ID - Edit Page - Scenes, Studios (#6340)
This commit is contained in:
parent
3d044896ad
commit
877491e62b
8 changed files with 339 additions and 38 deletions
|
|
@ -350,7 +350,7 @@ func (r *queryResolver) ScrapeSingleStudio(ctx context.Context, source scraper.S
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("stash_box_index must be set")
|
||||
return nil, errors.New("stash_box_endpoint must be set")
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSinglePerformer(ctx context.Context, source scraper.Source, input ScrapeSinglePerformerInput) ([]*models.ScrapedPerformer, error) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ type StashBoxGraphQLClient interface {
|
|||
FindPerformerByID(ctx context.Context, id string, interceptors ...clientv2.RequestInterceptor) (*FindPerformerByID, error)
|
||||
FindSceneByID(ctx context.Context, id string, interceptors ...clientv2.RequestInterceptor) (*FindSceneByID, error)
|
||||
FindStudio(ctx context.Context, id *string, name *string, interceptors ...clientv2.RequestInterceptor) (*FindStudio, error)
|
||||
FindTag(ctx context.Context, id *string, name *string, interceptors ...clientv2.RequestInterceptor) (*FindTag, error)
|
||||
SubmitFingerprint(ctx context.Context, input FingerprintSubmission, interceptors ...clientv2.RequestInterceptor) (*SubmitFingerprint, error)
|
||||
Me(ctx context.Context, interceptors ...clientv2.RequestInterceptor) (*Me, error)
|
||||
SubmitSceneDraft(ctx context.Context, input SceneDraftInput, interceptors ...clientv2.RequestInterceptor) (*SubmitSceneDraft, error)
|
||||
|
|
@ -763,6 +764,17 @@ func (t *FindStudio) GetFindStudio() *StudioFragment {
|
|||
return t.FindStudio
|
||||
}
|
||||
|
||||
type FindTag struct {
|
||||
FindTag *TagFragment "json:\"findTag,omitempty\" graphql:\"findTag\""
|
||||
}
|
||||
|
||||
func (t *FindTag) GetFindTag() *TagFragment {
|
||||
if t == nil {
|
||||
t = &FindTag{}
|
||||
}
|
||||
return t.FindTag
|
||||
}
|
||||
|
||||
type SubmitFingerprint struct {
|
||||
SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\""
|
||||
}
|
||||
|
|
@ -1695,6 +1707,35 @@ func (c *Client) FindStudio(ctx context.Context, id *string, name *string, inter
|
|||
return &res, nil
|
||||
}
|
||||
|
||||
const FindTagDocument = `query FindTag ($id: ID, $name: String) {
|
||||
findTag(id: $id, name: $name) {
|
||||
... TagFragment
|
||||
}
|
||||
}
|
||||
fragment TagFragment on Tag {
|
||||
name
|
||||
id
|
||||
}
|
||||
`
|
||||
|
||||
func (c *Client) FindTag(ctx context.Context, id *string, name *string, interceptors ...clientv2.RequestInterceptor) (*FindTag, error) {
|
||||
vars := map[string]any{
|
||||
"id": id,
|
||||
"name": name,
|
||||
}
|
||||
|
||||
var res FindTag
|
||||
if err := c.Client.Post(ctx, "FindTag", FindTagDocument, &res, vars, interceptors...); err != nil {
|
||||
if c.Client.ParseDataWhenErrors {
|
||||
return &res, err
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
const SubmitFingerprintDocument = `mutation SubmitFingerprint ($input: FingerprintSubmission!) {
|
||||
submitFingerprint(input: $input)
|
||||
}
|
||||
|
|
@ -1796,6 +1837,7 @@ var DocumentOperationNames = map[string]string{
|
|||
FindPerformerByIDDocument: "FindPerformerByID",
|
||||
FindSceneByIDDocument: "FindSceneByID",
|
||||
FindStudioDocument: "FindStudio",
|
||||
FindTagDocument: "FindTag",
|
||||
SubmitFingerprintDocument: "SubmitFingerprint",
|
||||
MeDocument: "Me",
|
||||
SubmitSceneDraftDocument: "SubmitSceneDraft",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { ImageInput } from "src/components/Shared/ImageInput";
|
|||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { CountrySelect } from "src/components/Shared/CountrySelect";
|
||||
import ImageUtils from "src/utils/image";
|
||||
import { getStashIDs } from "src/utils/stashIds";
|
||||
import { addUpdateStashID, getStashIDs } from "src/utils/stashIds";
|
||||
import { stashboxDisplayName } from "src/utils/stashbox";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { Prompt } from "react-router-dom";
|
||||
|
|
@ -574,23 +574,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
|
||||
function onStashIDSelected(item?: GQL.StashIdInput) {
|
||||
if (!item) return;
|
||||
|
||||
// Check if StashID with this endpoint already exists
|
||||
const existingIndex = formik.values.stash_ids.findIndex(
|
||||
(s) => s.endpoint === item.endpoint
|
||||
formik.setFieldValue(
|
||||
"stash_ids",
|
||||
addUpdateStashID(formik.values.stash_ids, item)
|
||||
);
|
||||
|
||||
let newStashIDs;
|
||||
if (existingIndex >= 0) {
|
||||
// Replace existing StashID
|
||||
newStashIDs = [...formik.values.stash_ids];
|
||||
newStashIDs[existingIndex] = item;
|
||||
} else {
|
||||
// Add new StashID
|
||||
newStashIDs = [...formik.values.stash_ids, item];
|
||||
}
|
||||
|
||||
formik.setFieldValue("stash_ids", newStashIDs);
|
||||
}
|
||||
|
||||
function renderButtons(classNames: string) {
|
||||
|
|
@ -685,6 +672,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
{maybeRenderScrapeDialog()}
|
||||
{isStashIDSearchOpen && (
|
||||
<StashBoxIDSearchModal
|
||||
entityType="performer"
|
||||
stashBoxes={stashConfig?.general.stashBoxes ?? []}
|
||||
excludedStashBoxEndpoints={formik.values.stash_ids.map(
|
||||
(s) => s.endpoint
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
|||
import { ImageInput } from "src/components/Shared/ImageInput";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import ImageUtils from "src/utils/image";
|
||||
import { getStashIDs } from "src/utils/stashIds";
|
||||
import { addUpdateStashID, getStashIDs } from "src/utils/stashIds";
|
||||
import { useFormik } from "formik";
|
||||
import { Prompt } from "react-router-dom";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { IGroupEntry, SceneGroupTable } from "./SceneGroupTable";
|
||||
import { faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faSearch, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { objectTitle } from "src/core/files";
|
||||
import { galleryTitle } from "src/core/galleries";
|
||||
import { lazyComponent } from "src/utils/lazyComponent";
|
||||
|
|
@ -41,6 +41,7 @@ import { Gallery, GallerySelect } from "src/components/Galleries/GallerySelect";
|
|||
import { Group } from "src/components/Groups/GroupSelect";
|
||||
import { useTagsEdit } from "src/hooks/tagsEdit";
|
||||
import { ScraperMenu } from "src/components/Shared/ScraperMenu";
|
||||
import StashBoxIDSearchModal from "src/components/Shared/StashBoxIDSearchModal";
|
||||
|
||||
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
||||
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
||||
|
|
@ -77,6 +78,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
const [scraper, setScraper] = useState<GQL.ScraperSourceInput>();
|
||||
const [isScraperQueryModalOpen, setIsScraperQueryModalOpen] =
|
||||
useState<boolean>(false);
|
||||
const [isStashIDSearchOpen, setIsStashIDSearchOpen] =
|
||||
useState<boolean>(false);
|
||||
const [scrapedScene, setScrapedScene] = useState<GQL.ScrapedScene | null>();
|
||||
const [endpoint, setEndpoint] = useState<string>();
|
||||
|
||||
|
|
@ -547,6 +550,14 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
}
|
||||
}
|
||||
|
||||
function onStashIDSelected(item?: GQL.StashIdInput) {
|
||||
if (!item) return;
|
||||
formik.setFieldValue(
|
||||
"stash_ids",
|
||||
addUpdateStashID(formik.values.stash_ids, item)
|
||||
);
|
||||
}
|
||||
|
||||
const image = useMemo(() => {
|
||||
if (encodingImage) {
|
||||
return (
|
||||
|
|
@ -696,6 +707,19 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
|
||||
{renderScrapeQueryModal()}
|
||||
{maybeRenderScrapeDialog()}
|
||||
{isStashIDSearchOpen && (
|
||||
<StashBoxIDSearchModal
|
||||
entityType="scene"
|
||||
stashBoxes={stashConfig?.general.stashBoxes ?? []}
|
||||
excludedStashBoxEndpoints={formik.values.stash_ids.map(
|
||||
(s) => s.endpoint
|
||||
)}
|
||||
onSelectItem={(item) => {
|
||||
onStashIDSelected(item);
|
||||
setIsStashIDSearchOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Form noValidate onSubmit={formik.handleSubmit}>
|
||||
<Row className="form-container edit-buttons-container px-3 pt-3">
|
||||
<div className="edit-buttons mb-3 pl-0">
|
||||
|
|
@ -761,7 +785,16 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
"stash_ids",
|
||||
"scenes",
|
||||
"stash_ids",
|
||||
fullWidthProps
|
||||
fullWidthProps,
|
||||
<Button
|
||||
variant="success"
|
||||
className="mr-2 py-0"
|
||||
onClick={() => setIsStashIDSearchOpen(true)}
|
||||
disabled={!stashConfig?.general.stashBoxes?.length}
|
||||
title={intl.formatMessage({ id: "actions.add_stash_id" })}
|
||||
>
|
||||
<Icon icon={faPlus} />
|
||||
</Button>
|
||||
)}
|
||||
</Col>
|
||||
<Col lg={5} xl={12}>
|
||||
|
|
|
|||
|
|
@ -11,11 +11,23 @@ import TextUtils from "src/utils/text";
|
|||
import GenderIcon from "src/components/Performers/GenderIcon";
|
||||
import { CountryFlag } from "src/components/Shared/CountryFlag";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { stashBoxPerformerQuery } from "src/core/StashService";
|
||||
import {
|
||||
stashBoxPerformerQuery,
|
||||
stashBoxSceneQuery,
|
||||
stashBoxStudioQuery,
|
||||
} from "src/core/StashService";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { stringToGender } from "src/utils/gender";
|
||||
|
||||
type SearchResultItem =
|
||||
| GQL.ScrapedPerformerDataFragment
|
||||
| GQL.ScrapedSceneDataFragment
|
||||
| GQL.ScrapedStudioDataFragment;
|
||||
|
||||
export type StashBoxEntityType = "performer" | "scene" | "studio";
|
||||
|
||||
interface IProps {
|
||||
entityType: StashBoxEntityType;
|
||||
stashBoxes: GQL.StashBox[];
|
||||
excludedStashBoxEndpoints?: string[];
|
||||
onSelectItem: (item?: GQL.StashIdInput) => void;
|
||||
|
|
@ -132,8 +144,121 @@ export const PerformerSearchResult: React.FC<IPerformerResultProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
// Scene Result Component
|
||||
interface ISceneResultProps {
|
||||
scene: GQL.ScrapedSceneDataFragment;
|
||||
}
|
||||
|
||||
const SceneSearchResultDetails: React.FC<ISceneResultProps> = ({ scene }) => {
|
||||
return (
|
||||
<div className="scene-result">
|
||||
<Row>
|
||||
<SearchResultImage imageUrl={scene.image} />
|
||||
<div className="col flex-column">
|
||||
<h4 className="scene-title">
|
||||
<span>{scene.title}</span>
|
||||
{scene.code && (
|
||||
<span className="scene-code">{` (${scene.code})`}</span>
|
||||
)}
|
||||
</h4>
|
||||
<h5 className="scene-details">
|
||||
{scene.studio?.name && <span>{scene.studio.name}</span>}
|
||||
{scene.date && (
|
||||
<span className="scene-date">{` • ${scene.date}`}</span>
|
||||
)}
|
||||
</h5>
|
||||
{scene.performers && scene.performers.length > 0 && (
|
||||
<div className="scene-performers">
|
||||
{scene.performers.map((p) => p.name).join(", ")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<TruncatedText text={scene.details ?? ""} lineCount={3} />
|
||||
</Col>
|
||||
</Row>
|
||||
<SearchResultTags tags={scene.tags} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SceneSearchResult: React.FC<ISceneResultProps> = ({ scene }) => {
|
||||
return (
|
||||
<div className="mt-3 search-item" style={{ cursor: "pointer" }}>
|
||||
<SceneSearchResultDetails scene={scene} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Studio Result Component
|
||||
interface IStudioResultProps {
|
||||
studio: GQL.ScrapedStudioDataFragment;
|
||||
}
|
||||
|
||||
const StudioSearchResultDetails: React.FC<IStudioResultProps> = ({
|
||||
studio,
|
||||
}) => {
|
||||
return (
|
||||
<div className="studio-result">
|
||||
<Row>
|
||||
<SearchResultImage imageUrl={studio.image} />
|
||||
<div className="col flex-column">
|
||||
<h4 className="studio-name">
|
||||
<span>{studio.name}</span>
|
||||
</h4>
|
||||
{studio.parent?.name && (
|
||||
<h5 className="studio-parent">
|
||||
<span>{studio.parent.name}</span>
|
||||
</h5>
|
||||
)}
|
||||
{studio.urls && studio.urls.length > 0 && (
|
||||
<div className="studio-url text-muted small">{studio.urls[0]}</div>
|
||||
)}
|
||||
</div>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const StudioSearchResult: React.FC<IStudioResultProps> = ({
|
||||
studio,
|
||||
}) => {
|
||||
return (
|
||||
<div className="mt-3 search-item" style={{ cursor: "pointer" }}>
|
||||
<StudioSearchResultDetails studio={studio} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Helper to get entity type message id for i18n
|
||||
function getEntityTypeMessageId(entityType: StashBoxEntityType): string {
|
||||
switch (entityType) {
|
||||
case "performer":
|
||||
return "performer";
|
||||
case "scene":
|
||||
return "scene";
|
||||
case "studio":
|
||||
return "studio";
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get the "found" message id based on entity type
|
||||
function getFoundMessageId(entityType: StashBoxEntityType): string {
|
||||
switch (entityType) {
|
||||
case "performer":
|
||||
return "dialogs.performers_found";
|
||||
case "scene":
|
||||
return "dialogs.scenes_found";
|
||||
case "studio":
|
||||
return "dialogs.studios_found";
|
||||
}
|
||||
}
|
||||
|
||||
// Main Modal Component
|
||||
export const StashBoxIDSearchModal: React.FC<IProps> = ({
|
||||
entityType,
|
||||
stashBoxes,
|
||||
excludedStashBoxEndpoints = [],
|
||||
onSelectItem,
|
||||
|
|
@ -146,9 +271,9 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
|
|||
null
|
||||
);
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [results, setResults] = useState<
|
||||
GQL.ScrapedPerformerDataFragment[] | undefined
|
||||
>(undefined);
|
||||
const [results, setResults] = useState<SearchResultItem[] | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -168,17 +293,38 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
|
|||
setResults([]);
|
||||
|
||||
try {
|
||||
const queryData = await stashBoxPerformerQuery(
|
||||
query,
|
||||
selectedStashBox.endpoint
|
||||
);
|
||||
setResults(queryData.data?.scrapeSinglePerformer ?? []);
|
||||
switch (entityType) {
|
||||
case "performer": {
|
||||
const queryData = await stashBoxPerformerQuery(
|
||||
query,
|
||||
selectedStashBox.endpoint
|
||||
);
|
||||
setResults(queryData.data?.scrapeSinglePerformer ?? []);
|
||||
break;
|
||||
}
|
||||
case "scene": {
|
||||
const queryData = await stashBoxSceneQuery(
|
||||
query,
|
||||
selectedStashBox.endpoint
|
||||
);
|
||||
setResults(queryData.data?.scrapeSingleScene ?? []);
|
||||
break;
|
||||
}
|
||||
case "studio": {
|
||||
const queryData = await stashBoxStudioQuery(
|
||||
query,
|
||||
selectedStashBox.endpoint
|
||||
);
|
||||
setResults(queryData.data?.scrapeSingleStudio ?? []);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Toast.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [query, selectedStashBox, Toast]);
|
||||
}, [query, selectedStashBox, Toast, entityType]);
|
||||
|
||||
function handleItemClick(item: IHasRemoteSiteID) {
|
||||
if (selectedStashBox && item.remote_site_id) {
|
||||
|
|
@ -195,6 +341,25 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
|
|||
onSelectItem(undefined);
|
||||
}
|
||||
|
||||
function renderResultItem(item: SearchResultItem) {
|
||||
switch (entityType) {
|
||||
case "performer":
|
||||
return (
|
||||
<PerformerSearchResult
|
||||
performer={item as GQL.ScrapedPerformerDataFragment}
|
||||
/>
|
||||
);
|
||||
case "scene":
|
||||
return (
|
||||
<SceneSearchResult scene={item as GQL.ScrapedSceneDataFragment} />
|
||||
);
|
||||
case "studio":
|
||||
return (
|
||||
<StudioSearchResult studio={item as GQL.ScrapedStudioDataFragment} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderResults() {
|
||||
if (!results || results.length === 0) {
|
||||
return null;
|
||||
|
|
@ -204,14 +369,14 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
|
|||
<div className={CLASSNAME_LIST_CONTAINER}>
|
||||
<div className="mt-1 mb-2">
|
||||
<FormattedMessage
|
||||
id="dialogs.performers_found"
|
||||
id={getFoundMessageId(entityType)}
|
||||
values={{ count: results.length }}
|
||||
/>
|
||||
</div>
|
||||
<ul className={CLASSNAME_LIST} style={{ listStyleType: "none" }}>
|
||||
{results.map((item, i) => (
|
||||
<li key={i} onClick={() => handleItemClick(item)}>
|
||||
<PerformerSearchResult performer={item} />
|
||||
{renderResultItem(item)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
@ -219,13 +384,17 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
const entityTypeDisplayName = intl.formatMessage({
|
||||
id: getEntityTypeMessageId(entityType),
|
||||
});
|
||||
|
||||
return (
|
||||
<ModalComponent
|
||||
show
|
||||
onHide={handleClose}
|
||||
header={intl.formatMessage(
|
||||
{ id: "stashbox_search.header" },
|
||||
{ entityType: "Performer" }
|
||||
{ entityType: entityTypeDisplayName }
|
||||
)}
|
||||
accept={{
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
|
|
@ -273,7 +442,7 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
|
|||
value={query}
|
||||
placeholder={intl.formatMessage(
|
||||
{ id: "stashbox_search.placeholder_name_or_id" },
|
||||
{ entityType: "Performer" }
|
||||
{ entityType: entityTypeDisplayName }
|
||||
)}
|
||||
className="text-input"
|
||||
ref={inputRef}
|
||||
|
|
|
|||
|
|
@ -5,18 +5,22 @@ import * as yup from "yup";
|
|||
import Mousetrap from "mousetrap";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import ImageUtils from "src/utils/image";
|
||||
import { getStashIDs } from "src/utils/stashIds";
|
||||
import { addUpdateStashID, getStashIDs } from "src/utils/stashIds";
|
||||
import { useFormik } from "formik";
|
||||
import { Prompt } from "react-router-dom";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { handleUnsavedChanges } from "src/utils/navigation";
|
||||
import { formikUtils } from "src/utils/form";
|
||||
import { yupFormikValidate, yupUniqueAliases } from "src/utils/yup";
|
||||
import { Studio, StudioSelect } from "../StudioSelect";
|
||||
import { useTagsEdit } from "src/hooks/tagsEdit";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import StashBoxIDSearchModal from "src/components/Shared/StashBoxIDSearchModal";
|
||||
|
||||
interface IStudioEditPanel {
|
||||
studio: Partial<GQL.StudioDataFragment>;
|
||||
|
|
@ -37,9 +41,13 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const { configuration: stashConfig } = useConfigurationContext();
|
||||
|
||||
const isNew = studio.id === undefined;
|
||||
|
||||
// Editing state
|
||||
const [isStashIDSearchOpen, setIsStashIDSearchOpen] = useState(false);
|
||||
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
|
|
@ -143,6 +151,14 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
ImageUtils.onImageChange(event, onImageLoad);
|
||||
}
|
||||
|
||||
function onStashIDSelected(item?: GQL.StashIdInput) {
|
||||
if (!item) return;
|
||||
formik.setFieldValue(
|
||||
"stash_ids",
|
||||
addUpdateStashID(formik.values.stash_ids, item)
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
renderField,
|
||||
renderInputField,
|
||||
|
|
@ -173,6 +189,20 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
{isStashIDSearchOpen && (
|
||||
<StashBoxIDSearchModal
|
||||
entityType="studio"
|
||||
stashBoxes={stashConfig?.general.stashBoxes ?? []}
|
||||
excludedStashBoxEndpoints={formik.values.stash_ids.map(
|
||||
(s) => s.endpoint
|
||||
)}
|
||||
onSelectItem={(item) => {
|
||||
onStashIDSelected(item);
|
||||
setIsStashIDSearchOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Prompt
|
||||
when={formik.dirty}
|
||||
message={(location, action) => {
|
||||
|
|
@ -191,7 +221,21 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||
{renderInputField("details", "textarea")}
|
||||
{renderParentStudioField()}
|
||||
{renderTagsField()}
|
||||
{renderStashIDsField("stash_ids", "studios")}
|
||||
{renderStashIDsField(
|
||||
"stash_ids",
|
||||
"studios",
|
||||
"stash_ids",
|
||||
undefined,
|
||||
<Button
|
||||
variant="success"
|
||||
className="mr-2 py-0"
|
||||
onClick={() => setIsStashIDSearchOpen(true)}
|
||||
disabled={!stashConfig?.general.stashBoxes?.length}
|
||||
title={intl.formatMessage({ id: "actions.add_stash_id" })}
|
||||
>
|
||||
<Icon icon={faPlus} />
|
||||
</Button>
|
||||
)}
|
||||
<hr />
|
||||
{renderInputField("ignore_auto_tag", "checkbox")}
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -1015,6 +1015,7 @@
|
|||
"video_previews_tooltip": "Video previews which play when hovering over a scene"
|
||||
},
|
||||
"scenes_found": "{count} scenes found",
|
||||
"studios_found": "{count} studios found",
|
||||
"scrape_entity_query": "{entity_type} Scrape Query",
|
||||
"scrape_entity_title": "{entity_type} Scrape Results",
|
||||
"scrape_results_existing": "Existing",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import * as GQL from "src/core/generated-graphql";
|
||||
|
||||
export const getStashIDs = (
|
||||
ids?: { stash_id: string; endpoint: string; updated_at: string }[]
|
||||
) =>
|
||||
|
|
@ -32,3 +34,25 @@ export const separateNamesAndStashIds = (
|
|||
|
||||
return { names, stashIds };
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility to add or update a StashID in an array.
|
||||
* If a StashID with the same endpoint exists, it will be replaced.
|
||||
* Otherwise, the new StashID will be appended.
|
||||
*/
|
||||
export const addUpdateStashID = (
|
||||
existingStashIDs: GQL.StashIdInput[],
|
||||
newItem: GQL.StashIdInput
|
||||
): GQL.StashIdInput[] => {
|
||||
const existingIndex = existingStashIDs.findIndex(
|
||||
(s) => s.endpoint === newItem.endpoint
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
const newStashIDs = [...existingStashIDs];
|
||||
newStashIDs[existingIndex] = newItem;
|
||||
return newStashIDs;
|
||||
}
|
||||
|
||||
return [...existingStashIDs, newItem];
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue