From beef28700929f04c8bbbd62f71beecb36e170679 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Fri, 15 Nov 2024 13:15:33 +0100 Subject: [PATCH 01/16] feat: ui widget for date picker commponent --- .config/dictionaries/project.dic | 1 + catalyst_voices/.gitignore | 1 + .../date_picker/voices_calendar_picker.dart | 82 +++++ .../date_picker/voices_date_picker_field.dart | 286 ++++++++++++++++++ .../date_picker/voices_time_picker.dart | 70 +++++ .../widgets/text_field/voices_text_field.dart | 34 ++- 6 files changed, 469 insertions(+), 5 deletions(-) create mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index f07f6deb191..e5d8915c529 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -21,6 +21,7 @@ auditability Autolayout autorecalculates autoresizing +autovalidate backendpython bech bimap diff --git a/catalyst_voices/.gitignore b/catalyst_voices/.gitignore index ac235143a1b..88159e97806 100644 --- a/catalyst_voices/.gitignore +++ b/catalyst_voices/.gitignore @@ -1,5 +1,6 @@ ### Dart ### # See https://www.dartlang.org/guides/libraries/private-files +devtools_options.yaml # Files and directories created by pub .dart_tool/ diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart new file mode 100644 index 00000000000..b1da0b0c288 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart @@ -0,0 +1,82 @@ +part of 'voices_date_picker_field.dart'; + +class VoicesCalendarDatePicker extends StatelessWidget { + final ValueChanged onDateSelected; + final VoidCallback cancelEvent; + final DateTime initialDate; + final DateTime firstDate; + final DateTime lastDate; + + factory VoicesCalendarDatePicker({ + Key? key, + required ValueChanged onDateSelected, + required VoidCallback cancelEvent, + DateTime? initialDate, + DateTime? firstDate, + DateTime? lastDate, + }) { + final now = DateTime.now(); + return VoicesCalendarDatePicker._( + key: key, + onDateSelected: onDateSelected, + initialDate: initialDate ?? now, + firstDate: firstDate ?? now, + lastDate: lastDate ?? now, + cancelEvent: cancelEvent, + ); + } + + const VoicesCalendarDatePicker._({ + super.key, + required this.onDateSelected, + required this.initialDate, + required this.firstDate, + required this.lastDate, + required this.cancelEvent, + }); + + @override + Widget build(BuildContext context) { + var selectedDate = DateTime.now(); + return SizedBox( + width: 450, + child: Material( + clipBehavior: Clip.hardEdge, + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1Grey, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: [ + CalendarDatePicker( + initialDate: DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 3650)), + onDateChanged: (val) { + selectedDate = val; + }, + ), + Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + VoicesTextButton( + onTap: cancelEvent, + child: Text(context.l10n.cancelButtonText), + ), + VoicesTextButton( + onTap: () => onDateSelected(selectedDate), + child: Text(context.l10n.snackbarOkButtonText), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart new file mode 100644 index 00000000000..6e2d865f315 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart @@ -0,0 +1,286 @@ +import 'package:catalyst_voices/widgets/text_field/date_picker/date_picker_controller.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:flutter/material.dart'; + +part 'voices_calendar_picker.dart'; +part 'voices_time_picker.dart'; + +enum DateTimePickerType { date, time } + +class ScrollControllerProvider extends InheritedWidget { + final ScrollController scrollController; + + const ScrollControllerProvider({ + super.key, + required this.scrollController, + required super.child, + }); + + static ScrollController of(BuildContext context) { + final provider = + context.dependOnInheritedWidgetOfExactType(); + return provider!.scrollController; + } + + @override + bool updateShouldNotify(ScrollControllerProvider oldWidget) { + return scrollController != oldWidget.scrollController; + } +} + +class VoicesDatePicker extends StatefulWidget { + final DatePickerController controller; + const VoicesDatePicker({ + super.key, + required this.controller, + }); + + @override + State createState() => _VoicesDatePickerState(); +} + +class _VoicesDatePickerState extends State { + @override + Widget build(BuildContext context) { + return Row( + children: [ + _FieldPicker( + controller: widget.controller.calendarPickerController, + pickerType: DateTimePickerType.date, + ), + _FieldPicker( + controller: widget.controller.timePickerController, + pickerType: DateTimePickerType.time, + timeZone: 'UTC', + ), + ], + ); + } + + @override + void dispose() { + super.dispose(); + widget.controller.dispose(); + } +} + +class _FieldPicker extends StatefulWidget { + final FieldDatePickerController controller; + final DateTimePickerType pickerType; + final String timeZone; + const _FieldPicker({ + required this.controller, + required this.pickerType, + this.timeZone = '', + }); + + @override + State<_FieldPicker> createState() => _FieldPickerState(); +} + +class _FieldPickerState extends State<_FieldPicker> { + OverlayEntry? _overlayEntry; + VoidCallback? _scrollListener; + + @override + void dispose() { + super.dispose(); + _overlayEntry?.remove(); + _overlayEntry = null; + } + + void dateOnTap() { + if (_overlayEntry != null) { + _removeOverlay(); + } else { + _removeOverlay(); + + _showOverlay( + VoicesCalendarDatePicker( + onDateSelected: (value) { + _removeOverlay(); + }, + cancelEvent: _removeOverlay, + ), + ); + } + } + + void timeOnTap() { + if (_overlayEntry != null) { + _removeOverlay(); + } else { + _showOverlay( + const VoicesTimePicker(), + ); + } + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: _getWidth, + child: Stack( + alignment: Alignment.center, + children: [ + VoicesTextField( + controller: widget.controller, + validator: (value) => widget.controller.validate(value), + decoration: _getInputDecoration(context), + onFieldSubmitted: (String value) {}, + showSuffix: false, + autovalidateMode: AutovalidateMode.onUserInteraction, + ), + Positioned( + right: 0, + child: VoicesIconButton( + child: _getIcon.buildIcon(), + onTap: () => widget.pickerType == DateTimePickerType.date + ? dateOnTap() + : timeOnTap(), + ), + ), + ], + ), + ); + } + + double get _getWidth => switch (widget.pickerType) { + DateTimePickerType.date => 210, + DateTimePickerType.time => 160, + }; + + SvgGenImage get _getIcon => switch (widget.pickerType) { + DateTimePickerType.date => VoicesAssets.icons.calendar, + DateTimePickerType.time => VoicesAssets.icons.clock, + }; + + VoicesTextFieldDecoration _getInputDecoration(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + return VoicesTextFieldDecoration( + fillColor: theme.colors.elevationsOnSurfaceNeutralLv1Grey, + filled: true, + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: theme.colors.outlineBorderVariant!, + width: 0.75, + ), + borderRadius: switch (widget.pickerType) { + DateTimePickerType.date => const BorderRadius.only( + topLeft: Radius.circular(16), + bottomLeft: Radius.circular(16), + ), + DateTimePickerType.time => const BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + }, + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: theme.primaryColor, + width: 2, + ), + borderRadius: switch (widget.pickerType) { + DateTimePickerType.date => const BorderRadius.only( + topLeft: Radius.circular(16), + bottomLeft: Radius.circular(16), + ), + DateTimePickerType.time => const BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + }, + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colors.errorContainer!, + width: 2, + ), + borderRadius: switch (widget.pickerType) { + DateTimePickerType.date => const BorderRadius.only( + topLeft: Radius.circular(16), + bottomLeft: Radius.circular(16), + ), + DateTimePickerType.time => const BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + }, + ), + hintText: switch (widget.pickerType) { + DateTimePickerType.date => 'DD/MM/YYYY', + DateTimePickerType.time => '00:00 ${widget.timeZone}', + }, + hintStyle: textTheme.bodyLarge?.copyWith( + color: theme.colors.textDisabled, + ), + ); + } + + void _showOverlay(Widget child) { + final overlay = Overlay.of(context, rootOverlay: true); + final renderBox = context.findRenderObject() as RenderBox?; + final scrollController = ScrollControllerProvider.of(context); + final initialPosition = renderBox!.localToGlobal( + Offset(0, scrollController.offset), + ); + + _overlayEntry = OverlayEntry( + builder: (context) => Stack( + children: [ + Positioned.fill( + child: MouseRegion( + opaque: false, + child: GestureDetector( + onTap: _removeOverlay, + behavior: HitTestBehavior.translucent, + excludeFromSemantics: true, + onPanUpdate: null, + onPanDown: null, + onPanCancel: null, + onPanEnd: null, + onPanStart: null, + child: Container( + color: Colors.transparent, + ), + ), + ), + ), + Positioned( + top: initialPosition.dy + + 50 - + (scrollController.hasClients ? scrollController.offset : 0), + left: initialPosition.dx, + child: child, + ), + ], + ), + ); + + void listener() { + if (_overlayEntry != null) { + _overlayEntry?.markNeedsBuild(); + } + } + + scrollController.addListener(listener); + _scrollListener = listener; + + overlay.insert(_overlayEntry!); + } + + void _removeOverlay() { + final scrollController = ScrollControllerProvider.of(context); + if (_scrollListener != null) { + scrollController.removeListener(_scrollListener!); + } + _scrollListener = null; + _overlayEntry?.remove(); + _overlayEntry = null; + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart new file mode 100644 index 00000000000..38fa433e393 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart @@ -0,0 +1,70 @@ +part of 'voices_date_picker_field.dart'; + +class VoicesTimePicker extends StatelessWidget { + const VoicesTimePicker({super.key}); + + List get timeList => _generateTimeList(); + + @override + Widget build(BuildContext context) { + return Material( + child: Container( + height: 300, + width: 150, + decoration: BoxDecoration( + color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1Grey, + borderRadius: BorderRadius.circular(20), + ), + child: ListView( + children: timeList + .map( + (e) => TimeText( + value: e, + ), + ) + .toList(), + ), + ), + ); + } + + List _generateTimeList() { + final times = []; + + for (var hour = 0; hour < 24; hour++) { + for (var minute = 0; minute < 60; minute += 30) { + times.add( + '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}'); + } + } + + return times; + } +} + +class TimeText extends StatelessWidget { + final String value; + const TimeText({ + super.key, + required this.value, + }); + + @override + Widget build(BuildContext context) { + return Material( + clipBehavior: Clip.hardEdge, + color: Colors.transparent, + child: InkWell( + onTap: () {}, + child: Padding( + key: key, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Text( + value, + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart index 31220324918..d992ed31035 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart @@ -68,6 +68,12 @@ class VoicesTextField extends StatefulWidget { /// [TextField.inputFormatters] final List? inputFormatters; + /// Optional suffix icon to be displayed at the end of the text field. + final bool showSuffix; + + /// [AutovalidateMode] + final AutovalidateMode? autovalidateMode; + const VoicesTextField({ super.key, this.controller, @@ -91,6 +97,8 @@ class VoicesTextField extends StatefulWidget { required this.onFieldSubmitted, this.onSaved, this.inputFormatters, + this.showSuffix = true, + this.autovalidateMode, }); @override @@ -182,6 +190,7 @@ class _VoicesTextFieldState extends State { onFieldSubmitted: widget.onFieldSubmitted, onSaved: widget.onSaved, inputFormatters: widget.inputFormatters, + autovalidateMode: widget.autovalidateMode, decoration: InputDecoration( filled: widget.decoration?.filled, fillColor: widget.decoration?.fillColor, @@ -244,10 +253,14 @@ class _VoicesTextFieldState extends State { : textTheme.bodySmall! .copyWith(color: theme.colors.textDisabled), hintText: widget.decoration?.hintText, - hintStyle: widget.enabled - ? textTheme.bodyLarge - : textTheme.bodyLarge! - .copyWith(color: theme.colors.textDisabled), + hintStyle: _getHintStyle( + textTheme, + theme, + orDefault: widget.enabled + ? textTheme.bodyLarge + : textTheme.bodyLarge! + .copyWith(color: theme.colors.textDisabled), + ), errorText: widget.decoration?.errorText ?? _validation.errorMessage, errorMaxLines: widget.decoration?.errorMaxLines, @@ -317,9 +330,16 @@ class _VoicesTextFieldState extends State { } } + TextStyle? _getHintStyle( + TextTheme textTheme, + ThemeData theme, { + TextStyle? orDefault, + }) => + widget.decoration?.hintStyle ?? orDefault; + Widget? _getStatusSuffixWidget() { final showStatusIcon = widget.decoration?.showStatusSuffixIcon ?? true; - if (!showStatusIcon) { + if (!showStatusIcon || !widget.showSuffix) { return null; } @@ -560,6 +580,9 @@ class VoicesTextFieldDecoration { /// [InputDecoration.hintText]. final String? hintText; + /// [InputDecoration.hintStyle]. + final TextStyle? hintStyle; + /// [InputDecoration.errorText]. final String? errorText; @@ -602,6 +625,7 @@ class VoicesTextFieldDecoration { this.labelText, this.helperText, this.hintText, + this.hintStyle, this.errorText, this.errorMaxLines, this.prefixIcon, From 9af15858c7f9c00067b9c5195e4e26545e77b6c0 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Fri, 15 Nov 2024 13:16:11 +0100 Subject: [PATCH 02/16] feat: custom controller for date picker widget --- .../date_picker/date_picker_controller.dart | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart new file mode 100644 index 00000000000..63d45b5a429 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart @@ -0,0 +1,135 @@ +import 'dart:developer'; + +import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +final class DatePickerControllerState extends Equatable { + final DateTime? selectedDate; + final String? selectedTime; + + factory DatePickerControllerState({ + DateTime? selectedDate, + String? selectedTime, + }) { + return DatePickerControllerState._( + selectedDate: selectedDate, + selectedTime: selectedTime, + ); + } + + const DatePickerControllerState._({ + this.selectedDate, + this.selectedTime, + }); + + DatePickerControllerState copyWith({ + Optional? selectedDate, + Optional? selectedTime, + }) { + return DatePickerControllerState( + selectedDate: selectedDate.dataOr(this.selectedDate), + selectedTime: selectedTime.dataOr(this.selectedTime), + ); + } + + @override + List get props => [ + selectedDate, + selectedTime, + ]; +} + +final class DatePickerController + extends ValueNotifier { + CalendarPickerController calendarPickerController = + CalendarPickerController(); + TimePickerController timePickerController = TimePickerController(); + + DatePickerController([super.value = const DatePickerControllerState._()]) { + calendarPickerController.addListener(_onCalendarPickerControllerChanged); + timePickerController.addListener(_onTimePickerControllerChanged); + } + + void _onCalendarPickerControllerChanged() { + if (calendarPickerController.isValid) { + log(timePickerController.text); + value = value.copyWith( + selectedDate: Optional(calendarPickerController.selectedDate), + ); + } + } + + void _onTimePickerControllerChanged() { + if (timePickerController.isValid) { + log(timePickerController.text); + value = value.copyWith( + selectedTime: Optional(timePickerController.text), + ); + } + } + + @override + void dispose() { + super.dispose(); + calendarPickerController.removeListener(_onCalendarPickerControllerChanged); + timePickerController.removeListener(_onTimePickerControllerChanged); + calendarPickerController.dispose(); + timePickerController.dispose(); + } +} + +abstract class FieldDatePickerController extends TextEditingController { + VoicesTextFieldValidationResult validate(String? value); + + bool get isValid => validate(text).status == VoicesTextFieldStatus.success; +} + +class CalendarPickerController extends FieldDatePickerController { + DateTime? get selectedDate => DateTime.tryParse(text); + + @override + VoicesTextFieldValidationResult validate(String? value) { + return const VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.success, + ); + } +} + +class TimePickerController extends FieldDatePickerController { + @override + VoicesTextFieldValidationResult validate(String? value) { + return const VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.success, + ); + } +} + +final class DatePickerControllerScope extends InheritedWidget { + final DatePickerController controller; + + const DatePickerControllerScope({ + super.key, + required this.controller, + required super.child, + }); + + static DatePickerController of(BuildContext context) { + final controller = context + .dependOnInheritedWidgetOfExactType() + ?.controller; + + assert( + controller != null, + 'Unable to find DatePickerControllerScope in widget tree', + ); + + return controller!; + } + + @override + bool updateShouldNotify(covariant DatePickerControllerScope oldWidget) { + return controller != oldWidget.controller; + } +} From 083439e1c308161581b05842b83ea1424e76df42 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Fri, 15 Nov 2024 16:43:43 +0100 Subject: [PATCH 03/16] feat: adding validation to textfields --- .../lib/pages/discovery/discovery_page.dart | 74 +++-- .../text_field/date_picker/base_picker.dart | 266 ++++++++++++++++++ .../date_picker/date_picker_controller.dart | 126 ++++++++- .../date_picker/voices_calendar_picker.dart | 10 +- .../date_picker/voices_date_picker_field.dart | 226 +-------------- .../date_picker/voices_time_picker.dart | 17 +- .../widgets/text_field/voices_text_field.dart | 6 +- 7 files changed, 455 insertions(+), 270 deletions(-) create mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart diff --git a/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart b/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart index 7049932a0dd..ee25863a4aa 100644 --- a/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart @@ -2,45 +2,64 @@ import 'dart:async'; import 'package:catalyst_voices/pages/discovery/current_status_text.dart'; import 'package:catalyst_voices/pages/discovery/toggle_state_text.dart'; +import 'package:catalyst_voices/widgets/text_field/date_picker/date_picker_controller.dart'; +import 'package:catalyst_voices/widgets/text_field/date_picker/voices_date_picker_field.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:flutter/material.dart'; -class DiscoveryPage extends StatelessWidget { +class DiscoveryPage extends StatefulWidget { const DiscoveryPage({ super.key, }); + @override + State createState() => _DiscoveryPageState(); +} + +class _DiscoveryPageState extends State { + final _scrollController = ScrollController(); + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return CustomScrollView( - slivers: [ - const SliverToBoxAdapter(child: _SpacesNavigationLocation()), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 32) - .add(const EdgeInsets.only(bottom: 32)), - sliver: SliverList( - delegate: SliverChildListDelegate( - [ - const _Segment(key: ValueKey('Segment1Key')), - const SizedBox(height: 24), - const _Segment(key: ValueKey('Segment2Key')), - const SizedBox(height: 24), - const _Segment(key: ValueKey('Segment3Key')), - ], + return ScrollControllerProvider( + scrollController: _scrollController, + child: CustomScrollView( + controller: _scrollController, + slivers: [ + const SliverToBoxAdapter(child: _SpacesNavigationLocation()), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 32) + .add(const EdgeInsets.only(bottom: 32)), + sliver: SliverList( + delegate: SliverChildListDelegate( + [ + const _Segment(key: ValueKey('Segment1Key')), + const SizedBox(height: 24), + const _Segment(key: ValueKey('Segment2Key')), + const SizedBox(height: 24), + const _Segment(key: ValueKey('Segment3Key')), + ], + ), ), ), - ), - const SliverFillRemaining( - hasScrollBody: false, - child: Column( - children: [ - Spacer(), - StandardLinksPageFooter(), - ], + const SliverFillRemaining( + hasScrollBody: false, + child: Column( + children: [ + Spacer(), + StandardLinksPageFooter(), + ], + ), ), - ), - ], + ], + ), ); } } @@ -80,6 +99,9 @@ class _Segment extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + VoicesDatePicker( + controller: DatePickerController(), + ), const CurrentUserStatusText(), const SizedBox(height: 8), const ToggleStateText(), diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart new file mode 100644 index 00000000000..9edfb984853 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart @@ -0,0 +1,266 @@ +part of 'voices_date_picker_field.dart'; + +abstract class BasePicker extends StatefulWidget { + final VoidCallback? onRemoveOverlay; + final DateTimePickerType pickerType; + + const BasePicker({ + super.key, + required this.pickerType, + this.onRemoveOverlay, + }); +} + +class CalendarFieldPicker extends BasePicker { + final CalendarPickerController controller; + + const CalendarFieldPicker({ + super.key, + required this.controller, + super.onRemoveOverlay, + }) : super(pickerType: DateTimePickerType.date); + + @override + State createState() => _CalendarFieldPickerState(); +} + +class TimeFieldPicker extends BasePicker { + final TimePickerController controller; + final String timeZone; + + const TimeFieldPicker({ + super.key, + required this.controller, + required this.timeZone, + super.onRemoveOverlay, + }) : super(pickerType: DateTimePickerType.time); + + @override + State createState() => _TimeFieldPickerState(); +} + +abstract class _BasePickerState extends State { + OverlayEntry? _overlayEntry; + VoidCallback? _scrollListener; + + @override + void dispose() { + super.dispose(); + _overlayEntry?.remove(); + _overlayEntry = null; + } + + double get _getWidth => switch (widget.pickerType) { + DateTimePickerType.date => 220, + DateTimePickerType.time => 170, + }; + + BorderRadius get _getBorderRadius => switch (widget.pickerType) { + DateTimePickerType.date => const BorderRadius.only( + topLeft: Radius.circular(16), + bottomLeft: Radius.circular(16), + ), + DateTimePickerType.time => const BorderRadius.only( + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + }; + + String get _getHintText => switch (widget.pickerType) { + DateTimePickerType.date => 'DD/MM/YYYY', + DateTimePickerType.time when widget is TimeFieldPicker => + '00:00 ${(widget as TimeFieldPicker).timeZone}', + _ => '00:00', + }; + + SvgGenImage get _getIcon => switch (widget.pickerType) { + DateTimePickerType.date => VoicesAssets.icons.calendar, + DateTimePickerType.time => VoicesAssets.icons.clock, + }; + + void _removeOverlay() { + final scrollController = ScrollControllerProvider.of(context); + if (_scrollListener != null) { + scrollController.removeListener(_scrollListener!); + } + _scrollListener = null; + _overlayEntry?.remove(); + _overlayEntry = null; + widget.onRemoveOverlay?.call(); + } + + void _showOverlay(Widget child) { + final overlay = Overlay.of(context, rootOverlay: true); + final renderBox = context.findRenderObject() as RenderBox?; + final scrollController = ScrollControllerProvider.of(context); + final initialPosition = renderBox!.localToGlobal( + Offset(0, scrollController.offset), + ); + + _overlayEntry = OverlayEntry( + builder: (context) => Stack( + children: [ + Positioned.fill( + child: MouseRegion( + opaque: false, + child: GestureDetector( + onTap: _removeOverlay, + behavior: HitTestBehavior.translucent, + excludeFromSemantics: true, + onPanUpdate: null, + onPanDown: null, + onPanCancel: null, + onPanEnd: null, + onPanStart: null, + child: Container( + color: Colors.transparent, + ), + ), + ), + ), + Positioned( + top: initialPosition.dy + + 50 - + (scrollController.hasClients ? scrollController.offset : 0), + left: initialPosition.dx, + child: child, + ), + ], + ), + ); + + void listener() { + if (_overlayEntry != null) { + _overlayEntry?.markNeedsBuild(); + } + } + + scrollController.addListener(listener); + _scrollListener = listener; + + overlay.insert(_overlayEntry!); + } + + VoicesTextFieldDecoration _getInputDecoration( + BuildContext context, + Widget suffixIcon, + ) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + return VoicesTextFieldDecoration( + suffixIcon: suffixIcon, + fillColor: theme.colors.elevationsOnSurfaceNeutralLv1Grey, + filled: true, + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: theme.colors.outlineBorderVariant!, + width: 0.75, + ), + borderRadius: _getBorderRadius, + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: theme.primaryColor, + width: 2, + ), + borderRadius: _getBorderRadius, + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + width: 2, + ), + borderRadius: _getBorderRadius, + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + width: 2, + ), + borderRadius: _getBorderRadius, + ), + hintText: _getHintText, + hintStyle: textTheme.bodyLarge?.copyWith( + color: theme.colors.textDisabled, + ), + errorMaxLines: 2, + ); + } +} + +class _CalendarFieldPickerState extends _BasePickerState { + void _onTap() { + if (_overlayEntry != null) { + _removeOverlay(); + } else { + _removeOverlay(); + _showOverlay( + VoicesCalendarDatePicker( + initialDate: widget.controller.selectedValue, + onDateSelected: (value) { + _removeOverlay(); + widget.controller.setValue(value); + }, + cancelEvent: _removeOverlay, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: _getWidth, + child: VoicesTextField( + controller: widget.controller, + validator: (value) => widget.controller.validate(value), + decoration: _getInputDecoration( + context, + VoicesIconButton( + onTap: _onTap, + child: _getIcon.buildIcon(), + ), + ), + onFieldSubmitted: (String value) {}, + autovalidateMode: AutovalidateMode.onUserInteraction, + ), + ); + } +} + +class _TimeFieldPickerState extends _BasePickerState { + void _onTap() { + if (_overlayEntry != null) { + _removeOverlay(); + } else { + _showOverlay( + VoicesTimePicker( + onTap: (value) { + _removeOverlay(); + widget.controller.setValue(value); + }, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: _getWidth, + child: VoicesTextField( + controller: widget.controller, + validator: (value) => widget.controller.validate(value), + decoration: _getInputDecoration( + context, + VoicesIconButton( + onTap: _onTap, + child: _getIcon.buildIcon(), + ), + ), + onFieldSubmitted: (String value) {}, + autovalidateMode: AutovalidateMode.onUserInteraction, + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart index 63d45b5a429..5948b1b2332 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart @@ -1,14 +1,15 @@ -import 'dart:developer'; - import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; final class DatePickerControllerState extends Equatable { final DateTime? selectedDate; final String? selectedTime; + bool get isValid => selectedDate != null && selectedTime != null; + factory DatePickerControllerState({ DateTime? selectedDate, String? selectedTime, @@ -54,16 +55,14 @@ final class DatePickerController void _onCalendarPickerControllerChanged() { if (calendarPickerController.isValid) { - log(timePickerController.text); value = value.copyWith( - selectedDate: Optional(calendarPickerController.selectedDate), + selectedDate: Optional(calendarPickerController.selectedValue), ); } } void _onTimePickerControllerChanged() { if (timePickerController.isValid) { - log(timePickerController.text); value = value.copyWith( selectedTime: Optional(timePickerController.text), ); @@ -80,30 +79,137 @@ final class DatePickerController } } -abstract class FieldDatePickerController extends TextEditingController { - VoicesTextFieldValidationResult validate(String? value); +sealed class FieldDatePickerController extends TextEditingController { + abstract final String pattern; bool get isValid => validate(text).status == VoicesTextFieldStatus.success; + T get selectedValue; + + VoicesTextFieldValidationResult validate(String? value); + + void setValue(T newValue); } -class CalendarPickerController extends FieldDatePickerController { - DateTime? get selectedDate => DateTime.tryParse(text); +final class CalendarPickerController + extends FieldDatePickerController { + @override + DateTime? get selectedValue { + if (text.isEmpty) return null; + + final parts = text.split('/'); + if (parts.length != 3) return null; + + try { + final day = int.parse(parts[0]); + final month = int.parse(parts[1]); + final year = int.parse(parts[2]); + + if (month < 1 || month > 12) return null; + if (day < 1 || day > 31) return null; + if (year < 1900 || year > 2100) return null; + + return DateTime(year, month, day); + } catch (e) { + return null; + } + } + + @override + String get pattern => 'DD/MM/YYYY'; @override VoicesTextFieldValidationResult validate(String? value) { + final today = DateTime.now(); + final maxDate = DateTime(today.year + 1, today.month, today.day); + final notValidFormat = VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.error, + errorMessage: 'Format: "$pattern"', + ); + + if (value == null || value == '') { + return const VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.success, + ); + } + + if (value.length != 10) return notValidFormat; + + final dateRegex = RegExp(r'^(\d{2})/(\d{2})/(\d{4})$'); + if (!dateRegex.hasMatch(value)) return notValidFormat; + + final parts = value.split('/'); + final day = int.parse(parts[0]); + final month = int.parse(parts[1]); + final year = int.parse(parts[2]); + + if (month < 1 || month > 12) return notValidFormat; + if (day < 1 || day > 31) return notValidFormat; + + final inputDate = DateTime(year, month, day); + + if (inputDate.isBefore(today.subtract(const Duration(days: 1))) || + inputDate.isAfter(maxDate)) { + return VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.error, + errorMessage: + 'Date must be between ${today.day}/${today.month}/${today.year} and ${maxDate.day}/${maxDate.month}/${maxDate.year}', + ); + } + + // Check days in month + final daysInMonth = DateTime(year, month + 1, 0).day; + if (day > daysInMonth) { + return VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.error, + errorMessage: 'Day must be between 1 and $daysInMonth', + ); + } + return const VoicesTextFieldValidationResult( status: VoicesTextFieldStatus.success, ); } + + @override + void setValue(DateTime? newValue) { + if (newValue == null) return; + final newT = newValue; + final formatter = DateFormat('dd/MM/yyyy'); + value = TextEditingValue(text: formatter.format(newT)); + } } -class TimePickerController extends FieldDatePickerController { +final class TimePickerController extends FieldDatePickerController { + @override + String get pattern => 'HH:MM'; + + @override + String? get selectedValue => text; + @override VoicesTextFieldValidationResult validate(String? value) { + if (value == null || value == '') { + return const VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.success, + ); + } + final pattern = RegExp(r'^(0?[0-9]|1[0-9]|2[0-3]):([0-5][0-9])$'); + if (!pattern.hasMatch(value)) { + return const VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.error, + errorMessage: 'Format: "00:00"', + ); + } + return const VoicesTextFieldValidationResult( status: VoicesTextFieldStatus.success, ); } + + @override + void setValue(String? newValue) { + value = TextEditingValue(text: newValue.toString()); + } } final class DatePickerControllerScope extends InheritedWidget { diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart index b1da0b0c288..05f6f4599e7 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart @@ -16,12 +16,13 @@ class VoicesCalendarDatePicker extends StatelessWidget { DateTime? lastDate, }) { final now = DateTime.now(); + final maxDate = DateTime(now.year + 1, now.month, now.day); return VoicesCalendarDatePicker._( key: key, onDateSelected: onDateSelected, initialDate: initialDate ?? now, firstDate: firstDate ?? now, - lastDate: lastDate ?? now, + lastDate: lastDate ?? maxDate, cancelEvent: cancelEvent, ); } @@ -42,6 +43,7 @@ class VoicesCalendarDatePicker extends StatelessWidget { width: 450, child: Material( clipBehavior: Clip.hardEdge, + color: Colors.transparent, child: DecoratedBox( decoration: BoxDecoration( color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1Grey, @@ -50,9 +52,9 @@ class VoicesCalendarDatePicker extends StatelessWidget { child: Column( children: [ CalendarDatePicker( - initialDate: DateTime.now(), - firstDate: DateTime.now(), - lastDate: DateTime.now().add(const Duration(days: 3650)), + initialDate: initialDate, + firstDate: firstDate, + lastDate: lastDate, onDateChanged: (val) { selectedDate = val; }, diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart index 6e2d865f315..0acbdea6fa6 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart @@ -5,6 +5,7 @@ import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:flutter/material.dart'; +part 'base_picker.dart'; part 'voices_calendar_picker.dart'; part 'voices_time_picker.dart'; @@ -46,14 +47,13 @@ class _VoicesDatePickerState extends State { @override Widget build(BuildContext context) { return Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _FieldPicker( + CalendarFieldPicker( controller: widget.controller.calendarPickerController, - pickerType: DateTimePickerType.date, ), - _FieldPicker( + TimeFieldPicker( controller: widget.controller.timePickerController, - pickerType: DateTimePickerType.time, timeZone: 'UTC', ), ], @@ -66,221 +66,3 @@ class _VoicesDatePickerState extends State { widget.controller.dispose(); } } - -class _FieldPicker extends StatefulWidget { - final FieldDatePickerController controller; - final DateTimePickerType pickerType; - final String timeZone; - const _FieldPicker({ - required this.controller, - required this.pickerType, - this.timeZone = '', - }); - - @override - State<_FieldPicker> createState() => _FieldPickerState(); -} - -class _FieldPickerState extends State<_FieldPicker> { - OverlayEntry? _overlayEntry; - VoidCallback? _scrollListener; - - @override - void dispose() { - super.dispose(); - _overlayEntry?.remove(); - _overlayEntry = null; - } - - void dateOnTap() { - if (_overlayEntry != null) { - _removeOverlay(); - } else { - _removeOverlay(); - - _showOverlay( - VoicesCalendarDatePicker( - onDateSelected: (value) { - _removeOverlay(); - }, - cancelEvent: _removeOverlay, - ), - ); - } - } - - void timeOnTap() { - if (_overlayEntry != null) { - _removeOverlay(); - } else { - _showOverlay( - const VoicesTimePicker(), - ); - } - } - - @override - Widget build(BuildContext context) { - return SizedBox( - width: _getWidth, - child: Stack( - alignment: Alignment.center, - children: [ - VoicesTextField( - controller: widget.controller, - validator: (value) => widget.controller.validate(value), - decoration: _getInputDecoration(context), - onFieldSubmitted: (String value) {}, - showSuffix: false, - autovalidateMode: AutovalidateMode.onUserInteraction, - ), - Positioned( - right: 0, - child: VoicesIconButton( - child: _getIcon.buildIcon(), - onTap: () => widget.pickerType == DateTimePickerType.date - ? dateOnTap() - : timeOnTap(), - ), - ), - ], - ), - ); - } - - double get _getWidth => switch (widget.pickerType) { - DateTimePickerType.date => 210, - DateTimePickerType.time => 160, - }; - - SvgGenImage get _getIcon => switch (widget.pickerType) { - DateTimePickerType.date => VoicesAssets.icons.calendar, - DateTimePickerType.time => VoicesAssets.icons.clock, - }; - - VoicesTextFieldDecoration _getInputDecoration(BuildContext context) { - final theme = Theme.of(context); - final textTheme = theme.textTheme; - return VoicesTextFieldDecoration( - fillColor: theme.colors.elevationsOnSurfaceNeutralLv1Grey, - filled: true, - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: theme.colors.outlineBorderVariant!, - width: 0.75, - ), - borderRadius: switch (widget.pickerType) { - DateTimePickerType.date => const BorderRadius.only( - topLeft: Radius.circular(16), - bottomLeft: Radius.circular(16), - ), - DateTimePickerType.time => const BorderRadius.only( - topRight: Radius.circular(16), - bottomRight: Radius.circular(16), - ), - }, - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: theme.primaryColor, - width: 2, - ), - borderRadius: switch (widget.pickerType) { - DateTimePickerType.date => const BorderRadius.only( - topLeft: Radius.circular(16), - bottomLeft: Radius.circular(16), - ), - DateTimePickerType.time => const BorderRadius.only( - topRight: Radius.circular(16), - bottomRight: Radius.circular(16), - ), - }, - ), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colors.errorContainer!, - width: 2, - ), - borderRadius: switch (widget.pickerType) { - DateTimePickerType.date => const BorderRadius.only( - topLeft: Radius.circular(16), - bottomLeft: Radius.circular(16), - ), - DateTimePickerType.time => const BorderRadius.only( - topRight: Radius.circular(16), - bottomRight: Radius.circular(16), - ), - }, - ), - hintText: switch (widget.pickerType) { - DateTimePickerType.date => 'DD/MM/YYYY', - DateTimePickerType.time => '00:00 ${widget.timeZone}', - }, - hintStyle: textTheme.bodyLarge?.copyWith( - color: theme.colors.textDisabled, - ), - ); - } - - void _showOverlay(Widget child) { - final overlay = Overlay.of(context, rootOverlay: true); - final renderBox = context.findRenderObject() as RenderBox?; - final scrollController = ScrollControllerProvider.of(context); - final initialPosition = renderBox!.localToGlobal( - Offset(0, scrollController.offset), - ); - - _overlayEntry = OverlayEntry( - builder: (context) => Stack( - children: [ - Positioned.fill( - child: MouseRegion( - opaque: false, - child: GestureDetector( - onTap: _removeOverlay, - behavior: HitTestBehavior.translucent, - excludeFromSemantics: true, - onPanUpdate: null, - onPanDown: null, - onPanCancel: null, - onPanEnd: null, - onPanStart: null, - child: Container( - color: Colors.transparent, - ), - ), - ), - ), - Positioned( - top: initialPosition.dy + - 50 - - (scrollController.hasClients ? scrollController.offset : 0), - left: initialPosition.dx, - child: child, - ), - ], - ), - ); - - void listener() { - if (_overlayEntry != null) { - _overlayEntry?.markNeedsBuild(); - } - } - - scrollController.addListener(listener); - _scrollListener = listener; - - overlay.insert(_overlayEntry!); - } - - void _removeOverlay() { - final scrollController = ScrollControllerProvider.of(context); - if (_scrollListener != null) { - scrollController.removeListener(_scrollListener!); - } - _scrollListener = null; - _overlayEntry?.remove(); - _overlayEntry = null; - } -} diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart index 38fa433e393..49d063086b0 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart @@ -1,13 +1,19 @@ part of 'voices_date_picker_field.dart'; class VoicesTimePicker extends StatelessWidget { - const VoicesTimePicker({super.key}); + final ValueChanged onTap; + const VoicesTimePicker({ + super.key, + required this.onTap, + }); List get timeList => _generateTimeList(); @override Widget build(BuildContext context) { return Material( + clipBehavior: Clip.hardEdge, + color: Colors.transparent, child: Container( height: 300, width: 150, @@ -20,6 +26,7 @@ class VoicesTimePicker extends StatelessWidget { .map( (e) => TimeText( value: e, + onTap: onTap, ), ) .toList(), @@ -34,7 +41,9 @@ class VoicesTimePicker extends StatelessWidget { for (var hour = 0; hour < 24; hour++) { for (var minute = 0; minute < 60; minute += 30) { times.add( - '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}'); + // ignore: lines_longer_than_80_chars + '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}', + ); } } @@ -43,10 +52,12 @@ class VoicesTimePicker extends StatelessWidget { } class TimeText extends StatelessWidget { + final ValueChanged onTap; final String value; const TimeText({ super.key, required this.value, + required this.onTap, }); @override @@ -55,7 +66,7 @@ class TimeText extends StatelessWidget { clipBehavior: Clip.hardEdge, color: Colors.transparent, child: InkWell( - onTap: () {}, + onTap: () => onTap(value), child: Padding( key: key, padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart index d992ed31035..bb09b2c34c7 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart @@ -68,9 +68,6 @@ class VoicesTextField extends StatefulWidget { /// [TextField.inputFormatters] final List? inputFormatters; - /// Optional suffix icon to be displayed at the end of the text field. - final bool showSuffix; - /// [AutovalidateMode] final AutovalidateMode? autovalidateMode; @@ -97,7 +94,6 @@ class VoicesTextField extends StatefulWidget { required this.onFieldSubmitted, this.onSaved, this.inputFormatters, - this.showSuffix = true, this.autovalidateMode, }); @@ -339,7 +335,7 @@ class _VoicesTextFieldState extends State { Widget? _getStatusSuffixWidget() { final showStatusIcon = widget.decoration?.showStatusSuffixIcon ?? true; - if (!showStatusIcon || !widget.showSuffix) { + if (!showStatusIcon) { return null; } From 8c71ef1b2f9834dc81246ad6b585d5f42fa25aee Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Mon, 18 Nov 2024 10:22:15 +0100 Subject: [PATCH 04/16] feat: enhance date picker with improved validation and error handling messages --- .../text_field/date_picker/base_picker.dart | 15 ++- .../date_picker/date_picker_controller.dart | 118 ++++++++++++------ .../date_picker/voices_calendar_picker.dart | 3 +- .../date_picker/voices_time_picker.dart | 73 +++++++++-- .../widgets/text_field/voices_text_field.dart | 18 ++- .../catalyst_voices_localizations.dart | 18 +++ .../catalyst_voices_localizations_en.dart | 9 ++ .../catalyst_voices_localizations_es.dart | 9 ++ .../lib/l10n/intl_en.arb | 12 ++ 9 files changed, 218 insertions(+), 57 deletions(-) diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart index 9edfb984853..41541c3afdf 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart @@ -183,6 +183,9 @@ abstract class _BasePickerState extends State { hintStyle: textTheme.bodyLarge?.copyWith( color: theme.colors.textDisabled, ), + errorStyle: textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.error, + ), errorMaxLines: 2, ); } @@ -213,7 +216,10 @@ class _CalendarFieldPickerState extends _BasePickerState { width: _getWidth, child: VoicesTextField( controller: widget.controller, - validator: (value) => widget.controller.validate(value), + validator: (value) { + final status = widget.controller.validate(value); + return status.message(context.l10n, widget.controller.pattern); + }, decoration: _getInputDecoration( context, VoicesIconButton( @@ -239,6 +245,8 @@ class _TimeFieldPickerState extends _BasePickerState { _removeOverlay(); widget.controller.setValue(value); }, + selectedTime: widget.controller.selectedValue, + timeZone: widget.timeZone, ), ); } @@ -250,7 +258,10 @@ class _TimeFieldPickerState extends _BasePickerState { width: _getWidth, child: VoicesTextField( controller: widget.controller, - validator: (value) => widget.controller.validate(value), + validator: (value) { + final status = widget.controller.validate(value); + return status.message(context.l10n, widget.controller.pattern); + }, decoration: _getInputDecoration( context, VoicesIconButton( diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart index 5948b1b2332..db79f6f6665 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart @@ -1,9 +1,66 @@ import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; +import 'package:catalyst_voices_localization/generated/catalyst_voices_localizations.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +enum DatePickerValidationStatus { + success, + dateFormatError, + timeFormatError, + dateRangeError, + daysInMonthError +} + +extension DatePickerValidationStatusExt on DatePickerValidationStatus { + // String? message(VoicesLocalizations l10n) { + // switch (this) { + // case DatePickerValidationStatus.success: + // return null; + // case DatePickerValidationStatus.dateFormatError: + // return 'Format: "DD/MM/YYYY"'; + // case DatePickerValidationStatus.timeFormatError: + // return 'Format: "HH:MM"'; + // case DatePickerValidationStatus.dateRangeError: + // // ignore: lines_longer_than_80_chars + // return 'Please select a date within the range of today and one year from today.'; + // case DatePickerValidationStatus.daysInMonthError: + // return 'Entered day exceeds the maximum days for this month.'; + // } + // } + VoicesTextFieldValidationResult message( + VoicesLocalizations l10n, + String pattern, + ) => + switch (this) { + DatePickerValidationStatus.success => + const VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.success, + ), + DatePickerValidationStatus.dateFormatError => + VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.error, + errorMessage: '${l10n.format}: ${pattern.toUpperCase()}', + ), + DatePickerValidationStatus.timeFormatError => + VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.error, + errorMessage: '${l10n.format}: $pattern', + ), + DatePickerValidationStatus.dateRangeError => + VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.error, + errorMessage: l10n.datePickerDateRangeError, + ), + DatePickerValidationStatus.daysInMonthError => + VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.error, + errorMessage: l10n.datePickerDaysInMonthError, + ), + }; +} + final class DatePickerControllerState extends Equatable { final DateTime? selectedDate; final String? selectedTime; @@ -82,10 +139,10 @@ final class DatePickerController sealed class FieldDatePickerController extends TextEditingController { abstract final String pattern; - bool get isValid => validate(text).status == VoicesTextFieldStatus.success; + bool get isValid => validate(text) == DatePickerValidationStatus.success; T get selectedValue; - VoicesTextFieldValidationResult validate(String? value); + DatePickerValidationStatus validate(String? value); void setValue(T newValue); } @@ -115,66 +172,54 @@ final class CalendarPickerController } @override - String get pattern => 'DD/MM/YYYY'; + String get pattern => 'dd/MM/yyyy'; @override - VoicesTextFieldValidationResult validate(String? value) { + DatePickerValidationStatus validate(String? value) { final today = DateTime.now(); final maxDate = DateTime(today.year + 1, today.month, today.day); - final notValidFormat = VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.error, - errorMessage: 'Format: "$pattern"', - ); if (value == null || value == '') { - return const VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.success, - ); + return DatePickerValidationStatus.success; } - if (value.length != 10) return notValidFormat; + if (value.length != 10) return DatePickerValidationStatus.dateFormatError; final dateRegex = RegExp(r'^(\d{2})/(\d{2})/(\d{4})$'); - if (!dateRegex.hasMatch(value)) return notValidFormat; + if (!dateRegex.hasMatch(value)) { + return DatePickerValidationStatus.dateFormatError; + } final parts = value.split('/'); final day = int.parse(parts[0]); final month = int.parse(parts[1]); final year = int.parse(parts[2]); - if (month < 1 || month > 12) return notValidFormat; - if (day < 1 || day > 31) return notValidFormat; + if (month < 1 || month > 12) { + return DatePickerValidationStatus.dateFormatError; + } + if (day < 1 || day > 31) return DatePickerValidationStatus.daysInMonthError; final inputDate = DateTime(year, month, day); if (inputDate.isBefore(today.subtract(const Duration(days: 1))) || inputDate.isAfter(maxDate)) { - return VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.error, - errorMessage: - 'Date must be between ${today.day}/${today.month}/${today.year} and ${maxDate.day}/${maxDate.month}/${maxDate.year}', - ); + return DatePickerValidationStatus.dateRangeError; } - // Check days in month final daysInMonth = DateTime(year, month + 1, 0).day; if (day > daysInMonth) { - return VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.error, - errorMessage: 'Day must be between 1 and $daysInMonth', - ); + return DatePickerValidationStatus.daysInMonthError; } - return const VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.success, - ); + return DatePickerValidationStatus.success; } @override void setValue(DateTime? newValue) { if (newValue == null) return; final newT = newValue; - final formatter = DateFormat('dd/MM/yyyy'); + final formatter = DateFormat(pattern); value = TextEditingValue(text: formatter.format(newT)); } } @@ -187,23 +232,16 @@ final class TimePickerController extends FieldDatePickerController { String? get selectedValue => text; @override - VoicesTextFieldValidationResult validate(String? value) { + DatePickerValidationStatus validate(String? value) { if (value == null || value == '') { - return const VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.success, - ); + return DatePickerValidationStatus.success; } final pattern = RegExp(r'^(0?[0-9]|1[0-9]|2[0-3]):([0-5][0-9])$'); if (!pattern.hasMatch(value)) { - return const VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.error, - errorMessage: 'Format: "00:00"', - ); + return DatePickerValidationStatus.timeFormatError; } - return const VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.success, - ); + return DatePickerValidationStatus.success; } @override diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart index 05f6f4599e7..e7f3bd762d8 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart @@ -70,7 +70,8 @@ class VoicesCalendarDatePicker extends StatelessWidget { ), VoicesTextButton( onTap: () => onDateSelected(selectedDate), - child: Text(context.l10n.snackbarOkButtonText), + child: + Text(context.l10n.snackbarOkButtonText.toUpperCase()), ), ], ), diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart index 49d063086b0..da29c4177a0 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart @@ -1,32 +1,69 @@ part of 'voices_date_picker_field.dart'; -class VoicesTimePicker extends StatelessWidget { +class VoicesTimePicker extends StatefulWidget { final ValueChanged onTap; + final String? selectedTime; + final String timeZone; const VoicesTimePicker({ super.key, required this.onTap, + this.selectedTime, + required this.timeZone, }); + @override + State createState() => _VoicesTimePickerState(); +} + +class _VoicesTimePickerState extends State { + late final ScrollController _scrollController; List get timeList => _generateTimeList(); + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + + if (widget.selectedTime != null) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + final index = timeList.indexOf(widget.selectedTime!); + if (index != -1) { + _scrollController.jumpTo( + index * 40.0, + ); + } + }); + } + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Material( clipBehavior: Clip.hardEdge, color: Colors.transparent, child: Container( - height: 300, + height: 350, width: 150, decoration: BoxDecoration( color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1Grey, borderRadius: BorderRadius.circular(20), ), child: ListView( + controller: _scrollController, children: timeList .map( (e) => TimeText( + key: ValueKey(e), value: e, - onTap: onTap, + onTap: widget.onTap, + selectedTime: widget.selectedTime, + timeZone: widget.timeZone, ), ) .toList(), @@ -41,7 +78,6 @@ class VoicesTimePicker extends StatelessWidget { for (var hour = 0; hour < 24; hour++) { for (var minute = 0; minute < 60; minute += 30) { times.add( - // ignore: lines_longer_than_80_chars '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}', ); } @@ -54,12 +90,18 @@ class VoicesTimePicker extends StatelessWidget { class TimeText extends StatelessWidget { final ValueChanged onTap; final String value; + final String? selectedTime; + final String timeZone; const TimeText({ super.key, required this.value, required this.onTap, + this.selectedTime, + required this.timeZone, }); + bool get isSelected => selectedTime == value; + @override Widget build(BuildContext context) { return Material( @@ -67,12 +109,23 @@ class TimeText extends StatelessWidget { color: Colors.transparent, child: InkWell( onTap: () => onTap(value), - child: Padding( - key: key, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - child: Text( - value, - style: Theme.of(context).textTheme.bodyLarge, + child: ColoredBox( + color: !isSelected + ? Colors.transparent + : Theme.of(context).colors.onSurfaceNeutral08!, + child: Padding( + key: key, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + value, + style: Theme.of(context).textTheme.bodyLarge, + ), + if (isSelected) Text(timeZone), + ], + ), ), ), ), diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart index bb09b2c34c7..001cb9be346 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart @@ -260,10 +260,7 @@ class _VoicesTextFieldState extends State { errorText: widget.decoration?.errorText ?? _validation.errorMessage, errorMaxLines: widget.decoration?.errorMaxLines, - errorStyle: widget.enabled - ? textTheme.bodySmall - : textTheme.bodySmall! - .copyWith(color: theme.colors.textDisabled), + errorStyle: _getErrorStyle(textTheme, theme), prefixIcon: _wrapIconIfExists( widget.decoration?.prefixIcon, const EdgeInsetsDirectional.only(start: 8, end: 4), @@ -406,6 +403,15 @@ class _VoicesTextFieldState extends State { return customController; } + TextStyle? _getErrorStyle(TextTheme textTheme, ThemeData theme) { + if (widget.decoration?.errorStyle != null) { + return widget.decoration?.errorStyle; + } + return widget.enabled + ? textTheme.bodySmall + : textTheme.bodySmall!.copyWith(color: theme.colors.textDisabled); + } + void _onChanged() { _validate(_obtainController().text); } @@ -582,6 +588,9 @@ class VoicesTextFieldDecoration { /// [InputDecoration.errorText]. final String? errorText; + /// [InputDecoration.errorStyle] + final TextStyle? errorStyle; + /// [InputDecoration.errorMaxLines]. final int? errorMaxLines; @@ -623,6 +632,7 @@ class VoicesTextFieldDecoration { this.hintText, this.hintStyle, this.errorText, + this.errorStyle, this.errorMaxLines, this.prefixIcon, this.prefixText, diff --git a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart index e25a788edee..a6b5f81a908 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart @@ -1911,6 +1911,24 @@ abstract class VoicesLocalizations { /// In en, this message translates to: /// **'Review registration transaction'** String get reviewRegistrationTransaction; + + /// A label for the format field in the date picker. + /// + /// In en, this message translates to: + /// **'Format'** + String get format; + + /// Error message for the date picker when the selected date is outside the range of today and one year from today. + /// + /// In en, this message translates to: + /// **'Please select a date within the range of today and one year from today.'** + String get datePickerDateRangeError; + + /// Error message for the date picker when the selected day is greater than the maximum days for the selected month. + /// + /// In en, this message translates to: + /// **'Entered day exceeds the maximum days for this month.'** + String get datePickerDaysInMonthError; } class _VoicesLocalizationsDelegate extends LocalizationsDelegate { diff --git a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart index 5cd6739f26a..ee2b99479b9 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart @@ -1000,4 +1000,13 @@ class VoicesLocalizationsEn extends VoicesLocalizations { @override String get reviewRegistrationTransaction => 'Review registration transaction'; + + @override + String get format => 'Format'; + + @override + String get datePickerDateRangeError => 'Please select a date within the range of today and one year from today.'; + + @override + String get datePickerDaysInMonthError => 'Entered day exceeds the maximum days for this month.'; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart index 4384ef7847c..5c6d89f3429 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart @@ -1000,4 +1000,13 @@ class VoicesLocalizationsEs extends VoicesLocalizations { @override String get reviewRegistrationTransaction => 'Review registration transaction'; + + @override + String get format => 'Format'; + + @override + String get datePickerDateRangeError => 'Please select a date within the range of today and one year from today.'; + + @override + String get datePickerDaysInMonthError => 'Entered day exceeds the maximum days for this month.'; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb index cee75b5d567..4308e66c649 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb +++ b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb @@ -967,5 +967,17 @@ "reviewRegistrationTransaction": "Review registration transaction", "@reviewRegistrationTransaction": { "description": "A button label to review the registration transaction in wallet detail panel." + }, + "format": "Format", + "@format": { + "description": "A label for the format field in the date picker." + }, + "datePickerDateRangeError": "Please select a date within the range of today and one year from today.", + "@datePickerDateRangeError": { + "description": "Error message for the date picker when the selected date is outside the range of today and one year from today." + }, + "datePickerDaysInMonthError": "Entered day exceeds the maximum days for this month.", + "@datePickerDaysInMonthError": { + "description": "Error message for the date picker when the selected day is greater than the maximum days for the selected month." } } \ No newline at end of file From 44db8a93c57c3a35ea4509e5d9fb66bf0632a4c1 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Mon, 18 Nov 2024 11:50:18 +0100 Subject: [PATCH 05/16] feat: improve overlay management in date picker and enhance scroll controller handling in text fields --- .../text_field/date_picker/base_picker.dart | 70 +++++++++++++------ .../date_picker/date_picker_controller.dart | 15 ---- .../date_picker/voices_date_picker_field.dart | 11 ++- 3 files changed, 57 insertions(+), 39 deletions(-) diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart index 41541c3afdf..b2474817d33 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart @@ -40,8 +40,10 @@ class TimeFieldPicker extends BasePicker { } abstract class _BasePickerState extends State { + static _BasePickerState? _activePicker; OverlayEntry? _overlayEntry; VoidCallback? _scrollListener; + bool _isOverlayOpen = false; @override void dispose() { @@ -79,22 +81,34 @@ abstract class _BasePickerState extends State { }; void _removeOverlay() { - final scrollController = ScrollControllerProvider.of(context); - if (_scrollListener != null) { - scrollController.removeListener(_scrollListener!); + if (_overlayEntry != null) { + setState(() { + _isOverlayOpen = false; + }); + final scrollController = ScrollControllerProvider.maybeOf(context); + if (scrollController != null && _scrollListener != null) { + scrollController.removeListener(_scrollListener!); + } + _overlayEntry?.remove(); + _overlayEntry = null; + _scrollListener = null; + _activePicker = null; } - _scrollListener = null; - _overlayEntry?.remove(); - _overlayEntry = null; - widget.onRemoveOverlay?.call(); } void _showOverlay(Widget child) { + FocusScope.of(context).unfocus(); + if (_activePicker != null && _activePicker != this) { + _activePicker!._removeOverlay(); + } + setState(() { + _isOverlayOpen = true; + }); final overlay = Overlay.of(context, rootOverlay: true); final renderBox = context.findRenderObject() as RenderBox?; - final scrollController = ScrollControllerProvider.of(context); + final scrollController = ScrollControllerProvider.maybeOf(context); final initialPosition = renderBox!.localToGlobal( - Offset(0, scrollController.offset), + Offset.zero, ); _overlayEntry = OverlayEntry( @@ -121,7 +135,9 @@ abstract class _BasePickerState extends State { Positioned( top: initialPosition.dy + 50 - - (scrollController.hasClients ? scrollController.offset : 0), + (scrollController?.hasClients ?? false + ? scrollController!.offset + : 0), left: initialPosition.dx, child: child, ), @@ -129,15 +145,18 @@ abstract class _BasePickerState extends State { ), ); - void listener() { - if (_overlayEntry != null) { - _overlayEntry?.markNeedsBuild(); + if (scrollController != null) { + void listener() { + if (_overlayEntry != null) { + _overlayEntry?.markNeedsBuild(); + } } - } - scrollController.addListener(listener); - _scrollListener = listener; + scrollController.addListener(listener); + _scrollListener = listener; + } + _activePicker = this; overlay.insert(_overlayEntry!); } @@ -147,15 +166,21 @@ abstract class _BasePickerState extends State { ) { final theme = Theme.of(context); final textTheme = theme.textTheme; + final borderSide = _isOverlayOpen + ? BorderSide( + color: theme.primaryColor, + width: 2, + ) + : BorderSide( + color: theme.colors.outlineBorderVariant!, + width: 0.75, + ); return VoicesTextFieldDecoration( suffixIcon: suffixIcon, fillColor: theme.colors.elevationsOnSurfaceNeutralLv1Grey, filled: true, enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: theme.colors.outlineBorderVariant!, - width: 0.75, - ), + borderSide: borderSide, borderRadius: _getBorderRadius, ), focusedBorder: OutlineInputBorder( @@ -193,10 +218,9 @@ abstract class _BasePickerState extends State { class _CalendarFieldPickerState extends _BasePickerState { void _onTap() { - if (_overlayEntry != null) { + if (_BasePickerState._activePicker == this) { _removeOverlay(); } else { - _removeOverlay(); _showOverlay( VoicesCalendarDatePicker( initialDate: widget.controller.selectedValue, @@ -236,7 +260,7 @@ class _CalendarFieldPickerState extends _BasePickerState { class _TimeFieldPickerState extends _BasePickerState { void _onTap() { - if (_overlayEntry != null) { + if (_BasePickerState._activePicker == this) { _removeOverlay(); } else { _showOverlay( diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart index db79f6f6665..5bf46f98899 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart @@ -14,21 +14,6 @@ enum DatePickerValidationStatus { } extension DatePickerValidationStatusExt on DatePickerValidationStatus { - // String? message(VoicesLocalizations l10n) { - // switch (this) { - // case DatePickerValidationStatus.success: - // return null; - // case DatePickerValidationStatus.dateFormatError: - // return 'Format: "DD/MM/YYYY"'; - // case DatePickerValidationStatus.timeFormatError: - // return 'Format: "HH:MM"'; - // case DatePickerValidationStatus.dateRangeError: - // // ignore: lines_longer_than_80_chars - // return 'Please select a date within the range of today and one year from today.'; - // case DatePickerValidationStatus.daysInMonthError: - // return 'Entered day exceeds the maximum days for this month.'; - // } - // } VoicesTextFieldValidationResult message( VoicesLocalizations l10n, String pattern, diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart index 0acbdea6fa6..544a670603f 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart @@ -4,6 +4,7 @@ import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; part 'base_picker.dart'; part 'voices_calendar_picker.dart'; @@ -26,6 +27,12 @@ class ScrollControllerProvider extends InheritedWidget { return provider!.scrollController; } + static ScrollController? maybeOf(BuildContext context) { + final provider = + context.dependOnInheritedWidgetOfExactType(); + return provider?.scrollController; + } + @override bool updateShouldNotify(ScrollControllerProvider oldWidget) { return scrollController != oldWidget.scrollController; @@ -34,9 +41,11 @@ class ScrollControllerProvider extends InheritedWidget { class VoicesDatePicker extends StatefulWidget { final DatePickerController controller; + final String timeZone; const VoicesDatePicker({ super.key, required this.controller, + required this.timeZone, }); @override @@ -54,7 +63,7 @@ class _VoicesDatePickerState extends State { ), TimeFieldPicker( controller: widget.controller.timePickerController, - timeZone: 'UTC', + timeZone: widget.timeZone, ), ], ); From 489a7ea11b4369a9d65f84b245e9e3f71ad9d854 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Mon, 18 Nov 2024 14:20:48 +0100 Subject: [PATCH 06/16] fix: overlay switch between date and time --- .../lib/pages/discovery/discovery_page.dart | 74 +++++++------------ .../text_field/date_picker/base_picker.dart | 40 +++++++++- .../date_picker/voices_date_picker_field.dart | 6 +- .../date_picker/voices_time_picker.dart | 1 + 4 files changed, 71 insertions(+), 50 deletions(-) diff --git a/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart b/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart index ee25863a4aa..7049932a0dd 100644 --- a/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart @@ -2,64 +2,45 @@ import 'dart:async'; import 'package:catalyst_voices/pages/discovery/current_status_text.dart'; import 'package:catalyst_voices/pages/discovery/toggle_state_text.dart'; -import 'package:catalyst_voices/widgets/text_field/date_picker/date_picker_controller.dart'; -import 'package:catalyst_voices/widgets/text_field/date_picker/voices_date_picker_field.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:flutter/material.dart'; -class DiscoveryPage extends StatefulWidget { +class DiscoveryPage extends StatelessWidget { const DiscoveryPage({ super.key, }); - @override - State createState() => _DiscoveryPageState(); -} - -class _DiscoveryPageState extends State { - final _scrollController = ScrollController(); - - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return ScrollControllerProvider( - scrollController: _scrollController, - child: CustomScrollView( - controller: _scrollController, - slivers: [ - const SliverToBoxAdapter(child: _SpacesNavigationLocation()), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 32) - .add(const EdgeInsets.only(bottom: 32)), - sliver: SliverList( - delegate: SliverChildListDelegate( - [ - const _Segment(key: ValueKey('Segment1Key')), - const SizedBox(height: 24), - const _Segment(key: ValueKey('Segment2Key')), - const SizedBox(height: 24), - const _Segment(key: ValueKey('Segment3Key')), - ], - ), - ), - ), - const SliverFillRemaining( - hasScrollBody: false, - child: Column( - children: [ - Spacer(), - StandardLinksPageFooter(), + return CustomScrollView( + slivers: [ + const SliverToBoxAdapter(child: _SpacesNavigationLocation()), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 32) + .add(const EdgeInsets.only(bottom: 32)), + sliver: SliverList( + delegate: SliverChildListDelegate( + [ + const _Segment(key: ValueKey('Segment1Key')), + const SizedBox(height: 24), + const _Segment(key: ValueKey('Segment2Key')), + const SizedBox(height: 24), + const _Segment(key: ValueKey('Segment3Key')), ], ), ), - ], - ), + ), + const SliverFillRemaining( + hasScrollBody: false, + child: Column( + children: [ + Spacer(), + StandardLinksPageFooter(), + ], + ), + ), + ], ); } } @@ -99,9 +80,6 @@ class _Segment extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - VoicesDatePicker( - controller: DatePickerController(), - ), const CurrentUserStatusText(), const SizedBox(height: 8), const ToggleStateText(), diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart index b2474817d33..5a6531a8fba 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart @@ -96,6 +96,31 @@ abstract class _BasePickerState extends State { } } + RenderBox? _getRenderBox(GlobalKey key) { + return key.currentContext?.findRenderObject() as RenderBox?; + } + + bool _isBoxTapped(RenderBox? box, Offset tapPosition) { + return box != null && + (box.localToGlobal(Offset.zero) & box.size).contains(tapPosition); + } + + void _handleCalendarTap() { + _activePicker?._removeOverlay(); + final calendarState = calendarKey.currentState; + if (calendarState is _CalendarFieldPickerState) { + calendarState._onTap(); + } + } + + void _handleTimeTap() { + _activePicker?._removeOverlay(); + final timeState = timeKey.currentState; + if (timeState is _TimeFieldPickerState) { + timeState._onTap(); + } + } + void _showOverlay(Widget child) { FocusScope.of(context).unfocus(); if (_activePicker != null && _activePicker != this) { @@ -117,8 +142,21 @@ abstract class _BasePickerState extends State { Positioned.fill( child: MouseRegion( opaque: false, + hitTestBehavior: HitTestBehavior.translucent, child: GestureDetector( - onTap: _removeOverlay, + onTapDown: (details) { + final tapPosition = details.globalPosition; + final calendarBox = _getRenderBox(calendarKey); + final timeBox = _getRenderBox(timeKey); + + if (_isBoxTapped(calendarBox, tapPosition)) { + _handleCalendarTap(); + } else if (_isBoxTapped(timeBox, tapPosition)) { + _handleTimeTap(); + } else { + _removeOverlay(); + } + }, behavior: HitTestBehavior.translucent, excludeFromSemantics: true, onPanUpdate: null, diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart index 544a670603f..0f2270a5e9f 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart @@ -4,7 +4,6 @@ import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; part 'base_picker.dart'; part 'voices_calendar_picker.dart'; @@ -12,6 +11,9 @@ part 'voices_time_picker.dart'; enum DateTimePickerType { date, time } +final GlobalKey calendarKey = GlobalKey(); +final GlobalKey timeKey = GlobalKey(); + class ScrollControllerProvider extends InheritedWidget { final ScrollController scrollController; @@ -59,9 +61,11 @@ class _VoicesDatePickerState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ CalendarFieldPicker( + key: calendarKey, controller: widget.controller.calendarPickerController, ), TimeFieldPicker( + key: timeKey, controller: widget.controller.timePickerController, timeZone: widget.timeZone, ), diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart index da29c4177a0..e055a4386f5 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart @@ -78,6 +78,7 @@ class _VoicesTimePickerState extends State { for (var hour = 0; hour < 24; hour++) { for (var minute = 0; minute < 60; minute += 30) { times.add( + // ignore: lines_longer_than_80_chars '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}', ); } From 82bf505f15e3f98721406be32b33edb7c18dc87a Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Mon, 18 Nov 2024 14:24:17 +0100 Subject: [PATCH 07/16] chore: remove cached gitignore files --- .../catalyst_voices_localizations.dart | 1964 ----------------- .../catalyst_voices_localizations_en.dart | 1012 --------- .../catalyst_voices_localizations_es.dart | 1012 --------- .../lib/generated/assets.gen.dart | 108 - 4 files changed, 4096 deletions(-) delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart delete mode 100644 catalyst_voices/utilities/uikit_example/lib/generated/assets.gen.dart diff --git a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart deleted file mode 100644 index a6b5f81a908..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart +++ /dev/null @@ -1,1964 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/widgets.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:intl/intl.dart' as intl; - -import 'catalyst_voices_localizations_en.dart' deferred as catalyst_voices_localizations_en; -import 'catalyst_voices_localizations_es.dart' deferred as catalyst_voices_localizations_es; - -// ignore_for_file: type=lint - -/// Callers can lookup localized strings with an instance of VoicesLocalizations -/// returned by `VoicesLocalizations.of(context)`. -/// -/// Applications need to include `VoicesLocalizations.delegate()` in their app's -/// `localizationDelegates` list, and the locales they support in the app's -/// `supportedLocales` list. For example: -/// -/// ```dart -/// import 'generated/catalyst_voices_localizations.dart'; -/// -/// return MaterialApp( -/// localizationsDelegates: VoicesLocalizations.localizationsDelegates, -/// supportedLocales: VoicesLocalizations.supportedLocales, -/// home: MyApplicationHome(), -/// ); -/// ``` -/// -/// ## Update pubspec.yaml -/// -/// Please make sure to update your pubspec.yaml to include the following -/// packages: -/// -/// ```yaml -/// dependencies: -/// # Internationalization support. -/// flutter_localizations: -/// sdk: flutter -/// intl: any # Use the pinned version from flutter_localizations -/// -/// # Rest of dependencies -/// ``` -/// -/// ## iOS Applications -/// -/// iOS applications define key application metadata, including supported -/// locales, in an Info.plist file that is built into the application bundle. -/// To configure the locales supported by your app, you’ll need to edit this -/// file. -/// -/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. -/// Then, in the Project Navigator, open the Info.plist file under the Runner -/// project’s Runner folder. -/// -/// Next, select the Information Property List item, select Add Item from the -/// Editor menu, then select Localizations from the pop-up menu. -/// -/// Select and expand the newly-created Localizations item then, for each -/// locale your application supports, add a new item and select the locale -/// you wish to add from the pop-up menu in the Value field. This list should -/// be consistent with the languages listed in the VoicesLocalizations.supportedLocales -/// property. -abstract class VoicesLocalizations { - VoicesLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); - - final String localeName; - - static VoicesLocalizations? of(BuildContext context) { - return Localizations.of(context, VoicesLocalizations); - } - - static const LocalizationsDelegate delegate = _VoicesLocalizationsDelegate(); - - /// A list of this localizations delegate along with the default localizations - /// delegates. - /// - /// Returns a list of localizations delegates containing this delegate along with - /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, - /// and GlobalWidgetsLocalizations.delegate. - /// - /// Additional delegates can be added by appending to this list in - /// MaterialApp. This list does not have to be used at all if a custom list - /// of delegates is preferred or required. - static const List> localizationsDelegates = >[ - delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ]; - - /// A list of this localizations delegate's supported locales. - static const List supportedLocales = [ - Locale('en'), - Locale('es') - ]; - - /// Text shown in email field - /// - /// In en, this message translates to: - /// **'Email'** - String get emailLabelText; - - /// Text shown in email field when empty - /// - /// In en, this message translates to: - /// **'mail@example.com'** - String get emailHintText; - - /// Text shown in email field when input is invalid - /// - /// In en, this message translates to: - /// **'mail@example.com'** - String get emailErrorText; - - /// Text shown in cancel button - /// - /// In en, this message translates to: - /// **'Cancel'** - String get cancelButtonText; - - /// Text shown in edit button - /// - /// In en, this message translates to: - /// **'Edit'** - String get editButtonText; - - /// Text shown in header tooltip - /// - /// In en, this message translates to: - /// **'Header'** - String get headerTooltipText; - - /// Text shown as placeholder in rich text editor - /// - /// In en, this message translates to: - /// **'Start writing your text...'** - String get placeholderRichText; - - /// Text shown as placeholder in rich text editor - /// - /// In en, this message translates to: - /// **'Supporting text'** - String get supportingTextLabelText; - - /// Text shown in save button - /// - /// In en, this message translates to: - /// **'Save'** - String get saveButtonText; - - /// Text shown in password field - /// - /// In en, this message translates to: - /// **'Password'** - String get passwordLabelText; - - /// Text shown in password field when empty - /// - /// In en, this message translates to: - /// **'My1SecretPassword'** - String get passwordHintText; - - /// Text shown in password field when input is invalid - /// - /// In en, this message translates to: - /// **'Password must be at least 8 characters long'** - String get passwordErrorText; - - /// Text shown in the login screen title - /// - /// In en, this message translates to: - /// **'Login'** - String get loginTitleText; - - /// Text shown in the login screen for the login button - /// - /// In en, this message translates to: - /// **'Login'** - String get loginButtonText; - - /// Text shown in the login screen when the user enters wrong credentials - /// - /// In en, this message translates to: - /// **'Wrong credentials'** - String get loginScreenErrorMessage; - - /// Text shown in the home screen - /// - /// In en, this message translates to: - /// **'Catalyst Voices'** - String get homeScreenText; - - /// Text shown after logo in coming soon page - /// - /// In en, this message translates to: - /// **'Voices'** - String get comingSoonSubtitle; - - /// Text shown as main title in coming soon page - /// - /// In en, this message translates to: - /// **'Coming'** - String get comingSoonTitle1; - - /// Text shown as main title in coming soon page - /// - /// In en, this message translates to: - /// **'soon'** - String get comingSoonTitle2; - - /// Text shown as description in coming soon page - /// - /// In en, this message translates to: - /// **'Project Catalyst is the world\'s largest decentralized innovation engine for solving real-world challenges.'** - String get comingSoonDescription; - - /// Label text shown in the ConnectingStatus widget during re-connection. - /// - /// In en, this message translates to: - /// **'re-connecting'** - String get connectingStatusLabelText; - - /// Label text shown in the FinishAccountButton widget. - /// - /// In en, this message translates to: - /// **'Finish account'** - String get finishAccountButtonLabelText; - - /// Label text shown in the GetStartedButton widget. - /// - /// In en, this message translates to: - /// **'Get Started'** - String get getStartedButtonLabelText; - - /// Label text shown in the UnlockButton widget. - /// - /// In en, this message translates to: - /// **'Unlock'** - String get unlockButtonLabelText; - - /// Label text shown in the UserProfileButton widget when a user is not connected. - /// - /// In en, this message translates to: - /// **'Guest'** - String get userProfileGuestLabelText; - - /// Label text shown in the Search widget. - /// - /// In en, this message translates to: - /// **'[cmd=K]'** - String get searchButtonLabelText; - - /// Label text shown in the Snackbar widget when the message is an info message. - /// - /// In en, this message translates to: - /// **'Info'** - String get snackbarInfoLabelText; - - /// Text shown in the Snackbar widget when the message is an info message. - /// - /// In en, this message translates to: - /// **'This is an info message!'** - String get snackbarInfoMessageText; - - /// Label text shown in the Snackbar widget when the message is an success message. - /// - /// In en, this message translates to: - /// **'Success'** - String get snackbarSuccessLabelText; - - /// Text shown in the Snackbar widget when the message is an success message. - /// - /// In en, this message translates to: - /// **'This is a success message!'** - String get snackbarSuccessMessageText; - - /// Label text shown in the Snackbar widget when the message is an warning message. - /// - /// In en, this message translates to: - /// **'Warning'** - String get snackbarWarningLabelText; - - /// Text shown in the Snackbar widget when the message is an warning message. - /// - /// In en, this message translates to: - /// **'This is a warning message!'** - String get snackbarWarningMessageText; - - /// Label text shown in the Snackbar widget when the message is an error message. - /// - /// In en, this message translates to: - /// **'Error'** - String get snackbarErrorLabelText; - - /// Text shown in the Snackbar widget when the message is an error message. - /// - /// In en, this message translates to: - /// **'This is an error message!'** - String get snackbarErrorMessageText; - - /// Text shown in the Snackbar widget for the refresh button. - /// - /// In en, this message translates to: - /// **'Refresh'** - String get snackbarRefreshButtonText; - - /// Text shown in the Snackbar widget for the more button. - /// - /// In en, this message translates to: - /// **'Learn more'** - String get snackbarMoreButtonText; - - /// Text shown in the Snackbar widget for the ok button. - /// - /// In en, this message translates to: - /// **'Ok'** - String get snackbarOkButtonText; - - /// When user arranges seed phrases this text is shown when phrase was not selected - /// - /// In en, this message translates to: - /// **'Slot {nr}'** - String seedPhraseSlotNr(int nr); - - /// Indicates to user that status is in ready mode - /// - /// In en, this message translates to: - /// **'Ready'** - String get proposalStatusReady; - - /// Indicates to user that status is in draft mode - /// - /// In en, this message translates to: - /// **'Draft'** - String get proposalStatusDraft; - - /// Indicates to user that status is in progress - /// - /// In en, this message translates to: - /// **'In progress'** - String get proposalStatusInProgress; - - /// Indicates to user that status is in private mode - /// - /// In en, this message translates to: - /// **'Private'** - String get proposalStatusPrivate; - - /// Indicates to user that status is in live mode - /// - /// In en, this message translates to: - /// **'LIVE'** - String get proposalStatusLive; - - /// Indicates to user that status is completed - /// - /// In en, this message translates to: - /// **'Completed'** - String get proposalStatusCompleted; - - /// Indicates to user that status is in open mode - /// - /// In en, this message translates to: - /// **'Open'** - String get proposalStatusOpen; - - /// Label shown on a proposal card indicating that the proposal is funded. - /// - /// In en, this message translates to: - /// **'Funded proposal'** - String get fundedProposal; - - /// Label shown on a proposal card indicating that the proposal is not yet funded. - /// - /// In en, this message translates to: - /// **'Published proposal'** - String get publishedProposal; - - /// Indicates date of funding (a proposal). - /// - /// In en, this message translates to: - /// **'Funded {date}'** - String fundedProposalDate(DateTime date); - - /// Indicates a last update date. - /// - /// In en, this message translates to: - /// **'Last update: {date}.'** - String lastUpdateDate(String date); - - /// Indicates the amount of ADA requested in a fund on a proposal card. - /// - /// In en, this message translates to: - /// **'Funds requested'** - String get fundsRequested; - - /// Indicates the amount of comments on a proposal card. - /// - /// In en, this message translates to: - /// **'{count} {count, plural, =0{comments} =1{comment} other{comments}}'** - String noOfComments(num count); - - /// Indicates the amount of comments on a proposal card. - /// - /// In en, this message translates to: - /// **'{completed} of {total} ({percentage}%) {total, plural, =0{segments} =1{segment} other{segments}} completed'** - String noOfSegmentsCompleted(num completed, num total, num percentage); - - /// Refers to date which is today. - /// - /// In en, this message translates to: - /// **'Today'** - String get today; - - /// Refers to date which is yesterday. - /// - /// In en, this message translates to: - /// **'Yesterday'** - String get yesterday; - - /// Refers to date which is two days ago. - /// - /// In en, this message translates to: - /// **'2 days ago'** - String get twoDaysAgo; - - /// Refers to date which is tomorrow. - /// - /// In en, this message translates to: - /// **'Tomorrow'** - String get tomorrow; - - /// Title of the voting space. - /// - /// In en, this message translates to: - /// **'Active voting round 14'** - String get activeVotingRound; - - /// Tab label for all proposals in voting space - /// - /// In en, this message translates to: - /// **'All proposals ({count})'** - String noOfAllProposals(int count); - - /// Refers to a list of favorites. - /// - /// In en, this message translates to: - /// **'Favorites'** - String get favorites; - - /// Left panel name in treasury space - /// - /// In en, this message translates to: - /// **'Campaign builder'** - String get treasuryCampaignBuilder; - - /// Tab name in campaign builder panel - /// - /// In en, this message translates to: - /// **'Segments'** - String get treasuryCampaignBuilderSegments; - - /// Segment name - /// - /// In en, this message translates to: - /// **'Setup Campaign'** - String get treasuryCampaignSetup; - - /// Campaign title - /// - /// In en, this message translates to: - /// **'Campaign title'** - String get treasuryCampaignTitle; - - /// Button name in step - /// - /// In en, this message translates to: - /// **'Edit'** - String get stepEdit; - - /// Left panel name in workspace - /// - /// In en, this message translates to: - /// **'Proposal navigation'** - String get workspaceProposalNavigation; - - /// Tab name in proposal setup panel - /// - /// In en, this message translates to: - /// **'Segments'** - String get workspaceProposalNavigationSegments; - - /// Segment name - /// - /// In en, this message translates to: - /// **'Proposal setup'** - String get workspaceProposalSetup; - - /// Name shown in spaces shell drawer - /// - /// In en, this message translates to: - /// **'Treasury'** - String get drawerSpaceTreasury; - - /// Name shown in spaces shell drawer - /// - /// In en, this message translates to: - /// **'Discovery'** - String get drawerSpaceDiscovery; - - /// Name shown in spaces shell drawer - /// - /// In en, this message translates to: - /// **'Workspace'** - String get drawerSpaceWorkspace; - - /// Name shown in spaces shell drawer - /// - /// In en, this message translates to: - /// **'Voting'** - String get drawerSpaceVoting; - - /// Name shown in spaces shell drawer - /// - /// In en, this message translates to: - /// **'Funded projects'** - String get drawerSpaceFundedProjects; - - /// Title of the funded project space - /// - /// In en, this message translates to: - /// **'Funded project space'** - String get fundedProjectSpace; - - /// Tab label for funded proposals in funded projects space - /// - /// In en, this message translates to: - /// **'Funded proposals ({count})'** - String noOfFundedProposals(int count); - - /// Refers to a list of followed items. - /// - /// In en, this message translates to: - /// **'Followed'** - String get followed; - - /// Overall spaces search brands tile name - /// - /// In en, this message translates to: - /// **'Search Brands'** - String get overallSpacesSearchBrands; - - /// Overall spaces tasks tile name - /// - /// In en, this message translates to: - /// **'Tasks'** - String get overallSpacesTasks; - - /// In different places update popup title - /// - /// In en, this message translates to: - /// **'Voices update ready'** - String get voicesUpdateReady; - - /// In different places update popup body - /// - /// In en, this message translates to: - /// **'Click to restart'** - String get clickToRestart; - - /// Name of space shown in different spaces that indicates its origin - /// - /// In en, this message translates to: - /// **'Treasury space'** - String get spaceTreasuryName; - - /// Name of space shown in different spaces that indicates its origin - /// - /// In en, this message translates to: - /// **'Discovery space'** - String get spaceDiscoveryName; - - /// Name of space shown in different spaces that indicates its origin - /// - /// In en, this message translates to: - /// **'Workspace'** - String get spaceWorkspaceName; - - /// Name of space shown in different spaces that indicates its origin - /// - /// In en, this message translates to: - /// **'Voting space'** - String get spaceVotingName; - - /// Name of space shown in different spaces that indicates its origin - /// - /// In en, this message translates to: - /// **'Funded project space'** - String get spaceFundedProjects; - - /// Refers to a lock action, i.e. to lock the session. - /// - /// In en, this message translates to: - /// **'Lock'** - String get lock; - - /// Refers to a unlock action, i.e. to unlock the session. - /// - /// In en, this message translates to: - /// **'Unlock'** - String get unlock; - - /// Refers to a get started action, i.e. to register. - /// - /// In en, this message translates to: - /// **'Get Started'** - String get getStarted; - - /// Refers to guest user. - /// - /// In en, this message translates to: - /// **'Guest'** - String get guest; - - /// Refers to user that created keychain but is locked - /// - /// In en, this message translates to: - /// **'Visitor'** - String get visitor; - - /// Text shown in the No Internet Connection Banner widget for the refresh button. - /// - /// In en, this message translates to: - /// **'Refresh'** - String get noConnectionBannerRefreshButtonText; - - /// Text shown in the No Internet Connection Banner widget for the title. - /// - /// In en, this message translates to: - /// **'No internet connection'** - String get noConnectionBannerTitle; - - /// Text shown in the No Internet Connection Banner widget for the description below the title. - /// - /// In en, this message translates to: - /// **'Your internet is playing hide and seek. Check your internet connection, or try again in a moment.'** - String get noConnectionBannerDescription; - - /// Describes a password that is weak - /// - /// In en, this message translates to: - /// **'Weak password strength'** - String get weakPasswordStrength; - - /// Describes a password that has medium strength. - /// - /// In en, this message translates to: - /// **'Normal password strength'** - String get normalPasswordStrength; - - /// Describes a password that is strong. - /// - /// In en, this message translates to: - /// **'Good password strength'** - String get goodPasswordStrength; - - /// A button label to select a cardano wallet. - /// - /// In en, this message translates to: - /// **'Choose Cardano Wallet'** - String get chooseCardanoWallet; - - /// A button label to select another cardano wallet. - /// - /// In en, this message translates to: - /// **'Choose other wallet'** - String get chooseOtherWallet; - - /// A label on a clickable element that can show more content. - /// - /// In en, this message translates to: - /// **'Learn More'** - String get learnMore; - - /// A header in link wallet flow in registration. - /// - /// In en, this message translates to: - /// **'Link keys to your Catalyst Keychain'** - String get walletLinkHeader; - - /// A subheader in link wallet flow in registration for wallet connection. - /// - /// In en, this message translates to: - /// **'Link your Cardano wallet'** - String get walletLinkWalletSubheader; - - /// A subheader in link wallet flow in registration for role chooser state. - /// - /// In en, this message translates to: - /// **'Select your Catalyst roles'** - String get walletLinkRolesSubheader; - - /// A subheader in link wallet flow in registration for RBAC transaction. - /// - /// In en, this message translates to: - /// **'Sign your Catalyst roles to the\nCardano mainnet'** - String get walletLinkTransactionSubheader; - - /// A title in link wallet flow on intro screen. - /// - /// In en, this message translates to: - /// **'Link Cardano Wallet & Catalyst Roles to you Catalyst Keychain.'** - String get walletLinkIntroTitle; - - /// A message (content) in link wallet flow on intro screen. - /// - /// In en, this message translates to: - /// **'You\'re almost there! This is the final and most important step in your account setup.\n\nWe\'re going to link a Cardano Wallet to your Catalyst Keychain, so you can start collecting Role Keys.\n\nRole Keys allow you to enter new spaces, discover new ways to participate, and unlock new ways to earn rewards.\n\nWe\'ll start with your Voter Key by default. You can decide to add a Proposer Key and Drep key if you want, or you can always add them later.'** - String get walletLinkIntroContent; - - /// A title in link wallet flow on select wallet screen. - /// - /// In en, this message translates to: - /// **'Select the Cardano wallet to link\nto your Catalyst Keychain.'** - String get walletLinkSelectWalletTitle; - - /// A message (content) in link wallet flow on select wallet screen. - /// - /// In en, this message translates to: - /// **'To complete this action, you\'ll submit a signed transaction to Cardano. There will be an ADA transaction fee.'** - String get walletLinkSelectWalletContent; - - /// A title in link wallet flow on wallet details screen. - /// - /// In en, this message translates to: - /// **'Cardano wallet detection'** - String get walletLinkWalletDetailsTitle; - - /// A message in link wallet flow on wallet details screen. - /// - /// In en, this message translates to: - /// **'{wallet} connected successfully!'** - String walletLinkWalletDetailsContent(String wallet); - - /// A message in link wallet flow on wallet details screen when a user wallet doesn't have enough balance. - /// - /// In en, this message translates to: - /// **'Wallet and role registrations require a minimal transaction fee. You can setup your default dApp connector wallet in your browser extension settings.'** - String get walletLinkWalletDetailsNotice; - - /// A message recommending the user to top up ADA in wallet link on wallet details screen. - /// - /// In en, this message translates to: - /// **'Top up ADA'** - String get walletLinkWalletDetailsNoticeTopUp; - - /// A link to top-up provide when the user doesn't have enough balance on wallet link screen - /// - /// In en, this message translates to: - /// **'Link to top-up provider'** - String get walletLinkWalletDetailsNoticeTopUpLink; - - /// A title in link wallet flow on transaction screen. - /// - /// In en, this message translates to: - /// **'Let\'s make sure everything looks right.'** - String get walletLinkTransactionTitle; - - /// A subtitle in link wallet flow on transaction screen. - /// - /// In en, this message translates to: - /// **'Account completion for Catalyst'** - String get walletLinkTransactionAccountCompletion; - - /// An item in the transaction summary for the wallet link. - /// - /// In en, this message translates to: - /// **'1 Link {wallet} to Catalyst Keychain'** - String walletLinkTransactionLinkItem(String wallet); - - /// A side note on transaction summary in the wallet link explaining the positives about the registration. - /// - /// In en, this message translates to: - /// **'Positive small print'** - String get walletLinkTransactionPositiveSmallPrint; - - /// The first item for the positive small print message. - /// - /// In en, this message translates to: - /// **'Your registration is a one time event, cost will not renew periodically.'** - String get walletLinkTransactionPositiveSmallPrintItem1; - - /// The second item for the positive small print message. - /// - /// In en, this message translates to: - /// **'Your registrations can be found under your account profile after completion.'** - String get walletLinkTransactionPositiveSmallPrintItem2; - - /// The third item for the positive small print message. - /// - /// In en, this message translates to: - /// **'All registration fees go into the Cardano Treasury.'** - String get walletLinkTransactionPositiveSmallPrintItem3; - - /// The primary button label to sign a transaction on transaction summary screen. - /// - /// In en, this message translates to: - /// **'Sign transaction with wallet'** - String get walletLinkTransactionSign; - - /// The secondary button label to change the roles on transaction summary screen. - /// - /// In en, this message translates to: - /// **'Change role setup'** - String get walletLinkTransactionChangeRoles; - - /// An item in the transaction summary for the role registration - /// - /// In en, this message translates to: - /// **'1 {role} registration to Catalyst Keychain'** - String walletLinkTransactionRoleItem(String role); - - /// Indicates an error when submitting a registration transaction failed. - /// - /// In en, this message translates to: - /// **'Transaction failed'** - String get registrationTransactionFailed; - - /// Indicates an error when preparing a transaction has failed due to low wallet balance. - /// - /// In en, this message translates to: - /// **'Insufficient balance, please top up your wallet.'** - String get registrationInsufficientBalance; - - /// Error message shown when attempting to register or recover account but seed phrase was not found - /// - /// In en, this message translates to: - /// **'Seed phrase was not found. Make sure correct words are correct.'** - String get registrationSeedPhraseNotFound; - - /// Error message shown when attempting to register or recover account but password was not found - /// - /// In en, this message translates to: - /// **'Password was not found. Make sure valid password was created.'** - String get registrationUnlockPasswordNotFound; - - /// Error message shown when connect wallet but matching was not found - /// - /// In en, this message translates to: - /// **'Wallet not found'** - String get registrationWalletNotFound; - - /// A title on the role chooser screen in registration. - /// - /// In en, this message translates to: - /// **'How do you want to participate in Catalyst?'** - String get walletLinkRoleChooserTitle; - - /// A message on the role chooser screen in registration. - /// - /// In en, this message translates to: - /// **'In Catalyst you can take on different roles, learn more below and choose your additional roles now.'** - String get walletLinkRoleChooserContent; - - /// A title on the role summary screen in registration. - /// - /// In en, this message translates to: - /// **'Is this your correct Catalyst role setup?'** - String get walletLinkRoleSummaryTitle; - - /// The first part of the message on the role summary screen in registration. - /// - /// In en, this message translates to: - /// **'You would like to register '** - String get walletLinkRoleSummaryContent1; - - /// The middle (bold) part of the message on the role summary screen in registration. - /// - /// In en, this message translates to: - /// **'{count} active {count, plural, =0{roles} =1{role} other{roles}}'** - String walletLinkRoleSummaryContent2(num count); - - /// The last part of the message on the role summary screen in registration. - /// - /// In en, this message translates to: - /// **' in Catalyst.'** - String get walletLinkRoleSummaryContent3; - - /// Message shown when redirecting to external content that describes which wallets are supported. - /// - /// In en, this message translates to: - /// **'See all supported wallets'** - String get seeAllSupportedWallets; - - /// Message shown when presenting the details of a connected wallet. - /// - /// In en, this message translates to: - /// **'Wallet detection summary'** - String get walletDetectionSummary; - - /// The wallet balance in terms of Ada. - /// - /// In en, this message translates to: - /// **'Wallet balance'** - String get walletBalance; - - /// A cardano wallet address - /// - /// In en, this message translates to: - /// **'Wallet address'** - String get walletAddress; - - /// No description provided for @accountCreationCreate. - /// - /// In en, this message translates to: - /// **'Create a new 
Catalyst Keychain'** - String get accountCreationCreate; - - /// No description provided for @accountCreationRecover. - /// - /// In en, this message translates to: - /// **'Recover your
Catalyst Keychain'** - String get accountCreationRecover; - - /// Indicates that created keychain will be stored in this device only - /// - /// In en, this message translates to: - /// **'On this device'** - String get accountCreationOnThisDevice; - - /// No description provided for @accountCreationGetStartedTitle. - /// - /// In en, this message translates to: - /// **'Welcome to Catalyst'** - String get accountCreationGetStartedTitle; - - /// No description provided for @accountCreationGetStatedDesc. - /// - /// In en, this message translates to: - /// **'If you already have a Catalyst keychain you can restore it on this device, or you can create a new Catalyst Keychain.'** - String get accountCreationGetStatedDesc; - - /// No description provided for @accountCreationGetStatedWhatNext. - /// - /// In en, this message translates to: - /// **'What do you want to do?'** - String get accountCreationGetStatedWhatNext; - - /// Title of My Account page - /// - /// In en, this message translates to: - /// **'My Account / Profile & Keychain'** - String get myAccountProfileKeychain; - - /// Subtitle of My Account page - /// - /// In en, this message translates to: - /// **'Your Catalyst keychain & role registration'** - String get yourCatalystKeychainAndRoleRegistration; - - /// Tab on My Account page - /// - /// In en, this message translates to: - /// **'Profile & Keychain'** - String get profileAndKeychain; - - /// Action on Catalyst Keychain card - /// - /// In en, this message translates to: - /// **'Remove Keychain'** - String get removeKeychain; - - /// Describes that wallet is connected on Catalyst Keychain card - /// - /// In en, this message translates to: - /// **'Wallet connected'** - String get walletConnected; - - /// Describes roles on Catalyst Keychain card - /// - /// In en, this message translates to: - /// **'Current Role registrations'** - String get currentRoleRegistrations; - - /// Account role - /// - /// In en, this message translates to: - /// **'Voter'** - String get voter; - - /// Account role - /// - /// In en, this message translates to: - /// **'Proposer'** - String get proposer; - - /// Account role - /// - /// In en, this message translates to: - /// **'Drep'** - String get drep; - - /// Related to account role - /// - /// In en, this message translates to: - /// **'Default'** - String get defaultRole; - - /// No description provided for @catalystKeychain. - /// - /// In en, this message translates to: - /// **'Catalyst Keychain'** - String get catalystKeychain; - - /// No description provided for @accountCreationSplashTitle. - /// - /// In en, this message translates to: - /// **'Create your Catalyst Keychain'** - String get accountCreationSplashTitle; - - /// No description provided for @accountCreationSplashMessage. - /// - /// In en, this message translates to: - /// **'Your keychain is your ticket to participate in 
distributed innovation on the global stage. 

