1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-01 08:31:59 +00:00

use current asset provider and loadAsset

This commit is contained in:
mertalev 2024-11-15 12:36:34 -05:00
parent 613ce513cd
commit 3b9a3d4037
No known key found for this signature in database
GPG key ID: CA85EF6600C9E8AD
6 changed files with 145 additions and 125 deletions

View file

@ -67,22 +67,17 @@ class GalleryViewerPage extends HookConsumerWidget {
? ref.watch(assetStackStateProvider(currentAsset)) ? ref.watch(assetStackStateProvider(currentAsset))
: <Asset>[]; : <Asset>[];
final stackElements = showStack ? [currentAsset, ...stack] : <Asset>[]; final stackElements = showStack ? [currentAsset, ...stack] : <Asset>[];
// Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
final isFromDto = currentAsset.id == noDbId;
Asset asset = stackIndex.value == -1
? currentAsset
: stackElements.elementAt(stackIndex.value);
// // Update is playing motion video // // Update is playing motion video
if (asset.isMotionPhoto) { ref.listen(
ref.listen( videoPlaybackValueProvider.select(
videoPlaybackValueProvider.select( (playback) => playback.state == VideoPlaybackState.playing,
(playback) => playback.state == VideoPlaybackState.playing, ), (_, isPlaying) {
), (_, isPlaying) { final asset = ref.read(currentAssetProvider);
if (asset != null && asset.isMotionPhoto) {
isPlayingMotionVideo.value = isPlaying; isPlayingMotionVideo.value = isPlaying;
}); }
} });
Future<void> precacheNextImage(int index) async { Future<void> precacheNextImage(int index) async {
if (!context.mounted) { if (!context.mounted) {
@ -114,26 +109,29 @@ class GalleryViewerPage extends HookConsumerWidget {
} }
} }
// Listen provider to prevent autoDispose when navigating to other routes from within the gallery page useEffect(
ref.listen(currentAssetProvider, (prev, cur) {}); () {
if (ref.read(showControlsProvider)) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}
useEffect(() { // Delay this a bit so we can finish loading the page
ref.read(currentAssetProvider.notifier).set(asset); Timer(const Duration(milliseconds: 400), () {
if (ref.read(showControlsProvider)) { precacheNextImage(currentIndex.value + 1);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); });
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}
// Delay this a bit so we can finish loading the page return null;
Timer(const Duration(milliseconds: 400), () { },
precacheNextImage(currentIndex.value + 1); [],
}); );
return null;
});
void showInfo() { void showInfo() {
final asset = ref.read(currentAssetProvider);
if (asset == null) {
return;
}
showModalBottomSheet( showModalBottomSheet(
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(15.0)), borderRadius: BorderRadius.all(Radius.circular(15.0)),
@ -205,7 +203,6 @@ class GalleryViewerPage extends HookConsumerWidget {
} }
return ListView.builder( return ListView.builder(
key: ValueKey(currentAsset),
shrinkWrap: true, shrinkWrap: true,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: stackElements.length, itemCount: stackElements.length,
@ -252,12 +249,6 @@ class GalleryViewerPage extends HookConsumerWidget {
); );
} }
Object getHeroTag(Asset asset) {
return isFromDto
? '${asset.remoteId}-$heroOffset'
: asset.id + heroOffset;
}
PhotoViewGalleryPageOptions buildImage(BuildContext context, Asset asset) { PhotoViewGalleryPageOptions buildImage(BuildContext context, Asset asset) {
return PhotoViewGalleryPageOptions( return PhotoViewGalleryPageOptions(
onDragStart: (_, details, __) { onDragStart: (_, details, __) {
@ -277,10 +268,7 @@ class GalleryViewerPage extends HookConsumerWidget {
} }
: null, : null,
imageProvider: ImmichImage.imageProvider(asset: asset), imageProvider: ImmichImage.imageProvider(asset: asset),
heroAttributes: PhotoViewHeroAttributes( heroAttributes: _getHeroAttributes(asset),
tag: getHeroTag(asset),
transitionOnUserGestures: true,
),
filterQuality: FilterQuality.high, filterQuality: FilterQuality.high,
tightMode: true, tightMode: true,
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
@ -294,40 +282,33 @@ class GalleryViewerPage extends HookConsumerWidget {
PhotoViewGalleryPageOptions buildVideo(BuildContext context, Asset asset) { PhotoViewGalleryPageOptions buildVideo(BuildContext context, Asset asset) {
// This key is to prevent the video player from being re-initialized during the hero animation // This key is to prevent the video player from being re-initialized during the hero animation
final key = GlobalKey(); final key = GlobalKey();
final tag = getHeroTag(asset);
return PhotoViewGalleryPageOptions.customChild( return PhotoViewGalleryPageOptions.customChild(
onDragStart: (_, details, __) => onDragStart: (_, details, __) =>
localPosition.value = details.localPosition, localPosition.value = details.localPosition,
onDragUpdate: (_, details, __) => handleSwipeUpDown(details), onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
heroAttributes: PhotoViewHeroAttributes( heroAttributes: _getHeroAttributes(asset),
tag: tag,
transitionOnUserGestures: true,
),
filterQuality: FilterQuality.high, filterQuality: FilterQuality.high,
initialScale: 1.0, initialScale: 1.0,
maxScale: 1.0, maxScale: 1.0,
minScale: 1.0, minScale: 1.0,
basePosition: Alignment.center, basePosition: Alignment.center,
child: Hero( child: SizedBox(
tag: tag, width: context.width,
child: SizedBox( height: context.height,
width: context.width, child: NativeVideoViewerPage(
height: context.height, key: key,
child: NativeVideoViewerPage( asset: asset,
key: key, placeholder: Image(
asset: asset, key: ValueKey(asset),
placeholder: Image( image: ImmichImage.imageProvider(
key: ValueKey(asset), asset: asset,
image: ImmichImage.imageProvider(
asset: asset,
width: context.width,
height: context.height,
),
fit: BoxFit.contain,
height: context.height,
width: context.width, width: context.width,
alignment: Alignment.center, height: context.height,
), ),
fit: BoxFit.contain,
height: context.height,
width: context.width,
alignment: Alignment.center,
), ),
), ),
), ),
@ -335,7 +316,7 @@ class GalleryViewerPage extends HookConsumerWidget {
} }
PhotoViewGalleryPageOptions buildAsset(BuildContext context, int index) { PhotoViewGalleryPageOptions buildAsset(BuildContext context, int index) {
final newAsset = index == currentIndex.value ? asset : loadAsset(index); final newAsset = loadAsset(index);
if (newAsset.isImage && !isPlayingMotionVideo.value) { if (newAsset.isImage && !isPlayingMotionVideo.value) {
return buildImage(context, newAsset); return buildImage(context, newAsset);
@ -343,6 +324,8 @@ class GalleryViewerPage extends HookConsumerWidget {
return buildVideo(context, newAsset); return buildVideo(context, newAsset);
} }
log.info('GalleryViewerPage: Building gallery viewer page');
return PopScope( return PopScope(
// Change immersive mode back to normal "edgeToEdge" mode // Change immersive mode back to normal "edgeToEdge" mode
onPopInvokedWithResult: (didPop, _) => onPopInvokedWithResult: (didPop, _) =>
@ -352,34 +335,41 @@ class GalleryViewerPage extends HookConsumerWidget {
body: Stack( body: Stack(
children: [ children: [
PhotoViewGallery.builder( PhotoViewGallery.builder(
key: ValueKey(asset), key: const ValueKey('gallery'),
scaleStateChangedCallback: (state) { scaleStateChangedCallback: (state) {
final asset = ref.read(currentAssetProvider);
if (asset == null) {
return;
}
if (asset.isImage && !isPlayingMotionVideo.value) { if (asset.isImage && !isPlayingMotionVideo.value) {
isZoomed.value = state != PhotoViewScaleState.initial; isZoomed.value = state != PhotoViewScaleState.initial;
ref.read(showControlsProvider.notifier).show = ref.read(showControlsProvider.notifier).show =
!isZoomed.value; !isZoomed.value;
} }
}, },
// wantKeepAlive: true,
gaplessPlayback: true, gaplessPlayback: true,
loadingBuilder: (context, event, index) => ClipRect( loadingBuilder: (context, event, index) {
child: Stack( final asset = loadAsset(index);
fit: StackFit.expand, return ClipRect(
children: [ child: Stack(
BackdropFilter( fit: StackFit.expand,
filter: ui.ImageFilter.blur( children: [
sigmaX: 10, BackdropFilter(
sigmaY: 10, filter: ui.ImageFilter.blur(
sigmaX: 10,
sigmaY: 10,
),
), ),
), ImmichThumbnail(
ImmichThumbnail( key: ValueKey(asset),
key: ValueKey(asset), asset: asset,
asset: asset, fit: BoxFit.contain,
fit: BoxFit.contain, ),
), ],
], ),
), );
), },
pageController: controller, pageController: controller,
scrollPhysics: isZoomed.value scrollPhysics: isZoomed.value
? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in ? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
@ -394,8 +384,7 @@ class GalleryViewerPage extends HookConsumerWidget {
ref.read(hapticFeedbackProvider.notifier).selectionClick(); ref.read(hapticFeedbackProvider.notifier).selectionClick();
final newAsset = final newAsset = loadAsset(value);
value == currentIndex.value ? asset : loadAsset(value);
currentIndex.value = value; currentIndex.value = value;
stackIndex.value = -1; stackIndex.value = -1;
@ -418,6 +407,7 @@ class GalleryViewerPage extends HookConsumerWidget {
left: 0, left: 0,
right: 0, right: 0,
child: GalleryAppBar( child: GalleryAppBar(
key: const ValueKey('app-bar'),
showInfo: showInfo, showInfo: showInfo,
isPlayingMotionVideo: isPlayingMotionVideo, isPlayingMotionVideo: isPlayingMotionVideo,
), ),
@ -436,6 +426,7 @@ class GalleryViewerPage extends HookConsumerWidget {
), ),
), ),
BottomGalleryBar( BottomGalleryBar(
key: const ValueKey('bottom-bar'),
renderList: renderList, renderList: renderList,
totalAssets: totalAssets, totalAssets: totalAssets,
controller: controller, controller: controller,
@ -452,4 +443,14 @@ class GalleryViewerPage extends HookConsumerWidget {
), ),
); );
} }
@pragma('vm:prefer-inline')
PhotoViewHeroAttributes _getHeroAttributes(Asset asset) {
return PhotoViewHeroAttributes(
tag: asset.isInDb
? asset.id + heroOffset
: '${asset.remoteId}-$heroOffset',
transitionOnUserGestures: true,
);
}
} }

View file

@ -24,7 +24,6 @@ import 'package:wakelock_plus/wakelock_plus.dart';
class NativeVideoViewerPage extends HookConsumerWidget { class NativeVideoViewerPage extends HookConsumerWidget {
final Asset asset; final Asset asset;
final bool showControls; final bool showControls;
final Duration hideControlsTimer;
final Widget placeholder; final Widget placeholder;
const NativeVideoViewerPage({ const NativeVideoViewerPage({
@ -32,7 +31,6 @@ class NativeVideoViewerPage extends HookConsumerWidget {
required this.asset, required this.asset,
required this.placeholder, required this.placeholder,
this.showControls = true, this.showControls = true,
this.hideControlsTimer = const Duration(seconds: 5),
}); });
@override @override
@ -370,6 +368,18 @@ class NativeVideoViewerPage extends HookConsumerWidget {
removeListeners(playerController); removeListeners(playerController);
} }
final curAsset = currentAsset.value;
if (curAsset == asset) {
return;
}
// no need to delay video playback when swiping from an image to a video
if (curAsset != null && !curAsset.isVideo) {
currentAsset.value = value;
onPlaybackReady();
return;
}
// Delay the video playback to avoid a stutter in the swipe animation // Delay the video playback to avoid a stutter in the swipe animation
Timer(const Duration(milliseconds: 300), () { Timer(const Duration(milliseconds: 300), () {
if (!context.mounted) { if (!context.mounted) {
@ -395,38 +405,30 @@ class NativeVideoViewerPage extends HookConsumerWidget {
log.severe('Error stopping video: $error'); log.severe('Error stopping video: $error');
}); });
controller.value = null;
WakelockPlus.disable(); WakelockPlus.disable();
}; };
}, },
[videoSource], [],
); );
return Stack( return Stack(
children: [ children: [
placeholder, // this is always under the video to avoid flickering placeholder, // this is always under the video to avoid flickering
Center( if (aspectRatio.value != null)
key: ValueKey('player-${asset.hashCode}'),
child: aspectRatio.value != null
? AspectRatio(
key: ValueKey(asset),
aspectRatio: aspectRatio.value!,
child: isCurrent
? NativeVideoPlayerView(
key: ValueKey(asset),
onViewReady: initController,
)
: null,
)
: null,
),
if (showControls)
Center( Center(
key: ValueKey('controls-${asset.hashCode}'), key: ValueKey(asset),
child: CustomVideoPlayerControls( child: AspectRatio(
hideTimerDuration: hideControlsTimer, key: ValueKey(asset),
aspectRatio: aspectRatio.value!,
child: isCurrent
? NativeVideoPlayerView(
key: ValueKey(asset),
onViewReady: initController,
)
: null,
), ),
), ),
if (showControls) const Center(child: CustomVideoPlayerControls()),
], ],
); );
} }

View file

@ -1087,7 +1087,6 @@ class NativeVideoViewerRoute extends PageRouteInfo<NativeVideoViewerRouteArgs> {
required Asset asset, required Asset asset,
required Widget placeholder, required Widget placeholder,
bool showControls = true, bool showControls = true,
Duration hideControlsTimer = const Duration(seconds: 5),
List<PageRouteInfo>? children, List<PageRouteInfo>? children,
}) : super( }) : super(
NativeVideoViewerRoute.name, NativeVideoViewerRoute.name,
@ -1096,7 +1095,6 @@ class NativeVideoViewerRoute extends PageRouteInfo<NativeVideoViewerRouteArgs> {
asset: asset, asset: asset,
placeholder: placeholder, placeholder: placeholder,
showControls: showControls, showControls: showControls,
hideControlsTimer: hideControlsTimer,
), ),
initialChildren: children, initialChildren: children,
); );
@ -1112,7 +1110,6 @@ class NativeVideoViewerRoute extends PageRouteInfo<NativeVideoViewerRouteArgs> {
asset: args.asset, asset: args.asset,
placeholder: args.placeholder, placeholder: args.placeholder,
showControls: args.showControls, showControls: args.showControls,
hideControlsTimer: args.hideControlsTimer,
); );
}, },
); );
@ -1124,7 +1121,6 @@ class NativeVideoViewerRouteArgs {
required this.asset, required this.asset,
required this.placeholder, required this.placeholder,
this.showControls = true, this.showControls = true,
this.hideControlsTimer = const Duration(seconds: 5),
}); });
final Key? key; final Key? key;
@ -1135,11 +1131,9 @@ class NativeVideoViewerRouteArgs {
final bool showControls; final bool showControls;
final Duration hideControlsTimer;
@override @override
String toString() { String toString() {
return 'NativeVideoViewerRouteArgs{key: $key, asset: $asset, placeholder: $placeholder, showControls: $showControls, hideControlsTimer: $hideControlsTimer}'; return 'NativeVideoViewerRouteArgs{key: $key, asset: $asset, placeholder: $placeholder, showControls: $showControls}';
} }
} }

