diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt index e8cf4877..bbe4bed2 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -141,7 +141,7 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { createOrUpdateEvent(cachedValues.calendarId, cachedValues.event, cachedValues.pendingChannelResult) } DELETE_EVENT_REQUEST_CODE -> { - deleteEvent(cachedValues.eventId, cachedValues.calendarId, cachedValues.pendingChannelResult) + deleteEvent(cachedValues.calendarId, cachedValues.eventId, cachedValues.pendingChannelResult) } REQUEST_PERMISSIONS_REQUEST_CODE -> { finishWithSuccess(permissionGranted, cachedValues.pendingChannelResult) @@ -477,13 +477,9 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { calendar.set(java.util.Calendar.SECOND, 0) calendar.set(java.util.Calendar.MILLISECOND, 0) - // All day events must have UTC timezone - val utcTimeZone = TimeZone.getTimeZone("UTC") - calendar.timeZone = utcTimeZone - values.put(Events.DTSTART, calendar.timeInMillis) values.put(Events.DTEND, calendar.timeInMillis) - values.put(Events.EVENT_TIMEZONE, utcTimeZone.id) + values.put(Events.EVENT_TIMEZONE, getTimeZone(event.startTimeZone).id) } else { values.put(Events.DTSTART, event.start!!) values.put(Events.EVENT_TIMEZONE, getTimeZone(event.startTimeZone).id) diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 49dc1482..9179e557 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -17,7 +17,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion 29 - ndkVersion '21.4.7075529' + ndkVersion '22.1.7171670' sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/example/ios/.gitignore b/example/ios/.gitignore index 2a8c8b60..1c202be0 100755 --- a/example/ios/.gitignore +++ b/example/ios/.gitignore @@ -41,4 +41,5 @@ Icon? /Flutter/Generated.xcconfig /ServiceDefinitions.json +**/.symlinks/ Pods/ diff --git a/example/ios/.symlinks/plugins/device_calendar b/example/ios/.symlinks/plugins/device_calendar deleted file mode 120000 index 74d64261..00000000 --- a/example/ios/.symlinks/plugins/device_calendar +++ /dev/null @@ -1 +0,0 @@ -/Volumes/MacData/Codebase/Flutter/device_calendar/ \ No newline at end of file diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index e8f48f2d..9b39e647 100755 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2,20 +2,26 @@ PODS: - device_calendar (0.0.1): - Flutter - Flutter (1.0.0) + - flutter_native_timezone (0.0.1): + - Flutter DEPENDENCIES: - device_calendar (from `.symlinks/plugins/device_calendar/ios`) - Flutter (from `Flutter`) + - flutter_native_timezone (from `.symlinks/plugins/flutter_native_timezone/ios`) EXTERNAL SOURCES: device_calendar: :path: ".symlinks/plugins/device_calendar/ios" Flutter: :path: Flutter + flutter_native_timezone: + :path: ".symlinks/plugins/flutter_native_timezone/ios" SPEC CHECKSUMS: device_calendar: 23b28a5f1ab3bf77e34542fb1167e1b8b29a98f5 Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c + flutter_native_timezone: 5f05b2de06c9776b4cc70e1839f03de178394d22 PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index ad32b602..39cc0292 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -213,10 +213,12 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", "${BUILT_PRODUCTS_DIR}/device_calendar/device_calendar.framework", + "${BUILT_PRODUCTS_DIR}/flutter_native_timezone/flutter_native_timezone.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_calendar.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_native_timezone.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -431,7 +433,6 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - // change the id at debug to avoid error PRODUCT_BUNDLE_IDENTIFIER = com.builttoroam.deviceCalendarExample00; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -464,7 +465,6 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - // change the id at debug to avoid error PRODUCT_BUNDLE_IDENTIFIER = com.builttoroam.deviceCalendarExample00; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/example/lib/presentation/date_time_picker.dart b/example/lib/presentation/date_time_picker.dart index 8f9cd9c6..e36219c7 100644 --- a/example/lib/presentation/date_time_picker.dart +++ b/example/lib/presentation/date_time_picker.dart @@ -26,15 +26,19 @@ class DateTimePicker extends StatelessWidget { Future _selectDate(BuildContext context) async { final picked = await showDatePicker( context: context, - initialDate: selectedDate != null ? selectDate as DateTime : DateTime.now(), + initialDate: selectedDate != null + ? DateTime.parse(selectedDate.toString()) + : DateTime.now(), firstDate: DateTime(2015, 8), lastDate: DateTime(2101)); - if (picked != null && picked != selectedDate && selectDate != null) selectDate!(picked); + if (picked != null && picked != selectedDate && selectDate != null) + selectDate!(picked); } Future _selectTime(BuildContext context) async { - if(selectedTime == null) return; - final picked = await showTimePicker(context: context, initialTime: selectedTime!); + if (selectedTime == null) return; + final picked = + await showTimePicker(context: context, initialTime: selectedTime!); if (picked != null && picked != selectedTime) selectTime!(picked); } diff --git a/example/lib/presentation/event_item.dart b/example/lib/presentation/event_item.dart index b44d44ca..02c63229 100644 --- a/example/lib/presentation/event_item.dart +++ b/example/lib/presentation/event_item.dart @@ -3,8 +3,10 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'recurring_event_dialog.dart'; +import 'package:timezone/timezone.dart'; +import 'package:flutter_native_timezone/flutter_native_timezone.dart'; -class EventItem extends StatelessWidget { +class EventItem extends StatefulWidget { final Event? _calendarEvent; final DeviceCalendarPlugin _deviceCalendarPlugin; final bool _isReadOnly; @@ -13,21 +15,38 @@ class EventItem extends StatelessWidget { final VoidCallback _onLoadingStarted; final Function(bool) _onDeleteFinished; - final double _eventFieldNameWidth = 75.0; - EventItem( this._calendarEvent, this._deviceCalendarPlugin, this._onLoadingStarted, this._onDeleteFinished, this._onTapped, - this._isReadOnly); + this._isReadOnly, + {Key? key}) + : super(key: key); + + @override + _EventItemState createState() { + return _EventItemState(); + } +} + +class _EventItemState extends State { + final double _eventFieldNameWidth = 75.0; + Location? _currentLocation; + + @override + void initState() { + super.initState(); + setCurentLocation(); + } @override Widget build(BuildContext context) { return GestureDetector( onTap: () { - if(_calendarEvent != null) _onTapped(_calendarEvent as Event); + if (widget._calendarEvent != null) + widget._onTapped(widget._calendarEvent as Event); }, child: Card( child: Column( @@ -38,47 +57,54 @@ class EventItem extends StatelessWidget { child: FlutterLogo(), ), ListTile( - title: Text(_calendarEvent?.title ?? ''), - subtitle: Text(_calendarEvent?.description ?? '')), + title: Text(widget._calendarEvent?.title ?? ''), + subtitle: Text(widget._calendarEvent?.description ?? '')), Container( padding: EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - Container( - width: _eventFieldNameWidth, - child: Text('Starts'), - ), - Text(_calendarEvent == null - ? '' - : DateFormat.yMd() - .add_jm() - .format(_calendarEvent?.start as DateTime)), - ], + if (_currentLocation != null) + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + Container( + width: _eventFieldNameWidth, + child: Text('Starts'), + ), + Text( + widget._calendarEvent == null + ? '' + : DateFormat('yyyy-MM-dd HH:mm:ss').format( + TZDateTime.from( + widget._calendarEvent!.start!, + _currentLocation!)), + ) + ], + ), ), - ), Padding( padding: EdgeInsets.symmetric(vertical: 5.0), ), - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - Container( - width: _eventFieldNameWidth, - child: Text('Ends'), - ), - Text(_calendarEvent?.end == null - ? '' - : DateFormat.yMd() - .add_jm() - .format(_calendarEvent?.end as DateTime)), - ], + if (_currentLocation != null) + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + Container( + width: _eventFieldNameWidth, + child: Text('Ends'), + ), + Text( + widget._calendarEvent?.end == null + ? '' + : DateFormat('yyyy-MM-dd HH:mm:ss').format( + TZDateTime.from(widget._calendarEvent!.end!, + _currentLocation!)), + ), + ], + ), ), - ), SizedBox( height: 10.0, ), @@ -90,8 +116,8 @@ class EventItem extends StatelessWidget { width: _eventFieldNameWidth, child: Text('All day?'), ), - Text(_calendarEvent?.allDay != null && - _calendarEvent?.allDay == true + Text(widget._calendarEvent?.allDay != null && + widget._calendarEvent?.allDay == true ? 'Yes' : 'No') ], @@ -110,7 +136,7 @@ class EventItem extends StatelessWidget { ), Expanded( child: Text( - _calendarEvent?.location ?? '', + widget._calendarEvent?.location ?? '', overflow: TextOverflow.ellipsis, ), ) @@ -130,7 +156,7 @@ class EventItem extends StatelessWidget { ), Expanded( child: Text( - _calendarEvent?.url?.data?.contentText ?? '', + widget._calendarEvent?.url?.data?.contentText ?? '', overflow: TextOverflow.ellipsis, ), ) @@ -150,8 +176,11 @@ class EventItem extends StatelessWidget { ), Expanded( child: Text( - _calendarEvent?.attendees?.where((a) => a?.name?.isNotEmpty ?? false).map((a) => a?.name).join(', ') - ?? '', + widget._calendarEvent?.attendees + ?.where((a) => a?.name?.isNotEmpty ?? false) + .map((a) => a?.name) + .join(', ') ?? + '', overflow: TextOverflow.ellipsis, ), ) @@ -171,7 +200,8 @@ class EventItem extends StatelessWidget { ), Expanded( child: Text( - _calendarEvent?.availability?.enumToString ?? '', + widget._calendarEvent?.availability?.enumToString ?? + '', overflow: TextOverflow.ellipsis, ), ) @@ -183,10 +213,11 @@ class EventItem extends StatelessWidget { ), ButtonBar( children: [ - if (!_isReadOnly) ...[ + if (!widget._isReadOnly) ...[ IconButton( onPressed: () { - if(_calendarEvent != null) _onTapped(_calendarEvent as Event); + if (widget._calendarEvent != null) + widget._onTapped(widget._calendarEvent as Event); }, icon: Icon(Icons.edit), ), @@ -196,7 +227,7 @@ class EventItem extends StatelessWidget { context: context, barrierDismissible: false, builder: (BuildContext context) { - if (_calendarEvent?.recurrenceRule == null) { + if (widget._calendarEvent?.recurrenceRule == null) { return AlertDialog( title: Text( 'Are you sure you want to delete this event?'), @@ -210,25 +241,28 @@ class EventItem extends StatelessWidget { TextButton( onPressed: () async { Navigator.of(context).pop(); - _onLoadingStarted(); - final deleteResult = - await _deviceCalendarPlugin.deleteEvent( - _calendarEvent?.calendarId, - _calendarEvent?.eventId); - _onDeleteFinished(deleteResult.isSuccess && - deleteResult.data != null); + widget._onLoadingStarted(); + final deleteResult = await widget + ._deviceCalendarPlugin + .deleteEvent( + widget._calendarEvent?.calendarId, + widget._calendarEvent?.eventId); + widget._onDeleteFinished( + deleteResult.isSuccess && + deleteResult.data != null); }, child: Text('Delete'), ), ], ); } else { - if(_calendarEvent == null) return SizedBox(); + if (widget._calendarEvent == null) + return SizedBox(); return RecurringEventDialog( - _deviceCalendarPlugin, - _calendarEvent!, - _onLoadingStarted, - _onDeleteFinished); + widget._deviceCalendarPlugin, + widget._calendarEvent!, + widget._onLoadingStarted, + widget._onDeleteFinished); } }, ); @@ -238,7 +272,8 @@ class EventItem extends StatelessWidget { ] else ...[ IconButton( onPressed: () { - if(_calendarEvent != null) _onTapped(_calendarEvent!); + if (widget._calendarEvent != null) + widget._onTapped(widget._calendarEvent!); }, icon: Icon(Icons.remove_red_eye), ), @@ -250,4 +285,16 @@ class EventItem extends StatelessWidget { ), ); } + + void setCurentLocation() async { + String? timezone; + try { + timezone = await FlutterNativeTimezone.getLocalTimezone(); + } catch (e) { + print('Could not get the local timezone'); + } + timezone ??= 'Etc/UTC'; + _currentLocation = timeZoneDatabase.locations[timezone]; + setState(() {}); + } } diff --git a/example/lib/presentation/input_dropdown.dart b/example/lib/presentation/input_dropdown.dart index 0a416bee..a6c19820 100644 --- a/example/lib/presentation/input_dropdown.dart +++ b/example/lib/presentation/input_dropdown.dart @@ -29,7 +29,7 @@ class InputDropdown extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min, children: [ - if(valueText != null) Text(valueText!, style: valueStyle), + if (valueText != null) Text(valueText!, style: valueStyle), Icon(Icons.arrow_drop_down, color: Theme.of(context).brightness == Brightness.light ? Colors.grey.shade700 diff --git a/example/lib/presentation/pages/calendar_add.dart b/example/lib/presentation/pages/calendar_add.dart index a3025966..519928eb 100644 --- a/example/lib/presentation/pages/calendar_add.dart +++ b/example/lib/presentation/pages/calendar_add.dart @@ -106,7 +106,7 @@ class _CalendarAddPageState extends State { } String? _validateCalendarName(String? value) { - if(value == null) return null; + if (value == null) return null; if (value.isEmpty) { return 'Calendar name is required.'; } diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 6a8f3512..d216f1c6 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -10,6 +10,8 @@ import '../date_time_picker.dart'; import '../recurring_event_dialog.dart'; import 'event_attendee.dart'; import 'event_reminders.dart'; +import 'package:timezone/timezone.dart'; +import 'package:flutter_native_timezone/flutter_native_timezone.dart'; enum RecurrenceRuleEndType { Indefinite, MaxOccurrences, SpecifiedEndDate } @@ -35,10 +37,10 @@ class _CalendarEventPageState extends State { late DeviceCalendarPlugin _deviceCalendarPlugin; final RecurringEventDialog? _recurringEventDialog; - late DateTime _startDate; + TZDateTime? _startDate; late TimeOfDay _startTime; - late DateTime _endDate; + TZDateTime? _endDate; late TimeOfDay _endTime; bool _autovalidate = false; @@ -53,17 +55,28 @@ class _CalendarEventPageState extends State { RecurrenceFrequency? _recurrenceFrequency = RecurrenceFrequency.Daily; List _daysOfWeek = []; int? _dayOfMonth; - List _validDaysOfMonth = []; + final List _validDaysOfMonth = []; MonthOfYear? _monthOfYear; WeekNumber? _weekOfMonth; DayOfWeek? _selectedDayOfWeek = DayOfWeek.Monday; - Availability? _availability = Availability.Busy; + Availability _availability = Availability.Busy; List _attendees = []; List _reminders = []; + String _timezone = 'Etc/UTC'; _CalendarEventPageState( this._calendar, this._event, this._recurringEventDialog) { + getCurentLocation(); + } + + void getCurentLocation() async { + try { + _timezone = await FlutterNativeTimezone.getLocalTimezone(); + } catch (e) { + print('Could not get the local timezone'); + } + _deviceCalendarPlugin = DeviceCalendarPlugin(); _attendees = []; @@ -71,11 +84,22 @@ class _CalendarEventPageState extends State { _recurrenceRuleEndType = RecurrenceRuleEndType.Indefinite; if (_event == null) { - _startDate = DateTime.now(); - _endDate = DateTime.now().add(Duration(hours: 1)); - _event = Event(_calendar.id, start: _startDate, end: _endDate); + print('calendar_event _timezone ------------------------- $_timezone'); + var currentLocation = timeZoneDatabase.locations[_timezone]; + if (currentLocation != null) { + _startDate = TZDateTime.now(currentLocation); + _endDate = TZDateTime.now(currentLocation).add(Duration(hours: 1)); + } else { + var fallbackLocation = timeZoneDatabase.locations['Etc/UTC']; + _startDate = TZDateTime.now(fallbackLocation!); + _endDate = TZDateTime.now(fallbackLocation).add(Duration(hours: 1)); + } + _event = Event(_calendar.id, + start: _startDate, end: _endDate, availability: Availability.Busy); - _recurrenceEndDate = _endDate; + print('DeviceCalendarPlugin calendar id is: ${_calendar.id}'); + + _recurrenceEndDate = _endDate as DateTime; _dayOfMonth = 1; _monthOfYear = MonthOfYear.January; _weekOfMonth = WeekNumber.First; @@ -109,10 +133,11 @@ class _CalendarEventPageState extends State { _isByDayOfMonth = _event?.recurrenceRule?.weekOfMonth == null; _daysOfWeek = _event?.recurrenceRule?.daysOfWeek ?? []; - _monthOfYear = _event?.recurrenceRule?.monthOfYear ?? MonthOfYear.January; + _monthOfYear = + _event?.recurrenceRule?.monthOfYear ?? MonthOfYear.January; _weekOfMonth = _event?.recurrenceRule?.weekOfMonth ?? WeekNumber.First; _selectedDayOfWeek = - _daysOfWeek.isNotEmpty ? _daysOfWeek.first : DayOfWeek.Monday; + _daysOfWeek.isNotEmpty ? _daysOfWeek.first : DayOfWeek.Monday; _dayOfMonth = _event?.recurrenceRule?.dayOfMonth ?? 1; if (_daysOfWeek.isNotEmpty) { @@ -120,23 +145,24 @@ class _CalendarEventPageState extends State { } } - _availability = _event?.availability; + _availability = _event!.availability; } - _startTime = TimeOfDay(hour: _startDate.hour, minute: _startDate.minute); - _endTime = TimeOfDay(hour: _endDate.hour, minute: _endDate.minute); + _startTime = TimeOfDay(hour: _startDate!.hour, minute: _startDate!.minute); + _endTime = TimeOfDay(hour: _endDate!.hour, minute: _endDate!.minute); // Getting days of the current month (or a selected month for the yearly recurrence) as a default _getValidDaysOfMonth(_recurrenceFrequency); + setState(() {}); } void printAttendeeDetails(Attendee attendee) { print( - 'attendee name: ${attendee.name}, email address: ${attendee.emailAddress}, type: ${attendee.iosAttendeeDetails?.role?.enumToString}'); + 'attendee name: ${attendee.name}, email address: ${attendee.emailAddress}, type: ${attendee.role?.enumToString}'); print( - 'ios specifics - status: ${attendee.iosAttendeeDetails?.attendanceStatus}, type: ${attendee.iosAttendeeDetails?.role?.enumToString}'); + 'ios specifics - status: ${attendee.iosAttendeeDetails?.attendanceStatus}, type: ${attendee.iosAttendeeDetails?.attendanceStatus?.enumToString}'); print( - 'android specifics - status ${attendee.androidAttendeeDetails?.attendanceStatus}, type: ${attendee.androidAttendeeDetails?.role?.enumToString}'); + 'android specifics - status ${attendee.androidAttendeeDetails?.attendanceStatus}, type: ${attendee.androidAttendeeDetails?.attendanceStatus?.enumToString}'); } @override @@ -205,7 +231,7 @@ class _CalendarEventPageState extends State { decoration: const InputDecoration( labelText: 'URL', hintText: 'https://google.com'), onSaved: (String? value) { - if(value != null) { + if (value != null) { var uri = Uri.dataFromString(value); _event?.url = uri; } @@ -221,8 +247,10 @@ class _CalendarEventPageState extends State { value: _availability, onChanged: (Availability? newValue) { setState(() { - _availability = newValue; - _event?.availability = newValue; + if (newValue != null) { + _availability = newValue; + _event?.availability = newValue; + } }); }, items: Availability.values @@ -241,42 +269,48 @@ class _CalendarEventPageState extends State { setState(() => _event?.allDay = value), title: Text('All Day'), ), - Padding( - padding: const EdgeInsets.all(10.0), - child: DateTimePicker( - labelText: 'From', - enableTime: _event?.allDay == false, - selectedDate: _startDate, - selectedTime: _startTime, - selectDate: (DateTime date) { - setState(() { - _startDate = date; - _event?.start = - _combineDateWithTime(_startDate, _startTime); - }); - }, - selectTime: (TimeOfDay time) { - setState( - () { - _startTime = time; - _event?.start = - _combineDateWithTime(_startDate, _startTime); - }, - ); - }, + if (_startDate != null) + Padding( + padding: const EdgeInsets.all(10.0), + child: DateTimePicker( + labelText: 'From', + enableTime: _event?.allDay == false, + selectedDate: _startDate, + selectedTime: _startTime, + selectDate: (DateTime date) { + setState(() { + var currentLocation = + timeZoneDatabase.locations[_timezone]; + if (currentLocation != null) { + _startDate = + TZDateTime.from(date, currentLocation); + _event?.start = _combineDateWithTime( + _startDate, _startTime); + } + }); + }, + selectTime: (TimeOfDay time) { + setState( + () { + _startTime = time; + _event?.start = _combineDateWithTime( + _startDate, _startTime); + }, + ); + }, + ), ), - ), if (_event?.allDay == false) ...[ if (Platform.isAndroid) Padding( padding: const EdgeInsets.all(10.0), child: TextFormField( - initialValue: _event?.startTimeZone, + initialValue: _event?.start?.location.name, decoration: const InputDecoration( labelText: 'Start date time zone', hintText: 'Australia/Sydney'), onSaved: (String? value) { - _event?.startTimeZone = value; + _event?.updateStartLocation(value); }, ), ), @@ -289,9 +323,14 @@ class _CalendarEventPageState extends State { selectDate: (DateTime date) { setState( () { - _endDate = date; - _event?.end = - _combineDateWithTime(_endDate, _endTime); + var currentLocation = + timeZoneDatabase.locations[_timezone]; + if (currentLocation != null) { + _endDate = + TZDateTime.from(date, currentLocation); + _event?.end = + _combineDateWithTime(_endDate, _endTime); + } }, ); }, @@ -309,17 +348,12 @@ class _CalendarEventPageState extends State { Padding( padding: const EdgeInsets.all(10.0), child: TextFormField( - initialValue: Platform.isAndroid - ? _event?.endTimeZone - : _event?.startTimeZone, + initialValue: _event?.end?.location.name, decoration: InputDecoration( - labelText: Platform.isAndroid - ? 'End date time zone' - : 'Start and end time zone', + labelText: 'End date time zone', hintText: 'Australia/Sydney'), - onSaved: (String? value) => Platform.isAndroid - ? _event?.endTimeZone = value - : _event?.startTimeZone = value, + onSaved: (String? value) => + _event?.updateEndLocation(value), ), ), ], @@ -364,14 +398,13 @@ class _CalendarEventPageState extends State { var result = await Navigator.push( context, MaterialPageRoute( - builder: (context) => - EventAttendeePage( - attendee: - _attendees[index]))); + builder: (context) => EventAttendeePage( + attendee: _attendees[index]))); if (result == null) return; _attendees[index] = result; }, - child: Text('${_attendees[index].emailAddress}'),), + child: Text('${_attendees[index].emailAddress}'), + ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -481,7 +514,8 @@ class _CalendarEventPageState extends State { validator: _validateInterval, textAlign: TextAlign.right, onSaved: (String? value) { - if(value != null) _interval = int.tryParse(value); + if (value != null) + _interval = int.tryParse(value); }, ), ), @@ -518,7 +552,8 @@ class _CalendarEventPageState extends State { groupValue: _dayOfWeekGroup, onChanged: (selected) { setState(() { - _dayOfWeekGroup = selected as DayOfWeekGroup; + _dayOfWeekGroup = + selected as DayOfWeekGroup; _updateDaysOfWeek(); }); }, @@ -591,10 +626,15 @@ class _CalendarEventPageState extends State { padding: const EdgeInsets.fromLTRB(15, 10, 15, 10), child: Align( alignment: Alignment.centerLeft, - child: _recurrenceFrequencyToText(_recurrenceFrequency).data != null - ? Text(_recurrenceFrequencyToText(_recurrenceFrequency).data! + ' on the ') - : Text('') - ), + child: _recurrenceFrequencyToText( + _recurrenceFrequency) + .data != + null + ? Text(_recurrenceFrequencyToText( + _recurrenceFrequency) + .data! + + ' on the ') + : Text('')), ), Padding( padding: const EdgeInsets.fromLTRB(15, 0, 15, 10), @@ -624,7 +664,10 @@ class _CalendarEventPageState extends State { _selectedDayOfWeek = value; }); }, - value: _selectedDayOfWeek != null? DayOfWeek.values[_selectedDayOfWeek!.index] : DayOfWeek.values[0], + value: _selectedDayOfWeek != null + ? DayOfWeek + .values[_selectedDayOfWeek!.index] + : DayOfWeek.values[0], items: DayOfWeek.values .map((day) => DropdownMenuItem( value: day, @@ -696,7 +739,8 @@ class _CalendarEventPageState extends State { validator: _validateTotalOccurrences, textAlign: TextAlign.right, onSaved: (String? value) { - if(value != null) _totalOccurrences = int.tryParse(value); + if (value != null) + _totalOccurrences = int.tryParse(value); }, ), ), @@ -728,9 +772,7 @@ class _CalendarEventPageState extends State { ElevatedButton( key: Key('deleteEventButton'), style: ElevatedButton.styleFrom( - primary: Colors.red, - onPrimary: Colors.white - ), + primary: Colors.red, onPrimary: Colors.white), onPressed: () async { bool? result = true; if (!_isRecurringEvent) { @@ -741,7 +783,9 @@ class _CalendarEventPageState extends State { context: context, barrierDismissible: false, builder: (BuildContext context) { - return _recurringEventDialog != null ? _recurringEventDialog as Widget : SizedBox(); + return _recurringEventDialog != null + ? _recurringEventDialog as Widget + : SizedBox(); }); } @@ -773,14 +817,18 @@ class _CalendarEventPageState extends State { _recurrenceFrequency == RecurrenceFrequency.Yearly)) { // Setting day of the week parameters for WeekNumber to avoid clashing with the weekly recurrence values _daysOfWeek.clear(); - if(_selectedDayOfWeek != null) _daysOfWeek.add(_selectedDayOfWeek as DayOfWeek); + if (_selectedDayOfWeek != null) + _daysOfWeek.add(_selectedDayOfWeek as DayOfWeek); } else { _weekOfMonth = null; } _event?.recurrenceRule = RecurrenceRule(_recurrenceFrequency, interval: _interval, - totalOccurrences: _totalOccurrences, + totalOccurrences: (_recurrenceRuleEndType == + RecurrenceRuleEndType.MaxOccurrences) + ? _totalOccurrences + : null, endDate: _recurrenceRuleEndType == RecurrenceRuleEndType.SpecifiedEndDate ? _recurrenceEndDate @@ -825,7 +873,8 @@ class _CalendarEventPageState extends State { } } - Text _recurrenceFrequencyToIntervalText(RecurrenceFrequency? recurrenceFrequency) { + Text _recurrenceFrequencyToIntervalText( + RecurrenceFrequency? recurrenceFrequency) { switch (recurrenceFrequency) { case RecurrenceFrequency.Daily: return Text(' Day(s)'); @@ -860,7 +909,9 @@ class _CalendarEventPageState extends State { // Year frequency: Get total days of the selected month if (frequency == RecurrenceFrequency.Yearly) { - totalDays = DateTime(DateTime.now().year, _monthOfYear?.value != null ? _monthOfYear!.value + 1 : 1, 0).day; + totalDays = DateTime(DateTime.now().year, + _monthOfYear?.value != null ? _monthOfYear!.value + 1 : 1, 0) + .day; } else { // Otherwise, get total days of the current month var now = DateTime.now(); @@ -873,7 +924,7 @@ class _CalendarEventPageState extends State { } void _updateDaysOfWeek() { - if(_dayOfWeekGroup == null) return; + if (_dayOfWeekGroup == null) return; var days = _dayOfWeekGroup!.getDays; switch (_dayOfWeekGroup) { @@ -886,7 +937,8 @@ class _CalendarEventPageState extends State { case DayOfWeekGroup.None: _daysOfWeek.clear(); break; - default: _daysOfWeek.clear(); + default: + _daysOfWeek.clear(); } } @@ -919,7 +971,7 @@ class _CalendarEventPageState extends State { } String? _validateTotalOccurrences(String? value) { - if(value == null) return null; + if (value == null) return null; if (value.isNotEmpty && int.tryParse(value) == null) { return 'Total occurrences needs to be a valid number'; } @@ -927,7 +979,7 @@ class _CalendarEventPageState extends State { } String? _validateInterval(String? value) { - if(value == null) return null; + if (value == null) return null; if (value.isNotEmpty && int.tryParse(value) == null) { return 'Interval needs to be a valid number'; } @@ -935,7 +987,7 @@ class _CalendarEventPageState extends State { } String? _validateTitle(String? value) { - if(value == null) return null; + if (value == null) return null; if (value.isEmpty) { return 'Name is required.'; } @@ -943,14 +995,18 @@ class _CalendarEventPageState extends State { return null; } - DateTime? _combineDateWithTime(DateTime? date, TimeOfDay? time) { + TZDateTime? _combineDateWithTime(TZDateTime? date, TimeOfDay? time) { if (date == null) return null; + var currentLocation = timeZoneDatabase.locations[_timezone]; - final dateWithoutTime = DateTime.parse(DateFormat('y-MM-dd 00:00:00').format(date)); + final dateWithoutTime = TZDateTime.from( + DateTime.parse(DateFormat('y-MM-dd 00:00:00').format(date)), + currentLocation!); if (time == null) return dateWithoutTime; - return dateWithoutTime.add(Duration(hours: time.hour, minutes: time.minute)); + return dateWithoutTime + .add(Duration(hours: time.hour, minutes: time.minute)); } void showInSnackBar(String value) { diff --git a/example/lib/presentation/pages/calendar_events.dart b/example/lib/presentation/pages/calendar_events.dart index d5b39ddb..2172b965 100644 --- a/example/lib/presentation/pages/calendar_events.dart +++ b/example/lib/presentation/pages/calendar_events.dart @@ -40,9 +40,10 @@ class _CalendarEventsPageState extends State { Widget build(BuildContext context) { return Scaffold( key: _scaffoldstate, - appBar: AppBar(title: Text('${_calendar.name} events'),actions: [ - _getDeleteButton() - ],), + appBar: AppBar( + title: Text('${_calendar.name} events'), + actions: [_getDeleteButton()], + ), body: (_calendarEvents.isNotEmpty || _isLoading) ? Stack( children: [ @@ -55,7 +56,8 @@ class _CalendarEventsPageState extends State { _onLoading, _onDeletedFinished, _onTapped, - _calendar.isReadOnly != null && _calendar.isReadOnly as bool); + _calendar.isReadOnly != null && + _calendar.isReadOnly as bool); }, ), if (_isLoading) @@ -98,7 +100,6 @@ class _CalendarEventsPageState extends State { if (deleteSucceeded) { await _retrieveCalendarEvents(); } else { - _scaffoldstate.currentState!.showSnackBar(SnackBar( content: Text('Oops, we ran into an issue deleting the event'), backgroundColor: Colors.red, @@ -166,8 +167,10 @@ class _CalendarEventsPageState extends State { actions: [ TextButton( onPressed: () async { - var returnValue = await _deviceCalendarPlugin.deleteCalendar(_calendar.id!); - print('returnValue: ${returnValue.data}, ${returnValue.errors}'); + var returnValue = + await _deviceCalendarPlugin.deleteCalendar(_calendar.id!); + print( + 'returnValue: ${returnValue.data}, ${returnValue.errors}'); Navigator.of(context).pop(); Navigator.of(context).pop(); }, @@ -185,5 +188,3 @@ class _CalendarEventsPageState extends State { ); } } - - diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index fcd7eeeb..e28dbc7f 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -38,9 +38,7 @@ class _CalendarsPageState extends State { return Scaffold( appBar: AppBar( title: Text('Calendars'), - actions: [ - _getRefreshButton() - ], + actions: [_getRefreshButton()], ), body: Column( children: [ @@ -124,9 +122,13 @@ class _CalendarsPageState extends State { void _retrieveCalendars() async { try { var permissionsGranted = await _deviceCalendarPlugin.hasPermissions(); - if (permissionsGranted.isSuccess && permissionsGranted.data == null) { + if (permissionsGranted.isSuccess && + (permissionsGranted.data == null || + permissionsGranted.data == false)) { permissionsGranted = await _deviceCalendarPlugin.requestPermissions(); - if (!permissionsGranted.isSuccess || permissionsGranted.data != null) { + if (!permissionsGranted.isSuccess || + permissionsGranted.data == null || + permissionsGranted.data == false) { return; } } diff --git a/example/lib/presentation/recurring_event_dialog.dart b/example/lib/presentation/recurring_event_dialog.dart index 7cc6d4db..bb667c28 100644 --- a/example/lib/presentation/recurring_event_dialog.dart +++ b/example/lib/presentation/recurring_event_dialog.dart @@ -43,7 +43,7 @@ class _RecurringEventDialogState extends State { SimpleDialogOption( onPressed: () async { Navigator.of(context).pop(true); - if(_onLoadingStarted != null) _onLoadingStarted!(); + if (_onLoadingStarted != null) _onLoadingStarted!(); final deleteResult = await _deviceCalendarPlugin.deleteEventInstance( _calendarEvent.calendarId, @@ -51,14 +51,16 @@ class _RecurringEventDialogState extends State { _calendarEvent.start?.millisecondsSinceEpoch, _calendarEvent.end?.millisecondsSinceEpoch, false); - if(_onDeleteFinished != null) _onDeleteFinished!(deleteResult.isSuccess && deleteResult.data != null); + if (_onDeleteFinished != null) + _onDeleteFinished!( + deleteResult.isSuccess && deleteResult.data != null); }, child: Text('This instance only'), ), SimpleDialogOption( onPressed: () async { Navigator.of(context).pop(true); - if(_onLoadingStarted != null) _onLoadingStarted!(); + if (_onLoadingStarted != null) _onLoadingStarted!(); final deleteResult = await _deviceCalendarPlugin.deleteEventInstance( _calendarEvent.calendarId, @@ -66,17 +68,21 @@ class _RecurringEventDialogState extends State { _calendarEvent.start?.millisecondsSinceEpoch, _calendarEvent.end?.millisecondsSinceEpoch, true); - if(_onDeleteFinished != null) _onDeleteFinished!(deleteResult.isSuccess && deleteResult.data != null); + if (_onDeleteFinished != null) + _onDeleteFinished!( + deleteResult.isSuccess && deleteResult.data != null); }, child: Text('This and following instances'), ), SimpleDialogOption( onPressed: () async { Navigator.of(context).pop(true); - if(_onLoadingStarted != null) _onLoadingStarted!(); + if (_onLoadingStarted != null) _onLoadingStarted!(); final deleteResult = await _deviceCalendarPlugin.deleteEvent( _calendarEvent.calendarId, _calendarEvent.eventId); - if(_onDeleteFinished != null) _onDeleteFinished!(deleteResult.isSuccess && deleteResult.data != null); + if (_onDeleteFinished != null) + _onDeleteFinished!( + deleteResult.isSuccess && deleteResult.data != null); }, child: Text('All instances'), ), diff --git a/example/test_driver/app_test.dart b/example/test_driver/app_test.dart index 4a4c895d..8c9afeae 100644 --- a/example/test_driver/app_test.dart +++ b/example/test_driver/app_test.dart @@ -16,7 +16,9 @@ void main() { // workaround for handling permissions based on info taken from https://github.com/flutter/flutter/issues/12561 // this is to be run in a Mac environment final envVars = Platform.environment; - final adbPath = envVars['ANDROID_HOME'] != null ? envVars['ANDROID_HOME']! + '/platform-tools/adb' : ''; + final adbPath = envVars['ANDROID_HOME'] != null + ? envVars['ANDROID_HOME']! + '/platform-tools/adb' + : ''; await Process.run(adbPath, [ 'shell', 'pm', diff --git a/lib/device_calendar.dart b/lib/device_calendar.dart index 85d3390d..15d51182 100644 --- a/lib/device_calendar.dart +++ b/lib/device_calendar.dart @@ -10,5 +10,7 @@ export 'src/models/event.dart'; export 'src/models/retrieve_events_params.dart'; export 'src/models/recurrence_rule.dart'; export 'src/models/platform_specifics/ios/attendee_details.dart'; +export 'src/models/platform_specifics/ios/attendance_status.dart'; export 'src/models/platform_specifics/android/attendee_details.dart'; +export 'src/models/platform_specifics/android/attendance_status.dart'; export 'src/device_calendar.dart'; diff --git a/lib/src/device_calendar.dart b/lib/src/device_calendar.dart index 9e6e98cf..7b90e662 100644 --- a/lib/src/device_calendar.dart +++ b/lib/src/device_calendar.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:meta/meta.dart'; import 'package:sprintf/sprintf.dart'; +import 'package:timezone/timezone.dart'; import 'common/channel_constants.dart'; import 'common/error_codes.dart'; @@ -14,6 +15,8 @@ import 'models/event.dart'; import 'models/result.dart'; import 'models/retrieve_events_params.dart'; +import 'package:timezone/data/latest.dart' as tz; + /// Provides functionality for working with device calendar(s) class DeviceCalendarPlugin { static const MethodChannel channel = @@ -22,6 +25,7 @@ class DeviceCalendarPlugin { static final DeviceCalendarPlugin _instance = DeviceCalendarPlugin.private(); factory DeviceCalendarPlugin() { + tz.initializeTimeZones(); return _instance; } @@ -90,15 +94,19 @@ class DeviceCalendarPlugin { retrieveEventsParams?.endDate == null) || (retrieveEventsParams?.startDate != null && retrieveEventsParams?.endDate != null && - (retrieveEventsParams != null && retrieveEventsParams.startDate!.isAfter(retrieveEventsParams.endDate!))))), + (retrieveEventsParams != null && + retrieveEventsParams.startDate! + .isAfter(retrieveEventsParams.endDate!))))), ErrorCodes.invalidArguments, ErrorMessages.invalidRetrieveEventsParams, ); }, arguments: () => { ChannelConstants.parameterNameCalendarId: calendarId, - ChannelConstants.parameterNameStartDate: retrieveEventsParams?.startDate?.millisecondsSinceEpoch, - ChannelConstants.parameterNameEndDate: retrieveEventsParams?.endDate?.millisecondsSinceEpoch, + ChannelConstants.parameterNameStartDate: + retrieveEventsParams?.startDate?.millisecondsSinceEpoch, + ChannelConstants.parameterNameEndDate: + retrieveEventsParams?.endDate?.millisecondsSinceEpoch, ChannelConstants.parameterNameEventIds: retrieveEventsParams?.eventIds, }, evaluateResponse: (rawData) => UnmodifiableListView( @@ -193,14 +201,24 @@ class DeviceCalendarPlugin { /// /// Returns a [Result] with the newly created or updated [Event.eventId] Future?> createOrUpdateEvent(Event? event) async { - if(event == null) return null; + if (event == null) return null; return _invokeChannelMethod( ChannelConstants.methodNameCreateOrUpdateEvent, assertParameters: (result) { // Setting time to 0 for all day events if (event.allDay == true) { - if(event.start != null) {event.start = DateTime(event.start!.year, event.start!.month, event.start!.day, 0, 0, 0);}; - if(event.end != null) {event.end = DateTime(event.end!.year, event.end!.month, event.end!.day, 0, 0, 0);} + if (event.start != null) { + var dateStart = DateTime(event.start!.year, event.start!.month, + event.start!.day, 0, 0, 0); + event.start = TZDateTime.from(dateStart, + timeZoneDatabase.locations[event.start!.location.name]!); + } + if (event.end != null) { + var dateEnd = DateTime( + event.end!.year, event.end!.month, event.end!.day, 0, 0, 0); + event.end = TZDateTime.from( + dateEnd, timeZoneDatabase.locations[event.end!.location.name]!); + } } _assertParameter( @@ -218,7 +236,9 @@ class DeviceCalendarPlugin { ((event.calendarId?.isEmpty ?? true) || event.start == null || event.end == null || - (event.start != null && event.end != null && event.start!.isAfter(event.end!)))), + (event.start != null && + event.end != null && + event.start!.isAfter(event.end!)))), ErrorCodes.invalidArguments, ErrorMessages.createOrUpdateEventInvalidArgumentsMessage, ); @@ -258,8 +278,10 @@ class DeviceCalendarPlugin { }, arguments: () => { ChannelConstants.parameterNameCalendarName: calendarName, - ChannelConstants.parameterNameCalendarColor: '0x${calendarColor?.value.toRadixString(16)}', - ChannelConstants.parameterNameLocalAccountName: localAccountName?.isEmpty ?? true + ChannelConstants.parameterNameCalendarColor: + '0x${calendarColor?.value.toRadixString(16)}', + ChannelConstants.parameterNameLocalAccountName: + localAccountName?.isEmpty ?? true ? 'Device Calendar' : localAccountName }, @@ -270,8 +292,8 @@ class DeviceCalendarPlugin { /// The `calendarId` parameter is the id of the calendar that plugin will try to delete the event from\/// /// Returns a [Result] indicating if the instance of the calendar has (true) or has not (false) been deleted Future> deleteCalendar( - String calendarId, - ) async { + String calendarId, + ) async { return _invokeChannelMethod( ChannelConstants.methodNameDeleteCalendar, assertParameters: (result) { diff --git a/lib/src/models/attendee.dart b/lib/src/models/attendee.dart index 7be8be75..b82a4a1f 100644 --- a/lib/src/models/attendee.dart +++ b/lib/src/models/attendee.dart @@ -28,12 +28,15 @@ class Attendee { AndroidAttendeeDetails? androidAttendeeDetails; Attendee( - {this.name, - this.emailAddress, - this.role, - this.isOrganiser = false, - this.iosAttendeeDetails, - this.androidAttendeeDetails}); + {this.name, this.emailAddress, this.role, this.isOrganiser = false}) { + if (Platform.isAndroid) { + androidAttendeeDetails = AndroidAttendeeDetails(); + } + + if (Platform.isIOS) { + iosAttendeeDetails = IosAttendeeDetails(); + } + } Attendee.fromJson(Map? json) { if (json == null) { @@ -43,10 +46,10 @@ class Attendee { name = json['name']; emailAddress = json['emailAddress']; role = AttendeeRole.values[json['role'] ?? 0]; + isOrganiser = json['isOrganizer'] ?? + false; // Getting and setting an organiser for Android if (Platform.isAndroid) { - isOrganiser = - json['isOrganizer']; // Getting and setting an organiser for Android androidAttendeeDetails = AndroidAttendeeDetails.fromJson(json); } @@ -59,20 +62,17 @@ class Attendee { final data = { 'name': name, 'emailAddress': emailAddress, - 'role': role?.index + 'role': role?.index, + 'isOrganizer': isOrganiser }; - if (Platform.isIOS && iosAttendeeDetails != null) { + if (iosAttendeeDetails != null) { data.addEntries(iosAttendeeDetails!.toJson().entries); } - if (Platform.isAndroid && androidAttendeeDetails != null) { + if (androidAttendeeDetails != null) { data.addEntries(androidAttendeeDetails!.toJson().entries); } - if (Platform.isIOS && iosAttendeeDetails != null) { - data.addEntries(iosAttendeeDetails!.toJson().entries); - } - return data; } } diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index 7d7a3b5c..355a0d65 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -3,6 +3,8 @@ import '../common/calendar_enums.dart'; import '../common/error_messages.dart'; import 'attendee.dart'; import 'recurrence_rule.dart'; +import 'package:timezone/timezone.dart'; +import 'package:collection/collection.dart'; /// An event associated with a calendar class Event { @@ -19,18 +21,10 @@ class Event { String? description; /// Indicates when the event starts - DateTime? start; + TZDateTime? start; /// Indicates when the event ends - DateTime? end; - - /// Time zone of the event start date\ - /// **Note**: In iOS this will set time zones for both start and end date - String? startTimeZone; - - /// Time zone of the event end date\ - /// **Note**: Not used in iOS, only single time zone is used. Please use `startTimeZone` - String? endTimeZone; + TZDateTime? end; /// Indicates if this is an all-day event bool? allDay; @@ -51,20 +45,18 @@ class Event { List? reminders; /// Indicates if this event counts as busy time, tentative, unavaiable or is still free time - Availability? availability; + late Availability availability; Event(this.calendarId, {this.eventId, this.title, this.start, this.end, - this.startTimeZone, - this.endTimeZone, this.description, this.attendees, this.recurrenceRule, this.reminders, - this.availability, + required this.availability, this.allDay = false}); Event.fromJson(Map? json) { @@ -76,16 +68,23 @@ class Event { calendarId = json['calendarId']; title = json['title']; description = json['description']; - int? startMillisecondsSinceEpoch = json['start']; - if (startMillisecondsSinceEpoch != null) { - start = DateTime.fromMillisecondsSinceEpoch(startMillisecondsSinceEpoch); - } - int? endMillisecondsSinceEpoch = json['end']; - if (endMillisecondsSinceEpoch != null) { - end = DateTime.fromMillisecondsSinceEpoch(endMillisecondsSinceEpoch); - } - startTimeZone = json['startTimeZone']; - endTimeZone = json['endTimeZone']; + + final int? startTimestamp = json['start']; + final String? startLocationName = json['startTimeZone']; + var startTimeZone = timeZoneDatabase.locations[startLocationName]; + startTimeZone ??= local; + start = startTimestamp != null + ? TZDateTime.fromMillisecondsSinceEpoch(startTimeZone, startTimestamp) + : TZDateTime.now(local); + + final int? endTimestamp = json['end']; + final String? endLocationName = json['endTimeZone']; + var endLocation = timeZoneDatabase.locations[endLocationName]; + endLocation ??= local; + end = endTimestamp != null + ? TZDateTime.fromMillisecondsSinceEpoch(endLocation, endTimestamp) + : TZDateTime.now(local); + allDay = json['allDay']; location = json['location']; availability = parseStringToAvailability(json['availability']); @@ -97,26 +96,30 @@ class Event { url = Uri.dataFromString(foundUrl as String); } + availability = parseStringToAvailability(json['availability']); + if (json['attendees'] != null) { attendees = json['attendees'].map((decodedAttendee) { return Attendee.fromJson(decodedAttendee); }).toList(); } - if (json['recurrenceRule'] != null) { - recurrenceRule = RecurrenceRule.fromJson(json['recurrenceRule']); - } + if (json['organizer'] != null) { // Getting and setting an organiser for iOS var organiser = Attendee.fromJson(json['organizer']); - var attendee = attendees?.firstWhere( - (at) => - at?.name == organiser.name && - at?.emailAddress == organiser.emailAddress); + var attendee = attendees?.firstWhereOrNull((at) => + at?.name == organiser.name && + at?.emailAddress == organiser.emailAddress); if (attendee != null) { attendee.isOrganiser = true; } } + + if (json['recurrenceRule'] != null) { + recurrenceRule = RecurrenceRule.fromJson(json['recurrenceRule']); + } + if (json['reminders'] != null) { reminders = json['reminders'].map((decodedReminder) { return Reminder.fromJson(decodedReminder); @@ -131,21 +134,28 @@ class Event { data['eventId'] = eventId; data['eventTitle'] = title; data['eventDescription'] = description; - data['eventStartDate'] = start?.millisecondsSinceEpoch; - data['eventEndDate'] = end?.millisecondsSinceEpoch; - data['eventStartTimeZone'] = startTimeZone; - data['eventEndTimeZone'] = endTimeZone; + data['eventStartDate'] = start!.millisecondsSinceEpoch; + data['eventStartTimeZone'] = start?.location.name; + data['eventEndDate'] = end!.millisecondsSinceEpoch; + data['eventEndTimeZone'] = end?.location.name; data['eventAllDay'] = allDay; data['eventLocation'] = location; data['eventURL'] = url?.data?.contentText; - data['availability'] = availability?.enumToString; + data['availability'] = availability.enumToString; if (attendees != null) { data['attendees'] = attendees?.map((a) => a?.toJson()).toList(); } + + if (attendees != null) { + data['organizer'] = + attendees?.firstWhereOrNull((a) => a!.isOrganiser)?.toJson(); + } + if (recurrenceRule != null) { data['recurrenceRule'] = recurrenceRule?.toJson(); } + if (reminders != null) { data['reminders'] = reminders?.map((r) => r.toJson()).toList(); } @@ -153,8 +163,9 @@ class Event { return data; } - Availability? parseStringToAvailability(String value) { - switch (value) { + Availability parseStringToAvailability(String? value) { + var testValue = value?.toUpperCase(); + switch (testValue) { case 'BUSY': return Availability.Busy; case 'FREE': @@ -164,6 +175,28 @@ class Event { case 'UNAVAILABLE': return Availability.Unavailable; } - return null; + return Availability.Busy; + } + + bool updateStartLocation(String? newStartLocation) { + if (newStartLocation == null) return false; + try { + var location = timeZoneDatabase.get(newStartLocation); + start = TZDateTime.from(start as TZDateTime, location); + return true; + } on LocationNotFoundException { + return false; + } + } + + bool updateEndLocation(String? newEndLocation) { + if (newEndLocation == null) return false; + try { + var location = timeZoneDatabase.get(newEndLocation); + end = TZDateTime.from(end as TZDateTime, location); + return true; + } on LocationNotFoundException { + return false; + } } } diff --git a/lib/src/models/platform_specifics/android/attendance_status.dart b/lib/src/models/platform_specifics/android/attendance_status.dart index 5c1a41f7..d895877f 100644 --- a/lib/src/models/platform_specifics/android/attendance_status.dart +++ b/lib/src/models/platform_specifics/android/attendance_status.dart @@ -5,3 +5,11 @@ enum AndroidAttendanceStatus { Invited, Tentative, } + +extension AndroidAttendanceStatusExtensions on AndroidAttendanceStatus { + String _enumToString(AndroidAttendanceStatus enumValue) { + return enumValue.toString().split('.').last; + } + + String get enumToString => _enumToString(this); +} diff --git a/lib/src/models/platform_specifics/android/attendee_details.dart b/lib/src/models/platform_specifics/android/attendee_details.dart index fda73cf9..ab5de7a5 100644 --- a/lib/src/models/platform_specifics/android/attendee_details.dart +++ b/lib/src/models/platform_specifics/android/attendee_details.dart @@ -1,34 +1,29 @@ -import 'package:flutter/foundation.dart'; - -import '../../../common/calendar_enums.dart'; import '../../../common/error_messages.dart'; import 'attendance_status.dart'; class AndroidAttendeeDetails { AndroidAttendanceStatus? _attendanceStatus; - /// An attendee role: None, Optional, Required or Resource - AttendeeRole? role; - /// The attendee's status for the event. This is read-only AndroidAttendanceStatus? get attendanceStatus => _attendanceStatus; - AndroidAttendeeDetails({@required this.role}); + AndroidAttendeeDetails(); AndroidAttendeeDetails.fromJson(Map? json) { if (json == null) { throw ArgumentError(ErrorMessages.fromJsonMapIsNull); } - role = AttendeeRole.values[json['role']]; - - if (json['attendanceStatus'] != null && json['attendanceStatus'] is int) { + if (json['androidAttendanceStatus'] != null && + json['androidAttendanceStatus'] is int) { _attendanceStatus = - AndroidAttendanceStatus.values[json['attendanceStatus']]; + AndroidAttendanceStatus.values[json['androidAttendanceStatus']]; } } Map toJson() { - return {'role': role?.index}; + return { + 'androidAttendanceStatus': _attendanceStatus?.index + }; } } diff --git a/lib/src/models/platform_specifics/ios/attendance_status.dart b/lib/src/models/platform_specifics/ios/attendance_status.dart index a8185318..bd958391 100644 --- a/lib/src/models/platform_specifics/ios/attendance_status.dart +++ b/lib/src/models/platform_specifics/ios/attendance_status.dart @@ -8,3 +8,11 @@ enum IosAttendanceStatus { Completed, InProcess, } + +extension IosAttendanceStatusExtensions on IosAttendanceStatus { + String _enumToString(IosAttendanceStatus enumValue) { + return enumValue.toString().split('.').last; + } + + String get enumToString => _enumToString(this); +} diff --git a/lib/src/models/platform_specifics/ios/attendee_details.dart b/lib/src/models/platform_specifics/ios/attendee_details.dart index 83b47c61..60e93d27 100644 --- a/lib/src/models/platform_specifics/ios/attendee_details.dart +++ b/lib/src/models/platform_specifics/ios/attendee_details.dart @@ -1,31 +1,27 @@ -import '../../../common/calendar_enums.dart'; import '../../../common/error_messages.dart'; import 'attendance_status.dart'; class IosAttendeeDetails { IosAttendanceStatus? _attendanceStatus; - /// An attendee role: None, Optional, Required or Resource - AttendeeRole? role; - /// The attendee's status for the event. This is read-only IosAttendanceStatus? get attendanceStatus => _attendanceStatus; - IosAttendeeDetails({this.role}); + IosAttendeeDetails(); IosAttendeeDetails.fromJson(Map? json) { if (json == null) { throw ArgumentError(ErrorMessages.fromJsonMapIsNull); } - role = AttendeeRole.values[json['role']]; - - if (json['attendanceStatus'] != null && json['attendanceStatus'] is int) { - _attendanceStatus = IosAttendanceStatus.values[json['attendanceStatus']]; + if (json['iOSAttendanceStatus'] != null && + json['iOSAttendanceStatus'] is int) { + _attendanceStatus = + IosAttendanceStatus.values[json['iOSAttendanceStatus']]; } } Map toJson() { - return {'role': role?.index}; + return {'iOSAttendanceStatus': _attendanceStatus?.index}; } } diff --git a/lib/src/models/recurrence_rule.dart b/lib/src/models/recurrence_rule.dart index 1434d61a..02825262 100644 --- a/lib/src/models/recurrence_rule.dart +++ b/lib/src/models/recurrence_rule.dart @@ -53,7 +53,8 @@ class RecurrenceRule { } int? recurrenceFrequencyIndex = json[_recurrenceFrequencyKey]; - if (recurrenceFrequencyIndex == null || recurrenceFrequencyIndex >= RecurrenceFrequency.values.length) { + if (recurrenceFrequencyIndex == null || + recurrenceFrequencyIndex >= RecurrenceFrequency.values.length) { throw ArgumentError(ErrorMessages.invalidRecurrencyFrequency); } recurrenceFrequency = RecurrenceFrequency.values[recurrenceFrequencyIndex]; diff --git a/lib/src/models/reminder.dart b/lib/src/models/reminder.dart index 17205f46..761ab676 100644 --- a/lib/src/models/reminder.dart +++ b/lib/src/models/reminder.dart @@ -5,7 +5,8 @@ class Reminder { int? minutes; Reminder({@required this.minutes}) - : assert(minutes != null && minutes >= 0, 'Minutes must be greater than or equal than zero'); + : assert(minutes != null && minutes >= 0, + 'Minutes must be greater than or equal than zero'); Reminder.fromJson(Map json) { minutes = json['minutes'] as int; diff --git a/pubspec.yaml b/pubspec.yaml index a30331e0..ea3f263a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: device_calendar description: A cross platform plugin for modifying calendars on the user's device. -version: 4.0.0 +version: 4.0.1 homepage: https://github.com/builttoroam/device_calendar/tree/master dependencies: @@ -9,6 +9,8 @@ dependencies: meta: ^1.1.8 collection: ^1.15.0 sprintf: ^6.0.0 + timezone: ^0.7.0 + flutter_native_timezone: ^1.0.10 dev_dependencies: flutter_test: @@ -27,4 +29,4 @@ flutter: environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13" \ No newline at end of file + flutter: ">=1.12.13" diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart index c96c129e..0b18cf0b 100644 --- a/test/device_calendar_test.dart +++ b/test/device_calendar_test.dart @@ -2,8 +2,11 @@ import 'package:device_calendar/device_calendar.dart'; import 'package:device_calendar/src/common/error_codes.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:timezone/timezone.dart'; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final channel = const MethodChannel('plugins.builttoroam.com/device_calendar'); var deviceCalendarPlugin = DeviceCalendarPlugin(); @@ -64,7 +67,7 @@ void main() { final result = await deviceCalendarPlugin.retrieveEvents(calendarId, params); expect(result.isSuccess, false); expect(result.errors.length, greaterThan(0)); - expect(result.errors[0], contains(ErrorCodes.invalidArguments.toString())); + expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); }); test('DeleteEvent_CalendarId_IsRequired', () async { @@ -74,7 +77,7 @@ void main() { final result = await deviceCalendarPlugin.deleteEvent(calendarId, eventId); expect(result.isSuccess, false); expect(result.errors.length, greaterThan(0)); - expect(result.errors[0], contains(ErrorCodes.invalidArguments.toString())); + expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); }); test('DeleteEvent_EventId_IsRequired', () async { @@ -84,7 +87,7 @@ void main() { final result = await deviceCalendarPlugin.deleteEvent(calendarId, eventId); expect(result.isSuccess, false); expect(result.errors.length, greaterThan(0)); - expect(result.errors[0], contains(ErrorCodes.invalidArguments.toString())); + expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); }); test('DeleteEvent_PassesArguments_Correctly', () async { @@ -102,12 +105,12 @@ void main() { test('CreateEvent_Arguments_Invalid', () async { final String? fakeCalendarId = null; - final event = Event(fakeCalendarId); + final event = Event(fakeCalendarId, availability: Availability.Busy); final result = await deviceCalendarPlugin.createOrUpdateEvent(event); - expect(result?.isSuccess, false); - expect(result?.errors, isNotEmpty); - expect(result?.errors[0], contains(ErrorCodes.invalidArguments.toString())); + expect(result!.isSuccess, false); + expect(result.errors, isNotEmpty); + expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); }); test('CreateEvent_Returns_Successfully', () async { @@ -117,10 +120,10 @@ void main() { }); final fakeCalendarId = 'fakeCalendarId'; - final event = Event(fakeCalendarId); + final event = Event(fakeCalendarId, availability: Availability.Busy); event.title = 'fakeEventTitle'; - event.start = DateTime.now(); - event.end = event.start?.add(Duration(hours: 1)); + event.start = TZDateTime.now(local); + event.end = event.start!.add(Duration(hours: 1)); final result = await deviceCalendarPlugin.createOrUpdateEvent(event); expect(result?.isSuccess, true); @@ -141,11 +144,11 @@ void main() { }); final fakeCalendarId = 'fakeCalendarId'; - final event = Event(fakeCalendarId); + final event = Event(fakeCalendarId, availability: Availability.Busy); event.eventId = 'fakeEventId'; event.title = 'fakeEventTitle'; - event.start = DateTime.now(); - event.end = event.start?.add(Duration(hours: 1)); + event.start = TZDateTime.now(local); + event.end = event.start!.add(Duration(hours: 1)); final result = await deviceCalendarPlugin.createOrUpdateEvent(event); expect(result?.isSuccess, true); @@ -153,4 +156,34 @@ void main() { expect(result?.data, isNotEmpty); expect(result?.data, fakeNewEventId); }); + + test('Attendee_Serialises_Correctly', () async { + final attendee = Attendee( + name: 'Test Attendee', + emailAddress: 'test@t.com', + role: AttendeeRole.Required, + isOrganiser: true); + final stringAttendee = attendee.toJson(); + expect(stringAttendee, isNotNull); + final newAttendee = Attendee.fromJson(stringAttendee); + expect(newAttendee, isNotNull); + expect(newAttendee.name, equals(attendee.name)); + expect(newAttendee.emailAddress, equals(attendee.emailAddress)); + expect(newAttendee.role, equals(attendee.role)); + expect(newAttendee.isOrganiser, equals(attendee.isOrganiser)); + expect(newAttendee.iosAttendeeDetails, isNull); + expect(newAttendee.androidAttendeeDetails, isNull); + }); + + test('Event_Serialises_Correctly', () async { + final event = Event('calendarId',eventId: 'eventId',start: TZDateTime( + timeZoneDatabase.locations.entries.skip(20).first.value, 1980, 10,1,0,0,0), availability: Availability.Busy); + final stringEvent = event.toJson(); + expect(stringEvent, isNotNull); + final newEvent = Event.fromJson(stringEvent); + expect(newEvent, isNotNull); + expect(newEvent.calendarId, equals(event.calendarId)); + expect(newEvent.eventId, equals(event.eventId)); + expect(newEvent.start, equals(event.start)); + }); }