Once you have it, you\'ll be able to enter different spaces, discover awesome ideas, and share your feedback to hep improve ideas. 

As you add new keys to your keychain, you\'ll be able to enter new spaces, unlock new rewards opportunities, and have your voice heard in community decisions.'** - String get accountCreationSplashMessage; - - /// No description provided for @accountCreationSplashNextButton. - /// - /// In en, this message translates to: - /// **'Create your Keychain now'** - String get accountCreationSplashNextButton; - - /// No description provided for @accountInstructionsTitle. - /// - /// In en, this message translates to: - /// **'Great! Your Catalyst Keychain 
has been created.'** - String get accountInstructionsTitle; - - /// No description provided for @accountInstructionsMessage. - /// - /// In en, this message translates to: - /// **'On the next screen, you\'re going to see 12 words. 
This is called your \"seed phrase\". 

It\'s like a super secure password that only you know, 
that allows you to prove ownership of your keychain. 

You\'ll use it to login and recover your account on 
different devices, so be sure to put it somewhere safe!\n\nYou need to write this seed phrase down with pen and paper, so get this ready.'** - String get accountInstructionsMessage; - - /// For example in button that goes to next stage of registration - /// - /// In en, this message translates to: - /// **'Next'** - String get next; - - /// For example in button that goes to previous stage of registration - /// - /// In en, this message translates to: - /// **'Back'** - String get back; - - /// Retry action when something goes wrong. - /// - /// In en, this message translates to: - /// **'Retry'** - String get retry; - - /// Error description when something goes wrong. - /// - /// In en, this message translates to: - /// **'Something went wrong.'** - String get somethingWentWrong; - - /// A description when no wallet extension was found. - /// - /// In en, this message translates to: - /// **'No wallet found.'** - String get noWalletFound; - - /// A title on delete keychain dialog - /// - /// In en, this message translates to: - /// **'Delete Keychain?'** - String get deleteKeychainDialogTitle; - - /// A subtitle on delete keychain dialog - /// - /// In en, this message translates to: - /// **'Are you sure you wants to delete your\nCatalyst Keychain from this device?'** - String get deleteKeychainDialogSubtitle; - - /// A warning on delete keychain dialog - /// - /// In en, this message translates to: - /// **'Make sure you have a working Catalyst 12-word seedphrase!'** - String get deleteKeychainDialogWarning; - - /// A warning info on delete keychain dialog - /// - /// In en, this message translates to: - /// **'Your Catalyst account will be removed,\nthis action cannot be undone!'** - String get deleteKeychainDialogWarningInfo; - - /// A typing info on delete keychain dialog - /// - /// In en, this message translates to: - /// **'To avoid mistakes, please type ‘Remove Keychain’ below.'** - String get deleteKeychainDialogTypingInfo; - - /// An input label on delete keychain dialog - /// - /// In en, this message translates to: - /// **'Confirm removal'** - String get deleteKeychainDialogInputLabel; - - /// An error text on text field on delete keychain dialog - /// - /// In en, this message translates to: - /// **'Error. Please type \'Remove Keychain\' to remove your account from this device.'** - String get deleteKeychainDialogErrorText; - - /// A removing phrase on delete keychain dialog - /// - /// In en, this message translates to: - /// **'Remove Keychain'** - String get deleteKeychainDialogRemovingPhrase; - - /// A title on account role dialog - /// - /// In en, this message translates to: - /// **'Learn about Catalyst Roles'** - String get accountRoleDialogTitle; - - /// A label on account role dialog's button - /// - /// In en, this message translates to: - /// **'Continue Role setup'** - String get accountRoleDialogButton; - - /// A title for role summary on account role dialog - /// - /// In en, this message translates to: - /// **'{role} role summary'** - String accountRoleDialogRoleSummaryTitle(String role); - - /// A verbose name for voter - /// - /// In en, this message translates to: - /// **'Treasury guardian'** - String get voterVerboseName; - - /// A verbose name for proposer - /// - /// In en, this message translates to: - /// **'Main proposer'** - String get proposerVerboseName; - - /// A verbose name for drep - /// - /// In en, this message translates to: - /// **'Community expert'** - String get drepVerboseName; - - /// A description for voter - /// - /// In en, this message translates to: - /// **'The Voters are the guardians of Cardano treasury. They vote in projects for the growth of the Cardano Ecosystem.'** - String get voterDescription; - - /// A description for proposer - /// - /// In en, this message translates to: - /// **'The Main Proposers are the Innovators in Project Catalyst, they are the shapers of the future.'** - String get proposerDescription; - - /// A description for drep - /// - /// In en, this message translates to: - /// **'The dRep has an Expert Role in the Cardano/Catalyst as people can delegate their vote to Cardano Experts.'** - String get drepDescription; - - /// No description provided for @voterSummarySelectFavorites. - /// - /// In en, this message translates to: - /// **'Select favorites'** - String get voterSummarySelectFavorites; - - /// No description provided for @voterSummaryComment. - /// - /// In en, this message translates to: - /// **'Comment/Vote on Proposals'** - String get voterSummaryComment; - - /// No description provided for @voterSummaryCastVotes. - /// - /// In en, this message translates to: - /// **'Cast your votes'** - String get voterSummaryCastVotes; - - /// No description provided for @voterSummaryVoterRewards. - /// - /// In en, this message translates to: - /// **'Voter rewards'** - String get voterSummaryVoterRewards; - - /// No description provided for @proposerSummaryWriteEdit. - /// - /// In en, this message translates to: - /// **'Write/edit functionality'** - String get proposerSummaryWriteEdit; - - /// No description provided for @proposerSummarySubmitToFund. - /// - /// In en, this message translates to: - /// **'Rights to Submit to Fund'** - String get proposerSummarySubmitToFund; - - /// No description provided for @proposerSummaryInviteTeamMembers. - /// - /// In en, this message translates to: - /// **'Invite Team Members'** - String get proposerSummaryInviteTeamMembers; - - /// No description provided for @proposerSummaryComment. - /// - /// In en, this message translates to: - /// **'Comment functionality'** - String get proposerSummaryComment; - - /// No description provided for @drepSummaryDelegatedVotes. - /// - /// In en, this message translates to: - /// **'Delegated Votes'** - String get drepSummaryDelegatedVotes; - - /// No description provided for @drepSummaryRewards. - /// - /// In en, this message translates to: - /// **'dRep rewards'** - String get drepSummaryRewards; - - /// No description provided for @drepSummaryCastVotes. - /// - /// In en, this message translates to: - /// **'Cast delegated votes'** - String get drepSummaryCastVotes; - - /// No description provided for @drepSummaryComment. - /// - /// In en, this message translates to: - /// **'Comment Functionality'** - String get drepSummaryComment; - - /// No description provided for @delete. - /// - /// In en, this message translates to: - /// **'Delete'** - String get delete; - - /// No description provided for @close. - /// - /// In en, this message translates to: - /// **'Close'** - String get close; - - /// No description provided for @notice. - /// - /// In en, this message translates to: - /// **'Notice'** - String get notice; - - /// No description provided for @yes. - /// - /// In en, this message translates to: - /// **'Yes'** - String get yes; - - /// No description provided for @no. - /// - /// In en, this message translates to: - /// **'No'** - String get no; - - /// No description provided for @total. - /// - /// In en, this message translates to: - /// **'Total'** - String get total; - - /// No description provided for @file. - /// - /// In en, this message translates to: - /// **'file'** - String get file; - - /// No description provided for @key. - /// - /// In en, this message translates to: - /// **'key'** - String get key; - - /// No description provided for @upload. - /// - /// In en, this message translates to: - /// **'Upload'** - String get upload; - - /// No description provided for @browse. - /// - /// In en, this message translates to: - /// **'browse'** - String get browse; - - /// An info on upload dialog - /// - /// In en, this message translates to: - /// **'Drop your {itemNameToUpload} here or '** - String uploadDropInfo(String itemNameToUpload); - - /// No description provided for @uploadProgressInfo. - /// - /// In en, this message translates to: - /// **'Upload in progress'** - String get uploadProgressInfo; - - /// A title on keychain upload dialog - /// - /// In en, this message translates to: - /// **'Upload Catalyst Keychain'** - String get uploadKeychainTitle; - - /// An info on keychain upload dialog - /// - /// In en, this message translates to: - /// **'Make sure it\'s a correct Catalyst keychain file.'** - String get uploadKeychainInfo; - - /// Refers to a light theme mode. - /// - /// In en, this message translates to: - /// **'Light'** - String get themeLight; - - /// Refers to a dark theme mode. - /// - /// In en, this message translates to: - /// **'Dark'** - String get themeDark; - - /// No description provided for @keychainDeletedDialogTitle. - /// - /// In en, this message translates to: - /// **'Catalyst keychain removed'** - String get keychainDeletedDialogTitle; - - /// No description provided for @keychainDeletedDialogSubtitle. - /// - /// In en, this message translates to: - /// **'Your Catalyst Keychain is removed successfully from this device.'** - String get keychainDeletedDialogSubtitle; - - /// No description provided for @keychainDeletedDialogInfo. - /// - /// In en, this message translates to: - /// **'We reverted this device to Catalyst first use.'** - String get keychainDeletedDialogInfo; - - /// No description provided for @registrationCompletedTitle. - /// - /// In en, this message translates to: - /// **'Catalyst account setup'** - String get registrationCompletedTitle; - - /// No description provided for @registrationCompletedSubtitle. - /// - /// In en, this message translates to: - /// **'Completed!'** - String get registrationCompletedSubtitle; - - /// No description provided for @registrationCompletedSummaryHeader. - /// - /// In en, this message translates to: - /// **'Summary'** - String get registrationCompletedSummaryHeader; - - /// No description provided for @registrationCompletedKeychainTitle. - /// - /// In en, this message translates to: - /// **'Catalyst Keychain created'** - String get registrationCompletedKeychainTitle; - - /// No description provided for @registrationCompletedKeychainInfo. - /// - /// In en, this message translates to: - /// **'You created a Catalyst Keychain, backed up its seed phrase and set an unlock password.'** - String get registrationCompletedKeychainInfo; - - /// No description provided for @registrationCompletedWalletTitle. - /// - /// In en, this message translates to: - /// **'Cardano {walletName} wallet selected'** - String registrationCompletedWalletTitle(String walletName); - - /// No description provided for @registrationCompletedWalletInfo. - /// - /// In en, this message translates to: - /// **'You selected your {walletName} wallet as primary wallet for your voting power.'** - String registrationCompletedWalletInfo(String walletName); - - /// No description provided for @registrationCompletedRolesTitle. - /// - /// In en, this message translates to: - /// **'Catalyst roles selected'** - String get registrationCompletedRolesTitle; - - /// No description provided for @registrationCompletedRolesInfo. - /// - /// In en, this message translates to: - /// **'You linked your Cardano wallet and selected Catalyst roles via a signed transaction.'** - String get registrationCompletedRolesInfo; - - /// No description provided for @registrationCompletedRoleRegistration. - /// - /// In en, this message translates to: - /// **'role registration'** - String get registrationCompletedRoleRegistration; - - /// No description provided for @registrationCompletedDiscoveryButton. - /// - /// In en, this message translates to: - /// **'Open Discovery Dashboard'** - String get registrationCompletedDiscoveryButton; - - /// No description provided for @registrationCompletedAccountButton. - /// - /// In en, this message translates to: - /// **'Review my account'** - String get registrationCompletedAccountButton; - - /// No description provided for @createKeychainSeedPhraseSubtitle. - /// - /// In en, this message translates to: - /// **'Write down your 12 Catalyst 
security words'** - String get createKeychainSeedPhraseSubtitle; - - /// No description provided for @createKeychainSeedPhraseBody. - /// - /// In en, this message translates to: - /// **'Make sure you create an offline backup of your recovery phrase as well.'** - String get createKeychainSeedPhraseBody; - - /// No description provided for @createKeychainSeedPhraseDownload. - /// - /// In en, this message translates to: - /// **'Download Catalyst key'** - String get createKeychainSeedPhraseDownload; - - /// No description provided for @createKeychainSeedPhraseStoreConfirmation. - /// - /// In en, this message translates to: - /// **'I have written down/downloaded my 12 words'** - String get createKeychainSeedPhraseStoreConfirmation; - - /// No description provided for @createKeychainSeedPhraseCheckInstructionsTitle. - /// - /// In en, this message translates to: - /// **'Check your Catalyst security keys'** - String get createKeychainSeedPhraseCheckInstructionsTitle; - - /// No description provided for @createKeychainSeedPhraseCheckInstructionsSubtitle. - /// - /// In en, this message translates to: - /// **'Next, we\'re going to make sure that you\'ve written down your words correctly. 

