mirror of
https://github.com/stashapp/stash.git
synced 2026-05-05 19:10:27 +02: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
|
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"
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
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"`
|
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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ func TestImporterPreImport(t *testing.T) {
|
||||||
Name: studioName,
|
Name: studioName,
|
||||||
Image: invalidImage,
|
Image: invalidImage,
|
||||||
IgnoreAutoTag: autoTagIgnored,
|
IgnoreAutoTag: autoTagIgnored,
|
||||||
|
Organized: studioOrganized,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,8 @@ fragment SlimStudioData on Studio {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
favorite
|
||||||
|
ignore_auto_tag
|
||||||
|
organized
|
||||||
o_counter
|
o_counter
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue