fix: mobile storage status check (#19986)

* fix: _shouldUseLocalAsset check

* show storage indicators in local album view

* update local thumb provider to work with remote asset

* update checks

* do not show upload button when selection is only merged assets

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
shenlong 2025-07-17 22:43:21 +05:30 committed by GitHub
parent 03ff425664
commit 2046dcc5b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 66 additions and 32 deletions

View File

@ -165,15 +165,25 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
useColumns: false, useColumns: false,
), ),
leftOuterJoin(
_db.remoteAssetEntity,
_db.localAssetEntity.checksum
.equalsExp(_db.remoteAssetEntity.checksum),
useColumns: false,
),
], ],
) )
..addColumns([_db.remoteAssetEntity.id])
..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)]) ..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)])
..limit(count, offset: offset); ..limit(count, offset: offset);
return query return query.map((row) {
.map((row) => row.readTable(_db.localAssetEntity).toDto()) final asset = row.readTable(_db.localAssetEntity).toDto();
.get(); return asset.copyWith(
remoteId: row.read(_db.remoteAssetEntity.id),
);
}).get();
} }
TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => ( TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => (

View File

@ -30,6 +30,7 @@ class LocalTimelinePage extends StatelessWidget {
child: Timeline( child: Timeline(
appBar: MesmerizingSliverAppBar(title: album.name), appBar: MesmerizingSliverAppBar(title: album.name),
bottomSheet: const LocalAlbumBottomSheet(), bottomSheet: const LocalAlbumBottomSheet(),
showStorageIndicator: true,
), ),
); );
} }

View File

@ -72,7 +72,7 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
// Guard no lat/lng // Guard no lat/lng
if (!hasCoordinates || if (!hasCoordinates ||
(asset is LocalAsset && !(asset as LocalAsset).hasRemote)) { (asset != null && asset is LocalAsset && asset!.hasRemote)) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }

View File

