From c775e06f77bd588f9643b70b80d1db0668cbd339 Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Thu, 1 Sep 2022 10:30:08 +0200 Subject: [PATCH 1/2] feat: added datetime_picker --- .../android/app/src/main/AndroidManifest.xml | 2 +- example/lib/main.dart | 66 ++++++++- example/pubspec.lock | 27 +++- lib/agenda.dart | 6 + lib/src/agenda.dart | 128 ++++++++++++++++++ lib/src/models/agenda_event.dart | 23 ++++ lib/src/models/agenda_theme.dart | 15 ++ pubspec.yaml | 14 +- 8 files changed, 276 insertions(+), 5 deletions(-) create mode 100644 lib/src/agenda.dart create mode 100644 lib/src/models/agenda_event.dart create mode 100644 lib/src/models/agenda_theme.dart diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 55ece1e..19f44cd 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ =2.18.0 <3.0.0" - flutter: ">=1.17.0" + flutter: ">=2.0.0" diff --git a/lib/agenda.dart b/lib/agenda.dart index e7e0021..4eba1ba 100644 --- a/lib/agenda.dart +++ b/lib/agenda.dart @@ -1 +1,7 @@ library agenda; + +export 'package:agenda/src/agenda.dart'; +export 'package:agenda/src/models/agenda_event.dart'; +export 'package:agenda/src/models/agenda_theme.dart'; + +export 'package:timetable/timetable.dart'; diff --git a/lib/src/agenda.dart b/lib/src/agenda.dart new file mode 100644 index 0000000..efa6f4e --- /dev/null +++ b/lib/src/agenda.dart @@ -0,0 +1,128 @@ +import 'package:agenda/src/models/agenda_event.dart'; +import 'package:agenda/src/models/agenda_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_date_time_picker/flutter_date_time_picker.dart'; +import 'package:timetable/timetable.dart'; + +class AgendaWidget extends StatefulWidget { + const AgendaWidget({ + required this.blocks, + this.highlightedDates = const [], + this.disabledDates = const [], + this.initialDate, + this.header, + this.scrollController, + this.scrollPhysics, + this.startHour = 0, + this.endHour = 24, + this.hourHeight = 80, + this.highlightToday = true, + this.blockWidth = 50, + this.blockColor = const Color(0x80FF0000), + this.theme = const AgendaTheme(), + super.key, + }); + + /// Header widget that is displayed above the datepicker. + final Widget? header; + + final List blocks; + + final List highlightedDates; + + final List disabledDates; + + /// The date that is initially selected. + final DateTime? initialDate; + + final bool highlightToday; + + /// Hour at which the timetable starts. + final int startHour; + + /// Hour at which the timetable ends. + final int endHour; + + /// The heigh of one hour in the timetable. + final double hourHeight; + + /// The width of the agendaItem if there is no child + final double blockWidth; + + /// The color of the agendaItem if there is no child + final Color blockColor; + + /// The theme used by the agenda. + /// The [TableTheme] used by the timetable is included. + final AgendaTheme theme; + + /// The scroll controller to control the scrolling of the timetable. + final ScrollController? scrollController; + + /// The scroll physics used for the SinglechildScrollView. + final ScrollPhysics? scrollPhysics; + + @override + State createState() => _AgendaWidgetState(); +} + +class _AgendaWidgetState extends State { + DateTime? _selectedDate; + + @override + void initState() { + super.initState(); + _selectedDate = widget.initialDate ?? DateTime.now(); + } + + @override + Widget build(BuildContext context) { + // filter out the blocks that are not on the selected date. + var events = + widget.blocks.where((block) => block.start.day == _selectedDate?.day); + return DateTimePicker( + initialDate: _selectedDate, + pickTime: false, + highlightToday: widget.highlightToday, + header: widget.header, + onTapDay: (p0) { + setState(() { + _selectedDate = p0; + }); + }, + disabledDates: widget.disabledDates, + markedDates: widget.highlightedDates, + dateTimePickerTheme: widget.theme.timePickerTheme, + child: Timetable( + scrollPhysics: widget.scrollPhysics, + scrollController: widget.scrollController, + blockColor: widget.blockColor, + blockWidth: widget.blockWidth, + hourHeight: widget.hourHeight, + startHour: widget.startHour, + endHour: widget.endHour, + timeBlocks: events.isEmpty + ? [] + : events + .map( + (e) => TimeBlock( + start: TimeOfDay( + hour: e.start.hour, + minute: e.start.minute, + ), + end: TimeOfDay( + hour: e.end.hour, + minute: e.end.minute, + ), + id: e.id ?? 0, + child: e.content, + ), + ) + .toList(), + theme: widget.theme.tableTheme, + combineBlocks: true, + mergeBlocks: true, + ), + ); + } +} diff --git a/lib/src/models/agenda_event.dart b/lib/src/models/agenda_event.dart new file mode 100644 index 0000000..7a694b7 --- /dev/null +++ b/lib/src/models/agenda_event.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class AgendaEvent { + const AgendaEvent({ + required this.start, + required this.end, + this.id, + this.content, + }); + + /// The start time of the event. + final DateTime start; + + /// The end time of the event. + final DateTime end; + + /// + final Widget? content; + + /// The identifier of the event that is used to combine events + /// with the same id. Leave empty or 0 if you don't want to combine events. + final int? id; +} diff --git a/lib/src/models/agenda_theme.dart b/lib/src/models/agenda_theme.dart new file mode 100644 index 0000000..32a36b8 --- /dev/null +++ b/lib/src/models/agenda_theme.dart @@ -0,0 +1,15 @@ +import 'package:flutter_date_time_picker/flutter_date_time_picker.dart'; +import 'package:timetable/timetable.dart'; + +class AgendaTheme { + const AgendaTheme({ + this.tableTheme = const TableTheme(), + this.timePickerTheme = const DateTimePickerTheme(), + }); + + /// The theme for the timetable. + final TableTheme tableTheme; + + /// The theme for the datetime picker. + final DateTimePickerTheme timePickerTheme; +} diff --git a/pubspec.yaml b/pubspec.yaml index e22da7f..77557d5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,17 +3,27 @@ description: Agenda widget with timetable version: 0.0.1 homepage: https://github.com/Iconica-Development/agenda +publish_to: none + environment: - sdk: '>=2.18.0 <3.0.0' + sdk: ">=2.18.0 <3.0.0" flutter: ">=1.17.0" dependencies: flutter: sdk: flutter + flutter_date_time_picker: + git: + url: https://github.com/Iconica-Development/flutter_date_time_picker + ref: main + timetable: + git: + url: https://github.com/Iconica-Development/timetable + ref: v0.0.2 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 -flutter: \ No newline at end of file +flutter: From 0073d90212baf7274de3b9b8df425c1ad2caaf9d Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Fri, 2 Sep 2022 16:21:52 +0200 Subject: [PATCH 2/2] feat: added documentation and tests --- .gitignore | 2 + CHANGELOG.md | 4 +- README.md | 22 ++++---- example/lib/main.dart | 4 ++ example/pubspec.lock | 8 +-- lib/src/agenda.dart | 68 +++++++++++++++++-------- lib/src/models/agenda_event.dart | 10 ++-- lib/src/models/agenda_theme.dart | 4 ++ pubspec.yaml | 6 +-- test/agenda_test.dart | 87 +++++++++++++++++++++++++++++++- 10 files changed, 165 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index d920ae6..4365314 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ migrate_working_dir/ .dart_tool/ .packages build/ + +coverage/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 41cc7d8..71d2704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ -## 0.0.1 +## [0.0.1] - 2 September 2022 -* TODO: Describe initial release. +* Initial release. diff --git a/README.md b/README.md index aa5cf48..5f8f2ee 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,20 @@ [![pub package](https://img.shields.io/pub/v/[PACKAGE NAME ON PUB].svg)](https://github.com/Iconica-Development) [![Build status](https://github.com/Iconica-Development/agenda)](URL TO GITHUB ACTIONS) [![style: effective dart](https://img.shields.io/badge/style-effective_dart-40c4ff.svg)](https://github.com/tenhobi/effective_dart) -Short description of what your package is, why you created it. What issues it fixes and how it works. Also mention the available platforms +# Agenda +A Flutter package for creating an agenda that displays events per day with an included calendar for picking the date. Multiple events at the same time are alongside each other. There is also the option to stack multiple items at the same time. +The underlying datepicker widget supports marking dates and disabling dates. -## Setup -What setup steps are neccesarry and why> -
-PLATFORM - -specific platform steps +Supports all Flutter platforms. -
+## Usage -## How to use +To use this package, add `agenda` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). -How can we use the package descibe the most common ways with examples in -```dart - codeblocks -``` +### Example + +See [Example Code](example/lib/main.dart) for more info. ## Issues diff --git a/example/lib/main.dart b/example/lib/main.dart index fc47789..988646d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -57,6 +57,10 @@ class AgendaDemo extends StatelessWidget { start: DateTime.now().subtract(const Duration(hours: 2)), end: DateTime.now().add(const Duration(hours: 1)), ), + AgendaEvent( + start: DateTime.now().add(const Duration(days: 2)), + end: DateTime.now().add(const Duration(days: 3)), + ), ], disabledDates: [ // yesterday diff --git a/example/pubspec.lock b/example/pubspec.lock index da281c3..33f8b45 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -66,9 +66,9 @@ packages: dependency: transitive description: path: "." - ref: main - resolved-ref: "7a3c66d28c2d29c1983d186435ed559a19fe34ff" - url: "https://github.com/Iconica-Development/flutter_date_time_picker" + ref: stable + resolved-ref: "6b3f1f5d12c1762bc208a1f1a4b9384dcb3369d4" + url: "git@github.com:Iconica-Development/flutter_date_time_picker.git" source: git version: "0.0.1" flutter_lints: @@ -178,7 +178,7 @@ packages: path: "." ref: "v0.0.2" resolved-ref: "62fdaa443818bd646058b536a12304725d1619fd" - url: "https://github.com/Iconica-Development/timetable" + url: "git@github.com:Iconica-Development/timetable.git" source: git version: "0.0.1" vector_math: diff --git a/lib/src/agenda.dart b/lib/src/agenda.dart index efa6f4e..01b65e4 100644 --- a/lib/src/agenda.dart +++ b/lib/src/agenda.dart @@ -5,6 +5,9 @@ import 'package:flutter_date_time_picker/flutter_date_time_picker.dart'; import 'package:timetable/timetable.dart'; class AgendaWidget extends StatefulWidget { + /// [AgendaWidget] is a widget that displays a timetable with events. + /// It is stateful and sorts the events based on the selected date. + /// All styling can be configured through the [AgendaTheme] class. const AgendaWidget({ required this.blocks, this.highlightedDates = const [], @@ -13,6 +16,7 @@ class AgendaWidget extends StatefulWidget { this.header, this.scrollController, this.scrollPhysics, + this.onTapDay, this.startHour = 0, this.endHour = 24, this.hourHeight = 80, @@ -26,15 +30,22 @@ class AgendaWidget extends StatefulWidget { /// Header widget that is displayed above the datepicker. final Widget? header; + /// The blocks that are displayed in the agenda final List blocks; + /// The highlighted dates that are displayed in the agenda final List highlightedDates; + /// The disabled dates that are displayed in the agenda final List disabledDates; /// The date that is initially selected. final DateTime? initialDate; + /// Function called when the user taps on a day in the datepicker. + final Function(DateTime)? onTapDay; + + /// Whether to highlight the current date in the agenda. final bool highlightToday; /// Hour at which the timetable starts. @@ -67,7 +78,7 @@ class AgendaWidget extends StatefulWidget { } class _AgendaWidgetState extends State { - DateTime? _selectedDate; + late DateTime _selectedDate; @override void initState() { @@ -77,9 +88,7 @@ class _AgendaWidgetState extends State { @override Widget build(BuildContext context) { - // filter out the blocks that are not on the selected date. - var events = - widget.blocks.where((block) => block.start.day == _selectedDate?.day); + var events = _filterEventsOnDay(widget.blocks, _selectedDate); return DateTimePicker( initialDate: _selectedDate, pickTime: false, @@ -101,28 +110,43 @@ class _AgendaWidgetState extends State { hourHeight: widget.hourHeight, startHour: widget.startHour, endHour: widget.endHour, - timeBlocks: events.isEmpty - ? [] - : events - .map( - (e) => TimeBlock( - start: TimeOfDay( - hour: e.start.hour, - minute: e.start.minute, - ), - end: TimeOfDay( - hour: e.end.hour, - minute: e.end.minute, - ), - id: e.id ?? 0, - child: e.content, - ), - ) - .toList(), + timeBlocks: events, theme: widget.theme.tableTheme, combineBlocks: true, mergeBlocks: true, ), ); } + + List _filterEventsOnDay(List events, DateTime day) { + return events + .where( + (e) => + (e.start.day == day.day && + e.start.month == day.month && + e.start.year == day.year) || + (e.end.day == day.day && + e.end.month == day.month && + e.end.year == day.year), + ) + .map( + (e) => TimeBlock( + start: (e.start.day != day.day) + ? TimeOfDay(hour: widget.startHour, minute: 0) + : TimeOfDay( + hour: e.start.hour, + minute: e.start.minute, + ), + end: (e.end.day != day.day) + ? TimeOfDay(hour: widget.endHour, minute: 0) + : TimeOfDay( + hour: e.end.hour, + minute: e.end.minute, + ), + id: e.id ?? 0, + child: e.content, + ), + ) + .toList(); + } } diff --git a/lib/src/models/agenda_event.dart b/lib/src/models/agenda_event.dart index 7a694b7..6301c97 100644 --- a/lib/src/models/agenda_event.dart +++ b/lib/src/models/agenda_event.dart @@ -1,20 +1,22 @@ import 'package:flutter/material.dart'; class AgendaEvent { - const AgendaEvent({ + /// The model used for a single event in the [AgendaWidget]. + /// AgendaEvent can be multiple days long. + AgendaEvent({ required this.start, required this.end, this.id, this.content, - }); + }) : assert(start.isBefore(end), 'start must be before end'); /// The start time of the event. final DateTime start; /// The end time of the event. final DateTime end; - - /// + + /// final Widget? content; /// The identifier of the event that is used to combine events diff --git a/lib/src/models/agenda_theme.dart b/lib/src/models/agenda_theme.dart index 32a36b8..d8ecd14 100644 --- a/lib/src/models/agenda_theme.dart +++ b/lib/src/models/agenda_theme.dart @@ -2,6 +2,10 @@ import 'package:flutter_date_time_picker/flutter_date_time_picker.dart'; import 'package:timetable/timetable.dart'; class AgendaTheme { + /// [AgendaTheme] is a class that contains all styling options + /// for the [AgendaWidget]. + /// The underlying [TableTheme] is used for the timetable. + /// The [DateTimePickerTheme] is used for the datepicker. const AgendaTheme({ this.tableTheme = const TableTheme(), this.timePickerTheme = const DateTimePickerTheme(), diff --git a/pubspec.yaml b/pubspec.yaml index 77557d5..467d7c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,11 +14,11 @@ dependencies: sdk: flutter flutter_date_time_picker: git: - url: https://github.com/Iconica-Development/flutter_date_time_picker - ref: main + url: git@github.com:Iconica-Development/flutter_date_time_picker.git + ref: stable timetable: git: - url: https://github.com/Iconica-Development/timetable + url: git@github.com:Iconica-Development/timetable.git ref: v0.0.2 dev_dependencies: diff --git a/test/agenda_test.dart b/test/agenda_test.dart index fe61a56..7b1d6c2 100644 --- a/test/agenda_test.dart +++ b/test/agenda_test.dart @@ -1,7 +1,90 @@ +import 'package:agenda/agenda.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - test('test', () { - expect(true, true); + testWidgets('header is shown', (tester) async { + // Act + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: AgendaWidget( + header: Text('Agenda'), + blocks: [], + ), + ), + ), + ); + + // Assert + expect(find.text('Agenda'), findsOneWidget); + }); + + testWidgets('blocks are shown', (tester) async { + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: AgendaWidget( + blocks: [ + AgendaEvent( + start: DateTime.now().subtract(const Duration(days: 3)), + end: DateTime.now().subtract(const Duration(days: 2)), + content: const Text('not shown'), + ), + AgendaEvent( + start: DateTime.now().add(const Duration(hours: 3)), + end: DateTime.now().add(const Duration(hours: 4)), + id: 4, + content: const Text('event 4'), + ), + AgendaEvent( + start: DateTime.now().add(const Duration(hours: 3)), + end: DateTime.now().add(const Duration(hours: 4)), + id: 4, + content: const Text('event 5'), + ), + ], + ), + ), + ), + ); + + // Assert + expect(find.text('event 4'), findsOneWidget); + expect(find.text('event 5'), findsOneWidget); + expect(find.text('not shown'), findsNothing); + }); + + testWidgets('event is removed on tap', (tester) async { + // Act + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: AgendaWidget( + initialDate: DateTime.now(), + blocks: [ + AgendaEvent( + start: DateTime.now(), + end: DateTime.now().add(const Duration(days: 1)), + content: const Text('single day event'), + ), + ], + ), + ), + ), + ); + var firstDay = find.text('single day event'); + expect(firstDay, findsOneWidget); + + var nextDay = DateTime.now().add(const Duration(days: 2)); + // if nextDay is monday or tuesday we need to go back 4 days + if (nextDay.weekday == DateTime.tuesday || + nextDay.weekday == DateTime.monday) { + nextDay = nextDay.subtract(const Duration(days: 4)); + } + await tester.tap(find.text(nextDay.day.toString())); + await tester.pump(); + expect(find.text('single day event'), findsNothing); }); }