浏览代码

feat(mobile): Transparent bottom Android navigation bar (#1953)

* transparent system overlay

* immersive view to gallery viewer, as well

* comments
martyfuhry 2 年之前
父节点
当前提交
950989a85e
共有 2 个文件被更改,包括 149 次插入132 次删除
  1. 3 0
      mobile/lib/main.dart
  2. 146 132
      mobile/lib/modules/asset_viewer/views/gallery_viewer.dart

+ 3 - 0
mobile/lib/main.dart

@@ -211,6 +211,9 @@ class ImmichAppState extends ConsumerState<ImmichApp>
     ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
     ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
 
 
     SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
     SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
+    SystemChrome.setSystemUIOverlayStyle(
+      const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent),
+    );
 
 
     return MaterialApp(
     return MaterialApp(
       localizationsDelegates: context.localizationDelegates,
       localizationsDelegates: context.localizationDelegates,

+ 146 - 132
mobile/lib/modules/asset_viewer/views/gallery_viewer.dart

@@ -247,6 +247,13 @@ class GalleryViewerPage extends HookConsumerWidget {
               (showAppBar.value && !isZoomed.value)) &&
               (showAppBar.value && !isZoomed.value)) &&
           !isPlayingVideo.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(
       return AnimatedOpacity(
         duration: const Duration(milliseconds: 100),
         duration: const Duration(milliseconds: 100),
         opacity: show ? 1.0 : 0.0,
         opacity: show ? 1.0 : 0.0,
@@ -291,145 +298,152 @@ class GalleryViewerPage extends HookConsumerWidget {
 
 
     return Scaffold(
     return Scaffold(
       backgroundColor: Colors.black,
       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 {
                     } 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]);
+                  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 {
                 } 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.customChild(
+                    onDragStart: (_, details, __) =>
+                        localPosition = details.localPosition,
+                    onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
+                    heroAttributes: PhotoViewHeroAttributes(
+                      tag: assetList[index].id,
                     ),
                     ),
-                  ),
-                );
-              }
-            },
-          ),
-          Positioned(
-            top: 0,
-            left: 0,
-            right: 0,
-            child: buildAppBar(),
-          ),
-        ],
+                    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(),
+            ),
+          ],
+        ),
       ),
       ),
     );
     );
   }
   }