From 122e4730a38c59b2574b74741ee337d68e22eb9c Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Tue, 21 Apr 2020 10:23:27 +0200 Subject: [PATCH] Select primary file for grouped photos Signed-off-by: Michael Mayer --- frontend/src/component/p-photo-details.vue | 1 - frontend/src/css/photos.css | 4 +++ frontend/src/dialog/photo/files.vue | 30 +++++++++++++---- frontend/src/model/photo.js | 16 ++++++--- internal/api/photo.go | 38 ++++++++++++++++++++++ internal/entity/photo.go | 2 +- internal/query/file.go | 10 +++++- internal/query/photo.go | 16 ++++----- internal/server/routes.go | 1 + 9 files changed, 95 insertions(+), 23 deletions(-) diff --git a/frontend/src/component/p-photo-details.vue b/frontend/src/component/p-photo-details.vue index f62242538..85427d271 100644 --- a/frontend/src/component/p-photo-details.vue +++ b/frontend/src/component/p-photo-details.vue @@ -31,7 +31,6 @@ style="cursor: pointer" class="accent lighten-2" @click.exact="openPhoto(index)" - > @@ -40,9 +44,10 @@ readonly: this.$config.getValue("readonly"), selected: [], listColumns: [ + {text: this.$gettext('Primary'), value: 'FilePrimary', sortable: false, align: 'center', class: 'p-col-primary'}, {text: this.$gettext('Name'), value: 'FileName', sortable: false, align: 'left'}, - {text: this.$gettext('Width'), value: 'FileWidth', sortable: false}, - {text: this.$gettext('Height'), value: 'FileHeight', sortable: false}, + {text: this.$gettext('Dimensions'), value: '', sortable: false, class: 'hidden-sm-and-down'}, + {text: this.$gettext('Size'), value: 'FileSize', sortable: false, class: 'hidden-xs-only'}, {text: this.$gettext('Type'), value: '', sortable: false, align: 'left'}, {text: this.$gettext('Status'), value: '', sortable: false, align: 'left'}, ], @@ -53,10 +58,21 @@ openPhoto() { this.$viewer.show([this.model], 0) }, + fileDimensions(file) { + if(!file.FileWidth || !file.FileHeight) { return ""; } + + return file.FileWidth + " × " + file.FileHeight; + }, + fileSize(file) { + if (!file.FileSize) { + return ""; + } + const kb = Number.parseFloat(file.FileSize) / 1048576; + + return kb.toFixed(1) + " MB"; + }, fileType(file) { - if (file.FilePrimary) { - return this.$gettext("Primary"); - } else if (file.FileVideo) { + if (file.FileVideo) { return this.$gettext("Video"); } else if (file.FileSidecar) { return this.$gettext("Sidecar"); diff --git a/frontend/src/model/photo.js b/frontend/src/model/photo.js index 9b6f720d4..4f7f4b065 100644 --- a/frontend/src/model/photo.js +++ b/frontend/src/model/photo.js @@ -116,7 +116,9 @@ class Photo extends RestModel { } getThumbnailUrl(type) { - if (this.FileHash) { + if (this.Files && this.Files.length) { + return "/api/v1/thumbnails/" + this.Files[0].FileHash + "/" + type; + } else if (this.FileHash) { return "/api/v1/thumbnails/" + this.FileHash + "/" + type; } @@ -215,6 +217,10 @@ class Photo extends RestModel { } } + setPrimary(fileUUID) { + return Api.post(this.getEntityResource() + "/primary/" + fileUUID).then((r) => Promise.resolve(this.setValues(r.data))); + } + like() { this.PhotoFavorite = true; return Api.post(this.getEntityResource() + "/like"); @@ -227,22 +233,22 @@ class Photo extends RestModel { addLabel(name) { return Api.post(this.getEntityResource() + "/label", {LabelName: name, LabelPriority: 10}) - .then((response) => Promise.resolve(this.setValues(response.data))); + .then((r) => Promise.resolve(this.setValues(r.data))); } activateLabel(id) { return Api.put(this.getEntityResource() + "/label/" + id, {Uncertainty: 0}) - .then((response) => Promise.resolve(this.setValues(response.data))); + .then((r) => Promise.resolve(this.setValues(r.data))); } renameLabel(id, name) { return Api.put(this.getEntityResource() + "/label/" + id, {Label: {LabelName: name}}) - .then((response) => Promise.resolve(this.setValues(response.data))); + .then((r) => Promise.resolve(this.setValues(r.data))); } removeLabel(id) { return Api.delete(this.getEntityResource() + "/label/" + id) - .then((response) => Promise.resolve(this.setValues(response.data))); + .then((r) => Promise.resolve(this.setValues(r.data))); } update() { diff --git a/internal/api/photo.go b/internal/api/photo.go index 198af6384..dc7c8e966 100644 --- a/internal/api/photo.go +++ b/internal/api/photo.go @@ -194,3 +194,41 @@ func DislikePhoto(router *gin.RouterGroup, conf *config.Config) { c.JSON(http.StatusOK, gin.H{"photo": m}) }) } + +// POST /api/v1/photos/:uuid/primary/:file_uuid +// +// Parameters: +// uuid: string PhotoUUID as returned by the API +func SetPhotoPrimary(router *gin.RouterGroup, conf *config.Config) { + router.POST("/photos/:uuid/primary/:file_uuid", func(c *gin.Context) { + if Unauthorized(c, conf) { + c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized) + return + } + + db := conf.Db() + + uuid := c.Param("uuid") + fileUUID := c.Param("file_uuid") + q := query.New(db) + err := q.SetPhotoPrimary(uuid, fileUUID) + + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + return + } + + PublishPhotoEvent(EntityUpdated, uuid, c, q) + + event.Success("photo saved") + + p, err := q.PreloadPhotoByUUID(uuid) + + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound) + return + } + + c.JSON(http.StatusOK, p) + }) +} diff --git a/internal/entity/photo.go b/internal/entity/photo.go index aa85c375a..efdbf2ba5 100644 --- a/internal/entity/photo.go +++ b/internal/entity/photo.go @@ -212,7 +212,7 @@ func (m *Photo) PreloadFiles(db *gorm.DB) { Table("files"). Select(`files.*`). Where("files.photo_id = ?", m.ID). - Order("files.file_primary DESC") + Order("files.file_name DESC") logError(q.Scan(&m.Files)) } diff --git a/internal/query/file.go b/internal/query/file.go index 482220ca2..b346e005b 100644 --- a/internal/query/file.go +++ b/internal/query/file.go @@ -1,6 +1,8 @@ package query -import "github.com/photoprism/photoprism/internal/entity" +import ( + "github.com/photoprism/photoprism/internal/entity" +) // Files finds files returning maximum results defined by limit // and finding them from an offest defined by offset. @@ -47,3 +49,9 @@ func (q *Query) FileByHash(fileHash string) (file entity.File, err error) { return file, nil } + +// SetPhotoPrimary sets a new primary image file for a photo. +func (q *Query) SetPhotoPrimary(photoUUID, fileUUID string) error { + q.db.Model(entity.File{}).Where("photo_uuid = ? AND file_uuid <> ?", photoUUID, fileUUID).UpdateColumn("file_primary", false) + return q.db.Model(entity.File{}).Where("photo_uuid = ? AND file_uuid = ?", photoUUID, fileUUID).UpdateColumn("file_primary", true).Error +} diff --git a/internal/query/photo.go b/internal/query/photo.go index 015b56276..646f80c7e 100644 --- a/internal/query/photo.go +++ b/internal/query/photo.go @@ -157,16 +157,16 @@ func (q *Query) Photos(f form.PhotoSearch) (results PhotoResults, count int, err lenses.lens_make, lenses.lens_model, places.loc_label, places.loc_city, places.loc_state, places.loc_country `). - Joins("JOIN files ON files.photo_id = photos.id AND files.file_type = 'jpg' AND files.deleted_at IS NULL"). + Joins("JOIN files ON files.photo_id = photos.id AND files.file_type = 'jpg' AND files.file_missing = 0 AND files.deleted_at IS NULL"). Joins("JOIN cameras ON cameras.id = photos.camera_id"). Joins("JOIN lenses ON lenses.id = photos.lens_id"). Joins("JOIN places ON photos.place_id = places.id"). Joins("LEFT JOIN photos_labels ON photos_labels.photo_id = photos.id AND photos_labels.uncertainty < 100"). - Where("files.file_missing = 0"). Group("photos.id, files.id") if f.ID != "" { s = s.Where("photos.photo_uuid = ?", f.ID) + s = s.Order("files.file_primary DESC") if result := s.Scan(&results); result.Error != nil { return results, 0, result.Error @@ -363,17 +363,17 @@ func (q *Query) Photos(f form.PhotoSearch) (results PhotoResults, count int, err switch f.Order { case entity.SortOrderRelevance: - s = s.Order("photo_story DESC, photo_favorite DESC, taken_at DESC") + s = s.Order("photo_story DESC, photo_favorite DESC, taken_at DESC, files.file_primary DESC") case entity.SortOrderNewest: - s = s.Order("taken_at DESC, photos.photo_uuid") + s = s.Order("taken_at DESC, photos.photo_uuid, files.file_primary DESC") case entity.SortOrderOldest: - s = s.Order("taken_at, photos.photo_uuid") + s = s.Order("taken_at, photos.photo_uuid, files.file_primary DESC") case entity.SortOrderImported: - s = s.Order("photos.id DESC") + s = s.Order("photos.id DESC, files.file_primary DESC") case entity.SortOrderSimilar: - s = s.Order("files.file_main_color, photos.location_id, files.file_diff, taken_at DESC") + s = s.Order("files.file_main_color, photos.location_id, files.file_diff, taken_at DESC, files.file_primary DESC") default: - s = s.Order("taken_at DESC, photos.photo_uuid") + s = s.Order("taken_at DESC, photos.photo_uuid, files.file_primary DESC") } if f.Count > 0 && f.Count <= 1000 { diff --git a/internal/server/routes.go b/internal/server/routes.go index 12ce62719..44bed0452 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -41,6 +41,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) { api.GetMomentsTime(v1, conf) api.GetFile(v1, conf) api.LinkFile(v1, conf) + api.SetPhotoPrimary(v1, conf) api.GetLabels(v1, conf) api.UpdateLabel(v1, conf)