feat(web): вкладка «Редактирование» с инструментом «Свет»

В правой панели фото появилась вкладка «Редактирование» с инструментом «Свет»:
ползунки экспозиции, контраста, светов, теней, белых и чёрных и точечная кривая
с выбором канала. Правки сохраняются автоматически, переключатель «Сброс»
удаляет параметры правок и отредактированную версию фото.

Редактирование неразрушающее: параметры хранятся в метаданных фото, а
отредактированная версия рендерится в фоне отдельным объектом (edit, edit_thumb,
edit_medium, edit_preview) рядом с исходными вариантами. Оригинал не меняется,
скачивание отдаёт исходный файл. Сетка, просмотр и обложки альбомов показывают
отредактированную версию, когда она отрендерена.
This commit is contained in:
iu
2026-10-20 23:06:24 +03:00
parent afaaaab5f0
commit 0548b1ab5e
32 changed files with 1954 additions and 24 deletions
+22 -5
View File
@@ -25,6 +25,10 @@ const sqlitePhotoColumns = `p.id, COALESCE(p.album_id, ''), p.uploader_id,
COALESCE(o_m.storage_key, ''),
COALESCE(o_p.storage_key, ''),
COALESCE(o_pb.storage_key, ''),
COALESCE(o_e.storage_key, ''),
COALESCE(o_et.storage_key, ''),
COALESCE(o_em.storage_key, ''),
COALESCE(o_ep.storage_key, ''),
p.original_filename, COALESCE(o_o.size, 0), COALESCE(o_o.width, 0), COALESCE(o_o.height, 0), COALESCE(o_o.mime_type, ''), COALESCE(o_o.checksum, ''),
p.kind, COALESCE(p.duration_ms, 0),
p.taken_at, p.latitude, p.longitude, COALESCE(p.caption, ''), p.created_at,
@@ -33,6 +37,7 @@ const sqlitePhotoColumns = `p.id, COALESCE(p.album_id, ''), p.uploader_id,
(SELECT count(*) FROM comments c WHERE c.photo_id = p.id),
EXISTS(SELECT 1 FROM likes l WHERE l.photo_id = p.id AND l.user_id = $2),
COALESCE(p.exif, '{}'), p.deleted_at,
COALESCE(p.edit_params, '{}'),
COALESCE((SELECT json_group_array(json_object('id', t.id, 'name', t.name))
FROM (SELECT t.id, t.name FROM photo_tags pt JOIN tags t ON t.id = pt.tag_id
WHERE pt.photo_id = p.id AND t.user_id = $2
@@ -48,21 +53,30 @@ const sqlitePhotoJoins = `
LEFT JOIN photo_objects po_p ON po_p.photo_id = p.id AND po_p.role = 'preview'
LEFT JOIN objects o_p ON o_p.id = po_p.object_id
LEFT JOIN photo_objects po_pb ON po_pb.photo_id = p.id AND po_pb.role = 'playback'
LEFT JOIN objects o_pb ON o_pb.id = po_pb.object_id`
LEFT JOIN objects o_pb ON o_pb.id = po_pb.object_id
LEFT JOIN photo_objects po_e ON po_e.photo_id = p.id AND po_e.role = 'edit'
LEFT JOIN objects o_e ON o_e.id = po_e.object_id
LEFT JOIN photo_objects po_et ON po_et.photo_id = p.id AND po_et.role = 'edit_thumb'
LEFT JOIN objects o_et ON o_et.id = po_et.object_id
LEFT JOIN photo_objects po_em ON po_em.photo_id = p.id AND po_em.role = 'edit_medium'
LEFT JOIN objects o_em ON o_em.id = po_em.object_id
LEFT JOIN photo_objects po_ep ON po_ep.photo_id = p.id AND po_ep.role = 'edit_preview'
LEFT JOIN objects o_ep ON o_ep.id = po_ep.object_id`
func scanSQLitePhoto(scan func(...any) error) (*models.Photo, error) {
var p models.Photo
var exifText, tagsText, createdAt string
var exifText, tagsText, editText, createdAt string
var takenAt, deletedAt sql.NullString
var lat, long sql.NullFloat64
var liked int
if err := scan(&p.ID, &p.AlbumID, &p.UploaderID, &p.StorageKey,
&p.ThumbKey, &p.MediumKey, &p.PreviewKey, &p.PlaybackKey,
&p.EditKey, &p.EditThumbKey, &p.EditMediumKey, &p.EditPreviewKey,
&p.OriginalFilename, &p.Size, &p.Width, &p.Height, &p.MimeType, &p.OriginalChecksum,
&p.Kind, &p.DurationMS,
&takenAt, &lat, &long, &p.Caption, &createdAt,
&p.Uploader.ID, &p.Uploader.Name, &p.Uploader.AvatarURL,
&p.LikesCount, &p.CommentsCount, &liked, &exifText, &deletedAt, &tagsText); err != nil {
&p.LikesCount, &p.CommentsCount, &liked, &exifText, &deletedAt, &editText, &tagsText); err != nil {
return nil, err
}
p.TakenAt = sqliteutil.ParseNullTime(takenAt)
@@ -76,6 +90,7 @@ func scanSQLitePhoto(scan func(...any) error) (*models.Photo, error) {
p.DeletedAt = sqliteutil.ParseNullTime(deletedAt)
p.LikedByMe = liked != 0
setExif(&p, exifText)
setEditParams(&p, editText)
setTags(&p, tagsText)
return &p, nil
}
@@ -277,17 +292,18 @@ func (s *SQLiteStore) ListTrash(ctx context.Context, userID string) ([]models.Ph
out := []models.Photo{}
for rows.Next() {
var p models.Photo
var exifText, tagsText, createdAt, albumTitle string
var exifText, tagsText, editText, createdAt, albumTitle string
var takenAt, deletedAt sql.NullString
var lat, long sql.NullFloat64
var liked int
if err := rows.Scan(&p.ID, &p.AlbumID, &p.UploaderID, &p.StorageKey,
&p.ThumbKey, &p.MediumKey, &p.PreviewKey, &p.PlaybackKey,
&p.EditKey, &p.EditThumbKey, &p.EditMediumKey, &p.EditPreviewKey,
&p.OriginalFilename, &p.Size, &p.Width, &p.Height, &p.MimeType, &p.OriginalChecksum,
&p.Kind, &p.DurationMS,
&takenAt, &lat, &long, &p.Caption, &createdAt,
&p.Uploader.ID, &p.Uploader.Name, &p.Uploader.AvatarURL,
&p.LikesCount, &p.CommentsCount, &liked, &exifText, &deletedAt, &albumTitle, &tagsText); err != nil {
&p.LikesCount, &p.CommentsCount, &liked, &exifText, &deletedAt, &editText, &albumTitle, &tagsText); err != nil {
return nil, err
}
p.TakenAt = sqliteutil.ParseNullTime(takenAt)
@@ -302,6 +318,7 @@ func (s *SQLiteStore) ListTrash(ctx context.Context, userID string) ([]models.Ph
p.AlbumTitle = albumTitle
p.LikedByMe = liked != 0
setExif(&p, exifText)
setEditParams(&p, editText)
setTags(&p, tagsText)
out = append(out, p)
}