various dialog improvements
This commit is contained in:
parent
4ef0bebc58
commit
f9fd937b16
6 changed files with 121 additions and 3 deletions
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
74
lib/widgets/common/action_delegates/add_shortcut_dialog.dart
Normal file
74
lib/widgets/common/action_delegates/add_shortcut_dialog.dart
Normal 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);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue