mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	Implemented bottom app bar with control buttons for asset's operation (#15)
This commit is contained in:
		| @@ -22,10 +22,10 @@ Loading ~4000 images/videos | ||||
|  | ||||
| # Note | ||||
|  | ||||
| This project is under heavy development, there will be continous functions, features and api changes. | ||||
|  | ||||
| **!! NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS !!** | ||||
|  | ||||
| This project is under heavy development, there will be continous functions, features and api changes. | ||||
|  | ||||
| # Features | ||||
|  | ||||
| [x] Upload assets(videos/images) | ||||
|   | ||||
| @@ -14,7 +14,7 @@ class AssetNotifier extends StateNotifier<List<ImmichAssetGroupByDate>> { | ||||
|   bool isFetching = false; | ||||
|  | ||||
|   // Get All assets | ||||
|   getImmichAssets() async { | ||||
|   getAllAssets() async { | ||||
|     GetAllAssetResponse? res = await _assetService.getAllAsset(); | ||||
|     nextPageKey = res?.nextPageKey; | ||||
|  | ||||
|   | ||||
							
								
								
									
										75
									
								
								mobile/lib/modules/home/ui/control_bottom_app_bar.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								mobile/lib/modules/home/ui/control_bottom_app_bar.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart'; | ||||
|  | ||||
| class ControlBottomAppBar extends StatelessWidget { | ||||
|   const ControlBottomAppBar({Key? key}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Positioned( | ||||
|       bottom: 0, | ||||
|       left: 0, | ||||
|       child: Container( | ||||
|         width: MediaQuery.of(context).size.width, | ||||
|         height: MediaQuery.of(context).size.height * 0.15, | ||||
|         decoration: BoxDecoration( | ||||
|           borderRadius: const BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)), | ||||
|           color: Colors.grey[300]?.withOpacity(0.98), | ||||
|         ), | ||||
|         child: Column( | ||||
|           children: [ | ||||
|             Padding( | ||||
|               padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), | ||||
|               child: Row( | ||||
|                 mainAxisAlignment: MainAxisAlignment.center, | ||||
|                 children: [ | ||||
|                   ControlBoxButton( | ||||
|                     iconData: Icons.delete_forever_rounded, | ||||
|                     label: "Delete", | ||||
|                     onPressed: () { | ||||
|                       showDialog( | ||||
|                         context: context, | ||||
|                         builder: (BuildContext context) { | ||||
|                           return const DeleteDialog(); | ||||
|                         }, | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ) | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ControlBoxButton extends StatelessWidget { | ||||
|   const ControlBoxButton({Key? key, required this.label, required this.iconData, required this.onPressed}) | ||||
|       : super(key: key); | ||||
|  | ||||
|   final String label; | ||||
|   final IconData iconData; | ||||
|   final Function onPressed; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return SizedBox( | ||||
|       width: 60, | ||||
|       child: Column( | ||||
|         mainAxisAlignment: MainAxisAlignment.start, | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           IconButton( | ||||
|             onPressed: () { | ||||
|               onPressed(); | ||||
|             }, | ||||
|             icon: Icon(iconData, size: 30), | ||||
|           ), | ||||
|           Text(label) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -24,6 +24,31 @@ class DailyTitleText extends ConsumerWidget { | ||||
|     var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup; | ||||
|     var selectedItems = ref.watch(homePageStateProvider).selectedItems; | ||||
|  | ||||
|     void _handleTitleIconClick() { | ||||
|       if (isMultiSelectEnable && | ||||
|           selectedDateGroup.contains(dateText) && | ||||
|           selectedDateGroup.length == 1 && | ||||
|           selectedItems.length <= assetGroup.length) { | ||||
|         // Multi select is active - click again on the icon while it is the only active group -> disable multi select | ||||
|         ref.watch(homePageStateProvider.notifier).disableMultiSelect(); | ||||
|       } else if (isMultiSelectEnable && | ||||
|           selectedDateGroup.contains(dateText) && | ||||
|           selectedItems.length != assetGroup.length) { | ||||
|         // Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items | ||||
|         ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText); | ||||
|         ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup); | ||||
|       } else if (isMultiSelectEnable && selectedDateGroup.contains(dateText) && selectedDateGroup.length > 1) { | ||||
|         ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText); | ||||
|         ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup); | ||||
|       } else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) { | ||||
|         ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText); | ||||
|         ref.watch(homePageStateProvider.notifier).addMultipleSelectedItems(assetGroup); | ||||
|       } else { | ||||
|         ref.watch(homePageStateProvider.notifier).enableMultiSelect(assetGroup.toSet()); | ||||
|         ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return SliverToBoxAdapter( | ||||
|       child: Padding( | ||||
|         padding: const EdgeInsets.only(top: 29.0, bottom: 29.0, left: 12.0, right: 12.0), | ||||
| @@ -39,33 +64,16 @@ class DailyTitleText extends ConsumerWidget { | ||||
|             ), | ||||
|             const Spacer(), | ||||
|             GestureDetector( | ||||
|               onTap: () { | ||||
|                 if (isMultiSelectEnable && | ||||
|                     selectedDateGroup.contains(dateText) && | ||||
|                     selectedDateGroup.length == 1 && | ||||
|                     selectedItems.length == assetGroup.length) { | ||||
|                   ref.watch(homePageStateProvider.notifier).disableMultiSelect(); | ||||
|                 } else if (isMultiSelectEnable && | ||||
|                     selectedDateGroup.contains(dateText) && | ||||
|                     selectedItems.length != assetGroup.length) { | ||||
|                   ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText); | ||||
|                   ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup); | ||||
|                 } else if (isMultiSelectEnable && | ||||
|                     selectedDateGroup.contains(dateText) && | ||||
|                     selectedDateGroup.length > 1) { | ||||
|                   ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText); | ||||
|                   ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup); | ||||
|                 } else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) { | ||||
|                   ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText); | ||||
|                   ref.watch(homePageStateProvider.notifier).addMultipleSelectedItems(assetGroup); | ||||
|                 } else { | ||||
|                   ref.watch(homePageStateProvider.notifier).enableMultiSelect(assetGroup.toSet()); | ||||
|                   ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText); | ||||
|                 } | ||||
|               }, | ||||
|               onTap: _handleTitleIconClick, | ||||
|               child: isMultiSelectEnable && selectedDateGroup.contains(dateText) | ||||
|                   ? const Icon(Icons.check_circle_rounded) | ||||
|                   : const Icon(Icons.check_circle_outline_rounded), | ||||
|                   ? Icon( | ||||
|                       Icons.check_circle_rounded, | ||||
|                       color: Theme.of(context).primaryColor, | ||||
|                     ) | ||||
|                   : const Icon( | ||||
|                       Icons.check_circle_outline_rounded, | ||||
|                       color: Colors.grey, | ||||
|                     ), | ||||
|             ) | ||||
|           ], | ||||
|         ), | ||||
|   | ||||
							
								
								
									
										33
									
								
								mobile/lib/modules/home/ui/delete_diaglog.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								mobile/lib/modules/home/ui/delete_diaglog.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
|  | ||||
| class DeleteDialog extends StatelessWidget { | ||||
|   const DeleteDialog({Key? key}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AlertDialog( | ||||
|       backgroundColor: Colors.grey[200], | ||||
|       shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), | ||||
|       title: const Text("Delete Permanently"), | ||||
|       content: const Text("These items will be permanently deleted from Immich and from your device"), | ||||
|       actions: [ | ||||
|         TextButton( | ||||
|           onPressed: () { | ||||
|             Navigator.of(context).pop(); | ||||
|           }, | ||||
|           child: const Text( | ||||
|             "Cancel", | ||||
|             style: TextStyle(color: Colors.blueGrey), | ||||
|           ), | ||||
|         ), | ||||
|         TextButton( | ||||
|           onPressed: () {}, | ||||
|           child: Text( | ||||
|             "Delete", | ||||
|             style: TextStyle(color: Colors.red[400]), | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										47
									
								
								mobile/lib/modules/home/ui/disable_multi_select_button.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								mobile/lib/modules/home/ui/disable_multi_select_button.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; | ||||
|  | ||||
| class DisableMultiSelectButton extends ConsumerWidget { | ||||
|   const DisableMultiSelectButton({ | ||||
|     Key? key, | ||||
|     required this.onPressed, | ||||
|     required this.selectedItemCount, | ||||
|   }) : super(key: key); | ||||
|  | ||||
|   final Function onPressed; | ||||
|   final int selectedItemCount; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     return Positioned( | ||||
|       top: 0, | ||||
|       left: 0, | ||||
|       child: Padding( | ||||
|         padding: const EdgeInsets.only(left: 16.0, top: 46), | ||||
|         child: Material( | ||||
|           elevation: 20, | ||||
|           borderRadius: BorderRadius.circular(35), | ||||
|           child: Container( | ||||
|             decoration: BoxDecoration( | ||||
|               borderRadius: BorderRadius.circular(35), | ||||
|               color: Colors.grey[100], | ||||
|             ), | ||||
|             child: Padding( | ||||
|               padding: const EdgeInsets.symmetric(horizontal: 4.0), | ||||
|               child: TextButton.icon( | ||||
|                   onPressed: () { | ||||
|                     onPressed(); | ||||
|                   }, | ||||
|                   icon: const Icon(Icons.close_rounded), | ||||
|                   label: Text( | ||||
|                     selectedItemCount.toString(), | ||||
|                     style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18), | ||||
|                   )), | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -57,8 +57,8 @@ class ProfileDrawer extends ConsumerWidget { | ||||
|               bool res = await ref.read(authenticationProvider.notifier).logout(); | ||||
|  | ||||
|               if (res) { | ||||
|                 ref.watch(assetProvider.notifier).clearAllAsset(); | ||||
|                 AutoRouter.of(context).popUntilRoot(); | ||||
|                 ref.read(assetProvider.notifier).clearAllAsset(); | ||||
|               } | ||||
|             }, | ||||
|           ) | ||||
|   | ||||
| @@ -49,6 +49,7 @@ class ThumbnailImage extends HookConsumerWidget { | ||||
|         } else if (isMultiSelectEnable && !selectedAsset.contains(asset)) { | ||||
|           ref.watch(homePageStateProvider.notifier).addSingleSelectedItem(asset); | ||||
|         } else { | ||||
|           print(asset.id); | ||||
|           if (asset.type == 'IMAGE') { | ||||
|             AutoRouter.of(context).push( | ||||
|               ImageViewerRoute( | ||||
|   | ||||
| @@ -1,7 +1,10 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/daily_title_text.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/disable_multi_select_button.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/image_grid.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart'; | ||||
| @@ -9,6 +12,7 @@ import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/profile_drawer.dart'; | ||||
| import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart'; | ||||
| import 'package:immich_mobile/modules/home/providers/asset.provider.dart'; | ||||
| import 'package:sliver_tools/sliver_tools.dart'; | ||||
|  | ||||
| class HomePage extends HookConsumerWidget { | ||||
|   const HomePage({Key? key}) : super(key: key); | ||||
| @@ -18,6 +22,8 @@ class HomePage extends HookConsumerWidget { | ||||
|     ScrollController _scrollController = useScrollController(); | ||||
|     List<ImmichAssetGroupByDate> _assetGroup = ref.watch(assetProvider); | ||||
|     List<Widget> _imageGridGroup = []; | ||||
|     var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable; | ||||
|     var homePageState = ref.watch(homePageStateProvider); | ||||
|  | ||||
|     _scrollControllerCallback() { | ||||
|       var endOfPage = _scrollController.position.maxScrollExtent; | ||||
| @@ -28,10 +34,9 @@ class HomePage extends HookConsumerWidget { | ||||
|     } | ||||
|  | ||||
|     useEffect(() { | ||||
|       ref.read(assetProvider.notifier).getImmichAssets(); | ||||
|       ref.read(assetProvider.notifier).getAllAssets(); | ||||
|  | ||||
|       _scrollController.addListener(_scrollControllerCallback); | ||||
|  | ||||
|       return () { | ||||
|         _scrollController.removeListener(_scrollControllerCallback); | ||||
|       }; | ||||
| @@ -45,7 +50,7 @@ class HomePage extends HookConsumerWidget { | ||||
|       if (_imageGridGroup.isNotEmpty && _imageGridGroup.length < 20) { | ||||
|         ref.read(assetProvider.notifier).getOlderAsset(); | ||||
|       } else if (_imageGridGroup.isEmpty) { | ||||
|         ref.read(assetProvider.notifier).getImmichAssets(); | ||||
|         ref.read(assetProvider.notifier).getAllAssets(); | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @@ -72,7 +77,10 @@ class HomePage extends HookConsumerWidget { | ||||
|  | ||||
|           // Add Daily Title Group | ||||
|           _imageGridGroup.add( | ||||
|             DailyTitleText(isoDate: dateTitle, assetGroup: assetGroup), | ||||
|             DailyTitleText( | ||||
|               isoDate: dateTitle, | ||||
|               assetGroup: assetGroup, | ||||
|             ), | ||||
|           ); | ||||
|  | ||||
|           // Add Image Group | ||||
| @@ -85,25 +93,49 @@ class HomePage extends HookConsumerWidget { | ||||
|       } | ||||
|  | ||||
|       return SafeArea( | ||||
|         child: DraggableScrollbar.semicircle( | ||||
|         bottom: !isMultiSelectEnable, | ||||
|         top: !isMultiSelectEnable, | ||||
|         child: Stack( | ||||
|           children: [ | ||||
|             DraggableScrollbar.semicircle( | ||||
|               backgroundColor: Theme.of(context).primaryColor, | ||||
|               controller: _scrollController, | ||||
|               heightScrollThumb: 48.0, | ||||
|               child: CustomScrollView( | ||||
|                 controller: _scrollController, | ||||
|                 slivers: [ | ||||
|               ImmichSliverAppBar( | ||||
|                   SliverAnimatedSwitcher( | ||||
|                     child: isMultiSelectEnable | ||||
|                         ? const SliverToBoxAdapter( | ||||
|                             child: SizedBox( | ||||
|                               height: 70, | ||||
|                               child: null, | ||||
|                             ), | ||||
|                           ) | ||||
|                         : ImmichSliverAppBar( | ||||
|                             imageGridGroup: _imageGridGroup, | ||||
|                             onPopBack: onPopBackFromBackupPage, | ||||
|                           ), | ||||
|               ..._imageGridGroup, | ||||
|                     duration: const Duration(milliseconds: 350), | ||||
|                   ), | ||||
|                   ..._imageGridGroup | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|             isMultiSelectEnable | ||||
|                 ? DisableMultiSelectButton( | ||||
|                     onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect, | ||||
|                     selectedItemCount: homePageState.selectedItems.length, | ||||
|                   ) | ||||
|                 : Container(), | ||||
|             isMultiSelectEnable ? const ControlBottomAppBar() : Container(), | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Scaffold( | ||||
|       // key: _scaffoldKey, | ||||
|       drawer: const ProfileDrawer(), | ||||
|       body: _buildBody(), | ||||
|     ); | ||||
|   | ||||
| @@ -513,13 +513,6 @@ packages: | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.12.11" | ||||
|   material_color_utilities: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: material_color_utilities | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.1.3" | ||||
|   meta: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -721,6 +714,13 @@ packages: | ||||
|     description: flutter | ||||
|     source: sdk | ||||
|     version: "0.0.99" | ||||
|   sliver_tools: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: sliver_tools | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.2.5" | ||||
|   source_gen: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -818,7 +818,7 @@ packages: | ||||
|       name: test_api | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.4.8" | ||||
|     version: "0.4.3" | ||||
|   timing: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
| @@ -30,6 +30,7 @@ dependencies: | ||||
|   fluttertoast: ^8.0.8 | ||||
|   video_player: ^2.2.18 | ||||
|   chewie: ^1.2.2 | ||||
|   sliver_tools: ^0.2.5 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user