mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
feat(mobile): sqlite asset viewer (#19552)
* add full image provider and refactor thumb providers * photo_view updates * wip: asset-viewer * fix controller dispose on page change * wip: bottom sheet * fix interactions * more bottomsheet changes * generate schema * PR feedback * refactor asset viewer * never rotate and fix background on page change * use photoview as the loading builder * precache after delay * claude: optimizing rebuild of image provider * claude: optimizing image decoding and caching * use proper cache for new full size image providers * chore: load local HEIC fullsize for iOS * make controller callbacks nullable * remove imageprovider cache * do not handle drag gestures when zoomed * use loadOriginal setting for HEIC / larger images * preload assets outside timer * never use same controllers in photo-view gallery * fix: cannot scroll down once swipe with bottom sheet --------- 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:
parent
ec603a008c
commit
7855974a29
47 changed files with 1867 additions and 490 deletions
|
|
@ -0,0 +1,38 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
|
||||
import 'package:octo_image/octo_image.dart';
|
||||
|
||||
class FullImage extends StatelessWidget {
|
||||
const FullImage(
|
||||
this.asset, {
|
||||
required this.size,
|
||||
this.fit = BoxFit.cover,
|
||||
this.placeholder = const ThumbnailPlaceholder(),
|
||||
super.key,
|
||||
});
|
||||
|
||||
final BaseAsset asset;
|
||||
final Size size;
|
||||
final Widget? placeholder;
|
||||
final BoxFit fit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = getFullImageProvider(asset, size: size);
|
||||
return OctoImage(
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
fadeOutDuration: const Duration(milliseconds: 100),
|
||||
placeholderBuilder: placeholder != null ? (_) => placeholder! : null,
|
||||
image: provider,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
fit: fit,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
provider.evict();
|
||||
return const Icon(Icons.image_not_supported_outlined, size: 32);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
63
mobile/lib/presentation/widgets/images/image_provider.dart
Normal file
63
mobile/lib/presentation/widgets/images/image_provider.dart
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
||||
|
||||
ImageProvider getFullImageProvider(
|
||||
BaseAsset asset, {
|
||||
Size size = const Size(1080, 1920),
|
||||
}) {
|
||||
// Create new provider and cache it
|
||||
final ImageProvider provider;
|
||||
if (_shouldUseLocalAsset(asset)) {
|
||||
provider = LocalFullImageProvider(asset: asset as LocalAsset, size: size);
|
||||
} else {
|
||||
final String assetId;
|
||||
if (asset is LocalAsset && asset.hasRemote) {
|
||||
assetId = asset.remoteId!;
|
||||
} else if (asset is RemoteAsset) {
|
||||
assetId = asset.id;
|
||||
} else {
|
||||
throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
|
||||
}
|
||||
provider = RemoteFullImageProvider(assetId: assetId);
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
ImageProvider getThumbnailImageProvider({
|
||||
BaseAsset? asset,
|
||||
String? remoteId,
|
||||
Size size = const Size.square(256),
|
||||
}) {
|
||||
assert(
|
||||
asset != null || remoteId != null,
|
||||
'Either asset or remoteId must be provided',
|
||||
);
|
||||
|
||||
if (remoteId != null) {
|
||||
return RemoteThumbProvider(assetId: remoteId);
|
||||
}
|
||||
|
||||
if (_shouldUseLocalAsset(asset!)) {
|
||||
return LocalThumbProvider(asset: asset as LocalAsset, size: size);
|
||||
}
|
||||
|
||||
final String assetId;
|
||||
if (asset is LocalAsset && asset.hasRemote) {
|
||||
assetId = asset.remoteId!;
|
||||
} else if (asset is RemoteAsset) {
|
||||
assetId = asset.id;
|
||||
} else {
|
||||
throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
|
||||
}
|
||||
|
||||
return RemoteThumbProvider(assetId: assetId);
|
||||
}
|
||||
|
||||
bool _shouldUseLocalAsset(BaseAsset asset) =>
|
||||
asset is LocalAsset &&
|
||||
(!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage));
|
||||
241
mobile/lib/presentation/widgets/images/local_image_provider.dart
Normal file
241
mobile/lib/presentation/widgets/images/local_image_provider.dart
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/asset_media.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
|
||||
import 'package:immich_mobile/providers/image/exceptions/image_loading_exception.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
||||
final AssetMediaRepository _assetMediaRepository =
|
||||
const AssetMediaRepository();
|
||||
final CacheManager? cacheManager;
|
||||
|
||||
final LocalAsset asset;
|
||||
final Size size;
|
||||
|
||||
const LocalThumbProvider({
|
||||
required this.asset,
|
||||
this.size = const Size.square(kTimelineFixedTileExtent),
|
||||
this.cacheManager,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<LocalThumbProvider> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
LocalThumbProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) {
|
||||
final cache = cacheManager ?? ThumbnailImageCacheManager();
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _codec(key, cache, decode),
|
||||
scale: 1.0,
|
||||
informationCollector: () => <DiagnosticsNode>[
|
||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||
DiagnosticsProperty<LocalAsset>('Asset', key.asset),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<Codec> _codec(
|
||||
LocalThumbProvider key,
|
||||
CacheManager cache,
|
||||
ImageDecoderCallback decode,
|
||||
) async {
|
||||
final cacheKey =
|
||||
'${key.asset.id}-${key.asset.updatedAt}-${key.size.width}x${key.size.height}';
|
||||
|
||||
final fileFromCache = await cache.getFileFromCache(cacheKey);
|
||||
if (fileFromCache != null) {
|
||||
try {
|
||||
final buffer =
|
||||
await ImmutableBuffer.fromFilePath(fileFromCache.file.path);
|
||||
return decode(buffer);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
final thumbnailBytes =
|
||||
await _assetMediaRepository.getThumbnail(key.asset.id, size: key.size);
|
||||
if (thumbnailBytes == null) {
|
||||
PaintingBinding.instance.imageCache.evict(key);
|
||||
throw StateError(
|
||||
"Loading thumb for local photo ${key.asset.name} failed",
|
||||
);
|
||||
}
|
||||
|
||||
final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes);
|
||||
unawaited(cache.putFile(cacheKey, thumbnailBytes));
|
||||
return decode(buffer);
|
||||
}
|
||||
|
||||
@override
|
||||
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 false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => asset.id.hashCode ^ asset.updatedAt.hashCode;
|
||||
}
|
||||
|
||||
class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
||||
final AssetMediaRepository _assetMediaRepository =
|
||||
const AssetMediaRepository();
|
||||
final StorageRepository _storageRepository = const StorageRepository();
|
||||
|
||||
final LocalAsset asset;
|
||||
final Size size;
|
||||
|
||||
const LocalFullImageProvider({
|
||||
required this.asset,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
LocalFullImageProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) {
|
||||
return MultiImageStreamCompleter(
|
||||
codec: _codec(key, decode),
|
||||
scale: 1.0,
|
||||
informationCollector: () sync* {
|
||||
yield ErrorDescription(asset.name);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Streams in each stage of the image as we ask for it
|
||||
Stream<Codec> _codec(
|
||||
LocalFullImageProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) async* {
|
||||
try {
|
||||
switch (key.asset.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}");
|
||||
}
|
||||
yield codec;
|
||||
break;
|
||||
case AssetType.other:
|
||||
case AssetType.audio:
|
||||
throw StateError('Unsupported asset type ${key.asset.type}');
|
||||
}
|
||||
} catch (error, stack) {
|
||||
Logger('ImmichLocalImageProvider')
|
||||
.severe('Error loading local image ${key.asset.name}', error, stack);
|
||||
throw const ImageLoadingException(
|
||||
'Could not load image from local storage',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Codec?> _getThumbnailCodec(
|
||||
LocalFullImageProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) async {
|
||||
final thumbBytes =
|
||||
await _assetMediaRepository.getThumbnail(key.asset.id, size: key.size);
|
||||
if (thumbBytes == null) {
|
||||
return null;
|
||||
}
|
||||
final buffer = await ImmutableBuffer.fromUint8List(thumbBytes);
|
||||
return decode(buffer);
|
||||
}
|
||||
|
||||
Stream<Codec> _decodeProgressive(
|
||||
LocalFullImageProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) async* {
|
||||
final file = await _storageRepository.getFileForAsset(key.asset);
|
||||
if (file == null) {
|
||||
throw StateError("Opening file for asset ${key.asset.name} failed");
|
||||
}
|
||||
|
||||
final fileSize = await file.length();
|
||||
final devicePixelRatio =
|
||||
PlatformDispatcher.instance.views.first.devicePixelRatio;
|
||||
final isLargeFile = fileSize > 20 * 1024 * 1024; // 20MB
|
||||
final isHEIC = file.path.toLowerCase().contains(RegExp(r'\.(heic|heif)$'));
|
||||
final isProgressive = isLargeFile || (isHEIC && !Platform.isIOS);
|
||||
|
||||
if (isProgressive) {
|
||||
try {
|
||||
final progressiveMultiplier = devicePixelRatio >= 3.0 ? 1.3 : 1.2;
|
||||
final size = Size(
|
||||
(key.size.width * progressiveMultiplier).clamp(256, 1024),
|
||||
(key.size.height * progressiveMultiplier).clamp(256, 1024),
|
||||
);
|
||||
final mediumThumb =
|
||||
await _assetMediaRepository.getThumbnail(key.asset.id, size: size);
|
||||
if (mediumThumb != null) {
|
||||
final mediumBuffer = await ImmutableBuffer.fromUint8List(mediumThumb);
|
||||
yield await decode(mediumBuffer);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// Load original only when the file is smaller or if the user wants to load original images
|
||||
// Or load a slightly larger image for progressive loading
|
||||
if (isProgressive && !(AppSetting.get(Setting.loadOriginal))) {
|
||||
final progressiveMultiplier = devicePixelRatio >= 3.0 ? 2.0 : 1.6;
|
||||
final size = Size(
|
||||
(key.size.width * progressiveMultiplier).clamp(512, 2048),
|
||||
(key.size.height * progressiveMultiplier).clamp(512, 2048),
|
||||
);
|
||||
final highThumb =
|
||||
await _assetMediaRepository.getThumbnail(key.asset.id, size: size);
|
||||
if (highThumb != null) {
|
||||
final highBuffer = await ImmutableBuffer.fromUint8List(highThumb);
|
||||
yield await decode(highBuffer);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final buffer = await ImmutableBuffer.fromFilePath(file.path);
|
||||
yield await decode(buffer);
|
||||
}
|
||||
|
||||
@override
|
||||
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 false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
asset.id.hashCode ^ asset.updatedAt.hashCode ^ size.hashCode;
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/asset_media.repository.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
|
||||
|
||||
class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
||||
final AssetMediaRepository _assetMediaRepository =
|
||||
const AssetMediaRepository();
|
||||
final CacheManager? cacheManager;
|
||||
|
||||
final LocalAsset asset;
|
||||
final double height;
|
||||
final double width;
|
||||
|
||||
LocalThumbProvider({
|
||||
required this.asset,
|
||||
this.height = kTimelineFixedTileExtent,
|
||||
this.width = kTimelineFixedTileExtent,
|
||||
this.cacheManager,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<LocalThumbProvider> obtainKey(
|
||||
ImageConfiguration configuration,
|
||||
) {
|
||||
return SynchronousFuture(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
LocalThumbProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) {
|
||||
final cache = cacheManager ?? ThumbnailImageCacheManager();
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _codec(key, cache, decode),
|
||||
scale: 1.0,
|
||||
informationCollector: () => <DiagnosticsNode>[
|
||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||
DiagnosticsProperty<LocalAsset>('Asset', key.asset),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<Codec> _codec(
|
||||
LocalThumbProvider key,
|
||||
CacheManager cache,
|
||||
ImageDecoderCallback decode,
|
||||
) async {
|
||||
final cacheKey = '${key.asset.id}-${key.asset.updatedAt}-${width}x$height';
|
||||
|
||||
final fileFromCache = await cache.getFileFromCache(cacheKey);
|
||||
if (fileFromCache != null) {
|
||||
try {
|
||||
final buffer =
|
||||
await ImmutableBuffer.fromFilePath(fileFromCache.file.path);
|
||||
return await decode(buffer);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
final thumbnailBytes = await _assetMediaRepository.getThumbnail(
|
||||
key.asset.id,
|
||||
size: Size(key.width, key.height),
|
||||
);
|
||||
if (thumbnailBytes == null) {
|
||||
PaintingBinding.instance.imageCache.evict(key);
|
||||
throw StateError(
|
||||
"Loading thumb for local photo ${key.asset.name} failed",
|
||||
);
|
||||
}
|
||||
|
||||
final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes);
|
||||
unawaited(cache.putFile(cacheKey, thumbnailBytes));
|
||||
return decode(buffer);
|
||||
}
|
||||
|
||||
@override
|
||||
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 false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => asset.id.hashCode ^ asset.updatedAt.hashCode;
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||
import 'package:immich_mobile/providers/image/cache/image_loader.dart';
|
||||
import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
|
||||
class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
|
||||
final String assetId;
|
||||
final CacheManager? cacheManager;
|
||||
|
||||
const RemoteThumbProvider({
|
||||
required this.assetId,
|
||||
this.cacheManager,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<RemoteThumbProvider> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
RemoteThumbProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) {
|
||||
final cache = cacheManager ?? RemoteImageCacheManager();
|
||||
final chunkController = StreamController<ImageChunkEvent>();
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _codec(key, cache, decode, chunkController),
|
||||
scale: 1.0,
|
||||
chunkEvents: chunkController.stream,
|
||||
informationCollector: () => <DiagnosticsNode>[
|
||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||
DiagnosticsProperty<String>('Asset Id', key.assetId),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<Codec> _codec(
|
||||
RemoteThumbProvider key,
|
||||
CacheManager cache,
|
||||
ImageDecoderCallback decode,
|
||||
StreamController<ImageChunkEvent> chunkController,
|
||||
) async {
|
||||
final preview = getThumbnailUrlForRemoteId(
|
||||
key.assetId,
|
||||
);
|
||||
|
||||
return ImageLoader.loadImageFromCache(
|
||||
preview,
|
||||
cache: cache,
|
||||
decode: decode,
|
||||
chunkEvents: chunkController,
|
||||
).whenComplete(chunkController.close);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is RemoteThumbProvider) {
|
||||
return assetId == other.assetId;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => assetId.hashCode;
|
||||
}
|
||||
|
||||
class RemoteFullImageProvider extends ImageProvider<RemoteFullImageProvider> {
|
||||
final String assetId;
|
||||
final CacheManager? cacheManager;
|
||||
|
||||
const RemoteFullImageProvider({
|
||||
required this.assetId,
|
||||
this.cacheManager,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<RemoteFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
RemoteFullImageProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) {
|
||||
final cache = cacheManager ?? RemoteImageCacheManager();
|
||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
||||
return MultiImageStreamCompleter(
|
||||
codec: _codec(key, cache, decode, chunkEvents),
|
||||
scale: 1.0,
|
||||
chunkEvents: chunkEvents.stream,
|
||||
);
|
||||
}
|
||||
|
||||
Stream<Codec> _codec(
|
||||
RemoteFullImageProvider key,
|
||||
CacheManager cache,
|
||||
ImageDecoderCallback decode,
|
||||
StreamController<ImageChunkEvent> chunkController,
|
||||
) async* {
|
||||
yield await ImageLoader.loadImageFromCache(
|
||||
getPreviewUrlForRemoteId(key.assetId),
|
||||
cache: cache,
|
||||
decode: decode,
|
||||
chunkEvents: chunkController,
|
||||
);
|
||||
|
||||
if (AppSetting.get(Setting.loadOriginal)) {
|
||||
yield await ImageLoader.loadImageFromCache(
|
||||
getOriginalUrlForRemoteId(key.assetId),
|
||||
cache: cache,
|
||||
decode: decode,
|
||||
chunkEvents: chunkController,
|
||||
);
|
||||
}
|
||||
await chunkController.close();
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is RemoteFullImageProvider) {
|
||||
return assetId == other.assetId;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => assetId.hashCode;
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||
import 'package:immich_mobile/providers/image/cache/image_loader.dart';
|
||||
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
|
||||
class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
|
||||
final String assetId;
|
||||
final double height;
|
||||
final double width;
|
||||
final CacheManager? cacheManager;
|
||||
|
||||
const RemoteThumbProvider({
|
||||
required this.assetId,
|
||||
this.height = kTimelineFixedTileExtent,
|
||||
this.width = kTimelineFixedTileExtent,
|
||||
this.cacheManager,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<RemoteThumbProvider> obtainKey(
|
||||
ImageConfiguration configuration,
|
||||
) {
|
||||
return SynchronousFuture(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
RemoteThumbProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) {
|
||||
final cache = cacheManager ?? ThumbnailImageCacheManager();
|
||||
final chunkController = StreamController<ImageChunkEvent>();
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _codec(key, cache, decode, chunkController),
|
||||
scale: 1.0,
|
||||
chunkEvents: chunkController.stream,
|
||||
informationCollector: () => <DiagnosticsNode>[
|
||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||
DiagnosticsProperty<String>('Asset Id', key.assetId),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<Codec> _codec(
|
||||
RemoteThumbProvider key,
|
||||
CacheManager cache,
|
||||
ImageDecoderCallback decode,
|
||||
StreamController<ImageChunkEvent> chunkController,
|
||||
) async {
|
||||
final preview = getThumbnailUrlForRemoteId(
|
||||
key.assetId,
|
||||
);
|
||||
|
||||
return ImageLoader.loadImageFromCache(
|
||||
preview,
|
||||
cache: cache,
|
||||
decode: decode,
|
||||
chunkEvents: chunkController,
|
||||
).whenComplete(chunkController.close);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is RemoteThumbProvider) {
|
||||
return assetId == other.assetId;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => assetId.hashCode;
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/local_thumb_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/remote_thumb_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumb_hash_provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
|
||||
import 'package:immich_mobile/widgets/common/fade_in_placeholder_image.dart';
|
||||
|
|
@ -25,49 +24,12 @@ class Thumbnail extends StatelessWidget {
|
|||
final Size size;
|
||||
final BoxFit fit;
|
||||
|
||||
static ImageProvider imageProvider({
|
||||
BaseAsset? asset,
|
||||
String? remoteId,
|
||||
Size size = const Size.square(256),
|
||||
}) {
|
||||
assert(
|
||||
asset != null || remoteId != null,
|
||||
'Either asset or remoteId must be provided',
|
||||
);
|
||||
|
||||
if (remoteId != null) {
|
||||
return RemoteThumbProvider(
|
||||
assetId: remoteId,
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
);
|
||||
}
|
||||
|
||||
if (asset is LocalAsset) {
|
||||
return LocalThumbProvider(
|
||||
asset: asset,
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
);
|
||||
}
|
||||
|
||||
if (asset is RemoteAsset) {
|
||||
return RemoteThumbProvider(
|
||||
assetId: asset.id,
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
);
|
||||
}
|
||||
|
||||
throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final thumbHash =
|
||||
asset is RemoteAsset ? (asset as RemoteAsset).thumbHash : null;
|
||||
final provider =
|
||||
imageProvider(asset: asset, remoteId: remoteId, size: size);
|
||||
getThumbnailImageProvider(asset: asset, remoteId: remoteId, size: size);
|
||||
|
||||
return OctoImage.fromSet(
|
||||
image: provider,
|
||||
|
|
|
|||
|
|
@ -59,10 +59,13 @@ class ThumbnailTile extends ConsumerWidget {
|
|||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Thumbnail(
|
||||
asset: asset,
|
||||
fit: fit,
|
||||
size: size,
|
||||
child: Hero(
|
||||
tag: asset.heroTag,
|
||||
child: Thumbnail(
|
||||
asset: asset,
|
||||
fit: fit,
|
||||
size: size,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (asset.isVideo)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue