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) {
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<void> 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<BreakViewModel> 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<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_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
?.where(
(a) =>
!a.availabilityModel.startDate
.isBefore(_selectedRange!.start) &&
!a.availabilityModel.endDate.isAfter(_selectedRange!.end),
)
.toList() ??
<AvailabilityWithTemplate>[]
: <AvailabilityWithTemplate>[];
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();
if (availabilities != null) {
selectedAvailabilities = availabilities;
}
}
var availabilitiesAreSelected = selectedAvailabilities.isNotEmpty;
@ -94,18 +97,18 @@ class _AvailabilityOverviewState extends State<AvailabilityOverview> {
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<void> onClearButtonClicked() async {
var confirmed = await options.confirmationDialogBuilder(
@ -135,53 +138,25 @@ 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),
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,
],
],
),
);
}
}

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(
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);
}
}

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),
),
),
),
],
);
}
}