Преглед изворни кода

feat(mobile): Responsive layout improvements with a navigation rail and album grid (#1583)

martyfuhry пре 2 година
родитељ
комит
dc9da7480c

+ 78 - 72
mobile/lib/modules/album/ui/album_thumbnail_card.dart

@@ -1,18 +1,19 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:cached_network_image/cached_network_image.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hive/hive.dart';
 import 'package:immich_mobile/constants/hive_box.dart';
-import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/utils/image_url_builder.dart';
 import 'package:openapi/api.dart';
 
 class AlbumThumbnailCard extends StatelessWidget {
+  final Function()? onTap;
+
   const AlbumThumbnailCard({
     Key? key,
     required this.album,
+    this.onTap,
   }) : super(key: key);
 
   final Album album;
@@ -20,89 +21,94 @@ class AlbumThumbnailCard extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     var box = Hive.box(userInfoBox);
-    var cardSize = MediaQuery.of(context).size.width / 2 - 18;
     var isDarkMode = Theme.of(context).brightness == Brightness.dark;
+    return LayoutBuilder(
+      builder: (context, constraints) {
+      var cardSize = constraints.maxWidth;
 
-    buildEmptyThumbnail() {
-      return Container(
-        decoration: BoxDecoration(
-          color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
-        ),
-        child: SizedBox(
+      buildEmptyThumbnail() {
+        return Container(
           height: cardSize,
           width: cardSize,
-          child: const Center(
-            child: Icon(Icons.no_photography),
+          decoration: BoxDecoration(
+            color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
           ),
-        ),
-      );
-    }
+          child: Center(
+            child: Icon(
+              Icons.no_photography,
+              size: cardSize * .15,
+            ),
+          ),
+        );
+      }
 
-    buildAlbumThumbnail() {
-      return CachedNetworkImage(
-        width: cardSize,
-        height: cardSize,
-        fit: BoxFit.cover,
-        fadeInDuration: const Duration(milliseconds: 200),
-        imageUrl: getAlbumThumbnailUrl(
-          album,
-          type: ThumbnailFormat.JPEG,
-        ),
-        httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
-        cacheKey: getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.JPEG),
-      );
-    }
+      buildAlbumThumbnail() {
+        return CachedNetworkImage(
+          width: cardSize,
+          height: cardSize,
+          fit: BoxFit.cover,
+          fadeInDuration: const Duration(milliseconds: 200),
+          imageUrl: getAlbumThumbnailUrl(
+            album,
+            type: ThumbnailFormat.JPEG,
+          ),
+          httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
+          cacheKey: getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.JPEG),
+        );
+      }
 
-    return GestureDetector(
-      onTap: () {
-        AutoRouter.of(context).push(AlbumViewerRoute(albumId: album.id));
-      },
-      child: Padding(
-        padding: const EdgeInsets.only(bottom: 32.0),
-        child: Column(
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            ClipRRect(
-              borderRadius: BorderRadius.circular(8),
-              child: album.albumThumbnailAssetId == null
-                  ? buildEmptyThumbnail()
-                  : buildAlbumThumbnail(),
-            ),
-            Padding(
-              padding: const EdgeInsets.only(top: 8.0),
-              child: SizedBox(
-                width: cardSize,
-                child: Text(
-                  album.name,
-                  style: const TextStyle(
-                    fontWeight: FontWeight.bold,
-                  ),
+      return GestureDetector(
+        onTap: onTap,
+        child: Padding(
+          padding: const EdgeInsets.only(bottom: 32.0),
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Expanded(
+                child: ClipRRect(
+                  borderRadius: BorderRadius.circular(8),
+                  child: album.albumThumbnailAssetId == null
+                    ? buildEmptyThumbnail()
+                    : buildAlbumThumbnail(),
                 ),
               ),
-            ),
-            Row(
-              mainAxisSize: MainAxisSize.min,
-              children: [
-                Text(
-                  album.assetCount == 1
-                      ? 'album_thumbnail_card_item'
-                      : 'album_thumbnail_card_items',
-                  style: const TextStyle(
-                    fontSize: 12,
+              Padding(
+                padding: const EdgeInsets.only(top: 8.0),
+                child: SizedBox(
+                  width: cardSize,
+                  child: Text(
+                    album.name,
+                    style: const TextStyle(
+                      fontWeight: FontWeight.bold,
+                    ),
                   ),
-                ).tr(args: ['${album.assetCount}']),
-                if (album.shared)
-                  const Text(
-                    'album_thumbnail_card_shared',
-                    style: TextStyle(
+                ),
+              ),
+              Row(
+                mainAxisSize: MainAxisSize.min,
+                children: [
+                  Text(
+                    album.assetCount == 1
+                        ? 'album_thumbnail_card_item'
+                        : 'album_thumbnail_card_items',
+                    style: const TextStyle(
                       fontSize: 12,
                     ),
-                  ).tr()
-              ],
-            )
-          ],
+                  ).tr(args: ['${album.assetCount}']),
+                  if (album.shared)
+                    const Text(
+                      'album_thumbnail_card_shared',
+                      style: TextStyle(
+                        fontSize: 12,
+                      ),
+                    ).tr()
+                ],
+              )
+            ],
+          ),
         ),
-      ),
+      );
+      },
     );
   }
 }

+ 59 - 38
mobile/lib/modules/album/views/library_page.dart

@@ -112,37 +112,43 @@ class LibraryPage extends HookConsumerWidget {
         onTap: () {
           AutoRouter.of(context).push(CreateAlbumRoute(isSharedAlbum: false));
         },
-        child: Column(
-          mainAxisAlignment: MainAxisAlignment.start,
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            Container(
-              width: MediaQuery.of(context).size.width / 2 - 18,
-              height: MediaQuery.of(context).size.width / 2 - 18,
-              decoration: BoxDecoration(
-                border: Border.all(
-                  color: Colors.grey,
+        child: Padding(
+          padding: const EdgeInsets.only(bottom: 32),
+          child: Column(
+            mainAxisAlignment: MainAxisAlignment.start,
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Expanded(
+                child: Container(
+                  decoration: BoxDecoration(
+                    border: Border.all(
+                      color: Colors.grey,
+                    ),
+                    borderRadius: BorderRadius.circular(8),
+                  ),
+                  child: Center(
+                    child: Icon(
+                      Icons.add_rounded,
+                      size: 28,
+                      color: Theme.of(context).primaryColor,
+                    ),
+                  ),
                 ),
-                borderRadius: BorderRadius.circular(8),
               ),
-              child: Center(
-                child: Icon(
-                  Icons.add_rounded,
-                  size: 28,
-                  color: Theme.of(context).primaryColor,
+              Padding(
+                padding: const EdgeInsets.only(
+                  top: 8.0,
+                  bottom: 16,
                 ),
+                child: const Text(
+                  'library_page_new_album',
+                  style: TextStyle(
+                    fontWeight: FontWeight.bold,
+                  ),
+                ).tr(),
               ),
-            ),
-            Padding(
-              padding: const EdgeInsets.only(top: 8.0),
-              child: const Text(
-                'library_page_new_album',
-                style: TextStyle(
-                  fontWeight: FontWeight.bold,
-                ),
-              ).tr(),
-            )
-          ],
+            ],
+          ),
         ),
       );
     }
@@ -185,6 +191,8 @@ class LibraryPage extends HookConsumerWidget {
       );
     }
 
+    final sorted = sortedAlbums();
+
     return Scaffold(
       appBar: buildAppBar(),
       body: CustomScrollView(
@@ -234,20 +242,33 @@ class LibraryPage extends HookConsumerWidget {
             ),
           ),
           SliverPadding(
-            padding: const EdgeInsets.only(left: 12.0, right: 12, bottom: 50),
-            sliver: SliverToBoxAdapter(
-              child: Wrap(
-                spacing: 12,
-                children: [
-                  buildCreateAlbumButton(),
-                  for (var album in sortedAlbums())
-                    AlbumThumbnailCard(
-                      album: album,
+            padding: const EdgeInsets.all(12.0),
+            sliver: SliverGrid(
+              gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
+                maxCrossAxisExtent: 250,
+                mainAxisSpacing: 12,
+                crossAxisSpacing: 12,
+                childAspectRatio: .7,
+              ),
+              delegate: SliverChildBuilderDelegate(
+                childCount: sorted.length + 1,
+                (context, index) {
+                  if (index  == 0) {
+                    return buildCreateAlbumButton();
+                  }
+
+                  return AlbumThumbnailCard(
+                    album: sorted[index - 1],
+                    onTap: () => AutoRouter.of(context).push(
+                      AlbumViewerRoute(
+                        albumId: sorted[index - 1].id,
+                      ),
                     ),
-                ],
+                  );
+                },
               ),
             ),
-          )
+          ),
         ],
       ),
     );

+ 22 - 22
mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart

@@ -66,11 +66,6 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
         assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
   }
 
-  double _getItemSize(BuildContext context) {
-    return MediaQuery.of(context).size.width / widget.assetsPerRow -
-        widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
-  }
-
   Widget _buildThumbnailOrPlaceholder(
     Asset asset,
     bool placeholder,
@@ -97,24 +92,29 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
     RenderAssetGridRow row,
     bool scrolling,
   ) {
-    double size = _getItemSize(context);
-
-    return Row(
-      key: Key("asset-row-${row.assets.first.id}"),
-      children: row.assets.map((Asset asset) {
-        bool last = asset.id == row.assets.last.id;
-
-        return Container(
-          key: Key("asset-${asset.id}"),
-          width: size,
-          height: size,
-          margin: EdgeInsets.only(
-            top: widget.margin,
-            right: last ? 0.0 : widget.margin,
-          ),
-          child: _buildThumbnailOrPlaceholder(asset, scrolling),
+
+    return LayoutBuilder(
+      builder: (context, constraints) {
+        final size = constraints.maxWidth / widget.assetsPerRow -
+          widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
+        return Row(
+          key: Key("asset-row-${row.assets.first.id}"),
+          children: row.assets.map((Asset asset) {
+            bool last = asset.id == row.assets.last.id;
+
+            return Container(
+              key: Key("asset-${asset.id}"),
+              width: size,
+              height: size,
+              margin: EdgeInsets.only(
+                top: widget.margin,
+                right: last ? 0.0 : widget.margin,
+              ),
+              child: _buildThumbnailOrPlaceholder(asset, scrolling),
+            );
+          }).toList(),
         );
-      }).toList(),
+      },
     );
   }
 

+ 122 - 44
mobile/lib/shared/views/tab_controller_page.dart

@@ -11,6 +11,96 @@ class TabControllerPage extends ConsumerWidget {
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
+
+    navigationRail(TabsRouter tabsRouter) {
+      return NavigationRail(
+        labelType: NavigationRailLabelType.all,
+        selectedIndex: tabsRouter.activeIndex,
+        onDestinationSelected: (index) {
+          HapticFeedback.selectionClick();
+          tabsRouter.setActiveIndex(index);
+        },
+        selectedIconTheme: IconThemeData(
+          color: Theme.of(context).primaryColor,
+        ),
+        selectedLabelTextStyle: TextStyle(
+          color: Theme.of(context).primaryColor,
+        ),
+        useIndicator: false,
+        destinations: [
+          NavigationRailDestination(
+            padding: EdgeInsets.only(
+              top: MediaQuery.of(context).padding.top + 4,
+              left: 4,
+              right: 4,
+              bottom: 4,
+            ),
+            icon: const Icon(Icons.photo_outlined), 
+            selectedIcon: const Icon(Icons.photo),
+            label: const Text('tab_controller_nav_photos').tr(),
+          ),
+          NavigationRailDestination(
+            padding: const EdgeInsets.all(4),
+            icon: const Icon(Icons.search_rounded), 
+            selectedIcon: const Icon(Icons.search), 
+            label: const Text('tab_controller_nav_search').tr(),
+          ),
+          NavigationRailDestination(
+            padding: const EdgeInsets.all(4),
+            icon: const Icon(Icons.share_rounded), 
+            selectedIcon: const Icon(Icons.share), 
+            label: const Text('tab_controller_nav_sharing').tr(),
+          ),
+          NavigationRailDestination(
+            padding: const EdgeInsets.all(4),
+            icon: const Icon(Icons.photo_album_outlined), 
+            selectedIcon: const Icon(Icons.photo_album), 
+            label: const Text('tab_controller_nav_library').tr(),
+          ),
+        ],
+      );
+    }
+
+    bottomNavigationBar(TabsRouter tabsRouter) {
+      return BottomNavigationBar(
+        selectedLabelStyle: const TextStyle(
+          fontSize: 13,
+          fontWeight: FontWeight.w600,
+        ),
+        unselectedLabelStyle: const TextStyle(
+          fontSize: 13,
+          fontWeight: FontWeight.w600,
+        ),
+        currentIndex: tabsRouter.activeIndex,
+        onTap: (index) {
+          HapticFeedback.selectionClick();
+          tabsRouter.setActiveIndex(index);
+        },
+        items: [
+          BottomNavigationBarItem(
+            label: 'tab_controller_nav_photos'.tr(),
+            icon: const Icon(Icons.photo_outlined),
+            activeIcon: const Icon(Icons.photo),
+          ),
+          BottomNavigationBarItem(
+            label: 'tab_controller_nav_search'.tr(),
+            icon: const Icon(Icons.search_rounded),
+            activeIcon: const Icon(Icons.search),
+          ),
+          BottomNavigationBarItem(
+            label: 'tab_controller_nav_sharing'.tr(),
+            icon: const Icon(Icons.group_outlined),
+            activeIcon: const Icon(Icons.group),
+          ),
+          BottomNavigationBarItem(
+            label: 'tab_controller_nav_library'.tr(),
+            icon: const Icon(Icons.photo_album_outlined),
+            activeIcon: const Icon(Icons.photo_album_rounded),
+          )
+        ],
+      );
+    }
+
     final multiselectEnabled = ref.watch(multiselectProvider);
     return AutoTabsRouter(
       routes: [
@@ -32,51 +122,39 @@ class TabControllerPage extends ConsumerWidget {
             }
             return atHomeTab;
           },
-          child: Scaffold(
-            body: FadeTransition(
-              opacity: animation,
-              child: child,
-            ),
-            bottomNavigationBar: multiselectEnabled
-                ? null
-                : BottomNavigationBar(
-                    selectedLabelStyle: const TextStyle(
-                      fontSize: 13,
-                      fontWeight: FontWeight.w600,
-                    ),
-                    unselectedLabelStyle: const TextStyle(
-                      fontSize: 13,
-                      fontWeight: FontWeight.w600,
-                    ),
-                    currentIndex: tabsRouter.activeIndex,
-                    onTap: (index) {
-                      HapticFeedback.selectionClick();
-                      tabsRouter.setActiveIndex(index);
-                    },
-                    items: [
-                      BottomNavigationBarItem(
-                        label: 'tab_controller_nav_photos'.tr(),
-                        icon: const Icon(Icons.photo_outlined),
-                        activeIcon: const Icon(Icons.photo),
+          child: LayoutBuilder(
+            builder: (context, constraints) {
+              const medium = 600;
+              final Widget? bottom;
+              final Widget body;
+              if (constraints.maxWidth < medium) {
+                // Normal phone width
+                bottom = bottomNavigationBar(tabsRouter);
+                body = FadeTransition(
+                  opacity: animation,
+                  child: child,
+                );
+              } else {
+                // Medium tablet width
+                bottom = null;
+                body = Row(
+                  children: [
+                    navigationRail(tabsRouter),
+                    Expanded(
+                      child: FadeTransition(
+                        opacity: animation,
+                        child: child,
                       ),
-                      BottomNavigationBarItem(
-                        label: 'tab_controller_nav_search'.tr(),
-                        icon: const Icon(Icons.search_rounded),
-                        activeIcon: const Icon(Icons.search),
-                      ),
-                      BottomNavigationBarItem(
-                        label: 'tab_controller_nav_sharing'.tr(),
-                        icon: const Icon(Icons.group_outlined),
-                        activeIcon: const Icon(Icons.group),
-                      ),
-                      BottomNavigationBarItem(
-                        label: 'tab_controller_nav_library'.tr(),
-                        icon: const Icon(Icons.photo_album_outlined),
-                        activeIcon: const Icon(Icons.photo_album_rounded),
-                      )
-                    ],
-                  ),
-          ),
+                    ),
+                  ],
+                );
+              }              return Scaffold(
+               body: body,
+               bottomNavigationBar: multiselectEnabled
+                  ? null
+                  : bottom,
+            );
+          },),
         );
       },
     );