mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
New scene select with additional fields (#4832)
This commit is contained in:
@@ -79,3 +79,19 @@ fragment SceneData on Scene {
|
|||||||
label
|
label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fragment SelectSceneData on Scene {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
date
|
||||||
|
code
|
||||||
|
studio {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
files {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
paths {
|
||||||
|
screenshot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -89,3 +89,16 @@ query SceneStreams($id: ID!) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query FindScenesForSelect(
|
||||||
|
$filter: FindFilterType
|
||||||
|
$scene_filter: SceneFilterType
|
||||||
|
$ids: [ID!]
|
||||||
|
) {
|
||||||
|
findScenes(filter: $filter, scene_filter: $scene_filter, ids: $ids) {
|
||||||
|
count
|
||||||
|
scenes {
|
||||||
|
...SelectSceneData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,14 +18,12 @@ import {
|
|||||||
useListGalleryScrapers,
|
useListGalleryScrapers,
|
||||||
mutateReloadScrapers,
|
mutateReloadScrapers,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { SceneSelect } from "src/components/Shared/Select";
|
|
||||||
import { Icon } from "src/components/Shared/Icon";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { GalleryScrapeDialog } from "./GalleryScrapeDialog";
|
import { GalleryScrapeDialog } from "./GalleryScrapeDialog";
|
||||||
import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { galleryTitle } from "src/core/galleries";
|
|
||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
import { handleUnsavedChanges } from "src/utils/navigation";
|
import { handleUnsavedChanges } from "src/utils/navigation";
|
||||||
import {
|
import {
|
||||||
@@ -40,6 +38,7 @@ import {
|
|||||||
import { formikUtils } from "src/utils/form";
|
import { formikUtils } from "src/utils/form";
|
||||||
import { Tag, TagSelect } from "src/components/Tags/TagSelect";
|
import { Tag, TagSelect } from "src/components/Tags/TagSelect";
|
||||||
import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
|
import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
|
||||||
|
import { Scene, SceneSelect } from "src/components/Scenes/SceneSelect";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
gallery: Partial<GQL.GalleryDataFragment>;
|
gallery: Partial<GQL.GalleryDataFragment>;
|
||||||
@@ -56,12 +55,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [scenes, setScenes] = useState<{ id: string; title: string }[]>(
|
const [scenes, setScenes] = useState<Scene[]>([]);
|
||||||
(gallery?.scenes ?? []).map((s) => ({
|
|
||||||
id: s.id,
|
|
||||||
title: galleryTitle(s),
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
const [performers, setPerformers] = useState<Performer[]>([]);
|
const [performers, setPerformers] = useState<Performer[]>([]);
|
||||||
const [tags, setTags] = useState<Tag[]>([]);
|
const [tags, setTags] = useState<Tag[]>([]);
|
||||||
@@ -116,12 +110,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
onSubmit: (values) => onSave(schema.cast(values)),
|
onSubmit: (values) => onSave(schema.cast(values)),
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ISceneSelectValue {
|
function onSetScenes(items: Scene[]) {
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSetScenes(items: ISceneSelectValue[]) {
|
|
||||||
setScenes(items);
|
setScenes(items);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
"scene_ids",
|
"scene_ids",
|
||||||
@@ -162,6 +151,10 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
setStudio(gallery.studio ?? null);
|
setStudio(gallery.studio ?? null);
|
||||||
}, [gallery.studio]);
|
}, [gallery.studio]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setScenes(gallery.scenes ?? []);
|
||||||
|
}, [gallery.scenes]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
Mousetrap.bind("s s", () => {
|
Mousetrap.bind("s s", () => {
|
||||||
@@ -412,7 +405,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
const title = intl.formatMessage({ id: "scenes" });
|
const title = intl.formatMessage({ id: "scenes" });
|
||||||
const control = (
|
const control = (
|
||||||
<SceneSelect
|
<SceneSelect
|
||||||
selected={scenes}
|
values={scenes}
|
||||||
onSelect={(items) => onSetScenes(items)}
|
onSelect={(items) => onSetScenes(items)}
|
||||||
isMulti
|
isMulti
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { useEffect, useMemo, useState } from "react";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Icon } from "../Shared/Icon";
|
import { Icon } from "../Shared/Icon";
|
||||||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||||
import { StringListSelect, GallerySelect, SceneSelect } from "../Shared/Select";
|
import { StringListSelect, GallerySelect } from "../Shared/Select";
|
||||||
import * as FormUtils from "src/utils/form";
|
import * as FormUtils from "src/utils/form";
|
||||||
import ImageUtils from "src/utils/image";
|
import ImageUtils from "src/utils/image";
|
||||||
import TextUtils from "src/utils/text";
|
import TextUtils from "src/utils/text";
|
||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
ScrapedStudioRow,
|
ScrapedStudioRow,
|
||||||
ScrapedTagsRow,
|
ScrapedTagsRow,
|
||||||
} from "../Shared/ScrapeDialog/ScrapedObjectsRow";
|
} from "../Shared/ScrapeDialog/ScrapedObjectsRow";
|
||||||
|
import { Scene, SceneSelect } from "src/components/Scenes/SceneSelect";
|
||||||
|
|
||||||
interface IStashIDsField {
|
interface IStashIDsField {
|
||||||
values: GQL.StashId[];
|
values: GQL.StashId[];
|
||||||
@@ -645,12 +646,8 @@ export const SceneMergeModal: React.FC<ISceneMergeModalProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
scenes,
|
scenes,
|
||||||
}) => {
|
}) => {
|
||||||
const [sourceScenes, setSourceScenes] = useState<
|
const [sourceScenes, setSourceScenes] = useState<Scene[]>([]);
|
||||||
{ id: string; title: string }[]
|
const [destScene, setDestScene] = useState<Scene[]>([]);
|
||||||
>([]);
|
|
||||||
const [destScene, setDestScene] = useState<{ id: string; title: string }[]>(
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [loadedSources, setLoadedSources] = useState<
|
const [loadedSources, setLoadedSources] = useState<
|
||||||
GQL.SlimSceneDataFragment[]
|
GQL.SlimSceneDataFragment[]
|
||||||
@@ -773,7 +770,7 @@ export const SceneMergeModal: React.FC<ISceneMergeModalProps> = ({
|
|||||||
<SceneSelect
|
<SceneSelect
|
||||||
isMulti
|
isMulti
|
||||||
onSelect={(items) => setSourceScenes(items)}
|
onSelect={(items) => setSourceScenes(items)}
|
||||||
selected={sourceScenes}
|
values={sourceScenes}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -805,7 +802,7 @@ export const SceneMergeModal: React.FC<ISceneMergeModalProps> = ({
|
|||||||
<Col sm={9} xl={12}>
|
<Col sm={9} xl={12}>
|
||||||
<SceneSelect
|
<SceneSelect
|
||||||
onSelect={(items) => setDestScene(items)}
|
onSelect={(items) => setDestScene(items)}
|
||||||
selected={destScene}
|
values={destScene}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|||||||
264
ui/v2.5/src/components/Scenes/SceneSelect.tsx
Normal file
264
ui/v2.5/src/components/Scenes/SceneSelect.tsx
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
OptionProps,
|
||||||
|
components as reactSelectComponents,
|
||||||
|
MultiValueGenericProps,
|
||||||
|
SingleValueProps,
|
||||||
|
} from "react-select";
|
||||||
|
import cx from "classnames";
|
||||||
|
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import {
|
||||||
|
queryFindScenesForSelect,
|
||||||
|
queryFindScenesByIDForSelect,
|
||||||
|
} from "src/core/StashService";
|
||||||
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
import { defaultMaxOptionsShown } from "src/core/config";
|
||||||
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
import {
|
||||||
|
FilterSelectComponent,
|
||||||
|
IFilterIDProps,
|
||||||
|
IFilterProps,
|
||||||
|
IFilterValueProps,
|
||||||
|
Option as SelectOption,
|
||||||
|
} from "../Shared/FilterSelect";
|
||||||
|
import { useCompare } from "src/hooks/state";
|
||||||
|
import { Placement } from "react-bootstrap/esm/Overlay";
|
||||||
|
import { sortByRelevance } from "src/utils/query";
|
||||||
|
import { objectTitle } from "src/core/files";
|
||||||
|
import { PatchComponent } from "src/patch";
|
||||||
|
import {
|
||||||
|
Criterion,
|
||||||
|
CriterionValue,
|
||||||
|
} from "src/models/list-filter/criteria/criterion";
|
||||||
|
import { TruncatedText } from "../Shared/TruncatedText";
|
||||||
|
|
||||||
|
export type Scene = Pick<GQL.Scene, "id" | "title" | "date" | "code"> & {
|
||||||
|
studio?: Pick<GQL.Studio, "name"> | null;
|
||||||
|
} & {
|
||||||
|
files?: Pick<GQL.VideoFile, "path">[];
|
||||||
|
} & {
|
||||||
|
paths?: Pick<GQL.ScenePathsType, "screenshot">;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Option = SelectOption<Scene>;
|
||||||
|
|
||||||
|
type ExtraSceneProps = {
|
||||||
|
hoverPlacement?: Placement;
|
||||||
|
excludeIds?: string[];
|
||||||
|
extraCriteria?: Array<Criterion<CriterionValue>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _SceneSelect: React.FC<
|
||||||
|
IFilterProps & IFilterValueProps<Scene> & ExtraSceneProps
|
||||||
|
> = (props) => {
|
||||||
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
|
const intl = useIntl();
|
||||||
|
const maxOptionsShown =
|
||||||
|
configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown;
|
||||||
|
|
||||||
|
const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]);
|
||||||
|
|
||||||
|
async function loadScenes(input: string): Promise<Option[]> {
|
||||||
|
const filter = new ListFilterModel(GQL.FilterMode.Scenes);
|
||||||
|
filter.searchTerm = input;
|
||||||
|
filter.currentPage = 1;
|
||||||
|
filter.itemsPerPage = maxOptionsShown;
|
||||||
|
filter.sortBy = "title";
|
||||||
|
filter.sortDirection = GQL.SortDirectionEnum.Asc;
|
||||||
|
|
||||||
|
if (props.extraCriteria) {
|
||||||
|
filter.criteria = [...props.extraCriteria];
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = await queryFindScenesForSelect(filter);
|
||||||
|
let ret = query.data.findScenes.scenes.filter((scene) => {
|
||||||
|
// HACK - we should probably exclude these in the backend query, but
|
||||||
|
// this will do in the short-term
|
||||||
|
return !exclude.includes(scene.id.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortByRelevance(input, ret, objectTitle, (s) => {
|
||||||
|
return s.files.map((f) => f.path);
|
||||||
|
}).map((scene) => ({
|
||||||
|
value: scene.id,
|
||||||
|
object: scene,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const SceneOption: React.FC<OptionProps<Option, boolean>> = (optionProps) => {
|
||||||
|
let thisOptionProps = optionProps;
|
||||||
|
|
||||||
|
const { object } = optionProps.data;
|
||||||
|
|
||||||
|
const title = objectTitle(object);
|
||||||
|
|
||||||
|
// if title does not match the input value but the path does, show the path
|
||||||
|
const { inputValue } = optionProps.selectProps;
|
||||||
|
let matchedPath: string | undefined = "";
|
||||||
|
if (!title.toLowerCase().includes(inputValue.toLowerCase())) {
|
||||||
|
matchedPath = object.files?.find((a) =>
|
||||||
|
a.path.toLowerCase().includes(inputValue.toLowerCase())
|
||||||
|
)?.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
thisOptionProps = {
|
||||||
|
...optionProps,
|
||||||
|
children: (
|
||||||
|
<span className="scene-select-option">
|
||||||
|
<span className="scene-select-row">
|
||||||
|
{object.paths?.screenshot && (
|
||||||
|
<img
|
||||||
|
className="scene-select-image"
|
||||||
|
src={object.paths.screenshot}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span className="scene-select-details">
|
||||||
|
<TruncatedText
|
||||||
|
className="scene-select-title"
|
||||||
|
text={title}
|
||||||
|
lineCount={1}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{object.studio?.name && (
|
||||||
|
<span className="scene-select-studio">
|
||||||
|
{object.studio?.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{object.date && (
|
||||||
|
<span className="scene-select-date">{object.date}</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{object.code && (
|
||||||
|
<span className="scene-select-code">{object.code}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{matchedPath && (
|
||||||
|
<span className="scene-select-alias">{`(${matchedPath})`}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return <reactSelectComponents.Option {...thisOptionProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SceneMultiValueLabel: React.FC<
|
||||||
|
MultiValueGenericProps<Option, boolean>
|
||||||
|
> = (optionProps) => {
|
||||||
|
let thisOptionProps = optionProps;
|
||||||
|
|
||||||
|
const { object } = optionProps.data;
|
||||||
|
|
||||||
|
thisOptionProps = {
|
||||||
|
...optionProps,
|
||||||
|
children: objectTitle(object),
|
||||||
|
};
|
||||||
|
|
||||||
|
return <reactSelectComponents.MultiValueLabel {...thisOptionProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SceneValueLabel: React.FC<SingleValueProps<Option, boolean>> = (
|
||||||
|
optionProps
|
||||||
|
) => {
|
||||||
|
let thisOptionProps = optionProps;
|
||||||
|
|
||||||
|
const { object } = optionProps.data;
|
||||||
|
|
||||||
|
thisOptionProps = {
|
||||||
|
...optionProps,
|
||||||
|
children: <>{objectTitle(object)}</>,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <reactSelectComponents.SingleValue {...thisOptionProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterSelectComponent<Scene, boolean>
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"scene-select",
|
||||||
|
{
|
||||||
|
"scene-select-active": props.active,
|
||||||
|
},
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
loadOptions={loadScenes}
|
||||||
|
components={{
|
||||||
|
Option: SceneOption,
|
||||||
|
MultiValueLabel: SceneMultiValueLabel,
|
||||||
|
SingleValue: SceneValueLabel,
|
||||||
|
}}
|
||||||
|
isMulti={props.isMulti ?? false}
|
||||||
|
placeholder={
|
||||||
|
props.noSelectionString ??
|
||||||
|
intl.formatMessage(
|
||||||
|
{ id: "actions.select_entity" },
|
||||||
|
{
|
||||||
|
entityType: intl.formatMessage({
|
||||||
|
id: props.isMulti ? "scenes" : "scene",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
closeMenuOnSelect={!props.isMulti}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SceneSelect = PatchComponent("SceneSelect", _SceneSelect);
|
||||||
|
|
||||||
|
const _SceneIDSelect: React.FC<
|
||||||
|
IFilterProps & IFilterIDProps<Scene> & ExtraSceneProps
|
||||||
|
> = (props) => {
|
||||||
|
const { ids, onSelect: onSelectValues } = props;
|
||||||
|
|
||||||
|
const [values, setValues] = useState<Scene[]>([]);
|
||||||
|
const idsChanged = useCompare(ids);
|
||||||
|
|
||||||
|
function onSelect(items: Scene[]) {
|
||||||
|
setValues(items);
|
||||||
|
onSelectValues?.(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadObjectsByID(idsToLoad: string[]): Promise<Scene[]> {
|
||||||
|
const query = await queryFindScenesByIDForSelect(idsToLoad);
|
||||||
|
const { scenes: loadedScenes } = query.data.findScenes;
|
||||||
|
|
||||||
|
return loadedScenes;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!idsChanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ids || ids?.length === 0) {
|
||||||
|
setValues([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the values if we have ids and they haven't been loaded yet
|
||||||
|
const filteredValues = values.filter((v) => ids.includes(v.id.toString()));
|
||||||
|
if (filteredValues.length === ids.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
const items = await loadObjectsByID(ids);
|
||||||
|
setValues(items);
|
||||||
|
};
|
||||||
|
|
||||||
|
load();
|
||||||
|
}, [ids, idsChanged, values]);
|
||||||
|
|
||||||
|
return <SceneSelect {...props} values={values} onSelect={onSelect} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SceneIDSelect = PatchComponent("SceneIDSelect", _SceneIDSelect);
|
||||||
@@ -842,3 +842,52 @@ input[type="range"].blue-slider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scene-select-option {
|
||||||
|
.scene-select-row {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.scene-select-image {
|
||||||
|
background-color: $body-bg;
|
||||||
|
margin-right: 0.4em;
|
||||||
|
max-height: 50px;
|
||||||
|
max-width: 89px;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-select-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
max-height: 4.1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.scene-select-title {
|
||||||
|
flex-shrink: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-select-date,
|
||||||
|
.scene-select-studio,
|
||||||
|
.scene-select-code {
|
||||||
|
color: $text-muted;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-select-alias {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 100%;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { ModalComponent } from "./Modal";
|
import { ModalComponent } from "./Modal";
|
||||||
import { SceneSelect } from "./Select";
|
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { faSignOutAlt } from "@fortawesome/free-solid-svg-icons";
|
import { faSignOutAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { Col, Form, Row } from "react-bootstrap";
|
import { Col, Form, Row } from "react-bootstrap";
|
||||||
import * as FormUtils from "src/utils/form";
|
import * as FormUtils from "src/utils/form";
|
||||||
import { mutateSceneAssignFile } from "src/core/StashService";
|
import { mutateSceneAssignFile } from "src/core/StashService";
|
||||||
|
import { Scene, SceneSelect } from "src/components/Scenes/SceneSelect";
|
||||||
|
|
||||||
interface IFile {
|
interface IFile {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -21,7 +21,7 @@ interface IReassignFilesDialogProps {
|
|||||||
export const ReassignFilesDialog: React.FC<IReassignFilesDialogProps> = (
|
export const ReassignFilesDialog: React.FC<IReassignFilesDialogProps> = (
|
||||||
props: IReassignFilesDialogProps
|
props: IReassignFilesDialogProps
|
||||||
) => {
|
) => {
|
||||||
const [scenes, setScenes] = useState<{ id: string; title: string }[]>([]);
|
const [scenes, setScenes] = useState<Scene[]>([]);
|
||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const singularEntity = intl.formatMessage({ id: "file" });
|
const singularEntity = intl.formatMessage({ id: "file" });
|
||||||
@@ -89,7 +89,7 @@ export const ReassignFilesDialog: React.FC<IReassignFilesDialogProps> = (
|
|||||||
})}
|
})}
|
||||||
<Col sm={9} xl={12}>
|
<Col sm={9} xl={12}>
|
||||||
<SceneSelect
|
<SceneSelect
|
||||||
selected={scenes}
|
values={scenes}
|
||||||
onSelect={(items) => setScenes(items)}
|
onSelect={(items) => setScenes(items)}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { TagIDSelect } from "../Tags/TagSelect";
|
|||||||
import { StudioIDSelect } from "../Studios/StudioSelect";
|
import { StudioIDSelect } from "../Studios/StudioSelect";
|
||||||
import { GalleryIDSelect } from "../Galleries/GallerySelect";
|
import { GalleryIDSelect } from "../Galleries/GallerySelect";
|
||||||
import { MovieIDSelect } from "../Movies/MovieSelect";
|
import { MovieIDSelect } from "../Movies/MovieSelect";
|
||||||
|
import { SceneIDSelect } from "../Scenes/SceneSelect";
|
||||||
|
|
||||||
export type SelectObject = {
|
export type SelectObject = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -254,54 +255,10 @@ export const GallerySelect: React.FC<
|
|||||||
return <GalleryIDSelect {...props} />;
|
return <GalleryIDSelect {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SceneSelect: React.FC<ITitledSelect> = (props) => {
|
export const SceneSelect: React.FC<IFilterProps & { excludeIds?: string[] }> = (
|
||||||
const [query, setQuery] = useState<string>("");
|
props
|
||||||
const { data, loading } = GQL.useFindScenesQuery({
|
) => {
|
||||||
skip: query === "",
|
return <SceneIDSelect {...props} />;
|
||||||
variables: {
|
|
||||||
filter: {
|
|
||||||
q: query,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const scenes = data?.findScenes.scenes ?? [];
|
|
||||||
const items = scenes.map((s) => ({
|
|
||||||
label: objectTitle(s),
|
|
||||||
value: s.id,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const onInputChange = useDebounce(setQuery, 500);
|
|
||||||
|
|
||||||
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
|
|
||||||
const selected = getSelectedItems(selectedItems);
|
|
||||||
props.onSelect(
|
|
||||||
(selected ?? []).map((s) => ({
|
|
||||||
id: s.value,
|
|
||||||
title: s.label,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = props.selected.map((s) => ({
|
|
||||||
value: s.id,
|
|
||||||
label: s.title,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SelectComponent
|
|
||||||
onChange={onChange}
|
|
||||||
onInputChange={onInputChange}
|
|
||||||
isLoading={loading}
|
|
||||||
items={items}
|
|
||||||
selectedOptions={options}
|
|
||||||
isMulti={props.isMulti ?? false}
|
|
||||||
placeholder="Search for scene..."
|
|
||||||
noOptionsMessage={query === "" ? null : "No scenes found."}
|
|
||||||
showDropdown={false}
|
|
||||||
isDisabled={props.disabled}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImageSelect: React.FC<ITitledSelect> = (props) => {
|
export const ImageSelect: React.FC<ITitledSelect> = (props) => {
|
||||||
|
|||||||
@@ -166,6 +166,23 @@ export const queryFindScenesByID = (sceneIDs: number[]) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const queryFindScenesForSelect = (filter: ListFilterModel) =>
|
||||||
|
client.query<GQL.FindScenesForSelectQuery>({
|
||||||
|
query: GQL.FindScenesForSelectDocument,
|
||||||
|
variables: {
|
||||||
|
filter: filter.makeFindFilter(),
|
||||||
|
scene_filter: filter.makeFilter(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const queryFindScenesByIDForSelect = (sceneIDs: string[]) =>
|
||||||
|
client.query<GQL.FindScenesForSelectQuery>({
|
||||||
|
query: GQL.FindScenesForSelectDocument,
|
||||||
|
variables: {
|
||||||
|
ids: sceneIDs,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const querySceneByPathRegex = (filter: GQL.FindFilterType) =>
|
export const querySceneByPathRegex = (filter: GQL.FindFilterType) =>
|
||||||
client.query<GQL.FindScenesByPathRegexQuery>({
|
client.query<GQL.FindScenesByPathRegexQuery>({
|
||||||
query: GQL.FindScenesByPathRegexDocument,
|
query: GQL.FindScenesByPathRegexDocument,
|
||||||
|
|||||||
2
ui/v2.5/src/pluginApi.d.ts
vendored
2
ui/v2.5/src/pluginApi.d.ts
vendored
@@ -672,6 +672,8 @@ declare namespace PluginApi {
|
|||||||
GalleryIDSelect: React.FC<any>;
|
GalleryIDSelect: React.FC<any>;
|
||||||
MovieSelect: React.FC<any>;
|
MovieSelect: React.FC<any>;
|
||||||
MovieIDSelect: React.FC<any>;
|
MovieIDSelect: React.FC<any>;
|
||||||
|
SceneSelect: React.FC<any>;
|
||||||
|
SceneIDSelect: React.FC<any>;
|
||||||
DateInput: React.FC<any>;
|
DateInput: React.FC<any>;
|
||||||
CountrySelect: React.FC<any>;
|
CountrySelect: React.FC<any>;
|
||||||
FolderSelect: React.FC<any>;
|
FolderSelect: React.FC<any>;
|
||||||
|
|||||||
Reference in New Issue
Block a user