We don\'t save your seed phrase, so it\'s important 
to make sure you have it right. That\'s why we do this confirmation before continuing. 

It\'s also good practice to get familiar with using a seed phrase if you\'re new to crypto.'** - String get createKeychainSeedPhraseCheckInstructionsSubtitle; - - /// No description provided for @createKeychainSeedPhraseCheckSubtitle. - /// - /// In en, this message translates to: - /// **'Input your Catalyst security keys'** - String get createKeychainSeedPhraseCheckSubtitle; - - /// No description provided for @createKeychainSeedPhraseCheckBody. - /// - /// In en, this message translates to: - /// **'Select your 12 written down words in 
the correct order.'** - String get createKeychainSeedPhraseCheckBody; - - /// When user checks correct seed phrase words order he can upload it too - /// - /// In en, this message translates to: - /// **'Upload Catalyst Key'** - String get uploadCatalystKey; - - /// No description provided for @reset. - /// - /// In en, this message translates to: - /// **'Reset'** - String get reset; - - /// No description provided for @createKeychainSeedPhraseCheckSuccessTitle. - /// - /// In en, this message translates to: - /// **'Nice job! You\'ve successfully verified the seed phrase for your keychain.'** - String get createKeychainSeedPhraseCheckSuccessTitle; - - /// No description provided for @createKeychainSeedPhraseCheckSuccessSubtitle. - /// - /// In en, this message translates to: - /// **'Enter your seed phrase to recover your Catalyst Keychain on any device.

