Photo edit: Publish event to keep clients in sync

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-01-30 09:51:23 +01:00
parent d9ec032371
commit 11c3ed70e3
12 changed files with 86 additions and 62 deletions

View File

@ -1,6 +1,5 @@
import Api from "common/api";
import Form from "common/form";
import Event from "pubsub-js";
class Abstract {
constructor(values) {
@ -26,18 +25,6 @@ class Abstract {
return this;
}
publishValues(values) {
if (!values) return;
this.setValues(values);
if(this.hasId()) {
Event.publish(`model.${this.constructor.getCollectionResource()}.${this.getId()}`, this.getValues());
}
return this;
}
getValues(changed) {
const result = {};
const defaults = this.getDefaults();
@ -91,11 +78,11 @@ class Abstract {
return this.update();
}
return Api.post(this.constructor.getCollectionResource(), this.getValues()).then((response) => Promise.resolve(this.publishValues(response.data)));
return Api.post(this.constructor.getCollectionResource(), this.getValues()).then((response) => Promise.resolve(this.setValues(response.data)));
}
update() {
return Api.put(this.getEntityResource(), this.getValues(true)).then((response) => Promise.resolve(this.publishValues(response.data)));
return Api.put(this.getEntityResource(), this.getValues(true)).then((response) => Promise.resolve(this.setValues(response.data)));
}
remove() {

View File

@ -79,10 +79,6 @@ class Photo extends Abstract {
}
}
getColors() {
return this.PhotoColors;
}
getGoogleMapsLink() {
return "https://www.google.com/maps/place/" + this.PhotoLat + "," + this.PhotoLng;
}
@ -215,12 +211,12 @@ class Photo extends Abstract {
addLabel(name) {
return Api.post(this.getEntityResource() + "/label", {LabelName: name})
.then((response) => Promise.resolve(this.publishValues(response.data)));
.then((response) => Promise.resolve(this.setValues(response.data)));
}
removeLabel(id) {
return Api.delete(this.getEntityResource() + "/label/" + id)
.then((response) => Promise.resolve(this.publishValues(response.data)));
.then((response) => Promise.resolve(this.setValues(response.data)));
}
static getCollectionResource() {

View File

@ -255,24 +255,28 @@
onImportCompleted() {
this.dirty = true;
if(this.selection.length === 0 && this.offset === 0) {
if (this.selection.length === 0 && this.offset === 0) {
this.refresh();
}
},
onCount() {
this.dirty = true;
},
onModelUpdate(ev, data) {
if(!this.listen) {
onPhotosUpdated(ev, data) {
if (!data || !data.entities) {
console.warn("onPhotosUpdated(): no entities found in event data");
return
}
const found = this.results.find((m) => m.ID === data.ID);
for (let i = 0; i < data.entities.length; i++) {
const values = data.entities[i];
const model = this.results.find((m) => m.ID === values.ID);
if(found) {
found.setValues(data);
this.$forceUpdate();
this.$forceUpdate();
for (let key in values) {
if (values.hasOwnProperty(key)) {
model[key] = values[key];
}
}
}
}
},
@ -281,7 +285,7 @@
this.uploadSubId = Event.subscribe("import.completed", (ev, data) => this.onImportCompleted(ev, data));
this.countSubId = Event.subscribe("count.photos", (ev, data) => this.onCount(ev, data));
this.modelSubId = Event.subscribe("model.photos", (ev, data) => this.onModelUpdate(ev, data));
this.modelSubId = Event.subscribe("photos.updated", (ev, data) => this.onPhotosUpdated(ev, data));
},
destroyed() {
Event.unsubscribe(this.uploadSubId);

View File

@ -70,7 +70,7 @@
this.$notify.blockUI();
setTimeout(() => window.location.reload(), 100);
} else {
this.$notify.info(this.$gettext("Settings saved"));
this.$notify.success(this.$gettext("Settings saved"));
}
})
},

View File

@ -8,8 +8,8 @@
"secondary-light": "#ECEFF1",
"accent": "#9E9E9E",
"error": "#E57373",
"info": "#0097A7",
"success": "#00897B",
"info": "#00ACC1",
"success": "#4DB6AC",
"warning": "#FFD740",
"remove": "#E57373",
"restore": "#64B5F6",
@ -31,8 +31,8 @@
"secondary-light": "#EEEEEE",
"accent": "#757575",
"error": "#E57373",
"info": "#0097A7",
"success": "#00897B",
"info": "#00ACC1",
"success": "#4DB6AC",
"warning": "#FFD740",
"remove": "#E57373",
"restore": "#64B5F6",
@ -54,8 +54,8 @@
"secondary-light": "#EEEEEE",
"accent": "#9E9E9E",
"error": "#E57373",
"info": "#0097A7",
"success": "#00897B",
"info": "#00ACC1",
"success": "#4DB6AC",
"warning": "#FFD740",
"remove": "#E57373",
"restore": "#64B5F6",
@ -77,8 +77,8 @@
"secondary-light": "#B0BEC5",
"accent": "#B0BEC5",
"error": "#E57373",
"info": "#0097A7",
"success": "#00897B",
"info": "#00ACC1",
"success": "#4DB6AC",
"warning": "#FFD740",
"remove": "#E57373",
"restore": "#64B5F6",
@ -100,8 +100,8 @@
"secondary-light": "#B0BEC5",
"accent": "#B0BEC5",
"error": "#E57373",
"info": "#0097A7",
"success": "#00897B",
"info": "#00ACC1",
"success": "#4DB6AC",
"warning": "#FFD740",
"remove": "#E57373",
"restore": "#64B5F6",
@ -123,8 +123,8 @@
"secondary-light": "#E0E0E0",
"accent": "#757575",
"error": "#E57373",
"info": "#0097A7",
"success": "#00897B",
"info": "#00ACC1",
"success": "#4DB6AC",
"warning": "#FFD740",
"remove": "#E57373",
"restore": "#64B5F6",
@ -147,7 +147,7 @@
"accent": "#656565",
"error": "#FF76DC",
"info": "#5A94DD",
"success": "#82BD7E",
"success": "#4DB6AC",
"warning": "#E3D181",
"love": "#EF5350",
"remove": "#E35333",

View File

@ -9,9 +9,10 @@ import (
)
var (
ErrUnauthorized = gin.H{"code": http.StatusUnauthorized, "error": txt.UcFirst(config.ErrUnauthorized.Error())}
ErrReadOnly = gin.H{"code": http.StatusForbidden, "error": txt.UcFirst(config.ErrReadOnly.Error())}
ErrUploadNSFW = gin.H{"code": http.StatusForbidden, "error": txt.UcFirst(config.ErrUploadNSFW.Error())}
ErrAlbumNotFound = gin.H{"code": http.StatusNotFound, "error": "Album not found"}
ErrPhotoNotFound = gin.H{"code": http.StatusNotFound, "error": "Photo not found"}
ErrUnauthorized = gin.H{"code": http.StatusUnauthorized, "error": txt.UcFirst(config.ErrUnauthorized.Error())}
ErrReadOnly = gin.H{"code": http.StatusForbidden, "error": txt.UcFirst(config.ErrReadOnly.Error())}
ErrUploadNSFW = gin.H{"code": http.StatusForbidden, "error": txt.UcFirst(config.ErrUploadNSFW.Error())}
ErrAlbumNotFound = gin.H{"code": http.StatusNotFound, "error": "Album not found"}
ErrPhotoNotFound = gin.H{"code": http.StatusNotFound, "error": "Photo not found"}
ErrUnexpectedError = gin.H{"code": http.StatusInternalServerError, "error": "Unexpected error"}
)

View File

@ -7,6 +7,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
@ -14,6 +15,21 @@ import (
"github.com/gin-gonic/gin"
)
func PublishPhotoUpdate(uuid string, c *gin.Context, q *query.Repo) {
f := form.PhotoSearch{ID: uuid}
result, err := q.Photos(f)
if err != nil {
log.Error(err)
c.AbortWithStatusJSON(http.StatusInternalServerError, ErrUnexpectedError)
return
}
event.Publish("photos.updated", event.Data{
"entities": result,
})
}
// GET /api/v1/photos/:uuid
//
// Parameters:
@ -45,9 +61,10 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
return
}
uuid := c.Param("uuid")
q := query.New(conf.OriginalsPath(), conf.Db())
m, err := q.FindPhotoByUUID(c.Param("uuid"))
m, err := q.FindPhotoByUUID(uuid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@ -61,9 +78,11 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
conf.Db().Save(&m)
PublishPhotoUpdate(uuid, c, q)
event.Success("photo saved")
p, err := q.PreloadPhotoByUUID(c.Param("uuid"))
p, err := q.PreloadPhotoByUUID(uuid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@ -119,8 +138,9 @@ func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
return
}
uuid := c.Param("uuid")
q := query.New(conf.OriginalsPath(), conf.Db())
m, err := q.FindPhotoByUUID(c.Param("uuid"))
m, err := q.FindPhotoByUUID(uuid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@ -134,6 +154,8 @@ func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
"count": 1,
})
PublishPhotoUpdate(uuid, c, q)
c.JSON(http.StatusOK, gin.H{"photo": m})
})
}
@ -149,8 +171,9 @@ func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
return
}
uuid := c.Param("uuid")
q := query.New(conf.OriginalsPath(), conf.Db())
m, err := q.FindPhotoByUUID(c.Param("uuid"))
m, err := q.FindPhotoByUUID(uuid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrPhotoNotFound)
@ -164,6 +187,8 @@ func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
"count": -1,
})
PublishPhotoUpdate(uuid, c, q)
c.JSON(http.StatusOK, gin.H{"photo": m})
})
}