View file

@ -12,7 +12,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/collection_extensions.dart'; import 'package:immich_mobile/extensions/collection_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_drag_region.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_drag_region.dart';
import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart'; import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart';
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
@ -89,6 +91,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
ScrollOffsetController(); ScrollOffsetController();
final ItemPositionsListener _itemPositionsListener = final ItemPositionsListener _itemPositionsListener =
ItemPositionsListener.create(); ItemPositionsListener.create();
late final KeepAliveLink currentAssetLink;
/// The timestamp when the haptic feedback was last invoked /// The timestamp when the haptic feedback was last invoked
int _hapticFeedbackTS = 0; int _hapticFeedbackTS = 0;
@ -201,6 +204,12 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
allAssetsSelected: _allAssetsSelected, allAssetsSelected: _allAssetsSelected,
showStack: widget.showStack, showStack: widget.showStack,
heroOffset: widget.heroOffset, heroOffset: widget.heroOffset,
onAssetTap: (asset) {
ref.read(currentAssetProvider.notifier).set(asset);
if (asset.isVideo) {
ref.read(showControlsProvider.notifier).show = false;
}
},
); );
} }
@ -348,6 +357,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
currentAssetLink = ref.read(currentAssetProvider.notifier).ref.keepAlive();
scrollToTopNotifierProvider.addListener(_scrollToTop); scrollToTopNotifierProvider.addListener(_scrollToTop);
scrollToDateNotifierProvider.addListener(_scrollToDate); scrollToDateNotifierProvider.addListener(_scrollToDate);
@ -369,6 +379,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
_itemPositionsListener.itemPositions.removeListener(_positionListener); _itemPositionsListener.itemPositions.removeListener(_positionListener);
} }
_itemPositionsListener.itemPositions.removeListener(_hapticsListener); _itemPositionsListener.itemPositions.removeListener(_hapticsListener);
currentAssetLink.close();
super.dispose(); super.dispose();
} }
@ -595,12 +606,13 @@ class _Section extends StatelessWidget {
final RenderList renderList; final RenderList renderList;
final bool selectionActive; final bool selectionActive;
final bool dynamicLayout; final bool dynamicLayout;
final Function(List<Asset>) selectAssets; final void Function(List<Asset>) selectAssets;
final Function(List<Asset>) deselectAssets; final void Function(List<Asset>) deselectAssets;
final bool Function(List<Asset>) allAssetsSelected; final bool Function(List<Asset>) allAssetsSelected;
final bool showStack; final bool showStack;
final int heroOffset; final int heroOffset;
final bool showStorageIndicator; final bool showStorageIndicator;
final void Function(Asset) onAssetTap;
const _Section({ const _Section({
required this.section, required this.section,
@ -618,6 +630,7 @@ class _Section extends StatelessWidget {
required this.showStack, required this.showStack,
required this.heroOffset, required this.heroOffset,
required this.showStorageIndicator, required this.showStorageIndicator,
required this.onAssetTap,
}); });
@override @override
@ -683,6 +696,7 @@ class _Section extends StatelessWidget {
selectionActive: selectionActive, selectionActive: selectionActive,
onSelect: (asset) => selectAssets([asset]), onSelect: (asset) => selectAssets([asset]),
onDeselect: (asset) => deselectAssets([asset]), onDeselect: (asset) => deselectAssets([asset]),
onAssetTap: onAssetTap,
), ),
], ],
); );
@ -724,9 +738,9 @@ class _Title extends StatelessWidget {
final String title; final String title;
final List<Asset> assets; final List<Asset> assets;
final bool selectionActive; final bool selectionActive;
final Function(List<Asset>) selectAssets; final void Function(List<Asset>) selectAssets;
final Function(List<Asset>) deselectAssets; final void Function(List<Asset>) deselectAssets;
final Function(List<Asset>) allAssetsSelected; final bool Function(List<Asset>) allAssetsSelected;
const _Title({ const _Title({
required this.title, required this.title,
@ -765,8 +779,9 @@ class _AssetRow extends StatelessWidget {
final bool showStorageIndicator; final bool showStorageIndicator;
final int heroOffset; final int heroOffset;
final bool showStack; final bool showStack;
final Function(Asset)? onSelect; final void Function(Asset) onAssetTap;
final Function(Asset)? onDeselect; final void Function(Asset)? onSelect;
final void Function(Asset)? onDeselect;
final bool isSelectionActive; final bool isSelectionActive;
const _AssetRow({ const _AssetRow({
@ -786,6 +801,7 @@ class _AssetRow extends StatelessWidget {
required this.showStack, required this.showStack,
required this.isSelectionActive, required this.isSelectionActive,
required this.selectedAssets, required this.selectedAssets,
required this.onAssetTap,
this.onSelect, this.onSelect,
this.onDeselect, this.onDeselect,
}); });
@ -838,6 +854,8 @@ class _AssetRow extends StatelessWidget {
onSelect?.call(asset); onSelect?.call(asset);
} }
} else { } else {
final asset = renderList.loadAsset(absoluteOffset + index);
onAssetTap(asset);
context.pushRoute( context.pushRoute(
GalleryViewerRoute( GalleryViewerRoute(
renderList: renderList, renderList: renderList,

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
@ -12,7 +13,7 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
const CustomVideoPlayerControls({ const CustomVideoPlayerControls({
super.key, super.key,
this.hideTimerDuration = const Duration(seconds: 3), this.hideTimerDuration = const Duration(seconds: 5),
}); });
@override @override
@ -28,7 +29,12 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
final state = ref.read(videoPlaybackValueProvider).state; final state = ref.read(videoPlaybackValueProvider).state;
// Do not hide on paused // Do not hide on paused
if (state != VideoPlaybackState.paused) { if (state == VideoPlaybackState.paused) {
return;
}
final asset = ref.read(currentAssetProvider);
if (asset != null && asset.isVideo) {
ref.read(showControlsProvider.notifier).show = false; ref.read(showControlsProvider.notifier).show = false;
} }
}, },

View file

@ -71,7 +71,6 @@ class MemoryCard extends StatelessWidget {
child: NativeVideoViewerPage( child: NativeVideoViewerPage(
key: ValueKey(asset.id), key: ValueKey(asset.id),
asset: asset, asset: asset,
hideControlsTimer: const Duration(seconds: 2),
showControls: false, showControls: false,
placeholder: SizedBox.expand( placeholder: SizedBox.expand(
child: ImmichImage( child: ImmichImage(