diff --git a/CHANGELOG.md b/CHANGELOG.md index 3742ad26..4cd10dec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# [1.0.5] (UnReleased)(https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/tree/1.0.5) +# [1.0.5] (UnReleased) - Fixed issue related to auto scroll to initial duration for day view. [#269](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/269) @@ -11,6 +11,13 @@ feature added Support for changing the week day position(top/bottom) in weekView. [#283](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/283) - Adds new flag `includeEdges` in `EventArrangers`. [#290](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/pull/294) - Fixes null check exception while adding events. [#282](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/282) +- Added new method `update` to update the events in `EventController`. [#125](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/issues/125) +- Adds new parameter `includeFullDayEvents` in `getEventsOnDay` to decide whether to include full-day events in the returned list or not. +- Adds getters `isRangingEvent` and `isFullDayEvent` in `CalendarEventData` to check if the event is a ranging event or a full-day event. +- Adds new method `occursOnDate` in `CalendarEventData` to check if the event occurs on the given date or not. +- Make `description` in `CalendarEventData` nullable. +- #### Deprecations + - Deprecate `events` getter in `EventController` and adds `allEvents` to replace it. # [1.0.4 - 9 Aug 2023](https://github.com/SimformSolutionsPvtLtd/flutter_calendar_view/tree/1.0.4) diff --git a/README.md b/README.md index 3a864ece..df79af43 100644 --- a/README.md +++ b/README.md @@ -226,12 +226,19 @@ assigned. Methods provided by `EventController` -| Name | Parameters | Description | -|----------------|---------------------------------------|------------------------------------------------------| -| add | CalendarEventData\ event | Adds one event in controller and rebuilds view. | -| addAll | List\\> events | Adds list of events in controller and rebuilds view. | -| remove | CalendarEventData\ event | Removes an event from controller and rebuilds view. | -| getEventsOnDay | DateTime date | Returns list of events on `date` | +| Name | Parameters | Description | +|-----------------|--------------------------------------------------------------|-----------------------------------------------------------| +| add | CalendarEventData\ event | Adds one event in controller and rebuilds view. | +| addAll | List\\> events | Adds list of events in controller and rebuilds view. | +| remove | CalendarEventData\ event | Removes an event from controller and rebuilds view. | +| removeAll | List\\> events | Removes all event defined in the list | +| removeWhere | TestPredicate\\> test | Removes all events for which test returns true. | +| update | CalendarEventData\ event, CalendarEventData\ updated | Updates event with updated event. | +| getFullDayEvent | DateTime date | Returns the list of full day events stored in controller | +| updateFilter | EventFilter\ newFilter | Updates the event filter of the controller. | +| getEventsOnDay | DateTime date | Returns list of events on `date` | + +Check [documentation](https://pub.dev/documentation/calendar_view/latest/calendar_view/EventController-class.html) for more info. ### Use of `GlobalKey` @@ -256,6 +263,9 @@ Methods defined by `MonthViewState` class: | jumpToMonth | DateTime month | Jumps to the page that has a calendar for month defined by `month` | | animateToMonth | DateTime month | Animate to the page that has a calendar for month defined by `month` | +Check [documentation](https://pub.dev/documentation/calendar_view/latest/calendar_view/MonthViewState-class.html) for more info. + + Methods defined by `DayViewState` class. | Name | Parameters | Description | @@ -270,6 +280,8 @@ Methods defined by `DayViewState` class. | animateToEvent | CalendarEventData event | Animates to the page where a given `event` is and then scrolls to make that `event` visible on the screen. | | jumpToEvent | CalendarEventData event | Jumps to the page where a given `event` is and then scrolls to make that `event` visible on the screen. | +Check [documentation](https://pub.dev/documentation/calendar_view/latest/calendar_view/DayViewState-class.html) for more info. + Methods defined by `WeekViewState` class. | Name | Parameters | Description | @@ -283,6 +295,8 @@ Methods defined by `WeekViewState` class. | animateToEvent | CalendarEventData event | Animates to the page where a given `event` is and then scrolls to make that `event` visible on the screen. | | jumpToEvent | CalendarEventData event | Jumps to the page where a given `event` is and then scrolls to make that `event` visible on the screen. | +Check [documentation](https://pub.dev/documentation/calendar_view/latest/calendar_view/WeekViewState-class.html) for more info. + ### Synchronize events between calendar views There are two ways to synchronize events between calendar views. @@ -353,7 +367,7 @@ copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/example/.gitignore b/example/.gitignore index 2b12cb7a..fc1f4cb6 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -32,7 +32,6 @@ /build/ # Web related -lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols diff --git a/example/lib/main.dart b/example/lib/main.dart index 720a6a67..146e23af 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,10 +3,7 @@ import 'dart:ui'; import 'package:calendar_view/calendar_view.dart'; import 'package:flutter/material.dart'; -import 'model/event.dart'; -import 'pages/mobile/mobile_home_page.dart'; -import 'pages/web/web_home_page.dart'; -import 'widgets/responsive_widget.dart'; +import 'pages/home_page.dart'; DateTime get _now => DateTime.now(); @@ -18,8 +15,8 @@ class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { - return CalendarControllerProvider( - controller: EventController()..addAll(_events), + return CalendarControllerProvider( + controller: EventController()..addAll(_events), child: MaterialApp( title: 'Flutter Calendar Page Demo', debugShowCheckedModeBanner: false, @@ -31,19 +28,15 @@ class MyApp extends StatelessWidget { PointerDeviceKind.touch, }, ), - home: ResponsiveWidget( - mobileWidget: MobileHomePage(), - webWidget: WebHomePage(), - ), + home: HomePage(), ), ); } } -List> _events = [ +List _events = [ CalendarEventData( date: _now, - event: Event(title: "Joe's Birthday"), title: "Project meeting", description: "Today is project meeting.", startTime: DateTime(_now.year, _now.month, _now.day, 18, 30), @@ -53,7 +46,6 @@ List> _events = [ date: _now.add(Duration(days: 1)), startTime: DateTime(_now.year, _now.month, _now.day, 18), endTime: DateTime(_now.year, _now.month, _now.day, 19), - event: Event(title: "Wedding anniversary"), title: "Wedding anniversary", description: "Attend uncle's wedding anniversary.", ), @@ -61,7 +53,6 @@ List> _events = [ date: _now, startTime: DateTime(_now.year, _now.month, _now.day, 14), endTime: DateTime(_now.year, _now.month, _now.day, 17), - event: Event(title: "Football Tournament"), title: "Football Tournament", description: "Go to football tournament.", ), @@ -71,7 +62,6 @@ List> _events = [ _now.add(Duration(days: 3)).month, _now.add(Duration(days: 3)).day, 10), endTime: DateTime(_now.add(Duration(days: 3)).year, _now.add(Duration(days: 3)).month, _now.add(Duration(days: 3)).day, 14), - event: Event(title: "Sprint Meeting."), title: "Sprint Meeting.", description: "Last day of project submission for last year.", ), @@ -87,7 +77,6 @@ List> _events = [ _now.subtract(Duration(days: 2)).month, _now.subtract(Duration(days: 2)).day, 16), - event: Event(title: "Team Meeting"), title: "Team Meeting", description: "Team Meeting", ), @@ -103,7 +92,6 @@ List> _events = [ _now.subtract(Duration(days: 2)).month, _now.subtract(Duration(days: 2)).day, 12), - event: Event(title: "Chemistry Viva"), title: "Chemistry Viva", description: "Today is Joe's birthday.", ), diff --git a/example/lib/model/event.dart b/example/lib/model/event.dart deleted file mode 100644 index faa565d2..00000000 --- a/example/lib/model/event.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/foundation.dart'; - -@immutable -class Event { - final String title; - - const Event({this.title = "Title"}); - - @override - bool operator ==(Object other) => other is Event && title == other.title; - - @override - int get hashCode => super.hashCode; - - @override - String toString() => title; -} diff --git a/example/lib/pages/create_event_page.dart b/example/lib/pages/create_event_page.dart index 6f9acf6c..3b8e2499 100644 --- a/example/lib/pages/create_event_page.dart +++ b/example/lib/pages/create_event_page.dart @@ -1,29 +1,14 @@ +import 'package:calendar_view/calendar_view.dart'; import 'package:flutter/material.dart'; import '../app_colors.dart'; import '../extension.dart'; -import '../widgets/add_event_widget.dart'; +import '../widgets/add_event_form.dart'; -class CreateEventPage extends StatefulWidget { - final bool withDuration; +class CreateEventPage extends StatelessWidget { + const CreateEventPage({super.key, this.event}); - const CreateEventPage({Key? key, this.withDuration = false}) - : super(key: key); - - @override - _CreateEventPageState createState() => _CreateEventPageState(); -} - -class _CreateEventPageState extends State { - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } + final CalendarEventData? event; @override Widget build(BuildContext context) { @@ -40,7 +25,7 @@ class _CreateEventPageState extends State { ), ), title: Text( - "Create New Event", + event == null ? "Create New Event" : "Update Event", style: TextStyle( color: AppColors.black, fontSize: 20.0, @@ -52,8 +37,19 @@ class _CreateEventPageState extends State { physics: ClampingScrollPhysics(), child: Padding( padding: EdgeInsets.all(20.0), - child: AddEventWidget( - onEventAdd: context.pop, + child: AddOrEditEventForm( + onEventAdd: (newEvent) { + if (this.event != null) { + CalendarControllerProvider.of(context) + .controller + .update(this.event!, newEvent); + } else { + CalendarControllerProvider.of(context).controller.add(newEvent); + } + + context.pop(true); + }, + event: event, ), ), ), diff --git a/example/lib/pages/day_view_page.dart b/example/lib/pages/day_view_page.dart index 2cb73a41..1a80aba2 100644 --- a/example/lib/pages/day_view_page.dart +++ b/example/lib/pages/day_view_page.dart @@ -1,13 +1,14 @@ -import 'package:calendar_view/calendar_view.dart'; import 'package:flutter/material.dart'; +import '../enumerations.dart'; import '../extension.dart'; -import '../model/event.dart'; import '../widgets/day_view_widget.dart'; +import '../widgets/responsive_widget.dart'; import 'create_event_page.dart'; +import 'web/web_home_page.dart'; class DayViewPageDemo extends StatefulWidget { - const DayViewPageDemo({Key? key}) : super(key: key); + const DayViewPageDemo({super.key}); @override _DayViewPageDemoState createState() => _DayViewPageDemoState(); @@ -16,20 +17,18 @@ class DayViewPageDemo extends StatefulWidget { class _DayViewPageDemoState extends State { @override Widget build(BuildContext context) { - return Scaffold( - floatingActionButton: FloatingActionButton( - child: Icon(Icons.add), - elevation: 8, - onPressed: () async { - final event = - await context.pushRoute>(CreateEventPage( - withDuration: true, - )); - if (event == null) return; - CalendarControllerProvider.of(context).controller.add(event); - }, + return ResponsiveWidget( + webWidget: WebHomePage( + selectedView: CalendarView.day, + ), + mobileWidget: Scaffold( + floatingActionButton: FloatingActionButton( + child: Icon(Icons.add), + elevation: 8, + onPressed: () => context.pushRoute(CreateEventPage()), + ), + body: DayViewWidget(), ), - body: DayViewWidget(), ); } } diff --git a/example/lib/pages/event_details_page.dart b/example/lib/pages/event_details_page.dart index 150f764c..2785688d 100644 --- a/example/lib/pages/event_details_page.dart +++ b/example/lib/pages/event_details_page.dart @@ -2,11 +2,12 @@ import 'package:calendar_view/calendar_view.dart'; import 'package:flutter/material.dart'; import '../extension.dart'; +import 'create_event_page.dart'; class DetailsPage extends StatelessWidget { final CalendarEventData event; - const DetailsPage({Key? key, required this.event}) : super(key: key); + const DetailsPage({super.key, required this.event}); @override Widget build(BuildContext context) { return Scaffold( @@ -76,14 +77,49 @@ class DetailsPage extends StatelessWidget { height: 30.0, ), ], - if (event.description != "") ...[ + if (event.description?.isNotEmpty ?? false) ...[ Divider(), Text("Description"), SizedBox( height: 10.0, ), - Text(event.description), - ] + Text(event.description!), + ], + const SizedBox(height: 50), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () { + CalendarControllerProvider.of(context) + .controller + .remove(event); + Navigator.of(context).pop(); + }, + child: Text('Delete Event'), + ), + ), + SizedBox(width: 30), + Expanded( + child: ElevatedButton( + onPressed: () async { + final result = await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => CreateEventPage( + event: event, + ), + ), + ); + + if (result) { + Navigator.of(context).pop(); + } + }, + child: Text('Edit Event'), + ), + ), + ], + ), ], ), ); diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart new file mode 100644 index 00000000..85d8f05d --- /dev/null +++ b/example/lib/pages/home_page.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +import '../widgets/responsive_widget.dart'; +import 'mobile/mobile_home_page.dart'; +import 'web/web_home_page.dart'; + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return ResponsiveWidget( + mobileWidget: MobileHomePage(), + webWidget: WebHomePage(), + ); + } +} diff --git a/example/lib/pages/month_view_page.dart b/example/lib/pages/month_view_page.dart index 7da6d0c3..9bd798c2 100644 --- a/example/lib/pages/month_view_page.dart +++ b/example/lib/pages/month_view_page.dart @@ -1,15 +1,16 @@ -import 'package:calendar_view/calendar_view.dart'; import 'package:flutter/material.dart'; +import '../enumerations.dart'; import '../extension.dart'; -import '../model/event.dart'; import '../widgets/month_view_widget.dart'; +import '../widgets/responsive_widget.dart'; import 'create_event_page.dart'; +import 'web/web_home_page.dart'; class MonthViewPageDemo extends StatefulWidget { const MonthViewPageDemo({ - Key? key, - }) : super(key: key); + super.key, + }); @override _MonthViewPageDemoState createState() => _MonthViewPageDemoState(); @@ -18,23 +19,18 @@ class MonthViewPageDemo extends StatefulWidget { class _MonthViewPageDemoState extends State { @override Widget build(BuildContext context) { - return Scaffold( - floatingActionButton: FloatingActionButton( - child: Icon(Icons.add), - elevation: 8, - onPressed: _addEvent, + return ResponsiveWidget( + webWidget: WebHomePage( + selectedView: CalendarView.month, ), - body: MonthViewWidget(), - ); - } - - Future _addEvent() async { - final event = await context.pushRoute>( - CreateEventPage( - withDuration: true, + mobileWidget: Scaffold( + floatingActionButton: FloatingActionButton( + child: Icon(Icons.add), + elevation: 8, + onPressed: () => context.pushRoute(CreateEventPage()), + ), + body: MonthViewWidget(), ), ); - if (event == null) return; - CalendarControllerProvider.of(context).controller.add(event); } } diff --git a/example/lib/pages/web/web_home_page.dart b/example/lib/pages/web/web_home_page.dart index 6c1ebfc4..3fe2b836 100644 --- a/example/lib/pages/web/web_home_page.dart +++ b/example/lib/pages/web/web_home_page.dart @@ -5,12 +5,18 @@ import '../../widgets/calendar_configs.dart'; import '../../widgets/calendar_views.dart'; class WebHomePage extends StatefulWidget { + WebHomePage({ + this.selectedView = CalendarView.month, + }); + + final CalendarView selectedView; + @override _WebHomePageState createState() => _WebHomePageState(); } class _WebHomePageState extends State { - CalendarView _selectedView = CalendarView.month; + late var _selectedView = widget.selectedView; void _setView(CalendarView view) { if (view != _selectedView && mounted) { diff --git a/example/lib/pages/week_view_page.dart b/example/lib/pages/week_view_page.dart index 0de511d7..3a183f5e 100644 --- a/example/lib/pages/week_view_page.dart +++ b/example/lib/pages/week_view_page.dart @@ -1,13 +1,14 @@ -import 'package:calendar_view/calendar_view.dart'; import 'package:flutter/material.dart'; +import '../enumerations.dart'; import '../extension.dart'; -import '../model/event.dart'; +import '../widgets/responsive_widget.dart'; import '../widgets/week_view_widget.dart'; import 'create_event_page.dart'; +import 'web/web_home_page.dart'; class WeekViewDemo extends StatefulWidget { - const WeekViewDemo({Key? key}) : super(key: key); + const WeekViewDemo({super.key}); @override _WeekViewDemoState createState() => _WeekViewDemoState(); @@ -16,22 +17,18 @@ class WeekViewDemo extends StatefulWidget { class _WeekViewDemoState extends State { @override Widget build(BuildContext context) { - return Scaffold( - floatingActionButton: FloatingActionButton( - child: Icon(Icons.add), - elevation: 8, - onPressed: _addEvent, + return ResponsiveWidget( + webWidget: WebHomePage( + selectedView: CalendarView.week, + ), + mobileWidget: Scaffold( + floatingActionButton: FloatingActionButton( + child: Icon(Icons.add), + elevation: 8, + onPressed: () => context.pushRoute(CreateEventPage()), + ), + body: WeekViewWidget(), ), - body: WeekViewWidget(), ); } - - Future _addEvent() async { - final event = - await context.pushRoute>(CreateEventPage( - withDuration: true, - )); - if (event == null) return; - CalendarControllerProvider.of(context).controller.add(event); - } } diff --git a/example/lib/widgets/add_event_widget.dart b/example/lib/widgets/add_event_form.dart similarity index 62% rename from example/lib/widgets/add_event_widget.dart rename to example/lib/widgets/add_event_form.dart index 52115696..a34414ad 100644 --- a/example/lib/widgets/add_event_widget.dart +++ b/example/lib/widgets/add_event_form.dart @@ -5,73 +5,53 @@ import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import '../app_colors.dart'; import '../constants.dart'; import '../extension.dart'; -import '../model/event.dart'; import 'custom_button.dart'; import 'date_time_selector.dart'; -class AddEventWidget extends StatefulWidget { - final void Function(CalendarEventData)? onEventAdd; +class AddOrEditEventForm extends StatefulWidget { + final void Function(CalendarEventData)? onEventAdd; + final CalendarEventData? event; - const AddEventWidget({ - Key? key, + const AddOrEditEventForm({ + super.key, this.onEventAdd, - }) : super(key: key); + this.event, + }); @override - _AddEventWidgetState createState() => _AddEventWidgetState(); + _AddOrEditEventFormState createState() => _AddOrEditEventFormState(); } -class _AddEventWidgetState extends State { - late DateTime _startDate; - late DateTime _endDate; +class _AddOrEditEventFormState extends State { + late DateTime _startDate = DateTime.now().withoutTime; + late DateTime _endDate = DateTime.now().withoutTime; DateTime? _startTime; - DateTime? _endTime; - String _title = ""; - - String _description = ""; - Color _color = Colors.blue; - late FocusNode _titleNode; - - late FocusNode _descriptionNode; + final _form = GlobalKey(); - late FocusNode _dateNode; - - final GlobalKey _form = GlobalKey(); - - late TextEditingController _startDateController; - late TextEditingController _startTimeController; - late TextEditingController _endTimeController; - late TextEditingController _endDateController; + late final _descriptionController = TextEditingController(); + late final _titleController = TextEditingController(); + late final _titleNode = FocusNode(); + late final _descriptionNode = FocusNode(); @override void initState() { super.initState(); - _titleNode = FocusNode(); - _descriptionNode = FocusNode(); - _dateNode = FocusNode(); - - _startDateController = TextEditingController(); - _endDateController = TextEditingController(); - _startTimeController = TextEditingController(); - _endTimeController = TextEditingController(); + _setDefaults(); } @override void dispose() { _titleNode.dispose(); _descriptionNode.dispose(); - _dateNode.dispose(); - _startDateController.dispose(); - _endDateController.dispose(); - _startTimeController.dispose(); - _endTimeController.dispose(); + _descriptionController.dispose(); + _titleController.dispose(); super.dispose(); } @@ -84,6 +64,7 @@ class _AddEventWidgetState extends State { mainAxisSize: MainAxisSize.min, children: [ TextFormField( + controller: _titleController, decoration: AppConstants.inputDecoration.copyWith( labelText: "Event Title", ), @@ -91,9 +72,10 @@ class _AddEventWidgetState extends State { color: AppColors.black, fontSize: 17.0, ), - onSaved: (value) => _title = value?.trim() ?? "", validator: (value) { - if (value == null || value == "") + final title = value?.trim(); + + if (title == null || title == "") return "Please enter event title."; return null; @@ -108,13 +90,25 @@ class _AddEventWidgetState extends State { children: [ Expanded( child: DateTimeSelectorFormField( - controller: _startDateController, decoration: AppConstants.inputDecoration.copyWith( labelText: "Start Date", ), + initialDateTime: _startDate, + onSelect: (date) { + if (date.withoutTime.withoutTime + .isAfter(_endDate.withoutTime)) { + _endDate = date.withoutTime; + } + + _startDate = date.withoutTime; + + if (mounted) { + setState(() {}); + } + }, validator: (value) { if (value == null || value == "") - return "Please select date."; + return "Please select start date."; return null; }, @@ -122,20 +116,34 @@ class _AddEventWidgetState extends State { color: AppColors.black, fontSize: 17.0, ), - onSave: (date) => _startDate = date, + onSave: (date) => _startDate = date ?? _startDate, type: DateTimeSelectionType.date, ), ), SizedBox(width: 20.0), Expanded( child: DateTimeSelectorFormField( - controller: _endDateController, + initialDateTime: _endDate, decoration: AppConstants.inputDecoration.copyWith( labelText: "End Date", ), + onSelect: (date) { + if (date.withoutTime.withoutTime + .isBefore(_startDate.withoutTime)) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('End date occurs before start date.'), + )); + } else { + _endDate = date.withoutTime; + } + + if (mounted) { + setState(() {}); + } + }, validator: (value) { if (value == null || value == "") - return "Please select date."; + return "Please select end date."; return null; }, @@ -143,28 +151,32 @@ class _AddEventWidgetState extends State { color: AppColors.black, fontSize: 17.0, ), - onSave: (date) => _endDate = date, + onSave: (date) => _endDate = date ?? _endDate, type: DateTimeSelectionType.date, ), ), ], ), - SizedBox( - height: 15, - ), + SizedBox(height: 15), Row( children: [ Expanded( child: DateTimeSelectorFormField( - controller: _startTimeController, decoration: AppConstants.inputDecoration.copyWith( labelText: "Start Time", ), - validator: (value) { - if (value == null || value == "") - return "Please select start time."; - - return null; + initialDateTime: _startTime, + minimumDateTime: CalendarConstants.epochDate, + onSelect: (date) { + if (_endTime != null && + date.getTotalMinutes > _endTime!.getTotalMinutes) { + _endTime = date.add(Duration(minutes: 1)); + } + _startTime = date; + + if (mounted) { + setState(() {}); + } }, onSave: (date) => _startTime = date, textStyle: TextStyle( @@ -177,15 +189,23 @@ class _AddEventWidgetState extends State { SizedBox(width: 20.0), Expanded( child: DateTimeSelectorFormField( - controller: _endTimeController, decoration: AppConstants.inputDecoration.copyWith( labelText: "End Time", ), - validator: (value) { - if (value == null || value == "") - return "Please select end time."; - - return null; + initialDateTime: _endTime, + onSelect: (date) { + if (_startTime != null && + date.getTotalMinutes < _startTime!.getTotalMinutes) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('End time is less then start time.'), + )); + } else { + _endTime = date; + } + + if (mounted) { + setState(() {}); + } }, onSave: (date) => _endTime = date, textStyle: TextStyle( @@ -201,6 +221,7 @@ class _AddEventWidgetState extends State { height: 15, ), TextFormField( + controller: _descriptionController, focusNode: _descriptionNode, style: TextStyle( color: AppColors.black, @@ -218,7 +239,6 @@ class _AddEventWidgetState extends State { return null; }, - onSaved: (value) => _description = value?.trim() ?? "", decoration: AppConstants.inputDecoration.copyWith( hintText: "Event Description", ), @@ -249,7 +269,7 @@ class _AddEventWidgetState extends State { ), CustomButton( onTap: _createEvent, - title: "Add Event", + title: widget.event == null ? "Add Event" : "Update Event", ), ], ), @@ -261,28 +281,44 @@ class _AddEventWidgetState extends State { _form.currentState?.save(); - final event = CalendarEventData( + final event = CalendarEventData( date: _startDate, - color: _color, endTime: _endTime, startTime: _startTime, - description: _description, endDate: _endDate, - title: _title, - event: Event( - title: _title, - ), + color: _color, + title: _titleController.text.trim(), + description: _descriptionController.text.trim(), ); widget.onEventAdd?.call(event); _resetForm(); } + void _setDefaults() { + if (widget.event == null) return; + + final event = widget.event!; + + _startDate = event.date; + _endDate = event.endDate; + _startTime = event.startTime ?? _startTime; + _endTime = event.endTime ?? _endTime; + _titleController.text = event.title; + _descriptionController.text = event.description ?? ''; + } + void _resetForm() { _form.currentState?.reset(); - _startDateController.text = ""; - _endTimeController.text = ""; - _startTimeController.text = ""; + _startDate = DateTime.now().withoutTime; + _endDate = DateTime.now().withoutTime; + _startTime = null; + _endTime = null; + _color = Colors.blue; + + if (mounted) { + setState(() {}); + } } void _displayColorPicker() { @@ -294,16 +330,12 @@ class _AddEventWidgetState extends State { builder: (_) => SimpleDialog( clipBehavior: Clip.hardEdge, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30.0), - side: BorderSide( - color: AppColors.bluishGrey, - width: 2, - ), + borderRadius: BorderRadius.circular(20.0), ), contentPadding: EdgeInsets.all(20.0), children: [ Text( - "Event Color", + "Select event color", style: TextStyle( color: AppColors.black, fontSize: 25.0, diff --git a/example/lib/widgets/calendar_configs.dart b/example/lib/widgets/calendar_configs.dart index b429f1fe..9f976f88 100644 --- a/example/lib/widgets/calendar_configs.dart +++ b/example/lib/widgets/calendar_configs.dart @@ -4,18 +4,17 @@ import 'package:flutter/material.dart'; import '../app_colors.dart'; import '../enumerations.dart'; import '../extension.dart'; -import '../model/event.dart'; -import 'add_event_widget.dart'; +import 'add_event_form.dart'; class CalendarConfig extends StatelessWidget { final void Function(CalendarView view) onViewChange; final CalendarView currentView; const CalendarConfig({ - Key? key, + super.key, required this.onViewChange, this.currentView = CalendarView.month, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -99,9 +98,9 @@ class CalendarConfig extends StatelessWidget { SizedBox( height: 20, ), - AddEventWidget( + AddOrEditEventForm( onEventAdd: (event) { - CalendarControllerProvider.of(context) + CalendarControllerProvider.of(context) .controller .add(event); }, diff --git a/example/lib/widgets/calendar_views.dart b/example/lib/widgets/calendar_views.dart index a07eaccd..28509775 100644 --- a/example/lib/widgets/calendar_views.dart +++ b/example/lib/widgets/calendar_views.dart @@ -11,8 +11,7 @@ import 'week_view_widget.dart'; class CalendarViews extends StatelessWidget { final CalendarView view; - const CalendarViews({Key? key, this.view = CalendarView.month}) - : super(key: key); + const CalendarViews({super.key, this.view = CalendarView.month}); final _breakPoint = 490.0; diff --git a/example/lib/widgets/custom_button.dart b/example/lib/widgets/custom_button.dart index bdfeb898..02a92a9b 100644 --- a/example/lib/widgets/custom_button.dart +++ b/example/lib/widgets/custom_button.dart @@ -6,8 +6,7 @@ class CustomButton extends StatelessWidget { final String title; final VoidCallback? onTap; - const CustomButton({Key? key, required this.title, this.onTap}) - : super(key: key); + const CustomButton({super.key, required this.title, this.onTap}); @override Widget build(BuildContext context) { diff --git a/example/lib/widgets/date_time_selector.dart b/example/lib/widgets/date_time_selector.dart index 77a2fcf3..008d5666 100644 --- a/example/lib/widgets/date_time_selector.dart +++ b/example/lib/widgets/date_time_selector.dart @@ -8,28 +8,36 @@ typedef Validator = String? Function(String? value); enum DateTimeSelectionType { date, time } class DateTimeSelectorFormField extends StatefulWidget { - final Function(DateTime?)? onSelect; - final DateTimeSelectionType? type; + /// Called when date is selected. + final Function(DateTime)? onSelect; + + /// Selection time, date or time. + final DateTimeSelectionType type; + final FocusNode? focusNode; + + /// Minimum date that can be selected. final DateTime? minimumDateTime; + final Validator? validator; - final bool displayDefault; + final TextStyle? textStyle; - final void Function(DateTime date)? onSave; + final void Function(DateTime? date)? onSave; final InputDecoration? decoration; - final TextEditingController controller; + final TextEditingController? controller; + final DateTime? initialDateTime; const DateTimeSelectorFormField({ this.onSelect, - this.type, + this.type = DateTimeSelectionType.time, this.onSave, this.decoration, this.focusNode, this.minimumDateTime, this.validator, - this.displayDefault = false, this.textStyle, - required this.controller, + this.controller, + this.initialDateTime, }); @override @@ -38,84 +46,126 @@ class DateTimeSelectorFormField extends StatefulWidget { } class _DateTimeSelectorFormFieldState extends State { - late TextEditingController _textEditingController; - late FocusNode _focusNode; + late var _minimumDate = CalendarConstants.minDate.withoutTime; + + late var _textEditingController = + widget.controller ?? TextEditingController(); + late var _focusNode = _getFocusNode(); - late DateTime _selectedDate; + late DateTime? _selectedDate; @override void initState() { super.initState(); - _textEditingController = widget.controller; - _focusNode = FocusNode(); - - _selectedDate = widget.minimumDateTime ?? DateTime.now(); - - if (widget.displayDefault && widget.minimumDateTime != null) { - if (widget.type == DateTimeSelectionType.date) { - _textEditingController.text = widget.minimumDateTime - ?.dateToStringWithFormat(format: "dd/MM/yyyy") ?? - ""; - } else { - _textEditingController.text = - widget.minimumDateTime?.getTimeInFormat(TimeStampFormat.parse_12) ?? - ""; - } + _setDates(); + } + + @override + void didUpdateWidget(covariant DateTimeSelectorFormField oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.focusNode != oldWidget.focusNode) { + _focusNode.dispose(); + _focusNode = _getFocusNode(); + } + + if (widget.controller != oldWidget.controller) { + _textEditingController.dispose(); + _textEditingController = widget.controller ?? TextEditingController(); + } + + if (_selectedDate != oldWidget.initialDateTime || + widget.minimumDateTime != oldWidget.minimumDateTime) { + _setDates(); } } + FocusNode _getFocusNode() { + final node = widget.focusNode ?? FocusNode(); + + // node.addListener(() { + // if (node.hasFocus) { + // _showSelector(); + // } + // }); + + return node; + } + @override void dispose() { - _focusNode.dispose(); + if (widget.controller == null) _textEditingController.dispose(); + if (widget.focusNode == null) _focusNode.dispose(); + super.dispose(); } + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: _showSelector, + child: TextFormField( + focusNode: _focusNode, + style: widget.textStyle, + controller: _textEditingController, + validator: widget.validator, + minLines: 1, + onSaved: (value) => widget.onSave?.call(_selectedDate), + enabled: false, + decoration: widget.decoration, + ), + ); + } + Future _showSelector() async { DateTime? date; if (widget.type == DateTimeSelectionType.date) { date = await _showDateSelector(); - _textEditingController.text = - (date ?? _selectedDate).dateToStringWithFormat(format: "dd/MM/yyyy"); + + _textEditingController.text = (date ?? _selectedDate) + ?.dateToStringWithFormat(format: "dd/MM/yyyy") ?? + ''; } else { date = await _showTimeSelector(); + _textEditingController.text = - (date ?? _selectedDate).getTimeInFormat(TimeStampFormat.parse_12); + (date ?? _selectedDate)?.getTimeInFormat(TimeStampFormat.parse_12) ?? + ''; } - _selectedDate = date ?? DateTime.now(); + _selectedDate = date ?? _selectedDate; if (mounted) { setState(() {}); } - widget.onSelect?.call(date); + if (date != null) { + widget.onSelect?.call(date); + } } Future _showDateSelector() async { - final now = widget.minimumDateTime ?? DateTime.now(); - final date = await showDatePicker( context: context, - initialDate: now, - firstDate: widget.minimumDateTime ?? now, + initialDate: _selectedDate ?? DateTime.now(), + firstDate: widget.minimumDateTime ?? CalendarConstants.minDate, lastDate: CalendarConstants.maxDate, ); - if (date == null) return null; - return date; } Future _showTimeSelector() async { - final now = widget.minimumDateTime ?? DateTime.now(); + final now = _selectedDate ?? DateTime.now(); + final time = await showTimePicker( context: context, builder: (context, widget) { - return widget ?? Container(); + return widget ?? SizedBox.shrink(); }, - initialTime: TimeOfDay(hour: now.hour, minute: now.minute), + initialTime: TimeOfDay.fromDateTime(now), ); if (time == null) return null; @@ -125,24 +175,43 @@ class _DateTimeSelectorFormFieldState extends State { minute: time.minute, ); - if (widget.minimumDateTime == null) return date; - return date; } - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: _showSelector, - child: TextFormField( - style: widget.textStyle, - controller: _textEditingController, - validator: widget.validator, - minLines: 1, - onSaved: (value) => widget.onSave?.call(_selectedDate), - enabled: false, - decoration: widget.decoration, - ), - ); + void _setDates() { + _minimumDate = widget.minimumDateTime ?? CalendarConstants.minDate; + _selectedDate = widget.initialDateTime; + + switch (widget.type) { + case DateTimeSelectionType.date: + if (_selectedDate?.withoutTime.isBefore(_minimumDate.withoutTime) ?? + false) { + throw 'InitialDate is smaller than Minimum date'; + } + + // We are adding this to avoid internal error while + // rebuilding the widget. + WidgetsBinding.instance.addPostFrameCallback((_) { + _textEditingController.text = + _selectedDate?.dateToStringWithFormat(format: "dd/MM/yyyy") ?? ''; + }); + + break; + + case DateTimeSelectionType.time: + if (_selectedDate != null && + _selectedDate!.getTotalMinutes < _minimumDate.getTotalMinutes) { + throw 'InitialDate is smaller than Minimum date'; + } + + // We are adding this to avoid internal error while + // rebuilding the widget. + WidgetsBinding.instance.addPostFrameCallback((_) { + _textEditingController.text = + _selectedDate?.getTimeInFormat(TimeStampFormat.parse_12) ?? ''; + }); + + break; + } } } diff --git a/example/lib/widgets/day_view_widget.dart b/example/lib/widgets/day_view_widget.dart index 452ebea7..8a886327 100644 --- a/example/lib/widgets/day_view_widget.dart +++ b/example/lib/widgets/day_view_widget.dart @@ -1,21 +1,21 @@ import 'package:calendar_view/calendar_view.dart'; import 'package:flutter/material.dart'; -import '../model/event.dart'; +import '../pages/event_details_page.dart'; class DayViewWidget extends StatelessWidget { final GlobalKey? state; final double? width; const DayViewWidget({ - Key? key, + super.key, this.state, this.width, - }) : super(key: key); + }); @override Widget build(BuildContext context) { - return DayView( + return DayView( key: state, width: width, startDuration: Duration(hours: 8), @@ -25,6 +25,15 @@ class DayViewWidget extends StatelessWidget { hourIndicatorSettings: HourIndicatorSettings( color: Theme.of(context).dividerColor, ), + onEventTap: (events, date) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => DetailsPage( + event: events.first, + ), + ), + ); + }, halfHourIndicatorSettings: HourIndicatorSettings( color: Theme.of(context).dividerColor, lineStyle: LineStyle.dashed, diff --git a/example/lib/widgets/month_view_widget.dart b/example/lib/widgets/month_view_widget.dart index be6ee093..9f287382 100644 --- a/example/lib/widgets/month_view_widget.dart +++ b/example/lib/widgets/month_view_widget.dart @@ -1,23 +1,32 @@ import 'package:calendar_view/calendar_view.dart'; import 'package:flutter/material.dart'; -import '../model/event.dart'; +import '../pages/event_details_page.dart'; class MonthViewWidget extends StatelessWidget { final GlobalKey? state; final double? width; const MonthViewWidget({ - Key? key, + super.key, this.state, this.width, - }) : super(key: key); + }); @override Widget build(BuildContext context) { - return MonthView( + return MonthView( key: state, width: width, + onEventTap: (event, date) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => DetailsPage( + event: event, + ), + ), + ); + }, ); } } diff --git a/example/lib/widgets/responsive_widget.dart b/example/lib/widgets/responsive_widget.dart index d0467dcb..8c0984b0 100644 --- a/example/lib/widgets/responsive_widget.dart +++ b/example/lib/widgets/responsive_widget.dart @@ -9,12 +9,12 @@ class ResponsiveWidget extends StatelessWidget { final Widget mobileWidget; const ResponsiveWidget({ - Key? key, + super.key, this.width, this.breakPoint = BreakPoints.web, required this.webWidget, required this.mobileWidget, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/example/lib/widgets/week_view_widget.dart b/example/lib/widgets/week_view_widget.dart index b54623dd..adde1e00 100644 --- a/example/lib/widgets/week_view_widget.dart +++ b/example/lib/widgets/week_view_widget.dart @@ -1,19 +1,28 @@ import 'package:calendar_view/calendar_view.dart'; import 'package:flutter/material.dart'; -import '../model/event.dart'; +import '../pages/event_details_page.dart'; class WeekViewWidget extends StatelessWidget { final GlobalKey? state; final double? width; - const WeekViewWidget({Key? key, this.state, this.width}) : super(key: key); + const WeekViewWidget({super.key, this.state, this.width}); @override Widget build(BuildContext context) { - return WeekView( + return WeekView( key: state, width: width, + onEventTap: (events, date) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => DetailsPage( + event: events.first, + ), + ), + ); + }, ); } } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 80e5991f..0a1fba67 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -18,7 +18,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ^3.3.10 dependencies: flutter: diff --git a/lib/src/calendar_event_data.dart b/lib/src/calendar_event_data.dart index da9ac3b2..cb93b92e 100644 --- a/lib/src/calendar_event_data.dart +++ b/lib/src/calendar_event_data.dart @@ -4,10 +4,11 @@ import 'package:flutter/material.dart'; -import 'extensions.dart'; +import '../calendar_view.dart'; -/// Stores all the events on [date] @immutable + +/// {@macro calendar_event_data_doc} class CalendarEventData { /// Specifies date on which all these events are. final DateTime date; @@ -26,7 +27,7 @@ class CalendarEventData { final String title; /// Description of the event. - final String description; + final String? description; /// Defines color of event. /// This color will be used in default widgets provided by plugin. @@ -43,10 +44,11 @@ class CalendarEventData { /// Define style of description. final TextStyle? descriptionStyle; - /// Stores all the events on [date] - const CalendarEventData({ + /// {@macro calendar_event_data_doc} + CalendarEventData({ required this.title, - this.description = "", + required DateTime date, + this.description, this.event, this.color = Colors.blue, this.startTime, @@ -54,11 +56,43 @@ class CalendarEventData { this.titleStyle, this.descriptionStyle, DateTime? endDate, - required this.date, - }) : _endDate = endDate; + }) : _endDate = endDate?.withoutTime, + date = date.withoutTime; DateTime get endDate => _endDate ?? date; + /// If this flag returns true that means event is occurring on multiple + /// days and is not a full day event. + /// + bool get isRangingEvent { + final diff = endDate.withoutTime.difference(date.withoutTime).inDays; + + return diff > 0 && !isFullDayEvent; + } + + /// Returns if the events is full day event or not. + /// + /// If it returns true that means the events is full day. but also it can + /// span across multiple days. + /// + bool get isFullDayEvent { + return (startTime == null || + endTime == null || + (startTime!.isDayStart && endTime!.isDayStart)); + } + + /// Returns a boolean that defines whether current event is occurring on + /// [currentDate] or not. + /// + bool occursOnDate(DateTime currentDate) { + return currentDate == date || + currentDate == endDate || + (currentDate.isBefore(endDate.withoutTime) && + currentDate.isAfter(date.withoutTime)); + } + + /// Returns event data in [Map] format. + /// Map toJson() => { "date": date, "startTime": startTime, @@ -69,6 +103,35 @@ class CalendarEventData { "endDate": endDate, }; + /// Returns new object of [CalendarEventData] with the updated values defined + /// as the arguments. + /// + CalendarEventData copyWith({ + String? title, + String? description, + T? event, + Color? color, + DateTime? startTime, + DateTime? endTime, + TextStyle? titleStyle, + TextStyle? descriptionStyle, + DateTime? endDate, + DateTime? date, + }) { + return CalendarEventData( + title: title ?? this.title, + date: date ?? this.date, + startTime: startTime ?? this.startTime, + endTime: endTime ?? this.endTime, + color: color ?? this.color, + description: description ?? this.description, + descriptionStyle: descriptionStyle ?? this.descriptionStyle, + endDate: endDate ?? this.endDate, + event: event ?? this.event, + titleStyle: titleStyle ?? this.titleStyle, + ); + } + @override String toString() => '${toJson()}'; @@ -97,3 +160,27 @@ class CalendarEventData { @override int get hashCode => super.hashCode; } + +/// {@template calendar_event_data_doc} +/// Stores all the events on [date]. +/// +/// If [startTime] and [endTime] both are 0 or either of them is null, then +/// event will be considered a full day event. +/// +/// - [date] and [endDate] are used to define dates only. So, If you +/// are providing any time information with these two arguments, +/// it will be ignored. +/// +/// - [startTime] and [endTime] are used to define the time span of the event. +/// So, If you are providing any day information (year, month, day), it will +/// be ignored. It will also, consider only hour and minutes as time. So, +/// seconds, milliseconds and microseconds will be ignored as well. +/// +/// - [startTime] and [endTime] can not span more then one day. For example, +/// If start time is 11th Nov 11:30 PM and end time is 12th Nov 1:30 AM, it +/// will not be considered as valid time. Because for [startTime] and [endTime], +/// day will be ignored so, 11:30 PM ([startTime]) occurs after +/// 1:30 AM ([endTime]). Events with invalid time will throw +/// [AssertionError] in debug mode and will be ignored in release mode +/// in [DayView] and [WeekView]. +/// {@endtemplate} diff --git a/lib/src/components/_internal_components.dart b/lib/src/components/_internal_components.dart index b4d63695..f1658124 100644 --- a/lib/src/components/_internal_components.dart +++ b/lib/src/components/_internal_components.dart @@ -321,6 +321,7 @@ class EventGenerator extends StatelessWidget { @override Widget build(BuildContext context) { + // TODO: Use SizedBox If possible. return Container( height: height, width: width, diff --git a/lib/src/components/day_view_components.dart b/lib/src/components/day_view_components.dart index 2cda9a06..98eb623e 100644 --- a/lib/src/components/day_view_components.dart +++ b/lib/src/components/day_view_components.dart @@ -18,7 +18,7 @@ class RoundedEventTile extends StatelessWidget { final String title; /// Description of the tile. - final String description; + final String? description; /// Background color of tile. /// Default color is [Colors.blue] @@ -49,7 +49,7 @@ class RoundedEventTile extends StatelessWidget { required this.title, this.padding = EdgeInsets.zero, this.margin = EdgeInsets.zero, - this.description = "", + this.description, this.borderRadius = BorderRadius.zero, this.totalEvents = 1, this.backgroundColor = Colors.blue, @@ -83,12 +83,12 @@ class RoundedEventTile extends StatelessWidget { overflow: TextOverflow.fade, ), ), - if (description.isNotEmpty) + if (description?.isNotEmpty ?? false) Expanded( child: Padding( padding: const EdgeInsets.only(bottom: 15.0), child: Text( - description, + description!, style: descriptionStyle ?? TextStyle( fontSize: 17, diff --git a/lib/src/day_view/_internal_day_view_page.dart b/lib/src/day_view/_internal_day_view_page.dart index 00d32ccf..47c4fbc4 100644 --- a/lib/src/day_view/_internal_day_view_page.dart +++ b/lib/src/day_view/_internal_day_view_page.dart @@ -225,7 +225,10 @@ class InternalDayViewPage extends StatelessWidget { date: date, onTileTap: onTileTap, eventArranger: eventArranger, - events: controller.getEventsOnDay(date), + events: controller.getEventsOnDay( + date, + includeFullDayEvents: false, + ), heightPerMinute: heightPerMinute, eventTileBuilder: eventTileBuilder, scrollNotifier: scrollNotifier, diff --git a/lib/src/event_controller.dart b/lib/src/event_controller.dart index 4a395a51..8ac88bb4 100644 --- a/lib/src/event_controller.dart +++ b/lib/src/event_controller.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a MIT-style license // that can be found in the LICENSE file. +import 'dart:collection'; + import 'package:flutter/material.dart'; import 'calendar_event_data.dart'; @@ -27,7 +29,7 @@ class EventController extends ChangeNotifier { //#region Private Fields EventFilter? _eventFilter; - // Store all calendar event data + /// Store all calendar event data final CalendarData _calendarData = CalendarData(); //#endregion @@ -40,10 +42,20 @@ class EventController extends ChangeNotifier { // Note: Do not use this getter inside of EventController class. // use _eventList instead. /// Returns list of [CalendarEventData] stored in this controller. + @Deprecated('This is deprecated and will be removed in next major release. ' + 'Use allEvents instead.') + + /// Lists all the events that are added in the Controller. + /// + /// NOTE: This field is deprecated. use [allEvents] instead. List> get events => - _calendarData.eventList.toList(growable: false); + _calendarData.events.toList(growable: false); + + /// Lists all the events that are added in the Controller. + UnmodifiableListView> get allEvents => + _calendarData.events; - /// This method will provide list of events on particular date. + /// Defines which events should be displayed on given date. /// /// This method is use full when you have recurring events. /// As of now this library does not support recurring events. @@ -60,145 +72,260 @@ class EventController extends ChangeNotifier { /// Add all the events in the list /// If there is an event with same date then void addAll(List> events) { - for (final event in events) { - _addEvent(event); - } - + for (final event in events) _calendarData.addEvent(event); notifyListeners(); } /// Adds a single event in [_events] void add(CalendarEventData event) { - _addEvent(event); - + _calendarData.addEvent(event); notifyListeners(); } /// Removes [event] from this controller. void remove(CalendarEventData event) { - final date = event.date.withoutTime; - - // Removes the event from single event map. - if (_calendarData.events[date] != null) { - if (_calendarData.events[date]!.remove(event)) { - _calendarData.eventList.remove(event); - notifyListeners(); - return; - } - } + _calendarData.removeEvent(event); + notifyListeners(); + } + + /// Updates the [event] to have the data from [updated] event. + /// + /// If [event] is not found in the controller, it will add the [updated] + /// event in the controller. + /// + void update(CalendarEventData event, CalendarEventData updated) { + _calendarData.updateEvent(event, updated); + notifyListeners(); + } - // Removes the event from ranging or full day event. - _calendarData.eventList.remove(event); - _calendarData.rangingEventList.remove(event); - _calendarData.fullDayEventList.remove(event); + /// Removes all the [events] from this controller. + void removeAll(List> events) { + for (final event in events) _calendarData.removeEvent(event); notifyListeners(); } /// Removes multiple [event] from this controller. - void removeWhere(bool Function(CalendarEventData element) test) { - for (final e in _calendarData.events.values) { - e.removeWhere(test); - } - _calendarData.rangingEventList.removeWhere(test); - _calendarData.eventList.removeWhere(test); - _calendarData.fullDayEventList.removeWhere(test); + void removeWhere(TestPredicate> test) { + _calendarData.removeWhere(test); notifyListeners(); } /// Returns events on given day. /// /// To overwrite default behaviour of this function, - /// provide [_eventFilter] argument in [EventController] constructor. - List> getEventsOnDay(DateTime date) { + /// provide [eventFilter] argument in [EventController] constructor. + /// + /// if [includeFullDayEvents] is true, it will include full day events + /// as well else, it will exclude full day events. + /// + /// NOTE: If [eventFilter] is set i.e, not null, [includeFullDayEvents] will + /// have no effect. As what events to be included will be decided + /// by the [eventFilter]. + /// + /// To get full day events exclusively, check [getFullDayEvent] method. + /// + List> getEventsOnDay(DateTime date, + {bool includeFullDayEvents = true}) { + //ignore: deprecated_member_use_from_same_package if (_eventFilter != null) return _eventFilter!.call(date, this.events); - final events = >[]; - - if (_calendarData.events[date] != null) { - events.addAll(_calendarData.events[date]!); - } - - for (final rangingEvent in _calendarData.rangingEventList) { - if (date == rangingEvent.date || - date == rangingEvent.endDate || - (date.isBefore(rangingEvent.endDate) && - date.isAfter(rangingEvent.date))) { - events.add(rangingEvent); - } - } - - events.addAll(getFullDayEvent(date)); - - return events; + return _calendarData.getEventsOnDay(date.withoutTime, + includeFullDayEvents: includeFullDayEvents); } /// Returns full day events on given day. - List> getFullDayEvent(DateTime dateTime) { - final events = >[]; - for (final event in _calendarData.fullDayEventList) { - if (dateTime.difference(event.date).inDays >= 0 && - event.endDate.difference(dateTime).inDays > 0) { - events.add(event); - } - } - return events; + List> getFullDayEvent(DateTime date) { + return _calendarData.getFullDayEvent(date.withoutTime); } + /// Updates the [eventFilter]. + /// + /// This will also refresh the UI to reflect the latest event filter. void updateFilter({required EventFilter newFilter}) { if (newFilter != _eventFilter) { _eventFilter = newFilter; notifyListeners(); } } - //#endregion +} - //#region Private Methods - void _addEvent(CalendarEventData event) { +/// Stores the list of the calendar events. +/// +/// Provides basic data structure to store the events. +/// +/// Exposes methods to manipulate stored data. +/// +/// +class CalendarData { + /// Stores all the events in a list(all the items in below 3 list will be + /// available in this list as global itemList of all events). + final _eventList = >[]; + + UnmodifiableListView> get events => + UnmodifiableListView(_eventList); + + /// Stores events that occurs only once in a map, Here the key will a day + /// and along to the day as key we will store all the events of that day as + /// list as value + final _singleDayEvents = >>{}; + + UnmodifiableMapView>> + get singleDayEvents => UnmodifiableMapView( + Map.fromIterable( + _singleDayEvents.keys.map((key) { + return MapEntry( + key, + UnmodifiableListView( + _singleDayEvents[key] ?? [], + )); + }), + ), + ); + + /// Stores all the ranging events in a list + /// + /// Events that occurs on multiple day from startDate to endDate. + /// + final _rangingEventList = >[]; + UnmodifiableListView> get rangingEventList => + UnmodifiableListView(_rangingEventList); + + /// Stores all full day events(24hr event). + /// + /// This includes all full day events that are recurring day events as well. + /// + /// + final _fullDayEventList = >[]; + UnmodifiableListView> get fullDayEventList => + UnmodifiableListView(_fullDayEventList); + + //#region Data Manipulation Methods + void addFullDayEvent(CalendarEventData event) { + // TODO: add separate logic for adding full day event and ranging event. + _fullDayEventList.addEventInSortedManner(event); + _eventList.add(event); + } + + void addRangingEvent(CalendarEventData event) { + _rangingEventList.addEventInSortedManner(event); + _eventList.add(event); + } + + void addSingleDayEvent(CalendarEventData event) { + final date = event.date; + + if (_singleDayEvents[date] == null) { + _singleDayEvents.addAll({ + date: [event], + }); + } else { + _singleDayEvents[date]!.addEventInSortedManner(event); + } + + _eventList.add(event); + } + + void addEvent(CalendarEventData event) { assert(event.endDate.difference(event.date).inDays >= 0, 'The end date must be greater or equal to the start date'); - if (_calendarData.eventList.contains(event)) return; - if (event.endDate.difference(event.date).inDays > 0) { - if (event.startTime == null || - event.endTime == null || - (event.startTime!.isDayStart && event.endTime!.isDayStart)) { - _calendarData.fullDayEventList.addEventInSortedManner(event); - } else { - _calendarData.rangingEventList.addEventInSortedManner(event); - } + + // TODO: improve this... + if (_eventList.contains(event)) return; + + if (event.isFullDayEvent) { + addFullDayEvent(event); + } else if (event.isRangingEvent) { + addRangingEvent(event); } else { - final date = event.date.withoutTime; - - if (_calendarData.events[date] == null) { - _calendarData.events.addAll({ - date: [event], - }); - } else { - _calendarData.events[date]!.addEventInSortedManner(event); - } + addSingleDayEvent(event); } + } - _calendarData.eventList.add(event); + void removeFullDayEvent(CalendarEventData event) { + if (_fullDayEventList.remove(event)) { + _eventList.remove(event); + } + } - notifyListeners(); + void removeRangingEvent(CalendarEventData event) { + if (_rangingEventList.remove(event)) { + _eventList.remove(event); + } } -//#endregion -} + void removeSingleDayEvent(CalendarEventData event) { + if (_singleDayEvents[event.date]?.remove(event) ?? false) { + _eventList.remove(event); + } + } -class CalendarData { - // Stores all the events in a list(all the items in below 3 list will be - // available in this list as global itemList of all events). - final eventList = >[]; + void removeEvent(CalendarEventData event) { + if (event.isFullDayEvent) { + removeFullDayEvent(event); + } else if (event.isRangingEvent) { + removeRangingEvent(event); + } else { + removeSingleDayEvent(event); + } + } - // Stores events that occurs only once in a map, Here the key will a day - // and along to the day as key we will store all the events of that day as - // list as value - final events = >>{}; + void removeWhere(TestPredicate> test) { + final _predicates = , bool>{}; - // Stores all the ranging events in a list - final rangingEventList = >[]; + bool wrappedPredicate(CalendarEventData event) { + return _predicates[event] = test(event); + } + + for (final e in _singleDayEvents.values) { + e.removeWhere(wrappedPredicate); + } + + _rangingEventList.removeWhere(wrappedPredicate); + _fullDayEventList.removeWhere(wrappedPredicate); + + _eventList.removeWhere((event) => _predicates[event] ?? false); + } - // Stores all full day events(24hr event) - final fullDayEventList = >[]; + void updateEvent( + CalendarEventData oldEvent, CalendarEventData newEvent) { + removeEvent(oldEvent); + addEvent(newEvent); + } + //#endregion + + //#region Data Fetch Methods + List> getEventsOnDay(DateTime date, + {bool includeFullDayEvents = true}) { + final events = >[]; + + if (_singleDayEvents[date] != null) { + events.addAll(_singleDayEvents[date]!); + } + + for (final rangingEvent in _rangingEventList) { + if (rangingEvent.occursOnDate(date)) { + events.add(rangingEvent); + } + } + + if (includeFullDayEvents) { + events.addAll(getFullDayEvent(date)); + } + + return events; + } + + /// Returns full day events on given day. + List> getFullDayEvent(DateTime date) { + final events = >[]; + + for (final event in fullDayEventList) { + if (event.occursOnDate(date)) { + events.add(event); + } + } + return events; + } + //#endregion } diff --git a/lib/src/extensions.dart b/lib/src/extensions.dart index e97c842b..b26c67da 100644 --- a/lib/src/extensions.dart +++ b/lib/src/extensions.dart @@ -168,6 +168,7 @@ extension MinutesExtension on MinuteSlotSize { extension MyList on List { // Below function will add the new event in sorted manner(startTimeWise) in // the existing list of CalendarEventData. + void addEventInSortedManner(CalendarEventData event) { var addIndex = -1; diff --git a/lib/src/typedefs.dart b/lib/src/typedefs.dart index 61e72021..f517c492 100644 --- a/lib/src/typedefs.dart +++ b/lib/src/typedefs.dart @@ -84,3 +84,5 @@ typedef CustomHourLinePainter = CustomPainter Function( double dashSpaceWidth, double emulateVerticalOffsetBy, ); + +typedef TestPredicate = bool Function(T element); diff --git a/lib/src/week_view/_internal_week_view_page.dart b/lib/src/week_view/_internal_week_view_page.dart index 1cd2466e..97287571 100644 --- a/lib/src/week_view/_internal_week_view_page.dart +++ b/lib/src/week_view/_internal_week_view_page.dart @@ -336,8 +336,10 @@ class InternalWeekViewPage extends StatelessWidget { eventArranger: eventArranger, eventTileBuilder: eventTileBuilder, scrollNotifier: scrollConfiguration, - events: controller - .getEventsOnDay(filteredDates[index]), + events: controller.getEventsOnDay( + filteredDates[index], + includeFullDayEvents: false, + ), heightPerMinute: heightPerMinute, ), ],