From 9c9feddf7da460454f5a9afdf339dafe8b2b01ae Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:49:44 +0530 Subject: [PATCH] refactor: folder page to use new models (#27657) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../domain/models/asset/base_asset.model.dart | 2 + .../models/asset/remote_asset.model.dart | 78 +++++++++++++++++++ .../lib/domain/services/search.service.dart | 43 +--------- .../lib/domain/services/timeline.service.dart | 1 + mobile/lib/extensions/asset_extensions.dart | 76 +++++++++++++++++- .../lib/pages/library/folder/folder.page.dart | 36 +++++---- mobile/lib/providers/folder.provider.dart | 12 +-- .../repositories/folder_api.repository.dart | 7 +- mobile/lib/services/folder.service.dart | 8 +- 9 files changed, 191 insertions(+), 72 deletions(-) diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart index cb40c8f76a..9ba8cd06f8 100644 --- a/mobile/lib/domain/models/asset/base_asset.model.dart +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -1,3 +1,5 @@ +import 'package:immich_mobile/domain/models/exif.model.dart'; + part 'local_asset.model.dart'; part 'remote_asset.model.dart'; diff --git a/mobile/lib/domain/models/asset/remote_asset.model.dart b/mobile/lib/domain/models/asset/remote_asset.model.dart index 43d49506e3..b9a0e64d6a 100644 --- a/mobile/lib/domain/models/asset/remote_asset.model.dart +++ b/mobile/lib/domain/models/asset/remote_asset.model.dart @@ -128,3 +128,81 @@ class RemoteAsset extends BaseAsset { ); } } + +class RemoteAssetExif extends RemoteAsset { + final ExifInfo exifInfo; + + const RemoteAssetExif({ + required super.id, + super.localId, + required super.name, + required super.ownerId, + required super.checksum, + required super.type, + required super.createdAt, + required super.updatedAt, + super.width, + super.height, + super.durationInSeconds, + super.isFavorite = false, + super.thumbHash, + super.visibility = AssetVisibility.timeline, + super.livePhotoVideoId, + super.stackId, + super.isEdited = false, + this.exifInfo = const ExifInfo(), + }); + + @override + bool operator ==(Object other) { + if (other is! RemoteAssetExif) return false; + if (identical(this, other)) return true; + return super == other && exifInfo == other.exifInfo; + } + + @override + int get hashCode => super.hashCode ^ exifInfo.hashCode; + + @override + RemoteAssetExif copyWith({ + String? id, + String? localId, + String? name, + String? ownerId, + String? checksum, + AssetType? type, + DateTime? createdAt, + DateTime? updatedAt, + int? width, + int? height, + int? durationInSeconds, + bool? isFavorite, + String? thumbHash, + AssetVisibility? visibility, + String? livePhotoVideoId, + String? stackId, + bool? isEdited, + ExifInfo? exifInfo, + }) { + return RemoteAssetExif( + id: id ?? this.id, + localId: localId ?? this.localId, + name: name ?? this.name, + ownerId: ownerId ?? this.ownerId, + checksum: checksum ?? this.checksum, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + isFavorite: isFavorite ?? this.isFavorite, + thumbHash: thumbHash ?? this.thumbHash, + visibility: visibility ?? this.visibility, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + stackId: stackId ?? this.stackId, + isEdited: isEdited ?? this.isEdited, + exifInfo: exifInfo ?? this.exifInfo, // Use the new parameter + ); + } +} diff --git a/mobile/lib/domain/services/search.service.dart b/mobile/lib/domain/services/search.service.dart index 004ad06b1b..8b93e9c8cc 100644 --- a/mobile/lib/domain/services/search.service.dart +++ b/mobile/lib/domain/services/search.service.dart @@ -1,10 +1,9 @@ -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/search_result.model.dart'; +import 'package:immich_mobile/extensions/asset_extensions.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart' as api show AssetVisibility; import 'package:openapi/api.dart' hide AssetVisibility; class SearchService { @@ -52,43 +51,3 @@ class SearchService { return null; } } - -extension on AssetResponseDto { - RemoteAsset toDto() { - return RemoteAsset( - id: id, - name: originalFileName, - checksum: checksum, - createdAt: fileCreatedAt, - updatedAt: updatedAt, - ownerId: ownerId, - visibility: switch (visibility) { - api.AssetVisibility.timeline => AssetVisibility.timeline, - api.AssetVisibility.hidden => AssetVisibility.hidden, - api.AssetVisibility.archive => AssetVisibility.archive, - api.AssetVisibility.locked => AssetVisibility.locked, - _ => AssetVisibility.timeline, - }, - durationInSeconds: duration.toDuration()?.inSeconds ?? 0, - height: height?.toInt(), - width: width?.toInt(), - isFavorite: isFavorite, - livePhotoVideoId: livePhotoVideoId, - thumbHash: thumbhash, - localId: null, - type: type.toAssetType(), - stackId: stack?.id, - isEdited: isEdited, - ); - } -} - -extension on AssetTypeEnum { - AssetType toAssetType() => switch (this) { - AssetTypeEnum.IMAGE => AssetType.image, - AssetTypeEnum.VIDEO => AssetType.video, - AssetTypeEnum.AUDIO => AssetType.audio, - AssetTypeEnum.OTHER => AssetType.other, - _ => throw Exception('Unknown AssetType value: $this'), - }; -} diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index b33940eacd..a055f8bcae 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -34,6 +34,7 @@ enum TimelineOrigin { search, deepLink, albumActivities, + folder, } class TimelineFactory { diff --git a/mobile/lib/extensions/asset_extensions.dart b/mobile/lib/extensions/asset_extensions.dart index a8ca7ef2aa..f7f98b3da7 100644 --- a/mobile/lib/extensions/asset_extensions.dart +++ b/mobile/lib/extensions/asset_extensions.dart @@ -1,7 +1,12 @@ -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/exif.model.dart'; +import 'package:immich_mobile/entities/asset.entity.dart' as isar hide AssetTypeEnumHelper; +import 'package:immich_mobile/extensions/string_extensions.dart'; +import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; import 'package:immich_mobile/utils/timezone.dart'; +import 'package:openapi/api.dart' as api; -extension TZExtension on Asset { +extension TZExtension on isar.Asset { /// Returns the created time of the asset from the exif info (if available) or from /// the fileCreatedAt field, adjusted to the timezone value from the exif info along with /// the timezone offset in [Duration] @@ -15,3 +20,70 @@ extension TZExtension on Asset { return (dt, dt.timeZoneOffset); } } + +extension DTOToAsset on api.AssetResponseDto { + RemoteAsset toDto() { + return RemoteAsset( + id: id, + name: originalFileName, + checksum: checksum, + createdAt: fileCreatedAt, + updatedAt: updatedAt, + ownerId: ownerId, + visibility: visibility.toAssetVisibility(), + durationInSeconds: duration.toDuration()?.inSeconds ?? 0, + height: height?.toInt(), + width: width?.toInt(), + isFavorite: isFavorite, + livePhotoVideoId: livePhotoVideoId, + thumbHash: thumbhash, + localId: null, + type: type.toAssetType(), + stackId: stack?.id, + isEdited: isEdited, + ); + } + + RemoteAssetExif toDtoWithExif() { + return RemoteAssetExif( + id: id, + name: originalFileName, + checksum: checksum, + createdAt: fileCreatedAt, + updatedAt: updatedAt, + ownerId: ownerId, + visibility: visibility.toAssetVisibility(), + durationInSeconds: duration.toDuration()?.inSeconds ?? 0, + height: height?.toInt(), + width: width?.toInt(), + isFavorite: isFavorite, + livePhotoVideoId: livePhotoVideoId, + thumbHash: thumbhash, + localId: null, + type: type.toAssetType(), + stackId: stack?.id, + isEdited: isEdited, + exifInfo: exifInfo != null ? ExifDtoConverter.fromDto(exifInfo!) : const ExifInfo(), + ); + } +} + +extension on api.AssetVisibility { + AssetVisibility toAssetVisibility() => switch (this) { + api.AssetVisibility.timeline => AssetVisibility.timeline, + api.AssetVisibility.hidden => AssetVisibility.hidden, + api.AssetVisibility.archive => AssetVisibility.archive, + api.AssetVisibility.locked => AssetVisibility.locked, + _ => AssetVisibility.timeline, + }; +} + +extension on api.AssetTypeEnum { + AssetType toAssetType() => switch (this) { + api.AssetTypeEnum.IMAGE => AssetType.image, + api.AssetTypeEnum.VIDEO => AssetType.video, + api.AssetTypeEnum.AUDIO => AssetType.audio, + api.AssetTypeEnum.OTHER => AssetType.other, + _ => throw Exception('Unknown AssetType value: $this'), + }; +} diff --git a/mobile/lib/pages/library/folder/folder.page.dart b/mobile/lib/pages/library/folder/folder.page.dart index 497d3e5151..9de230d550 100644 --- a/mobile/lib/pages/library/folder/folder.page.dart +++ b/mobile/lib/pages/library/folder/folder.page.dart @@ -1,19 +1,22 @@ import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; import 'package:immich_mobile/models/folder/root_folder.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail_tile.widget.dart'; import 'package:immich_mobile/providers/folder.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; -import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; RecursiveFolder? _findFolderInStructure(RootFolder rootFolder, RecursiveFolder targetFolder) { @@ -136,8 +139,8 @@ class FolderContent extends HookConsumerWidget { FolderPath(currentFolder: folder!, root: root), Expanded( child: folderRenderlist.when( - data: (list) { - if (folder!.subfolders.isEmpty && list.isEmpty) { + data: (folderAssets) { + if (folder!.subfolders.isEmpty && folderAssets.isEmpty) { return Center(child: const Text("empty_folder").tr()); } @@ -164,32 +167,33 @@ class FolderContent extends HookConsumerWidget { onTap: () => context.pushRoute(FolderRoute(folder: subfolder)), ), ), - if (!list.isEmpty && list.allAssets != null && list.allAssets!.isNotEmpty) - ...list.allAssets!.map( - (asset) => LargeLeadingTile( + if (folderAssets.isNotEmpty) + ...folderAssets.mapIndexed( + (index, asset) => LargeLeadingTile( onTap: () { - ref.read(currentAssetProvider.notifier).set(asset); + AssetViewer.setAsset(ref, asset); context.pushRoute( - GalleryViewerRoute(renderList: list, initialIndex: list.allAssets!.indexOf(asset)), + AssetViewerRoute( + initialIndex: index, + timelineService: ref + .read(timelineFactoryProvider) + .fromAssets(folderAssets, TimelineOrigin.folder), + ), ); }, leading: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(15)), - child: SizedBox( - width: 80, - height: 80, - child: ThumbnailImage(asset: asset, showStorageIndicator: false), - ), + child: SizedBox(width: 80, height: 80, child: ThumbnailTile(asset)), ), title: Text( - asset.fileName, + asset.name, maxLines: 2, softWrap: false, overflow: TextOverflow.ellipsis, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), ), subtitle: Text( - "${asset.exifInfo?.fileSize != null ? formatBytes(asset.exifInfo?.fileSize ?? 0) : ""} • ${DateFormat.yMMMd().format(asset.fileCreatedAt)}", + "${asset.exifInfo.fileSize != null ? formatBytes(asset.exifInfo.fileSize ?? 0) : ""} • ${DateFormat.yMMMd().format(asset.createdAt)}", style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ), diff --git a/mobile/lib/providers/folder.provider.dart b/mobile/lib/providers/folder.provider.dart index 696d7e19fd..816a88996e 100644 --- a/mobile/lib/providers/folder.provider.dart +++ b/mobile/lib/providers/folder.provider.dart @@ -1,8 +1,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/models/folder/root_folder.model.dart'; import 'package:immich_mobile/services/folder.service.dart'; -import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:logging/logging.dart'; class FolderStructureNotifier extends StateNotifier> { @@ -26,7 +26,7 @@ final folderStructureProvider = StateNotifierProvider> { +class FolderRenderListNotifier extends StateNotifier>> { final FolderService _folderService; final RootFolder _folder; final Logger _log = Logger("FolderAssetsNotifier"); @@ -36,8 +36,7 @@ class FolderRenderListNotifier extends StateNotifier> { Future fetchAssets(SortOrder order) async { try { final assets = await _folderService.getFolderAssets(_folder, order); - final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.none); - state = AsyncData(renderList); + state = AsyncData(assets); } catch (e, stack) { _log.severe("Failed to fetch folder assets", e, stack); state = AsyncError(e, stack); @@ -46,6 +45,9 @@ class FolderRenderListNotifier extends StateNotifier> { } final folderRenderListProvider = - StateNotifierProvider.family, RootFolder>((ref, folder) { + StateNotifierProvider.family>, RootFolder>(( + ref, + folder, + ) { return FolderRenderListNotifier(ref.watch(folderServiceProvider), folder); }); diff --git a/mobile/lib/repositories/folder_api.repository.dart b/mobile/lib/repositories/folder_api.repository.dart index d20ca8e0a9..8c9959389c 100644 --- a/mobile/lib/repositories/folder_api.repository.dart +++ b/mobile/lib/repositories/folder_api.repository.dart @@ -1,5 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/asset_extensions.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/repositories/api.repository.dart'; import 'package:logging/logging.dart'; @@ -23,10 +24,10 @@ class FolderApiRepository extends ApiRepository { } } - Future> getAssetsForPath(String? path) async { + Future> getAssetsForPath(String? path) async { try { final list = await _api.getAssetsByOriginalPath(path ?? '/'); - return list != null ? list.map(Asset.remote).toList() : []; + return list != null ? list.map((e) => e.toDtoWithExif()).toList() : []; } catch (e, stack) { _log.severe("Failed to fetch Assets by original path", e, stack); return []; diff --git a/mobile/lib/services/folder.service.dart b/mobile/lib/services/folder.service.dart index 91fb455110..bf7590ce54 100644 --- a/mobile/lib/services/folder.service.dart +++ b/mobile/lib/services/folder.service.dart @@ -1,6 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; import 'package:immich_mobile/models/folder/root_folder.model.dart'; import 'package:immich_mobile/repositories/folder_api.repository.dart'; @@ -76,7 +76,7 @@ class FolderService { return RootFolder(subfolders: rootSubfolders, path: '/'); } - Future> getFolderAssets(RootFolder folder, SortOrder order) async { + Future> getFolderAssets(RootFolder folder, SortOrder order) async { try { if (folder is RecursiveFolder) { String fullPath = folder.path.isEmpty ? folder.name : '${folder.path}/${folder.name}'; @@ -84,9 +84,9 @@ class FolderService { var result = await _folderApiRepository.getAssetsForPath(fullPath); if (order == SortOrder.desc) { - result.sort((a, b) => b.fileCreatedAt.compareTo(a.fileCreatedAt)); + result.sort((a, b) => b.createdAt.compareTo(a.createdAt)); } else { - result.sort((a, b) => a.fileCreatedAt.compareTo(b.fileCreatedAt)); + result.sort((a, b) => a.createdAt.compareTo(b.createdAt)); } return result;