various dialog improvements

This commit is contained in:
Thibault Deckers 2020-09-20 00:36:12 +09:00
parent 4ef0bebc58
commit f9fd937b16
6 changed files with 121 additions and 3 deletions

View file

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

View file

@ -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<CollectionAppBar> with SingleTickerPr
_goToStats();
break;
case CollectionAction.addShortcut:
unawaited(AppShortcutService.pin('Collection', collection.filters));
unawaited(_showShortcutDialog(context));
break;
case CollectionAction.group:
final value = await showDialog<EntryGroupFactor>(
@ -348,6 +349,16 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
}
}
Future<void> _showShortcutDialog(BuildContext context) async {
final name = await showDialog<String>(
context: context,
builder: (context) => AddShortcutDialog(collection.filters),
);
if (name == null || name.isEmpty) return;
unawaited(AppShortcutService.pin(name, collection.filters));
}
void _goToSearch() {
Navigator.push(
context,

View file

@ -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<CollectionFilter> filters;
const AddShortcutDialog(this.filters);
@override
_AddShortcutDialogState createState() => _AddShortcutDialogState();
}
class _AddShortcutDialogState extends State<AddShortcutDialog> {
final TextEditingController _nameController = TextEditingController();
final ValueNotifier<bool> _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<bool>(
valueListenable: _isValidNotifier,
builder: (context, isValid, child) {
return FlatButton(
onPressed: isValid ? () => _submit(context) : null,
child: Text('Add'.toUpperCase()),
);
},
)
],
);
}
Future<void> _validate() async {
final name = _nameController.text ?? '';
_isValidNotifier.value = name.isNotEmpty;
}
void _submit(BuildContext context) => Navigator.pop(context, _nameController.text);
}

View file

@ -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<CreateAlbumDialog> {
final ScrollController _scrollController = ScrollController();
final TextEditingController _nameController = TextEditingController();
final FocusNode _nameFieldFocusNode = FocusNode();
final ValueNotifier<bool> _existsNotifier = ValueNotifier(false);
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
Set<StorageVolume> _allVolumes;
@ -25,11 +28,13 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
_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<CreateAlbumDialog> {
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<CreateAlbumDialog> {
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<CreateAlbumDialog> {
);
}
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);

View file

@ -25,6 +25,7 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
void initState() {
super.initState();
_nameController.text = entry.filenameWithoutExtension ?? entry.sourceTitle;
_validate();
}
@override
@ -38,6 +39,10 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
return AvesDialog(
content: TextField(
controller: _nameController,
decoration: InputDecoration(
labelText: 'New name',
suffixText: entry.extension,
),
autofocus: true,
onChanged: (_) => _validate(),
onSubmitted: (_) => _submit(context),

View file

@ -6,6 +6,7 @@ class AvesDialog extends AlertDialog {
AvesDialog({
String title,
ScrollController scrollController,
List<Widget> scrollableContent,
Widget content,
@required List<Widget> 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(