From 941999d4fe6792073d23d80d758036685a9b1a32 Mon Sep 17 00:00:00 2001 From: Vilius Drumsta Date: Thu, 1 Jan 2026 15:11:19 +0000 Subject: [PATCH] Add multi-day share --- lib/main.dart | 4 + lib/pages/home_page.dart | 139 +++++++++++++----- lib/providers/selection_provider.dart | 29 ++++ lib/widgets/entry_day_cell.dart | 202 ++++++++++++++++---------- 4 files changed, 258 insertions(+), 116 deletions(-) create mode 100644 lib/providers/selection_provider.dart diff --git a/lib/main.dart b/lib/main.dart index 65f2d7f..4fa3bc3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:daily_you/notification_manager.dart'; import 'package:daily_you/pages/launch_page.dart'; import 'package:daily_you/providers/entries_provider.dart'; import 'package:daily_you/providers/entry_images_provider.dart'; +import 'package:daily_you/providers/selection_provider.dart'; import 'package:daily_you/providers/templates_provider.dart'; import 'package:daily_you/time_manager.dart'; import 'package:flutter/material.dart'; @@ -106,6 +107,9 @@ void main() async { ), ChangeNotifierProvider( create: (_) => ConfigProvider.instance, + ), + ChangeNotifierProvider( + create: (_) => SelectionProvider(), ) ], builder: (context, child) => const MainApp())); } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 6fc8450..e272d19 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -7,16 +7,20 @@ import 'package:daily_you/models/image.dart'; import 'package:daily_you/notification_manager.dart'; import 'package:daily_you/providers/entries_provider.dart'; import 'package:daily_you/providers/entry_images_provider.dart'; +import 'package:daily_you/providers/selection_provider.dart'; import 'package:daily_you/time_manager.dart'; import 'package:daily_you/widgets/entry_calendar.dart'; import 'package:daily_you/widgets/hiding_widget.dart'; import 'package:daily_you/widgets/large_entry_card_widget.dart'; +import 'package:daily_you/widgets/mood_icon.dart'; import 'package:flutter/material.dart'; import 'package:daily_you/l10n/generated/app_localizations.dart'; import 'package:daily_you/models/entry.dart'; import 'package:daily_you/pages/entry_detail_page.dart'; import 'package:daily_you/pages/edit_entry_page.dart'; +import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; +import 'package:share_plus/share_plus.dart'; import '../widgets/entry_card_widget.dart'; class HomePage extends StatefulWidget { @@ -94,12 +98,50 @@ class _HomePageState extends State } } + Future _shareSelectedEntries(BuildContext context) async { + final selectionProvider = + Provider.of(context, listen: false); + final entriesProvider = + Provider.of(context, listen: false); + + // Get entries for selected dates + List entries = selectionProvider.selectedDates + .map((date) => entriesProvider.getEntryForDate(date)) + .whereType() // Filter out nulls (dates with no entries) + .toList() + ..sort((a, b) => a.timeCreate.compareTo(b.timeCreate)); + + if (entries.isEmpty) { + // Exit selection mode if no entries to share + selectionProvider.clearSelection(); + return; + } + + // Build concatenated text + String sharedText = entries.map((entry) { + String line = ""; + if (entry.mood != null) { + line = "${MoodIcon.getMoodIcon(entry.mood)} "; + } + line += + "${DateFormat.yMMMEd(TimeManager.currentLocale(context)).format(entry.timeCreate)}\n${entry.text}"; + return line; + }).join("\n\n"); + + // Share text only (no images for multi-day shares) + await Share.share(sharedText); + + // Exit selection mode after sharing + selectionProvider.clearSelection(); + } + @override Widget build(BuildContext context) { super.build(context); final configProvider = Provider.of(context); final entriesProvider = Provider.of(context); final entryImagesProvider = Provider.of(context); + final selectionProvider = Provider.of(context); Entry? todayEntry = entriesProvider.getEntryForToday(); List todayImages = @@ -111,61 +153,78 @@ class _HomePageState extends State String viewMode = ConfigProvider.instance.get(ConfigKey.homePageViewMode); bool listView = viewMode == 'list'; - return Center( - child: Stack(alignment: Alignment.bottomCenter, children: [ - buildEntries(context, configProvider, flashbacks, listView), - HidingWidget( - duration: Duration(milliseconds: 200), - hideDirection: HideDirection.down, - scrollController: _scrollController, - child: Padding( - padding: const EdgeInsets.only(left: 8.0, right: 8.0, bottom: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (Platform.isAndroid) + return Scaffold( + appBar: selectionProvider.isSelectionMode + ? AppBar( + leading: IconButton( + icon: Icon(Icons.close), + onPressed: () => selectionProvider.clearSelection(), + ), + title: Text('${selectionProvider.selectedCount} selected'), + actions: [ + IconButton( + icon: Icon(Icons.share_rounded), + onPressed: () => _shareSelectedEntries(context), + ), + ], + ) + : null, + body: Center( + child: Stack(alignment: Alignment.bottomCenter, children: [ + buildEntries(context, configProvider, flashbacks, listView), + HidingWidget( + duration: Duration(milliseconds: 200), + hideDirection: HideDirection.down, + scrollController: _scrollController, + child: Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0, bottom: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (Platform.isAndroid) + IconButton( + icon: Icon( + Icons.camera_alt_rounded, + color: Theme.of(context).colorScheme.primaryContainer, + size: 24, + ), + onPressed: () async { + await addOrEditTodayEntry(todayEntry, todayImages, true); + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(8), + backgroundColor: Theme.of(context).colorScheme.primary, + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(80.0), + ), + ), + ), IconButton( icon: Icon( - Icons.camera_alt_rounded, + todayEntry == null ? Icons.add_rounded : Icons.edit_rounded, color: Theme.of(context).colorScheme.primaryContainer, - size: 24, + size: 28, ), onPressed: () async { - await addOrEditTodayEntry(todayEntry, todayImages, true); + await addOrEditTodayEntry(todayEntry, todayImages, false); }, style: ElevatedButton.styleFrom( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(16), backgroundColor: Theme.of(context).colorScheme.primary, elevation: 3, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(80.0), + borderRadius: BorderRadius.circular(16.0), ), ), ), - IconButton( - icon: Icon( - todayEntry == null ? Icons.add_rounded : Icons.edit_rounded, - color: Theme.of(context).colorScheme.primaryContainer, - size: 28, - ), - onPressed: () async { - await addOrEditTodayEntry(todayEntry, todayImages, false); - }, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.all(16), - backgroundColor: Theme.of(context).colorScheme.primary, - elevation: 3, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.0), - ), - ), - ), - ], + ], + ), ), ), - ), - ]), + ]), + ), ); } diff --git a/lib/providers/selection_provider.dart b/lib/providers/selection_provider.dart new file mode 100644 index 0000000..06884f8 --- /dev/null +++ b/lib/providers/selection_provider.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class SelectionProvider extends ChangeNotifier { + final Set _selectedDates = {}; + + bool get isSelectionMode => _selectedDates.isNotEmpty; + Set get selectedDates => Set.unmodifiable(_selectedDates); + int get selectedCount => _selectedDates.length; + + void toggleDate(DateTime date) { + DateTime normalized = DateTime(date.year, date.month, date.day); + if (_selectedDates.contains(normalized)) { + _selectedDates.remove(normalized); + } else { + _selectedDates.add(normalized); + } + notifyListeners(); + } + + bool isSelected(DateTime date) { + DateTime normalized = DateTime(date.year, date.month, date.day); + return _selectedDates.contains(normalized); + } + + void clearSelection() { + _selectedDates.clear(); + notifyListeners(); + } +} diff --git a/lib/widgets/entry_day_cell.dart b/lib/widgets/entry_day_cell.dart index ffa0ae2..8c76b9d 100644 --- a/lib/widgets/entry_day_cell.dart +++ b/lib/widgets/entry_day_cell.dart @@ -2,6 +2,7 @@ import 'package:daily_you/config_provider.dart'; import 'package:daily_you/models/image.dart'; import 'package:daily_you/providers/entries_provider.dart'; import 'package:daily_you/providers/entry_images_provider.dart'; +import 'package:daily_you/providers/selection_provider.dart'; import 'package:daily_you/time_manager.dart'; import 'package:daily_you/widgets/local_image_loader.dart'; import 'package:flutter/material.dart'; @@ -20,13 +21,82 @@ class EntryDayCell extends StatelessWidget { const EntryDayCell( {super.key, required this.date, required this.currentMonth}); + Future _handleTap( + BuildContext context, + Entry? entry, + bool isSelectionMode, + SelectionProvider selectionProvider, + EntriesProvider entriesProvider, + ) async { + if (isSelectionMode) { + // Toggle selection instead of navigating + selectionProvider.toggleDate(date); + } else { + // Existing navigation logic + if (entry != null) { + await Navigator.of(context).push(MaterialPageRoute( + allowSnapshotting: false, + builder: (context) => EntryDetailPage( + filtered: false, + index: entriesProvider.getIndexOfEntry(entry.id!), + ), + )); + } else { + await Navigator.of(context).push(MaterialPageRoute( + allowSnapshotting: false, + builder: (context) => AddEditEntryPage( + overrideCreateDate: TimeManager.currentTimeOnDifferentDate(date) + .copyWith(isUtc: false), + ), + )); + } + } + } + + Widget _wrapWithSelection( + BuildContext context, + Widget cellContent, + bool isSelected, + ) { + if (isSelected) { + return Stack( + children: [ + cellContent, + // Color overlay + Positioned.fill( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(8), + ), + ), + ), + // Checkmark icon + Positioned( + top: 2, + right: 2, + child: Icon( + Icons.check_circle, + color: Theme.of(context).colorScheme.primary, + size: 20, + ), + ), + ], + ); + } + return cellContent; + } + @override Widget build(BuildContext context) { final entriesProvider = Provider.of(context); final entryImagesProvider = Provider.of(context); final configProvider = Provider.of(context); + final selectionProvider = Provider.of(context); Entry? entry = entriesProvider.getEntryForDate(date); + bool isSelected = selectionProvider.isSelected(date); + bool isSelectionMode = selectionProvider.isSelectionMode; EntryImage? image; if (entry != null) { @@ -35,97 +105,77 @@ class EntryDayCell extends StatelessWidget { bool showMood = configProvider.get(ConfigKey.calendarViewMode) == 'mood'; + Widget cellContent; + if (entry != null) { if (showMood || image == null) { // Show mood - return FittedBox( + cellContent = FittedBox( fit: BoxFit.scaleDown, - child: GestureDetector( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '${date.day}', - style: const TextStyle(fontSize: 16), - ), - MoodIcon(moodValue: entry.mood) - ], - ), - onTap: () async { - await Navigator.of(context).push(MaterialPageRoute( - allowSnapshotting: false, - builder: (context) => EntryDetailPage( - filtered: false, - index: entriesProvider.getIndexOfEntry(entry.id!), - ), - )); - }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '${date.day}', + style: const TextStyle(fontSize: 16), + ), + MoodIcon(moodValue: entry.mood) + ], ), ); } else { // Show image - return GestureDetector( - child: Container( + cellContent = Container( + alignment: Alignment.center, + child: Stack( alignment: Alignment.center, - child: Stack( - alignment: Alignment.center, - children: [ - SizedBox( - height: 57, - child: Card( - margin: EdgeInsets.all(2), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(8))), - clipBehavior: Clip.antiAlias, - child: LocalImageLoader( - imagePath: image.imgPath, - cacheSize: 100, - )), - ), - Text('${date.day}', - style: - TextStyle(fontSize: 18, color: Colors.white, shadows: [ - Shadow( - color: Colors.black.withValues(alpha: 0.8), - blurRadius: 6, - offset: Offset(0, 0)), - ])), - ], - ), + children: [ + SizedBox( + height: 57, + child: Card( + margin: EdgeInsets.all(2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8))), + clipBehavior: Clip.antiAlias, + child: LocalImageLoader( + imagePath: image.imgPath, + cacheSize: 100, + )), + ), + Text('${date.day}', + style: + TextStyle(fontSize: 18, color: Colors.white, shadows: [ + Shadow( + color: Colors.black.withValues(alpha: 0.8), + blurRadius: 6, + offset: Offset(0, 0)), + ])), + ], ), - onTap: () async { - await Navigator.of(context).push(MaterialPageRoute( - allowSnapshotting: false, - builder: (context) => EntryDetailPage( - filtered: false, - index: entriesProvider.getIndexOfEntry(entry.id!)), - )); - }, ); } } else { // No entry - return GestureDetector( - behavior: HitTestBehavior.translucent, - child: Center( - child: Text('${date.day}', - style: isSameDay(date, DateTime.now()) - ? TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary) - : TextStyle(fontSize: 16)), - ), - onTap: () async { - await Navigator.of(context).push(MaterialPageRoute( - allowSnapshotting: false, - builder: (context) => AddEditEntryPage( - overrideCreateDate: TimeManager.currentTimeOnDifferentDate(date) - .copyWith(isUtc: false), - ), - )); - }, + cellContent = Center( + child: Text('${date.day}', + style: isSameDay(date, DateTime.now()) + ? TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary) + : TextStyle(fontSize: 16)), ); } + + // Wrap with selection overlay if selected + cellContent = _wrapWithSelection(context, cellContent, isSelected); + + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => + _handleTap(context, entry, isSelectionMode, selectionProvider, entriesProvider), + onLongPress: () => selectionProvider.toggleDate(date), + child: cellContent, + ); } }