diff --git a/README.md b/README.md index ae03bcf..2a38d3e 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ To add bottom picker to your project add this line to your pubspec.yaml file ```yaml dependencies: - bottom_picker: ^3.2.1 + bottom_picker: ^3.2.1 ``` ## Parameters @@ -323,8 +323,8 @@ dependencies: ```dart BottomPicker( - items: items, - title: Text("Choose your country", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15)), + items: items, + title: Text("Choose your country", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15)), ).show(context); ``` @@ -334,7 +334,7 @@ BottomPicker( ```dart BottomPicker.date( - pickerTitle: Text( + pickerTitle: Text( 'Set your Birthday', style: TextStyle( fontWeight: FontWeight.bold, @@ -430,7 +430,7 @@ BottomPicker.timer( ```dart BottomPicker.dateTime( - pickerTitle: Text( + pickerTitle: Text( 'Set the event exact time and date', style: TextStyle( fontWeight: FontWeight.bold, @@ -476,7 +476,7 @@ BottomPicker.monthYear( ```dart BottomPicker( - items: [ + items: [ Center( child: Text('Leonardo DiCaprio'), ), @@ -517,7 +517,7 @@ BottomPicker( ```dart BottomPicker.range( - pickerTitle: Text( + pickerTitle: Text( 'Set date range', style: TextStyle( fontWeight: FontWeight.bold, @@ -555,7 +555,7 @@ BottomPicker.range( ```dart BottomPicker.rangeTime( - pickerTitle: Text( + pickerTitle: Text( 'Set Time range', style: TextStyle( fontWeight: FontWeight.bold, @@ -598,11 +598,11 @@ We warmly welcome contributions to the `bottom_picker` package! Your help in mak - **Found a bug?** Please [open a new issue](https://github.com/koukibadr/Bottom-Picker/issues/new?assignees=&labels=bug&template=bug_report.md&title=) with clear steps to reproduce the problem. The more detail you provide, the easier it will be to fix. - **Have a great idea for a new feature?** We'd love to hear it! Please [open a new issue](https://github.com/koukibadr/Bottom-Picker/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=) to discuss your suggestion. Explain the use case and how it would benefit users. - **Want to get your hands dirty and contribute code?** Fantastic! Here's how: - 1. Fork the [repository](https://github.com/koukibadr/Bottom-Picker). - 2. Create a new branch for your feature or bug fix. - 3. Make your changes, ensuring you follow the project's coding style and conventions. - 4. Write clear and concise commit messages. - 5. Submit a [pull request](https://github.com/koukibadr/Bottom-Picker/pulls) with a detailed description of your changes and why they should be merged. + 1. Fork the [repository](https://github.com/koukibadr/Bottom-Picker). + 2. Create a new branch for your feature or bug fix. + 3. Make your changes, ensuring you follow the project's coding style and conventions. + 4. Write clear and concise commit messages. + 5. Submit a [pull request](https://github.com/koukibadr/Bottom-Picker/pulls) with a detailed description of your changes and why they should be merged. We'll review your contributions and provide feedback as soon as possible. Thank you for your interest in improving `bottom_picker`! diff --git a/example/lib/main.dart b/example/lib/main.dart index 197dd73..8cccd47 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,7 @@ // ignore_for_file: avoid_print import 'package:bottom_picker/bottom_picker.dart'; +import 'package:bottom_picker/cupertino/cupertino_date_picker.dart'; import 'package:bottom_picker/resources/arrays.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -169,11 +170,38 @@ class ExampleApp extends StatelessWidget { width: buttonWidth, child: ElevatedButton( onPressed: () { - _openDateTimePicker(context); + _openDateTimePicker( + context, + CupertinoDatePickerCalendarType.fullWeek, + ); }, child: Text('Date and Time Picker', textAlign: TextAlign.center), ), ), + SizedBox( + width: buttonWidth, + child: ElevatedButton( + onPressed: () { + _openDateTimePicker( + context, + CupertinoDatePickerCalendarType.workDays, + ); + }, + child: Text('Workday Picker', textAlign: TextAlign.center), + ), + ), + SizedBox( + width: buttonWidth, + child: ElevatedButton( + onPressed: () { + _openDateTimePicker( + context, + CupertinoDatePickerCalendarType.weekend, + ); + }, + child: Text('Weekend Day Picker', textAlign: TextAlign.center), + ), + ), ], ), ); @@ -558,7 +586,8 @@ class ExampleApp extends StatelessWidget { ).show(context); } - void _openDateTimePicker(BuildContext context) { + void _openDateTimePicker( + BuildContext context, CupertinoDatePickerCalendarType calendarType,) { BottomPicker.dateTime( minuteInterval: 2, headerBuilder: (context) { @@ -598,6 +627,7 @@ class ExampleApp extends StatelessWidget { Color(0xfffdcbf1), Color(0xffe6dee9), ], + calendarType: calendarType, ).show(context); } } diff --git a/lib/bottom_picker.dart b/lib/bottom_picker.dart index 00edd75..a7793e5 100644 --- a/lib/bottom_picker.dart +++ b/lib/bottom_picker.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:bottom_picker/cupertino/cupertino_date_picker.dart'; import 'package:bottom_picker/resources/arrays.dart'; import 'package:bottom_picker/resources/context_extension.dart'; import 'package:bottom_picker/resources/time.dart'; @@ -38,7 +39,8 @@ class BottomPicker extends StatefulWidget { @Deprecated('should use headerBuilder instead') this.pickerDescription, required this.items, @Deprecated('should use headerBuilder instead') this.titleAlignment, - @Deprecated('should use headerBuilder instead') this.titlePadding = const EdgeInsets.all(0), + @Deprecated('should use headerBuilder instead') + this.titlePadding = const EdgeInsets.all(0), this.dismissable = false, this.onChange, this.onSubmit, @@ -71,6 +73,7 @@ class BottomPicker extends StatefulWidget { @Deprecated('should use headerBuilder instead') this.closeWidget, this.closeOnSubmit = true, this.headerBuilder, + this.calendarType = CupertinoDatePickerCalendarType.fullWeek, }) { dateOrder = null; onRangeDateSubmitPressed = null; @@ -86,7 +89,8 @@ class BottomPicker extends StatefulWidget { super.key, @Deprecated('should use headerBuilder instead') this.pickerTitle, @Deprecated('should use headerBuilder instead') this.pickerDescription, - @Deprecated('should use headerBuilder instead') this.titlePadding = const EdgeInsets.all(0), + @Deprecated('should use headerBuilder instead') + this.titlePadding = const EdgeInsets.all(0), @Deprecated('should use headerBuilder instead') this.titleAlignment, this.dismissable = false, this.onChange, @@ -122,6 +126,7 @@ class BottomPicker extends StatefulWidget { @Deprecated('should use headerBuilder instead') this.closeWidget, this.closeOnSubmit = true, this.headerBuilder, + this.calendarType = CupertinoDatePickerCalendarType.fullWeek, }) { datePickerMode = CupertinoDatePickerMode.date; bottomPickerType = BottomPickerType.dateTime; @@ -134,7 +139,8 @@ class BottomPicker extends StatefulWidget { super.key, @Deprecated('should use headerBuilder instead') this.pickerTitle, @Deprecated('should use headerBuilder instead') this.pickerDescription, - @Deprecated('should use headerBuilder instead') this.titlePadding = const EdgeInsets.all(0), + @Deprecated('should use headerBuilder instead') + this.titlePadding = const EdgeInsets.all(0), @Deprecated('should use headerBuilder instead') this.titleAlignment, this.dismissable = false, this.onChange, @@ -169,6 +175,7 @@ class BottomPicker extends StatefulWidget { @Deprecated('should use headerBuilder instead') this.closeWidget, this.closeOnSubmit = true, this.headerBuilder, + this.calendarType = CupertinoDatePickerCalendarType.fullWeek, }) { datePickerMode = CupertinoDatePickerMode.monthYear; bottomPickerType = BottomPickerType.dateTime; @@ -182,7 +189,8 @@ class BottomPicker extends StatefulWidget { super.key, @Deprecated('should use headerBuilder instead') this.pickerTitle, @Deprecated('should use headerBuilder instead') this.pickerDescription, - @Deprecated('should use headerBuilder instead') this.titlePadding = const EdgeInsets.all(0), + @Deprecated('should use headerBuilder instead') + this.titlePadding = const EdgeInsets.all(0), @Deprecated('should use headerBuilder instead') this.titleAlignment, this.dismissable = false, this.onChange, @@ -221,6 +229,7 @@ class BottomPicker extends StatefulWidget { @Deprecated('should use headerBuilder instead') this.closeWidget, this.closeOnSubmit = true, this.headerBuilder, + this.calendarType = CupertinoDatePickerCalendarType.fullWeek, }) { datePickerMode = CupertinoDatePickerMode.dateAndTime; bottomPickerType = BottomPickerType.dateTime; @@ -235,7 +244,8 @@ class BottomPicker extends StatefulWidget { required this.initialTime, this.maxTime, this.minTime, - @Deprecated('should use headerBuilder instead') this.titlePadding = const EdgeInsets.all(0), + @Deprecated('should use headerBuilder instead') + this.titlePadding = const EdgeInsets.all(0), @Deprecated('should use headerBuilder instead') this.titleAlignment, this.dismissable = false, this.onChange, @@ -270,6 +280,7 @@ class BottomPicker extends StatefulWidget { @Deprecated('should use headerBuilder instead') this.closeWidget, this.closeOnSubmit = true, this.headerBuilder, + this.calendarType = CupertinoDatePickerCalendarType.fullWeek, }) { datePickerMode = CupertinoDatePickerMode.time; bottomPickerType = BottomPickerType.time; @@ -286,7 +297,8 @@ class BottomPicker extends StatefulWidget { this.timerSecondsInterval = 1, @Deprecated('should use headerBuilder instead') this.pickerDescription, this.initialTimerDuration, - @Deprecated('should use headerBuilder instead') this.titlePadding = const EdgeInsets.all(0), + @Deprecated('should use headerBuilder instead') + this.titlePadding = const EdgeInsets.all(0), @Deprecated('should use headerBuilder instead') this.titleAlignment, this.dismissable = false, this.onChange, @@ -319,6 +331,7 @@ class BottomPicker extends StatefulWidget { @Deprecated('should use headerBuilder instead') this.closeWidget, this.closeOnSubmit = true, this.headerBuilder, + this.calendarType = CupertinoDatePickerCalendarType.fullWeek, }) { dateOrder = null; onRangeDateSubmitPressed = null; @@ -334,7 +347,8 @@ class BottomPicker extends StatefulWidget { @Deprecated('should use headerBuilder instead') this.pickerDescription, required this.onRangeDateSubmitPressed, this.onRangePickerDismissed, - @Deprecated('should use headerBuilder instead') this.titlePadding = const EdgeInsets.all(0), + @Deprecated('should use headerBuilder instead') + this.titlePadding = const EdgeInsets.all(0), @Deprecated('should use headerBuilder instead') this.titleAlignment, this.dismissable = false, @Deprecated('should use headerBuilder instead') this.onCloseButtonPressed, @@ -369,6 +383,7 @@ class BottomPicker extends StatefulWidget { @Deprecated('should use headerBuilder instead') this.closeWidget, this.closeOnSubmit = true, this.headerBuilder, + this.calendarType = CupertinoDatePickerCalendarType.fullWeek, }) { datePickerMode = CupertinoDatePickerMode.date; bottomPickerType = BottomPickerType.rangeDate; @@ -399,7 +414,8 @@ class BottomPicker extends StatefulWidget { required this.onRangeTimeSubmitPressed, this.onRangePickerDismissed, this.use24hFormat = true, - @Deprecated('should use headerBuilder instead') this.titlePadding = const EdgeInsets.all(0), + @Deprecated('should use headerBuilder instead') + this.titlePadding = const EdgeInsets.all(0), @Deprecated('should use headerBuilder instead') this.titleAlignment, this.dismissable = false, @Deprecated('should use headerBuilder instead') this.onCloseButtonPressed, @@ -435,6 +451,7 @@ class BottomPicker extends StatefulWidget { @Deprecated('should use headerBuilder instead') this.closeWidget, this.closeOnSubmit = true, this.headerBuilder, + this.calendarType = CupertinoDatePickerCalendarType.fullWeek, }) { datePickerMode = CupertinoDatePickerMode.time; bottomPickerType = BottomPickerType.rangeTime; @@ -726,6 +743,9 @@ class BottomPicker extends StatefulWidget { /// By default closeOnSubmit = true. final bool closeOnSubmit; + /// The datepicker calendar type + final CupertinoDatePickerCalendarType calendarType; + ///display the bottom picker popup ///[context] the app context to display the popup void show(BuildContext context) { @@ -839,7 +859,9 @@ class BottomPickerState extends State { mainAxisAlignment: MainAxisAlignment.start, children: [ Padding( - padding: widget.headerBuilder == null ? widget.titlePadding : EdgeInsetsGeometry.zero, + padding: widget.headerBuilder == null + ? widget.titlePadding + : EdgeInsetsGeometry.zero, child: Row( children: [ if (widget.headerBuilder != null) @@ -911,6 +933,7 @@ class BottomPickerState extends State { textStyle: widget.pickerTextStyle, itemExtent: widget.itemExtent, showTimeSeparator: widget.showTimeSeparator, + calendarType: widget.calendarType, ) : widget.bottomPickerType == BottomPickerType.dateTime ? DatePicker( @@ -928,6 +951,7 @@ class BottomPickerState extends State { textStyle: widget.pickerTextStyle, itemExtent: widget.itemExtent, showTimeSeparator: widget.showTimeSeparator, + calendarType: widget.calendarType, ) : widget.bottomPickerType == BottomPickerType.rangeTime diff --git a/lib/cupertino/cupertino_date_picker.dart b/lib/cupertino/cupertino_date_picker.dart index 7c31d01..f7fc7d5 100644 --- a/lib/cupertino/cupertino_date_picker.dart +++ b/lib/cupertino/cupertino_date_picker.dart @@ -187,6 +187,12 @@ enum _PickerColumnType { twoPoints, } +enum CupertinoDatePickerCalendarType { + fullWeek, + workDays, + weekend, +} + /// A date picker widget in iOS style. /// /// There are several modes of the date picker listed in [CupertinoDatePickerMode]. @@ -283,6 +289,7 @@ class CupertinoDatePickerWidget extends StatefulWidget { this.itemExtent = _kItemExtent, this.selectionOverlayBuilder, this.showTimeSeparator = false, + this.calendarType = CupertinoDatePickerCalendarType.fullWeek, }) : initialDateTime = initialDateTime ?? DateTime.now(), assert( itemExtent > 0, @@ -423,6 +430,8 @@ class CupertinoDatePickerWidget extends StatefulWidget { /// Defaults to a value that matches the default iOS date picker wheel. final double itemExtent; + final CupertinoDatePickerCalendarType calendarType; + /// A function that returns a widget that is overlaid on the picker /// to highlight the currently selected entry. /// @@ -707,6 +716,16 @@ class _CupertinoDatePickerDateTimeState super.initState(); initialDateTime = widget.initialDateTime; + switch (widget.calendarType) { + case CupertinoDatePickerCalendarType.workDays: + _handleWorkDaysInitialDateTime(); + break; + case CupertinoDatePickerCalendarType.weekend: + _handleWeekendInitialDateTime(); + break; + default: + } + // Initially each of the "physical" regions is mapped to the meridiem region // with the same number, e.g., the first 12 items are mapped to the first 12 // hours of a day. Such mapping is flipped when the meridiem picker is scrolled @@ -725,6 +744,37 @@ class _CupertinoDatePickerDateTimeState PaintingBinding.instance.systemFonts.addListener(_handleSystemFontsChange); } + void _handleWorkDaysInitialDateTime() { + int daysToBeAdded = 0; + if (widget.calendarType == CupertinoDatePickerCalendarType.workDays && + initialDateTime.weekday == DateTime.saturday) { + daysToBeAdded = 2; + } else if (widget.calendarType == + CupertinoDatePickerCalendarType.workDays && + initialDateTime.weekday == DateTime.sunday) { + daysToBeAdded = 1; + } + initialDateTime = DateTime( + initialDateTime.year, + initialDateTime.month, + initialDateTime.day + daysToBeAdded, + ); + } + + void _handleWeekendInitialDateTime() { + int daysToBeAdded = 0; + if (widget.calendarType == CupertinoDatePickerCalendarType.weekend && + (initialDateTime.weekday != DateTime.saturday && + initialDateTime.weekday != DateTime.saturday)) { + daysToBeAdded = DateTime.saturday - initialDateTime.weekday; + } + initialDateTime = DateTime( + initialDateTime.year, + initialDateTime.month, + initialDateTime.day + daysToBeAdded, + ); + } + void _handleSystemFontsChange() { setState(() { // System fonts change might cause the text layout width to change. @@ -808,9 +858,8 @@ class _CupertinoDatePickerDateTimeState void _onSelectedItemChange(int index) { final DateTime selected = selectedDateTime; - final bool isDateInvalid = - (widget.minimumDate?.isAfter(selected) ?? false) || - (widget.maximumDate?.isBefore(selected) ?? false); + bool isDateInvalid = (widget.minimumDate?.isAfter(selected) ?? false) || + (widget.maximumDate?.isBefore(selected) ?? false); if (isDateInvalid) { return; @@ -848,6 +897,27 @@ class _CupertinoDatePickerDateTimeState _onSelectedItemChange(index); }, itemBuilder: (BuildContext context, int index) { + DateTime tempDateTime = DateTime( + initialDateTime.year, + initialDateTime.month, + initialDateTime.day + index, + ); + + bool isValidDate = true; + + if (widget.calendarType == CupertinoDatePickerCalendarType.workDays) { + if (tempDateTime.weekday == DateTime.saturday || + tempDateTime.weekday == DateTime.sunday) { + isValidDate = false; + } + } else if (widget.calendarType == + CupertinoDatePickerCalendarType.weekend) { + if (tempDateTime.weekday != DateTime.saturday && + tempDateTime.weekday != DateTime.sunday) { + isValidDate = false; + } + } + final DateTime rangeStart = DateTime( initialDateTime.year, initialDateTime.month, @@ -877,7 +947,10 @@ class _CupertinoDatePickerDateTimeState return itemPositioningBuilder( context, - Text(dateText, style: _themeTextStyle(context)), + Text( + dateText, + style: _themeTextStyle(context, isValid: isValidDate), + ), ); }, selectionOverlay: selectionOverlay, @@ -1124,6 +1197,42 @@ class _CupertinoDatePickerDateTimeState final bool minCheck = widget.minimumDate?.isAfter(selectedDate) ?? false; final bool maxCheck = widget.maximumDate?.isBefore(selectedDate) ?? false; + if (widget.calendarType == CupertinoDatePickerCalendarType.workDays) { + int daysThreshold = 0; + if (selectedDate.weekday == DateTime.saturday) { + daysThreshold = -1; + } else if (selectedDate.weekday == DateTime.sunday) { + daysThreshold = -2; + } + + if ((selectedDate.weekday == DateTime.sunday || + selectedDate.weekday == DateTime.saturday) && + (_isDateBeforeMinDate(selectedDate))) { + daysThreshold = selectedDate.weekday == DateTime.sunday ? 1 : 2; + } + DateTime targetDate = selectedDate.add(Duration(days: daysThreshold)); + + _scrollToDate( + targetDate, + selectedDate, + minCheck, + newItemIndex: dateController.selectedItem + daysThreshold, + ); + } else if (widget.calendarType == CupertinoDatePickerCalendarType.weekend) { + if (selectedDate.weekday != DateTime.sunday && + selectedDate.weekday != DateTime.saturday) { + int daysThreshold = DateTime.saturday - selectedDate.weekday; + DateTime targetDate = selectedDate.add(Duration(days: daysThreshold)); + + _scrollToDate( + targetDate, + selectedDate, + minCheck, + newItemIndex: dateController.selectedItem + daysThreshold, + ); + } + } + if (minCheck || maxCheck) { // We have minCheck === !maxCheck. final DateTime targetDate = @@ -1132,7 +1241,19 @@ class _CupertinoDatePickerDateTimeState } } - void _scrollToDate(DateTime newDate, DateTime fromDate, bool minCheck) { + bool _isDateBeforeMinDate(DateTime dateTime) { + if (widget.minimumDate == null) return false; + return widget.minimumDate!.day >= dateTime.day && + widget.minimumDate!.month >= dateTime.month && + widget.minimumDate!.year >= dateTime.year; + } + + void _scrollToDate( + DateTime newDate, + DateTime fromDate, + bool minCheck, { + int? newItemIndex, + }) { SchedulerBinding.instance.addPostFrameCallback( (Duration timestamp) { if (fromDate.year != newDate.year || @@ -1140,7 +1261,9 @@ class _CupertinoDatePickerDateTimeState fromDate.day != newDate.day) { _animateColumnControllerToItem( dateController, - selectedDayFromInitial, + widget.calendarType != CupertinoDatePickerCalendarType.fullWeek + ? newItemIndex! + : selectedDayFromInitial, ); } @@ -2214,29 +2337,6 @@ class _CupertinoDatePickerMonthYearState // If the maximum width given to the picker is smaller than 320.0, the picker's // layout will be broken. -/// Different modes of [CupertinoTimerPicker]. -/// -/// See also: -/// -/// * [CupertinoTimerPicker], the class that implements the iOS-style timer picker. -/// * [CupertinoPicker], the class that implements a content agnostic spinner UI. -enum CupertinoTimerPickerMode { - /// Mode that shows the timer duration in hour and minute. - /// - /// Examples: 16 hours | 14 min. - hm, - - /// Mode that shows the timer duration in minute and second. - /// - /// Examples: 14 min | 43 sec. - ms, - - /// Mode that shows the timer duration in hour, minute, and second. - /// - /// Examples: 16 hours | 14 min | 43 sec. - hms, -} - /// A countdown timer picker in iOS style. /// /// This picker shows a countdown duration with hour, minute and second spinners. diff --git a/lib/widgets/date_picker.dart b/lib/widgets/date_picker.dart index 8dbcd41..5002111 100644 --- a/lib/widgets/date_picker.dart +++ b/lib/widgets/date_picker.dart @@ -13,6 +13,7 @@ class DatePicker extends StatelessWidget { final TextStyle textStyle; final double? itemExtent; final bool showTimeSeparator; + final CupertinoDatePickerCalendarType calendarType; const DatePicker({ super.key, @@ -27,6 +28,7 @@ class DatePicker extends StatelessWidget { this.dateOrder, this.itemExtent = 0, this.showTimeSeparator = false, + required this.calendarType, }); @override @@ -48,6 +50,7 @@ class DatePicker extends StatelessWidget { minimumDate: minDateTime, use24hFormat: use24hFormat, dateOrder: dateOrder, + calendarType: calendarType, ), ); } diff --git a/lib/widgets/range_picker.dart b/lib/widgets/range_picker.dart index 9feae3a..5f0da7b 100644 --- a/lib/widgets/range_picker.dart +++ b/lib/widgets/range_picker.dart @@ -1,3 +1,4 @@ +import 'package:bottom_picker/cupertino/cupertino_date_picker.dart'; import 'package:bottom_picker/widgets/date_picker.dart'; import 'package:flutter/cupertino.dart'; @@ -104,6 +105,7 @@ class _RangePickerState extends State { }, dateOrder: widget.dateOrder, textStyle: widget.textStyle, + calendarType: CupertinoDatePickerCalendarType.fullWeek, ), ), Expanded( @@ -120,6 +122,7 @@ class _RangePickerState extends State { minuteInterval: widget.minuteInterval ?? 1, itemExtent: widget.itemExtent, showTimeSeparator: widget.showTimeSeperator, + calendarType: CupertinoDatePickerCalendarType.fullWeek, ), ), ],