From f9fd937b161cfb3115e78b836af61572dbcc440f Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 20 Sep 2020 00:36:12 +0900 Subject: [PATCH] various dialog improvements --- lib/utils/durations.dart | 2 + lib/widgets/collection/app_bar.dart | 13 +++- .../action_delegates/add_shortcut_dialog.dart | 74 +++++++++++++++++++ .../action_delegates/create_album_dialog.dart | 26 ++++++- .../action_delegates/rename_entry_dialog.dart | 5 ++ lib/widgets/common/aves_dialog.dart | 4 +- 6 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 lib/widgets/common/action_delegates/add_shortcut_dialog.dart diff --git a/lib/utils/durations.dart b/lib/utils/durations.dart index f4d7c2a26..a5709e238 100644 --- a/lib/utils/durations.dart +++ b/lib/utils/durations.dart @@ -8,6 +8,7 @@ class Durations { static const sweepingAnimation = Duration(milliseconds: 650); static const popupMenuAnimation = Duration(milliseconds: 300); // ref _PopupMenuRoute._kMenuDuration static const staggeredAnimation = Duration(milliseconds: 375); + static const dialogFieldReachAnimation = Duration(milliseconds: 300); // collection animations static const appBarTitleAnimation = Duration(milliseconds: 300); @@ -32,4 +33,5 @@ class Durations { static const videoProgressTimerInterval = Duration(milliseconds: 300); static Duration staggeredAnimationDelay = Durations.staggeredAnimation ~/ 6 * timeDilation; static const doubleBackTimerDelay = Duration(milliseconds: 1000); + static const softKeyboardDisplayDelay = Duration(milliseconds: 300); } diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index f83b8dc36..e9b2fe665 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -10,6 +10,7 @@ import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/collection/collection_actions.dart'; import 'package:aves/widgets/collection/filter_bar.dart'; import 'package:aves/widgets/collection/search/search_delegate.dart'; +import 'package:aves/widgets/common/action_delegates/add_shortcut_dialog.dart'; import 'package:aves/widgets/common/action_delegates/selection_action_delegate.dart'; import 'package:aves/widgets/common/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar_title.dart'; @@ -306,7 +307,7 @@ class _CollectionAppBarState extends State with SingleTickerPr _goToStats(); break; case CollectionAction.addShortcut: - unawaited(AppShortcutService.pin('Collection', collection.filters)); + unawaited(_showShortcutDialog(context)); break; case CollectionAction.group: final value = await showDialog( @@ -348,6 +349,16 @@ class _CollectionAppBarState extends State with SingleTickerPr } } + Future _showShortcutDialog(BuildContext context) async { + final name = await showDialog( + context: context, + builder: (context) => AddShortcutDialog(collection.filters), + ); + if (name == null || name.isEmpty) return; + + unawaited(AppShortcutService.pin(name, collection.filters)); + } + void _goToSearch() { Navigator.push( context, diff --git a/lib/widgets/common/action_delegates/add_shortcut_dialog.dart b/lib/widgets/common/action_delegates/add_shortcut_dialog.dart new file mode 100644 index 000000000..1b353f519 --- /dev/null +++ b/lib/widgets/common/action_delegates/add_shortcut_dialog.dart @@ -0,0 +1,74 @@ +import 'package:aves/model/filters/filters.dart'; +import 'package:flutter/material.dart'; + +import '../aves_dialog.dart'; + +class AddShortcutDialog extends StatefulWidget { + final Set filters; + + const AddShortcutDialog(this.filters); + + @override + _AddShortcutDialogState createState() => _AddShortcutDialogState(); +} + +class _AddShortcutDialogState extends State { + final TextEditingController _nameController = TextEditingController(); + final ValueNotifier _isValidNotifier = ValueNotifier(false); + + @override + void initState() { + super.initState(); + final filters = List.from(widget.filters)..sort(); + if (filters.isEmpty) { + _nameController.text = 'Collection'; + } else { + _nameController.text = filters.first.label; + } + _validate(); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AvesDialog( + content: TextField( + controller: _nameController, + decoration: InputDecoration( + labelText: 'Shortcut label', + ), + autofocus: true, + maxLength: 10, + onChanged: (_) => _validate(), + onSubmitted: (_) => _submit(context), + ), + actions: [ + FlatButton( + onPressed: () => Navigator.pop(context), + child: Text('Cancel'.toUpperCase()), + ), + ValueListenableBuilder( + valueListenable: _isValidNotifier, + builder: (context, isValid, child) { + return FlatButton( + onPressed: isValid ? () => _submit(context) : null, + child: Text('Add'.toUpperCase()), + ); + }, + ) + ], + ); + } + + Future _validate() async { + final name = _nameController.text ?? ''; + _isValidNotifier.value = name.isNotEmpty; + } + + void _submit(BuildContext context) => Navigator.pop(context, _nameController.text); +} diff --git a/lib/widgets/common/action_delegates/create_album_dialog.dart b/lib/widgets/common/action_delegates/create_album_dialog.dart index 87e943911..8f2ad8ee5 100644 --- a/lib/widgets/common/action_delegates/create_album_dialog.dart +++ b/lib/widgets/common/action_delegates/create_album_dialog.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/utils/durations.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:path/path.dart'; @@ -13,7 +14,9 @@ class CreateAlbumDialog extends StatefulWidget { } class _CreateAlbumDialogState extends State { + final ScrollController _scrollController = ScrollController(); final TextEditingController _nameController = TextEditingController(); + final FocusNode _nameFieldFocusNode = FocusNode(); final ValueNotifier _existsNotifier = ValueNotifier(false); final ValueNotifier _isValidNotifier = ValueNotifier(false); Set _allVolumes; @@ -25,11 +28,13 @@ class _CreateAlbumDialogState extends State { _allVolumes = androidFileUtils.storageVolumes; _primaryVolume = _allVolumes.firstWhere((volume) => volume.isPrimary, orElse: () => _allVolumes.first); _selectedVolume = _primaryVolume; + _nameFieldFocusNode.addListener(_onFocus); } @override void dispose() { _nameController.dispose(); + _nameFieldFocusNode.removeListener(_onFocus); super.dispose(); } @@ -37,6 +42,7 @@ class _CreateAlbumDialogState extends State { Widget build(BuildContext context) { return AvesDialog( title: 'New Album', + scrollController: _scrollController, scrollableContent: [ if (_allVolumes.length > 1) ...[ Padding( @@ -73,9 +79,10 @@ class _CreateAlbumDialogState extends State { builder: (context, exists, child) { return TextField( controller: _nameController, + focusNode: _nameFieldFocusNode, decoration: InputDecoration( + labelText: 'Album name', helperText: exists ? 'Album already exists' : '', - hintText: 'Album name', ), autofocus: _allVolumes.length == 1, onChanged: (_) => _validate(), @@ -102,6 +109,23 @@ class _CreateAlbumDialogState extends State { ); } + void _onFocus() async { + // when the field gets focus, we wait for the soft keyboard to appear + // then scroll to the bottom to make sure the field is in view + if (_nameFieldFocusNode.hasFocus) { + await Future.delayed(Durations.softKeyboardDisplayDelay); + _scrollToBottom(); + } + } + + void _scrollToBottom() { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: Durations.dialogFieldReachAnimation, + curve: Curves.easeInOut, + ); + } + String _buildAlbumPath(String name) { if (name == null || name.isEmpty) return ''; return join(_selectedVolume.path, 'Pictures', name); diff --git a/lib/widgets/common/action_delegates/rename_entry_dialog.dart b/lib/widgets/common/action_delegates/rename_entry_dialog.dart index 766566caf..b42f14180 100644 --- a/lib/widgets/common/action_delegates/rename_entry_dialog.dart +++ b/lib/widgets/common/action_delegates/rename_entry_dialog.dart @@ -25,6 +25,7 @@ class _RenameEntryDialogState extends State { void initState() { super.initState(); _nameController.text = entry.filenameWithoutExtension ?? entry.sourceTitle; + _validate(); } @override @@ -38,6 +39,10 @@ class _RenameEntryDialogState extends State { return AvesDialog( content: TextField( controller: _nameController, + decoration: InputDecoration( + labelText: 'New name', + suffixText: entry.extension, + ), autofocus: true, onChanged: (_) => _validate(), onSubmitted: (_) => _submit(context), diff --git a/lib/widgets/common/aves_dialog.dart b/lib/widgets/common/aves_dialog.dart index e487b9b78..aa3829d67 100644 --- a/lib/widgets/common/aves_dialog.dart +++ b/lib/widgets/common/aves_dialog.dart @@ -6,6 +6,7 @@ class AvesDialog extends AlertDialog { AvesDialog({ String title, + ScrollController scrollController, List scrollableContent, Widget content, @required List actions, @@ -31,6 +32,7 @@ class AvesDialog extends AlertDialog { ), ), child: ListView( + controller: scrollController ?? ScrollController(), shrinkWrap: true, children: scrollableContent, ), @@ -38,7 +40,7 @@ class AvesDialog extends AlertDialog { ), ) : content, - contentPadding: scrollableContent != null ? EdgeInsets.zero : EdgeInsets.fromLTRB(24, 20, 24, 24), + contentPadding: scrollableContent != null ? EdgeInsets.zero : EdgeInsets.fromLTRB(24, 20, 24, 0), actions: actions, actionsPadding: EdgeInsets.symmetric(horizontal: 8), shape: RoundedRectangleBorder(