mirror of
https://github.com/Iconica-Development/flutter_availability.git
synced 2025-05-20 05:33:44 +02:00
feat: add pause selection with dialog for modification
This commit is contained in:
parent
71d76bba04
commit
aaba808a17
6 changed files with 500 additions and 41 deletions
|
@ -29,8 +29,14 @@ class AvailabilityTranslations {
|
|||
required this.templateColorLabel,
|
||||
required this.time,
|
||||
required this.timeSeparator,
|
||||
required this.timeMinutes,
|
||||
required this.templateTimeLabel,
|
||||
required this.pauseSectionTitle,
|
||||
required this.pauseSectionOptional,
|
||||
required this.pauseDialogTitle,
|
||||
required this.pauseDialogDescription,
|
||||
required this.pauseDialogPeriodTitle,
|
||||
required this.pauseDialogPeriodDescription,
|
||||
required this.saveButton,
|
||||
required this.addButton,
|
||||
required this.timeFormatter,
|
||||
|
@ -59,8 +65,16 @@ class AvailabilityTranslations {
|
|||
this.templateColorLabel = "Colorlabel",
|
||||
this.time = "Time",
|
||||
this.timeSeparator = "to",
|
||||
this.timeMinutes = "minutes",
|
||||
this.templateTimeLabel = "When are you available?",
|
||||
this.pauseSectionTitle = "Add a pause (optional)",
|
||||
this.pauseSectionTitle = "Add a pause",
|
||||
this.pauseSectionOptional = "(Optional)",
|
||||
this.pauseDialogTitle = "Add a pause",
|
||||
this.pauseDialogDescription = "Add a pause to your availability. "
|
||||
"Choose how long you want to take a break",
|
||||
this.pauseDialogPeriodTitle = "Time slot",
|
||||
this.pauseDialogPeriodDescription =
|
||||
"Select between which times you want to take a break",
|
||||
this.saveButton = "Save",
|
||||
this.addButton = "Add",
|
||||
this.monthYearFormatter = _defaultMonthYearFormatter,
|
||||
|
@ -122,12 +136,31 @@ class AvailabilityTranslations {
|
|||
/// The text between start and end time
|
||||
final String timeSeparator;
|
||||
|
||||
/// The text used for minutes
|
||||
final String timeMinutes;
|
||||
|
||||
/// The label for the template time input
|
||||
final String templateTimeLabel;
|
||||
|
||||
/// The title for pause configuration sections
|
||||
final String pauseSectionTitle;
|
||||
|
||||
/// The label for optional indication on pause sections
|
||||
final String pauseSectionOptional;
|
||||
|
||||
/// The title for the pause dialog
|
||||
final String pauseDialogTitle;
|
||||
|
||||
/// The description for the pause dialog displayed below the title
|
||||
final String pauseDialogDescription;
|
||||
|
||||
/// The title for the section in the pause dialog where you select the period
|
||||
final String pauseDialogPeriodTitle;
|
||||
|
||||
/// The description for the section in the pause dialog where you select
|
||||
/// the period
|
||||
final String pauseDialogPeriodDescription;
|
||||
|
||||
/// The text on the save button
|
||||
final String saveButton;
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_availability/src/ui/widgets/color_selection.dart";
|
||||
import "package:flutter_availability/src/ui/widgets/pause_selection.dart";
|
||||
import "package:flutter_availability/src/ui/widgets/template_name_input.dart";
|
||||
import "package:flutter_availability/src/ui/widgets/template_time_selection.dart";
|
||||
import "package:flutter_availability/src/util/scope.dart";
|
||||
|
@ -135,9 +136,16 @@ class _AvailabilityDayTemplateEditState
|
|||
},
|
||||
);
|
||||
|
||||
var pauseSection = const SizedBox(
|
||||
height: 200,
|
||||
child: Placeholder(),
|
||||
var pauseSection = PauseSelection(
|
||||
breaks: (_template.templateData as DayTemplateData).breaks,
|
||||
onBreaksChanged: (breaks) {
|
||||
setState(() {
|
||||
_template = _template.copyWith(
|
||||
templateData: (_template.templateData as DayTemplateData)
|
||||
.copyWith(breaks: breaks),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
var body = CustomScrollView(
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_availability/src/ui/widgets/input_fields.dart";
|
||||
import "package:flutter_availability/src/util/scope.dart";
|
||||
|
||||
///
|
||||
class TimeSelection extends StatelessWidget {
|
||||
///
|
||||
const TimeSelection({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.onStartChanged,
|
||||
required this.onEndChanged,
|
||||
this.crossAxisAlignment = CrossAxisAlignment.start,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The title of the time selection
|
||||
final String title;
|
||||
|
||||
/// The description of the time selection
|
||||
final String description;
|
||||
|
||||
/// the axis aligment for the column
|
||||
final CrossAxisAlignment crossAxisAlignment;
|
||||
|
||||
///
|
||||
final DateTime? startTime;
|
||||
|
||||
///
|
||||
final DateTime? endTime;
|
||||
|
||||
///
|
||||
final void Function(DateTime) onStartChanged;
|
||||
|
||||
///
|
||||
final void Function(DateTime) onEndChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
var textTheme = theme.textTheme;
|
||||
var availabilityScope = AvailabilityScope.of(context);
|
||||
var options = availabilityScope.options;
|
||||
var translations = options.translations;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: crossAxisAlignment,
|
||||
children: [
|
||||
Text(title, style: textTheme.titleMedium),
|
||||
const SizedBox(height: 4),
|
||||
Text(description, style: textTheme.bodyMedium),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: TimeInputField(
|
||||
initialValue: startTime,
|
||||
onTimeChanged: onStartChanged,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
translations.timeSeparator,
|
||||
style: textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: TimeInputField(
|
||||
initialValue: endTime,
|
||||
onTimeChanged: onEndChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -59,3 +59,47 @@ class TimeInputField extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An input field for giving a duration in minutes
|
||||
class DurationInputField extends StatelessWidget {
|
||||
///
|
||||
const DurationInputField({
|
||||
required this.initialValue,
|
||||
required this.onDurationChanged,
|
||||
super.key,
|
||||
});
|
||||
|
||||
///
|
||||
final Duration? initialValue;
|
||||
|
||||
///
|
||||
final void Function(Duration) onDurationChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var availabilityScope = AvailabilityScope.of(context);
|
||||
var options = availabilityScope.options;
|
||||
var translations = options.translations;
|
||||
|
||||
void onFieldChanged(String value) {
|
||||
var duration = int.tryParse(value);
|
||||
if (duration != null) {
|
||||
onDurationChanged(Duration(minutes: duration));
|
||||
}
|
||||
}
|
||||
|
||||
return TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: translations.time,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
suffixIcon: const Icon(Icons.access_time),
|
||||
),
|
||||
initialValue: initialValue?.inMinutes.toString(),
|
||||
keyboardType: TextInputType.number,
|
||||
style: options.textStyles.inputFieldTextStyle,
|
||||
onChanged: onFieldChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,319 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_availability/flutter_availability.dart";
|
||||
import "package:flutter_availability/src/service/availability_service.dart";
|
||||
import "package:flutter_availability/src/ui/widgets/generic_time_selection.dart";
|
||||
import "package:flutter_availability/src/ui/widgets/input_fields.dart";
|
||||
import "package:flutter_availability/src/util/scope.dart";
|
||||
import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart";
|
||||
|
||||
///
|
||||
class PauseSelection extends StatelessWidget {
|
||||
///
|
||||
const PauseSelection({
|
||||
required this.breaks,
|
||||
required this.onBreaksChanged,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The breaks that are currently set
|
||||
final List<AvailabilityBreakModel> breaks;
|
||||
|
||||
/// Callback for when the breaks are changed
|
||||
final void Function(List<AvailabilityBreakModel>) onBreaksChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
var textTheme = theme.textTheme;
|
||||
var availabilityScope = AvailabilityScope.of(context);
|
||||
var options = availabilityScope.options;
|
||||
var translations = options.translations;
|
||||
|
||||
Future<AvailabilityBreakModel?> openBreakDialog(
|
||||
AvailabilityBreakModel? initialBreak,
|
||||
) async =>
|
||||
AvailabilityBreakSelectionDialog.show(
|
||||
context,
|
||||
userId: availabilityScope.userId,
|
||||
options: options,
|
||||
service: availabilityScope.service,
|
||||
);
|
||||
|
||||
Future<void> onClickAddBreak() async {
|
||||
var newBreak = await openBreakDialog(null);
|
||||
if (newBreak == null) return;
|
||||
|
||||
var updatedBreaks = [...breaks, newBreak];
|
||||
onBreaksChanged(updatedBreaks);
|
||||
}
|
||||
|
||||
Future<void> onEditBreak(AvailabilityBreakModel availabilityBreak) async {
|
||||
var updatedBreak = await openBreakDialog(availabilityBreak);
|
||||
if (updatedBreak == null) return;
|
||||
|
||||
var updatedBreaks = [...breaks, updatedBreak];
|
||||
onBreaksChanged(updatedBreaks);
|
||||
}
|
||||
|
||||
void onDeleteBreak(AvailabilityBreakModel availabilityBreak) {
|
||||
var updatedBreaks = breaks.where((b) => b != availabilityBreak).toList();
|
||||
onBreaksChanged(updatedBreaks);
|
||||
}
|
||||
|
||||
var sortedBreaks = breaks.toList()
|
||||
..sort((a, b) => a.startTime.compareTo(b.startTime));
|
||||
|
||||
var addButton = DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.primary,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: options.textButtonBuilder(
|
||||
context,
|
||||
onClickAddBreak,
|
||||
Text(translations.addButton),
|
||||
),
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
translations.pauseSectionTitle,
|
||||
style: textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
translations.pauseSectionOptional,
|
||||
style: textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
for (var breakModel in sortedBreaks) ...[
|
||||
const SizedBox(height: 8),
|
||||
BreakDisplay(
|
||||
breakModel: breakModel,
|
||||
onRemove: () => onDeleteBreak(breakModel),
|
||||
onClick: () async => onEditBreak(breakModel),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
addButton,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays a single break with buttons to edit or delete it
|
||||
class BreakDisplay extends StatelessWidget {
|
||||
/// Creates a new break display
|
||||
const BreakDisplay({
|
||||
required this.breakModel,
|
||||
required this.onRemove,
|
||||
required this.onClick,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The break to display
|
||||
final AvailabilityBreakModel breakModel;
|
||||
|
||||
/// Callback for when the minus button is clicked
|
||||
final VoidCallback onRemove;
|
||||
|
||||
/// Callback for when the break is clicked except for the minus button
|
||||
final VoidCallback onClick;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
var availabilityScope = AvailabilityScope.of(context);
|
||||
var options = availabilityScope.options;
|
||||
var colors = options.colors;
|
||||
var translations = options.translations;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onClick,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colors.selectedDayColor,
|
||||
border: Border.all(color: theme.colorScheme.primary, width: 1),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"${breakModel.duration.inMinutes} "
|
||||
"${translations.timeMinutes} | "
|
||||
"${translations.timeFormatter(context, breakModel.startTime)} - "
|
||||
"${translations.timeFormatter(context, breakModel.endTime)}",
|
||||
),
|
||||
const Spacer(),
|
||||
GestureDetector(onTap: onRemove, child: const Icon(Icons.remove)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
class AvailabilityBreakSelectionDialog extends StatefulWidget {
|
||||
///
|
||||
const AvailabilityBreakSelectionDialog({
|
||||
required this.initialBreak,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The initial break to show in the dialog if any
|
||||
final AvailabilityBreakModel? initialBreak;
|
||||
|
||||
/// Opens the dialog to add a break
|
||||
static Future<AvailabilityBreakModel?> show(
|
||||
BuildContext context, {
|
||||
required AvailabilityOptions options,
|
||||
required String userId,
|
||||
required AvailabilityService service,
|
||||
AvailabilityBreakModel? initialBreak,
|
||||
}) async =>
|
||||
showModalBottomSheet<AvailabilityBreakModel>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
builder: (context) => AvailabilityScope(
|
||||
userId: userId,
|
||||
options: options,
|
||||
service: service,
|
||||
child: AvailabilityBreakSelectionDialog(
|
||||
initialBreak: initialBreak,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
State<AvailabilityBreakSelectionDialog> createState() =>
|
||||
_AvailabilityBreakSelectionDialogState();
|
||||
}
|
||||
|
||||
class _AvailabilityBreakSelectionDialogState
|
||||
extends State<AvailabilityBreakSelectionDialog> {
|
||||
late DateTime? _startTime;
|
||||
late DateTime? _endTime;
|
||||
late Duration? _duration;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startTime = widget.initialBreak?.startTime;
|
||||
_endTime = widget.initialBreak?.endTime;
|
||||
_duration = widget.initialBreak?.duration;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
var textTheme = theme.textTheme;
|
||||
var availabilityScope = AvailabilityScope.of(context);
|
||||
var options = availabilityScope.options;
|
||||
var translations = options.translations;
|
||||
var spacing = options.spacing;
|
||||
|
||||
void onUpdateDuration(Duration duration) {
|
||||
setState(() {
|
||||
_duration = duration;
|
||||
});
|
||||
}
|
||||
|
||||
void onUpdateStart(DateTime time) {
|
||||
setState(() {
|
||||
_startTime = time;
|
||||
});
|
||||
}
|
||||
|
||||
void onUpdateEnd(DateTime time) {
|
||||
setState(() {
|
||||
_endTime = time;
|
||||
});
|
||||
}
|
||||
|
||||
var canSave = _startTime != null && _endTime != null;
|
||||
|
||||
var onSaveButtonPress = canSave
|
||||
? () {
|
||||
var breakModel = AvailabilityBreakModel(
|
||||
startTime: _startTime!,
|
||||
endTime: _endTime!,
|
||||
duration: _duration,
|
||||
);
|
||||
Navigator.of(context).pop(breakModel);
|
||||
}
|
||||
: null;
|
||||
|
||||
var saveButton = options.primaryButtonBuilder(
|
||||
context,
|
||||
onSaveButtonPress,
|
||||
Text(
|
||||
widget.initialBreak == null
|
||||
? translations.addButton
|
||||
: translations.saveButton,
|
||||
),
|
||||
);
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: spacing.sidePadding,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 44),
|
||||
Text(translations.pauseDialogTitle, style: textTheme.titleMedium),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
translations.pauseDialogDescription,
|
||||
style: textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: DurationInputField(
|
||||
initialValue: _duration,
|
||||
onDurationChanged: onUpdateDuration,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
TimeSelection(
|
||||
// rebuild the widget when the start or end time changes
|
||||
key: ValueKey([_startTime, _endTime]),
|
||||
title: translations.pauseDialogPeriodTitle,
|
||||
description: translations.pauseDialogPeriodDescription,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
startTime: _startTime,
|
||||
endTime: _endTime,
|
||||
onStartChanged: onUpdateStart,
|
||||
onEndChanged: onUpdateEnd,
|
||||
),
|
||||
const SizedBox(height: 36),
|
||||
saveButton,
|
||||
SizedBox(height: spacing.bottomButtonPadding),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_availability/src/ui/widgets/input_fields.dart";
|
||||
import "package:flutter_availability/src/ui/widgets/generic_time_selection.dart";
|
||||
import "package:flutter_availability/src/util/scope.dart";
|
||||
|
||||
///
|
||||
|
@ -27,46 +27,17 @@ class TemplateTimeSelection extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
var textTheme = theme.textTheme;
|
||||
var availabilityScope = AvailabilityScope.of(context);
|
||||
var options = availabilityScope.options;
|
||||
var translations = options.translations;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(translations.time, style: textTheme.titleMedium),
|
||||
const SizedBox(height: 4),
|
||||
Text(translations.templateTimeLabel, style: textTheme.bodyMedium),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: TimeInputField(
|
||||
initialValue: startTime,
|
||||
onTimeChanged: onStartChanged,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
translations.timeSeparator,
|
||||
style: textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: TimeInputField(
|
||||
initialValue: endTime,
|
||||
onTimeChanged: onEndChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
return TimeSelection(
|
||||
title: translations.time,
|
||||
description: translations.templateTimeLabel,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
onStartChanged: onStartChanged,
|
||||
onEndChanged: onEndChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue