Feature Request: Add organized flag to studios (#6303)

This commit is contained in:
Gykes 2026-02-18 16:05:17 -06:00 committed by GitHub
parent 8bc4107e54
commit 3dc86239d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 118 additions and 5 deletions

View file

@ -502,6 +502,8 @@ input StudioFilterType {
child_count: IntCriterionInput child_count: IntCriterionInput
"Filter by autotag ignore value" "Filter by autotag ignore value"
ignore_auto_tag: Boolean ignore_auto_tag: Boolean
"Filter by organized"
organized: Boolean
"Filter by related scenes that meet this criteria" "Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType scenes_filter: SceneFilterType
"Filter by related images that meet this criteria" "Filter by related images that meet this criteria"

View file

@ -8,6 +8,7 @@ type Studio {
aliases: [String!]! aliases: [String!]!
tags: [Tag!]! tags: [Tag!]!
ignore_auto_tag: Boolean! ignore_auto_tag: Boolean!
organized: Boolean!
image_path: String # Resolver image_path: String # Resolver
scene_count(depth: Int): Int! # Resolver scene_count(depth: Int): Int! # Resolver
@ -46,6 +47,7 @@ input StudioCreateInput {
aliases: [String!] aliases: [String!]
tag_ids: [ID!] tag_ids: [ID!]
ignore_auto_tag: Boolean ignore_auto_tag: Boolean
organized: Boolean
custom_fields: Map custom_fields: Map
} }
@ -67,6 +69,7 @@ input StudioUpdateInput {
aliases: [String!] aliases: [String!]
tag_ids: [ID!] tag_ids: [ID!]
ignore_auto_tag: Boolean ignore_auto_tag: Boolean
organized: Boolean
custom_fields: CustomFieldsInput custom_fields: CustomFieldsInput
} }
@ -82,6 +85,7 @@ input BulkStudioUpdateInput {
details: String details: String
tag_ids: BulkUpdateIds tag_ids: BulkUpdateIds
ignore_auto_tag: Boolean ignore_auto_tag: Boolean
organized: Boolean
} }
input StudioDestroyInput { input StudioDestroyInput {

View file

@ -38,6 +38,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
newStudio.Favorite = translator.bool(input.Favorite) newStudio.Favorite = translator.bool(input.Favorite)
newStudio.Details = translator.string(input.Details) newStudio.Details = translator.string(input.Details)
newStudio.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag) 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.Aliases = models.NewRelatedStrings(stringslice.UniqueExcludeFold(stringslice.TrimSpace(input.Aliases), newStudio.Name))
newStudio.StashIDs = models.NewRelatedStashIDs(models.StashIDInputs(input.StashIds).ToStashIDs()) 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.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedStudio.Favorite = translator.optionalBool(input.Favorite, "favorite") updatedStudio.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedStudio.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag") updatedStudio.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
updatedStudio.Organized = translator.optionalBool(input.Organized, "organized")
updatedStudio.Aliases = translator.updateStrings(input.Aliases, "aliases") updatedStudio.Aliases = translator.updateStrings(input.Aliases, "aliases")
updatedStudio.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids") 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.Rating = translator.optionalInt(input.Rating100, "rating100")
partial.Details = translator.optionalString(input.Details, "details") partial.Details = translator.optionalString(input.Details, "details")
partial.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag") 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") partial.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids")
if err != nil { if err != nil {

View file

@ -275,6 +275,12 @@ func (t *stashBoxBatchStudioTagTask) getName() string {
} }
func (t *stashBoxBatchStudioTagTask) Start(ctx context.Context) { 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) studio, err := t.findStashBoxStudio(ctx)
if err != nil { if err != nil {
logger.Errorf("Error fetching studio data from stash-box: %v", err) logger.Errorf("Error fetching studio data from stash-box: %v", err)

View file

@ -24,6 +24,7 @@ type Studio struct {
StashIDs []models.StashID `json:"stash_ids,omitempty"` StashIDs []models.StashID `json:"stash_ids,omitempty"`
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags,omitempty"`
IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"` IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"`
Organized bool `json:"organized,omitempty"`
CustomFields map[string]interface{} `json:"custom_fields,omitempty"` CustomFields map[string]interface{} `json:"custom_fields,omitempty"`

View file

@ -16,6 +16,7 @@ type Studio struct {
Favorite bool `json:"favorite"` Favorite bool `json:"favorite"`
Details string `json:"details"` Details string `json:"details"`
IgnoreAutoTag bool `json:"ignore_auto_tag"` IgnoreAutoTag bool `json:"ignore_auto_tag"`
Organized bool `json:"organized"`
Aliases RelatedStrings `json:"aliases"` Aliases RelatedStrings `json:"aliases"`
URLs RelatedStrings `json:"urls"` URLs RelatedStrings `json:"urls"`
@ -62,6 +63,7 @@ type StudioPartial struct {
CreatedAt OptionalTime CreatedAt OptionalTime
UpdatedAt OptionalTime UpdatedAt OptionalTime
IgnoreAutoTag OptionalBool IgnoreAutoTag OptionalBool
Organized OptionalBool
Aliases *UpdateStrings Aliases *UpdateStrings
URLs *UpdateStrings URLs *UpdateStrings

View file

@ -38,6 +38,8 @@ type StudioFilterType struct {
ChildCount *IntCriterionInput `json:"child_count"` ChildCount *IntCriterionInput `json:"child_count"`
// Filter by autotag ignore value // Filter by autotag ignore value
IgnoreAutoTag *bool `json:"ignore_auto_tag"` IgnoreAutoTag *bool `json:"ignore_auto_tag"`
// Filter by organized
Organized *bool `json:"organized"`
// Filter by related scenes that meet this criteria // Filter by related scenes that meet this criteria
ScenesFilter *SceneFilterType `json:"scenes_filter"` ScenesFilter *SceneFilterType `json:"scenes_filter"`
// Filter by related images that meet this criteria // Filter by related images that meet this criteria
@ -69,6 +71,7 @@ type StudioCreateInput struct {
Aliases []string `json:"aliases"` Aliases []string `json:"aliases"`
TagIds []string `json:"tag_ids"` TagIds []string `json:"tag_ids"`
IgnoreAutoTag *bool `json:"ignore_auto_tag"` IgnoreAutoTag *bool `json:"ignore_auto_tag"`
Organized *bool `json:"organized"`
CustomFields map[string]interface{} `json:"custom_fields"` CustomFields map[string]interface{} `json:"custom_fields"`
} }
@ -88,6 +91,7 @@ type StudioUpdateInput struct {
Aliases []string `json:"aliases"` Aliases []string `json:"aliases"`
TagIds []string `json:"tag_ids"` TagIds []string `json:"tag_ids"`
IgnoreAutoTag *bool `json:"ignore_auto_tag"` IgnoreAutoTag *bool `json:"ignore_auto_tag"`
Organized *bool `json:"organized"`
CustomFields CustomFieldsInput `json:"custom_fields"` CustomFields CustomFieldsInput `json:"custom_fields"`
} }

View file

@ -34,7 +34,7 @@ const (
cacheSizeEnv = "STASH_SQLITE_CACHE_SIZE" cacheSizeEnv = "STASH_SQLITE_CACHE_SIZE"
) )
var appSchemaVersion uint = 79 var appSchemaVersion uint = 80
//go:embed migrations/*.sql //go:embed migrations/*.sql
var migrationsBox embed.FS var migrationsBox embed.FS

View file

@ -0,0 +1 @@
ALTER TABLE `studios` ADD COLUMN `organized` boolean not null default '0';

View file

@ -44,6 +44,7 @@ type studioRow struct {
Favorite bool `db:"favorite"` Favorite bool `db:"favorite"`
Details zero.String `db:"details"` Details zero.String `db:"details"`
IgnoreAutoTag bool `db:"ignore_auto_tag"` IgnoreAutoTag bool `db:"ignore_auto_tag"`
Organized bool `db:"organized"`
// not used in resolutions or updates // not used in resolutions or updates
ImageBlob zero.String `db:"image_blob"` ImageBlob zero.String `db:"image_blob"`
@ -59,6 +60,7 @@ func (r *studioRow) fromStudio(o models.Studio) {
r.Favorite = o.Favorite r.Favorite = o.Favorite
r.Details = zero.StringFrom(o.Details) r.Details = zero.StringFrom(o.Details)
r.IgnoreAutoTag = o.IgnoreAutoTag r.IgnoreAutoTag = o.IgnoreAutoTag
r.Organized = o.Organized
} }
func (r *studioRow) resolve() *models.Studio { func (r *studioRow) resolve() *models.Studio {
@ -72,6 +74,7 @@ func (r *studioRow) resolve() *models.Studio {
Favorite: r.Favorite, Favorite: r.Favorite,
Details: r.Details.String, Details: r.Details.String,
IgnoreAutoTag: r.IgnoreAutoTag, IgnoreAutoTag: r.IgnoreAutoTag,
Organized: r.Organized,
} }
return ret return ret
@ -90,6 +93,7 @@ func (r *studioRowRecord) fromPartial(o models.StudioPartial) {
r.setBool("favorite", o.Favorite) r.setBool("favorite", o.Favorite)
r.setNullString("details", o.Details) r.setNullString("details", o.Details)
r.setBool("ignore_auto_tag", o.IgnoreAutoTag) r.setBool("ignore_auto_tag", o.IgnoreAutoTag)
r.setBool("organized", o.Organized)
} }
type studioRepositoryType struct { type studioRepositoryType struct {

View file

@ -59,6 +59,7 @@ func (qb *studioFilterHandler) criterionHandler() criterionHandler {
intCriterionHandler(studioFilter.Rating100, studioTable+".rating", nil), intCriterionHandler(studioFilter.Rating100, studioTable+".rating", nil),
boolCriterionHandler(studioFilter.Favorite, studioTable+".favorite", nil), boolCriterionHandler(studioFilter.Favorite, studioTable+".favorite", nil),
boolCriterionHandler(studioFilter.IgnoreAutoTag, studioTable+".ignore_auto_tag", nil), boolCriterionHandler(studioFilter.IgnoreAutoTag, studioTable+".ignore_auto_tag", nil),
boolCriterionHandler(studioFilter.Organized, studioTable+".organized", nil),
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if studioFilter.StashID != nil { if studioFilter.StashID != nil {

View file

@ -81,6 +81,7 @@ func Test_StudioStore_Create(t *testing.T) {
rating = 3 rating = 3
aliases = []string{"alias1", "alias2"} aliases = []string{"alias1", "alias2"}
ignoreAutoTag = true ignoreAutoTag = true
organized = true
favorite = true favorite = true
endpoint1 = "endpoint1" endpoint1 = "endpoint1"
endpoint2 = "endpoint2" endpoint2 = "endpoint2"
@ -105,6 +106,7 @@ func Test_StudioStore_Create(t *testing.T) {
Rating: &rating, Rating: &rating,
Details: details, Details: details,
IgnoreAutoTag: ignoreAutoTag, IgnoreAutoTag: ignoreAutoTag,
Organized: organized,
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithStudio], tagIDs[tagIdx1WithDupName]}), TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithStudio], tagIDs[tagIdx1WithDupName]}),
Aliases: models.NewRelatedStrings(aliases), Aliases: models.NewRelatedStrings(aliases),
StashIDs: models.NewRelatedStashIDs([]models.StashID{ StashIDs: models.NewRelatedStashIDs([]models.StashID{
@ -206,6 +208,7 @@ func Test_StudioStore_Update(t *testing.T) {
rating = 3 rating = 3
aliases = []string{"aliasX", "aliasY"} aliases = []string{"aliasX", "aliasY"}
ignoreAutoTag = true ignoreAutoTag = true
organized = true
favorite = true favorite = true
endpoint1 = "endpoint1" endpoint1 = "endpoint1"
endpoint2 = "endpoint2" endpoint2 = "endpoint2"
@ -231,6 +234,7 @@ func Test_StudioStore_Update(t *testing.T) {
Rating: &rating, Rating: &rating,
Details: details, Details: details,
IgnoreAutoTag: ignoreAutoTag, IgnoreAutoTag: ignoreAutoTag,
Organized: organized,
Aliases: models.NewRelatedStrings(aliases), Aliases: models.NewRelatedStrings(aliases),
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithStudio]}), TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithStudio]}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{ StashIDs: models.NewRelatedStashIDs([]models.StashID{
@ -380,6 +384,7 @@ func Test_StudioStore_UpdatePartial(t *testing.T) {
aliases = []string{"aliasX", "aliasY"} aliases = []string{"aliasX", "aliasY"}
rating = 3 rating = 3
ignoreAutoTag = true ignoreAutoTag = true
organized = true
favorite = true favorite = true
endpoint1 = "endpoint1" endpoint1 = "endpoint1"
endpoint2 = "endpoint2" endpoint2 = "endpoint2"
@ -413,6 +418,7 @@ func Test_StudioStore_UpdatePartial(t *testing.T) {
Rating: models.NewOptionalInt(rating), Rating: models.NewOptionalInt(rating),
Details: models.NewOptionalString(details), Details: models.NewOptionalString(details),
IgnoreAutoTag: models.NewOptionalBool(ignoreAutoTag), IgnoreAutoTag: models.NewOptionalBool(ignoreAutoTag),
Organized: models.NewOptionalBool(organized),
TagIDs: &models.UpdateIDs{ TagIDs: &models.UpdateIDs{
IDs: []int{tagIDs[tagIdx1WithStudio], tagIDs[tagIdx1WithDupName]}, IDs: []int{tagIDs[tagIdx1WithStudio], tagIDs[tagIdx1WithDupName]},
Mode: models.RelationshipUpdateModeSet, Mode: models.RelationshipUpdateModeSet,
@ -444,6 +450,7 @@ func Test_StudioStore_UpdatePartial(t *testing.T) {
Rating: &rating, Rating: &rating,
Details: details, Details: details,
IgnoreAutoTag: ignoreAutoTag, IgnoreAutoTag: ignoreAutoTag,
Organized: organized,
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithStudio]}), TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithStudio]}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{ StashIDs: models.NewRelatedStashIDs([]models.StashID{
{ {

View file

@ -27,6 +27,7 @@ func ToJSON(ctx context.Context, reader FinderImageStashIDGetter, studio *models
Details: studio.Details, Details: studio.Details,
Favorite: studio.Favorite, Favorite: studio.Favorite,
IgnoreAutoTag: studio.IgnoreAutoTag, IgnoreAutoTag: studio.IgnoreAutoTag,
Organized: studio.Organized,
CreatedAt: json.JSONTime{Time: studio.CreatedAt}, CreatedAt: json.JSONTime{Time: studio.CreatedAt},
UpdatedAt: json.JSONTime{Time: studio.UpdatedAt}, UpdatedAt: json.JSONTime{Time: studio.UpdatedAt},
} }

View file

@ -32,6 +32,7 @@ var (
details = "details" details = "details"
parentStudioName = "parentStudio" parentStudioName = "parentStudio"
autoTagIgnored = true autoTagIgnored = true
studioOrganized = true
emptyCustomFields = make(map[string]interface{}) emptyCustomFields = make(map[string]interface{})
customFields = map[string]interface{}{ customFields = map[string]interface{}{
"customField1": "customValue1", "customField1": "customValue1",
@ -73,6 +74,7 @@ func createFullStudio(id int, parentID int) models.Studio {
UpdatedAt: updateTime, UpdatedAt: updateTime,
Rating: &rating, Rating: &rating,
IgnoreAutoTag: autoTagIgnored, IgnoreAutoTag: autoTagIgnored,
Organized: studioOrganized,
Aliases: models.NewRelatedStrings(aliases), Aliases: models.NewRelatedStrings(aliases),
TagIDs: models.NewRelatedIDs([]int{}), TagIDs: models.NewRelatedIDs([]int{}),
StashIDs: models.NewRelatedStashIDs(stashIDs), StashIDs: models.NewRelatedStashIDs(stashIDs),
@ -115,6 +117,7 @@ func createFullJSONStudio(parentStudio, image string, aliases []string, customFi
Aliases: aliases, Aliases: aliases,
StashIDs: stashIDs, StashIDs: stashIDs,
IgnoreAutoTag: autoTagIgnored, IgnoreAutoTag: autoTagIgnored,
Organized: studioOrganized,
CustomFields: customFields, CustomFields: customFields,
} }
} }

View file

@ -233,6 +233,7 @@ func studioJSONtoStudio(studioJSON jsonschema.Studio) models.Studio {
Details: studioJSON.Details, Details: studioJSON.Details,
Favorite: studioJSON.Favorite, Favorite: studioJSON.Favorite,
IgnoreAutoTag: studioJSON.IgnoreAutoTag, IgnoreAutoTag: studioJSON.IgnoreAutoTag,
Organized: studioJSON.Organized,
CreatedAt: studioJSON.CreatedAt.GetTime(), CreatedAt: studioJSON.CreatedAt.GetTime(),
UpdatedAt: studioJSON.UpdatedAt.GetTime(), UpdatedAt: studioJSON.UpdatedAt.GetTime(),

View file

@ -49,6 +49,7 @@ func TestImporterPreImport(t *testing.T) {
Name: studioName, Name: studioName,
Image: invalidImage, Image: invalidImage,
IgnoreAutoTag: autoTagIgnored, IgnoreAutoTag: autoTagIgnored,
Organized: studioOrganized,
}, },
} }

View file

@ -17,5 +17,8 @@ fragment SlimStudioData on Studio {
id id
name name
} }
favorite
ignore_auto_tag
organized
o_counter o_counter
} }

View file

@ -16,6 +16,7 @@ fragment StudioData on Studio {
image_path image_path
} }
ignore_auto_tag ignore_auto_tag
organized
image_path image_path
scene_count scene_count
scene_count_all: scene_count(depth: -1) scene_count_all: scene_count(depth: -1)

View file

@ -23,7 +23,13 @@ interface IListOperationProps {
onClose: (applied: boolean) => void; 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> = ( export const EditStudiosDialog: React.FC<IListOperationProps> = (
props: IListOperationProps props: IListOperationProps
@ -236,6 +242,14 @@ export const EditStudiosDialog: React.FC<IListOperationProps> = (
checked={updateInput.ignore_auto_tag ?? undefined} checked={updateInput.ignore_auto_tag ?? undefined}
/> />
</Form.Group> </Form.Group>
<Form.Group controlId="organized">
<IndeterminateCheckbox
label={intl.formatMessage({ id: "organized" })}
setChecked={(checked) => setUpdateField({ organized: checked })}
checked={updateInput.organized ?? undefined}
/>
</Form.Group>
</Form> </Form>
</ModalComponent> </ModalComponent>
); );

View file

@ -7,13 +7,13 @@ import { PatchComponent } from "src/patch";
import { HoverPopover } from "../Shared/HoverPopover"; import { HoverPopover } from "../Shared/HoverPopover";
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
import { TagLink } from "../Shared/TagLink"; 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 { FormattedMessage } from "react-intl";
import { PopoverCountButton } from "../Shared/PopoverCountButton"; import { PopoverCountButton } from "../Shared/PopoverCountButton";
import { RatingBanner } from "../Shared/RatingBanner"; import { RatingBanner } from "../Shared/RatingBanner";
import { FavoriteIcon } from "../Shared/FavoriteIcon"; import { FavoriteIcon } from "../Shared/FavoriteIcon";
import { useStudioUpdate } from "src/core/StashService"; 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"; import { OCounterButton } from "../Shared/CountButton";
interface IProps { interface IProps {
@ -185,6 +185,27 @@ export const StudioCard: React.FC<IProps> = PatchComponent(
return <OCounterButton value={studio.o_counter} />; 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() { function maybeRenderPopoverButtonGroup() {
if ( if (
studio.scene_count || studio.scene_count ||
@ -193,7 +214,8 @@ export const StudioCard: React.FC<IProps> = PatchComponent(
studio.group_count || studio.group_count ||
studio.performer_count || studio.performer_count ||
studio.o_counter || studio.o_counter ||
studio.tags.length > 0 studio.tags.length > 0 ||
studio.organized
) { ) {
return ( return (
<> <>
@ -206,6 +228,7 @@ export const StudioCard: React.FC<IProps> = PatchComponent(
{maybeRenderPerformersPopoverButton()} {maybeRenderPerformersPopoverButton()}
{maybeRenderTagPopoverButton()} {maybeRenderTagPopoverButton()}
{maybeRenderOCounter()} {maybeRenderOCounter()}
{maybeRenderOrganized()}
</ButtonGroup> </ButtonGroup>
</> </>
); );

View file

@ -49,6 +49,7 @@ import { AliasList } from "src/components/Shared/DetailsPage/AliasList";
import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage"; import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage";
import { goBackOrReplace } from "src/utils/history"; import { goBackOrReplace } from "src/utils/history";
import { OCounterButton } from "src/components/Shared/CountButton"; import { OCounterButton } from "src/components/Shared/CountButton";
import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton";
interface IProps { interface IProps {
studio: GQL.StudioDataFragment; 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 // set up hotkeys
useEffect(() => { useEffect(() => {
Mousetrap.bind("e", () => toggleEditing()); Mousetrap.bind("e", () => toggleEditing());
@ -467,6 +490,11 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
favorite={studio.favorite} favorite={studio.favorite}
onToggleFavorite={(v) => setFavorite(v)} onToggleFavorite={(v) => setFavorite(v)}
/> />
<OrganizedButton
loading={organizedLoading}
organized={studio.organized}
onClick={onOrganizedClick}
/>
<ExternalLinkButtons urls={studio.urls} /> <ExternalLinkButtons urls={studio.urls} />
</span> </span>
</DetailTitle> </DetailTitle>

View file

@ -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. 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 ### Ignore Auto tag flag
Performers or Tags that have Ignore Auto tag flag added to them will be skipped by the Auto tag task. Performers or Tags that have Ignore Auto tag flag added to them will be skipped by the Auto tag task.

View file

@ -53,6 +53,7 @@ const criterionOptions = [
TagsCriterionOption, TagsCriterionOption,
RatingCriterionOption, RatingCriterionOption,
createBooleanCriterionOption("ignore_auto_tag"), createBooleanCriterionOption("ignore_auto_tag"),
createBooleanCriterionOption("organized"),
createMandatoryNumberCriterionOption("tag_count"), createMandatoryNumberCriterionOption("tag_count"),
createMandatoryNumberCriterionOption("scene_count"), createMandatoryNumberCriterionOption("scene_count"),
createMandatoryNumberCriterionOption("image_count"), createMandatoryNumberCriterionOption("image_count"),