From 0945394004090a1edf15f48dba3291f620b3fa24 Mon Sep 17 00:00:00 2001 From: Joey Boerwinkel Date: Thu, 4 Jul 2024 17:25:08 +0200 Subject: [PATCH] feat(availability-overview): create basic layout with options integration --- apps/example/lib/main.dart | 20 +- .../lib/flutter_availability.dart | 5 +- .../lib/src/config/availability_options.dart | 64 +++- .../src/config/availability_translations.dart | 79 ++--- .../flutter_availability/lib/src/routes.dart | 26 ++ .../src/screens/availability_overview.dart | 174 ----------- .../lib/src/service/availability_service.dart | 16 + ...service.dart => local_data_interface.dart} | 0 .../src/ui/screens/availability_overview.dart | 96 ++++++ .../template_availability_day_overview.dart} | 276 +++++++++--------- .../src/ui/widgets/default_base_screen.dart | 35 +++ .../lib/src/ui/widgets/default_buttons.dart | 65 +++++ .../lib/src/userstories.dart | 83 ++++++ .../src/userstory/navigator_userstory.dart | 75 ----- .../userstory/userstory_configuration.dart | 22 -- .../lib/src/util/scope.dart | 29 ++ 16 files changed, 584 insertions(+), 481 deletions(-) create mode 100644 packages/flutter_availability/lib/src/routes.dart delete mode 100644 packages/flutter_availability/lib/src/screens/availability_overview.dart create mode 100644 packages/flutter_availability/lib/src/service/availability_service.dart rename packages/flutter_availability/lib/src/service/{local_service.dart => local_data_interface.dart} (100%) create mode 100644 packages/flutter_availability/lib/src/ui/screens/availability_overview.dart rename packages/flutter_availability/lib/src/{screens/availability_day_overview.dart => ui/screens/template_availability_day_overview.dart} (60%) create mode 100644 packages/flutter_availability/lib/src/ui/widgets/default_base_screen.dart create mode 100644 packages/flutter_availability/lib/src/ui/widgets/default_buttons.dart create mode 100644 packages/flutter_availability/lib/src/userstories.dart delete mode 100644 packages/flutter_availability/lib/src/userstory/navigator_userstory.dart delete mode 100644 packages/flutter_availability/lib/src/userstory/userstory_configuration.dart create mode 100644 packages/flutter_availability/lib/src/util/scope.dart diff --git a/apps/example/lib/main.dart b/apps/example/lib/main.dart index cc25019..c97957c 100644 --- a/apps/example/lib/main.dart +++ b/apps/example/lib/main.dart @@ -9,8 +9,11 @@ class App extends StatelessWidget { const App({super.key}); @override - Widget build(BuildContext context) => const MaterialApp( - home: Home(), + Widget build(BuildContext context) => MaterialApp( + home: AvailabilityUserStory( + userId: "", + options: AvailabilityOptions(), + ), ); } @@ -18,7 +21,16 @@ class Home extends StatelessWidget { const Home({super.key}); @override - Widget build(BuildContext context) => availabilityNavigatorUserStory( - context, + Widget build(BuildContext context) => Scaffold( + body: const Center( + child: Text("Hello World"), + ), + floatingActionButton: FloatingActionButton( + onPressed: () async { + debugPrint("starting availability user story"); + await openAvailabilitiesForUser(context, "anonymous", null); + debugPrint("finishing availability user story"); + }, + ), ); } diff --git a/packages/flutter_availability/lib/flutter_availability.dart b/packages/flutter_availability/lib/flutter_availability.dart index 2f3c892..f1c6ef3 100644 --- a/packages/flutter_availability/lib/flutter_availability.dart +++ b/packages/flutter_availability/lib/flutter_availability.dart @@ -3,6 +3,5 @@ library flutter_availability; export "src/config/availability_options.dart"; export "src/config/availability_translations.dart"; -export "src/screens/availability_overview.dart"; -export "src/userstory/navigator_userstory.dart"; -export "src/userstory/userstory_configuration.dart"; +export "src/ui/screens/availability_overview.dart"; +export "src/userstories.dart"; diff --git a/packages/flutter_availability/lib/src/config/availability_options.dart b/packages/flutter_availability/lib/src/config/availability_options.dart index 10e00a9..cd5224d 100644 --- a/packages/flutter_availability/lib/src/config/availability_options.dart +++ b/packages/flutter_availability/lib/src/config/availability_options.dart @@ -1,12 +1,72 @@ +import "dart:async"; + +import "package:flutter/material.dart"; import "package:flutter_availability/src/config/availability_translations.dart"; +import "package:flutter_availability/src/service/local_data_interface.dart"; +import "package:flutter_availability/src/ui/widgets/default_base_screen.dart"; +import "package:flutter_availability/src/ui/widgets/default_buttons.dart"; +import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; /// Class that holds all options for the availability userstory class AvailabilityOptions { /// AvailabilityOptions constructor where everything is optional. - const AvailabilityOptions({ + AvailabilityOptions({ this.translations = const AvailabilityTranslations.empty(), - }); + this.baseScreenBuilder = DefaultBaseScreen.builder, + this.primaryButtonBuilder = DefaultPrimaryButton.builder, + this.textButtonBuilder = DefaultTextButton.builder, + this.spacing = const AvailabilitySpacing(), + AvailabilityDataInterface? dataInterface, + }) : dataInterface = dataInterface ?? LocalAvailabilityDataInterface(); /// The translations for the availability userstory final AvailabilityTranslations translations; + + /// The implementation for communicating with the persistance layer + final AvailabilityDataInterface dataInterface; + + /// A method to wrap your availability screens with a base frame. + /// + /// If you provide a screen here make sure to use a [Scaffold], as some + /// elements require a [Material] or other elements that a [Scaffold] + /// provides + final BaseScreenBuilder baseScreenBuilder; + + /// A way to provide your own primary button implementation + final ButtonBuilder primaryButtonBuilder; + + /// A way to provide your own text button implementation + final ButtonBuilder textButtonBuilder; + + /// The spacing between elements + final AvailabilitySpacing spacing; } + +/// All configurable paddings and whitespaces withing the userstory +class AvailabilitySpacing { + /// Creates an AvailabilitySpacing + const AvailabilitySpacing({ + this.sidePadding = 32, + this.bottomButtonPadding = 40, + }); + + /// the padding below the button at the bottom + final double bottomButtonPadding; + + /// the padding applied on both sides of the screen + final double sidePadding; +} + +/// Builder definition for providing a base screen surrounding each page +typedef BaseScreenBuilder = Widget Function( + BuildContext context, + VoidCallback onBack, + Widget child, +); + +/// Builder definition for providing a button implementation +typedef ButtonBuilder = Widget Function( + BuildContext context, + FutureOr? Function() onPressed, + Widget child, +); diff --git a/packages/flutter_availability/lib/src/config/availability_translations.dart b/packages/flutter_availability/lib/src/config/availability_translations.dart index 8e10904..4c5b123 100644 --- a/packages/flutter_availability/lib/src/config/availability_translations.dart +++ b/packages/flutter_availability/lib/src/config/availability_translations.dart @@ -9,76 +9,35 @@ class AvailabilityTranslations { /// You can copyWith the values to override some default translations. /// It is recommended to use this constructor. const AvailabilityTranslations({ - required this.calendarTitle, - required this.addAvailableDayButtonText, - required this.availabilityOverviewTitle, - required this.availabilityDate, - required this.availabilityHours, - required this.availabilityEmptyMessage, - required this.availabilitySubmit, - required this.availabilitySave, + required this.appbarTitle, + required this.editAvailabilityButton, + required this.templateLegendTitle, + required this.overviewScreenTitle, + required this.createTemplateButton, }); /// AvailabilityTranslations constructor where everything is optional. /// This provides default english values for the availability userstory. const AvailabilityTranslations.empty({ - this.calendarTitle = "Availability", - this.addAvailableDayButtonText = "Add availability", - this.availabilityOverviewTitle = "Overview of your availabilities", - this.availabilityDate = "Date", - this.availabilityHours = "Hours", - this.availabilityEmptyMessage = - "No availabilities filled in for this month", - this.availabilitySubmit = "Submit", - this.availabilitySave = "Save", + this.appbarTitle = "Availability", + this.editAvailabilityButton = "Edit availability", + this.templateLegendTitle = "Templates", + this.createTemplateButton = "Create a new template", + this.overviewScreenTitle = "Availability", }); /// The title shown above the calendar - final String calendarTitle; + final String appbarTitle; - /// The text shown on the button to add an available day - final String addAvailableDayButtonText; + /// The text shown on the button to edit availabilities for a range + final String editAvailabilityButton; - /// The title for the overview of the availabilities - final String availabilityOverviewTitle; + /// The title for the legend template section on the overview screen + final String templateLegendTitle; - /// The label for the date of an availability - final String availabilityDate; + /// The title on the overview screen + final String overviewScreenTitle; - /// The label for the hours of an availability - final String availabilityHours; - - /// The text shown when there are no availabilities filled in for a month - final String availabilityEmptyMessage; - - /// The text shown on the button to submit the availabilities - final String availabilitySubmit; - - /// The text shown on the button to save a single availability - final String availabilitySave; - - /// Method to update the translations with new values - AvailabilityTranslations copyWith({ - String? calendarTitle, - String? addAvailableDayButtonText, - String? availabilityOverviewTitle, - String? availabilityDate, - String? availabilityHours, - String? availabilityEmptyMessage, - String? availabilitySubmit, - String? availabilitySave, - }) => - AvailabilityTranslations( - calendarTitle: calendarTitle ?? this.calendarTitle, - addAvailableDayButtonText: - addAvailableDayButtonText ?? this.addAvailableDayButtonText, - availabilityOverviewTitle: - availabilityOverviewTitle ?? this.availabilityOverviewTitle, - availabilityDate: availabilityDate ?? this.availabilityDate, - availabilityHours: availabilityHours ?? this.availabilityHours, - availabilityEmptyMessage: - availabilityEmptyMessage ?? this.availabilityEmptyMessage, - availabilitySubmit: availabilitySubmit ?? this.availabilitySubmit, - availabilitySave: availabilitySave ?? this.availabilitySave, - ); + /// The label on the button to go to the tempalte creation page + final String createTemplateButton; } diff --git a/packages/flutter_availability/lib/src/routes.dart b/packages/flutter_availability/lib/src/routes.dart new file mode 100644 index 0000000..66c52a1 --- /dev/null +++ b/packages/flutter_availability/lib/src/routes.dart @@ -0,0 +1,26 @@ +import "package:flutter/material.dart"; +import "package:flutter_availability/flutter_availability.dart"; +import "package:flutter_availability/src/ui/screens/template_availability_day_overview.dart"; + +/// +final homePageRoute = MaterialPageRoute( + builder: (context) => AvailabilityOverview( + onEditDateRange: (range) { + Navigator.of(context).push(availabilityViewRoute(range.start)); + }, + onViewTemplates: () {}, + ), +); + +/// +MaterialPageRoute availabilityViewRoute( + DateTime date, +) => + MaterialPageRoute( + builder: (context) => AvailabilityDayOverview( + date: date, + onAvailabilitySaved: () { + Navigator.of(context).pop(); + }, + ), + ); diff --git a/packages/flutter_availability/lib/src/screens/availability_overview.dart b/packages/flutter_availability/lib/src/screens/availability_overview.dart deleted file mode 100644 index 7bd17b3..0000000 --- a/packages/flutter_availability/lib/src/screens/availability_overview.dart +++ /dev/null @@ -1,174 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_availability/src/config/availability_options.dart"; -import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; -import "package:intl/intl.dart"; - -/// -class AvailabilityOverview extends StatefulWidget { - /// - const AvailabilityOverview({ - required this.userId, - required this.service, - required this.options, - required this.onDayClicked, - required this.onAvailabilityClicked, - super.key, - }); - - /// The user whose availability is being managed - final String userId; - - /// The service to use for managing availability - final AvailabilityDataInterface service; - - /// The configuration option for the availability overview - final AvailabilityOptions options; - - /// Callback for when the user clicks on a day - final void Function(DateTime date) onDayClicked; - - /// Callback for when the user clicks on an availability - final void Function(AvailabilityModel availability) onAvailabilityClicked; - - @override - State createState() => _AvailabilityOverviewState(); -} - -class _AvailabilityOverviewState extends State { - Stream>? _availabilityStream; - - void _startLoadingAvailabilities(DateTime start, DateTime end) { - setState(() { - _availabilityStream = widget.service.getAvailabilityForUser( - userId: widget.userId, - start: start, - end: end, - ); - }); - } - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return Stack( - children: [ - Column( - children: [ - Text( - widget.options.translations.calendarTitle, - style: theme.textTheme.displaySmall, - ), - Expanded( - child: _availabilityStream == null - ? const Center( - child: Text("Press the button to load availabilities."), - ) - : StreamBuilder>( - stream: _availabilityStream, - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(), - ); - } else if (snapshot.hasError) { - return Center( - child: Text("Error: ${snapshot.error}"), - ); - } else if (!snapshot.hasData || - snapshot.data!.isEmpty) { - return const Center( - child: Text("No availabilities found."), - ); - } else { - var sortedAvailabilities = snapshot.data! - ..sort( - (a, b) => a.startDate.compareTo(b.startDate), - ); - return ListView.builder( - itemCount: snapshot.data!.length, - itemBuilder: (context, index) { - var availability = sortedAvailabilities[index]; - return ListTile( - title: Text( - "Available from ${DateFormat( - "dd-MM-yyyy HH:mm", - ).format(availability.startDate)} " - "\nto \n" - "${DateFormat("dd-MM-yyyy HH:mm").format( - availability.endDate, - )}", - ), - trailing: IconButton( - icon: const Icon(Icons.delete), - onPressed: () async { - if (availability.id == null) { - return; - } - await widget.service - .deleteAvailabilityForUser( - widget.userId, - availability.id!, - ); - }, - ), - onTap: () => - widget.onAvailabilityClicked(availability), - ); - }, - ); - } - }, - ), - ), - ], - ), - Align( - alignment: Alignment.bottomCenter, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ElevatedButton( - onPressed: () async { - // ask the user to select a date - var date = await showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: - DateTime.now().subtract(const Duration(days: 365)), - lastDate: DateTime.now().add(const Duration(days: 365)), - ); - - if (date == null) { - return; - } - widget.onDayClicked(date); - }, - child: Text( - widget.options.translations.addAvailableDayButtonText, - ), - ), - ElevatedButton( - onPressed: () async { - // ask the user to select a date - var dateRange = await showDateRangePicker( - context: context, - firstDate: - DateTime.now().subtract(const Duration(days: 365)), - lastDate: DateTime.now().add(const Duration(days: 365)), - ); - - if (dateRange == null) { - return; - } - _startLoadingAvailabilities(dateRange.start, dateRange.end); - }, - child: const Text("Load availabilities for a date range"), - ), - ], - ), - ), - ], - ); - } -} diff --git a/packages/flutter_availability/lib/src/service/availability_service.dart b/packages/flutter_availability/lib/src/service/availability_service.dart new file mode 100644 index 0000000..cea7b60 --- /dev/null +++ b/packages/flutter_availability/lib/src/service/availability_service.dart @@ -0,0 +1,16 @@ +import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; + +/// +class AvailabilityService { + /// + const AvailabilityService({ + required this.userId, + required this.dataInterface, + }); + + /// + final String userId; + + /// + final AvailabilityDataInterface dataInterface; +} diff --git a/packages/flutter_availability/lib/src/service/local_service.dart b/packages/flutter_availability/lib/src/service/local_data_interface.dart similarity index 100% rename from packages/flutter_availability/lib/src/service/local_service.dart rename to packages/flutter_availability/lib/src/service/local_data_interface.dart diff --git a/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart b/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart new file mode 100644 index 0000000..acf0610 --- /dev/null +++ b/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart @@ -0,0 +1,96 @@ +import "package:flutter/material.dart"; +import "package:flutter_availability/src/util/scope.dart"; + +/// +class AvailabilityOverview extends StatelessWidget { + /// + const AvailabilityOverview({ + required this.onEditDateRange, + required this.onViewTemplates, + super.key, + }); + + /// Callback for when the user clicks on a day + final void Function(DateTimeRange range) onEditDateRange; + + /// Callback for when the user wants to navigate to the overview of templates + final VoidCallback onViewTemplates; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var availabilityScope = AvailabilityScope.of(context); + var options = availabilityScope.options; + var translations = options.translations; + var spacing = options.spacing; + + void onBack() { + Navigator.of(context).pop(); + } + + var title = Center( + child: Text( + translations.overviewScreenTitle, + style: theme.textTheme.displaySmall, + ), + ); + + const calendar = SizedBox( + height: 320, + child: Placeholder(), + ); + + const templateLegend = SizedBox( + height: 40, + child: Placeholder(), + ); + + var startEditButton = options.primaryButtonBuilder( + context, + () { + onEditDateRange(DateTimeRange(start: DateTime(1), end: DateTime(2))); + }, + Text(translations.editAvailabilityButton), + ); + + var body = CustomScrollView( + slivers: [ + SliverPadding( + padding: EdgeInsets.symmetric(horizontal: spacing.sidePadding), + sliver: SliverList.list( + children: [ + const SizedBox(height: 40), + title, + const SizedBox(height: 24), + calendar, + const SizedBox(height: 32), + templateLegend, + const SizedBox(height: 32), + ], + ), + ), + SliverFillRemaining( + fillOverscroll: false, + hasScrollBody: false, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: spacing.sidePadding, + ).copyWith( + bottom: spacing.bottomButtonPadding, + ), + child: Align( + alignment: Alignment.bottomCenter, + child: startEditButton, + ), + ), + ), + ], + ); + + return options.baseScreenBuilder( + context, + onBack, + body, + ); + } +} diff --git a/packages/flutter_availability/lib/src/screens/availability_day_overview.dart b/packages/flutter_availability/lib/src/ui/screens/template_availability_day_overview.dart similarity index 60% rename from packages/flutter_availability/lib/src/screens/availability_day_overview.dart rename to packages/flutter_availability/lib/src/ui/screens/template_availability_day_overview.dart index 43a22b6..7bc03f3 100644 --- a/packages/flutter_availability/lib/src/screens/availability_day_overview.dart +++ b/packages/flutter_availability/lib/src/ui/screens/template_availability_day_overview.dart @@ -1,5 +1,5 @@ import "package:flutter/material.dart"; -import "package:flutter_availability/src/config/availability_options.dart"; +import "package:flutter_availability/src/util/scope.dart"; import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; import "package:intl/intl.dart"; @@ -7,24 +7,12 @@ import "package:intl/intl.dart"; class AvailabilityDayOverview extends StatefulWidget { /// const AvailabilityDayOverview({ - required this.userId, - required this.service, - required this.options, required this.date, required this.onAvailabilitySaved, this.initialAvailability, super.key, }); - /// The user whose availability is being managed - final String userId; - - /// The service to use for managing availability - final AvailabilityDataInterface service; - - /// The configuration option for the availability overview - final AvailabilityOptions options; - /// The date for which the availability is being managed final DateTime date; @@ -50,7 +38,7 @@ class _AvailabilityDayOverviewState extends State { super.initState(); _availability = widget.initialAvailability ?? AvailabilityModel( - userId: widget.userId, + userId: "", startDate: widget.date, endDate: widget.date, breaks: [], @@ -86,6 +74,10 @@ class _AvailabilityDayOverviewState extends State { @override Widget build(BuildContext context) { + var availabilityScope = AvailabilityScope.of(context); + var userId = availabilityScope.userId; + var service = availabilityScope.service; + Future updateAvailabilityStart() async { var selectedTime = await _selectTime(_startDateController); if (selectedTime == null) return; @@ -125,15 +117,15 @@ class _AvailabilityDayOverviewState extends State { if (_clearAvailableToday) { // remove the availability for the user if (_availability.id != null) { - await widget.service.deleteAvailabilityForUser( - widget.userId, + await service.dataInterface.deleteAvailabilityForUser( + userId, _availability.id!, ); } } else { // add an availability for the user - await widget.service.createAvailabilityForUser( - widget.userId, + await service.dataInterface.createAvailabilityForUser( + userId, _availability, ); } @@ -143,140 +135,142 @@ class _AvailabilityDayOverviewState extends State { } var theme = Theme.of(context); - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - Text( - DateFormat.yMMMMd().format(widget.date), - style: theme.textTheme.bodyLarge, - ), - Row( - children: [ - Checkbox( - value: _clearAvailableToday, - onChanged: (value) { - setState(() { - _clearAvailableToday = value!; - }); - }, - ), - const Text("Clear availability for today"), - ], - ), - Opacity( - opacity: _clearAvailableToday ? 0.5 : 1, - child: IgnorePointer( - ignoring: _clearAvailableToday, - child: Column( - children: [ - Row( - children: [ - Expanded( - child: GestureDetector( - onTap: updateAvailabilityStart, - child: AbsorbPointer( - child: TextField( - controller: _startDateController, - decoration: const InputDecoration( - labelText: "Begin tijd", - suffixIcon: Icon(Icons.access_time), + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Text( + DateFormat.yMMMMd().format(widget.date), + style: theme.textTheme.bodyLarge, + ), + Row( + children: [ + Checkbox( + value: _clearAvailableToday, + onChanged: (value) { + setState(() { + _clearAvailableToday = value!; + }); + }, + ), + const Text("Clear availability for today"), + ], + ), + Opacity( + opacity: _clearAvailableToday ? 0.5 : 1, + child: IgnorePointer( + ignoring: _clearAvailableToday, + child: Column( + children: [ + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: updateAvailabilityStart, + child: AbsorbPointer( + child: TextField( + controller: _startDateController, + decoration: const InputDecoration( + labelText: "Begin tijd", + suffixIcon: Icon(Icons.access_time), + ), ), ), ), ), - ), - const SizedBox(width: 16), - const Text("tot"), - const SizedBox(width: 16), - Expanded( - child: GestureDetector( - onTap: updateAvailabilityEnd, - child: AbsorbPointer( - child: TextField( - controller: _endDateController, - decoration: const InputDecoration( - labelText: "Eind tijd", - suffixIcon: Icon(Icons.access_time), + const SizedBox(width: 16), + const Text("tot"), + const SizedBox(width: 16), + Expanded( + child: GestureDetector( + onTap: updateAvailabilityEnd, + child: AbsorbPointer( + child: TextField( + controller: _endDateController, + decoration: const InputDecoration( + labelText: "Eind tijd", + suffixIcon: Icon(Icons.access_time), + ), ), ), ), ), - ), - ], - ), - const SizedBox( - height: 16, - ), - const Text("Add pause (optional)"), - ListView( - shrinkWrap: true, - children: _availability.breaks.map( - (breakModel) { - var start = - DateFormat("HH:mm").format(breakModel.startTime); - var end = - DateFormat("HH:mm").format(breakModel.endTime); - return GestureDetector( - onTap: () async { - var updatedBreak = - await AvailabilityBreakSelectionDialog.show( - context, - breakModel, - ); - if (updatedBreak != null) { - setState(() { - _availability.breaks.remove(breakModel); - _availability.breaks.add(updatedBreak); - }); - } - }, - child: Container( - decoration: const BoxDecoration( - color: Colors.lightBlue, + ], + ), + const SizedBox( + height: 16, + ), + const Text("Add pause (optional)"), + ListView( + shrinkWrap: true, + children: _availability.breaks.map( + (breakModel) { + var start = + DateFormat("HH:mm").format(breakModel.startTime); + var end = + DateFormat("HH:mm").format(breakModel.endTime); + return GestureDetector( + onTap: () async { + var updatedBreak = + await AvailabilityBreakSelectionDialog.show( + context, + breakModel, + ); + if (updatedBreak != null) { + setState(() { + _availability.breaks.remove(breakModel); + _availability.breaks.add(updatedBreak); + }); + } + }, + child: Container( + decoration: const BoxDecoration( + color: Colors.lightBlue, + ), + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Text( + "${breakModel.duration.inMinutes}" + " minutes | ", + ), + Text( + "$start - " + "$end", + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + setState(() { + _availability.breaks.remove(breakModel); + }); + }, + ), + ], + ), ), - margin: const EdgeInsets.only(bottom: 16), - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Text( - "${breakModel.duration.inMinutes}" - " minutes | ", - ), - Text( - "$start - " - "$end", - ), - const Spacer(), - IconButton( - icon: const Icon(Icons.delete), - onPressed: () { - setState(() { - _availability.breaks.remove(breakModel); - }); - }, - ), - ], - ), - ), - ); - }, - ).toList(), - ), - TextButton( - onPressed: onClickAddPause, - child: const Text("Add"), - ), - ], + ); + }, + ).toList(), + ), + TextButton( + onPressed: onClickAddPause, + child: const Text("Add"), + ), + ], + ), ), ), - ), - const Spacer(), - ElevatedButton( - onPressed: () async => onClickSave(), - child: Text(widget.options.translations.availabilitySave), - ), - ], + const Spacer(), + ElevatedButton( + onPressed: () async => onClickSave(), + child: const Text(""), + ), + ], + ), ), ); } diff --git a/packages/flutter_availability/lib/src/ui/widgets/default_base_screen.dart b/packages/flutter_availability/lib/src/ui/widgets/default_base_screen.dart new file mode 100644 index 0000000..08e745e --- /dev/null +++ b/packages/flutter_availability/lib/src/ui/widgets/default_base_screen.dart @@ -0,0 +1,35 @@ +import "package:flutter/material.dart"; +import "package:flutter_availability/src/util/scope.dart"; + +/// Default base screen for any availability screen +class DefaultBaseScreen extends StatelessWidget { + + /// Create a base screen + const DefaultBaseScreen({ + required this.child, + super.key, + }); + /// Builder as default option + static Widget builder( + BuildContext context, + VoidCallback onBack, + Widget child, + ) => + DefaultBaseScreen(child: child); + + /// Content of the page + final Widget child; + + @override + Widget build(BuildContext context) { + var translations = AvailabilityScope.of(context).options.translations; + return Scaffold( + appBar: AppBar( + title: Text(translations.appbarTitle), + ), + body: SafeArea( + child: child, + ), + ); + } +} diff --git a/packages/flutter_availability/lib/src/ui/widgets/default_buttons.dart b/packages/flutter_availability/lib/src/ui/widgets/default_buttons.dart new file mode 100644 index 0000000..daa43cd --- /dev/null +++ b/packages/flutter_availability/lib/src/ui/widgets/default_buttons.dart @@ -0,0 +1,65 @@ +import "dart:async"; + +import "package:flutter/material.dart"; + +/// +class DefaultPrimaryButton extends StatelessWidget { + /// + const DefaultPrimaryButton({ + required this.child, + required this.onPressed, + super.key, + }); + + /// + static Widget builder( + BuildContext context, + FutureOr Function()? onPressed, + Widget child, + ) => + DefaultPrimaryButton( + onPressed: onPressed, + child: child, + ); + + /// + final Widget child; + + /// + final FutureOr Function()? onPressed; + + @override + Widget build(BuildContext context) => + FilledButton(onPressed: onPressed, child: child); +} + +/// +class DefaultTextButton extends StatelessWidget { + /// + const DefaultTextButton({ + required this.child, + required this.onPressed, + super.key, + }); + + /// + static Widget builder( + BuildContext context, + FutureOr Function()? onPressed, + Widget child, + ) => + DefaultTextButton( + onPressed: onPressed, + child: child, + ); + + /// + final Widget child; + + /// + final FutureOr Function()? onPressed; + + @override + Widget build(BuildContext context) => + TextButton(onPressed: onPressed, child: child); +} diff --git a/packages/flutter_availability/lib/src/userstories.dart b/packages/flutter_availability/lib/src/userstories.dart new file mode 100644 index 0000000..40893c9 --- /dev/null +++ b/packages/flutter_availability/lib/src/userstories.dart @@ -0,0 +1,83 @@ +import "package:flutter/material.dart"; +import "package:flutter_availability/src/config/availability_options.dart"; +import "package:flutter_availability/src/routes.dart"; +import "package:flutter_availability/src/service/availability_service.dart"; +import "package:flutter_availability/src/util/scope.dart"; + +/// This pushes the availability user story to the navigator stack. +Future openAvailabilitiesForUser( + BuildContext context, + String userId, + AvailabilityOptions? options, +) async { + var navigator = Navigator.of(context); + + await navigator.push( + AvailabilityUserStory.route( + userId, + options ?? AvailabilityOptions(), + ), + ); +} + +/// +class AvailabilityUserStory extends StatefulWidget { + /// + const AvailabilityUserStory({ + required this.userId, + required this.options, + super.key, + }); + + /// + final String userId; + + /// + final AvailabilityOptions options; + + /// + static MaterialPageRoute route(String userId, AvailabilityOptions options) => + MaterialPageRoute( + builder: (context) => AvailabilityUserStory( + userId: userId, + options: options, + ), + ); + + @override + State createState() => _AvailabilityUserStoryState(); +} + +class _AvailabilityUserStoryState extends State { + late AvailabilityService _service = AvailabilityService( + userId: widget.userId, + dataInterface: widget.options.dataInterface, + ); + + @override + Widget build(BuildContext context) => AvailabilityScope( + userId: widget.userId, + options: widget.options, + service: _service, + child: Navigator( + onGenerateInitialRoutes: (state, route) => [ + homePageRoute, + ], + ), + ); + + @override + void didUpdateWidget(covariant AvailabilityUserStory oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.userId != widget.userId || + oldWidget.options != widget.options) { + setState(() { + _service = AvailabilityService( + userId: widget.userId, + dataInterface: widget.options.dataInterface, + ); + }); + } + } +} diff --git a/packages/flutter_availability/lib/src/userstory/navigator_userstory.dart b/packages/flutter_availability/lib/src/userstory/navigator_userstory.dart deleted file mode 100644 index c1e3abc..0000000 --- a/packages/flutter_availability/lib/src/userstory/navigator_userstory.dart +++ /dev/null @@ -1,75 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_availability/src/screens/availability_day_overview.dart"; -import "package:flutter_availability/src/screens/availability_overview.dart"; -import "package:flutter_availability/src/service/local_service.dart"; -import "package:flutter_availability/src/userstory/userstory_configuration.dart"; -import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; - -/// -Widget availabilityNavigatorUserStory( - BuildContext context, { - AvailabiltyUserstoryConfiguration? configuration, -}) => - _availabiltyScreenRoute( - context, - configuration ?? - AvailabiltyUserstoryConfiguration( - service: LocalAvailabilityDataInterface(), - getUserId: (_) => "no-user", - ), - ); - -Widget _availabiltyScreenRoute( - BuildContext context, - AvailabiltyUserstoryConfiguration configuration, -) => - SafeArea( - child: Scaffold( - body: AvailabilityOverview( - service: configuration.service, - options: configuration.options, - userId: configuration.getUserId(context), - onDayClicked: (date) async => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => _avaibiltyDayOverviewRoute( - context, - configuration, - date, - null, - ), - ), - ), - onAvailabilityClicked: (availability) async => - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => _avaibiltyDayOverviewRoute( - context, - configuration, - availability.startDate, - availability, - ), - ), - ), - ), - ), - ); - -Widget _avaibiltyDayOverviewRoute( - BuildContext context, - AvailabiltyUserstoryConfiguration configuration, - DateTime date, - AvailabilityModel? availability, -) => - SafeArea( - child: Scaffold( - appBar: AppBar(), - body: AvailabilityDayOverview( - service: configuration.service, - options: configuration.options, - userId: configuration.getUserId(context), - date: date, - initialAvailability: availability, - onAvailabilitySaved: () => Navigator.of(context).pop(), - ), - ), - ); diff --git a/packages/flutter_availability/lib/src/userstory/userstory_configuration.dart b/packages/flutter_availability/lib/src/userstory/userstory_configuration.dart deleted file mode 100644 index dd29789..0000000 --- a/packages/flutter_availability/lib/src/userstory/userstory_configuration.dart +++ /dev/null @@ -1,22 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_availability/src/config/availability_options.dart"; -import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; - -/// -class AvailabiltyUserstoryConfiguration { - /// - const AvailabiltyUserstoryConfiguration({ - required this.service, - required this.getUserId, - this.options = const AvailabilityOptions(), - }); - - /// - final AvailabilityOptions options; - - /// - final AvailabilityDataInterface service; - - /// - final Function(BuildContext context) getUserId; -} diff --git a/packages/flutter_availability/lib/src/util/scope.dart b/packages/flutter_availability/lib/src/util/scope.dart new file mode 100644 index 0000000..c575d8f --- /dev/null +++ b/packages/flutter_availability/lib/src/util/scope.dart @@ -0,0 +1,29 @@ +import "package:flutter/widgets.dart"; +import "package:flutter_availability/src/config/availability_options.dart"; +import "package:flutter_availability/src/service/availability_service.dart"; + +/// +class AvailabilityScope extends InheritedWidget { + /// + const AvailabilityScope({ + required this.userId, + required this.options, + required this.service, + required super.child, + super.key, + }); + /// + final String userId; + /// + final AvailabilityOptions options; + /// + final AvailabilityService service; + + @override + bool updateShouldNotify(AvailabilityScope oldWidget) => + oldWidget.userId != userId || options != options; + + /// + static AvailabilityScope of(BuildContext context) => + context.dependOnInheritedWidgetOfExactType()!; +}