It\'s kinda like your email and password all rolled into one, so keep it somewhere safe!

In the next step we\'ll add a password to your Catalyst Keychain, so you can lock/unlock access to Voices.'** - String get createKeychainSeedPhraseCheckSuccessSubtitle; - - /// No description provided for @yourNextStep. - /// - /// In en, this message translates to: - /// **'Your next step'** - String get yourNextStep; - - /// No description provided for @createKeychainSeedPhraseCheckSuccessNextStep. - /// - /// In en, this message translates to: - /// **'Now let’s set your Unlock password for this device!'** - String get createKeychainSeedPhraseCheckSuccessNextStep; - - /// No description provided for @createKeychainUnlockPasswordInstructionsTitle. - /// - /// In en, this message translates to: - /// **'Set your Catalyst unlock password 
for this device'** - String get createKeychainUnlockPasswordInstructionsTitle; - - /// No description provided for @createKeychainUnlockPasswordInstructionsSubtitle. - /// - /// In en, this message translates to: - /// **'With over 300 trillion possible combinations, your 12 word seed phrase is great for keeping your account safe. 

But it can be a bit tedious to enter every single time you want to use the app. 

In this next step, you\'ll set your Unlock Password for your current device. It\'s like a shortcut for proving ownership of your Keychain. 

Whenever you recover your account for the first time on a new device, you\'ll need to use your Catalyst Keychain to get started. Every time after that, you can use your Unlock Password to quickly regain access.'** - String get createKeychainUnlockPasswordInstructionsSubtitle; - - /// No description provided for @createKeychainCreatedTitle. - /// - /// In en, this message translates to: - /// **'Congratulations your Catalyst 
Keychain is created!'** - String get createKeychainCreatedTitle; - - /// No description provided for @createKeychainCreatedNextStep. - /// - /// In en, this message translates to: - /// **'In the next step you write your Catalyst roles and 
account to the Cardano Mainnet.'** - String get createKeychainCreatedNextStep; - - /// No description provided for @createKeychainLinkWalletAndRoles. - /// - /// In en, this message translates to: - /// **'Link your Cardano Wallet & Roles'** - String get createKeychainLinkWalletAndRoles; - - /// No description provided for @registrationCreateKeychainStepGroup. - /// - /// In en, this message translates to: - /// **'Catalyst Keychain created'** - String get registrationCreateKeychainStepGroup; - - /// No description provided for @registrationLinkWalletStepGroup. - /// - /// In en, this message translates to: - /// **'Link Cardano Wallet & Roles'** - String get registrationLinkWalletStepGroup; - - /// No description provided for @registrationCompletedStepGroup. - /// - /// In en, this message translates to: - /// **'Catalyst account creation completed!'** - String get registrationCompletedStepGroup; - - /// No description provided for @createKeychainUnlockPasswordIntoSubtitle. - /// - /// In en, this message translates to: - /// **'Catalyst unlock password'** - String get createKeychainUnlockPasswordIntoSubtitle; - - /// No description provided for @createKeychainUnlockPasswordIntoBody. - /// - /// In en, this message translates to: - /// **'Please provide a password for your Catalyst Keychain.'** - String get createKeychainUnlockPasswordIntoBody; - - /// No description provided for @enterPassword. - /// - /// In en, this message translates to: - /// **'Enter password'** - String get enterPassword; - - /// No description provided for @confirmPassword. - /// - /// In en, this message translates to: - /// **'Confirm password'** - String get confirmPassword; - - /// No description provided for @xCharactersMinimum. - /// - /// In en, this message translates to: - /// **'{number} characters minimum length'** - String xCharactersMinimum(int number); - - /// When user confirms password but it does not match original one. - /// - /// In en, this message translates to: - /// **'Passwords do not match, please correct'** - String get passwordDoNotMatch; - - /// No description provided for @warning. - /// - /// In en, this message translates to: - /// **'Warning'** - String get warning; - - /// No description provided for @alert. - /// - /// In en, this message translates to: - /// **'Alert'** - String get alert; - - /// No description provided for @registrationExitConfirmDialogSubtitle. - /// - /// In en, this message translates to: - /// **'Account creation incomplete!'** - String get registrationExitConfirmDialogSubtitle; - - /// No description provided for @registrationExitConfirmDialogContent. - /// - /// In en, this message translates to: - /// **'If attempt to leave without creating your keychain - account creation will be incomplete. 

You are not able to login without 
completing your keychain.'** - String get registrationExitConfirmDialogContent; - - /// No description provided for @registrationExitConfirmDialogContinue. - /// - /// In en, this message translates to: - /// **'Continue keychain creation'** - String get registrationExitConfirmDialogContinue; - - /// No description provided for @cancelAnyways. - /// - /// In en, this message translates to: - /// **'Cancel anyway'** - String get cancelAnyways; - - /// No description provided for @recoverCatalystKeychain. - /// - /// In en, this message translates to: - /// **'Restore Catalyst keychain'** - String get recoverCatalystKeychain; - - /// No description provided for @recoverKeychainMethodsTitle. - /// - /// In en, this message translates to: - /// **'Restore your Catalyst Keychain'** - String get recoverKeychainMethodsTitle; - - /// No description provided for @recoverKeychainMethodsNoKeychainFound. - /// - /// In en, this message translates to: - /// **'No Catalyst Keychain found on this device.'** - String get recoverKeychainMethodsNoKeychainFound; - - /// No description provided for @recoverKeychainMethodsSubtitle. - /// - /// In en, this message translates to: - /// **'Not to worry, in the next step you can choose the recovery option that applies to you for this device!'** - String get recoverKeychainMethodsSubtitle; - - /// No description provided for @recoverKeychainMethodsListTitle. - /// - /// In en, this message translates to: - /// **'How do you want Restore your Catalyst Keychain?'** - String get recoverKeychainMethodsListTitle; - - /// No description provided for @recoverKeychainNonFound. - /// - /// In en, this message translates to: - /// **'No Catalyst Keychain found
on this device.'** - String get recoverKeychainNonFound; - - /// No description provided for @recoverKeychainFound. - /// - /// In en, this message translates to: - /// **'Keychain found! 
Please unlock your device.'** - String get recoverKeychainFound; - - /// No description provided for @seedPhrase12Words. - /// - /// In en, this message translates to: - /// **'12 security words'** - String get seedPhrase12Words; - - /// No description provided for @recoverySeedPhraseInstructionsTitle. - /// - /// In en, this message translates to: - /// **'Restore your Catalyst Keychain with 
your 12 security words.'** - String get recoverySeedPhraseInstructionsTitle; - - /// No description provided for @recoverySeedPhraseInstructionsSubtitle. - /// - /// In en, this message translates to: - /// **'Enter your security words in the correct order, and sign into your Catalyst account on a new device.'** - String get recoverySeedPhraseInstructionsSubtitle; - - /// No description provided for @recoverySeedPhraseInputTitle. - /// - /// In en, this message translates to: - /// **'Restore your Catalyst Keychain with 
your 12 security words'** - String get recoverySeedPhraseInputTitle; - - /// No description provided for @recoverySeedPhraseInputSubtitle. - /// - /// In en, this message translates to: - /// **'Enter each word of your Catalyst Key in the right order 
to bring your Catalyst account to this device.'** - String get recoverySeedPhraseInputSubtitle; - - /// No description provided for @recoveryAccountTitle. - /// - /// In en, this message translates to: - /// **'Catalyst account recovery'** - String get recoveryAccountTitle; - - /// No description provided for @recoveryAccountSuccessTitle. - /// - /// In en, this message translates to: - /// **'Keychain recovered successfully!'** - String get recoveryAccountSuccessTitle; - - /// No description provided for @recoveryAccountDetailsAction. - /// - /// In en, this message translates to: - /// **'Set unlock password for this device'** - String get recoveryAccountDetailsAction; - - /// No description provided for @recoveryUnlockPasswordInstructionsTitle. - /// - /// In en, this message translates to: - /// **'Set your Catalyst unlock password f
or this device'** - String get recoveryUnlockPasswordInstructionsTitle; - - /// No description provided for @recoveryUnlockPasswordInstructionsSubtitle. - /// - /// In en, this message translates to: - /// **'With over 300 trillion possible combinations, your 12 word seed phrase is great for keeping your account safe. 

But it can be a bit tedious to enter every single time you want to use the app. 

In this next step, you\'ll set your Unlock Password for your current device. It\'s like a shortcut for proving ownership of your Keychain. 

Whenever you recover your account for the first time on a new device, you\'ll need to use your Catalyst Keychain to get started. Every time after that, you can use your Unlock Password to quickly regain access.'** - String get recoveryUnlockPasswordInstructionsSubtitle; - - /// No description provided for @recoverDifferentKeychain. - /// - /// In en, this message translates to: - /// **'Restore a different keychain'** - String get recoverDifferentKeychain; - - /// The header label in unlock dialog. - /// - /// In en, this message translates to: - /// **'Unlock Catalyst'** - String get unlockDialogHeader; - - /// The title label in unlock dialog. - /// - /// In en, this message translates to: - /// **'Welcome back!'** - String get unlockDialogTitle; - - /// The content (body) in unlock dialog. - /// - /// In en, this message translates to: - /// **'Please enter your device specific unlock password\nto unlock Catalyst Voices.'** - String get unlockDialogContent; - - /// The hint for the unlock password field. - /// - /// In en, this message translates to: - /// **'Enter your Unlock password'** - String get unlockDialogHint; - - /// An error message shown below the password field when the password is incorrect. - /// - /// In en, this message translates to: - /// **'Password is incorrect, try again.'** - String get unlockDialogIncorrectPassword; - - /// The message shown when asking the user to login/unlock and the user wants to cancel the process. - /// - /// In en, this message translates to: - /// **'Continue as guest'** - String get continueAsGuest; - - /// The title shown in confirmation snackbar after unlocking the keychain. - /// - /// In en, this message translates to: - /// **'Catalyst unlocked!'** - String get unlockSnackbarTitle; - - /// The message shown below the title in confirmation snackbar after unlocking the keychain. - /// - /// In en, this message translates to: - /// **'You can now fully use the application.'** - String get unlockSnackbarMessage; - - /// The title shown in confirmation snackbar after locking the keychain. - /// - /// In en, this message translates to: - /// **'Catalyst locked'** - String get lockSnackbarTitle; - - /// The message shown below the title in confirmation snackbar after locking the keychain. - /// - /// In en, this message translates to: - /// **'Catalyst is now in guest/locked mode.'** - String get lockSnackbarMessage; - - /// No description provided for @recoverySuccessTitle. - /// - /// In en, this message translates to: - /// **'Congratulations your Catalyst 
Keychain is restored!'** - String get recoverySuccessTitle; - - /// No description provided for @recoverySuccessSubtitle. - /// - /// In en, this message translates to: - /// **'You have successfully restored your Catalyst Keychain, and unlocked Catalyst Voices on this device.'** - String get recoverySuccessSubtitle; - - /// No description provided for @recoverySuccessGoToDashboard. - /// - /// In en, this message translates to: - /// **'Jump into the Discovery space / Dashboard'** - String get recoverySuccessGoToDashboard; - - /// No description provided for @recoverySuccessGoAccount. - /// - /// In en, this message translates to: - /// **'Check my account'** - String get recoverySuccessGoAccount; - - /// No description provided for @recoveryExitConfirmDialogSubtitle. - /// - /// In en, this message translates to: - /// **'12 word keychain restoration incomplete'** - String get recoveryExitConfirmDialogSubtitle; - - /// No description provided for @recoveryExitConfirmDialogContent. - /// - /// In en, this message translates to: - /// **'Please continue your Catalyst Keychain restoration, if you cancel all input will be lost.'** - String get recoveryExitConfirmDialogContent; - - /// No description provided for @recoveryExitConfirmDialogContinue. - /// - /// In en, this message translates to: - /// **'Continue recovery process'** - String get recoveryExitConfirmDialogContinue; - - /// Refers to the action label for recovering the user account. - /// - /// In en, this message translates to: - /// **'Recover account'** - String get recoverAccount; - - /// No description provided for @uploadConfirmDialogSubtitle. - /// - /// In en, this message translates to: - /// **'SWITCH TO FILE UPLOAD'** - String get uploadConfirmDialogSubtitle; - - /// No description provided for @uploadConfirmDialogContent. - /// - /// In en, this message translates to: - /// **'Do you want to cancel manual input, and switch to Catalyst key upload?'** - String get uploadConfirmDialogContent; - - /// No description provided for @uploadConfirmDialogYesButton. - /// - /// In en, this message translates to: - /// **'Yes, switch to Catalyst key upload'** - String get uploadConfirmDialogYesButton; - - /// No description provided for @uploadConfirmDialogResumeButton. - /// - /// In en, this message translates to: - /// **'Resume manual inputs'** - String get uploadConfirmDialogResumeButton; - - /// No description provided for @incorrectUploadDialogSubtitle. - /// - /// In en, this message translates to: - /// **'CATALYST KEY INCORRECT'** - String get incorrectUploadDialogSubtitle; - - /// No description provided for @incorrectUploadDialogContent. - /// - /// In en, this message translates to: - /// **'The Catalyst keychain that you entered or uploaded is incorrect, please try again.'** - String get incorrectUploadDialogContent; - - /// No description provided for @incorrectUploadDialogTryAgainButton. - /// - /// In en, this message translates to: - /// **'Try again'** - String get incorrectUploadDialogTryAgainButton; - - /// No description provided for @finishAccountCreation. - /// - /// In en, this message translates to: - /// **'Finish account creation'** - String get finishAccountCreation; - - /// A button label to connect a different wallet in wallet detail panel. - /// - /// In en, this message translates to: - /// **'Connect a different wallet'** - String get connectDifferentWallet; - - /// A button label to review the registration transaction in wallet detail panel. - /// - /// In en, this message translates to: - /// **'Review registration transaction'** - String get reviewRegistrationTransaction; - - /// A label for the format field in the date picker. - /// - /// In en, this message translates to: - /// **'Format'** - String get format; - - /// Error message for the date picker when the selected date is outside the range of today and one year from today. - /// - /// In en, this message translates to: - /// **'Please select a date within the range of today and one year from today.'** - String get datePickerDateRangeError; - - /// Error message for the date picker when the selected day is greater than the maximum days for the selected month. - /// - /// In en, this message translates to: - /// **'Entered day exceeds the maximum days for this month.'** - String get datePickerDaysInMonthError; -} - -class _VoicesLocalizationsDelegate extends LocalizationsDelegate { - const _VoicesLocalizationsDelegate(); - - @override - Future load(Locale locale) { - return lookupVoicesLocalizations(locale); - } - - @override - bool isSupported(Locale locale) => ['en', 'es'].contains(locale.languageCode); - - @override - bool shouldReload(_VoicesLocalizationsDelegate old) => false; -} - -Future lookupVoicesLocalizations(Locale locale) { - - - // Lookup logic when only language code is specified. - switch (locale.languageCode) { - case 'en': return catalyst_voices_localizations_en.loadLibrary().then((dynamic _) => catalyst_voices_localizations_en.VoicesLocalizationsEn()); - case 'es': return catalyst_voices_localizations_es.loadLibrary().then((dynamic _) => catalyst_voices_localizations_es.VoicesLocalizationsEs()); - } - - throw FlutterError( - 'VoicesLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' - 'an issue with the localizations generation tool. Please file an issue ' - 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.' - ); -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart deleted file mode 100644 index ee2b99479b9..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart +++ /dev/null @@ -1,1012 +0,0 @@ -import 'package:intl/intl.dart' as intl; - -import 'catalyst_voices_localizations.dart'; - -// ignore_for_file: type=lint - -/// The translations for English (`en`). -class VoicesLocalizationsEn extends VoicesLocalizations { - VoicesLocalizationsEn([String locale = 'en']) : super(locale); - - @override - String get emailLabelText => 'Email'; - - @override - String get emailHintText => 'mail@example.com'; - - @override - String get emailErrorText => 'mail@example.com'; - - @override - String get cancelButtonText => 'Cancel'; - - @override - String get editButtonText => 'Edit'; - - @override - String get headerTooltipText => 'Header'; - - @override - String get placeholderRichText => 'Start writing your text...'; - - @override - String get supportingTextLabelText => 'Supporting text'; - - @override - String get saveButtonText => 'Save'; - - @override - String get passwordLabelText => 'Password'; - - @override - String get passwordHintText => 'My1SecretPassword'; - - @override - String get passwordErrorText => 'Password must be at least 8 characters long'; - - @override - String get loginTitleText => 'Login'; - - @override - String get loginButtonText => 'Login'; - - @override - String get loginScreenErrorMessage => 'Wrong credentials'; - - @override - String get homeScreenText => 'Catalyst Voices'; - - @override - String get comingSoonSubtitle => 'Voices'; - - @override - String get comingSoonTitle1 => 'Coming'; - - @override - String get comingSoonTitle2 => 'soon'; - - @override - String get comingSoonDescription => 'Project Catalyst is the world\'s largest decentralized innovation engine for solving real-world challenges.'; - - @override - String get connectingStatusLabelText => 're-connecting'; - - @override - String get finishAccountButtonLabelText => 'Finish account'; - - @override - String get getStartedButtonLabelText => 'Get Started'; - - @override - String get unlockButtonLabelText => 'Unlock'; - - @override - String get userProfileGuestLabelText => 'Guest'; - - @override - String get searchButtonLabelText => '[cmd=K]'; - - @override - String get snackbarInfoLabelText => 'Info'; - - @override - String get snackbarInfoMessageText => 'This is an info message!'; - - @override - String get snackbarSuccessLabelText => 'Success'; - - @override - String get snackbarSuccessMessageText => 'This is a success message!'; - - @override - String get snackbarWarningLabelText => 'Warning'; - - @override - String get snackbarWarningMessageText => 'This is a warning message!'; - - @override - String get snackbarErrorLabelText => 'Error'; - - @override - String get snackbarErrorMessageText => 'This is an error message!'; - - @override - String get snackbarRefreshButtonText => 'Refresh'; - - @override - String get snackbarMoreButtonText => 'Learn more'; - - @override - String get snackbarOkButtonText => 'Ok'; - - @override - String seedPhraseSlotNr(int nr) { - return 'Slot $nr'; - } - - @override - String get proposalStatusReady => 'Ready'; - - @override - String get proposalStatusDraft => 'Draft'; - - @override - String get proposalStatusInProgress => 'In progress'; - - @override - String get proposalStatusPrivate => 'Private'; - - @override - String get proposalStatusLive => 'LIVE'; - - @override - String get proposalStatusCompleted => 'Completed'; - - @override - String get proposalStatusOpen => 'Open'; - - @override - String get fundedProposal => 'Funded proposal'; - - @override - String get publishedProposal => 'Published proposal'; - - @override - String fundedProposalDate(DateTime date) { - final intl.DateFormat dateDateFormat = intl.DateFormat.yMMMMd(localeName); - final String dateString = dateDateFormat.format(date); - - return 'Funded $dateString'; - } - - @override - String lastUpdateDate(String date) { - return 'Last update: $date.'; - } - - @override - String get fundsRequested => 'Funds requested'; - - @override - String noOfComments(num count) { - final intl.NumberFormat countNumberFormat = intl.NumberFormat.compact( - locale: localeName, - - ); - final String countString = countNumberFormat.format(count); - - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'comments', - one: 'comment', - zero: 'comments', - ); - return '$countString $_temp0'; - } - - @override - String noOfSegmentsCompleted(num completed, num total, num percentage) { - final intl.NumberFormat completedNumberFormat = intl.NumberFormat.compact( - locale: localeName, - - ); - final String completedString = completedNumberFormat.format(completed); - final intl.NumberFormat totalNumberFormat = intl.NumberFormat.compact( - locale: localeName, - - ); - final String totalString = totalNumberFormat.format(total); - final intl.NumberFormat percentageNumberFormat = intl.NumberFormat.compact( - locale: localeName, - - ); - final String percentageString = percentageNumberFormat.format(percentage); - - String _temp0 = intl.Intl.pluralLogic( - total, - locale: localeName, - other: 'segments', - one: 'segment', - zero: 'segments', - ); - return '$completedString of $totalString ($percentageString%) $_temp0 completed'; - } - - @override - String get today => 'Today'; - - @override - String get yesterday => 'Yesterday'; - - @override - String get twoDaysAgo => '2 days ago'; - - @override - String get tomorrow => 'Tomorrow'; - - @override - String get activeVotingRound => 'Active voting round 14'; - - @override - String noOfAllProposals(int count) { - return 'All proposals ($count)'; - } - - @override - String get favorites => 'Favorites'; - - @override - String get treasuryCampaignBuilder => 'Campaign builder'; - - @override - String get treasuryCampaignBuilderSegments => 'Segments'; - - @override - String get treasuryCampaignSetup => 'Setup Campaign'; - - @override - String get treasuryCampaignTitle => 'Campaign title'; - - @override - String get stepEdit => 'Edit'; - - @override - String get workspaceProposalNavigation => 'Proposal navigation'; - - @override - String get workspaceProposalNavigationSegments => 'Segments'; - - @override - String get workspaceProposalSetup => 'Proposal setup'; - - @override - String get drawerSpaceTreasury => 'Treasury'; - - @override - String get drawerSpaceDiscovery => 'Discovery'; - - @override - String get drawerSpaceWorkspace => 'Workspace'; - - @override - String get drawerSpaceVoting => 'Voting'; - - @override - String get drawerSpaceFundedProjects => 'Funded projects'; - - @override - String get fundedProjectSpace => 'Funded project space'; - - @override - String noOfFundedProposals(int count) { - return 'Funded proposals ($count)'; - } - - @override - String get followed => 'Followed'; - - @override - String get overallSpacesSearchBrands => 'Search Brands'; - - @override - String get overallSpacesTasks => 'Tasks'; - - @override - String get voicesUpdateReady => 'Voices update ready'; - - @override - String get clickToRestart => 'Click to restart'; - - @override - String get spaceTreasuryName => 'Treasury space'; - - @override - String get spaceDiscoveryName => 'Discovery space'; - - @override - String get spaceWorkspaceName => 'Workspace'; - - @override - String get spaceVotingName => 'Voting space'; - - @override - String get spaceFundedProjects => 'Funded project space'; - - @override - String get lock => 'Lock'; - - @override - String get unlock => 'Unlock'; - - @override - String get getStarted => 'Get Started'; - - @override - String get guest => 'Guest'; - - @override - String get visitor => 'Visitor'; - - @override - String get noConnectionBannerRefreshButtonText => 'Refresh'; - - @override - String get noConnectionBannerTitle => 'No internet connection'; - - @override - String get noConnectionBannerDescription => 'Your internet is playing hide and seek. Check your internet connection, or try again in a moment.'; - - @override - String get weakPasswordStrength => 'Weak password strength'; - - @override - String get normalPasswordStrength => 'Normal password strength'; - - @override - String get goodPasswordStrength => 'Good password strength'; - - @override - String get chooseCardanoWallet => 'Choose Cardano Wallet'; - - @override - String get chooseOtherWallet => 'Choose other wallet'; - - @override - String get learnMore => 'Learn More'; - - @override - String get walletLinkHeader => 'Link keys to your Catalyst Keychain'; - - @override - String get walletLinkWalletSubheader => 'Link your Cardano wallet'; - - @override - String get walletLinkRolesSubheader => 'Select your Catalyst roles'; - - @override - String get walletLinkTransactionSubheader => 'Sign your Catalyst roles to the\nCardano mainnet'; - - @override - String get walletLinkIntroTitle => 'Link Cardano Wallet & Catalyst Roles to you Catalyst Keychain.'; - - @override - String get walletLinkIntroContent => 'You\'re almost there! This is the final and most important step in your account setup.\n\nWe\'re going to link a Cardano Wallet to your Catalyst Keychain, so you can start collecting Role Keys.\n\nRole Keys allow you to enter new spaces, discover new ways to participate, and unlock new ways to earn rewards.\n\nWe\'ll start with your Voter Key by default. You can decide to add a Proposer Key and Drep key if you want, or you can always add them later.'; - - @override - String get walletLinkSelectWalletTitle => 'Select the Cardano wallet to link\nto your Catalyst Keychain.'; - - @override - String get walletLinkSelectWalletContent => 'To complete this action, you\'ll submit a signed transaction to Cardano. There will be an ADA transaction fee.'; - - @override - String get walletLinkWalletDetailsTitle => 'Cardano wallet detection'; - - @override - String walletLinkWalletDetailsContent(String wallet) { - return '$wallet connected successfully!'; - } - - @override - String get walletLinkWalletDetailsNotice => 'Wallet and role registrations require a minimal transaction fee. You can setup your default dApp connector wallet in your browser extension settings.'; - - @override - String get walletLinkWalletDetailsNoticeTopUp => 'Top up ADA'; - - @override - String get walletLinkWalletDetailsNoticeTopUpLink => 'Link to top-up provider'; - - @override - String get walletLinkTransactionTitle => 'Let\'s make sure everything looks right.'; - - @override - String get walletLinkTransactionAccountCompletion => 'Account completion for Catalyst'; - - @override - String walletLinkTransactionLinkItem(String wallet) { - return '1 Link $wallet to Catalyst Keychain'; - } - - @override - String get walletLinkTransactionPositiveSmallPrint => 'Positive small print'; - - @override - String get walletLinkTransactionPositiveSmallPrintItem1 => 'Your registration is a one time event, cost will not renew periodically.'; - - @override - String get walletLinkTransactionPositiveSmallPrintItem2 => 'Your registrations can be found under your account profile after completion.'; - - @override - String get walletLinkTransactionPositiveSmallPrintItem3 => 'All registration fees go into the Cardano Treasury.'; - - @override - String get walletLinkTransactionSign => 'Sign transaction with wallet'; - - @override - String get walletLinkTransactionChangeRoles => 'Change role setup'; - - @override - String walletLinkTransactionRoleItem(String role) { - return '1 $role registration to Catalyst Keychain'; - } - - @override - String get registrationTransactionFailed => 'Transaction failed'; - - @override - String get registrationInsufficientBalance => 'Insufficient balance, please top up your wallet.'; - - @override - String get registrationSeedPhraseNotFound => 'Seed phrase was not found. Make sure correct words are correct.'; - - @override - String get registrationUnlockPasswordNotFound => 'Password was not found. Make sure valid password was created.'; - - @override - String get registrationWalletNotFound => 'Wallet not found'; - - @override - String get walletLinkRoleChooserTitle => 'How do you want to participate in Catalyst?'; - - @override - String get walletLinkRoleChooserContent => 'In Catalyst you can take on different roles, learn more below and choose your additional roles now.'; - - @override - String get walletLinkRoleSummaryTitle => 'Is this your correct Catalyst role setup?'; - - @override - String get walletLinkRoleSummaryContent1 => 'You would like to register '; - - @override - String walletLinkRoleSummaryContent2(num count) { - final intl.NumberFormat countNumberFormat = intl.NumberFormat.compact( - locale: localeName, - - ); - final String countString = countNumberFormat.format(count); - - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'roles', - one: 'role', - zero: 'roles', - ); - return '$countString active $_temp0'; - } - - @override - String get walletLinkRoleSummaryContent3 => ' in Catalyst.'; - - @override - String get seeAllSupportedWallets => 'See all supported wallets'; - - @override - String get walletDetectionSummary => 'Wallet detection summary'; - - @override - String get walletBalance => 'Wallet balance'; - - @override - String get walletAddress => 'Wallet address'; - - @override - String get accountCreationCreate => 'Create a new 
Catalyst Keychain'; - - @override - String get accountCreationRecover => 'Recover your
Catalyst Keychain'; - - @override - String get accountCreationOnThisDevice => 'On this device'; - - @override - String get accountCreationGetStartedTitle => 'Welcome to Catalyst'; - - @override - String get accountCreationGetStatedDesc => 'If you already have a Catalyst keychain you can restore it on this device, or you can create a new Catalyst Keychain.'; - - @override - String get accountCreationGetStatedWhatNext => 'What do you want to do?'; - - @override - String get myAccountProfileKeychain => 'My Account / Profile & Keychain'; - - @override - String get yourCatalystKeychainAndRoleRegistration => 'Your Catalyst keychain & role registration'; - - @override - String get profileAndKeychain => 'Profile & Keychain'; - - @override - String get removeKeychain => 'Remove Keychain'; - - @override - String get walletConnected => 'Wallet connected'; - - @override - String get currentRoleRegistrations => 'Current Role registrations'; - - @override - String get voter => 'Voter'; - - @override - String get proposer => 'Proposer'; - - @override - String get drep => 'Drep'; - - @override - String get defaultRole => 'Default'; - - @override - String get catalystKeychain => 'Catalyst Keychain'; - - @override - String get accountCreationSplashTitle => 'Create your Catalyst Keychain'; - - @override - String get accountCreationSplashMessage => 'Your keychain is your ticket to participate in 
distributed innovation on the global stage. 

Once you have it, you\'ll be able to enter different spaces, discover awesome ideas, and share your feedback to hep improve ideas. 

As you add new keys to your keychain, you\'ll be able to enter new spaces, unlock new rewards opportunities, and have your voice heard in community decisions.'; - - @override - String get accountCreationSplashNextButton => 'Create your Keychain now'; - - @override - String get accountInstructionsTitle => 'Great! Your Catalyst Keychain 
has been created.'; - - @override - String get accountInstructionsMessage => 'On the next screen, you\'re going to see 12 words. 
This is called your \"seed phrase\". 

It\'s like a super secure password that only you know, 
that allows you to prove ownership of your keychain. 

