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 sweepingAnimation = Duration(milliseconds: 650);
|
||||||
static const popupMenuAnimation = Duration(milliseconds: 300); // ref _PopupMenuRoute._kMenuDuration
|
static const popupMenuAnimation = Duration(milliseconds: 300); // ref _PopupMenuRoute._kMenuDuration
|
||||||
static const staggeredAnimation = Duration(milliseconds: 375);
|
static const staggeredAnimation = Duration(milliseconds: 375);
|
||||||
|
static const dialogFieldReachAnimation = Duration(milliseconds: 300);
|
||||||
|
|
||||||
// collection animations
|
// collection animations
|
||||||
static const appBarTitleAnimation = Duration(milliseconds: 300);
|
static const appBarTitleAnimation = Duration(milliseconds: 300);
|
||||||
|
@ -32,4 +33,5 @@ class Durations {
|
||||||
static const videoProgressTimerInterval = Duration(milliseconds: 300);
|
static const videoProgressTimerInterval = Duration(milliseconds: 300);
|
||||||
static Duration staggeredAnimationDelay = Durations.staggeredAnimation ~/ 6 * timeDilation;
|
static Duration staggeredAnimationDelay = Durations.staggeredAnimation ~/ 6 * timeDilation;
|
||||||
static const doubleBackTimerDelay = Duration(milliseconds: 1000);
|
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/collection_actions.dart';
|
||||||
import 'package:aves/widgets/collection/filter_bar.dart';
|
import 'package:aves/widgets/collection/filter_bar.dart';
|
||||||
import 'package:aves/widgets/collection/search/search_delegate.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/action_delegates/selection_action_delegate.dart';
|
||||||
import 'package:aves/widgets/common/app_bar_subtitle.dart';
|
import 'package:aves/widgets/common/app_bar_subtitle.dart';
|
||||||
import 'package:aves/widgets/common/app_bar_title.dart';
|
import 'package:aves/widgets/common/app_bar_title.dart';
|
||||||
|
@ -306,7 +307,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
_goToStats();
|
_goToStats();
|
||||||
break;
|
break;
|
||||||
case CollectionAction.addShortcut:
|
case CollectionAction.addShortcut:
|
||||||
unawaited(AppShortcutService.pin('Collection', collection.filters));
|
unawaited(_showShortcutDialog(context));
|
||||||
break;
|
break;
|
||||||
case CollectionAction.group:
|
case CollectionAction.group:
|
||||||
final value = await showDialog<EntryGroupFactor>(
|
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() {
|
void _goToSearch() {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
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 'dart:io';
|
||||||
|
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
import 'package:aves/utils/durations.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
@ -13,7 +14,9 @@ class CreateAlbumDialog extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
final TextEditingController _nameController = TextEditingController();
|
final TextEditingController _nameController = TextEditingController();
|
||||||
|
final FocusNode _nameFieldFocusNode = FocusNode();
|
||||||
final ValueNotifier<bool> _existsNotifier = ValueNotifier(false);
|
final ValueNotifier<bool> _existsNotifier = ValueNotifier(false);
|
||||||
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
||||||
Set<StorageVolume> _allVolumes;
|
Set<StorageVolume> _allVolumes;
|
||||||
|
@ -25,11 +28,13 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
||||||
_allVolumes = androidFileUtils.storageVolumes;
|
_allVolumes = androidFileUtils.storageVolumes;
|
||||||
_primaryVolume = _allVolumes.firstWhere((volume) => volume.isPrimary, orElse: () => _allVolumes.first);
|
_primaryVolume = _allVolumes.firstWhere((volume) => volume.isPrimary, orElse: () => _allVolumes.first);
|
||||||
_selectedVolume = _primaryVolume;
|
_selectedVolume = _primaryVolume;
|
||||||
|
_nameFieldFocusNode.addListener(_onFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_nameController.dispose();
|
_nameController.dispose();
|
||||||
|
_nameFieldFocusNode.removeListener(_onFocus);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +42,7 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AvesDialog(
|
return AvesDialog(
|
||||||
title: 'New Album',
|
title: 'New Album',
|
||||||
|
scrollController: _scrollController,
|
||||||
scrollableContent: [
|
scrollableContent: [
|
||||||
if (_allVolumes.length > 1) ...[
|
if (_allVolumes.length > 1) ...[
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -73,9 +79,10 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
||||||
builder: (context, exists, child) {
|
builder: (context, exists, child) {
|
||||||
return TextField(
|
return TextField(
|
||||||
controller: _nameController,
|
controller: _nameController,
|
||||||
|
focusNode: _nameFieldFocusNode,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Album name',
|
||||||
helperText: exists ? 'Album already exists' : '',
|
helperText: exists ? 'Album already exists' : '',
|
||||||
hintText: 'Album name',
|
|
||||||
),
|
),
|
||||||
autofocus: _allVolumes.length == 1,
|
autofocus: _allVolumes.length == 1,
|
||||||
onChanged: (_) => _validate(),
|
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) {
|
String _buildAlbumPath(String name) {
|
||||||
if (name == null || name.isEmpty) return '';
|
if (name == null || name.isEmpty) return '';
|
||||||
return join(_selectedVolume.path, 'Pictures', name);
|
return join(_selectedVolume.path, 'Pictures', name);
|
||||||
|
|
|
@ -25,6 +25,7 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_nameController.text = entry.filenameWithoutExtension ?? entry.sourceTitle;
|
_nameController.text = entry.filenameWithoutExtension ?? entry.sourceTitle;
|
||||||
|
_validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -38,6 +39,10 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
|
||||||
return AvesDialog(
|
return AvesDialog(
|
||||||
content: TextField(
|
content: TextField(
|
||||||
controller: _nameController,
|
controller: _nameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'New name',
|
||||||
|
suffixText: entry.extension,
|
||||||
|
),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
onChanged: (_) => _validate(),
|
onChanged: (_) => _validate(),
|
||||||
onSubmitted: (_) => _submit(context),
|
onSubmitted: (_) => _submit(context),
|
||||||
|
|
|
@ -6,6 +6,7 @@ class AvesDialog extends AlertDialog {
|
||||||
|
|
||||||
AvesDialog({
|
AvesDialog({
|
||||||
String title,
|
String title,
|
||||||
|
ScrollController scrollController,
|
||||||
List<Widget> scrollableContent,
|
List<Widget> scrollableContent,
|
||||||
Widget content,
|
Widget content,
|
||||||
@required List<Widget> actions,
|
@required List<Widget> actions,
|
||||||
|
@ -31,6 +32,7 @@ class AvesDialog extends AlertDialog {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
controller: scrollController ?? ScrollController(),
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
children: scrollableContent,
|
children: scrollableContent,
|
||||||
),
|
),
|
||||||
|
@ -38,7 +40,7 @@ class AvesDialog extends AlertDialog {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: content,
|
: 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,
|
actions: actions,
|
||||||
actionsPadding: EdgeInsets.symmetric(horizontal: 8),
|
actionsPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
|
|
Loading…
Reference in a new issue