From ff63d87600fe0573f6f040794283ff74bd700562 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 22 Aug 2022 20:03:31 +0200 Subject: [PATCH] added save from calendar functionality --- lib/extensions/date.dart | 14 +- lib/foreign_types/memory.dart | 14 + lib/locale/l10n/app_en.arb | 11 + lib/models/calendar_model.dart | 64 ++++ lib/screens/calendar_screen.dart | 133 ++++++--- .../calendar_screen/calendar_month.dart | 278 ++++++++++-------- .../save_to_gallery_modal.dart | 72 +++++ lib/screens/timeline_screen/memory_sheet.dart | 13 +- lib/widgets/modal_sheet.dart | 60 ++-- 9 files changed, 459 insertions(+), 200 deletions(-) create mode 100644 lib/models/calendar_model.dart create mode 100644 lib/screens/calendar_screen/save_to_gallery_modal.dart diff --git a/lib/extensions/date.dart b/lib/extensions/date.dart index 46902af..31d8449 100644 --- a/lib/extensions/date.dart +++ b/lib/extensions/date.dart @@ -3,6 +3,16 @@ extension DateExtensions on DateTime { return year == other.year && month == other.month && day == other.day; } - DateTime asNormalizedDate() => DateTime(year, month, day); - DateTime asNormalizedDateTime() => DateTime(year, month, day, hour, minute); + DateTime asNormalizedDate() => DateTime( + year, + month, + day, + 0, + 0, + 0, + 0, + 0, + ); + DateTime asNormalizedDateTime() => + DateTime(year, month, day, hour, minute, 0, 0, 0); } diff --git a/lib/foreign_types/memory.dart b/lib/foreign_types/memory.dart index a747dcc..f91144e 100644 --- a/lib/foreign_types/memory.dart +++ b/lib/foreign_types/memory.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:gallery_saver/gallery_saver.dart'; import 'package:path/path.dart'; import 'package:quid_faciam_hodie/enums.dart'; import 'package:quid_faciam_hodie/managers/file_manager.dart'; @@ -44,4 +45,17 @@ class Memory { 'memories', filePath, ); + + Future saveFileToGallery() async { + final file = await downloadToFile(); + + switch (type) { + case MemoryType.photo: + await GallerySaver.saveImage(file.path); + break; + case MemoryType.video: + await GallerySaver.saveVideo(file.path); + break; + } + } } diff --git a/lib/locale/l10n/app_en.arb b/lib/locale/l10n/app_en.arb index 566df6f..409d1ec 100644 --- a/lib/locale/l10n/app_en.arb +++ b/lib/locale/l10n/app_en.arb @@ -166,6 +166,17 @@ "calendarScreenTitle": "Calendar", + "calendarScreenSelectionTitle": "Selected {memories,plural, =1{one memory} other{{memories} memories}}", + "@calendarScreenSelectionTitle": { + "placeholders": { + "days": { + "type": "int" + }, + "memories": { + "type": "int" + } + } + }, "timelineScreenTitle": "Timeline", diff --git a/lib/models/calendar_model.dart b/lib/models/calendar_model.dart new file mode 100644 index 0000000..c3a0f51 --- /dev/null +++ b/lib/models/calendar_model.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:quid_faciam_hodie/extensions/date.dart'; +import 'package:quid_faciam_hodie/foreign_types/memory.dart'; + +// The `CalendarModel` is only used to give the calendar day's a way to +// send whether they have been pressed to the parent widget. +class CalendarModel extends ChangeNotifier { + bool _isSavingToGallery = false; + final Set _selectedDates = {}; + + bool get isInSelectMode => _selectedDates.isNotEmpty; + bool get isSavingToGallery => _isSavingToGallery; + Set get selectedDates => _selectedDates; + + void setIsInSelectMode(final bool value) { + if (_isSavingToGallery) { + return; + } + + if (!value) { + _selectedDates.clear(); + } + notifyListeners(); + } + + void setIsSavingToGallery(final bool value) { + _isSavingToGallery = value; + notifyListeners(); + } + + void addDate(final DateTime date) { + _selectedDates.add(date.asNormalizedDate()); + notifyListeners(); + } + + void removeDate(final DateTime date) { + _selectedDates.removeWhere((element) => element == date.asNormalizedDate()); + notifyListeners(); + } + + void clearDates() { + _selectedDates.clear(); + notifyListeners(); + } + + void toggleDate(final DateTime date) { + if (checkWhetherDateIsSelected(date)) { + removeDate(date); + } else { + addDate(date); + } + } + + Iterable filterMemories(final Iterable memories) => + memories.where( + (memory) => _selectedDates.contains( + memory.creationDate.asNormalizedDate(), + ), + ); + + bool checkWhetherDateIsSelected(final DateTime date) { + return _selectedDates.contains(date.asNormalizedDate()); + } +} diff --git a/lib/screens/calendar_screen.dart b/lib/screens/calendar_screen.dart index 63d01a3..0582307 100644 --- a/lib/screens/calendar_screen.dart +++ b/lib/screens/calendar_screen.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; @@ -5,7 +6,9 @@ import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:provider/provider.dart'; import 'package:quid_faciam_hodie/constants/spacing.dart'; import 'package:quid_faciam_hodie/managers/calendar_manager.dart'; +import 'package:quid_faciam_hodie/models/calendar_model.dart'; import 'package:quid_faciam_hodie/models/memories.dart'; +import 'package:quid_faciam_hodie/screens/calendar_screen/save_to_gallery_modal.dart'; import 'calendar_screen/calendar_month.dart'; import 'calendar_screen/days_of_week_strip.dart'; @@ -27,50 +30,98 @@ class CalendarScreen extends StatelessWidget { final monthMapping = calendarManager.getMappingForList(); return Consumer( - builder: (context, memories, _) => PlatformScaffold( - appBar: isCupertino(context) - ? PlatformAppBar( - title: Text(localizations.calendarScreenTitle), - ) - : null, - body: Padding( - padding: EdgeInsets.only( - top: isCupertino(context) ? HUGE_SPACE : MEDIUM_SPACE, - ), - child: CustomScrollView( - reverse: true, - slivers: [ - SliverStickyHeader( - header: Container( - color: platformThemeData( - context, - material: (data) => data.canvasColor, - cupertino: (data) => data.barBackgroundColor, - ), - padding: const EdgeInsets.symmetric(vertical: SMALL_SPACE), - child: const DaysOfWeekStrip(), - ), - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index == monthMapping.length) { - return MemoriesData(); - } + builder: (context, memories, _) => ChangeNotifierProvider( + create: (_) => CalendarModel(), + child: Consumer( + builder: (_, calendar, __) => PlatformScaffold( + appBar: (calendar.isInSelectMode || isCupertino(context)) + ? PlatformAppBar( + leading: calendar.isInSelectMode + ? PlatformIconButton( + icon: Icon(context.platformIcons.clear), + onPressed: calendar.clearDates, + ) + : null, + trailingActions: calendar.isInSelectMode + ? [ + IconButton( + onPressed: () async { + calendar.setIsSavingToGallery(true); - final date = monthMapping.keys.elementAt(index); - final dayMapping = monthMapping.values.elementAt(index); + final memoriesToSave = + calendar.filterMemories(memories.memories); - return CalendarMonth( - year: date.year, - month: date.month, - dayAmountMap: dayMapping, - ); - }, - childCount: monthMapping.length + 1, - ), - ), + final hasSavedAll = await showPlatformDialog( + context: context, + builder: (_) => SaveToGalleryModal( + memories: memoriesToSave, + ), + ); + + if (hasSavedAll == true) { + calendar.clearDates(); + } + + calendar.setIsSavingToGallery(true); + }, + icon: Icon(isMaterial(context) + ? Icons.download + : CupertinoIcons.down_arrow), + ), + ] + : null, + title: Text( + calendar.isInSelectMode + ? localizations.calendarScreenSelectionTitle( + calendar.selectedDates.length, + calendar.filterMemories(memories.memories).length, + ) + : localizations.calendarScreenTitle, + ), + ) + : null, + body: Padding( + padding: EdgeInsets.only( + top: isCupertino(context) ? HUGE_SPACE : MEDIUM_SPACE, ), - ], + child: CustomScrollView( + reverse: true, + slivers: [ + SliverStickyHeader( + header: Container( + color: platformThemeData( + context, + material: (data) => data.canvasColor, + cupertino: (data) => data.barBackgroundColor, + ), + padding: + const EdgeInsets.symmetric(vertical: SMALL_SPACE), + child: const DaysOfWeekStrip(), + ), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == monthMapping.length) { + return const MemoriesData(); + } + + final date = monthMapping.keys.elementAt(index); + final dayMapping = + monthMapping.values.elementAt(index); + + return CalendarMonth( + year: date.year, + month: date.month, + dayAmountMap: dayMapping, + ); + }, + childCount: monthMapping.length + 1, + ), + ), + ), + ], + ), + ), ), ), ), diff --git a/lib/screens/calendar_screen/calendar_month.dart b/lib/screens/calendar_screen/calendar_month.dart index 5a8608f..b4618a6 100644 --- a/lib/screens/calendar_screen/calendar_month.dart +++ b/lib/screens/calendar_screen/calendar_month.dart @@ -1,12 +1,15 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_calendar_widget/flutter_calendar_widget.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; import 'package:quid_faciam_hodie/constants/spacing.dart'; import 'package:quid_faciam_hodie/constants/values.dart'; import 'package:quid_faciam_hodie/extensions/date.dart'; +import 'package:quid_faciam_hodie/models/calendar_model.dart'; import 'package:quid_faciam_hodie/screens/timeline_screen.dart'; import 'package:quid_faciam_hodie/widgets/delay_render.dart'; import 'package:quid_faciam_hodie/widgets/fade_and_move_in_animation.dart'; @@ -30,75 +33,95 @@ class MonthCalendarBuilder extends CalendarBuilder { @override Widget buildDate(DateTime dateTime, DateType type, List events) { - final backgroundPercentage = () { - if (events.isEmpty) { - return 0.0; - } - - final highestAmountOfEvents = events[0]; - final amount = events[1]; - return amount / highestAmountOfEvents; - }(); - final delay = Duration( microseconds: Random().nextInt(CALENDAR_DATE_IN_MAX_DELAY.inMicroseconds), ); - return DelayRender( - delay: delay, - child: FadeAndMoveInAnimation( - opacityDuration: - DEFAULT_OPACITY_DURATION * CALENDAR_DATE_IN_DURATION_MULTIPLIER, - translationDuration: - DEFAULT_TRANSLATION_DURATION * CALENDAR_DATE_IN_DURATION_MULTIPLIER, - translationOffset: const Offset(0.0, -MEDIUM_SPACE), - child: Opacity( - opacity: () { - if (type.isOutSide) { - return 0.0; - } - - if (dateTime.isAfter(DateTime.now())) { - return 0.4; - } - + return Consumer( + builder: (_, calendar, __) { + final isSelected = calendar.checkWhetherDateIsSelected(dateTime); + final backgroundPercentage = () { + if (isSelected) { return 1.0; - }(), - child: Padding( - padding: const EdgeInsets.all(TINY_SPACE), - child: ClipRRect( - borderRadius: BorderRadius.circular(SMALL_SPACE), - child: Stack( - alignment: style.dayAlignment, - children: [ - SizedBox( - child: Container( - color: textStyle.selectedDayTextColor - .withOpacity(backgroundPercentage), - ), - ), - Container( - alignment: Alignment.center, - child: Text( - dateTime.day.toString(), - style: TextStyle( - color: backgroundPercentage > .5 - ? textStyle.focusedDayTextColor - : textStyle.dayTextColor, + } + + if (events.isEmpty) { + return 0.0; + } + + final highestAmountOfEvents = events[0]; + final amount = events[1]; + return amount / highestAmountOfEvents; + }(); + + return DelayRender( + delay: delay, + child: FadeAndMoveInAnimation( + opacityDuration: + DEFAULT_OPACITY_DURATION * CALENDAR_DATE_IN_DURATION_MULTIPLIER, + translationDuration: DEFAULT_TRANSLATION_DURATION * + CALENDAR_DATE_IN_DURATION_MULTIPLIER, + translationOffset: const Offset(0.0, -MEDIUM_SPACE), + child: Opacity( + opacity: () { + if (type.isOutSide) { + return 0.0; + } + + if (dateTime.isAfter(DateTime.now())) { + return 0.4; + } + + return 1.0; + }(), + child: GestureDetector( + onLongPress: () { + if (events.isNotEmpty) { + HapticFeedback.heavyImpact(); + + calendar.addDate(dateTime); + } + }, + child: Material( + child: Padding( + padding: const EdgeInsets.all(TINY_SPACE), + child: ClipRRect( + borderRadius: BorderRadius.circular(SMALL_SPACE), + child: Stack( + alignment: Alignment.center, + children: [ + Container( + color: isSelected + ? textStyle.outsideDayTextColor + : textStyle.selectedDayTextColor + .withOpacity(backgroundPercentage), + ), + Container( + alignment: Alignment.center, + child: Text( + dateTime.day.toString(), + style: TextStyle( + color: backgroundPercentage > .5 + ? textStyle.focusedDayTextColor + : textStyle.dayTextColor, + ), + ), + ), + ], ), ), ), - ], + ), ), ), ), - ), - ), + ); + }, ); } } -class CalendarMonth extends StatelessWidget { +class CalendarMonth extends StatefulWidget { final Map dayAmountMap; final int year; final int month; @@ -110,73 +133,98 @@ class CalendarMonth extends StatelessWidget { required this.month, }) : super(key: key); - int get highestAmount => - dayAmountMap.values.isEmpty ? 0 : dayAmountMap.values.reduce(max); - DateTime get firstDate => DateTime(year, month, 1); - DateTime get lastDate => - DateTime(year, month, DateUtils.getDaysInMonth(year, month)); + @override + State createState() => _CalendarMonthState(); +} + +class _CalendarMonthState extends State { + int get highestAmount => widget.dayAmountMap.values.isEmpty + ? 0 + : widget.dayAmountMap.values.reduce(max); + + DateTime get firstDate => DateTime(widget.year, widget.month, 1); + + DateTime get lastDate => DateTime(widget.year, widget.month, + DateUtils.getDaysInMonth(widget.year, widget.month)); + bool doesDateExist(final DateTime date) => - dayAmountMap.keys.contains(date.asNormalizedDate()); + widget.dayAmountMap.keys.contains(date.asNormalizedDate()); @override Widget build(BuildContext context) { - return FlutterCalendar( - focusedDate: firstDate, - selectionMode: CalendarSelectionMode.single, - calendarBuilder: MonthCalendarBuilder(), - events: EventList(events: { - DateTime(1990, 1, 1): [ - highestAmount, - ], - ...Map.fromEntries( - dayAmountMap.entries.map((entry) { - return MapEntry( - entry.key, - [highestAmount, entry.value], - ); - }), - ), - }), - minDate: firstDate, - maxDate: lastDate, - startingDayOfWeek: DayOfWeek.mon, - onDayPressed: (date) { - if (!doesDateExist(date)) { - return; - } - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TimelineScreen(date: date), + return Consumer( + builder: (_, calendar, __) => FlutterCalendar( + focusedDate: firstDate, + selectionMode: CalendarSelectionMode.single, + calendarBuilder: MonthCalendarBuilder(), + events: EventList(events: { + DateTime(1990, 1, 1): [ + highestAmount, + ], + ...Map.fromEntries( + widget.dayAmountMap.entries.map((entry) { + return MapEntry( + entry.key, + [highestAmount, entry.value], + ); + }), ), - ); - }, - style: const CalendarStyle( - calenderMargin: EdgeInsets.symmetric(vertical: MEDIUM_SPACE), - ), - textStyle: CalendarTextStyle( - headerTextStyle: platformThemeData( - context, - material: (data) => data.textTheme.subtitle1!, - cupertino: (data) => data.textTheme.navTitleTextStyle, + }), + minDate: firstDate, + maxDate: lastDate, + startingDayOfWeek: DayOfWeek.mon, + onDayPressed: (date) { + if (calendar.isInSelectMode && + widget.dayAmountMap.keys.contains(date.asNormalizedDate())) { + HapticFeedback.selectionClick(); + + calendar.toggleDate(date); + return; + } + + if (!doesDateExist(date)) { + return; + } + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TimelineScreen(date: date), + ), + ); + }, + style: const CalendarStyle( + calenderMargin: EdgeInsets.symmetric(vertical: MEDIUM_SPACE), ), - dayTextColor: platformThemeData( - context, - material: (data) => data.textTheme.bodyText1!.color!, - cupertino: (data) => data.textTheme.textStyle.color!, - ), - // Background color - selectedDayTextColor: platformThemeData( - context, - material: (data) => data.textTheme.bodyText1!.color!, - cupertino: (data) => data.textTheme.textStyle.color!, - ), - // Foreground color - focusedDayTextColor: platformThemeData( - context, - material: (data) => data.dialogBackgroundColor, - cupertino: (data) => data.barBackgroundColor, + textStyle: CalendarTextStyle( + headerTextStyle: platformThemeData( + context, + material: (data) => data.textTheme.subtitle1!, + cupertino: (data) => data.textTheme.navTitleTextStyle, + ), + dayTextColor: platformThemeData( + context, + material: (data) => data.textTheme.bodyText1!.color!, + cupertino: (data) => data.textTheme.textStyle.color!, + ), + // Background color + selectedDayTextColor: platformThemeData( + context, + material: (data) => data.textTheme.bodyText1!.color!, + cupertino: (data) => data.textTheme.textStyle.color!, + ), + // Foreground color + focusedDayTextColor: platformThemeData( + context, + material: (data) => data.dialogBackgroundColor, + cupertino: (data) => data.barBackgroundColor, + ), + // Primary theme color + outsideDayTextColor: platformThemeData( + context, + material: (data) => data.colorScheme.primary, + cupertino: (data) => data.primaryColor, + ), ), ), ); diff --git a/lib/screens/calendar_screen/save_to_gallery_modal.dart b/lib/screens/calendar_screen/save_to_gallery_modal.dart new file mode 100644 index 0000000..c98b2c2 --- /dev/null +++ b/lib/screens/calendar_screen/save_to_gallery_modal.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:quid_faciam_hodie/constants/spacing.dart'; +import 'package:quid_faciam_hodie/foreign_types/memory.dart'; +import 'package:quid_faciam_hodie/utils/theme.dart'; +import 'package:quid_faciam_hodie/widgets/modal_sheet.dart'; + +class SaveToGalleryModal extends StatefulWidget { + final Iterable memories; + + const SaveToGalleryModal({ + Key? key, + required this.memories, + }) : super(key: key); + + @override + State createState() => _SaveToGalleryModalState(); +} + +class _SaveToGalleryModalState extends State { + int currentMemory = 0; + + @override + void initState() { + super.initState(); + + downloadMemories(); + } + + Future downloadMemories() async { + for (final memory in widget.memories) { + await memory.saveFileToGallery(); + + if (widget.memories.last != memory) { + setState(() { + currentMemory = currentMemory + 1; + }); + } + } + + if (mounted) { + Navigator.pop(context, true); + } + } + + @override + Widget build(BuildContext context) { + return ModalSheet( + child: Column( + children: [ + Text( + 'Saving to gallery', + style: getTitleTextStyle(context), + ), + const SizedBox(height: LARGE_SPACE), + Text( + '${currentMemory}/${widget.memories.length}', + style: getBodyTextTextStyle(context), + ), + const SizedBox(height: MEDIUM_SPACE), + LinearProgressIndicator( + value: currentMemory / widget.memories.length, + ), + const SizedBox(height: MEDIUM_SPACE), + Text( + widget.memories.elementAt(currentMemory).annotation, + style: getSubTitleTextStyle(context), + ) + ], + ), + ); + } +} diff --git a/lib/screens/timeline_screen/memory_sheet.dart b/lib/screens/timeline_screen/memory_sheet.dart index 81b65c7..373d29f 100644 --- a/lib/screens/timeline_screen/memory_sheet.dart +++ b/lib/screens/timeline_screen/memory_sheet.dart @@ -3,9 +3,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:gallery_saver/gallery_saver.dart'; import 'package:quid_faciam_hodie/constants/spacing.dart'; -import 'package:quid_faciam_hodie/enums.dart'; import 'package:quid_faciam_hodie/extensions/snackbar.dart'; import 'package:quid_faciam_hodie/foreign_types/memory.dart'; import 'package:quid_faciam_hodie/managers/file_manager.dart'; @@ -50,16 +48,7 @@ class _MemorySheetState extends State with Loadable { final localizations = AppLocalizations.of(context)!; try { - final file = await widget.memory.downloadToFile(); - - switch (widget.memory.type) { - case MemoryType.photo: - await GallerySaver.saveImage(file.path); - break; - case MemoryType.video: - await GallerySaver.saveVideo(file.path); - break; - } + await widget.memory.saveFileToGallery(); if (!mounted) { return; diff --git a/lib/widgets/modal_sheet.dart b/lib/widgets/modal_sheet.dart index eb0418c..808ce3d 100644 --- a/lib/widgets/modal_sheet.dart +++ b/lib/widgets/modal_sheet.dart @@ -14,38 +14,38 @@ class ModalSheet extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( - padding: const EdgeInsets.only(top: MEDIUM_SPACE), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - PlatformWidget( - material: (_, __) => Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(LARGE_SPACE), - topRight: Radius.circular(LARGE_SPACE), - ), - color: getSheetColor(context), - ), - padding: const EdgeInsets.symmetric(vertical: LARGE_SPACE), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: MEDIUM_SPACE), - child: child, - ), + return Align( + alignment: Alignment.bottomCenter, + child: PlatformWidget( + material: (_, __) => Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(LARGE_SPACE), + topRight: Radius.circular(LARGE_SPACE), ), - cupertino: (_, __) => CupertinoPopupSurface( - isSurfacePainted: false, - child: Container( - color: Colors.white, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: LARGE_SPACE), - child: child, - ), - ), + color: getSheetColor(context), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric( + vertical: LARGE_SPACE, + horizontal: MEDIUM_SPACE, ), - ) - ], + child: child, + ), + ), + cupertino: (_, __) => CupertinoPopupSurface( + isSurfacePainted: false, + child: Container( + color: Colors.white, + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric( + vertical: LARGE_SPACE, + horizontal: MEDIUM_SPACE, + ), + child: child, + ), + ), + ), ), ); }