mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Gallery URLs (#4114)
* Initial backend changes * Fix unit tests * UI changes * Fix missing URL filters
This commit is contained in:
parent
a369e395e7
commit
9577600804
29 changed files with 361 additions and 117 deletions
|
|
@ -2,7 +2,7 @@ fragment SlimGalleryData on Gallery {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
date
|
date
|
||||||
url
|
urls
|
||||||
details
|
details
|
||||||
rating100
|
rating100
|
||||||
organized
|
organized
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ fragment GalleryData on Gallery {
|
||||||
updated_at
|
updated_at
|
||||||
title
|
title
|
||||||
date
|
date
|
||||||
url
|
urls
|
||||||
details
|
details
|
||||||
rating100
|
rating100
|
||||||
organized
|
organized
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,7 @@ fragment ScrapedSceneData on ScrapedScene {
|
||||||
fragment ScrapedGalleryData on ScrapedGallery {
|
fragment ScrapedGalleryData on ScrapedGallery {
|
||||||
title
|
title
|
||||||
details
|
details
|
||||||
url
|
urls
|
||||||
date
|
date
|
||||||
|
|
||||||
studio {
|
studio {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ type Gallery {
|
||||||
checksum: String! @deprecated(reason: "Use files.fingerprints")
|
checksum: String! @deprecated(reason: "Use files.fingerprints")
|
||||||
path: String @deprecated(reason: "Use files.path")
|
path: String @deprecated(reason: "Use files.path")
|
||||||
title: String
|
title: String
|
||||||
url: String
|
url: String @deprecated(reason: "Use urls")
|
||||||
|
urls: [String!]!
|
||||||
date: String
|
date: String
|
||||||
details: String
|
details: String
|
||||||
# rating expressed as 1-5
|
# rating expressed as 1-5
|
||||||
|
|
@ -33,7 +34,8 @@ type Gallery {
|
||||||
|
|
||||||
input GalleryCreateInput {
|
input GalleryCreateInput {
|
||||||
title: String!
|
title: String!
|
||||||
url: String
|
url: String @deprecated(reason: "Use urls")
|
||||||
|
urls: [String!]
|
||||||
date: String
|
date: String
|
||||||
details: String
|
details: String
|
||||||
# rating expressed as 1-5
|
# rating expressed as 1-5
|
||||||
|
|
@ -51,7 +53,8 @@ input GalleryUpdateInput {
|
||||||
clientMutationId: String
|
clientMutationId: String
|
||||||
id: ID!
|
id: ID!
|
||||||
title: String
|
title: String
|
||||||
url: String
|
url: String @deprecated(reason: "Use urls")
|
||||||
|
urls: [String!]
|
||||||
date: String
|
date: String
|
||||||
details: String
|
details: String
|
||||||
# rating expressed as 1-5
|
# rating expressed as 1-5
|
||||||
|
|
@ -70,7 +73,8 @@ input GalleryUpdateInput {
|
||||||
input BulkGalleryUpdateInput {
|
input BulkGalleryUpdateInput {
|
||||||
clientMutationId: String
|
clientMutationId: String
|
||||||
ids: [ID!]
|
ids: [ID!]
|
||||||
url: String
|
url: String @deprecated(reason: "Use urls")
|
||||||
|
urls: BulkUpdateStrings
|
||||||
date: String
|
date: String
|
||||||
details: String
|
details: String
|
||||||
# rating expressed as 1-5
|
# rating expressed as 1-5
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,8 @@ input ScrapedSceneInput {
|
||||||
type ScrapedGallery {
|
type ScrapedGallery {
|
||||||
title: String
|
title: String
|
||||||
details: String
|
details: String
|
||||||
url: String
|
url: String @deprecated(reason: "use urls")
|
||||||
|
urls: [String!]
|
||||||
date: String
|
date: String
|
||||||
|
|
||||||
studio: ScrapedStudio
|
studio: ScrapedStudio
|
||||||
|
|
@ -111,7 +112,8 @@ type ScrapedGallery {
|
||||||
input ScrapedGalleryInput {
|
input ScrapedGalleryInput {
|
||||||
title: String
|
title: String
|
||||||
details: String
|
details: String
|
||||||
url: String
|
url: String @deprecated(reason: "use urls")
|
||||||
|
urls: [String!]
|
||||||
date: String
|
date: String
|
||||||
|
|
||||||
# no studio, tags or performers
|
# no studio, tags or performers
|
||||||
|
|
|
||||||
|
|
@ -226,3 +226,32 @@ func (r *galleryResolver) Chapters(ctx context.Context, obj *models.Gallery) (re
|
||||||
|
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *galleryResolver) URL(ctx context.Context, obj *models.Gallery) (*string, error) {
|
||||||
|
if !obj.URLs.Loaded() {
|
||||||
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
|
return obj.LoadURLs(ctx, r.repository.Gallery)
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
urls := obj.URLs.List()
|
||||||
|
if len(urls) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &urls[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *galleryResolver) Urls(ctx context.Context, obj *models.Gallery) ([]string, error) {
|
||||||
|
if !obj.URLs.Loaded() {
|
||||||
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
|
return obj.LoadURLs(ctx, r.repository.Gallery)
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj.URLs.List(), nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
|
||||||
newGallery := models.NewGallery()
|
newGallery := models.NewGallery()
|
||||||
|
|
||||||
newGallery.Title = input.Title
|
newGallery.Title = input.Title
|
||||||
newGallery.URL = translator.string(input.URL)
|
|
||||||
newGallery.Details = translator.string(input.Details)
|
newGallery.Details = translator.string(input.Details)
|
||||||
newGallery.Rating = translator.ratingConversion(input.Rating, input.Rating100)
|
newGallery.Rating = translator.ratingConversion(input.Rating, input.Rating100)
|
||||||
|
|
||||||
|
|
@ -71,6 +70,12 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
|
||||||
return nil, fmt.Errorf("converting scene ids: %w", err)
|
return nil, fmt.Errorf("converting scene ids: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if input.Urls != nil {
|
||||||
|
newGallery.URLs = models.NewRelatedStrings(input.Urls)
|
||||||
|
} else if input.URL != nil {
|
||||||
|
newGallery.URLs = models.NewRelatedStrings([]string{*input.URL})
|
||||||
|
}
|
||||||
|
|
||||||
// Start the transaction and save the gallery
|
// Start the transaction and save the gallery
|
||||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||||
qb := r.repository.Gallery
|
qb := r.repository.Gallery
|
||||||
|
|
@ -178,7 +183,6 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedGallery.Details = translator.optionalString(input.Details, "details")
|
updatedGallery.Details = translator.optionalString(input.Details, "details")
|
||||||
updatedGallery.URL = translator.optionalString(input.URL, "url")
|
|
||||||
updatedGallery.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
|
updatedGallery.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
|
||||||
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
|
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
|
||||||
|
|
||||||
|
|
@ -191,6 +195,8 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
|
||||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updatedGallery.URLs = translator.optionalURLs(input.Urls, input.URL)
|
||||||
|
|
||||||
updatedGallery.PrimaryFileID, err = translator.fileIDPtrFromString(input.PrimaryFileID)
|
updatedGallery.PrimaryFileID, err = translator.fileIDPtrFromString(input.PrimaryFileID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("converting primary file id: %w", err)
|
return nil, fmt.Errorf("converting primary file id: %w", err)
|
||||||
|
|
@ -252,9 +258,9 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall
|
||||||
updatedGallery := models.NewGalleryPartial()
|
updatedGallery := models.NewGalleryPartial()
|
||||||
|
|
||||||
updatedGallery.Details = translator.optionalString(input.Details, "details")
|
updatedGallery.Details = translator.optionalString(input.Details, "details")
|
||||||
updatedGallery.URL = translator.optionalString(input.URL, "url")
|
|
||||||
updatedGallery.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
|
updatedGallery.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
|
||||||
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
|
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
|
||||||
|
updatedGallery.URLs = translator.optionalURLsBulk(input.Urls, input.URL)
|
||||||
|
|
||||||
updatedGallery.Date, err = translator.optionalDate(input.Date, "date")
|
updatedGallery.Date, err = translator.optionalDate(input.Date, "date")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import (
|
||||||
func ToBasicJSON(gallery *models.Gallery) (*jsonschema.Gallery, error) {
|
func ToBasicJSON(gallery *models.Gallery) (*jsonschema.Gallery, error) {
|
||||||
newGalleryJSON := jsonschema.Gallery{
|
newGalleryJSON := jsonschema.Gallery{
|
||||||
Title: gallery.Title,
|
Title: gallery.Title,
|
||||||
URL: gallery.URL,
|
URLs: gallery.URLs.List(),
|
||||||
Details: gallery.Details,
|
Details: gallery.Details,
|
||||||
CreatedAt: json.JSONTime{Time: gallery.CreatedAt},
|
CreatedAt: json.JSONTime{Time: gallery.CreatedAt},
|
||||||
UpdatedAt: json.JSONTime{Time: gallery.UpdatedAt},
|
UpdatedAt: json.JSONTime{Time: gallery.UpdatedAt},
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ func createFullGallery(id int) models.Gallery {
|
||||||
Details: details,
|
Details: details,
|
||||||
Rating: &rating,
|
Rating: &rating,
|
||||||
Organized: organized,
|
Organized: organized,
|
||||||
URL: url,
|
URLs: models.NewRelatedStrings([]string{url}),
|
||||||
CreatedAt: createTime,
|
CreatedAt: createTime,
|
||||||
UpdatedAt: updateTime,
|
UpdatedAt: updateTime,
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +85,7 @@ func createFullJSONGallery() *jsonschema.Gallery {
|
||||||
Details: details,
|
Details: details,
|
||||||
Rating: rating,
|
Rating: rating,
|
||||||
Organized: organized,
|
Organized: organized,
|
||||||
URL: url,
|
URLs: []string{url},
|
||||||
ZipFiles: []string{path},
|
ZipFiles: []string{path},
|
||||||
CreatedAt: json.JSONTime{
|
CreatedAt: json.JSONTime{
|
||||||
Time: createTime,
|
Time: createTime,
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,10 @@ func (i *Importer) galleryJSONToGallery(galleryJSON jsonschema.Gallery) models.G
|
||||||
if galleryJSON.Details != "" {
|
if galleryJSON.Details != "" {
|
||||||
newGallery.Details = galleryJSON.Details
|
newGallery.Details = galleryJSON.Details
|
||||||
}
|
}
|
||||||
if galleryJSON.URL != "" {
|
if len(galleryJSON.URLs) > 0 {
|
||||||
newGallery.URL = galleryJSON.URL
|
newGallery.URLs = models.NewRelatedStrings(galleryJSON.URLs)
|
||||||
|
} else if galleryJSON.URL != "" {
|
||||||
|
newGallery.URLs = models.NewRelatedStrings([]string{galleryJSON.URL})
|
||||||
}
|
}
|
||||||
if galleryJSON.Date != "" {
|
if galleryJSON.Date != "" {
|
||||||
d, err := models.ParseDate(galleryJSON.Date)
|
d, err := models.ParseDate(galleryJSON.Date)
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ func TestImporterPreImport(t *testing.T) {
|
||||||
Details: details,
|
Details: details,
|
||||||
Rating: &rating,
|
Rating: &rating,
|
||||||
Organized: organized,
|
Organized: organized,
|
||||||
URL: url,
|
URLs: models.NewRelatedStrings([]string{url}),
|
||||||
Files: models.NewRelatedFiles([]models.File{}),
|
Files: models.NewRelatedFiles([]models.File{}),
|
||||||
TagIDs: models.NewRelatedIDs([]int{}),
|
TagIDs: models.NewRelatedIDs([]int{}),
|
||||||
PerformerIDs: models.NewRelatedIDs([]int{}),
|
PerformerIDs: models.NewRelatedIDs([]int{}),
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ type GalleryUpdateInput struct {
|
||||||
ClientMutationID *string `json:"clientMutationId"`
|
ClientMutationID *string `json:"clientMutationId"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title *string `json:"title"`
|
Title *string `json:"title"`
|
||||||
URL *string `json:"url"`
|
Urls []string `json:"urls"`
|
||||||
Date *string `json:"date"`
|
Date *string `json:"date"`
|
||||||
Details *string `json:"details"`
|
Details *string `json:"details"`
|
||||||
Rating *int `json:"rating"`
|
Rating *int `json:"rating"`
|
||||||
|
|
@ -70,6 +70,9 @@ type GalleryUpdateInput struct {
|
||||||
TagIds []string `json:"tag_ids"`
|
TagIds []string `json:"tag_ids"`
|
||||||
PerformerIds []string `json:"performer_ids"`
|
PerformerIds []string `json:"performer_ids"`
|
||||||
PrimaryFileID *string `json:"primary_file_id"`
|
PrimaryFileID *string `json:"primary_file_id"`
|
||||||
|
|
||||||
|
// deprecated
|
||||||
|
URL *string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GalleryDestroyInput struct {
|
type GalleryDestroyInput struct {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ type Gallery struct {
|
||||||
ZipFiles []string `json:"zip_files,omitempty"`
|
ZipFiles []string `json:"zip_files,omitempty"`
|
||||||
FolderPath string `json:"folder_path,omitempty"`
|
FolderPath string `json:"folder_path,omitempty"`
|
||||||
Title string `json:"title,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
URL string `json:"url,omitempty"`
|
URLs []string `json:"urls,omitempty"`
|
||||||
Date string `json:"date,omitempty"`
|
Date string `json:"date,omitempty"`
|
||||||
Details string `json:"details,omitempty"`
|
Details string `json:"details,omitempty"`
|
||||||
Rating int `json:"rating,omitempty"`
|
Rating int `json:"rating,omitempty"`
|
||||||
|
|
@ -32,6 +32,9 @@ type Gallery struct {
|
||||||
Tags []string `json:"tags,omitempty"`
|
Tags []string `json:"tags,omitempty"`
|
||||||
CreatedAt json.JSONTime `json:"created_at,omitempty"`
|
CreatedAt json.JSONTime `json:"created_at,omitempty"`
|
||||||
UpdatedAt json.JSONTime `json:"updated_at,omitempty"`
|
UpdatedAt json.JSONTime `json:"updated_at,omitempty"`
|
||||||
|
|
||||||
|
// deprecated - for import only
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Gallery) Filename(basename string, hash string) string {
|
func (s Gallery) Filename(basename string, hash string) string {
|
||||||
|
|
|
||||||
|
|
@ -533,6 +533,29 @@ func (_m *GalleryReaderWriter) GetTagIDs(ctx context.Context, relatedID int) ([]
|
||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetURLs provides a mock function with given fields: ctx, relatedID
|
||||||
|
func (_m *GalleryReaderWriter) GetURLs(ctx context.Context, relatedID int) ([]string, error) {
|
||||||
|
ret := _m.Called(ctx, relatedID)
|
||||||
|
|
||||||
|
var r0 []string
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int) []string); ok {
|
||||||
|
r0 = rf(ctx, relatedID)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
|
||||||
|
r1 = rf(ctx, relatedID)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// Query provides a mock function with given fields: ctx, galleryFilter, findFilter
|
// Query provides a mock function with given fields: ctx, galleryFilter, findFilter
|
||||||
func (_m *GalleryReaderWriter) Query(ctx context.Context, galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) ([]*models.Gallery, int, error) {
|
func (_m *GalleryReaderWriter) Query(ctx context.Context, galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) ([]*models.Gallery, int, error) {
|
||||||
ret := _m.Called(ctx, galleryFilter, findFilter)
|
ret := _m.Called(ctx, galleryFilter, findFilter)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ type Gallery struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
|
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
URL string `json:"url"`
|
|
||||||
Date *Date `json:"date"`
|
Date *Date `json:"date"`
|
||||||
Details string `json:"details"`
|
Details string `json:"details"`
|
||||||
// Rating expressed in 1-100 scale
|
// Rating expressed in 1-100 scale
|
||||||
|
|
@ -31,9 +30,10 @@ type Gallery struct {
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
SceneIDs RelatedIDs `json:"scene_ids"`
|
URLs RelatedStrings `json:"urls"`
|
||||||
TagIDs RelatedIDs `json:"tag_ids"`
|
SceneIDs RelatedIDs `json:"scene_ids"`
|
||||||
PerformerIDs RelatedIDs `json:"performer_ids"`
|
TagIDs RelatedIDs `json:"tag_ids"`
|
||||||
|
PerformerIDs RelatedIDs `json:"performer_ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGallery() Gallery {
|
func NewGallery() Gallery {
|
||||||
|
|
@ -51,7 +51,7 @@ type GalleryPartial struct {
|
||||||
// Checksum OptionalString
|
// Checksum OptionalString
|
||||||
// Zip OptionalBool
|
// Zip OptionalBool
|
||||||
Title OptionalString
|
Title OptionalString
|
||||||
URL OptionalString
|
URLs *UpdateStrings
|
||||||
Date OptionalDate
|
Date OptionalDate
|
||||||
Details OptionalString
|
Details OptionalString
|
||||||
// Rating expressed in 1-100 scale
|
// Rating expressed in 1-100 scale
|
||||||
|
|
@ -81,6 +81,12 @@ func (g *Gallery) IsUserCreated() bool {
|
||||||
return g.PrimaryFileID == nil && g.FolderID == nil
|
return g.PrimaryFileID == nil && g.FolderID == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *Gallery) LoadURLs(ctx context.Context, l URLLoader) error {
|
||||||
|
return g.URLs.load(func() ([]string, error) {
|
||||||
|
return l.GetURLs(ctx, g.ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (g *Gallery) LoadFiles(ctx context.Context, l FileLoader) error {
|
func (g *Gallery) LoadFiles(ctx context.Context, l FileLoader) error {
|
||||||
return g.Files.load(func() ([]File, error) {
|
return g.Files.load(func() ([]File, error) {
|
||||||
return l.GetFiles(ctx, g.ID)
|
return l.GetFiles(ctx, g.ID)
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ type GalleryReader interface {
|
||||||
GalleryQueryer
|
GalleryQueryer
|
||||||
GalleryCounter
|
GalleryCounter
|
||||||
|
|
||||||
|
URLLoader
|
||||||
FileIDLoader
|
FileIDLoader
|
||||||
ImageIDLoader
|
ImageIDLoader
|
||||||
SceneIDLoader
|
SceneIDLoader
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,24 @@ import "github.com/stashapp/stash/pkg/models"
|
||||||
type ScrapedGallery struct {
|
type ScrapedGallery struct {
|
||||||
Title *string `json:"title"`
|
Title *string `json:"title"`
|
||||||
Details *string `json:"details"`
|
Details *string `json:"details"`
|
||||||
URL *string `json:"url"`
|
URLs []string `json:"urls"`
|
||||||
Date *string `json:"date"`
|
Date *string `json:"date"`
|
||||||
Studio *models.ScrapedStudio `json:"studio"`
|
Studio *models.ScrapedStudio `json:"studio"`
|
||||||
Tags []*models.ScrapedTag `json:"tags"`
|
Tags []*models.ScrapedTag `json:"tags"`
|
||||||
Performers []*models.ScrapedPerformer `json:"performers"`
|
Performers []*models.ScrapedPerformer `json:"performers"`
|
||||||
|
|
||||||
|
// deprecated
|
||||||
|
URL *string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ScrapedGallery) IsScrapedContent() {}
|
func (ScrapedGallery) IsScrapedContent() {}
|
||||||
|
|
||||||
type ScrapedGalleryInput struct {
|
type ScrapedGalleryInput struct {
|
||||||
Title *string `json:"title"`
|
Title *string `json:"title"`
|
||||||
Details *string `json:"details"`
|
Details *string `json:"details"`
|
||||||
URL *string `json:"url"`
|
URLs []string `json:"urls"`
|
||||||
Date *string `json:"date"`
|
Date *string `json:"date"`
|
||||||
|
|
||||||
|
// deprecated
|
||||||
|
URL *string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,8 @@ func queryURLParametersFromGallery(gallery *models.Gallery) queryURLParameters {
|
||||||
ret["title"] = gallery.Title
|
ret["title"] = gallery.Title
|
||||||
}
|
}
|
||||||
|
|
||||||
if gallery.URL != "" {
|
if len(gallery.URLs.List()) > 0 {
|
||||||
ret["url"] = gallery.URL
|
ret["url"] = gallery.URLs.List()[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
|
||||||
|
|
@ -354,11 +354,18 @@ func galleryToUpdateInput(gallery *models.Gallery) models.GalleryUpdateInput {
|
||||||
// fallback to file basename if title is empty
|
// fallback to file basename if title is empty
|
||||||
title := gallery.GetTitle()
|
title := gallery.GetTitle()
|
||||||
|
|
||||||
|
var url *string
|
||||||
|
urls := gallery.URLs.List()
|
||||||
|
if len(urls) > 0 {
|
||||||
|
url = &urls[0]
|
||||||
|
}
|
||||||
|
|
||||||
return models.GalleryUpdateInput{
|
return models.GalleryUpdateInput{
|
||||||
ID: strconv.Itoa(gallery.ID),
|
ID: strconv.Itoa(gallery.ID),
|
||||||
Title: &title,
|
Title: &title,
|
||||||
Details: &gallery.Details,
|
Details: &gallery.Details,
|
||||||
URL: &gallery.URL,
|
URL: url,
|
||||||
|
Urls: urls,
|
||||||
Date: dateToStringPtr(gallery.Date),
|
Date: dateToStringPtr(gallery.Date),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ const (
|
||||||
dbConnTimeout = 30
|
dbConnTimeout = 30
|
||||||
)
|
)
|
||||||
|
|
||||||
var appSchemaVersion uint = 50
|
var appSchemaVersion uint = 51
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*.sql
|
||||||
var migrationsBox embed.FS
|
var migrationsBox embed.FS
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,13 @@ const (
|
||||||
galleriesImagesTable = "galleries_images"
|
galleriesImagesTable = "galleries_images"
|
||||||
galleriesScenesTable = "scenes_galleries"
|
galleriesScenesTable = "scenes_galleries"
|
||||||
galleryIDColumn = "gallery_id"
|
galleryIDColumn = "gallery_id"
|
||||||
|
galleriesURLsTable = "gallery_urls"
|
||||||
|
galleriesURLColumn = "url"
|
||||||
)
|
)
|
||||||
|
|
||||||
type galleryRow struct {
|
type galleryRow struct {
|
||||||
ID int `db:"id" goqu:"skipinsert"`
|
ID int `db:"id" goqu:"skipinsert"`
|
||||||
Title zero.String `db:"title"`
|
Title zero.String `db:"title"`
|
||||||
URL zero.String `db:"url"`
|
|
||||||
Date NullDate `db:"date"`
|
Date NullDate `db:"date"`
|
||||||
Details zero.String `db:"details"`
|
Details zero.String `db:"details"`
|
||||||
// expressed as 1-100
|
// expressed as 1-100
|
||||||
|
|
@ -46,7 +47,6 @@ type galleryRow struct {
|
||||||
func (r *galleryRow) fromGallery(o models.Gallery) {
|
func (r *galleryRow) fromGallery(o models.Gallery) {
|
||||||
r.ID = o.ID
|
r.ID = o.ID
|
||||||
r.Title = zero.StringFrom(o.Title)
|
r.Title = zero.StringFrom(o.Title)
|
||||||
r.URL = zero.StringFrom(o.URL)
|
|
||||||
r.Date = NullDateFromDatePtr(o.Date)
|
r.Date = NullDateFromDatePtr(o.Date)
|
||||||
r.Details = zero.StringFrom(o.Details)
|
r.Details = zero.StringFrom(o.Details)
|
||||||
r.Rating = intFromPtr(o.Rating)
|
r.Rating = intFromPtr(o.Rating)
|
||||||
|
|
@ -70,7 +70,6 @@ func (r *galleryQueryRow) resolve() *models.Gallery {
|
||||||
ret := &models.Gallery{
|
ret := &models.Gallery{
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
Title: r.Title.String,
|
Title: r.Title.String,
|
||||||
URL: r.URL.String,
|
|
||||||
Date: r.Date.DatePtr(),
|
Date: r.Date.DatePtr(),
|
||||||
Details: r.Details.String,
|
Details: r.Details.String,
|
||||||
Rating: nullIntPtr(r.Rating),
|
Rating: nullIntPtr(r.Rating),
|
||||||
|
|
@ -97,7 +96,6 @@ type galleryRowRecord struct {
|
||||||
|
|
||||||
func (r *galleryRowRecord) fromPartial(o models.GalleryPartial) {
|
func (r *galleryRowRecord) fromPartial(o models.GalleryPartial) {
|
||||||
r.setNullString("title", o.Title)
|
r.setNullString("title", o.Title)
|
||||||
r.setNullString("url", o.URL)
|
|
||||||
r.setNullDate("date", o.Date)
|
r.setNullDate("date", o.Date)
|
||||||
r.setNullString("details", o.Details)
|
r.setNullString("details", o.Details)
|
||||||
r.setNullInt("rating", o.Rating)
|
r.setNullInt("rating", o.Rating)
|
||||||
|
|
@ -178,6 +176,12 @@ func (qb *GalleryStore) Create(ctx context.Context, newObject *models.Gallery, f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if newObject.URLs.Loaded() {
|
||||||
|
const startPos = 0
|
||||||
|
if err := galleriesURLsTableMgr.insertJoins(ctx, id, startPos, newObject.URLs.List()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if newObject.PerformerIDs.Loaded() {
|
if newObject.PerformerIDs.Loaded() {
|
||||||
if err := galleriesPerformersTableMgr.insertJoins(ctx, id, newObject.PerformerIDs.List()); err != nil {
|
if err := galleriesPerformersTableMgr.insertJoins(ctx, id, newObject.PerformerIDs.List()); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -212,6 +216,11 @@ func (qb *GalleryStore) Update(ctx context.Context, updatedObject *models.Galler
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if updatedObject.URLs.Loaded() {
|
||||||
|
if err := galleriesURLsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.URLs.List()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if updatedObject.PerformerIDs.Loaded() {
|
if updatedObject.PerformerIDs.Loaded() {
|
||||||
if err := galleriesPerformersTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.PerformerIDs.List()); err != nil {
|
if err := galleriesPerformersTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.PerformerIDs.List()); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -257,6 +266,11 @@ func (qb *GalleryStore) UpdatePartial(ctx context.Context, id int, partial model
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if partial.URLs != nil {
|
||||||
|
if err := galleriesURLsTableMgr.modifyJoins(ctx, id, partial.URLs.Values, partial.URLs.Mode); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if partial.PerformerIDs != nil {
|
if partial.PerformerIDs != nil {
|
||||||
if err := galleriesPerformersTableMgr.modifyJoins(ctx, id, partial.PerformerIDs.IDs, partial.PerformerIDs.Mode); err != nil {
|
if err := galleriesPerformersTableMgr.modifyJoins(ctx, id, partial.PerformerIDs.IDs, partial.PerformerIDs.Mode); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -669,7 +683,7 @@ func (qb *GalleryStore) makeFilter(ctx context.Context, galleryFilter *models.Ga
|
||||||
query.handleCriterion(ctx, intCriterionHandler(galleryFilter.Rating100, "galleries.rating", nil))
|
query.handleCriterion(ctx, intCriterionHandler(galleryFilter.Rating100, "galleries.rating", nil))
|
||||||
// legacy rating handler
|
// legacy rating handler
|
||||||
query.handleCriterion(ctx, rating5CriterionHandler(galleryFilter.Rating, "galleries.rating", nil))
|
query.handleCriterion(ctx, rating5CriterionHandler(galleryFilter.Rating, "galleries.rating", nil))
|
||||||
query.handleCriterion(ctx, stringCriterionHandler(galleryFilter.URL, "galleries.url"))
|
query.handleCriterion(ctx, galleryURLsCriterionHandler(galleryFilter.URL))
|
||||||
query.handleCriterion(ctx, boolCriterionHandler(galleryFilter.Organized, "galleries.organized", nil))
|
query.handleCriterion(ctx, boolCriterionHandler(galleryFilter.Organized, "galleries.organized", nil))
|
||||||
query.handleCriterion(ctx, galleryIsMissingCriterionHandler(qb, galleryFilter.IsMissing))
|
query.handleCriterion(ctx, galleryIsMissingCriterionHandler(qb, galleryFilter.IsMissing))
|
||||||
query.handleCriterion(ctx, galleryTagsCriterionHandler(qb, galleryFilter.Tags))
|
query.handleCriterion(ctx, galleryTagsCriterionHandler(qb, galleryFilter.Tags))
|
||||||
|
|
@ -793,6 +807,18 @@ func (qb *GalleryStore) QueryCount(ctx context.Context, galleryFilter *models.Ga
|
||||||
return query.executeCount(ctx)
|
return query.executeCount(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func galleryURLsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc {
|
||||||
|
h := stringListCriterionHandlerBuilder{
|
||||||
|
joinTable: galleriesURLsTable,
|
||||||
|
stringColumn: galleriesURLColumn,
|
||||||
|
addJoinTable: func(f *filterBuilder) {
|
||||||
|
galleriesURLsTableMgr.join(f, "", "galleries.id")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.handler(url)
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *GalleryStore) galleryPathCriterionHandler(c *models.StringCriterionInput) criterionHandlerFunc {
|
func (qb *GalleryStore) galleryPathCriterionHandler(c *models.StringCriterionInput) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if c != nil {
|
if c != nil {
|
||||||
|
|
@ -874,6 +900,9 @@ func galleryIsMissingCriterionHandler(qb *GalleryStore, isMissing *string) crite
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if isMissing != nil && *isMissing != "" {
|
if isMissing != nil && *isMissing != "" {
|
||||||
switch *isMissing {
|
switch *isMissing {
|
||||||
|
case "url":
|
||||||
|
galleriesURLsTableMgr.join(f, "", "galleries.id")
|
||||||
|
f.addWhere("gallery_urls.url IS NULL")
|
||||||
case "scenes":
|
case "scenes":
|
||||||
f.addLeftJoin("scenes_galleries", "scenes_join", "scenes_join.gallery_id = galleries.id")
|
f.addLeftJoin("scenes_galleries", "scenes_join", "scenes_join.gallery_id = galleries.id")
|
||||||
f.addWhere("scenes_join.gallery_id IS NULL")
|
f.addWhere("scenes_join.gallery_id IS NULL")
|
||||||
|
|
@ -1107,6 +1136,10 @@ func (qb *GalleryStore) setGallerySort(query *queryBuilder, findFilter *models.F
|
||||||
query.sortAndPagination += ", COALESCE(galleries.title, galleries.id) COLLATE NATURAL_CI ASC"
|
query.sortAndPagination += ", COALESCE(galleries.title, galleries.id) COLLATE NATURAL_CI ASC"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *GalleryStore) GetURLs(ctx context.Context, galleryID int) ([]string, error) {
|
||||||
|
return galleriesURLsTableMgr.get(ctx, galleryID)
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *GalleryStore) filesRepository() *filesRepository {
|
func (qb *GalleryStore) filesRepository() *filesRepository {
|
||||||
return &filesRepository{
|
return &filesRepository{
|
||||||
repository: repository{
|
repository: repository{
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,11 @@ import (
|
||||||
var invalidID = -1
|
var invalidID = -1
|
||||||
|
|
||||||
func loadGalleryRelationships(ctx context.Context, expected models.Gallery, actual *models.Gallery) error {
|
func loadGalleryRelationships(ctx context.Context, expected models.Gallery, actual *models.Gallery) error {
|
||||||
|
if expected.URLs.Loaded() {
|
||||||
|
if err := actual.LoadURLs(ctx, db.Gallery); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if expected.SceneIDs.Loaded() {
|
if expected.SceneIDs.Loaded() {
|
||||||
if err := actual.LoadSceneIDs(ctx, db.Gallery); err != nil {
|
if err := actual.LoadSceneIDs(ctx, db.Gallery); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -72,7 +77,7 @@ func Test_galleryQueryBuilder_Create(t *testing.T) {
|
||||||
"full",
|
"full",
|
||||||
models.Gallery{
|
models.Gallery{
|
||||||
Title: title,
|
Title: title,
|
||||||
URL: url,
|
URLs: models.NewRelatedStrings([]string{url}),
|
||||||
Date: &date,
|
Date: &date,
|
||||||
Details: details,
|
Details: details,
|
||||||
Rating: &rating,
|
Rating: &rating,
|
||||||
|
|
@ -90,7 +95,7 @@ func Test_galleryQueryBuilder_Create(t *testing.T) {
|
||||||
"with file",
|
"with file",
|
||||||
models.Gallery{
|
models.Gallery{
|
||||||
Title: title,
|
Title: title,
|
||||||
URL: url,
|
URLs: models.NewRelatedStrings([]string{url}),
|
||||||
Date: &date,
|
Date: &date,
|
||||||
Details: details,
|
Details: details,
|
||||||
Rating: &rating,
|
Rating: &rating,
|
||||||
|
|
@ -222,7 +227,7 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
|
||||||
&models.Gallery{
|
&models.Gallery{
|
||||||
ID: galleryIDs[galleryIdxWithScene],
|
ID: galleryIDs[galleryIdxWithScene],
|
||||||
Title: title,
|
Title: title,
|
||||||
URL: url,
|
URLs: models.NewRelatedStrings([]string{url}),
|
||||||
Date: &date,
|
Date: &date,
|
||||||
Details: details,
|
Details: details,
|
||||||
Rating: &rating,
|
Rating: &rating,
|
||||||
|
|
@ -243,6 +248,7 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
|
||||||
"clear nullables",
|
"clear nullables",
|
||||||
&models.Gallery{
|
&models.Gallery{
|
||||||
ID: galleryIDs[galleryIdxWithImage],
|
ID: galleryIDs[galleryIdxWithImage],
|
||||||
|
URLs: models.NewRelatedStrings([]string{}),
|
||||||
SceneIDs: models.NewRelatedIDs([]int{}),
|
SceneIDs: models.NewRelatedIDs([]int{}),
|
||||||
TagIDs: models.NewRelatedIDs([]int{}),
|
TagIDs: models.NewRelatedIDs([]int{}),
|
||||||
PerformerIDs: models.NewRelatedIDs([]int{}),
|
PerformerIDs: models.NewRelatedIDs([]int{}),
|
||||||
|
|
@ -384,7 +390,7 @@ func clearGalleryPartial() models.GalleryPartial {
|
||||||
return models.GalleryPartial{
|
return models.GalleryPartial{
|
||||||
Title: models.OptionalString{Set: true, Null: true},
|
Title: models.OptionalString{Set: true, Null: true},
|
||||||
Details: models.OptionalString{Set: true, Null: true},
|
Details: models.OptionalString{Set: true, Null: true},
|
||||||
URL: models.OptionalString{Set: true, Null: true},
|
URLs: &models.UpdateStrings{Mode: models.RelationshipUpdateModeSet},
|
||||||
Date: models.OptionalDate{Set: true, Null: true},
|
Date: models.OptionalDate{Set: true, Null: true},
|
||||||
Rating: models.OptionalInt{Set: true, Null: true},
|
Rating: models.OptionalInt{Set: true, Null: true},
|
||||||
StudioID: models.OptionalInt{Set: true, Null: true},
|
StudioID: models.OptionalInt{Set: true, Null: true},
|
||||||
|
|
@ -416,9 +422,12 @@ func Test_galleryQueryBuilder_UpdatePartial(t *testing.T) {
|
||||||
"full",
|
"full",
|
||||||
galleryIDs[galleryIdxWithImage],
|
galleryIDs[galleryIdxWithImage],
|
||||||
models.GalleryPartial{
|
models.GalleryPartial{
|
||||||
Title: models.NewOptionalString(title),
|
Title: models.NewOptionalString(title),
|
||||||
Details: models.NewOptionalString(details),
|
Details: models.NewOptionalString(details),
|
||||||
URL: models.NewOptionalString(url),
|
URLs: &models.UpdateStrings{
|
||||||
|
Values: []string{url},
|
||||||
|
Mode: models.RelationshipUpdateModeSet,
|
||||||
|
},
|
||||||
Date: models.NewOptionalDate(date),
|
Date: models.NewOptionalDate(date),
|
||||||
Rating: models.NewOptionalInt(rating),
|
Rating: models.NewOptionalInt(rating),
|
||||||
Organized: models.NewOptionalBool(true),
|
Organized: models.NewOptionalBool(true),
|
||||||
|
|
@ -443,7 +452,7 @@ func Test_galleryQueryBuilder_UpdatePartial(t *testing.T) {
|
||||||
ID: galleryIDs[galleryIdxWithImage],
|
ID: galleryIDs[galleryIdxWithImage],
|
||||||
Title: title,
|
Title: title,
|
||||||
Details: details,
|
Details: details,
|
||||||
URL: url,
|
URLs: models.NewRelatedStrings([]string{url}),
|
||||||
Date: &date,
|
Date: &date,
|
||||||
Rating: &rating,
|
Rating: &rating,
|
||||||
Organized: true,
|
Organized: true,
|
||||||
|
|
@ -1653,7 +1662,13 @@ func TestGalleryQueryURL(t *testing.T) {
|
||||||
|
|
||||||
verifyFn := func(g *models.Gallery) {
|
verifyFn := func(g *models.Gallery) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
verifyString(t, g.URL, urlCriterion)
|
urls := g.URLs.List()
|
||||||
|
var url string
|
||||||
|
if len(urls) > 0 {
|
||||||
|
url = urls[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyString(t, url, urlCriterion)
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyGalleryQuery(t, filter, verifyFn)
|
verifyGalleryQuery(t, filter, verifyFn)
|
||||||
|
|
@ -1683,6 +1698,12 @@ func verifyGalleryQuery(t *testing.T, filter models.GalleryFilterType, verifyFn
|
||||||
|
|
||||||
galleries := queryGallery(ctx, t, sqb, &filter, nil)
|
galleries := queryGallery(ctx, t, sqb, &filter, nil)
|
||||||
|
|
||||||
|
for _, g := range galleries {
|
||||||
|
if err := g.LoadURLs(ctx, sqb); err != nil {
|
||||||
|
t.Errorf("Error loading gallery URLs: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// assume it should find at least one
|
// assume it should find at least one
|
||||||
assert.Greater(t, len(galleries), 0)
|
assert.Greater(t, len(galleries), 0)
|
||||||
|
|
||||||
|
|
|
||||||
76
pkg/sqlite/migrations/51_gallery_urls.up.sql
Normal file
76
pkg/sqlite/migrations/51_gallery_urls.up.sql
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
|
||||||
|
CREATE TABLE `gallery_urls` (
|
||||||
|
`gallery_id` integer NOT NULL,
|
||||||
|
`position` integer NOT NULL,
|
||||||
|
`url` varchar(255) NOT NULL,
|
||||||
|
foreign key(`gallery_id`) references `galleries`(`id`) on delete CASCADE,
|
||||||
|
PRIMARY KEY(`gallery_id`, `position`, `url`)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX `gallery_urls_url` on `gallery_urls` (`url`);
|
||||||
|
|
||||||
|
-- drop url
|
||||||
|
CREATE TABLE `galleries_new` (
|
||||||
|
`id` integer not null primary key autoincrement,
|
||||||
|
`folder_id` integer,
|
||||||
|
`title` varchar(255),
|
||||||
|
`date` date,
|
||||||
|
`details` text,
|
||||||
|
`studio_id` integer,
|
||||||
|
`rating` tinyint,
|
||||||
|
`organized` boolean not null default '0',
|
||||||
|
`created_at` datetime not null,
|
||||||
|
`updated_at` datetime not null,
|
||||||
|
foreign key(`studio_id`) references `studios`(`id`) on delete SET NULL,
|
||||||
|
foreign key(`folder_id`) references `folders`(`id`) on delete SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `galleries_new`
|
||||||
|
(
|
||||||
|
`id`,
|
||||||
|
`folder_id`,
|
||||||
|
`title`,
|
||||||
|
`date`,
|
||||||
|
`details`,
|
||||||
|
`studio_id`,
|
||||||
|
`rating`,
|
||||||
|
`organized`,
|
||||||
|
`created_at`,
|
||||||
|
`updated_at`
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
`id`,
|
||||||
|
`folder_id`,
|
||||||
|
`title`,
|
||||||
|
`date`,
|
||||||
|
`details`,
|
||||||
|
`studio_id`,
|
||||||
|
`rating`,
|
||||||
|
`organized`,
|
||||||
|
`created_at`,
|
||||||
|
`updated_at`
|
||||||
|
FROM `galleries`;
|
||||||
|
|
||||||
|
INSERT INTO `gallery_urls`
|
||||||
|
(
|
||||||
|
`gallery_id`,
|
||||||
|
`position`,
|
||||||
|
`url`
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
`id`,
|
||||||
|
'0',
|
||||||
|
`url`
|
||||||
|
FROM `galleries`
|
||||||
|
WHERE `galleries`.`url` IS NOT NULL AND `galleries`.`url` != '';
|
||||||
|
|
||||||
|
DROP INDEX `index_galleries_on_studio_id`;
|
||||||
|
DROP INDEX `index_galleries_on_folder_id_unique`;
|
||||||
|
DROP TABLE `galleries`;
|
||||||
|
ALTER TABLE `galleries_new` rename to `galleries`;
|
||||||
|
|
||||||
|
CREATE INDEX `index_galleries_on_studio_id` on `galleries` (`studio_id`);
|
||||||
|
CREATE UNIQUE INDEX `index_galleries_on_folder_id_unique` on `galleries` (`folder_id`);
|
||||||
|
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
|
@ -1291,6 +1291,9 @@ func sceneIsMissingCriterionHandler(qb *SceneStore, isMissing *string) criterion
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if isMissing != nil && *isMissing != "" {
|
if isMissing != nil && *isMissing != "" {
|
||||||
switch *isMissing {
|
switch *isMissing {
|
||||||
|
case "url":
|
||||||
|
scenesURLsTableMgr.join(f, "", "scenes.id")
|
||||||
|
f.addWhere("scene_urls.url IS NULL")
|
||||||
case "galleries":
|
case "galleries":
|
||||||
qb.galleriesRepository().join(f, "galleries_join", "scenes.id")
|
qb.galleriesRepository().join(f, "galleries_join", "scenes.id")
|
||||||
f.addWhere("galleries_join.scene_id IS NULL")
|
f.addWhere("galleries_join.scene_id IS NULL")
|
||||||
|
|
|
||||||
|
|
@ -1213,7 +1213,16 @@ func getGalleryNullStringValue(index int, field string) sql.NullString {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getGalleryNullStringPtr(index int, field string) *string {
|
func getGalleryNullStringPtr(index int, field string) *string {
|
||||||
return getStringPtr(getPrefixedStringValue("gallery", index, field))
|
return getStringPtrFromNullString(getPrefixedNullStringValue("gallery", index, field))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGalleryEmptyString(index int, field string) string {
|
||||||
|
v := getGalleryNullStringPtr(index, field)
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return *v
|
||||||
}
|
}
|
||||||
|
|
||||||
func getGalleryBasename(index int) string {
|
func getGalleryBasename(index int) string {
|
||||||
|
|
@ -1245,8 +1254,10 @@ func makeGallery(i int, includeScenes bool) *models.Gallery {
|
||||||
tids := indexesToIDs(tagIDs, galleryTags[i])
|
tids := indexesToIDs(tagIDs, galleryTags[i])
|
||||||
|
|
||||||
ret := &models.Gallery{
|
ret := &models.Gallery{
|
||||||
Title: getGalleryStringValue(i, titleField),
|
Title: getGalleryStringValue(i, titleField),
|
||||||
URL: getGalleryNullStringValue(i, urlField).String,
|
URLs: models.NewRelatedStrings([]string{
|
||||||
|
getGalleryEmptyString(i, urlField),
|
||||||
|
}),
|
||||||
Rating: getIntPtr(getRating(i)),
|
Rating: getIntPtr(getRating(i)),
|
||||||
Date: getObjectDate(i),
|
Date: getObjectDate(i),
|
||||||
StudioID: studioID,
|
StudioID: studioID,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ var (
|
||||||
galleriesTagsJoinTable = goqu.T(galleriesTagsTable)
|
galleriesTagsJoinTable = goqu.T(galleriesTagsTable)
|
||||||
performersGalleriesJoinTable = goqu.T(performersGalleriesTable)
|
performersGalleriesJoinTable = goqu.T(performersGalleriesTable)
|
||||||
galleriesScenesJoinTable = goqu.T(galleriesScenesTable)
|
galleriesScenesJoinTable = goqu.T(galleriesScenesTable)
|
||||||
|
galleriesURLsJoinTable = goqu.T(galleriesURLsTable)
|
||||||
|
|
||||||
scenesFilesJoinTable = goqu.T(scenesFilesTable)
|
scenesFilesJoinTable = goqu.T(scenesFilesTable)
|
||||||
scenesTagsJoinTable = goqu.T(scenesTagsTable)
|
scenesTagsJoinTable = goqu.T(scenesTagsTable)
|
||||||
|
|
@ -122,6 +123,14 @@ var (
|
||||||
table: goqu.T(galleriesChaptersTable),
|
table: goqu.T(galleriesChaptersTable),
|
||||||
idColumn: goqu.T(galleriesChaptersTable).Col(idColumn),
|
idColumn: goqu.T(galleriesChaptersTable).Col(idColumn),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
galleriesURLsTableMgr = &orderedValueTable[string]{
|
||||||
|
table: table{
|
||||||
|
table: galleriesURLsJoinTable,
|
||||||
|
idColumn: galleriesURLsJoinTable.Col(galleryIDColumn),
|
||||||
|
},
|
||||||
|
valueColumn: galleriesURLsJoinTable.Col(galleriesURLColumn),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import {
|
||||||
} from "src/components/Shared/Select";
|
} from "src/components/Shared/Select";
|
||||||
import { Icon } from "src/components/Shared/Icon";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { URLField } from "src/components/Shared/URLField";
|
import { URLListInput } from "src/components/Shared/URLField";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import FormUtils from "src/utils/form";
|
import FormUtils from "src/utils/form";
|
||||||
|
|
@ -42,6 +42,7 @@ import {
|
||||||
Performer,
|
Performer,
|
||||||
PerformerSelect,
|
PerformerSelect,
|
||||||
} from "src/components/Performers/PerformerSelect";
|
} from "src/components/Performers/PerformerSelect";
|
||||||
|
import { yupDateString, yupUniqueStringList } from "src/utils/yup";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
gallery: Partial<GQL.GalleryDataFragment>;
|
gallery: Partial<GQL.GalleryDataFragment>;
|
||||||
|
|
@ -84,20 +85,8 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
title: titleRequired ? yup.string().required() : yup.string().ensure(),
|
title: titleRequired ? yup.string().required() : yup.string().ensure(),
|
||||||
url: yup.string().ensure(),
|
urls: yupUniqueStringList("urls"),
|
||||||
date: yup
|
date: yupDateString(intl),
|
||||||
.string()
|
|
||||||
.ensure()
|
|
||||||
.test({
|
|
||||||
name: "date",
|
|
||||||
test: (value) => {
|
|
||||||
if (!value) return true;
|
|
||||||
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
|
||||||
if (Number.isNaN(Date.parse(value))) return false;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
|
||||||
}),
|
|
||||||
rating100: yup.number().nullable().defined(),
|
rating100: yup.number().nullable().defined(),
|
||||||
studio_id: yup.string().required().nullable(),
|
studio_id: yup.string().required().nullable(),
|
||||||
performer_ids: yup.array(yup.string().required()).defined(),
|
performer_ids: yup.array(yup.string().required()).defined(),
|
||||||
|
|
@ -108,7 +97,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
title: gallery?.title ?? "",
|
title: gallery?.title ?? "",
|
||||||
url: gallery?.url ?? "",
|
urls: gallery?.urls ?? [],
|
||||||
date: gallery?.date ?? "",
|
date: gallery?.date ?? "",
|
||||||
rating100: gallery?.rating100 ?? null,
|
rating100: gallery?.rating100 ?? null,
|
||||||
studio_id: gallery?.studio?.id ?? null,
|
studio_id: gallery?.studio?.id ?? null,
|
||||||
|
|
@ -313,8 +302,8 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||||
formik.setFieldValue("date", galleryData.date);
|
formik.setFieldValue("date", galleryData.date);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (galleryData.url) {
|
if (galleryData.urls) {
|
||||||
formik.setFieldValue("url", galleryData.url);
|
formik.setFieldValue("url", galleryData.urls);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (galleryData.studio?.stored_id) {
|
if (galleryData.studio?.stored_id) {
|
||||||
|
|
@ -351,13 +340,13 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onScrapeGalleryURL() {
|
async function onScrapeGalleryURL(url: string) {
|
||||||
if (!formik.values.url) {
|
if (!url) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await queryScrapeGalleryURL(formik.values.url);
|
const result = await queryScrapeGalleryURL(url);
|
||||||
if (!result || !result.data || !result.data.scrapeGalleryURL) {
|
if (!result || !result.data || !result.data.scrapeGalleryURL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -392,6 +381,14 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||||
|
|
||||||
if (isLoading) return <LoadingIndicator />;
|
if (isLoading) return <LoadingIndicator />;
|
||||||
|
|
||||||
|
const urlsErrors = Array.isArray(formik.errors.urls)
|
||||||
|
? formik.errors.urls[0]
|
||||||
|
: formik.errors.urls;
|
||||||
|
const urlsErrorMsg = urlsErrors
|
||||||
|
? intl.formatMessage({ id: "validation.urls_must_be_unique" })
|
||||||
|
: undefined;
|
||||||
|
const urlsErrorIdx = urlsErrors?.split(" ").map((e) => parseInt(e));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="gallery-edit-details">
|
<div id="gallery-edit-details">
|
||||||
<Prompt
|
<Prompt
|
||||||
|
|
@ -428,18 +425,20 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||||
<div className="form-container row px-3">
|
<div className="form-container row px-3">
|
||||||
<div className="col-12 col-lg-6 col-xl-12">
|
<div className="col-12 col-lg-6 col-xl-12">
|
||||||
{renderTextField("title", intl.formatMessage({ id: "title" }))}
|
{renderTextField("title", intl.formatMessage({ id: "title" }))}
|
||||||
<Form.Group controlId="url" as={Row}>
|
<Form.Group controlId="urls" as={Row}>
|
||||||
<Col xs={3} className="pr-0 url-label">
|
<Col xs={3} className="pr-0 url-label">
|
||||||
<Form.Label className="col-form-label">
|
<Form.Label className="col-form-label">
|
||||||
<FormattedMessage id="url" />
|
<FormattedMessage id="urls" />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<URLField
|
<URLListInput
|
||||||
{...formik.getFieldProps("url")}
|
value={formik.values.urls ?? []}
|
||||||
onScrapeClick={onScrapeGalleryURL}
|
setValue={(value) => formik.setFieldValue("urls", value)}
|
||||||
|
errors={urlsErrorMsg}
|
||||||
|
errorIdx={urlsErrorIdx}
|
||||||
|
onScrapeClick={(url) => onScrapeGalleryURL(url)}
|
||||||
urlScrapable={urlScrapable}
|
urlScrapable={urlScrapable}
|
||||||
isInvalid={!!formik.getFieldMeta("url").error}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import * as GQL from "src/core/generated-graphql";
|
||||||
import { mutateGallerySetPrimaryFile } from "src/core/StashService";
|
import { mutateGallerySetPrimaryFile } from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import TextUtils from "src/utils/text";
|
import TextUtils from "src/utils/text";
|
||||||
import { TextField, URLField } from "src/utils/field";
|
import { TextField, URLField, URLsField } from "src/utils/field";
|
||||||
|
|
||||||
interface IFileInfoPanelProps {
|
interface IFileInfoPanelProps {
|
||||||
folder?: Pick<GQL.Folder, "id" | "path">;
|
folder?: Pick<GQL.Folder, "id" | "path">;
|
||||||
|
|
@ -147,12 +147,7 @@ export const GalleryFileInfoPanel: React.FC<IGalleryFileInfoPanelProps> = (
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<dl className="container gallery-file-info details-list">
|
<dl className="container gallery-file-info details-list">
|
||||||
<URLField
|
<URLsField id="urls" urls={props.gallery.urls} truncate />
|
||||||
id="media_info.downloaded_from"
|
|
||||||
url={props.gallery.url}
|
|
||||||
value={props.gallery.url}
|
|
||||||
truncate
|
|
||||||
/>
|
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{filesPanel}
|
{filesPanel}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
ScrapeDialog,
|
ScrapeDialog,
|
||||||
ScrapedInputGroupRow,
|
ScrapedInputGroupRow,
|
||||||
|
ScrapedStringListRow,
|
||||||
ScrapedTextAreaRow,
|
ScrapedTextAreaRow,
|
||||||
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||||
import clone from "lodash-es/clone";
|
import clone from "lodash-es/clone";
|
||||||
|
|
@ -23,6 +24,7 @@ import {
|
||||||
useCreateScrapedStudio,
|
useCreateScrapedStudio,
|
||||||
useCreateScrapedTag,
|
useCreateScrapedTag,
|
||||||
} from "src/components/Shared/ScrapeDialog/createObjects";
|
} from "src/components/Shared/ScrapeDialog/createObjects";
|
||||||
|
import { uniq } from "lodash-es";
|
||||||
|
|
||||||
interface IGalleryScrapeDialogProps {
|
interface IGalleryScrapeDialogProps {
|
||||||
gallery: Partial<GQL.GalleryUpdateInput>;
|
gallery: Partial<GQL.GalleryUpdateInput>;
|
||||||
|
|
@ -36,29 +38,32 @@ interface IHasStoredID {
|
||||||
stored_id?: string | null;
|
stored_id?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = ({
|
||||||
props: IGalleryScrapeDialogProps
|
gallery,
|
||||||
) => {
|
galleryPerformers,
|
||||||
|
scraped,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [title, setTitle] = useState<ScrapeResult<string>>(
|
const [title, setTitle] = useState<ScrapeResult<string>>(
|
||||||
new ScrapeResult<string>(props.gallery.title, props.scraped.title)
|
new ScrapeResult<string>(gallery.title, scraped.title)
|
||||||
);
|
);
|
||||||
const [url, setURL] = useState<ScrapeResult<string>>(
|
const [urls, setURLs] = useState<ScrapeResult<string[]>>(
|
||||||
new ScrapeResult<string>(props.gallery.url, props.scraped.url)
|
new ScrapeResult<string[]>(
|
||||||
);
|
gallery.urls,
|
||||||
const [date, setDate] = useState<ScrapeResult<string>>(
|
scraped.urls
|
||||||
new ScrapeResult<string>(props.gallery.date, props.scraped.date)
|
? uniq((gallery.urls ?? []).concat(scraped.urls ?? []))
|
||||||
);
|
: undefined
|
||||||
const [studio, setStudio] = useState<ScrapeResult<string>>(
|
|
||||||
new ScrapeResult<string>(
|
|
||||||
props.gallery.studio_id,
|
|
||||||
props.scraped.studio?.stored_id
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
const [date, setDate] = useState<ScrapeResult<string>>(
|
||||||
|
new ScrapeResult<string>(gallery.date, scraped.date)
|
||||||
|
);
|
||||||
|
const [studio, setStudio] = useState<ScrapeResult<string>>(
|
||||||
|
new ScrapeResult<string>(gallery.studio_id, scraped.studio?.stored_id)
|
||||||
|
);
|
||||||
const [newStudio, setNewStudio] = useState<GQL.ScrapedStudio | undefined>(
|
const [newStudio, setNewStudio] = useState<GQL.ScrapedStudio | undefined>(
|
||||||
props.scraped.studio && !props.scraped.studio.stored_id
|
scraped.studio && !scraped.studio.stored_id ? scraped.studio : undefined
|
||||||
? props.scraped.studio
|
|
||||||
: undefined
|
|
||||||
);
|
);
|
||||||
|
|
||||||
function mapStoredIdObjects(
|
function mapStoredIdObjects(
|
||||||
|
|
@ -104,30 +109,30 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||||
>(
|
>(
|
||||||
new ObjectListScrapeResult<GQL.ScrapedPerformer>(
|
new ObjectListScrapeResult<GQL.ScrapedPerformer>(
|
||||||
sortStoredIdObjects(
|
sortStoredIdObjects(
|
||||||
props.galleryPerformers.map((p) => ({
|
galleryPerformers.map((p) => ({
|
||||||
stored_id: p.id,
|
stored_id: p.id,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
}))
|
}))
|
||||||
),
|
),
|
||||||
sortStoredIdObjects(props.scraped.performers ?? undefined)
|
sortStoredIdObjects(scraped.performers ?? undefined)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const [newPerformers, setNewPerformers] = useState<GQL.ScrapedPerformer[]>(
|
const [newPerformers, setNewPerformers] = useState<GQL.ScrapedPerformer[]>(
|
||||||
props.scraped.performers?.filter((t) => !t.stored_id) ?? []
|
scraped.performers?.filter((t) => !t.stored_id) ?? []
|
||||||
);
|
);
|
||||||
|
|
||||||
const [tags, setTags] = useState<ScrapeResult<string[]>>(
|
const [tags, setTags] = useState<ScrapeResult<string[]>>(
|
||||||
new ScrapeResult<string[]>(
|
new ScrapeResult<string[]>(
|
||||||
sortIdList(props.gallery.tag_ids),
|
sortIdList(gallery.tag_ids),
|
||||||
mapStoredIdObjects(props.scraped.tags ?? undefined)
|
mapStoredIdObjects(scraped.tags ?? undefined)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const [newTags, setNewTags] = useState<GQL.ScrapedTag[]>(
|
const [newTags, setNewTags] = useState<GQL.ScrapedTag[]>(
|
||||||
props.scraped.tags?.filter((t) => !t.stored_id) ?? []
|
scraped.tags?.filter((t) => !t.stored_id) ?? []
|
||||||
);
|
);
|
||||||
|
|
||||||
const [details, setDetails] = useState<ScrapeResult<string>>(
|
const [details, setDetails] = useState<ScrapeResult<string>>(
|
||||||
new ScrapeResult<string>(props.gallery.details, props.scraped.details)
|
new ScrapeResult<string>(gallery.details, scraped.details)
|
||||||
);
|
);
|
||||||
|
|
||||||
const createNewStudio = useCreateScrapedStudio({
|
const createNewStudio = useCreateScrapedStudio({
|
||||||
|
|
@ -152,14 +157,14 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||||
|
|
||||||
// don't show the dialog if nothing was scraped
|
// don't show the dialog if nothing was scraped
|
||||||
if (
|
if (
|
||||||
[title, url, date, studio, performers, tags, details].every(
|
[title, urls, date, studio, performers, tags, details].every(
|
||||||
(r) => !r.scraped
|
(r) => !r.scraped
|
||||||
) &&
|
) &&
|
||||||
!newStudio &&
|
!newStudio &&
|
||||||
newPerformers.length === 0 &&
|
newPerformers.length === 0 &&
|
||||||
newTags.length === 0
|
newTags.length === 0
|
||||||
) {
|
) {
|
||||||
props.onClose();
|
onClose();
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,7 +173,7 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: title.getNewValue(),
|
title: title.getNewValue(),
|
||||||
url: url.getNewValue(),
|
urls: urls.getNewValue(),
|
||||||
date: date.getNewValue(),
|
date: date.getNewValue(),
|
||||||
studio: newStudioValue
|
studio: newStudioValue
|
||||||
? {
|
? {
|
||||||
|
|
@ -195,10 +200,10 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||||
result={title}
|
result={title}
|
||||||
onChange={(value) => setTitle(value)}
|
onChange={(value) => setTitle(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedStringListRow
|
||||||
title={intl.formatMessage({ id: "url" })}
|
title={intl.formatMessage({ id: "urls" })}
|
||||||
result={url}
|
result={urls}
|
||||||
onChange={(value) => setURL(value)}
|
onChange={(value) => setURLs(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title={intl.formatMessage({ id: "date" })}
|
title={intl.formatMessage({ id: "date" })}
|
||||||
|
|
@ -244,7 +249,7 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||||
)}
|
)}
|
||||||
renderScrapeRows={renderScrapeRows}
|
renderScrapeRows={renderScrapeRows}
|
||||||
onClose={(apply) => {
|
onClose={(apply) => {
|
||||||
props.onClose(apply ? makeNewScrapedItem() : undefined);
|
onClose(apply ? makeNewScrapedItem() : undefined);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue