feat(mobile): native_video_player (#12104)

* add native player library

* splitup the player

* stateful widget

* refactor: native_video_player

* fix: handle buffering

* turn on volume when video plays

* fix: aspect ratio

* fix: handle remote asset orientation

* refinements and fixes

fix orientation for remote assets

wip separate widget

separate video loader widget

fixed memory leak

optimized seeking, cleanup

debug context pop

use global key

back to one widget

fixed rebuild

wait for swipe animation to finish

smooth hero animation for remote videos

faster scroll animation

* clean up logging

* refactor aspect ratio calculation

* removed unnecessary import

* transitive dependencies

* fixed referencing uninitialized orientation

* use correct ref to build android

* higher res placeholder for local videos

* slightly lower delay

* await things

* fix controls when swiping between image and video

* linting

* extra smooth seeking, add comments

* chore: generate router page

* use current asset provider and loadAsset

* fix stack handling

* improved motion photo handling

* use visibility for motion videos

* error handling for async calls

* fix duplicate key error

* maybe fix duplicate key error

* increase delay for hero animation

* faster initialization for remote videos

* ensure dimensions for memory cards

* make aspect ratio logic reusable, optimizations

* refactor: move exif search from aspect ratio to orientation

* local orientation on ios is unreliable; prefer remote

* fix no audio in silent mode on ios

* increase bottom bar opacity to account for hdr

* remove unused import

* fix live photo play button not updating

* fix map marker -> galleryviewer

* remove video_player

* fix hdr playback on android

* fix looping

* remove unused dependencies

* update to latest player commit

* fix player controls hiding when video is not playing

* fix restart video

* stop showing motion video after ending when looping is disabled

* delay video initialization to avoid placeholder flicker

* faster animation

* shorter delay

* small delay for image -> video on android

* fix: lint

* hide stacked children when controls are hidden, avoid bottom bar dropping

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
This commit is contained in:
shenlong 2024-12-05 02:33:46 +05:30 committed by GitHub
parent 5060ee95c2
commit 3c38851d50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 1625 additions and 1243 deletions

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

View file

@ -5,11 +5,11 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
import 'package:immich_mobile/services/stack.service.dart';
@ -26,12 +26,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/pages/editing/edit.page.dart';
class BottomGalleryBar extends ConsumerWidget {
final Asset asset;
final ValueNotifier<int> assetIndex;
final bool showStack;
final int stackIndex;
final ValueNotifier<int> stackIndex;
final ValueNotifier<int> totalAssets;
final bool showVideoPlayerControls;
final PageController controller;
final RenderList renderList;
@ -39,20 +37,24 @@ class BottomGalleryBar extends ConsumerWidget {
super.key,
required this.showStack,
required this.stackIndex,
required this.asset,
required this.assetIndex,
required this.controller,
required this.totalAssets,
required this.showVideoPlayerControls,
required this.renderList,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asset = ref.watch(currentAssetProvider);
if (asset == null) {
return const SizedBox();
}
final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId;
final showControls = ref.watch(showControlsProvider);
final stackId = asset.stackId;
final stackItems = showStack && asset.stackCount > 0
? ref.watch(assetStackStateProvider(asset))
final stackItems = showStack && stackId != null
? ref.watch(assetStackStateProvider(stackId))
: <Asset>[];
bool isStackPrimaryAsset = asset.stackPrimaryAssetId == null;
final navStack = AutoRouter.of(context).stackData;
@ -64,10 +66,10 @@ class BottomGalleryBar extends ConsumerWidget {
final isInAlbum = ref.watch(currentAlbumProvider)?.isRemote ?? false;
void removeAssetFromStack() {
if (stackIndex > 0 && showStack) {
if (stackIndex.value > 0 && showStack && stackId != null) {
ref
.read(assetStackStateProvider(asset).notifier)
.removeChild(stackIndex - 1);
.read(assetStackStateProvider(stackId).notifier)
.removeChild(stackIndex.value - 1);
}
}
@ -135,7 +137,7 @@ class BottomGalleryBar extends ConsumerWidget {
await ref
.read(stackServiceProvider)
.deleteStack(asset.stackId!, [asset, ...stackItems]);
.deleteStack(asset.stackId!, stackItems);
}
void showStackActionItems() {
@ -324,16 +326,16 @@ class BottomGalleryBar extends ConsumerWidget {
},
];
return IgnorePointer(
ignoring: !ref.watch(showControlsProvider),
ignoring: !showControls,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 100),
opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
opacity: showControls ? 1.0 : 0.0,
child: DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [blackOpacity90, Colors.transparent],
colors: [Colors.black, Colors.transparent],
),
),
position: DecorationPosition.background,
@ -341,7 +343,7 @@ class BottomGalleryBar extends ConsumerWidget {
padding: const EdgeInsets.only(top: 40.0),
child: Column(
children: [
if (showVideoPlayerControls) const VideoControls(),
if (asset.isVideo) const VideoControls(),
BottomNavigationBar(
elevation: 0.0,
backgroundColor: Colors.transparent,

View file

@ -1,38 +1,48 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/video_player_controls_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
import 'package:immich_mobile/utils/hooks/timer_hook.dart';
import 'package:immich_mobile/widgets/asset_viewer/center_play_button.dart';
import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart';
import 'package:immich_mobile/utils/hooks/timer_hook.dart';
class CustomVideoPlayerControls extends HookConsumerWidget {
final Duration hideTimerDuration;
const CustomVideoPlayerControls({
super.key,
this.hideTimerDuration = const Duration(seconds: 3),
this.hideTimerDuration = const Duration(seconds: 5),
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final assetIsVideo = ref.watch(
currentAssetProvider.select((asset) => asset != null && asset.isVideo),
);
final showControls = ref.watch(showControlsProvider);
final VideoPlaybackState state =
ref.watch(videoPlaybackValueProvider.select((value) => value.state));
// A timer to hide the controls
final hideTimer = useTimer(
hideTimerDuration,
() {
if (!context.mounted) {
return;
}
final state = ref.read(videoPlaybackValueProvider).state;
// Do not hide on paused
if (state != VideoPlaybackState.paused) {
if (state != VideoPlaybackState.paused &&
state != VideoPlaybackState.completed &&
assetIsVideo) {
ref.read(showControlsProvider.notifier).show = false;
}
},
);
final showBuffering = useState(false);
final VideoPlaybackState state =
ref.watch(videoPlaybackValueProvider).state;
final showBuffering = state == VideoPlaybackState.buffering;
/// Shows the controls and starts the timer to hide them
void showControlsAndStartHideTimer() {
@ -40,28 +50,15 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
ref.read(showControlsProvider.notifier).show = true;
}
// When we mute, show the controls
ref.listen(videoPlayerControlsProvider.select((v) => v.mute),
(previous, next) {
showControlsAndStartHideTimer();
});
// When we change position, show or hide timer
ref.listen(videoPlayerControlsProvider.select((v) => v.position),
(previous, next) {
showControlsAndStartHideTimer();
});
ref.listen(videoPlaybackValueProvider.select((value) => value.state),
(_, state) {
// Show buffering
showBuffering.value = state == VideoPlaybackState.buffering;
});
/// Toggles between playing and pausing depending on the state of the video
void togglePlay() {
showControlsAndStartHideTimer();
final state = ref.read(videoPlaybackValueProvider).state;
if (state == VideoPlaybackState.playing) {
ref.read(videoPlayerControlsProvider.notifier).pause();
} else if (state == VideoPlaybackState.completed) {
@ -75,10 +72,10 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
behavior: HitTestBehavior.opaque,
onTap: showControlsAndStartHideTimer,
child: AbsorbPointer(
absorbing: !ref.watch(showControlsProvider),
absorbing: !showControls,
child: Stack(
children: [
if (showBuffering.value)
if (showBuffering)
const Center(
child: DelayedLoadingIndicator(
fadeInDuration: Duration(milliseconds: 400),
@ -86,18 +83,14 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
)
else
GestureDetector(
onTap: () {
if (state != VideoPlaybackState.playing) {
togglePlay();
}
ref.read(showControlsProvider.notifier).show = false;
},
onTap: () =>
ref.read(showControlsProvider.notifier).show = false,
child: CenterPlayButton(
backgroundColor: Colors.black54,
iconColor: Colors.white,
isFinished: state == VideoPlaybackState.completed,
isPlaying: state == VideoPlaybackState.playing,
show: ref.watch(showControlsProvider),
show: assetIsVideo && showControls,
onPressed: togglePlay,
),
),

View file

@ -15,9 +15,10 @@ class FileInfo extends StatelessWidget {
Widget build(BuildContext context) {
final textColor = context.isDarkTheme ? Colors.white : Colors.black;
String resolution = asset.width != null && asset.height != null
? "${asset.height} x ${asset.width} "
: "";
final height = asset.orientatedHeight ?? asset.height;
final width = asset.orientatedWidth ?? asset.width;
String resolution =
height != null && width != null ? "$height x $width " : "";
String fileSize = asset.exifInfo?.fileSize != null
? formatBytes(asset.exifInfo!.fileSize!)
: "";

View file

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart';
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
@ -19,23 +20,19 @@ import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class GalleryAppBar extends ConsumerWidget {
final Asset asset;
final void Function() showInfo;
final void Function() onToggleMotionVideo;
final bool isPlayingVideo;
const GalleryAppBar({
super.key,
required this.asset,
required this.showInfo,
required this.onToggleMotionVideo,
required this.isPlayingVideo,
});
const GalleryAppBar({super.key, required this.showInfo});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asset = ref.watch(currentAssetProvider);
if (asset == null) {
return const SizedBox();
}
final album = ref.watch(currentAlbumProvider);
final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId;
final showControls = ref.watch(showControlsProvider);
final isPartner = ref
.watch(partnerSharedWithProvider)
@ -98,23 +95,21 @@ class GalleryAppBar extends ConsumerWidget {
}
return IgnorePointer(
ignoring: !ref.watch(showControlsProvider),
ignoring: !showControls,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 100),
opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
opacity: showControls ? 1.0 : 0.0,
child: Container(
color: Colors.black.withOpacity(0.4),
child: TopControlAppBar(
isOwner: isOwner,
isPartner: isPartner,
isPlayingMotionVideo: isPlayingVideo,
asset: asset,
onMoreInfoPressed: showInfo,
onFavorite: toggleFavorite,
onRestorePressed: () => handleRestore(asset),
onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null,
onDownloadPressed: asset.isLocal ? null : handleDownloadAsset,
onToggleMotionVideo: onToggleMotionVideo,
onAddToAlbumPressed: () => addToAlbum(asset),
onActivitiesPressed: handleActivities,
),

View file

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
class MotionPhotoButton extends ConsumerWidget {
const MotionPhotoButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isPlaying = ref.watch(isPlayingMotionVideoProvider);
return IconButton(
onPressed: () {
ref.read(isPlayingMotionVideoProvider.notifier).toggle();
},
icon: isPlaying
? const Icon(Icons.motion_photos_pause_outlined, color: grey200)
: const Icon(Icons.play_circle_outline_rounded, color: grey200),
);
}
}

View file

@ -5,6 +5,7 @@ import 'package:immich_mobile/providers/activity_statistics.provider.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart';
class TopControlAppBar extends HookConsumerWidget {
const TopControlAppBar({
@ -14,8 +15,6 @@ class TopControlAppBar extends HookConsumerWidget {
required this.onDownloadPressed,
required this.onAddToAlbumPressed,
required this.onRestorePressed,
required this.onToggleMotionVideo,
required this.isPlayingMotionVideo,
required this.onFavorite,
required this.onUploadPressed,
required this.isOwner,
@ -27,12 +26,10 @@ class TopControlAppBar extends HookConsumerWidget {
final Function onMoreInfoPressed;
final VoidCallback? onUploadPressed;
final VoidCallback? onDownloadPressed;
final VoidCallback onToggleMotionVideo;
final VoidCallback onAddToAlbumPressed;
final VoidCallback onRestorePressed;
final VoidCallback onActivitiesPressed;
final Function(Asset) onFavorite;
final bool isPlayingMotionVideo;
final bool isOwner;
final bool isPartner;
@ -57,23 +54,6 @@ class TopControlAppBar extends HookConsumerWidget {
);
}
Widget buildLivePhotoButton() {
return IconButton(
onPressed: () {
onToggleMotionVideo();
},
icon: isPlayingMotionVideo
? Icon(
Icons.motion_photos_pause_outlined,
color: Colors.grey[200],
)
: Icon(
Icons.play_circle_outline_rounded,
color: Colors.grey[200],
),
);
}
Widget buildMoreInfoButton() {
return IconButton(
onPressed: () {
@ -175,13 +155,11 @@ class TopControlAppBar extends HookConsumerWidget {
foregroundColor: Colors.grey[100],
backgroundColor: Colors.transparent,
leading: buildBackButton(),
actionsIconTheme: const IconThemeData(
size: iconSize,
),
actionsIconTheme: const IconThemeData(size: iconSize),
shape: const Border(),
actions: [
if (asset.isRemote && isOwner) buildFavoriteButton(a),
if (asset.livePhotoVideoId != null) buildLivePhotoButton(),
if (asset.livePhotoVideoId != null) const MotionPhotoButton(),
if (asset.isLocal && !asset.isRemote) buildUploadButton(),
if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
if (asset.isRemote && (isOwner || isPartner) && !asset.isTrashed)

View file

@ -1,48 +0,0 @@
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/utils/hooks/chewiew_controller_hook.dart';
import 'package:immich_mobile/widgets/asset_viewer/custom_video_player_controls.dart';
import 'package:video_player/video_player.dart';
class VideoPlayerViewer extends HookConsumerWidget {
final VideoPlayerController controller;
final bool isMotionVideo;
final Widget? placeholder;
final Duration hideControlsTimer;
final bool showControls;
final bool showDownloadingIndicator;
final bool loopVideo;
const VideoPlayerViewer({
super.key,
required this.controller,
required this.isMotionVideo,
this.placeholder,
required this.hideControlsTimer,
required this.showControls,
required this.showDownloadingIndicator,
required this.loopVideo,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final chewie = useChewieController(
controller: controller,
controlsSafeAreaMinimum: const EdgeInsets.only(
bottom: 100,
),
placeholder: SizedBox.expand(child: placeholder),
customControls: CustomVideoPlayerControls(
hideTimerDuration: hideControlsTimer,
),
showControls: showControls && !isMotionVideo,
hideControlsTimer: hideControlsTimer,
loopVideo: loopVideo,
);
return Chewie(
controller: chewie,
);
}
}

View file

@ -56,10 +56,16 @@ class VideoPosition extends HookConsumerWidget {
ref.read(videoPlayerControlsProvider.notifier).play();
}
},
onChanged: (position) {
onChanged: (value) {
final inSeconds =
(duration * (value / 100.0)).inSeconds;
final position = inSeconds.toDouble();
ref
.read(videoPlayerControlsProvider.notifier)
.position = position;
// This immediately updates the slider position without waiting for the video to update
ref.read(videoPlaybackValueProvider.notifier).position =
Duration(seconds: inSeconds);
},
),
),

View file

@ -28,12 +28,11 @@ class ImmichImage extends StatelessWidget {
// either by using the asset ID or the asset itself
/// [asset] is the Asset to request, or else use [assetId] to get a remote
/// image provider
/// Use [isThumbnail] and [thumbnailSize] if you'd like to request a thumbnail
/// The size of the square thumbnail to request. Ignored if isThumbnail
/// is not true
static ImageProvider imageProvider({
Asset? asset,
String? assetId,
double width = 1080,
double height = 1920,
}) {
if (asset == null && assetId == null) {
throw Exception('Must supply either asset or assetId');
@ -48,6 +47,8 @@ class ImmichImage extends StatelessWidget {
if (useLocal(asset)) {
return ImmichLocalImageProvider(
asset: asset,
width: width,
height: height,
);
} else {
return ImmichRemoteImageProvider(
@ -87,6 +88,8 @@ class ImmichImage extends StatelessWidget {
},
image: ImmichImage.imageProvider(
asset: asset,
width: context.width,
height: context.height,
),
width: width,
height: height,

View file

@ -2,9 +2,9 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/pages/common/video_viewer.page.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
import 'package:immich_mobile/utils/hooks/blurhash_hook.dart';
import 'package:immich_mobile/widgets/common/immich_image.dart';
@ -68,18 +68,20 @@ class MemoryCard extends StatelessWidget {
} else {
return Hero(
tag: 'memory-${asset.id}',
child: VideoViewerPage(
key: ValueKey(asset),
asset: asset,
showDownloadingIndicator: false,
placeholder: SizedBox.expand(
child: ImmichImage(
child: SizedBox(
width: context.width,
height: context.height,
child: NativeVideoViewerPage(
key: ValueKey(asset.id),
asset: asset,
showControls: false,
image: ImmichImage(
asset,
width: context.width,
height: context.height,
fit: fit,
),
),
hideControlsTimer: const Duration(seconds: 2),
showControls: false,
),
);
}
@ -137,6 +139,8 @@ class _BlurredBackdrop extends HookWidget {
image: DecorationImage(
image: ImmichImage.imageProvider(
asset: asset,
height: context.height,
width: context.width,
),
fit: BoxFit.cover,
),