diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index a125d87d8d..4ec8a93b8d 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -165,15 +165,25 @@ class DriftTimelineRepository extends DriftDatabaseRepository { _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), useColumns: false, ), + leftOuterJoin( + _db.remoteAssetEntity, + _db.localAssetEntity.checksum + .equalsExp(_db.remoteAssetEntity.checksum), + useColumns: false, + ), ], ) + ..addColumns([_db.remoteAssetEntity.id]) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) ..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)]) ..limit(count, offset: offset); - return query - .map((row) => row.readTable(_db.localAssetEntity).toDto()) - .get(); + return query.map((row) { + final asset = row.readTable(_db.localAssetEntity).toDto(); + return asset.copyWith( + remoteId: row.read(_db.remoteAssetEntity.id), + ); + }).get(); } TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => ( diff --git a/mobile/lib/presentation/pages/local_timeline.page.dart b/mobile/lib/presentation/pages/local_timeline.page.dart index b4df0f64e2..fd4e44616b 100644 --- a/mobile/lib/presentation/pages/local_timeline.page.dart +++ b/mobile/lib/presentation/pages/local_timeline.page.dart @@ -30,6 +30,7 @@ class LocalTimelinePage extends StatelessWidget { child: Timeline( appBar: MesmerizingSliverAppBar(title: album.name), bottomSheet: const LocalAlbumBottomSheet(), + showStorageIndicator: true, ), ); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart index 2d22d063bd..2bf52bd094 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart @@ -72,7 +72,7 @@ class _SheetLocationDetailsState extends ConsumerState { // Guard no lat/lng if (!hasCoordinates || - (asset is LocalAsset && !(asset as LocalAsset).hasRemote)) { + (asset != null && asset is LocalAsset && asset!.hasRemote)) { return const SizedBox.shrink(); } diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index e79665baf7..970bb581cf 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -12,7 +12,13 @@ ImageProvider getFullImageProvider( // Create new provider and cache it final ImageProvider provider; if (_shouldUseLocalAsset(asset)) { - provider = LocalFullImageProvider(asset: asset as LocalAsset, size: size); + final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; + provider = LocalFullImageProvider( + id: id, + name: asset.name, + size: size, + type: asset.type, + ); } else { final String assetId; if (asset is LocalAsset && asset.hasRemote) { @@ -43,7 +49,13 @@ ImageProvider getThumbnailImageProvider({ } if (_shouldUseLocalAsset(asset!)) { - return LocalThumbProvider(asset: asset as LocalAsset, size: size); + final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; + return LocalThumbProvider( + id: id, + updatedAt: asset.updatedAt, + name: asset.name, + size: size, + ); } final String assetId; @@ -59,5 +71,5 @@ ImageProvider getThumbnailImageProvider({ } bool _shouldUseLocalAsset(BaseAsset asset) => - asset is LocalAsset && + asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage)); diff --git a/mobile/lib/presentation/widgets/images/local_image_provider.dart b/mobile/lib/presentation/widgets/images/local_image_provider.dart index f046fcad47..65311de48a 100644 --- a/mobile/lib/presentation/widgets/images/local_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/local_image_provider.dart @@ -21,11 +21,15 @@ class LocalThumbProvider extends ImageProvider { const AssetMediaRepository(); final CacheManager? cacheManager; - final LocalAsset asset; + final String id; + final DateTime updatedAt; + final String name; final Size size; const LocalThumbProvider({ - required this.asset, + required this.id, + required this.updatedAt, + required this.name, this.size = const Size.square(kTimelineFixedTileExtent), this.cacheManager, }); @@ -46,7 +50,10 @@ class LocalThumbProvider extends ImageProvider { scale: 1.0, informationCollector: () => [ DiagnosticsProperty('Image provider', this), - DiagnosticsProperty('Asset', key.asset), + DiagnosticsProperty('Id', key.id), + DiagnosticsProperty('Updated at', key.updatedAt), + DiagnosticsProperty('Name', key.name), + DiagnosticsProperty('Size', key.size), ], ); } @@ -57,7 +64,7 @@ class LocalThumbProvider extends ImageProvider { ImageDecoderCallback decode, ) async { final cacheKey = - '${key.asset.id}-${key.asset.updatedAt}-${key.size.width}x${key.size.height}'; + '${key.id}-${key.updatedAt}-${key.size.width}x${key.size.height}'; final fileFromCache = await cache.getFileFromCache(cacheKey); if (fileFromCache != null) { @@ -69,11 +76,11 @@ class LocalThumbProvider extends ImageProvider { } final thumbnailBytes = - await _assetMediaRepository.getThumbnail(key.asset.id, size: key.size); + await _assetMediaRepository.getThumbnail(key.id, size: key.size); if (thumbnailBytes == null) { PaintingBinding.instance.imageCache.evict(key); throw StateError( - "Loading thumb for local photo ${key.asset.name} failed", + "Loading thumb for local photo ${key.name} failed", ); } @@ -86,14 +93,13 @@ class LocalThumbProvider extends ImageProvider { bool operator ==(Object other) { if (identical(this, other)) return true; if (other is LocalThumbProvider) { - return asset.id == other.asset.id && - asset.updatedAt == other.asset.updatedAt; + return id == other.id && updatedAt == other.updatedAt; } return false; } @override - int get hashCode => asset.id.hashCode ^ asset.updatedAt.hashCode; + int get hashCode => id.hashCode ^ updatedAt.hashCode; } class LocalFullImageProvider extends ImageProvider { @@ -101,12 +107,16 @@ class LocalFullImageProvider extends ImageProvider { const AssetMediaRepository(); final StorageRepository _storageRepository = const StorageRepository(); - final LocalAsset asset; + final String id; + final String name; final Size size; + final AssetType type; const LocalFullImageProvider({ - required this.asset, + required this.id, + required this.name, required this.size, + required this.type, }); @override @@ -123,7 +133,7 @@ class LocalFullImageProvider extends ImageProvider { codec: _codec(key, decode), scale: 1.0, informationCollector: () sync* { - yield ErrorDescription(asset.name); + yield ErrorDescription(name); }, ); } @@ -134,24 +144,24 @@ class LocalFullImageProvider extends ImageProvider { ImageDecoderCallback decode, ) async* { try { - switch (key.asset.type) { + switch (key.type) { case AssetType.image: yield* _decodeProgressive(key, decode); break; case AssetType.video: final codec = await _getThumbnailCodec(key, decode); if (codec == null) { - throw StateError("Failed to load preview for ${key.asset.name}"); + throw StateError("Failed to load preview for ${key.name}"); } yield codec; break; case AssetType.other: case AssetType.audio: - throw StateError('Unsupported asset type ${key.asset.type}'); + throw StateError('Unsupported asset type ${key.type}'); } } catch (error, stack) { Logger('ImmichLocalImageProvider') - .severe('Error loading local image ${key.asset.name}', error, stack); + .severe('Error loading local image ${key.name}', error, stack); throw const ImageLoadingException( 'Could not load image from local storage', ); @@ -163,7 +173,7 @@ class LocalFullImageProvider extends ImageProvider { ImageDecoderCallback decode, ) async { final thumbBytes = - await _assetMediaRepository.getThumbnail(key.asset.id, size: key.size); + await _assetMediaRepository.getThumbnail(key.id, size: key.size); if (thumbBytes == null) { return null; } @@ -175,9 +185,9 @@ class LocalFullImageProvider extends ImageProvider { LocalFullImageProvider key, ImageDecoderCallback decode, ) async* { - final file = await _storageRepository.getFileForAsset(key.asset.id); + final file = await _storageRepository.getFileForAsset(key.id); if (file == null) { - throw StateError("Opening file for asset ${key.asset.name} failed"); + throw StateError("Opening file for asset ${key.name} failed"); } final fileSize = await file.length(); @@ -195,7 +205,7 @@ class LocalFullImageProvider extends ImageProvider { (key.size.height * progressiveMultiplier).clamp(256, 1024), ); final mediumThumb = - await _assetMediaRepository.getThumbnail(key.asset.id, size: size); + await _assetMediaRepository.getThumbnail(key.id, size: size); if (mediumThumb != null) { final mediumBuffer = await ImmutableBuffer.fromUint8List(mediumThumb); yield await decode(mediumBuffer); @@ -212,7 +222,7 @@ class LocalFullImageProvider extends ImageProvider { (key.size.height * progressiveMultiplier).clamp(512, 2048), ); final highThumb = - await _assetMediaRepository.getThumbnail(key.asset.id, size: size); + await _assetMediaRepository.getThumbnail(key.id, size: size); if (highThumb != null) { final highBuffer = await ImmutableBuffer.fromUint8List(highThumb); yield await decode(highBuffer); @@ -228,14 +238,15 @@ class LocalFullImageProvider extends ImageProvider { bool operator ==(Object other) { if (identical(this, other)) return true; if (other is LocalFullImageProvider) { - return asset.id == other.asset.id && - asset.updatedAt == other.asset.updatedAt && - size == other.size; + return id == other.id && + size == other.size && + type == other.type && + name == other.name; } return false; } @override int get hashCode => - asset.id.hashCode ^ asset.updatedAt.hashCode ^ size.hashCode; + id.hashCode ^ size.hashCode ^ type.hashCode ^ name.hashCode; }