diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index a9ac5b338..6081988b7 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -180,4 +180,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d -COCOAPODS: 1.12.1 +COCOAPODS: 1.11.3 diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index 2af7679a9..c556adbec 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -218,18 +218,19 @@ class GalleryViewerPage extends HookConsumerWidget { scrollDirection: Axis.horizontal, itemCount: stackElements.length, padding: const EdgeInsets.only( - left: 10, - right: 10, + left: 5, + right: 5, bottom: 30, ), itemBuilder: (context, index) { final assetId = stackElements.elementAt(index).remoteId; return Padding( - padding: const EdgeInsets.only(right: 10), + padding: const EdgeInsets.only(right: 5), child: GestureDetector( onTap: () => stackIndex.value = index, child: Container( - width: 40, + width: 60, + height: 60, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6), @@ -391,7 +392,7 @@ class GalleryViewerPage extends HookConsumerWidget { Visibility( visible: stack.isNotEmpty, child: SizedBox( - height: 40, + height: 80, child: buildStackedChildren(), ), ), diff --git a/server/e2e/api/specs/asset.e2e-spec.ts b/server/e2e/api/specs/asset.e2e-spec.ts index 748418718..3f49fb407 100644 --- a/server/e2e/api/specs/asset.e2e-spec.ts +++ b/server/e2e/api/specs/asset.e2e-spec.ts @@ -14,6 +14,7 @@ import { AssetEntity, AssetStackEntity, AssetType, SharedLinkType } from '@app/i import { AssetRepository } from '@app/infra/repositories'; import { INestApplication } from '@nestjs/common'; import { errorStub, userDto, uuidStub } from '@test/fixtures'; +import { assetApi } from 'e2e/client/asset-api'; import { randomBytes } from 'node:crypto'; import request from 'supertest'; import { api } from '../../client'; @@ -532,6 +533,23 @@ describe(`${AssetController.name} (e2e)`, () => { } }); } + + it('should return stack data', async () => { + const parentId = asset1.id; + const childIds = [asset2.id, asset3.id]; + await request(server) + .put('/asset') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ stackParentId: parentId, ids: childIds }); + + const body = await assetApi.getAllAssets(server, user1.accessToken); + // Response includes parent with stack children count + const parentDto = body.find((a) => a.id == parentId); + expect(parentDto?.stackCount).toEqual(3); + + // Response includes children at the root level + expect.arrayContaining([expect.objectContaining({ id: asset1.id }), expect.objectContaining({ id: asset2.id })]); + }); }); describe('POST /asset/upload', () => { diff --git a/server/src/domain/repositories/search.repository.ts b/server/src/domain/repositories/search.repository.ts index c9fec3cf7..10182a44e 100644 --- a/server/src/domain/repositories/search.repository.ts +++ b/server/src/domain/repositories/search.repository.ts @@ -92,12 +92,12 @@ export interface SearchStatusOptions { export interface SearchOneToOneRelationOptions { withExif?: boolean; withSmartInfo?: boolean; + withStacked?: boolean; } export interface SearchRelationOptions extends SearchOneToOneRelationOptions { withFaces?: boolean; withPeople?: boolean; - withStacked?: boolean; } export interface SearchDateOptions { diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts index 5dcc487be..b8c3222ee 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/immich/api-v1/asset/asset.service.ts @@ -116,9 +116,17 @@ export class AssetService { await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); const assets = await this.assetRepository.getAllByFileCreationDate( { take: dto.take ?? 1000, skip: dto.skip }, - { ...dto, userIds: [userId], withDeleted: true, orderDirection: 'DESC', withExif: true, isVisible: true }, + { + ...dto, + userIds: [userId], + withDeleted: true, + orderDirection: 'DESC', + withExif: true, + isVisible: true, + withStacked: true, + }, ); - return assets.items.map((asset) => mapAsset(asset)); + return assets.items.map((asset) => mapAsset(asset, { withStack: true })); } async serveThumbnail(auth: AuthDto, assetId: string, dto: GetAssetThumbnailDto): Promise { diff --git a/server/src/infra/infra.utils.ts b/server/src/infra/infra.utils.ts index 2c6e4b747..652bced6b 100644 --- a/server/src/infra/infra.utils.ts +++ b/server/src/infra/infra.utils.ts @@ -2,7 +2,6 @@ import { AssetSearchBuilderOptions, Paginated, PaginationOptions } from '@app/do import _ from 'lodash'; import { Between, - Brackets, FindManyOptions, IsNull, LessThanOrEqual, @@ -229,12 +228,7 @@ export function searchAssetBuilder( } if (withStacked) { - builder - .leftJoinAndSelect(`${builder.alias}.stack`, 'stack') - .leftJoinAndSelect('stack.assets', 'stackedAssets') - .andWhere( - new Brackets((qb) => qb.where(`stack.primaryAssetId = ${builder.alias}.id`).orWhere('asset.stackId IS NULL')), - ); + builder.leftJoinAndSelect(`${builder.alias}.stack`, 'stack').leftJoinAndSelect('stack.assets', 'stackedAssets'); } const withDeleted = options.withDeleted ?? (trashedAfter !== undefined || trashedBefore !== undefined); diff --git a/server/src/infra/sql/search.repository.sql b/server/src/infra/sql/search.repository.sql index c45d90a7a..48a7fc8e5 100644 --- a/server/src/infra/sql/search.repository.sql +++ b/server/src/infra/sql/search.repository.sql @@ -83,10 +83,6 @@ FROM "asset"."isFavorite" = $3 AND "asset"."isArchived" = $4 ) - AND ( - "stack"."primaryAssetId" = "asset"."id" - OR "asset"."stackId" IS NULL - ) ) AND ("asset"."deletedAt" IS NULL) ) "distinctAlias" @@ -184,10 +180,6 @@ WHERE "asset"."isFavorite" = $3 AND "asset"."isArchived" = $4 ) - AND ( - "stack"."primaryAssetId" = "asset"."id" - OR "asset"."stackId" IS NULL - ) AND "asset"."ownerId" IN ($5) ) AND ("asset"."deletedAt" IS NULL)