View File

@ -73,7 +73,7 @@ func wsReader(ws *websocket.Conn, connId string, conf *config.Config) {
func wsWriter(ws *websocket.Conn, connId string) {
pingTicker := time.NewTicker(15 * time.Second)
s := event.Subscribe("log.*", "notify.*", "index.*", "upload.*", "import.*", "config.*", "count.*")
s := event.Subscribe("log.*", "notify.*", "index.*", "upload.*", "import.*", "config.*", "count.*", "photos.*", "albums.*")
defer func() {
pingTicker.Stop()

View File

@ -17,7 +17,6 @@ type Photo struct {
PhotoPath string `gorm:"type:varbinary(512);index;"`
PhotoName string `gorm:"type:varbinary(256);"`
PhotoTitle string `json:"PhotoTitle"`
PhotoTitleChanged bool `json:"PhotoTitleChanged"`
PhotoDescription string `gorm:"type:text;" json:"PhotoDescription"`
PhotoNotes string `gorm:"type:text;" json:"PhotoNotes"`
PhotoArtist string `json:"PhotoArtist"`
@ -37,16 +36,16 @@ type Photo struct {
LensID uint `gorm:"index:idx_photos_camera_lens;" json:"LensID"`
LocationID string `gorm:"type:varbinary(16);index;" json:"LocationID"`
PlaceID string `gorm:"type:varbinary(16);index;" json:"PlaceID"`
LocationChanged bool `json:"LocationChanged"`
LocationEstimated bool `json:"LocationEstimated"`
PhotoCountry string `gorm:"index:idx_photos_country_year_month;" json:"PhotoCountry"`
PhotoYear int `gorm:"index:idx_photos_country_year_month;"`
PhotoMonth int `gorm:"index:idx_photos_country_year_month;"`
TimeZone string `gorm:"type:varbinary(64);" json:"TimeZone"`
TakenAtLocal time.Time `gorm:"type:datetime;"`
TakenAtChanged bool
PhotoViews uint
CountryChanged bool
ModifiedTitle bool `json:"ModifiedTitle"`
ModifiedDetails bool `json:"ModifiedDetails"`
ModifiedLocation bool `json:"ModifiedLocation"`
ModifiedDate bool `json:"ModifiedDate"`
Camera *Camera `json:"Camera"`
Lens *Lens `json:"Lens"`
Location *Location `json:"-"`

View File

@ -7,6 +7,7 @@ import (
// PhotoSearch represents search form fields for "/api/v1/photos".
type PhotoSearch struct {
Query string `form:"q"`
ID string `form:"id"`
Title string `form:"title"`
Description string `form:"description"`
Notes string `form:"notes"`

View File

@ -133,7 +133,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult {
labels = append(labels, locLabels...)
}
if photo.NoTitle() || (fileChanged || o.UpdateTitle) && photo.PhotoTitleChanged == false && photo.NoLocation() {
if photo.NoTitle() || (fileChanged || o.UpdateTitle) && photo.ModifiedTitle == false && photo.NoLocation() {
if len(labels) > 0 && labels[0].Priority >= -1 && labels[0].Uncertainty <= 85 && labels[0].Name != "" {
photo.PhotoTitle = fmt.Sprintf("%s / %s", txt.Title(labels[0].Name), m.DateCreated().Format("2006"))
} else if !photo.TakenAtLocal.IsZero() {
@ -164,7 +164,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult {
} else if m.IsXMP() {
// TODO: Proof-of-concept for indexing XMP sidecar files
if data, err := meta.XMP(m.Filename()); err == nil {
if data.Title != "" && !photo.PhotoTitleChanged {
if data.Title != "" && !photo.ModifiedTitle {
photo.PhotoTitle = data.Title
}
@ -430,7 +430,7 @@ func (ind *Index) indexLocation(mediaFile *MediaFile, photo *entity.Photo, label
labels = append(labels, classify.LocationLabel(locCategory, 0, -1))
}
if (fileChanged || o.UpdateTitle) && photo.PhotoTitleChanged == false {
if (fileChanged || o.UpdateTitle) && photo.ModifiedTitle == false {
if title := labels.Title(location.Name()); title != "" { // TODO: User defined title format
log.Infof("index: using label \"%s\" to create photo title", title)
if location.NoCity() || location.LongCity() || location.CityContains(title) {

View File

@ -126,6 +126,17 @@ func (s *Repo) Photos(f form.PhotoSearch) (results []PhotoResult, err error) {
Joins("LEFT JOIN photos_labels ON photos_labels.photo_id = photos.id").
Where("files.file_missing = 0").
Group("photos.id, files.id")
if f.ID != "" {
q = q.Where("photos.photo_uuid = ?", f.ID)
if result := q.Scan(&results); result.Error != nil {
return results, result.Error
}
return results, nil
}
var categories []entity.Category
var label entity.Label
var labelIds []uint