Files
ds-galo/backend/internal/photos/edit_service.go
T
iu 0548b1ab5e feat(web): вкладка «Редактирование» с инструментом «Свет»
В правой панели фото появилась вкладка «Редактирование» с инструментом «Свет»:
ползунки экспозиции, контраста, светов, теней, белых и чёрных и точечная кривая
с выбором канала. Правки сохраняются автоматически, переключатель «Сброс»
удаляет параметры правок и отредактированную версию фото.

Редактирование неразрушающее: параметры хранятся в метаданных фото, а
отредактированная версия рендерится в фоне отдельным объектом (edit, edit_thumb,
edit_medium, edit_preview) рядом с исходными вариантами. Оригинал не меняется,
скачивание отдаёт исходный файл. Сетка, просмотр и обложки альбомов показывают
отредактированную версию, когда она отрендерена.
2026-10-20 23:06:24 +03:00

112 lines
3.3 KiB
Go

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
}