1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00: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:
martyfuhry 2024-01-30 18:47:47 -05:00 committed by GitHub
parent 9c7dee8551
commit 149bc71eba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 200 additions and 92 deletions

View 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,
),
),
],
),
],
),
);
}
}

View file

@ -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,
),
),
),
],
),

View 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,
),
),
],
),
],
);
}
}

View file

@ -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]),
],
);
},