You\'ll use it to login and recover your account on 
different devices, so be sure to put it somewhere safe!\n\nYou need to write this seed phrase down with pen and paper, so get this ready.'; - - @override - String get next => 'Next'; - - @override - String get back => 'Back'; - - @override - String get retry => 'Retry'; - - @override - String get somethingWentWrong => 'Something went wrong.'; - - @override - String get noWalletFound => 'No wallet found.'; - - @override - String get deleteKeychainDialogTitle => 'Delete Keychain?'; - - @override - String get deleteKeychainDialogSubtitle => 'Are you sure you wants to delete your\nCatalyst Keychain from this device?'; - - @override - String get deleteKeychainDialogWarning => 'Make sure you have a working Catalyst 12-word seedphrase!'; - - @override - String get deleteKeychainDialogWarningInfo => 'Your Catalyst account will be removed,\nthis action cannot be undone!'; - - @override - String get deleteKeychainDialogTypingInfo => 'To avoid mistakes, please type ‘Remove Keychain’ below.'; - - @override - String get deleteKeychainDialogInputLabel => 'Confirm removal'; - - @override - String get deleteKeychainDialogErrorText => 'Error. Please type \'Remove Keychain\' to remove your account from this device.'; - - @override - String get deleteKeychainDialogRemovingPhrase => 'Remove Keychain'; - - @override - String get accountRoleDialogTitle => 'Learn about Catalyst Roles'; - - @override - String get accountRoleDialogButton => 'Continue Role setup'; - - @override - String accountRoleDialogRoleSummaryTitle(String role) { - return '$role role summary'; - } - - @override - String get voterVerboseName => 'Treasury guardian'; - - @override - String get proposerVerboseName => 'Main proposer'; - - @override - String get drepVerboseName => 'Community expert'; - - @override - String get voterDescription => 'The Voters are the guardians of Cardano treasury. They vote in projects for the growth of the Cardano Ecosystem.'; - - @override - String get proposerDescription => 'The Main Proposers are the Innovators in Project Catalyst, they are the shapers of the future.'; - - @override - String get drepDescription => 'The dRep has an Expert Role in the Cardano/Catalyst as people can delegate their vote to Cardano Experts.'; - - @override - String get voterSummarySelectFavorites => 'Select favorites'; - - @override - String get voterSummaryComment => 'Comment/Vote on Proposals'; - - @override - String get voterSummaryCastVotes => 'Cast your votes'; - - @override - String get voterSummaryVoterRewards => 'Voter rewards'; - - @override - String get proposerSummaryWriteEdit => 'Write/edit functionality'; - - @override - String get proposerSummarySubmitToFund => 'Rights to Submit to Fund'; - - @override - String get proposerSummaryInviteTeamMembers => 'Invite Team Members'; - - @override - String get proposerSummaryComment => 'Comment functionality'; - - @override - String get drepSummaryDelegatedVotes => 'Delegated Votes'; - - @override - String get drepSummaryRewards => 'dRep rewards'; - - @override - String get drepSummaryCastVotes => 'Cast delegated votes'; - - @override - String get drepSummaryComment => 'Comment Functionality'; - - @override - String get delete => 'Delete'; - - @override - String get close => 'Close'; - - @override - String get notice => 'Notice'; - - @override - String get yes => 'Yes'; - - @override - String get no => 'No'; - - @override - String get total => 'Total'; - - @override - String get file => 'file'; - - @override - String get key => 'key'; - - @override - String get upload => 'Upload'; - - @override - String get browse => 'browse'; - - @override - String uploadDropInfo(String itemNameToUpload) { - return 'Drop your $itemNameToUpload here or '; - } - - @override - String get uploadProgressInfo => 'Upload in progress'; - - @override - String get uploadKeychainTitle => 'Upload Catalyst Keychain'; - - @override - String get uploadKeychainInfo => 'Make sure it\'s a correct Catalyst keychain file.'; - - @override - String get themeLight => 'Light'; - - @override - String get themeDark => 'Dark'; - - @override - String get keychainDeletedDialogTitle => 'Catalyst keychain removed'; - - @override - String get keychainDeletedDialogSubtitle => 'Your Catalyst Keychain is removed successfully from this device.'; - - @override - String get keychainDeletedDialogInfo => 'We reverted this device to Catalyst first use.'; - - @override - String get registrationCompletedTitle => 'Catalyst account setup'; - - @override - String get registrationCompletedSubtitle => 'Completed!'; - - @override - String get registrationCompletedSummaryHeader => 'Summary'; - - @override - String get registrationCompletedKeychainTitle => 'Catalyst Keychain created'; - - @override - String get registrationCompletedKeychainInfo => 'You created a Catalyst Keychain, backed up its seed phrase and set an unlock password.'; - - @override - String registrationCompletedWalletTitle(String walletName) { - return 'Cardano $walletName wallet selected'; - } - - @override - String registrationCompletedWalletInfo(String walletName) { - return 'You selected your $walletName wallet as primary wallet for your voting power.'; - } - - @override - String get registrationCompletedRolesTitle => 'Catalyst roles selected'; - - @override - String get registrationCompletedRolesInfo => 'You linked your Cardano wallet and selected Catalyst roles via a signed transaction.'; - - @override - String get registrationCompletedRoleRegistration => 'role registration'; - - @override - String get registrationCompletedDiscoveryButton => 'Open Discovery Dashboard'; - - @override - String get registrationCompletedAccountButton => 'Review my account'; - - @override - String get createKeychainSeedPhraseSubtitle => 'Write down your 12 Catalyst 
security words'; - - @override - String get createKeychainSeedPhraseBody => 'Make sure you create an offline backup of your recovery phrase as well.'; - - @override - String get createKeychainSeedPhraseDownload => 'Download Catalyst key'; - - @override - String get createKeychainSeedPhraseStoreConfirmation => 'I have written down/downloaded my 12 words'; - - @override - String get createKeychainSeedPhraseCheckInstructionsTitle => 'Check your Catalyst security keys'; - - @override - String get createKeychainSeedPhraseCheckInstructionsSubtitle => 'Next, we\'re going to make sure that you\'ve written down your words correctly. 

We don\'t save your seed phrase, so it\'s important 
to make sure you have it right. That\'s why we do this confirmation before continuing. 

It\'s also good practice to get familiar with using a seed phrase if you\'re new to crypto.'; - - @override - String get createKeychainSeedPhraseCheckSubtitle => 'Input your Catalyst security keys'; - - @override - String get createKeychainSeedPhraseCheckBody => 'Select your 12 written down words in 
the correct order.'; - - @override - String get uploadCatalystKey => 'Upload Catalyst Key'; - - @override - String get reset => 'Reset'; - - @override - String get createKeychainSeedPhraseCheckSuccessTitle => 'Nice job! You\'ve successfully verified the seed phrase for your keychain.'; - - @override - String get createKeychainSeedPhraseCheckSuccessSubtitle => 'Enter your seed phrase to recover your Catalyst Keychain on any device.

It\'s kinda like your email and password all rolled into one, so keep it somewhere safe!

In the next step we\'ll add a password to your Catalyst Keychain, so you can lock/unlock access to Voices.'; - - @override - String get yourNextStep => 'Your next step'; - - @override - String get createKeychainSeedPhraseCheckSuccessNextStep => 'Now let’s set your Unlock password for this device!'; - - @override - String get createKeychainUnlockPasswordInstructionsTitle => 'Set your Catalyst unlock password 
for this device'; - - @override - String get createKeychainUnlockPasswordInstructionsSubtitle => 'With over 300 trillion possible combinations, your 12 word seed phrase is great for keeping your account safe. 

But it can be a bit tedious to enter every single time you want to use the app. 

In this next step, you\'ll set your Unlock Password for your current device. It\'s like a shortcut for proving ownership of your Keychain. 

Whenever you recover your account for the first time on a new device, you\'ll need to use your Catalyst Keychain to get started. Every time after that, you can use your Unlock Password to quickly regain access.'; - - @override - String get createKeychainCreatedTitle => 'Congratulations your Catalyst 
Keychain is created!'; - - @override - String get createKeychainCreatedNextStep => 'In the next step you write your Catalyst roles and 
account to the Cardano Mainnet.'; - - @override - String get createKeychainLinkWalletAndRoles => 'Link your Cardano Wallet & Roles'; - - @override - String get registrationCreateKeychainStepGroup => 'Catalyst Keychain created'; - - @override - String get registrationLinkWalletStepGroup => 'Link Cardano Wallet & Roles'; - - @override - String get registrationCompletedStepGroup => 'Catalyst account creation completed!'; - - @override - String get createKeychainUnlockPasswordIntoSubtitle => 'Catalyst unlock password'; - - @override - String get createKeychainUnlockPasswordIntoBody => 'Please provide a password for your Catalyst Keychain.'; - - @override - String get enterPassword => 'Enter password'; - - @override - String get confirmPassword => 'Confirm password'; - - @override - String xCharactersMinimum(int number) { - return '$number characters minimum length'; - } - - @override - String get passwordDoNotMatch => 'Passwords do not match, please correct'; - - @override - String get warning => 'Warning'; - - @override - String get alert => 'Alert'; - - @override - String get registrationExitConfirmDialogSubtitle => 'Account creation incomplete!'; - - @override - String get registrationExitConfirmDialogContent => 'If attempt to leave without creating your keychain - account creation will be incomplete. 

You are not able to login without 
completing your keychain.'; - - @override - String get registrationExitConfirmDialogContinue => 'Continue keychain creation'; - - @override - String get cancelAnyways => 'Cancel anyway'; - - @override - String get recoverCatalystKeychain => 'Restore Catalyst keychain'; - - @override - String get recoverKeychainMethodsTitle => 'Restore your Catalyst Keychain'; - - @override - String get recoverKeychainMethodsNoKeychainFound => 'No Catalyst Keychain found on this device.'; - - @override - String get recoverKeychainMethodsSubtitle => 'Not to worry, in the next step you can choose the recovery option that applies to you for this device!'; - - @override - String get recoverKeychainMethodsListTitle => 'How do you want Restore your Catalyst Keychain?'; - - @override - String get recoverKeychainNonFound => 'No Catalyst Keychain found
on this device.'; - - @override - String get recoverKeychainFound => 'Keychain found! 
Please unlock your device.'; - - @override - String get seedPhrase12Words => '12 security words'; - - @override - String get recoverySeedPhraseInstructionsTitle => 'Restore your Catalyst Keychain with 
your 12 security words.'; - - @override - String get recoverySeedPhraseInstructionsSubtitle => 'Enter your security words in the correct order, and sign into your Catalyst account on a new device.'; - - @override - String get recoverySeedPhraseInputTitle => 'Restore your Catalyst Keychain with 
your 12 security words'; - - @override - String get recoverySeedPhraseInputSubtitle => 'Enter each word of your Catalyst Key in the right order 
to bring your Catalyst account to this device.'; - - @override - String get recoveryAccountTitle => 'Catalyst account recovery'; - - @override - String get recoveryAccountSuccessTitle => 'Keychain recovered successfully!'; - - @override - String get recoveryAccountDetailsAction => 'Set unlock password for this device'; - - @override - String get recoveryUnlockPasswordInstructionsTitle => 'Set your Catalyst unlock password f
or this device'; - - @override - String get recoveryUnlockPasswordInstructionsSubtitle => 'With over 300 trillion possible combinations, your 12 word seed phrase is great for keeping your account safe. 

But it can be a bit tedious to enter every single time you want to use the app. 

In this next step, you\'ll set your Unlock Password for your current device. It\'s like a shortcut for proving ownership of your Keychain. 

Whenever you recover your account for the first time on a new device, you\'ll need to use your Catalyst Keychain to get started. Every time after that, you can use your Unlock Password to quickly regain access.'; - - @override - String get recoverDifferentKeychain => 'Restore a different keychain'; - - @override - String get unlockDialogHeader => 'Unlock Catalyst'; - - @override - String get unlockDialogTitle => 'Welcome back!'; - - @override - String get unlockDialogContent => 'Please enter your device specific unlock password\nto unlock Catalyst Voices.'; - - @override - String get unlockDialogHint => 'Enter your Unlock password'; - - @override - String get unlockDialogIncorrectPassword => 'Password is incorrect, try again.'; - - @override - String get continueAsGuest => 'Continue as guest'; - - @override - String get unlockSnackbarTitle => 'Catalyst unlocked!'; - - @override - String get unlockSnackbarMessage => 'You can now fully use the application.'; - - @override - String get lockSnackbarTitle => 'Catalyst locked'; - - @override - String get lockSnackbarMessage => 'Catalyst is now in guest/locked mode.'; - - @override - String get recoverySuccessTitle => 'Congratulations your Catalyst 
Keychain is restored!'; - - @override - String get recoverySuccessSubtitle => 'You have successfully restored your Catalyst Keychain, and unlocked Catalyst Voices on this device.'; - - @override - String get recoverySuccessGoToDashboard => 'Jump into the Discovery space / Dashboard'; - - @override - String get recoverySuccessGoAccount => 'Check my account'; - - @override - String get recoveryExitConfirmDialogSubtitle => '12 word keychain restoration incomplete'; - - @override - String get recoveryExitConfirmDialogContent => 'Please continue your Catalyst Keychain restoration, if you cancel all input will be lost.'; - - @override - String get recoveryExitConfirmDialogContinue => 'Continue recovery process'; - - @override - String get recoverAccount => 'Recover account'; - - @override - String get uploadConfirmDialogSubtitle => 'SWITCH TO FILE UPLOAD'; - - @override - String get uploadConfirmDialogContent => 'Do you want to cancel manual input, and switch to Catalyst key upload?'; - - @override - String get uploadConfirmDialogYesButton => 'Yes, switch to Catalyst key upload'; - - @override - String get uploadConfirmDialogResumeButton => 'Resume manual inputs'; - - @override - String get incorrectUploadDialogSubtitle => 'CATALYST KEY INCORRECT'; - - @override - String get incorrectUploadDialogContent => 'The Catalyst keychain that you entered or uploaded is incorrect, please try again.'; - - @override - String get incorrectUploadDialogTryAgainButton => 'Try again'; - - @override - String get finishAccountCreation => 'Finish account creation'; - - @override - String get connectDifferentWallet => 'Connect a different wallet'; - - @override - String get reviewRegistrationTransaction => 'Review registration transaction'; - - @override - String get format => 'Format'; - - @override - String get datePickerDateRangeError => 'Please select a date within the range of today and one year from today.'; - - @override - String get datePickerDaysInMonthError => 'Entered day exceeds the maximum days for this month.'; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart deleted file mode 100644 index 5c6d89f3429..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart +++ /dev/null @@ -1,1012 +0,0 @@ -import 'package:intl/intl.dart' as intl; - -import 'catalyst_voices_localizations.dart'; - -// ignore_for_file: type=lint - -/// The translations for Spanish Castilian (`es`). -class VoicesLocalizationsEs extends VoicesLocalizations { - VoicesLocalizationsEs([String locale = 'es']) : super(locale); - - @override - String get emailLabelText => 'Email'; - - @override - String get emailHintText => 'mail@example.com'; - - @override - String get emailErrorText => 'mail@example.com'; - - @override - String get cancelButtonText => 'Cancel'; - - @override - String get editButtonText => 'Edit'; - - @override - String get headerTooltipText => 'Header'; - - @override - String get placeholderRichText => 'Start writing your text...'; - - @override - String get supportingTextLabelText => 'Supporting text'; - - @override - String get saveButtonText => 'Save'; - - @override - String get passwordLabelText => 'Contraseña'; - - @override - String get passwordHintText => 'Mi1ContraseñaSecreta'; - - @override - String get passwordErrorText => 'La contraseña debe tener al menos 8 caracteres'; - - @override - String get loginTitleText => 'Acceso'; - - @override - String get loginButtonText => 'Acceso'; - - @override - String get loginScreenErrorMessage => 'Credenciales incorrectas'; - - @override - String get homeScreenText => 'Catalyst Voices'; - - @override - String get comingSoonSubtitle => 'Voices'; - - @override - String get comingSoonTitle1 => 'Coming'; - - @override - String get comingSoonTitle2 => 'soon'; - - @override - String get comingSoonDescription => 'Project Catalyst is the world\'s largest decentralized innovation engine for solving real-world challenges.'; - - @override - String get connectingStatusLabelText => 're-connecting'; - - @override - String get finishAccountButtonLabelText => 'Finish account'; - - @override - String get getStartedButtonLabelText => 'Get Started'; - - @override - String get unlockButtonLabelText => 'Unlock'; - - @override - String get userProfileGuestLabelText => 'Guest'; - - @override - String get searchButtonLabelText => '[cmd=K]'; - - @override - String get snackbarInfoLabelText => 'Info'; - - @override - String get snackbarInfoMessageText => 'This is an info message!'; - - @override - String get snackbarSuccessLabelText => 'Success'; - - @override - String get snackbarSuccessMessageText => 'This is a success message!'; - - @override - String get snackbarWarningLabelText => 'Warning'; - - @override - String get snackbarWarningMessageText => 'This is a warning message!'; - - @override - String get snackbarErrorLabelText => 'Error'; - - @override - String get snackbarErrorMessageText => 'This is an error message!'; - - @override - String get snackbarRefreshButtonText => 'Refresh'; - - @override - String get snackbarMoreButtonText => 'Learn more'; - - @override - String get snackbarOkButtonText => 'Ok'; - - @override - String seedPhraseSlotNr(int nr) { - return 'Slot $nr'; - } - - @override - String get proposalStatusReady => 'Ready'; - - @override - String get proposalStatusDraft => 'Draft'; - - @override - String get proposalStatusInProgress => 'In progress'; - - @override - String get proposalStatusPrivate => 'Private'; - - @override - String get proposalStatusLive => 'LIVE'; - - @override - String get proposalStatusCompleted => 'Completed'; - - @override - String get proposalStatusOpen => 'Open'; - - @override - String get fundedProposal => 'Funded proposal'; - - @override - String get publishedProposal => 'Published proposal'; - - @override - String fundedProposalDate(DateTime date) { - final intl.DateFormat dateDateFormat = intl.DateFormat.yMMMMd(localeName); - final String dateString = dateDateFormat.format(date); - - return 'Funded $dateString'; - } - - @override - String lastUpdateDate(String date) { - return 'Last update: $date.'; - } - - @override - String get fundsRequested => 'Funds requested'; - - @override - String noOfComments(num count) { - final intl.NumberFormat countNumberFormat = intl.NumberFormat.compact( - locale: localeName, - - ); - final String countString = countNumberFormat.format(count); - - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'comments', - one: 'comment', - zero: 'comments', - ); - return '$countString $_temp0'; - } - - @override - String noOfSegmentsCompleted(num completed, num total, num percentage) { - final intl.NumberFormat completedNumberFormat = intl.NumberFormat.compact( - locale: localeName, - - ); - final String completedString = completedNumberFormat.format(completed); - final intl.NumberFormat totalNumberFormat = intl.NumberFormat.compact( - locale: localeName, - - ); - final String totalString = totalNumberFormat.format(total); - final intl.NumberFormat percentageNumberFormat = intl.NumberFormat.compact( - locale: localeName, - - ); - final String percentageString = percentageNumberFormat.format(percentage); - - String _temp0 = intl.Intl.pluralLogic( - total, - locale: localeName, - other: 'segments', - one: 'segment', - zero: 'segments', - ); - return '$completedString of $totalString ($percentageString%) $_temp0 completed'; - } - - @override - String get today => 'Today'; - - @override - String get yesterday => 'Yesterday'; - - @override - String get twoDaysAgo => '2 days ago'; - - @override - String get tomorrow => 'Tomorrow'; - - @override - String get activeVotingRound => 'Active voting round 14'; - - @override - String noOfAllProposals(int count) { - return 'All proposals ($count)'; - } - - @override - String get favorites => 'Favorites'; - - @override - String get treasuryCampaignBuilder => 'Campaign builder'; - - @override - String get treasuryCampaignBuilderSegments => 'Segments'; - - @override - String get treasuryCampaignSetup => 'Setup Campaign'; - - @override - String get treasuryCampaignTitle => 'Campaign title'; - - @override - String get stepEdit => 'Edit'; - - @override - String get workspaceProposalNavigation => 'Proposal navigation'; - - @override - String get workspaceProposalNavigationSegments => 'Segments'; - - @override - String get workspaceProposalSetup => 'Proposal setup'; - - @override - String get drawerSpaceTreasury => 'Treasury'; - - @override - String get drawerSpaceDiscovery => 'Discovery'; - - @override - String get drawerSpaceWorkspace => 'Workspace'; - - @override - String get drawerSpaceVoting => 'Voting'; - - @override - String get drawerSpaceFundedProjects => 'Funded projects'; - - @override - String get fundedProjectSpace => 'Funded project space'; - - @override - String noOfFundedProposals(int count) { - return 'Funded proposals ($count)'; - } - - @override - String get followed => 'Followed'; - - @override - String get overallSpacesSearchBrands => 'Search Brands'; - - @override - String get overallSpacesTasks => 'Tasks'; - - @override - String get voicesUpdateReady => 'Voices update ready'; - - @override - String get clickToRestart => 'Click to restart'; - - @override - String get spaceTreasuryName => 'Treasury space'; - - @override - String get spaceDiscoveryName => 'Discovery space'; - - @override - String get spaceWorkspaceName => 'Workspace'; - - @override - String get spaceVotingName => 'Voting space'; - - @override - String get spaceFundedProjects => 'Funded project space'; - - @override - String get lock => 'Lock'; - - @override - String get unlock => 'Unlock'; - - @override - String get getStarted => 'Get Started'; - - @override - String get guest => 'Guest'; - - @override - String get visitor => 'Visitor'; - - @override - String get noConnectionBannerRefreshButtonText => 'Refresh'; - - @override - String get noConnectionBannerTitle => 'No internet connection'; - - @override - String get noConnectionBannerDescription => 'Your internet is playing hide and seek. Check your internet connection, or try again in a moment.'; - - @override - String get weakPasswordStrength => 'Weak password strength'; - - @override - String get normalPasswordStrength => 'Normal password strength'; - - @override - String get goodPasswordStrength => 'Good password strength'; - - @override - String get chooseCardanoWallet => 'Choose Cardano Wallet'; - - @override - String get chooseOtherWallet => 'Choose other wallet'; - - @override - String get learnMore => 'Learn More'; - - @override - String get walletLinkHeader => 'Link keys to your Catalyst Keychain'; - - @override - String get walletLinkWalletSubheader => 'Link your Cardano wallet'; - - @override - String get walletLinkRolesSubheader => 'Select your Catalyst roles'; - - @override - String get walletLinkTransactionSubheader => 'Sign your Catalyst roles to the\nCardano mainnet'; - - @override - String get walletLinkIntroTitle => 'Link Cardano Wallet & Catalyst Roles to you Catalyst Keychain.'; - - @override - String get walletLinkIntroContent => 'You\'re almost there! This is the final and most important step in your account setup.\n\nWe\'re going to link a Cardano Wallet to your Catalyst Keychain, so you can start collecting Role Keys.\n\nRole Keys allow you to enter new spaces, discover new ways to participate, and unlock new ways to earn rewards.\n\nWe\'ll start with your Voter Key by default. You can decide to add a Proposer Key and Drep key if you want, or you can always add them later.'; - - @override - String get walletLinkSelectWalletTitle => 'Select the Cardano wallet to link\nto your Catalyst Keychain.'; - - @override - String get walletLinkSelectWalletContent => 'To complete this action, you\'ll submit a signed transaction to Cardano. There will be an ADA transaction fee.'; - - @override - String get walletLinkWalletDetailsTitle => 'Cardano wallet detection'; - - @override - String walletLinkWalletDetailsContent(String wallet) { - return '$wallet connected successfully!'; - } - - @override - String get walletLinkWalletDetailsNotice => 'Wallet and role registrations require a minimal transaction fee. You can setup your default dApp connector wallet in your browser extension settings.'; - - @override - String get walletLinkWalletDetailsNoticeTopUp => 'Top up ADA'; - - @override - String get walletLinkWalletDetailsNoticeTopUpLink => 'Link to top-up provider'; - - @override - String get walletLinkTransactionTitle => 'Let\'s make sure everything looks right.'; - - @override - String get walletLinkTransactionAccountCompletion => 'Account completion for Catalyst'; - - @override - String walletLinkTransactionLinkItem(String wallet) { - return '1 Link $wallet to Catalyst Keychain'; - } - - @override - String get walletLinkTransactionPositiveSmallPrint => 'Positive small print'; - - @override - String get walletLinkTransactionPositiveSmallPrintItem1 => 'Your registration is a one time event, cost will not renew periodically.'; - - @override - String get walletLinkTransactionPositiveSmallPrintItem2 => 'Your registrations can be found under your account profile after completion.'; - - @override - String get walletLinkTransactionPositiveSmallPrintItem3 => 'All registration fees go into the Cardano Treasury.'; - - @override - String get walletLinkTransactionSign => 'Sign transaction with wallet'; - - @override - String get walletLinkTransactionChangeRoles => 'Change role setup'; - - @override - String walletLinkTransactionRoleItem(String role) { - return '1 $role registration to Catalyst Keychain'; - } - - @override - String get registrationTransactionFailed => 'Transaction failed'; - - @override - String get registrationInsufficientBalance => 'Insufficient balance, please top up your wallet.'; - - @override - String get registrationSeedPhraseNotFound => 'Seed phrase was not found. Make sure correct words are correct.'; - - @override - String get registrationUnlockPasswordNotFound => 'Password was not found. Make sure valid password was created.'; - - @override - String get registrationWalletNotFound => 'Wallet not found'; - - @override - String get walletLinkRoleChooserTitle => 'How do you want to participate in Catalyst?'; - - @override - String get walletLinkRoleChooserContent => 'In Catalyst you can take on different roles, learn more below and choose your additional roles now.'; - - @override - String get walletLinkRoleSummaryTitle => 'Is this your correct Catalyst role setup?'; - - @override - String get walletLinkRoleSummaryContent1 => 'You would like to register '; - - @override - String walletLinkRoleSummaryContent2(num count) { - final intl.NumberFormat countNumberFormat = intl.NumberFormat.compact( - locale: localeName, - - ); - final String countString = countNumberFormat.format(count); - - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'roles', - one: 'role', - zero: 'roles', - ); - return '$countString active $_temp0'; - } - - @override - String get walletLinkRoleSummaryContent3 => ' in Catalyst.'; - - @override - String get seeAllSupportedWallets => 'See all supported wallets'; - - @override - String get walletDetectionSummary => 'Wallet detection summary'; - - @override - String get walletBalance => 'Wallet balance'; - - @override - String get walletAddress => 'Wallet address'; - - @override - String get accountCreationCreate => 'Create a new 
Catalyst Keychain'; - - @override - String get accountCreationRecover => 'Recover your
Catalyst Keychain'; - - @override - String get accountCreationOnThisDevice => 'On this device'; - - @override - String get accountCreationGetStartedTitle => 'Welcome to Catalyst'; - - @override - String get accountCreationGetStatedDesc => 'If you already have a Catalyst keychain you can restore it on this device, or you can create a new Catalyst Keychain.'; - - @override - String get accountCreationGetStatedWhatNext => 'What do you want to do?'; - - @override - String get myAccountProfileKeychain => 'My Account / Profile & Keychain'; - - @override - String get yourCatalystKeychainAndRoleRegistration => 'Your Catalyst keychain & role registration'; - - @override - String get profileAndKeychain => 'Profile & Keychain'; - - @override - String get removeKeychain => 'Remove Keychain'; - - @override - String get walletConnected => 'Wallet connected'; - - @override - String get currentRoleRegistrations => 'Current Role registrations'; - - @override - String get voter => 'Voter'; - - @override - String get proposer => 'Proposer'; - - @override - String get drep => 'Drep'; - - @override - String get defaultRole => 'Default'; - - @override - String get catalystKeychain => 'Catalyst Keychain'; - - @override - String get accountCreationSplashTitle => 'Create your Catalyst Keychain'; - - @override - String get accountCreationSplashMessage => 'Your keychain is your ticket to participate in 
distributed innovation on the global stage. 

Once you have it, you\'ll be able to enter different spaces, discover awesome ideas, and share your feedback to hep improve ideas. 

As you add new keys to your keychain, you\'ll be able to enter new spaces, unlock new rewards opportunities, and have your voice heard in community decisions.'; - - @override - String get accountCreationSplashNextButton => 'Create your Keychain now'; - - @override - String get accountInstructionsTitle => 'Great! Your Catalyst Keychain 
has been created.'; - - @override - String get accountInstructionsMessage => 'On the next screen, you\'re going to see 12 words. 
This is called your \"seed phrase\". 

It\'s like a super secure password that only you know, 
that allows you to prove ownership of your keychain. 

You\'ll use it to login and recover your account on 
different devices, so be sure to put it somewhere safe!\n\nYou need to write this seed phrase down with pen and paper, so get this ready.'; - - @override - String get next => 'Next'; - - @override - String get back => 'Back'; - - @override - String get retry => 'Retry'; - - @override - String get somethingWentWrong => 'Something went wrong.'; - - @override - String get noWalletFound => 'No wallet found.'; - - @override - String get deleteKeychainDialogTitle => 'Delete Keychain?'; - - @override - String get deleteKeychainDialogSubtitle => 'Are you sure you wants to delete your\nCatalyst Keychain from this device?'; - - @override - String get deleteKeychainDialogWarning => 'Make sure you have a working Catalyst 12-word seedphrase!'; - - @override - String get deleteKeychainDialogWarningInfo => 'Your Catalyst account will be removed,\nthis action cannot be undone!'; - - @override - String get deleteKeychainDialogTypingInfo => 'To avoid mistakes, please type ‘Remove Keychain’ below.'; - - @override - String get deleteKeychainDialogInputLabel => 'Confirm removal'; - - @override - String get deleteKeychainDialogErrorText => 'Error. Please type \'Remove Keychain\' to remove your account from this device.'; - - @override - String get deleteKeychainDialogRemovingPhrase => 'Remove Keychain'; - - @override - String get accountRoleDialogTitle => 'Learn about Catalyst Roles'; - - @override - String get accountRoleDialogButton => 'Continue Role setup'; - - @override - String accountRoleDialogRoleSummaryTitle(String role) { - return '$role role summary'; - } - - @override - String get voterVerboseName => 'Treasury guardian'; - - @override - String get proposerVerboseName => 'Main proposer'; - - @override - String get drepVerboseName => 'Community expert'; - - @override - String get voterDescription => 'The Voters are the guardians of Cardano treasury. They vote in projects for the growth of the Cardano Ecosystem.'; - - @override - String get proposerDescription => 'The Main Proposers are the Innovators in Project Catalyst, they are the shapers of the future.'; - - @override - String get drepDescription => 'The dRep has an Expert Role in the Cardano/Catalyst as people can delegate their vote to Cardano Experts.'; - - @override - String get voterSummarySelectFavorites => 'Select favorites'; - - @override - String get voterSummaryComment => 'Comment/Vote on Proposals'; - - @override - String get voterSummaryCastVotes => 'Cast your votes'; - - @override - String get voterSummaryVoterRewards => 'Voter rewards'; - - @override - String get proposerSummaryWriteEdit => 'Write/edit functionality'; - - @override - String get proposerSummarySubmitToFund => 'Rights to Submit to Fund'; - - @override - String get proposerSummaryInviteTeamMembers => 'Invite Team Members'; - - @override - String get proposerSummaryComment => 'Comment functionality'; - - @override - String get drepSummaryDelegatedVotes => 'Delegated Votes'; - - @override - String get drepSummaryRewards => 'dRep rewards'; - - @override - String get drepSummaryCastVotes => 'Cast delegated votes'; - - @override - String get drepSummaryComment => 'Comment Functionality'; - - @override - String get delete => 'Delete'; - - @override - String get close => 'Close'; - - @override - String get notice => 'Notice'; - - @override - String get yes => 'Yes'; - - @override - String get no => 'No'; - - @override - String get total => 'Total'; - - @override - String get file => 'file'; - - @override - String get key => 'key'; - - @override - String get upload => 'Upload'; - - @override - String get browse => 'browse'; - - @override - String uploadDropInfo(String itemNameToUpload) { - return 'Drop your $itemNameToUpload here or '; - } - - @override - String get uploadProgressInfo => 'Upload in progress'; - - @override - String get uploadKeychainTitle => 'Upload Catalyst Keychain'; - - @override - String get uploadKeychainInfo => 'Make sure it\'s a correct Catalyst keychain file.'; - - @override - String get themeLight => 'Light'; - - @override - String get themeDark => 'Dark'; - - @override - String get keychainDeletedDialogTitle => 'Catalyst keychain removed'; - - @override - String get keychainDeletedDialogSubtitle => 'Your Catalyst Keychain is removed successfully from this device.'; - - @override - String get keychainDeletedDialogInfo => 'We reverted this device to Catalyst first use.'; - - @override - String get registrationCompletedTitle => 'Catalyst account setup'; - - @override - String get registrationCompletedSubtitle => 'Completed!'; - - @override - String get registrationCompletedSummaryHeader => 'Summary'; - - @override - String get registrationCompletedKeychainTitle => 'Catalyst Keychain created'; - - @override - String get registrationCompletedKeychainInfo => 'You created a Catalyst Keychain, backed up its seed phrase and set an unlock password.'; - - @override - String registrationCompletedWalletTitle(String walletName) { - return 'Cardano $walletName wallet selected'; - } - - @override - String registrationCompletedWalletInfo(String walletName) { - return 'You selected your $walletName wallet as primary wallet for your voting power.'; - } - - @override - String get registrationCompletedRolesTitle => 'Catalyst roles selected'; - - @override - String get registrationCompletedRolesInfo => 'You linked your Cardano wallet and selected Catalyst roles via a signed transaction.'; - - @override - String get registrationCompletedRoleRegistration => 'role registration'; - - @override - String get registrationCompletedDiscoveryButton => 'Open Discovery Dashboard'; - - @override - String get registrationCompletedAccountButton => 'Review my account'; - - @override - String get createKeychainSeedPhraseSubtitle => 'Write down your 12 Catalyst 
security words'; - - @override - String get createKeychainSeedPhraseBody => 'Make sure you create an offline backup of your recovery phrase as well.'; - - @override - String get createKeychainSeedPhraseDownload => 'Download Catalyst key'; - - @override - String get createKeychainSeedPhraseStoreConfirmation => 'I have written down/downloaded my 12 words'; - - @override - String get createKeychainSeedPhraseCheckInstructionsTitle => 'Check your Catalyst security keys'; - - @override - String get createKeychainSeedPhraseCheckInstructionsSubtitle => 'Next, we\'re going to make sure that you\'ve written down your words correctly. 

We don\'t save your seed phrase, so it\'s important 
to make sure you have it right. That\'s why we do this confirmation before continuing. 

It\'s also good practice to get familiar with using a seed phrase if you\'re new to crypto.'; - - @override - String get createKeychainSeedPhraseCheckSubtitle => 'Input your Catalyst security keys'; - - @override - String get createKeychainSeedPhraseCheckBody => 'Select your 12 written down words in 
the correct order.'; - - @override - String get uploadCatalystKey => 'Upload Catalyst Key'; - - @override - String get reset => 'Reset'; - - @override - String get createKeychainSeedPhraseCheckSuccessTitle => 'Nice job! You\'ve successfully verified the seed phrase for your keychain.'; - - @override - String get createKeychainSeedPhraseCheckSuccessSubtitle => 'Enter your seed phrase to recover your Catalyst Keychain on any device.

It\'s kinda like your email and password all rolled into one, so keep it somewhere safe!

In the next step we\'ll add a password to your Catalyst Keychain, so you can lock/unlock access to Voices.'; - - @override - String get yourNextStep => 'Your next step'; - - @override - String get createKeychainSeedPhraseCheckSuccessNextStep => 'Now let’s set your Unlock password for this device!'; - - @override - String get createKeychainUnlockPasswordInstructionsTitle => 'Set your Catalyst unlock password 
for this device'; - - @override - String get createKeychainUnlockPasswordInstructionsSubtitle => 'With over 300 trillion possible combinations, your 12 word seed phrase is great for keeping your account safe. 

But it can be a bit tedious to enter every single time you want to use the app. 

In this next step, you\'ll set your Unlock Password for your current device. It\'s like a shortcut for proving ownership of your Keychain. 

