mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
Support for assigning any image from a gallery as the cover (#5053)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
@@ -317,6 +317,8 @@ type Mutation {
|
|||||||
|
|
||||||
addGalleryImages(input: GalleryAddInput!): Boolean!
|
addGalleryImages(input: GalleryAddInput!): Boolean!
|
||||||
removeGalleryImages(input: GalleryRemoveInput!): Boolean!
|
removeGalleryImages(input: GalleryRemoveInput!): Boolean!
|
||||||
|
setGalleryCover(input: GallerySetCoverInput!): Boolean!
|
||||||
|
resetGalleryCover(input: GalleryResetCoverInput!): Boolean!
|
||||||
|
|
||||||
galleryChapterCreate(input: GalleryChapterCreateInput!): GalleryChapter
|
galleryChapterCreate(input: GalleryChapterCreateInput!): GalleryChapter
|
||||||
galleryChapterUpdate(input: GalleryChapterUpdateInput!): GalleryChapter
|
galleryChapterUpdate(input: GalleryChapterUpdateInput!): GalleryChapter
|
||||||
|
|||||||
@@ -115,3 +115,12 @@ input GalleryRemoveInput {
|
|||||||
gallery_id: ID!
|
gallery_id: ID!
|
||||||
image_ids: [ID!]!
|
image_ids: [ID!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input GallerySetCoverInput {
|
||||||
|
gallery_id: ID!
|
||||||
|
cover_image_id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
input GalleryResetCoverInput {
|
||||||
|
gallery_id: ID!
|
||||||
|
}
|
||||||
|
|||||||
@@ -478,6 +478,61 @@ func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input Galler
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) SetGalleryCover(ctx context.Context, input GallerySetCoverInput) (bool, error) {
|
||||||
|
galleryID, err := strconv.Atoi(input.GalleryID)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("converting gallery id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
coverImageID, err := strconv.Atoi(input.CoverImageID)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("converting cover image id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||||
|
qb := r.repository.Gallery
|
||||||
|
gallery, err := qb.Find(ctx, galleryID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if gallery == nil {
|
||||||
|
return fmt.Errorf("gallery with id %d not found", galleryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.galleryService.SetCover(ctx, gallery, coverImageID)
|
||||||
|
}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) ResetGalleryCover(ctx context.Context, input GalleryResetCoverInput) (bool, error) {
|
||||||
|
galleryID, err := strconv.Atoi(input.GalleryID)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("converting gallery id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||||
|
qb := r.repository.Gallery
|
||||||
|
gallery, err := qb.Find(ctx, galleryID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if gallery == nil {
|
||||||
|
return fmt.Errorf("gallery with id %d not found", galleryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.galleryService.ResetCover(ctx, gallery)
|
||||||
|
}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) getGalleryChapter(ctx context.Context, id int) (ret *models.GalleryChapter, err error) {
|
func (r *mutationResolver) getGalleryChapter(ctx context.Context, id int) (ret *models.GalleryChapter, err error) {
|
||||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||||
ret, err = r.repository.GalleryChapter.Find(ctx, id)
|
ret, err = r.repository.GalleryChapter.Find(ctx, id)
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ type GalleryService interface {
|
|||||||
AddImages(ctx context.Context, g *models.Gallery, toAdd ...int) error
|
AddImages(ctx context.Context, g *models.Gallery, toAdd ...int) error
|
||||||
RemoveImages(ctx context.Context, g *models.Gallery, toRemove ...int) error
|
RemoveImages(ctx context.Context, g *models.Gallery, toRemove ...int) error
|
||||||
|
|
||||||
|
SetCover(ctx context.Context, g *models.Gallery, coverImageId int) error
|
||||||
|
ResetCover(ctx context.Context, g *models.Gallery) error
|
||||||
|
|
||||||
Destroy(ctx context.Context, i *models.Gallery, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) ([]*models.Image, error)
|
Destroy(ctx context.Context, i *models.Gallery, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) ([]*models.Image, error)
|
||||||
|
|
||||||
ValidateImageGalleryChange(ctx context.Context, i *models.Image, updateIDs models.UpdateIDs) error
|
ValidateImageGalleryChange(ctx context.Context, i *models.Image, updateIDs models.UpdateIDs) error
|
||||||
|
|||||||
@@ -52,6 +52,22 @@ func (s *Service) RemoveImages(ctx context.Context, g *models.Gallery, toRemove
|
|||||||
return s.Updated(ctx, g.ID)
|
return s.Updated(ctx, g.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) SetCover(ctx context.Context, g *models.Gallery, coverImageID int) error {
|
||||||
|
if err := s.Repository.SetCover(ctx, g.ID, coverImageID); err != nil {
|
||||||
|
return fmt.Errorf("failed to set cover: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Updated(ctx, g.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ResetCover(ctx context.Context, g *models.Gallery) error {
|
||||||
|
if err := s.Repository.ResetCover(ctx, g.ID); err != nil {
|
||||||
|
return fmt.Errorf("failed to reset cover: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Updated(ctx, g.ID)
|
||||||
|
}
|
||||||
|
|
||||||
func AddPerformer(ctx context.Context, qb models.GalleryUpdater, o *models.Gallery, performerID int) error {
|
func AddPerformer(ctx context.Context, qb models.GalleryUpdater, o *models.Gallery, performerID int) error {
|
||||||
galleryPartial := models.NewGalleryPartial()
|
galleryPartial := models.NewGalleryPartial()
|
||||||
galleryPartial.PerformerIDs = &models.UpdateIDs{
|
galleryPartial.PerformerIDs = &models.UpdateIDs{
|
||||||
|
|||||||
@@ -107,6 +107,13 @@ func FindGalleryCover(ctx context.Context, r models.ImageQueryer, galleryID int,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findGalleryCover(ctx context.Context, r models.ImageQueryer, galleryID int, useCoverJpg bool, galleryCoverRegex string) (*models.Image, error) {
|
func findGalleryCover(ctx context.Context, r models.ImageQueryer, galleryID int, useCoverJpg bool, galleryCoverRegex string) (*models.Image, error) {
|
||||||
|
img, err := r.CoverByGalleryID(ctx, galleryID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if img != nil {
|
||||||
|
return img, nil
|
||||||
|
}
|
||||||
|
|
||||||
// try to find cover.jpg in the gallery
|
// try to find cover.jpg in the gallery
|
||||||
perPage := 1
|
perPage := 1
|
||||||
sortBy := "path"
|
sortBy := "path"
|
||||||
|
|||||||
@@ -628,6 +628,34 @@ func (_m *GalleryReaderWriter) RemoveImages(ctx context.Context, galleryID int,
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetCover provides a mock function with given fields: ctx, galleryID
|
||||||
|
func (_m *GalleryReaderWriter) ResetCover(ctx context.Context, galleryID int) error {
|
||||||
|
ret := _m.Called(ctx, galleryID)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int) error); ok {
|
||||||
|
r0 = rf(ctx, galleryID)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCover provides a mock function with given fields: ctx, galleryID, coverImageID
|
||||||
|
func (_m *GalleryReaderWriter) SetCover(ctx context.Context, galleryID int, coverImageID int) error {
|
||||||
|
ret := _m.Called(ctx, galleryID, coverImageID)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int, int) error); ok {
|
||||||
|
r0 = rf(ctx, galleryID, coverImageID)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// Update provides a mock function with given fields: ctx, updatedGallery
|
// Update provides a mock function with given fields: ctx, updatedGallery
|
||||||
func (_m *GalleryReaderWriter) Update(ctx context.Context, updatedGallery *models.Gallery) error {
|
func (_m *GalleryReaderWriter) Update(ctx context.Context, updatedGallery *models.Gallery) error {
|
||||||
ret := _m.Called(ctx, updatedGallery)
|
ret := _m.Called(ctx, updatedGallery)
|
||||||
|
|||||||
@@ -114,6 +114,29 @@ func (_m *ImageReaderWriter) CountByGalleryID(ctx context.Context, galleryID int
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CoverByGalleryID provides a mock function with given fields: ctx, galleryId
|
||||||
|
func (_m *ImageReaderWriter) CoverByGalleryID(ctx context.Context, galleryId int) (*models.Image, error) {
|
||||||
|
ret := _m.Called(ctx, galleryId)
|
||||||
|
|
||||||
|
var r0 *models.Image
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, int) *models.Image); ok {
|
||||||
|
r0 = rf(ctx, galleryId)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*models.Image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
|
||||||
|
r1 = rf(ctx, galleryId)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// Create provides a mock function with given fields: ctx, newImage, fileIDs
|
// Create provides a mock function with given fields: ctx, newImage, fileIDs
|
||||||
func (_m *ImageReaderWriter) Create(ctx context.Context, newImage *models.Image, fileIDs []models.FileID) error {
|
func (_m *ImageReaderWriter) Create(ctx context.Context, newImage *models.Image, fileIDs []models.FileID) error {
|
||||||
ret := _m.Called(ctx, newImage, fileIDs)
|
ret := _m.Called(ctx, newImage, fileIDs)
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ type GalleryWriter interface {
|
|||||||
AddFileID(ctx context.Context, id int, fileID FileID) error
|
AddFileID(ctx context.Context, id int, fileID FileID) error
|
||||||
AddImages(ctx context.Context, galleryID int, imageIDs ...int) error
|
AddImages(ctx context.Context, galleryID int, imageIDs ...int) error
|
||||||
RemoveImages(ctx context.Context, galleryID int, imageIDs ...int) error
|
RemoveImages(ctx context.Context, galleryID int, imageIDs ...int) error
|
||||||
|
SetCover(ctx context.Context, galleryID int, coverImageID int) error
|
||||||
|
ResetCover(ctx context.Context, galleryID int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// GalleryReaderWriter provides all gallery methods.
|
// GalleryReaderWriter provides all gallery methods.
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type ImageFinder interface {
|
|||||||
type ImageQueryer interface {
|
type ImageQueryer interface {
|
||||||
Query(ctx context.Context, options ImageQueryOptions) (*ImageQueryResult, error)
|
Query(ctx context.Context, options ImageQueryOptions) (*ImageQueryResult, error)
|
||||||
QueryCount(ctx context.Context, imageFilter *ImageFilterType, findFilter *FindFilterType) (int, error)
|
QueryCount(ctx context.Context, imageFilter *ImageFilterType, findFilter *FindFilterType) (int, error)
|
||||||
|
CoverByGalleryID(ctx context.Context, galleryId int) (*Image, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageCounter provides methods to count images.
|
// ImageCounter provides methods to count images.
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const (
|
|||||||
dbConnTimeout = 30
|
dbConnTimeout = 30
|
||||||
)
|
)
|
||||||
|
|
||||||
var appSchemaVersion uint = 65
|
var appSchemaVersion uint = 66
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*.sql
|
||||||
var migrationsBox embed.FS
|
var migrationsBox embed.FS
|
||||||
|
|||||||
@@ -890,6 +890,14 @@ func (qb *GalleryStore) UpdateImages(ctx context.Context, galleryID int, imageID
|
|||||||
return galleryRepository.images.replace(ctx, galleryID, imageIDs)
|
return galleryRepository.images.replace(ctx, galleryID, imageIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *GalleryStore) SetCover(ctx context.Context, galleryID int, coverImageID int) error {
|
||||||
|
return imageGalleriesTableMgr.setCover(ctx, coverImageID, galleryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *GalleryStore) ResetCover(ctx context.Context, galleryID int) error {
|
||||||
|
return imageGalleriesTableMgr.resetCover(ctx, galleryID)
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *GalleryStore) GetSceneIDs(ctx context.Context, id int) ([]int, error) {
|
func (qb *GalleryStore) GetSceneIDs(ctx context.Context, id int) ([]int, error) {
|
||||||
return galleryRepository.scenes.getIDs(ctx, id)
|
return galleryRepository.scenes.getIDs(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2973,6 +2973,34 @@ func TestGalleryQueryHasChapters(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGallerySetAndResetCover(t *testing.T) {
|
||||||
|
withTxn(func(ctx context.Context) error {
|
||||||
|
sqb := db.Gallery
|
||||||
|
|
||||||
|
imagePath2 := getFilePath(folderIdxWithImageFiles, getImageBasename(imageIdx2WithGallery))
|
||||||
|
|
||||||
|
result, err := db.Image.CoverByGalleryID(ctx, galleryIDs[galleryIdxWithTwoImages])
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
|
||||||
|
err = sqb.SetCover(ctx, galleryIDs[galleryIdxWithTwoImages], imageIDs[imageIdx2WithGallery])
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
result, err = db.Image.CoverByGalleryID(ctx, galleryIDs[galleryIdxWithTwoImages])
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, result.Path, imagePath2)
|
||||||
|
|
||||||
|
err = sqb.ResetCover(ctx, galleryIDs[galleryIdxWithTwoImages])
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
result, err = db.Image.CoverByGalleryID(ctx, galleryIDs[galleryIdxWithTwoImages])
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// TODO Count
|
// TODO Count
|
||||||
// TODO All
|
// TODO All
|
||||||
// TODO Query
|
// TODO Query
|
||||||
|
|||||||
@@ -480,6 +480,42 @@ func (qb *ImageStore) getMany(ctx context.Context, q *goqu.SelectDataset) ([]*mo
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the custom cover for the gallery, if one has been set.
|
||||||
|
func (qb *ImageStore) CoverByGalleryID(ctx context.Context, galleryID int) (*models.Image, error) {
|
||||||
|
table := qb.table()
|
||||||
|
|
||||||
|
sq := dialect.From(table).
|
||||||
|
InnerJoin(
|
||||||
|
galleriesImagesJoinTable,
|
||||||
|
goqu.On(table.Col(idColumn).Eq(galleriesImagesJoinTable.Col(imageIDColumn))),
|
||||||
|
).
|
||||||
|
Select(table.Col(idColumn)).
|
||||||
|
Where(goqu.And(
|
||||||
|
galleriesImagesJoinTable.Col("gallery_id").Eq(galleryID),
|
||||||
|
galleriesImagesJoinTable.Col("cover").Eq(true),
|
||||||
|
))
|
||||||
|
|
||||||
|
q := qb.selectDataset().Prepared(true).Where(
|
||||||
|
table.Col(idColumn).Eq(
|
||||||
|
sq,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
ret, err := qb.getMany(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getting cover for gallery %d: %w", galleryID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case len(ret) > 1:
|
||||||
|
return nil, fmt.Errorf("internal error: multiple covers returned for gallery %d", galleryID)
|
||||||
|
case len(ret) == 1:
|
||||||
|
return ret[0], nil
|
||||||
|
default:
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *ImageStore) GetFiles(ctx context.Context, id int) ([]models.File, error) {
|
func (qb *ImageStore) GetFiles(ctx context.Context, id int) ([]models.File, error) {
|
||||||
fileIDs, err := imageRepository.files.get(ctx, id)
|
fileIDs, err := imageRepository.files.get(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
2
pkg/sqlite/migrations/66_gallery_cover.up.sql
Normal file
2
pkg/sqlite/migrations/66_gallery_cover.up.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `galleries_images` ADD COLUMN `cover` BOOLEAN NOT NULL DEFAULT 0;
|
||||||
|
CREATE UNIQUE INDEX `index_galleries_images_gallery_id_cover` on `galleries_images` (`gallery_id`, `cover`) WHERE `cover` = 1;
|
||||||
@@ -710,6 +710,45 @@ func (t *scenesGroupsTable) modifyJoins(ctx context.Context, id int, v []models.
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type imageGalleriesTable struct {
|
||||||
|
joinTable
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *imageGalleriesTable) setCover(ctx context.Context, id int, galleryID int) error {
|
||||||
|
if err := t.resetCover(ctx, galleryID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
table := t.table.table
|
||||||
|
|
||||||
|
q := dialect.Update(table).Prepared(true).Set(goqu.Record{
|
||||||
|
"cover": true,
|
||||||
|
}).Where(t.idColumn.Eq(id), table.Col(galleryIDColumn).Eq(galleryID))
|
||||||
|
|
||||||
|
if _, err := exec(ctx, q); err != nil {
|
||||||
|
return fmt.Errorf("setting cover flag in %s: %w", t.table.table.GetTable(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *imageGalleriesTable) resetCover(ctx context.Context, galleryID int) error {
|
||||||
|
table := t.table.table
|
||||||
|
|
||||||
|
q := dialect.Update(table).Prepared(true).Set(goqu.Record{
|
||||||
|
"cover": false,
|
||||||
|
}).Where(
|
||||||
|
table.Col(galleryIDColumn).Eq(galleryID),
|
||||||
|
table.Col("cover").Eq(true),
|
||||||
|
)
|
||||||
|
|
||||||
|
if _, err := exec(ctx, q); err != nil {
|
||||||
|
return fmt.Errorf("unsetting cover flags in %s: %w", t.table.table.GetTable(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type relatedFilesTable struct {
|
type relatedFilesTable struct {
|
||||||
table
|
table
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,12 +57,14 @@ var (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
imageGalleriesTableMgr = &joinTable{
|
imageGalleriesTableMgr = &imageGalleriesTable{
|
||||||
table: table{
|
joinTable: joinTable{
|
||||||
table: galleriesImagesJoinTable,
|
table: table{
|
||||||
idColumn: galleriesImagesJoinTable.Col(imageIDColumn),
|
table: galleriesImagesJoinTable,
|
||||||
|
idColumn: galleriesImagesJoinTable.Col(imageIDColumn),
|
||||||
|
},
|
||||||
|
fkColumn: galleriesImagesJoinTable.Col(galleryIDColumn),
|
||||||
},
|
},
|
||||||
fkColumn: galleriesImagesJoinTable.Col(galleryIDColumn),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
imagesTagsTableMgr = &joinTable{
|
imagesTagsTableMgr = &joinTable{
|
||||||
|
|||||||
@@ -43,3 +43,13 @@ mutation AddGalleryImages($gallery_id: ID!, $image_ids: [ID!]!) {
|
|||||||
mutation RemoveGalleryImages($gallery_id: ID!, $image_ids: [ID!]!) {
|
mutation RemoveGalleryImages($gallery_id: ID!, $image_ids: [ID!]!) {
|
||||||
removeGalleryImages(input: { gallery_id: $gallery_id, image_ids: $image_ids })
|
removeGalleryImages(input: { gallery_id: $gallery_id, image_ids: $image_ids })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutation SetGalleryCover($gallery_id: ID!, $cover_image_id: ID!) {
|
||||||
|
setGalleryCover(
|
||||||
|
input: { gallery_id: $gallery_id, cover_image_id: $cover_image_id }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation ResetGalleryCover($gallery_id: ID!) {
|
||||||
|
resetGalleryCover(input: { gallery_id: $gallery_id })
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Helmet } from "react-helmet";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
mutateMetadataScan,
|
mutateMetadataScan,
|
||||||
|
mutateResetGalleryCover,
|
||||||
useFindGallery,
|
useFindGallery,
|
||||||
useGalleryUpdate,
|
useGalleryUpdate,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
@@ -138,6 +139,25 @@ export const GalleryPage: React.FC<IProps> = ({ gallery, add }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onResetCover() {
|
||||||
|
try {
|
||||||
|
await mutateResetGalleryCover({
|
||||||
|
gallery_id: gallery.id!,
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast.success(
|
||||||
|
intl.formatMessage(
|
||||||
|
{ id: "toast.updated_entity" },
|
||||||
|
{
|
||||||
|
entity: intl.formatMessage({ id: "gallery" }).toLocaleLowerCase(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onClickChapter(imageindex: number) {
|
async function onClickChapter(imageindex: number) {
|
||||||
showLightbox(imageindex - 1);
|
showLightbox(imageindex - 1);
|
||||||
}
|
}
|
||||||
@@ -176,7 +196,6 @@ export const GalleryPage: React.FC<IProps> = ({ gallery, add }) => {
|
|||||||
<Dropdown.Menu className="bg-secondary text-white">
|
<Dropdown.Menu className="bg-secondary text-white">
|
||||||
{path ? (
|
{path ? (
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
key="rescan"
|
|
||||||
className="bg-secondary text-white"
|
className="bg-secondary text-white"
|
||||||
onClick={() => onRescan()}
|
onClick={() => onRescan()}
|
||||||
>
|
>
|
||||||
@@ -184,7 +203,12 @@ export const GalleryPage: React.FC<IProps> = ({ gallery, add }) => {
|
|||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
key="delete-gallery"
|
className="bg-secondary text-white"
|
||||||
|
onClick={() => onResetCover()}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="actions.reset_cover" />
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item
|
||||||
className="bg-secondary text-white"
|
className="bg-secondary text-white"
|
||||||
onClick={() => setIsDeleteAlertOpen(true)}
|
onClick={() => setIsDeleteAlertOpen(true)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,8 +3,14 @@ import * as GQL from "src/core/generated-graphql";
|
|||||||
import { GalleriesCriterion } from "src/models/list-filter/criteria/galleries";
|
import { GalleriesCriterion } from "src/models/list-filter/criteria/galleries";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { ImageList } from "src/components/Images/ImageList";
|
import { ImageList } from "src/components/Images/ImageList";
|
||||||
import { mutateRemoveGalleryImages } from "src/core/StashService";
|
import {
|
||||||
import { showWhenSelected } from "src/components/List/ItemList";
|
mutateRemoveGalleryImages,
|
||||||
|
mutateSetGalleryCover,
|
||||||
|
} from "src/core/StashService";
|
||||||
|
import {
|
||||||
|
showWhenSelected,
|
||||||
|
showWhenSingleSelection,
|
||||||
|
} from "src/components/List/ItemList";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { faMinus } from "@fortawesome/free-solid-svg-icons";
|
import { faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -58,6 +64,35 @@ export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({
|
|||||||
return filter;
|
return filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setCover(
|
||||||
|
result: GQL.FindImagesQueryResult,
|
||||||
|
filter: ListFilterModel,
|
||||||
|
selectedIds: Set<string>
|
||||||
|
) {
|
||||||
|
const coverImageID = selectedIds.values().next();
|
||||||
|
if (coverImageID.done) {
|
||||||
|
// operation should only be displayed when exactly one image is selected
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await mutateSetGalleryCover({
|
||||||
|
gallery_id: gallery.id!,
|
||||||
|
cover_image_id: coverImageID.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast.success(
|
||||||
|
intl.formatMessage(
|
||||||
|
{ id: "toast.updated_entity" },
|
||||||
|
{
|
||||||
|
entity: intl.formatMessage({ id: "gallery" }).toLocaleLowerCase(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function removeImages(
|
async function removeImages(
|
||||||
result: GQL.FindImagesQueryResult,
|
result: GQL.FindImagesQueryResult,
|
||||||
filter: ListFilterModel,
|
filter: ListFilterModel,
|
||||||
@@ -85,6 +120,11 @@ export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const otherOperations = [
|
const otherOperations = [
|
||||||
|
{
|
||||||
|
text: intl.formatMessage({ id: "actions.set_cover" }),
|
||||||
|
onClick: setCover,
|
||||||
|
isDisplayed: showWhenSingleSelection,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: intl.formatMessage({ id: "actions.remove_from_gallery" }),
|
text: intl.formatMessage({ id: "actions.remove_from_gallery" }),
|
||||||
onClick: removeImages,
|
onClick: removeImages,
|
||||||
|
|||||||
@@ -335,3 +335,11 @@ export const showWhenSelected = <T extends QueryResult>(
|
|||||||
) => {
|
) => {
|
||||||
return selectedIds.size > 0;
|
return selectedIds.size > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const showWhenSingleSelection = <T extends QueryResult>(
|
||||||
|
result: T,
|
||||||
|
filter: ListFilterModel,
|
||||||
|
selectedIds: Set<string>
|
||||||
|
) => {
|
||||||
|
return selectedIds.size == 1;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1526,6 +1526,34 @@ export const mutateAddGalleryImages = (input: GQL.GalleryAddInput) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mutateSetGalleryCover = (input: GQL.GallerySetCoverInput) =>
|
||||||
|
client.mutate<GQL.SetGalleryCoverMutation>({
|
||||||
|
mutation: GQL.SetGalleryCoverDocument,
|
||||||
|
variables: input,
|
||||||
|
update(cache, result) {
|
||||||
|
if (!result.data?.setGalleryCover) return;
|
||||||
|
|
||||||
|
cache.evict({
|
||||||
|
id: cache.identify({ __typename: "Gallery", id: input.gallery_id }),
|
||||||
|
fieldName: "cover",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mutateResetGalleryCover = (input: GQL.GalleryResetCoverInput) =>
|
||||||
|
client.mutate<GQL.ResetGalleryCoverMutation>({
|
||||||
|
mutation: GQL.ResetGalleryCoverDocument,
|
||||||
|
variables: input,
|
||||||
|
update(cache, result) {
|
||||||
|
if (!result.data?.resetGalleryCover) return;
|
||||||
|
|
||||||
|
cache.evict({
|
||||||
|
id: cache.identify({ __typename: "Gallery", id: input.gallery_id }),
|
||||||
|
fieldName: "cover",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const mutateRemoveGalleryImages = (input: GQL.GalleryRemoveInput) =>
|
export const mutateRemoveGalleryImages = (input: GQL.GalleryRemoveInput) =>
|
||||||
client.mutate<GQL.RemoveGalleryImagesMutation>({
|
client.mutate<GQL.RemoveGalleryImagesMutation>({
|
||||||
mutation: GQL.RemoveGalleryImagesDocument,
|
mutation: GQL.RemoveGalleryImagesDocument,
|
||||||
|
|||||||
@@ -94,6 +94,7 @@
|
|||||||
"remove_from_gallery": "Remove from Gallery",
|
"remove_from_gallery": "Remove from Gallery",
|
||||||
"rename_gen_files": "Rename generated files",
|
"rename_gen_files": "Rename generated files",
|
||||||
"rescan": "Rescan",
|
"rescan": "Rescan",
|
||||||
|
"reset_cover": "Restore Default Cover",
|
||||||
"reshuffle": "Reshuffle",
|
"reshuffle": "Reshuffle",
|
||||||
"running": "running",
|
"running": "running",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
@@ -114,6 +115,7 @@
|
|||||||
"selective_scan": "Selective Scan",
|
"selective_scan": "Selective Scan",
|
||||||
"set_as_default": "Set as default",
|
"set_as_default": "Set as default",
|
||||||
"set_back_image": "Back image…",
|
"set_back_image": "Back image…",
|
||||||
|
"set_cover": "Set as Cover",
|
||||||
"set_front_image": "Front image…",
|
"set_front_image": "Front image…",
|
||||||
"set_image": "Set image…",
|
"set_image": "Set image…",
|
||||||
"show": "Show",
|
"show": "Show",
|
||||||
|
|||||||
Reference in New Issue
Block a user