Select primary file for grouped photos
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
f31c405475
commit
122e4730a3
9 changed files with 95 additions and 23 deletions
|
@ -31,7 +31,6 @@
|
|||
style="cursor: pointer"
|
||||
class="accent lighten-2"
|
||||
@click.exact="openPhoto(index)"
|
||||
|
||||
>
|
||||
<v-layout
|
||||
slot="placeholder"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue