From 149bc71ebaaf9765f19a7499ccb83d4ec412e47b Mon Sep 17 00:00:00 2001 From: martyfuhry Date: Tue, 30 Jan 2024 18:47:47 -0500 Subject: [PATCH] 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 --- .../memories/ui/memory_bottom_info.dart | 44 +++++++ .../lib/modules/memories/ui/memory_card.dart | 27 ++--- .../modules/memories/ui/memory_epilogue.dart | 114 ++++++++++++++++++ .../modules/memories/views/memory_page.dart | 107 +++++----------- 4 files changed, 200 insertions(+), 92 deletions(-) create mode 100644 mobile/lib/modules/memories/ui/memory_bottom_info.dart create mode 100644 mobile/lib/modules/memories/ui/memory_epilogue.dart diff --git a/mobile/lib/modules/memories/ui/memory_bottom_info.dart b/mobile/lib/modules/memories/ui/memory_bottom_info.dart new file mode 100644 index 0000000000..b09d83c9f9 --- /dev/null +++ b/mobile/lib/modules/memories/ui/memory_bottom_info.dart @@ -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, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/mobile/lib/modules/memories/ui/memory_card.dart b/mobile/lib/modules/memories/ui/memory_card.dart index 8ed8b631f2..d9ccaed39f 100644 --- a/mobile/lib/modules/memories/ui/memory_card.dart +++ b/mobile/lib/modules/memories/ui/memory_card.dart @@ -2,7 +2,6 @@ import 'dart:ui'; import 'package:cached_network_image/cached_network_image.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/shared/models/asset.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:openapi/api.dart'; -class MemoryCard extends HookConsumerWidget { +class MemoryCard extends StatelessWidget { final Asset asset; final void Function() onTap; final void Function() onClose; @@ -28,20 +27,10 @@ class MemoryCard extends HookConsumerWidget { super.key, }); + String get authToken => 'Bearer ${Store.get(StoreKey.accessToken)}'; + @override - Widget build(BuildContext context, WidgetRef ref) { - final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}'; - - buildTitle() { - return Text( - title, - style: context.textTheme.headlineMedium?.copyWith( - color: Colors.white, - fontWeight: FontWeight.w500, - ), - ); - } - + Widget build(BuildContext context) { return Card( color: Colors.black, shape: RoundedRectangleBorder( @@ -110,7 +99,13 @@ class MemoryCard extends HookConsumerWidget { Positioned( left: 18.0, bottom: 18.0, - child: buildTitle(), + child: Text( + title, + style: context.textTheme.headlineMedium?.copyWith( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), ), ], ), diff --git a/mobile/lib/modules/memories/ui/memory_epilogue.dart b/mobile/lib/modules/memories/ui/memory_epilogue.dart new file mode 100644 index 0000000000..4e32ae6ac5 --- /dev/null +++ b/mobile/lib/modules/memories/ui/memory_epilogue.dart @@ -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 createState() => _MemoryEpilogueState(); +} + +class _MemoryEpilogueState extends State + 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, + ), + ), + ], + ), + ], + ); + } +} diff --git a/mobile/lib/modules/memories/views/memory_page.dart b/mobile/lib/modules/memories/views/memory_page.dart index b057fffe22..fbc04feae5 100644 --- a/mobile/lib/modules/memories/views/memory_page.dart +++ b/mobile/lib/modules/memories/views/memory_page.dart @@ -4,10 +4,11 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.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_epilogue.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; -import 'package:intl/intl.dart'; import 'package:openapi/api.dart' as api; @RoutePage() @@ -26,7 +27,6 @@ class MemoryPage extends HookConsumerWidget { final memoryPageController = usePageController(initialPage: memoryIndex); final memoryAssetPageController = usePageController(); final currentMemory = useState(memories[memoryIndex]); - final previousMemoryIndex = useState(memoryIndex); final currentAssetPage = useState(0); final assetProgress = useState( "${currentAssetPage.value + 1}|${currentMemory.value.assets.length}", @@ -129,39 +129,6 @@ class MemoryPage extends HookConsumerWidget { 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 * 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 @@ -172,49 +139,17 @@ class MemoryPage extends HookConsumerWidget { // maxScrollExtend contains the sum of horizontal pixels of all assets for depth = 1 // or sum of vertical pixels of all memories for depth = 0 if (notification is ScrollUpdateNotification) { + final isEpiloguePage = + (memoryPageController.page?.floor() ?? 0) >= memories.length; + final offset = notification.metrics.pixels; - final isLastMemory = - (memories.indexOf(currentMemory.value) + 1) >= memories.length; - if (isLastMemory) { - // Vertical scroll handling only at the last asset. - // 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 (isEpiloguePage && + (offset > notification.metrics.maxScrollExtent + 150)) { + 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; }, child: Scaffold( @@ -226,8 +161,28 @@ class MemoryPage extends HookConsumerWidget { ), scrollDirection: Axis.vertical, 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) { + // Build last page + if (mIndex == memories.length) { + return MemoryEpilogue( + onStartOver: () => memoryPageController.animateToPage( + 0, + duration: const Duration(seconds: 1), + curve: Curves.easeInOut, + ), + ); + } // Build horizontal page return Column( children: [ @@ -256,7 +211,7 @@ class MemoryPage extends HookConsumerWidget { }, ), ), - buildBottomInfo(memories[mIndex]), + MemoryBottomInfo(memory: memories[mIndex]), ], ); },