feat(web): вкладка «Редактирование» с инструментом «Свет»
В правой панели фото появилась вкладка «Редактирование» с инструментом «Свет»: ползунки экспозиции, контраста, светов, теней, белых и чёрных и точечная кривая с выбором канала. Правки сохраняются автоматически, переключатель «Сброс» удаляет параметры правок и отредактированную версию фото. Редактирование неразрушающее: параметры хранятся в метаданных фото, а отредактированная версия рендерится в фоне отдельным объектом (edit, edit_thumb, edit_medium, edit_preview) рядом с исходными вариантами. Оригинал не меняется, скачивание отдаёт исходный файл. Сетка, просмотр и обложки альбомов показывают отредактированную версию, когда она отрендерена.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
package photos
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"galo/internal/httpx"
|
||||
"galo/internal/models"
|
||||
"galo/internal/roles"
|
||||
)
|
||||
|
||||
// editablePhoto loads a photo and checks that the user may change its
|
||||
// editing parameters: the uploader of the photo or an album owner/admin.
|
||||
func (s *Service) editablePhoto(ctx context.Context, photoID, userID string) (*models.Photo, error) {
|
||||
p, err := s.store.GetByID(ctx, photoID, userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return nil, httpx.NotFound("photo_not_found", "photo not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if p.DeletedAt != nil {
|
||||
return nil, httpx.NotFound("photo_not_found", "photo not found")
|
||||
}
|
||||
_, role, err := s.albums.Access(ctx, p.AlbumID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
own := p.UploaderID == userID
|
||||
if !(roles.CanDeleteAnyPhoto(role) || (own && roles.CanDeleteOwnPhoto(role))) {
|
||||
return nil, httpx.Forbidden("forbidden", "you cannot edit this photo")
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// EditPhoto stores new editing parameters for a photo and schedules the
|
||||
// render of the edited version. The original object is never modified, so the
|
||||
// edit can always be reset.
|
||||
func (s *Service) EditPhoto(ctx context.Context, photoID, userID string, params EditParams) (*models.Photo, error) {
|
||||
p, err := s.editablePhoto(ctx, photoID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.Kind != KindImage {
|
||||
return nil, httpx.BadRequest("invalid_request", "only images can be edited")
|
||||
}
|
||||
params = params.Normalized()
|
||||
if params.IsEmpty() {
|
||||
return s.ResetEdit(ctx, photoID, userID)
|
||||
}
|
||||
if err := s.store.SetEditParams(ctx, photoID, params.JSON()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.store.EnqueueEditJob(ctx, photoID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.publishEdit(ctx, photoID, userID)
|
||||
}
|
||||
|
||||
// ResetEdit removes the editing parameters of a photo together with the
|
||||
// edited version, returning the photo to its original look.
|
||||
func (s *Service) ResetEdit(ctx context.Context, photoID, userID string) (*models.Photo, error) {
|
||||
if _, err := s.editablePhoto(ctx, photoID, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.store.ClearEditParams(ctx, photoID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.store.DeleteEditJob(ctx, photoID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.store.DeleteObjectBindings(ctx, photoID,
|
||||
RoleEdit, RoleEditThumb, RoleEditMedium, RoleEditPreview); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.publishEdit(ctx, photoID, userID)
|
||||
}
|
||||
|
||||
// publishEdit reloads a photo with its current editing state and notifies the
|
||||
// album subscribers.
|
||||
func (s *Service) publishEdit(ctx context.Context, photoID, userID string) (*models.Photo, error) {
|
||||
p, err := s.store.GetByID(ctx, photoID, userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return nil, httpx.NotFound("photo_not_found", "photo not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if err := s.attachURLs(ctx, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.hub.Publish(p.AlbumID, "photo.updated", p)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// EditParamsOf returns the editing parameters stored on a photo.
|
||||
func EditParamsOf(p *models.Photo) EditParams {
|
||||
if p == nil || len(p.EditParams) == 0 {
|
||||
return EditParams{}
|
||||
}
|
||||
data, err := json.Marshal(p.EditParams)
|
||||
if err != nil {
|
||||
return EditParams{}
|
||||
}
|
||||
params, err := ParseEditParams(data)
|
||||
if err != nil {
|
||||
return EditParams{}
|
||||
}
|
||||
return params
|
||||
}
|
||||
Reference in New Issue
Block a user