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(
fillOverscroll: false,
hasScrollBody: false,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: spacing.sidePadding,
).copyWith(
bottom: spacing.bottomButtonPadding,
),
child: Align(
alignment: Alignment.bottomCenter,
child: saveButton,
),
),
),
],
); );
}
}
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<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( return options.baseScreenBuilder(
context, context,
widget.onExit, onExit,
body, 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),
),
),
),
],
);
}
}