1
Fork 0

Merge pull request #526 from PJ-Watson/master

Selectable Album Covers
This commit is contained in:
Viktor Strate Kløvedal 2021-09-23 21:15:22 +02:00 committed by GitHub
commit e6da096aa3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 7253 additions and 165 deletions

View File

@ -1,6 +1,6 @@
#!/bin/sh
gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep '.go$')
gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep --silent '.go$')
[ -z "$gofiles" ] && exit 0
# Automatically format go code, exit on error

View File

@ -153,8 +153,10 @@ type ComplexityRoot struct {
MoveImageFaces func(childComplexity int, imageFaceIDs []int, destinationFaceGroupID int) int
ProtectShareToken func(childComplexity int, token string, password *string) int
RecognizeUnlabeledFaces func(childComplexity int) int
ResetAlbumCover func(childComplexity int, albumID int) int
ScanAll func(childComplexity int) int
ScanUser func(childComplexity int, userID int) int
SetAlbumCover func(childComplexity int, coverID int) int
SetFaceGroupLabel func(childComplexity int, faceGroupID int, label *string) int
SetPeriodicScanInterval func(childComplexity int, interval int) int
SetScannerConcurrentWorkers func(childComplexity int, workers int) int
@ -312,6 +314,8 @@ type MutationResolver interface {
SetPeriodicScanInterval(ctx context.Context, interval int) (int, error)
SetScannerConcurrentWorkers(ctx context.Context, workers int) (int, error)
ChangeUserPreferences(ctx context.Context, language *string) (*models.UserPreferences, error)
ResetAlbumCover(ctx context.Context, albumID int) (*models.Album, error)
SetAlbumCover(ctx context.Context, coverID int) (*models.Album, error)
SetFaceGroupLabel(ctx context.Context, faceGroupID int, label *string) (*models.FaceGroup, error)
CombineFaceGroups(ctx context.Context, destinationFaceGroupID int, sourceFaceGroupID int) (*models.FaceGroup, error)
MoveImageFaces(ctx context.Context, imageFaceIDs []int, destinationFaceGroupID int) (*models.FaceGroup, error)
@ -919,6 +923,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.RecognizeUnlabeledFaces(childComplexity), true
case "Mutation.resetAlbumCover":
if e.complexity.Mutation.ResetAlbumCover == nil {
break
}
args, err := ec.field_Mutation_resetAlbumCover_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.ResetAlbumCover(childComplexity, args["albumID"].(int)), true
case "Mutation.scanAll":
if e.complexity.Mutation.ScanAll == nil {
break
@ -938,6 +954,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.ScanUser(childComplexity, args["userId"].(int)), true
case "Mutation.setAlbumCover":
if e.complexity.Mutation.SetAlbumCover == nil {
break
}
args, err := ec.field_Mutation_setAlbumCover_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.SetAlbumCover(childComplexity, args["coverID"].(int)), true
case "Mutation.setFaceGroupLabel":
if e.complexity.Mutation.SetFaceGroupLabel == nil {
break
@ -1700,7 +1728,7 @@ type Query {
"Get media owned by the logged in user, returned in GeoJson format"
myMediaGeoJson: Any! @isAuthorized
"Get the mapbox api token, returns null if mapbox is not enabled"
mapboxToken: String
mapboxToken: String @isAuthorized
shareToken(credentials: ShareTokenCredentials!): ShareToken!
shareTokenValidatePassword(credentials: ShareTokenCredentials!): Boolean!
@ -1766,6 +1794,11 @@ type Mutation {
changeUserPreferences(language: String): UserPreferences! @isAuthorized
"Reset the assigned cover photo for an album"
resetAlbumCover(albumID: ID!): Album! @isAuthorized
"Assign a cover photo to an album"
setAlbumCover(coverID: ID!): Album! @isAuthorized
"Assign a label to a face group, set label to null to remove the current one"
setFaceGroupLabel(faceGroupID: ID!, label: String): FaceGroup! @isAuthorized
"Merge two face groups into a single one, all ImageFaces from source will be moved to destination"
@ -1894,7 +1927,7 @@ type Album {
paginate: Pagination
): [Album!]!
"The album witch contains this album"
"The album which contains this album"
parentAlbum: Album
"The user who owns this album"
owner: User!
@ -1905,6 +1938,8 @@ type Album {
path: [Album!]!
shares: [ShareToken!]!
#coverID: Int
}
type MediaURL {
@ -2349,6 +2384,21 @@ func (ec *executionContext) field_Mutation_protectShareToken_args(ctx context.Co
return args, nil
}
func (ec *executionContext) field_Mutation_resetAlbumCover_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 int
if tmp, ok := rawArgs["albumID"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("albumID"))
arg0, err = ec.unmarshalNID2int(ctx, tmp)
if err != nil {
return nil, err
}
}
args["albumID"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_scanUser_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@ -2364,6 +2414,21 @@ func (ec *executionContext) field_Mutation_scanUser_args(ctx context.Context, ra
return args, nil
}
func (ec *executionContext) field_Mutation_setAlbumCover_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 int
if tmp, ok := rawArgs["coverID"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("coverID"))
arg0, err = ec.unmarshalNID2int(ctx, tmp)
if err != nil {
return nil, err
}
}
args["coverID"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_setFaceGroupLabel_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@ -5863,6 +5928,130 @@ func (ec *executionContext) _Mutation_changeUserPreferences(ctx context.Context,
return ec.marshalNUserPreferences2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUserPreferences(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_resetAlbumCover(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_resetAlbumCover_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().ResetAlbumCover(rctx, args["albumID"].(int))
}
directive1 := func(ctx context.Context) (interface{}, error) {
if ec.directives.IsAuthorized == nil {
return nil, errors.New("directive isAuthorized is not implemented")
}
return ec.directives.IsAuthorized(ctx, nil, directive0)
}
tmp, err := directive1(rctx)
if err != nil {
return nil, graphql.ErrorOnPath(ctx, err)
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.(*models.Album); ok {
return data, nil
}
return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.Album`, tmp)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*models.Album)
fc.Result = res
return ec.marshalNAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_setAlbumCover(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_setAlbumCover_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().SetAlbumCover(rctx, args["coverID"].(int))
}
directive1 := func(ctx context.Context) (interface{}, error) {
if ec.directives.IsAuthorized == nil {
return nil, errors.New("directive isAuthorized is not implemented")
}
return ec.directives.IsAuthorized(ctx, nil, directive0)
}
tmp, err := directive1(rctx)
if err != nil {
return nil, graphql.ErrorOnPath(ctx, err)
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.(*models.Album); ok {
return data, nil
}
return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.Album`, tmp)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*models.Album)
fc.Result = res
return ec.marshalNAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_setFaceGroupLabel(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -7031,8 +7220,28 @@ func (ec *executionContext) _Query_mapboxToken(ctx context.Context, field graphq
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().MapboxToken(rctx)
}
directive1 := func(ctx context.Context) (interface{}, error) {
if ec.directives.IsAuthorized == nil {
return nil, errors.New("directive isAuthorized is not implemented")
}
return ec.directives.IsAuthorized(ctx, nil, directive0)
}
tmp, err := directive1(rctx)
if err != nil {
return nil, graphql.ErrorOnPath(ctx, err)
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.(*string); ok {
return data, nil
}
return nil, fmt.Errorf(`unexpected type %T from directive, should be *string`, tmp)
})
if err != nil {
ec.Error(ctx, err)
@ -10682,6 +10891,16 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
invalids++
}
case "resetAlbumCover":
out.Values[i] = ec._Mutation_resetAlbumCover(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
case "setAlbumCover":
out.Values[i] = ec._Mutation_setAlbumCover(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
case "setFaceGroupLabel":
out.Values[i] = ec._Mutation_setFaceGroupLabel(ctx, field)
if out.Values[i] == graphql.Null {

View File

@ -0,0 +1,124 @@
package actions
import (
"github.com/photoview/photoview/api/graphql/models"
"github.com/pkg/errors"
"gorm.io/gorm"
)
func MyAlbums(db *gorm.DB, user *models.User, order *models.Ordering, paginate *models.Pagination, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) ([]*models.Album, error) {
if err := user.FillAlbums(db); err != nil {
return nil, err
}
if len(user.Albums) == 0 {
return nil, nil
}
userAlbumIDs := make([]int, len(user.Albums))
for i, album := range user.Albums {
userAlbumIDs[i] = album.ID
}
query := db.Model(models.Album{}).Where("id IN (?)", userAlbumIDs)
if onlyRoot != nil && *onlyRoot {
query = query.Where("parent_album_id IS NULL")
}
if showEmpty == nil || !*showEmpty {
subQuery := db.Model(&models.Media{}).Where("album_id = albums.id")
if onlyWithFavorites != nil && *onlyWithFavorites {
favoritesSubquery := db.
Model(&models.UserMediaData{UserID: user.ID}).
Where("user_media_data.media_id = media.id").
Where("user_media_data.favorite = true")
subQuery = subQuery.Where("EXISTS (?)", favoritesSubquery)
}
query = query.Where("EXISTS (?)", subQuery)
}
query = models.FormatSQL(query, order, paginate)
var albums []*models.Album
if err := query.Find(&albums).Error; err != nil {
return nil, err
}
return albums, nil
}
func Album(db *gorm.DB, user *models.User, id int) (*models.Album, error) {
var album models.Album
if err := db.First(&album, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("album not found")
}
return nil, err
}
ownsAlbum, err := user.OwnsAlbum(db, &album)
if err != nil {
return nil, err
}
if !ownsAlbum {
return nil, errors.New("forbidden")
}
return &album, nil
}
func SetAlbumCover(db *gorm.DB, user *models.User, mediaID int) (*models.Album, error) {
var media models.Media
if err := db.Find(&media, mediaID).Error; err != nil {
return nil, err
}
var album models.Album
if err := db.Find(&album, &media.AlbumID).Error; err != nil {
return nil, err
}
ownsAlbum, err := user.OwnsAlbum(db, &album)
if err != nil {
return nil, err
}
if !ownsAlbum {
return nil, errors.New("forbidden")
}
if err := db.Model(&album).Update("cover_id", mediaID).Error; err != nil {
return nil, err
}
return &album, nil
}
func ResetAlbumCover(db *gorm.DB, user *models.User, albumID int) (*models.Album, error) {
var album models.Album
if err := db.Find(&album, albumID).Error; err != nil {
return nil, err
}
ownsAlbum, err := user.OwnsAlbum(db, &album)
if err != nil {
return nil, err
}
if !ownsAlbum {
return nil, errors.New("forbidden")
}
if err := db.Model(&album).Update("cover_id", nil).Error; err != nil {
return nil, err
}
return &album, nil
}

View File

@ -0,0 +1,178 @@
package actions_test
import (
"testing"
"github.com/photoview/photoview/api/graphql/models"
"github.com/photoview/photoview/api/graphql/models/actions"
"github.com/photoview/photoview/api/test_utils"
"github.com/stretchr/testify/assert"
)
func TestAlbumCover(t *testing.T) {
db := test_utils.DatabaseTest(t)
rootAlbum := models.Album{
Title: "root",
Path: "/photos",
}
if !assert.NoError(t, db.Save(&rootAlbum).Error) {
return
}
children := []models.Album{
{
Title: "child1",
Path: "/photos/child1",
ParentAlbumID: &rootAlbum.ID,
},
{
Title: "child2",
Path: "/photos/child2",
ParentAlbumID: &rootAlbum.ID,
},
}
if !assert.NoError(t, db.Save(&children).Error) {
return
}
photos := []models.Media{
{
Title: "pic1",
Path: "/photos/pic1",
AlbumID: rootAlbum.ID,
},
{
Title: "pic2",
Path: "/photos/pic2",
AlbumID: rootAlbum.ID,
},
{
Title: "pic3",
Path: "/photos/child1/pic3",
AlbumID: children[0].ID,
},
{
Title: "pic4",
Path: "/photos/child1/pic4",
AlbumID: children[0].ID,
},
{
Title: "pic5",
Path: "/photos/child2/pic5",
AlbumID: children[1].ID,
},
{
Title: "pic6",
Path: "/photos/child2/pic6",
AlbumID: children[1].ID,
},
}
if !assert.NoError(t, db.Save(&photos).Error) {
return
}
if !assert.NoError(t, db.Model(&children[0]).Update("cover_id", &photos[3].ID).Error) {
return
}
photoUrls := []models.MediaURL{
{
MediaID: photos[0].ID,
Media: &photos[0],
},
{
MediaID: photos[1].ID,
Media: &photos[1],
},
{
MediaID: photos[2].ID,
Media: &photos[2],
},
{
MediaID: photos[3].ID,
Media: &photos[3],
},
{
MediaID: photos[4].ID,
Media: &photos[4],
},
{
MediaID: photos[5].ID,
Media: &photos[5],
},
}
if !assert.NoError(t, db.Save(&photoUrls).Error) {
return
}
user_pass := "password"
regularUser, err := models.RegisterUser(db, "user1", &user_pass, false)
if !assert.NoError(t, err) {
return
}
if !assert.NoError(t, db.Model(&regularUser).Association("Albums").Append(&rootAlbum)) {
return
}
if !assert.NoError(t, db.Model(&regularUser).Association("Albums").Append(&children)) {
return
}
// Single test since we cannot rely on the tests being performed sequentially
t.Run("Album get and reset cover photos", func(t *testing.T) {
{
album, err := actions.Album(db, regularUser, rootAlbum.ID)
assert.NoError(t, err)
albumThumb, err := album.Thumbnail(db)
assert.NoError(t, err)
// Should return pic1 since no coverID has been set
assert.EqualValues(t, "pic1", albumThumb.Title)
}
{
album, err := actions.Album(db, regularUser, children[0].ID)
assert.NoError(t, err)
albumThumb, err := album.Thumbnail(db)
assert.NoError(t, err)
// coverID has already been set
assert.EqualValues(t, "pic4", albumThumb.Title)
}
resetAlbum, err := actions.ResetAlbumCover(db, regularUser, children[0].ID)
assert.NoError(t, err)
assert.Nil(t, resetAlbum.CoverID)
resetThumb, err := resetAlbum.Thumbnail(db)
assert.NoError(t, err)
assert.Equal(t, "pic3", resetThumb.Title)
})
t.Run("Album change cover photos", func(t *testing.T) {
assert.Nil(t, children[1].CoverID)
album, err := actions.SetAlbumCover(db, regularUser, photos[4].ID)
assert.NoError(t, err)
assert.Equal(t, children[1].ID, album.ID)
assert.NotNil(t, album.CoverID)
assert.Equal(t, photos[4].ID, *album.CoverID)
albumThumb, err := album.Thumbnail(db)
assert.NoError(t, err)
assert.Equal(t, photos[4].ID, albumThumb.ID)
})
}

View File

@ -17,6 +17,7 @@ type Album struct {
Owners []User `gorm:"many2many:user_albums;constraint:OnDelete:CASCADE;"`
Path string `gorm:"not null"`
PathHash string `gorm:"unique"`
CoverID *int
}
func (a *Album) FilePath() string {
@ -78,3 +79,31 @@ func GetParentsFromAlbums(db *gorm.DB, filter func(*gorm.DB) *gorm.DB, albumID i
return parents, err
}
func (a *Album) Thumbnail(db *gorm.DB) (*Media, error) {
var media Media
if a.CoverID == nil {
if err := db.Raw(`
WITH recursive sub_albums AS (
SELECT * FROM albums AS root WHERE id = ?
UNION ALL
SELECT child.* FROM albums AS child JOIN sub_albums ON child.parent_album_id = sub_albums.id
)
SELECT * FROM media WHERE media.album_id IN (
SELECT id FROM sub_albums
) AND media.id IN (
SELECT media_id FROM media_urls WHERE media_urls.media_id = media.id
) ORDER BY id LIMIT 1
`, a.ID).Find(&media).Error; err != nil {
return nil, err
}
} else {
if err := db.Where("id = ?", a.CoverID).Find(&media).Error; err != nil {
return nil, err
}
}
return &media, nil
}

View File

@ -6,6 +6,7 @@ import (
api "github.com/photoview/photoview/api/graphql"
"github.com/photoview/photoview/api/graphql/auth"
"github.com/photoview/photoview/api/graphql/models"
"github.com/photoview/photoview/api/graphql/models/actions"
"github.com/pkg/errors"
"gorm.io/gorm"
)
@ -16,48 +17,7 @@ func (r *queryResolver) MyAlbums(ctx context.Context, order *models.Ordering, pa
return nil, auth.ErrUnauthorized
}
if err := user.FillAlbums(r.Database); err != nil {
return nil, err
}
if len(user.Albums) == 0 {
return nil, nil
}
userAlbumIDs := make([]int, len(user.Albums))
for i, album := range user.Albums {
userAlbumIDs[i] = album.ID
}
query := r.Database.Model(models.Album{}).Where("id IN (?)", userAlbumIDs)
if onlyRoot != nil && *onlyRoot == true {
query = query.Where("parent_album_id IS NULL")
}
if showEmpty == nil || *showEmpty == false {
subQuery := r.Database.Model(&models.Media{}).Where("album_id = albums.id")
if onlyWithFavorites != nil && *onlyWithFavorites == true {
favoritesSubquery := r.Database.
Model(&models.UserMediaData{UserID: user.ID}).
Where("user_media_data.media_id = media.id").
Where("user_media_data.favorite = true")
subQuery = subQuery.Where("EXISTS (?)", favoritesSubquery)
}
query = query.Where("EXISTS (?)", subQuery)
}
query = models.FormatSQL(query, order, paginate)
var albums []*models.Album
if err := query.Find(&albums).Error; err != nil {
return nil, err
}
return albums, nil
return actions.MyAlbums(r.Database, user, order, paginate, onlyRoot, showEmpty, onlyWithFavorites)
}
func (r *queryResolver) Album(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Album, error) {
@ -89,24 +49,7 @@ func (r *queryResolver) Album(ctx context.Context, id int, tokenCredentials *mod
return nil, auth.ErrUnauthorized
}
var album models.Album
if err := r.Database.First(&album, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("album not found")
}
return nil, err
}
ownsAlbum, err := user.OwnsAlbum(r.Database, &album)
if err != nil {
return nil, err
}
if !ownsAlbum {
return nil, errors.New("forbidden")
}
return &album, nil
return actions.Album(r.Database, user, id)
}
func (r *Resolver) Album() api.AlbumResolver {
@ -144,29 +87,8 @@ func (r *albumResolver) Media(ctx context.Context, album *models.Album, order *m
return media, nil
}
func (r *albumResolver) Thumbnail(ctx context.Context, obj *models.Album) (*models.Media, error) {
var media models.Media
err := r.Database.Raw(`
WITH recursive sub_albums AS (
SELECT * FROM albums AS root WHERE id = ?
UNION ALL
SELECT child.* FROM albums AS child JOIN sub_albums ON child.parent_album_id = sub_albums.id
)
SELECT * FROM media WHERE media.album_id IN (
SELECT id FROM sub_albums
) AND media.id IN (
SELECT media_id FROM media_urls WHERE media_urls.media_id = media.id
) LIMIT 1
`, obj.ID).Find(&media).Error
if err != nil {
return nil, err
}
return &media, nil
func (r *albumResolver) Thumbnail(ctx context.Context, album *models.Album) (*models.Media, error) {
return album.Thumbnail(r.Database)
}
func (r *albumResolver) SubAlbums(ctx context.Context, parent *models.Album, order *models.Ordering, paginate *models.Pagination) ([]*models.Album, error) {
@ -242,3 +164,22 @@ func (r *albumResolver) Path(ctx context.Context, obj *models.Album) ([]*models.
return album_path, nil
}
// Takes album_id, resets album.cover_id to 0 (null)
func (r *mutationResolver) ResetAlbumCover(ctx context.Context, albumID int) (*models.Album, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
return actions.ResetAlbumCover(r.Database, user, albumID)
}
func (r *mutationResolver) SetAlbumCover(ctx context.Context, mediaID int) (*models.Album, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
return actions.SetAlbumCover(r.Database, user, mediaID)
}

View File

@ -134,6 +134,11 @@ type Mutation {
changeUserPreferences(language: String): UserPreferences! @isAuthorized
"Reset the assigned cover photo for an album"
resetAlbumCover(albumID: ID!): Album! @isAuthorized
"Assign a cover photo to an album"
setAlbumCover(coverID: ID!): Album! @isAuthorized
"Assign a label to a face group, set label to null to remove the current one"
setFaceGroupLabel(faceGroupID: ID!, label: String): FaceGroup! @isAuthorized
"Merge two face groups into a single one, all ImageFaces from source will be moved to destination"
@ -262,7 +267,7 @@ type Album {
paginate: Pagination
): [Album!]!
"The album witch contains this album"
"The album which contains this album"
parentAlbum: Album
"The user who owns this album"
owner: User!

6400
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -71,6 +71,7 @@
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.1.9",
"apollo": "^2.33.4",
"husky": "^6.0.0",
"i18next-parser": "^4.2.0",
"lint-staged": "^11.0.1",

View File

@ -25,6 +25,7 @@ const ALBUM_QUERY = gql`
id
title
thumbnail {
id
thumbnail {
url
}

View File

@ -19,6 +19,7 @@ export interface albumQuery_album_subAlbums_thumbnail_thumbnail {
export interface albumQuery_album_subAlbums_thumbnail {
__typename: 'Media'
id: string
/**
* URL to display the media in a smaller resolution
*/

View File

@ -10,6 +10,7 @@ const getAlbumsQuery = gql`
id
title
thumbnail {
id
thumbnail {
url
}

View File

@ -8,34 +8,35 @@
// ====================================================
export interface getMyAlbums_myAlbums_thumbnail_thumbnail {
__typename: "MediaURL";
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string;
url: string
}
export interface getMyAlbums_myAlbums_thumbnail {
__typename: "Media";
__typename: 'Media'
id: string
/**
* URL to display the media in a smaller resolution
*/
thumbnail: getMyAlbums_myAlbums_thumbnail_thumbnail | null;
thumbnail: getMyAlbums_myAlbums_thumbnail_thumbnail | null
}
export interface getMyAlbums_myAlbums {
__typename: "Album";
id: string;
title: string;
__typename: 'Album'
id: string
title: string
/**
* An image in this album used for previewing this album
*/
thumbnail: getMyAlbums_myAlbums_thumbnail | null;
thumbnail: getMyAlbums_myAlbums_thumbnail | null
}
export interface getMyAlbums {
/**
* List of albums owned by the logged in user.
*/
myAlbums: getMyAlbums_myAlbums[];
myAlbums: getMyAlbums_myAlbums[]
}

View File

@ -8,10 +8,13 @@
// ====================================================
export interface CheckInitialSetup_siteInfo {
__typename: "SiteInfo";
initialSetup: boolean;
__typename: 'SiteInfo'
/**
* Whether or not the initial setup wizard should be shown
*/
initialSetup: boolean
}
export interface CheckInitialSetup {
siteInfo: CheckInitialSetup_siteInfo;
siteInfo: CheckInitialSetup_siteInfo
}

View File

@ -27,6 +27,7 @@ export const SHARE_ALBUM_QUERY = gql`
id
title
thumbnail {
id
thumbnail {
url
}

View File

@ -19,6 +19,7 @@ export interface shareAlbumQuery_album_subAlbums_thumbnail_thumbnail {
export interface shareAlbumQuery_album_subAlbums_thumbnail {
__typename: 'Media'
id: string
/**
* URL to display the media in a smaller resolution
*/

View File

@ -14,12 +14,12 @@ export enum LanguageTranslation {
German = 'German',
Italian = 'Italian',
Polish = 'Polish',
Portuguese = 'Portuguese',
Russian = 'Russian',
SimplifiedChinese = 'SimplifiedChinese',
Spanish = 'Spanish',
Swedish = 'Swedish',
TraditionalChinese = 'TraditionalChinese',
SimplifiedChinese = 'SimplifiedChinese',
Portuguese = 'Portuguese',
}
export enum MediaType {

View File

@ -0,0 +1,20 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: faceDetectionEnabled
// ====================================================
export interface faceDetectionEnabled_siteInfo {
__typename: 'SiteInfo'
/**
* Whether or not face detection is enabled and working
*/
faceDetectionEnabled: boolean
}
export interface faceDetectionEnabled {
siteInfo: faceDetectionEnabled_siteInfo
}

View File

@ -0,0 +1,155 @@
import React, { useState, useEffect } from 'react'
import { useMutation, gql } from '@apollo/client'
import { useTranslation } from 'react-i18next'
import { SidebarSection, SidebarSectionTitle } from './SidebarComponents'
import {
setAlbumCover,
setAlbumCoverVariables,
} from './__generated__/setAlbumCover'
import {
resetAlbumCover,
resetAlbumCoverVariables,
} from './__generated__/resetAlbumCover'
const RESET_ALBUM_COVER_MUTATION = gql`
mutation resetAlbumCover($albumID: ID!) {
resetAlbumCover(albumID: $albumID) {
id
thumbnail {
id
thumbnail {
url
}
}
}
}
`
const SET_ALBUM_COVER_MUTATION = gql`
mutation setAlbumCover($coverID: ID!) {
setAlbumCover(coverID: $coverID) {
id
thumbnail {
id
thumbnail {
url
}
}
}
}
`
type SidebarPhotoCoverProps = {
cover_id: string
}
export const SidebarPhotoCover = ({ cover_id }: SidebarPhotoCoverProps) => {
const { t } = useTranslation()
const [setAlbumCover] = useMutation<setAlbumCover, setAlbumCoverVariables>(
SET_ALBUM_COVER_MUTATION,
{
variables: {
coverID: cover_id,
},
}
)
const [buttonDisabled, setButtonDisabled] = useState(false)
useEffect(() => {
setButtonDisabled(false)
}, [cover_id])
return (
<SidebarSection>
<SidebarSectionTitle>
{t('sidebar.album.cover_photo', 'Album cover')}
</SidebarSectionTitle>
<div>
<table className="border-collapse w-full">
<tfoot>
<tr className="text-left border-gray-100 border-b border-t">
<td colSpan={2} className="pl-4 py-2">
<button
className="disabled:opacity-50 text-green-500 font-bold uppercase text-xs"
disabled={buttonDisabled}
onClick={() => {
setButtonDisabled(true),
setAlbumCover({
variables: {
coverID: cover_id,
},
})
}}
>
<span>
{t('sidebar.album.set_cover', 'Set as album cover photo')}
</span>
</button>
</td>
</tr>
</tfoot>
</table>
</div>
</SidebarSection>
)
}
type SidebarAlbumCoverProps = {
id: string
}
export const SidebarAlbumCover = ({ id }: SidebarAlbumCoverProps) => {
const { t } = useTranslation()
const [resetAlbumCover] = useMutation<
resetAlbumCover,
resetAlbumCoverVariables
>(RESET_ALBUM_COVER_MUTATION, {
variables: {
albumID: id,
},
})
const [buttonDisabled, setButtonDisabled] = useState(false)
useEffect(() => {
setButtonDisabled(false)
}, [id])
return (
<SidebarSection>
<SidebarSectionTitle>
{t('sidebar.album.album_cover', 'Album cover')}
</SidebarSectionTitle>
<div>
<table className="border-collapse w-full">
<tfoot>
<tr className="text-left border-gray-100 border-b border-t">
<td colSpan={2} className="pl-4 py-2">
<button
className="disabled:opacity-50 text-red-500 font-bold uppercase text-xs"
disabled={buttonDisabled}
onClick={() => {
setButtonDisabled(true),
resetAlbumCover({
variables: {
albumID: id,
},
})
}}
>
<span>
{t('sidebar.album.reset_cover', 'Reset cover photo')}
</span>
</button>
</td>
</tr>
</tfoot>
</table>
</div>
</SidebarSection>
)
}

View File

@ -7,6 +7,7 @@ import {
getAlbumSidebar,
getAlbumSidebarVariables,
} from './__generated__/getAlbumSidebar'
import { SidebarAlbumCover } from './AlbumCovers'
const albumQuery = gql`
query getAlbumSidebar($id: ID!) {
@ -46,6 +47,9 @@ const AlbumSidebar = ({ albumId }: AlbumSidebarProps) => {
{/* <h1 className="text-3xl font-semibold">{data.album.title}</h1> */}
<SidebarAlbumShare id={albumId} />
</div>
<div className="mt-8">
<SidebarAlbumCover id={albumId} />
</div>
</div>
)
}

View File

@ -26,6 +26,7 @@ import {
import { sidebarDownloadQuery_media_downloads } from './__generated__/sidebarDownloadQuery'
import SidebarHeader from './SidebarHeader'
import { SidebarPhotoCover } from './AlbumCovers'
const SIDEBAR_MEDIA_QUERY = gql`
query sidebarPhoto($id: ID!) {
@ -365,6 +366,9 @@ const SidebarContent = ({ media, hidePreview }: SidebarContentProps) => {
<MetadataInfo media={media} />
<SidebarDownload media={media} />
<SidebarPhotoShare id={media.id} />
<div className="mt-8">
<SidebarPhotoCover cover_id={media.id} />
</div>
</div>
)
}

View File

@ -312,8 +312,7 @@ export const SidebarPhotoShare = ({ id }: SidebarSharePhotoProps) => {
const [
loadShares,
{ loading: queryLoading, error: sharesError, data: sharesData },
] =
useLazyQuery<sidebarGetPhotoShares, sidebarGetPhotoSharesVariables>(
] = useLazyQuery<sidebarGetPhotoShares, sidebarGetPhotoSharesVariables>(
SHARE_PHOTO_QUERY
)

View File

@ -0,0 +1,45 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL mutation operation: resetAlbumCover
// ====================================================
export interface resetAlbumCover_resetAlbumCover_thumbnail_thumbnail {
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string
}
export interface resetAlbumCover_resetAlbumCover_thumbnail {
__typename: 'Media'
id: string
/**
* URL to display the media in a smaller resolution
*/
thumbnail: resetAlbumCover_resetAlbumCover_thumbnail_thumbnail | null
}
export interface resetAlbumCover_resetAlbumCover {
__typename: 'Album'
id: string
/**
* An image in this album used for previewing this album
*/
thumbnail: resetAlbumCover_resetAlbumCover_thumbnail | null
}
export interface resetAlbumCover {
/**
* Reset the assigned cover photo for an album
*/
resetAlbumCover: resetAlbumCover_resetAlbumCover
}
export interface resetAlbumCoverVariables {
albumID: string
}

View File

@ -0,0 +1,45 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL mutation operation: setAlbumCover
// ====================================================
export interface setAlbumCover_setAlbumCover_thumbnail_thumbnail {
__typename: 'MediaURL'
/**
* URL for previewing the image
*/
url: string
}
export interface setAlbumCover_setAlbumCover_thumbnail {
__typename: 'Media'
id: string
/**
* URL to display the media in a smaller resolution
*/
thumbnail: setAlbumCover_setAlbumCover_thumbnail_thumbnail | null
}
export interface setAlbumCover_setAlbumCover {
__typename: 'Album'
id: string
/**
* An image in this album used for previewing this album
*/
thumbnail: setAlbumCover_setAlbumCover_thumbnail | null
}
export interface setAlbumCover {
/**
* Assign a cover photo to an album
*/
setAlbumCover: setAlbumCover_setAlbumCover
}
export interface setAlbumCoverVariables {
coverID: string
}

View File

@ -0,0 +1,26 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL mutation operation: setAlbumCoverID
// ====================================================
export interface setAlbumCoverID_setAlbumCoverID {
__typename: 'Album'
id: string
coverID: string
}
export interface setAlbumCoverID {
/**
* Assign a cover image to an album, set coverID to -1 to remove the current one
*/
setAlbumCoverID: setAlbumCoverID_setAlbumCoverID
}
export interface setAlbumCoverIDVariables {
albumID: string
coverID: string
}