fix: use a BasePage to reduce code duplication in comparable layouts

This commit is contained in:
Bart Ribbers 2024-07-18 14:14:14 +02:00 committed by Freek van de Ven
parent d765a7a0f8
commit a9f744d626
4 changed files with 298 additions and 249 deletions

View file

@ -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_clear.dart";
import "package:flutter_availability/src/ui/widgets/availability_template_selection.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/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/ui/widgets/pause_selection.dart";
import "package:flutter_availability/src/util/scope.dart"; import "package:flutter_availability/src/util/scope.dart";
import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart";
@ -127,111 +128,156 @@ class _AvailabilitiesModificationScreenState
Text(translations.saveButton), Text(translations.saveButton),
); );
var clearSection = AvailabilityClearSection( // ignore: avoid_positional_boolean_parameters
range: widget.dateRange, void onClearSection(bool isChecked) {
clearAvailable: _clearAvailability, setState(() {
// TODO(Joey): Extract this function _clearAvailability = isChecked;
onChanged: (isChecked) { });
setState(() { }
_clearAvailability = isChecked;
});
},
);
var templateSelection = AvailabilityTemplateSelection( Future<void> onTemplateSelected() async {
selectedTemplates: _selectedTemplates, var template = await widget.onTemplateSelection();
// TODO(Joey): Extract this function if (template != null) {
onTemplateAdd: () async {
var template = await widget.onTemplateSelection();
if (template != null) {
setState(() {
_selectedTemplates = [template];
});
}
},
// TODO(Joey): Extract these functions
onTemplatesRemoved: () {
setState(() { 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<BreakViewModel> breaks) {
setState(() {
_availability = _availability.copyWith(
breaks: breaks.map((b) => b.toBreak()).toList(),
);
});
}
return _AvailabilitiesModificationScreenLayout(
dateRange: widget.dateRange, dateRange: widget.dateRange,
clearAvailability: _clearAvailability,
onClearSection: onClearSection,
selectedTemplates: _selectedTemplates,
onTemplateSelected: onTemplateSelected,
onTemplatesRemoved: onTemplatesRemoved,
startTime: _startTime, startTime: _startTime,
endTime: _endTime, endTime: _endTime,
key: ValueKey([_startTime, _endTime]), onStartChanged: onStartChanged,
// TODO(Joey): Extract these onEndChanged: onEndChanged,
onStartChanged: (start) => setState(() {
_startTime = start;
}),
// TODO(Joey): Extract these
onEndChanged: (end) => setState(() {
_endTime = end;
}),
);
var pauseSelection = PauseSelection(
breaks: _availability.breaks breaks: _availability.breaks
.map(BreakViewModel.fromAvailabilityBreakModel) .map(BreakViewModel.fromAvailabilityBreakModel)
.toList(), .toList(),
editingTemplate: false, onBreaksChanged: onBreaksChanged,
// TODO(Joey): Extract these sidePadding: spacing.sidePadding,
onBreaksChanged: (breaks) { bottomButtonPadding: spacing.bottomButtonPadding,
setState(() { saveButton: saveButton,
_availability = _availability.copyWith( onExit: widget.onExit,
breaks: breaks.map((b) => b.toBreak()).toList(), );
); }
}); }
},
); class _AvailabilitiesModificationScreenLayout extends StatelessWidget {
const _AvailabilitiesModificationScreenLayout({
// TODO(Joey): this structure is defined multiple times, we should create required this.dateRange,
// a widget to handle this consistently required this.clearAvailability,
var body = CustomScrollView( required this.onClearSection,
slivers: [ required this.selectedTemplates,
SliverPadding( required this.onTemplateSelected,
padding: required this.onTemplatesRemoved,
EdgeInsets.symmetric(horizontal: options.spacing.sidePadding), required this.startTime,
sliver: SliverList.list( required this.endTime,
children: [ required this.onStartChanged,
const SizedBox(height: 40), required this.onEndChanged,
clearSection, required this.breaks,
if (!_clearAvailability) ...[ required this.onBreaksChanged,
const SizedBox(height: 24), required this.sidePadding,
templateSelection, required this.bottomButtonPadding,
const SizedBox(height: 24), required this.saveButton,
timeSelection, required this.onExit,
// TODO(Joey): Not divisible by 4 });
const SizedBox(height: 26),
pauseSelection, final DateTimeRange dateRange;
], final bool clearAvailability;
], // ignore: avoid_positional_boolean_parameters
), final void Function(bool isChecked) onClearSection;
),
SliverFillRemaining( final List<AvailabilityTemplateModel> selectedTemplates;
fillOverscroll: false, final void Function() onTemplateSelected;
hasScrollBody: false, final void Function() onTemplatesRemoved;
child: Padding(
padding: EdgeInsets.symmetric( final TimeOfDay? startTime;
horizontal: spacing.sidePadding, final TimeOfDay? endTime;
).copyWith( final void Function(TimeOfDay start) onStartChanged;
bottom: spacing.bottomButtonPadding, final void Function(TimeOfDay start) onEndChanged;
),
child: Align( final List<BreakViewModel> breaks;
alignment: Alignment.bottomCenter, final void Function(List<BreakViewModel> breaks) onBreaksChanged;
child: saveButton,
), final double sidePadding;
), final double bottomButtonPadding;
),
], final Widget saveButton;
);
final VoidCallback onExit;
return options.baseScreenBuilder(
context, @override
widget.onExit, Widget build(BuildContext context) {
body, 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],
),
); );
} }
} }

