added save from calendar functionality

This commit is contained in:
Myzel394 2022-08-22 20:03:31 +02:00
parent 4170ec99ea
commit ff63d87600
9 changed files with 459 additions and 200 deletions

View File

@ -3,6 +3,16 @@ extension DateExtensions on DateTime {
return year == other.year && month == other.month && day == other.day; return year == other.year && month == other.month && day == other.day;
} }
DateTime asNormalizedDate() => DateTime(year, month, day); DateTime asNormalizedDate() => DateTime(
DateTime asNormalizedDateTime() => DateTime(year, month, day, hour, minute); year,
month,
day,
0,
0,
0,
0,
0,
);
DateTime asNormalizedDateTime() =>
DateTime(year, month, day, hour, minute, 0, 0, 0);
} }

View File

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:gallery_saver/gallery_saver.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:quid_faciam_hodie/enums.dart'; import 'package:quid_faciam_hodie/enums.dart';
import 'package:quid_faciam_hodie/managers/file_manager.dart'; import 'package:quid_faciam_hodie/managers/file_manager.dart';
@ -44,4 +45,17 @@ class Memory {
'memories', 'memories',
filePath, filePath,
); );
Future<void> 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;
}
}
} }

View File

@ -166,6 +166,17 @@
"calendarScreenTitle": "Calendar", "calendarScreenTitle": "Calendar",
"calendarScreenSelectionTitle": "Selected {memories,plural, =1{one memory} other{{memories} memories}}",
"@calendarScreenSelectionTitle": {
"placeholders": {
"days": {
"type": "int"
},
"memories": {
"type": "int"
}
}
},
"timelineScreenTitle": "Timeline", "timelineScreenTitle": "Timeline",

View File

@ -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<DateTime> _selectedDates = {};
bool get isInSelectMode => _selectedDates.isNotEmpty;
bool get isSavingToGallery => _isSavingToGallery;
Set<DateTime> 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<Memory> filterMemories(final Iterable<Memory> memories) =>
memories.where(
(memory) => _selectedDates.contains(
memory.creationDate.asNormalizedDate(),
),
);
bool checkWhetherDateIsSelected(final DateTime date) {
return _selectedDates.contains(date.asNormalizedDate());
}
}

View File