Whenever you recover your account for the first time on a new device, you\'ll need to use your Catalyst Keychain to get started. Every time after that, you can use your Unlock Password to quickly regain access.'; - - @override - String get createKeychainCreatedTitle => 'Congratulations your Catalyst 
Keychain is created!'; - - @override - String get createKeychainCreatedNextStep => 'In the next step you write your Catalyst roles and 
account to the Cardano Mainnet.'; - - @override - String get createKeychainLinkWalletAndRoles => 'Link your Cardano Wallet & Roles'; - - @override - String get registrationCreateKeychainStepGroup => 'Catalyst Keychain created'; - - @override - String get registrationLinkWalletStepGroup => 'Link Cardano Wallet & Roles'; - - @override - String get registrationCompletedStepGroup => 'Catalyst account creation completed!'; - - @override - String get createKeychainUnlockPasswordIntoSubtitle => 'Catalyst unlock password'; - - @override - String get createKeychainUnlockPasswordIntoBody => 'Please provide a password for your Catalyst Keychain.'; - - @override - String get enterPassword => 'Enter password'; - - @override - String get confirmPassword => 'Confirm password'; - - @override - String xCharactersMinimum(int number) { - return '$number characters minimum length'; - } - - @override - String get passwordDoNotMatch => 'Passwords do not match, please correct'; - - @override - String get warning => 'Warning'; - - @override - String get alert => 'Alert'; - - @override - String get registrationExitConfirmDialogSubtitle => 'Account creation incomplete!'; - - @override - String get registrationExitConfirmDialogContent => 'If attempt to leave without creating your keychain - account creation will be incomplete. 

You are not able to login without 
completing your keychain.'; - - @override - String get registrationExitConfirmDialogContinue => 'Continue keychain creation'; - - @override - String get cancelAnyways => 'Cancel anyway'; - - @override - String get recoverCatalystKeychain => 'Restore Catalyst keychain'; - - @override - String get recoverKeychainMethodsTitle => 'Restore your Catalyst Keychain'; - - @override - String get recoverKeychainMethodsNoKeychainFound => 'No Catalyst Keychain found on this device.'; - - @override - String get recoverKeychainMethodsSubtitle => 'Not to worry, in the next step you can choose the recovery option that applies to you for this device!'; - - @override - String get recoverKeychainMethodsListTitle => 'How do you want Restore your Catalyst Keychain?'; - - @override - String get recoverKeychainNonFound => 'No Catalyst Keychain found
on this device.'; - - @override - String get recoverKeychainFound => 'Keychain found! 
Please unlock your device.'; - - @override - String get seedPhrase12Words => '12 security words'; - - @override - String get recoverySeedPhraseInstructionsTitle => 'Restore your Catalyst Keychain with 
your 12 security words.'; - - @override - String get recoverySeedPhraseInstructionsSubtitle => 'Enter your security words in the correct order, and sign into your Catalyst account on a new device.'; - - @override - String get recoverySeedPhraseInputTitle => 'Restore your Catalyst Keychain with 
your 12 security words'; - - @override - String get recoverySeedPhraseInputSubtitle => 'Enter each word of your Catalyst Key in the right order 
to bring your Catalyst account to this device.'; - - @override - String get recoveryAccountTitle => 'Catalyst account recovery'; - - @override - String get recoveryAccountSuccessTitle => 'Keychain recovered successfully!'; - - @override - String get recoveryAccountDetailsAction => 'Set unlock password for this device'; - - @override - String get recoveryUnlockPasswordInstructionsTitle => 'Set your Catalyst unlock password f
or this device'; - - @override - String get recoveryUnlockPasswordInstructionsSubtitle => 'With over 300 trillion possible combinations, your 12 word seed phrase is great for keeping your account safe. 

But it can be a bit tedious to enter every single time you want to use the app. 

In this next step, you\'ll set your Unlock Password for your current device. It\'s like a shortcut for proving ownership of your Keychain. 

