@@ -5,6 +5,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
55import 'package:openfoodfacts/openfoodfacts.dart' ;
66import 'package:smooth_app/generic_lib/bottom_sheets/smooth_bottom_sheet.dart' ;
77import 'package:smooth_app/generic_lib/design_constants.dart' ;
8+ import 'package:smooth_app/generic_lib/duration_constants.dart' ;
89import 'package:smooth_app/generic_lib/widgets/smooth_card.dart' ;
910import 'package:smooth_app/generic_lib/widgets/smooth_snackbar.dart' ;
1011import 'package:smooth_app/helpers/collections_helper.dart' ;
@@ -13,7 +14,10 @@ import 'package:smooth_app/pages/product/explanation_widget.dart';
1314import 'package:smooth_app/pages/product/owner_field_info.dart' ;
1415import 'package:smooth_app/pages/product/simple_input_page_helpers.dart' ;
1516import 'package:smooth_app/pages/product/simple_input_text_field.dart' ;
17+ import 'package:smooth_app/pages/text_field_helper.dart' ;
1618import 'package:smooth_app/resources/app_icons.dart' as icons;
19+ import 'package:smooth_app/themes/smooth_theme.dart' ;
20+ import 'package:smooth_app/themes/smooth_theme_colors.dart' ;
1721import 'package:smooth_app/themes/theme_provider.dart' ;
1822
1923/// Simple input widget: we have a list of terms, we add, we remove.
@@ -271,34 +275,15 @@ class _SimpleInputWidgetState extends State<SimpleInputWidget> {
271275 BuildContext context,
272276 int position,
273277 ) {
274- final AppLocalizations appLocalizations = AppLocalizations .of (context);
275-
276- final String term = _localTerms[position];
277- final Text child = Text (term);
278-
279- return ListTile (
280- leading: widget.helper.reorderable
281- ? ReorderableDelayedDragStartListener (
282- index: position,
283- child: const icons.Menu .hamburger (),
284- )
285- : null ,
286- trailing: Tooltip (
287- message: appLocalizations.edit_product_form_item_remove_item_tooltip,
288- child: InkWell (
289- customBorder: const CircleBorder (),
290- onTap: () => _onRemoveItem (term, child),
291- child: const Padding (
292- padding: EdgeInsets .all (SMALL_SPACE ),
293- child: Icon (Icons .delete),
294- ),
295- ),
296- ),
297- contentPadding: const EdgeInsetsDirectional .only (
298- start: LARGE_SPACE ,
299- ),
300- minTileHeight: 48.0 ,
301- title: child,
278+ return _SimpleInputListItem (
279+ term: _localTerms[position],
280+ reorderable: widget.helper.reorderable,
281+ editable: widget.helper.editable,
282+ position: position,
283+ onChanged: (int position, String term) {
284+ widget.helper.replaceItem (position, term);
285+ },
286+ onRemoveItem: _onRemoveItem,
302287 );
303288 }
304289
@@ -412,3 +397,258 @@ class ExplanationTitleIcon extends StatelessWidget {
412397 );
413398 }
414399}
400+
401+ class _SimpleInputListItem extends StatefulWidget {
402+ const _SimpleInputListItem ({
403+ required this .term,
404+ required this .reorderable,
405+ required this .editable,
406+ required this .position,
407+ required this .onChanged,
408+ required this .onRemoveItem,
409+ });
410+
411+ final String term;
412+ final bool reorderable;
413+ final bool editable;
414+ final int position;
415+ final Function (int position, String term) onChanged;
416+ final Function (String term, Widget child) onRemoveItem;
417+
418+ @override
419+ State <_SimpleInputListItem > createState () => _SimpleInputListItemState ();
420+ }
421+
422+ class _SimpleInputListItemState extends State <_SimpleInputListItem > {
423+ late final TextEditingControllerWithHistory _controller;
424+ late final FocusNode _focusNode;
425+
426+ bool _isEditing = false ;
427+
428+ @override
429+ void initState () {
430+ super .initState ();
431+
432+ if (widget.editable) {
433+ _controller = TextEditingControllerWithHistory (text: widget.term);
434+ _focusNode = FocusNode ()..addListener (_onFocus);
435+ }
436+ }
437+
438+ @override
439+ Widget build (BuildContext context) {
440+ final AppLocalizations appLocalizations = AppLocalizations .of (context);
441+ final SmoothColorsThemeExtension extension =
442+ context.extension < SmoothColorsThemeExtension > ();
443+
444+ Widget child;
445+ if (widget.editable) {
446+ child = _getEditableItem ();
447+ } else {
448+ child = _getItem ();
449+ }
450+
451+ child = ListTile (
452+ leading: widget.reorderable
453+ ? ReorderableDelayedDragStartListener (
454+ index: widget.position,
455+ child: const icons.Menu .hamburger (),
456+ )
457+ : null ,
458+ trailing: Row (
459+ mainAxisSize: MainAxisSize .min,
460+ children: < Widget > [
461+ if (widget.editable) ...< Widget > [
462+ _SimpleInputListItemAction (
463+ tooltip: appLocalizations
464+ .edit_product_form_item_save_edit_item_tooltip,
465+ icon: Icon (
466+ Icons .check_circle_rounded,
467+ color: extension .success,
468+ ),
469+ visible: _isEditing,
470+ onTap: _saveEdit,
471+ ),
472+ _SimpleInputListItemAction (
473+ tooltip: appLocalizations
474+ .edit_product_form_item_cancel_edit_item_tooltip,
475+ icon: Icon (
476+ Icons .cancel,
477+ color: extension .error,
478+ ),
479+ visible: _isEditing,
480+ onTap: _cancelEdit,
481+ )
482+ ],
483+ _SimpleInputListItemAction (
484+ tooltip:
485+ appLocalizations.edit_product_form_item_remove_item_tooltip,
486+ icon: const Icon (Icons .delete),
487+ onTap: () => widget.onRemoveItem (widget.term, child),
488+ visible: ! _isEditing,
489+ ),
490+ ],
491+ ),
492+ contentPadding: const EdgeInsetsDirectional .only (
493+ start: LARGE_SPACE ,
494+ ),
495+ minTileHeight: 48.0 ,
496+ title: child,
497+ );
498+
499+ if (widget.editable) {
500+ return ClipRRect (child: child);
501+ } else {
502+ return child;
503+ }
504+ }
505+
506+ Widget _getItem () {
507+ return Text (widget.term);
508+ }
509+
510+ Widget _getEditableItem () {
511+ return TextField (
512+ controller: _controller,
513+ focusNode: _focusNode,
514+ style: TextTheme .of (context).bodyLarge,
515+ decoration: const InputDecoration (
516+ isDense: true ,
517+ border: InputBorder .none,
518+ contentPadding: EdgeInsets .zero,
519+ ),
520+ maxLines: 1 ,
521+ onEditingComplete: _saveEdit,
522+ );
523+ }
524+
525+ void _saveEdit () {
526+ if (_controller.text.trim ().isEmpty) {
527+ widget.onRemoveItem (widget.term, _getItem ());
528+ } else {
529+ widget.onChanged (widget.position, _controller.text);
530+ }
531+
532+ setState (() => _isEditing = false );
533+ _focusNode.unfocus ();
534+ }
535+
536+ void _cancelEdit () {
537+ _controller.resetToInitialValue ();
538+ _focusNode.unfocus ();
539+ setState (() => _isEditing = false );
540+ }
541+
542+ void _onFocus () {
543+ if (_focusNode.hasFocus && ! _isEditing) {
544+ setState (() => _isEditing = true );
545+ } else if (! _focusNode.hasFocus && _isEditing) {
546+ _cancelEdit ();
547+ }
548+ }
549+
550+ @override
551+ void dispose () {
552+ if (widget.editable) {
553+ _controller.dispose ();
554+ }
555+ super .dispose ();
556+ }
557+ }
558+
559+ class _SimpleInputListItemAction extends StatefulWidget {
560+ const _SimpleInputListItemAction ({
561+ required this .onTap,
562+ required this .icon,
563+ required this .tooltip,
564+ this .visible = true ,
565+ });
566+
567+ final VoidCallback onTap;
568+ final Widget icon;
569+ final String tooltip;
570+ final bool visible;
571+
572+ @override
573+ State <_SimpleInputListItemAction > createState () =>
574+ _SimpleInputListItemActionState ();
575+ }
576+
577+ class _SimpleInputListItemActionState extends State <_SimpleInputListItemAction >
578+ with SingleTickerProviderStateMixin {
579+ late AnimationController _controller;
580+ late Animation <double > _opacityAnimation;
581+ late Animation <double > _sizeAnimation;
582+
583+ @override
584+ void initState () {
585+ super .initState ();
586+
587+ _controller = AnimationController (
588+ vsync: this ,
589+ duration: SmoothAnimationsDuration .medium,
590+ reverseDuration: SmoothAnimationsDuration .short,
591+ )..addListener (() => setState (() {}));
592+
593+ if (widget.visible) {
594+ _controller.forward (from: 1.0 );
595+ }
596+
597+ _opacityAnimation = Tween <double >(begin: 0.0 , end: 1.0 ).animate (
598+ CurvedAnimation (
599+ parent: _controller,
600+ curve: Curves .easeInOut,
601+ ),
602+ );
603+
604+ _sizeAnimation = Tween <double >(begin: 0.0 , end: 1.0 ).animate (
605+ CurvedAnimation (
606+ parent: _controller,
607+ curve: const Interval (0.2 , 0.8 , curve: Curves .easeInOut),
608+ ),
609+ );
610+ }
611+
612+ @override
613+ void didUpdateWidget (_SimpleInputListItemAction oldWidget) {
614+ super .didUpdateWidget (oldWidget);
615+
616+ if (widget.visible != oldWidget.visible) {
617+ if (widget.visible) {
618+ _controller.forward (from: 0.0 );
619+ } else {
620+ _controller.reverse (from: 1.0 );
621+ }
622+ }
623+ }
624+
625+ @override
626+ Widget build (BuildContext context) {
627+ return Offstage (
628+ offstage: _controller.value == 0.0 ,
629+ child: Opacity (
630+ opacity: _opacityAnimation.value,
631+ child: SizedBox (
632+ width: _sizeAnimation.value * (SMALL_SPACE * 2 + 24.0 ),
633+ child: Tooltip (
634+ message: widget.tooltip,
635+ child: InkWell (
636+ customBorder: const CircleBorder (),
637+ onTap: widget.onTap,
638+ child: Padding (
639+ padding: const EdgeInsets .all (SMALL_SPACE ),
640+ child: widget.icon,
641+ ),
642+ ),
643+ ),
644+ ),
645+ ),
646+ );
647+ }
648+
649+ @override
650+ void dispose () {
651+ _controller.dispose ();
652+ super .dispose ();
653+ }
654+ }
0 commit comments