@ -12,7 +12,13 @@ ImageProvider getFullImageProvider(
// Create new provider and cache it // Create new provider and cache it
final ImageProvider provider; final ImageProvider provider;
if (_shouldUseLocalAsset(asset)) { 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 { } else {
final String assetId; final String assetId;
if (asset is LocalAsset && asset.hasRemote) { if (asset is LocalAsset && asset.hasRemote) {
@ -43,7 +49,13 @@ ImageProvider getThumbnailImageProvider({
} }
if (_shouldUseLocalAsset(asset!)) { 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; final String assetId;
@ -59,5 +71,5 @@ ImageProvider getThumbnailImageProvider({
} }
bool _shouldUseLocalAsset(BaseAsset asset) => bool _shouldUseLocalAsset(BaseAsset asset) =>
asset is LocalAsset && asset.hasLocal &&
(!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage)); (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage));

View File

@ -21,11 +21,15 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
const AssetMediaRepository(); const AssetMediaRepository();
final CacheManager? cacheManager; final CacheManager? cacheManager;
final LocalAsset asset; final String id;
final DateTime updatedAt;
final String name;
final Size size; final Size size;
const LocalThumbProvider({ const LocalThumbProvider({
required this.asset, required this.id,
required this.updatedAt,
required this.name,
this.size = const Size.square(kTimelineFixedTileExtent), this.size = const Size.square(kTimelineFixedTileExtent),
this.cacheManager, this.cacheManager,
}); });
@ -46,7 +50,10 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
scale: 1.0, scale: 1.0,
informationCollector: () => <DiagnosticsNode>[ informationCollector: () => <DiagnosticsNode>[
DiagnosticsProperty<ImageProvider>('Image provider', this), DiagnosticsProperty<ImageProvider>('Image provider', this),
DiagnosticsProperty<LocalAsset>('Asset', key.asset), DiagnosticsProperty<String>('Id', key.id),
DiagnosticsProperty<DateTime>('Updated at', key.updatedAt),
DiagnosticsProperty<String>('Name', key.name),
DiagnosticsProperty<Size>('Size', key.size),
], ],
); );
} }
@ -57,7 +64,7 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
ImageDecoderCallback decode, ImageDecoderCallback decode,
) async { ) async {
final cacheKey = 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); final fileFromCache = await cache.getFileFromCache(cacheKey);
if (fileFromCache != null) { if (fileFromCache != null) {
@ -69,11 +76,11 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
} }
final thumbnailBytes = final thumbnailBytes =
await _assetMediaRepository.getThumbnail(key.asset.id, size: key.size); await _assetMediaRepository.getThumbnail(key.id, size: key.size);
if (thumbnailBytes == null) { if (thumbnailBytes == null) {
PaintingBinding.instance.imageCache.evict(key); PaintingBinding.instance.imageCache.evict(key);
throw StateError( 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<LocalThumbProvider> {
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
if (other is LocalThumbProvider) { if (other is LocalThumbProvider) {
return asset.id == other.asset.id && return id == other.id && updatedAt == other.updatedAt;
asset.updatedAt == other.asset.updatedAt;
} }
return false; return false;
} }
@override @override
int get hashCode => asset.id.hashCode ^ asset.updatedAt.hashCode; int get hashCode => id.hashCode ^ updatedAt.hashCode;
} }
class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> { class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
@ -101,12 +107,16 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
const AssetMediaRepository(); const AssetMediaRepository();
final StorageRepository _storageRepository = const StorageRepository(); final StorageRepository _storageRepository = const StorageRepository();
final LocalAsset asset; final String id;
final String name;
final Size size; final Size size;
final AssetType type;
const LocalFullImageProvider({ const LocalFullImageProvider({
required this.asset, required this.id,
required this.name,
required this.size, required this.size,
required this.type,
}); });
@override @override
@ -123,7 +133,7 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
codec: _codec(key, decode), codec: _codec(key, decode),
scale: 1.0, scale: 1.0,
informationCollector: () sync* { informationCollector: () sync* {
yield ErrorDescription(asset.name); yield ErrorDescription(name);
}, },
); );
} }
@ -134,24 +144,24 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
ImageDecoderCallback decode, ImageDecoderCallback decode,
) async* { ) async* {
try { try {
switch (key.asset.type) { switch (key.type) {
case AssetType.image: case AssetType.image:
yield* _decodeProgressive(key, decode); yield* _decodeProgressive(key, decode);
break; break;
case AssetType.video: case AssetType.video:
final codec = await _getThumbnailCodec(key, decode); final codec = await _getThumbnailCodec(key, decode);
if (codec == null) { if (codec == null) {
throw StateError("Failed to load preview for ${key.asset.name}"); throw StateError("Failed to load preview for ${key.name}");
} }
yield codec; yield codec;
break; break;
case AssetType.other: case AssetType.other:
case AssetType.audio: case AssetType.audio:
throw StateError('Unsupported asset type ${key.asset.type}'); throw StateError('Unsupported asset type ${key.type}');
} }
} catch (error, stack) { } catch (error, stack) {
Logger('ImmichLocalImageProvider') Logger('ImmichLocalImageProvider')
.severe('Error loading local image ${key.asset.name}', error, stack); .severe('Error loading local image ${key.name}', error, stack);
throw const ImageLoadingException( throw const ImageLoadingException(
'Could not load image from local storage', 'Could not load image from local storage',
); );
@ -163,7 +173,7 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
ImageDecoderCallback decode, ImageDecoderCallback decode,
) async { ) async {
final thumbBytes = final thumbBytes =
await _assetMediaRepository.getThumbnail(key.asset.id, size: key.size); await _assetMediaRepository.getThumbnail(key.id, size: key.size);
if (thumbBytes == null) { if (thumbBytes == null) {
return null; return null;
} }
@ -175,9 +185,9 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
LocalFullImageProvider key, LocalFullImageProvider key,
ImageDecoderCallback decode, ImageDecoderCallback decode,
) async* { ) async* {
final file = await _storageRepository.getFileForAsset(key.asset.id); final file = await _storageRepository.getFileForAsset(key.id);
if (file == null) { 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(); final fileSize = await file.length();
@ -195,7 +205,7 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
(key.size.height * progressiveMultiplier).clamp(256, 1024), (key.size.height * progressiveMultiplier).clamp(256, 1024),
); );
final mediumThumb = final mediumThumb =
await _assetMediaRepository.getThumbnail(key.asset.id, size: size); await _assetMediaRepository.getThumbnail(key.id, size: size);
if (mediumThumb != null) { if (mediumThumb != null) {
final mediumBuffer = await ImmutableBuffer.fromUint8List(mediumThumb); final mediumBuffer = await ImmutableBuffer.fromUint8List(mediumThumb);
yield await decode(mediumBuffer); yield await decode(mediumBuffer);
@ -212,7 +222,7 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
(key.size.height * progressiveMultiplier).clamp(512, 2048), (key.size.height * progressiveMultiplier).clamp(512, 2048),
); );
final highThumb = final highThumb =
await _assetMediaRepository.getThumbnail(key.asset.id, size: size); await _assetMediaRepository.getThumbnail(key.id, size: size);
if (highThumb != null) { if (highThumb != null) {
final highBuffer = await ImmutableBuffer.fromUint8List(highThumb); final highBuffer = await ImmutableBuffer.fromUint8List(highThumb);
yield await decode(highBuffer); yield await decode(highBuffer);
@ -228,14 +238,15 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
if (other is LocalFullImageProvider) { if (other is LocalFullImageProvider) {
return asset.id == other.asset.id && return id == other.id &&
asset.updatedAt == other.asset.updatedAt && size == other.size &&
size == other.size; type == other.type &&
name == other.name;
} }
return false; return false;
} }
@override @override
int get hashCode => int get hashCode =>
asset.id.hashCode ^ asset.updatedAt.hashCode ^ size.hashCode; id.hashCode ^ size.hashCode ^ type.hashCode ^ name.hashCode;
} }