refactor: folder page to use new models (#27657)

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong 2026-04-13 22:49:44 +05:30 committed by GitHub
parent bfcf34d8b5
commit 9c9feddf7d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 191 additions and 72 deletions

View File

@ -1,3 +1,5 @@
import 'package:immich_mobile/domain/models/exif.model.dart';
part 'local_asset.model.dart';
part 'remote_asset.model.dart';

View File

@ -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
);
}
}

View File

@ -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'),
};
}

View File

@ -34,6 +34,7 @@ enum TimelineOrigin {
search,
deepLink,
albumActivities,
folder,
}
class TimelineFactory {

View File

@ -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'),
};
}

View File

@ -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),
),
),

View File

@ -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<AsyncValue<RootFolder>> {
@ -26,7 +26,7 @@ final folderStructureProvider = StateNotifierProvider<FolderStructureNotifier, A
return FolderStructureNotifier(ref.watch(folderServiceProvider));
});
class FolderRenderListNotifier extends StateNotifier<AsyncValue<RenderList>> {
class FolderRenderListNotifier extends StateNotifier<AsyncValue<List<RemoteAssetExif>>> {
final FolderService _folderService;
final RootFolder _folder;
final Logger _log = Logger("FolderAssetsNotifier");
@ -36,8 +36,7 @@ class FolderRenderListNotifier extends StateNotifier<AsyncValue<RenderList>> {
Future<void> 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<AsyncValue<RenderList>> {
}
final folderRenderListProvider =
StateNotifierProvider.family<FolderRenderListNotifier, AsyncValue<RenderList>, RootFolder>((ref, folder) {
StateNotifierProvider.family<FolderRenderListNotifier, AsyncValue<List<RemoteAssetExif>>, RootFolder>((
ref,
folder,
) {
return FolderRenderListNotifier(ref.watch(folderServiceProvider), folder);
});

View File

@ -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<List<Asset>> getAssetsForPath(String? path) async {
Future<List<RemoteAssetExif>> 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 [];

View File

@ -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<List<Asset>> getFolderAssets(RootFolder folder, SortOrder order) async {
Future<List<RemoteAssetExif>> 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;