View file

@ -1,5 +1,6 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_availability/src/service/availability_service.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/calendar.dart";
import "package:flutter_availability/src/ui/widgets/template_legend.dart"; import "package:flutter_availability/src/ui/widgets/template_legend.dart";
import "package:flutter_availability/src/util/scope.dart"; import "package:flutter_availability/src/util/scope.dart";
@ -42,7 +43,6 @@ class _AvailabilityOverviewState extends State<AvailabilityOverview> {
var service = availabilityScope.service; var service = availabilityScope.service;
var options = availabilityScope.options; var options = availabilityScope.options;
var translations = options.translations; var translations = options.translations;
var spacing = options.spacing;
var availabilityStream = useMemoized( var availabilityStream = useMemoized(
() => service.getOverviewDataForMonth(_selectedDate), () => service.getOverviewDataForMonth(_selectedDate),
@ -51,18 +51,21 @@ class _AvailabilityOverviewState extends State<AvailabilityOverview> {
var availabilitySnapshot = useStream(availabilityStream); var availabilitySnapshot = useStream(availabilityStream);
// TODO(Joey): Way too complex of a function var selectedAvailabilities = <AvailabilityWithTemplate>[];
var selectedAvailabilities = _selectedRange != null if (_selectedRange != null) {
? availabilitySnapshot.data var availabilities = availabilitySnapshot.data
?.where( ?.where(
(a) => (a) =>
!a.availabilityModel.startDate !a.availabilityModel.startDate
.isBefore(_selectedRange!.start) && .isBefore(_selectedRange!.start) &&
!a.availabilityModel.endDate.isAfter(_selectedRange!.end), !a.availabilityModel.endDate.isAfter(_selectedRange!.end),
) )
.toList() ?? .toList();
<AvailabilityWithTemplate>[]
: <AvailabilityWithTemplate>[]; if (availabilities != null) {
selectedAvailabilities = availabilities;
}
}
var availabilitiesAreSelected = selectedAvailabilities.isNotEmpty; var availabilitiesAreSelected = selectedAvailabilities.isNotEmpty;
@ -94,18 +97,18 @@ class _AvailabilityOverviewState extends State<AvailabilityOverview> {
availabilities: availabilitySnapshot, availabilities: availabilitySnapshot,
); );
// TODO(Joey): too complex of a definition for the function VoidCallback? onButtonPress;
var onButtonPress = _selectedRange == null if (_selectedRange != null) {
? null onButtonPress = () {
: () { widget.onEditDateRange(
widget.onEditDateRange( _selectedRange!,
_selectedRange!, selectedAvailabilities,
selectedAvailabilities, );
); setState(() {
setState(() { _selectedRange = null;
_selectedRange = null; });
}); };
}; }
Future<void> onClearButtonClicked() async { Future<void> onClearButtonClicked() async {
var confirmed = await options.confirmationDialogBuilder( var confirmed = await options.confirmationDialogBuilder(
@ -135,53 +138,25 @@ class _AvailabilityOverviewState extends State<AvailabilityOverview> {
Text(translations.editAvailabilityButton), 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( return options.baseScreenBuilder(
context, context,
widget.onExit, 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,
],
],
),
); );
} }
} }

View file

@ -1,5 +1,7 @@
import "package:flutter/material.dart"; 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/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/color_selection.dart";
import "package:flutter_availability/src/ui/widgets/template_name_input.dart"; import "package:flutter_availability/src/ui/widgets/template_name_input.dart";
import "package:flutter_availability/src/ui/widgets/template_time_break.dart"; import "package:flutter_availability/src/ui/widgets/template_time_break.dart";
@ -47,7 +49,6 @@ class _DayTemplateModificationScreenState
var service = availabilityScope.service; var service = availabilityScope.service;
var options = availabilityScope.options; var options = availabilityScope.options;
var translations = options.translations; var translations = options.translations;
var spacing = options.spacing;
Future<void> onDeletePressed() async { Future<void> onDeletePressed() async {
await service.deleteTemplate(widget.template!); await service.deleteTemplate(widget.template!);
@ -66,97 +67,69 @@ class _DayTemplateModificationScreenState
var canSave = _viewModel.canSave; var canSave = _viewModel.canSave;
var saveButton = options.primaryButtonBuilder(
context,
canSave ? onSavePressed : null,
Text(translations.saveButton),
);
var deleteButton = options.bigTextButtonBuilder( var deleteButton = options.bigTextButtonBuilder(
context, context,
onDeletePressed, onDeletePressed,
Text(translations.deleteTemplateButton), Text(translations.deleteTemplateButton),
); );
var title = Center( void onNameChanged(String name) {
child: Text( setState(() {
translations.dayTemplateTitle, _viewModel = _viewModel.copyWith(name: name);
style: theme.textTheme.displaySmall, });
}
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);
} }
} }

View file

@ -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<Widget> body;
///
final List<Widget> 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),
),
),
),
],
);
}
}