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_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) {
// ignore: avoid_positional_boolean_parameters
void onClearSection(bool isChecked) {
setState(() {
_clearAvailability = isChecked;
});
},
);
}
var templateSelection = AvailabilityTemplateSelection(
selectedTemplates: _selectedTemplates,
// TODO(Joey): Extract this function
onTemplateAdd: () async {
Future<void> onTemplateSelected() async {
var template = await widget.onTemplateSelection();
if (template != null) {
setState(() {
_selectedTemplates = [template];
});
}
},
// TODO(Joey): Extract these functions
onTemplatesRemoved: () {
}
void onTemplatesRemoved() {
setState(() {
_selectedTemplates = [];
});
},
);
}
var timeSelection = AvailabilityTimeSelection(
dateRange: widget.dateRange,
startTime: _startTime,
endTime: _endTime,
key: ValueKey([_startTime, _endTime]),
// TODO(Joey): Extract these
onStartChanged: (start) => setState(() {
void onStartChanged(TimeOfDay start) {
setState(() {
_startTime = start;
}),
// TODO(Joey): Extract these
onEndChanged: (end) => setState(() {
_endTime = end;
}),
);
});
}
var pauseSelection = PauseSelection(
breaks: _availability.breaks
.map(BreakViewModel.fromAvailabilityBreakModel)
.toList(),
editingTemplate: false,
// TODO(Joey): Extract these
onBreaksChanged: (breaks) {
void onEndChanged(TimeOfDay end) {
setState(() {
_endTime = end;
});
}
void onBreaksChanged(List<BreakViewModel> 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 _AvailabilitiesModificationScreenLayout(
dateRange: widget.dateRange,
clearAvailability: _clearAvailability,
onClearSection: onClearSection,
selectedTemplates: _selectedTemplates,
onTemplateSelected: onTemplateSelected,
onTemplatesRemoved: onTemplatesRemoved,
startTime: _startTime,
endTime: _endTime,
onStartChanged: onStartChanged,
onEndChanged: onEndChanged,
breaks: _availability.breaks
.map(BreakViewModel.fromAvailabilityBreakModel)
.toList(),
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<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,
widget.onExit,
body,
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_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<AvailabilityOverview> {
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<AvailabilityOverview> {
var availabilitySnapshot = useStream(availabilityStream);
// TODO(Joey): Way too complex of a function
var selectedAvailabilities = _selectedRange != null
? availabilitySnapshot.data
var selectedAvailabilities = <AvailabilityWithTemplate>[];
if (_selectedRange != null) {
var availabilities = availabilitySnapshot.data
?.where(
(a) =>
!a.availabilityModel.startDate
.isBefore(_selectedRange!.start) &&
!a.availabilityModel.endDate.isAfter(_selectedRange!.end),
)
.toList() ??
<AvailabilityWithTemplate>[]
: <AvailabilityWithTemplate>[];
.toList();
if (availabilities != null) {
selectedAvailabilities = availabilities;
}
}
var availabilitiesAreSelected = selectedAvailabilities.isNotEmpty;
@ -94,10 +97,9 @@ class _AvailabilityOverviewState extends State<AvailabilityOverview> {
availabilities: availabilitySnapshot,
);
// TODO(Joey): too complex of a definition for the function
var onButtonPress = _selectedRange == null
? null
: () {
VoidCallback? onButtonPress;
if (_selectedRange != null) {
onButtonPress = () {
widget.onEditDateRange(
_selectedRange!,
selectedAvailabilities,
@ -106,6 +108,7 @@ class _AvailabilityOverviewState extends State<AvailabilityOverview> {
_selectedRange = null;
});
};
}
Future<void> onClearButtonClicked() async {
var confirmed = await options.confirmationDialogBuilder(
@ -135,36 +138,18 @@ class _AvailabilityOverviewState extends State<AvailabilityOverview> {
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),
return options.baseScreenBuilder(
context,
widget.onExit,
BasePage(
body: [
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: [
buttons: [
startEditButton,
if (availabilitiesAreSelected) ...[
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_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<void> 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(
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,
),
);
var templateTitleSection = TemplateNameInput(
),
const SizedBox(height: 24),
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(
onNameChanged: onNameChanged,
),
const SizedBox(height: 24),
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,
onDayDataChanged: onDayDataChanged,
),
const SizedBox(height: 24),
templateTitleSection,
const SizedBox(height: 24),
availabilitySection,
const SizedBox(height: 24),
colorSection,
const SizedBox(height: 32),
TemplateColorSelection(
selectedColor: _viewModel.color,
onColorSelected: onColorSelected,
),
],
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) ...[
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),
),
),
),
],
);
}
}