mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
Add Details, Studio Code, and Photographer to Images (#4217)
* Add Details, Code, and Photographer to Images * Add date and details to image card --------- Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
@@ -1,8 +1,11 @@
|
|||||||
fragment SlimImageData on Image {
|
fragment SlimImageData on Image {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
|
code
|
||||||
date
|
date
|
||||||
urls
|
urls
|
||||||
|
details
|
||||||
|
photographer
|
||||||
rating100
|
rating100
|
||||||
organized
|
organized
|
||||||
o_counter
|
o_counter
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
fragment ImageData on Image {
|
fragment ImageData on Image {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
|
code
|
||||||
rating100
|
rating100
|
||||||
date
|
date
|
||||||
urls
|
urls
|
||||||
|
details
|
||||||
|
photographer
|
||||||
organized
|
organized
|
||||||
o_counter
|
o_counter
|
||||||
created_at
|
created_at
|
||||||
|
|||||||
@@ -443,6 +443,7 @@ input ImageFilterType {
|
|||||||
NOT: ImageFilterType
|
NOT: ImageFilterType
|
||||||
|
|
||||||
title: StringCriterionInput
|
title: StringCriterionInput
|
||||||
|
details: StringCriterionInput
|
||||||
|
|
||||||
" Filter by image id"
|
" Filter by image id"
|
||||||
id: IntCriterionInput
|
id: IntCriterionInput
|
||||||
@@ -486,6 +487,10 @@ input ImageFilterType {
|
|||||||
created_at: TimestampCriterionInput
|
created_at: TimestampCriterionInput
|
||||||
"Filter by last update time"
|
"Filter by last update time"
|
||||||
updated_at: TimestampCriterionInput
|
updated_at: TimestampCriterionInput
|
||||||
|
"Filter by studio code"
|
||||||
|
code: StringCriterionInput
|
||||||
|
"Filter by photographer"
|
||||||
|
photographer: StringCriterionInput
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CriterionModifier {
|
enum CriterionModifier {
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
type Image {
|
type Image {
|
||||||
id: ID!
|
id: ID!
|
||||||
title: String
|
title: String
|
||||||
|
code: String
|
||||||
# rating expressed as 1-100
|
# rating expressed as 1-100
|
||||||
rating100: Int
|
rating100: Int
|
||||||
url: String @deprecated(reason: "Use urls")
|
url: String @deprecated(reason: "Use urls")
|
||||||
urls: [String!]!
|
urls: [String!]!
|
||||||
date: String
|
date: String
|
||||||
|
details: String
|
||||||
|
photographer: String
|
||||||
o_counter: Int
|
o_counter: Int
|
||||||
organized: Boolean!
|
organized: Boolean!
|
||||||
created_at: Time!
|
created_at: Time!
|
||||||
@@ -37,12 +40,15 @@ input ImageUpdateInput {
|
|||||||
clientMutationId: String
|
clientMutationId: String
|
||||||
id: ID!
|
id: ID!
|
||||||
title: String
|
title: String
|
||||||
|
code: String
|
||||||
# rating expressed as 1-100
|
# rating expressed as 1-100
|
||||||
rating100: Int
|
rating100: Int
|
||||||
organized: Boolean
|
organized: Boolean
|
||||||
url: String @deprecated(reason: "Use urls")
|
url: String @deprecated(reason: "Use urls")
|
||||||
urls: [String!]
|
urls: [String!]
|
||||||
date: String
|
date: String
|
||||||
|
details: String
|
||||||
|
photographer: String
|
||||||
|
|
||||||
studio_id: ID
|
studio_id: ID
|
||||||
performer_ids: [ID!]
|
performer_ids: [ID!]
|
||||||
@@ -56,12 +62,15 @@ input BulkImageUpdateInput {
|
|||||||
clientMutationId: String
|
clientMutationId: String
|
||||||
ids: [ID!]
|
ids: [ID!]
|
||||||
title: String
|
title: String
|
||||||
|
code: String
|
||||||
# rating expressed as 1-100
|
# rating expressed as 1-100
|
||||||
rating100: Int
|
rating100: Int
|
||||||
organized: Boolean
|
organized: Boolean
|
||||||
url: String @deprecated(reason: "Use urls")
|
url: String @deprecated(reason: "Use urls")
|
||||||
urls: BulkUpdateStrings
|
urls: BulkUpdateStrings
|
||||||
date: String
|
date: String
|
||||||
|
details: String
|
||||||
|
photographer: String
|
||||||
|
|
||||||
studio_id: ID
|
studio_id: ID
|
||||||
performer_ids: BulkUpdateIds
|
performer_ids: BulkUpdateIds
|
||||||
|
|||||||
@@ -107,6 +107,9 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp
|
|||||||
updatedImage := models.NewImagePartial()
|
updatedImage := models.NewImagePartial()
|
||||||
|
|
||||||
updatedImage.Title = translator.optionalString(input.Title, "title")
|
updatedImage.Title = translator.optionalString(input.Title, "title")
|
||||||
|
updatedImage.Code = translator.optionalString(input.Code, "code")
|
||||||
|
updatedImage.Details = translator.optionalString(input.Details, "details")
|
||||||
|
updatedImage.Photographer = translator.optionalString(input.Photographer, "photographer")
|
||||||
updatedImage.Rating = translator.optionalInt(input.Rating100, "rating100")
|
updatedImage.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||||
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")
|
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")
|
||||||
|
|
||||||
@@ -203,6 +206,9 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
|
|||||||
updatedImage := models.NewImagePartial()
|
updatedImage := models.NewImagePartial()
|
||||||
|
|
||||||
updatedImage.Title = translator.optionalString(input.Title, "title")
|
updatedImage.Title = translator.optionalString(input.Title, "title")
|
||||||
|
updatedImage.Code = translator.optionalString(input.Code, "code")
|
||||||
|
updatedImage.Details = translator.optionalString(input.Details, "details")
|
||||||
|
updatedImage.Photographer = translator.optionalString(input.Photographer, "photographer")
|
||||||
updatedImage.Rating = translator.optionalInt(input.Rating100, "rating100")
|
updatedImage.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||||
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")
|
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ import (
|
|||||||
// of cover image.
|
// of cover image.
|
||||||
func ToBasicJSON(image *models.Image) *jsonschema.Image {
|
func ToBasicJSON(image *models.Image) *jsonschema.Image {
|
||||||
newImageJSON := jsonschema.Image{
|
newImageJSON := jsonschema.Image{
|
||||||
Title: image.Title,
|
Title: image.Title,
|
||||||
URLs: image.URLs.List(),
|
Code: image.Code,
|
||||||
CreatedAt: json.JSONTime{Time: image.CreatedAt},
|
URLs: image.URLs.List(),
|
||||||
UpdatedAt: json.JSONTime{Time: image.UpdatedAt},
|
Details: image.Details,
|
||||||
|
Photographer: image.Photographer,
|
||||||
|
CreatedAt: json.JSONTime{Time: image.CreatedAt},
|
||||||
|
UpdatedAt: json.JSONTime{Time: image.UpdatedAt},
|
||||||
}
|
}
|
||||||
|
|
||||||
if image.Rating != nil {
|
if image.Rating != nil {
|
||||||
|
|||||||
@@ -76,6 +76,15 @@ func (i *Importer) imageJSONToImage(imageJSON jsonschema.Image) models.Image {
|
|||||||
if imageJSON.Title != "" {
|
if imageJSON.Title != "" {
|
||||||
newImage.Title = imageJSON.Title
|
newImage.Title = imageJSON.Title
|
||||||
}
|
}
|
||||||
|
if imageJSON.Code != "" {
|
||||||
|
newImage.Code = imageJSON.Code
|
||||||
|
}
|
||||||
|
if imageJSON.Details != "" {
|
||||||
|
newImage.Details = imageJSON.Details
|
||||||
|
}
|
||||||
|
if imageJSON.Photographer != "" {
|
||||||
|
newImage.Photographer = imageJSON.Photographer
|
||||||
|
}
|
||||||
if imageJSON.Rating != 0 {
|
if imageJSON.Rating != 0 {
|
||||||
newImage.Rating = &imageJSON.Rating
|
newImage.Rating = &imageJSON.Rating
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ package models
|
|||||||
import "context"
|
import "context"
|
||||||
|
|
||||||
type ImageFilterType struct {
|
type ImageFilterType struct {
|
||||||
And *ImageFilterType `json:"AND"`
|
And *ImageFilterType `json:"AND"`
|
||||||
Or *ImageFilterType `json:"OR"`
|
Or *ImageFilterType `json:"OR"`
|
||||||
Not *ImageFilterType `json:"NOT"`
|
Not *ImageFilterType `json:"NOT"`
|
||||||
ID *IntCriterionInput `json:"id"`
|
ID *IntCriterionInput `json:"id"`
|
||||||
Title *StringCriterionInput `json:"title"`
|
Title *StringCriterionInput `json:"title"`
|
||||||
|
Code *StringCriterionInput `json:"code"`
|
||||||
|
Details *StringCriterionInput `json:"details"`
|
||||||
|
Photographer *StringCriterionInput `json:"photographer"`
|
||||||
// Filter by file checksum
|
// Filter by file checksum
|
||||||
Checksum *StringCriterionInput `json:"checksum"`
|
Checksum *StringCriterionInput `json:"checksum"`
|
||||||
// Filter by path
|
// Filter by path
|
||||||
|
|||||||
@@ -11,22 +11,25 @@ import (
|
|||||||
|
|
||||||
type Image struct {
|
type Image struct {
|
||||||
Title string `json:"title,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
|
Code string `json:"code,omitempty"`
|
||||||
Studio string `json:"studio,omitempty"`
|
Studio string `json:"studio,omitempty"`
|
||||||
Rating int `json:"rating,omitempty"`
|
Rating int `json:"rating,omitempty"`
|
||||||
|
|
||||||
// deprecated - for import only
|
// deprecated - for import only
|
||||||
URL string `json:"url,omitempty"`
|
URL string `json:"url,omitempty"`
|
||||||
|
|
||||||
URLs []string `json:"urls,omitempty"`
|
URLs []string `json:"urls,omitempty"`
|
||||||
Date string `json:"date,omitempty"`
|
Date string `json:"date,omitempty"`
|
||||||
Organized bool `json:"organized,omitempty"`
|
Details string `json:"details,omitempty"`
|
||||||
OCounter int `json:"o_counter,omitempty"`
|
Photographer string `json:"photographer,omitempty"`
|
||||||
Galleries []GalleryRef `json:"galleries,omitempty"`
|
Organized bool `json:"organized,omitempty"`
|
||||||
Performers []string `json:"performers,omitempty"`
|
OCounter int `json:"o_counter,omitempty"`
|
||||||
Tags []string `json:"tags,omitempty"`
|
Galleries []GalleryRef `json:"galleries,omitempty"`
|
||||||
Files []string `json:"files,omitempty"`
|
Performers []string `json:"performers,omitempty"`
|
||||||
CreatedAt json.JSONTime `json:"created_at,omitempty"`
|
Tags []string `json:"tags,omitempty"`
|
||||||
UpdatedAt json.JSONTime `json:"updated_at,omitempty"`
|
Files []string `json:"files,omitempty"`
|
||||||
|
CreatedAt json.JSONTime `json:"created_at,omitempty"`
|
||||||
|
UpdatedAt json.JSONTime `json:"updated_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Image) Filename(basename string, hash string) string {
|
func (s Image) Filename(basename string, hash string) string {
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import (
|
|||||||
type Image struct {
|
type Image struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
|
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Details string `json:"details"`
|
||||||
|
Photographer string `json:"photographer"`
|
||||||
// Rating expressed in 1-100 scale
|
// Rating expressed in 1-100 scale
|
||||||
Rating *int `json:"rating"`
|
Rating *int `json:"rating"`
|
||||||
Organized bool `json:"organized"`
|
Organized bool `json:"organized"`
|
||||||
@@ -46,15 +49,18 @@ func NewImage() Image {
|
|||||||
|
|
||||||
type ImagePartial struct {
|
type ImagePartial struct {
|
||||||
Title OptionalString
|
Title OptionalString
|
||||||
|
Code OptionalString
|
||||||
// Rating expressed in 1-100 scale
|
// Rating expressed in 1-100 scale
|
||||||
Rating OptionalInt
|
Rating OptionalInt
|
||||||
URLs *UpdateStrings
|
URLs *UpdateStrings
|
||||||
Date OptionalDate
|
Date OptionalDate
|
||||||
Organized OptionalBool
|
Details OptionalString
|
||||||
OCounter OptionalInt
|
Photographer OptionalString
|
||||||
StudioID OptionalInt
|
Organized OptionalBool
|
||||||
CreatedAt OptionalTime
|
OCounter OptionalInt
|
||||||
UpdatedAt OptionalTime
|
StudioID OptionalInt
|
||||||
|
CreatedAt OptionalTime
|
||||||
|
UpdatedAt OptionalTime
|
||||||
|
|
||||||
GalleryIDs *UpdateIDs
|
GalleryIDs *UpdateIDs
|
||||||
TagIDs *UpdateIDs
|
TagIDs *UpdateIDs
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const (
|
|||||||
dbConnTimeout = 30
|
dbConnTimeout = 30
|
||||||
)
|
)
|
||||||
|
|
||||||
var appSchemaVersion uint = 53
|
var appSchemaVersion uint = 54
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*.sql
|
||||||
var migrationsBox embed.FS
|
var migrationsBox embed.FS
|
||||||
|
|||||||
@@ -31,21 +31,27 @@ const (
|
|||||||
type imageRow struct {
|
type imageRow struct {
|
||||||
ID int `db:"id" goqu:"skipinsert"`
|
ID int `db:"id" goqu:"skipinsert"`
|
||||||
Title zero.String `db:"title"`
|
Title zero.String `db:"title"`
|
||||||
|
Code zero.String `db:"code"`
|
||||||
// expressed as 1-100
|
// expressed as 1-100
|
||||||
Rating null.Int `db:"rating"`
|
Rating null.Int `db:"rating"`
|
||||||
Date NullDate `db:"date"`
|
Date NullDate `db:"date"`
|
||||||
Organized bool `db:"organized"`
|
Details zero.String `db:"details"`
|
||||||
OCounter int `db:"o_counter"`
|
Photographer zero.String `db:"photographer"`
|
||||||
StudioID null.Int `db:"studio_id,omitempty"`
|
Organized bool `db:"organized"`
|
||||||
CreatedAt Timestamp `db:"created_at"`
|
OCounter int `db:"o_counter"`
|
||||||
UpdatedAt Timestamp `db:"updated_at"`
|
StudioID null.Int `db:"studio_id,omitempty"`
|
||||||
|
CreatedAt Timestamp `db:"created_at"`
|
||||||
|
UpdatedAt Timestamp `db:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *imageRow) fromImage(i models.Image) {
|
func (r *imageRow) fromImage(i models.Image) {
|
||||||
r.ID = i.ID
|
r.ID = i.ID
|
||||||
r.Title = zero.StringFrom(i.Title)
|
r.Title = zero.StringFrom(i.Title)
|
||||||
|
r.Code = zero.StringFrom(i.Code)
|
||||||
r.Rating = intFromPtr(i.Rating)
|
r.Rating = intFromPtr(i.Rating)
|
||||||
r.Date = NullDateFromDatePtr(i.Date)
|
r.Date = NullDateFromDatePtr(i.Date)
|
||||||
|
r.Details = zero.StringFrom(i.Details)
|
||||||
|
r.Photographer = zero.StringFrom(i.Photographer)
|
||||||
r.Organized = i.Organized
|
r.Organized = i.Organized
|
||||||
r.OCounter = i.OCounter
|
r.OCounter = i.OCounter
|
||||||
r.StudioID = intFromPtr(i.StudioID)
|
r.StudioID = intFromPtr(i.StudioID)
|
||||||
@@ -63,13 +69,16 @@ type imageQueryRow struct {
|
|||||||
|
|
||||||
func (r *imageQueryRow) resolve() *models.Image {
|
func (r *imageQueryRow) resolve() *models.Image {
|
||||||
ret := &models.Image{
|
ret := &models.Image{
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
Title: r.Title.String,
|
Title: r.Title.String,
|
||||||
Rating: nullIntPtr(r.Rating),
|
Code: r.Code.String,
|
||||||
Date: r.Date.DatePtr(),
|
Rating: nullIntPtr(r.Rating),
|
||||||
Organized: r.Organized,
|
Date: r.Date.DatePtr(),
|
||||||
OCounter: r.OCounter,
|
Details: r.Details.String,
|
||||||
StudioID: nullIntPtr(r.StudioID),
|
Photographer: r.Photographer.String,
|
||||||
|
Organized: r.Organized,
|
||||||
|
OCounter: r.OCounter,
|
||||||
|
StudioID: nullIntPtr(r.StudioID),
|
||||||
|
|
||||||
PrimaryFileID: nullIntFileIDPtr(r.PrimaryFileID),
|
PrimaryFileID: nullIntFileIDPtr(r.PrimaryFileID),
|
||||||
Checksum: r.PrimaryFileChecksum.String,
|
Checksum: r.PrimaryFileChecksum.String,
|
||||||
@@ -91,8 +100,11 @@ type imageRowRecord struct {
|
|||||||
|
|
||||||
func (r *imageRowRecord) fromPartial(i models.ImagePartial) {
|
func (r *imageRowRecord) fromPartial(i models.ImagePartial) {
|
||||||
r.setNullString("title", i.Title)
|
r.setNullString("title", i.Title)
|
||||||
|
r.setNullString("code", i.Code)
|
||||||
r.setNullInt("rating", i.Rating)
|
r.setNullInt("rating", i.Rating)
|
||||||
r.setNullDate("date", i.Date)
|
r.setNullDate("date", i.Date)
|
||||||
|
r.setNullString("details", i.Details)
|
||||||
|
r.setNullString("photographer", i.Photographer)
|
||||||
r.setBool("organized", i.Organized)
|
r.setBool("organized", i.Organized)
|
||||||
r.setInt("o_counter", i.OCounter)
|
r.setInt("o_counter", i.OCounter)
|
||||||
r.setNullInt("studio_id", i.StudioID)
|
r.setNullInt("studio_id", i.StudioID)
|
||||||
@@ -672,6 +684,9 @@ func (qb *ImageStore) makeFilter(ctx context.Context, imageFilter *models.ImageF
|
|||||||
stringCriterionHandler(imageFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
|
stringCriterionHandler(imageFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
|
||||||
}))
|
}))
|
||||||
query.handleCriterion(ctx, stringCriterionHandler(imageFilter.Title, "images.title"))
|
query.handleCriterion(ctx, stringCriterionHandler(imageFilter.Title, "images.title"))
|
||||||
|
query.handleCriterion(ctx, stringCriterionHandler(imageFilter.Code, "images.code"))
|
||||||
|
query.handleCriterion(ctx, stringCriterionHandler(imageFilter.Details, "images.details"))
|
||||||
|
query.handleCriterion(ctx, stringCriterionHandler(imageFilter.Photographer, "images.photographer"))
|
||||||
|
|
||||||
query.handleCriterion(ctx, pathCriterionHandler(imageFilter.Path, "folders.path", "files.basename", qb.addFoldersTable))
|
query.handleCriterion(ctx, pathCriterionHandler(imageFilter.Path, "folders.path", "files.basename", qb.addFoldersTable))
|
||||||
query.handleCriterion(ctx, imageFileCountCriterionHandler(qb, imageFilter.FileCount))
|
query.handleCriterion(ctx, imageFileCountCriterionHandler(qb, imageFilter.FileCount))
|
||||||
|
|||||||
@@ -57,13 +57,16 @@ func loadImageRelationships(ctx context.Context, expected models.Image, actual *
|
|||||||
|
|
||||||
func Test_imageQueryBuilder_Create(t *testing.T) {
|
func Test_imageQueryBuilder_Create(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
title = "title"
|
title = "title"
|
||||||
rating = 60
|
code = "code"
|
||||||
ocounter = 5
|
rating = 60
|
||||||
url = "url"
|
details = "details"
|
||||||
date, _ = models.ParseDate("2003-02-01")
|
photographer = "photographer"
|
||||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
ocounter = 5
|
||||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
url = "url"
|
||||||
|
date, _ = models.ParseDate("2003-02-01")
|
||||||
|
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
imageFile = makeFileWithID(fileIdxStartImageFiles)
|
imageFile = makeFileWithID(fileIdxStartImageFiles)
|
||||||
)
|
)
|
||||||
@@ -77,8 +80,11 @@ func Test_imageQueryBuilder_Create(t *testing.T) {
|
|||||||
"full",
|
"full",
|
||||||
models.Image{
|
models.Image{
|
||||||
Title: title,
|
Title: title,
|
||||||
|
Code: code,
|
||||||
Rating: &rating,
|
Rating: &rating,
|
||||||
Date: &date,
|
Date: &date,
|
||||||
|
Details: details,
|
||||||
|
Photographer: photographer,
|
||||||
URLs: models.NewRelatedStrings([]string{url}),
|
URLs: models.NewRelatedStrings([]string{url}),
|
||||||
Organized: true,
|
Organized: true,
|
||||||
OCounter: ocounter,
|
OCounter: ocounter,
|
||||||
@@ -94,13 +100,16 @@ func Test_imageQueryBuilder_Create(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"with file",
|
"with file",
|
||||||
models.Image{
|
models.Image{
|
||||||
Title: title,
|
Title: title,
|
||||||
Rating: &rating,
|
Code: code,
|
||||||
Date: &date,
|
Rating: &rating,
|
||||||
URLs: models.NewRelatedStrings([]string{url}),
|
Date: &date,
|
||||||
Organized: true,
|
Details: details,
|
||||||
OCounter: ocounter,
|
Photographer: photographer,
|
||||||
StudioID: &studioIDs[studioIdxWithImage],
|
URLs: models.NewRelatedStrings([]string{url}),
|
||||||
|
Organized: true,
|
||||||
|
OCounter: ocounter,
|
||||||
|
StudioID: &studioIDs[studioIdxWithImage],
|
||||||
Files: models.NewRelatedFiles([]models.File{
|
Files: models.NewRelatedFiles([]models.File{
|
||||||
imageFile.(*models.ImageFile),
|
imageFile.(*models.ImageFile),
|
||||||
}),
|
}),
|
||||||
@@ -214,13 +223,16 @@ func makeImageFileWithID(i int) *models.ImageFile {
|
|||||||
|
|
||||||
func Test_imageQueryBuilder_Update(t *testing.T) {
|
func Test_imageQueryBuilder_Update(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
title = "title"
|
title = "title"
|
||||||
rating = 60
|
code = "code"
|
||||||
url = "url"
|
rating = 60
|
||||||
date, _ = models.ParseDate("2003-02-01")
|
url = "url"
|
||||||
ocounter = 5
|
details = "details"
|
||||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
photographer = "photographer"
|
||||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
date, _ = models.ParseDate("2003-02-01")
|
||||||
|
ocounter = 5
|
||||||
|
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
)
|
)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -233,9 +245,12 @@ func Test_imageQueryBuilder_Update(t *testing.T) {
|
|||||||
&models.Image{
|
&models.Image{
|
||||||
ID: imageIDs[imageIdxWithGallery],
|
ID: imageIDs[imageIdxWithGallery],
|
||||||
Title: title,
|
Title: title,
|
||||||
|
Code: code,
|
||||||
Rating: &rating,
|
Rating: &rating,
|
||||||
URLs: models.NewRelatedStrings([]string{url}),
|
URLs: models.NewRelatedStrings([]string{url}),
|
||||||
Date: &date,
|
Date: &date,
|
||||||
|
Details: details,
|
||||||
|
Photographer: photographer,
|
||||||
Organized: true,
|
Organized: true,
|
||||||
OCounter: ocounter,
|
OCounter: ocounter,
|
||||||
StudioID: &studioIDs[studioIdxWithImage],
|
StudioID: &studioIDs[studioIdxWithImage],
|
||||||
@@ -382,6 +397,9 @@ func clearImagePartial() models.ImagePartial {
|
|||||||
// leave mandatory fields
|
// leave mandatory fields
|
||||||
return models.ImagePartial{
|
return models.ImagePartial{
|
||||||
Title: models.OptionalString{Set: true, Null: true},
|
Title: models.OptionalString{Set: true, Null: true},
|
||||||
|
Code: models.OptionalString{Set: true, Null: true},
|
||||||
|
Details: models.OptionalString{Set: true, Null: true},
|
||||||
|
Photographer: models.OptionalString{Set: true, Null: true},
|
||||||
Rating: models.OptionalInt{Set: true, Null: true},
|
Rating: models.OptionalInt{Set: true, Null: true},
|
||||||
URLs: &models.UpdateStrings{Mode: models.RelationshipUpdateModeSet},
|
URLs: &models.UpdateStrings{Mode: models.RelationshipUpdateModeSet},
|
||||||
Date: models.OptionalDate{Set: true, Null: true},
|
Date: models.OptionalDate{Set: true, Null: true},
|
||||||
@@ -394,13 +412,16 @@ func clearImagePartial() models.ImagePartial {
|
|||||||
|
|
||||||
func Test_imageQueryBuilder_UpdatePartial(t *testing.T) {
|
func Test_imageQueryBuilder_UpdatePartial(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
title = "title"
|
title = "title"
|
||||||
rating = 60
|
code = "code"
|
||||||
url = "url"
|
details = "details"
|
||||||
date, _ = models.ParseDate("2003-02-01")
|
photographer = "photographer"
|
||||||
ocounter = 5
|
rating = 60
|
||||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
url = "url"
|
||||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
date, _ = models.ParseDate("2003-02-01")
|
||||||
|
ocounter = 5
|
||||||
|
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
)
|
)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -414,8 +435,11 @@ func Test_imageQueryBuilder_UpdatePartial(t *testing.T) {
|
|||||||
"full",
|
"full",
|
||||||
imageIDs[imageIdx1WithGallery],
|
imageIDs[imageIdx1WithGallery],
|
||||||
models.ImagePartial{
|
models.ImagePartial{
|
||||||
Title: models.NewOptionalString(title),
|
Title: models.NewOptionalString(title),
|
||||||
Rating: models.NewOptionalInt(rating),
|
Code: models.NewOptionalString(code),
|
||||||
|
Details: models.NewOptionalString(details),
|
||||||
|
Photographer: models.NewOptionalString(photographer),
|
||||||
|
Rating: models.NewOptionalInt(rating),
|
||||||
URLs: &models.UpdateStrings{
|
URLs: &models.UpdateStrings{
|
||||||
Values: []string{url},
|
Values: []string{url},
|
||||||
Mode: models.RelationshipUpdateModeSet,
|
Mode: models.RelationshipUpdateModeSet,
|
||||||
@@ -440,14 +464,17 @@ func Test_imageQueryBuilder_UpdatePartial(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
models.Image{
|
models.Image{
|
||||||
ID: imageIDs[imageIdx1WithGallery],
|
ID: imageIDs[imageIdx1WithGallery],
|
||||||
Title: title,
|
Title: title,
|
||||||
Rating: &rating,
|
Code: code,
|
||||||
URLs: models.NewRelatedStrings([]string{url}),
|
Details: details,
|
||||||
Date: &date,
|
Photographer: photographer,
|
||||||
Organized: true,
|
Rating: &rating,
|
||||||
OCounter: ocounter,
|
URLs: models.NewRelatedStrings([]string{url}),
|
||||||
StudioID: &studioIDs[studioIdxWithImage],
|
Date: &date,
|
||||||
|
Organized: true,
|
||||||
|
OCounter: ocounter,
|
||||||
|
StudioID: &studioIDs[studioIdxWithImage],
|
||||||
Files: models.NewRelatedFiles([]models.File{
|
Files: models.NewRelatedFiles([]models.File{
|
||||||
makeImageFile(imageIdx1WithGallery),
|
makeImageFile(imageIdx1WithGallery),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE `images` ADD COLUMN `code` text;
|
||||||
|
ALTER TABLE `images` ADD COLUMN `photographer` text;
|
||||||
|
ALTER TABLE `images` ADD COLUMN `details` text;
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
faTag,
|
faTag,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { objectTitle } from "src/core/files";
|
import { objectTitle } from "src/core/files";
|
||||||
|
import { TruncatedText } from "../Shared/TruncatedText";
|
||||||
|
|
||||||
interface IImageCardProps {
|
interface IImageCardProps {
|
||||||
image: GQL.SlimImageDataFragment;
|
image: GQL.SlimImageDataFragment;
|
||||||
@@ -175,6 +176,16 @@ export const ImageCard: React.FC<IImageCardProps> = (
|
|||||||
<RatingBanner rating={props.image.rating100} />
|
<RatingBanner rating={props.image.rating100} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
details={
|
||||||
|
<div className="image-card__details">
|
||||||
|
<span className="image-card__date">{props.image.date}</span>
|
||||||
|
<TruncatedText
|
||||||
|
className="image-card__description"
|
||||||
|
text={props.image.details}
|
||||||
|
lineCount={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
popovers={maybeRenderPopoverButtonGroup()}
|
popovers={maybeRenderPopoverButtonGroup()}
|
||||||
selected={props.selected}
|
selected={props.selected}
|
||||||
selecting={props.selecting}
|
selecting={props.selecting}
|
||||||
|
|||||||
@@ -21,6 +21,18 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = (props) => {
|
|||||||
[props.image]
|
[props.image]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function renderDetails() {
|
||||||
|
if (!props.image.details) return;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h6>
|
||||||
|
<FormattedMessage id="details" />:{" "}
|
||||||
|
</h6>
|
||||||
|
<p className="pre">{props.image.details}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function renderTags() {
|
function renderTags() {
|
||||||
if (props.image.tags.length === 0) return;
|
if (props.image.tags.length === 0) return;
|
||||||
const tags = props.image.tags.map((tag) => (
|
const tags = props.image.tags.map((tag) => (
|
||||||
@@ -135,6 +147,16 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = (props) => {
|
|||||||
{TextUtils.formatDateTime(intl, props.image.updated_at)}{" "}
|
{TextUtils.formatDateTime(intl, props.image.updated_at)}{" "}
|
||||||
</h6>
|
</h6>
|
||||||
}
|
}
|
||||||
|
{props.image.code && (
|
||||||
|
<h6>
|
||||||
|
<FormattedMessage id="scene_code" />: {props.image.code}{" "}
|
||||||
|
</h6>
|
||||||
|
)}
|
||||||
|
{props.image.photographer && (
|
||||||
|
<h6>
|
||||||
|
<FormattedMessage id="photographer" />: {props.image.photographer}{" "}
|
||||||
|
</h6>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{props.image.studio && (
|
{props.image.studio && (
|
||||||
<div className="col-3 d-xl-none">
|
<div className="col-3 d-xl-none">
|
||||||
@@ -150,6 +172,7 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
|
{renderDetails()}
|
||||||
{renderTags()}
|
{renderTags()}
|
||||||
{renderPerformers()}
|
{renderPerformers()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,8 +48,11 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
title: yup.string().ensure(),
|
title: yup.string().ensure(),
|
||||||
|
code: yup.string().ensure(),
|
||||||
urls: yupUniqueStringList("urls"),
|
urls: yupUniqueStringList("urls"),
|
||||||
date: yupDateString(intl),
|
date: yupDateString(intl),
|
||||||
|
details: yup.string().ensure(),
|
||||||
|
photographer: yup.string().ensure(),
|
||||||
rating100: yup.number().integer().nullable().defined(),
|
rating100: yup.number().integer().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(),
|
||||||
@@ -58,8 +61,11 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
title: image.title ?? "",
|
title: image.title ?? "",
|
||||||
|
code: image.code ?? "",
|
||||||
urls: image?.urls ?? [],
|
urls: image?.urls ?? [],
|
||||||
date: image?.date ?? "",
|
date: image?.date ?? "",
|
||||||
|
details: image.details ?? "",
|
||||||
|
photographer: image.photographer ?? "",
|
||||||
rating100: image.rating100 ?? null,
|
rating100: image.rating100 ?? null,
|
||||||
studio_id: image.studio?.id ?? null,
|
studio_id: image.studio?.id ?? null,
|
||||||
performer_ids: (image.performers ?? []).map((p) => p.id),
|
performer_ids: (image.performers ?? []).map((p) => p.id),
|
||||||
@@ -204,6 +210,22 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
return renderField("tag_ids", title, control, fullWidthProps);
|
return renderField("tag_ids", title, control, fullWidthProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderDetailsField() {
|
||||||
|
const props = {
|
||||||
|
labelProps: {
|
||||||
|
column: true,
|
||||||
|
sm: 3,
|
||||||
|
lg: 12,
|
||||||
|
},
|
||||||
|
fieldProps: {
|
||||||
|
sm: 9,
|
||||||
|
lg: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return renderInputField("details", "textarea", "details", props);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="image-edit-details">
|
<div id="image-edit-details">
|
||||||
<Prompt
|
<Prompt
|
||||||
@@ -234,16 +256,21 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
<Row className="form-container px-3">
|
<Row className="form-container px-3">
|
||||||
<Col lg={7} xl={12}>
|
<Col lg={7} xl={12}>
|
||||||
{renderInputField("title")}
|
{renderInputField("title")}
|
||||||
|
{renderInputField("code", "text", "scene_code")}
|
||||||
|
|
||||||
{renderURLListField("urls", "validation.urls_must_be_unique")}
|
{renderURLListField("urls", "validation.urls_must_be_unique")}
|
||||||
|
|
||||||
{renderDateField("date")}
|
{renderDateField("date")}
|
||||||
|
{renderInputField("photographer")}
|
||||||
{renderRatingField("rating100", "rating")}
|
{renderRatingField("rating100", "rating")}
|
||||||
|
|
||||||
{renderStudioField()}
|
{renderStudioField()}
|
||||||
{renderPerformersField()}
|
{renderPerformersField()}
|
||||||
{renderTagsField()}
|
{renderTagsField()}
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col lg={5} xl={12}>
|
||||||
|
{renderDetailsField()}
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -131,3 +131,7 @@ $imageTabWidth: 450px;
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.col-form-label {
|
||||||
|
padding-right: 2px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ const sortByOptions = [
|
|||||||
const displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall];
|
const displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall];
|
||||||
const criterionOptions = [
|
const criterionOptions = [
|
||||||
createStringCriterionOption("title"),
|
createStringCriterionOption("title"),
|
||||||
|
createStringCriterionOption("code", "scene_code"),
|
||||||
|
createStringCriterionOption("details"),
|
||||||
|
createStringCriterionOption("photographer"),
|
||||||
createMandatoryStringCriterionOption("checksum", "media_info.checksum"),
|
createMandatoryStringCriterionOption("checksum", "media_info.checksum"),
|
||||||
PathCriterionOption,
|
PathCriterionOption,
|
||||||
OrganizedCriterionOption,
|
OrganizedCriterionOption,
|
||||||
|
|||||||
Reference in New Issue
Block a user