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

This commit is contained in:
martyfuhry 2023-02-08 14:42:45 -05:00 committed by GitHub
parent 18647203cc
commit dc9da7480c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 281 additions and 176 deletions

View File

@ -1,18 +1,19 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:immich_mobile/constants/hive_box.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/shared/models/album.dart';
import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class AlbumThumbnailCard extends StatelessWidget { class AlbumThumbnailCard extends StatelessWidget {
final Function()? onTap;
const AlbumThumbnailCard({ const AlbumThumbnailCard({
Key? key, Key? key,
required this.album, required this.album,
this.onTap,
}) : super(key: key); }) : super(key: key);
final Album album; final Album album;
@ -20,19 +21,22 @@ class AlbumThumbnailCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var box = Hive.box(userInfoBox); var box = Hive.box(userInfoBox);
var cardSize = MediaQuery.of(context).size.width / 2 - 18;
var isDarkMode = Theme.of(context).brightness == Brightness.dark; var isDarkMode = Theme.of(context).brightness == Brightness.dark;
return LayoutBuilder(
builder: (context, constraints) {
var cardSize = constraints.maxWidth;
buildEmptyThumbnail() { buildEmptyThumbnail() {
return Container( return Container(
height: cardSize,
width: cardSize,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200], color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
), ),
child: SizedBox( child: Center(
height: cardSize, child: Icon(
width: cardSize, Icons.no_photography,
child: const Center( size: cardSize * .15,
child: Icon(Icons.no_photography),
), ),
), ),
); );
@ -54,20 +58,20 @@ class AlbumThumbnailCard extends StatelessWidget {
} }
return GestureDetector( return GestureDetector(
onTap: () { onTap: onTap,
AutoRouter.of(context).push(AlbumViewerRoute(albumId: album.id));
},
child: Padding( child: Padding(
padding: const EdgeInsets.only(bottom: 32.0), padding: const EdgeInsets.only(bottom: 32.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ClipRRect( Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: album.albumThumbnailAssetId == null child: album.albumThumbnailAssetId == null
? buildEmptyThumbnail() ? buildEmptyThumbnail()
: buildAlbumThumbnail(), : buildAlbumThumbnail(),
), ),
),
Padding( Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: SizedBox( child: SizedBox(
@ -104,5 +108,7 @@ class AlbumThumbnailCard extends StatelessWidget {
), ),
), ),
); );
},
);
} }
} }

View File

@ -112,13 +112,14 @@ class LibraryPage extends HookConsumerWidget {
onTap: () { onTap: () {
AutoRouter.of(context).push(CreateAlbumRoute(isSharedAlbum: false)); AutoRouter.of(context).push(CreateAlbumRoute(isSharedAlbum: false));
}, },
child: Padding(
padding: const EdgeInsets.only(bottom: 32),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Expanded(
width: MediaQuery.of(context).size.width / 2 - 18, child: Container(
height: MediaQuery.of(context).size.width / 2 - 18,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: Colors.grey, color: Colors.grey,
@ -133,17 +134,22 @@ class LibraryPage extends HookConsumerWidget {
), ),
), ),
), ),
),
Padding( Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(
top: 8.0,
bottom: 16,
),
child: const Text( child: const Text(
'library_page_new_album', 'library_page_new_album',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
).tr(), ).tr(),
) ),
], ],
), ),
),
); );
} }
@ -185,6 +191,8 @@ class LibraryPage extends HookConsumerWidget {
); );
} }
final sorted = sortedAlbums();
return Scaffold( return Scaffold(
appBar: buildAppBar(), appBar: buildAppBar(),
body: CustomScrollView( body: CustomScrollView(
@ -234,20 +242,33 @@ class LibraryPage extends HookConsumerWidget {
), ),
), ),
SliverPadding( SliverPadding(
padding: const EdgeInsets.only(left: 12.0, right: 12, bottom: 50), padding: const EdgeInsets.all(12.0),
sliver: SliverToBoxAdapter( sliver: SliverGrid(
child: Wrap( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
spacing: 12, maxCrossAxisExtent: 250,
children: [ mainAxisSpacing: 12,
buildCreateAlbumButton(), crossAxisSpacing: 12,
for (var album in sortedAlbums()) childAspectRatio: .7,
AlbumThumbnailCard( ),
album: album, 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,
),
),
);
},
), ),
],
), ),
), ),
)
], ],
), ),
); );

View File

@ -66,11 +66,6 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null; 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( Widget _buildThumbnailOrPlaceholder(
Asset asset, Asset asset,
bool placeholder, bool placeholder,
@ -97,8 +92,11 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
RenderAssetGridRow row, RenderAssetGridRow row,
bool scrolling, bool scrolling,
) { ) {
double size = _getItemSize(context);
return LayoutBuilder(
builder: (context, constraints) {
final size = constraints.maxWidth / widget.assetsPerRow -
widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
return Row( return Row(
key: Key("asset-row-${row.assets.first.id}"), key: Key("asset-row-${row.assets.first.id}"),
children: row.assets.map((Asset asset) { children: row.assets.map((Asset asset) {
@ -116,6 +114,8 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
); );
}).toList(), }).toList(),
); );
},
);
} }
Widget _buildTitle( Widget _buildTitle(

View File

@ -11,35 +11,58 @@ class TabControllerPage extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final multiselectEnabled = ref.watch(multiselectProvider);
return AutoTabsRouter( navigationRail(TabsRouter tabsRouter) {
routes: [ return NavigationRail(
const HomeRoute(), labelType: NavigationRailLabelType.all,
SearchRoute(), selectedIndex: tabsRouter.activeIndex,
const SharingRoute(), onDestinationSelected: (index) {
const LibraryRoute() HapticFeedback.selectionClick();
], tabsRouter.setActiveIndex(index);
builder: (context, child, animation) {
final tabsRouter = AutoTabsRouter.of(context);
final appRouter = AutoRouter.of(context);
return WillPopScope(
onWillPop: () async {
bool atHomeTab = tabsRouter.activeIndex == 0;
if (!atHomeTab) {
tabsRouter.setActiveIndex(0);
} else {
appRouter.navigateBack();
}
return atHomeTab;
}, },
child: Scaffold( selectedIconTheme: IconThemeData(
body: FadeTransition( color: Theme.of(context).primaryColor,
opacity: animation,
child: child,
), ),
bottomNavigationBar: multiselectEnabled selectedLabelTextStyle: TextStyle(
? null color: Theme.of(context).primaryColor,
: BottomNavigationBar( ),
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( selectedLabelStyle: const TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -75,8 +98,63 @@ class TabControllerPage extends ConsumerWidget {
activeIcon: const Icon(Icons.photo_album_rounded), activeIcon: const Icon(Icons.photo_album_rounded),
) )
], ],
);
}
final multiselectEnabled = ref.watch(multiselectProvider);
return AutoTabsRouter(
routes: [
const HomeRoute(),
SearchRoute(),
const SharingRoute(),
const LibraryRoute()
],
builder: (context, child, animation) {
final tabsRouter = AutoTabsRouter.of(context);
final appRouter = AutoRouter.of(context);
return WillPopScope(
onWillPop: () async {
bool atHomeTab = tabsRouter.activeIndex == 0;
if (!atHomeTab) {
tabsRouter.setActiveIndex(0);
} else {
appRouter.navigateBack();
}
return atHomeTab;
},
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,
), ),
), ),
],
);
} return Scaffold(
body: body,
bottomNavigationBar: multiselectEnabled
? null
: bottom,
);
},),
); );
}, },
); );