From a9f744d626310096f834fa86e136f4e30d592f1a Mon Sep 17 00:00:00 2001 From: Bart Ribbers Date: Thu, 18 Jul 2024 14:14:14 +0200 Subject: [PATCH] fix: use a BasePage to reduce code duplication in comparable layouts --- .../ui/screens/availability_modification.dart | 236 +++++++++++------- .../src/ui/screens/availability_overview.dart | 113 ++++----- .../ui/screens/template_day_modification.dart | 143 +++++------ .../lib/src/ui/widgets/base_page.dart | 55 ++++ 4 files changed, 298 insertions(+), 249 deletions(-) create mode 100644 packages/flutter_availability/lib/src/ui/widgets/base_page.dart diff --git a/packages/flutter_availability/lib/src/ui/screens/availability_modification.dart b/packages/flutter_availability/lib/src/ui/screens/availability_modification.dart index 787614f..de63178 100644 --- a/packages/flutter_availability/lib/src/ui/screens/availability_modification.dart +++ b/packages/flutter_availability/lib/src/ui/screens/availability_modification.dart @@ -4,6 +4,7 @@ import "package:flutter_availability/src/ui/view_models/break_view_model.dart"; import "package:flutter_availability/src/ui/widgets/availability_clear.dart"; import "package:flutter_availability/src/ui/widgets/availability_template_selection.dart"; import "package:flutter_availability/src/ui/widgets/availabillity_time_selection.dart"; +import "package:flutter_availability/src/ui/widgets/base_page.dart"; import "package:flutter_availability/src/ui/widgets/pause_selection.dart"; import "package:flutter_availability/src/util/scope.dart"; import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; @@ -127,111 +128,156 @@ class _AvailabilitiesModificationScreenState Text(translations.saveButton), ); - var clearSection = AvailabilityClearSection( - range: widget.dateRange, - clearAvailable: _clearAvailability, - // TODO(Joey): Extract this function - onChanged: (isChecked) { - setState(() { - _clearAvailability = isChecked; - }); - }, - ); + // ignore: avoid_positional_boolean_parameters + void onClearSection(bool isChecked) { + setState(() { + _clearAvailability = isChecked; + }); + } - var templateSelection = AvailabilityTemplateSelection( - selectedTemplates: _selectedTemplates, - // TODO(Joey): Extract this function - onTemplateAdd: () async { - var template = await widget.onTemplateSelection(); - if (template != null) { - setState(() { - _selectedTemplates = [template]; - }); - } - }, - // TODO(Joey): Extract these functions - onTemplatesRemoved: () { + Future onTemplateSelected() async { + var template = await widget.onTemplateSelection(); + if (template != null) { setState(() { - _selectedTemplates = []; + _selectedTemplates = [template]; }); - }, - ); + } + } - var timeSelection = AvailabilityTimeSelection( + void onTemplatesRemoved() { + setState(() { + _selectedTemplates = []; + }); + } + + void onStartChanged(TimeOfDay start) { + setState(() { + _startTime = start; + }); + } + + void onEndChanged(TimeOfDay end) { + setState(() { + _endTime = end; + }); + } + + void onBreaksChanged(List breaks) { + setState(() { + _availability = _availability.copyWith( + breaks: breaks.map((b) => b.toBreak()).toList(), + ); + }); + } + + return _AvailabilitiesModificationScreenLayout( dateRange: widget.dateRange, + clearAvailability: _clearAvailability, + onClearSection: onClearSection, + selectedTemplates: _selectedTemplates, + onTemplateSelected: onTemplateSelected, + onTemplatesRemoved: onTemplatesRemoved, startTime: _startTime, endTime: _endTime, - key: ValueKey([_startTime, _endTime]), - // TODO(Joey): Extract these - onStartChanged: (start) => setState(() { - _startTime = start; - }), - // TODO(Joey): Extract these - onEndChanged: (end) => setState(() { - _endTime = end; - }), - ); - - var pauseSelection = PauseSelection( + onStartChanged: onStartChanged, + onEndChanged: onEndChanged, breaks: _availability.breaks .map(BreakViewModel.fromAvailabilityBreakModel) .toList(), - editingTemplate: false, - // TODO(Joey): Extract these - onBreaksChanged: (breaks) { - setState(() { - _availability = _availability.copyWith( - breaks: breaks.map((b) => b.toBreak()).toList(), - ); - }); - }, - ); - - // TODO(Joey): this structure is defined multiple times, we should create - // a widget to handle this consistently - var body = CustomScrollView( - slivers: [ - SliverPadding( - padding: - EdgeInsets.symmetric(horizontal: options.spacing.sidePadding), - sliver: SliverList.list( - children: [ - const SizedBox(height: 40), - clearSection, - if (!_clearAvailability) ...[ - const SizedBox(height: 24), - templateSelection, - const SizedBox(height: 24), - timeSelection, - // TODO(Joey): Not divisible by 4 - const SizedBox(height: 26), - pauseSelection, - ], - ], - ), - ), - SliverFillRemaining( - fillOverscroll: false, - hasScrollBody: false, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: spacing.sidePadding, - ).copyWith( - bottom: spacing.bottomButtonPadding, - ), - child: Align( - alignment: Alignment.bottomCenter, - child: saveButton, - ), - ), - ), - ], - ); - - return options.baseScreenBuilder( - context, - widget.onExit, - body, + onBreaksChanged: onBreaksChanged, + sidePadding: spacing.sidePadding, + bottomButtonPadding: spacing.bottomButtonPadding, + saveButton: saveButton, + onExit: widget.onExit, + ); + } +} + +class _AvailabilitiesModificationScreenLayout extends StatelessWidget { + const _AvailabilitiesModificationScreenLayout({ + required this.dateRange, + required this.clearAvailability, + required this.onClearSection, + required this.selectedTemplates, + required this.onTemplateSelected, + required this.onTemplatesRemoved, + required this.startTime, + required this.endTime, + required this.onStartChanged, + required this.onEndChanged, + required this.breaks, + required this.onBreaksChanged, + required this.sidePadding, + required this.bottomButtonPadding, + required this.saveButton, + required this.onExit, + }); + + final DateTimeRange dateRange; + final bool clearAvailability; + // ignore: avoid_positional_boolean_parameters + final void Function(bool isChecked) onClearSection; + + final List selectedTemplates; + final void Function() onTemplateSelected; + final void Function() onTemplatesRemoved; + + final TimeOfDay? startTime; + final TimeOfDay? endTime; + final void Function(TimeOfDay start) onStartChanged; + final void Function(TimeOfDay start) onEndChanged; + + final List breaks; + final void Function(List breaks) onBreaksChanged; + + final double sidePadding; + final double bottomButtonPadding; + + final Widget saveButton; + + final VoidCallback onExit; + + @override + Widget build(BuildContext context) { + var availabilityScope = AvailabilityScope.of(context); + var options = availabilityScope.options; + + return options.baseScreenBuilder( + context, + onExit, + BasePage( + body: [ + AvailabilityClearSection( + range: dateRange, + clearAvailable: clearAvailability, + onChanged: onClearSection, + ), + if (!clearAvailability) ...[ + const SizedBox(height: 24), + AvailabilityTemplateSelection( + selectedTemplates: selectedTemplates, + onTemplateAdd: onTemplateSelected, + onTemplatesRemoved: onTemplatesRemoved, + ), + const SizedBox(height: 24), + AvailabilityTimeSelection( + dateRange: dateRange, + startTime: startTime, + endTime: endTime, + key: ValueKey([startTime, endTime]), + onStartChanged: onStartChanged, + onEndChanged: onEndChanged, + ), + const SizedBox(height: 24), + PauseSelection( + breaks: breaks, + editingTemplate: false, + onBreaksChanged: onBreaksChanged, + ), + ], + ], + buttons: [saveButton], + ), ); } } diff --git a/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart b/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart index 7a94991..50764a6 100644 --- a/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart +++ b/packages/flutter_availability/lib/src/ui/screens/availability_overview.dart @@ -1,5 +1,6 @@ import "package:flutter/material.dart"; import "package:flutter_availability/src/service/availability_service.dart"; +import "package:flutter_availability/src/ui/widgets/base_page.dart"; import "package:flutter_availability/src/ui/widgets/calendar.dart"; import "package:flutter_availability/src/ui/widgets/template_legend.dart"; import "package:flutter_availability/src/util/scope.dart"; @@ -42,7 +43,6 @@ class _AvailabilityOverviewState extends State { var service = availabilityScope.service; var options = availabilityScope.options; var translations = options.translations; - var spacing = options.spacing; var availabilityStream = useMemoized( () => service.getOverviewDataForMonth(_selectedDate), @@ -51,18 +51,21 @@ class _AvailabilityOverviewState extends State { var availabilitySnapshot = useStream(availabilityStream); - // TODO(Joey): Way too complex of a function - var selectedAvailabilities = _selectedRange != null - ? availabilitySnapshot.data - ?.where( - (a) => - !a.availabilityModel.startDate - .isBefore(_selectedRange!.start) && - !a.availabilityModel.endDate.isAfter(_selectedRange!.end), - ) - .toList() ?? - [] - : []; + var selectedAvailabilities = []; + if (_selectedRange != null) { + var availabilities = availabilitySnapshot.data + ?.where( + (a) => + !a.availabilityModel.startDate + .isBefore(_selectedRange!.start) && + !a.availabilityModel.endDate.isAfter(_selectedRange!.end), + ) + .toList(); + + if (availabilities != null) { + selectedAvailabilities = availabilities; + } + } var availabilitiesAreSelected = selectedAvailabilities.isNotEmpty; @@ -94,18 +97,18 @@ class _AvailabilityOverviewState extends State { availabilities: availabilitySnapshot, ); - // TODO(Joey): too complex of a definition for the function - var onButtonPress = _selectedRange == null - ? null - : () { - widget.onEditDateRange( - _selectedRange!, - selectedAvailabilities, - ); - setState(() { - _selectedRange = null; - }); - }; + VoidCallback? onButtonPress; + if (_selectedRange != null) { + onButtonPress = () { + widget.onEditDateRange( + _selectedRange!, + selectedAvailabilities, + ); + setState(() { + _selectedRange = null; + }); + }; + } Future onClearButtonClicked() async { var confirmed = await options.confirmationDialogBuilder( @@ -135,53 +138,25 @@ class _AvailabilityOverviewState extends State { Text(translations.editAvailabilityButton), ); - // TODO(Joey): This structure is defined multiple times - 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: Column( - children: [ - startEditButton, - if (availabilitiesAreSelected) ...[ - const SizedBox(height: 8), - clearSelectedButton, - ], - ], - ), - ), - ), - ), - ], - ); - return options.baseScreenBuilder( context, widget.onExit, - body, + BasePage( + body: [ + title, + const SizedBox(height: 24), + calendar, + const SizedBox(height: 32), + templateLegend, + ], + buttons: [ + startEditButton, + if (availabilitiesAreSelected) ...[ + const SizedBox(height: 8), + clearSelectedButton, + ], + ], + ), ); } } diff --git a/packages/flutter_availability/lib/src/ui/screens/template_day_modification.dart b/packages/flutter_availability/lib/src/ui/screens/template_day_modification.dart index ac4bf62..52154de 100644 --- a/packages/flutter_availability/lib/src/ui/screens/template_day_modification.dart +++ b/packages/flutter_availability/lib/src/ui/screens/template_day_modification.dart @@ -1,5 +1,7 @@ import "package:flutter/material.dart"; import "package:flutter_availability/src/ui/view_models/day_template_view_model.dart"; +import "package:flutter_availability/src/ui/view_models/template_daydata_view_model.dart"; +import "package:flutter_availability/src/ui/widgets/base_page.dart"; import "package:flutter_availability/src/ui/widgets/color_selection.dart"; import "package:flutter_availability/src/ui/widgets/template_name_input.dart"; import "package:flutter_availability/src/ui/widgets/template_time_break.dart"; @@ -47,7 +49,6 @@ class _DayTemplateModificationScreenState var service = availabilityScope.service; var options = availabilityScope.options; var translations = options.translations; - var spacing = options.spacing; Future onDeletePressed() async { await service.deleteTemplate(widget.template!); @@ -66,97 +67,69 @@ class _DayTemplateModificationScreenState var canSave = _viewModel.canSave; - var saveButton = options.primaryButtonBuilder( - context, - canSave ? onSavePressed : null, - Text(translations.saveButton), - ); - var deleteButton = options.bigTextButtonBuilder( context, onDeletePressed, Text(translations.deleteTemplateButton), ); - var title = Center( - child: Text( - translations.dayTemplateTitle, - style: theme.textTheme.displaySmall, + void onNameChanged(String name) { + setState(() { + _viewModel = _viewModel.copyWith(name: name); + }); + } + + void onColorSelected(int? color) { + setState(() { + _viewModel = _viewModel.copyWith(color: color); + }); + } + + void onDayDataChanged(DayTemplateDataViewModel data) { + setState(() { + _viewModel = _viewModel.copyWith(data: data); + }); + } + + return options.baseScreenBuilder( + context, + widget.onExit, + BasePage( + body: [ + Center( + child: Text( + translations.dayTemplateTitle, + style: theme.textTheme.displaySmall, + ), + ), + const SizedBox(height: 24), + TemplateNameInput( + initialValue: _viewModel.name, + onNameChanged: onNameChanged, + ), + const SizedBox(height: 24), + TemplateTimeAndBreakSection( + dayData: _viewModel.data, + onDayDataChanged: onDayDataChanged, + ), + const SizedBox(height: 24), + TemplateColorSelection( + selectedColor: _viewModel.color, + onColorSelected: onColorSelected, + ), + ], + buttons: [ + options.primaryButtonBuilder( + context, + canSave ? onSavePressed : null, + Text(translations.saveButton), + ), + if (widget.template != null) ...[ + const SizedBox(height: 8), + deleteButton, + ], + ], ), ); - - var templateTitleSection = TemplateNameInput( - initialValue: _viewModel.name, - onNameChanged: (name) { - setState(() { - _viewModel = _viewModel.copyWith(name: name); - }); - }, - ); - - var colorSection = TemplateColorSelection( - selectedColor: _viewModel.color, - // TODO(Joey): Extract this - onColorSelected: (color) { - setState(() { - _viewModel = _viewModel.copyWith(color: color); - }); - }, - ); - - var availabilitySection = TemplateTimeAndBreakSection( - dayData: _viewModel.data, - onDayDataChanged: (data) { - setState(() { - _viewModel = _viewModel.copyWith(data: data); - }); - }, - ); - - var body = CustomScrollView( - slivers: [ - SliverPadding( - padding: EdgeInsets.symmetric(horizontal: spacing.sidePadding), - sliver: SliverList.list( - children: [ - const SizedBox(height: 40), - title, - const SizedBox(height: 24), - templateTitleSection, - const SizedBox(height: 24), - availabilitySection, - const SizedBox(height: 24), - colorSection, - 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: Column( - children: [ - saveButton, - if (widget.template != null) ...[ - const SizedBox(height: 8), - deleteButton, - ], - ], - ), - ), - ), - ), - ], - ); - - return options.baseScreenBuilder(context, widget.onExit, body); } } diff --git a/packages/flutter_availability/lib/src/ui/widgets/base_page.dart b/packages/flutter_availability/lib/src/ui/widgets/base_page.dart new file mode 100644 index 0000000..9d14433 --- /dev/null +++ b/packages/flutter_availability/lib/src/ui/widgets/base_page.dart @@ -0,0 +1,55 @@ +import "package:flutter/material.dart"; +import "package:flutter_availability/src/util/scope.dart"; + +/// +class BasePage extends StatelessWidget { + /// + const BasePage({ + required this.body, + required this.buttons, + super.key, + }); + + /// + final List body; + + /// + final List buttons; + + @override + Widget build(BuildContext context) { + var availabilityScope = AvailabilityScope.of(context); + var options = availabilityScope.options; + var spacing = options.spacing; + + return CustomScrollView( + slivers: [ + SliverPadding( + padding: EdgeInsets.symmetric(horizontal: spacing.sidePadding), + sliver: SliverList.list( + children: [ + const SizedBox(height: 40), + ...body, + 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: Column(children: buttons), + ), + ), + ), + ], + ); + } +}