toggle cached image

This commit is contained in:
mertalev 2025-08-07 19:18:17 -04:00
parent dfd9ed988e
commit f78b151b64
No known key found for this signature in database
GPG key ID: DF6ABC77AAD98C95
4 changed files with 45 additions and 25 deletions

View file

@ -113,10 +113,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
super.dispose(); super.dispose();
} }
bool get showingBottomSheet => ref.read(assetViewerProvider.select((s) => s.showingBottomSheet)); bool get showingBottomSheet => ref.read(assetViewerProvider).showingBottomSheet;
Color get backgroundColor { Color get backgroundColor {
final opacity = ref.read(assetViewerProvider.select((s) => s.backgroundOpacity)); final opacity = ref.read(assetViewerProvider).backgroundOpacity;
return Colors.black.withAlpha(opacity); return Colors.black.withAlpha(opacity);
} }
@ -223,18 +223,17 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
void _onPageChanged(int index, PhotoViewControllerBase? controller) { void _onPageChanged(int index, PhotoViewControllerBase? controller) {
_onAssetChanged(index); _onAssetChanged(index);
viewController = controller; viewController = controller;
initialPhotoViewState = controller?.value ?? initialPhotoViewState;
// If the bottom sheet is showing, we need to adjust scale the asset to // If the bottom sheet is showing, we need to adjust scale the asset to
// emulate the zoom effect // emulate the zoom effect
if (showingBottomSheet) { if (showingBottomSheet) {
initialScale = controller?.scale; initialScale = controller?.scale;
// controller?.scale = _getScaleForBottomSheet; controller?.scale = _getScaleForBottomSheet;
} }
} }
void _onDragStart( void _onDragStart(
_, BuildContext ctx,
DragStartDetails details, DragStartDetails details,
PhotoViewControllerBase controller, PhotoViewControllerBase controller,
PhotoViewScaleStateController scaleStateController, PhotoViewScaleStateController scaleStateController,
@ -250,7 +249,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
} }
} }
void _onDragEnd(BuildContext ctx, _, __) { void _onDragEnd(BuildContext ctx, DragEndDetails details, PhotoViewControllerValue value) {
dragInProgress = false; dragInProgress = false;
if (shouldPopOnDrag) { if (shouldPopOnDrag) {
@ -281,7 +280,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
ref.read(assetViewerProvider.notifier).setOpacity(255); ref.read(assetViewerProvider.notifier).setOpacity(255);
} }
void _onDragUpdate(BuildContext ctx, DragUpdateDetails details, _) { void _onDragUpdate(BuildContext ctx, DragUpdateDetails details, PhotoViewControllerValue value) {
if (blockGestures) { if (blockGestures) {
return; return;
} }
@ -335,7 +334,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
ref.read(assetViewerProvider.notifier).setOpacity(backgroundOpacity); ref.read(assetViewerProvider.notifier).setOpacity(backgroundOpacity);
} }
void _onTapDown(_, __, ___) { void _onTapDown(BuildContext ctx, TapDownDetails details, PhotoViewControllerValue value) {
if (!showingBottomSheet) { if (!showingBottomSheet) {
ref.read(assetViewerProvider.notifier).toggleControls(); ref.read(assetViewerProvider.notifier).toggleControls();
} }
@ -431,7 +430,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
void _openBottomSheet(BuildContext ctx, {double extent = _kBottomSheetMinimumExtent}) { void _openBottomSheet(BuildContext ctx, {double extent = _kBottomSheetMinimumExtent}) {
ref.read(assetViewerProvider.notifier).setBottomSheet(true); ref.read(assetViewerProvider.notifier).setBottomSheet(true);
initialScale = viewController?.scale; initialScale = viewController?.scale;
// viewController?.updateMultiple(scale: _getScaleForBottomSheet); viewController?.updateMultiple(scale: _getScaleForBottomSheet);
previousExtent = _kBottomSheetMinimumExtent; previousExtent = _kBottomSheetMinimumExtent;
sheetCloseController = showBottomSheet( sheetCloseController = showBottomSheet(
context: ctx, context: ctx,
@ -450,8 +449,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
} }
void _handleSheetClose() { void _handleSheetClose() {
// viewController?.animateMultiple(position: Offset.zero); viewController?.animateMultiple(position: Offset.zero);
// viewController?.updateMultiple(scale: initialScale); viewController?.updateMultiple(scale: initialScale);
ref.read(assetViewerProvider.notifier).setBottomSheet(false); ref.read(assetViewerProvider.notifier).setBottomSheet(false);
sheetCloseController = null; sheetCloseController = null;
shouldPopOnDrag = false; shouldPopOnDrag = false;
@ -472,7 +471,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
BaseAsset asset = ref.read(timelineServiceProvider).getAsset(index); BaseAsset asset = ref.read(timelineServiceProvider).getAsset(index);
final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull; final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull;
if (stackChildren != null && stackChildren.isNotEmpty) { if (stackChildren != null && stackChildren.isNotEmpty) {
asset = stackChildren.elementAt(ref.read(assetViewerProvider.select((s) => s.stackIndex))); asset = stackChildren.elementAt(ref.read(assetViewerProvider).stackIndex);
} }
return Container( return Container(
width: double.infinity, width: double.infinity,
@ -488,7 +487,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
} }
} }
void _onLongPress(_, __, ___) { void _onLongPress(BuildContext ctx, LongPressStartDetails details, PhotoViewControllerValue value) {
ref.read(isPlayingMotionVideoProvider.notifier).playing = true; ref.read(isPlayingMotionVideoProvider.notifier).playing = true;
} }
@ -497,7 +496,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
BaseAsset asset = ref.read(timelineServiceProvider).getAsset(index); BaseAsset asset = ref.read(timelineServiceProvider).getAsset(index);
final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull; final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull;
if (stackChildren != null && stackChildren.isNotEmpty) { if (stackChildren != null && stackChildren.isNotEmpty) {
asset = stackChildren.elementAt(ref.read(assetViewerProvider.select((s) => s.stackIndex))); asset = stackChildren.elementAt(ref.read(assetViewerProvider).stackIndex);
} }
final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider); final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider);
@ -512,7 +511,12 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
final size = ctx.sizeData; final size = ctx.sizeData;
return PhotoViewGalleryPageOptions( return PhotoViewGalleryPageOptions(
key: ValueKey(asset.heroTag), key: ValueKey(asset.heroTag),
imageProvider: getFullImageProvider(asset, size: size), // When the bottom sheet is shown and the asset is changed,
// the cached image can have different position and scale than the normal one,
// causing incorrect animation calculations once the image provider yields a new image.
// This is a workaround to ensure the animation is handled correctly in this case.
// TODO: handle this without needing to disable caching
imageProvider: getFullImageProvider(asset, size: size, showCached: !showingBottomSheet),
heroAttributes: PhotoViewHeroAttributes(tag: '${asset.heroTag}_$heroOffset'), heroAttributes: PhotoViewHeroAttributes(tag: '${asset.heroTag}_$heroOffset'),
filterQuality: FilterQuality.high, filterQuality: FilterQuality.high,
tightMode: true, tightMode: true,
@ -577,9 +581,11 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Rebuild the widget when the asset viewer state changes // Rebuild the widget when the asset viewer state changes
// Using multiple selectors to avoid unnecessary rebuilds for other state changes // Using multiple selectors to avoid unnecessary rebuilds for other state changes
ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet)); ref.watch(
ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)); assetViewerProvider.select(
ref.watch(assetViewerProvider.select((s) => s.stackIndex)); (s) => s.showingBottomSheet.hashCode ^ s.backgroundOpacity.hashCode ^ s.stackIndex.hashCode,
),
);
ref.watch(isPlayingMotionVideoProvider); ref.watch(isPlayingMotionVideoProvider);
// Listen for casting changes and send initial asset to the cast provider // Listen for casting changes and send initial asset to the cast provider

View file

@ -6,12 +6,18 @@ import 'package:immich_mobile/presentation/widgets/images/local_image_provider.d
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920)}) { ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080, 1920), bool showCached = true}) {
// Create new provider and cache it // Create new provider and cache it
final ImageProvider provider; final ImageProvider provider;
if (_shouldUseLocalAsset(asset)) { if (_shouldUseLocalAsset(asset)) {
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
provider = LocalFullImageProvider(id: id, size: size, type: asset.type, updatedAt: asset.updatedAt); provider = LocalFullImageProvider(
id: id,
size: size,
type: asset.type,
updatedAt: asset.updatedAt,
showCached: showCached,
);
} else { } else {
final String assetId; final String assetId;
if (asset is LocalAsset && asset.hasRemote) { if (asset is LocalAsset && asset.hasRemote) {
@ -21,7 +27,7 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080
} else { } else {
throw ArgumentError("Unsupported asset type: ${asset.runtimeType}"); throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
} }
provider = RemoteFullImageProvider(assetId: assetId); provider = RemoteFullImageProvider(assetId: assetId, showCached: showCached);
} }
return provider; return provider;

View file

@ -95,8 +95,15 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
final Size size; final Size size;
final AssetType type; final AssetType type;
final DateTime updatedAt; // temporary, only exists to fetch cached thumbnail until local disk cache is removed final DateTime updatedAt; // temporary, only exists to fetch cached thumbnail until local disk cache is removed
final bool showCached;
const LocalFullImageProvider({required this.id, required this.size, required this.type, required this.updatedAt}); const LocalFullImageProvider({
required this.id,
required this.size,
required this.type,
required this.updatedAt,
this.showCached = true,
});
@override @override
Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) { Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) {
@ -107,7 +114,7 @@ class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
ImageStreamCompleter loadImage(LocalFullImageProvider key, ImageDecoderCallback decode) { ImageStreamCompleter loadImage(LocalFullImageProvider key, ImageDecoderCallback decode) {
return OneFramePlaceholderImageStreamCompleter( return OneFramePlaceholderImageStreamCompleter(
_codec(key, decode), _codec(key, decode),
initialImage: getCachedImage(LocalThumbProvider(id: key.id, updatedAt: key.updatedAt)), initialImage: showCached ? getCachedImage(LocalThumbProvider(id: key.id, updatedAt: key.updatedAt)) : null,
informationCollector: () => <DiagnosticsNode>[ informationCollector: () => <DiagnosticsNode>[
DiagnosticsProperty<String>('Id', key.id), DiagnosticsProperty<String>('Id', key.id),
DiagnosticsProperty<DateTime>('Updated at', key.updatedAt), DiagnosticsProperty<DateTime>('Updated at', key.updatedAt),

View file

@ -71,9 +71,10 @@ class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
class RemoteFullImageProvider extends ImageProvider<RemoteFullImageProvider> { class RemoteFullImageProvider extends ImageProvider<RemoteFullImageProvider> {
final String assetId; final String assetId;
final bool showCached;
final CacheManager? cacheManager; final CacheManager? cacheManager;
const RemoteFullImageProvider({required this.assetId, this.cacheManager}); const RemoteFullImageProvider({required this.assetId, this.cacheManager, this.showCached = true});
@override @override
Future<RemoteFullImageProvider> obtainKey(ImageConfiguration configuration) { Future<RemoteFullImageProvider> obtainKey(ImageConfiguration configuration) {
@ -85,7 +86,7 @@ class RemoteFullImageProvider extends ImageProvider<RemoteFullImageProvider> {
final cache = cacheManager ?? RemoteImageCacheManager(); final cache = cacheManager ?? RemoteImageCacheManager();
return OneFramePlaceholderImageStreamCompleter( return OneFramePlaceholderImageStreamCompleter(
_codec(key, cache, decode), _codec(key, cache, decode),
initialImage: getCachedImage(RemoteThumbProvider(assetId: key.assetId)), initialImage: showCached ? getCachedImage(RemoteThumbProvider(assetId: key.assetId)) : null,
); );
} }