mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
Support setting galleries in multiple images (#4608)
This commit is contained in:
@@ -32,6 +32,7 @@ import {
|
||||
Criterion,
|
||||
CriterionValue,
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
import { PathCriterion } from "src/models/list-filter/criteria/path";
|
||||
|
||||
export type Gallery = Pick<GQL.Gallery, "id" | "title"> & {
|
||||
files: Pick<GQL.GalleryFile, "path">[];
|
||||
@@ -39,14 +40,14 @@ export type Gallery = Pick<GQL.Gallery, "id" | "title"> & {
|
||||
};
|
||||
type Option = SelectOption<Gallery>;
|
||||
|
||||
type ExtraGalleryProps = {
|
||||
hoverPlacement?: Placement;
|
||||
excludeIds?: string[];
|
||||
extraCriteria?: Array<Criterion<CriterionValue>>;
|
||||
};
|
||||
|
||||
const _GallerySelect: React.FC<
|
||||
IFilterProps &
|
||||
IFilterValueProps<Gallery> & {
|
||||
hoverPlacement?: Placement;
|
||||
excludeIds?: string[];
|
||||
} & {
|
||||
extraCriteria?: Array<Criterion<CriterionValue>>;
|
||||
}
|
||||
IFilterProps & IFilterValueProps<Gallery> & ExtraGalleryProps
|
||||
> = (props) => {
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const intl = useIntl();
|
||||
@@ -187,9 +188,9 @@ const _GallerySelect: React.FC<
|
||||
|
||||
export const GallerySelect = PatchComponent("GallerySelect", _GallerySelect);
|
||||
|
||||
const _GalleryIDSelect: React.FC<IFilterProps & IFilterIDProps<Gallery>> = (
|
||||
props
|
||||
) => {
|
||||
const _GalleryIDSelect: React.FC<
|
||||
IFilterProps & IFilterIDProps<Gallery> & ExtraGalleryProps
|
||||
> = (props) => {
|
||||
const { ids, onSelect: onSelectValues } = props;
|
||||
|
||||
const [values, setValues] = useState<Gallery[]>([]);
|
||||
@@ -238,3 +239,11 @@ export const GalleryIDSelect = PatchComponent(
|
||||
"GalleryIDSelect",
|
||||
_GalleryIDSelect
|
||||
);
|
||||
|
||||
function getExcludeFilebaseGalleriesFilter() {
|
||||
const ret = new PathCriterion();
|
||||
ret.modifier = GQL.CriterionModifier.IsNull;
|
||||
return ret;
|
||||
}
|
||||
|
||||
export const excludeFileBasedGalleries = [getExcludeFilebaseGalleriesFilter()];
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as FormUtils from "src/utils/form";
|
||||
import { MultiSet } from "../Shared/MultiSet";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
import {
|
||||
getAggregateGalleryIds,
|
||||
getAggregateInputIDs,
|
||||
getAggregateInputValue,
|
||||
getAggregatePerformerIds,
|
||||
@@ -36,11 +37,19 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
|
||||
const [performerIds, setPerformerIds] = useState<string[]>();
|
||||
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
|
||||
|
||||
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
||||
GQL.BulkUpdateIdMode.Add
|
||||
);
|
||||
const [tagIds, setTagIds] = useState<string[]>();
|
||||
const [existingTagIds, setExistingTagIds] = useState<string[]>();
|
||||
|
||||
const [galleryMode, setGalleryMode] = React.useState<GQL.BulkUpdateIdMode>(
|
||||
GQL.BulkUpdateIdMode.Add
|
||||
);
|
||||
const [galleryIds, setGalleryIds] = useState<string[]>();
|
||||
const [existingGalleryIds, setExistingGalleryIds] = useState<string[]>();
|
||||
|
||||
const [organized, setOrganized] = useState<boolean | undefined>();
|
||||
|
||||
const [updateImages] = useBulkImageUpdate();
|
||||
@@ -56,6 +65,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
const aggregateStudioId = getAggregateStudioId(props.selected);
|
||||
const aggregatePerformerIds = getAggregatePerformerIds(props.selected);
|
||||
const aggregateTagIds = getAggregateTagIds(props.selected);
|
||||
const aggregateGalleryIds = getAggregateGalleryIds(props.selected);
|
||||
|
||||
const imageInput: GQL.BulkImageUpdateInput = {
|
||||
ids: props.selected.map((image) => {
|
||||
@@ -72,6 +82,11 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
aggregatePerformerIds
|
||||
);
|
||||
imageInput.tag_ids = getAggregateInputIDs(tagMode, tagIds, aggregateTagIds);
|
||||
imageInput.gallery_ids = getAggregateInputIDs(
|
||||
galleryMode,
|
||||
galleryIds,
|
||||
aggregateGalleryIds
|
||||
);
|
||||
|
||||
if (organized !== undefined) {
|
||||
imageInput.organized = organized;
|
||||
@@ -107,6 +122,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
let updateStudioID: string | undefined;
|
||||
let updatePerformerIds: string[] = [];
|
||||
let updateTagIds: string[] = [];
|
||||
let updateGalleryIds: string[] = [];
|
||||
let updateOrganized: boolean | undefined;
|
||||
let first = true;
|
||||
|
||||
@@ -117,12 +133,14 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
.map((p) => p.id)
|
||||
.sort();
|
||||
const imageTagIDs = (image.tags ?? []).map((p) => p.id).sort();
|
||||
const imageGalleryIDs = (image.galleries ?? []).map((p) => p.id).sort();
|
||||
|
||||
if (first) {
|
||||
updateRating = imageRating ?? undefined;
|
||||
updateStudioID = imageStudioID;
|
||||
updatePerformerIds = imagePerformerIDs;
|
||||
updateTagIds = imageTagIDs;
|
||||
updateGalleryIds = imageGalleryIDs;
|
||||
updateOrganized = image.organized;
|
||||
first = false;
|
||||
} else {
|
||||
@@ -138,6 +156,9 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
if (!isEqual(imageTagIDs, updateTagIds)) {
|
||||
updateTagIds = [];
|
||||
}
|
||||
if (!isEqual(imageGalleryIDs, updateGalleryIds)) {
|
||||
updateGalleryIds = [];
|
||||
}
|
||||
if (image.organized !== updateOrganized) {
|
||||
updateOrganized = undefined;
|
||||
}
|
||||
@@ -148,6 +169,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
setStudioId(updateStudioID);
|
||||
setExistingPerformerIds(updatePerformerIds);
|
||||
setExistingTagIds(updateTagIds);
|
||||
setExistingGalleryIds(updateGalleryIds);
|
||||
setOrganized(updateOrganized);
|
||||
}, [props.selected, performerMode, tagMode]);
|
||||
|
||||
@@ -157,54 +179,6 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
}
|
||||
}, [organized, checkboxRef]);
|
||||
|
||||
function renderMultiSelect(
|
||||
type: "performers" | "tags",
|
||||
ids: string[] | undefined
|
||||
) {
|
||||
let mode = GQL.BulkUpdateIdMode.Add;
|
||||
let existingIds: string[] | undefined = [];
|
||||
switch (type) {
|
||||
case "performers":
|
||||
mode = performerMode;
|
||||
existingIds = existingPerformerIds;
|
||||
break;
|
||||
case "tags":
|
||||
mode = tagMode;
|
||||
existingIds = existingTagIds;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<MultiSet
|
||||
type={type}
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => {
|
||||
switch (type) {
|
||||
case "performers":
|
||||
setPerformerIds(itemIDs);
|
||||
break;
|
||||
case "tags":
|
||||
setTagIds(itemIDs);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
onSetMode={(newMode) => {
|
||||
switch (type) {
|
||||
case "performers":
|
||||
setPerformerMode(newMode);
|
||||
break;
|
||||
case "tags":
|
||||
setTagMode(newMode);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
existingIds={existingIds ?? []}
|
||||
ids={ids ?? []}
|
||||
mode={mode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function cycleOrganized() {
|
||||
if (organized) {
|
||||
setOrganized(undefined);
|
||||
@@ -271,14 +245,45 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
<Form.Label>
|
||||
<FormattedMessage id="performers" />
|
||||
</Form.Label>
|
||||
{renderMultiSelect("performers", performerIds)}
|
||||
<MultiSet
|
||||
type="performers"
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => setPerformerIds(itemIDs)}
|
||||
onSetMode={(newMode) => setPerformerMode(newMode)}
|
||||
existingIds={existingPerformerIds ?? []}
|
||||
ids={performerIds ?? []}
|
||||
mode={performerMode}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="tags">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="tags" />
|
||||
</Form.Label>
|
||||
{renderMultiSelect("tags", tagIds)}
|
||||
<MultiSet
|
||||
type="tags"
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => setTagIds(itemIDs)}
|
||||
onSetMode={(newMode) => setTagMode(newMode)}
|
||||
existingIds={existingTagIds ?? []}
|
||||
ids={tagIds ?? []}
|
||||
mode={tagMode}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="galleries">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="galleries" />
|
||||
</Form.Label>
|
||||
<MultiSet
|
||||
type="galleries"
|
||||
disabled={isUpdating}
|
||||
onUpdate={(itemIDs) => setGalleryIds(itemIDs)}
|
||||
onSetMode={(newMode) => setGalleryMode(newMode)}
|
||||
existingIds={existingGalleryIds ?? []}
|
||||
ids={galleryIds ?? []}
|
||||
mode={galleryMode}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="organized">
|
||||
|
||||
@@ -24,8 +24,11 @@ import { formikUtils } from "src/utils/form";
|
||||
import { Tag, TagSelect } from "src/components/Tags/TagSelect";
|
||||
import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
|
||||
import { galleryTitle } from "src/core/galleries";
|
||||
import { Gallery, GallerySelect } from "src/components/Galleries/GallerySelect";
|
||||
import { PathCriterion } from "src/models/list-filter/criteria/path";
|
||||
import {
|
||||
Gallery,
|
||||
GallerySelect,
|
||||
excludeFileBasedGalleries,
|
||||
} from "src/components/Galleries/GallerySelect";
|
||||
|
||||
interface IProps {
|
||||
image: GQL.ImageDataFragment;
|
||||
@@ -34,14 +37,6 @@ interface IProps {
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
function getExcludeFilebaseGalleriesFilter() {
|
||||
const ret = new PathCriterion();
|
||||
ret.modifier = GQL.CriterionModifier.IsNull;
|
||||
return ret;
|
||||
}
|
||||
|
||||
const excludeFileBasedGalleries = [getExcludeFilebaseGalleriesFilter()];
|
||||
|
||||
export const ImageEditPanel: React.FC<IProps> = ({
|
||||
image,
|
||||
isVisible,
|
||||
|
||||
@@ -4,9 +4,13 @@ import { useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import { FilterSelect, SelectObject } from "./Select";
|
||||
import {
|
||||
GalleryIDSelect,
|
||||
excludeFileBasedGalleries,
|
||||
} from "../Galleries/GallerySelect";
|
||||
|
||||
interface IMultiSetProps {
|
||||
type: "performers" | "studios" | "tags" | "movies";
|
||||
type: "performers" | "studios" | "tags" | "movies" | "galleries";
|
||||
existingIds?: string[];
|
||||
ids?: string[];
|
||||
mode: GQL.BulkUpdateIdMode;
|
||||
@@ -15,6 +19,39 @@ interface IMultiSetProps {
|
||||
onSetMode: (mode: GQL.BulkUpdateIdMode) => void;
|
||||
}
|
||||
|
||||
const Select: React.FC<IMultiSetProps> = (props) => {
|
||||
const { type, disabled } = props;
|
||||
|
||||
function onUpdate(items: SelectObject[]) {
|
||||
props.onUpdate(items.map((i) => i.id));
|
||||
}
|
||||
|
||||
if (type === "galleries") {
|
||||
return (
|
||||
<GalleryIDSelect
|
||||
isDisabled={disabled}
|
||||
isMulti
|
||||
isClearable={false}
|
||||
onSelect={onUpdate}
|
||||
ids={props.ids ?? []}
|
||||
// exclude file-based galleries when setting galleries
|
||||
extraCriteria={excludeFileBasedGalleries}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FilterSelect
|
||||
type={type}
|
||||
isDisabled={disabled}
|
||||
isMulti
|
||||
isClearable={false}
|
||||
onSelect={onUpdate}
|
||||
ids={props.ids ?? []}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const MultiSet: React.FC<IMultiSetProps> = (props) => {
|
||||
const intl = useIntl();
|
||||
const modes = [
|
||||
@@ -23,10 +60,6 @@ export const MultiSet: React.FC<IMultiSetProps> = (props) => {
|
||||
GQL.BulkUpdateIdMode.Remove,
|
||||
];
|
||||
|
||||
function onUpdate(items: SelectObject[]) {
|
||||
props.onUpdate(items.map((i) => i.id));
|
||||
}
|
||||
|
||||
function getModeText(mode: GQL.BulkUpdateIdMode) {
|
||||
switch (mode) {
|
||||
case GQL.BulkUpdateIdMode.Set:
|
||||
@@ -83,14 +116,7 @@ export const MultiSet: React.FC<IMultiSetProps> = (props) => {
|
||||
<ButtonGroup className="button-group-above">
|
||||
{modes.map((m) => renderModeButton(m))}
|
||||
</ButtonGroup>
|
||||
<FilterSelect
|
||||
type={props.type}
|
||||
isDisabled={props.disabled}
|
||||
isMulti
|
||||
isClearable={false}
|
||||
onSelect={onUpdate}
|
||||
ids={props.ids ?? []}
|
||||
/>
|
||||
<Select {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -48,22 +48,16 @@ export function getAggregateStudioId(state: IHasStudio[]) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
interface IHasPerformers {
|
||||
performers: IHasID[];
|
||||
}
|
||||
|
||||
export function getAggregatePerformerIds(state: IHasPerformers[]) {
|
||||
export function getAggregateIds(sortedLists: string[][]) {
|
||||
let ret: string[] = [];
|
||||
let first = true;
|
||||
|
||||
state.forEach((o) => {
|
||||
sortedLists.forEach((l) => {
|
||||
if (first) {
|
||||
ret = o.performers ? o.performers.map((p) => p.id).sort() : [];
|
||||
ret = l;
|
||||
first = false;
|
||||
} else {
|
||||
const perfIds = o.performers ? o.performers.map((p) => p.id).sort() : [];
|
||||
|
||||
if (!isEqual(ret, perfIds)) {
|
||||
if (!isEqual(ret, l)) {
|
||||
ret = [];
|
||||
}
|
||||
}
|
||||
@@ -72,56 +66,30 @@ export function getAggregatePerformerIds(state: IHasPerformers[]) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
interface IHasTags {
|
||||
tags: IHasID[];
|
||||
export function getAggregateGalleryIds(state: { galleries: IHasID[] }[]) {
|
||||
const sortedLists = state.map((o) => o.galleries.map((oo) => oo.id).sort());
|
||||
return getAggregateIds(sortedLists);
|
||||
}
|
||||
|
||||
export function getAggregateTagIds(state: IHasTags[]) {
|
||||
let ret: string[] = [];
|
||||
let first = true;
|
||||
export function getAggregatePerformerIds(state: { performers: IHasID[] }[]) {
|
||||
const sortedLists = state.map((o) => o.performers.map((oo) => oo.id).sort());
|
||||
return getAggregateIds(sortedLists);
|
||||
}
|
||||
|
||||
state.forEach((o) => {
|
||||
if (first) {
|
||||
ret = o.tags ? o.tags.map((t) => t.id).sort() : [];
|
||||
first = false;
|
||||
} else {
|
||||
const tIds = o.tags ? o.tags.map((t) => t.id).sort() : [];
|
||||
|
||||
if (!isEqual(ret, tIds)) {
|
||||
ret = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return ret;
|
||||
export function getAggregateTagIds(state: { tags: IHasID[] }[]) {
|
||||
const sortedLists = state.map((o) => o.tags.map((oo) => oo.id).sort());
|
||||
return getAggregateIds(sortedLists);
|
||||
}
|
||||
|
||||
interface IMovie {
|
||||
movie: IHasID;
|
||||
}
|
||||
|
||||
interface IHasMovies {
|
||||
movies: IMovie[];
|
||||
}
|
||||
|
||||
export function getAggregateMovieIds(state: IHasMovies[]) {
|
||||
let ret: string[] = [];
|
||||
let first = true;
|
||||
|
||||
state.forEach((o) => {
|
||||
if (first) {
|
||||
ret = o.movies ? o.movies.map((m) => m.movie.id).sort() : [];
|
||||
first = false;
|
||||
} else {
|
||||
const mIds = o.movies ? o.movies.map((m) => m.movie.id).sort() : [];
|
||||
|
||||
if (!isEqual(ret, mIds)) {
|
||||
ret = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return ret;
|
||||
export function getAggregateMovieIds(state: { movies: IMovie[] }[]) {
|
||||
const sortedLists = state.map((o) =>
|
||||
o.movies.map((oo) => oo.movie.id).sort()
|
||||
);
|
||||
return getAggregateIds(sortedLists);
|
||||
}
|
||||
|
||||
export function makeBulkUpdateIds(
|
||||
|
||||
Reference in New Issue
Block a user