feat: initial asset editing implementation

feat: db insertions for edits

feat: get asset edits endpoint

feat: wip apply edits

feat: finish asset files changes

feat: wip

feat: wip

fix: openapi

fix: tests

the failing tests were so scuffed. Simply solved by adding [] to the param list

feat: more wip

feat: more wip

feat: some more tests and fixes

chore: fix default for getting thumbnail and add todo for tests

feat: LRTB validation

chore: code cleanup

chore: more test checks for cleanup

feat: show edit pane

fix: state issues

chore: restructure web editor

feat: restructure edit manager

feat: refactor cropManager

chore: combine all editing

chore: web editing improvements

fix: handling when no crops

fix: openapi enum

chore: more edit refactoring

fix: make image decoding more efficient

chore: more refactoring

fix: getCrop LRTB algorithm

fix: missing await

chore: use relative coordinates for edit

chore: update sql

fix: use resize observer instead of svelte:doc resize hook

chore: simplify quad box generation

fix: light mode styling

chore: refactor to not be a recursive job call

this simplifies the logic and the job only completes once thumbhash and others are properly updated

chore: more refactoring

feat: use affine transforms for most operations

feat: bounding box edit transformation

feat: tests

chore: sql and openapi sync

fix: medium tests

fix: rotated OCR

chore: cleanup transform test

fix: remove rebase issue

fix(server): block edits for live photos, gifs, panoramic photos

fix: openapi enum validation

chore: rename edit endpoint

chore: remove public modifiers

feat: delete endpoint

chore: use === and !== explicitly

fix: require 1 edit for the editAsset endpoint

fix: remove thumbnail edit notification and use on_upload_success instead

fix: primary key on asset edit table

chore: refactor to isPanorama

chore: rename editRepository to assetEditRepository

fix: missing toLowerCase

fix: db migrations

chore: update sql files
This commit is contained in:
bwees
2025-11-20 23:26:45 -06:00
parent 1565d15537
commit 01fb712691
93 changed files with 5846 additions and 1304 deletions

View File

