mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
Manually Search Stash ID - Edit Page - Scenes, Studios (#6340)
This commit is contained in:
@@ -350,7 +350,7 @@ func (r *queryResolver) ScrapeSingleStudio(ctx context.Context, source scraper.S
|
|||||||
return nil, nil
|
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) {
|
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)
|
FindPerformerByID(ctx context.Context, id string, interceptors ...clientv2.RequestInterceptor) (*FindPerformerByID, error)
|
||||||
FindSceneByID(ctx context.Context, id string, interceptors ...clientv2.RequestInterceptor) (*FindSceneByID, 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)
|
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)
|
SubmitFingerprint(ctx context.Context, input FingerprintSubmission, interceptors ...clientv2.RequestInterceptor) (*SubmitFingerprint, error)
|
||||||
Me(ctx context.Context, interceptors ...clientv2.RequestInterceptor) (*Me, error)
|
Me(ctx context.Context, interceptors ...clientv2.RequestInterceptor) (*Me, error)
|
||||||
SubmitSceneDraft(ctx context.Context, input SceneDraftInput, interceptors ...clientv2.RequestInterceptor) (*SubmitSceneDraft, error)
|
SubmitSceneDraft(ctx context.Context, input SceneDraftInput, interceptors ...clientv2.RequestInterceptor) (*SubmitSceneDraft, error)
|
||||||
@@ -763,6 +764,17 @@ func (t *FindStudio) GetFindStudio() *StudioFragment {
|
|||||||
return t.FindStudio
|
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 {
|
type SubmitFingerprint struct {
|
||||||
SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\""
|
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
|
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!) {
|
const SubmitFingerprintDocument = `mutation SubmitFingerprint ($input: FingerprintSubmission!) {
|
||||||
submitFingerprint(input: $input)
|
submitFingerprint(input: $input)
|
||||||
}
|
}
|
||||||
@@ -1796,6 +1837,7 @@ var DocumentOperationNames = map[string]string{
|
|||||||
FindPerformerByIDDocument: "FindPerformerByID",
|
FindPerformerByIDDocument: "FindPerformerByID",
|
||||||
FindSceneByIDDocument: "FindSceneByID",
|
FindSceneByIDDocument: "FindSceneByID",
|
||||||
FindStudioDocument: "FindStudio",
|
FindStudioDocument: "FindStudio",
|
||||||
|
FindTagDocument: "FindTag",
|
||||||
SubmitFingerprintDocument: "SubmitFingerprint",
|
SubmitFingerprintDocument: "SubmitFingerprint",
|
||||||
MeDocument: "Me",
|
MeDocument: "Me",
|
||||||
SubmitSceneDraftDocument: "SubmitSceneDraft",
|
SubmitSceneDraftDocument: "SubmitSceneDraft",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { ImageInput } from "src/components/Shared/ImageInput";
|
|||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { CountrySelect } from "src/components/Shared/CountrySelect";
|
import { CountrySelect } from "src/components/Shared/CountrySelect";
|
||||||
import ImageUtils from "src/utils/image";
|
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 { stashboxDisplayName } from "src/utils/stashbox";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
@@ -574,23 +574,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
|
|
||||||
function onStashIDSelected(item?: GQL.StashIdInput) {
|
function onStashIDSelected(item?: GQL.StashIdInput) {
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
formik.setFieldValue(
|
||||||
// Check if StashID with this endpoint already exists
|
"stash_ids",
|
||||||
const existingIndex = formik.values.stash_ids.findIndex(
|
addUpdateStashID(formik.values.stash_ids, item)
|
||||||
(s) => s.endpoint === item.endpoint
|
|
||||||
);
|
);
|
||||||
|
|
||||||
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) {
|
function renderButtons(classNames: string) {
|
||||||
@@ -685,6 +672,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
{maybeRenderScrapeDialog()}
|
{maybeRenderScrapeDialog()}
|
||||||
{isStashIDSearchOpen && (
|
{isStashIDSearchOpen && (
|
||||||
<StashBoxIDSearchModal
|
<StashBoxIDSearchModal
|
||||||
|
entityType="performer"
|
||||||
stashBoxes={stashConfig?.general.stashBoxes ?? []}
|
stashBoxes={stashConfig?.general.stashBoxes ?? []}
|
||||||
excludedStashBoxEndpoints={formik.values.stash_ids.map(
|
excludedStashBoxEndpoints={formik.values.stash_ids.map(
|
||||||
(s) => s.endpoint
|
(s) => s.endpoint
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
|||||||
import { ImageInput } from "src/components/Shared/ImageInput";
|
import { ImageInput } from "src/components/Shared/ImageInput";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import ImageUtils from "src/utils/image";
|
import ImageUtils from "src/utils/image";
|
||||||
import { getStashIDs } from "src/utils/stashIds";
|
import { addUpdateStashID, getStashIDs } from "src/utils/stashIds";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
import { useConfigurationContext } from "src/hooks/Config";
|
import { useConfigurationContext } from "src/hooks/Config";
|
||||||
import { IGroupEntry, SceneGroupTable } from "./SceneGroupTable";
|
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 { objectTitle } from "src/core/files";
|
||||||
import { galleryTitle } from "src/core/galleries";
|
import { galleryTitle } from "src/core/galleries";
|
||||||
import { lazyComponent } from "src/utils/lazyComponent";
|
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 { Group } from "src/components/Groups/GroupSelect";
|
||||||
import { useTagsEdit } from "src/hooks/tagsEdit";
|
import { useTagsEdit } from "src/hooks/tagsEdit";
|
||||||
import { ScraperMenu } from "src/components/Shared/ScraperMenu";
|
import { ScraperMenu } from "src/components/Shared/ScraperMenu";
|
||||||
|
import StashBoxIDSearchModal from "src/components/Shared/StashBoxIDSearchModal";
|
||||||
|
|
||||||
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
||||||
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
||||||
@@ -77,6 +78,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
const [scraper, setScraper] = useState<GQL.ScraperSourceInput>();
|
const [scraper, setScraper] = useState<GQL.ScraperSourceInput>();
|
||||||
const [isScraperQueryModalOpen, setIsScraperQueryModalOpen] =
|
const [isScraperQueryModalOpen, setIsScraperQueryModalOpen] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
|
const [isStashIDSearchOpen, setIsStashIDSearchOpen] =
|
||||||
|
useState<boolean>(false);
|
||||||
const [scrapedScene, setScrapedScene] = useState<GQL.ScrapedScene | null>();
|
const [scrapedScene, setScrapedScene] = useState<GQL.ScrapedScene | null>();
|
||||||
const [endpoint, setEndpoint] = useState<string>();
|
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(() => {
|
const image = useMemo(() => {
|
||||||
if (encodingImage) {
|
if (encodingImage) {
|
||||||
return (
|
return (
|
||||||
@@ -696,6 +707,19 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
{renderScrapeQueryModal()}
|
{renderScrapeQueryModal()}
|
||||||
{maybeRenderScrapeDialog()}
|
{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}>
|
<Form noValidate onSubmit={formik.handleSubmit}>
|
||||||
<Row className="form-container edit-buttons-container px-3 pt-3">
|
<Row className="form-container edit-buttons-container px-3 pt-3">
|
||||||
<div className="edit-buttons mb-3 pl-0">
|
<div className="edit-buttons mb-3 pl-0">
|
||||||
@@ -761,7 +785,16 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
"stash_ids",
|
"stash_ids",
|
||||||
"scenes",
|
"scenes",
|
||||||
"stash_ids",
|
"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>
|
||||||
<Col lg={5} xl={12}>
|
<Col lg={5} xl={12}>
|
||||||
|
|||||||
@@ -11,11 +11,23 @@ import TextUtils from "src/utils/text";
|
|||||||
import GenderIcon from "src/components/Performers/GenderIcon";
|
import GenderIcon from "src/components/Performers/GenderIcon";
|
||||||
import { CountryFlag } from "src/components/Shared/CountryFlag";
|
import { CountryFlag } from "src/components/Shared/CountryFlag";
|
||||||
import { Icon } from "src/components/Shared/Icon";
|
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 { useToast } from "src/hooks/Toast";
|
||||||
import { stringToGender } from "src/utils/gender";
|
import { stringToGender } from "src/utils/gender";
|
||||||
|
|
||||||
|
type SearchResultItem =
|
||||||
|
| GQL.ScrapedPerformerDataFragment
|
||||||
|
| GQL.ScrapedSceneDataFragment
|
||||||
|
| GQL.ScrapedStudioDataFragment;
|
||||||
|
|
||||||
|
export type StashBoxEntityType = "performer" | "scene" | "studio";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
entityType: StashBoxEntityType;
|
||||||
stashBoxes: GQL.StashBox[];
|
stashBoxes: GQL.StashBox[];
|
||||||
excludedStashBoxEndpoints?: string[];
|
excludedStashBoxEndpoints?: string[];
|
||||||
onSelectItem: (item?: GQL.StashIdInput) => void;
|
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
|
// Main Modal Component
|
||||||
export const StashBoxIDSearchModal: React.FC<IProps> = ({
|
export const StashBoxIDSearchModal: React.FC<IProps> = ({
|
||||||
|
entityType,
|
||||||
stashBoxes,
|
stashBoxes,
|
||||||
excludedStashBoxEndpoints = [],
|
excludedStashBoxEndpoints = [],
|
||||||
onSelectItem,
|
onSelectItem,
|
||||||
@@ -146,9 +271,9 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [query, setQuery] = useState<string>("");
|
const [query, setQuery] = useState<string>("");
|
||||||
const [results, setResults] = useState<
|
const [results, setResults] = useState<SearchResultItem[] | undefined>(
|
||||||
GQL.ScrapedPerformerDataFragment[] | undefined
|
undefined
|
||||||
>(undefined);
|
);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -168,17 +293,38 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
|
|||||||
setResults([]);
|
setResults([]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const queryData = await stashBoxPerformerQuery(
|
switch (entityType) {
|
||||||
query,
|
case "performer": {
|
||||||
selectedStashBox.endpoint
|
const queryData = await stashBoxPerformerQuery(
|
||||||
);
|
query,
|
||||||
setResults(queryData.data?.scrapeSinglePerformer ?? []);
|
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) {
|
} catch (error) {
|
||||||
Toast.error(error);
|
Toast.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [query, selectedStashBox, Toast]);
|
}, [query, selectedStashBox, Toast, entityType]);
|
||||||
|
|
||||||
function handleItemClick(item: IHasRemoteSiteID) {
|
function handleItemClick(item: IHasRemoteSiteID) {
|
||||||
if (selectedStashBox && item.remote_site_id) {
|
if (selectedStashBox && item.remote_site_id) {
|
||||||
@@ -195,6 +341,25 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
|
|||||||
onSelectItem(undefined);
|
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() {
|
function renderResults() {
|
||||||
if (!results || results.length === 0) {
|
if (!results || results.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -204,14 +369,14 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
|
|||||||
<div className={CLASSNAME_LIST_CONTAINER}>
|
<div className={CLASSNAME_LIST_CONTAINER}>
|
||||||
<div className="mt-1 mb-2">
|
<div className="mt-1 mb-2">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="dialogs.performers_found"
|
id={getFoundMessageId(entityType)}
|
||||||
values={{ count: results.length }}
|
values={{ count: results.length }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ul className={CLASSNAME_LIST} style={{ listStyleType: "none" }}>
|
<ul className={CLASSNAME_LIST} style={{ listStyleType: "none" }}>
|
||||||
{results.map((item, i) => (
|
{results.map((item, i) => (
|
||||||
<li key={i} onClick={() => handleItemClick(item)}>
|
<li key={i} onClick={() => handleItemClick(item)}>
|
||||||
<PerformerSearchResult performer={item} />
|
{renderResultItem(item)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -219,13 +384,17 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const entityTypeDisplayName = intl.formatMessage({
|
||||||
|
id: getEntityTypeMessageId(entityType),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalComponent
|
<ModalComponent
|
||||||
show
|
show
|
||||||
onHide={handleClose}
|
onHide={handleClose}
|
||||||
header={intl.formatMessage(
|
header={intl.formatMessage(
|
||||||
{ id: "stashbox_search.header" },
|
{ id: "stashbox_search.header" },
|
||||||
{ entityType: "Performer" }
|
{ entityType: entityTypeDisplayName }
|
||||||
)}
|
)}
|
||||||
accept={{
|
accept={{
|
||||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
@@ -273,7 +442,7 @@ export const StashBoxIDSearchModal: React.FC<IProps> = ({
|
|||||||
value={query}
|
value={query}
|
||||||
placeholder={intl.formatMessage(
|
placeholder={intl.formatMessage(
|
||||||
{ id: "stashbox_search.placeholder_name_or_id" },
|
{ id: "stashbox_search.placeholder_name_or_id" },
|
||||||
{ entityType: "Performer" }
|
{ entityType: entityTypeDisplayName }
|
||||||
)}
|
)}
|
||||||
className="text-input"
|
className="text-input"
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
|||||||
@@ -5,18 +5,22 @@ import * as yup from "yup";
|
|||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
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 ImageUtils from "src/utils/image";
|
||||||
import { getStashIDs } from "src/utils/stashIds";
|
import { addUpdateStashID, getStashIDs } from "src/utils/stashIds";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
|
import { useConfigurationContext } from "src/hooks/Config";
|
||||||
import { handleUnsavedChanges } from "src/utils/navigation";
|
import { handleUnsavedChanges } from "src/utils/navigation";
|
||||||
import { formikUtils } from "src/utils/form";
|
import { formikUtils } from "src/utils/form";
|
||||||
import { yupFormikValidate, yupUniqueAliases } from "src/utils/yup";
|
import { yupFormikValidate, yupUniqueAliases } from "src/utils/yup";
|
||||||
import { Studio, StudioSelect } from "../StudioSelect";
|
import { Studio, StudioSelect } from "../StudioSelect";
|
||||||
import { useTagsEdit } from "src/hooks/tagsEdit";
|
import { useTagsEdit } from "src/hooks/tagsEdit";
|
||||||
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
|
import StashBoxIDSearchModal from "src/components/Shared/StashBoxIDSearchModal";
|
||||||
|
|
||||||
interface IStudioEditPanel {
|
interface IStudioEditPanel {
|
||||||
studio: Partial<GQL.StudioDataFragment>;
|
studio: Partial<GQL.StudioDataFragment>;
|
||||||
@@ -37,9 +41,13 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
const { configuration: stashConfig } = useConfigurationContext();
|
||||||
|
|
||||||
const isNew = studio.id === undefined;
|
const isNew = studio.id === undefined;
|
||||||
|
|
||||||
|
// Editing state
|
||||||
|
const [isStashIDSearchOpen, setIsStashIDSearchOpen] = useState(false);
|
||||||
|
|
||||||
// Network state
|
// Network state
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
@@ -143,6 +151,14 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
ImageUtils.onImageChange(event, onImageLoad);
|
ImageUtils.onImageChange(event, onImageLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onStashIDSelected(item?: GQL.StashIdInput) {
|
||||||
|
if (!item) return;
|
||||||
|
formik.setFieldValue(
|
||||||
|
"stash_ids",
|
||||||
|
addUpdateStashID(formik.values.stash_ids, item)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
renderField,
|
renderField,
|
||||||
renderInputField,
|
renderInputField,
|
||||||
@@ -173,6 +189,20 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
|
|
||||||
return (
|
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
|
<Prompt
|
||||||
when={formik.dirty}
|
when={formik.dirty}
|
||||||
message={(location, action) => {
|
message={(location, action) => {
|
||||||
@@ -191,7 +221,21 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
{renderInputField("details", "textarea")}
|
{renderInputField("details", "textarea")}
|
||||||
{renderParentStudioField()}
|
{renderParentStudioField()}
|
||||||
{renderTagsField()}
|
{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 />
|
<hr />
|
||||||
{renderInputField("ignore_auto_tag", "checkbox")}
|
{renderInputField("ignore_auto_tag", "checkbox")}
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1015,6 +1015,7 @@
|
|||||||
"video_previews_tooltip": "Video previews which play when hovering over a scene"
|
"video_previews_tooltip": "Video previews which play when hovering over a scene"
|
||||||
},
|
},
|
||||||
"scenes_found": "{count} scenes found",
|
"scenes_found": "{count} scenes found",
|
||||||
|
"studios_found": "{count} studios found",
|
||||||
"scrape_entity_query": "{entity_type} Scrape Query",
|
"scrape_entity_query": "{entity_type} Scrape Query",
|
||||||
"scrape_entity_title": "{entity_type} Scrape Results",
|
"scrape_entity_title": "{entity_type} Scrape Results",
|
||||||
"scrape_results_existing": "Existing",
|
"scrape_results_existing": "Existing",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
|
||||||
export const getStashIDs = (
|
export const getStashIDs = (
|
||||||
ids?: { stash_id: string; endpoint: string; updated_at: string }[]
|
ids?: { stash_id: string; endpoint: string; updated_at: string }[]
|
||||||
) =>
|
) =>
|
||||||
@@ -32,3 +34,25 @@ export const separateNamesAndStashIds = (
|
|||||||
|
|
||||||
return { names, stashIds };
|
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];
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user