feat: orientation toggle and mismatch indicator in crop editor
CI / test (push) Has been cancelled

The crop tool now exposes a landscape/portrait toggle next to the
device-name label, and the canvas crop frame snaps to the chosen
aspect when toggled. Choosing an orientation that does not match
the target frame's current orientation surfaces a yellow informational
chip — purely informational, no action required, clears as soon as
the user toggles back to the matching orientation (or changes the
frame in Settings).

The chosen orientation rides along on the upload/reprocess request
as a new cropOrientation form field and is persisted on the Image
entity, so the library view and rotation logic can later surface
the same mismatch state for already-uploaded photos. Existing photos
without a stored orientation get null and are unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 14:45:59 -04:00
parent c387260ee7
commit 52e85703f7
11 changed files with 225 additions and 38 deletions
+1
View File
@@ -12,6 +12,7 @@ const makeImage = (overrides: Partial<Image> = {}): Image => ({
approvedDeviceIds: [],
cropParams: null,
stickerState: null,
cropOrientation: null,
...overrides,
})
+4 -2
View File
@@ -59,7 +59,7 @@ describe('upload store', () => {
expect(store.selectedDeviceIds).toEqual([])
})
it('setCrop stores croppedBlob and cropParams', () => {
it('setCrop stores croppedBlob, cropParams, and cropOrientation', () => {
const store = useUploadStore()
const file = new File(['data'], 'photo.jpg')
store.init(file)
@@ -67,12 +67,13 @@ describe('upload store', () => {
const blob = new Blob(['crop'], { type: 'image/jpeg' })
const params = { natX: 0, natY: 0, natW: 200, natH: 200 }
store.setCrop(blob, params)
store.setCrop(blob, params, 'portrait')
// Pinia wraps refs in a reactive proxy, use toStrictEqual for value equality
expect(store.croppedBlob).toStrictEqual(blob)
expect(store.croppedUrl).toBe('blob:mock-url')
expect(store.cropParams).toEqual(params)
expect(store.cropOrientation).toBe('portrait')
})
it('addSticker appends to stickers', () => {
@@ -137,6 +138,7 @@ describe('upload store', () => {
expect(store.croppedBlob).toBeNull()
expect(store.croppedUrl).toBeNull()
expect(store.cropParams).toBeNull()
expect(store.cropOrientation).toBeNull()
expect(store.stickers).toHaveLength(0)
expect(store.contextDeviceId).toBeNull()
expect(store.selectedDeviceIds).toEqual([])
@@ -65,6 +65,7 @@ const makeImage = (overrides: Partial<Image> = {}): Image => ({
approvedDeviceIds: [],
cropParams: null,
stickerState: null,
cropOrientation: null,
...overrides,
})