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

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,10 +97,9 @@ 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,
@ -106,6 +108,7 @@ class _AvailabilityOverviewState extends State<AvailabilityOverview> {
_selectedRange = null; _selectedRange = null;
}); });
}; };
}
Future<void> onClearButtonClicked() async { Future<void> onClearButtonClicked() async {
var confirmed = await options.confirmationDialogBuilder( var confirmed = await options.confirmationDialogBuilder(
@ -135,36 +138,18 @@ class _AvailabilityOverviewState extends State<AvailabilityOverview> {
Text(translations.editAvailabilityButton), Text(translations.editAvailabilityButton),
); );
// TODO(Joey): This structure is defined multiple times return options.baseScreenBuilder(
var body = CustomScrollView( context,
slivers: [ widget.onExit,
SliverPadding( BasePage(
padding: EdgeInsets.symmetric(horizontal: spacing.sidePadding), body: [
sliver: SliverList.list(
children: [
const SizedBox(height: 40),
title, title,
const SizedBox(height: 24), const SizedBox(height: 24),
calendar, calendar,
const SizedBox(height: 32), const SizedBox(height: 32),
templateLegend, templateLegend,
const SizedBox(height: 32),
], ],
), buttons: [
),
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, startEditButton,
if (availabilitiesAreSelected) ...[ if (availabilitiesAreSelected) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
@ -172,16 +157,6 @@ class _AvailabilityOverviewState extends State<AvailabilityOverview> {
], ],
], ],
), ),
),
),
),
],
);
return options.baseScreenBuilder(
context,
widget.onExit,
body,
); );
} }
} }

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) {
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( child: Text(
translations.dayTemplateTitle, translations.dayTemplateTitle,
style: theme.textTheme.displaySmall, style: theme.textTheme.displaySmall,
), ),
); ),
const SizedBox(height: 24),
var templateTitleSection = TemplateNameInput( TemplateNameInput(
initialValue: _viewModel.name, initialValue: _viewModel.name,
onNameChanged: (name) { onNameChanged: onNameChanged,
setState(() { ),
_viewModel = _viewModel.copyWith(name: name); const SizedBox(height: 24),
}); TemplateTimeAndBreakSection(
},
);
var colorSection = TemplateColorSelection(
selectedColor: _viewModel.color,
// TODO(Joey): Extract this
onColorSelected: (color) {
setState(() {
_viewModel = _viewModel.copyWith(color: color);
});
},
);
var availabilitySection = TemplateTimeAndBreakSection(
dayData: _viewModel.data, dayData: _viewModel.data,
onDayDataChanged: (data) { onDayDataChanged: onDayDataChanged,
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), const SizedBox(height: 24),
templateTitleSection, TemplateColorSelection(
const SizedBox(height: 24), selectedColor: _viewModel.color,
availabilitySection, onColorSelected: onColorSelected,
const SizedBox(height: 24), ),
colorSection,
const SizedBox(height: 32),
], ],
buttons: [
options.primaryButtonBuilder(
context,
canSave ? onSavePressed : null,
Text(translations.saveButton),
), ),
),
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) ...[ if (widget.template != null) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
deleteButton, 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),
),
),
),
],
);
}
}