feat: context menus, unified photo panel, tags, album trash and counters
- backend: deleting an album moves its photos to the trash without losing files (photos.album_id nullable, FK ON DELETE SET NULL, migration 000006; dropped S3 prefix delete); restore accepts an album target (album_required when the album is gone); user-scoped tags (000007) with GET /tags, POST/DELETE /photos/:id/tags, tags included in photo responses; GET /trash/count - frontend: right-click context menus for photos (grid + viewer) and albums (rail + overview); single shared PhotoPanel reused in viewer and gallery selection (not a copy); viewer download/delete moved to the panel 'Действия'; transparent compact viewer top bar; trash entry as last rail item with per-album photo counts and trash count; album rename/delete dialogs - openspec: sync lightroom-workspace-ui artifacts (specs photos/ui-workspace, design #24-30, tasks 9.17-9.24)
This commit is contained in:
@@ -143,7 +143,11 @@ func (h *Handler) PermanentDelete(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Handler) Restore(c *gin.Context) {
|
||||
p, err := h.svc.Restore(c.Request.Context(), c.Param("id"), auth.UserID(c))
|
||||
var req struct {
|
||||
AlbumID string `json:"album_id"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
p, err := h.svc.Restore(c.Request.Context(), c.Param("id"), auth.UserID(c), req.AlbumID)
|
||||
if err != nil {
|
||||
httpx.RespondError(c, err)
|
||||
return
|
||||
@@ -160,6 +164,49 @@ func (h *Handler) Trash(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"photos": list})
|
||||
}
|
||||
|
||||
func (h *Handler) TrashCount(c *gin.Context) {
|
||||
n, err := h.svc.TrashCount(c.Request.Context(), auth.UserID(c))
|
||||
if err != nil {
|
||||
httpx.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"count": n})
|
||||
}
|
||||
|
||||
func (h *Handler) Tags(c *gin.Context) {
|
||||
tags, err := h.svc.Tags(c.Request.Context(), auth.UserID(c), c.Query("q"))
|
||||
if err != nil {
|
||||
httpx.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"tags": tags})
|
||||
}
|
||||
|
||||
func (h *Handler) AddTag(c *gin.Context) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.Name == "" {
|
||||
httpx.RespondError(c, httpx.BadRequest("invalid_request", "name is required"))
|
||||
return
|
||||
}
|
||||
p, err := h.svc.AddTag(c.Request.Context(), c.Param("id"), auth.UserID(c), req.Name)
|
||||
if err != nil {
|
||||
httpx.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"photo": p})
|
||||
}
|
||||
|
||||
func (h *Handler) RemoveTag(c *gin.Context) {
|
||||
p, err := h.svc.RemoveTag(c.Request.Context(), c.Param("id"), auth.UserID(c), c.Param("tag_id"))
|
||||
if err != nil {
|
||||
httpx.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"photo": p})
|
||||
}
|
||||
|
||||
type batchRequest struct {
|
||||
IDs []string `json:"ids" binding:"required"`
|
||||
AlbumID string `json:"album_id"`
|
||||
|
||||
Reference in New Issue
Block a user