From c482c1e53efef96082194f1d0adf4e73087088cd Mon Sep 17 00:00:00 2001 From: Kairui Liu Date: Tue, 27 Oct 2026 23:07:45 +0800 Subject: [PATCH] fix: preserve stack primary asset selection (#31380) * fix: preserve stack primary asset selection * test: select stacked assets by id * chore: limit dart formatting changes * fix: address stack viewer review feedback * fix: align stack ordering with server * fix: keep stack primary asset first --------- Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> (cherry picked from commit 5a5d7be11084969c3c00a294ce6c0527042f88e7) --- e2e/src/ui/specs/asset-viewer/ocr.e2e-spec.ts | 2 +- .../ui/specs/asset-viewer/stack.e2e-spec.ts | 4 +-- mobile/lib/domain/services/asset.service.dart | 1 - .../repositories/remote_asset.repository.dart | 2 +- .../remote_asset_repository_test.dart | 31 +++++++++++++++++++ .../asset-viewer/AssetViewer.svelte | 7 ++--- .../DuplicatesCompareControl.svelte | 4 ++- 7 files changed, 41 insertions(+), 10 deletions(-) diff --git a/e2e/src/ui/specs/asset-viewer/ocr.e2e-spec.ts b/e2e/src/ui/specs/asset-viewer/ocr.e2e-spec.ts index 928ee9afe..057c0d925 100644 --- a/e2e/src/ui/specs/asset-viewer/ocr.e2e-spec.ts +++ b/e2e/src/ui/specs/asset-viewer/ocr.e2e-spec.ts @@ -100,7 +100,7 @@ test.describe('OCR with stacked assets', () => { const stackThumbnails = page.locator('#stack-slideshow [data-asset]'); await expect(stackThumbnails).toHaveCount(2); - await stackThumbnails.nth(1).click(); + await page.locator(`#stack-slideshow [data-asset="${secondAssetDto.id}"]`).click(); // refreshOcr() clears showOverlay when switching assets, so re-enable it await expect(ocrBoxes).toHaveCount(0); diff --git a/e2e/src/ui/specs/asset-viewer/stack.e2e-spec.ts b/e2e/src/ui/specs/asset-viewer/stack.e2e-spec.ts index 24c3ceb50..5afe47793 100644 --- a/e2e/src/ui/specs/asset-viewer/stack.e2e-spec.ts +++ b/e2e/src/ui/specs/asset-viewer/stack.e2e-spec.ts @@ -75,8 +75,8 @@ test.describe('asset-viewer stack', () => { await page.goto(`/photos/${fixture.primaryAsset.id}`); await ensureDetailPanelVisible(page); - const stackAssets = page.locator('#stack-slideshow [data-asset]'); - await stackAssets.nth(1).click(); + const secondAsset = page.locator(`#stack-slideshow [data-asset="${secondAssetDto.id}"]`); + await secondAsset.click(); const tags = page.getByTestId('detail-panel-tags').getByRole('link'); await expect(tags.first()).toHaveText('test/2'); diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index 58de42ba7..e5086efb5 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -60,7 +60,6 @@ class AssetService { } final stack = await _remoteRepository.getStackChildren(asset); - // Include the primary asset in the stack as the first item return [asset, ...stack]; } diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index 7b8650e9c..664ff0e6f 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -76,7 +76,7 @@ class RemoteAssetRepository extends DatabaseAccessor with $RemoteAssetRep final query = _db.remoteAssetEntity.select() ..where((row) => row.stackId.equals(stackId) & row.id.equals(asset.id).not()) - ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]); + ..orderBy([(row) => OrderingTerm.asc(row.createdAt)]); return query.map((row) => row.toDto()).get(); } diff --git a/mobile/test/medium/repositories/remote_asset_repository_test.dart b/mobile/test/medium/repositories/remote_asset_repository_test.dart index 13269857e..853605b67 100644 --- a/mobile/test/medium/repositories/remote_asset_repository_test.dart +++ b/mobile/test/medium/repositories/remote_asset_repository_test.dart @@ -18,6 +18,37 @@ void main() { await ctx.dispose(); }); + group('getStackChildren', () { + test('returns stack children from oldest to newest', () async { + final user = await ctx.newUser(); + const stackId = 'stack'; + final primary = await ctx.newRemoteAsset( + id: 'primary', + ownerId: user.id, + stackId: stackId, + createdAt: DateTime(2026, 1, 2), + ); + final beforePrimary = await ctx.newRemoteAsset( + id: 'before-primary', + ownerId: user.id, + stackId: stackId, + createdAt: DateTime(2026, 1, 1), + ); + final afterPrimary = await ctx.newRemoteAsset( + id: 'after-primary', + ownerId: user.id, + stackId: stackId, + createdAt: DateTime(2026, 1, 3), + ); + await ctx.newRemoteAsset(ownerId: user.id, stackId: 'other-stack'); + + final primaryAsset = await sut.get(primary.id); + final stack = await sut.getStackChildren(primaryAsset!); + + expect(stack.map((asset) => asset.id), [beforePrimary.id, afterPrimary.id]); + }); + }); + group('getByChecksum', () { late String userId; diff --git a/web/src/lib/components/asset-viewer/AssetViewer.svelte b/web/src/lib/components/asset-viewer/AssetViewer.svelte index 703007ac4..03b80c0b2 100644 --- a/web/src/lib/components/asset-viewer/AssetViewer.svelte +++ b/web/src/lib/components/asset-viewer/AssetViewer.svelte @@ -291,16 +291,15 @@ if (!stack || !withStacked || assetViewerManager.isShowEditor) { return; } - const assets = stack.assets; - const currentIndex = assets.findIndex(({ id }) => id === asset.id); + const currentIndex = stack.assets.findIndex(({ id }) => id === asset.id); if (currentIndex === -1) { return; } const nextIndex = direction === 'previous' ? currentIndex - 1 : currentIndex + 1; - if (nextIndex < 0 || nextIndex >= assets.length) { + if (nextIndex < 0 || nextIndex >= stack.assets.length) { return; } - cursor.current = assets[nextIndex]; + cursor.current = stack.assets[nextIndex]; }; /** diff --git a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/DuplicatesCompareControl.svelte b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/DuplicatesCompareControl.svelte index 2c2dee827..96fd166b6 100644 --- a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/DuplicatesCompareControl.svelte +++ b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/DuplicatesCompareControl.svelte @@ -94,7 +94,9 @@ }; const handleStack = () => { - onStack(assets); + const selectedAssets = assets.filter(({ id }) => selectedAssetIds.has(id)); + const unselectedAssets = assets.filter(({ id }) => !selectedAssetIds.has(id)); + onStack([...selectedAssets, ...unselectedAssets]); }; const assetCursor = $derived({