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),
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) => (

View File

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

View File

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

View File

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

View File

@ -21,11 +21,15 @@ class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
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<LocalThumbProvider> {
scale: 1.0,
informationCollector: () => <DiagnosticsNode>[
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,
) 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<LocalThumbProvider> {
}
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<LocalThumbProvider> {
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<LocalFullImageProvider> {
@ -101,12 +107,16 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
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<LocalFullImageProvider> {
codec: _codec(key, decode),
scale: 1.0,
informationCollector: () sync* {
yield ErrorDescription(asset.name);
yield ErrorDescription(name);
},
);
}
@ -134,24 +144,24 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
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<LocalFullImageProvider> {
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> {
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<LocalFullImageProvider> {
(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<LocalFullImageProvider> {
(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<LocalFullImageProvider> {
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;
}