Select primary file for grouped photos

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-04-21 10:23:27 +02:00
parent f31c405475
commit 122e4730a3
9 changed files with 95 additions and 23 deletions

View file

@ -31,7 +31,6 @@
style="cursor: pointer"
class="accent lighten-2"
@click.exact="openPhoto(index)"
>
<v-layout
slot="placeholder"

View file

@ -2,6 +2,10 @@
width: 66px;
}
#photoprism .p-col-primary {
width: 50px;
}
#photoprism .p-photo-list tr td:first-child {
padding: 0 0 0 8px;
text-align: center;

View file

@ -11,6 +11,10 @@
:no-data-text="this.$gettext('No files found')"
>
<template slot="items" slot-scope="props" class="p-file">
<td><v-btn v-if="props.item.FileType === 'jpg'" flat :ripple="false" icon small @click.stop.prevent="model.setPrimary(props.item.FileUUID)">
<v-icon v-if="props.item.FilePrimary" color="secondary-dark">radio_button_checked</v-icon>
<v-icon v-else color="secondary-dark">radio_button_unchecked</v-icon>
</v-btn></td>
<td>
<a :href="'/api/v1/download/' + props.item.FileHash" class="secondary-dark--text" target="_blank" v-if="$config.feature('download')">
{{ props.item.FileName }}
@ -19,8 +23,8 @@
{{ props.item.FileName }}
</span>
</td>
<td>{{ props.item.FileWidth ? props.item.FileWidth : "" }}</td>
<td>{{ props.item.FileHeight ? props.item.FileHeight : "" }}</td>
<td class="hidden-sm-and-down">{{ fileDimensions(props.item) }}</td>
<td class="hidden-xs-only">{{ fileSize(props.item) }}</td>
<td>{{ fileType(props.item) }}</td>
<td>{{ fileStatus(props.item) }}</td>
</template>
@ -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");

View file

@ -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() {

View file

@ -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)
})
}

View file

@ -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))
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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)