mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-30 10:24:58 -04:00 
			
		
		
		
	feat(mobile): Add end page to the end to memories (#6780)
* Adding memory epilogue card * Adds epilogue page to memories * Fixes a next / back issue * color --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
		
							parent
							
								
									9c7dee8551
								
							
						
					
					
						commit
						149bc71eba
					
				
							
								
								
									
										44
									
								
								mobile/lib/modules/memories/ui/memory_bottom_info.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								mobile/lib/modules/memories/ui/memory_bottom_info.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | |||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:immich_mobile/modules/memories/models/memory.dart'; | ||||||
|  | 
 | ||||||
|  | class MemoryBottomInfo extends StatelessWidget { | ||||||
|  |   final Memory memory; | ||||||
|  | 
 | ||||||
|  |   const MemoryBottomInfo({super.key, required this.memory}); | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final df = DateFormat.yMMMMd(); | ||||||
|  |     return Padding( | ||||||
|  |       padding: const EdgeInsets.all(16.0), | ||||||
|  |       child: Row( | ||||||
|  |         children: [ | ||||||
|  |           Column( | ||||||
|  |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |             children: [ | ||||||
|  |               Text( | ||||||
|  |                 memory.title, | ||||||
|  |                 style: TextStyle( | ||||||
|  |                   color: Colors.grey[400], | ||||||
|  |                   fontSize: 13.0, | ||||||
|  |                   fontWeight: FontWeight.w500, | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |               Text( | ||||||
|  |                 df.format( | ||||||
|  |                   memory.assets[0].fileCreatedAt, | ||||||
|  |                 ), | ||||||
|  |                 style: const TextStyle( | ||||||
|  |                   color: Colors.white, | ||||||
|  |                   fontSize: 15.0, | ||||||
|  |                   fontWeight: FontWeight.w500, | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -2,7 +2,6 @@ import 'dart:ui'; | |||||||
| 
 | 
 | ||||||
| import 'package:cached_network_image/cached_network_image.dart'; | import 'package:cached_network_image/cached_network_image.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; |  | ||||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||||
| import 'package:immich_mobile/shared/models/asset.dart'; | import 'package:immich_mobile/shared/models/asset.dart'; | ||||||
| import 'package:immich_mobile/shared/models/store.dart'; | import 'package:immich_mobile/shared/models/store.dart'; | ||||||
| @ -10,7 +9,7 @@ import 'package:immich_mobile/shared/ui/immich_image.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 MemoryCard extends HookConsumerWidget { | class MemoryCard extends StatelessWidget { | ||||||
|   final Asset asset; |   final Asset asset; | ||||||
|   final void Function() onTap; |   final void Function() onTap; | ||||||
|   final void Function() onClose; |   final void Function() onClose; | ||||||
| @ -28,20 +27,10 @@ class MemoryCard extends HookConsumerWidget { | |||||||
|     super.key, |     super.key, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   String get authToken => 'Bearer ${Store.get(StoreKey.accessToken)}'; | ||||||
|  | 
 | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context) { | ||||||
|     final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}'; |  | ||||||
| 
 |  | ||||||
|     buildTitle() { |  | ||||||
|       return Text( |  | ||||||
|         title, |  | ||||||
|         style: context.textTheme.headlineMedium?.copyWith( |  | ||||||
|           color: Colors.white, |  | ||||||
|           fontWeight: FontWeight.w500, |  | ||||||
|         ), |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return Card( |     return Card( | ||||||
|       color: Colors.black, |       color: Colors.black, | ||||||
|       shape: RoundedRectangleBorder( |       shape: RoundedRectangleBorder( | ||||||
| @ -110,7 +99,13 @@ class MemoryCard extends HookConsumerWidget { | |||||||
|             Positioned( |             Positioned( | ||||||
|               left: 18.0, |               left: 18.0, | ||||||
|               bottom: 18.0, |               bottom: 18.0, | ||||||
|               child: buildTitle(), |               child: Text( | ||||||
|  |                 title, | ||||||
|  |                 style: context.textTheme.headlineMedium?.copyWith( | ||||||
|  |                   color: Colors.white, | ||||||
|  |                   fontWeight: FontWeight.w500, | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|             ), |             ), | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|  | |||||||
							
								
								
									
										114
									
								
								mobile/lib/modules/memories/ui/memory_epilogue.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								mobile/lib/modules/memories/ui/memory_epilogue.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,114 @@ | |||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:immich_mobile/constants/immich_colors.dart'; | ||||||
|  | import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||||
|  | 
 | ||||||
|  | class MemoryEpilogue extends StatefulWidget { | ||||||
|  |   final Function()? onStartOver; | ||||||
|  | 
 | ||||||
|  |   const MemoryEpilogue({super.key, this.onStartOver}); | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   State<MemoryEpilogue> createState() => _MemoryEpilogueState(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class _MemoryEpilogueState extends State<MemoryEpilogue> | ||||||
|  |     with TickerProviderStateMixin { | ||||||
|  |   late final _animationController = AnimationController( | ||||||
|  |     vsync: this, | ||||||
|  |     duration: const Duration( | ||||||
|  |       seconds: 3, | ||||||
|  |     ), | ||||||
|  |   )..repeat( | ||||||
|  |       reverse: true, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |   late final Animation _animation; | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  |     _animation = CurvedAnimation( | ||||||
|  |       parent: _animationController, | ||||||
|  |       curve: Curves.easeInOut, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   void dispose() { | ||||||
|  |     _animationController.dispose(); | ||||||
|  |     super.dispose(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Column( | ||||||
|  |       crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |       mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |       children: [ | ||||||
|  |         Expanded( | ||||||
|  |           child: Column( | ||||||
|  |             mainAxisAlignment: MainAxisAlignment.center, | ||||||
|  |             children: [ | ||||||
|  |               const Icon( | ||||||
|  |                 Icons.check_circle_outline_sharp, | ||||||
|  |                 color: immichDarkThemePrimaryColor, | ||||||
|  |                 size: 64.0, | ||||||
|  |               ), | ||||||
|  |               const SizedBox(height: 16.0), | ||||||
|  |               Text( | ||||||
|  |                 'All caught up', | ||||||
|  |                 style: Theme.of(context).textTheme.headlineMedium?.copyWith( | ||||||
|  |                       color: Colors.white, | ||||||
|  |                     ), | ||||||
|  |               ), | ||||||
|  |               const SizedBox(height: 16.0), | ||||||
|  |               Text( | ||||||
|  |                 'Check back tomorrow for more memories', | ||||||
|  |                 style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||||
|  |                       color: Colors.white, | ||||||
|  |                     ), | ||||||
|  |               ), | ||||||
|  |               const SizedBox(height: 16.0), | ||||||
|  |               TextButton( | ||||||
|  |                 onPressed: widget.onStartOver, | ||||||
|  |                 child: Text( | ||||||
|  |                   'Start Over', | ||||||
|  |                   style: context.textTheme.displayMedium?.copyWith( | ||||||
|  |                     color: immichDarkThemePrimaryColor, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |         Column( | ||||||
|  |           children: [ | ||||||
|  |             SizedBox( | ||||||
|  |               height: 48, | ||||||
|  |               child: AnimatedBuilder( | ||||||
|  |                 animation: _animation, | ||||||
|  |                 builder: (context, child) { | ||||||
|  |                   return Transform.translate( | ||||||
|  |                     offset: Offset(0, 5 * _animationController.value), | ||||||
|  |                     child: child, | ||||||
|  |                   ); | ||||||
|  |                 }, | ||||||
|  |                 child: const Icon( | ||||||
|  |                   size: 32, | ||||||
|  |                   Icons.expand_less_sharp, | ||||||
|  |                   color: Colors.white, | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |             Text( | ||||||
|  |               'Swipe up to close', | ||||||
|  |               style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||||
|  |                     color: Colors.white, | ||||||
|  |                   ), | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -4,10 +4,11 @@ import 'package:flutter/services.dart'; | |||||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/memories/models/memory.dart'; | import 'package:immich_mobile/modules/memories/models/memory.dart'; | ||||||
|  | import 'package:immich_mobile/modules/memories/ui/memory_bottom_info.dart'; | ||||||
| import 'package:immich_mobile/modules/memories/ui/memory_card.dart'; | import 'package:immich_mobile/modules/memories/ui/memory_card.dart'; | ||||||
|  | import 'package:immich_mobile/modules/memories/ui/memory_epilogue.dart'; | ||||||
| import 'package:immich_mobile/shared/models/asset.dart'; | import 'package:immich_mobile/shared/models/asset.dart'; | ||||||
| import 'package:immich_mobile/shared/ui/immich_image.dart'; | import 'package:immich_mobile/shared/ui/immich_image.dart'; | ||||||
| import 'package:intl/intl.dart'; |  | ||||||
| import 'package:openapi/api.dart' as api; | import 'package:openapi/api.dart' as api; | ||||||
| 
 | 
 | ||||||
| @RoutePage() | @RoutePage() | ||||||
| @ -26,7 +27,6 @@ class MemoryPage extends HookConsumerWidget { | |||||||
|     final memoryPageController = usePageController(initialPage: memoryIndex); |     final memoryPageController = usePageController(initialPage: memoryIndex); | ||||||
|     final memoryAssetPageController = usePageController(); |     final memoryAssetPageController = usePageController(); | ||||||
|     final currentMemory = useState(memories[memoryIndex]); |     final currentMemory = useState(memories[memoryIndex]); | ||||||
|     final previousMemoryIndex = useState(memoryIndex); |  | ||||||
|     final currentAssetPage = useState(0); |     final currentAssetPage = useState(0); | ||||||
|     final assetProgress = useState( |     final assetProgress = useState( | ||||||
|       "${currentAssetPage.value + 1}|${currentMemory.value.assets.length}", |       "${currentAssetPage.value + 1}|${currentMemory.value.assets.length}", | ||||||
| @ -129,39 +129,6 @@ class MemoryPage extends HookConsumerWidget { | |||||||
|       updateProgressText(); |       updateProgressText(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     buildBottomInfo(Memory memory) { |  | ||||||
|       return Padding( |  | ||||||
|         padding: const EdgeInsets.all(16.0), |  | ||||||
|         child: Row( |  | ||||||
|           children: [ |  | ||||||
|             Column( |  | ||||||
|               crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|               children: [ |  | ||||||
|                 Text( |  | ||||||
|                   memory.title, |  | ||||||
|                   style: TextStyle( |  | ||||||
|                     color: Colors.grey[400], |  | ||||||
|                     fontSize: 13.0, |  | ||||||
|                     fontWeight: FontWeight.w500, |  | ||||||
|                   ), |  | ||||||
|                 ), |  | ||||||
|                 Text( |  | ||||||
|                   DateFormat.yMMMMd().format( |  | ||||||
|                     memory.assets[0].fileCreatedAt, |  | ||||||
|                   ), |  | ||||||
|                   style: const TextStyle( |  | ||||||
|                     color: Colors.white, |  | ||||||
|                     fontSize: 15.0, |  | ||||||
|                     fontWeight: FontWeight.w500, |  | ||||||
|                   ), |  | ||||||
|                 ), |  | ||||||
|               ], |  | ||||||
|             ), |  | ||||||
|           ], |  | ||||||
|         ), |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /* Notification listener is used instead of OnPageChanged callback since OnPageChanged is called |     /* Notification listener is used instead of OnPageChanged callback since OnPageChanged is called | ||||||
|      * when the page in the **center** of the viewer changes. We want to reset currentAssetPage only when the final |      * when the page in the **center** of the viewer changes. We want to reset currentAssetPage only when the final | ||||||
|      * page during the end of scroll is different than the current page |      * page during the end of scroll is different than the current page | ||||||
| @ -172,49 +139,17 @@ class MemoryPage extends HookConsumerWidget { | |||||||
|         // maxScrollExtend contains the sum of horizontal pixels of all assets for depth = 1 |         // maxScrollExtend contains the sum of horizontal pixels of all assets for depth = 1 | ||||||
|         // or sum of vertical pixels of all memories for depth = 0 |         // or sum of vertical pixels of all memories for depth = 0 | ||||||
|         if (notification is ScrollUpdateNotification) { |         if (notification is ScrollUpdateNotification) { | ||||||
|  |           final isEpiloguePage = | ||||||
|  |               (memoryPageController.page?.floor() ?? 0) >= memories.length; | ||||||
|  | 
 | ||||||
|           final offset = notification.metrics.pixels; |           final offset = notification.metrics.pixels; | ||||||
|           final isLastMemory = |           if (isEpiloguePage && | ||||||
|               (memories.indexOf(currentMemory.value) + 1) >= memories.length; |               (offset > notification.metrics.maxScrollExtent + 150)) { | ||||||
|           if (isLastMemory) { |             context.popRoute(); | ||||||
|             // Vertical scroll handling only at the last asset. |             return true; | ||||||
|             // Tapping on the last asset instead of swiping will trigger the scroll |  | ||||||
|             // implicitly which will trigger the below handling and thereby closes the |  | ||||||
|             // memory lane as well |  | ||||||
|             if (notification.depth == 0) { |  | ||||||
|               final isLastAsset = (currentAssetPage.value + 1) == |  | ||||||
|                   currentMemory.value.assets.length; |  | ||||||
|               if (isLastAsset && |  | ||||||
|                   (offset > notification.metrics.maxScrollExtent + 150)) { |  | ||||||
|                 context.popRoute(); |  | ||||||
|                 return true; |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|             // Horizontal scroll handling |  | ||||||
|             if (notification.depth == 1 && |  | ||||||
|                 (offset > notification.metrics.maxScrollExtent + 100)) { |  | ||||||
|               context.popRoute(); |  | ||||||
|               return true; |  | ||||||
|             } |  | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (notification.depth == 0) { |  | ||||||
|           if (notification is ScrollStartNotification) { |  | ||||||
|             assetProgress.value = ""; |  | ||||||
|             return true; |  | ||||||
|           } |  | ||||||
|           var currentPageNumber = memoryPageController.page!.toInt(); |  | ||||||
|           currentMemory.value = memories[currentPageNumber]; |  | ||||||
|           if (notification is ScrollEndNotification) { |  | ||||||
|             HapticFeedback.mediumImpact(); |  | ||||||
|             if (currentPageNumber != previousMemoryIndex.value) { |  | ||||||
|               currentAssetPage.value = 0; |  | ||||||
|               previousMemoryIndex.value = currentPageNumber; |  | ||||||
|             } |  | ||||||
|             updateProgressText(); |  | ||||||
|             return true; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|         return false; |         return false; | ||||||
|       }, |       }, | ||||||
|       child: Scaffold( |       child: Scaffold( | ||||||
| @ -226,8 +161,28 @@ class MemoryPage extends HookConsumerWidget { | |||||||
|             ), |             ), | ||||||
|             scrollDirection: Axis.vertical, |             scrollDirection: Axis.vertical, | ||||||
|             controller: memoryPageController, |             controller: memoryPageController, | ||||||
|             itemCount: memories.length, |             onPageChanged: (pageNumber) { | ||||||
|  |               HapticFeedback.mediumImpact(); | ||||||
|  |               if (pageNumber < memories.length) { | ||||||
|  |                 currentMemory.value = memories[pageNumber]; | ||||||
|  |               } | ||||||
|  | 
 | ||||||
|  |               currentAssetPage.value = 0; | ||||||
|  | 
 | ||||||
|  |               updateProgressText(); | ||||||
|  |             }, | ||||||
|  |             itemCount: memories.length + 1, | ||||||
|             itemBuilder: (context, mIndex) { |             itemBuilder: (context, mIndex) { | ||||||
|  |               // Build last page | ||||||
|  |               if (mIndex == memories.length) { | ||||||
|  |                 return MemoryEpilogue( | ||||||
|  |                   onStartOver: () => memoryPageController.animateToPage( | ||||||
|  |                     0, | ||||||
|  |                     duration: const Duration(seconds: 1), | ||||||
|  |                     curve: Curves.easeInOut, | ||||||
|  |                   ), | ||||||
|  |                 ); | ||||||
|  |               } | ||||||
|               // Build horizontal page |               // Build horizontal page | ||||||
|               return Column( |               return Column( | ||||||
|                 children: [ |                 children: [ | ||||||
| @ -256,7 +211,7 @@ class MemoryPage extends HookConsumerWidget { | |||||||
|                       }, |                       }, | ||||||
|                     ), |                     ), | ||||||
|                   ), |                   ), | ||||||
|                   buildBottomInfo(memories[mIndex]), |                   MemoryBottomInfo(memory: memories[mIndex]), | ||||||
|                 ], |                 ], | ||||||
|               ); |               ); | ||||||
|             }, |             }, | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user