From 950989a85e13e83ada11dfc30d5a3ebedb77da00 Mon Sep 17 00:00:00 2001 From: martyfuhry Date: Sun, 5 Mar 2023 23:51:18 -0500 Subject: [PATCH] feat(mobile): Transparent bottom Android navigation bar (#1953) * transparent system overlay * immersive view to gallery viewer, as well * comments --- mobile/lib/main.dart | 3 + .../asset_viewer/views/gallery_viewer.dart | 280 +++++++++--------- 2 files changed, 150 insertions(+), 133 deletions(-) diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 0b50df1196..d6615b53b5 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -211,6 +211,9 @@ class ImmichAppState extends ConsumerState ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent), + ); return MaterialApp( localizationsDelegates: context.localizationDelegates, diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index 8a9a5e8643..b793323ed1 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -247,6 +247,13 @@ class GalleryViewerPage extends HookConsumerWidget { (showAppBar.value && !isZoomed.value)) && !isPlayingVideo.value; + // Change to and from immersive mode, hiding navigation and app bar + if (show) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } else { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + } + return AnimatedOpacity( duration: const Duration(milliseconds: 100), opacity: show ? 1.0 : 0.0, @@ -291,145 +298,152 @@ class GalleryViewerPage extends HookConsumerWidget { return Scaffold( backgroundColor: Colors.black, - body: Stack( - children: [ - PhotoViewGallery.builder( - scaleStateChangedCallback: (state) { - isZoomed.value = state != PhotoViewScaleState.initial; - showAppBar.value = !isZoomed.value; - }, - pageController: controller, - scrollPhysics: isZoomed.value - ? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in - : (Platform.isIOS - ? const BouncingScrollPhysics() // Use bouncing physics for iOS - : const ClampingScrollPhysics() // Use heavy physics for Android - ), - itemCount: assetList.length, - scrollDirection: Axis.horizontal, - onPageChanged: (value) { - // Precache image - if (indexOfAsset.value < value) { - // Moving forwards, so precache the next asset - precacheNextImage(value + 1); - } else { - // Moving backwards, so precache previous asset - precacheNextImage(value - 1); - } - indexOfAsset.value = value; - HapticFeedback.selectionClick(); - }, - loadingBuilder: isLoadPreview.value - ? (context, event) { - final asset = assetList[indexOfAsset.value]; - if (!asset.isLocal) { - // Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to achieve - // Three-Stage Loading (WEBP -> JPEG -> Original) - final webPThumbnail = CachedNetworkImage( - imageUrl: getThumbnailUrl( - asset, - type: api.ThumbnailFormat.WEBP, - ), - cacheKey: getThumbnailCacheKey( - asset, - type: api.ThumbnailFormat.WEBP, - ), - httpHeaders: {'Authorization': authToken}, - progressIndicatorBuilder: (_, __, ___) => const Center( - child: ImmichLoadingIndicator(), - ), - fadeInDuration: const Duration(milliseconds: 0), - fit: BoxFit.contain, - ); + body: WillPopScope( + onWillPop: () async { + // Change immersive mode back to normal "edgeToEdge" mode + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + return true; + }, + child: Stack( + children: [ + PhotoViewGallery.builder( + scaleStateChangedCallback: (state) { + isZoomed.value = state != PhotoViewScaleState.initial; + showAppBar.value = !isZoomed.value; + }, + pageController: controller, + scrollPhysics: isZoomed.value + ? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in + : (Platform.isIOS + ? const BouncingScrollPhysics() // Use bouncing physics for iOS + : const ClampingScrollPhysics() // Use heavy physics for Android + ), + itemCount: assetList.length, + scrollDirection: Axis.horizontal, + onPageChanged: (value) { + // Precache image + if (indexOfAsset.value < value) { + // Moving forwards, so precache the next asset + precacheNextImage(value + 1); + } else { + // Moving backwards, so precache previous asset + precacheNextImage(value - 1); + } + indexOfAsset.value = value; + HapticFeedback.selectionClick(); + }, + loadingBuilder: isLoadPreview.value + ? (context, event) { + final asset = assetList[indexOfAsset.value]; + if (!asset.isLocal) { + // Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to achieve + // Three-Stage Loading (WEBP -> JPEG -> Original) + final webPThumbnail = CachedNetworkImage( + imageUrl: getThumbnailUrl( + asset, + type: api.ThumbnailFormat.WEBP, + ), + cacheKey: getThumbnailCacheKey( + asset, + type: api.ThumbnailFormat.WEBP, + ), + httpHeaders: {'Authorization': authToken}, + progressIndicatorBuilder: (_, __, ___) => const Center( + child: ImmichLoadingIndicator(), + ), + fadeInDuration: const Duration(milliseconds: 0), + fit: BoxFit.contain, + ); - return CachedNetworkImage( - imageUrl: getThumbnailUrl( - asset, - type: api.ThumbnailFormat.JPEG, - ), - cacheKey: getThumbnailCacheKey( - asset, - type: api.ThumbnailFormat.JPEG, - ), - httpHeaders: {'Authorization': authToken}, - fit: BoxFit.contain, - fadeInDuration: const Duration(milliseconds: 0), - placeholder: (_, __) => webPThumbnail, - ); + return CachedNetworkImage( + imageUrl: getThumbnailUrl( + asset, + type: api.ThumbnailFormat.JPEG, + ), + cacheKey: getThumbnailCacheKey( + asset, + type: api.ThumbnailFormat.JPEG, + ), + httpHeaders: {'Authorization': authToken}, + fit: BoxFit.contain, + fadeInDuration: const Duration(milliseconds: 0), + placeholder: (_, __) => webPThumbnail, + ); + } else { + return Image( + image: localThumbnailImageProvider(asset), + fit: BoxFit.contain, + ); + } + } + : null, + builder: (context, index) { + getAssetExif(); + if (assetList[index].isImage && !isPlayingMotionVideo.value) { + // Show photo + final ImageProvider provider; + if (assetList[index].isLocal) { + provider = localImageProvider(assetList[index]); + } else { + if (isLoadOriginal.value) { + provider = originalImageProvider(assetList[index]); } else { - return Image( - image: localThumbnailImageProvider(asset), - fit: BoxFit.contain, + provider = remoteThumbnailImageProvider( + assetList[index], + api.ThumbnailFormat.JPEG, ); } } - : null, - builder: (context, index) { - getAssetExif(); - if (assetList[index].isImage && !isPlayingMotionVideo.value) { - // Show photo - final ImageProvider provider; - if (assetList[index].isLocal) { - provider = localImageProvider(assetList[index]); - } else { - if (isLoadOriginal.value) { - provider = originalImageProvider(assetList[index]); - } else { - provider = remoteThumbnailImageProvider( - assetList[index], - api.ThumbnailFormat.JPEG, - ); - } - } - return PhotoViewGalleryPageOptions( - onDragStart: (_, details, __) => - localPosition = details.localPosition, - onDragUpdate: (_, details, __) => handleSwipeUpDown(details), - onTapDown: (_, __, ___) => - showAppBar.value = !showAppBar.value, - imageProvider: provider, - heroAttributes: PhotoViewHeroAttributes( - tag: assetList[index].id, - ), - filterQuality: FilterQuality.high, - tightMode: true, - minScale: PhotoViewComputedScale.contained, - ); - } else { - return PhotoViewGalleryPageOptions.customChild( - onDragStart: (_, details, __) => - localPosition = details.localPosition, - onDragUpdate: (_, details, __) => handleSwipeUpDown(details), - heroAttributes: PhotoViewHeroAttributes( - tag: assetList[index].id, - ), - filterQuality: FilterQuality.high, - maxScale: 1.0, - minScale: 1.0, - child: SafeArea( - child: VideoViewerPage( - onPlaying: () => isPlayingVideo.value = true, - onPaused: () => isPlayingVideo.value = false, - asset: assetList[index], - isMotionVideo: isPlayingMotionVideo.value, - onVideoEnded: () { - if (isPlayingMotionVideo.value) { - isPlayingMotionVideo.value = false; - } - }, + return PhotoViewGalleryPageOptions( + onDragStart: (_, details, __) => + localPosition = details.localPosition, + onDragUpdate: (_, details, __) => handleSwipeUpDown(details), + onTapDown: (_, __, ___) => + showAppBar.value = !showAppBar.value, + imageProvider: provider, + heroAttributes: PhotoViewHeroAttributes( + tag: assetList[index].id, ), - ), - ); - } - }, - ), - Positioned( - top: 0, - left: 0, - right: 0, - child: buildAppBar(), - ), - ], + filterQuality: FilterQuality.high, + tightMode: true, + minScale: PhotoViewComputedScale.contained, + ); + } else { + return PhotoViewGalleryPageOptions.customChild( + onDragStart: (_, details, __) => + localPosition = details.localPosition, + onDragUpdate: (_, details, __) => handleSwipeUpDown(details), + heroAttributes: PhotoViewHeroAttributes( + tag: assetList[index].id, + ), + filterQuality: FilterQuality.high, + maxScale: 1.0, + minScale: 1.0, + child: SafeArea( + child: VideoViewerPage( + onPlaying: () => isPlayingVideo.value = true, + onPaused: () => isPlayingVideo.value = false, + asset: assetList[index], + isMotionVideo: isPlayingMotionVideo.value, + onVideoEnded: () { + if (isPlayingMotionVideo.value) { + isPlayingMotionVideo.value = false; + } + }, + ), + ), + ); + } + }, + ), + Positioned( + top: 0, + left: 0, + right: 0, + child: buildAppBar(), + ), + ], + ), ), ); }