@@ -1,5 +1,6 @@
import { AssetFace, AssetFile, Exif } from 'src/database';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { EditAction, EditActionItem } from 'src/dtos/editing.dto';
import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { StorageAsset } from 'src/types';
import { authStub } from 'test/fixtures/auth.stub';
@@ -36,8 +37,35 @@ const sidecarFileWithoutExt: AssetFile = {
path: '/original/path.xmp',
};
const editedPreviewFile: AssetFile = {
id: 'file-4',
type: AssetFileType.EditedPreview,
path: '/uploads/user-id/preview/path_edited.jpg',
};
const editedThumbnailFile: AssetFile = {
id: 'file-5',
type: AssetFileType.EditedThumbnail,
path: '/uploads/user-id/thumbnail/path_edited.jpg',
};
const editedFullsizeFile: AssetFile = {
id: 'file-6',
type: AssetFileType.EditedFullSize,
path: '/uploads/user-id/fullsize/path_edited.jpg',
};
const files: AssetFile[] = [fullsizeFile, previewFile, thumbnailFile];
const editedFiles: AssetFile[] = [
fullsizeFile,
previewFile,
thumbnailFile,
editedFullsizeFile,
editedPreviewFile,
editedThumbnailFile,
];
export const stackStub = (stackId: string, assets: (MapAsset & { exifInfo: Exif })[]) => {
return {
id: stackId,
@@ -101,6 +129,9 @@ export const assetStub = {
stackId: null,
updateId: '42',
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
noWebpPath: Object.freeze({
@@ -139,6 +170,9 @@ export const assetStub = {
stackId: null,
updateId: '42',
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
noThumbhash: Object.freeze({
@@ -174,6 +208,9 @@ export const assetStub = {
stackId: null,
updateId: '42',
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
primaryImage: Object.freeze({
@@ -219,6 +256,9 @@ export const assetStub = {
updateId: '42',
libraryId: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
image: Object.freeze({
@@ -264,6 +304,7 @@ export const assetStub = {
height: null,
width: null,
visibility: AssetVisibility.Timeline,
edits: [],
}),
trashed: Object.freeze({
@@ -304,6 +345,9 @@ export const assetStub = {
stackId: null,
updateId: '42',
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
trashedOffline: Object.freeze({
@@ -344,6 +388,9 @@ export const assetStub = {
stackId: null,
updateId: '42',
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
archived: Object.freeze({
id: 'asset-id',
@@ -383,6 +430,9 @@ export const assetStub = {
stackId: null,
updateId: '42',
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
external: Object.freeze({
@@ -422,6 +472,9 @@ export const assetStub = {
stackId: null,
stack: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
image1: Object.freeze({
@@ -461,6 +514,9 @@ export const assetStub = {
libraryId: null,
stack: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
imageFrom2015: Object.freeze({
@@ -499,6 +555,9 @@ export const assetStub = {
duplicateId: null,
isOffline: false,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
video: Object.freeze({
@@ -539,6 +598,9 @@ export const assetStub = {
libraryId: null,
stackId: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
livePhotoMotionAsset: Object.freeze({
@@ -556,7 +618,10 @@ export const assetStub = {
files: [] as AssetFile[],
libraryId: null,
visibility: AssetVisibility.Hidden,
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif }),
width: null,
height: null,
edits: [] as EditActionItem[],
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif; edits: EditActionItem[] }),
livePhotoStillAsset: Object.freeze({
id: 'live-photo-still-asset',
@@ -574,7 +639,10 @@ export const assetStub = {
files,
faces: [] as AssetFace[],
visibility: AssetVisibility.Timeline,
} as MapAsset & { faces: AssetFace[]; files: AssetFile[] }),
width: null,
height: null,
edits: [] as EditActionItem[],
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: EditActionItem[] }),
livePhotoWithOriginalFileName: Object.freeze({
id: 'live-photo-still-asset',
@@ -594,7 +662,10 @@ export const assetStub = {
libraryId: null,
faces: [] as AssetFace[],
visibility: AssetVisibility.Timeline,
} as MapAsset & { faces: AssetFace[]; files: AssetFile[] }),
width: null,
height: null,
edits: [] as EditActionItem[],
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: EditActionItem[] }),
withLocation: Object.freeze({
id: 'asset-with-favorite-id',
@@ -638,6 +709,9 @@ export const assetStub = {
isOffline: false,
tags: [],
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
sidecar: Object.freeze({
@@ -673,6 +747,9 @@ export const assetStub = {
libraryId: null,
stackId: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
sidecarWithoutExt: Object.freeze({
@@ -705,6 +782,9 @@ export const assetStub = {
duplicateId: null,
isOffline: false,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
hasEncodedVideo: Object.freeze({
@@ -744,6 +824,9 @@ export const assetStub = {
stackId: null,
stack: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
hasFileExtension: Object.freeze({
@@ -780,6 +863,9 @@ export const assetStub = {
duplicateId: null,
isOffline: false,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
imageDng: Object.freeze({
@@ -820,6 +906,9 @@ export const assetStub = {
libraryId: null,
stackId: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
imageHif: Object.freeze({
@@ -860,6 +949,9 @@ export const assetStub = {
libraryId: null,
stackId: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
panoramaTif: Object.freeze({
id: 'asset-id',
@@ -899,5 +991,110 @@ export const assetStub = {
libraryId: null,
stackId: null,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
edits: [],
}),
withCropEdit: Object.freeze({
id: 'asset-id',
status: AssetStatus.Active,
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
owner: userStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/original/path.jpg',
files,
checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.Image,
thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2025-01-01T01:02:03.456Z'),
isFavorite: true,
duration: null,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
updateId: 'foo',
libraryId: null,
stackId: null,
sharedLinks: [],
originalFileName: 'asset-id.jpg',
faces: [],
deletedAt: null,
sidecarPath: null,
exifInfo: {
fileSizeInByte: 5000,
exifImageHeight: 3840,
exifImageWidth: 2160,
} as Exif,
duplicateId: null,
isOffline: false,
stack: null,
orientation: '',
projectionType: null,
height: 3840,
width: 2160,
visibility: AssetVisibility.Timeline,
edits: [
{
action: EditAction.Crop,
parameters: {
width: 1512,
height: 1152,
x: 216,
y: 1512,
},
},
] as EditActionItem[],
}),
withRevertedEdits: Object.freeze({
id: 'asset-id',
status: AssetStatus.Active,
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
owner: userStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/original/path.jpg',
files: editedFiles,
checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.Image,
thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2025-01-01T01:02:03.456Z'),
isFavorite: true,
duration: null,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
updateId: 'foo',
libraryId: null,
stackId: null,
sharedLinks: [],
originalFileName: 'asset-id.jpg',
faces: [],
deletedAt: null,
sidecarPath: null,
exifInfo: {
fileSizeInByte: 5000,
exifImageHeight: 3840,
exifImageWidth: 2160,
} as Exif,
duplicateId: null,
isOffline: false,
stack: null,
orientation: '',
projectionType: null,
height: 3840,
width: 2160,
visibility: AssetVisibility.Timeline,
edits: [],
}),
};