diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..82082166 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Report unexpected behavior +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To reproduce** +Please include a short code sample that can be used to reproduce the problem. +
+Code sample +
+ +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Output of `flutter doctor`** +Paste the result of this command here. +
+Output +
+ +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..876ae2a9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for table_calendar +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore index 446ed0d1..6395d38a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,70 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp .DS_Store -.dart_tool/ +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies .packages +.pub-cache/ .pub/ - build/ -ios/.generated/ -ios/Flutter/Generated.xcconfig -ios/Runner/GeneratedPluginRegistrant.* + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/ephemeral +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml deleted file mode 100644 index 98ceccb1..00000000 --- a/.idea/libraries/Dart_SDK.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index dcbe982e..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 5b3388cc..00000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.metadata b/.metadata index 6209bc9f..30b175a6 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b - channel: stable + revision: f30b7f4db93ee747cd727df747941a28ead25ff5 + channel: beta project_type: package diff --git a/.vscode/launch.json b/.vscode/launch.json index 3287bb67..4566494e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,9 +5,24 @@ "version": "0.2.0", "configurations": [ { - "name": "Flutter", + "name": "example", + "cwd": "example", "request": "launch", "type": "dart" + }, + { + "name": "example (profile mode)", + "cwd": "example", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "example (release mode)", + "cwd": "example", + "request": "launch", + "type": "dart", + "flutterMode": "release" } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..8d4d525e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dart.lineLength": 80, +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e025dc0..af61d0b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,109 @@ +## [3.2.0] + +* Added loadEventsForDisabledDays property to enable loading events for disabled days as well +* Fixed empty weekendDays assertion issue +* Updated intl version to 0.20.0 + +## [3.1.3] + +* Updated gradle config for example project +* Added and applied lint rules, refactored code + +## [3.1.2] + +* Added dayTextFormatter property to CalendarStyle that allows to customize the text within day cells +* Reverted the default day cell's text formatting to just the day's number + +## [3.1.1] + +* Added cell text localization based on current locale + +## [3.1.0] + +* Upgraded to Dart 3 +* Updated intl version to 0.19.0 + +## [3.0.9] + +* Updated intl version to 0.18.0 +* Added explicit android:exported value to AndroidManifest + +## [3.0.8] + +* Added tablePadding property to CalendarStyle + +## [3.0.7] + +* Added week numbering feature + +## [3.0.6] + +* Fixed issue with missing Flutter Web platform tag + +## [3.0.5] + +* Added a visual indicator to FormatButton +* Header buttons are now platform-aware + +## [3.0.4] + +* Updated dependencies +* Removed deprecated fields + +## [3.0.3] + +* Added semantic label to prioritizedBuilder +* Added tableBorder property to CalendarStyle +* Added cellAlignment property to CalendarStyle +* Added cellPadding property to CalendarStyle + +## [3.0.2] + +* Improved semantic labels for screen readers + +## [3.0.1] + +* Added pageAnimationEnabled property +* Added currentDay property to improve widget testability + +## [3.0.0] + +* Migrated to null safety +* Removed CalendarController +* Improved horizontal scrolling +* Improved widget performance +* Improved documentation +* Added date range selection +* Added multiple date selection +* Added selective CalendarBuilders +* Added firstDay and lastDay scroll boundaries +* Added shouldFillViewport property +* Added sixWeekMonthsEnforced property +* Added more options to customize calendar's behavior + +## [2.3.3] + +* Updated dependencies + +## [2.3.2] + +* Added previousPage and nextPage methods to CalendarController + +## [2.3.1] + +* Added chevron visibility properties to HeaderStyle +* Added cellMargin property to CalendarStyle +* Added eventDayStyle property to CalendarStyle +* Added availableCalendarFormats dynamic update +* Added optional BoxDecoration for each calendar row +* Added optional BoxDecoration for days of week row + +## [2.3.0] + +* Migrated to AndroidX +* Added holidays to onDaySelected callback +* Replaced deprecated overflow property with clipBehavior + ## [2.2.3] * Added onCalendarCreated callback diff --git a/README.md b/README.md index 2d63eb40..67f8fe98 100644 --- a/README.md +++ b/README.md @@ -1,86 +1,201 @@ -# Table Calendar +# TableCalendar [![Pub Package](https://img.shields.io/pub/v/table_calendar.svg?style=flat-square)](https://pub.dartlang.org/packages/table_calendar) [![Awesome Flutter](https://img.shields.io/badge/Awesome-Flutter-52bdeb.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) -Highly customizable, feature-packed Flutter Calendar with gestures, animations and multiple formats. +Highly customizable, feature-packed calendar widget for Flutter. | ![Image](https://raw.githubusercontent.com/aleksanderwozniak/table_calendar/assets/table_calendar_styles.gif) | ![Image](https://raw.githubusercontent.com/aleksanderwozniak/table_calendar/assets/table_calendar_builders.gif) | | :------------: | :------------: | -| **Table Calendar** with custom styles | **Table Calendar** with Builders | +| **TableCalendar** with custom styles | **TableCalendar** with custom builders | ## Features * Extensive, yet easy to use API -* Custom Builders for truly flexible UI -* Complete programmatic control with CalendarController -* Dynamic events -* Interface for holidays +* Preconfigured UI with customizable styling +* Custom selective builders for unlimited UI design * Locale support -* Vertical autosizing -* Beautiful animations -* Gesture handling -* Multiple Calendar formats -* Multiple days of the week formats -* Specifying available date range -* Nice, configurable UI out of the box +* Range selection support +* Multiple selection support +* Dynamic events and holidays +* Vertical autosizing - fit the content, or fill the viewport +* Multiple calendar formats (month, two weeks, week) +* Horizontal swipe boundaries (first day, last day) ## Usage -Make sure to check out [example project](https://github.com/aleksanderwozniak/table_calendar/tree/master/example). -For additional info please refer to [API docs](https://pub.dartlang.org/documentation/table_calendar/latest/table_calendar/table_calendar-library.html). +Make sure to check out [examples](https://github.com/aleksanderwozniak/table_calendar/tree/master/example/lib/pages) and [API docs](https://pub.dev/documentation/table_calendar/latest/) for more details. ### Installation -Add to pubspec.yaml: +Add the following line to `pubspec.yaml`: ```yaml dependencies: - table_calendar: ^2.2.3 + table_calendar: ^3.2.0 ``` -Then import it to your project: +### Basic setup + +*The complete example is available [here](https://github.com/aleksanderwozniak/table_calendar/blob/master/example/lib/pages/basics_example.dart).* + +**TableCalendar** requires you to provide `firstDay`, `lastDay` and `focusedDay`: +* `firstDay` is the first available day for the calendar. Users will not be able to access days before it. +* `lastDay` is the last available day for the calendar. Users will not be able to access days after it. +* `focusedDay` is the currently targeted day. Use this property to determine which month should be currently visible. ```dart -import 'package:table_calendar/table_calendar.dart'; +TableCalendar( + firstDay: DateTime.utc(2010, 10, 16), + lastDay: DateTime.utc(2030, 3, 14), + focusedDay: DateTime.now(), +); ``` -And finally create the **TableCalendar** with a `CalendarController`: +#### Adding interactivity + +You will surely notice that previously set up calendar widget isn't quite interactive - you can only swipe it horizontally, to change the currently visible month. While it may be sufficient in certain situations, you can easily bring it to life by specifying a couple of callbacks. + +Adding the following code to the calendar widget will allow it to respond to user's taps, marking the tapped day as selected: ```dart -@override -void initState() { - super.initState(); - _calendarController = CalendarController(); -} +selectedDayPredicate: (day) { + return isSameDay(_selectedDay, day); +}, +onDaySelected: (selectedDay, focusedDay) { + setState(() { + _selectedDay = selectedDay; + _focusedDay = focusedDay; // update `_focusedDay` here as well + }); +}, +``` + +In order to dynamically update visible calendar format, add those lines to the widget: + +```dart +calendarFormat: _calendarFormat, +onFormatChanged: (format) { + setState(() { + _calendarFormat = format; + }); +}, +``` + +Those two changes will make the calendar interactive and responsive to user's input. + +#### Updating focusedDay + +Setting `focusedDay` to a static value means that whenever **TableCalendar** widget rebuilds, it will use that specific `focusedDay`. You can quickly test it by using hot reload: set `focusedDay` to `DateTime.now()`, swipe to next month and trigger a hot reload - the calendar will "reset" to its initial state. To prevent this from happening, you should store and update `focusedDay` whenever any callback exposes it. + +Add this one callback to complete the basic setup: + +```dart +onPageChanged: (focusedDay) { + _focusedDay = focusedDay; +}, +``` + +It is worth noting that you don't need to call `setState()` inside `onPageChanged()` callback. You should just update the stored value, so that if the widget gets rebuilt later on, it will use the proper `focusedDay`. + +*The complete example is available [here](https://github.com/aleksanderwozniak/table_calendar/blob/master/example/lib/pages/basics_example.dart). You can find other examples [here](https://github.com/aleksanderwozniak/table_calendar/tree/master/example/lib/pages).* -@override -void dispose() { - _calendarController.dispose(); - super.dispose(); +### Events + +*The complete example is available [here](https://github.com/aleksanderwozniak/table_calendar/blob/master/example/lib/pages/events_example.dart).* + +You can supply custom events to **TableCalendar** widget. To do so, use `eventLoader` property - you will be given a `DateTime` object, to which you need to assign a list of events. + +```dart +eventLoader: (day) { + return _getEventsForDay(day); +}, +``` + +`_getEventsForDay()` can be of any implementation. For example, a `Map>` can be used: + +```dart +List _getEventsForDay(DateTime day) { + return events[day] ?? []; } +``` + +One thing worth remembering is that `DateTime` objects consist of both date and time parts. In many cases this time part is redundant for calendar related aspects. + +If you decide to use a `Map`, I suggest making it a `LinkedHashMap` - this will allow you to override equality comparison for two `DateTime` objects, comparing them just by their date parts: + +```dart +final events = LinkedHashMap( + equals: isSameDay, + hashCode: getHashCode, +)..addAll(eventSource); +``` + +#### Cyclic events + +`eventLoader` allows you to easily add events that repeat in a pattern. For example, this will add an event to every Monday: + +```dart +eventLoader: (day) { + if (day.weekday == DateTime.monday) { + return [Event('Cyclic event')]; + } + + return []; +}, +``` + +#### Events selected on tap -@override -Widget build(BuildContext context) { - return TableCalendar( - calendarController: _calendarController, - ); +Often times having a sublist of events that are selected by tapping on a day is desired. You can achieve that by using the same method you provided to `eventLoader` inside of `onDaySelected` callback: + +```dart +void _onDaySelected(DateTime selectedDay, DateTime focusedDay) { + if (!isSameDay(_selectedDay, selectedDay)) { + setState(() { + _focusedDay = focusedDay; + _selectedDay = selectedDay; + _selectedEvents = _getEventsForDay(selectedDay); + }); + } } ``` +*The complete example is available [here](https://github.com/aleksanderwozniak/table_calendar/blob/master/example/lib/pages/events_example.dart).* + +### Custom UI with CalendarBuilders + +To customize the UI with your own widgets, use [CalendarBuilders](https://pub.dev/documentation/table_calendar/latest/table_calendar/CalendarBuilders-class.html). Each builder can be used to selectively override the UI, allowing you to implement highly specific designs with minimal hassle. + +You can return `null` from any builder to use the default style. For example, the following snippet will override only the Sunday's day of the week label (Sun), leaving other dow labels unchanged: + +```dart +calendarBuilders: CalendarBuilders( + dowBuilder: (context, day) { + if (day.weekday == DateTime.sunday) { + final text = DateFormat.E().format(day); + + return Center( + child: Text( + text, + style: TextStyle(color: Colors.red), + ), + ); + } + }, +), +``` + ### Locale -**Table Calendar** supports locales. To display the Calendar in desired language, use `locale` property. +To display the calendar in desired language, use `locale` property. If you don't specify it, a default locale will be used. #### Initialization -Before you can use a locale, you need to initialize the i18n formatting. - -*This is independent of **Table Calendar** package, so I encourage you to do your own research.* +Before you can use a locale, you might need to initialize date formatting. A simple way of doing it is as follows: -* First of all, add [intl](https://pub.dartlang.org/packages/intl) package to your pubspec.yaml file +* First of all, add [intl](https://pub.dev/packages/intl) package to your pubspec.yaml file * Then make modifications to your `main()`: ```dart @@ -91,13 +206,13 @@ void main() { } ``` -After those two steps your app should be ready to use **Table Calendar** with different languages. +After those two steps your app should be ready to use **TableCalendar** with different languages. #### Specifying a language To specify a language, simply pass it as a String code to `locale` property. -For example, this will make **Table Calendar** use Polish language: +For example, this will make **TableCalendar** use Polish language: ```dart TableCalendar( @@ -109,57 +224,6 @@ TableCalendar( | :------------: | :------------: | :------------: | :------------: | | `'en_US'` | `'pl_PL'` | `'fr_FR'` | `'zh_CN'` | -Note, that if you want to change the language of `FormatButton`'s text, you have to do this yourself. Use `availableCalendarFormats` property and pass the translated Strings there. -Use i18n method of your choice. +Note, that if you want to change the language of `FormatButton`'s text, you have to do this yourself. Use `availableCalendarFormats` property and pass the translated Strings there. Use i18n method of your choice. You can also hide the button altogether by setting `formatButtonVisible` to false. - -### Holidays - -**Table Calendar** provides a simple interface for displaying holidays. Here are a few steps to follow: - -* Fetch a map of holidays tied to dates. You can search for it manually, or perhaps use some online API -* Convert it to a proper format - note that these are lists of holidays, since one date could have a couple of holidays: -```dart -{ - `DateTime A`: [`Holiday A1`, `Holiday A2`, ...], - `DateTime B`: [`Holiday B1`, `Holiday B2`, ...], - ... -} -``` -* Link it to **Table Calendar**. Use `holidays` property - -And that's your basic setup! Now you can add some styling: - -* By using `CalendarStyle` properties: `holidayStyle` and `outsideHolidayStyle` -* By using `CalendarBuilders` for complete UI control over calendar cell - -You can also add custom holiday markers thanks to improved marker API. Check out [example project](https://github.com/aleksanderwozniak/table_calendar/tree/master/example) for more details. - -```dart -markersBuilder: (context, date, events, holidays) { - final children = []; - - if (events.isNotEmpty) { - children.add( - Positioned( - right: 1, - bottom: 1, - child: _buildEventsMarker(date, events), - ), - ); - } - - if (holidays.isNotEmpty) { - children.add( - Positioned( - right: -2, - top: -2, - child: _buildHolidaysMarker(), - ), - ); - } - - return children; -}, -``` \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..d7855022 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,25 @@ +# This file configures the analyzer to use the lint rule set from `package:lint` + +# include: package:lint/strict.yaml # For production apps +# include: package:lint/casual.yaml # For code samples, hackathons and other non-production code +include: package:lint/package.yaml # Use this for packages with public API + +# You might want to exclude auto-generated files from dart analysis +analyzer: + exclude: + #- '**.freezed.dart' + #- '**.g.dart' + +# You can customize the lint rules set to your own liking. A list of all rules +# can be found at https://dart-lang.github.io/linter/lints/options/options.html +linter: + rules: + # Util classes are awesome! + # avoid_classes_with_only_static_members: false + + # Make constructors the first thing in every class + # sort_constructors_first: true + + # Choose wisely, but you don't have to + # prefer_double_quotes: true + # prefer_single_quotes: true diff --git a/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java deleted file mode 100644 index d007606a..00000000 --- a/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.flutter.plugins; - -import io.flutter.plugin.common.PluginRegistry; - -/** - * Generated file. Do not edit. - */ -public final class GeneratedPluginRegistrant { - public static void registerWith(PluginRegistry registry) { - if (alreadyRegisteredWith(registry)) { - return; - } - } - - private static boolean alreadyRegisteredWith(PluginRegistry registry) { - final String key = GeneratedPluginRegistrant.class.getCanonicalName(); - if (registry.hasPlugin(key)) { - return true; - } - registry.registrarFor(key); - return false; - } -} diff --git a/example/.gitignore b/example/.gitignore index 01f4ac79..b4e3d223 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -1,6 +1,5 @@ # Miscellaneous *.class -*.lock *.log *.pyc *.swp @@ -21,52 +20,20 @@ # Flutter/Dart/Pub related **/doc/api/ +**/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins +.flutter-plugins-dependencies .packages .pub-cache/ .pub/ -build/ +/build/ -# Android related -**/android/**/gradle-wrapper.jar -**/android/.gradle -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java +# Web related +lib/generated_plugin_registrant.dart -# iOS/XCode related -**/ios/**/*.mode1v3 -**/ios/**/*.mode2v3 -**/ios/**/*.moved-aside -**/ios/**/*.pbxuser -**/ios/**/*.perspectivev3 -**/ios/**/*sync/ -**/ios/**/.sconsign.dblite -**/ios/**/.tags* -**/ios/**/.vagrant/ -**/ios/**/DerivedData/ -**/ios/**/Icon? -**/ios/**/Pods/ -**/ios/**/.symlinks/ -**/ios/**/profile -**/ios/**/xcuserdata -**/ios/.generated/ -**/ios/Flutter/App.framework -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/Flutter/flutter_export_environment.sh -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* +# Symbolication related +app.*.symbols -# Exceptions to above rules. -!**/ios/**/default.mode1v3 -!**/ios/**/default.mode2v3 -!**/ios/**/default.pbxuser -!**/ios/**/default.perspectivev3 -!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages +# Obfuscation related +app.*.map.json diff --git a/example/.metadata b/example/.metadata index 460bc20b..f6caaefd 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b - channel: stable + revision: f30b7f4db93ee747cd727df747941a28ead25ff5 + channel: beta project_type: app diff --git a/example/android/.gitignore b/example/android/.gitignore new file mode 100644 index 00000000..0a741cb4 --- /dev/null +++ b/example/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 4ed2e254..60a550dc 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,11 +22,13 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { - compileSdkVersion 28 + compileSdkVersion 34 + namespace "com.example.example" + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } lintOptions { disable 'InvalidPackage' @@ -34,11 +37,10 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.example" - minSdkVersion 16 - targetSdkVersion 28 + minSdkVersion flutter.minSdkVersion + targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -48,14 +50,18 @@ android { signingConfig signingConfigs.debug } } + + kotlinOptions { + jvmTarget = "1.8" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } } flutter { source '../..' } -dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' -} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..c208884f --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 1b515f8b..e8825bfa 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,39 +1,39 @@ - - - - - + + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" + /> + + diff --git a/example/android/app/src/main/java/com/example/example/MainActivity.java b/example/android/app/src/main/java/com/example/example/MainActivity.java deleted file mode 100644 index 84f8920f..00000000 --- a/example/android/app/src/main/java/com/example/example/MainActivity.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.example; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class MainActivity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} diff --git a/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt new file mode 100644 index 00000000..e793a000 --- /dev/null +++ b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..3db14bb5 --- /dev/null +++ b/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml index 00fa4417..1f83a33f 100644 --- a/example/android/app/src/main/res/values/styles.xml +++ b/example/android/app/src/main/res/values/styles.xml @@ -1,8 +1,18 @@ + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..c208884f --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle index bb8a3038..8f31e8ca 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,18 +1,7 @@ -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' - } -} - allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -24,6 +13,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir -} +} \ No newline at end of file diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 7be3d8b4..a6738207 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,2 +1,4 @@ org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true android.enableR8=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 2819f022..fe63c6c4 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 5a2f14fb..9301c074 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,15 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } } -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.4.1" apply false + id "org.jetbrains.kotlin.android" version "2.0.0" apply false } + +include ":app" \ No newline at end of file diff --git a/example/ios/.gitignore b/example/ios/.gitignore new file mode 100644 index 00000000..e96ef602 --- /dev/null +++ b/example/ios/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 9367d483..4f8d4d24 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) CFBundleExecutable App CFBundleIdentifier @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 11.0 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 5229fcae..bb9a61d7 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,20 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -29,8 +22,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -40,17 +31,13 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -62,8 +49,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -73,10 +58,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -90,7 +72,6 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, ); sourceTree = ""; }; @@ -105,27 +86,18 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -155,17 +127,18 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0910; - ORGANIZATIONNAME = "The Chromium Authors"; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -188,9 +161,7 @@ files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -210,7 +181,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -233,8 +204,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -263,7 +233,6 @@ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -275,12 +244,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -301,9 +272,10 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -314,8 +286,8 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = S8QB4VV633; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -329,13 +301,14 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.example.example; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -347,12 +320,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -379,7 +354,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -389,7 +364,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -401,12 +375,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -427,9 +403,11 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -440,6 +418,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -454,6 +433,9 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.example.example; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -463,6 +445,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -477,6 +460,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.example.example; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16..919434a6 100644 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 786d6aad..3db53b6e 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ @@ -46,7 +45,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner/AppDelegate.h b/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 36e21bbf..00000000 --- a/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/example/ios/Runner/AppDelegate.m b/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 59a72e90..00000000 --- a/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,13 +0,0 @@ -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..70693e4a --- /dev/null +++ b/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 3d43d11e..dc9ada47 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 0513117f..8ac112d9 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -11,7 +11,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - example + table_calendar example CFBundlePackageType APPL CFBundleShortVersionString @@ -41,5 +41,7 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example/ios/Runner/main.m b/example/ios/Runner/main.m deleted file mode 100644 index dff6597e..00000000 --- a/example/ios/Runner/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/example/lib/main.dart b/example/lib/main.dart index e812dbf1..6cb6ced5 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,342 +1,95 @@ -// Copyright (c) 2019 Aleksander Woźniak -// Licensed under Apache License v2.0 +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 import 'package:flutter/material.dart'; import 'package:intl/date_symbol_data_local.dart'; -import 'package:table_calendar/table_calendar.dart'; - -// Example holidays -final Map _holidays = { - DateTime(2019, 1, 1): ['New Year\'s Day'], - DateTime(2019, 1, 6): ['Epiphany'], - DateTime(2019, 2, 14): ['Valentine\'s Day'], - DateTime(2019, 4, 21): ['Easter Sunday'], - DateTime(2019, 4, 22): ['Easter Monday'], -}; +import 'package:table_calendar_example/pages/basics_example.dart'; +import 'package:table_calendar_example/pages/complex_example.dart'; +import 'package:table_calendar_example/pages/events_example.dart'; +import 'package:table_calendar_example/pages/multi_example.dart'; +import 'package:table_calendar_example/pages/range_example.dart'; void main() { - initializeDateFormatting().then((_) => runApp(MyApp())); + initializeDateFormatting().then((_) => runApp(const MyApp())); } class MyApp extends StatelessWidget { + const MyApp({super.key}); + @override Widget build(BuildContext context) { return MaterialApp( - title: 'Table Calendar Demo', + title: 'TableCalendar Example', theme: ThemeData( primarySwatch: Colors.blue, ), - home: MyHomePage(title: 'Table Calendar Demo'), + home: const StartPage(), ); } } -class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); - - final String title; +class StartPage extends StatefulWidget { + const StartPage({super.key}); @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _StartPageState(); } -class _MyHomePageState extends State with TickerProviderStateMixin { - Map _events; - List _selectedEvents; - AnimationController _animationController; - CalendarController _calendarController; - - @override - void initState() { - super.initState(); - final _selectedDay = DateTime.now(); - - _events = { - _selectedDay.subtract(Duration(days: 30)): ['Event A0', 'Event B0', 'Event C0'], - _selectedDay.subtract(Duration(days: 27)): ['Event A1'], - _selectedDay.subtract(Duration(days: 20)): ['Event A2', 'Event B2', 'Event C2', 'Event D2'], - _selectedDay.subtract(Duration(days: 16)): ['Event A3', 'Event B3'], - _selectedDay.subtract(Duration(days: 10)): ['Event A4', 'Event B4', 'Event C4'], - _selectedDay.subtract(Duration(days: 4)): ['Event A5', 'Event B5', 'Event C5'], - _selectedDay.subtract(Duration(days: 2)): ['Event A6', 'Event B6'], - _selectedDay: ['Event A7', 'Event B7', 'Event C7', 'Event D7'], - _selectedDay.add(Duration(days: 1)): ['Event A8', 'Event B8', 'Event C8', 'Event D8'], - _selectedDay.add(Duration(days: 3)): Set.from(['Event A9', 'Event A9', 'Event B9']).toList(), - _selectedDay.add(Duration(days: 7)): ['Event A10', 'Event B10', 'Event C10'], - _selectedDay.add(Duration(days: 11)): ['Event A11', 'Event B11'], - _selectedDay.add(Duration(days: 17)): ['Event A12', 'Event B12', 'Event C12', 'Event D12'], - _selectedDay.add(Duration(days: 22)): ['Event A13', 'Event B13'], - _selectedDay.add(Duration(days: 26)): ['Event A14', 'Event B14', 'Event C14'], - }; - - _selectedEvents = _events[_selectedDay] ?? []; - _calendarController = CalendarController(); - - _animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 400), - ); - - _animationController.forward(); - } - - @override - void dispose() { - _animationController.dispose(); - _calendarController.dispose(); - super.dispose(); - } - - void _onDaySelected(DateTime day, List events, List holidays) { - print('CALLBACK: _onDaySelected'); - setState(() { - _selectedEvents = events; - }); - } - - void _onVisibleDaysChanged(DateTime first, DateTime last, CalendarFormat format) { - print('CALLBACK: _onVisibleDaysChanged'); - } - - void _onCalendarCreated(DateTime first, DateTime last, CalendarFormat format) { - print('CALLBACK: _onCalendarCreated'); - } - +class _StartPageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(widget.title), - ), - body: Column( - mainAxisSize: MainAxisSize.max, - children: [ - // Switch out 2 lines below to play with TableCalendar's settings - //----------------------- - _buildTableCalendar(), - // _buildTableCalendarWithBuilders(), - const SizedBox(height: 8.0), - _buildButtons(), - const SizedBox(height: 8.0), - Expanded(child: _buildEventList()), - ], - ), - ); - } - - // Simple TableCalendar configuration (using Styles) - Widget _buildTableCalendar() { - return TableCalendar( - calendarController: _calendarController, - events: _events, - holidays: _holidays, - startingDayOfWeek: StartingDayOfWeek.monday, - calendarStyle: CalendarStyle( - selectedColor: Colors.deepOrange[400], - todayColor: Colors.deepOrange[200], - markersColor: Colors.brown[700], - outsideDaysVisible: false, - ), - headerStyle: HeaderStyle( - formatButtonTextStyle: TextStyle().copyWith(color: Colors.white, fontSize: 15.0), - formatButtonDecoration: BoxDecoration( - color: Colors.deepOrange[400], - borderRadius: BorderRadius.circular(16.0), - ), - ), - onDaySelected: _onDaySelected, - onVisibleDaysChanged: _onVisibleDaysChanged, - onCalendarCreated: _onCalendarCreated, - ); - } - - // More advanced TableCalendar configuration (using Builders & Styles) - Widget _buildTableCalendarWithBuilders() { - return TableCalendar( - locale: 'pl_PL', - calendarController: _calendarController, - events: _events, - holidays: _holidays, - initialCalendarFormat: CalendarFormat.month, - formatAnimation: FormatAnimation.slide, - startingDayOfWeek: StartingDayOfWeek.sunday, - availableGestures: AvailableGestures.all, - availableCalendarFormats: const { - CalendarFormat.month: '', - CalendarFormat.week: '', - }, - calendarStyle: CalendarStyle( - outsideDaysVisible: false, - weekendStyle: TextStyle().copyWith(color: Colors.blue[800]), - holidayStyle: TextStyle().copyWith(color: Colors.blue[800]), - ), - daysOfWeekStyle: DaysOfWeekStyle( - weekendStyle: TextStyle().copyWith(color: Colors.blue[600]), - ), - headerStyle: HeaderStyle( - centerHeaderTitle: true, - formatButtonVisible: false, + title: const Text('TableCalendar Example'), ), - builders: CalendarBuilders( - selectedDayBuilder: (context, date, _) { - return FadeTransition( - opacity: Tween(begin: 0.0, end: 1.0).animate(_animationController), - child: Container( - margin: const EdgeInsets.all(4.0), - padding: const EdgeInsets.only(top: 5.0, left: 6.0), - color: Colors.deepOrange[300], - width: 100, - height: 100, - child: Text( - '${date.day}', - style: TextStyle().copyWith(fontSize: 16.0), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 20.0), + ElevatedButton( + child: const Text('Basics'), + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const TableBasicsExample()), ), ), - ); - }, - todayDayBuilder: (context, date, _) { - return Container( - margin: const EdgeInsets.all(4.0), - padding: const EdgeInsets.only(top: 5.0, left: 6.0), - color: Colors.amber[400], - width: 100, - height: 100, - child: Text( - '${date.day}', - style: TextStyle().copyWith(fontSize: 16.0), - ), - ); - }, - markersBuilder: (context, date, events, holidays) { - final children = []; - - if (events.isNotEmpty) { - children.add( - Positioned( - right: 1, - bottom: 1, - child: _buildEventsMarker(date, events), + const SizedBox(height: 12.0), + ElevatedButton( + child: const Text('Range Selection'), + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const TableRangeExample()), ), - ); - } - - if (holidays.isNotEmpty) { - children.add( - Positioned( - right: -2, - top: -2, - child: _buildHolidaysMarker(), + ), + const SizedBox(height: 12.0), + ElevatedButton( + child: const Text('Events'), + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const TableEventsExample()), ), - ); - } - - return children; - }, - ), - onDaySelected: (date, events, holidays) { - _onDaySelected(date, events, holidays); - _animationController.forward(from: 0.0); - }, - onVisibleDaysChanged: _onVisibleDaysChanged, - onCalendarCreated: _onCalendarCreated, - ); - } - - Widget _buildEventsMarker(DateTime date, List events) { - return AnimatedContainer( - duration: const Duration(milliseconds: 300), - decoration: BoxDecoration( - shape: BoxShape.rectangle, - color: _calendarController.isSelected(date) - ? Colors.brown[500] - : _calendarController.isToday(date) ? Colors.brown[300] : Colors.blue[400], - ), - width: 16.0, - height: 16.0, - child: Center( - child: Text( - '${events.length}', - style: TextStyle().copyWith( - color: Colors.white, - fontSize: 12.0, - ), - ), - ), - ); - } - - Widget _buildHolidaysMarker() { - return Icon( - Icons.add_box, - size: 20.0, - color: Colors.blueGrey[800], - ); - } - - Widget _buildButtons() { - final dateTime = _events.keys.elementAt(_events.length - 2); - - return Column( - children: [ - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - RaisedButton( - child: Text('Month'), - onPressed: () { - setState(() { - _calendarController.setCalendarFormat(CalendarFormat.month); - }); - }, ), - RaisedButton( - child: Text('2 weeks'), - onPressed: () { - setState(() { - _calendarController.setCalendarFormat(CalendarFormat.twoWeeks); - }); - }, + const SizedBox(height: 12.0), + ElevatedButton( + child: const Text('Multiple Selection'), + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const TableMultiExample()), + ), ), - RaisedButton( - child: Text('Week'), - onPressed: () { - setState(() { - _calendarController.setCalendarFormat(CalendarFormat.week); - }); - }, + const SizedBox(height: 12.0), + ElevatedButton( + child: const Text('Complex'), + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const TableComplexExample()), + ), ), + const SizedBox(height: 20.0), ], ), - const SizedBox(height: 8.0), - RaisedButton( - child: Text('Set day ${dateTime.day}-${dateTime.month}-${dateTime.year}'), - onPressed: () { - _calendarController.setSelectedDay( - DateTime(dateTime.year, dateTime.month, dateTime.day), - runCallback: true, - ); - }, - ), - ], - ); - } - - Widget _buildEventList() { - return ListView( - children: _selectedEvents - .map((event) => Container( - decoration: BoxDecoration( - border: Border.all(width: 0.8), - borderRadius: BorderRadius.circular(12.0), - ), - margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: ListTile( - title: Text(event.toString()), - onTap: () => print('$event tapped!'), - ), - )) - .toList(), + ), ); } } diff --git a/example/lib/pages/basics_example.dart b/example/lib/pages/basics_example.dart new file mode 100644 index 00000000..fda26a75 --- /dev/null +++ b/example/lib/pages/basics_example.dart @@ -0,0 +1,63 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/material.dart'; +import 'package:table_calendar/table_calendar.dart'; +import 'package:table_calendar_example/utils.dart'; + +class TableBasicsExample extends StatefulWidget { + const TableBasicsExample({super.key}); + + @override + State createState() => _TableBasicsExampleState(); +} + +class _TableBasicsExampleState extends State { + CalendarFormat _calendarFormat = CalendarFormat.month; + DateTime _focusedDay = DateTime.now(); + DateTime? _selectedDay; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('TableCalendar - Basics'), + ), + body: TableCalendar( + firstDay: kFirstDay, + lastDay: kLastDay, + focusedDay: _focusedDay, + calendarFormat: _calendarFormat, + selectedDayPredicate: (day) { + // Use `selectedDayPredicate` to determine which day is currently selected. + // If this returns true, then `day` will be marked as selected. + + // Using `isSameDay` is recommended to disregard + // the time-part of compared DateTime objects. + return isSameDay(_selectedDay, day); + }, + onDaySelected: (selectedDay, focusedDay) { + if (!isSameDay(_selectedDay, selectedDay)) { + // Call `setState()` when updating the selected day + setState(() { + _selectedDay = selectedDay; + _focusedDay = focusedDay; + }); + } + }, + onFormatChanged: (format) { + if (_calendarFormat != format) { + // Call `setState()` when updating calendar format + setState(() { + _calendarFormat = format; + }); + } + }, + onPageChanged: (focusedDay) { + // No need to call `setState()` here + _focusedDay = focusedDay; + }, + ), + ); + } +} diff --git a/example/lib/pages/complex_example.dart b/example/lib/pages/complex_example.dart new file mode 100644 index 00000000..81c6e4a8 --- /dev/null +++ b/example/lib/pages/complex_example.dart @@ -0,0 +1,257 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +// ignore_for_file: avoid_print + +import 'dart:collection'; + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:table_calendar/table_calendar.dart'; +import 'package:table_calendar_example/utils.dart'; + +class TableComplexExample extends StatefulWidget { + const TableComplexExample({super.key}); + + @override + State createState() => _TableComplexExampleState(); +} + +class _TableComplexExampleState extends State { + late final ValueNotifier> _selectedEvents; + final ValueNotifier _focusedDay = ValueNotifier(DateTime.now()); + final Set _selectedDays = LinkedHashSet( + equals: isSameDay, + hashCode: getHashCode, + ); + + late PageController _pageController; + CalendarFormat _calendarFormat = CalendarFormat.month; + RangeSelectionMode _rangeSelectionMode = RangeSelectionMode.toggledOff; + DateTime? _rangeStart; + DateTime? _rangeEnd; + + @override + void initState() { + super.initState(); + + _selectedDays.add(_focusedDay.value); + _selectedEvents = ValueNotifier(_getEventsForDay(_focusedDay.value)); + } + + @override + void dispose() { + _focusedDay.dispose(); + _selectedEvents.dispose(); + super.dispose(); + } + + bool get canClearSelection => + _selectedDays.isNotEmpty || _rangeStart != null || _rangeEnd != null; + + List _getEventsForDay(DateTime day) { + return kEvents[day] ?? []; + } + + List _getEventsForDays(Iterable days) { + return [ + for (final d in days) ..._getEventsForDay(d), + ]; + } + + List _getEventsForRange(DateTime start, DateTime end) { + final days = daysInRange(start, end); + return _getEventsForDays(days); + } + + void _onDaySelected(DateTime selectedDay, DateTime focusedDay) { + setState(() { + if (_selectedDays.contains(selectedDay)) { + _selectedDays.remove(selectedDay); + } else { + _selectedDays.add(selectedDay); + } + + _focusedDay.value = focusedDay; + _rangeStart = null; + _rangeEnd = null; + _rangeSelectionMode = RangeSelectionMode.toggledOff; + }); + + _selectedEvents.value = _getEventsForDays(_selectedDays); + } + + void _onRangeSelected(DateTime? start, DateTime? end, DateTime focusedDay) { + setState(() { + _focusedDay.value = focusedDay; + _rangeStart = start; + _rangeEnd = end; + _selectedDays.clear(); + _rangeSelectionMode = RangeSelectionMode.toggledOn; + }); + + if (start != null && end != null) { + _selectedEvents.value = _getEventsForRange(start, end); + } else if (start != null) { + _selectedEvents.value = _getEventsForDay(start); + } else if (end != null) { + _selectedEvents.value = _getEventsForDay(end); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('TableCalendar - Complex'), + ), + body: Column( + children: [ + ValueListenableBuilder( + valueListenable: _focusedDay, + builder: (context, value, _) { + return _CalendarHeader( + focusedDay: value, + clearButtonVisible: canClearSelection, + onTodayButtonTap: () { + setState(() => _focusedDay.value = DateTime.now()); + }, + onClearButtonTap: () { + setState(() { + _rangeStart = null; + _rangeEnd = null; + _selectedDays.clear(); + _selectedEvents.value = []; + }); + }, + onLeftArrowTap: () { + _pageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + }, + onRightArrowTap: () { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + }, + ); + }, + ), + TableCalendar( + firstDay: kFirstDay, + lastDay: kLastDay, + focusedDay: _focusedDay.value, + headerVisible: false, + selectedDayPredicate: (day) => _selectedDays.contains(day), + rangeStartDay: _rangeStart, + rangeEndDay: _rangeEnd, + calendarFormat: _calendarFormat, + rangeSelectionMode: _rangeSelectionMode, + eventLoader: _getEventsForDay, + holidayPredicate: (day) { + // Every 20th day of the month will be treated as a holiday + return day.day == 20; + }, + onDaySelected: _onDaySelected, + onRangeSelected: _onRangeSelected, + onCalendarCreated: (controller) => _pageController = controller, + onPageChanged: (focusedDay) => _focusedDay.value = focusedDay, + onFormatChanged: (format) { + if (_calendarFormat != format) { + setState(() => _calendarFormat = format); + } + }, + ), + const SizedBox(height: 8.0), + Expanded( + child: ValueListenableBuilder>( + valueListenable: _selectedEvents, + builder: (context, value, _) { + return ListView.builder( + itemCount: value.length, + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 4.0, + ), + decoration: BoxDecoration( + border: Border.all(), + borderRadius: BorderRadius.circular(12.0), + ), + child: ListTile( + onTap: () => print('${value[index]}'), + title: Text('${value[index]}'), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ); + } +} + +class _CalendarHeader extends StatelessWidget { + final DateTime focusedDay; + final VoidCallback onLeftArrowTap; + final VoidCallback onRightArrowTap; + final VoidCallback onTodayButtonTap; + final VoidCallback onClearButtonTap; + final bool clearButtonVisible; + + const _CalendarHeader({ + required this.focusedDay, + required this.onLeftArrowTap, + required this.onRightArrowTap, + required this.onTodayButtonTap, + required this.onClearButtonTap, + required this.clearButtonVisible, + }); + + @override + Widget build(BuildContext context) { + final headerText = DateFormat.yMMM().format(focusedDay); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + const SizedBox(width: 16.0), + SizedBox( + width: 120.0, + child: Text( + headerText, + style: const TextStyle(fontSize: 26.0), + ), + ), + IconButton( + icon: const Icon(Icons.calendar_today, size: 20.0), + visualDensity: VisualDensity.compact, + onPressed: onTodayButtonTap, + ), + if (clearButtonVisible) + IconButton( + icon: const Icon(Icons.clear, size: 20.0), + visualDensity: VisualDensity.compact, + onPressed: onClearButtonTap, + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.chevron_left), + onPressed: onLeftArrowTap, + ), + IconButton( + icon: const Icon(Icons.chevron_right), + onPressed: onRightArrowTap, + ), + ], + ), + ); + } +} diff --git a/example/lib/pages/events_example.dart b/example/lib/pages/events_example.dart new file mode 100644 index 00000000..0d5d8143 --- /dev/null +++ b/example/lib/pages/events_example.dart @@ -0,0 +1,155 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +// ignore_for_file: avoid_print + +import 'package:flutter/material.dart'; +import 'package:table_calendar/table_calendar.dart'; +import 'package:table_calendar_example/utils.dart'; + +class TableEventsExample extends StatefulWidget { + const TableEventsExample({super.key}); + + @override + State createState() => _TableEventsExampleState(); +} + +class _TableEventsExampleState extends State { + late final ValueNotifier> _selectedEvents; + CalendarFormat _calendarFormat = CalendarFormat.month; + RangeSelectionMode _rangeSelectionMode = RangeSelectionMode + .toggledOff; // Can be toggled on/off by longpressing a date + DateTime _focusedDay = DateTime.now(); + DateTime? _selectedDay; + DateTime? _rangeStart; + DateTime? _rangeEnd; + + @override + void initState() { + super.initState(); + + _selectedDay = _focusedDay; + _selectedEvents = ValueNotifier(_getEventsForDay(_selectedDay!)); + } + + @override + void dispose() { + _selectedEvents.dispose(); + super.dispose(); + } + + List _getEventsForDay(DateTime day) { + // Implementation example + return kEvents[day] ?? []; + } + + List _getEventsForRange(DateTime start, DateTime end) { + // Implementation example + final days = daysInRange(start, end); + + return [ + for (final d in days) ..._getEventsForDay(d), + ]; + } + + void _onDaySelected(DateTime selectedDay, DateTime focusedDay) { + if (!isSameDay(_selectedDay, selectedDay)) { + setState(() { + _selectedDay = selectedDay; + _focusedDay = focusedDay; + _rangeStart = null; // Important to clean those + _rangeEnd = null; + _rangeSelectionMode = RangeSelectionMode.toggledOff; + }); + + _selectedEvents.value = _getEventsForDay(selectedDay); + } + } + + void _onRangeSelected(DateTime? start, DateTime? end, DateTime focusedDay) { + setState(() { + _selectedDay = null; + _focusedDay = focusedDay; + _rangeStart = start; + _rangeEnd = end; + _rangeSelectionMode = RangeSelectionMode.toggledOn; + }); + + // `start` or `end` could be null + if (start != null && end != null) { + _selectedEvents.value = _getEventsForRange(start, end); + } else if (start != null) { + _selectedEvents.value = _getEventsForDay(start); + } else if (end != null) { + _selectedEvents.value = _getEventsForDay(end); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('TableCalendar - Events'), + ), + body: Column( + children: [ + TableCalendar( + firstDay: kFirstDay, + lastDay: kLastDay, + focusedDay: _focusedDay, + selectedDayPredicate: (day) => isSameDay(_selectedDay, day), + rangeStartDay: _rangeStart, + rangeEndDay: _rangeEnd, + calendarFormat: _calendarFormat, + rangeSelectionMode: _rangeSelectionMode, + eventLoader: _getEventsForDay, + startingDayOfWeek: StartingDayOfWeek.monday, + calendarStyle: const CalendarStyle( + // Use `CalendarStyle` to customize the UI + outsideDaysVisible: false, + ), + onDaySelected: _onDaySelected, + onRangeSelected: _onRangeSelected, + onFormatChanged: (format) { + if (_calendarFormat != format) { + setState(() { + _calendarFormat = format; + }); + } + }, + onPageChanged: (focusedDay) { + _focusedDay = focusedDay; + }, + ), + const SizedBox(height: 8.0), + Expanded( + child: ValueListenableBuilder>( + valueListenable: _selectedEvents, + builder: (context, value, _) { + return ListView.builder( + itemCount: value.length, + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 4.0, + ), + decoration: BoxDecoration( + border: Border.all(), + borderRadius: BorderRadius.circular(12.0), + ), + child: ListTile( + onTap: () => print('${value[index]}'), + title: Text('${value[index]}'), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/pages/multi_example.dart b/example/lib/pages/multi_example.dart new file mode 100644 index 00000000..bf3b570a --- /dev/null +++ b/example/lib/pages/multi_example.dart @@ -0,0 +1,135 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +// ignore_for_file: avoid_print + +import 'dart:collection'; + +import 'package:flutter/material.dart'; +import 'package:table_calendar/table_calendar.dart'; +import 'package:table_calendar_example/utils.dart'; + +class TableMultiExample extends StatefulWidget { + const TableMultiExample({super.key}); + + @override + State createState() => _TableMultiExampleState(); +} + +class _TableMultiExampleState extends State { + final ValueNotifier> _selectedEvents = ValueNotifier([]); + + // Using a `LinkedHashSet` is recommended due to equality comparison override + final Set _selectedDays = LinkedHashSet( + equals: isSameDay, + hashCode: getHashCode, + ); + + CalendarFormat _calendarFormat = CalendarFormat.month; + DateTime _focusedDay = DateTime.now(); + + @override + void dispose() { + _selectedEvents.dispose(); + super.dispose(); + } + + List _getEventsForDay(DateTime day) { + // Implementation example + return kEvents[day] ?? []; + } + + List _getEventsForDays(Set days) { + // Implementation example + // Note that days are in selection order (same applies to events) + return [ + for (final d in days) ..._getEventsForDay(d), + ]; + } + + void _onDaySelected(DateTime selectedDay, DateTime focusedDay) { + setState(() { + _focusedDay = focusedDay; + // Update values in a Set + if (_selectedDays.contains(selectedDay)) { + _selectedDays.remove(selectedDay); + } else { + _selectedDays.add(selectedDay); + } + }); + + _selectedEvents.value = _getEventsForDays(_selectedDays); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('TableCalendar - Multi'), + ), + body: Column( + children: [ + TableCalendar( + firstDay: kFirstDay, + lastDay: kLastDay, + focusedDay: _focusedDay, + calendarFormat: _calendarFormat, + eventLoader: _getEventsForDay, + startingDayOfWeek: StartingDayOfWeek.monday, + selectedDayPredicate: (day) { + // Use values from Set to mark multiple days as selected + return _selectedDays.contains(day); + }, + onDaySelected: _onDaySelected, + onFormatChanged: (format) { + if (_calendarFormat != format) { + setState(() { + _calendarFormat = format; + }); + } + }, + onPageChanged: (focusedDay) { + _focusedDay = focusedDay; + }, + ), + ElevatedButton( + child: const Text('Clear selection'), + onPressed: () { + setState(() { + _selectedDays.clear(); + _selectedEvents.value = []; + }); + }, + ), + const SizedBox(height: 8.0), + Expanded( + child: ValueListenableBuilder>( + valueListenable: _selectedEvents, + builder: (context, value, _) { + return ListView.builder( + itemCount: value.length, + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 4.0, + ), + decoration: BoxDecoration( + border: Border.all(), + borderRadius: BorderRadius.circular(12.0), + ), + child: ListTile( + onTap: () => print('${value[index]}'), + title: Text('${value[index]}'), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/pages/range_example.dart b/example/lib/pages/range_example.dart new file mode 100644 index 00000000..6243fa5f --- /dev/null +++ b/example/lib/pages/range_example.dart @@ -0,0 +1,72 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/material.dart'; +import 'package:table_calendar/table_calendar.dart'; +import 'package:table_calendar_example/utils.dart'; + +class TableRangeExample extends StatefulWidget { + const TableRangeExample({super.key}); + + @override + State createState() => _TableRangeExampleState(); +} + +class _TableRangeExampleState extends State { + CalendarFormat _calendarFormat = CalendarFormat.month; + RangeSelectionMode _rangeSelectionMode = RangeSelectionMode + .toggledOn; // Can be toggled on/off by longpressing a date + DateTime _focusedDay = DateTime.now(); + DateTime? _selectedDay; + DateTime? _rangeStart; + DateTime? _rangeEnd; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('TableCalendar - Range'), + ), + body: TableCalendar( + firstDay: kFirstDay, + lastDay: kLastDay, + focusedDay: _focusedDay, + selectedDayPredicate: (day) => isSameDay(_selectedDay, day), + rangeStartDay: _rangeStart, + rangeEndDay: _rangeEnd, + calendarFormat: _calendarFormat, + rangeSelectionMode: _rangeSelectionMode, + onDaySelected: (selectedDay, focusedDay) { + if (!isSameDay(_selectedDay, selectedDay)) { + setState(() { + _selectedDay = selectedDay; + _focusedDay = focusedDay; + _rangeStart = null; // Important to clean those + _rangeEnd = null; + _rangeSelectionMode = RangeSelectionMode.toggledOff; + }); + } + }, + onRangeSelected: (start, end, focusedDay) { + setState(() { + _selectedDay = null; + _focusedDay = focusedDay; + _rangeStart = start; + _rangeEnd = end; + _rangeSelectionMode = RangeSelectionMode.toggledOn; + }); + }, + onFormatChanged: (format) { + if (_calendarFormat != format) { + setState(() { + _calendarFormat = format; + }); + } + }, + onPageChanged: (focusedDay) { + _focusedDay = focusedDay; + }, + ), + ); + } +} diff --git a/example/lib/utils.dart b/example/lib/utils.dart new file mode 100644 index 00000000..f75484c9 --- /dev/null +++ b/example/lib/utils.dart @@ -0,0 +1,54 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:collection'; + +import 'package:table_calendar/table_calendar.dart'; + +/// Example event class. +class Event { + final String title; + + const Event(this.title); + + @override + String toString() => title; +} + +/// Example events. +/// +/// Using a [LinkedHashMap] is highly recommended if you decide to use a map. +final kEvents = LinkedHashMap>( + equals: isSameDay, + hashCode: getHashCode, +)..addAll(_kEventSource); + +final _kEventSource = { + for (var item in List.generate(50, (index) => index)) + DateTime.utc(kFirstDay.year, kFirstDay.month, item * 5): List.generate( + item % 4 + 1, + (index) => Event('Event $item | ${index + 1}'), + ), +}..addAll({ + kToday: [ + const Event("Today's Event 1"), + const Event("Today's Event 2"), + ], + }); + +int getHashCode(DateTime key) { + return key.day * 1000000 + key.month * 10000 + key.year; +} + +/// Returns a list of [DateTime] objects from [first] to [last], inclusive. +List daysInRange(DateTime first, DateTime last) { + final dayCount = last.difference(first).inDays + 1; + return List.generate( + dayCount, + (index) => DateTime.utc(first.year, first.month, first.day + index), + ); +} + +final kToday = DateTime.now(); +final kFirstDay = DateTime(kToday.year, kToday.month - 3, kToday.day); +final kLastDay = DateTime(kToday.year, kToday.month + 3, kToday.day); diff --git a/example/pubspec.lock b/example/pubspec.lock new file mode 100644 index 00000000..ede0d144 --- /dev/null +++ b/example/pubspec.lock @@ -0,0 +1,244 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "00f33b908655e606b86d2ade4710a231b802eec6f11e87e4ea3783fd72077a50" + url: "https://pub.dev" + source: hosted + version: "0.20.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + simple_gesture_detector: + dependency: transitive + description: + name: simple_gesture_detector + sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3 + url: "https://pub.dev" + source: hosted + version: "0.2.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + table_calendar: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "3.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" + source: hosted + version: "14.2.5" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" +sdks: + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b164eef7..cd6a009f 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,25 +1,30 @@ name: table_calendar_example description: A short demo of table_calendar package. +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. -# Read more about versioning at semver.org. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html version: 1.0.0+1 environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.2 - + + intl: any table_calendar: path: ../ @@ -27,9 +32,8 @@ dev_dependencies: flutter_test: sdk: flutter - # For information on the generic Dart part of this file, see the -# following page: https://www.dartlang.org/tools/pub/pubspec +# following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: @@ -41,14 +45,14 @@ flutter: # To add assets to your application, add an assets section, like this: # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.io/assets-and-images/#resolution-aware. + # https://flutter.dev/assets-and-images/#resolution-aware. # For details regarding adding assets from package dependencies, see - # https://flutter.io/assets-and-images/#from-packages + # https://flutter.dev/assets-and-images/#from-packages # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a @@ -68,4 +72,4 @@ flutter: # weight: 700 # # For details regarding fonts from package dependencies, - # see https://flutter.io/custom-fonts/#from-packages + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/example/web/favicon.png b/example/web/favicon.png new file mode 100644 index 00000000..8aaa46ac Binary files /dev/null and b/example/web/favicon.png differ diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png new file mode 100644 index 00000000..b749bfef Binary files /dev/null and b/example/web/icons/Icon-192.png differ diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png new file mode 100644 index 00000000..88cfd48d Binary files /dev/null and b/example/web/icons/Icon-512.png differ diff --git a/example/web/icons/Icon-maskable-192.png b/example/web/icons/Icon-maskable-192.png new file mode 100644 index 00000000..eb9b4d76 Binary files /dev/null and b/example/web/icons/Icon-maskable-192.png differ diff --git a/example/web/icons/Icon-maskable-512.png b/example/web/icons/Icon-maskable-512.png new file mode 100644 index 00000000..d69c5669 Binary files /dev/null and b/example/web/icons/Icon-maskable-512.png differ diff --git a/example/web/index.html b/example/web/index.html new file mode 100644 index 00000000..b6b9dd23 --- /dev/null +++ b/example/web/index.html @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + example + + + + + + + diff --git a/example/web/manifest.json b/example/web/manifest.json new file mode 100644 index 00000000..096edf8f --- /dev/null +++ b/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/lib/src/calendar.dart b/lib/src/calendar.dart deleted file mode 100644 index 49119ad8..00000000 --- a/lib/src/calendar.dart +++ /dev/null @@ -1,719 +0,0 @@ -// Copyright (c) 2019 Aleksander Woźniak -// Licensed under Apache License v2.0 - -part of table_calendar; - -/// Callback exposing currently selected day. -typedef void OnDaySelected(DateTime day, List events, List holidays); - -/// Callback exposing currently visible days (first and last of them), as well as current `CalendarFormat`. -typedef void OnVisibleDaysChanged(DateTime first, DateTime last, CalendarFormat format); - -/// Callback exposing initially visible days (first and last of them), as well as initial `CalendarFormat`. -typedef void OnCalendarCreated(DateTime first, DateTime last, CalendarFormat format); - -/// Signature for reacting to header gestures. Exposes current month and year as a `DateTime` object. -typedef void HeaderGestureCallback(DateTime focusedDay); - -/// Builder signature for any text that can be localized and formatted with `DateFormat`. -typedef String TextBuilder(DateTime date, dynamic locale); - -/// Signature for enabling days. -typedef bool EnabledDayPredicate(DateTime day); - -/// Format to display the `TableCalendar` with. -enum CalendarFormat { month, twoWeeks, week } - -/// Available animations to update the `CalendarFormat` with. -enum FormatAnimation { slide, scale } - -/// Available day of week formats. `TableCalendar` will start the week with chosen day. -/// * `StartingDayOfWeek.monday`: Monday - Sunday -/// * `StartingDayOfWeek.tuesday`: Tuesday - Monday -/// * `StartingDayOfWeek.wednesday`: Wednesday - Tuesday -/// * `StartingDayOfWeek.thursday`: Thursday - Wednesday -/// * `StartingDayOfWeek.friday`: Friday - Thursday -/// * `StartingDayOfWeek.saturday`: Saturday - Friday -/// * `StartingDayOfWeek.sunday`: Sunday - Saturday -enum StartingDayOfWeek { monday, tuesday, wednesday, thursday, friday, saturday, sunday } - -int _getWeekdayNumber(StartingDayOfWeek weekday) { - return StartingDayOfWeek.values.indexOf(weekday) + 1; -} - -/// Gestures available to interal `TableCalendar`'s logic. -enum AvailableGestures { none, verticalSwipe, horizontalSwipe, all } - -/// Highly customizable, feature-packed Flutter Calendar with gestures, animations and multiple formats. -class TableCalendar extends StatefulWidget { - /// Controller required for `TableCalendar`. - /// Use it to update `events`, `holidays`, etc. - final CalendarController calendarController; - - /// Locale to format `TableCalendar` dates with, for example: `'en_US'`. - /// - /// If nothing is provided, a default locale will be used. - final dynamic locale; - - /// `Map` of events. - /// Each `DateTime` inside this `Map` should get its own `List` of objects (i.e. events). - final Map events; - - /// `Map` of holidays. - /// This property allows you to provide custom holiday rules. - final Map holidays; - - /// Called whenever any day gets tapped. - final OnDaySelected onDaySelected; - - /// Called whenever any day gets long pressed. - final OnDaySelected onDayLongPressed; - - /// Called whenever any unavailable day gets tapped. - /// Replaces `onDaySelected` for those days. - final VoidCallback onUnavailableDaySelected; - - /// Called whenever any unavailable day gets long pressed. - /// Replaces `onDaySelected` for those days. - final VoidCallback onUnavailableDayLongPressed; - - /// Called whenever header gets tapped. - final HeaderGestureCallback onHeaderTapped; - - /// Called whenever header gets long pressed. - final HeaderGestureCallback onHeaderLongPressed; - - /// Called whenever the range of visible days changes. - final OnVisibleDaysChanged onVisibleDaysChanged; - - /// Called once when the CalendarController gets initialized. - final OnCalendarCreated onCalendarCreated; - - /// Initially selected DateTime. Usually it will be `DateTime.now()`. - final DateTime initialSelectedDay; - - /// The first day of `TableCalendar`. - /// Days before it will use `unavailableStyle` and run `onUnavailableDaySelected` callback. - final DateTime startDay; - - /// The last day of `TableCalendar`. - /// Days after it will use `unavailableStyle` and run `onUnavailableDaySelected` callback. - final DateTime endDay; - - /// List of days treated as weekend days. - /// Use built-in `DateTime` weekday constants (e.g. `DateTime.monday`) instead of `int` literals (e.q. `1`). - final List weekendDays; - - /// `CalendarFormat` which will be displayed first. - final CalendarFormat initialCalendarFormat; - - /// `Map` of `CalendarFormat`s and `String` names associated with them. - /// Those `CalendarFormat`s will be used by internal logic to manage displayed format. - /// - /// To ensure proper vertical Swipe behavior, `CalendarFormat`s should be in descending order (eg. from biggest to smallest). - /// - /// For example: - /// ```dart - /// availableCalendarFormats: const { - /// CalendarFormat.month: 'Month', - /// CalendarFormat.week: 'Week', - /// } - /// ``` - final Map availableCalendarFormats; - - /// Used to show/hide Header. - final bool headerVisible; - - /// Function deciding whether given day should be enabled or not. - /// If `false` is returned, this day will be unavailable. - final EnabledDayPredicate enabledDayPredicate; - - /// Used for setting the height of `TableCalendar`'s rows. - final double rowHeight; - - /// Animation to run when `CalendarFormat` gets changed. - final FormatAnimation formatAnimation; - - /// `TableCalendar` will start weeks with provided day. - /// Use `StartingDayOfWeek.monday` for Monday - Sunday week format. - /// Use `StartingDayOfWeek.sunday` for Sunday - Saturday week format. - final StartingDayOfWeek startingDayOfWeek; - - /// `HitTestBehavior` for every day cell inside `TableCalendar`. - final HitTestBehavior dayHitTestBehavior; - - /// Specify Gestures available to `TableCalendar`. - /// If `AvailableGestures.none` is used, the Calendar will only be interactive via buttons. - final AvailableGestures availableGestures; - - /// Configuration for vertical Swipe detector. - final SimpleSwipeConfig simpleSwipeConfig; - - /// Style for `TableCalendar`'s content. - final CalendarStyle calendarStyle; - - /// Style for DaysOfWeek displayed between `TableCalendar`'s Header and content. - final DaysOfWeekStyle daysOfWeekStyle; - - /// Style for `TableCalendar`'s Header. - final HeaderStyle headerStyle; - - /// Set of Builders for `TableCalendar` to work with. - final CalendarBuilders builders; - - TableCalendar({ - Key key, - @required this.calendarController, - this.locale, - this.events = const {}, - this.holidays = const {}, - this.onDaySelected, - this.onDayLongPressed, - this.onUnavailableDaySelected, - this.onUnavailableDayLongPressed, - this.onHeaderTapped, - this.onHeaderLongPressed, - this.onVisibleDaysChanged, - this.onCalendarCreated, - this.initialSelectedDay, - this.startDay, - this.endDay, - this.weekendDays = const [DateTime.saturday, DateTime.sunday], - this.initialCalendarFormat = CalendarFormat.month, - this.availableCalendarFormats = const { - CalendarFormat.month: 'Month', - CalendarFormat.twoWeeks: '2 weeks', - CalendarFormat.week: 'Week', - }, - this.headerVisible = true, - this.enabledDayPredicate, - this.rowHeight, - this.formatAnimation = FormatAnimation.slide, - this.startingDayOfWeek = StartingDayOfWeek.sunday, - this.dayHitTestBehavior = HitTestBehavior.deferToChild, - this.availableGestures = AvailableGestures.all, - this.simpleSwipeConfig = const SimpleSwipeConfig( - verticalThreshold: 25.0, - swipeDetectionBehavior: SwipeDetectionBehavior.continuousDistinct, - ), - this.calendarStyle = const CalendarStyle(), - this.daysOfWeekStyle = const DaysOfWeekStyle(), - this.headerStyle = const HeaderStyle(), - this.builders = const CalendarBuilders(), - }) : assert(calendarController != null), - assert(availableCalendarFormats.keys.contains(initialCalendarFormat)), - assert(availableCalendarFormats.length <= CalendarFormat.values.length), - assert(weekendDays != null), - assert(weekendDays.isNotEmpty - ? weekendDays.every((day) => day >= DateTime.monday && day <= DateTime.sunday) - : true), - super(key: key); - - @override - _TableCalendarState createState() => _TableCalendarState(); -} - -class _TableCalendarState extends State with SingleTickerProviderStateMixin { - @override - void initState() { - super.initState(); - - widget.calendarController._init( - events: widget.events, - holidays: widget.holidays, - initialDay: widget.initialSelectedDay, - initialFormat: widget.initialCalendarFormat, - availableCalendarFormats: widget.availableCalendarFormats, - useNextCalendarFormat: widget.headerStyle.formatButtonShowsNext, - startingDayOfWeek: widget.startingDayOfWeek, - selectedDayCallback: _selectedDayCallback, - onVisibleDaysChanged: widget.onVisibleDaysChanged, - onCalendarCreated: widget.onCalendarCreated, - includeInvisibleDays: widget.calendarStyle.outsideDaysVisible, - ); - } - - @override - void didUpdateWidget(TableCalendar oldWidget) { - super.didUpdateWidget(oldWidget); - - if (oldWidget.events != widget.events) { - widget.calendarController._events = widget.events; - } - - if (oldWidget.holidays != widget.holidays) { - widget.calendarController._holidays = widget.holidays; - } - - if (oldWidget.availableCalendarFormats != widget.availableCalendarFormats) { - widget.calendarController._availableCalendarFormats = widget.availableCalendarFormats; - } - } - - void _selectedDayCallback(DateTime day) { - if (widget.onDaySelected != null) { - widget.onDaySelected( - day, - widget.calendarController.visibleEvents[_getEventKey(day)] ?? [], - widget.calendarController.visibleHolidays[_getHolidayKey(day)] ?? [], - ); - } - } - - void _selectPrevious() { - setState(() { - widget.calendarController._selectPrevious(); - }); - } - - void _selectNext() { - setState(() { - widget.calendarController._selectNext(); - }); - } - - void _selectDay(DateTime day) { - setState(() { - widget.calendarController.setSelectedDay(day, isProgrammatic: false); - _selectedDayCallback(day); - }); - } - - void _onDayLongPressed(DateTime day) { - if (widget.onDayLongPressed != null) { - widget.onDayLongPressed( - day, - widget.calendarController.visibleEvents[_getEventKey(day)] ?? [], - widget.calendarController.visibleHolidays[_getHolidayKey(day)] ?? [], - ); - } - } - - void _toggleCalendarFormat() { - setState(() { - widget.calendarController.toggleCalendarFormat(); - }); - } - - void _onHorizontalSwipe(DismissDirection direction) { - if (direction == DismissDirection.startToEnd) { - // Swipe right - _selectPrevious(); - } else { - // Swipe left - _selectNext(); - } - } - - void _onUnavailableDaySelected() { - if (widget.onUnavailableDaySelected != null) { - widget.onUnavailableDaySelected(); - } - } - - void _onUnavailableDayLongPressed() { - if (widget.onUnavailableDayLongPressed != null) { - widget.onUnavailableDayLongPressed(); - } - } - - void _onHeaderTapped() { - if (widget.onHeaderTapped != null) { - widget.onHeaderTapped(widget.calendarController.focusedDay); - } - } - - void _onHeaderLongPressed() { - if (widget.onHeaderLongPressed != null) { - widget.onHeaderLongPressed(widget.calendarController.focusedDay); - } - } - - bool _isDayUnavailable(DateTime day) { - return (widget.startDay != null && day.isBefore(widget.calendarController._normalizeDate(widget.startDay))) || - (widget.endDay != null && day.isAfter(widget.calendarController._normalizeDate(widget.endDay))) || - (!_isDayEnabled(day)); - } - - bool _isDayEnabled(DateTime day) { - return widget.enabledDayPredicate == null ? true : widget.enabledDayPredicate(day); - } - - DateTime _getEventKey(DateTime day) { - return widget.calendarController._getEventKey(day); - } - - DateTime _getHolidayKey(DateTime day) { - return widget.calendarController._getHolidayKey(day); - } - - @override - Widget build(BuildContext context) { - return ClipRect( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.headerVisible) _buildHeader(), - Padding( - padding: widget.calendarStyle.contentPadding, - child: _buildCalendarContent(), - ), - ], - ), - ); - } - - Widget _buildHeader() { - final children = [ - widget.headerStyle.showLeftChevron ? - _CustomIconButton( - icon: widget.headerStyle.leftChevronIcon, - onTap: _selectPrevious, - margin: widget.headerStyle.leftChevronMargin, - padding: widget.headerStyle.leftChevronPadding, - ) : Container(), - Expanded( - child: GestureDetector( - onTap: _onHeaderTapped, - onLongPress: _onHeaderLongPressed, - child: Text( - widget.headerStyle.titleTextBuilder != null - ? widget.headerStyle.titleTextBuilder(widget.calendarController.focusedDay, widget.locale) - : DateFormat.yMMMM(widget.locale).format(widget.calendarController.focusedDay), - style: widget.headerStyle.titleTextStyle, - textAlign: widget.headerStyle.centerHeaderTitle ? TextAlign.center : TextAlign.start, - ), - ), - ), - widget.headerStyle.showRightChevron ? - _CustomIconButton( - icon: widget.headerStyle.rightChevronIcon, - onTap: _selectNext, - margin: widget.headerStyle.rightChevronMargin, - padding: widget.headerStyle.rightChevronPadding, - ) : Container() - ]; - - if (widget.headerStyle.formatButtonVisible && widget.availableCalendarFormats.length > 1) { - children.insert(2, const SizedBox(width: 8.0)); - children.insert(3, _buildFormatButton()); - } - - return Container( - decoration: widget.headerStyle.decoration, - margin: widget.headerStyle.headerMargin, - padding: widget.headerStyle.headerPadding, - child: Row( - mainAxisSize: MainAxisSize.max, - children: children, - ), - ); - } - - Widget _buildFormatButton() { - return GestureDetector( - onTap: _toggleCalendarFormat, - child: Container( - decoration: widget.headerStyle.formatButtonDecoration, - padding: widget.headerStyle.formatButtonPadding, - child: Text( - widget.calendarController._getFormatButtonText(), - style: widget.headerStyle.formatButtonTextStyle, - ), - ), - ); - } - - Widget _buildCalendarContent() { - if (widget.formatAnimation == FormatAnimation.slide) { - return AnimatedSize( - duration: Duration(milliseconds: widget.calendarController.calendarFormat == CalendarFormat.month ? 330 : 220), - curve: Curves.fastOutSlowIn, - alignment: Alignment(0, -1), - vsync: this, - child: _buildWrapper(), - ); - } else { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 350), - transitionBuilder: (child, animation) { - return SizeTransition( - sizeFactor: animation, - child: ScaleTransition( - scale: animation, - child: child, - ), - ); - }, - child: _buildWrapper( - key: ValueKey(widget.calendarController.calendarFormat), - ), - ); - } - } - - Widget _buildWrapper({Key key}) { - Widget wrappedChild = _buildTable(); - - switch (widget.availableGestures) { - case AvailableGestures.all: - wrappedChild = _buildVerticalSwipeWrapper( - child: _buildHorizontalSwipeWrapper( - child: wrappedChild, - ), - ); - break; - case AvailableGestures.verticalSwipe: - wrappedChild = _buildVerticalSwipeWrapper( - child: wrappedChild, - ); - break; - case AvailableGestures.horizontalSwipe: - wrappedChild = _buildHorizontalSwipeWrapper( - child: wrappedChild, - ); - break; - case AvailableGestures.none: - break; - } - - return Container( - key: key, - child: wrappedChild, - ); - } - - Widget _buildVerticalSwipeWrapper({Widget child}) { - return SimpleGestureDetector( - child: child, - onVerticalSwipe: (direction) { - setState(() { - widget.calendarController.swipeCalendarFormat(isSwipeUp: direction == SwipeDirection.up); - }); - }, - swipeConfig: widget.simpleSwipeConfig, - ); - } - - Widget _buildHorizontalSwipeWrapper({Widget child}) { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 350), - switchInCurve: Curves.decelerate, - transitionBuilder: (child, animation) { - return SlideTransition( - position: - Tween(begin: Offset(widget.calendarController._dx, 0), end: Offset(0, 0)).animate(animation), - child: child, - ); - }, - layoutBuilder: (currentChild, _) => currentChild, - child: Dismissible( - key: ValueKey(widget.calendarController._pageId), - resizeDuration: null, - onDismissed: _onHorizontalSwipe, - direction: DismissDirection.horizontal, - child: child, - ), - ); - } - - Widget _buildTable() { - final daysInWeek = 7; - final children = [ - if (widget.calendarStyle.renderDaysOfWeek) _buildDaysOfWeek(), - ]; - - int x = 0; - while (x < widget.calendarController._visibleDays.value.length) { - children.add(_buildTableRow(widget.calendarController._visibleDays.value.skip(x).take(daysInWeek).toList())); - x += daysInWeek; - } - - return Table( - // Makes this Table fill its parent horizontally - defaultColumnWidth: FractionColumnWidth(1.0 / daysInWeek), - children: children, - ); - } - - TableRow _buildDaysOfWeek() { - return TableRow( - decoration: widget.daysOfWeekStyle.decoration, - children: widget.calendarController._visibleDays.value.take(7).map((date) { - final weekdayString = widget.daysOfWeekStyle.dowTextBuilder != null - ? widget.daysOfWeekStyle.dowTextBuilder(date, widget.locale) - : DateFormat.E(widget.locale).format(date); - final isWeekend = widget.calendarController._isWeekend(date, widget.weekendDays); - - if (isWeekend && widget.builders.dowWeekendBuilder != null) { - return widget.builders.dowWeekendBuilder(context, weekdayString); - } - if (widget.builders.dowWeekdayBuilder != null) { - return widget.builders.dowWeekdayBuilder(context, weekdayString); - } - return Center( - child: Text( - weekdayString, - style: isWeekend ? widget.daysOfWeekStyle.weekendStyle : widget.daysOfWeekStyle.weekdayStyle, - ), - ); - }).toList(), - ); - } - - TableRow _buildTableRow(List days) { - return TableRow( - decoration: widget.calendarStyle.contentDecoration, - children: days.map((date) => _buildTableCell(date)).toList(), - ); - } - - // TableCell will have equal width and height - Widget _buildTableCell(DateTime date) { - return LayoutBuilder( - builder: (context, constraints) => ConstrainedBox( - constraints: BoxConstraints( - maxHeight: widget.rowHeight ?? constraints.maxWidth, - minHeight: widget.rowHeight ?? constraints.maxWidth, - ), - child: _buildCell(date), - ), - ); - } - - Widget _buildCell(DateTime date) { - if (!widget.calendarStyle.outsideDaysVisible && - widget.calendarController._isExtraDay(date) && - widget.calendarController.calendarFormat == CalendarFormat.month) { - return Container(); - } - - Widget content = _buildCellContent(date); - - final eventKey = _getEventKey(date); - final holidayKey = _getHolidayKey(date); - final key = eventKey ?? holidayKey; - - if (key != null) { - final children = [content]; - final events = eventKey != null ? widget.calendarController.visibleEvents[eventKey] : []; - final holidays = holidayKey != null ? widget.calendarController.visibleHolidays[holidayKey] : []; - - if (!_isDayUnavailable(date)) { - if (widget.builders.markersBuilder != null) { - children.addAll( - widget.builders.markersBuilder( - context, - key, - events, - holidays, - ), - ); - } else { - children.add( - Positioned( - top: widget.calendarStyle.markersPositionTop, - bottom: widget.calendarStyle.markersPositionBottom, - left: widget.calendarStyle.markersPositionLeft, - right: widget.calendarStyle.markersPositionRight, - child: Row( - mainAxisSize: MainAxisSize.min, - children: events - .take(widget.calendarStyle.markersMaxAmount) - .map((event) => _buildMarker(eventKey, event)) - .toList(), - ), - ), - ); - } - } - - if (children.length > 1) { - content = Stack( - alignment: widget.calendarStyle.markersAlignment, - children: children, - overflow: widget.calendarStyle.canEventMarkersOverflow ? Overflow.visible : Overflow.clip, - ); - } - } - - return GestureDetector( - behavior: widget.dayHitTestBehavior, - onTap: () => _isDayUnavailable(date) ? _onUnavailableDaySelected() : _selectDay(date), - onLongPress: () => _isDayUnavailable(date) ? _onUnavailableDayLongPressed() : _onDayLongPressed(date), - child: content, - ); - } - - Widget _buildCellContent(DateTime date) { - final eventKey = _getEventKey(date); - - final tIsUnavailable = _isDayUnavailable(date); - final tIsSelected = widget.calendarController.isSelected(date); - final tIsToday = widget.calendarController.isToday(date); - final tIsOutside = widget.calendarController._isExtraDay(date); - final tIsHoliday = widget.calendarController.visibleHolidays.containsKey(_getHolidayKey(date)); - final tIsWeekend = widget.calendarController._isWeekend(date, widget.weekendDays); - final tIsEventDay = widget.calendarController.visibleEvents.containsKey(eventKey); - - final isUnavailable = widget.builders.unavailableDayBuilder != null && tIsUnavailable; - final isSelected = widget.builders.selectedDayBuilder != null && tIsSelected; - final isToday = widget.builders.todayDayBuilder != null && tIsToday; - final isOutsideHoliday = widget.builders.outsideHolidayDayBuilder != null && tIsOutside && tIsHoliday; - final isHoliday = widget.builders.holidayDayBuilder != null && !tIsOutside && tIsHoliday; - final isOutsideWeekend = - widget.builders.outsideWeekendDayBuilder != null && tIsOutside && tIsWeekend && !tIsHoliday; - final isOutside = widget.builders.outsideDayBuilder != null && tIsOutside && !tIsWeekend && !tIsHoliday; - final isWeekend = widget.builders.weekendDayBuilder != null && !tIsOutside && tIsWeekend && !tIsHoliday; - - if (isUnavailable) { - return widget.builders.unavailableDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]); - } else if (isSelected && widget.calendarStyle.renderSelectedFirst) { - return widget.builders.selectedDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]); - } else if (isToday) { - return widget.builders.todayDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]); - } else if (isSelected) { - return widget.builders.selectedDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]); - } else if (isOutsideHoliday) { - return widget.builders.outsideHolidayDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]); - } else if (isHoliday) { - return widget.builders.holidayDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]); - } else if (isOutsideWeekend) { - return widget.builders.outsideWeekendDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]); - } else if (isOutside) { - return widget.builders.outsideDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]); - } else if (isWeekend) { - return widget.builders.weekendDayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]); - } else if (widget.builders.dayBuilder != null) { - return widget.builders.dayBuilder(context, date, widget.calendarController.visibleEvents[eventKey]); - } else { - return _CellWidget( - text: '${date.day}', - isUnavailable: tIsUnavailable, - isSelected: tIsSelected, - isToday: tIsToday, - isWeekend: tIsWeekend, - isOutsideMonth: tIsOutside, - isHoliday: tIsHoliday, - isEventDay: tIsEventDay, - calendarStyle: widget.calendarStyle, - ); - } - } - - Widget _buildMarker(DateTime date, dynamic event) { - if (widget.builders.singleMarkerBuilder != null) { - return widget.builders.singleMarkerBuilder(context, date, event); - } else { - return Container( - width: 8.0, - height: 8.0, - margin: const EdgeInsets.symmetric(horizontal: 0.3), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: widget.calendarStyle.markersColor, - ), - ); - } - } -} diff --git a/lib/src/calendar_controller.dart b/lib/src/calendar_controller.dart deleted file mode 100644 index 7539268a..00000000 --- a/lib/src/calendar_controller.dart +++ /dev/null @@ -1,485 +0,0 @@ -// Copyright (c) 2019 Aleksander Woźniak -// Licensed under Apache License v2.0 - -part of table_calendar; - -const double _dxMax = 1.2; -const double _dxMin = -1.2; - -typedef void _SelectedDayCallback(DateTime day); - -/// Controller required for `TableCalendar`. -/// -/// Should be created in `initState()`, and then disposed in `dispose()`: -/// ```dart -/// @override -/// void initState() { -/// super.initState(); -/// _calendarController = CalendarController(); -/// } -/// -/// @override -/// void dispose() { -/// _calendarController.dispose(); -/// super.dispose(); -/// } -/// ``` -class CalendarController { - /// Currently focused day (used to determine which year/month should be visible). - DateTime get focusedDay => _focusedDay; - - /// Currently selected day. - DateTime get selectedDay => _selectedDay; - - /// Currently visible calendar format. - CalendarFormat get calendarFormat => _calendarFormat.value; - - /// List of currently visible days. - List get visibleDays => calendarFormat == CalendarFormat.month && !_includeInvisibleDays - ? _visibleDays.value.where((day) => !_isExtraDay(day)).toList() - : _visibleDays.value; - - /// `Map` of currently visible events. - Map get visibleEvents { - if (_events == null) { - return {}; - } - - return Map.fromEntries( - _events.entries.where((entry) { - for (final day in visibleDays) { - if (_isSameDay(day, entry.key)) { - return true; - } - } - - return false; - }), - ); - } - - /// `Map` of currently visible holidays. - Map get visibleHolidays { - if (_holidays == null) { - return {}; - } - - return Map.fromEntries( - _holidays.entries.where((entry) { - for (final day in visibleDays) { - if (_isSameDay(day, entry.key)) { - return true; - } - } - - return false; - }), - ); - } - - Map _events; - Map _holidays; - DateTime _focusedDay; - DateTime _selectedDay; - StartingDayOfWeek _startingDayOfWeek; - ValueNotifier _calendarFormat; - ValueNotifier> _visibleDays; - Map _availableCalendarFormats; - DateTime _previousFirstDay; - DateTime _previousLastDay; - int _pageId; - double _dx; - bool _useNextCalendarFormat; - bool _includeInvisibleDays; - _SelectedDayCallback _selectedDayCallback; - - void _init({ - @required Map events, - @required Map holidays, - @required DateTime initialDay, - @required CalendarFormat initialFormat, - @required Map availableCalendarFormats, - @required bool useNextCalendarFormat, - @required StartingDayOfWeek startingDayOfWeek, - @required _SelectedDayCallback selectedDayCallback, - @required OnVisibleDaysChanged onVisibleDaysChanged, - @required OnCalendarCreated onCalendarCreated, - @required bool includeInvisibleDays, - }) { - _events = events; - _holidays = holidays; - _availableCalendarFormats = availableCalendarFormats; - _startingDayOfWeek = startingDayOfWeek; - _useNextCalendarFormat = useNextCalendarFormat; - _selectedDayCallback = selectedDayCallback; - _includeInvisibleDays = includeInvisibleDays; - - _pageId = 0; - _dx = 0; - - final now = DateTime.now(); - _focusedDay = initialDay ?? _normalizeDate(now); - _selectedDay = _focusedDay; - _calendarFormat = ValueNotifier(initialFormat); - _visibleDays = ValueNotifier(_getVisibleDays()); - _previousFirstDay = _visibleDays.value.first; - _previousLastDay = _visibleDays.value.last; - - _calendarFormat.addListener(() { - _visibleDays.value = _getVisibleDays(); - }); - - if (onVisibleDaysChanged != null) { - _visibleDays.addListener(() { - if (!_isSameDay(_visibleDays.value.first, _previousFirstDay) || - !_isSameDay(_visibleDays.value.last, _previousLastDay)) { - _previousFirstDay = _visibleDays.value.first; - _previousLastDay = _visibleDays.value.last; - onVisibleDaysChanged( - _getFirstDay(includeInvisible: _includeInvisibleDays), - _getLastDay(includeInvisible: _includeInvisibleDays), - _calendarFormat.value, - ); - } - }); - } - - if (onCalendarCreated != null) { - onCalendarCreated( - _getFirstDay(includeInvisible: _includeInvisibleDays), - _getLastDay(includeInvisible: _includeInvisibleDays), - _calendarFormat.value, - ); - } - } - - /// Disposes the controller. - /// ```dart - /// @override - /// void dispose() { - /// _calendarController.dispose(); - /// super.dispose(); - /// } - /// ``` - void dispose() { - _calendarFormat?.dispose(); - _visibleDays?.dispose(); - } - - /// Toggles calendar format. Same as using `FormatButton`. - void toggleCalendarFormat() { - _calendarFormat.value = _nextFormat(); - } - - /// Sets calendar format by emulating swipe. - void swipeCalendarFormat({@required bool isSwipeUp}) { - assert(isSwipeUp != null); - - final formats = _availableCalendarFormats.keys.toList(); - int id = formats.indexOf(_calendarFormat.value); - - // Order of CalendarFormats must be from biggest to smallest, - // eg.: [month, twoWeeks, week] - if (isSwipeUp) { - id = _clamp(0, formats.length - 1, id + 1); - } else { - id = _clamp(0, formats.length - 1, id - 1); - } - _calendarFormat.value = formats[id]; - } - - /// Sets calendar format to a given `value`. - void setCalendarFormat(CalendarFormat value) { - _calendarFormat.value = value; - } - - /// Sets selected day to a given `value`. - /// Use `runCallback: true` if this should trigger `OnDaySelected` callback. - void setSelectedDay( - DateTime value, { - bool isProgrammatic = true, - bool animate = true, - bool runCallback = false, - }) { - final normalizedDate = _normalizeDate(value); - - if (animate) { - if (normalizedDate.isBefore(_getFirstDay(includeInvisible: false))) { - _decrementPage(); - } else if (normalizedDate.isAfter(_getLastDay(includeInvisible: false))) { - _incrementPage(); - } - } - - _selectedDay = normalizedDate; - _focusedDay = normalizedDate; - _updateVisibleDays(isProgrammatic); - - if (isProgrammatic && runCallback && _selectedDayCallback != null) { - _selectedDayCallback(normalizedDate); - } - } - - /// Sets displayed month/year without changing the currently selected day. - void setFocusedDay(DateTime value) { - _focusedDay = _normalizeDate(value); - _updateVisibleDays(true); - } - - void _updateVisibleDays(bool isProgrammatic) { - if (calendarFormat != CalendarFormat.twoWeeks || isProgrammatic) { - _visibleDays.value = _getVisibleDays(); - } - } - - CalendarFormat _nextFormat() { - final formats = _availableCalendarFormats.keys.toList(); - int id = formats.indexOf(_calendarFormat.value); - id = (id + 1) % formats.length; - - return formats[id]; - } - - String _getFormatButtonText() => - _useNextCalendarFormat ? _availableCalendarFormats[_nextFormat()] : _availableCalendarFormats[_calendarFormat.value]; - - void _selectPrevious() { - if (calendarFormat == CalendarFormat.month) { - _selectPreviousMonth(); - } else if (calendarFormat == CalendarFormat.twoWeeks) { - _selectPreviousTwoWeeks(); - } else { - _selectPreviousWeek(); - } - - _visibleDays.value = _getVisibleDays(); - _decrementPage(); - } - - void _selectNext() { - if (calendarFormat == CalendarFormat.month) { - _selectNextMonth(); - } else if (calendarFormat == CalendarFormat.twoWeeks) { - _selectNextTwoWeeks(); - } else { - _selectNextWeek(); - } - - _visibleDays.value = _getVisibleDays(); - _incrementPage(); - } - - void _selectPreviousMonth() { - _focusedDay = _previousMonth(_focusedDay); - } - - void _selectNextMonth() { - _focusedDay = _nextMonth(_focusedDay); - } - - void _selectPreviousTwoWeeks() { - if (_visibleDays.value.take(7).contains(_focusedDay)) { - // in top row - _focusedDay = _previousWeek(_focusedDay); - } else { - // in bottom row OR not visible - _focusedDay = _previousWeek(_focusedDay.subtract(const Duration(days: 7))); - } - } - - void _selectNextTwoWeeks() { - if (!_visibleDays.value.skip(7).contains(_focusedDay)) { - // not in bottom row [eg: in top row OR not visible] - _focusedDay = _nextWeek(_focusedDay); - } - } - - void _selectPreviousWeek() { - _focusedDay = _previousWeek(_focusedDay); - } - - void _selectNextWeek() { - _focusedDay = _nextWeek(_focusedDay); - } - - DateTime _getFirstDay({@required bool includeInvisible}) { - if (_calendarFormat.value == CalendarFormat.month && !includeInvisible) { - return _firstDayOfMonth(_focusedDay); - } else { - return _visibleDays.value.first; - } - } - - DateTime _getLastDay({@required bool includeInvisible}) { - if (_calendarFormat.value == CalendarFormat.month && !includeInvisible) { - return _lastDayOfMonth(_focusedDay); - } else { - return _visibleDays.value.last; - } - } - - List _getVisibleDays() { - if (calendarFormat == CalendarFormat.month) { - return _daysInMonth(_focusedDay); - } else if (calendarFormat == CalendarFormat.twoWeeks) { - return _daysInWeek(_focusedDay) - ..addAll(_daysInWeek( - _focusedDay.add(const Duration(days: 7)), - )); - } else { - return _daysInWeek(_focusedDay); - } - } - - void _decrementPage() { - _pageId--; - _dx = _dxMin; - } - - void _incrementPage() { - _pageId++; - _dx = _dxMax; - } - - List _daysInMonth(DateTime month) { - final first = _firstDayOfMonth(month); - final daysBefore = _getDaysBefore(first); - final firstToDisplay = first.subtract(Duration(days: daysBefore)); - - final last = _lastDayOfMonth(month); - final daysAfter = _getDaysAfter(last); - - final lastToDisplay = last.add(Duration(days: daysAfter)); - return _daysInRange(firstToDisplay, lastToDisplay).toList(); - } - - int _getDaysBefore(DateTime firstDay) { - return (firstDay.weekday + 7 - _getWeekdayNumber(_startingDayOfWeek)) % 7; - } - - int _getDaysAfter(DateTime lastDay) { - int invertedStartingWeekday = 8 - _getWeekdayNumber(_startingDayOfWeek); - - int daysAfter = 7 - ((lastDay.weekday + invertedStartingWeekday) % 7) + 1; - if (daysAfter == 8) { - daysAfter = 1; - } - - return daysAfter; - } - - List _daysInWeek(DateTime week) { - final first = _firstDayOfWeek(week); - final last = _lastDayOfWeek(week); - - return _daysInRange(first, last).toList(); - } - - DateTime _firstDayOfWeek(DateTime day) { - day = _normalizeDate(day); - - final decreaseNum = _getDaysBefore(day); - return day.subtract(Duration(days: decreaseNum)); - } - - DateTime _lastDayOfWeek(DateTime day) { - day = _normalizeDate(day); - - final increaseNum = _getDaysBefore(day); - return day.add(Duration(days: 7 - increaseNum)); - } - - DateTime _firstDayOfMonth(DateTime month) { - return DateTime.utc(month.year, month.month, 1, 12); - } - - DateTime _lastDayOfMonth(DateTime month) { - final date = month.month < 12 ? DateTime.utc(month.year, month.month + 1, 1, 12) : DateTime.utc(month.year + 1, 1, 1, 12); - return date.subtract(const Duration(days: 1)); - } - - DateTime _previousWeek(DateTime week) { - return week.subtract(const Duration(days: 7)); - } - - DateTime _nextWeek(DateTime week) { - return week.add(const Duration(days: 7)); - } - - DateTime _previousMonth(DateTime month) { - if (month.month == 1) { - return DateTime(month.year - 1, 12); - } else { - return DateTime(month.year, month.month - 1); - } - } - - DateTime _nextMonth(DateTime month) { - if (month.month == 12) { - return DateTime(month.year + 1, 1); - } else { - return DateTime(month.year, month.month + 1); - } - } - - Iterable _daysInRange(DateTime firstDay, DateTime lastDay) sync* { - var temp = firstDay; - - while (temp.isBefore(lastDay)) { - yield _normalizeDate(temp); - temp = temp.add(const Duration(days: 1)); - } - } - - DateTime _normalizeDate(DateTime value) { - return DateTime.utc(value.year, value.month, value.day, 12); - } - - DateTime _getEventKey(DateTime day) { - return visibleEvents.keys.firstWhere((it) => _isSameDay(it, day), orElse: () => null); - } - - DateTime _getHolidayKey(DateTime day) { - return visibleHolidays.keys.firstWhere((it) => _isSameDay(it, day), orElse: () => null); - } - - /// Returns true if `day` is currently selected. - bool isSelected(DateTime day) { - return _isSameDay(day, selectedDay); - } - - /// Returns true if `day` is the same day as `DateTime.now()`. - bool isToday(DateTime day) { - return _isSameDay(day, DateTime.now()); - } - - bool _isSameDay(DateTime dayA, DateTime dayB) { - return dayA.year == dayB.year && dayA.month == dayB.month && dayA.day == dayB.day; - } - - bool _isWeekend(DateTime day, List weekendDays) { - return weekendDays.contains(day.weekday); - } - - bool _isExtraDay(DateTime day) { - return _isExtraDayBefore(day) || _isExtraDayAfter(day); - } - - bool _isExtraDayBefore(DateTime day) { - return day.month < _focusedDay.month; - } - - bool _isExtraDayAfter(DateTime day) { - return day.month > _focusedDay.month; - } - - int _clamp(int min, int max, int value) { - if (value > max) { - return max; - } else if (value < min) { - return min; - } else { - return value; - } - } -} diff --git a/lib/src/customization/calendar_builders.dart b/lib/src/customization/calendar_builders.dart index 90e6d107..cf21d2e4 100644 --- a/lib/src/customization/calendar_builders.dart +++ b/lib/src/customization/calendar_builders.dart @@ -1,91 +1,113 @@ -// Copyright (c) 2019 Aleksander Woźniak -// Licensed under Apache License v2.0 - -part of table_calendar; - -/// Main Builder signature for `TableCalendar`. Contains `date` and list of all `events` associated with that `date`. -/// Note that most of the time, `events` param will be ommited, however it is there if needed. -/// `events` param can be null. -typedef FullBuilder = Widget Function(BuildContext context, DateTime date, List events); - -/// Builder signature for a list of event markers. Contains `date` and list of all `events` associated with that `date`. -/// Both `events` and `holidays` params can be null. -typedef FullListBuilder = List Function(BuildContext context, DateTime date, List events, List holidays); - -/// Builder signature for weekday names row. Contains `weekday` string, which is formatted by `dowTextBuilder` -/// or by default function (DateFormat.E(widget.locale).format(date)), if `dowTextBuilder` is null. -typedef DowBuilder = Widget Function(BuildContext context, String weekday); - -/// Builder signature for a single event marker. Contains `date` and a single `event` associated with that `date`. -typedef SingleMarkerBuilder = Widget Function(BuildContext context, DateTime date, dynamic event); - -/// Class containing all custom Builders for `TableCalendar`. -class CalendarBuilders { - /// The most general custom Builder. Use to provide your own UI for every day cell. - /// If `dayBuilder` is not specified, a default day cell will be displayed. - /// Default day cells are customizable with `CalendarStyle`. - final FullBuilder dayBuilder; - - /// Custom Builder for currently selected day. Will overwrite `dayBuilder` on selected day. - final FullBuilder selectedDayBuilder; - - /// Custom Builder for today. Will overwrite `dayBuilder` on today. - final FullBuilder todayDayBuilder; - - /// Custom Builder for holidays. Will overwrite `dayBuilder` on holidays. - final FullBuilder holidayDayBuilder; - - /// Custom Builder for weekends. Will overwrite `dayBuilder` on weekends. - final FullBuilder weekendDayBuilder; - - /// Custom Builder for days outside of current month. Will overwrite `dayBuilder` on days outside of current month. - final FullBuilder outsideDayBuilder; - - /// Custom Builder for weekends outside of current month. Will overwrite `dayBuilder`on weekends outside of current month. - final FullBuilder outsideWeekendDayBuilder; +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/widgets.dart'; +import 'package:table_calendar/src/shared/utils.dart' + show DayBuilder, FocusedDayBuilder; + +/// Signature for a function that creates a single event marker for a given `day`. +/// Contains a single `event` associated with that `day`. +typedef SingleMarkerBuilder = Widget? Function( + BuildContext context, + DateTime day, + T event, +); + +/// Signature for a function that creates an event marker for a given `day`. +/// Contains a list of `events` associated with that `day`. +typedef MarkerBuilder = Widget? Function( + BuildContext context, + DateTime day, + List events, +); + +/// Signature for a function that creates a background highlight for a given `day`. +/// +/// Used for highlighting current range selection. +/// Contains a value determining if the given `day` falls within the selected range. +typedef HighlightBuilder = Widget? Function( + BuildContext context, + DateTime day, + bool isWithinRange, +); + +/// Class containing all custom builders for `TableCalendar`. +class CalendarBuilders { + /// Custom builder for day cells, with a priority over any other builder. + final FocusedDayBuilder? prioritizedBuilder; + + /// Custom builder for a day cell that matches the current day. + final FocusedDayBuilder? todayBuilder; + + /// Custom builder for day cells that are currently marked as selected by `selectedDayPredicate`. + final FocusedDayBuilder? selectedBuilder; + + /// Custom builder for a day cell that is the start of current range selection. + final FocusedDayBuilder? rangeStartBuilder; + + /// Custom builder for a day cell that is the end of current range selection. + final FocusedDayBuilder? rangeEndBuilder; + + /// Custom builder for day cells that fall within the currently selected range. + final FocusedDayBuilder? withinRangeBuilder; + + /// Custom builder for day cells, of which the `day.month` is different than `focusedDay.month`. + /// This will affect day cells that do not match the currently focused month. + final FocusedDayBuilder? outsideBuilder; + + /// Custom builder for day cells that have been disabled. + /// + /// This refers to dates disabled by returning false in `enabledDayPredicate`, + /// as well as dates that are outside of the bounds set up by `firstDay` and `lastDay`. + final FocusedDayBuilder? disabledBuilder; - /// Custom Builder for holidays outside of current month. Will overwrite `dayBuilder` on holidays outside of current month. - final FullBuilder outsideHolidayDayBuilder; + /// Custom builder for day cells that are marked as holidays by `holidayPredicate`. + final FocusedDayBuilder? holidayBuilder; - /// Custom Builder for days outside of `startDay` - `endDay` Date range. Will overwrite `dayBuilder` for aforementioned days. - final FullBuilder unavailableDayBuilder; + /// Custom builder for day cells that do not match any other builder. + final FocusedDayBuilder? defaultBuilder; - /// Custom Builder for a whole group of event markers. Use to provide your own marker UI for each day cell. - /// Every `Widget` passed here will be placed in a `Stack`, above the cell content. - /// Wrap them with `Positioned` to gain more control over their placement. - /// - /// If `markersBuilder` is not specified, `TableCalendar` will try to use `singleMarkerBuilder` or default markers (customizable with `CalendarStyle`). - /// Mutually exclusive with `singleMarkerBuilder`. - final FullListBuilder markersBuilder; + /// Custom builder for background highlight of range selection. + /// If `isWithinRange` is true, then `day` is within the selected range. + final HighlightBuilder? rangeHighlightBuilder; - /// Custom Builder for a single event marker. Each of those will be displayed in a `Row` above of the day cell. + /// Custom builder for a single event marker. Each of those will be displayed in a `Row` above of the day cell. /// You can adjust markers' position with `CalendarStyle` properties. /// /// If `singleMarkerBuilder` is not specified, a default event marker will be displayed (customizable with `CalendarStyle`). - /// Mutually exclusive with `markersBuilder`. - final SingleMarkerBuilder singleMarkerBuilder; + final SingleMarkerBuilder? singleMarkerBuilder; + + /// Custom builder for event markers. Use to provide your own marker UI for each day cell. + /// Using `markerBuilder` will override `singleMarkerBuilder` and default event markers. + final MarkerBuilder? markerBuilder; + + /// Custom builder for days of the week labels (Mon, Tue, Wed, etc.). + final DayBuilder? dowBuilder; - /// Custom builder for dow weekday names (displayed between `HeaderRow` and calendar days). - /// Will overwrite `weekdayStyle` and `weekendStyle` from `DaysOfWeekStyle`. - final DowBuilder dowWeekdayBuilder; + /// Use to customize header's title using different widget + final DayBuilder? headerTitleBuilder; - /// Custom builder for dow weekend names (displayed between `HeaderRow` and calendar days). - /// Will overwrite `weekendStyle` from `DaysOfWeekStyle` and `dowWeekdayBuilder` for weekends, if it also exists. - final DowBuilder dowWeekendBuilder; + /// Custom builder for number of the week labels. + final Widget? Function(BuildContext context, int weekNumber)? + weekNumberBuilder; + /// Creates `CalendarBuilders` for `TableCalendar` widget. const CalendarBuilders({ - this.dayBuilder, - this.selectedDayBuilder, - this.todayDayBuilder, - this.holidayDayBuilder, - this.weekendDayBuilder, - this.outsideDayBuilder, - this.outsideWeekendDayBuilder, - this.outsideHolidayDayBuilder, - this.unavailableDayBuilder, - this.markersBuilder, + this.prioritizedBuilder, + this.todayBuilder, + this.selectedBuilder, + this.rangeStartBuilder, + this.rangeEndBuilder, + this.withinRangeBuilder, + this.outsideBuilder, + this.disabledBuilder, + this.holidayBuilder, + this.defaultBuilder, + this.rangeHighlightBuilder, this.singleMarkerBuilder, - this.dowWeekdayBuilder, - this.dowWeekendBuilder, - }) : assert(!(singleMarkerBuilder != null && markersBuilder != null)); + this.markerBuilder, + this.dowBuilder, + this.headerTitleBuilder, + this.weekNumberBuilder, + }); } diff --git a/lib/src/customization/calendar_style.dart b/lib/src/customization/calendar_style.dart index 6f83eb1c..ecfa578e 100644 --- a/lib/src/customization/calendar_style.dart +++ b/lib/src/customization/calendar_style.dart @@ -1,131 +1,260 @@ -// Copyright (c) 2019 Aleksander Woźniak -// Licensed under Apache License v2.0 +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 -part of table_calendar; +import 'package:flutter/widgets.dart'; +import 'package:table_calendar/table_calendar.dart'; -/// Class containing styling for `TableCalendar`'s content. +/// Class containing styling and configuration for `TableCalendar`'s content. class CalendarStyle { - /// BoxDecoration for each interior row of the table - final BoxDecoration contentDecoration; + /// Maximum amount of single event marker dots to be displayed. + final int markersMaxCount; - /// Style of foreground Text for regular weekdays. - final TextStyle weekdayStyle; + /// Specifies if event markers rendered for a day cell can overflow cell's boundaries. + /// * `true` - Event markers will be drawn over the cell boundaries + /// * `false` - Event markers will be clipped if they are too big + final bool canMarkersOverflow; - /// Style of foreground Text for regular weekends. - final TextStyle weekendStyle; + /// Determines if single event marker dots should be aligned automatically with `markersAnchor`. + /// If `false`, `markersOffset` will be used instead. + final bool markersAutoAligned; - /// Style of foreground Text for holidays. - final TextStyle holidayStyle; + /// Specifies the anchor point of single event markers if `markersAutoAligned` is `true`. + /// A value of `0.5` will center the markers at the bottom edge of day cell's decoration. + /// + /// Includes `cellMargin` for calculations. + final double markersAnchor; + + /// The size of single event marker dot. + /// + /// By default `markerSizeScale` is used. To use `markerSize` instead, simply provide a non-null value. + final double? markerSize; - /// Style of foreground Text for selected day. - final TextStyle selectedStyle; + /// Proportion of single event marker dot size in relation to day cell size. + /// + /// Includes `cellMargin` for calculations. + final double markerSizeScale; - /// Style of foreground Text for today. - final TextStyle todayStyle; + /// `PositionedOffset` for event markers. Allows to specify `top`, `bottom`, `start` and `end`. + final PositionedOffset markersOffset; - /// Style of foreground Text for weekdays outside of current month. - final TextStyle outsideStyle; + /// General `Alignment` for event markers. + /// Will have no effect on markers if `markersAutoAligned` or `markersOffset` is used. + final AlignmentGeometry markersAlignment; - /// Style of foreground Text for weekends outside of current month. - final TextStyle outsideWeekendStyle; + /// Decoration of single event markers. Affects each marker dot. + final Decoration markerDecoration; - /// Style of foreground Text for holidays outside of current month. - final TextStyle outsideHolidayStyle; + /// Margin of single event markers. Affects each marker dot. + final EdgeInsets markerMargin; - /// Style of foreground Text for days outside of `startDay` - `endDay` Date range. - final TextStyle unavailableStyle; + /// Margin of each individual day cell. + final EdgeInsets cellMargin; - /// Style of foreground Text for days that contain events. - final TextStyle eventDayStyle; + /// Padding of each individual day cell. + final EdgeInsets cellPadding; - /// Background Color of selected day. - final Color selectedColor; + /// Alignment of each individual day cell. + final AlignmentGeometry cellAlignment; - /// Background Color of today. - final Color todayColor; + /// Proportion of range selection highlight size in relation to day cell size. + /// + /// Includes `cellMargin` for calculations. + final double rangeHighlightScale; - /// Color of event markers placed on the bottom of every day containing events. - final Color markersColor; + /// Color of range selection highlight. + final Color rangeHighlightColor; - /// General `Alignment` for event markers. - /// NOTE: `markersPositionBottom` defaults to `5.0`, so you might want to set it to `null` when using `markersAlignment`. - final Alignment markersAlignment; + /// Determines if day cells that do not match the currently focused month should be visible. + /// + /// Affects only `CalendarFormat.month`. + final bool outsideDaysVisible; + + /// Determines if a day cell that matches the current day should be highlighted. + final bool isTodayHighlighted; + + /// TextStyle for a day cell that matches the current day. + final TextStyle todayTextStyle; + + /// Decoration for a day cell that matches the current day. + final Decoration todayDecoration; + + /// TextStyle for day cells that are currently marked as selected by `selectedDayPredicate`. + final TextStyle selectedTextStyle; + + /// Decoration for day cells that are currently marked as selected by `selectedDayPredicate`. + final Decoration selectedDecoration; + + /// TextStyle for a day cell that is the start of current range selection. + final TextStyle rangeStartTextStyle; + + /// Decoration for a day cell that is the start of current range selection. + final Decoration rangeStartDecoration; + + /// TextStyle for a day cell that is the end of current range selection. + final TextStyle rangeEndTextStyle; - /// `top` property of `Positioned` widget used for event markers. - final double markersPositionTop; + /// Decoration for a day cell that is the end of current range selection. + final Decoration rangeEndDecoration; - /// `bottom` property of `Positioned` widget used for event markers. - /// NOTE: This defaults to `5.0`, so you might occasionally want to set it to `null`. - final double markersPositionBottom; + /// TextStyle for day cells that fall within the currently selected range. + final TextStyle withinRangeTextStyle; - /// `left` property of `Positioned` widget used for event markers. - final double markersPositionLeft; + /// Decoration for day cells that fall within the currently selected range. + final Decoration withinRangeDecoration; - /// `right` property of `Positioned` widget used for event markers. - final double markersPositionRight; + /// TextStyle for day cells, of which the `day.month` is different than `focusedDay.month`. + /// This will affect day cells that do not match the currently focused month. + final TextStyle outsideTextStyle; - /// Maximum amount of event markers to be displayed. - final int markersMaxAmount; + /// Decoration for day cells, of which the `day.month` is different than `focusedDay.month`. + /// This will affect day cells that do not match the currently focused month. + final Decoration outsideDecoration; - /// Specifies whether or not days outside of current month should be displayed. + /// TextStyle for day cells that have been disabled. /// - /// Sometimes a fragment of previous month's last week (or next month's first week) appears in current month's view. - /// This property defines if those should be visible (eg. with custom style) or hidden. - final bool outsideDaysVisible; + /// This refers to dates disabled by returning false in `enabledDayPredicate`, + /// as well as dates that are outside of the bounds set up by `firstDay` and `lastDay`. + final TextStyle disabledTextStyle; - /// Determines rendering priority for SelectedDay and Today. - /// * `true` - SelectedDay will have higher priority than Today - /// * `false` - Today will have higher priority than SelectedDay - final bool renderSelectedFirst; + /// Decoration for day cells that have been disabled. + /// + /// This refers to dates disabled by returning false in `enabledDayPredicate`, + /// as well as dates that are outside of the bounds set up by `firstDay` and `lastDay`. + final Decoration disabledDecoration; - /// Determines whether the row of days of the week should be rendered or not. - final bool renderDaysOfWeek; + /// TextStyle for day cells that are marked as holidays by `holidayPredicate`. + final TextStyle holidayTextStyle; - /// Padding of `TableCalendar`'s content. - final EdgeInsets contentPadding; + /// Decoration for day cells that are marked as holidays by `holidayPredicate`. + final Decoration holidayDecoration; - /// Margin of Cells' decoration. - final EdgeInsets cellMargin; + /// TextStyle for day cells that match `weekendDay` list. + final TextStyle weekendTextStyle; - /// Specifies if event markers rendered for a day cell can overflow cell's boundaries. - /// * `true` - Event markers will be drawn over the cell boundaries - /// * `false` - Event markers will not be drawn over the cell boundaries and will be clipped if they are too big - final bool canEventMarkersOverflow; + /// Decoration for day cells that match `weekendDay` list. + final Decoration weekendDecoration; - /// Specifies whether or not SelectedDay should be highlighted. - final bool highlightSelected; + /// TextStyle for week number. + final TextStyle weekNumberTextStyle; - /// Specifies whether or not Today should be highlighted. - final bool highlightToday; + /// TextStyle for day cells that do not match any other styles. + final TextStyle defaultTextStyle; + /// Decoration for day cells that do not match any other styles. + final Decoration defaultDecoration; + + /// Decoration for each interior row of day cells. + final Decoration rowDecoration; + + /// Border for the internal `Table` widget. + final TableBorder tableBorder; + + /// Padding for the internal `Table` widget. + final EdgeInsets tablePadding; + + /// Use to customize the text within each day cell. + /// Defaults to `'${date.day}'`, to show just the day number. + /// + /// Example usage: + /// ```dart + /// dayTextFormatter: (date, locale) => DateFormat.d(locale).format(date), + /// ``` + final TextFormatter? dayTextFormatter; + + /// Creates a `CalendarStyle` used by `TableCalendar` widget. const CalendarStyle({ - this.contentDecoration = const BoxDecoration(), - this.weekdayStyle = const TextStyle(), - this.weekendStyle = const TextStyle(color: const Color(0xFFF44336)), // Material red[500] - this.holidayStyle = const TextStyle(color: const Color(0xFFF44336)), // Material red[500] - this.selectedStyle = const TextStyle(color: const Color(0xFFFAFAFA), fontSize: 16.0), // Material grey[50] - this.todayStyle = const TextStyle(color: const Color(0xFFFAFAFA), fontSize: 16.0), // Material grey[50] - this.outsideStyle = const TextStyle(color: const Color(0xFF9E9E9E)), // Material grey[500] - this.outsideWeekendStyle = const TextStyle(color: const Color(0xFFEF9A9A)), // Material red[200] - this.outsideHolidayStyle = const TextStyle(color: const Color(0xFFEF9A9A)), // Material red[200] - this.unavailableStyle = const TextStyle(color: const Color(0xFFBFBFBF)), - this.eventDayStyle = const TextStyle(), - this.selectedColor = const Color(0xFF5C6BC0), // Material indigo[400] - this.todayColor = const Color(0xFF9FA8DA), // Material indigo[200] - this.markersColor = const Color(0xFF263238), // Material blueGrey[900] - this.markersAlignment = Alignment.bottomCenter, - this.markersPositionTop, - this.markersPositionBottom = 5.0, - this.markersPositionLeft, - this.markersPositionRight, - this.markersMaxAmount = 4, + this.isTodayHighlighted = true, + this.canMarkersOverflow = true, this.outsideDaysVisible = true, - this.renderSelectedFirst = true, - this.renderDaysOfWeek = true, - this.contentPadding = const EdgeInsets.only(bottom: 4.0, left: 8.0, right: 8.0), + this.markersAutoAligned = true, + this.markerSize, + this.markerSizeScale = 0.2, + this.markersAnchor = 0.7, + this.rangeHighlightScale = 1.0, + this.markerMargin = const EdgeInsets.symmetric(horizontal: 0.3), + this.markersAlignment = Alignment.bottomCenter, + this.markersMaxCount = 4, this.cellMargin = const EdgeInsets.all(6.0), - this.canEventMarkersOverflow = false, - this.highlightSelected = true, - this.highlightToday = true, + this.cellPadding = EdgeInsets.zero, + this.cellAlignment = Alignment.center, + this.markersOffset = const PositionedOffset(), + this.rangeHighlightColor = const Color(0xFFBBDDFF), + this.markerDecoration = const BoxDecoration( + color: Color(0xFF263238), + shape: BoxShape.circle, + ), + this.todayTextStyle = const TextStyle( + color: Color(0xFFFAFAFA), + fontSize: 16.0, + ), // + this.todayDecoration = const BoxDecoration( + color: Color(0xFF9FA8DA), + shape: BoxShape.circle, + ), + this.selectedTextStyle = const TextStyle( + color: Color(0xFFFAFAFA), + fontSize: 16.0, + ), + this.selectedDecoration = const BoxDecoration( + color: Color(0xFF5C6BC0), + shape: BoxShape.circle, + ), + this.rangeStartTextStyle = const TextStyle( + color: Color(0xFFFAFAFA), + fontSize: 16.0, + ), + this.rangeStartDecoration = const BoxDecoration( + color: Color(0xFF6699FF), + shape: BoxShape.circle, + ), + this.rangeEndTextStyle = const TextStyle( + color: Color(0xFFFAFAFA), + fontSize: 16.0, + ), + this.rangeEndDecoration = const BoxDecoration( + color: Color(0xFF6699FF), + shape: BoxShape.circle, + ), + this.withinRangeTextStyle = const TextStyle(), + this.withinRangeDecoration = const BoxDecoration(shape: BoxShape.circle), + this.outsideTextStyle = const TextStyle(color: Color(0xFFAEAEAE)), + this.outsideDecoration = const BoxDecoration(shape: BoxShape.circle), + this.disabledTextStyle = const TextStyle(color: Color(0xFFBFBFBF)), + this.disabledDecoration = const BoxDecoration(shape: BoxShape.circle), + this.holidayTextStyle = const TextStyle(color: Color(0xFF5C6BC0)), + this.holidayDecoration = const BoxDecoration( + border: Border.fromBorderSide( + BorderSide(color: Color(0xFF9FA8DA), width: 1.4), + ), + shape: BoxShape.circle, + ), + this.weekendTextStyle = const TextStyle(color: Color(0xFF5A5A5A)), + this.weekendDecoration = const BoxDecoration(shape: BoxShape.circle), + this.weekNumberTextStyle = + const TextStyle(fontSize: 12, color: Color(0xFFBFBFBF)), + this.defaultTextStyle = const TextStyle(), + this.defaultDecoration = const BoxDecoration(shape: BoxShape.circle), + this.rowDecoration = const BoxDecoration(), + this.tableBorder = const TableBorder(), + this.tablePadding = EdgeInsets.zero, + this.dayTextFormatter, }); } + +/// Helper class containing data for internal `Positioned` widget. +class PositionedOffset { + /// Distance from the top edge. + final double? top; + + /// Distance from the bottom edge. + final double? bottom; + + /// Distance from the leading edge. + final double? start; + + /// Distance from the trailing edge. + final double? end; + + /// Creates a `PositionedOffset`. Values are set to `null` by default. + const PositionedOffset({this.top, this.bottom, this.start, this.end}); +} diff --git a/lib/src/customization/days_of_week_style.dart b/lib/src/customization/days_of_week_style.dart index fe5dcd26..8ec79212 100644 --- a/lib/src/customization/days_of_week_style.dart +++ b/lib/src/customization/days_of_week_style.dart @@ -1,33 +1,35 @@ -// Copyright (c) 2019 Aleksander Woźniak -// Licensed under Apache License v2.0 +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 -part of table_calendar; +import 'package:flutter/widgets.dart'; +import 'package:table_calendar/src/shared/utils.dart' show TextFormatter; /// Class containing styling for `TableCalendar`'s days of week panel. class DaysOfWeekStyle { - /// Use to customize days of week panel text (eg. with different `DateFormat`). + /// Use to customize days of week panel text (e.g. with different `DateFormat`). /// You can use `String` transformations to further customize the text. - /// Defaults to simple `'E'` format (eg. Mon, Tue, Wed, etc.). + /// Defaults to simple `'E'` format (i.e. Mon, Tue, Wed, etc.). /// /// Example usage: /// ```dart - /// dowTextBuilder: (date, locale) => DateFormat.E(locale).format(date)[0], + /// dowTextFormatter: (date, locale) => DateFormat.E(locale).format(date)[0], /// ``` - final TextBuilder dowTextBuilder; + final TextFormatter? dowTextFormatter; - /// BoxDecoration for the top row of the table - final BoxDecoration decoration; + /// Decoration for the top row of the table + final Decoration decoration; - /// Style for weekdays on the top of Calendar. + /// Style for weekdays on the top of calendar. final TextStyle weekdayStyle; - /// Style for weekend days on the top of Calendar. + /// Style for weekend days on the top of calendar. final TextStyle weekendStyle; + /// Creates a `DaysOfWeekStyle` used by `TableCalendar` widget. const DaysOfWeekStyle({ - this.dowTextBuilder, + this.dowTextFormatter, this.decoration = const BoxDecoration(), - this.weekdayStyle = const TextStyle(color: const Color(0xFF616161)), // Material grey[700] - this.weekendStyle = const TextStyle(color: const Color(0xFFF44336)), // Material red[500] + this.weekdayStyle = const TextStyle(color: Color(0xFF4F4F4F)), + this.weekendStyle = const TextStyle(color: Color(0xFF6A6A6A)), }); } diff --git a/lib/src/customization/header_style.dart b/lib/src/customization/header_style.dart index 9ef5fee4..71ac67df 100644 --- a/lib/src/customization/header_style.dart +++ b/lib/src/customization/header_style.dart @@ -1,12 +1,13 @@ -// Copyright (c) 2019 Aleksander Woźniak -// Licensed under Apache License v2.0 +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 -part of table_calendar; +import 'package:flutter/material.dart'; +import 'package:table_calendar/src/shared/utils.dart' show TextFormatter; /// Class containing styling and configuration of `TableCalendar`'s header. class HeaderStyle { /// Responsible for making title Text centered. - final bool centerHeaderTitle; + final bool titleCentered; /// Responsible for FormatButton visibility. final bool formatButtonVisible; @@ -16,15 +17,15 @@ class HeaderStyle { /// * `false` - the button will show current CalendarFormat final bool formatButtonShowsNext; - /// Use to customize header's title text (eg. with different `DateFormat`). + /// Use to customize header's title text (e.g. with different `DateFormat`). /// You can use `String` transformations to further customize the text. - /// Defaults to simple `'yMMMM'` format (eg. January 2019, February 2019, March 2019, etc.). + /// Defaults to simple `'yMMMM'` format (i.e. January 2019, February 2019, March 2019, etc.). /// /// Example usage: /// ```dart - /// titleTextBuilder: (date, locale) => DateFormat.yM(locale).format(date), + /// titleTextFormatter: (date, locale) => DateFormat.yM(locale).format(date), /// ``` - final TextBuilder titleTextBuilder; + final TextFormatter? titleTextFormatter; /// Style for title Text (month-year) displayed in header. final TextStyle titleTextStyle; @@ -33,68 +34,74 @@ class HeaderStyle { final TextStyle formatButtonTextStyle; /// Background `Decoration` for FormatButton. - final Decoration formatButtonDecoration; + final BoxDecoration formatButtonDecoration; - /// Inside padding of the whole header. + /// Internal padding of the whole header. final EdgeInsets headerPadding; - /// Outside margin of the whole header. + /// External margin of the whole header. final EdgeInsets headerMargin; - /// Inside padding for FormatButton. + /// Internal padding of FormatButton. final EdgeInsets formatButtonPadding; - /// Inside padding for left chevron. + /// Internal padding of left chevron. + /// Determines how much of ripple animation is visible during taps. final EdgeInsets leftChevronPadding; - /// Inside padding for right chevron. + /// Internal padding of right chevron. + /// Determines how much of ripple animation is visible during taps. final EdgeInsets rightChevronPadding; - /// Outside margin for left chevron. + /// External margin of left chevron. final EdgeInsets leftChevronMargin; - /// Outside margin for right chevron. + /// External margin of right chevron. final EdgeInsets rightChevronMargin; - /// Icon used for left chevron. - /// Defaults to black `Icons.chevron_left`. - final Icon leftChevronIcon; + /// Widget used for left chevron. + /// + /// Tapping on it will navigate to previous calendar page. + final Widget leftChevronIcon; + + /// Widget used for right chevron. + /// + /// Tapping on it will navigate to next calendar page. + final Widget rightChevronIcon; - /// Icon used for right chevron. - /// Defaults to black `Icons.chevron_right`. - final Icon rightChevronIcon; + /// Determines left chevron's visibility. + final bool leftChevronVisible; - /// Show or hide chevrons. - /// Defaults to `true`. - final bool showLeftChevron; - final bool showRightChevron; + /// Determines right chevron's visibility. + final bool rightChevronVisible; - /// Header decoration, used to draw border or shadow or change color of the header - /// Defaults to empty BoxDecoration. + /// Decoration of the header. final BoxDecoration decoration; + /// Creates a `HeaderStyle` used by `TableCalendar` widget. const HeaderStyle({ - this.centerHeaderTitle = false, + this.titleCentered = false, this.formatButtonVisible = true, this.formatButtonShowsNext = true, - this.titleTextBuilder, + this.titleTextFormatter, this.titleTextStyle = const TextStyle(fontSize: 17.0), - this.formatButtonTextStyle = const TextStyle(), + this.formatButtonTextStyle = const TextStyle(fontSize: 14.0), this.formatButtonDecoration = const BoxDecoration( - border: const Border(top: BorderSide(), bottom: BorderSide(), left: BorderSide(), right: BorderSide()), - borderRadius: const BorderRadius.all(Radius.circular(12.0)), + border: Border.fromBorderSide(BorderSide()), + borderRadius: BorderRadius.all(Radius.circular(12.0)), ), - this.headerMargin, + this.headerMargin = EdgeInsets.zero, this.headerPadding = const EdgeInsets.symmetric(vertical: 8.0), - this.formatButtonPadding = const EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0), + this.formatButtonPadding = + const EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0), this.leftChevronPadding = const EdgeInsets.all(12.0), this.rightChevronPadding = const EdgeInsets.all(12.0), this.leftChevronMargin = const EdgeInsets.symmetric(horizontal: 8.0), this.rightChevronMargin = const EdgeInsets.symmetric(horizontal: 8.0), this.leftChevronIcon = const Icon(Icons.chevron_left), this.rightChevronIcon = const Icon(Icons.chevron_right), - this.showLeftChevron = true, - this.showRightChevron = true, + this.leftChevronVisible = true, + this.rightChevronVisible = true, this.decoration = const BoxDecoration(), }); } diff --git a/lib/src/shared/utils.dart b/lib/src/shared/utils.dart new file mode 100644 index 00000000..7e63f1ec --- /dev/null +++ b/lib/src/shared/utils.dart @@ -0,0 +1,57 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/widgets.dart'; + +/// Signature for a function that creates a widget for a given `day`. +typedef DayBuilder = Widget? Function(BuildContext context, DateTime day); + +/// Signature for a function that creates a widget for a given `day`. +/// Additionally, contains the currently focused day. +typedef FocusedDayBuilder = Widget? Function( + BuildContext context, + DateTime day, + DateTime focusedDay, +); + +/// Signature for a function returning text that can be localized and formatted with `DateFormat`. +typedef TextFormatter = String Function(DateTime date, dynamic locale); + +/// Gestures available for the calendar. +enum AvailableGestures { none, verticalSwipe, horizontalSwipe, all } + +/// Formats that the calendar can display. +enum CalendarFormat { month, twoWeeks, week } + +/// Days of the week that the calendar can start with. +enum StartingDayOfWeek { + monday, + tuesday, + wednesday, + thursday, + friday, + saturday, + sunday, +} + +/// Returns a numerical value associated with given `weekday`. +/// +/// Returns 1 for `StartingDayOfWeek.monday`, all the way to 7 for `StartingDayOfWeek.sunday`. +int getWeekdayNumber(StartingDayOfWeek weekday) { + return StartingDayOfWeek.values.indexOf(weekday) + 1; +} + +/// Returns `date` in UTC format, without its time part. +DateTime normalizeDate(DateTime date) { + return DateTime.utc(date.year, date.month, date.day); +} + +/// Checks if two DateTime objects are the same day. +/// Returns `false` if either of them is null. +bool isSameDay(DateTime? a, DateTime? b) { + if (a == null || b == null) { + return false; + } + + return a.year == b.year && a.month == b.month && a.day == b.day; +} diff --git a/lib/src/table_calendar.dart b/lib/src/table_calendar.dart new file mode 100644 index 00000000..aca0a03c --- /dev/null +++ b/lib/src/table_calendar.dart @@ -0,0 +1,787 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:math'; + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:simple_gesture_detector/simple_gesture_detector.dart'; +import 'package:table_calendar/src/customization/calendar_builders.dart'; +import 'package:table_calendar/src/customization/calendar_style.dart'; +import 'package:table_calendar/src/customization/days_of_week_style.dart'; +import 'package:table_calendar/src/customization/header_style.dart'; +import 'package:table_calendar/src/shared/utils.dart'; +import 'package:table_calendar/src/table_calendar_base.dart'; +import 'package:table_calendar/src/widgets/calendar_header.dart'; +import 'package:table_calendar/src/widgets/cell_content.dart'; + +/// Signature for `onDaySelected` callback. Contains the selected day and focused day. +typedef OnDaySelected = void Function( + DateTime selectedDay, + DateTime focusedDay, +); + +/// Signature for `onRangeSelected` callback. +/// Contains start and end of the selected range, as well as currently focused day. +typedef OnRangeSelected = void Function( + DateTime? start, + DateTime? end, + DateTime focusedDay, +); + +/// Modes that range selection can operate in. +enum RangeSelectionMode { disabled, toggledOff, toggledOn, enforced } + +/// Highly customizable, feature-packed Flutter calendar with gestures, animations and multiple formats. +class TableCalendar extends StatefulWidget { + /// Locale to format `TableCalendar` dates with, for example: `'en_US'`. + /// + /// If nothing is provided, a default locale will be used. + final dynamic locale; + + /// The start of the selected day range. + final DateTime? rangeStartDay; + + /// The end of the selected day range. + final DateTime? rangeEndDay; + + /// DateTime that determines which days are currently visible and focused. + final DateTime focusedDay; + + /// The first active day of `TableCalendar`. + /// Blocks swiping to days before it. + /// + /// Days before it will use `disabledStyle` and trigger `onDisabledDayTapped` callback. + final DateTime firstDay; + + /// The last active day of `TableCalendar`. + /// Blocks swiping to days after it. + /// + /// Days after it will use `disabledStyle` and trigger `onDisabledDayTapped` callback. + final DateTime lastDay; + + /// DateTime that will be treated as today. Defaults to `DateTime.now()`. + /// + /// Overriding this property might be useful for testing. + final DateTime? currentDay; + + /// List of days treated as weekend days. + /// Use built-in `DateTime` weekday constants (e.g. `DateTime.monday`) instead of `int` literals (e.g. `1`). + final List weekendDays; + + /// Specifies `TableCalendar`'s current format. + final CalendarFormat calendarFormat; + + /// `Map` of `CalendarFormat`s and `String` names associated with them. + /// Those `CalendarFormat`s will be used by internal logic to manage displayed format. + /// + /// To ensure proper vertical swipe behavior, `CalendarFormat`s should be in descending order (i.e. from biggest to smallest). + /// + /// For example: + /// ```dart + /// availableCalendarFormats: const { + /// CalendarFormat.month: 'Month', + /// CalendarFormat.week: 'Week', + /// } + /// ``` + final Map availableCalendarFormats; + + /// Determines the visibility of calendar header. + final bool headerVisible; + + /// Determines the visibility of the row of days of the week. + final bool daysOfWeekVisible; + + /// When set to true, tapping on an outside day in `CalendarFormat.month` format + /// will jump to the calendar page of the tapped month. + final bool pageJumpingEnabled; + + /// When set to true, updating the `focusedDay` will display a scrolling animation + /// if the currently visible calendar page is changed. + final bool pageAnimationEnabled; + + /// When set to true, `CalendarFormat.month` will always display six weeks, + /// even if the content would fit in less. + final bool sixWeekMonthsEnforced; + + /// When set to true, `TableCalendar` will fill available height. + final bool shouldFillViewport; + + /// Whether to display week numbers on calendar. + final bool weekNumbersVisible; + + /// Used for setting the height of `TableCalendar`'s rows. + final double rowHeight; + + /// Used for setting the height of `TableCalendar`'s days of week row. + final double daysOfWeekHeight; + + /// Specifies the duration of size animation that takes place whenever `calendarFormat` is changed. + final Duration formatAnimationDuration; + + /// Specifies the curve of size animation that takes place whenever `calendarFormat` is changed. + final Curve formatAnimationCurve; + + /// Specifies the duration of scrolling animation that takes place whenever the visible calendar page is changed. + final Duration pageAnimationDuration; + + /// Specifies the curve of scrolling animation that takes place whenever the visible calendar page is changed. + final Curve pageAnimationCurve; + + /// `TableCalendar` will start weeks with provided day. + /// + /// Use `StartingDayOfWeek.monday` for Monday - Sunday week format. + /// Use `StartingDayOfWeek.sunday` for Sunday - Saturday week format. + final StartingDayOfWeek startingDayOfWeek; + + /// `HitTestBehavior` for every day cell inside `TableCalendar`. + final HitTestBehavior dayHitTestBehavior; + + /// Specifies swipe gestures available to `TableCalendar`. + /// If `AvailableGestures.none` is used, the calendar will only be interactive via buttons. + final AvailableGestures availableGestures; + + /// Configuration for vertical swipe detector. + final SimpleSwipeConfig simpleSwipeConfig; + + /// Style for `TableCalendar`'s header. + final HeaderStyle headerStyle; + + /// Style for days of week displayed between `TableCalendar`'s header and content. + final DaysOfWeekStyle daysOfWeekStyle; + + /// Style for `TableCalendar`'s content. + final CalendarStyle calendarStyle; + + /// Set of custom builders for `TableCalendar` to work with. + /// Use those to fully tailor the UI. + final CalendarBuilders calendarBuilders; + + /// Current mode of range selection. + /// + /// * `RangeSelectionMode.disabled` - range selection is always off. + /// * `RangeSelectionMode.toggledOff` - range selection is currently off, can be toggled by longpressing a day cell. + /// * `RangeSelectionMode.toggledOn` - range selection is currently on, can be toggled by longpressing a day cell. + /// * `RangeSelectionMode.enforced` - range selection is always on. + final RangeSelectionMode rangeSelectionMode; + + /// Allows to load events for days that are not enabled + /// If `true` it will ignore `enabledDayPredicate` when calling `eventLoader`. + /// If `false` then `enabledDayPredicate` will be used to check when to call `eventLoader` + final bool loadEventsForDisabledDays; + + /// Function that assigns a list of events to a specified day. + final List Function(DateTime day)? eventLoader; + + /// Function deciding whether given day should be enabled or not. + /// If `false` is returned, this day will be disabled. + final bool Function(DateTime day)? enabledDayPredicate; + + /// Function deciding whether given day should be marked as selected. + final bool Function(DateTime day)? selectedDayPredicate; + + /// Function deciding whether given day is treated as a holiday. + final bool Function(DateTime day)? holidayPredicate; + + /// Called whenever a day range gets selected. + final OnRangeSelected? onRangeSelected; + + /// Called whenever any day gets tapped. + final OnDaySelected? onDaySelected; + + /// Called whenever any day gets long pressed. + final OnDaySelected? onDayLongPressed; + + /// Called whenever any disabled day gets tapped. + final void Function(DateTime day)? onDisabledDayTapped; + + /// Called whenever any disabled day gets long pressed. + final void Function(DateTime day)? onDisabledDayLongPressed; + + /// Called whenever header gets tapped. + final void Function(DateTime focusedDay)? onHeaderTapped; + + /// Called whenever header gets long pressed. + final void Function(DateTime focusedDay)? onHeaderLongPressed; + + /// Called whenever currently visible calendar page is changed. + final void Function(DateTime focusedDay)? onPageChanged; + + /// Called whenever `calendarFormat` is changed. + final void Function(CalendarFormat format)? onFormatChanged; + + /// Called when the calendar is created. Exposes its PageController. + final void Function(PageController pageController)? onCalendarCreated; + + /// Creates a `TableCalendar` widget. + TableCalendar({ + super.key, + required DateTime focusedDay, + required DateTime firstDay, + required DateTime lastDay, + DateTime? currentDay, + this.locale, + this.rangeStartDay, + this.rangeEndDay, + this.weekendDays = const [DateTime.saturday, DateTime.sunday], + this.calendarFormat = CalendarFormat.month, + this.availableCalendarFormats = const { + CalendarFormat.month: 'Month', + CalendarFormat.twoWeeks: '2 weeks', + CalendarFormat.week: 'Week', + }, + this.headerVisible = true, + this.daysOfWeekVisible = true, + this.pageJumpingEnabled = false, + this.pageAnimationEnabled = true, + this.sixWeekMonthsEnforced = false, + this.shouldFillViewport = false, + this.weekNumbersVisible = false, + this.rowHeight = 52.0, + this.daysOfWeekHeight = 16.0, + this.formatAnimationDuration = const Duration(milliseconds: 200), + this.formatAnimationCurve = Curves.linear, + this.pageAnimationDuration = const Duration(milliseconds: 300), + this.pageAnimationCurve = Curves.easeOut, + this.startingDayOfWeek = StartingDayOfWeek.sunday, + this.dayHitTestBehavior = HitTestBehavior.opaque, + this.availableGestures = AvailableGestures.all, + this.simpleSwipeConfig = const SimpleSwipeConfig( + verticalThreshold: 25.0, + swipeDetectionBehavior: SwipeDetectionBehavior.continuousDistinct, + ), + this.headerStyle = const HeaderStyle(), + this.daysOfWeekStyle = const DaysOfWeekStyle(), + this.calendarStyle = const CalendarStyle(), + this.calendarBuilders = const CalendarBuilders(), + this.rangeSelectionMode = RangeSelectionMode.toggledOff, + this.eventLoader, + this.enabledDayPredicate, + this.loadEventsForDisabledDays = false, + this.selectedDayPredicate, + this.holidayPredicate, + this.onRangeSelected, + this.onDaySelected, + this.onDayLongPressed, + this.onDisabledDayTapped, + this.onDisabledDayLongPressed, + this.onHeaderTapped, + this.onHeaderLongPressed, + this.onPageChanged, + this.onFormatChanged, + this.onCalendarCreated, + }) : assert(availableCalendarFormats.keys.contains(calendarFormat)), + assert(availableCalendarFormats.length <= CalendarFormat.values.length), + assert( + weekendDays.isEmpty || + weekendDays.every( + (day) => day >= DateTime.monday && day <= DateTime.sunday, + ), + ), + focusedDay = normalizeDate(focusedDay), + firstDay = normalizeDate(firstDay), + lastDay = normalizeDate(lastDay), + currentDay = currentDay ?? DateTime.now(); + + @override + State> createState() => _TableCalendarState(); +} + +class _TableCalendarState extends State> { + late final PageController _pageController; + late final ValueNotifier _focusedDay; + late RangeSelectionMode _rangeSelectionMode; + DateTime? _firstSelectedDay; + + @override + void initState() { + super.initState(); + _focusedDay = ValueNotifier(widget.focusedDay); + _rangeSelectionMode = widget.rangeSelectionMode; + } + + @override + void didUpdateWidget(TableCalendar oldWidget) { + super.didUpdateWidget(oldWidget); + + if (_focusedDay.value != widget.focusedDay) { + _focusedDay.value = widget.focusedDay; + } + + if (_rangeSelectionMode != widget.rangeSelectionMode) { + _rangeSelectionMode = widget.rangeSelectionMode; + } + + if (widget.rangeStartDay == null && widget.rangeEndDay == null) { + _firstSelectedDay = null; + } + } + + @override + void dispose() { + _focusedDay.dispose(); + super.dispose(); + } + + bool get _isRangeSelectionToggleable => + _rangeSelectionMode == RangeSelectionMode.toggledOn || + _rangeSelectionMode == RangeSelectionMode.toggledOff; + + bool get _isRangeSelectionOn => + _rangeSelectionMode == RangeSelectionMode.toggledOn || + _rangeSelectionMode == RangeSelectionMode.enforced; + + bool get _shouldBlockOutsideDays => + !widget.calendarStyle.outsideDaysVisible && + widget.calendarFormat == CalendarFormat.month; + + void _swipeCalendarFormat(SwipeDirection direction) { + if (widget.onFormatChanged != null) { + final formats = widget.availableCalendarFormats.keys.toList(); + + final isSwipeUp = direction == SwipeDirection.up; + int id = formats.indexOf(widget.calendarFormat); + + // Order of CalendarFormats must be from biggest to smallest, + // e.g.: [month, twoWeeks, week] + if (isSwipeUp) { + id = min(formats.length - 1, id + 1); + } else { + id = max(0, id - 1); + } + + widget.onFormatChanged!(formats[id]); + } + } + + void _onDayTapped(DateTime day) { + final isOutside = day.month != _focusedDay.value.month; + if (isOutside && _shouldBlockOutsideDays) { + return; + } + + if (_isDayDisabled(day)) { + return widget.onDisabledDayTapped?.call(day); + } + + _updateFocusOnTap(day); + + if (_isRangeSelectionOn && widget.onRangeSelected != null) { + if (_firstSelectedDay == null) { + _firstSelectedDay = day; + widget.onRangeSelected!(_firstSelectedDay, null, _focusedDay.value); + } else { + if (day.isAfter(_firstSelectedDay!)) { + widget.onRangeSelected!(_firstSelectedDay, day, _focusedDay.value); + _firstSelectedDay = null; + } else if (day.isBefore(_firstSelectedDay!)) { + widget.onRangeSelected!(day, _firstSelectedDay, _focusedDay.value); + _firstSelectedDay = null; + } + } + } else { + widget.onDaySelected?.call(day, _focusedDay.value); + } + } + + void _onDayLongPressed(DateTime day) { + final isOutside = day.month != _focusedDay.value.month; + if (isOutside && _shouldBlockOutsideDays) { + return; + } + + if (_isDayDisabled(day)) { + return widget.onDisabledDayLongPressed?.call(day); + } + + if (widget.onDayLongPressed != null) { + _updateFocusOnTap(day); + return widget.onDayLongPressed!(day, _focusedDay.value); + } + + if (widget.onRangeSelected != null) { + if (_isRangeSelectionToggleable) { + _updateFocusOnTap(day); + _toggleRangeSelection(); + + if (_isRangeSelectionOn) { + _firstSelectedDay = day; + widget.onRangeSelected!(_firstSelectedDay, null, _focusedDay.value); + } else { + _firstSelectedDay = null; + widget.onDaySelected?.call(day, _focusedDay.value); + } + } + } + } + + void _updateFocusOnTap(DateTime day) { + if (widget.pageJumpingEnabled) { + _focusedDay.value = day; + return; + } + + if (widget.calendarFormat == CalendarFormat.month) { + if (_isBeforeMonth(day, _focusedDay.value)) { + _focusedDay.value = _firstDayOfMonth(_focusedDay.value); + } else if (_isAfterMonth(day, _focusedDay.value)) { + _focusedDay.value = _lastDayOfMonth(_focusedDay.value); + } else { + _focusedDay.value = day; + } + } else { + _focusedDay.value = day; + } + } + + void _toggleRangeSelection() { + if (_rangeSelectionMode == RangeSelectionMode.toggledOn) { + _rangeSelectionMode = RangeSelectionMode.toggledOff; + } else { + _rangeSelectionMode = RangeSelectionMode.toggledOn; + } + } + + void _onLeftChevronTap() { + _pageController.previousPage( + duration: widget.pageAnimationDuration, + curve: widget.pageAnimationCurve, + ); + } + + void _onRightChevronTap() { + _pageController.nextPage( + duration: widget.pageAnimationDuration, + curve: widget.pageAnimationCurve, + ); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (widget.headerVisible) + ValueListenableBuilder( + valueListenable: _focusedDay, + builder: (context, value, _) { + return CalendarHeader( + headerTitleBuilder: widget.calendarBuilders.headerTitleBuilder, + focusedMonth: value, + onLeftChevronTap: _onLeftChevronTap, + onRightChevronTap: _onRightChevronTap, + onHeaderTap: () => widget.onHeaderTapped?.call(value), + onHeaderLongPress: () => + widget.onHeaderLongPressed?.call(value), + headerStyle: widget.headerStyle, + availableCalendarFormats: widget.availableCalendarFormats, + calendarFormat: widget.calendarFormat, + locale: widget.locale, + onFormatButtonTap: (format) { + assert( + widget.onFormatChanged != null, + 'Using `FormatButton` without providing `onFormatChanged` will have no effect.', + ); + + widget.onFormatChanged?.call(format); + }, + ); + }, + ), + Flexible( + flex: widget.shouldFillViewport ? 1 : 0, + child: TableCalendarBase( + onCalendarCreated: (pageController) { + _pageController = pageController; + widget.onCalendarCreated?.call(pageController); + }, + focusedDay: _focusedDay.value, + calendarFormat: widget.calendarFormat, + availableGestures: widget.availableGestures, + firstDay: widget.firstDay, + lastDay: widget.lastDay, + startingDayOfWeek: widget.startingDayOfWeek, + dowDecoration: widget.daysOfWeekStyle.decoration, + rowDecoration: widget.calendarStyle.rowDecoration, + tableBorder: widget.calendarStyle.tableBorder, + tablePadding: widget.calendarStyle.tablePadding, + dowVisible: widget.daysOfWeekVisible, + dowHeight: widget.daysOfWeekHeight, + rowHeight: widget.rowHeight, + formatAnimationDuration: widget.formatAnimationDuration, + formatAnimationCurve: widget.formatAnimationCurve, + pageAnimationEnabled: widget.pageAnimationEnabled, + pageAnimationDuration: widget.pageAnimationDuration, + pageAnimationCurve: widget.pageAnimationCurve, + availableCalendarFormats: widget.availableCalendarFormats, + simpleSwipeConfig: widget.simpleSwipeConfig, + sixWeekMonthsEnforced: widget.sixWeekMonthsEnforced, + onVerticalSwipe: _swipeCalendarFormat, + onPageChanged: (focusedDay) { + _focusedDay.value = focusedDay; + widget.onPageChanged?.call(focusedDay); + }, + weekNumbersVisible: widget.weekNumbersVisible, + weekNumberBuilder: (BuildContext context, DateTime day) { + final weekNumber = _calculateWeekNumber(day); + final cell = widget.calendarBuilders.weekNumberBuilder + ?.call(context, weekNumber); + + return cell ?? + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Center( + child: Text( + weekNumber.toString(), + style: widget.calendarStyle.weekNumberTextStyle, + ), + ), + ); + }, + dowBuilder: (BuildContext context, DateTime day) { + Widget? dowCell = + widget.calendarBuilders.dowBuilder?.call(context, day); + + if (dowCell == null) { + final weekdayString = widget.daysOfWeekStyle.dowTextFormatter + ?.call(day, widget.locale) ?? + DateFormat.E(widget.locale).format(day); + + final isWeekend = + _isWeekend(day, weekendDays: widget.weekendDays); + + dowCell = Center( + child: ExcludeSemantics( + child: Text( + weekdayString, + style: isWeekend + ? widget.daysOfWeekStyle.weekendStyle + : widget.daysOfWeekStyle.weekdayStyle, + ), + ), + ); + } + + return dowCell; + }, + dayBuilder: (context, day, focusedMonth) { + return GestureDetector( + behavior: widget.dayHitTestBehavior, + onTap: () => _onDayTapped(day), + onLongPress: () => _onDayLongPressed(day), + child: _buildCell(day, focusedMonth), + ); + }, + ), + ), + ], + ); + } + + Widget _buildCell(DateTime day, DateTime focusedDay) { + final isOutside = day.month != focusedDay.month; + + if (isOutside && _shouldBlockOutsideDays) { + return Container(); + } + + return LayoutBuilder( + builder: (context, constraints) { + final shorterSide = constraints.maxHeight > constraints.maxWidth + ? constraints.maxWidth + : constraints.maxHeight; + + final children = []; + + final isWithinRange = widget.rangeStartDay != null && + widget.rangeEndDay != null && + _isWithinRange(day, widget.rangeStartDay!, widget.rangeEndDay!); + + final isRangeStart = isSameDay(day, widget.rangeStartDay); + final isRangeEnd = isSameDay(day, widget.rangeEndDay); + + Widget? rangeHighlight = widget.calendarBuilders.rangeHighlightBuilder + ?.call(context, day, isWithinRange); + + if (rangeHighlight == null) { + if (isWithinRange) { + rangeHighlight = Center( + child: Container( + margin: EdgeInsetsDirectional.only( + start: isRangeStart ? constraints.maxWidth * 0.5 : 0.0, + end: isRangeEnd ? constraints.maxWidth * 0.5 : 0.0, + ), + height: + (shorterSide - widget.calendarStyle.cellMargin.vertical) * + widget.calendarStyle.rangeHighlightScale, + color: widget.calendarStyle.rangeHighlightColor, + ), + ); + } + } + + if (rangeHighlight != null) { + children.add(rangeHighlight); + } + + final isToday = isSameDay(day, widget.currentDay); + final isDisabled = _isDayDisabled(day); + final isWeekend = _isWeekend(day, weekendDays: widget.weekendDays); + + final content = CellContent( + key: ValueKey('CellContent-${day.year}-${day.month}-${day.day}'), + day: day, + focusedDay: focusedDay, + calendarStyle: widget.calendarStyle, + calendarBuilders: widget.calendarBuilders, + isTodayHighlighted: widget.calendarStyle.isTodayHighlighted, + isToday: isToday, + isSelected: widget.selectedDayPredicate?.call(day) ?? false, + isRangeStart: isRangeStart, + isRangeEnd: isRangeEnd, + isWithinRange: isWithinRange, + isOutside: isOutside, + isDisabled: isDisabled, + isWeekend: isWeekend, + isHoliday: widget.holidayPredicate?.call(day) ?? false, + locale: widget.locale, + ); + + children.add(content); + + if (widget.loadEventsForDisabledDays || !isDisabled) { + final events = widget.eventLoader?.call(day) ?? []; + Widget? markerWidget = + widget.calendarBuilders.markerBuilder?.call(context, day, events); + + if (events.isNotEmpty && markerWidget == null) { + final center = constraints.maxHeight / 2; + + final markerSize = widget.calendarStyle.markerSize ?? + (shorterSide - widget.calendarStyle.cellMargin.vertical) * + widget.calendarStyle.markerSizeScale; + + final markerAutoAlignmentTop = center + + (shorterSide - widget.calendarStyle.cellMargin.vertical) / 2 - + (markerSize * widget.calendarStyle.markersAnchor); + + markerWidget = PositionedDirectional( + top: widget.calendarStyle.markersAutoAligned + ? markerAutoAlignmentTop + : widget.calendarStyle.markersOffset.top, + bottom: widget.calendarStyle.markersAutoAligned + ? null + : widget.calendarStyle.markersOffset.bottom, + start: widget.calendarStyle.markersAutoAligned + ? null + : widget.calendarStyle.markersOffset.start, + end: widget.calendarStyle.markersAutoAligned + ? null + : widget.calendarStyle.markersOffset.end, + child: Row( + mainAxisSize: MainAxisSize.min, + children: events + .take(widget.calendarStyle.markersMaxCount) + .map((event) => _buildSingleMarker(day, event, markerSize)) + .toList(), + ), + ); + } + + if (markerWidget != null) { + children.add(markerWidget); + } + } + + return Stack( + alignment: widget.calendarStyle.markersAlignment, + clipBehavior: widget.calendarStyle.canMarkersOverflow + ? Clip.none + : Clip.hardEdge, + children: children, + ); + }, + ); + } + + Widget _buildSingleMarker(DateTime day, T event, double markerSize) { + return widget.calendarBuilders.singleMarkerBuilder + ?.call(context, day, event) ?? + Container( + width: markerSize, + height: markerSize, + margin: widget.calendarStyle.markerMargin, + decoration: widget.calendarStyle.markerDecoration, + ); + } + + int _calculateWeekNumber(DateTime date) { + final middleDay = date.add(const Duration(days: 3)); + final dayOfYear = _dayOfYear(middleDay); + + return 1 + ((dayOfYear - 1) / 7).floor(); + } + + int _dayOfYear(DateTime date) { + return normalizeDate(date).difference(DateTime.utc(date.year)).inDays + 1; + } + + bool _isWithinRange(DateTime day, DateTime start, DateTime end) { + if (isSameDay(day, start) || isSameDay(day, end)) { + return true; + } + + if (day.isAfter(start) && day.isBefore(end)) { + return true; + } + + return false; + } + + bool _isDayDisabled(DateTime day) { + return day.isBefore(widget.firstDay) || + day.isAfter(widget.lastDay) || + !_isDayAvailable(day); + } + + bool _isDayAvailable(DateTime day) { + if (widget.enabledDayPredicate == null) { + return true; + } + + return widget.enabledDayPredicate!(day); + } + + DateTime _firstDayOfMonth(DateTime month) { + return DateTime.utc(month.year, month.month); + } + + DateTime _lastDayOfMonth(DateTime month) { + final date = month.month < 12 + ? DateTime.utc(month.year, month.month + 1) + : DateTime.utc(month.year + 1); + return date.subtract(const Duration(days: 1)); + } + + bool _isBeforeMonth(DateTime day, DateTime month) { + if (day.year == month.year) { + return day.month < month.month; + } else { + return day.isBefore(month); + } + } + + bool _isAfterMonth(DateTime day, DateTime month) { + if (day.year == month.year) { + return day.month > month.month; + } else { + return day.isAfter(month); + } + } + + bool _isWeekend( + DateTime day, { + List weekendDays = const [DateTime.saturday, DateTime.sunday], + }) { + return weekendDays.contains(day.weekday); + } +} diff --git a/lib/src/table_calendar_base.dart b/lib/src/table_calendar_base.dart new file mode 100644 index 00000000..a5727160 --- /dev/null +++ b/lib/src/table_calendar_base.dart @@ -0,0 +1,356 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/material.dart'; +import 'package:simple_gesture_detector/simple_gesture_detector.dart'; +import 'package:table_calendar/src/shared/utils.dart'; +import 'package:table_calendar/src/widgets/calendar_core.dart'; + +class TableCalendarBase extends StatefulWidget { + final DateTime firstDay; + final DateTime lastDay; + final DateTime focusedDay; + final CalendarFormat calendarFormat; + final DayBuilder? dowBuilder; + final DayBuilder? weekNumberBuilder; + final FocusedDayBuilder dayBuilder; + final double? dowHeight; + final double rowHeight; + final bool sixWeekMonthsEnforced; + final bool dowVisible; + final bool weekNumbersVisible; + final Decoration? dowDecoration; + final Decoration? rowDecoration; + final TableBorder? tableBorder; + final EdgeInsets? tablePadding; + final Duration formatAnimationDuration; + final Curve formatAnimationCurve; + final bool pageAnimationEnabled; + final Duration pageAnimationDuration; + final Curve pageAnimationCurve; + final StartingDayOfWeek startingDayOfWeek; + final AvailableGestures availableGestures; + final SimpleSwipeConfig simpleSwipeConfig; + final Map availableCalendarFormats; + final SwipeCallback? onVerticalSwipe; + final void Function(DateTime focusedDay)? onPageChanged; + final void Function(PageController pageController)? onCalendarCreated; + + TableCalendarBase({ + super.key, + required this.firstDay, + required this.lastDay, + required this.focusedDay, + this.calendarFormat = CalendarFormat.month, + this.dowBuilder, + required this.dayBuilder, + this.dowHeight, + required this.rowHeight, + this.sixWeekMonthsEnforced = false, + this.dowVisible = true, + this.weekNumberBuilder, + this.weekNumbersVisible = false, + this.dowDecoration, + this.rowDecoration, + this.tableBorder, + this.tablePadding, + this.formatAnimationDuration = const Duration(milliseconds: 200), + this.formatAnimationCurve = Curves.linear, + this.pageAnimationEnabled = true, + this.pageAnimationDuration = const Duration(milliseconds: 300), + this.pageAnimationCurve = Curves.easeOut, + this.startingDayOfWeek = StartingDayOfWeek.sunday, + this.availableGestures = AvailableGestures.all, + this.simpleSwipeConfig = const SimpleSwipeConfig( + verticalThreshold: 25.0, + swipeDetectionBehavior: SwipeDetectionBehavior.continuousDistinct, + ), + this.availableCalendarFormats = const { + CalendarFormat.month: 'Month', + CalendarFormat.twoWeeks: '2 weeks', + CalendarFormat.week: 'Week', + }, + this.onVerticalSwipe, + this.onPageChanged, + this.onCalendarCreated, + }) : assert(!dowVisible || (dowHeight != null && dowBuilder != null)), + assert(isSameDay(focusedDay, firstDay) || focusedDay.isAfter(firstDay)), + assert(isSameDay(focusedDay, lastDay) || focusedDay.isBefore(lastDay)); + + @override + State createState() => _TableCalendarBaseState(); +} + +class _TableCalendarBaseState extends State { + late final ValueNotifier _pageHeight; + late final PageController _pageController; + late DateTime _focusedDay; + late int _previousIndex; + late bool _pageCallbackDisabled; + + @override + void initState() { + super.initState(); + _focusedDay = widget.focusedDay; + + final rowCount = _getRowCount(widget.calendarFormat, _focusedDay); + _pageHeight = ValueNotifier(_getPageHeight(rowCount)); + + final initialPage = _calculateFocusedPage( + widget.calendarFormat, + widget.firstDay, + _focusedDay, + ); + + _pageController = PageController(initialPage: initialPage); + widget.onCalendarCreated?.call(_pageController); + + _previousIndex = initialPage; + _pageCallbackDisabled = false; + } + + @override + void didUpdateWidget(TableCalendarBase oldWidget) { + super.didUpdateWidget(oldWidget); + + if (_focusedDay != widget.focusedDay || + widget.calendarFormat != oldWidget.calendarFormat || + widget.startingDayOfWeek != oldWidget.startingDayOfWeek) { + final shouldAnimate = _focusedDay != widget.focusedDay; + + _focusedDay = widget.focusedDay; + _updatePage(shouldAnimate: shouldAnimate); + } + + if (widget.rowHeight != oldWidget.rowHeight || + widget.dowHeight != oldWidget.dowHeight || + widget.dowVisible != oldWidget.dowVisible || + widget.sixWeekMonthsEnforced != oldWidget.sixWeekMonthsEnforced) { + final rowCount = _getRowCount(widget.calendarFormat, _focusedDay); + _pageHeight.value = _getPageHeight(rowCount); + } + } + + @override + void dispose() { + _pageController.dispose(); + _pageHeight.dispose(); + super.dispose(); + } + + bool get _canScrollHorizontally => + widget.availableGestures == AvailableGestures.all || + widget.availableGestures == AvailableGestures.horizontalSwipe; + + bool get _canScrollVertically => + widget.availableGestures == AvailableGestures.all || + widget.availableGestures == AvailableGestures.verticalSwipe; + + void _updatePage({bool shouldAnimate = false}) { + final currentIndex = _calculateFocusedPage( + widget.calendarFormat, + widget.firstDay, + _focusedDay, + ); + + final endIndex = _calculateFocusedPage( + widget.calendarFormat, + widget.firstDay, + widget.lastDay, + ); + + if (currentIndex != _previousIndex || + currentIndex == 0 || + currentIndex == endIndex) { + _pageCallbackDisabled = true; + } + + if (shouldAnimate && widget.pageAnimationEnabled) { + if ((currentIndex - _previousIndex).abs() > 1) { + final jumpIndex = + currentIndex > _previousIndex ? currentIndex - 1 : currentIndex + 1; + + _pageController.jumpToPage(jumpIndex); + } + + _pageController.animateToPage( + currentIndex, + duration: widget.pageAnimationDuration, + curve: widget.pageAnimationCurve, + ); + } else { + _pageController.jumpToPage(currentIndex); + } + + _previousIndex = currentIndex; + final rowCount = _getRowCount(widget.calendarFormat, _focusedDay); + _pageHeight.value = _getPageHeight(rowCount); + + _pageCallbackDisabled = false; + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return SimpleGestureDetector( + onVerticalSwipe: _canScrollVertically ? widget.onVerticalSwipe : null, + swipeConfig: widget.simpleSwipeConfig, + child: ValueListenableBuilder( + valueListenable: _pageHeight, + builder: (context, value, child) { + final height = + constraints.hasBoundedHeight ? constraints.maxHeight : value; + + return AnimatedSize( + duration: widget.formatAnimationDuration, + curve: widget.formatAnimationCurve, + alignment: Alignment.topCenter, + child: SizedBox( + height: height, + child: child, + ), + ); + }, + child: CalendarCore( + constraints: constraints, + pageController: _pageController, + scrollPhysics: _canScrollHorizontally + ? const PageScrollPhysics() + : const NeverScrollableScrollPhysics(), + firstDay: widget.firstDay, + lastDay: widget.lastDay, + startingDayOfWeek: widget.startingDayOfWeek, + calendarFormat: widget.calendarFormat, + previousIndex: _previousIndex, + focusedDay: _focusedDay, + sixWeekMonthsEnforced: widget.sixWeekMonthsEnforced, + dowVisible: widget.dowVisible, + dowHeight: widget.dowHeight, + rowHeight: widget.rowHeight, + weekNumbersVisible: widget.weekNumbersVisible, + weekNumberBuilder: widget.weekNumberBuilder, + dowDecoration: widget.dowDecoration, + rowDecoration: widget.rowDecoration, + tableBorder: widget.tableBorder, + tablePadding: widget.tablePadding, + onPageChanged: (index, focusedMonth) { + if (!_pageCallbackDisabled) { + if (!isSameDay(_focusedDay, focusedMonth)) { + _focusedDay = focusedMonth; + } + + if (widget.calendarFormat == CalendarFormat.month && + !widget.sixWeekMonthsEnforced && + !constraints.hasBoundedHeight) { + final rowCount = _getRowCount( + widget.calendarFormat, + focusedMonth, + ); + _pageHeight.value = _getPageHeight(rowCount); + } + + _previousIndex = index; + widget.onPageChanged?.call(focusedMonth); + } + + _pageCallbackDisabled = false; + }, + dowBuilder: widget.dowBuilder, + dayBuilder: widget.dayBuilder, + ), + ), + ); + }, + ); + } + + double _getPageHeight(int rowCount) { + final tablePaddingHeight = widget.tablePadding?.vertical ?? 0.0; + final dowHeight = widget.dowVisible ? widget.dowHeight! : 0.0; + return dowHeight + rowCount * widget.rowHeight + tablePaddingHeight; + } + + int _calculateFocusedPage( + CalendarFormat format, + DateTime startDay, + DateTime focusedDay, + ) { + switch (format) { + case CalendarFormat.month: + return _getMonthCount(startDay, focusedDay); + case CalendarFormat.twoWeeks: + return _getTwoWeekCount(startDay, focusedDay); + case CalendarFormat.week: + return _getWeekCount(startDay, focusedDay); + } + } + + int _getMonthCount(DateTime first, DateTime last) { + final yearDif = last.year - first.year; + final monthDif = last.month - first.month; + + return yearDif * 12 + monthDif; + } + + int _getWeekCount(DateTime first, DateTime last) { + return last.difference(_firstDayOfWeek(first)).inDays ~/ 7; + } + + int _getTwoWeekCount(DateTime first, DateTime last) { + return last.difference(_firstDayOfWeek(first)).inDays ~/ 14; + } + + int _getRowCount(CalendarFormat format, DateTime focusedDay) { + if (format == CalendarFormat.twoWeeks) { + return 2; + } else if (format == CalendarFormat.week) { + return 1; + } else if (widget.sixWeekMonthsEnforced) { + return 6; + } + + final first = _firstDayOfMonth(focusedDay); + final daysBefore = _getDaysBefore(first); + final firstToDisplay = first.subtract(Duration(days: daysBefore)); + + final last = _lastDayOfMonth(focusedDay); + final daysAfter = _getDaysAfter(last); + final lastToDisplay = last.add(Duration(days: daysAfter)); + + return (lastToDisplay.difference(firstToDisplay).inDays + 1) ~/ 7; + } + + int _getDaysBefore(DateTime firstDay) { + return (firstDay.weekday + 7 - getWeekdayNumber(widget.startingDayOfWeek)) % + 7; + } + + int _getDaysAfter(DateTime lastDay) { + final invertedStartingWeekday = + 8 - getWeekdayNumber(widget.startingDayOfWeek); + + final daysAfter = 7 - ((lastDay.weekday + invertedStartingWeekday) % 7); + if (daysAfter == 7) { + return 0; + } + + return daysAfter; + } + + DateTime _firstDayOfWeek(DateTime week) { + final daysBefore = _getDaysBefore(week); + return week.subtract(Duration(days: daysBefore)); + } + + DateTime _firstDayOfMonth(DateTime month) { + return DateTime.utc(month.year, month.month); + } + + DateTime _lastDayOfMonth(DateTime month) { + final date = month.month < 12 + ? DateTime.utc(month.year, month.month + 1) + : DateTime.utc(month.year + 1); + return date.subtract(const Duration(days: 1)); + } +} diff --git a/lib/src/widgets/calendar_core.dart b/lib/src/widgets/calendar_core.dart new file mode 100644 index 00000000..573ef532 --- /dev/null +++ b/lib/src/widgets/calendar_core.dart @@ -0,0 +1,320 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/material.dart'; +import 'package:table_calendar/src/shared/utils.dart'; +import 'package:table_calendar/src/widgets/calendar_page.dart'; + +class CalendarCore extends StatelessWidget { + final DateTime? focusedDay; + final DateTime firstDay; + final DateTime lastDay; + final CalendarFormat calendarFormat; + final DayBuilder? dowBuilder; + final DayBuilder? weekNumberBuilder; + final FocusedDayBuilder dayBuilder; + final bool sixWeekMonthsEnforced; + final bool dowVisible; + final bool weekNumbersVisible; + final Decoration? dowDecoration; + final Decoration? rowDecoration; + final TableBorder? tableBorder; + final EdgeInsets? tablePadding; + final double? dowHeight; + final double? rowHeight; + final BoxConstraints constraints; + final int? previousIndex; + final StartingDayOfWeek startingDayOfWeek; + final PageController? pageController; + final ScrollPhysics? scrollPhysics; + final void Function(int, DateTime) onPageChanged; + + const CalendarCore({ + super.key, + this.dowBuilder, + required this.dayBuilder, + required this.onPageChanged, + required this.firstDay, + required this.lastDay, + required this.constraints, + this.dowHeight, + this.rowHeight, + this.startingDayOfWeek = StartingDayOfWeek.sunday, + this.calendarFormat = CalendarFormat.month, + this.pageController, + this.focusedDay, + this.previousIndex, + this.sixWeekMonthsEnforced = false, + this.dowVisible = true, + this.weekNumberBuilder, + required this.weekNumbersVisible, + this.dowDecoration, + this.rowDecoration, + this.tableBorder, + this.tablePadding, + this.scrollPhysics, + }) : assert(!dowVisible || (dowHeight != null && dowBuilder != null)); + + @override + Widget build(BuildContext context) { + return PageView.builder( + controller: pageController, + physics: scrollPhysics, + itemCount: _getPageCount(calendarFormat, firstDay, lastDay), + itemBuilder: (context, index) { + final baseDay = _getBaseDay(calendarFormat, index); + final visibleRange = _getVisibleRange(calendarFormat, baseDay); + final visibleDays = _daysInRange(visibleRange.start, visibleRange.end); + + final actualDowHeight = dowVisible ? dowHeight! : 0.0; + final constrainedRowHeight = constraints.hasBoundedHeight + ? (constraints.maxHeight - actualDowHeight) / + _getRowCount(calendarFormat, baseDay) + : null; + + return CalendarPage( + visibleDays: visibleDays, + dowVisible: dowVisible, + dowDecoration: dowDecoration, + rowDecoration: rowDecoration, + tableBorder: tableBorder, + tablePadding: tablePadding, + dowBuilder: (context, day) { + return SizedBox( + height: dowHeight, + child: dowBuilder?.call(context, day), + ); + }, + dayBuilder: (context, day) { + DateTime baseDay; + final previousFocusedDay = focusedDay; + if (previousFocusedDay == null || previousIndex == null) { + baseDay = _getBaseDay(calendarFormat, index); + } else { + baseDay = + _getFocusedDay(calendarFormat, previousFocusedDay, index); + } + + return SizedBox( + height: constrainedRowHeight ?? rowHeight, + child: dayBuilder(context, day, baseDay), + ); + }, + dowHeight: dowHeight, + weekNumberVisible: weekNumbersVisible, + weekNumberBuilder: (context, day) { + return SizedBox( + height: constrainedRowHeight ?? rowHeight, + child: weekNumberBuilder?.call(context, day), + ); + }, + ); + }, + onPageChanged: (index) { + DateTime baseDay; + final previousFocusedDay = focusedDay; + if (previousFocusedDay == null || previousIndex == null) { + baseDay = _getBaseDay(calendarFormat, index); + } else { + baseDay = _getFocusedDay(calendarFormat, previousFocusedDay, index); + } + + return onPageChanged(index, baseDay); + }, + ); + } + + int _getPageCount(CalendarFormat format, DateTime first, DateTime last) { + switch (format) { + case CalendarFormat.month: + return _getMonthCount(first, last) + 1; + case CalendarFormat.twoWeeks: + return _getTwoWeekCount(first, last) + 1; + case CalendarFormat.week: + return _getWeekCount(first, last) + 1; + } + } + + int _getMonthCount(DateTime first, DateTime last) { + final yearDif = last.year - first.year; + final monthDif = last.month - first.month; + + return yearDif * 12 + monthDif; + } + + int _getWeekCount(DateTime first, DateTime last) { + return last.difference(_firstDayOfWeek(first)).inDays ~/ 7; + } + + int _getTwoWeekCount(DateTime first, DateTime last) { + return last.difference(_firstDayOfWeek(first)).inDays ~/ 14; + } + + DateTime _getFocusedDay( + CalendarFormat format, + DateTime prevFocusedDay, + int pageIndex, + ) { + if (pageIndex == previousIndex) { + return prevFocusedDay; + } + + final pageDif = pageIndex - previousIndex!; + DateTime day; + + switch (format) { + case CalendarFormat.month: + day = DateTime.utc(prevFocusedDay.year, prevFocusedDay.month + pageDif); + case CalendarFormat.twoWeeks: + day = DateTime.utc( + prevFocusedDay.year, + prevFocusedDay.month, + prevFocusedDay.day + pageDif * 14, + ); + case CalendarFormat.week: + day = DateTime.utc( + prevFocusedDay.year, + prevFocusedDay.month, + prevFocusedDay.day + pageDif * 7, + ); + } + + if (day.isBefore(firstDay)) { + day = firstDay; + } else if (day.isAfter(lastDay)) { + day = lastDay; + } + + return day; + } + + DateTime _getBaseDay(CalendarFormat format, int pageIndex) { + DateTime day; + + switch (format) { + case CalendarFormat.month: + day = DateTime.utc(firstDay.year, firstDay.month + pageIndex); + case CalendarFormat.twoWeeks: + day = DateTime.utc( + firstDay.year, + firstDay.month, + firstDay.day + pageIndex * 14, + ); + case CalendarFormat.week: + day = DateTime.utc( + firstDay.year, + firstDay.month, + firstDay.day + pageIndex * 7, + ); + } + + if (day.isBefore(firstDay)) { + day = firstDay; + } else if (day.isAfter(lastDay)) { + day = lastDay; + } + + return day; + } + + DateTimeRange _getVisibleRange(CalendarFormat format, DateTime focusedDay) { + switch (format) { + case CalendarFormat.month: + return _daysInMonth(focusedDay); + case CalendarFormat.twoWeeks: + return _daysInTwoWeeks(focusedDay); + case CalendarFormat.week: + return _daysInWeek(focusedDay); + } + } + + DateTimeRange _daysInWeek(DateTime focusedDay) { + final daysBefore = _getDaysBefore(focusedDay); + final firstToDisplay = focusedDay.subtract(Duration(days: daysBefore)); + final lastToDisplay = firstToDisplay.add(const Duration(days: 7)); + return DateTimeRange(start: firstToDisplay, end: lastToDisplay); + } + + DateTimeRange _daysInTwoWeeks(DateTime focusedDay) { + final daysBefore = _getDaysBefore(focusedDay); + final firstToDisplay = focusedDay.subtract(Duration(days: daysBefore)); + final lastToDisplay = firstToDisplay.add(const Duration(days: 14)); + return DateTimeRange(start: firstToDisplay, end: lastToDisplay); + } + + DateTimeRange _daysInMonth(DateTime focusedDay) { + final first = _firstDayOfMonth(focusedDay); + final daysBefore = _getDaysBefore(first); + final firstToDisplay = first.subtract(Duration(days: daysBefore)); + + if (sixWeekMonthsEnforced) { + final end = firstToDisplay.add(const Duration(days: 42)); + return DateTimeRange(start: firstToDisplay, end: end); + } + + final last = _lastDayOfMonth(focusedDay); + final daysAfter = _getDaysAfter(last); + final lastToDisplay = last.add(Duration(days: daysAfter)); + + return DateTimeRange(start: firstToDisplay, end: lastToDisplay); + } + + List _daysInRange(DateTime first, DateTime last) { + final dayCount = last.difference(first).inDays + 1; + return List.generate( + dayCount, + (index) => DateTime.utc(first.year, first.month, first.day + index), + ); + } + + DateTime _firstDayOfWeek(DateTime week) { + final daysBefore = _getDaysBefore(week); + return week.subtract(Duration(days: daysBefore)); + } + + DateTime _firstDayOfMonth(DateTime month) { + return DateTime.utc(month.year, month.month); + } + + DateTime _lastDayOfMonth(DateTime month) { + final date = month.month < 12 + ? DateTime.utc(month.year, month.month + 1) + : DateTime.utc(month.year + 1); + return date.subtract(const Duration(days: 1)); + } + + int _getRowCount(CalendarFormat format, DateTime focusedDay) { + if (format == CalendarFormat.twoWeeks) { + return 2; + } else if (format == CalendarFormat.week) { + return 1; + } else if (sixWeekMonthsEnforced) { + return 6; + } + + final first = _firstDayOfMonth(focusedDay); + final daysBefore = _getDaysBefore(first); + final firstToDisplay = first.subtract(Duration(days: daysBefore)); + + final last = _lastDayOfMonth(focusedDay); + final daysAfter = _getDaysAfter(last); + final lastToDisplay = last.add(Duration(days: daysAfter)); + + return (lastToDisplay.difference(firstToDisplay).inDays + 1) ~/ 7; + } + + int _getDaysBefore(DateTime firstDay) { + return (firstDay.weekday + 7 - getWeekdayNumber(startingDayOfWeek)) % 7; + } + + int _getDaysAfter(DateTime lastDay) { + final invertedStartingWeekday = 8 - getWeekdayNumber(startingDayOfWeek); + + final daysAfter = 7 - ((lastDay.weekday + invertedStartingWeekday) % 7); + if (daysAfter == 7) { + return 0; + } + + return daysAfter; + } +} diff --git a/lib/src/widgets/calendar_header.dart b/lib/src/widgets/calendar_header.dart new file mode 100644 index 00000000..6a54336e --- /dev/null +++ b/lib/src/widgets/calendar_header.dart @@ -0,0 +1,97 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:table_calendar/src/customization/header_style.dart'; +import 'package:table_calendar/src/shared/utils.dart' + show CalendarFormat, DayBuilder; +import 'package:table_calendar/src/widgets/custom_icon_button.dart'; +import 'package:table_calendar/src/widgets/format_button.dart'; + +class CalendarHeader extends StatelessWidget { + final dynamic locale; + final DateTime focusedMonth; + final CalendarFormat calendarFormat; + final HeaderStyle headerStyle; + final VoidCallback onLeftChevronTap; + final VoidCallback onRightChevronTap; + final VoidCallback onHeaderTap; + final VoidCallback onHeaderLongPress; + final ValueChanged onFormatButtonTap; + final Map availableCalendarFormats; + final DayBuilder? headerTitleBuilder; + + const CalendarHeader({ + super.key, + this.locale, + required this.focusedMonth, + required this.calendarFormat, + required this.headerStyle, + required this.onLeftChevronTap, + required this.onRightChevronTap, + required this.onHeaderTap, + required this.onHeaderLongPress, + required this.onFormatButtonTap, + required this.availableCalendarFormats, + this.headerTitleBuilder, + }); + + @override + Widget build(BuildContext context) { + final text = headerStyle.titleTextFormatter?.call(focusedMonth, locale) ?? + DateFormat.yMMMM(locale).format(focusedMonth); + + return Container( + decoration: headerStyle.decoration, + margin: headerStyle.headerMargin, + padding: headerStyle.headerPadding, + child: Row( + children: [ + if (headerStyle.leftChevronVisible) + CustomIconButton( + icon: headerStyle.leftChevronIcon, + onTap: onLeftChevronTap, + margin: headerStyle.leftChevronMargin, + padding: headerStyle.leftChevronPadding, + ), + Expanded( + child: headerTitleBuilder?.call(context, focusedMonth) ?? + GestureDetector( + onTap: onHeaderTap, + onLongPress: onHeaderLongPress, + child: Text( + text, + style: headerStyle.titleTextStyle, + textAlign: headerStyle.titleCentered + ? TextAlign.center + : TextAlign.start, + ), + ), + ), + if (headerStyle.formatButtonVisible && + availableCalendarFormats.length > 1) + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: FormatButton( + onTap: onFormatButtonTap, + availableCalendarFormats: availableCalendarFormats, + calendarFormat: calendarFormat, + decoration: headerStyle.formatButtonDecoration, + padding: headerStyle.formatButtonPadding, + textStyle: headerStyle.formatButtonTextStyle, + showsNextFormat: headerStyle.formatButtonShowsNext, + ), + ), + if (headerStyle.rightChevronVisible) + CustomIconButton( + icon: headerStyle.rightChevronIcon, + onTap: onRightChevronTap, + margin: headerStyle.rightChevronMargin, + padding: headerStyle.rightChevronPadding, + ), + ], + ), + ); + } +} diff --git a/lib/src/widgets/calendar_page.dart b/lib/src/widgets/calendar_page.dart new file mode 100644 index 00000000..0b3cb08d --- /dev/null +++ b/lib/src/widgets/calendar_page.dart @@ -0,0 +1,97 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/widgets.dart'; + +class CalendarPage extends StatelessWidget { + final Widget Function(BuildContext context, DateTime day)? dowBuilder; + final Widget Function(BuildContext context, DateTime day) dayBuilder; + final Widget Function(BuildContext context, DateTime day)? weekNumberBuilder; + final List visibleDays; + final Decoration? dowDecoration; + final Decoration? rowDecoration; + final TableBorder? tableBorder; + final EdgeInsets? tablePadding; + final bool dowVisible; + final bool weekNumberVisible; + final double? dowHeight; + + const CalendarPage({ + super.key, + required this.visibleDays, + this.dowBuilder, + required this.dayBuilder, + this.weekNumberBuilder, + this.dowDecoration, + this.rowDecoration, + this.tableBorder, + this.tablePadding, + this.dowVisible = true, + this.weekNumberVisible = false, + this.dowHeight, + }) : assert(!dowVisible || (dowHeight != null && dowBuilder != null)), + assert(!weekNumberVisible || weekNumberBuilder != null); + + @override + Widget build(BuildContext context) { + return Padding( + padding: tablePadding ?? EdgeInsets.zero, + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (weekNumberVisible) _buildWeekNumbers(context), + Expanded( + child: Table( + border: tableBorder, + children: [ + if (dowVisible) _buildDaysOfWeek(context), + ..._buildCalendarDays(context), + ], + ), + ), + ], + ), + ); + } + + Widget _buildWeekNumbers(BuildContext context) { + final rowAmount = visibleDays.length ~/ 7; + + return Column( + children: [ + if (dowVisible) SizedBox(height: dowHeight ?? 0), + ...List.generate( + rowAmount, + (index) => Expanded( + child: weekNumberBuilder!(context, visibleDays[index * 7]), + ), + ), + ], + ); + } + + TableRow _buildDaysOfWeek(BuildContext context) { + return TableRow( + decoration: dowDecoration, + children: List.generate( + 7, + (index) => dowBuilder!(context, visibleDays[index]), + ), + ); + } + + List _buildCalendarDays(BuildContext context) { + final rowAmount = visibleDays.length ~/ 7; + + return List.generate( + rowAmount, + (index) => TableRow( + decoration: rowDecoration, + children: List.generate( + 7, + (id) => dayBuilder(context, visibleDays[index * 7 + id]), + ), + ), + ); + } +} diff --git a/lib/src/widgets/cell_content.dart b/lib/src/widgets/cell_content.dart new file mode 100644 index 00000000..c2b52b4a --- /dev/null +++ b/lib/src/widgets/cell_content.dart @@ -0,0 +1,176 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:table_calendar/src/customization/calendar_builders.dart'; +import 'package:table_calendar/src/customization/calendar_style.dart'; + +class CellContent extends StatelessWidget { + final DateTime day; + final DateTime focusedDay; + final dynamic locale; + final bool isTodayHighlighted; + final bool isToday; + final bool isSelected; + final bool isRangeStart; + final bool isRangeEnd; + final bool isWithinRange; + final bool isOutside; + final bool isDisabled; + final bool isHoliday; + final bool isWeekend; + final CalendarStyle calendarStyle; + final CalendarBuilders calendarBuilders; + + const CellContent({ + super.key, + required this.day, + required this.focusedDay, + required this.calendarStyle, + required this.calendarBuilders, + required this.isTodayHighlighted, + required this.isToday, + required this.isSelected, + required this.isRangeStart, + required this.isRangeEnd, + required this.isWithinRange, + required this.isOutside, + required this.isDisabled, + required this.isHoliday, + required this.isWeekend, + this.locale, + }); + + @override + Widget build(BuildContext context) { + final dowLabel = DateFormat.EEEE(locale).format(day); + final dayLabel = DateFormat.yMMMMd(locale).format(day); + final semanticsLabel = '$dowLabel, $dayLabel'; + + Widget? cell = + calendarBuilders.prioritizedBuilder?.call(context, day, focusedDay); + + if (cell != null) { + return Semantics( + label: semanticsLabel, + excludeSemantics: true, + child: cell, + ); + } + + final text = + calendarStyle.dayTextFormatter?.call(day, locale) ?? '${day.day}'; + final margin = calendarStyle.cellMargin; + final padding = calendarStyle.cellPadding; + final alignment = calendarStyle.cellAlignment; + const duration = Duration(milliseconds: 250); + + if (isDisabled) { + cell = calendarBuilders.disabledBuilder?.call(context, day, focusedDay) ?? + AnimatedContainer( + duration: duration, + margin: margin, + padding: padding, + decoration: calendarStyle.disabledDecoration, + alignment: alignment, + child: Text(text, style: calendarStyle.disabledTextStyle), + ); + } else if (isSelected) { + cell = calendarBuilders.selectedBuilder?.call(context, day, focusedDay) ?? + AnimatedContainer( + duration: duration, + margin: margin, + padding: padding, + decoration: calendarStyle.selectedDecoration, + alignment: alignment, + child: Text(text, style: calendarStyle.selectedTextStyle), + ); + } else if (isRangeStart) { + cell = + calendarBuilders.rangeStartBuilder?.call(context, day, focusedDay) ?? + AnimatedContainer( + duration: duration, + margin: margin, + padding: padding, + decoration: calendarStyle.rangeStartDecoration, + alignment: alignment, + child: Text(text, style: calendarStyle.rangeStartTextStyle), + ); + } else if (isRangeEnd) { + cell = calendarBuilders.rangeEndBuilder?.call(context, day, focusedDay) ?? + AnimatedContainer( + duration: duration, + margin: margin, + padding: padding, + decoration: calendarStyle.rangeEndDecoration, + alignment: alignment, + child: Text(text, style: calendarStyle.rangeEndTextStyle), + ); + } else if (isToday && isTodayHighlighted) { + cell = calendarBuilders.todayBuilder?.call(context, day, focusedDay) ?? + AnimatedContainer( + duration: duration, + margin: margin, + padding: padding, + decoration: calendarStyle.todayDecoration, + alignment: alignment, + child: Text(text, style: calendarStyle.todayTextStyle), + ); + } else if (isHoliday) { + cell = calendarBuilders.holidayBuilder?.call(context, day, focusedDay) ?? + AnimatedContainer( + duration: duration, + margin: margin, + padding: padding, + decoration: calendarStyle.holidayDecoration, + alignment: alignment, + child: Text(text, style: calendarStyle.holidayTextStyle), + ); + } else if (isWithinRange) { + cell = + calendarBuilders.withinRangeBuilder?.call(context, day, focusedDay) ?? + AnimatedContainer( + duration: duration, + margin: margin, + padding: padding, + decoration: calendarStyle.withinRangeDecoration, + alignment: alignment, + child: Text(text, style: calendarStyle.withinRangeTextStyle), + ); + } else if (isOutside) { + cell = calendarBuilders.outsideBuilder?.call(context, day, focusedDay) ?? + AnimatedContainer( + duration: duration, + margin: margin, + padding: padding, + decoration: calendarStyle.outsideDecoration, + alignment: alignment, + child: Text(text, style: calendarStyle.outsideTextStyle), + ); + } else { + cell = calendarBuilders.defaultBuilder?.call(context, day, focusedDay) ?? + AnimatedContainer( + duration: duration, + margin: margin, + padding: padding, + decoration: isWeekend + ? calendarStyle.weekendDecoration + : calendarStyle.defaultDecoration, + alignment: alignment, + child: Text( + text, + style: isWeekend + ? calendarStyle.weekendTextStyle + : calendarStyle.defaultTextStyle, + ), + ); + } + + return Semantics( + label: semanticsLabel, + excludeSemantics: true, + child: cell, + ); + } +} diff --git a/lib/src/widgets/cell_widget.dart b/lib/src/widgets/cell_widget.dart deleted file mode 100644 index eb3d2a40..00000000 --- a/lib/src/widgets/cell_widget.dart +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2019 Aleksander Woźniak -// Licensed under Apache License v2.0 - -part of table_calendar; - -class _CellWidget extends StatelessWidget { - final String text; - final bool isUnavailable; - final bool isSelected; - final bool isToday; - final bool isWeekend; - final bool isOutsideMonth; - final bool isHoliday; - final bool isEventDay; - final CalendarStyle calendarStyle; - - const _CellWidget({ - Key key, - @required this.text, - this.isUnavailable = false, - this.isSelected = false, - this.isToday = false, - this.isWeekend = false, - this.isOutsideMonth = false, - this.isHoliday = false, - this.isEventDay = false, - @required this.calendarStyle, - }) : assert(text != null), - assert(calendarStyle != null), - super(key: key); - - @override - Widget build(BuildContext context) { - return AnimatedContainer( - duration: const Duration(milliseconds: 250), - decoration: _buildCellDecoration(), - margin: calendarStyle.cellMargin, - alignment: Alignment.center, - child: Text( - text, - style: _buildCellTextStyle(), - ), - ); - } - - Decoration _buildCellDecoration() { - if (isSelected && calendarStyle.renderSelectedFirst && calendarStyle.highlightSelected) { - return BoxDecoration(shape: BoxShape.circle, color: calendarStyle.selectedColor); - } else if (isToday && calendarStyle.highlightToday) { - return BoxDecoration(shape: BoxShape.circle, color: calendarStyle.todayColor); - } else if (isSelected && calendarStyle.highlightSelected) { - return BoxDecoration(shape: BoxShape.circle, color: calendarStyle.selectedColor); - } else { - return BoxDecoration(shape: BoxShape.circle); - } - } - - TextStyle _buildCellTextStyle() { - if (isUnavailable) { - return calendarStyle.unavailableStyle; - } else if (isSelected && calendarStyle.renderSelectedFirst && calendarStyle.highlightSelected) { - return calendarStyle.selectedStyle; - } else if (isToday && calendarStyle.highlightToday) { - return calendarStyle.todayStyle; - } else if (isSelected && calendarStyle.highlightSelected) { - return calendarStyle.selectedStyle; - } else if (isOutsideMonth && isHoliday) { - return calendarStyle.outsideHolidayStyle; - } else if (isHoliday) { - return calendarStyle.holidayStyle; - } else if (isOutsideMonth && isWeekend) { - return calendarStyle.outsideWeekendStyle; - } else if (isOutsideMonth) { - return calendarStyle.outsideStyle; - } else if (isWeekend) { - return calendarStyle.weekendStyle; - } else if (isEventDay) { - return calendarStyle.eventDayStyle; - } else { - return calendarStyle.weekdayStyle; - } - } -} diff --git a/lib/src/widgets/custom_icon_button.dart b/lib/src/widgets/custom_icon_button.dart index 06be1983..0b31b98a 100644 --- a/lib/src/widgets/custom_icon_button.dart +++ b/lib/src/widgets/custom_icon_button.dart @@ -1,36 +1,46 @@ -// Copyright (c) 2019 Aleksander Woźniak -// Licensed under Apache License v2.0 +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 -part of table_calendar; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; -class _CustomIconButton extends StatelessWidget { - final Icon icon; +class CustomIconButton extends StatelessWidget { + final Widget icon; final VoidCallback onTap; final EdgeInsets margin; final EdgeInsets padding; - const _CustomIconButton({ - Key key, - @required this.icon, - @required this.onTap, - this.margin, - this.padding, - }) : assert(icon != null), - assert(onTap != null), - super(key: key); + const CustomIconButton({ + super.key, + required this.icon, + required this.onTap, + this.margin = EdgeInsets.zero, + this.padding = const EdgeInsets.all(8.0), + }); @override Widget build(BuildContext context) { + final platform = Theme.of(context).platform; + return Padding( padding: margin, - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(100.0), - child: Padding( - padding: padding, - child: icon, - ), - ), + child: !kIsWeb && + (platform == TargetPlatform.iOS || + platform == TargetPlatform.macOS) + ? CupertinoButton( + onPressed: onTap, + padding: padding, + child: icon, + ) + : InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(100.0), + child: Padding( + padding: padding, + child: icon, + ), + ), ); } } diff --git a/lib/src/widgets/format_button.dart b/lib/src/widgets/format_button.dart new file mode 100644 index 00000000..76bbc523 --- /dev/null +++ b/lib/src/widgets/format_button.dart @@ -0,0 +1,68 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:table_calendar/src/shared/utils.dart' show CalendarFormat; + +class FormatButton extends StatelessWidget { + final CalendarFormat calendarFormat; + final ValueChanged onTap; + final TextStyle textStyle; + final BoxDecoration decoration; + final EdgeInsets padding; + final bool showsNextFormat; + final Map availableCalendarFormats; + + const FormatButton({ + super.key, + required this.calendarFormat, + required this.onTap, + required this.textStyle, + required this.decoration, + required this.padding, + required this.showsNextFormat, + required this.availableCalendarFormats, + }); + + @override + Widget build(BuildContext context) { + final child = Container( + decoration: decoration, + padding: padding, + child: Text( + _formatButtonText, + style: textStyle, + ), + ); + + final platform = Theme.of(context).platform; + + return !kIsWeb && + (platform == TargetPlatform.iOS || platform == TargetPlatform.macOS) + ? CupertinoButton( + onPressed: () => onTap(_nextFormat()), + padding: EdgeInsets.zero, + child: child, + ) + : InkWell( + borderRadius: + decoration.borderRadius?.resolve(Directionality.of(context)), + onTap: () => onTap(_nextFormat()), + child: child, + ); + } + + String get _formatButtonText => showsNextFormat + ? availableCalendarFormats[_nextFormat()]! + : availableCalendarFormats[calendarFormat]!; + + CalendarFormat _nextFormat() { + final formats = availableCalendarFormats.keys.toList(); + int id = formats.indexOf(calendarFormat); + id = (id + 1) % formats.length; + + return formats[id]; + } +} diff --git a/lib/table_calendar.dart b/lib/table_calendar.dart index f0649629..4115dc30 100644 --- a/lib/table_calendar.dart +++ b/lib/table_calendar.dart @@ -1,17 +1,10 @@ -// Copyright (c) 2019 Aleksander Woźniak -// Licensed under Apache License v2.0 +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 -library table_calendar; - -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:simple_gesture_detector/simple_gesture_detector.dart'; - -part 'src/calendar.dart'; -part 'src/calendar_controller.dart'; -part 'src/customization/calendar_builders.dart'; -part 'src/customization/calendar_style.dart'; -part 'src/customization/days_of_week_style.dart'; -part 'src/customization/header_style.dart'; -part 'src/widgets/cell_widget.dart'; -part 'src/widgets/custom_icon_button.dart'; +export 'src/customization/calendar_builders.dart'; +export 'src/customization/calendar_style.dart'; +export 'src/customization/days_of_week_style.dart'; +export 'src/customization/header_style.dart'; +export 'src/shared/utils.dart'; +export 'src/table_calendar.dart'; +export 'src/table_calendar_base.dart'; diff --git a/pubspec.lock b/pubspec.lock index cda66d9a..d4a0886f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,62 +1,54 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - archive: - dependency: transitive - description: - name: archive - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.11" - args: - dependency: transitive - description: - name: args - url: "https://pub.dartlang.org" - source: hosted - version: "1.5.2" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "1.0.5" - charcode: + version: "2.1.1" + characters: dependency: transitive description: - name: charcode - url: "https://pub.dartlang.org" + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.1.2" - collection: + version: "1.3.0" + clock: dependency: transitive description: - name: collection - url: "https://pub.dartlang.org" + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.14.11" - convert: + version: "1.1.1" + collection: dependency: transitive description: - name: convert - url: "https://pub.dartlang.org" + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" source: hosted - version: "2.1.1" - crypto: + version: "1.18.0" + fake_async: dependency: transitive description: - name: crypto - url: "https://pub.dartlang.org" + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "1.3.1" flutter: dependency: "direct main" description: flutter @@ -67,69 +59,102 @@ packages: description: flutter source: sdk version: "0.0.0" - image: + http: dependency: transitive description: - name: image - url: "https://pub.dartlang.org" + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "1.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" intl: dependency: "direct main" description: name: intl - url: "https://pub.dartlang.org" + sha256: "00f33b908655e606b86d2ade4710a231b802eec6f11e87e4ea3783fd72077a50" + url: "https://pub.dev" source: hosted - version: "0.16.0" - matcher: + version: "0.20.1" + leak_tracker: dependency: transitive description: - name: matcher - url: "https://pub.dartlang.org" + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" source: hosted - version: "0.12.6" - meta: + version: "10.0.5" + leak_tracker_flutter_testing: dependency: transitive description: - name: meta - url: "https://pub.dartlang.org" + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" source: hosted - version: "1.1.8" - path: + version: "3.0.5" + leak_tracker_testing: dependency: transitive description: - name: path - url: "https://pub.dartlang.org" + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lint: + dependency: "direct dev" + description: + name: lint + sha256: d758a5211fce7fd3f5e316f804daefecdc34c7e53559716125e6da7388ae8565 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" source: hosted - version: "1.6.4" - pedantic: + version: "0.12.16+1" + material_color_utilities: dependency: transitive description: - name: pedantic - url: "https://pub.dartlang.org" + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" source: hosted - version: "1.8.0+1" - petitparser: + version: "0.11.1" + meta: dependency: transitive description: - name: petitparser - url: "https://pub.dartlang.org" + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" source: hosted - version: "2.4.0" - quiver: + version: "1.15.0" + path: dependency: transitive description: - name: quiver - url: "https://pub.dartlang.org" + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "1.9.0" simple_gesture_detector: dependency: "direct main" description: name: simple_gesture_detector - url: "https://pub.dartlang.org" + sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3 + url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.2.1" sky_engine: dependency: transitive description: flutter @@ -139,64 +164,82 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" source: hosted - version: "1.5.5" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" source: hosted - version: "1.9.3" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.2" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" source: hosted - version: "0.2.11" + version: "0.7.2" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" source: hosted - version: "1.1.6" + version: "1.4.0" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.0.8" - xml: + version: "2.1.4" + vm_service: dependency: transitive description: - name: xml - url: "https://pub.dartlang.org" + name: vm_service + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" source: hosted - version: "3.5.0" + version: "14.2.5" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: - dart: ">=2.4.0 <3.0.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/pubspec.yaml b/pubspec.yaml index 9f247263..c1fb94c0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,21 +1,22 @@ name: table_calendar -description: Highly customizable, feature-packed Flutter Calendar with gestures, animations and multiple formats. -version: 2.2.3 +description: Highly customizable, feature-packed calendar widget for Flutter. +version: 3.2.0 author: Aleksander Woźniak homepage: https://github.com/aleksanderwozniak/table_calendar environment: - sdk: ">=2.2.2 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: sdk: flutter - - intl: ">=0.15.0 <0.17.0" - simple_gesture_detector: ^0.1.3 + + intl: ^0.20.0 + simple_gesture_detector: ^0.2.0 dev_dependencies: flutter_test: sdk: flutter + lint: ^2.0.1 -flutter: \ No newline at end of file +flutter: diff --git a/table_calendar.iml b/table_calendar.iml deleted file mode 100644 index 499f388f..00000000 --- a/table_calendar.iml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/test/calendar_header_test.dart b/test/calendar_header_test.dart new file mode 100644 index 00000000..0df34d6d --- /dev/null +++ b/test/calendar_header_test.dart @@ -0,0 +1,201 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:intl/intl.dart' as intl; +import 'package:table_calendar/src/customization/header_style.dart'; +import 'package:table_calendar/src/shared/utils.dart'; +import 'package:table_calendar/src/widgets/calendar_header.dart'; +import 'package:table_calendar/src/widgets/custom_icon_button.dart'; +import 'package:table_calendar/src/widgets/format_button.dart'; + +import 'common.dart'; + +final focusedMonth = DateTime.utc(2021, 7, 15); + +Widget setupTestWidget({ + HeaderStyle headerStyle = const HeaderStyle(), + VoidCallback? onLeftChevronTap, + VoidCallback? onRightChevronTap, + VoidCallback? onHeaderTap, + VoidCallback? onHeaderLongPress, + Function(CalendarFormat)? onFormatButtonTap, + Map availableCalendarFormats = calendarFormatMap, +}) { + return Directionality( + textDirection: TextDirection.ltr, + child: Material( + child: CalendarHeader( + focusedMonth: focusedMonth, + calendarFormat: CalendarFormat.month, + headerStyle: headerStyle, + onLeftChevronTap: () => onLeftChevronTap?.call(), + onRightChevronTap: () => onRightChevronTap?.call(), + onHeaderTap: () => onHeaderTap?.call(), + onHeaderLongPress: () => onHeaderLongPress?.call(), + onFormatButtonTap: (format) => onFormatButtonTap?.call(format), + availableCalendarFormats: availableCalendarFormats, + ), + ), + ); +} + +void main() { + testWidgets( + 'Displays corrent month and year for given focusedMonth', + (tester) async { + await tester.pumpWidget(setupTestWidget()); + + final headerText = intl.DateFormat.yMMMM().format(focusedMonth); + + expect(find.byType(CalendarHeader), findsOneWidget); + expect(find.text(headerText), findsOneWidget); + }, + ); + testWidgets( + 'Ensure chevrons and FormatButton are visible by default, test onTap callbacks', + (tester) async { + bool leftChevronTapped = false; + bool rightChevronTapped = false; + bool headerTapped = false; + bool headerLongPressed = false; + bool formatButtonTapped = false; + + await tester.pumpWidget( + setupTestWidget( + onLeftChevronTap: () => leftChevronTapped = true, + onRightChevronTap: () => rightChevronTapped = true, + onHeaderTap: () => headerTapped = true, + onHeaderLongPress: () => headerLongPressed = true, + onFormatButtonTap: (_) => formatButtonTapped = true, + ), + ); + + final leftChevron = find.widgetWithIcon( + CustomIconButton, + Icons.chevron_left, + ); + + final rightChevron = find.widgetWithIcon( + CustomIconButton, + Icons.chevron_right, + ); + + final header = find.byType(CalendarHeader); + final formatButton = find.byType(FormatButton); + + expect(leftChevron, findsOneWidget); + expect(rightChevron, findsOneWidget); + expect(header, findsOneWidget); + expect(formatButton, findsOneWidget); + + expect(leftChevronTapped, false); + expect(rightChevronTapped, false); + expect(headerTapped, false); + expect(headerLongPressed, false); + expect(formatButtonTapped, false); + + await tester.tap(leftChevron); + await tester.pumpAndSettle(); + + await tester.tap(rightChevron); + await tester.pumpAndSettle(); + + await tester.tap(header); + await tester.pumpAndSettle(); + + await tester.longPress(header); + await tester.pumpAndSettle(); + + await tester.tap(formatButton); + await tester.pumpAndSettle(); + + expect(leftChevronTapped, true); + expect(rightChevronTapped, true); + expect(headerTapped, true); + expect(headerLongPressed, true); + expect(formatButtonTapped, true); + }, + ); + + testWidgets( + 'When leftChevronVisible is false, do not show the left chevron', + (tester) async { + await tester.pumpWidget( + setupTestWidget( + headerStyle: const HeaderStyle( + leftChevronVisible: false, + ), + ), + ); + + final leftChevron = find.widgetWithIcon( + CustomIconButton, + Icons.chevron_left, + ); + + final rightChevron = find.widgetWithIcon( + CustomIconButton, + Icons.chevron_right, + ); + + expect(leftChevron, findsNothing); + expect(rightChevron, findsOneWidget); + }, + ); + + testWidgets( + 'When rightChevronVisible is false, do not show the right chevron', + (tester) async { + await tester.pumpWidget( + setupTestWidget( + headerStyle: const HeaderStyle( + rightChevronVisible: false, + ), + ), + ); + + final leftChevron = find.widgetWithIcon( + CustomIconButton, + Icons.chevron_left, + ); + + final rightChevron = find.widgetWithIcon( + CustomIconButton, + Icons.chevron_right, + ); + + expect(leftChevron, findsOneWidget); + expect(rightChevron, findsNothing); + }, + ); + + testWidgets( + 'When availableCalendarFormats has a single format, do not show the FormatButton', + (tester) async { + await tester.pumpWidget( + setupTestWidget( + availableCalendarFormats: const {CalendarFormat.month: 'Month'}, + ), + ); + + final formatButton = find.byType(FormatButton); + expect(formatButton, findsNothing); + }, + ); + + testWidgets( + 'When formatButtonVisible is false, do not show the FormatButton', + (tester) async { + await tester.pumpWidget( + setupTestWidget( + headerStyle: const HeaderStyle(formatButtonVisible: false), + ), + ); + + final formatButton = find.byType(FormatButton); + expect(formatButton, findsNothing); + }, + ); +} diff --git a/test/calendar_page_test.dart b/test/calendar_page_test.dart new file mode 100644 index 00000000..19fc8267 --- /dev/null +++ b/test/calendar_page_test.dart @@ -0,0 +1,152 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:table_calendar/src/widgets/calendar_page.dart'; + +Widget setupTestWidget(Widget child) { + return Directionality( + textDirection: TextDirection.ltr, + child: child, + ); +} + +List visibleDays = getDaysInRange( + DateTime.utc(2021, 6, 27), + DateTime.utc(2021, 7, 31), +); + +List getDaysInRange(DateTime first, DateTime last) { + final dayCount = last.difference(first).inDays + 1; + return List.generate( + dayCount, + (index) => DateTime.utc(first.year, first.month, first.day + index), + ); +} + +void main() { + testWidgets( + 'CalendarPage lays out all the visible days', + (tester) async { + await tester.pumpWidget( + setupTestWidget( + CalendarPage( + visibleDays: visibleDays, + dayBuilder: (context, day) { + return Text('${day.day}'); + }, + dowVisible: false, + ), + ), + ); + + final expectedCellCount = visibleDays.length; + expect(find.byType(Text), findsNWidgets(expectedCellCount)); + }, + ); + + testWidgets( + 'CalendarPage lays out 7 DOW labels', + (tester) async { + await tester.pumpWidget( + setupTestWidget( + CalendarPage( + visibleDays: visibleDays, + dayBuilder: (context, day) { + return Text('${day.day}'); + }, + dowBuilder: (context, day) { + return Text('${day.weekday}'); + }, + dowHeight: 5, + ), + ), + ); + + final expectedCellCount = visibleDays.length; + const expectedDowLabels = 7; + + expect( + find.byType(Text), + findsNWidgets(expectedCellCount + expectedDowLabels), + ); + }, + ); + + testWidgets( + 'Throw AssertionError when CalendarPage is built with dowVisible set to true, but dowBuilder is absent', + (tester) async { + expect( + () async { + await tester.pumpWidget( + setupTestWidget( + CalendarPage( + visibleDays: visibleDays, + dayBuilder: (context, day) { + return Text('${day.day}'); + }, + ), + ), + ); + }, + throwsAssertionError, + ); + }, + ); + + testWidgets( + 'Week numbers are not visible by default', + (tester) async { + await tester.pumpWidget( + setupTestWidget( + CalendarPage( + visibleDays: visibleDays, + dayBuilder: (context, day) { + return Text('${day.day}'); + }, + dowBuilder: (context, day) { + return Text('${day.weekday}'); + }, + dowHeight: 5, + ), + ), + ); + + expect( + find.byType(Column), + findsNWidgets(0), + ); + }, + ); + + testWidgets( + 'Week numbers are visible', + (tester) async { + await tester.pumpWidget( + setupTestWidget( + CalendarPage( + visibleDays: visibleDays, + dayBuilder: (context, day) { + return Text('${day.day}'); + }, + dowBuilder: (context, day) { + return Text('${day.weekday}'); + }, + dowHeight: 5, + weekNumberVisible: true, + weekNumberBuilder: (BuildContext context, DateTime day) { + return Text(day.weekday.toString()); + }, + ), + ), + ); + + expect( + find.byType(Column), + findsNWidgets(1), + ); + }, + ); +} diff --git a/test/cell_content_test.dart b/test/cell_content_test.dart new file mode 100644 index 00000000..9f43e2b6 --- /dev/null +++ b/test/cell_content_test.dart @@ -0,0 +1,390 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:intl/intl.dart' hide TextDirection; +import 'package:table_calendar/src/widgets/cell_content.dart'; +import 'package:table_calendar/table_calendar.dart'; + +Widget setupTestWidget( + DateTime cellDay, { + CalendarBuilders calendarBuilders = const CalendarBuilders(), + CalendarStyle calendarStyle = const CalendarStyle(), + bool isDisabled = false, + bool isToday = false, + bool isWeekend = false, + bool isOutside = false, + bool isSelected = false, + bool isRangeStart = false, + bool isRangeEnd = false, + bool isWithinRange = false, + bool isHoliday = false, + bool isTodayHighlighted = true, + String? locale, +}) { + return Directionality( + textDirection: TextDirection.ltr, + child: CellContent( + day: cellDay, + focusedDay: cellDay, + calendarBuilders: calendarBuilders, + calendarStyle: calendarStyle, + isDisabled: isDisabled, + isToday: isToday, + isWeekend: isWeekend, + isOutside: isOutside, + isSelected: isSelected, + isRangeStart: isRangeStart, + isRangeEnd: isRangeEnd, + isWithinRange: isWithinRange, + isHoliday: isHoliday, + isTodayHighlighted: isTodayHighlighted, + locale: locale, + ), + ); +} + +void main() { + group('CalendarBuilders flag test:', () { + testWidgets('selectedBuilder', (tester) async { + DateTime? builderDay; + + final calendarBuilders = CalendarBuilders( + selectedBuilder: (context, day, focusedDay) { + builderDay = day; + return Text('${day.day}'); + }, + ); + + final cellDay = DateTime.utc(2021, 7, 15); + expect(builderDay, isNull); + + await tester.pumpWidget( + setupTestWidget( + cellDay, + calendarBuilders: calendarBuilders, + isSelected: true, + ), + ); + + expect(builderDay, cellDay); + }); + + testWidgets('rangeStartBuilder', (tester) async { + DateTime? builderDay; + + final calendarBuilders = CalendarBuilders( + rangeStartBuilder: (context, day, focusedDay) { + builderDay = day; + return Text('${day.day}'); + }, + ); + + final cellDay = DateTime.utc(2021, 7, 15); + expect(builderDay, isNull); + + await tester.pumpWidget( + setupTestWidget( + cellDay, + calendarBuilders: calendarBuilders, + isRangeStart: true, + ), + ); + + expect(builderDay, cellDay); + }); + + testWidgets('rangeEndBuilder', (tester) async { + DateTime? builderDay; + + final calendarBuilders = CalendarBuilders( + rangeEndBuilder: (context, day, focusedDay) { + builderDay = day; + return Text('${day.day}'); + }, + ); + + final cellDay = DateTime.utc(2021, 7, 15); + expect(builderDay, isNull); + + await tester.pumpWidget( + setupTestWidget( + cellDay, + calendarBuilders: calendarBuilders, + isRangeEnd: true, + ), + ); + + expect(builderDay, cellDay); + }); + + testWidgets('withinRangeBuilder', (tester) async { + DateTime? builderDay; + + final calendarBuilders = CalendarBuilders( + withinRangeBuilder: (context, day, focusedDay) { + builderDay = day; + return Text('${day.day}'); + }, + ); + + final cellDay = DateTime.utc(2021, 7, 15); + expect(builderDay, isNull); + + await tester.pumpWidget( + setupTestWidget( + cellDay, + calendarBuilders: calendarBuilders, + isWithinRange: true, + ), + ); + + expect(builderDay, cellDay); + }); + + testWidgets('todayBuilder', (tester) async { + DateTime? builderDay; + + final calendarBuilders = CalendarBuilders( + todayBuilder: (context, day, focusedDay) { + builderDay = day; + return Text('${day.day}'); + }, + ); + + final cellDay = DateTime.utc(2021, 7, 15); + expect(builderDay, isNull); + + await tester.pumpWidget( + setupTestWidget( + cellDay, + calendarBuilders: calendarBuilders, + isToday: true, + ), + ); + + expect(builderDay, cellDay); + }); + + testWidgets('holidayBuilder', (tester) async { + DateTime? builderDay; + + final calendarBuilders = CalendarBuilders( + holidayBuilder: (context, day, focusedDay) { + builderDay = day; + return Text('${day.day}'); + }, + ); + + final cellDay = DateTime.utc(2021, 7, 15); + expect(builderDay, isNull); + + await tester.pumpWidget( + setupTestWidget( + cellDay, + calendarBuilders: calendarBuilders, + isHoliday: true, + ), + ); + + expect(builderDay, cellDay); + }); + + testWidgets('outsideBuilder', (tester) async { + DateTime? builderDay; + + final calendarBuilders = CalendarBuilders( + outsideBuilder: (context, day, focusedDay) { + builderDay = day; + return Text('${day.day}'); + }, + ); + + final cellDay = DateTime.utc(2021, 7, 15); + expect(builderDay, isNull); + + await tester.pumpWidget( + setupTestWidget( + cellDay, + calendarBuilders: calendarBuilders, + isOutside: true, + ), + ); + + expect(builderDay, cellDay); + }); + + testWidgets( + 'defaultBuilder gets triggered when no other flags are active', + (tester) async { + DateTime? builderDay; + + final calendarBuilders = CalendarBuilders( + defaultBuilder: (context, day, focusedDay) { + builderDay = day; + return Text('${day.day}'); + }, + ); + + final cellDay = DateTime.utc(2021, 7, 15); + expect(builderDay, isNull); + + await tester.pumpWidget( + setupTestWidget( + cellDay, + calendarBuilders: calendarBuilders, + ), + ); + + expect(builderDay, cellDay); + }, + ); + + testWidgets( + 'disabledBuilder has higher build order priority than selectedBuilder', + (tester) async { + DateTime? builderDay; + String builderName = ''; + + final calendarBuilders = CalendarBuilders( + selectedBuilder: (context, day, focusedDay) { + builderName = 'selectedBuilder'; + builderDay = day; + return Text('${day.day}'); + }, + disabledBuilder: (context, day, focusedDay) { + builderName = 'disabledBuilder'; + builderDay = day; + return Text('${day.day}'); + }, + ); + + final cellDay = DateTime.utc(2021, 7, 15); + expect(builderDay, isNull); + + await tester.pumpWidget( + setupTestWidget( + cellDay, + calendarBuilders: calendarBuilders, + isDisabled: true, + isSelected: true, + ), + ); + + expect(builderDay, cellDay); + expect(builderName, 'disabledBuilder'); + }, + ); + + testWidgets( + 'prioritizedBuilder has the highest build order priority', + (tester) async { + DateTime? builderDay; + String builderName = ''; + + final calendarBuilders = CalendarBuilders( + prioritizedBuilder: (context, day, focusedDay) { + builderName = 'prioritizedBuilder'; + builderDay = day; + return Text('${day.day}'); + }, + disabledBuilder: (context, day, focusedDay) { + builderName = 'disabledBuilder'; + builderDay = day; + return Text('${day.day}'); + }, + ); + + final cellDay = DateTime.utc(2021, 7, 15); + expect(builderDay, isNull); + + await tester.pumpWidget( + setupTestWidget( + cellDay, + calendarBuilders: calendarBuilders, + isDisabled: true, + ), + ); + + expect(builderDay, cellDay); + expect(builderName, 'prioritizedBuilder'); + }, + ); + }); + + group('CalendarBuilders Locale test:', () { + testWidgets('en locale with default dayTextFormatter', (tester) async { + const locale = 'en'; + initializeDateFormatting(locale); + + final cellDay = DateTime.utc(2021, 7, 15); + await tester.pumpWidget( + setupTestWidget( + cellDay, + locale: locale, + ), + ); + + final dayFinder = find.text('${cellDay.day}'); + expect(dayFinder, findsOneWidget); + }); + + testWidgets('en locale with custom dayTextFormatter', (tester) async { + const locale = 'en'; + initializeDateFormatting(locale); + + final cellDay = DateTime.utc(2021, 7, 15); + await tester.pumpWidget( + setupTestWidget( + cellDay, + locale: locale, + calendarStyle: CalendarStyle( + dayTextFormatter: (date, locale) => + DateFormat.d(locale).format(date), + ), + ), + ); + + final dayFinder = find.text(DateFormat.d(locale).format(cellDay)); + expect(dayFinder, findsOneWidget); + }); + + testWidgets('ar locale with default dayTextFormatter', (tester) async { + const locale = 'ar'; + initializeDateFormatting(locale); + + final cellDay = DateTime.utc(2021, 7, 15); + await tester.pumpWidget( + setupTestWidget( + cellDay, + locale: locale, + ), + ); + + final dayFinder = find.text('${cellDay.day}'); + expect(dayFinder, findsOneWidget); + }); + + testWidgets('ar locale with custom dayTextFormatter', (tester) async { + const locale = 'ar'; + initializeDateFormatting(locale); + + final cellDay = DateTime.utc(2021, 7, 15); + await tester.pumpWidget( + setupTestWidget( + cellDay, + locale: locale, + calendarStyle: CalendarStyle( + dayTextFormatter: (date, locale) => + DateFormat.d(locale).format(date), + ), + ), + ); + + final dayFinder = find.text(DateFormat.d(locale).format(cellDay)); + expect(dayFinder, findsOneWidget); + }); + }); +} diff --git a/test/common.dart b/test/common.dart new file mode 100644 index 00000000..7e869196 --- /dev/null +++ b/test/common.dart @@ -0,0 +1,15 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/foundation.dart'; +import 'package:table_calendar/src/shared/utils.dart'; + +ValueKey dateToKey(DateTime date, {String prefix = ''}) { + return ValueKey('$prefix${date.year}-${date.month}-${date.day}'); +} + +const calendarFormatMap = { + CalendarFormat.month: 'Month', + CalendarFormat.twoWeeks: 'Two weeks', + CalendarFormat.week: 'week', +}; diff --git a/test/custom_icon_button_test.dart b/test/custom_icon_button_test.dart new file mode 100644 index 00000000..de53539b --- /dev/null +++ b/test/custom_icon_button_test.dart @@ -0,0 +1,41 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:table_calendar/src/widgets/custom_icon_button.dart'; + +Widget setupTestWidget(Widget child) { + return Directionality( + textDirection: TextDirection.ltr, + child: Material(child: child), + ); +} + +void main() { + testWidgets( + 'onTap gets called when CustomIconButton is tapped', + (tester) async { + bool buttonTapped = false; + + await tester.pumpWidget( + setupTestWidget( + CustomIconButton( + icon: const Icon(Icons.chevron_left), + onTap: () { + buttonTapped = true; + }, + ), + ), + ); + + final button = find.byType(CustomIconButton); + expect(button, findsOneWidget); + expect(buttonTapped, false); + + await tester.tap(button); + await tester.pumpAndSettle(); + expect(buttonTapped, true); + }, + ); +} diff --git a/test/format_button_test.dart b/test/format_button_test.dart new file mode 100644 index 00000000..61c4bdeb --- /dev/null +++ b/test/format_button_test.dart @@ -0,0 +1,183 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:table_calendar/src/widgets/format_button.dart'; +import 'package:table_calendar/table_calendar.dart'; + +import 'common.dart'; + +Widget setupTestWidget(Widget child) { + return Directionality( + textDirection: TextDirection.ltr, + child: Material(child: child), + ); +} + +void main() { + group('onTap callback tests:', () { + testWidgets( + 'Initial format month returns twoWeeks when tapped', + (tester) async { + const headerStyle = HeaderStyle(); + CalendarFormat? calendarFormat; + + await tester.pumpWidget( + setupTestWidget( + FormatButton( + availableCalendarFormats: calendarFormatMap, + calendarFormat: CalendarFormat.month, + decoration: headerStyle.formatButtonDecoration, + padding: headerStyle.formatButtonPadding, + textStyle: headerStyle.formatButtonTextStyle, + showsNextFormat: headerStyle.formatButtonShowsNext, + onTap: (format) { + calendarFormat = format; + }, + ), + ), + ); + + expect(find.byType(FormatButton), findsOneWidget); + expect(calendarFormat, isNull); + + await tester.tap(find.byType(FormatButton)); + await tester.pumpAndSettle(); + expect(calendarFormat, CalendarFormat.twoWeeks); + }, + ); + + testWidgets( + 'Initial format twoWeeks returns week when tapped', + (tester) async { + const headerStyle = HeaderStyle(); + CalendarFormat? calendarFormat; + + await tester.pumpWidget( + setupTestWidget( + FormatButton( + availableCalendarFormats: calendarFormatMap, + calendarFormat: CalendarFormat.twoWeeks, + decoration: headerStyle.formatButtonDecoration, + padding: headerStyle.formatButtonPadding, + textStyle: headerStyle.formatButtonTextStyle, + showsNextFormat: headerStyle.formatButtonShowsNext, + onTap: (format) { + calendarFormat = format; + }, + ), + ), + ); + + expect(find.byType(FormatButton), findsOneWidget); + expect(calendarFormat, isNull); + + await tester.tap(find.byType(FormatButton)); + await tester.pumpAndSettle(); + expect(calendarFormat, CalendarFormat.week); + }, + ); + + testWidgets( + 'Initial format week return month when tapped', + (tester) async { + const headerStyle = HeaderStyle(); + CalendarFormat? calendarFormat; + + await tester.pumpWidget( + setupTestWidget( + FormatButton( + availableCalendarFormats: calendarFormatMap, + calendarFormat: CalendarFormat.week, + decoration: headerStyle.formatButtonDecoration, + padding: headerStyle.formatButtonPadding, + textStyle: headerStyle.formatButtonTextStyle, + showsNextFormat: headerStyle.formatButtonShowsNext, + onTap: (format) { + calendarFormat = format; + }, + ), + ), + ); + + expect(find.byType(FormatButton), findsOneWidget); + expect(calendarFormat, isNull); + + await tester.tap(find.byType(FormatButton)); + await tester.pumpAndSettle(); + expect(calendarFormat, CalendarFormat.month); + }, + ); + }); + + group('showsNextFormat tests:', () { + testWidgets( + 'true - display next calendar format', + (tester) async { + const headerStyle = HeaderStyle(); + + const currentFormatIndex = 0; + final currentFormat = + calendarFormatMap.keys.elementAt(currentFormatIndex); + final currentFormatText = + calendarFormatMap.values.elementAt(currentFormatIndex); + + const nextFormatIndex = 1; + final nextFormatText = + calendarFormatMap.values.elementAt(nextFormatIndex); + + await tester.pumpWidget( + setupTestWidget( + FormatButton( + availableCalendarFormats: calendarFormatMap, + calendarFormat: currentFormat, + decoration: headerStyle.formatButtonDecoration, + padding: headerStyle.formatButtonPadding, + textStyle: headerStyle.formatButtonTextStyle, + showsNextFormat: headerStyle.formatButtonShowsNext, + onTap: (format) {}, + ), + ), + ); + + expect(find.byType(FormatButton), findsOneWidget); + expect(currentFormatText, isNotNull); + expect(find.text(currentFormatText), findsNothing); + expect(nextFormatText, isNotNull); + expect(find.text(nextFormatText), findsOneWidget); + }, + ); + + testWidgets( + 'false - display current calendar format', + (tester) async { + const headerStyle = HeaderStyle(formatButtonShowsNext: false); + + const currentFormatIndex = 0; + final currentFormat = + calendarFormatMap.keys.elementAt(currentFormatIndex); + final currentFormatText = + calendarFormatMap.values.elementAt(currentFormatIndex); + + await tester.pumpWidget( + setupTestWidget( + FormatButton( + availableCalendarFormats: calendarFormatMap, + calendarFormat: currentFormat, + decoration: headerStyle.formatButtonDecoration, + padding: headerStyle.formatButtonPadding, + textStyle: headerStyle.formatButtonTextStyle, + showsNextFormat: headerStyle.formatButtonShowsNext, + onTap: (format) {}, + ), + ), + ); + + expect(find.byType(FormatButton), findsOneWidget); + expect(currentFormatText, isNotNull); + expect(find.text(currentFormatText), findsOneWidget); + }, + ); + }); +} diff --git a/test/table_calendar_base_test.dart b/test/table_calendar_base_test.dart new file mode 100644 index 00000000..c0fddf66 --- /dev/null +++ b/test/table_calendar_base_test.dart @@ -0,0 +1,378 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:simple_gesture_detector/simple_gesture_detector.dart'; +import 'package:table_calendar/table_calendar.dart'; + +import 'common.dart'; + +Widget setupTestWidget(Widget child) { + return Directionality( + textDirection: TextDirection.ltr, + child: child, + ); +} + +void main() { + group('Correct days are displayed for given focusedDay when:', () { + testWidgets( + 'in month format, starting day is Sunday', + (tester) async { + final focusedDay = DateTime.utc(2021, 7, 15); + + await tester.pumpWidget( + setupTestWidget( + TableCalendarBase( + firstDay: DateTime.utc(2021, 5, 15), + lastDay: DateTime.utc(2021, 8, 18), + focusedDay: focusedDay, + dayBuilder: (context, day, focusedDay) { + return Text( + '${day.day}', + key: dateToKey(day), + ); + }, + rowHeight: 52, + dowVisible: false, + ), + ), + ); + + final firstVisibleDay = DateTime.utc(2021, 6, 27); + final lastVisibleDay = DateTime.utc(2021, 7, 31); + + final focusedDayKey = dateToKey(focusedDay); + final firstVisibleDayKey = dateToKey(firstVisibleDay); + final lastVisibleDayKey = dateToKey(lastVisibleDay); + + final startOOBKey = + dateToKey(firstVisibleDay.subtract(const Duration(days: 1))); + final endOOBKey = + dateToKey(lastVisibleDay.add(const Duration(days: 1))); + + expect(find.byKey(focusedDayKey), findsOneWidget); + expect(find.byKey(firstVisibleDayKey), findsOneWidget); + expect(find.byKey(lastVisibleDayKey), findsOneWidget); + + expect(find.byKey(startOOBKey), findsNothing); + expect(find.byKey(endOOBKey), findsNothing); + }, + ); + + testWidgets( + 'in two weeks format, starting day is Sunday', + (tester) async { + final focusedDay = DateTime.utc(2021, 7, 15); + + await tester.pumpWidget( + setupTestWidget( + TableCalendarBase( + firstDay: DateTime.utc(2021, 5, 15), + lastDay: DateTime.utc(2021, 8, 18), + focusedDay: focusedDay, + dayBuilder: (context, day, focusedDay) { + return Text( + '${day.day}', + key: dateToKey(day), + ); + }, + rowHeight: 52, + dowVisible: false, + calendarFormat: CalendarFormat.twoWeeks, + ), + ), + ); + + final firstVisibleDay = DateTime.utc(2021, 7, 4); + final lastVisibleDay = DateTime.utc(2021, 7, 17); + + final focusedDayKey = dateToKey(focusedDay); + final firstVisibleDayKey = dateToKey(firstVisibleDay); + final lastVisibleDayKey = dateToKey(lastVisibleDay); + + final startOOBKey = + dateToKey(firstVisibleDay.subtract(const Duration(days: 1))); + final endOOBKey = + dateToKey(lastVisibleDay.add(const Duration(days: 1))); + + expect(find.byKey(focusedDayKey), findsOneWidget); + expect(find.byKey(firstVisibleDayKey), findsOneWidget); + expect(find.byKey(lastVisibleDayKey), findsOneWidget); + + expect(find.byKey(startOOBKey), findsNothing); + expect(find.byKey(endOOBKey), findsNothing); + }, + ); + + testWidgets( + 'in week format, starting day is Sunday', + (tester) async { + final focusedDay = DateTime.utc(2021, 7, 15); + + await tester.pumpWidget( + setupTestWidget( + TableCalendarBase( + firstDay: DateTime.utc(2021, 5, 15), + lastDay: DateTime.utc(2021, 8, 18), + focusedDay: focusedDay, + dayBuilder: (context, day, focusedDay) { + return Text( + '${day.day}', + key: dateToKey(day), + ); + }, + rowHeight: 52, + dowVisible: false, + calendarFormat: CalendarFormat.week, + ), + ), + ); + + final firstVisibleDay = DateTime.utc(2021, 7, 11); + final lastVisibleDay = DateTime.utc(2021, 7, 17); + + final focusedDayKey = dateToKey(focusedDay); + final firstVisibleDayKey = dateToKey(firstVisibleDay); + final lastVisibleDayKey = dateToKey(lastVisibleDay); + + final startOOBKey = + dateToKey(firstVisibleDay.subtract(const Duration(days: 1))); + final endOOBKey = + dateToKey(lastVisibleDay.add(const Duration(days: 1))); + + expect(find.byKey(focusedDayKey), findsOneWidget); + expect(find.byKey(firstVisibleDayKey), findsOneWidget); + expect(find.byKey(lastVisibleDayKey), findsOneWidget); + + expect(find.byKey(startOOBKey), findsNothing); + expect(find.byKey(endOOBKey), findsNothing); + }, + ); + + testWidgets( + 'in month format, starting day is Monday', + (tester) async { + final focusedDay = DateTime.utc(2021, 7, 15); + + await tester.pumpWidget( + setupTestWidget( + TableCalendarBase( + firstDay: DateTime.utc(2021, 5, 15), + lastDay: DateTime.utc(2021, 8, 18), + focusedDay: focusedDay, + dayBuilder: (context, day, focusedDay) { + return Text( + '${day.day}', + key: dateToKey(day), + ); + }, + rowHeight: 52, + dowVisible: false, + startingDayOfWeek: StartingDayOfWeek.monday, + ), + ), + ); + + final firstVisibleDay = DateTime.utc(2021, 6, 28); + final lastVisibleDay = DateTime.utc(2021, 8); + + final focusedDayKey = dateToKey(focusedDay); + final firstVisibleDayKey = dateToKey(firstVisibleDay); + final lastVisibleDayKey = dateToKey(lastVisibleDay); + + final startOOBKey = + dateToKey(firstVisibleDay.subtract(const Duration(days: 1))); + final endOOBKey = + dateToKey(lastVisibleDay.add(const Duration(days: 1))); + + expect(find.byKey(focusedDayKey), findsOneWidget); + expect(find.byKey(firstVisibleDayKey), findsOneWidget); + expect(find.byKey(lastVisibleDayKey), findsOneWidget); + + expect(find.byKey(startOOBKey), findsNothing); + expect(find.byKey(endOOBKey), findsNothing); + }, + ); + + testWidgets( + 'in two weeks format, starting day is Monday', + (tester) async { + final focusedDay = DateTime.utc(2021, 7, 15); + + await tester.pumpWidget( + setupTestWidget( + TableCalendarBase( + firstDay: DateTime.utc(2021, 5, 15), + lastDay: DateTime.utc(2021, 8, 18), + focusedDay: focusedDay, + dayBuilder: (context, day, focusedDay) { + return Text( + '${day.day}', + key: dateToKey(day), + ); + }, + rowHeight: 52, + dowVisible: false, + calendarFormat: CalendarFormat.twoWeeks, + startingDayOfWeek: StartingDayOfWeek.monday, + ), + ), + ); + + final firstVisibleDay = DateTime.utc(2021, 7, 5); + final lastVisibleDay = DateTime.utc(2021, 7, 18); + + final focusedDayKey = dateToKey(focusedDay); + final firstVisibleDayKey = dateToKey(firstVisibleDay); + final lastVisibleDayKey = dateToKey(lastVisibleDay); + + final startOOBKey = + dateToKey(firstVisibleDay.subtract(const Duration(days: 1))); + final endOOBKey = + dateToKey(lastVisibleDay.add(const Duration(days: 1))); + + expect(find.byKey(focusedDayKey), findsOneWidget); + expect(find.byKey(firstVisibleDayKey), findsOneWidget); + expect(find.byKey(lastVisibleDayKey), findsOneWidget); + + expect(find.byKey(startOOBKey), findsNothing); + expect(find.byKey(endOOBKey), findsNothing); + }, + ); + + testWidgets( + 'in week format, starting day is Monday', + (tester) async { + final focusedDay = DateTime.utc(2021, 7, 15); + + await tester.pumpWidget( + setupTestWidget( + TableCalendarBase( + firstDay: DateTime.utc(2021, 5, 15), + lastDay: DateTime.utc(2021, 8, 18), + focusedDay: focusedDay, + dayBuilder: (context, day, focusedDay) { + return Text( + '${day.day}', + key: dateToKey(day), + ); + }, + rowHeight: 52, + dowVisible: false, + calendarFormat: CalendarFormat.week, + startingDayOfWeek: StartingDayOfWeek.monday, + ), + ), + ); + + final firstVisibleDay = DateTime.utc(2021, 7, 12); + final lastVisibleDay = DateTime.utc(2021, 7, 18); + + final focusedDayKey = dateToKey(focusedDay); + final firstVisibleDayKey = dateToKey(firstVisibleDay); + final lastVisibleDayKey = dateToKey(lastVisibleDay); + + final startOOBKey = + dateToKey(firstVisibleDay.subtract(const Duration(days: 1))); + final endOOBKey = + dateToKey(lastVisibleDay.add(const Duration(days: 1))); + + expect(find.byKey(focusedDayKey), findsOneWidget); + expect(find.byKey(firstVisibleDayKey), findsOneWidget); + expect(find.byKey(lastVisibleDayKey), findsOneWidget); + + expect(find.byKey(startOOBKey), findsNothing); + expect(find.byKey(endOOBKey), findsNothing); + }, + ); + }); + + testWidgets( + 'Callbacks return expected values', + (tester) async { + DateTime focusedDay = DateTime.utc(2021, 7, 15); + final nextMonth = focusedDay.add(const Duration(days: 31)).month; + + bool calendarCreatedFlag = false; + SwipeDirection? verticalSwipeDirection; + + await tester.pumpWidget( + setupTestWidget( + TableCalendarBase( + firstDay: DateTime.utc(2021, 5, 15), + lastDay: DateTime.utc(2021, 8, 18), + focusedDay: focusedDay, + dayBuilder: (context, day, focusedDay) { + return Text( + '${day.day}', + key: dateToKey(day), + ); + }, + onCalendarCreated: (pageController) { + calendarCreatedFlag = true; + }, + onPageChanged: (focusedDay2) { + focusedDay = focusedDay2; + }, + onVerticalSwipe: (direction) { + verticalSwipeDirection = direction; + }, + rowHeight: 52, + dowVisible: false, + ), + ), + ); + + expect(calendarCreatedFlag, true); + + // Swipe left + await tester.drag( + find.byKey(dateToKey(focusedDay)), + const Offset(-500, 0), + ); + await tester.pumpAndSettle(); + expect(focusedDay.month, nextMonth); + + // Swipe up + await tester.drag( + find.byKey(dateToKey(focusedDay)), + const Offset(0, -500), + ); + await tester.pumpAndSettle(); + expect(verticalSwipeDirection, SwipeDirection.up); + }, + ); + + testWidgets( + 'Throw AssertionError when TableCalendarBase is built with dowVisible and dowBuilder, but dowHeight is absent', + (tester) async { + expect( + () async { + await tester.pumpWidget( + setupTestWidget( + TableCalendarBase( + firstDay: DateTime.utc(2021, 5, 15), + lastDay: DateTime.utc(2021, 8, 18), + focusedDay: DateTime.utc(2021, 7, 15), + dayBuilder: (context, day, focusedDay) { + return Text( + '${day.day}', + key: dateToKey(day), + ); + }, + rowHeight: 52, + dowBuilder: (context, day) { + return Text('${day.weekday}'); + }, + ), + ), + ); + }, + throwsAssertionError, + ); + }, + ); +} diff --git a/test/table_calendar_test.dart b/test/table_calendar_test.dart new file mode 100644 index 00000000..2889b947 --- /dev/null +++ b/test/table_calendar_test.dart @@ -0,0 +1,1464 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:intl/intl.dart' as intl; +import 'package:table_calendar/src/widgets/calendar_header.dart'; +import 'package:table_calendar/src/widgets/cell_content.dart'; +import 'package:table_calendar/src/widgets/custom_icon_button.dart'; +import 'package:table_calendar/table_calendar.dart'; + +import 'common.dart'; + +final initialFocusedDay = DateTime.utc(2021, 7, 15); +final today = initialFocusedDay; +final firstDay = DateTime.utc(2021, 5, 15); +final lastDay = DateTime.utc(2021, 9, 18); + +Widget setupTestWidget(Widget child) { + return Directionality( + textDirection: TextDirection.ltr, + child: Material(child: child), + ); +} + +Widget createTableCalendar({ + DateTime? focusedDay, + CalendarFormat calendarFormat = CalendarFormat.month, + Function(DateTime)? onPageChanged, + bool sixWeekMonthsEnforced = false, +}) { + return setupTestWidget( + TableCalendar( + focusedDay: focusedDay ?? initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + calendarFormat: calendarFormat, + onPageChanged: onPageChanged, + sixWeekMonthsEnforced: sixWeekMonthsEnforced, + ), + ); +} + +ValueKey cellContentKey(DateTime date) { + return dateToKey(date, prefix: 'CellContent-'); +} + +void main() { + group('TableCalendar correctly displays:', () { + testWidgets( + 'visible day cells for given focusedDay', + (tester) async { + await tester.pumpWidget(createTableCalendar()); + + final firstVisibleDay = DateTime.utc(2021, 6, 27); + final lastVisibleDay = DateTime.utc(2021, 7, 31); + + final focusedDayKey = cellContentKey(initialFocusedDay); + final firstVisibleDayKey = cellContentKey(firstVisibleDay); + final lastVisibleDayKey = cellContentKey(lastVisibleDay); + + final startOOBKey = + cellContentKey(firstVisibleDay.subtract(const Duration(days: 1))); + final endOOBKey = + cellContentKey(lastVisibleDay.add(const Duration(days: 1))); + + expect(find.byKey(focusedDayKey), findsOneWidget); + expect(find.byKey(firstVisibleDayKey), findsOneWidget); + expect(find.byKey(lastVisibleDayKey), findsOneWidget); + + expect(find.byKey(startOOBKey), findsNothing); + expect(find.byKey(endOOBKey), findsNothing); + }, + ); + + testWidgets( + 'visible day cells after swipe right when in week format', + (tester) async { + DateTime? updatedFocusedDay; + + await tester.pumpWidget( + createTableCalendar( + calendarFormat: CalendarFormat.week, + onPageChanged: (focusedDay) { + updatedFocusedDay = focusedDay; + }, + ), + ); + + await tester.drag( + find.byType(CellContent).first, + const Offset(500, 0), + ); + await tester.pumpAndSettle(); + + expect(updatedFocusedDay, isNotNull); + + final firstVisibleDay = DateTime.utc(2021, 7, 4); + final lastVisibleDay = DateTime.utc(2021, 7, 10); + + final focusedDayKey = cellContentKey(updatedFocusedDay!); + final firstVisibleDayKey = cellContentKey(firstVisibleDay); + final lastVisibleDayKey = cellContentKey(lastVisibleDay); + + final startOOBKey = + cellContentKey(firstVisibleDay.subtract(const Duration(days: 1))); + final endOOBKey = + cellContentKey(lastVisibleDay.add(const Duration(days: 1))); + + expect(find.byKey(focusedDayKey), findsOneWidget); + expect(find.byKey(firstVisibleDayKey), findsOneWidget); + expect(find.byKey(lastVisibleDayKey), findsOneWidget); + + expect(find.byKey(startOOBKey), findsNothing); + expect(find.byKey(endOOBKey), findsNothing); + }, + ); + + testWidgets( + 'visible day cells after swipe left when in week format', + (tester) async { + DateTime? updatedFocusedDay; + + await tester.pumpWidget( + createTableCalendar( + calendarFormat: CalendarFormat.week, + onPageChanged: (focusedDay) { + updatedFocusedDay = focusedDay; + }, + ), + ); + + await tester.drag( + find.byType(CellContent).first, + const Offset(-500, 0), + ); + await tester.pumpAndSettle(); + + expect(updatedFocusedDay, isNotNull); + + final firstVisibleDay = DateTime.utc(2021, 7, 18); + final lastVisibleDay = DateTime.utc(2021, 7, 24); + + final focusedDayKey = cellContentKey(updatedFocusedDay!); + final firstVisibleDayKey = cellContentKey(firstVisibleDay); + final lastVisibleDayKey = cellContentKey(lastVisibleDay); + + final startOOBKey = + cellContentKey(firstVisibleDay.subtract(const Duration(days: 1))); + final endOOBKey = + cellContentKey(lastVisibleDay.add(const Duration(days: 1))); + + expect(find.byKey(focusedDayKey), findsOneWidget); + expect(find.byKey(firstVisibleDayKey), findsOneWidget); + expect(find.byKey(lastVisibleDayKey), findsOneWidget); + + expect(find.byKey(startOOBKey), findsNothing); + expect(find.byKey(endOOBKey), findsNothing); + }, + ); + + testWidgets( + 'visible day cells after swipe right when in two weeks format', + (tester) async { + DateTime? updatedFocusedDay; + + await tester.pumpWidget( + createTableCalendar( + calendarFormat: CalendarFormat.twoWeeks, + onPageChanged: (focusedDay) { + updatedFocusedDay = focusedDay; + }, + ), + ); + + await tester.drag( + find.byType(CellContent).first, + const Offset(500, 0), + ); + await tester.pumpAndSettle(); + + expect(updatedFocusedDay, isNotNull); + + final firstVisibleDay = DateTime.utc(2021, 6, 20); + final lastVisibleDay = DateTime.utc(2021, 7, 3); + + final focusedDayKey = cellContentKey(updatedFocusedDay!); + final firstVisibleDayKey = cellContentKey(firstVisibleDay); + final lastVisibleDayKey = cellContentKey(lastVisibleDay); + + final startOOBKey = + cellContentKey(firstVisibleDay.subtract(const Duration(days: 1))); + final endOOBKey = + cellContentKey(lastVisibleDay.add(const Duration(days: 1))); + + expect(find.byKey(focusedDayKey), findsOneWidget); + expect(find.byKey(firstVisibleDayKey), findsOneWidget); + expect(find.byKey(lastVisibleDayKey), findsOneWidget); + + expect(find.byKey(startOOBKey), findsNothing); + expect(find.byKey(endOOBKey), findsNothing); + }, + ); + + testWidgets( + 'visible day cells after swipe left when in two weeks format', + (tester) async { + DateTime? updatedFocusedDay; + + await tester.pumpWidget( + createTableCalendar( + calendarFormat: CalendarFormat.twoWeeks, + onPageChanged: (focusedDay) { + updatedFocusedDay = focusedDay; + }, + ), + ); + + await tester.drag( + find.byType(CellContent).first, + const Offset(-500, 0), + ); + await tester.pumpAndSettle(); + + expect(updatedFocusedDay, isNotNull); + + final firstVisibleDay = DateTime.utc(2021, 7, 18); + final lastVisibleDay = DateTime.utc(2021, 7, 31); + + final focusedDayKey = cellContentKey(updatedFocusedDay!); + final firstVisibleDayKey = cellContentKey(firstVisibleDay); + final lastVisibleDayKey = cellContentKey(lastVisibleDay); + + final startOOBKey = + cellContentKey(firstVisibleDay.subtract(const Duration(days: 1))); + final endOOBKey = + cellContentKey(lastVisibleDay.add(const Duration(days: 1))); + + expect(find.byKey(focusedDayKey), findsOneWidget); + expect(find.byKey(firstVisibleDayKey), findsOneWidget); + expect(find.byKey(lastVisibleDayKey), findsOneWidget); + + expect(find.byKey(startOOBKey), findsNothing); + expect(find.byKey(endOOBKey), findsNothing); + }, + ); + + testWidgets( + '7 day cells in week format', + (tester) async { + await tester.pumpWidget( + createTableCalendar( + calendarFormat: CalendarFormat.week, + ), + ); + + final dayCells = tester.widgetList(find.byType(CellContent)); + expect(dayCells.length, 7); + }, + ); + + testWidgets( + '14 day cells in two weeks format', + (tester) async { + await tester.pumpWidget( + createTableCalendar( + calendarFormat: CalendarFormat.twoWeeks, + ), + ); + + final dayCells = tester.widgetList(find.byType(CellContent)); + expect(dayCells.length, 14); + }, + ); + + testWidgets( + '35 day cells in month format for July 2021', + (tester) async { + await tester.pumpWidget( + createTableCalendar(), + ); + + final dayCells = tester.widgetList(find.byType(CellContent)); + expect(dayCells.length, 35); + }, + ); + + testWidgets( + '42 day cells in month format for July 2021, when sixWeekMonthsEnforced is set to true', + (tester) async { + await tester.pumpWidget( + createTableCalendar( + sixWeekMonthsEnforced: true, + ), + ); + + final dayCells = tester.widgetList(find.byType(CellContent)); + expect(dayCells.length, 42); + }, + ); + + testWidgets( + 'CalendarHeader with updated month and year when focusedDay is changed', + (tester) async { + await tester.pumpWidget(createTableCalendar()); + + String headerText = intl.DateFormat.yMMMM().format(initialFocusedDay); + expect(find.byType(CalendarHeader), findsOneWidget); + expect(find.text(headerText), findsOneWidget); + + final updatedFocusedDay = DateTime.utc(2021, 8, 4); + + await tester.pumpWidget( + createTableCalendar(focusedDay: updatedFocusedDay), + ); + + headerText = intl.DateFormat.yMMMM().format(updatedFocusedDay); + expect(find.byType(CalendarHeader), findsOneWidget); + expect(find.text(headerText), findsOneWidget); + }, + ); + + testWidgets( + 'CalendarHeader with updated month and year when TableCalendar is swiped left', + (tester) async { + DateTime? updatedFocusedDay; + + await tester.pumpWidget( + createTableCalendar( + onPageChanged: (focusedDay) { + updatedFocusedDay = focusedDay; + }, + ), + ); + + String headerText = intl.DateFormat.yMMMM().format(initialFocusedDay); + expect(find.byType(CalendarHeader), findsOneWidget); + expect(find.text(headerText), findsOneWidget); + + await tester.drag( + find.byType(CellContent).first, + const Offset(-500, 0), + ); + await tester.pumpAndSettle(); + + expect(updatedFocusedDay, isNotNull); + expect(updatedFocusedDay!.month, initialFocusedDay.month + 1); + + headerText = intl.DateFormat.yMMMM().format(updatedFocusedDay!); + expect(find.byType(CalendarHeader), findsOneWidget); + expect(find.text(headerText), findsOneWidget); + + updatedFocusedDay = null; + + await tester.drag( + find.byType(CellContent).first, + const Offset(-500, 0), + ); + await tester.pumpAndSettle(); + + expect(updatedFocusedDay, isNotNull); + expect(updatedFocusedDay!.month, initialFocusedDay.month + 2); + + headerText = intl.DateFormat.yMMMM().format(updatedFocusedDay!); + expect(find.byType(CalendarHeader), findsOneWidget); + expect(find.text(headerText), findsOneWidget); + }, + ); + + testWidgets( + 'CalendarHeader with updated month and year when TableCalendar is swiped right', + (tester) async { + DateTime? updatedFocusedDay; + + await tester.pumpWidget( + createTableCalendar( + onPageChanged: (focusedDay) { + updatedFocusedDay = focusedDay; + }, + ), + ); + + String headerText = intl.DateFormat.yMMMM().format(initialFocusedDay); + expect(find.byType(CalendarHeader), findsOneWidget); + expect(find.text(headerText), findsOneWidget); + + await tester.drag( + find.byType(CellContent).first, + const Offset(500, 0), + ); + await tester.pumpAndSettle(); + + expect(updatedFocusedDay, isNotNull); + expect(updatedFocusedDay!.month, initialFocusedDay.month - 1); + + headerText = intl.DateFormat.yMMMM().format(updatedFocusedDay!); + expect(find.byType(CalendarHeader), findsOneWidget); + expect(find.text(headerText), findsOneWidget); + + updatedFocusedDay = null; + + await tester.drag( + find.byType(CellContent).first, + const Offset(500, 0), + ); + await tester.pumpAndSettle(); + + expect(updatedFocusedDay, isNotNull); + expect(updatedFocusedDay!.month, initialFocusedDay.month - 2); + + headerText = intl.DateFormat.yMMMM().format(updatedFocusedDay!); + expect(find.byType(CalendarHeader), findsOneWidget); + expect(find.text(headerText), findsOneWidget); + }, + ); + + testWidgets( + '3 event markers are visible when 3 events are assigned to a given day', + (tester) async { + final eventDay = DateTime.utc(2021, 7, 20); + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + eventLoader: (day) { + if (day.day == eventDay.day && day.month == eventDay.month) { + return ['Event 1', 'Event 2', 'Event 3']; + } + + return []; + }, + ), + ), + ); + + final eventDayKey = cellContentKey(eventDay); + final eventDayCellContent = find.byKey(eventDayKey); + + final eventDayStack = find.ancestor( + of: eventDayCellContent, + matching: find.byType(Stack), + ); + + final eventMarkers = tester.widgetList( + find.descendant( + of: eventDayStack, + matching: find.byWidgetPredicate( + (Widget marker) => marker is Container && marker.child == null, + ), + ), + ); + + expect(eventMarkers.length, 3); + }, + ); + + testWidgets( + 'Event loader is called for disabled days when loadEventForDisabledDays is set to true', + (tester) async { + final eventDay = DateTime.utc(2021, 7, 20); + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + loadEventsForDisabledDays: true, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + eventLoader: (day) { + if (day.day == eventDay.day && day.month == eventDay.month) { + return ['Event 1', 'Event 2', 'Event 3']; + } + + return []; + }, + enabledDayPredicate: (day) => false, + ), + ), + ); + + final eventDayKey = cellContentKey(eventDay); + final eventDayCellContent = find.byKey(eventDayKey); + + final eventDayStack = find.ancestor( + of: eventDayCellContent, + matching: find.byType(Stack), + ); + + final eventMarkers = tester.widgetList( + find.descendant( + of: eventDayStack, + matching: find.byWidgetPredicate( + (Widget marker) => marker is Container && marker.child == null, + ), + ), + ); + + expect(eventMarkers.length, 3); + }, + ); + + testWidgets( + 'currentDay correctly marks given day as today', + (tester) async { + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + ), + ), + ); + + final currentDayKey = cellContentKey(today); + final currentDayCellContent = + tester.widget(find.byKey(currentDayKey)) as CellContent; + + expect(currentDayCellContent.isToday, true); + }, + ); + + testWidgets( + 'if currentDay is absent, DateTime.now() is marked as today', + (tester) async { + final now = DateTime.now(); + final firstDay = DateTime.utc(now.year, now.month - 3, now.day); + final lastDay = DateTime.utc(now.year, now.month + 3, now.day); + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: now, + firstDay: firstDay, + lastDay: lastDay, + ), + ), + ); + + final currentDayKey = cellContentKey(now); + final currentDayCellContent = + tester.widget(find.byKey(currentDayKey)) as CellContent; + + expect(currentDayCellContent.isToday, true); + }, + ); + + testWidgets( + 'selectedDayPredicate correctly marks given day as selected', + (tester) async { + final selectedDay = DateTime.utc(2021, 7, 20); + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + selectedDayPredicate: (day) { + return isSameDay(day, selectedDay); + }, + ), + ), + ); + + final selectedDayKey = cellContentKey(selectedDay); + final selectedDayCellContent = + tester.widget(find.byKey(selectedDayKey)) as CellContent; + + expect(selectedDayCellContent.isSelected, true); + }, + ); + + testWidgets( + 'holidayPredicate correctly marks given day as holiday', + (tester) async { + final holiday = DateTime.utc(2021, 7, 20); + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + holidayPredicate: (day) { + return isSameDay(day, holiday); + }, + ), + ), + ); + + final holidayKey = cellContentKey(holiday); + final holidayCellContent = + tester.widget(find.byKey(holidayKey)) as CellContent; + + expect(holidayCellContent.isHoliday, true); + }, + ); + }); + + group('CalendarHeader chevrons test:', () { + testWidgets( + 'tapping on a left chevron navigates to previous calendar page', + (tester) async { + await tester.pumpWidget(createTableCalendar()); + + expect(find.text('July 2021'), findsOneWidget); + + final leftChevron = find.widgetWithIcon( + CustomIconButton, + Icons.chevron_left, + ); + + await tester.tap(leftChevron); + await tester.pumpAndSettle(); + + expect(find.text('June 2021'), findsOneWidget); + }, + ); + + testWidgets( + 'tapping on a right chevron navigates to next calendar page', + (tester) async { + await tester.pumpWidget(createTableCalendar()); + + expect(find.text('July 2021'), findsOneWidget); + + final rightChevron = find.widgetWithIcon( + CustomIconButton, + Icons.chevron_right, + ); + + await tester.tap(rightChevron); + await tester.pumpAndSettle(); + + expect(find.text('August 2021'), findsOneWidget); + }, + ); + }); + + group('Scrolling boundaries are set up properly:', () { + testWidgets('starting scroll boundary works correctly', (tester) async { + final focusedDay = DateTime.utc(2021, 6, 15); + + await tester.pumpWidget(createTableCalendar(focusedDay: focusedDay)); + + expect(find.byType(TableCalendar), findsOneWidget); + expect(find.text('June 2021'), findsOneWidget); + + await tester.drag(find.byType(CellContent).first, const Offset(500, 0)); + await tester.pumpAndSettle(); + expect(find.text('May 2021'), findsOneWidget); + + await tester.drag(find.byType(CellContent).first, const Offset(500, 0)); + await tester.pumpAndSettle(); + expect(find.text('May 2021'), findsOneWidget); + }); + + testWidgets('ending scroll boundary works correctly', (tester) async { + final focusedDay = DateTime.utc(2021, 8, 15); + + await tester.pumpWidget(createTableCalendar(focusedDay: focusedDay)); + + expect(find.byType(TableCalendar), findsOneWidget); + expect(find.text('August 2021'), findsOneWidget); + + await tester.drag(find.byType(CellContent).first, const Offset(-500, 0)); + await tester.pumpAndSettle(); + expect(find.text('September 2021'), findsOneWidget); + + await tester.drag(find.byType(CellContent).first, const Offset(-500, 0)); + await tester.pumpAndSettle(); + expect(find.text('September 2021'), findsOneWidget); + }); + }); + + group('onFormatChanged callback returns correct values:', () { + testWidgets('when initial format is month', (tester) async { + CalendarFormat calendarFormat = CalendarFormat.month; + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: today, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + calendarFormat: calendarFormat, + onFormatChanged: (format) { + calendarFormat = format; + }, + ), + ), + ); + + await tester.drag(find.byType(CellContent).first, const Offset(0, -500)); + await tester.pumpAndSettle(); + expect(calendarFormat, CalendarFormat.twoWeeks); + + await tester.drag(find.byType(CellContent).first, const Offset(0, 500)); + await tester.pumpAndSettle(); + expect(calendarFormat, CalendarFormat.month); + }); + + testWidgets('when initial format is two weeks', (tester) async { + CalendarFormat calendarFormat = CalendarFormat.twoWeeks; + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: today, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + calendarFormat: calendarFormat, + onFormatChanged: (format) { + calendarFormat = format; + }, + ), + ), + ); + + await tester.drag(find.byType(CellContent).first, const Offset(0, -500)); + await tester.pumpAndSettle(); + expect(calendarFormat, CalendarFormat.week); + + await tester.drag(find.byType(CellContent).first, const Offset(0, 500)); + await tester.pumpAndSettle(); + expect(calendarFormat, CalendarFormat.month); + }); + + testWidgets('when initial format is week', (tester) async { + CalendarFormat calendarFormat = CalendarFormat.week; + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: today, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + calendarFormat: calendarFormat, + onFormatChanged: (format) { + calendarFormat = format; + }, + ), + ), + ); + + await tester.drag(find.byType(CellContent).first, const Offset(0, -500)); + await tester.pumpAndSettle(); + expect(calendarFormat, CalendarFormat.week); + + await tester.drag(find.byType(CellContent).first, const Offset(0, 500)); + await tester.pumpAndSettle(); + expect(calendarFormat, CalendarFormat.twoWeeks); + }); + }); + + group('onDaySelected callback test:', () { + testWidgets( + 'selects correct day when tapped', + (tester) async { + DateTime? selectedDay; + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + onDaySelected: (selected, focused) { + selectedDay = selected; + }, + ), + ), + ); + + expect(selectedDay, isNull); + + final tappedDay = DateTime.utc(2021, 7, 18); + final tappedDayKey = cellContentKey(tappedDay); + + await tester.tap(find.byKey(tappedDayKey)); + await tester.pumpAndSettle(); + expect(selectedDay, tappedDay); + }, + ); + + testWidgets( + 'focuses correct day when tapped', + (tester) async { + DateTime? focusedDay; + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + onDaySelected: (selected, focused) { + focusedDay = focused; + }, + ), + ), + ); + + expect(focusedDay, isNull); + + final tappedDay = DateTime.utc(2021, 7, 18); + final tappedDayKey = cellContentKey(tappedDay); + + await tester.tap(find.byKey(tappedDayKey)); + await tester.pumpAndSettle(); + expect(focusedDay, tappedDay); + }, + ); + + testWidgets( + 'properly selects and focuses on outside cell tap - previous month (when in month format)', + (tester) async { + DateTime? selectedDay; + DateTime? focusedDay; + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + onDaySelected: (selected, focused) { + selectedDay = selected; + focusedDay = focused; + }, + ), + ), + ); + + expect(selectedDay, isNull); + expect(focusedDay, isNull); + + final tappedDay = DateTime.utc(2021, 6, 30); + final tappedDayKey = cellContentKey(tappedDay); + + final expectedFocusedDay = DateTime.utc(2021, 7); + + await tester.tap(find.byKey(tappedDayKey)); + await tester.pumpAndSettle(); + expect(selectedDay, tappedDay); + expect(focusedDay, expectedFocusedDay); + }, + ); + + testWidgets( + 'properly selects and focuses on outside cell tap - next month (when in month format)', + (tester) async { + DateTime? selectedDay; + DateTime? focusedDay; + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: DateTime.utc(2021, 8, 16), + firstDay: firstDay, + lastDay: lastDay, + currentDay: DateTime.utc(2021, 8, 16), + onDaySelected: (selected, focused) { + selectedDay = selected; + focusedDay = focused; + }, + ), + ), + ); + + expect(selectedDay, isNull); + expect(focusedDay, isNull); + + final tappedDay = DateTime.utc(2021, 9); + final tappedDayKey = cellContentKey(tappedDay); + + final expectedFocusedDay = DateTime.utc(2021, 8, 31); + + await tester.tap(find.byKey(tappedDayKey)); + await tester.pumpAndSettle(); + expect(selectedDay, tappedDay); + expect(focusedDay, expectedFocusedDay); + }, + ); + }); + + group('onDayLongPressed callback test:', () { + testWidgets( + 'selects correct day when long pressed', + (tester) async { + DateTime? selectedDay; + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + onDayLongPressed: (selected, focused) { + selectedDay = selected; + }, + ), + ), + ); + + expect(selectedDay, isNull); + + final longPressedDay = DateTime.utc(2021, 7, 18); + final longPressedDayKey = cellContentKey(longPressedDay); + + await tester.longPress(find.byKey(longPressedDayKey)); + await tester.pumpAndSettle(); + expect(selectedDay, longPressedDay); + }, + ); + + testWidgets( + 'focuses correct day when long pressed', + (tester) async { + DateTime? focusedDay; + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + onDayLongPressed: (selected, focused) { + focusedDay = focused; + }, + ), + ), + ); + + expect(focusedDay, isNull); + + final longPressedDay = DateTime.utc(2021, 7, 18); + final longPressedDayKey = cellContentKey(longPressedDay); + + await tester.longPress(find.byKey(longPressedDayKey)); + await tester.pumpAndSettle(); + expect(focusedDay, longPressedDay); + }, + ); + + testWidgets( + 'properly selects and focuses on outside cell long press - previous month (when in month format)', + (tester) async { + DateTime? selectedDay; + DateTime? focusedDay; + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + onDayLongPressed: (selected, focused) { + selectedDay = selected; + focusedDay = focused; + }, + ), + ), + ); + + expect(selectedDay, isNull); + expect(focusedDay, isNull); + + final longPressedDay = DateTime.utc(2021, 6, 30); + final longPressedDayKey = cellContentKey(longPressedDay); + + final expectedFocusedDay = DateTime.utc(2021, 7); + + await tester.longPress(find.byKey(longPressedDayKey)); + await tester.pumpAndSettle(); + expect(selectedDay, longPressedDay); + expect(focusedDay, expectedFocusedDay); + }, + ); + + testWidgets( + 'properly selects and focuses on outside cell long press - next month (when in month format)', + (tester) async { + DateTime? selectedDay; + DateTime? focusedDay; + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: DateTime.utc(2021, 8, 16), + firstDay: firstDay, + lastDay: lastDay, + currentDay: DateTime.utc(2021, 8, 16), + onDayLongPressed: (selected, focused) { + selectedDay = selected; + focusedDay = focused; + }, + ), + ), + ); + + expect(selectedDay, isNull); + expect(focusedDay, isNull); + + final longPressedDay = DateTime.utc(2021, 9); + final longPressedDayKey = cellContentKey(longPressedDay); + + final expectedFocusedDay = DateTime.utc(2021, 8, 31); + + await tester.longPress(find.byKey(longPressedDayKey)); + await tester.pumpAndSettle(); + expect(selectedDay, longPressedDay); + expect(focusedDay, expectedFocusedDay); + }, + ); + }); + + group('onRangeSelection callback test:', () { + testWidgets( + 'proper values are returned when second tapped day is after the first one', + (tester) async { + DateTime? rangeStart; + DateTime? rangeEnd; + DateTime? focusedDay; + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + rangeSelectionMode: RangeSelectionMode.enforced, + onRangeSelected: (start, end, focused) { + rangeStart = start; + rangeEnd = end; + focusedDay = focused; + }, + ), + ), + ); + + expect(rangeStart, isNull); + expect(rangeEnd, isNull); + expect(focusedDay, isNull); + + final firstTappedDay = DateTime.utc(2021, 7, 8); + final secondTappedDay = DateTime.utc(2021, 7, 21); + + final firstTappedDayKey = cellContentKey(firstTappedDay); + final secondTappedDayKey = cellContentKey(secondTappedDay); + + final expectedFocusedDay = secondTappedDay; + + await tester.tap(find.byKey(firstTappedDayKey)); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(secondTappedDayKey)); + await tester.pumpAndSettle(); + expect(rangeStart, firstTappedDay); + expect(rangeEnd, secondTappedDay); + expect(focusedDay, expectedFocusedDay); + }, + ); + + testWidgets( + 'proper values are returned when second tapped day is before the first one', + (tester) async { + DateTime? rangeStart; + DateTime? rangeEnd; + DateTime? focusedDay; + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + rangeSelectionMode: RangeSelectionMode.enforced, + onRangeSelected: (start, end, focused) { + rangeStart = start; + rangeEnd = end; + focusedDay = focused; + }, + ), + ), + ); + + expect(rangeStart, isNull); + expect(rangeEnd, isNull); + expect(focusedDay, isNull); + + final firstTappedDay = DateTime.utc(2021, 7, 14); + final secondTappedDay = DateTime.utc(2021, 7, 7); + + final firstTappedDayKey = cellContentKey(firstTappedDay); + final secondTappedDayKey = cellContentKey(secondTappedDay); + + final expectedFocusedDay = secondTappedDay; + + await tester.tap(find.byKey(firstTappedDayKey)); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(secondTappedDayKey)); + await tester.pumpAndSettle(); + expect(rangeStart, secondTappedDay); + expect(rangeEnd, firstTappedDay); + expect(focusedDay, expectedFocusedDay); + }, + ); + + testWidgets( + 'long press toggles rangeSelectionMode when onDayLongPress callback is null - initial mode is toggledOff', + (tester) async { + DateTime? rangeStart; + DateTime? rangeEnd; + DateTime? focusedDay; + DateTime? selectedDay; + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + onDaySelected: (selected, focused) { + selectedDay = selected; + focusedDay = focused; + }, + onRangeSelected: (start, end, focused) { + rangeStart = start; + rangeEnd = end; + focusedDay = focused; + }, + ), + ), + ); + + expect(rangeStart, isNull); + expect(rangeEnd, isNull); + expect(focusedDay, isNull); + expect(selectedDay, isNull); + + final firstTappedDay = DateTime.utc(2021, 7, 8); + final secondTappedDay = DateTime.utc(2021, 7, 21); + + final firstTappedDayKey = cellContentKey(firstTappedDay); + final secondTappedDayKey = cellContentKey(secondTappedDay); + + final expectedFocusedDay = secondTappedDay; + + await tester.longPress(find.byKey(firstTappedDayKey)); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(firstTappedDayKey)); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(secondTappedDayKey)); + await tester.pumpAndSettle(); + expect(rangeStart, firstTappedDay); + expect(rangeEnd, secondTappedDay); + expect(focusedDay, expectedFocusedDay); + expect(selectedDay, isNull); + }, + ); + + testWidgets( + 'long press toggles rangeSelectionMode when onDayLongPress callback is null - initial mode is toggledOn', + (tester) async { + DateTime? rangeStart; + DateTime? rangeEnd; + DateTime? focusedDay; + DateTime? selectedDay; + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + rangeSelectionMode: RangeSelectionMode.toggledOn, + onDaySelected: (selected, focused) { + selectedDay = selected; + focusedDay = focused; + }, + onRangeSelected: (start, end, focused) { + rangeStart = start; + rangeEnd = end; + focusedDay = focused; + }, + ), + ), + ); + + expect(rangeStart, isNull); + expect(rangeEnd, isNull); + expect(focusedDay, isNull); + expect(selectedDay, isNull); + + final firstTappedDay = DateTime.utc(2021, 7, 8); + final secondTappedDay = DateTime.utc(2021, 7, 21); + + final firstTappedDayKey = cellContentKey(firstTappedDay); + final secondTappedDayKey = cellContentKey(secondTappedDay); + + final expectedFocusedDay = secondTappedDay; + + await tester.longPress(find.byKey(firstTappedDayKey)); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(firstTappedDayKey)); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(secondTappedDayKey)); + await tester.pumpAndSettle(); + expect(rangeStart, isNull); + expect(rangeEnd, isNull); + expect(focusedDay, expectedFocusedDay); + expect(selectedDay, secondTappedDay); + }, + ); + + testWidgets( + 'rangeSelectionMode.enforced disables onDaySelected callback', + (tester) async { + DateTime? rangeStart; + DateTime? rangeEnd; + DateTime? focusedDay; + DateTime? selectedDay; + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + rangeSelectionMode: RangeSelectionMode.enforced, + onDaySelected: (selected, focused) { + selectedDay = selected; + focusedDay = focused; + }, + onRangeSelected: (start, end, focused) { + rangeStart = start; + rangeEnd = end; + focusedDay = focused; + }, + ), + ), + ); + + expect(rangeStart, isNull); + expect(rangeEnd, isNull); + expect(focusedDay, isNull); + expect(selectedDay, isNull); + + final firstTappedDay = DateTime.utc(2021, 7, 8); + final secondTappedDay = DateTime.utc(2021, 7, 21); + + final firstTappedDayKey = cellContentKey(firstTappedDay); + final secondTappedDayKey = cellContentKey(secondTappedDay); + + final expectedFocusedDay = secondTappedDay; + + await tester.longPress(find.byKey(firstTappedDayKey)); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(firstTappedDayKey)); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(secondTappedDayKey)); + await tester.pumpAndSettle(); + expect(rangeStart, firstTappedDay); + expect(rangeEnd, secondTappedDay); + expect(focusedDay, expectedFocusedDay); + expect(selectedDay, isNull); + }, + ); + + testWidgets( + 'rangeSelectionMode.disabled enforces onDaySelected callback', + (tester) async { + DateTime? rangeStart; + DateTime? rangeEnd; + DateTime? focusedDay; + DateTime? selectedDay; + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + rangeSelectionMode: RangeSelectionMode.disabled, + onDaySelected: (selected, focused) { + selectedDay = selected; + focusedDay = focused; + }, + onRangeSelected: (start, end, focused) { + rangeStart = start; + rangeEnd = end; + focusedDay = focused; + }, + ), + ), + ); + + expect(rangeStart, isNull); + expect(rangeEnd, isNull); + expect(focusedDay, isNull); + expect(selectedDay, isNull); + + final firstTappedDay = DateTime.utc(2021, 7, 8); + final secondTappedDay = DateTime.utc(2021, 7, 21); + + final firstTappedDayKey = cellContentKey(firstTappedDay); + final secondTappedDayKey = cellContentKey(secondTappedDay); + + final expectedFocusedDay = secondTappedDay; + + await tester.longPress(find.byKey(firstTappedDayKey)); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(firstTappedDayKey)); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(secondTappedDayKey)); + await tester.pumpAndSettle(); + expect(rangeStart, isNull); + expect(rangeEnd, isNull); + expect(focusedDay, expectedFocusedDay); + expect(selectedDay, secondTappedDay); + }, + ); + }); + + group('Range selection test:', () { + testWidgets( + 'range selection has correct start and end point', + (tester) async { + final rangeStart = DateTime.utc(2021, 7, 8); + final rangeEnd = DateTime.utc(2021, 7, 21); + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + rangeStartDay: rangeStart, + rangeEndDay: rangeEnd, + ), + ), + ); + + final rangeStartKey = cellContentKey(rangeStart); + final rangeStartCellContent = + tester.widget(find.byKey(rangeStartKey)) as CellContent; + + expect(rangeStartCellContent.isRangeStart, true); + expect(rangeStartCellContent.isRangeEnd, false); + expect(rangeStartCellContent.isWithinRange, true); + + final rangeEndKey = cellContentKey(rangeEnd); + final rangeEndCellContent = + tester.widget(find.byKey(rangeEndKey)) as CellContent; + + expect(rangeEndCellContent.isRangeStart, false); + expect(rangeEndCellContent.isRangeEnd, true); + expect(rangeEndCellContent.isWithinRange, true); + }, + ); + + testWidgets( + 'days within range selection are marked as inWithinRange', + (tester) async { + final rangeStart = DateTime.utc(2021, 7, 8); + final rangeEnd = DateTime.utc(2021, 7, 13); + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + rangeStartDay: rangeStart, + rangeEndDay: rangeEnd, + ), + ), + ); + + final dayCount = rangeEnd.difference(rangeStart).inDays - 1; + expect(dayCount, 4); + + for (int i = 1; i <= dayCount; i++) { + final testDay = rangeStart.add(Duration(days: i)); + + expect(testDay.isAfter(rangeStart), true); + expect(testDay.isBefore(rangeEnd), true); + + final testDayKey = cellContentKey(testDay); + final testDayCellContent = + tester.widget(find.byKey(testDayKey)) as CellContent; + + expect(testDayCellContent.isWithinRange, true); + } + }, + ); + + testWidgets( + 'days outside range selection are not marked as inWithinRange', + (tester) async { + final rangeStart = DateTime.utc(2021, 7, 8); + final rangeEnd = DateTime.utc(2021, 7, 13); + + await tester.pumpWidget( + setupTestWidget( + TableCalendar( + focusedDay: initialFocusedDay, + firstDay: firstDay, + lastDay: lastDay, + currentDay: today, + rangeStartDay: rangeStart, + rangeEndDay: rangeEnd, + ), + ), + ); + + final oobStart = rangeStart.subtract(const Duration(days: 1)); + final oobEnd = rangeEnd.add(const Duration(days: 1)); + + final oobStartKey = cellContentKey(oobStart); + final oobStartCellContent = + tester.widget(find.byKey(oobStartKey)) as CellContent; + + final oobEndKey = cellContentKey(oobEnd); + final oobEndCellContent = + tester.widget(find.byKey(oobEndKey)) as CellContent; + + expect(oobStartCellContent.isWithinRange, false); + expect(oobEndCellContent.isWithinRange, false); + }, + ); + }); +} diff --git a/test/utils_test.dart b/test/utils_test.dart new file mode 100644 index 00000000..472c0afb --- /dev/null +++ b/test/utils_test.dart @@ -0,0 +1,69 @@ +// Copyright 2019 Aleksander Woźniak +// SPDX-License-Identifier: Apache-2.0 + +import 'package:flutter_test/flutter_test.dart'; +import 'package:table_calendar/src/shared/utils.dart'; + +void main() { + group('isSameDay() tests:', () { + test('Same day, different time', () { + final dateA = DateTime(2020, 5, 10, 4, 32, 16); + final dateB = DateTime(2020, 5, 10, 8, 21, 44); + + expect(isSameDay(dateA, dateB), true); + }); + + test('Different day, same time', () { + final dateA = DateTime(2020, 5, 10, 4, 32, 16); + final dateB = DateTime(2020, 5, 11, 4, 32, 16); + + expect(isSameDay(dateA, dateB), false); + }); + + test('UTC and local time zone', () { + final dateA = DateTime.utc(2020, 5, 10); + final dateB = DateTime(2020, 5, 10); + + expect(isSameDay(dateA, dateB), true); + }); + }); + + group('normalizeDate() tests:', () { + test('Local time zone gets converted to UTC', () { + final dateA = DateTime(2020, 5, 10, 4, 32, 16); + final dateB = normalizeDate(dateA); + + expect(dateB.isUtc, true); + }); + + test('Date is unchanged', () { + final dateA = DateTime(2020, 5, 10, 4, 32, 16); + final dateB = normalizeDate(dateA); + + expect(dateB.year, 2020); + expect(dateB.month, 5); + expect(dateB.day, 10); + }); + + test('Time gets trimmed', () { + final dateA = DateTime(2020, 5, 10, 4, 32, 16); + final dateB = normalizeDate(dateA); + + expect(dateB.hour, 0); + expect(dateB.minute, 0); + expect(dateB.second, 0); + expect(dateB.millisecond, 0); + expect(dateB.microsecond, 0); + }); + }); + + group('getWeekdayNumber() tests:', () { + test('Monday returns number 1', () { + expect(getWeekdayNumber(StartingDayOfWeek.monday), 1); + }); + + test('Sunday returns number 7', () { + expect(getWeekdayNumber(StartingDayOfWeek.sunday), 7); + }); + }); +}