Whenever you recover your account for the first time on a new device, you\'ll need to use your Catalyst Keychain to get started. Every time after that, you can use your Unlock Password to quickly regain access.'; - - @override - String get recoverDifferentKeychain => 'Restore a different keychain'; - - @override - String get unlockDialogHeader => 'Unlock Catalyst'; - - @override - String get unlockDialogTitle => 'Welcome back!'; - - @override - String get unlockDialogContent => 'Please enter your device specific unlock password\nto unlock Catalyst Voices.'; - - @override - String get unlockDialogHint => 'Enter your Unlock password'; - - @override - String get unlockDialogIncorrectPassword => 'Password is incorrect, try again.'; - - @override - String get continueAsGuest => 'Continue as guest'; - - @override - String get unlockSnackbarTitle => 'Catalyst unlocked!'; - - @override - String get unlockSnackbarMessage => 'You can now fully use the application.'; - - @override - String get lockSnackbarTitle => 'Catalyst locked'; - - @override - String get lockSnackbarMessage => 'Catalyst is now in guest/locked mode.'; - - @override - String get recoverySuccessTitle => 'Congratulations your Catalyst 
Keychain is restored!'; - - @override - String get recoverySuccessSubtitle => 'You have successfully restored your Catalyst Keychain, and unlocked Catalyst Voices on this device.'; - - @override - String get recoverySuccessGoToDashboard => 'Jump into the Discovery space / Dashboard'; - - @override - String get recoverySuccessGoAccount => 'Check my account'; - - @override - String get recoveryExitConfirmDialogSubtitle => '12 word keychain restoration incomplete'; - - @override - String get recoveryExitConfirmDialogContent => 'Please continue your Catalyst Keychain restoration, if you cancel all input will be lost.'; - - @override - String get recoveryExitConfirmDialogContinue => 'Continue recovery process'; - - @override - String get recoverAccount => 'Recover account'; - - @override - String get uploadConfirmDialogSubtitle => 'SWITCH TO FILE UPLOAD'; - - @override - String get uploadConfirmDialogContent => 'Do you want to cancel manual input, and switch to Catalyst key upload?'; - - @override - String get uploadConfirmDialogYesButton => 'Yes, switch to Catalyst key upload'; - - @override - String get uploadConfirmDialogResumeButton => 'Resume manual inputs'; - - @override - String get incorrectUploadDialogSubtitle => 'CATALYST KEY INCORRECT'; - - @override - String get incorrectUploadDialogContent => 'The Catalyst keychain that you entered or uploaded is incorrect, please try again.'; - - @override - String get incorrectUploadDialogTryAgainButton => 'Try again'; - - @override - String get finishAccountCreation => 'Finish account creation'; - - @override - String get connectDifferentWallet => 'Connect a different wallet'; - - @override - String get reviewRegistrationTransaction => 'Review registration transaction'; - - @override - String get format => 'Format'; - - @override - String get datePickerDateRangeError => 'Please select a date within the range of today and one year from today.'; - - @override - String get datePickerDaysInMonthError => 'Entered day exceeds the maximum days for this month.'; -} diff --git a/catalyst_voices/utilities/uikit_example/lib/generated/assets.gen.dart b/catalyst_voices/utilities/uikit_example/lib/generated/assets.gen.dart deleted file mode 100644 index fea7cf30ebd..00000000000 --- a/catalyst_voices/utilities/uikit_example/lib/generated/assets.gen.dart +++ /dev/null @@ -1,108 +0,0 @@ -/// GENERATED CODE - DO NOT MODIFY BY HAND -/// ***************************************************** -/// FlutterGen -/// ***************************************************** - -// coverage:ignore-file -// ignore_for_file: type=lint -// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use - -import 'package:flutter/widgets.dart'; - -class $AssetsImagesGen { - const $AssetsImagesGen(); - - /// File path: assets/images/robot_avatar.png - AssetGenImage get robotAvatar => - const AssetGenImage('assets/images/robot_avatar.png'); - - /// List of all assets - List get values => [robotAvatar]; -} - -class UiKitAssets { - UiKitAssets._(); - - static const $AssetsImagesGen images = $AssetsImagesGen(); -} - -class AssetGenImage { - const AssetGenImage( - this._assetName, { - this.size, - this.flavors = const {}, - }); - - final String _assetName; - - final Size? size; - final Set flavors; - - Image image({ - Key? key, - AssetBundle? bundle, - ImageFrameBuilder? frameBuilder, - ImageErrorWidgetBuilder? errorBuilder, - String? semanticLabel, - bool excludeFromSemantics = false, - double? scale, - double? width, - double? height, - Color? color, - Animation? opacity, - BlendMode? colorBlendMode, - BoxFit? fit, - AlignmentGeometry alignment = Alignment.center, - ImageRepeat repeat = ImageRepeat.noRepeat, - Rect? centerSlice, - bool matchTextDirection = false, - bool gaplessPlayback = true, - bool isAntiAlias = false, - String? package, - FilterQuality filterQuality = FilterQuality.low, - int? cacheWidth, - int? cacheHeight, - }) { - return Image.asset( - _assetName, - key: key, - bundle: bundle, - frameBuilder: frameBuilder, - errorBuilder: errorBuilder, - semanticLabel: semanticLabel, - excludeFromSemantics: excludeFromSemantics, - scale: scale, - width: width, - height: height, - color: color, - opacity: opacity, - colorBlendMode: colorBlendMode, - fit: fit, - alignment: alignment, - repeat: repeat, - centerSlice: centerSlice, - matchTextDirection: matchTextDirection, - gaplessPlayback: gaplessPlayback, - isAntiAlias: isAntiAlias, - package: package, - filterQuality: filterQuality, - cacheWidth: cacheWidth, - cacheHeight: cacheHeight, - ); - } - - ImageProvider provider({ - AssetBundle? bundle, - String? package, - }) { - return AssetImage( - _assetName, - bundle: bundle, - package: package, - ); - } - - String get path => _assetName; - - String get keyName => _assetName; -} From 8667ccf34d70a11c59027bea5c46306fc46cbdeb Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Mon, 18 Nov 2024 14:25:43 +0100 Subject: [PATCH 08/16] fix: update OK button text in VoicesCalendarDatePicker for localization consistency --- .../widgets/text_field/date_picker/voices_calendar_picker.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart index e7f3bd762d8..12fbb110c87 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart @@ -70,8 +70,7 @@ class VoicesCalendarDatePicker extends StatelessWidget { ), VoicesTextButton( onTap: () => onDateSelected(selectedDate), - child: - Text(context.l10n.snackbarOkButtonText.toUpperCase()), + child: Text(context.l10n.ok.toUpperCase()), ), ], ), From 84bc0eea6a6925aec32f459e5b01f35f31d7a08f Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Tue, 19 Nov 2024 14:46:40 +0100 Subject: [PATCH 09/16] feat: refactor date and time pickers to use DateTime for better consistency and state management across the application --- .../text_field/date_picker/base_picker.dart | 41 +++++----- .../date_picker/date_picker_controller.dart | 75 +++++++++++------- .../date_picker/voices_calendar_picker.dart | 23 +++--- .../date_picker/voices_date_picker_field.dart | 49 ++++-------- .../date_picker/voices_time_picker.dart | 77 +++++++++---------- .../lib/src/catalyst_voices_view_models.dart | 1 + .../lib/src/date_picker/time_slot.dart | 16 ++++ 7 files changed, 153 insertions(+), 129 deletions(-) create mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/date_picker/time_slot.dart diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart index 5a6531a8fba..84c8cbcf6fb 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart @@ -26,6 +26,7 @@ class CalendarFieldPicker extends BasePicker { class TimeFieldPicker extends BasePicker { final TimePickerController controller; + final String timeZone; const TimeFieldPicker({ @@ -85,9 +86,9 @@ abstract class _BasePickerState extends State { setState(() { _isOverlayOpen = false; }); - final scrollController = ScrollControllerProvider.maybeOf(context); - if (scrollController != null && _scrollListener != null) { - scrollController.removeListener(_scrollListener!); + final scrollPosition = Scrollable.maybeOf(context)?.position; + if (scrollPosition != null && _scrollListener != null) { + scrollPosition.removeListener(_scrollListener!); } _overlayEntry?.remove(); _overlayEntry = null; @@ -131,7 +132,7 @@ abstract class _BasePickerState extends State { }); final overlay = Overlay.of(context, rootOverlay: true); final renderBox = context.findRenderObject() as RenderBox?; - final scrollController = ScrollControllerProvider.maybeOf(context); + final scrollPosition = Scrollable.maybeOf(context)?.position; final initialPosition = renderBox!.localToGlobal( Offset.zero, ); @@ -150,9 +151,9 @@ abstract class _BasePickerState extends State { final timeBox = _getRenderBox(timeKey); if (_isBoxTapped(calendarBox, tapPosition)) { - _handleCalendarTap(); + return _handleCalendarTap(); } else if (_isBoxTapped(timeBox, tapPosition)) { - _handleTimeTap(); + return _handleTimeTap(); } else { _removeOverlay(); } @@ -171,11 +172,7 @@ abstract class _BasePickerState extends State { ), ), Positioned( - top: initialPosition.dy + - 50 - - (scrollController?.hasClients ?? false - ? scrollController!.offset - : 0), + top: initialPosition.dy + 50 - (scrollPosition?.pixels ?? 0), left: initialPosition.dx, child: child, ), @@ -183,14 +180,14 @@ abstract class _BasePickerState extends State { ), ); - if (scrollController != null) { + if (scrollPosition != null) { void listener() { if (_overlayEntry != null) { _overlayEntry?.markNeedsBuild(); } } - scrollController.addListener(listener); + scrollPosition.addListener(listener); _scrollListener = listener; } @@ -284,9 +281,11 @@ class _CalendarFieldPickerState extends _BasePickerState { }, decoration: _getInputDecoration( context, - VoicesIconButton( - onTap: _onTap, - child: _getIcon.buildIcon(), + ExcludeFocus( + child: VoicesIconButton( + onTap: _onTap, + child: _getIcon.buildIcon(), + ), ), ), onFieldSubmitted: (String value) {}, @@ -307,7 +306,7 @@ class _TimeFieldPickerState extends _BasePickerState { _removeOverlay(); widget.controller.setValue(value); }, - selectedTime: widget.controller.selectedValue, + selectedTime: widget.controller.text, timeZone: widget.timeZone, ), ); @@ -326,9 +325,11 @@ class _TimeFieldPickerState extends _BasePickerState { }, decoration: _getInputDecoration( context, - VoicesIconButton( - onTap: _onTap, - child: _getIcon.buildIcon(), + ExcludeFocus( + child: VoicesIconButton( + onTap: _onTap, + child: _getIcon.buildIcon(), + ), ), ), onFieldSubmitted: (String value) {}, diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart index 5bf46f98899..6a57f5113f3 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart @@ -48,13 +48,23 @@ extension DatePickerValidationStatusExt on DatePickerValidationStatus { final class DatePickerControllerState extends Equatable { final DateTime? selectedDate; - final String? selectedTime; + final DateTime? selectedTime; bool get isValid => selectedDate != null && selectedTime != null; + DateTime get date { + return DateTime( + selectedDate?.year ?? 0, + selectedDate?.month ?? 0, + selectedDate?.day ?? 0, + selectedTime?.hour ?? 0, + selectedTime?.minute ?? 0, + ); + } + factory DatePickerControllerState({ DateTime? selectedDate, - String? selectedTime, + DateTime? selectedTime, }) { return DatePickerControllerState._( selectedDate: selectedDate, @@ -69,7 +79,7 @@ final class DatePickerControllerState extends Equatable { DatePickerControllerState copyWith({ Optional? selectedDate, - Optional? selectedTime, + Optional? selectedTime, }) { return DatePickerControllerState( selectedDate: selectedDate.dataOr(this.selectedDate), @@ -106,7 +116,7 @@ final class DatePickerController void _onTimePickerControllerChanged() { if (timePickerController.isValid) { value = value.copyWith( - selectedTime: Optional(timePickerController.text), + selectedTime: Optional(timePickerController.selectedValue), ); } } @@ -114,8 +124,6 @@ final class DatePickerController @override void dispose() { super.dispose(); - calendarPickerController.removeListener(_onCalendarPickerControllerChanged); - timePickerController.removeListener(_onTimePickerControllerChanged); calendarPickerController.dispose(); timePickerController.dispose(); } @@ -142,15 +150,14 @@ final class CalendarPickerController if (parts.length != 3) return null; try { - final day = int.parse(parts[0]); - final month = int.parse(parts[1]); - final year = int.parse(parts[2]); + final reformatted = '${parts[2]}-${parts[1]}-${parts[0]}'; + final formatDt = DateTime.parse(reformatted); - if (month < 1 || month > 12) return null; - if (day < 1 || day > 31) return null; - if (year < 1900 || year > 2100) return null; + if (formatDt.month < 1 || formatDt.month > 12) return null; + if (formatDt.day < 1 || formatDt.day > 31) return null; + if (formatDt.year < 1900 || formatDt.year > 2100) return null; - return DateTime(year, month, day); + return formatDt; } catch (e) { return null; } @@ -176,24 +183,25 @@ final class CalendarPickerController } final parts = value.split('/'); - final day = int.parse(parts[0]); - final month = int.parse(parts[1]); - final year = int.parse(parts[2]); + final reformatted = '${parts[2]}-${parts[1]}-${parts[0]}'; + final formatDt = DateTime.parse(reformatted); - if (month < 1 || month > 12) { + if (formatDt.month < 1 || formatDt.month > 12) { return DatePickerValidationStatus.dateFormatError; } - if (day < 1 || day > 31) return DatePickerValidationStatus.daysInMonthError; + if (formatDt.day < 1 || formatDt.day > 31) { + return DatePickerValidationStatus.daysInMonthError; + } - final inputDate = DateTime(year, month, day); + final inputDate = DateTime(formatDt.year, formatDt.month, formatDt.day); if (inputDate.isBefore(today.subtract(const Duration(days: 1))) || inputDate.isAfter(maxDate)) { return DatePickerValidationStatus.dateRangeError; } - final daysInMonth = DateTime(year, month + 1, 0).day; - if (day > daysInMonth) { + final daysInMonth = DateTime(formatDt.year, formatDt.month + 1, 0).day; + if (formatDt.day > daysInMonth) { return DatePickerValidationStatus.daysInMonthError; } @@ -209,12 +217,22 @@ final class CalendarPickerController } } -final class TimePickerController extends FieldDatePickerController { +final class TimePickerController extends FieldDatePickerController { @override - String get pattern => 'HH:MM'; + DateTime? get selectedValue { + if (text.isEmpty) return null; + final parts = text.split(':'); + return DateTime( + 0, + 0, + 0, + int.parse(parts[0]), + int.parse(parts[1]), + ); + } @override - String? get selectedValue => text; + String get pattern => 'HH:MM'; @override DatePickerValidationStatus validate(String? value) { @@ -230,8 +248,13 @@ final class TimePickerController extends FieldDatePickerController { } @override - void setValue(String? newValue) { - value = TextEditingValue(text: newValue.toString()); + void setValue(DateTime? newValue) { + if (newValue == null) return; + value = TextEditingValue( + text: + // ignore: lines_longer_than_80_chars + '${newValue.hour.toString().padLeft(2, '0')}:${newValue.minute.toString().padLeft(2, '0')}', + ); } } diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart index 12fbb110c87..d69af162ba0 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart @@ -1,6 +1,6 @@ part of 'voices_date_picker_field.dart'; -class VoicesCalendarDatePicker extends StatelessWidget { +class VoicesCalendarDatePicker extends StatefulWidget { final ValueChanged onDateSelected; final VoidCallback cancelEvent; final DateTime initialDate; @@ -16,13 +16,12 @@ class VoicesCalendarDatePicker extends StatelessWidget { DateTime? lastDate, }) { final now = DateTime.now(); - final maxDate = DateTime(now.year + 1, now.month, now.day); return VoicesCalendarDatePicker._( key: key, onDateSelected: onDateSelected, initialDate: initialDate ?? now, firstDate: firstDate ?? now, - lastDate: lastDate ?? maxDate, + lastDate: lastDate ?? DateTime(now.year + 1, now.month, now.day), cancelEvent: cancelEvent, ); } @@ -36,9 +35,15 @@ class VoicesCalendarDatePicker extends StatelessWidget { required this.cancelEvent, }); + @override + State createState() => + _VoicesCalendarDatePickerState(); +} + +class _VoicesCalendarDatePickerState extends State { + DateTime selectedDate = DateTime.now(); @override Widget build(BuildContext context) { - var selectedDate = DateTime.now(); return SizedBox( width: 450, child: Material( @@ -52,9 +57,9 @@ class VoicesCalendarDatePicker extends StatelessWidget { child: Column( children: [ CalendarDatePicker( - initialDate: initialDate, - firstDate: firstDate, - lastDate: lastDate, + initialDate: widget.initialDate, + firstDate: widget.firstDate, + lastDate: widget.lastDate, onDateChanged: (val) { selectedDate = val; }, @@ -65,11 +70,11 @@ class VoicesCalendarDatePicker extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ VoicesTextButton( - onTap: cancelEvent, + onTap: widget.cancelEvent, child: Text(context.l10n.cancelButtonText), ), VoicesTextButton( - onTap: () => onDateSelected(selectedDate), + onTap: () => widget.onDateSelected(selectedDate), child: Text(context.l10n.ok.toUpperCase()), ), ], diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart index 0f2270a5e9f..fa147bc790a 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart @@ -3,6 +3,7 @@ import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/material.dart'; part 'base_picker.dart'; @@ -14,33 +15,6 @@ enum DateTimePickerType { date, time } final GlobalKey calendarKey = GlobalKey(); final GlobalKey timeKey = GlobalKey(); -class ScrollControllerProvider extends InheritedWidget { - final ScrollController scrollController; - - const ScrollControllerProvider({ - super.key, - required this.scrollController, - required super.child, - }); - - static ScrollController of(BuildContext context) { - final provider = - context.dependOnInheritedWidgetOfExactType(); - return provider!.scrollController; - } - - static ScrollController? maybeOf(BuildContext context) { - final provider = - context.dependOnInheritedWidgetOfExactType(); - return provider?.scrollController; - } - - @override - bool updateShouldNotify(ScrollControllerProvider oldWidget) { - return scrollController != oldWidget.scrollController; - } -} - class VoicesDatePicker extends StatefulWidget { final DatePickerController controller; final String timeZone; @@ -55,6 +29,17 @@ class VoicesDatePicker extends StatefulWidget { } class _VoicesDatePickerState extends State { + final CalendarPickerController _calendarPickerController = + CalendarPickerController(); + final TimePickerController _timePickerController = TimePickerController(); + + @override + void dispose() { + super.dispose(); + _calendarPickerController.dispose(); + _timePickerController.dispose(); + } + @override Widget build(BuildContext context) { return Row( @@ -62,20 +47,14 @@ class _VoicesDatePickerState extends State { children: [ CalendarFieldPicker( key: calendarKey, - controller: widget.controller.calendarPickerController, + controller: _calendarPickerController, ), TimeFieldPicker( key: timeKey, - controller: widget.controller.timePickerController, + controller: _timePickerController, timeZone: widget.timeZone, ), ], ); } - - @override - void dispose() { - super.dispose(); - widget.controller.dispose(); - } } diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart index e055a4386f5..50dfa5d987b 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart @@ -1,9 +1,10 @@ part of 'voices_date_picker_field.dart'; class VoicesTimePicker extends StatefulWidget { - final ValueChanged onTap; + final ValueChanged onTap; final String? selectedTime; final String timeZone; + const VoicesTimePicker({ super.key, required this.onTap, @@ -17,21 +18,18 @@ class VoicesTimePicker extends StatefulWidget { class _VoicesTimePickerState extends State { late final ScrollController _scrollController; - List get timeList => _generateTimeList(); + final double itemExtent = 40; + List get timeList => _generateTimeList(); @override void initState() { super.initState(); _scrollController = ScrollController(); - if (widget.selectedTime != null) { - WidgetsBinding.instance.addPostFrameCallback((_) async { - final index = timeList.indexOf(widget.selectedTime!); - if (index != -1) { - _scrollController.jumpTo( - index * 40.0, - ); - } + final initialSelection = widget.selectedTime; + if (initialSelection != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToTimeZone(initialSelection); }); } } @@ -54,46 +52,47 @@ class _VoicesTimePickerState extends State { color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1Grey, borderRadius: BorderRadius.circular(20), ), - child: ListView( + child: ListView.builder( controller: _scrollController, - children: timeList - .map( - (e) => TimeText( - key: ValueKey(e), - value: e, - onTap: widget.onTap, - selectedTime: widget.selectedTime, - timeZone: widget.timeZone, - ), - ) - .toList(), + itemExtent: itemExtent, + itemCount: timeList.length, + itemBuilder: (context, index) => _TimeText( + key: ValueKey(timeList[index].formattedTime), + value: timeList[index], + onTap: widget.onTap, + selectedTime: widget.selectedTime, + timeZone: widget.timeZone, + ), ), ), ); } - List _generateTimeList() { - final times = []; - - for (var hour = 0; hour < 24; hour++) { - for (var minute = 0; minute < 60; minute += 30) { - times.add( - // ignore: lines_longer_than_80_chars - '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}', - ); - } + void _scrollToTimeZone(String value) { + final index = + timeList.indexWhere((e) => e.formattedTime == widget.selectedTime); + if (index != -1) { + _scrollController.jumpTo( + index * itemExtent, + ); } + } - return times; + List _generateTimeList() { + return [ + for (var hour = 0; hour < 24; hour++) + for (final minute in [0, 30]) TimeSlot(hour: hour, minute: minute), + ]; } } -class TimeText extends StatelessWidget { - final ValueChanged onTap; - final String value; +class _TimeText extends StatelessWidget { + final ValueChanged onTap; + final TimeSlot value; final String? selectedTime; final String timeZone; - const TimeText({ + + const _TimeText({ super.key, required this.value, required this.onTap, @@ -109,7 +108,7 @@ class TimeText extends StatelessWidget { clipBehavior: Clip.hardEdge, color: Colors.transparent, child: InkWell( - onTap: () => onTap(value), + onTap: () => onTap(value.dateTime), child: ColoredBox( color: !isSelected ? Colors.transparent @@ -121,7 +120,7 @@ class TimeText extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - value, + value.formattedTime, style: Theme.of(context).textTheme.bodyLarge, ), if (isSelected) Text(timeZone), diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/catalyst_voices_view_models.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/catalyst_voices_view_models.dart index 6932612b432..8e938187541 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/catalyst_voices_view_models.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/catalyst_voices_view_models.dart @@ -1,4 +1,5 @@ export 'authentication/authentication.dart'; +export 'date_picker/time_slot.dart'; export 'exception/localized_exception.dart'; export 'exception/localized_unknown_exception.dart'; export 'navigation/sections_list_view_item.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/date_picker/time_slot.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/date_picker/time_slot.dart new file mode 100644 index 00000000000..0c02c5fe6bc --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/date_picker/time_slot.dart @@ -0,0 +1,16 @@ +import 'package:equatable/equatable.dart'; + +class TimeSlot extends Equatable { + final int hour; + final int minute; + + const TimeSlot({required this.hour, required this.minute}); + + String get formattedTime => + '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}'; + + DateTime get dateTime => DateTime(0, 0, 0, hour, minute); + + @override + List get props => [hour, minute]; +} From d9afad0c7afa07f680b9e6e838280ecca94eed07 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Tue, 26 Nov 2024 21:52:54 +0100 Subject: [PATCH 10/16] feat: implement new date and time picker modules with validation and controllers for better UI interaction --- .../date_picker/voices_date_picker_field.dart | 60 ----- .../base_picker.dart | 4 +- .../field_date_picker_controller.dart} | 234 +++++------------- .../pickers}/voices_calendar_picker.dart | 2 +- .../pickers}/voices_time_picker.dart | 2 +- .../voices_date_time_picker.dart | 106 ++++++++ 6 files changed, 173 insertions(+), 235 deletions(-) delete mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart rename catalyst_voices/apps/voices/lib/widgets/text_field/{date_picker => date_time_picker}/base_picker.dart (99%) rename catalyst_voices/apps/voices/lib/widgets/text_field/{date_picker/date_picker_controller.dart => date_time_picker/field_date_picker_controller.dart} (56%) rename catalyst_voices/apps/voices/lib/widgets/text_field/{date_picker => date_time_picker/pickers}/voices_calendar_picker.dart (98%) rename catalyst_voices/apps/voices/lib/widgets/text_field/{date_picker => date_time_picker/pickers}/voices_time_picker.dart (98%) create mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/voices_date_time_picker.dart diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart deleted file mode 100644 index fa147bc790a..00000000000 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_date_picker_field.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:catalyst_voices/widgets/text_field/date_picker/date_picker_controller.dart'; -import 'package:catalyst_voices/widgets/widgets.dart'; -import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; -import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; -import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; -import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; -import 'package:flutter/material.dart'; - -part 'base_picker.dart'; -part 'voices_calendar_picker.dart'; -part 'voices_time_picker.dart'; - -enum DateTimePickerType { date, time } - -final GlobalKey calendarKey = GlobalKey(); -final GlobalKey timeKey = GlobalKey(); - -class VoicesDatePicker extends StatefulWidget { - final DatePickerController controller; - final String timeZone; - const VoicesDatePicker({ - super.key, - required this.controller, - required this.timeZone, - }); - - @override - State createState() => _VoicesDatePickerState(); -} - -class _VoicesDatePickerState extends State { - final CalendarPickerController _calendarPickerController = - CalendarPickerController(); - final TimePickerController _timePickerController = TimePickerController(); - - @override - void dispose() { - super.dispose(); - _calendarPickerController.dispose(); - _timePickerController.dispose(); - } - - @override - Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CalendarFieldPicker( - key: calendarKey, - controller: _calendarPickerController, - ), - TimeFieldPicker( - key: timeKey, - controller: _timePickerController, - timeZone: widget.timeZone, - ), - ], - ); - } -} diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/base_picker.dart similarity index 99% rename from catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart rename to catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/base_picker.dart index 84c8cbcf6fb..c77506ea45e 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/base_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/base_picker.dart @@ -1,4 +1,4 @@ -part of 'voices_date_picker_field.dart'; +part of 'voices_date_time_picker.dart'; abstract class BasePicker extends StatefulWidget { final VoidCallback? onRemoveOverlay; @@ -246,7 +246,7 @@ abstract class _BasePickerState extends State { errorStyle: textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.error, ), - errorMaxLines: 2, + errorMaxLines: 3, ); } } diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/field_date_picker_controller.dart similarity index 56% rename from catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart rename to catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/field_date_picker_controller.dart index 6a57f5113f3..16930808118 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/date_picker_controller.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/field_date_picker_controller.dart @@ -1,134 +1,8 @@ import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; import 'package:catalyst_voices_localization/generated/catalyst_voices_localizations.dart'; -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -enum DatePickerValidationStatus { - success, - dateFormatError, - timeFormatError, - dateRangeError, - daysInMonthError -} - -extension DatePickerValidationStatusExt on DatePickerValidationStatus { - VoicesTextFieldValidationResult message( - VoicesLocalizations l10n, - String pattern, - ) => - switch (this) { - DatePickerValidationStatus.success => - const VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.success, - ), - DatePickerValidationStatus.dateFormatError => - VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.error, - errorMessage: '${l10n.format}: ${pattern.toUpperCase()}', - ), - DatePickerValidationStatus.timeFormatError => - VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.error, - errorMessage: '${l10n.format}: $pattern', - ), - DatePickerValidationStatus.dateRangeError => - VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.error, - errorMessage: l10n.datePickerDateRangeError, - ), - DatePickerValidationStatus.daysInMonthError => - VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.error, - errorMessage: l10n.datePickerDaysInMonthError, - ), - }; -} - -final class DatePickerControllerState extends Equatable { - final DateTime? selectedDate; - final DateTime? selectedTime; - - bool get isValid => selectedDate != null && selectedTime != null; - - DateTime get date { - return DateTime( - selectedDate?.year ?? 0, - selectedDate?.month ?? 0, - selectedDate?.day ?? 0, - selectedTime?.hour ?? 0, - selectedTime?.minute ?? 0, - ); - } - - factory DatePickerControllerState({ - DateTime? selectedDate, - DateTime? selectedTime, - }) { - return DatePickerControllerState._( - selectedDate: selectedDate, - selectedTime: selectedTime, - ); - } - - const DatePickerControllerState._({ - this.selectedDate, - this.selectedTime, - }); - - DatePickerControllerState copyWith({ - Optional? selectedDate, - Optional? selectedTime, - }) { - return DatePickerControllerState( - selectedDate: selectedDate.dataOr(this.selectedDate), - selectedTime: selectedTime.dataOr(this.selectedTime), - ); - } - - @override - List get props => [ - selectedDate, - selectedTime, - ]; -} - -final class DatePickerController - extends ValueNotifier { - CalendarPickerController calendarPickerController = - CalendarPickerController(); - TimePickerController timePickerController = TimePickerController(); - - DatePickerController([super.value = const DatePickerControllerState._()]) { - calendarPickerController.addListener(_onCalendarPickerControllerChanged); - timePickerController.addListener(_onTimePickerControllerChanged); - } - - void _onCalendarPickerControllerChanged() { - if (calendarPickerController.isValid) { - value = value.copyWith( - selectedDate: Optional(calendarPickerController.selectedValue), - ); - } - } - - void _onTimePickerControllerChanged() { - if (timePickerController.isValid) { - value = value.copyWith( - selectedTime: Optional(timePickerController.selectedValue), - ); - } - } - - @override - void dispose() { - super.dispose(); - calendarPickerController.dispose(); - timePickerController.dispose(); - } -} - sealed class FieldDatePickerController extends TextEditingController { abstract final String pattern; @@ -184,26 +58,28 @@ final class CalendarPickerController final parts = value.split('/'); final reformatted = '${parts[2]}-${parts[1]}-${parts[0]}'; - final formatDt = DateTime.parse(reformatted); - if (formatDt.month < 1 || formatDt.month > 12) { + // Need this because DateTime.parse accepts out-of-range component values + // and interprets them as overflows into the next larger component + final day = int.parse(parts[0]); + final month = int.parse(parts[1]); + final year = int.parse(parts[2]); + final formatDt = DateTime.parse(reformatted); + if (month < 1 || month > 12) { return DatePickerValidationStatus.dateFormatError; } - if (formatDt.day < 1 || formatDt.day > 31) { + if (day < 1 || day > 31) { return DatePickerValidationStatus.daysInMonthError; } - final inputDate = DateTime(formatDt.year, formatDt.month, formatDt.day); - - if (inputDate.isBefore(today.subtract(const Duration(days: 1))) || - inputDate.isAfter(maxDate)) { - return DatePickerValidationStatus.dateRangeError; - } - - final daysInMonth = DateTime(formatDt.year, formatDt.month + 1, 0).day; - if (formatDt.day > daysInMonth) { + final daysInMonth = DateTime(year, month + 1, 0).day; + if (day > daysInMonth) { return DatePickerValidationStatus.daysInMonthError; } + if (formatDt.isBefore(today.subtract(const Duration(days: 1))) || + formatDt.isAfter(maxDate)) { + return DatePickerValidationStatus.dateRangeError; + } return DatePickerValidationStatus.success; } @@ -221,14 +97,19 @@ final class TimePickerController extends FieldDatePickerController { @override DateTime? get selectedValue { if (text.isEmpty) return null; - final parts = text.split(':'); - return DateTime( - 0, - 0, - 0, - int.parse(parts[0]), - int.parse(parts[1]), - ); + if (!isValid) return null; + try { + final parts = text.split(':'); + return DateTime( + 0, + 0, + 0, + int.parse(parts[0]), + int.parse(parts[1]), + ); + } catch (e) { + return null; + } } @override @@ -258,30 +139,41 @@ final class TimePickerController extends FieldDatePickerController { } } -final class DatePickerControllerScope extends InheritedWidget { - final DatePickerController controller; - - const DatePickerControllerScope({ - super.key, - required this.controller, - required super.child, - }); - - static DatePickerController of(BuildContext context) { - final controller = context - .dependOnInheritedWidgetOfExactType() - ?.controller; - - assert( - controller != null, - 'Unable to find DatePickerControllerScope in widget tree', - ); - - return controller!; - } +enum DatePickerValidationStatus { + success, + dateFormatError, + timeFormatError, + dateRangeError, + daysInMonthError; - @override - bool updateShouldNotify(covariant DatePickerControllerScope oldWidget) { - return controller != oldWidget.controller; - } + VoicesTextFieldValidationResult message( + VoicesLocalizations l10n, + String pattern, + ) => + switch (this) { + DatePickerValidationStatus.success => + const VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.success, + ), + DatePickerValidationStatus.dateFormatError => + VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.error, + errorMessage: '${l10n.format}: ${pattern.toUpperCase()}', + ), + DatePickerValidationStatus.timeFormatError => + VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.error, + errorMessage: '${l10n.format}: $pattern', + ), + DatePickerValidationStatus.dateRangeError => + VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.error, + errorMessage: l10n.datePickerDateRangeError, + ), + DatePickerValidationStatus.daysInMonthError => + VoicesTextFieldValidationResult( + status: VoicesTextFieldStatus.error, + errorMessage: l10n.datePickerDaysInMonthError, + ), + }; } diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/pickers/voices_calendar_picker.dart similarity index 98% rename from catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart rename to catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/pickers/voices_calendar_picker.dart index d69af162ba0..17df9532a66 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_calendar_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/pickers/voices_calendar_picker.dart @@ -1,4 +1,4 @@ -part of 'voices_date_picker_field.dart'; +part of '../voices_date_time_picker.dart'; class VoicesCalendarDatePicker extends StatefulWidget { final ValueChanged onDateSelected; diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/pickers/voices_time_picker.dart similarity index 98% rename from catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart rename to catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/pickers/voices_time_picker.dart index 50dfa5d987b..13e2879ce5c 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_picker/voices_time_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/pickers/voices_time_picker.dart @@ -1,4 +1,4 @@ -part of 'voices_date_picker_field.dart'; +part of '../voices_date_time_picker.dart'; class VoicesTimePicker extends StatefulWidget { final ValueChanged onTap; diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/voices_date_time_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/voices_date_time_picker.dart new file mode 100644 index 00000000000..498740f010e --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/voices_date_time_picker.dart @@ -0,0 +1,106 @@ +import 'package:catalyst_voices/widgets/buttons/voices_icon_button.dart'; +import 'package:catalyst_voices/widgets/buttons/voices_text_button.dart'; +import 'package:catalyst_voices/widgets/text_field/date_time_picker/field_date_picker_controller.dart'; +import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter/material.dart'; + +part 'base_picker.dart'; +part 'pickers/voices_calendar_picker.dart'; +part 'pickers/voices_time_picker.dart'; + +enum DateTimePickerType { date, time } + +final GlobalKey calendarKey = GlobalKey(); +final GlobalKey timeKey = GlobalKey(); + +class VoicesDateTimeController extends ValueNotifier { + DateTime? date; + DateTime? time; + + VoicesDateTimeController([super.value]); + + DateTime? get _value { + if (date != null && time != null) { + return DateTime( + date!.year, + date!.month, + date!.day, + time!.hour, + time!.minute, + ); + } + return null; + } + + bool get isValid => _value != null; + + void updateDate(DateTime? date) { + this.date = date; + value = _value; + } + + void updateTimeOfDate(DateTime? time) { + this.time = time; + value = _value; + } +} + +class VoicesDateTimePicker extends StatefulWidget { + final VoicesDateTimeController controller; + final String timezone; + const VoicesDateTimePicker({ + super.key, + required this.controller, + required this.timezone, + }); + + @override + State createState() => _VoicesDateTimePickerState(); +} + +class _VoicesDateTimePickerState extends State { + late final CalendarPickerController _dateController; + late final TimePickerController _timeController; + + @override + void initState() { + super.initState(); + _dateController = CalendarPickerController()..addListener(_dateListiner); + _timeController = TimePickerController()..addListener(_timeListiner); + } + + void _dateListiner() { + widget.controller.updateDate(_dateController.selectedValue); + } + + void _timeListiner() { + widget.controller.updateTimeOfDate(_timeController.selectedValue); + } + + @override + void dispose() { + super.dispose(); + _dateController.dispose(); + _timeController.dispose(); + } + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CalendarFieldPicker( + controller: _dateController, + ), + TimeFieldPicker( + controller: _timeController, + timeZone: widget.timezone, + ), + ], + ); + } +} From b09e9f8ba6b241a4423ab30e9dc2dba34445b43d Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Wed, 27 Nov 2024 10:20:05 +0100 Subject: [PATCH 11/16] fix: static-analytics --- .../date_time_picker/pickers/voices_time_picker.dart | 2 +- .../date_time_picker/voices_date_time_picker.dart | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/pickers/voices_time_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/pickers/voices_time_picker.dart index 13e2879ce5c..b8efc80a27d 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/pickers/voices_time_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/pickers/voices_time_picker.dart @@ -100,7 +100,7 @@ class _TimeText extends StatelessWidget { required this.timeZone, }); - bool get isSelected => selectedTime == value; + bool get isSelected => selectedTime == value.formattedTime; @override Widget build(BuildContext context) { diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/voices_date_time_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/voices_date_time_picker.dart index 498740f010e..2ca05a35ead 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/voices_date_time_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/voices_date_time_picker.dart @@ -69,15 +69,15 @@ class _VoicesDateTimePickerState extends State { @override void initState() { super.initState(); - _dateController = CalendarPickerController()..addListener(_dateListiner); - _timeController = TimePickerController()..addListener(_timeListiner); + _dateController = CalendarPickerController()..addListener(_dateListener); + _timeController = TimePickerController()..addListener(_timeListener); } - void _dateListiner() { + void _dateListener() { widget.controller.updateDate(_dateController.selectedValue); } - void _timeListiner() { + void _timeListener() { widget.controller.updateTimeOfDate(_timeController.selectedValue); } From 7fcb3af089231ef110f3cb20e47fd172742936aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20Moli=C5=84ski?= <47773413+damian-molinski@users.noreply.github.com> Date: Fri, 29 Nov 2024 09:31:45 +0100 Subject: [PATCH 12/16] refactor: date time text field (#1293) * refactor: date time text field * fix: doc reference --- .../lib/common/ext/time_of_day_ext.dart | 10 + .../pickers/voices_calendar_picker.dart | 6 +- .../pickers/voices_time_picker.dart | 79 ++-- .../date_time_picker/base_picker.dart | 340 ------------------ .../field_date_picker_controller.dart | 179 --------- .../voices_date_time_picker.dart | 106 ------ .../widgets/text_field/voices_date_field.dart | 210 +++++++++++ .../text_field/voices_date_time_field.dart | 282 +++++++++++++++ .../voices_date_time_text_field.dart | 92 +++++ .../widgets/text_field/voices_text_field.dart | 36 +- .../widgets/text_field/voices_time_field.dart | 160 +++++++++ .../apps/voices/lib/widgets/widgets.dart | 1 + .../text_field/voices_text_field_test.dart | 4 +- .../lib/src/catalyst_voices_view_models.dart | 1 - .../lib/src/date_picker/time_slot.dart | 16 - .../examples/voices_text_field_example.dart | 36 +- 16 files changed, 852 insertions(+), 706 deletions(-) create mode 100644 catalyst_voices/apps/voices/lib/common/ext/time_of_day_ext.dart rename catalyst_voices/apps/voices/lib/widgets/{text_field/date_time_picker => }/pickers/voices_calendar_picker.dart (90%) rename catalyst_voices/apps/voices/lib/widgets/{text_field/date_time_picker => }/pickers/voices_time_picker.dart (58%) delete mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/base_picker.dart delete mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/field_date_picker_controller.dart delete mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/voices_date_time_picker.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_field.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_text_field.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/date_picker/time_slot.dart diff --git a/catalyst_voices/apps/voices/lib/common/ext/time_of_day_ext.dart b/catalyst_voices/apps/voices/lib/common/ext/time_of_day_ext.dart new file mode 100644 index 00000000000..8bd65d9a602 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/common/ext/time_of_day_ext.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +extension TimeOfDayExt on TimeOfDay { + String get formatted { + final hour = this.hour.toString().padLeft(2, '0'); + final minute = this.minute.toString().padLeft(2, '0'); + + return '$hour:$minute'; + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/pickers/voices_calendar_picker.dart b/catalyst_voices/apps/voices/lib/widgets/pickers/voices_calendar_picker.dart similarity index 90% rename from catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/pickers/voices_calendar_picker.dart rename to catalyst_voices/apps/voices/lib/widgets/pickers/voices_calendar_picker.dart index 17df9532a66..591253add14 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/pickers/voices_calendar_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/pickers/voices_calendar_picker.dart @@ -1,4 +1,7 @@ -part of '../voices_date_time_picker.dart'; +import 'package:catalyst_voices/widgets/buttons/voices_text_button.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:flutter/material.dart'; class VoicesCalendarDatePicker extends StatefulWidget { final ValueChanged onDateSelected; @@ -42,6 +45,7 @@ class VoicesCalendarDatePicker extends StatefulWidget { class _VoicesCalendarDatePickerState extends State { DateTime selectedDate = DateTime.now(); + @override Widget build(BuildContext context) { return SizedBox( diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/pickers/voices_time_picker.dart b/catalyst_voices/apps/voices/lib/widgets/pickers/voices_time_picker.dart similarity index 58% rename from catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/pickers/voices_time_picker.dart rename to catalyst_voices/apps/voices/lib/widgets/pickers/voices_time_picker.dart index b8efc80a27d..36c79ccaf4e 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/pickers/voices_time_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/pickers/voices_time_picker.dart @@ -1,15 +1,17 @@ -part of '../voices_date_time_picker.dart'; +import 'package:catalyst_voices/common/ext/time_of_day_ext.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:flutter/material.dart'; class VoicesTimePicker extends StatefulWidget { - final ValueChanged onTap; - final String? selectedTime; - final String timeZone; + final ValueChanged onTap; + final TimeOfDay? selectedTime; + final String? timeZone; const VoicesTimePicker({ super.key, required this.onTap, this.selectedTime, - required this.timeZone, + this.timeZone, }); @override @@ -18,18 +20,21 @@ class VoicesTimePicker extends StatefulWidget { class _VoicesTimePickerState extends State { late final ScrollController _scrollController; + late final List _timeList; + final double itemExtent = 40; - List get timeList => _generateTimeList(); @override void initState() { super.initState(); + + _timeList = _generateTimeList(); _scrollController = ScrollController(); final initialSelection = widget.selectedTime; if (initialSelection != null) { WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToTimeZone(initialSelection); + _scrollToTime(initialSelection); }); } } @@ -43,8 +48,8 @@ class _VoicesTimePickerState extends State { @override Widget build(BuildContext context) { return Material( + type: MaterialType.transparency, clipBehavior: Clip.hardEdge, - color: Colors.transparent, child: Container( height: 350, width: 150, @@ -55,60 +60,62 @@ class _VoicesTimePickerState extends State { child: ListView.builder( controller: _scrollController, itemExtent: itemExtent, - itemCount: timeList.length, - itemBuilder: (context, index) => _TimeText( - key: ValueKey(timeList[index].formattedTime), - value: timeList[index], - onTap: widget.onTap, - selectedTime: widget.selectedTime, - timeZone: widget.timeZone, - ), + itemCount: _timeList.length, + itemBuilder: (context, index) { + final timeOfDay = _timeList[index]; + + return _TimeText( + key: ValueKey(timeOfDay.formatted), + value: timeOfDay, + onTap: widget.onTap, + isSelected: timeOfDay == widget.selectedTime, + timeZone: widget.timeZone, + ); + }, ), ), ); } - void _scrollToTimeZone(String value) { - final index = - timeList.indexWhere((e) => e.formattedTime == widget.selectedTime); + void _scrollToTime(TimeOfDay value) { + final index = _timeList.indexWhere((e) => e == widget.selectedTime); + if (index != -1) { - _scrollController.jumpTo( - index * itemExtent, - ); + _scrollController.jumpTo(index * itemExtent); } } - List _generateTimeList() { + List _generateTimeList() { return [ for (var hour = 0; hour < 24; hour++) - for (final minute in [0, 30]) TimeSlot(hour: hour, minute: minute), + for (final minute in [0, 30]) TimeOfDay(hour: hour, minute: minute), ]; } } class _TimeText extends StatelessWidget { - final ValueChanged onTap; - final TimeSlot value; - final String? selectedTime; - final String timeZone; + final ValueChanged onTap; + final TimeOfDay value; + final bool isSelected; + final String? timeZone; const _TimeText({ super.key, required this.value, required this.onTap, - this.selectedTime, - required this.timeZone, + this.isSelected = false, + this.timeZone, }); - bool get isSelected => selectedTime == value.formattedTime; - @override Widget build(BuildContext context) { + final timeZone = this.timeZone; + return Material( clipBehavior: Clip.hardEdge, - color: Colors.transparent, + type: MaterialType.transparency, child: InkWell( - onTap: () => onTap(value.dateTime), + onTap: () => onTap(value), child: ColoredBox( color: !isSelected ? Colors.transparent @@ -120,10 +127,10 @@ class _TimeText extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - value.formattedTime, + value.formatted, style: Theme.of(context).textTheme.bodyLarge, ), - if (isSelected) Text(timeZone), + if (isSelected && timeZone != null) Text(timeZone), ], ), ), diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/base_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/base_picker.dart deleted file mode 100644 index c77506ea45e..00000000000 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/base_picker.dart +++ /dev/null @@ -1,340 +0,0 @@ -part of 'voices_date_time_picker.dart'; - -abstract class BasePicker extends StatefulWidget { - final VoidCallback? onRemoveOverlay; - final DateTimePickerType pickerType; - - const BasePicker({ - super.key, - required this.pickerType, - this.onRemoveOverlay, - }); -} - -class CalendarFieldPicker extends BasePicker { - final CalendarPickerController controller; - - const CalendarFieldPicker({ - super.key, - required this.controller, - super.onRemoveOverlay, - }) : super(pickerType: DateTimePickerType.date); - - @override - State createState() => _CalendarFieldPickerState(); -} - -class TimeFieldPicker extends BasePicker { - final TimePickerController controller; - - final String timeZone; - - const TimeFieldPicker({ - super.key, - required this.controller, - required this.timeZone, - super.onRemoveOverlay, - }) : super(pickerType: DateTimePickerType.time); - - @override - State createState() => _TimeFieldPickerState(); -} - -abstract class _BasePickerState extends State { - static _BasePickerState? _activePicker; - OverlayEntry? _overlayEntry; - VoidCallback? _scrollListener; - bool _isOverlayOpen = false; - - @override - void dispose() { - super.dispose(); - _overlayEntry?.remove(); - _overlayEntry = null; - } - - double get _getWidth => switch (widget.pickerType) { - DateTimePickerType.date => 220, - DateTimePickerType.time => 170, - }; - - BorderRadius get _getBorderRadius => switch (widget.pickerType) { - DateTimePickerType.date => const BorderRadius.only( - topLeft: Radius.circular(16), - bottomLeft: Radius.circular(16), - ), - DateTimePickerType.time => const BorderRadius.only( - topRight: Radius.circular(16), - bottomRight: Radius.circular(16), - ), - }; - - String get _getHintText => switch (widget.pickerType) { - DateTimePickerType.date => 'DD/MM/YYYY', - DateTimePickerType.time when widget is TimeFieldPicker => - '00:00 ${(widget as TimeFieldPicker).timeZone}', - _ => '00:00', - }; - - SvgGenImage get _getIcon => switch (widget.pickerType) { - DateTimePickerType.date => VoicesAssets.icons.calendar, - DateTimePickerType.time => VoicesAssets.icons.clock, - }; - - void _removeOverlay() { - if (_overlayEntry != null) { - setState(() { - _isOverlayOpen = false; - }); - final scrollPosition = Scrollable.maybeOf(context)?.position; - if (scrollPosition != null && _scrollListener != null) { - scrollPosition.removeListener(_scrollListener!); - } - _overlayEntry?.remove(); - _overlayEntry = null; - _scrollListener = null; - _activePicker = null; - } - } - - RenderBox? _getRenderBox(GlobalKey key) { - return key.currentContext?.findRenderObject() as RenderBox?; - } - - bool _isBoxTapped(RenderBox? box, Offset tapPosition) { - return box != null && - (box.localToGlobal(Offset.zero) & box.size).contains(tapPosition); - } - - void _handleCalendarTap() { - _activePicker?._removeOverlay(); - final calendarState = calendarKey.currentState; - if (calendarState is _CalendarFieldPickerState) { - calendarState._onTap(); - } - } - - void _handleTimeTap() { - _activePicker?._removeOverlay(); - final timeState = timeKey.currentState; - if (timeState is _TimeFieldPickerState) { - timeState._onTap(); - } - } - - void _showOverlay(Widget child) { - FocusScope.of(context).unfocus(); - if (_activePicker != null && _activePicker != this) { - _activePicker!._removeOverlay(); - } - setState(() { - _isOverlayOpen = true; - }); - final overlay = Overlay.of(context, rootOverlay: true); - final renderBox = context.findRenderObject() as RenderBox?; - final scrollPosition = Scrollable.maybeOf(context)?.position; - final initialPosition = renderBox!.localToGlobal( - Offset.zero, - ); - - _overlayEntry = OverlayEntry( - builder: (context) => Stack( - children: [ - Positioned.fill( - child: MouseRegion( - opaque: false, - hitTestBehavior: HitTestBehavior.translucent, - child: GestureDetector( - onTapDown: (details) { - final tapPosition = details.globalPosition; - final calendarBox = _getRenderBox(calendarKey); - final timeBox = _getRenderBox(timeKey); - - if (_isBoxTapped(calendarBox, tapPosition)) { - return _handleCalendarTap(); - } else if (_isBoxTapped(timeBox, tapPosition)) { - return _handleTimeTap(); - } else { - _removeOverlay(); - } - }, - behavior: HitTestBehavior.translucent, - excludeFromSemantics: true, - onPanUpdate: null, - onPanDown: null, - onPanCancel: null, - onPanEnd: null, - onPanStart: null, - child: Container( - color: Colors.transparent, - ), - ), - ), - ), - Positioned( - top: initialPosition.dy + 50 - (scrollPosition?.pixels ?? 0), - left: initialPosition.dx, - child: child, - ), - ], - ), - ); - - if (scrollPosition != null) { - void listener() { - if (_overlayEntry != null) { - _overlayEntry?.markNeedsBuild(); - } - } - - scrollPosition.addListener(listener); - _scrollListener = listener; - } - - _activePicker = this; - overlay.insert(_overlayEntry!); - } - - VoicesTextFieldDecoration _getInputDecoration( - BuildContext context, - Widget suffixIcon, - ) { - final theme = Theme.of(context); - final textTheme = theme.textTheme; - final borderSide = _isOverlayOpen - ? BorderSide( - color: theme.primaryColor, - width: 2, - ) - : BorderSide( - color: theme.colors.outlineBorderVariant!, - width: 0.75, - ); - return VoicesTextFieldDecoration( - suffixIcon: suffixIcon, - fillColor: theme.colors.elevationsOnSurfaceNeutralLv1Grey, - filled: true, - enabledBorder: OutlineInputBorder( - borderSide: borderSide, - borderRadius: _getBorderRadius, - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: theme.primaryColor, - width: 2, - ), - borderRadius: _getBorderRadius, - ), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.error, - width: 2, - ), - borderRadius: _getBorderRadius, - ), - focusedErrorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.error, - width: 2, - ), - borderRadius: _getBorderRadius, - ), - hintText: _getHintText, - hintStyle: textTheme.bodyLarge?.copyWith( - color: theme.colors.textDisabled, - ), - errorStyle: textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.error, - ), - errorMaxLines: 3, - ); - } -} - -class _CalendarFieldPickerState extends _BasePickerState { - void _onTap() { - if (_BasePickerState._activePicker == this) { - _removeOverlay(); - } else { - _showOverlay( - VoicesCalendarDatePicker( - initialDate: widget.controller.selectedValue, - onDateSelected: (value) { - _removeOverlay(); - widget.controller.setValue(value); - }, - cancelEvent: _removeOverlay, - ), - ); - } - } - - @override - Widget build(BuildContext context) { - return SizedBox( - width: _getWidth, - child: VoicesTextField( - controller: widget.controller, - validator: (value) { - final status = widget.controller.validate(value); - return status.message(context.l10n, widget.controller.pattern); - }, - decoration: _getInputDecoration( - context, - ExcludeFocus( - child: VoicesIconButton( - onTap: _onTap, - child: _getIcon.buildIcon(), - ), - ), - ), - onFieldSubmitted: (String value) {}, - autovalidateMode: AutovalidateMode.onUserInteraction, - ), - ); - } -} - -class _TimeFieldPickerState extends _BasePickerState { - void _onTap() { - if (_BasePickerState._activePicker == this) { - _removeOverlay(); - } else { - _showOverlay( - VoicesTimePicker( - onTap: (value) { - _removeOverlay(); - widget.controller.setValue(value); - }, - selectedTime: widget.controller.text, - timeZone: widget.timeZone, - ), - ); - } - } - - @override - Widget build(BuildContext context) { - return SizedBox( - width: _getWidth, - child: VoicesTextField( - controller: widget.controller, - validator: (value) { - final status = widget.controller.validate(value); - return status.message(context.l10n, widget.controller.pattern); - }, - decoration: _getInputDecoration( - context, - ExcludeFocus( - child: VoicesIconButton( - onTap: _onTap, - child: _getIcon.buildIcon(), - ), - ), - ), - onFieldSubmitted: (String value) {}, - autovalidateMode: AutovalidateMode.onUserInteraction, - ), - ); - } -} diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/field_date_picker_controller.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/field_date_picker_controller.dart deleted file mode 100644 index 16930808118..00000000000 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/field_date_picker_controller.dart +++ /dev/null @@ -1,179 +0,0 @@ -import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; -import 'package:catalyst_voices_localization/generated/catalyst_voices_localizations.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - -sealed class FieldDatePickerController extends TextEditingController { - abstract final String pattern; - - bool get isValid => validate(text) == DatePickerValidationStatus.success; - T get selectedValue; - - DatePickerValidationStatus validate(String? value); - - void setValue(T newValue); -} - -final class CalendarPickerController - extends FieldDatePickerController { - @override - DateTime? get selectedValue { - if (text.isEmpty) return null; - - final parts = text.split('/'); - if (parts.length != 3) return null; - - try { - final reformatted = '${parts[2]}-${parts[1]}-${parts[0]}'; - final formatDt = DateTime.parse(reformatted); - - if (formatDt.month < 1 || formatDt.month > 12) return null; - if (formatDt.day < 1 || formatDt.day > 31) return null; - if (formatDt.year < 1900 || formatDt.year > 2100) return null; - - return formatDt; - } catch (e) { - return null; - } - } - - @override - String get pattern => 'dd/MM/yyyy'; - - @override - DatePickerValidationStatus validate(String? value) { - final today = DateTime.now(); - final maxDate = DateTime(today.year + 1, today.month, today.day); - - if (value == null || value == '') { - return DatePickerValidationStatus.success; - } - - if (value.length != 10) return DatePickerValidationStatus.dateFormatError; - - final dateRegex = RegExp(r'^(\d{2})/(\d{2})/(\d{4})$'); - if (!dateRegex.hasMatch(value)) { - return DatePickerValidationStatus.dateFormatError; - } - - final parts = value.split('/'); - final reformatted = '${parts[2]}-${parts[1]}-${parts[0]}'; - - // Need this because DateTime.parse accepts out-of-range component values - // and interprets them as overflows into the next larger component - final day = int.parse(parts[0]); - final month = int.parse(parts[1]); - final year = int.parse(parts[2]); - final formatDt = DateTime.parse(reformatted); - if (month < 1 || month > 12) { - return DatePickerValidationStatus.dateFormatError; - } - if (day < 1 || day > 31) { - return DatePickerValidationStatus.daysInMonthError; - } - - final daysInMonth = DateTime(year, month + 1, 0).day; - if (day > daysInMonth) { - return DatePickerValidationStatus.daysInMonthError; - } - if (formatDt.isBefore(today.subtract(const Duration(days: 1))) || - formatDt.isAfter(maxDate)) { - return DatePickerValidationStatus.dateRangeError; - } - - return DatePickerValidationStatus.success; - } - - @override - void setValue(DateTime? newValue) { - if (newValue == null) return; - final newT = newValue; - final formatter = DateFormat(pattern); - value = TextEditingValue(text: formatter.format(newT)); - } -} - -final class TimePickerController extends FieldDatePickerController { - @override - DateTime? get selectedValue { - if (text.isEmpty) return null; - if (!isValid) return null; - try { - final parts = text.split(':'); - return DateTime( - 0, - 0, - 0, - int.parse(parts[0]), - int.parse(parts[1]), - ); - } catch (e) { - return null; - } - } - - @override - String get pattern => 'HH:MM'; - - @override - DatePickerValidationStatus validate(String? value) { - if (value == null || value == '') { - return DatePickerValidationStatus.success; - } - final pattern = RegExp(r'^(0?[0-9]|1[0-9]|2[0-3]):([0-5][0-9])$'); - if (!pattern.hasMatch(value)) { - return DatePickerValidationStatus.timeFormatError; - } - - return DatePickerValidationStatus.success; - } - - @override - void setValue(DateTime? newValue) { - if (newValue == null) return; - value = TextEditingValue( - text: - // ignore: lines_longer_than_80_chars - '${newValue.hour.toString().padLeft(2, '0')}:${newValue.minute.toString().padLeft(2, '0')}', - ); - } -} - -enum DatePickerValidationStatus { - success, - dateFormatError, - timeFormatError, - dateRangeError, - daysInMonthError; - - VoicesTextFieldValidationResult message( - VoicesLocalizations l10n, - String pattern, - ) => - switch (this) { - DatePickerValidationStatus.success => - const VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.success, - ), - DatePickerValidationStatus.dateFormatError => - VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.error, - errorMessage: '${l10n.format}: ${pattern.toUpperCase()}', - ), - DatePickerValidationStatus.timeFormatError => - VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.error, - errorMessage: '${l10n.format}: $pattern', - ), - DatePickerValidationStatus.dateRangeError => - VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.error, - errorMessage: l10n.datePickerDateRangeError, - ), - DatePickerValidationStatus.daysInMonthError => - VoicesTextFieldValidationResult( - status: VoicesTextFieldStatus.error, - errorMessage: l10n.datePickerDaysInMonthError, - ), - }; -} diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/voices_date_time_picker.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/voices_date_time_picker.dart deleted file mode 100644 index 2ca05a35ead..00000000000 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/date_time_picker/voices_date_time_picker.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:catalyst_voices/widgets/buttons/voices_icon_button.dart'; -import 'package:catalyst_voices/widgets/buttons/voices_text_button.dart'; -import 'package:catalyst_voices/widgets/text_field/date_time_picker/field_date_picker_controller.dart'; -import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; -import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; -import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; -import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; -import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; -import 'package:flutter/material.dart'; - -part 'base_picker.dart'; -part 'pickers/voices_calendar_picker.dart'; -part 'pickers/voices_time_picker.dart'; - -enum DateTimePickerType { date, time } - -final GlobalKey calendarKey = GlobalKey(); -final GlobalKey timeKey = GlobalKey(); - -class VoicesDateTimeController extends ValueNotifier { - DateTime? date; - DateTime? time; - - VoicesDateTimeController([super.value]); - - DateTime? get _value { - if (date != null && time != null) { - return DateTime( - date!.year, - date!.month, - date!.day, - time!.hour, - time!.minute, - ); - } - return null; - } - - bool get isValid => _value != null; - - void updateDate(DateTime? date) { - this.date = date; - value = _value; - } - - void updateTimeOfDate(DateTime? time) { - this.time = time; - value = _value; - } -} - -class VoicesDateTimePicker extends StatefulWidget { - final VoicesDateTimeController controller; - final String timezone; - const VoicesDateTimePicker({ - super.key, - required this.controller, - required this.timezone, - }); - - @override - State createState() => _VoicesDateTimePickerState(); -} - -class _VoicesDateTimePickerState extends State { - late final CalendarPickerController _dateController; - late final TimePickerController _timeController; - - @override - void initState() { - super.initState(); - _dateController = CalendarPickerController()..addListener(_dateListener); - _timeController = TimePickerController()..addListener(_timeListener); - } - - void _dateListener() { - widget.controller.updateDate(_dateController.selectedValue); - } - - void _timeListener() { - widget.controller.updateTimeOfDate(_timeController.selectedValue); - } - - @override - void dispose() { - super.dispose(); - _dateController.dispose(); - _timeController.dispose(); - } - - @override - Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CalendarFieldPicker( - controller: _dateController, - ), - TimeFieldPicker( - controller: _timeController, - timeZone: widget.timezone, - ), - ], - ); - } -} diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart new file mode 100644 index 00000000000..1e02ad22b86 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart @@ -0,0 +1,210 @@ +import 'package:catalyst_voices/widgets/buttons/voices_icon_button.dart'; +import 'package:catalyst_voices/widgets/text_field/voices_date_time_text_field.dart'; +import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:flutter/material.dart'; + +final class VoicesDateFieldController extends ValueNotifier { + VoicesDateFieldController([super._value]); +} + +class VoicesDateField extends StatefulWidget { + final VoicesDateFieldController? controller; + final ValueChanged? onChanged; + final ValueChanged? onFieldSubmitted; + final VoidCallback? onCalendarTap; + final bool dimBorder; + final BorderRadius borderRadius; + + const VoicesDateField({ + super.key, + this.controller, + this.onChanged, + required this.onFieldSubmitted, + this.onCalendarTap, + this.dimBorder = false, + this.borderRadius = const BorderRadius.all(Radius.circular(16)), + }); + + @override + State createState() => _VoicesDateFieldState(); +} + +class _VoicesDateFieldState extends State { + late final TextEditingController _textEditingController; + + VoicesDateFieldController? _controller; + + VoicesDateFieldController get _effectiveController { + return widget.controller ?? (_controller ??= VoicesDateFieldController()); + } + + String get _pattern => 'dd/MM/yyyy'; + + @override + void initState() { + final initialDate = _effectiveController.value; + final initialText = _convertDateToText(initialDate); + + _textEditingController = TextEditingController(text: initialText); + _textEditingController.addListener(_handleTextChanged); + + _effectiveController.addListener(_handleDateChanged); + + super.initState(); + } + + @override + void didUpdateWidget(covariant VoicesDateField oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.controller != oldWidget.controller) { + (oldWidget.controller ?? _controller)?.removeListener(_handleDateChanged); + (widget.controller ?? _controller)?.addListener(_handleDateChanged); + + final date = _effectiveController.value; + _textEditingController.text = _convertDateToText(date); + } + } + + @override + void dispose() { + _textEditingController.dispose(); + + _controller?.dispose(); + _controller = null; + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final onChanged = widget.onChanged; + final onFieldSubmitted = widget.onFieldSubmitted; + + return VoicesDateTimeTextField( + controller: _textEditingController, + onChanged: onChanged != null + ? (value) => onChanged(_convertTextToDate(value)) + : null, + validator: _validate, + hintText: _pattern.toUpperCase(), + onFieldSubmitted: onFieldSubmitted != null + ? (value) => onFieldSubmitted(_convertTextToDate(value)) + : null, + suffixIcon: ExcludeFocus( + child: VoicesIconButton( + onTap: widget.onCalendarTap, + child: VoicesAssets.icons.calendar.buildIcon(), + ), + ), + borderRadius: widget.borderRadius, + dimBorder: widget.dimBorder, + ); + } + + void _handleTextChanged() { + final date = _convertTextToDate(_textEditingController.text); + if (_effectiveController.value != date) { + _effectiveController.value = date; + } + } + + void _handleDateChanged() { + final text = _convertDateToText(_effectiveController.value); + if (_textEditingController.text != text) { + _textEditingController.text = text; + } + } + + String _convertDateToText(DateTime? value) { + if (value == null) { + return ''; + } + + final day = value.day.toString().padLeft(2, '0'); + final month = value.month.toString().padLeft(2, '0'); + final year = value.year; + + return '$day/$month/$year'; + } + + DateTime? _convertTextToDate(String value) { + if (value.isEmpty) return null; + + final parts = value.split('/'); + if (parts.length != 3) return null; + + try { + final reformatted = '${parts[2]}-${parts[1]}-${parts[0]}'; + final formatDt = DateTime.parse(reformatted); + + if (formatDt.month < 1 || formatDt.month > 12) return null; + if (formatDt.day < 1 || formatDt.day > 31) return null; + if (formatDt.year < 1900 || formatDt.year > 2100) return null; + + return formatDt; + } catch (e) { + return null; + } + } + + VoicesTextFieldValidationResult _validate(String value) { + final today = DateTime.now(); + final maxDate = DateTime(today.year + 1, today.month, today.day); + + if (value.isEmpty) { + return const VoicesTextFieldValidationResult.success(); + } + + final l10n = context.l10n; + + if (value.length != 10) { + return VoicesTextFieldValidationResult.error( + '${l10n.format}: ${_pattern.toUpperCase()}', + ); + } + + final dateRegex = RegExp(r'^(\d{2})/(\d{2})/(\d{4})$'); + if (!dateRegex.hasMatch(value)) { + return VoicesTextFieldValidationResult.error( + '${l10n.format}: ${_pattern.toUpperCase()}', + ); + } + + final parts = value.split('/'); + final reformatted = '${parts[2]}-${parts[1]}-${parts[0]}'; + + // Need this because DateTime.parse accepts out-of-range component values + // and interprets them as overflows into the next larger component + final day = int.parse(parts[0]); + final month = int.parse(parts[1]); + final year = int.parse(parts[2]); + final formatDt = DateTime.parse(reformatted); + if (month < 1 || month > 12) { + return VoicesTextFieldValidationResult.error( + '${l10n.format}: ${_pattern.toUpperCase()}', + ); + } + if (day < 1 || day > 31) { + return VoicesTextFieldValidationResult.error( + l10n.datePickerDaysInMonthError, + ); + } + + final daysInMonth = DateTime(year, month + 1, 0).day; + if (day > daysInMonth) { + return VoicesTextFieldValidationResult.error( + l10n.datePickerDaysInMonthError, + ); + } + if (formatDt.isBefore(today.subtract(const Duration(days: 1))) || + formatDt.isAfter(maxDate)) { + return VoicesTextFieldValidationResult.error( + l10n.datePickerDateRangeError, + ); + } + + return const VoicesTextFieldValidationResult.success(); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_field.dart new file mode 100644 index 00000000000..3497316289c --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_field.dart @@ -0,0 +1,282 @@ +import 'package:catalyst_voices/widgets/pickers/voices_calendar_picker.dart'; +import 'package:catalyst_voices/widgets/pickers/voices_time_picker.dart'; +import 'package:catalyst_voices/widgets/text_field/voices_date_field.dart'; +import 'package:catalyst_voices/widgets/text_field/voices_time_field.dart'; +import 'package:flutter/material.dart'; + +typedef DateTimeParts = ({DateTime date, TimeOfDay time}); + +final class VoicesDateTimeFieldController extends ValueNotifier { + VoicesDateTimeFieldController([super._value]); +} + +class VoicesDateTimeField extends StatefulWidget { + final VoicesDateTimeFieldController? controller; + final ValueChanged? onChanged; + final ValueChanged? onFieldSubmitted; + final String? timeZone; + + const VoicesDateTimeField({ + super.key, + this.controller, + this.onChanged, + required this.onFieldSubmitted, + this.timeZone, + }); + + @override + State createState() => _VoicesDateTimeFieldState(); +} + +class _VoicesDateTimeFieldState extends State { + late final VoicesDateFieldController _dateController; + late final VoicesTimeFieldController _timeController; + + final GlobalKey _dateFiledKey = GlobalKey(); + final GlobalKey _timeFieldKey = GlobalKey(); + + VoicesDateTimeFieldController? _controller; + + VoicesDateTimeFieldController get _effectiveController { + return widget.controller ?? + (_controller ??= VoicesDateTimeFieldController()); + } + + OverlayEntry? _overlayEntry; + VoidCallback? _scrollListener; + bool _isOverlayOpen = false; + + @override + void initState() { + super.initState(); + + final dateTime = _effectiveController.value; + final parts = dateTime != null ? _convertDateTimeToParts(dateTime) : null; + + _effectiveController.addListener(_handleDateTimeChanged); + + _dateController = VoicesDateFieldController(parts?.date); + _dateController.addListener(_handleDateChanged); + + _timeController = VoicesTimeFieldController(parts?.time); + _timeController.addListener(_handleTimeChanged); + } + + @override + void didUpdateWidget(covariant VoicesDateTimeField oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.controller != oldWidget.controller) { + (oldWidget.controller ?? _controller) + ?.removeListener(_handleDateTimeChanged); + (widget.controller ?? _controller)?.addListener(_handleDateTimeChanged); + } + } + + @override + void dispose() { + _controller?.dispose(); + _controller = null; + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: 220), + child: VoicesDateField( + key: _dateFiledKey, + controller: _dateController, + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(16), + ), + dimBorder: _isOverlayOpen, + onCalendarTap: _showDatePicker, + onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(), + ), + ), + ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: 170), + child: VoicesTimeField( + key: _timeFieldKey, + controller: _timeController, + borderRadius: const BorderRadius.horizontal( + right: Radius.circular(16), + ), + dimBorder: _isOverlayOpen, + onClockTap: _showTimePicker, + onFieldSubmitted: (_) { + widget.onFieldSubmitted?.call(_effectiveController.value); + }, + ), + ), + ], + ); + } + + Future _showDatePicker() async { + final picker = VoicesCalendarDatePicker( + initialDate: _dateController.value, + onDateSelected: (value) { + _removeOverlay(); + _dateController.value = value; + }, + cancelEvent: _removeOverlay, + ); + + final initialPosition = _getRenderBoxOffset(_dateFiledKey); + + _showOverlay( + initialPosition: initialPosition, + child: picker, + ); + } + + Future _showTimePicker() async { + final picker = VoicesTimePicker( + onTap: (value) { + _removeOverlay(); + _timeController.value = value; + }, + selectedTime: _timeController.value, + timeZone: widget.timeZone, + ); + + final initialPosition = _getRenderBoxOffset(_timeFieldKey); + + _showOverlay( + initialPosition: initialPosition, + child: picker, + ); + } + + void _handleDateTimeChanged() { + final dateTime = _effectiveController.value; + + final parts = dateTime != null ? _convertDateTimeToParts(dateTime) : null; + + if (_dateController.value != parts?.date) { + _dateController.value = parts?.date; + } + + if (_timeController.value != parts?.time) { + _timeController.value = parts?.time; + } + } + + void _handleDateChanged() => _syncControllers(); + + void _handleTimeChanged() => _syncControllers(); + + void _syncControllers() { + final date = _dateController.value; + final time = _timeController.value; + + final dateTime = date != null && time != null + ? DateTime(date.year, date.month, date.day, time.hour, time.minute) + : null; + + if (_effectiveController.value != dateTime) { + _effectiveController.value = dateTime; + } + } + + DateTimeParts? _convertDateTimeToParts(DateTime value) { + final date = DateTime(value.year, value.month, value.day); + final time = TimeOfDay(hour: value.hour, minute: value.minute); + + return (date: date, time: time); + } + + void _showOverlay({ + Offset initialPosition = Offset.zero, + required Widget child, + }) { + FocusScope.of(context).unfocus(); + + if (_isOverlayOpen) { + _removeOverlay(); + } + + setState(() { + _isOverlayOpen = true; + }); + + final overlay = Overlay.of(context, rootOverlay: true); + final scrollPosition = Scrollable.maybeOf(context)?.position; + + _overlayEntry = OverlayEntry( + builder: (context) => Stack( + children: [ + Positioned.fill( + child: MouseRegion( + opaque: false, + hitTestBehavior: HitTestBehavior.translucent, + child: GestureDetector( + onTapDown: (_) => _removeOverlay(), + behavior: HitTestBehavior.translucent, + excludeFromSemantics: true, + onPanUpdate: null, + onPanDown: null, + onPanCancel: null, + onPanEnd: null, + onPanStart: null, + child: Container( + color: Colors.transparent, + ), + ), + ), + ), + Positioned( + top: initialPosition.dy + 50 - (scrollPosition?.pixels ?? 0), + left: initialPosition.dx, + child: child, + ), + ], + ), + ); + + if (scrollPosition != null) { + void listener() { + if (_overlayEntry != null) { + _overlayEntry?.markNeedsBuild(); + } + } + + scrollPosition.addListener(listener); + _scrollListener = listener; + } + + overlay.insert(_overlayEntry!); + } + + void _removeOverlay() { + if (_overlayEntry != null) { + setState(() { + _isOverlayOpen = false; + }); + + final scrollPosition = Scrollable.maybeOf(context)?.position; + if (scrollPosition != null && _scrollListener != null) { + scrollPosition.removeListener(_scrollListener!); + } + + _overlayEntry?.remove(); + _overlayEntry = null; + _scrollListener = null; + } + } + + Offset _getRenderBoxOffset(GlobalKey key) { + final renderObject = key.currentContext?.findRenderObject(); + if (renderObject is! RenderBox) { + return Offset.zero; + } + + return renderObject.localToGlobal(Offset.zero); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_text_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_text_field.dart new file mode 100644 index 00000000000..934bad31ece --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_text_field.dart @@ -0,0 +1,92 @@ +import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:flutter/material.dart'; + +class VoicesDateTimeTextField extends StatelessWidget { + final TextEditingController? controller; + final ValueChanged? onChanged; + final VoicesTextFieldValidator? validator; + final String hintText; + final ValueChanged? onFieldSubmitted; + final Widget suffixIcon; + final bool dimBorder; + final BorderRadius borderRadius; + final AutovalidateMode? autovalidateMode; + + const VoicesDateTimeTextField({ + super.key, + this.controller, + this.onChanged, + this.validator, + required this.hintText, + required this.onFieldSubmitted, + required this.suffixIcon, + this.dimBorder = false, + this.borderRadius = const BorderRadius.all(Radius.circular(16)), + this.autovalidateMode = AutovalidateMode.onUserInteraction, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + + final onFieldSubmitted = this.onFieldSubmitted; + + final borderSide = dimBorder + ? BorderSide( + color: theme.colors.outlineBorderVariant!, + width: 0.75, + ) + : BorderSide( + color: theme.primaryColor, + width: 2, + ); + + return VoicesTextField( + controller: controller, + onChanged: onChanged, + validator: validator, + decoration: VoicesTextFieldDecoration( + suffixIcon: ExcludeFocus(child: suffixIcon), + fillColor: theme.colors.elevationsOnSurfaceNeutralLv1Grey, + filled: true, + enabledBorder: OutlineInputBorder( + borderSide: borderSide, + borderRadius: borderRadius, + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: theme.primaryColor, + width: 2, + ), + borderRadius: borderRadius, + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + width: 2, + ), + borderRadius: borderRadius, + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + width: 2, + ), + borderRadius: borderRadius, + ), + hintText: hintText, + hintStyle: textTheme.bodyLarge?.copyWith( + color: theme.colors.textDisabled, + ), + errorStyle: textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + errorMaxLines: 3, + ), + onFieldSubmitted: onFieldSubmitted, + autovalidateMode: autovalidateMode, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart index 8eee1092f50..40ff880a4d0 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart @@ -468,7 +468,8 @@ class VoicesTextFieldValidationResult with EquatableMixin { /// The validation can be either a success, a warning or an error. final VoicesTextFieldStatus status; - /// The error message to be used in case of a [warning] or an [error]. + /// The error message to be used in case of a [VoicesTextFieldStatus.warning] + /// or an [VoicesTextFieldStatus.error]. final String? errorMessage; const VoicesTextFieldValidationResult({ @@ -481,25 +482,23 @@ class VoicesTextFieldValidationResult with EquatableMixin { 'errorMessage can be only used for warning or error status', ); - @override - List get props => [status, errorMessage]; - /// Returns a successful validation result. /// /// The method was designed to be used as /// [VoicesTextField.validator] param: /// /// ``` - /// validator: VoicesTextFieldValidationResult.success, + /// validator: (value) { + /// return const VoicesTextFieldValidationResult.success(); + /// }, /// ``` /// /// in cases where the text field state is known in advance /// and dynamic validation is not needed. - static VoicesTextFieldValidator success() { - return (_) => const VoicesTextFieldValidationResult( + const VoicesTextFieldValidationResult.success() + : this( status: VoicesTextFieldStatus.success, ); - } /// Returns a warning validation result. /// @@ -507,17 +506,18 @@ class VoicesTextFieldValidationResult with EquatableMixin { /// [VoicesTextField.validator] param: /// /// ``` - /// validator: VoicesTextFieldValidationResult.warning, + /// validator: (value) { + /// return const VoicesTextFieldValidationResult.warning(); + /// }, /// ``` /// /// in cases where the text field state is known in advance /// and dynamic validation is not needed. - static VoicesTextFieldValidator warning([String? message]) { - return (_) => VoicesTextFieldValidationResult( + const VoicesTextFieldValidationResult.warning([String? message]) + : this( status: VoicesTextFieldStatus.warning, errorMessage: message, ); - } /// Returns an error validation result. /// @@ -525,17 +525,21 @@ class VoicesTextFieldValidationResult with EquatableMixin { /// [VoicesTextField.validator] param: /// /// ``` - /// validator: VoicesTextFieldValidationResult.error, + /// validator: (value) { + /// return const VoicesTextFieldValidationResult.error(); + /// }, /// ``` /// /// in cases where the text field state is known in advance /// and dynamic validation is not needed. - static VoicesTextFieldValidator error([String? message]) { - return (_) => VoicesTextFieldValidationResult( + const VoicesTextFieldValidationResult.error([String? message]) + : this( status: VoicesTextFieldStatus.error, errorMessage: message, ); - } + + @override + List get props => [status, errorMessage]; } /// Defines the appearance of the [VoicesTextField]. diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart new file mode 100644 index 00000000000..408aef341fc --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart @@ -0,0 +1,160 @@ +import 'package:catalyst_voices/common/ext/time_of_day_ext.dart'; +import 'package:catalyst_voices/widgets/buttons/voices_icon_button.dart'; +import 'package:catalyst_voices/widgets/text_field/voices_date_time_text_field.dart'; +import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:flutter/material.dart'; + +final class VoicesTimeFieldController extends ValueNotifier { + VoicesTimeFieldController([super._value]); +} + +class VoicesTimeField extends StatefulWidget { + final VoicesTimeFieldController? controller; + final ValueChanged? onChanged; + final ValueChanged? onFieldSubmitted; + final VoidCallback? onClockTap; + final bool dimBorder; + final BorderRadius borderRadius; + + const VoicesTimeField({ + super.key, + this.controller, + this.onChanged, + required this.onFieldSubmitted, + this.onClockTap, + this.dimBorder = false, + this.borderRadius = const BorderRadius.all(Radius.circular(16)), + }); + + @override + State createState() => _VoicesTimeFieldState(); +} + +class _VoicesTimeFieldState extends State { + late final TextEditingController _textEditingController; + + VoicesTimeFieldController? _controller; + + VoicesTimeFieldController get _effectiveController { + return widget.controller ?? (_controller ??= VoicesTimeFieldController()); + } + + String get _pattern => 'HH:MM'; + + @override + void initState() { + final initialTime = _effectiveController.value; + final initialText = _convertTimeToText(initialTime); + + _textEditingController = TextEditingController(text: initialText); + _textEditingController.addListener(_handleTextChanged); + + _effectiveController.addListener(_handleDateChanged); + + super.initState(); + } + + @override + void didUpdateWidget(covariant VoicesTimeField oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.controller != oldWidget.controller) { + (oldWidget.controller ?? _controller)?.removeListener(_handleDateChanged); + (widget.controller ?? _controller)?.addListener(_handleDateChanged); + + final time = _effectiveController.value; + _textEditingController.text = _convertTimeToText(time); + } + } + + @override + void dispose() { + _textEditingController.dispose(); + + _controller?.dispose(); + _controller = null; + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final onChanged = widget.onChanged; + final onFieldSubmitted = widget.onFieldSubmitted; + + return VoicesDateTimeTextField( + controller: _textEditingController, + onChanged: onChanged != null + ? (value) => onChanged(_convertTextToTime(value)) + : null, + validator: _validate, + hintText: _pattern.toUpperCase(), + onFieldSubmitted: onFieldSubmitted != null + ? (value) => onFieldSubmitted(_convertTextToTime(value)) + : null, + suffixIcon: ExcludeFocus( + child: VoicesIconButton( + onTap: widget.onClockTap, + child: VoicesAssets.icons.clock.buildIcon(), + ), + ), + dimBorder: widget.dimBorder, + borderRadius: widget.borderRadius, + ); + } + + void _handleTextChanged() { + final text = _textEditingController.text; + final time = _convertTextToTime(text); + if (_effectiveController.value != time) { + _effectiveController.value = time; + } + } + + void _handleDateChanged() { + final time = _effectiveController.value; + final text = _convertTimeToText(time); + if (_textEditingController.text != text) { + _textEditingController.text = text; + } + } + + String _convertTimeToText(TimeOfDay? value) { + return value?.formatted ?? ''; + } + + TimeOfDay? _convertTextToTime(String value) { + if (value.isEmpty) return null; + if (_validate(value).status != VoicesTextFieldStatus.success) { + return null; + } + + try { + final parts = value.split(':'); + final rawHours = parts[0]; + final rawMinutes = parts[1]; + + final hour = int.parse(rawHours); + final minute = int.parse(rawMinutes); + + return TimeOfDay(hour: hour, minute: minute); + } catch (e) { + return null; + } + } + + VoicesTextFieldValidationResult _validate(String value) { + if (value.isEmpty) { + return const VoicesTextFieldValidationResult.success(); + } + final l10n = context.l10n; + + final pattern = RegExp(r'^(0?[0-9]|1[0-9]|2[0-3]):([0-5][0-9])$'); + if (!pattern.hasMatch(value)) { + return VoicesTextFieldValidationResult.error('${l10n.format}: $_pattern'); + } + + return const VoicesTextFieldValidationResult.success(); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/widgets.dart b/catalyst_voices/apps/voices/lib/widgets/widgets.dart index a43e5437a5a..ce1a723e087 100644 --- a/catalyst_voices/apps/voices/lib/widgets/widgets.dart +++ b/catalyst_voices/apps/voices/lib/widgets/widgets.dart @@ -69,6 +69,7 @@ export 'separators/voices_text_divider.dart'; export 'separators/voices_vertical_divider.dart'; export 'text_field/seed_phrase_field.dart'; export 'text_field/voices_autocomplete.dart'; +export 'text_field/voices_date_time_field.dart'; export 'text_field/voices_email_text_field.dart'; export 'text_field/voices_password_text_field.dart'; export 'text_field/voices_text_field.dart'; diff --git a/catalyst_voices/apps/voices/test/widgets/text_field/voices_text_field_test.dart b/catalyst_voices/apps/voices/test/widgets/text_field/voices_text_field_test.dart index 7afb9e3c309..11956edf72b 100644 --- a/catalyst_voices/apps/voices/test/widgets/text_field/voices_text_field_test.dart +++ b/catalyst_voices/apps/voices/test/widgets/text_field/voices_text_field_test.dart @@ -106,7 +106,9 @@ void main() { await tester.pumpWidget( _MaterialApp( child: VoicesTextField( - validator: VoicesTextFieldValidationResult.success(), + validator: (value) { + return const VoicesTextFieldValidationResult.success(); + }, onFieldSubmitted: (value) {}, ), ), diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/catalyst_voices_view_models.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/catalyst_voices_view_models.dart index 6c58d2a7af7..2b86a26c535 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/catalyst_voices_view_models.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/catalyst_voices_view_models.dart @@ -1,7 +1,6 @@ export 'authentication/authentication.dart'; export 'campaign/campaign_category.dart'; export 'campaign/campaign_section.dart'; -export 'date_picker/time_slot.dart'; export 'exception/localized_exception.dart'; export 'exception/localized_unknown_exception.dart'; export 'menu/menu_item.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/date_picker/time_slot.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/date_picker/time_slot.dart deleted file mode 100644 index 0c02c5fe6bc..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/date_picker/time_slot.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:equatable/equatable.dart'; - -class TimeSlot extends Equatable { - final int hour; - final int minute; - - const TimeSlot({required this.hour, required this.minute}); - - String get formattedTime => - '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}'; - - DateTime get dateTime => DateTime(0, 0, 0, hour, minute); - - @override - List get props => [hour, minute]; -} diff --git a/catalyst_voices/utilities/uikit_example/lib/examples/voices_text_field_example.dart b/catalyst_voices/utilities/uikit_example/lib/examples/voices_text_field_example.dart index 059f16ab035..7b97322ed0b 100644 --- a/catalyst_voices/utilities/uikit_example/lib/examples/voices_text_field_example.dart +++ b/catalyst_voices/utilities/uikit_example/lib/examples/voices_text_field_example.dart @@ -85,7 +85,9 @@ class _VoicesTextFieldExampleState extends State { hintText: 'Hint text', ), maxLength: 200, - validator: VoicesTextFieldValidationResult.success(), + validator: (value) { + return const VoicesTextFieldValidationResult.success(); + }, onFieldSubmitted: (value) {}, ), ), @@ -99,8 +101,11 @@ class _VoicesTextFieldExampleState extends State { hintText: 'Hint text', ), maxLength: 200, - validator: - VoicesTextFieldValidationResult.warning('Warning message'), + validator: (value) { + return const VoicesTextFieldValidationResult.warning( + 'Warning message', + ); + }, onFieldSubmitted: (value) {}, ), ), @@ -114,8 +119,11 @@ class _VoicesTextFieldExampleState extends State { hintText: 'Hint text', ), maxLength: 200, - validator: - VoicesTextFieldValidationResult.error('Error message'), + validator: (value) { + return const VoicesTextFieldValidationResult.error( + 'Error message', + ); + }, onFieldSubmitted: (value) {}, ), ), @@ -129,7 +137,9 @@ class _VoicesTextFieldExampleState extends State { hintText: 'Hint text', ), maxLength: 200, - validator: VoicesTextFieldValidationResult.success(), + validator: (value) { + return const VoicesTextFieldValidationResult.success(); + }, enabled: false, onFieldSubmitted: (value) {}, ), @@ -144,8 +154,11 @@ class _VoicesTextFieldExampleState extends State { hintText: 'Hint text', ), maxLength: 200, - validator: - VoicesTextFieldValidationResult.warning('Warning message'), + validator: (value) { + return const VoicesTextFieldValidationResult.warning( + 'Warning message', + ); + }, enabled: false, onFieldSubmitted: (value) {}, ), @@ -160,8 +173,11 @@ class _VoicesTextFieldExampleState extends State { hintText: 'Hint text', ), maxLength: 200, - validator: - VoicesTextFieldValidationResult.error('Error message'), + validator: (value) { + return const VoicesTextFieldValidationResult.error( + 'Error message', + ); + }, enabled: false, onFieldSubmitted: (value) {}, ), From 20bba9942974ca8bdf9a2f003776c5613f6fcf08 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Fri, 29 Nov 2024 12:12:17 +0100 Subject: [PATCH 13/16] feat: add date/time input formatting to date and time fields --- .../widgets/text_field/voices_date_field.dart | 13 ++++ .../text_field/voices_date_time_field.dart | 62 ++++++++++++++++--- .../voices_date_time_text_field.dart | 6 +- .../widgets/text_field/voices_time_field.dart | 17 ++++- catalyst_voices/apps/voices/pubspec.yaml | 1 + 5 files changed, 89 insertions(+), 10 deletions(-) diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart index 1e02ad22b86..6c3c8b19ea3 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart @@ -4,6 +4,7 @@ import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:flutter/material.dart'; +import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; final class VoicesDateFieldController extends ValueNotifier { VoicesDateFieldController([super._value]); @@ -41,6 +42,7 @@ class _VoicesDateFieldState extends State { } String get _pattern => 'dd/MM/yyyy'; + late MaskTextInputFormatter dateFormatter; @override void initState() { @@ -52,6 +54,16 @@ class _VoicesDateFieldState extends State { _effectiveController.addListener(_handleDateChanged); + dateFormatter = MaskTextInputFormatter( + mask: _pattern, + filter: { + 'd': RegExp('[0-9]'), + 'M': RegExp('[0-9]'), + 'y': RegExp('[0-9]'), + }, + type: MaskAutoCompletionType.eager, + ); + super.initState(); } @@ -100,6 +112,7 @@ class _VoicesDateFieldState extends State { ), borderRadius: widget.borderRadius, dimBorder: widget.dimBorder, + inputFormatters: [dateFormatter], ); } diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_field.dart index 3497316289c..06d387f5901 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_field.dart @@ -6,6 +6,8 @@ import 'package:flutter/material.dart'; typedef DateTimeParts = ({DateTime date, TimeOfDay time}); +enum PickerType { date, time } + final class VoicesDateTimeFieldController extends ValueNotifier { VoicesDateTimeFieldController([super._value]); } @@ -43,8 +45,10 @@ class _VoicesDateTimeFieldState extends State { } OverlayEntry? _overlayEntry; + PickerType? _pickerType; VoidCallback? _scrollListener; - bool _isOverlayOpen = false; + bool _isDateOverlayOpen = false; + bool _isTimeOverlayOpen = false; @override void initState() { @@ -94,24 +98,25 @@ class _VoicesDateTimeFieldState extends State { borderRadius: const BorderRadius.horizontal( left: Radius.circular(16), ), - dimBorder: _isOverlayOpen, + dimBorder: _isDateOverlayOpen, onCalendarTap: _showDatePicker, onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(), ), ), ConstrainedBox( - constraints: const BoxConstraints.tightFor(width: 170), + constraints: const BoxConstraints.tightFor(width: 200), child: VoicesTimeField( key: _timeFieldKey, controller: _timeController, borderRadius: const BorderRadius.horizontal( right: Radius.circular(16), ), - dimBorder: _isOverlayOpen, + dimBorder: _isTimeOverlayOpen, onClockTap: _showTimePicker, onFieldSubmitted: (_) { widget.onFieldSubmitted?.call(_effectiveController.value); }, + timeZone: widget.timeZone, ), ), ], @@ -129,6 +134,7 @@ class _VoicesDateTimeFieldState extends State { ); final initialPosition = _getRenderBoxOffset(_dateFiledKey); + _pickerType = PickerType.date; _showOverlay( initialPosition: initialPosition, @@ -147,6 +153,7 @@ class _VoicesDateTimeFieldState extends State { ); final initialPosition = _getRenderBoxOffset(_timeFieldKey); + _pickerType = PickerType.time; _showOverlay( initialPosition: initialPosition, @@ -198,12 +205,16 @@ class _VoicesDateTimeFieldState extends State { }) { FocusScope.of(context).unfocus(); - if (_isOverlayOpen) { + if (_pickerType != null) { _removeOverlay(); } setState(() { - _isOverlayOpen = true; + if (_pickerType == PickerType.date) { + _isDateOverlayOpen = true; + } else { + _isTimeOverlayOpen = true; + } }); final overlay = Overlay.of(context, rootOverlay: true); @@ -217,7 +228,19 @@ class _VoicesDateTimeFieldState extends State { opaque: false, hitTestBehavior: HitTestBehavior.translucent, child: GestureDetector( - onTapDown: (_) => _removeOverlay(), + onTapDown: (details) async { + final tapPosition = details.globalPosition; + final dateBox = _getRenderBox(_dateFiledKey); + final timeBox = _getRenderBox(_timeFieldKey); + + if (_isBoxTapped(dateBox, tapPosition)) { + return _handleTap(PickerType.date); + } else if (_isBoxTapped(timeBox, tapPosition)) { + return _handleTap(PickerType.time); + } else { + _removeOverlay(); + } + }, behavior: HitTestBehavior.translucent, excludeFromSemantics: true, onPanUpdate: null, @@ -257,7 +280,11 @@ class _VoicesDateTimeFieldState extends State { void _removeOverlay() { if (_overlayEntry != null) { setState(() { - _isOverlayOpen = false; + if (_pickerType == PickerType.date) { + _isDateOverlayOpen = false; + } else { + _isTimeOverlayOpen = false; + } }); final scrollPosition = Scrollable.maybeOf(context)?.position; @@ -268,6 +295,7 @@ class _VoicesDateTimeFieldState extends State { _overlayEntry?.remove(); _overlayEntry = null; _scrollListener = null; + _pickerType = null; } } @@ -279,4 +307,22 @@ class _VoicesDateTimeFieldState extends State { return renderObject.localToGlobal(Offset.zero); } + + RenderBox? _getRenderBox(GlobalKey key) { + return key.currentContext?.findRenderObject() as RenderBox?; + } + + bool _isBoxTapped(RenderBox? box, Offset tapPosition) { + return box != null && + (box.localToGlobal(Offset.zero) & box.size).contains(tapPosition); + } + + Future _handleTap(PickerType pickerType) async { + _removeOverlay(); + if (pickerType == PickerType.date) { + await _showDatePicker(); + } else { + await _showTimePicker(); + } + } } diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_text_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_text_field.dart index 934bad31ece..b1ad08e0a86 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_text_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_text_field.dart @@ -1,6 +1,7 @@ import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class VoicesDateTimeTextField extends StatelessWidget { final TextEditingController? controller; @@ -11,6 +12,7 @@ class VoicesDateTimeTextField extends StatelessWidget { final Widget suffixIcon; final bool dimBorder; final BorderRadius borderRadius; + final List? inputFormatters; final AutovalidateMode? autovalidateMode; const VoicesDateTimeTextField({ @@ -23,6 +25,7 @@ class VoicesDateTimeTextField extends StatelessWidget { required this.suffixIcon, this.dimBorder = false, this.borderRadius = const BorderRadius.all(Radius.circular(16)), + this.inputFormatters, this.autovalidateMode = AutovalidateMode.onUserInteraction, }); @@ -33,7 +36,7 @@ class VoicesDateTimeTextField extends StatelessWidget { final onFieldSubmitted = this.onFieldSubmitted; - final borderSide = dimBorder + final borderSide = !dimBorder ? BorderSide( color: theme.colors.outlineBorderVariant!, width: 0.75, @@ -86,6 +89,7 @@ class VoicesDateTimeTextField extends StatelessWidget { errorMaxLines: 3, ), onFieldSubmitted: onFieldSubmitted, + inputFormatters: inputFormatters, autovalidateMode: autovalidateMode, ); } diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart index 408aef341fc..0c81964fadb 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart @@ -5,6 +5,7 @@ import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:flutter/material.dart'; +import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; final class VoicesTimeFieldController extends ValueNotifier { VoicesTimeFieldController([super._value]); @@ -17,6 +18,7 @@ class VoicesTimeField extends StatefulWidget { final VoidCallback? onClockTap; final bool dimBorder; final BorderRadius borderRadius; + final String? timeZone; const VoicesTimeField({ super.key, @@ -26,6 +28,7 @@ class VoicesTimeField extends StatefulWidget { this.onClockTap, this.dimBorder = false, this.borderRadius = const BorderRadius.all(Radius.circular(16)), + this.timeZone, }); @override @@ -42,6 +45,8 @@ class _VoicesTimeFieldState extends State { } String get _pattern => 'HH:MM'; + String get timeZone => widget.timeZone ?? ''; + late MaskTextInputFormatter timeFormatter; @override void initState() { @@ -53,6 +58,15 @@ class _VoicesTimeFieldState extends State { _effectiveController.addListener(_handleDateChanged); + timeFormatter = MaskTextInputFormatter( + mask: _pattern, + filter: { + 'H': RegExp('[0-9]'), + 'M': RegExp('[0-9]'), + }, + type: MaskAutoCompletionType.eager, + ); + super.initState(); } @@ -89,7 +103,7 @@ class _VoicesTimeFieldState extends State { ? (value) => onChanged(_convertTextToTime(value)) : null, validator: _validate, - hintText: _pattern.toUpperCase(), + hintText: '${_pattern.toUpperCase()} $timeZone', onFieldSubmitted: onFieldSubmitted != null ? (value) => onFieldSubmitted(_convertTextToTime(value)) : null, @@ -101,6 +115,7 @@ class _VoicesTimeFieldState extends State { ), dimBorder: widget.dimBorder, borderRadius: widget.borderRadius, + inputFormatters: [timeFormatter], ); } diff --git a/catalyst_voices/apps/voices/pubspec.yaml b/catalyst_voices/apps/voices/pubspec.yaml index 27987e9e804..3b00fa0d7e6 100644 --- a/catalyst_voices/apps/voices/pubspec.yaml +++ b/catalyst_voices/apps/voices/pubspec.yaml @@ -54,6 +54,7 @@ dependencies: go_router: ^14.0.2 google_fonts: ^6.2.1 intl: ^0.19.0 + mask_text_input_formatter: ^2.9.0 result_type: ^0.2.0 scrollable_positioned_list: ^0.3.8 sentry_flutter: ^8.8.0 From d4c0d304aa622dec513e1631296ac21a5251bfd2 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler <51096731+LynxLynxx@users.noreply.github.com> Date: Fri, 29 Nov 2024 13:16:25 +0100 Subject: [PATCH 14/16] fix: late final in voices_date_field.dart --- .../apps/voices/lib/widgets/text_field/voices_date_field.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart index 6c3c8b19ea3..7e0cff19cb7 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart @@ -34,6 +34,7 @@ class VoicesDateField extends StatefulWidget { class _VoicesDateFieldState extends State { late final TextEditingController _textEditingController; + late final MaskTextInputFormatter dateFormatter; VoicesDateFieldController? _controller; @@ -42,7 +43,7 @@ class _VoicesDateFieldState extends State { } String get _pattern => 'dd/MM/yyyy'; - late MaskTextInputFormatter dateFormatter; + @override void initState() { From fa68604e1bd8c7ae28cd176ae6472ebaaa4684cb Mon Sep 17 00:00:00 2001 From: Ryszard Schossler <51096731+LynxLynxx@users.noreply.github.com> Date: Fri, 29 Nov 2024 13:18:01 +0100 Subject: [PATCH 15/16] fix: late final in voices_time_field.dart --- .../apps/voices/lib/widgets/text_field/voices_time_field.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart index 0c81964fadb..79cf1e38cee 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart @@ -37,6 +37,7 @@ class VoicesTimeField extends StatefulWidget { class _VoicesTimeFieldState extends State { late final TextEditingController _textEditingController; + late final MaskTextInputFormatter timeFormatter; VoicesTimeFieldController? _controller; @@ -46,7 +47,7 @@ class _VoicesTimeFieldState extends State { String get _pattern => 'HH:MM'; String get timeZone => widget.timeZone ?? ''; - late MaskTextInputFormatter timeFormatter; + @override void initState() { From 447da0cb25e86b214006e69e6c8062ec3937c9f9 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Fri, 29 Nov 2024 15:30:33 +0100 Subject: [PATCH 16/16] fix: formatting --- .../apps/voices/lib/widgets/text_field/voices_date_field.dart | 1 - .../apps/voices/lib/widgets/text_field/voices_time_field.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart index 7e0cff19cb7..441c961a2e9 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart @@ -43,7 +43,6 @@ class _VoicesDateFieldState extends State { } String get _pattern => 'dd/MM/yyyy'; - @override void initState() { diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart index 79cf1e38cee..72ff91f17fb 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart @@ -47,7 +47,6 @@ class _VoicesTimeFieldState extends State { String get _pattern => 'HH:MM'; String get timeZone => widget.timeZone ?? ''; - @override void initState() {