Organised flag (#988)

* Add organized boolean to scene model (#729)
* Add organized button to scene page
* Add flag to galleries and images
* Import/export changes
* Make organized flag not null
* Ignore organized scenes for autotag

Co-authored-by: com1234 <com1234@notarealemail.com>
This commit is contained in:
WithoutPants 2020-12-18 08:06:49 +11:00 committed by GitHub
parent 99bd7bc750
commit aadbcaeec2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 543 additions and 81 deletions

View File

@ -7,6 +7,7 @@ fragment GallerySlimData on Gallery {
url
details
rating
organized
image_count
cover {
...SlimImageData

View File

@ -7,6 +7,7 @@ fragment GalleryData on Gallery {
url
details
rating
organized
images {
...SlimImageData
}

View File

@ -3,6 +3,7 @@ fragment SlimImageData on Image {
checksum
title
rating
organized
o_counter
path

View File

@ -3,6 +3,7 @@ fragment ImageData on Image {
checksum
title
rating
organized
o_counter
path

View File

@ -8,6 +8,7 @@ fragment SlimSceneData on Scene {
date
rating
o_counter
organized
path
file {

View File

@ -8,6 +8,7 @@ fragment SceneData on Scene {
date
rating
o_counter
organized
path
file {

View File

@ -4,6 +4,7 @@ mutation GalleryCreate(
$url: String,
$date: String,
$rating: Int,
$organized: Boolean,
$scene_id: ID,
$studio_id: ID,
$performer_ids: [ID!] = [],

View File

@ -70,6 +70,8 @@ input SceneFilterType {
path: StringCriterionInput
"""Filter by rating"""
rating: IntCriterionInput
"""Filter by organized"""
organized: Boolean
"""Filter by o-counter"""
o_counter: IntCriterionInput
"""Filter by resolution"""
@ -117,6 +119,8 @@ input GalleryFilterType {
is_zip: Boolean
"""Filter by rating"""
rating: IntCriterionInput
"""Filter by organized"""
organized: Boolean
"""Filter by average image resolution"""
average_resolution: ResolutionEnum
"""Filter to only include scenes with this studio"""
@ -145,6 +149,8 @@ input ImageFilterType {
path: StringCriterionInput
"""Filter by rating"""
rating: IntCriterionInput
"""Filter by organized"""
organized: Boolean
"""Filter by o-counter"""
o_counter: IntCriterionInput
"""Filter by resolution"""

View File

@ -8,6 +8,7 @@ type Gallery {
date: String
details: String
rating: Int
organized: Boolean!
scene: Scene
studio: Studio
image_count: Int!
@ -31,6 +32,7 @@ input GalleryCreateInput {
date: String
details: String
rating: Int
organized: Boolean
scene_id: ID
studio_id: ID
tag_ids: [ID!]
@ -45,6 +47,7 @@ input GalleryUpdateInput {
date: String
details: String
rating: Int
organized: Boolean
scene_id: ID
studio_id: ID
tag_ids: [ID!]
@ -58,6 +61,7 @@ input BulkGalleryUpdateInput {
date: String
details: String
rating: Int
organized: Boolean
scene_id: ID
studio_id: ID
tag_ids: BulkUpdateIds

View File

@ -4,6 +4,7 @@ type Image {
title: String
rating: Int
o_counter: Int
organized: Boolean!
path: String!
file: ImageFileType! # Resolver
@ -31,6 +32,7 @@ input ImageUpdateInput {
id: ID!
title: String
rating: Int
organized: Boolean
studio_id: ID
performer_ids: [ID!]
@ -43,6 +45,7 @@ input BulkImageUpdateInput {
ids: [ID!]
title: String
rating: Int
organized: Boolean
studio_id: ID
performer_ids: BulkUpdateIds

View File

@ -32,6 +32,7 @@ type Scene {
url: String
date: String
rating: Int
organized: Boolean!
o_counter: Int
path: String!
@ -60,6 +61,7 @@ input SceneUpdateInput {
url: String
date: String
rating: Int
organized: Boolean
studio_id: ID
gallery_id: ID
performer_ids: [ID!]
@ -89,6 +91,7 @@ input BulkSceneUpdateInput {
url: String
date: String
rating: Int
organized: Boolean
studio_id: ID
gallery_id: ID
performer_ids: BulkUpdateIds

View File

@ -205,6 +205,7 @@ func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, transl
updatedGallery.Date = translator.sqliteDate(input.Date, "date")
updatedGallery.Rating = translator.nullInt64(input.Rating, "rating")
updatedGallery.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedGallery.Organized = input.Organized
// gallery scene is set from the scene only
@ -272,6 +273,7 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input models.B
updatedGallery.Rating = translator.nullInt64(input.Rating, "rating")
updatedGallery.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedGallery.SceneID = translator.nullInt64FromString(input.SceneID, "scene_id")
updatedGallery.Organized = input.Organized
ret := []*models.Gallery{}

View File

@ -77,6 +77,7 @@ func (r *mutationResolver) imageUpdate(input models.ImageUpdateInput, translator
updatedImage.Title = translator.nullString(input.Title, "title")
updatedImage.Rating = translator.nullInt64(input.Rating, "rating")
updatedImage.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedImage.Organized = input.Organized
qb := models.NewImageQueryBuilder()
jqb := models.NewJoinsQueryBuilder()
@ -142,6 +143,7 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input models.Bul
updatedImage.Title = translator.nullString(input.Title, "title")
updatedImage.Rating = translator.nullInt64(input.Rating, "rating")
updatedImage.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedImage.Organized = input.Organized
ret := []*models.Image{}

View File

@ -85,6 +85,7 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, translator
updatedScene.Date = translator.sqliteDate(input.Date, "date")
updatedScene.Rating = translator.nullInt64(input.Rating, "rating")
updatedScene.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedScene.Organized = input.Organized
if input.CoverImage != nil && *input.CoverImage != "" {
var err error
@ -242,6 +243,7 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
updatedScene.Date = translator.sqliteDate(input.Date, "date")
updatedScene.Rating = translator.nullInt64(input.Rating, "rating")
updatedScene.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedScene.Organized = input.Organized
ret := []*models.Scene{}

View File

@ -20,7 +20,7 @@ import (
var DB *sqlx.DB
var dbPath string
var appSchemaVersion uint = 15
var appSchemaVersion uint = 16
var databaseSchemaVersion uint
const sqlite3Driver = "sqlite3ex"

View File

@ -0,0 +1,3 @@
ALTER TABLE `scenes` ADD COLUMN `organized` boolean not null default '0';
ALTER TABLE `images` ADD COLUMN `organized` boolean not null default '0';
ALTER TABLE `galleries` ADD COLUMN `organized` boolean not null default '0';

View File

@ -40,6 +40,8 @@ func ToBasicJSON(gallery *models.Gallery) (*jsonschema.Gallery, error) {
newGalleryJSON.Rating = int(gallery.Rating.Int64)
}
newGalleryJSON.Organized = gallery.Organized
if gallery.Details.Valid {
newGalleryJSON.Details = gallery.Details.String
}

View File

@ -25,14 +25,15 @@ const (
)
const (
path = "path"
zip = true
url = "url"
checksum = "checksum"
title = "title"
date = "2001-01-01"
rating = 5
details = "details"
path = "path"
zip = true
url = "url"
checksum = "checksum"
title = "title"
date = "2001-01-01"
rating = 5
organized = true
details = "details"
)
const (
@ -58,9 +59,10 @@ func createFullGallery(id int) models.Gallery {
String: date,
Valid: true,
},
Details: modelstest.NullString(details),
Rating: modelstest.NullInt64(rating),
URL: modelstest.NullString(url),
Details: modelstest.NullString(details),
Rating: modelstest.NullInt64(rating),
Organized: organized,
URL: modelstest.NullString(url),
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
@ -84,14 +86,15 @@ func createEmptyGallery(id int) models.Gallery {
func createFullJSONGallery() *jsonschema.Gallery {
return &jsonschema.Gallery{
Title: title,
Path: path,
Zip: zip,
Checksum: checksum,
Date: date,
Details: details,
Rating: rating,
URL: url,
Title: title,
Path: path,
Zip: zip,
Checksum: checksum,
Date: date,
Details: details,
Rating: rating,
Organized: organized,
URL: url,
CreatedAt: models.JSONTime{
Time: createTime,
},

View File

@ -68,6 +68,7 @@ func (i *Importer) galleryJSONToGallery(galleryJSON jsonschema.Gallery) models.G
newGallery.Rating = sql.NullInt64{Int64: int64(galleryJSON.Rating), Valid: true}
}
newGallery.Organized = galleryJSON.Organized
newGallery.CreatedAt = models.SQLiteTimestamp{Timestamp: galleryJSON.CreatedAt.GetTime()}
newGallery.UpdatedAt = models.SQLiteTimestamp{Timestamp: galleryJSON.UpdatedAt.GetTime()}

View File

@ -56,13 +56,14 @@ func TestImporterName(t *testing.T) {
func TestImporterPreImport(t *testing.T) {
i := Importer{
Input: jsonschema.Gallery{
Path: path,
Checksum: checksum,
Title: title,
Date: date,
Details: details,
Rating: rating,
URL: url,
Path: path,
Checksum: checksum,
Title: title,
Date: date,
Details: details,
Rating: rating,
Organized: organized,
URL: url,
CreatedAt: models.JSONTime{
Time: createdAt,
},
@ -83,9 +84,10 @@ func TestImporterPreImport(t *testing.T) {
String: date,
Valid: true,
},
Details: modelstest.NullString(details),
Rating: modelstest.NullInt64(rating),
URL: modelstest.NullString(url),
Details: modelstest.NullString(details),
Rating: modelstest.NullInt64(rating),
Organized: organized,
URL: modelstest.NullString(url),
CreatedAt: models.SQLiteTimestamp{
Timestamp: createdAt,
},

View File

@ -23,6 +23,7 @@ func ToBasicJSON(image *models.Image) *jsonschema.Image {
newImageJSON.Rating = int(image.Rating.Int64)
}
newImageJSON.Organized = image.Organized
newImageJSON.OCounter = image.OCounter
newImageJSON.File = getImageFileJSON(image)

View File

@ -39,13 +39,14 @@ const (
)
const (
checksum = "checksum"
title = "title"
rating = 5
ocounter = 2
size = 123
width = 100
height = 100
checksum = "checksum"
title = "title"
rating = 5
organized = true
ocounter = 2
size = 123
width = 100
height = 100
)
const (
@ -63,14 +64,15 @@ var updateTime time.Time = time.Date(2002, 01, 01, 0, 0, 0, 0, time.UTC)
func createFullImage(id int) models.Image {
return models.Image{
ID: id,
Title: modelstest.NullString(title),
Checksum: checksum,
Height: modelstest.NullInt64(height),
OCounter: ocounter,
Rating: modelstest.NullInt64(rating),
Size: modelstest.NullInt64(int64(size)),
Width: modelstest.NullInt64(width),
ID: id,
Title: modelstest.NullString(title),
Checksum: checksum,
Height: modelstest.NullInt64(height),
OCounter: ocounter,
Rating: modelstest.NullInt64(rating),
Organized: organized,
Size: modelstest.NullInt64(int64(size)),
Width: modelstest.NullInt64(width),
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
@ -94,10 +96,11 @@ func createEmptyImage(id int) models.Image {
func createFullJSONImage() *jsonschema.Image {
return &jsonschema.Image{
Title: title,
Checksum: checksum,
OCounter: ocounter,
Rating: rating,
Title: title,
Checksum: checksum,
OCounter: ocounter,
Rating: rating,
Organized: organized,
File: &jsonschema.ImageFile{
Height: height,
Size: size,

View File

@ -63,6 +63,7 @@ func (i *Importer) imageJSONToImage(imageJSON jsonschema.Image) models.Image {
newImage.Rating = sql.NullInt64{Int64: int64(imageJSON.Rating), Valid: true}
}
newImage.Organized = imageJSON.Organized
newImage.OCounter = imageJSON.OCounter
newImage.CreatedAt = models.SQLiteTimestamp{Timestamp: imageJSON.CreatedAt.GetTime()}
newImage.UpdatedAt = models.SQLiteTimestamp{Timestamp: imageJSON.UpdatedAt.GetTime()}

View File

@ -17,6 +17,7 @@ type Gallery struct {
Date string `json:"date,omitempty"`
Details string `json:"details,omitempty"`
Rating int `json:"rating,omitempty"`
Organized bool `json:"organized,omitempty"`
Studio string `json:"studio,omitempty"`
Performers []string `json:"performers,omitempty"`
Tags []string `json:"tags,omitempty"`

View File

@ -20,6 +20,7 @@ type Image struct {
Checksum string `json:"checksum,omitempty"`
Studio string `json:"studio,omitempty"`
Rating int `json:"rating,omitempty"`
Organized bool `json:"organized,omitempty"`
OCounter int `json:"o_counter,omitempty"`
Galleries []string `json:"galleries,omitempty"`
Performers []string `json:"performers,omitempty"`

View File

@ -43,6 +43,7 @@ type Scene struct {
URL string `json:"url,omitempty"`
Date string `json:"date,omitempty"`
Rating int `json:"rating,omitempty"`
Organized bool `json:"organized,omitempty"`
OCounter int `json:"o_counter,omitempty"`
Details string `json:"details,omitempty"`
Gallery string `json:"gallery,omitempty"`

View File

@ -37,7 +37,8 @@ func (t *AutoTagPerformerTask) autoTagPerformer() {
regex := getQueryRegex(t.performer.Name.String)
scenes, err := qb.QueryAllByPathRegex(regex)
const ignoreOrganized = true
scenes, err := qb.QueryAllByPathRegex(regex, ignoreOrganized)
if err != nil {
logger.Infof("Error querying scenes with regex '%s': %s", regex, err.Error())
@ -82,7 +83,8 @@ func (t *AutoTagStudioTask) autoTagStudio() {
regex := getQueryRegex(t.studio.Name.String)
scenes, err := qb.QueryAllByPathRegex(regex)
const ignoreOrganized = true
scenes, err := qb.QueryAllByPathRegex(regex, ignoreOrganized)
if err != nil {
logger.Infof("Error querying scenes with regex '%s': %s", regex, err.Error())
@ -139,7 +141,8 @@ func (t *AutoTagTagTask) autoTagTag() {
regex := getQueryRegex(t.tag.Name)
scenes, err := qb.QueryAllByPathRegex(regex)
const ignoreOrganized = true
scenes, err := qb.QueryAllByPathRegex(regex, ignoreOrganized)
if err != nil {
logger.Infof("Error querying scenes with regex '%s': %s", regex, err.Error())

View File

@ -187,6 +187,16 @@ func createScenes(tx *sqlx.Tx) error {
}
}
// add organized scenes
for _, fn := range scenePatterns {
s := makeScene("organized"+fn, false)
s.Organized = true
err := createScene(sqb, tx, s)
if err != nil {
return err
}
}
// create scene with existing studio io
studioScene := makeScene(existingStudioSceneName, true)
studioScene.StudioID = sql.NullInt64{Valid: true, Int64: int64(existingStudioID)}

View File

@ -14,6 +14,7 @@ type Gallery struct {
Date SQLiteDate `db:"date" json:"date"`
Details sql.NullString `db:"details" json:"details"`
Rating sql.NullInt64 `db:"rating" json:"rating"`
Organized bool `db:"organized" json:"organized"`
StudioID sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"`
SceneID sql.NullInt64 `db:"scene_id,omitempty" json:"scene_id"`
FileModTime NullSQLiteTimestamp `db:"file_mod_time" json:"file_mod_time"`
@ -32,6 +33,7 @@ type GalleryPartial struct {
Date *SQLiteDate `db:"date" json:"date"`
Details *sql.NullString `db:"details" json:"details"`
Rating *sql.NullInt64 `db:"rating" json:"rating"`
Organized *bool `db:"organized" json:"organized"`
StudioID *sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"`
SceneID *sql.NullInt64 `db:"scene_id,omitempty" json:"scene_id"`
FileModTime *NullSQLiteTimestamp `db:"file_mod_time" json:"file_mod_time"`

View File

@ -11,6 +11,7 @@ type Image struct {
Path string `db:"path" json:"path"`
Title sql.NullString `db:"title" json:"title"`
Rating sql.NullInt64 `db:"rating" json:"rating"`
Organized bool `db:"organized" json:"organized"`
OCounter int `db:"o_counter" json:"o_counter"`
Size sql.NullInt64 `db:"size" json:"size"`
Width sql.NullInt64 `db:"width" json:"width"`
@ -29,6 +30,7 @@ type ImagePartial struct {
Path *string `db:"path" json:"path"`
Title *sql.NullString `db:"title" json:"title"`
Rating *sql.NullInt64 `db:"rating" json:"rating"`
Organized *bool `db:"organized" json:"organized"`
Size *sql.NullInt64 `db:"size" json:"size"`
Width *sql.NullInt64 `db:"width" json:"width"`
Height *sql.NullInt64 `db:"height" json:"height"`

View File

@ -16,6 +16,7 @@ type Scene struct {
URL sql.NullString `db:"url" json:"url"`
Date SQLiteDate `db:"date" json:"date"`
Rating sql.NullInt64 `db:"rating" json:"rating"`
Organized bool `db:"organized" json:"organized"`
OCounter int `db:"o_counter" json:"o_counter"`
Size sql.NullString `db:"size" json:"size"`
Duration sql.NullFloat64 `db:"duration" json:"duration"`
@ -44,6 +45,7 @@ type ScenePartial struct {
URL *sql.NullString `db:"url" json:"url"`
Date *SQLiteDate `db:"date" json:"date"`
Rating *sql.NullInt64 `db:"rating" json:"rating"`
Organized *bool `db:"organized" json:"organized"`
Size *sql.NullString `db:"size" json:"size"`
Duration *sql.NullFloat64 `db:"duration" json:"duration"`
VideoCodec *sql.NullString `db:"video_codec" json:"video_codec"`

View File

@ -15,3 +15,10 @@ func NullInt64(v int64) sql.NullInt64 {
Valid: true,
}
}
func NullBool(v bool) sql.NullBool {
return sql.NullBool{
Bool: v,
Valid: true,
}
}

View File

@ -21,8 +21,8 @@ func NewGalleryQueryBuilder() GalleryQueryBuilder {
func (qb *GalleryQueryBuilder) Create(newGallery Gallery, tx *sqlx.Tx) (*Gallery, error) {
ensureTx(tx)
result, err := tx.NamedExec(
`INSERT INTO galleries (path, checksum, zip, title, date, details, url, studio_id, rating, scene_id, file_mod_time, created_at, updated_at)
VALUES (:path, :checksum, :zip, :title, :date, :details, :url, :studio_id, :rating, :scene_id, :file_mod_time, :created_at, :updated_at)
`INSERT INTO galleries (path, checksum, zip, title, date, details, url, studio_id, rating, organized, scene_id, file_mod_time, created_at, updated_at)
VALUES (:path, :checksum, :zip, :title, :date, :details, :url, :studio_id, :rating, :organized, :scene_id, :file_mod_time, :created_at, :updated_at)
`,
newGallery,
)
@ -232,6 +232,16 @@ func (qb *GalleryQueryBuilder) Query(galleryFilter *GalleryFilterType, findFilte
query.handleIntCriterionInput(galleryFilter.Rating, "galleries.rating")
qb.handleAverageResolutionFilter(&query, galleryFilter.AverageResolution)
if Organized := galleryFilter.Organized; Organized != nil {
var organized string
if *Organized == true {
organized = "1"
} else {
organized = "0"
}
query.addWhere("galleries.organized = " + organized)
}
if isMissingFilter := galleryFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
switch *isMissingFilter {
case "scene":

View File

@ -61,9 +61,9 @@ func NewImageQueryBuilder() ImageQueryBuilder {
func (qb *ImageQueryBuilder) Create(newImage Image, tx *sqlx.Tx) (*Image, error) {
ensureTx(tx)
result, err := tx.NamedExec(
`INSERT INTO images (checksum, path, title, rating, o_counter, size,
`INSERT INTO images (checksum, path, title, rating, organized, o_counter, size,
width, height, studio_id, file_mod_time, created_at, updated_at)
VALUES (:checksum, :path, :title, :rating, :o_counter, :size,
VALUES (:checksum, :path, :title, :rating, :organized, :o_counter, :size,
:width, :height, :studio_id, :file_mod_time, :created_at, :updated_at)
`,
newImage,
@ -309,6 +309,16 @@ func (qb *ImageQueryBuilder) Query(imageFilter *ImageFilterType, findFilter *Fin
}
}
if Organized := imageFilter.Organized; Organized != nil {
var organized string
if *Organized == true {
organized = "1"
} else {
organized = "0"
}
query.addWhere("images.organized = " + organized)
}
if resolutionFilter := imageFilter.Resolution; resolutionFilter != nil {
if resolution := resolutionFilter.String(); resolutionFilter.IsValid() {
switch resolution {

View File

@ -59,9 +59,9 @@ func NewSceneQueryBuilder() SceneQueryBuilder {
func (qb *SceneQueryBuilder) Create(newScene Scene, tx *sqlx.Tx) (*Scene, error) {
ensureTx(tx)
result, err := tx.NamedExec(
`INSERT INTO scenes (oshash, checksum, path, title, details, url, date, rating, o_counter, size, duration, video_codec,
`INSERT INTO scenes (oshash, checksum, path, title, details, url, date, rating, organized, o_counter, size, duration, video_codec,
audio_codec, format, width, height, framerate, bitrate, studio_id, file_mod_time, created_at, updated_at)
VALUES (:oshash, :checksum, :path, :title, :details, :url, :date, :rating, :o_counter, :size, :duration, :video_codec,
VALUES (:oshash, :checksum, :path, :title, :details, :url, :date, :rating, :organized, :o_counter, :size, :duration, :video_codec,
:audio_codec, :format, :width, :height, :framerate, :bitrate, :studio_id, :file_mod_time, :created_at, :updated_at)
`,
newScene,
@ -325,6 +325,16 @@ func (qb *SceneQueryBuilder) Query(sceneFilter *SceneFilterType, findFilter *Fin
query.handleIntCriterionInput(sceneFilter.Rating, "scenes.rating")
query.handleIntCriterionInput(sceneFilter.OCounter, "scenes.o_counter")
if Organized := sceneFilter.Organized; Organized != nil {
var organized string
if *Organized == true {
organized = "1"
} else {
organized = "0"
}
query.addWhere("scenes.organized = " + organized)
}
if durationFilter := sceneFilter.Duration; durationFilter != nil {
clause, thisArgs := getDurationWhereClause(*durationFilter)
query.addWhere(clause)
@ -473,10 +483,14 @@ func getDurationWhereClause(durationFilter IntCriterionInput) (string, []interfa
return clause, args
}
func (qb *SceneQueryBuilder) QueryAllByPathRegex(regex string) ([]*Scene, error) {
func (qb *SceneQueryBuilder) QueryAllByPathRegex(regex string, ignoreOrganized bool) ([]*Scene, error) {
var args []interface{}
body := selectDistinctIDs("scenes") + " WHERE scenes.path regexp ?"
if ignoreOrganized {
body += " AND scenes.organized = 0"
}
args = append(args, "(?i)"+regex)
idsResult, err := runIdsQuery(body, args)

View File

@ -43,6 +43,7 @@ func ToBasicJSON(reader models.SceneReader, scene *models.Scene) (*jsonschema.Sc
newSceneJSON.Rating = int(scene.Rating.Int64)
}
newSceneJSON.Organized = scene.Organized
newSceneJSON.OCounter = scene.OCounter
if scene.Details.Valid {

View File

@ -47,6 +47,7 @@ const (
date = "2001-01-01"
rating = 5
ocounter = 2
organized = true
details = "details"
size = "size"
duration = 1.23
@ -113,6 +114,7 @@ func createFullScene(id int) models.Scene {
OCounter: ocounter,
OSHash: modelstest.NullString(oshash),
Rating: modelstest.NullInt64(rating),
Organized: organized,
Size: modelstest.NullString(size),
VideoCodec: modelstest.NullString(videoCodec),
Width: modelstest.NullInt64(width),
@ -140,14 +142,15 @@ func createEmptyScene(id int) models.Scene {
func createFullJSONScene(image string) *jsonschema.Scene {
return &jsonschema.Scene{
Title: title,
Checksum: checksum,
Date: date,
Details: details,
OCounter: ocounter,
OSHash: oshash,
Rating: rating,
URL: url,
Title: title,
Checksum: checksum,
Date: date,
Details: details,
OCounter: ocounter,
OSHash: oshash,
Rating: rating,
Organized: organized,
URL: url,
File: &jsonschema.SceneFile{
AudioCodec: audioCodec,
Bitrate: bitrate,

View File

@ -90,6 +90,7 @@ func (i *Importer) sceneJSONToScene(sceneJSON jsonschema.Scene) models.Scene {
newScene.Rating = sql.NullInt64{Int64: int64(sceneJSON.Rating), Valid: true}
}
newScene.Organized = sceneJSON.Organized
newScene.OCounter = sceneJSON.OCounter
newScene.CreatedAt = models.SQLiteTimestamp{Timestamp: sceneJSON.CreatedAt.GetTime()}
newScene.UpdatedAt = models.SQLiteTimestamp{Timestamp: sceneJSON.UpdatedAt.GetTime()}

View File

@ -1,4 +1,5 @@
### ✨ New Features
* Add organized flag for scenes, galleries and images.
* Allow configuration of visible navbar items.
### 🎨 Improvements

View File

@ -28,12 +28,15 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
GQL.BulkUpdateIdMode.Add
);
const [tagIds, setTagIds] = useState<string[]>();
const [organized, setOrganized] = useState<boolean | undefined>();
const [updateGalleries] = useBulkGalleryUpdate();
// Network state
const [isUpdating, setIsUpdating] = useState(false);
const checkboxRef = React.createRef<HTMLInputElement>();
function makeBulkUpdateIds(
ids: string[],
mode: GQL.BulkUpdateIdMode
@ -119,6 +122,10 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
galleryInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
}
if (organized !== undefined) {
galleryInput.organized = organized;
}
return galleryInput;
}
@ -223,6 +230,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
let updateStudioID: string | undefined;
let updatePerformerIds: string[] = [];
let updateTagIds: string[] = [];
let updateOrganized: boolean | undefined;
let first = true;
state.forEach((gallery: GQL.GallerySlimDataFragment) => {
@ -238,6 +246,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
updateStudioID = GalleriestudioID;
updatePerformerIds = galleryPerformerIDs;
updateTagIds = galleryTagIDs;
updateOrganized = gallery.organized;
first = false;
} else {
if (galleryRating !== updateRating) {
@ -252,6 +261,9 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
if (!_.isEqual(galleryTagIDs, updateTagIds)) {
updateTagIds = [];
}
if (gallery.organized !== updateOrganized) {
updateOrganized = undefined;
}
}
});
@ -264,8 +276,16 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
if (tagMode === GQL.BulkUpdateIdMode.Set) {
setTagIds(updateTagIds);
}
setOrganized(updateOrganized);
}, [props.selected, performerMode, tagMode]);
useEffect(() => {
if (checkboxRef.current) {
checkboxRef.current.indeterminate = organized === undefined;
}
}, [organized, checkboxRef]);
function renderMultiSelect(
type: "performers" | "tags",
ids: string[] | undefined
@ -311,6 +331,16 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
);
}
function cycleOrganized() {
if (organized) {
setOrganized(undefined);
} else if (organized === undefined) {
setOrganized(false);
} else {
setOrganized(true);
}
}
function render() {
return (
<Modal
@ -359,10 +389,20 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
{renderMultiSelect("performers", performerIds)}
</Form.Group>
<Form.Group controlId="performers">
<Form.Group controlId="tags">
<Form.Label>Tags</Form.Label>
{renderMultiSelect("tags", tagIds)}
</Form.Group>
<Form.Group controlId="organized">
<Form.Check
type="checkbox"
label="Organized"
checked={organized}
ref={checkboxRef}
onChange={() => cycleOrganized()}
/>
</Form.Group>
</Form>
</Modal>
);

View File

@ -104,11 +104,24 @@ export const GalleryCard: React.FC<IProps> = (props) => {
);
}
function maybeRenderOrganized() {
if (props.gallery.organized) {
return (
<div>
<Button className="minimal">
<Icon icon="box" />
</Button>
</div>
);
}
}
function maybeRenderPopoverButtonGroup() {
if (
props.gallery.scene ||
props.gallery.performers.length > 0 ||
props.gallery.tags.length > 0
props.gallery.tags.length > 0 ||
props.gallery.organized
) {
return (
<>
@ -117,6 +130,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
{maybeRenderTagPopoverButton()}
{maybeRenderPerformerPopoverButton()}
{maybeRenderScenePopoverButton()}
{maybeRenderOrganized()}
</ButtonGroup>
</>
);

View File

@ -1,10 +1,12 @@
import { Tab, Nav, Dropdown } from "react-bootstrap";
import React, { useEffect, useState } from "react";
import { useParams, useHistory, Link } from "react-router-dom";
import { useFindGallery } from "src/core/StashService";
import { useFindGallery, useGalleryUpdate } from "src/core/StashService";
import { ErrorMessage, LoadingIndicator, Icon } from "src/components/Shared";
import { TextUtils } from "src/utils";
import * as Mousetrap from "mousetrap";
import { useToast } from "src/hooks";
import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton";
import { GalleryEditPanel } from "./GalleryEditPanel";
import { GalleryDetailPanel } from "./GalleryDetailPanel";
import { DeleteGalleriesDialog } from "../DeleteGalleriesDialog";
@ -20,6 +22,7 @@ interface IGalleryParams {
export const Gallery: React.FC = () => {
const { tab = "images", id = "new" } = useParams<IGalleryParams>();
const history = useHistory();
const Toast = useToast();
const isNew = id === "new";
const { data, error, loading } = useFindGallery(id);
@ -34,6 +37,28 @@ export const Gallery: React.FC = () => {
}
};
const [updateGallery] = useGalleryUpdate();
const [organizedLoading, setOrganizedLoading] = useState(false);
const onOrganizedClick = async () => {
try {
setOrganizedLoading(true);
await updateGallery({
variables: {
input: {
id: gallery?.id ?? "",
organized: !gallery?.organized,
},
},
});
} catch (e) {
Toast.error(e);
} finally {
setOrganizedLoading(false);
}
};
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
function onDeleteDialogClosed(deleted: boolean) {
@ -103,7 +128,14 @@ export const Gallery: React.FC = () => {
<Nav.Item>
<Nav.Link eventKey="gallery-edit-panel">Edit</Nav.Link>
</Nav.Item>
<Nav.Item className="ml-auto">{renderOperations()}</Nav.Item>
<Nav.Item className="ml-auto">
<OrganizedButton
loading={organizedLoading}
organized={gallery.organized}
onClick={onOrganizedClick}
/>
</Nav.Item>
<Nav.Item>{renderOperations()}</Nav.Item>
</Nav>
</div>

View File

@ -28,12 +28,15 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
GQL.BulkUpdateIdMode.Add
);
const [tagIds, setTagIds] = useState<string[]>();
const [organized, setOrganized] = useState<boolean | undefined>();
const [updateImages] = useBulkImageUpdate();
// Network state
const [isUpdating, setIsUpdating] = useState(false);
const checkboxRef = React.createRef<HTMLInputElement>();
function makeBulkUpdateIds(
ids: string[],
mode: GQL.BulkUpdateIdMode
@ -119,6 +122,10 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
imageInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
}
if (organized !== undefined) {
imageInput.organized = organized;
}
return imageInput;
}
@ -221,6 +228,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
let updateStudioID: string | undefined;
let updatePerformerIds: string[] = [];
let updateTagIds: string[] = [];
let updateOrganized: boolean | undefined;
let first = true;
state.forEach((image: GQL.SlimImageDataFragment) => {
@ -236,6 +244,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
updateStudioID = imageStudioID;
updatePerformerIds = imagePerformerIDs;
updateTagIds = imageTagIDs;
updateOrganized = image.organized;
first = false;
} else {
if (imageRating !== updateRating) {
@ -250,6 +259,9 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
if (!_.isEqual(imageTagIDs, updateTagIds)) {
updateTagIds = [];
}
if (image.organized !== updateOrganized) {
updateOrganized = undefined;
}
}
});
@ -262,8 +274,15 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
if (tagMode === GQL.BulkUpdateIdMode.Set) {
setTagIds(updateTagIds);
}
setOrganized(updateOrganized);
}, [props.selected, performerMode, tagMode]);
useEffect(() => {
if (checkboxRef.current) {
checkboxRef.current.indeterminate = organized === undefined;
}
}, [organized, checkboxRef]);
function renderMultiSelect(
type: "performers" | "tags",
ids: string[] | undefined
@ -309,6 +328,16 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
);
}
function cycleOrganized() {
if (organized) {
setOrganized(undefined);
} else if (organized === undefined) {
setOrganized(false);
} else {
setOrganized(true);
}
}
function render() {
return (
<Modal
@ -357,10 +386,20 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
{renderMultiSelect("performers", performerIds)}
</Form.Group>
<Form.Group controlId="performers">
<Form.Group controlId="tags">
<Form.Label>Tags</Form.Label>
{renderMultiSelect("tags", tagIds)}
</Form.Group>
<Form.Group controlId="organized">
<Form.Check
type="checkbox"
label="Organized"
checked={organized}
ref={checkboxRef}
onChange={() => cycleOrganized()}
/>
</Form.Group>
</Form>
</Modal>
);

View File

@ -93,11 +93,24 @@ export const ImageCard: React.FC<IImageCardProps> = (
}
}
function maybeRenderOrganized() {
if (props.image.organized) {
return (
<div>
<Button className="minimal">
<Icon icon="box" />
</Button>
</div>
);
}
}
function maybeRenderPopoverButtonGroup() {
if (
props.image.tags.length > 0 ||
props.image.performers.length > 0 ||
props.image?.o_counter
props.image.o_counter ||
props.image.organized
) {
return (
<>
@ -106,6 +119,7 @@ export const ImageCard: React.FC<IImageCardProps> = (
{maybeRenderTagPopoverButton()}
{maybeRenderPerformerPopoverButton()}
{maybeRenderOCounter()}
{maybeRenderOrganized()}
</ButtonGroup>
</>
);

View File

@ -6,12 +6,14 @@ import {
useImageIncrementO,
useImageDecrementO,
useImageResetO,
useImageUpdate,
} from "src/core/StashService";
import { ErrorMessage, LoadingIndicator, Icon } from "src/components/Shared";
import { useToast } from "src/hooks";
import { TextUtils } from "src/utils";
import * as Mousetrap from "mousetrap";
import { OCounterButton } from "src/components/Scenes/SceneDetails/OCounterButton";
import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton";
import { ImageFileInfoPanel } from "./ImageFileInfoPanel";
import { ImageEditPanel } from "./ImageEditPanel";
import { ImageDetailPanel } from "./ImageDetailPanel";
@ -33,10 +35,32 @@ export const Image: React.FC = () => {
const [decrementO] = useImageDecrementO(image?.id ?? "0");
const [resetO] = useImageResetO(image?.id ?? "0");
const [updateImage] = useImageUpdate();
const [organizedLoading, setOrganizedLoading] = useState(false);
const [activeTabKey, setActiveTabKey] = useState("image-details-panel");
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
const onOrganizedClick = async () => {
try {
setOrganizedLoading(true);
await updateImage({
variables: {
input: {
id: image?.id ?? "",
organized: !image?.organized,
},
},
});
} catch (e) {
Toast.error(e);
} finally {
setOrganizedLoading(false);
}
};
const onIncrementClick = async () => {
try {
setOLoading(true);
@ -139,6 +163,13 @@ export const Image: React.FC = () => {
onReset={onResetClick}
/>
</Nav.Item>
<Nav.Item>
<OrganizedButton
loading={organizedLoading}
organized={image.organized}
onClick={onOrganizedClick}
/>
</Nav.Item>
<Nav.Item>{renderOperations()}</Nav.Item>
</Nav>
</div>

View File

@ -28,12 +28,15 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
GQL.BulkUpdateIdMode.Add
);
const [tagIds, setTagIds] = useState<string[]>();
const [organized, setOrganized] = useState<boolean | undefined>();
const [updateScenes] = useBulkSceneUpdate(getSceneInput());
// Network state
const [isUpdating, setIsUpdating] = useState(false);
const checkboxRef = React.createRef<HTMLInputElement>();
function makeBulkUpdateIds(
ids: string[],
mode: GQL.BulkUpdateIdMode
@ -119,6 +122,10 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
sceneInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
}
if (organized !== undefined) {
sceneInput.organized = organized;
}
return sceneInput;
}
@ -217,6 +224,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
let updateStudioID: string | undefined;
let updatePerformerIds: string[] = [];
let updateTagIds: string[] = [];
let updateOrganized: boolean | undefined;
let first = true;
state.forEach((scene: GQL.SlimSceneDataFragment) => {
@ -233,6 +241,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
updatePerformerIds = scenePerformerIDs;
updateTagIds = sceneTagIDs;
first = false;
updateOrganized = scene.organized;
} else {
if (sceneRating !== updateRating) {
updateRating = undefined;
@ -246,6 +255,9 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
if (!_.isEqual(sceneTagIDs, updateTagIds)) {
updateTagIds = [];
}
if (scene.organized !== updateOrganized) {
updateOrganized = undefined;
}
}
});
@ -258,8 +270,15 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
if (tagMode === GQL.BulkUpdateIdMode.Set) {
setTagIds(updateTagIds);
}
setOrganized(updateOrganized);
}, [props.selected, performerMode, tagMode]);
useEffect(() => {
if (checkboxRef.current) {
checkboxRef.current.indeterminate = organized === undefined;
}
}, [organized, checkboxRef]);
function renderMultiSelect(
type: "performers" | "tags",
ids: string[] | undefined
@ -305,6 +324,16 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
);
}
function cycleOrganized() {
if (organized) {
setOrganized(undefined);
} else if (organized === undefined) {
setOrganized(false);
} else {
setOrganized(true);
}
}
function render() {
return (
<Modal
@ -353,10 +382,20 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
{renderMultiSelect("performers", performerIds)}
</Form.Group>
<Form.Group controlId="performers">
<Form.Group controlId="tags">
<Form.Label>Tags</Form.Label>
{renderMultiSelect("tags", tagIds)}
</Form.Group>
<Form.Group controlId="organized">
<Form.Check
type="checkbox"
label="Organized"
checked={organized}
ref={checkboxRef}
onChange={() => cycleOrganized()}
/>
</Form.Group>
</Form>
</Modal>
);

View File

@ -267,6 +267,18 @@ export const SceneCard: React.FC<ISceneCardProps> = (
}
}
function maybeRenderOrganized() {
if (props.scene.organized) {
return (
<div>
<Button className="minimal">
<Icon icon="box" />
</Button>
</div>
);
}
}
function maybeRenderPopoverButtonGroup() {
if (
props.scene.tags.length > 0 ||
@ -274,7 +286,8 @@ export const SceneCard: React.FC<ISceneCardProps> = (
props.scene.movies.length > 0 ||
props.scene.scene_markers.length > 0 ||
props.scene?.o_counter ||
props.scene.gallery
props.scene.gallery ||
props.scene.organized
) {
return (
<>
@ -286,6 +299,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
{maybeRenderSceneMarkerPopoverButton()}
{maybeRenderOCounter()}
{maybeRenderGallery()}
{maybeRenderOrganized()}
</ButtonGroup>
</>
);

View File

@ -0,0 +1,40 @@
import React from "react";
import cx from "classnames";
import { Button, Spinner } from "react-bootstrap";
import { Icon } from "src/components/Shared";
import { defineMessages, useIntl } from "react-intl";
export interface IOrganizedButtonProps {
loading: boolean;
organized: boolean;
onClick: () => void;
}
export const OrganizedButton: React.FC<IOrganizedButtonProps> = (
props: IOrganizedButtonProps
) => {
const intl = useIntl();
const messages = defineMessages({
organized: {
id: "organized",
defaultMessage: "Organized",
},
});
if (props.loading) return <Spinner animation="border" role="status" />;
return (
<Button
variant="secondary"
title={intl.formatMessage(messages.organized)}
className={cx(
"minimal",
"organized-button",
props.organized ? "organized" : "not-organized"
)}
onClick={props.onClick}
>
<Icon icon="box" />
</Button>
);
};

View File

@ -10,6 +10,7 @@ import {
useSceneResetO,
useSceneStreams,
useSceneGenerateScreenshot,
useSceneUpdate,
} from "src/core/StashService";
import { GalleryViewer } from "src/components/Galleries/GalleryViewer";
import { ErrorMessage, LoadingIndicator, Icon } from "src/components/Shared";
@ -26,6 +27,7 @@ import { SceneMoviePanel } from "./SceneMoviePanel";
import { DeleteScenesDialog } from "../DeleteScenesDialog";
import { SceneGenerateDialog } from "../SceneGenerateDialog";
import { SceneVideoFilterPanel } from "./SceneVideoFilterPanel";
import { OrganizedButton } from "./OrganizedButton";
interface ISceneParams {
id?: string;
@ -36,6 +38,7 @@ export const Scene: React.FC = () => {
const location = useLocation();
const history = useHistory();
const Toast = useToast();
const [updateScene] = useSceneUpdate();
const [generateScreenshot] = useSceneGenerateScreenshot();
const [timestamp, setTimestamp] = useState<number>(getInitialTimestamp());
const [collapsed, setCollapsed] = useState(false);
@ -52,6 +55,8 @@ export const Scene: React.FC = () => {
const [decrementO] = useSceneDecrementO(scene?.id ?? "0");
const [resetO] = useSceneResetO(scene?.id ?? "0");
const [organizedLoading, setOrganizedLoading] = useState(false);
const [activeTabKey, setActiveTabKey] = useState("scene-details-panel");
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
@ -69,6 +74,24 @@ export const Scene: React.FC = () => {
);
}
const onOrganizedClick = async () => {
try {
setOrganizedLoading(true);
await updateScene({
variables: {
input: {
id: scene?.id ?? "",
organized: !scene?.organized,
},
},
});
} catch (e) {
Toast.error(e);
} finally {
setOrganizedLoading(false);
}
};
const onIncrementClick = async () => {
try {
setOLoading(true);
@ -246,6 +269,13 @@ export const Scene: React.FC = () => {
onReset={onResetClick}
/>
</Nav.Item>
<Nav.Item>
<OrganizedButton
loading={organizedLoading}
organized={scene.organized}
onClick={onOrganizedClick}
/>
</Nav.Item>
<Nav.Item>{renderOperations()}</Nav.Item>
</ButtonGroup>
</Nav>

View File

@ -70,7 +70,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
// Network state
const [isLoading, setIsLoading] = useState(true);
const [updateScene] = useSceneUpdate(getSceneInput());
const [updateScene] = useSceneUpdate();
useEffect(() => {
if (props.isVisible) {
@ -230,7 +230,11 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
async function onSave() {
setIsLoading(true);
try {
const result = await updateScene();
const result = await updateScene({
variables: {
input: getSceneInput(),
},
});
if (result.data?.sceneUpdate) {
Toast.success({ content: "Updated scene" });
}

View File

@ -535,3 +535,13 @@ input[type="range"].blue-slider {
}
}
}
.organized-button {
&.not-organized {
color: rgba(191, 204, 214, 0.5);
}
&.organized {
color: #664c3f;
}
}

View File

@ -323,11 +323,8 @@ const sceneMutationImpactedQueries = [
GQL.AllTagsDocument,
];
export const useSceneUpdate = (input: GQL.SceneUpdateInput) =>
export const useSceneUpdate = () =>
GQL.useSceneUpdateMutation({
variables: {
input,
},
update: deleteCache(sceneMutationImpactedQueries),
});

View File

@ -6,6 +6,7 @@
"markers": "Markers",
"movies": "Movies",
"new": "New",
"organized": "Organised",
"performers": "Performers",
"scenes": "Scenes",
"studios": "Studios",

View File

@ -6,6 +6,7 @@
"markers": "Markers",
"movies": "Movies",
"new": "New",
"organized": "Organized",
"performers": "Performers",
"scenes": "Scenes",
"studios": "Studios",

View File

@ -8,6 +8,7 @@ export type CriterionType =
| "none"
| "path"
| "rating"
| "organized"
| "o_counter"
| "resolution"
| "average_resolution"
@ -56,6 +57,8 @@ export abstract class Criterion {
return "Path";
case "rating":
return "Rating";
case "organized":
return "Organized";
case "o_counter":
return "O-Counter";
case "resolution":

View File

@ -0,0 +1,16 @@
import { CriterionModifier } from "src/core/generated-graphql";
import { Criterion, CriterionType, ICriterionOption } from "./criterion";
export class OrganizedCriterion extends Criterion {
public type: CriterionType = "organized";
public parameterName: string = "organized";
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: string[] = [true.toString(), false.toString()];
public value: string = "";
}
export class OrganizedCriterionOption implements ICriterionOption {
public label: string = Criterion.getLabel("organized");
public value: CriterionType = "organized";
}

View File

@ -8,6 +8,7 @@ import {
DurationCriterion,
MandatoryStringCriterion,
} from "./criterion";
import { OrganizedCriterion } from "./organized";
import { FavoriteCriterion } from "./favorite";
import { HasMarkersCriterion } from "./has-markers";
import {
@ -37,6 +38,8 @@ export function makeCriteria(type: CriterionType = "none") {
return new MandatoryStringCriterion(type, type);
case "rating":
return new RatingCriterion();
case "organized":
return new OrganizedCriterion();
case "o_counter":
case "scene_count":
case "marker_count":

View File

@ -27,6 +27,10 @@ import {
FavoriteCriterion,
FavoriteCriterionOption,
} from "./criteria/favorite";
import {
OrganizedCriterion,
OrganizedCriterionOption,
} from "./criteria/organized";
import {
HasMarkersCriterion,
HasMarkersCriterionOption,
@ -115,6 +119,7 @@ export class ListFilterModel {
"title",
"path",
"rating",
"organized",
"o_counter",
"date",
"filesize",
@ -134,6 +139,7 @@ export class ListFilterModel {
new NoneCriterionOption(),
ListFilterModel.createCriterionOption("path"),
new RatingCriterionOption(),
new OrganizedCriterionOption(),
ListFilterModel.createCriterionOption("o_counter"),
new ResolutionCriterionOption(),
ListFilterModel.createCriterionOption("duration"),
@ -161,6 +167,7 @@ export class ListFilterModel {
new NoneCriterionOption(),
ListFilterModel.createCriterionOption("path"),
new RatingCriterionOption(),
new OrganizedCriterionOption(),
ListFilterModel.createCriterionOption("o_counter"),
new ResolutionCriterionOption(),
new ImageIsMissingCriterionOption(),
@ -234,6 +241,7 @@ export class ListFilterModel {
new NoneCriterionOption(),
ListFilterModel.createCriterionOption("path"),
new RatingCriterionOption(),
new OrganizedCriterionOption(),
new AverageResolutionCriterionOption(),
new GalleryIsMissingCriterionOption(),
new TagsCriterionOption(),
@ -435,6 +443,10 @@ export class ListFilterModel {
};
break;
}
case "organized": {
result.organized = (criterion as OrganizedCriterion).value === "true";
break;
}
case "o_counter": {
const oCounterCrit = criterion as NumberCriterion;
result.o_counter = {
@ -669,6 +681,10 @@ export class ListFilterModel {
};
break;
}
case "organized": {
result.organized = (criterion as OrganizedCriterion).value === "true";
break;
}
case "o_counter": {
const oCounterCrit = criterion as NumberCriterion;
result.o_counter = {
@ -800,6 +816,10 @@ export class ListFilterModel {
};
break;
}
case "organized": {
result.organized = (criterion as OrganizedCriterion).value === "true";
break;
}
case "average_resolution": {
switch ((criterion as AverageResolutionCriterion).value) {
case "240p":