Photo edit: Publish event to keep clients in sync
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
d9ec032371
commit
11c3ed70e3
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
@ -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"));
|
||||
}
|
||||
})
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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"}
|
||||
)
|
||||
|
@ -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})
|
||||
})
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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:"-"`
|
||||
|
@ -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"`
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user