-
+
{!isNew && (
-
+
{text.map((t, i) => (
-
@@ -123,15 +122,14 @@ const StudioDetails: React.FC = ({
}
function maybeRenderStashBoxLink() {
- if (!link) return;
+ const base = endpoint?.match(/https?:\/\/.*?\//)?.[0];
+ if (!base || !studio.remote_site_id) return;
return (
-
-
-
-
-
-
+
);
}
@@ -145,7 +143,12 @@ const StudioDetails: React.FC = ({
{maybeRenderField("details", studio.details)}
{maybeRenderField("aliases", studio.aliases)}
{maybeRenderField("tags", studio.tags?.map((t) => t.name).join(", "))}
- {maybeRenderField("parent_studio", studio.parent?.name, false)}
+ {maybeRenderField(
+ "parent_id",
+ studio.parent?.name,
+ true,
+ "parent_studio"
+ )}
{maybeRenderStashBoxLink()}
@@ -207,6 +210,10 @@ const StudioModal: React.FC
= ({
!!studio.parent
);
+ useEffect(() => {
+ setCreateParentStudio(!excluded.parent_id && !!studio.parent);
+ }, [excluded.parent_id, studio.parent]);
+
let sendParentStudio = true;
// The parent studio exists, need to check if it has a Stash ID.
const queryResult = useFindStudio(studio.parent?.stored_id ?? "");
@@ -303,30 +310,28 @@ const StudioModal: React.FC = ({
handleStudioCreate(studioData, parentData);
}
- const base = endpoint?.match(/https?:\/\/.*?\//)?.[0];
- const link = base ? `${base}studios/${studio.remote_site_id}` : undefined;
- const parentLink = base
- ? `${base}studios/${studio.parent?.remote_site_id}`
- : undefined;
-
function maybeRenderParentStudio() {
// There is no parent studio or it already has a Stash ID
- if (!studio.parent || !sendParentStudio) {
+ if (!studio.parent || !sendParentStudio || excluded.parent_id) {
return;
}
+ // force create if there is no current parent studio and parent studio is not excluded
+ const mustCreateParent = !studio.parent.stored_id;
+
return (
-
+
setCreateParentStudio(!createParentStudio)}
/>
-
+
{maybeRenderParentStudioDetails()}
);
@@ -342,7 +347,7 @@ const StudioModal: React.FC = ({
studio={studio.parent}
excluded={parentExcluded}
toggleField={(field) => toggleParentField(field)}
- link={parentLink}
+ endpoint={endpoint}
isNew
/>
);
@@ -365,7 +370,7 @@ const StudioModal: React.FC = ({
studio={studio}
excluded={excluded}
toggleField={(field) => toggleField(field)}
- link={link}
+ endpoint={endpoint}
/>
{maybeRenderParentStudio()}
diff --git a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx
index 5446257e5..5ad895fc2 100644
--- a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx
+++ b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx
@@ -11,7 +11,10 @@ import { StashIDPill } from "src/components/Shared/StashID";
import { PerformerLink, TagLink } from "src/components/Shared/TagLink";
import { TruncatedText } from "src/components/Shared/TruncatedText";
import { parsePath, prepareQueryString } from "src/components/Tagger/utils";
-import { ScenePreview } from "src/components/Scenes/SceneCard";
+import {
+ ScenePreview,
+ SceneSpecsOverlay,
+} from "src/components/Scenes/SceneCard";
import { TaggerStateContext } from "../context";
import {
faChevronDown,
@@ -271,6 +274,7 @@ export const TaggerScene: React.FC> = ({
vttPath={scene.paths.vtt ?? undefined}
onScrubberClick={onScrubberClick}
/>
+
{maybeRenderSpriteIcon()}
diff --git a/ui/v2.5/src/components/Tagger/studios/StudioTagger.tsx b/ui/v2.5/src/components/Tagger/studios/StudioTagger.tsx
index 64bb99b72..968b66b57 100644
--- a/ui/v2.5/src/components/Tagger/studios/StudioTagger.tsx
+++ b/ui/v2.5/src/components/Tagger/studios/StudioTagger.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useMemo, useRef, useState } from "react";
+import React, { useEffect, useMemo, useState } from "react";
import { Button, Card, Form, InputGroup, ProgressBar } from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl";
import { Link } from "react-router-dom";
@@ -6,7 +6,6 @@ import { HashLink } from "react-router-hash-link";
import * as GQL from "src/core/generated-graphql";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
-import { ModalComponent } from "src/components/Shared/Modal";
import {
stashBoxStudioQuery,
useJobsSubscribe,
@@ -16,20 +15,24 @@ import {
useStudioCreate,
evictQueries,
} from "src/core/StashService";
-import { Manual } from "src/components/Help/Manual";
import { useConfigurationContext } from "src/hooks/Config";
import StashSearchResult from "./StashSearchResult";
-import TaggerConfig from "../TaggerConfig";
+import TaggerConfig, { ConfigButton } from "../TaggerConfig";
import { ITaggerConfig, STUDIO_FIELDS } from "../constants";
import StudioModal from "../scenes/StudioModal";
import { useUpdateStudio } from "../queries";
import { apolloError } from "src/utils";
-import { faStar, faTags } from "@fortawesome/free-solid-svg-icons";
+import { faTags } from "@fortawesome/free-solid-svg-icons";
import { ExternalLink } from "src/components/Shared/ExternalLink";
import { mergeStudioStashIDs } from "../utils";
import { separateNamesAndStashIds } from "src/utils/stashIds";
import { useTaggerConfig } from "../config";
+import {
+ BatchUpdateModal,
+ BatchAddModal,
+} from "src/components/Shared/BatchModals";
+import { StashBoxSelectorField } from "../StashBoxSelector";
type JobFragment = Pick<
GQL.Job,
@@ -38,232 +41,6 @@ type JobFragment = Pick<
const CLASSNAME = "StudioTagger";
-interface IStudioBatchUpdateModal {
- studios: GQL.StudioDataFragment[];
- isIdle: boolean;
- selectedEndpoint: { endpoint: string; index: number };
- onBatchUpdate: (queryAll: boolean, refresh: boolean) => void;
- batchAddParents: boolean;
- setBatchAddParents: (addParents: boolean) => void;
- close: () => void;
-}
-
-const StudioBatchUpdateModal: React.FC
= ({
- studios,
- isIdle,
- selectedEndpoint,
- onBatchUpdate,
- batchAddParents,
- setBatchAddParents,
- close,
-}) => {
- const intl = useIntl();
-
- const [queryAll, setQueryAll] = useState(false);
-
- const [refresh, setRefresh] = useState(false);
- const { data: allStudios } = GQL.useFindStudiosQuery({
- variables: {
- studio_filter: {
- stash_id_endpoint: {
- endpoint: selectedEndpoint.endpoint,
- modifier: refresh
- ? GQL.CriterionModifier.NotNull
- : GQL.CriterionModifier.IsNull,
- },
- },
- filter: {
- per_page: 0,
- },
- },
- });
-
- const studioCount = useMemo(() => {
- // get all stash ids for the selected endpoint
- const filteredStashIDs = studios.map((p) =>
- p.stash_ids.filter((s) => s.endpoint === selectedEndpoint.endpoint)
- );
-
- return queryAll
- ? allStudios?.findStudios.count
- : filteredStashIDs.filter((s) =>
- // if refresh, then we filter out the studios without a stash id
- // otherwise, we want untagged studios, filtering out those with a stash id
- refresh ? s.length > 0 : s.length === 0
- ).length;
- }, [queryAll, refresh, studios, allStudios, selectedEndpoint.endpoint]);
-
- return (
- onBatchUpdate(queryAll, refresh),
- }}
- cancel={{
- text: intl.formatMessage({ id: "actions.cancel" }),
- variant: "danger",
- onClick: () => close(),
- }}
- disabled={!isIdle}
- >
-
-
-
-
-
-
- }
- checked={!queryAll}
- onChange={() => setQueryAll(false)}
- />
- setQueryAll(true)}
- />
-
-
-
-
-
-
-
- setRefresh(false)}
- />
-
-
-
- setRefresh(true)}
- />
-
-
-
-
-
setBatchAddParents(!batchAddParents)}
- />
-
-
-
-
-
-
- );
-};
-
-interface IStudioBatchAddModal {
- isIdle: boolean;
- onBatchAdd: (input: string) => void;
- batchAddParents: boolean;
- setBatchAddParents: (addParents: boolean) => void;
- close: () => void;
-}
-
-const StudioBatchAddModal: React.FC = ({
- isIdle,
- onBatchAdd,
- batchAddParents,
- setBatchAddParents,
- close,
-}) => {
- const intl = useIntl();
-
- const studioInput = useRef(null);
-
- return (
- {
- if (studioInput.current) {
- onBatchAdd(studioInput.current.value);
- } else {
- close();
- }
- },
- }}
- cancel={{
- text: intl.formatMessage({ id: "actions.cancel" }),
- variant: "danger",
- onClick: () => close(),
- }}
- disabled={!isIdle}
- >
-
-
-
-
-
-
setBatchAddParents(!batchAddParents)}
- />
-
-
- );
-};
-
interface IStudioTaggerListProps {
studios: GQL.StudioDataFragment[];
selectedEndpoint: { endpoint: string; index: number };
@@ -305,6 +82,24 @@ const StudioTaggerList: React.FC = ({
config.createParentStudios || false
);
+ const [batchUpdateRefresh, setBatchUpdateRefresh] = useState(false);
+ const { data: allStudios } = GQL.useFindStudiosQuery({
+ skip: !showBatchUpdate,
+ variables: {
+ studio_filter: {
+ stash_id_endpoint: {
+ endpoint: selectedEndpoint.endpoint,
+ modifier: batchUpdateRefresh
+ ? GQL.CriterionModifier.NotNull
+ : GQL.CriterionModifier.IsNull,
+ },
+ },
+ filter: {
+ per_page: 0,
+ },
+ },
+ });
+
const [error, setError] = useState<
Record
>({});
@@ -386,6 +181,13 @@ const StudioTaggerList: React.FC = ({
});
};
+ // clear tagged studios when source is changed
+ useEffect(() => {
+ setTaggedStudios({});
+ setSearchResults({});
+ setSearchErrors({});
+ }, [selectedEndpoint]);
+
const [createStudio] = useStudioCreate();
const updateStudio = useUpdateStudio();
@@ -590,20 +392,6 @@ const StudioTaggerList: React.FC = ({
return (
- {modalStudio && (
-
setModalStudio(undefined)}
- modalVisible={modalStudio.stored_id === studio.id}
- studio={modalStudio}
- handleStudioCreate={handleStudioUpdate}
- excludedStudioFields={config.excludedStudioFields}
- icon={faTags}
- header={intl.formatMessage({
- id: "studio_tagger.update_studio",
- })}
- endpoint={selectedEndpoint.endpoint}
- />
- )}
@@ -630,24 +418,45 @@ const StudioTaggerList: React.FC
= ({
return (
{showBatchUpdate && (
- setShowBatchUpdate(false)}
isIdle={isIdle}
selectedEndpoint={selectedEndpoint}
- studios={studios}
+ entities={studios}
+ allCount={allStudios?.findStudios.count}
onBatchUpdate={handleBatchUpdate}
+ onRefreshChange={setBatchUpdateRefresh}
batchAddParents={batchAddParents}
setBatchAddParents={setBatchAddParents}
+ localePrefix="studio_tagger"
+ entityName="studio"
+ countVariableName="studio_count"
/>
)}
{showBatchAdd && (
- setShowBatchAdd(false)}
isIdle={isIdle}
onBatchAdd={handleBatchAdd}
batchAddParents={batchAddParents}
setBatchAddParents={setBatchAddParents}
+ localePrefix="studio_tagger"
+ entityName="studio"
+ />
+ )}
+ {modalStudio && (
+ setModalStudio(undefined)}
+ modalVisible={!!modalStudio.stored_id}
+ studio={modalStudio}
+ handleStudioCreate={handleStudioUpdate}
+ excludedStudioFields={config.excludedStudioFields}
+ icon={faTags}
+ header={intl.formatMessage({
+ id: "studio_tagger.update_studio",
+ })}
+ endpoint={selectedEndpoint.endpoint}
/>
)}
@@ -669,11 +478,9 @@ interface ITaggerProps {
export const StudioTagger: React.FC
= ({ studios }) => {
const jobsSubscribe = useJobsSubscribe();
- const intl = useIntl();
const { configuration: stashConfig } = useConfigurationContext();
const { config, setConfig } = useTaggerConfig();
const [showConfig, setShowConfig] = useState(false);
- const [showManual, setShowManual] = useState(false);
const [batchJobID, setBatchJobID] = useState();
const [batchJob, setBatchJob] = useState();
@@ -701,8 +508,6 @@ export const StudioTagger: React.FC = ({ studios }) => {
}
}, [jobsSubscribe, batchJobID]);
- if (!config) return ;
-
const savedEndpointIndex =
stashConfig?.general.stashBoxes.findIndex(
(s) => s.endpoint === config.selectedEndpoint
@@ -714,6 +519,16 @@ export const StudioTagger: React.FC = ({ studios }) => {
const selectedEndpoint =
stashConfig?.general.stashBoxes[selectedEndpointIndex];
+ const selectedEndpointInput = useMemo(
+ () => ({
+ endpoint: selectedEndpoint.endpoint,
+ index: selectedEndpointIndex,
+ }),
+ [selectedEndpoint, selectedEndpointIndex]
+ );
+
+ if (!config) return ;
+
async function batchAdd(studioInput: string, createParent: boolean) {
if (studioInput && selectedEndpoint) {
const inputs = studioInput
@@ -796,98 +611,99 @@ export const StudioTagger: React.FC = ({ studios }) => {
}
}
- const showHideConfigId = showConfig
- ? "actions.hide_configuration"
- : "actions.show_configuration";
+ if (selectedEndpointIndex === -1 || !selectedEndpoint) {
+ return (
+
+
+
+
+
+
+ el.scrollIntoView({ behavior: "smooth", block: "center" })
+ }
+ >
+
+
+ ),
+ }}
+ />
+
+
+ );
+ }
return (
<>
- setShowManual(false)}
- defaultActiveTab="Tagger.md"
- />
{renderStatus()}
- {selectedEndpointIndex !== -1 && selectedEndpoint ? (
- <>
-
-
-
-
-
-
- setConfig({ ...config, excludedStudioFields: fields })
- }
- fields={STUDIO_FIELDS}
- entityName="studios"
- extraConfig={
-
-
- }
- checked={config.createParentStudios}
- onChange={(e: React.ChangeEvent) =>
- setConfig({
- ...config,
- createParentStudios: e.currentTarget.checked,
- })
- }
- />
-
-
-
-
- }
- />
-
- >
- ) : (
-
-
-
-
-
- Please see{" "}
-
- el.scrollIntoView({ behavior: "smooth", block: "center" })
+
+
+
+
+ setConfig({ ...config, selectedEndpoint: endpoint })
}
- >
- Settings.
-
-
+ />
+
+
+
+ setShowConfig(!showConfig)}
+ />
+
+
- )}
+
+
+ setConfig({ ...config, excludedStudioFields: fields })
+ }
+ fields={STUDIO_FIELDS}
+ entityName="studios"
+ extraConfig={
+
+
+ }
+ checked={config.createParentStudios}
+ onChange={(e: React.ChangeEvent) =>
+ setConfig({
+ ...config,
+ createParentStudios: e.currentTarget.checked,
+ })
+ }
+ />
+
+
+
+
+ }
+ />
+
+
+
>
);
diff --git a/ui/v2.5/src/components/Tagger/styles.scss b/ui/v2.5/src/components/Tagger/styles.scss
index 8861d0043..0e9db45a6 100644
--- a/ui/v2.5/src/components/Tagger/styles.scss
+++ b/ui/v2.5/src/components/Tagger/styles.scss
@@ -8,6 +8,11 @@
.scene-card {
position: relative;
+
+ .scene-specs-overlay {
+ bottom: 5px;
+ right: 5px;
+ }
}
.scene-card-preview {
@@ -46,6 +51,10 @@
flex-direction: column;
overflow-wrap: anywhere;
width: 100%;
+
+ .optional-field-content {
+ min-width: 0;
+ }
}
.original-scene-details {
@@ -126,6 +135,24 @@
font-weight: 500;
}
+.create-modal-field {
+ margin-bottom: 5px;
+
+ .btn {
+ margin-right: 5px;
+ }
+
+ .fa-icon {
+ width: 12px;
+ }
+}
+
+.create-modal-value ul {
+ font-size: 0.8em;
+ list-style-type: none;
+ padding-inline-start: 0;
+}
+
.performer-create-modal {
font-size: 1.2rem;
max-width: 800px;
@@ -154,24 +181,6 @@
.LoadingIndicator {
height: 100%;
}
-
- &-field {
- margin-bottom: 5px;
-
- .btn {
- margin-right: 5px;
- }
-
- .fa-icon {
- width: 12px;
- }
- }
-
- &-value ul {
- font-size: 0.8em;
- list-style-type: none;
- padding-inline-start: 0;
- }
}
.PerformerTagger {
@@ -241,7 +250,8 @@
}
}
-.studio-create-modal {
+.studio-create-modal,
+.tag-create-modal {
font-size: 1.2rem;
max-width: 800px;
@@ -269,20 +279,17 @@
height: 100%;
}
- &-field {
- margin-bottom: 5px;
+ .form-check {
+ font-size: 1rem;
+ }
- .btn {
- margin-right: 5px;
- }
-
- .fa-icon {
- width: 12px;
- }
+ p.lead {
+ margin-top: 1rem;
}
}
-.StudioTagger {
+.StudioTagger,
+.TagTagger {
display: flex;
flex-wrap: wrap;
justify-content: center;
@@ -296,7 +303,8 @@
}
}
- &-studio {
+ &-studio,
+ &-tag {
background-color: #495b68;
border-radius: 3px;
display: flex;
@@ -304,7 +312,8 @@
max-width: 100%;
padding: 1rem;
- .studio-card {
+ .studio-card,
+ .tag-card {
box-shadow: none;
flex-shrink: 0;
margin: 0;
@@ -337,7 +346,8 @@
vertical-align: bottom;
}
- &-studio-search {
+ &-studio-search,
+ &-tag-search {
display: flex;
flex-wrap: wrap;
diff --git a/ui/v2.5/src/components/Tagger/tags/StashSearchResult.tsx b/ui/v2.5/src/components/Tagger/tags/StashSearchResult.tsx
index cd6abca02..55b86c931 100644
--- a/ui/v2.5/src/components/Tagger/tags/StashSearchResult.tsx
+++ b/ui/v2.5/src/components/Tagger/tags/StashSearchResult.tsx
@@ -7,6 +7,8 @@ import TagModal from "./TagModal";
import { faTags } from "@fortawesome/free-solid-svg-icons";
import { useIntl } from "react-intl";
import { mergeTagStashIDs } from "../utils";
+import { useTagCreate } from "src/core/StashService";
+import { apolloError } from "src/utils";
interface IStashSearchResultProps {
tag: GQL.TagListDataFragment;
@@ -34,13 +36,49 @@ const StashSearchResult: React.FC = ({
{}
);
+ const [createTag] = useTagCreate();
const updateTag = useUpdateTag();
- const handleSave = async (input: GQL.TagCreateInput) => {
+ function handleSaveError(name: string, message: string) {
+ setError({
+ message: intl.formatMessage(
+ { id: "tag_tagger.failed_to_save_tag" },
+ { tag: name }
+ ),
+ details:
+ message === "UNIQUE constraint failed: tags.name"
+ ? intl.formatMessage({
+ id: "tag_tagger.name_already_exists",
+ })
+ : message,
+ });
+ }
+
+ const handleSave = async (
+ input: GQL.TagCreateInput,
+ parentInput?: GQL.TagCreateInput
+ ) => {
setError({});
setModalTag(undefined);
- setSaveState("Saving tag");
+ if (parentInput) {
+ setSaveState("Saving parent tag");
+
+ try {
+ const parentRes = await createTag({
+ variables: { input: parentInput },
+ });
+ input.parent_ids = [parentRes.data?.tagCreate?.id].filter(
+ Boolean
+ ) as string[];
+ } catch (e) {
+ handleSaveError(parentInput.name, apolloError(e));
+ setSaveState("");
+ return;
+ }
+ }
+
+ setSaveState("Saving tag");
const updateData: GQL.TagUpdateInput = {
...input,
id: tag.id,
@@ -54,18 +92,7 @@ const StashSearchResult: React.FC = ({
const res = await updateTag(updateData);
if (!res?.data?.tagUpdate) {
- setError({
- message: intl.formatMessage(
- { id: "tag_tagger.failed_to_save_tag" },
- { tag: input.name ?? tag.name }
- ),
- details:
- res?.errors?.[0]?.message === "UNIQUE constraint failed: tags.name"
- ? intl.formatMessage({
- id: "tag_tagger.name_already_exists",
- })
- : res?.errors?.[0]?.message ?? "",
- });
+ handleSaveError(input.name ?? tag.name, res?.errors?.[0]?.message ?? "");
} else {
onTagTagged(tag);
}
@@ -74,7 +101,7 @@ const StashSearchResult: React.FC = ({
const tags = stashboxTags.map((p) => (