@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.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:provider/provider.dart';
import 'package:quid_faciam_hodie/constants/spacing.dart'; import 'package:quid_faciam_hodie/constants/spacing.dart';
import 'package:quid_faciam_hodie/managers/calendar_manager.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/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/calendar_month.dart';
import 'calendar_screen/days_of_week_strip.dart'; import 'calendar_screen/days_of_week_strip.dart';
@ -27,50 +30,98 @@ class CalendarScreen extends StatelessWidget {
final monthMapping = calendarManager.getMappingForList(); final monthMapping = calendarManager.getMappingForList();
return Consumer<Memories>( return Consumer<Memories>(
builder: (context, memories, _) => PlatformScaffold( builder: (context, memories, _) => ChangeNotifierProvider<CalendarModel>(
appBar: isCupertino(context) create: (_) => CalendarModel(),
? PlatformAppBar( child: Consumer<CalendarModel>(
title: Text(localizations.calendarScreenTitle), builder: (_, calendar, __) => PlatformScaffold(
) appBar: (calendar.isInSelectMode || isCupertino(context))
: null, ? PlatformAppBar(
body: Padding( leading: calendar.isInSelectMode
padding: EdgeInsets.only( ? PlatformIconButton(
top: isCupertino(context) ? HUGE_SPACE : MEDIUM_SPACE, icon: Icon(context.platformIcons.clear),
), onPressed: calendar.clearDates,
child: CustomScrollView( )
reverse: true, : null,
slivers: [ trailingActions: calendar.isInSelectMode
SliverStickyHeader( ? <Widget>[
header: Container( IconButton(
color: platformThemeData( onPressed: () async {
context, calendar.setIsSavingToGallery(true);
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();
}
final date = monthMapping.keys.elementAt(index); final memoriesToSave =
final dayMapping = monthMapping.values.elementAt(index); calendar.filterMemories(memories.memories);
return CalendarMonth( final hasSavedAll = await showPlatformDialog(
year: date.year, context: context,
month: date.month, builder: (_) => SaveToGalleryModal(
dayAmountMap: dayMapping, memories: memoriesToSave,
); ),
}, );
childCount: monthMapping.length + 1,
), 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,
),
),
),
],
),
),
), ),
), ),
), ),

View File

@ -1,12 +1,15 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_calendar_widget/flutter_calendar_widget.dart'; import 'package:flutter_calendar_widget/flutter_calendar_widget.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:intl/intl.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/spacing.dart';
import 'package:quid_faciam_hodie/constants/values.dart'; import 'package:quid_faciam_hodie/constants/values.dart';
import 'package:quid_faciam_hodie/extensions/date.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/screens/timeline_screen.dart';
import 'package:quid_faciam_hodie/widgets/delay_render.dart'; import 'package:quid_faciam_hodie/widgets/delay_render.dart';
import 'package:quid_faciam_hodie/widgets/fade_and_move_in_animation.dart'; import 'package:quid_faciam_hodie/widgets/fade_and_move_in_animation.dart';
@ -30,75 +33,95 @@ class MonthCalendarBuilder extends CalendarBuilder {
@override @override
Widget buildDate(DateTime dateTime, DateType type, List events) { 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( final delay = Duration(
microseconds: Random().nextInt(CALENDAR_DATE_IN_MAX_DELAY.inMicroseconds), microseconds: Random().nextInt(CALENDAR_DATE_IN_MAX_DELAY.inMicroseconds),
); );
return DelayRender( return Consumer<CalendarModel>(
delay: delay, builder: (_, calendar, __) {
child: FadeAndMoveInAnimation( final isSelected = calendar.checkWhetherDateIsSelected(dateTime);
opacityDuration: final backgroundPercentage = () {
DEFAULT_OPACITY_DURATION * CALENDAR_DATE_IN_DURATION_MULTIPLIER, if (isSelected) {
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; return 1.0;
}(), }
child: Padding(
padding: const EdgeInsets.all(TINY_SPACE), if (events.isEmpty) {
child: ClipRRect( return 0.0;
borderRadius: BorderRadius.circular(SMALL_SPACE), }
child: Stack(
alignment: style.dayAlignment, final highestAmountOfEvents = events[0];
children: [ final amount = events[1];
SizedBox( return amount / highestAmountOfEvents;
child: Container( }();
color: textStyle.selectedDayTextColor
.withOpacity(backgroundPercentage), return DelayRender(
), delay: delay,
), child: FadeAndMoveInAnimation(
Container( opacityDuration:
alignment: Alignment.center, DEFAULT_OPACITY_DURATION * CALENDAR_DATE_IN_DURATION_MULTIPLIER,
child: Text( translationDuration: DEFAULT_TRANSLATION_DURATION *
dateTime.day.toString(), CALENDAR_DATE_IN_DURATION_MULTIPLIER,
style: TextStyle( translationOffset: const Offset(0.0, -MEDIUM_SPACE),
color: backgroundPercentage > .5 child: Opacity(
? textStyle.focusedDayTextColor opacity: () {
: textStyle.dayTextColor, 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<DateTime, int> dayAmountMap; final Map<DateTime, int> dayAmountMap;
final int year; final int year;
final int month; final int month;
@ -110,73 +133,98 @@ class CalendarMonth extends StatelessWidget {
required this.month, required this.month,
}) : super(key: key); }) : super(key: key);
int get highestAmount => @override
dayAmountMap.values.isEmpty ? 0 : dayAmountMap.values.reduce(max); State<CalendarMonth> createState() => _CalendarMonthState();
DateTime get firstDate => DateTime(year, month, 1); }
DateTime get lastDate =>
DateTime(year, month, DateUtils.getDaysInMonth(year, month)); class _CalendarMonthState extends State<CalendarMonth> {
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) => bool doesDateExist(final DateTime date) =>
dayAmountMap.keys.contains(date.asNormalizedDate()); widget.dayAmountMap.keys.contains(date.asNormalizedDate());
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FlutterCalendar( return Consumer<CalendarModel>(
focusedDate: firstDate, builder: (_, calendar, __) => FlutterCalendar(
selectionMode: CalendarSelectionMode.single, focusedDate: firstDate,
calendarBuilder: MonthCalendarBuilder(), selectionMode: CalendarSelectionMode.single,
events: EventList(events: { calendarBuilder: MonthCalendarBuilder(),
DateTime(1990, 1, 1): [ events: EventList(events: {
highestAmount, DateTime(1990, 1, 1): [
], highestAmount,
...Map.fromEntries( ],
dayAmountMap.entries.map((entry) { ...Map.fromEntries(
return MapEntry( widget.dayAmountMap.entries.map((entry) {
entry.key, return MapEntry(
[highestAmount, entry.value], 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),
), ),
); }),
}, minDate: firstDate,
style: const CalendarStyle( maxDate: lastDate,
calenderMargin: EdgeInsets.symmetric(vertical: MEDIUM_SPACE), startingDayOfWeek: DayOfWeek.mon,
), onDayPressed: (date) {
textStyle: CalendarTextStyle( if (calendar.isInSelectMode &&
headerTextStyle: platformThemeData( widget.dayAmountMap.keys.contains(date.asNormalizedDate())) {
context, HapticFeedback.selectionClick();
material: (data) => data.textTheme.subtitle1!,
cupertino: (data) => data.textTheme.navTitleTextStyle, 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( textStyle: CalendarTextStyle(
context, headerTextStyle: platformThemeData(
material: (data) => data.textTheme.bodyText1!.color!, context,
cupertino: (data) => data.textTheme.textStyle.color!, material: (data) => data.textTheme.subtitle1!,
), cupertino: (data) => data.textTheme.navTitleTextStyle,
// Background color ),
selectedDayTextColor: platformThemeData( dayTextColor: platformThemeData(
context, context,
material: (data) => data.textTheme.bodyText1!.color!, material: (data) => data.textTheme.bodyText1!.color!,
cupertino: (data) => data.textTheme.textStyle.color!, cupertino: (data) => data.textTheme.textStyle.color!,
), ),
// Foreground color // Background color
focusedDayTextColor: platformThemeData( selectedDayTextColor: platformThemeData(
context, context,
material: (data) => data.dialogBackgroundColor, material: (data) => data.textTheme.bodyText1!.color!,
cupertino: (data) => data.barBackgroundColor, 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,
),
), ),
), ),
); );

View File

@ -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<Memory> memories;
const SaveToGalleryModal({
Key? key,
required this.memories,
}) : super(key: key);
@override
State<SaveToGalleryModal> createState() => _SaveToGalleryModalState();
}
class _SaveToGalleryModalState extends State<SaveToGalleryModal> {
int currentMemory = 0;
@override
void initState() {
super.initState();
downloadMemories();
}
Future<void> 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: <Widget>[
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),
)
],
),
);
}
}

View File

@ -3,9 +3,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.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/constants/spacing.dart';
import 'package:quid_faciam_hodie/enums.dart';
import 'package:quid_faciam_hodie/extensions/snackbar.dart'; import 'package:quid_faciam_hodie/extensions/snackbar.dart';
import 'package:quid_faciam_hodie/foreign_types/memory.dart'; import 'package:quid_faciam_hodie/foreign_types/memory.dart';
import 'package:quid_faciam_hodie/managers/file_manager.dart'; import 'package:quid_faciam_hodie/managers/file_manager.dart';
@ -50,16 +48,7 @@ class _MemorySheetState extends State<MemorySheet> with Loadable {
final localizations = AppLocalizations.of(context)!; final localizations = AppLocalizations.of(context)!;
try { try {
final file = await widget.memory.downloadToFile(); await widget.memory.saveFileToGallery();
switch (widget.memory.type) {
case MemoryType.photo:
await GallerySaver.saveImage(file.path);
break;
case MemoryType.video:
await GallerySaver.saveVideo(file.path);
break;
}
if (!mounted) { if (!mounted) {
return; return;

View File

@ -14,38 +14,38 @@ class ModalSheet extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView( return Align(
padding: const EdgeInsets.only(top: MEDIUM_SPACE), alignment: Alignment.bottomCenter,
child: Column( child: PlatformWidget(
mainAxisSize: MainAxisSize.min, material: (_, __) => Container(
children: [ decoration: BoxDecoration(
PlatformWidget( borderRadius: const BorderRadius.only(
material: (_, __) => Container( topLeft: Radius.circular(LARGE_SPACE),
decoration: BoxDecoration( topRight: Radius.circular(LARGE_SPACE),
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,
),
), ),
cupertino: (_, __) => CupertinoPopupSurface( color: getSheetColor(context),
isSurfacePainted: false, ),
child: Container( child: SingleChildScrollView(
color: Colors.white, padding: const EdgeInsets.symmetric(
child: Padding( vertical: LARGE_SPACE,
padding: const EdgeInsets.symmetric(vertical: LARGE_SPACE), horizontal: MEDIUM_SPACE,
child: child,
),
),
), ),
) 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,
),
),
),
), ),
); );
} }