mirror of
https://github.com/stashapp/stash.git
synced 2026-02-25 16:54:44 +01:00
Feature Request: Add organized flag to studios (#6303)
This commit is contained in:
parent
8bc4107e54
commit
3dc86239d2
23 changed files with 118 additions and 5 deletions
|
|
@ -502,6 +502,8 @@ input StudioFilterType {
|
|||
child_count: IntCriterionInput
|
||||
"Filter by autotag ignore value"
|
||||
ignore_auto_tag: Boolean
|
||||
"Filter by organized"
|
||||
organized: Boolean
|
||||
"Filter by related scenes that meet this criteria"
|
||||
scenes_filter: SceneFilterType
|
||||
"Filter by related images that meet this criteria"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ type Studio {
|
|||
aliases: [String!]!
|
||||
tags: [Tag!]!
|
||||
ignore_auto_tag: Boolean!
|
||||
organized: Boolean!
|
||||
|
||||
image_path: String # Resolver
|
||||
scene_count(depth: Int): Int! # Resolver
|
||||
|
|
@ -46,6 +47,7 @@ input StudioCreateInput {
|
|||
aliases: [String!]
|
||||
tag_ids: [ID!]
|
||||
ignore_auto_tag: Boolean
|
||||
organized: Boolean
|
||||
|
||||
custom_fields: Map
|
||||
}
|
||||
|
|
@ -67,6 +69,7 @@ input StudioUpdateInput {
|
|||
aliases: [String!]
|
||||
tag_ids: [ID!]
|
||||
ignore_auto_tag: Boolean
|
||||
organized: Boolean
|
||||
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
|
@ -82,6 +85,7 @@ input BulkStudioUpdateInput {
|
|||
details: String
|
||||
tag_ids: BulkUpdateIds
|
||||
ignore_auto_tag: Boolean
|
||||
organized: Boolean
|
||||
}
|
||||
|
||||
input StudioDestroyInput {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
|||
newStudio.Favorite = translator.bool(input.Favorite)
|
||||
newStudio.Details = translator.string(input.Details)
|
||||
newStudio.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
|
||||
newStudio.Organized = translator.bool(input.Organized)
|
||||
newStudio.Aliases = models.NewRelatedStrings(stringslice.UniqueExcludeFold(stringslice.TrimSpace(input.Aliases), newStudio.Name))
|
||||
newStudio.StashIDs = models.NewRelatedStashIDs(models.StashIDInputs(input.StashIds).ToStashIDs())
|
||||
|
||||
|
|
@ -120,6 +121,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
|||
updatedStudio.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedStudio.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||
updatedStudio.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
|
||||
updatedStudio.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
updatedStudio.Aliases = translator.updateStrings(input.Aliases, "aliases")
|
||||
updatedStudio.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids")
|
||||
|
||||
|
|
@ -261,6 +263,7 @@ func (r *mutationResolver) BulkStudioUpdate(ctx context.Context, input BulkStudi
|
|||
partial.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
partial.Details = translator.optionalString(input.Details, "details")
|
||||
partial.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
|
||||
partial.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
partial.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids")
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -275,6 +275,12 @@ func (t *stashBoxBatchStudioTagTask) getName() string {
|
|||
}
|
||||
|
||||
func (t *stashBoxBatchStudioTagTask) Start(ctx context.Context) {
|
||||
// Skip organized studios
|
||||
if t.studio != nil && t.studio.Organized {
|
||||
logger.Infof("Skipping organized studio %s", t.studio.Name)
|
||||
return
|
||||
}
|
||||
|
||||
studio, err := t.findStashBoxStudio(ctx)
|
||||
if err != nil {
|
||||
logger.Errorf("Error fetching studio data from stash-box: %v", err)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ type Studio struct {
|
|||
StashIDs []models.StashID `json:"stash_ids,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"`
|
||||
Organized bool `json:"organized,omitempty"`
|
||||
|
||||
CustomFields map[string]interface{} `json:"custom_fields,omitempty"`
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ type Studio struct {
|
|||
Favorite bool `json:"favorite"`
|
||||
Details string `json:"details"`
|
||||
IgnoreAutoTag bool `json:"ignore_auto_tag"`
|
||||
Organized bool `json:"organized"`
|
||||
|
||||
Aliases RelatedStrings `json:"aliases"`
|
||||
URLs RelatedStrings `json:"urls"`
|
||||
|
|
@ -62,6 +63,7 @@ type StudioPartial struct {
|
|||
CreatedAt OptionalTime
|
||||
UpdatedAt OptionalTime
|
||||
IgnoreAutoTag OptionalBool
|
||||
Organized OptionalBool
|
||||
|
||||
Aliases *UpdateStrings
|
||||
URLs *UpdateStrings
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ type StudioFilterType struct {
|
|||
ChildCount *IntCriterionInput `json:"child_count"`
|
||||
// Filter by autotag ignore value
|
||||
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
|
||||
// Filter by organized
|
||||
Organized *bool `json:"organized"`
|
||||
// Filter by related scenes that meet this criteria
|
||||
ScenesFilter *SceneFilterType `json:"scenes_filter"`
|
||||
// Filter by related images that meet this criteria
|
||||
|
|
@ -69,6 +71,7 @@ type StudioCreateInput struct {
|
|||
Aliases []string `json:"aliases"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
|
||||
Organized *bool `json:"organized"`
|
||||
|
||||
CustomFields map[string]interface{} `json:"custom_fields"`
|
||||
}
|
||||
|
|
@ -88,6 +91,7 @@ type StudioUpdateInput struct {
|
|||
Aliases []string `json:"aliases"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
|
||||
Organized *bool `json:"organized"`
|
||||
|
||||
CustomFields CustomFieldsInput `json:"custom_fields"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const (
|
|||
cacheSizeEnv = "STASH_SQLITE_CACHE_SIZE"
|
||||
)
|
||||
|
||||
var appSchemaVersion uint = 79
|
||||
var appSchemaVersion uint = 80
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsBox embed.FS
|
||||
|
|
|
|||
1
pkg/sqlite/migrations/80_studio_organized.up.sql
Normal file
1
pkg/sqlite/migrations/80_studio_organized.up.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE `studios` ADD COLUMN `organized` boolean not null default '0';
|
||||
|
|
@ -44,6 +44,7 @@ type studioRow struct {
|
|||
Favorite bool `db:"favorite"`
|
||||
Details zero.String `db:"details"`
|
||||
IgnoreAutoTag bool `db:"ignore_auto_tag"`
|
||||
Organized bool `db:"organized"`
|
||||
|
||||
// not used in resolutions or updates
|
||||
ImageBlob zero.String `db:"image_blob"`
|
||||
|
|
@ -59,6 +60,7 @@ func (r *studioRow) fromStudio(o models.Studio) {
|
|||
r.Favorite = o.Favorite
|
||||
r.Details = zero.StringFrom(o.Details)
|
||||
r.IgnoreAutoTag = o.IgnoreAutoTag
|
||||
r.Organized = o.Organized
|
||||
}
|
||||
|
||||
func (r *studioRow) resolve() *models.Studio {
|
||||
|
|
@ -72,6 +74,7 @@ func (r *studioRow) resolve() *models.Studio {
|
|||
Favorite: r.Favorite,
|
||||
Details: r.Details.String,
|
||||
IgnoreAutoTag: r.IgnoreAutoTag,
|
||||
Organized: r.Organized,
|
||||
}
|
||||
|
||||
return ret
|
||||
|
|
@ -90,6 +93,7 @@ func (r *studioRowRecord) fromPartial(o models.StudioPartial) {
|
|||
r.setBool("favorite", o.Favorite)
|
||||
r.setNullString("details", o.Details)
|
||||
r.setBool("ignore_auto_tag", o.IgnoreAutoTag)
|
||||
r.setBool("organized", o.Organized)
|
||||
}
|
||||
|
||||
type studioRepositoryType struct {
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ func (qb *studioFilterHandler) criterionHandler() criterionHandler {
|
|||
intCriterionHandler(studioFilter.Rating100, studioTable+".rating", nil),
|
||||
boolCriterionHandler(studioFilter.Favorite, studioTable+".favorite", nil),
|
||||
boolCriterionHandler(studioFilter.IgnoreAutoTag, studioTable+".ignore_auto_tag", nil),
|
||||
boolCriterionHandler(studioFilter.Organized, studioTable+".organized", nil),
|
||||
|
||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if studioFilter.StashID != nil {
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ func Test_StudioStore_Create(t *testing.T) {
|
|||
rating = 3
|
||||
aliases = []string{"alias1", "alias2"}
|
||||
ignoreAutoTag = true
|
||||
organized = true
|
||||
favorite = true
|
||||
endpoint1 = "endpoint1"
|
||||
endpoint2 = "endpoint2"
|
||||
|
|
@ -105,6 +106,7 @@ func Test_StudioStore_Create(t *testing.T) {
|
|||
Rating: &rating,
|
||||
Details: details,
|
||||
IgnoreAutoTag: ignoreAutoTag,
|
||||
Organized: organized,
|
||||
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithStudio], tagIDs[tagIdx1WithDupName]}),
|
||||
Aliases: models.NewRelatedStrings(aliases),
|
||||
StashIDs: models.NewRelatedStashIDs([]models.StashID{
|
||||
|
|
@ -206,6 +208,7 @@ func Test_StudioStore_Update(t *testing.T) {
|
|||
rating = 3
|
||||
aliases = []string{"aliasX", "aliasY"}
|
||||
ignoreAutoTag = true
|
||||
organized = true
|
||||
favorite = true
|
||||
endpoint1 = "endpoint1"
|
||||
endpoint2 = "endpoint2"
|
||||
|
|
@ -231,6 +234,7 @@ func Test_StudioStore_Update(t *testing.T) {
|
|||
Rating: &rating,
|
||||
Details: details,
|
||||
IgnoreAutoTag: ignoreAutoTag,
|
||||
Organized: organized,
|
||||
Aliases: models.NewRelatedStrings(aliases),
|
||||
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithStudio]}),
|
||||
StashIDs: models.NewRelatedStashIDs([]models.StashID{
|
||||
|
|
@ -380,6 +384,7 @@ func Test_StudioStore_UpdatePartial(t *testing.T) {
|
|||
aliases = []string{"aliasX", "aliasY"}
|
||||
rating = 3
|
||||
ignoreAutoTag = true
|
||||
organized = true
|
||||
favorite = true
|
||||
endpoint1 = "endpoint1"
|
||||
endpoint2 = "endpoint2"
|
||||
|
|
@ -413,6 +418,7 @@ func Test_StudioStore_UpdatePartial(t *testing.T) {
|
|||
Rating: models.NewOptionalInt(rating),
|
||||
Details: models.NewOptionalString(details),
|
||||
IgnoreAutoTag: models.NewOptionalBool(ignoreAutoTag),
|
||||
Organized: models.NewOptionalBool(organized),
|
||||
TagIDs: &models.UpdateIDs{
|
||||
IDs: []int{tagIDs[tagIdx1WithStudio], tagIDs[tagIdx1WithDupName]},
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
|
|
@ -444,6 +450,7 @@ func Test_StudioStore_UpdatePartial(t *testing.T) {
|
|||
Rating: &rating,
|
||||
Details: details,
|
||||
IgnoreAutoTag: ignoreAutoTag,
|
||||
Organized: organized,
|
||||
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithStudio]}),
|
||||
StashIDs: models.NewRelatedStashIDs([]models.StashID{
|
||||
{
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ func ToJSON(ctx context.Context, reader FinderImageStashIDGetter, studio *models
|
|||
Details: studio.Details,
|
||||
Favorite: studio.Favorite,
|
||||
IgnoreAutoTag: studio.IgnoreAutoTag,
|
||||
Organized: studio.Organized,
|
||||
CreatedAt: json.JSONTime{Time: studio.CreatedAt},
|
||||
UpdatedAt: json.JSONTime{Time: studio.UpdatedAt},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ var (
|
|||
details = "details"
|
||||
parentStudioName = "parentStudio"
|
||||
autoTagIgnored = true
|
||||
studioOrganized = true
|
||||
emptyCustomFields = make(map[string]interface{})
|
||||
customFields = map[string]interface{}{
|
||||
"customField1": "customValue1",
|
||||
|
|
@ -73,6 +74,7 @@ func createFullStudio(id int, parentID int) models.Studio {
|
|||
UpdatedAt: updateTime,
|
||||
Rating: &rating,
|
||||
IgnoreAutoTag: autoTagIgnored,
|
||||
Organized: studioOrganized,
|
||||
Aliases: models.NewRelatedStrings(aliases),
|
||||
TagIDs: models.NewRelatedIDs([]int{}),
|
||||
StashIDs: models.NewRelatedStashIDs(stashIDs),
|
||||
|
|
@ -115,6 +117,7 @@ func createFullJSONStudio(parentStudio, image string, aliases []string, customFi
|
|||
Aliases: aliases,
|
||||
StashIDs: stashIDs,
|
||||
IgnoreAutoTag: autoTagIgnored,
|
||||
Organized: studioOrganized,
|
||||
CustomFields: customFields,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,6 +233,7 @@ func studioJSONtoStudio(studioJSON jsonschema.Studio) models.Studio {
|
|||
Details: studioJSON.Details,
|
||||
Favorite: studioJSON.Favorite,
|
||||
IgnoreAutoTag: studioJSON.IgnoreAutoTag,
|
||||
Organized: studioJSON.Organized,
|
||||
CreatedAt: studioJSON.CreatedAt.GetTime(),
|
||||
UpdatedAt: studioJSON.UpdatedAt.GetTime(),
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ func TestImporterPreImport(t *testing.T) {
|
|||
Name: studioName,
|
||||
Image: invalidImage,
|
||||
IgnoreAutoTag: autoTagIgnored,
|
||||
Organized: studioOrganized,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,5 +17,8 @@ fragment SlimStudioData on Studio {
|
|||
id
|
||||
name
|
||||
}
|
||||
favorite
|
||||
ignore_auto_tag
|
||||
organized
|
||||
o_counter
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ fragment StudioData on Studio {
|
|||
image_path
|
||||
}
|
||||
ignore_auto_tag
|
||||
organized
|
||||
image_path
|
||||
scene_count
|
||||
scene_count_all: scene_count(depth: -1)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,13 @@ interface IListOperationProps {
|
|||
onClose: (applied: boolean) => void;
|
||||
}
|
||||
|
||||
const studioFields = ["favorite", "rating100", "details", "ignore_auto_tag"];
|
||||
const studioFields = [
|
||||
"favorite",
|
||||
"rating100",
|
||||
"details",
|
||||
"ignore_auto_tag",
|
||||
"organized",
|
||||
];
|
||||
|
||||
export const EditStudiosDialog: React.FC<IListOperationProps> = (
|
||||
props: IListOperationProps
|
||||
|
|
@ -236,6 +242,14 @@ export const EditStudiosDialog: React.FC<IListOperationProps> = (
|
|||
checked={updateInput.ignore_auto_tag ?? undefined}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="organized">
|
||||
<IndeterminateCheckbox
|
||||
label={intl.formatMessage({ id: "organized" })}
|
||||
setChecked={(checked) => setUpdateField({ organized: checked })}
|
||||
checked={updateInput.organized ?? undefined}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</ModalComponent>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ import { PatchComponent } from "src/patch";
|
|||
import { HoverPopover } from "../Shared/HoverPopover";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { TagLink } from "../Shared/TagLink";
|
||||
import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import { Button, ButtonGroup, OverlayTrigger, Tooltip } from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { PopoverCountButton } from "../Shared/PopoverCountButton";
|
||||
import { RatingBanner } from "../Shared/RatingBanner";
|
||||
import { FavoriteIcon } from "../Shared/FavoriteIcon";
|
||||
import { useStudioUpdate } from "src/core/StashService";
|
||||
import { faTag } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faTag, faBox } from "@fortawesome/free-solid-svg-icons";
|
||||
import { OCounterButton } from "../Shared/CountButton";
|
||||
|
||||
interface IProps {
|
||||
|
|
@ -185,6 +185,27 @@ export const StudioCard: React.FC<IProps> = PatchComponent(
|
|||
return <OCounterButton value={studio.o_counter} />;
|
||||
}
|
||||
|
||||
function maybeRenderOrganized() {
|
||||
if (studio.organized) {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
<Tooltip id="organized-tooltip">
|
||||
<FormattedMessage id="organized" />
|
||||
</Tooltip>
|
||||
}
|
||||
placement="bottom"
|
||||
>
|
||||
<div className="organized">
|
||||
<Button className="minimal">
|
||||
<Icon icon={faBox} />
|
||||
</Button>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderPopoverButtonGroup() {
|
||||
if (
|
||||
studio.scene_count ||
|
||||
|
|
@ -193,7 +214,8 @@ export const StudioCard: React.FC<IProps> = PatchComponent(
|
|||
studio.group_count ||
|
||||
studio.performer_count ||
|
||||
studio.o_counter ||
|
||||
studio.tags.length > 0
|
||||
studio.tags.length > 0 ||
|
||||
studio.organized
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
|
|
@ -206,6 +228,7 @@ export const StudioCard: React.FC<IProps> = PatchComponent(
|
|||
{maybeRenderPerformersPopoverButton()}
|
||||
{maybeRenderTagPopoverButton()}
|
||||
{maybeRenderOCounter()}
|
||||
{maybeRenderOrganized()}
|
||||
</ButtonGroup>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import { AliasList } from "src/components/Shared/DetailsPage/AliasList";
|
|||
import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage";
|
||||
import { goBackOrReplace } from "src/utils/history";
|
||||
import { OCounterButton } from "src/components/Shared/CountButton";
|
||||
import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton";
|
||||
|
||||
interface IProps {
|
||||
studio: GQL.StudioDataFragment;
|
||||
|
|
@ -316,6 +317,28 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
|
|||
}
|
||||
}
|
||||
|
||||
const [organizedLoading, setOrganizedLoading] = useState(false);
|
||||
|
||||
async function onOrganizedClick() {
|
||||
if (!studio.id) return;
|
||||
|
||||
setOrganizedLoading(true);
|
||||
try {
|
||||
await updateStudio({
|
||||
variables: {
|
||||
input: {
|
||||
id: studio.id,
|
||||
organized: !studio.organized,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
setOrganizedLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("e", () => toggleEditing());
|
||||
|
|
@ -467,6 +490,11 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
|
|||
favorite={studio.favorite}
|
||||
onToggleFavorite={(v) => setFavorite(v)}
|
||||
/>
|
||||
<OrganizedButton
|
||||
loading={organizedLoading}
|
||||
organized={studio.organized}
|
||||
onClick={onOrganizedClick}
|
||||
/>
|
||||
<ExternalLinkButtons urls={studio.urls} />
|
||||
</span>
|
||||
</DetailTitle>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ This task is part of the advanced settings mode.
|
|||
|
||||
Scenes, images, and galleries that have the Organized flag added to them will not be modified by Auto tag. You can also use Organized flag status as a filter.
|
||||
|
||||
Studios also support the Organized flag, however it is purely informational. It serves as a front-end indicator for the user to mark that a studio's collection is complete and does not affect Auto tag behavior. The Ignore Auto tag flag should be used to exclude a studio from Auto tag.
|
||||
|
||||
### Ignore Auto tag flag
|
||||
|
||||
Performers or Tags that have Ignore Auto tag flag added to them will be skipped by the Auto tag task.
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ const criterionOptions = [
|
|||
TagsCriterionOption,
|
||||
RatingCriterionOption,
|
||||
createBooleanCriterionOption("ignore_auto_tag"),
|
||||
createBooleanCriterionOption("organized"),
|
||||
createMandatoryNumberCriterionOption("tag_count"),
|
||||
createMandatoryNumberCriterionOption("scene_count"),
|
||||
createMandatoryNumberCriterionOption("image_count"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue