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({