Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2020-08-02 19:30:16 +09:00
commit 041565b34a
17 changed files with 271 additions and 143 deletions

View file

@ -85,6 +85,8 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
bool get showHeaders {
if (sortFactor == SortFactor.size) return false;
if (sortFactor == SortFactor.date && groupFactor == GroupFactor.none) return false;
final albumSections = sortFactor == SortFactor.name || (sortFactor == SortFactor.date && groupFactor == GroupFactor.album);
final filterByAlbum = filters.any((f) => f is AlbumFilter);
if (albumSections && filterByAlbum) return false;
@ -160,6 +162,11 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
case GroupFactor.day:
sections = groupBy<ImageEntry, DateTime>(_filteredEntries, (entry) => entry.dayTaken);
break;
case GroupFactor.none:
sections = Map.fromEntries([
MapEntry(null, _filteredEntries),
]);
break;
}
break;
case SortFactor.size:
@ -209,7 +216,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
enum SortFactor { date, size, name }
enum GroupFactor { album, month, day }
enum GroupFactor { none, album, month, day }
enum Activity { browse, select }

View file

@ -22,6 +22,14 @@ class Constants {
static const svgBackground = Colors.white;
static const svgColorFilter = ColorFilter.mode(svgBackground, BlendMode.dstOver);
static const dialogContentHorizontalPadding = EdgeInsets.symmetric(horizontal: 24);
static const dialogActionsPadding = EdgeInsets.symmetric(horizontal: 8);
static const dialogShape = RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(24),
),
);
static const List<Dependency> androidDependencies = [
Dependency(
name: 'CWAC-Document',

View file

@ -11,7 +11,7 @@ class Durations {
// collection animations
static const appBarTitleAnimation = Duration(milliseconds: 300);
static const filterBarRemovalAnimation = Duration(milliseconds: 200);
static const filterBarRemovalAnimation = Duration(milliseconds: 400);
static const collectionOpOverlayAnimation = Duration(milliseconds: 300);
static const collectionScalingBackgroundAnimation = Duration(milliseconds: 200);
static const sectionHeaderAnimation = Duration(milliseconds: 200);

View file

@ -6,7 +6,9 @@ import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/utils/durations.dart';
import 'package:aves/widgets/album/filter_bar.dart';
import 'package:aves/widgets/album/search/search_delegate.dart';
import 'package:aves/widgets/common/action_delegates/group_collection_dialog.dart';
import 'package:aves/widgets/common/action_delegates/selection_action_delegate.dart';
import 'package:aves/widgets/common/action_delegates/sort_collection_dialog.dart';
import 'package:aves/widgets/common/app_bar_subtitle.dart';
import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart';
import 'package:aves/widgets/common/entry_actions.dart';
@ -185,8 +187,15 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
itemBuilder: (context) {
final hasSelection = collection.selection.isNotEmpty;
return [
..._buildSortMenuItems(),
..._buildGroupMenuItems(),
PopupMenuItem(
value: CollectionAction.sort,
child: MenuRow(text: 'Sort...', icon: AIcons.sort),
),
if (collection.sortFactor == SortFactor.date)
PopupMenuItem(
value: CollectionAction.group,
child: MenuRow(text: 'Group...', icon: AIcons.group),
),
if (collection.isBrowsing) ...[
if (AvesApp.mode == AppMode.main)
if (kDebugMode)
@ -204,6 +213,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
),
],
if (collection.isSelecting) ...[
PopupMenuDivider(),
PopupMenuItem(
value: CollectionAction.copy,
enabled: hasSelection,
@ -238,44 +248,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
];
}
List<PopupMenuEntry<CollectionAction>> _buildSortMenuItems() {
return [
PopupMenuItem(
value: CollectionAction.sortByDate,
child: MenuRow(text: 'Sort by date', checked: collection.sortFactor == SortFactor.date),
),
PopupMenuItem(
value: CollectionAction.sortBySize,
child: MenuRow(text: 'Sort by size', checked: collection.sortFactor == SortFactor.size),
),
PopupMenuItem(
value: CollectionAction.sortByName,
child: MenuRow(text: 'Sort by name', checked: collection.sortFactor == SortFactor.name),
),
PopupMenuDivider(),
];
}
List<PopupMenuEntry<CollectionAction>> _buildGroupMenuItems() {
return collection.sortFactor == SortFactor.date
? [
PopupMenuItem(
value: CollectionAction.groupByAlbum,
child: MenuRow(text: 'Group by album', checked: collection.groupFactor == GroupFactor.album),
),
PopupMenuItem(
value: CollectionAction.groupByMonth,
child: MenuRow(text: 'Group by month', checked: collection.groupFactor == GroupFactor.month),
),
PopupMenuItem(
value: CollectionAction.groupByDay,
child: MenuRow(text: 'Group by day', checked: collection.groupFactor == GroupFactor.day),
),
PopupMenuDivider(),
]
: [];
}
void _onActivityChange() {
if (collection.isSelecting) {
_browseToSelectAnimation.forward();
@ -317,29 +289,25 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
case CollectionAction.stats:
unawaited(_goToStats());
break;
case CollectionAction.groupByAlbum:
settings.collectionGroupFactor = GroupFactor.album;
collection.group(GroupFactor.album);
case CollectionAction.group:
final factor = await showDialog<GroupFactor>(
context: context,
builder: (context) => GroupCollectionDialog(),
);
if (factor != null) {
settings.collectionGroupFactor = factor;
collection.group(factor);
}
break;
case CollectionAction.groupByMonth:
settings.collectionGroupFactor = GroupFactor.month;
collection.group(GroupFactor.month);
break;
case CollectionAction.groupByDay:
settings.collectionGroupFactor = GroupFactor.day;
collection.group(GroupFactor.day);
break;
case CollectionAction.sortByDate:
settings.collectionSortFactor = SortFactor.date;
collection.sort(SortFactor.date);
break;
case CollectionAction.sortBySize:
settings.collectionSortFactor = SortFactor.size;
collection.sort(SortFactor.size);
break;
case CollectionAction.sortByName:
settings.collectionSortFactor = SortFactor.name;
collection.sort(SortFactor.name);
case CollectionAction.sort:
final factor = await showDialog<SortFactor>(
context: context,
builder: (context) => SortCollectionDialog(),
);
if (factor != null) {
settings.collectionSortFactor = factor;
collection.sort(factor);
}
break;
}
}
@ -365,17 +333,13 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
enum CollectionAction {
copy,
group,
move,
refresh,
refreshMetadata,
select,
selectAll,
selectNone,
sort,
stats,
groupByAlbum,
groupByMonth,
groupByDay,
sortByDate,
sortBySize,
sortByName,
}

View file

@ -45,7 +45,9 @@ class _FilterBarState extends State<FilterBar> {
listState.removeItem(
index,
animate
? (context, animation) => FadeTransition(
? (context, animation) {
animation = animation.drive(CurveTween(curve: Curves.easeInOutBack));
return FadeTransition(
opacity: animation,
child: SizeTransition(
axis: Axis.horizontal,
@ -55,7 +57,8 @@ class _FilterBarState extends State<FilterBar> {
child: _buildChip(filter),
),
),
)
);
}
: (context, animation) => _buildChip(filter),
duration: animate ? Durations.filterBarRemovalAnimation : Duration.zero,
);

View file

@ -29,18 +29,18 @@ class SectionHeader extends StatelessWidget {
Widget header;
switch (collection.sortFactor) {
case SortFactor.date:
if (collection.sortFactor == SortFactor.date) {
switch (collection.groupFactor) {
case GroupFactor.album:
header = _buildAlbumSectionHeader();
break;
case GroupFactor.month:
header = MonthSectionHeader(key: ValueKey(sectionKey), date: sectionKey as DateTime);
break;
case GroupFactor.day:
header = DaySectionHeader(key: ValueKey(sectionKey), date: sectionKey as DateTime);
break;
}
switch (collection.groupFactor) {
case GroupFactor.album:
header = _buildAlbumSectionHeader();
break;
case GroupFactor.month:
header = MonthSectionHeader(key: ValueKey(sectionKey), date: sectionKey as DateTime);
break;
case GroupFactor.day:
header = DaySectionHeader(key: ValueKey(sectionKey), date: sectionKey as DateTime);
break;
case GroupFactor.none:
break;
}
break;
case SortFactor.size:

View file

@ -9,12 +9,14 @@ class ExpandableFilterRow extends StatelessWidget {
final String title;
final Iterable<CollectionFilter> filters;
final ValueNotifier<String> expandedNotifier;
final HeroType Function(CollectionFilter filter) heroTypeBuilder;
final FilterCallback onPressed;
const ExpandableFilterRow({
this.title,
@required this.filters,
this.expandedNotifier,
this.heroTypeBuilder,
@required this.onPressed,
});
@ -59,12 +61,7 @@ class ExpandableFilterRow extends StatelessWidget {
child: Wrap(
spacing: horizontalPadding,
runSpacing: verticalPadding,
children: filtersList
.map((filter) => AvesFilterChip(
filter: filter,
onPressed: onPressed,
))
.toList(),
children: filtersList.map(_buildFilterChip).toList(),
),
);
final list = Container(
@ -78,12 +75,7 @@ class ExpandableFilterRow extends StatelessWidget {
physics: BouncingScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
itemBuilder: (context, index) {
if (index >= filtersList.length) return null;
final filter = filtersList[index];
return AvesFilterChip(
filter: filter,
onPressed: onPressed,
);
return index < filtersList.length ? _buildFilterChip(filtersList[index]) : null;
},
separatorBuilder: (context, index) => SizedBox(width: 8),
itemCount: filtersList.length,
@ -109,4 +101,12 @@ class ExpandableFilterRow extends StatelessWidget {
)
: filterChips;
}
Widget _buildFilterChip(CollectionFilter filter) {
return AvesFilterChip(
filter: filter,
heroType: heroTypeBuilder?.call(filter) ?? HeroType.onTap,
onPressed: onPressed,
);
}
}

View file

@ -62,19 +62,23 @@ class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
child: ValueListenableBuilder<String>(
valueListenable: expandedSectionNotifier,
builder: (context, expandedSection, child) {
var queryFilter = _buildQueryFilter(false);
return ListView(
padding: EdgeInsets.only(top: 8),
children: [
_buildFilterRow(
context: context,
filters: [
_buildQueryFilter(false),
queryFilter,
FavouriteFilter(),
MimeFilter(MimeTypes.anyImage),
MimeFilter(MimeTypes.anyVideo),
MimeFilter(MimeFilter.animated),
MimeFilter(MimeTypes.svg),
].where((f) => f != null && containQuery(f.label)),
// usually perform hero animation only on tapped chips,
// but we also need to animate the query chip when it is selected by submitting the search query
heroTypeBuilder: (filter) => filter == queryFilter ? HeroType.always : HeroType.onTap,
),
StreamBuilder(
stream: source.eventBus.on<AlbumsChangedEvent>(),
@ -118,11 +122,17 @@ class ImageSearchDelegate extends SearchDelegate<CollectionFilter> {
);
}
Widget _buildFilterRow({@required BuildContext context, String title, @required Iterable<CollectionFilter> filters}) {
Widget _buildFilterRow({
@required BuildContext context,
String title,
@required Iterable<CollectionFilter> filters,
HeroType Function(CollectionFilter filter) heroTypeBuilder,
}) {
return ExpandableFilterRow(
title: title,
filters: filters,
expandedNotifier: expandedSectionNotifier,
heroTypeBuilder: heroTypeBuilder,
onPressed: (filter) => _select(context, filter is QueryFilter ? QueryFilter(filter.query) : filter),
);
}

View file

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/constants.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:path/path.dart';
@ -35,56 +36,55 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
Widget build(BuildContext context) {
return AlertDialog(
title: Text('New Album'),
content: Column(
mainAxisSize: MainAxisSize.min,
content: ListView(
shrinkWrap: true,
children: [
if (_allVolumes.length > 1) ...[
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('Storage:'),
SizedBox(width: 8),
Expanded(
child: DropdownButton<StorageVolume>(
isExpanded: true,
items: _allVolumes
.map((volume) => DropdownMenuItem(
value: volume,
child: Text(
volume.description,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
))
.toList(),
value: _selectedVolume,
onChanged: (volume) {
_selectedVolume = volume;
_checkAlbumExists();
setState(() {});
},
),
),
],
Padding(
padding: Constants.dialogContentHorizontalPadding,
child: Text('Storage:'),
),
SizedBox(height: 16),
],
ValueListenableBuilder<bool>(
valueListenable: _existsNotifier,
builder: (context, exists, child) {
return TextField(
controller: _nameController,
decoration: InputDecoration(
helperText: exists ? 'Album already exists' : '',
..._allVolumes.map((volume) => RadioListTile<StorageVolume>(
value: volume,
groupValue: _selectedVolume,
onChanged: (volume) {
_selectedVolume = volume;
_checkAlbumExists();
setState(() {});
},
title: Text(
volume.description,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
onChanged: (_) => _checkAlbumExists(),
onSubmitted: (_) => _submit(context),
);
}),
subtitle: Text(
volume.path,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
)),
SizedBox(height: 8),
],
Padding(
padding: Constants.dialogContentHorizontalPadding,
child: ValueListenableBuilder<bool>(
valueListenable: _existsNotifier,
builder: (context, exists, child) {
return TextField(
controller: _nameController,
decoration: InputDecoration(
helperText: exists ? 'Album already exists' : '',
),
onChanged: (_) => _checkAlbumExists(),
onSubmitted: (_) => _submit(context),
);
}),
),
],
),
contentPadding: EdgeInsets.fromLTRB(24, 20, 24, 0),
contentPadding: EdgeInsets.only(top: 20),
actions: [
FlatButton(
onPressed: () => Navigator.pop(context),
@ -95,6 +95,8 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
child: Text('Create'.toUpperCase()),
),
],
actionsPadding: Constants.dialogActionsPadding,
shape: Constants.dialogShape,
);
}

View file

@ -4,6 +4,7 @@ import 'package:aves/model/image_entry.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/image_file_service.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/action_delegates/feedback.dart';
import 'package:aves/widgets/common/action_delegates/permission_aware.dart';
import 'package:aves/widgets/common/action_delegates/rename_entry_dialog.dart';
@ -131,6 +132,8 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin {
child: Text('Delete'.toUpperCase()),
),
],
actionsPadding: Constants.dialogActionsPadding,
shape: Constants.dialogShape,
);
},
);

View file

@ -0,0 +1,61 @@
import 'package:aves/model/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/utils/constants.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class GroupCollectionDialog extends StatefulWidget {
@override
_GroupCollectionDialogState createState() => _GroupCollectionDialogState();
}
class _GroupCollectionDialogState extends State<GroupCollectionDialog> {
GroupFactor _selectedGroup;
@override
void initState() {
super.initState();
_selectedGroup = settings.collectionGroupFactor;
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Group'),
content: ListView(
shrinkWrap: true,
children: [
_buildRadioListTile(GroupFactor.album, 'By album'),
_buildRadioListTile(GroupFactor.month, 'By month'),
_buildRadioListTile(GroupFactor.day, 'By day'),
_buildRadioListTile(GroupFactor.none, 'Do not group'),
],
),
contentPadding: EdgeInsets.only(top: 20),
actions: [
FlatButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'.toUpperCase()),
),
FlatButton(
onPressed: () => Navigator.pop(context, _selectedGroup),
child: Text('Apply'.toUpperCase()),
),
],
actionsPadding: Constants.dialogActionsPadding,
shape: Constants.dialogShape,
);
}
Widget _buildRadioListTile(GroupFactor group, String title) => RadioListTile<GroupFactor>(
value: group,
groupValue: _selectedGroup,
onChanged: (group) => setState(() => _selectedGroup = group),
title: Text(
title,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
);
}

View file

@ -1,5 +1,6 @@
import 'package:aves/model/image_entry.dart';
import 'package:aves/services/android_file_service.dart';
import 'package:aves/utils/constants.dart';
import 'package:flutter/material.dart';
mixin PermissionAwareMixin {
@ -35,6 +36,8 @@ mixin PermissionAwareMixin {
child: Text('OK'.toUpperCase()),
),
],
actionsPadding: Constants.dialogActionsPadding,
shape: Constants.dialogShape,
);
},
);

View file

@ -1,4 +1,5 @@
import 'package:aves/model/image_entry.dart';
import 'package:aves/utils/constants.dart';
import 'package:flutter/material.dart';
class RenameEntryDialog extends StatefulWidget {
@ -43,6 +44,8 @@ class _RenameEntryDialogState extends State<RenameEntryDialog> {
child: Text('Apply'.toUpperCase()),
),
],
actionsPadding: Constants.dialogActionsPadding,
shape: Constants.dialogShape,
);
}
}

View file

@ -8,6 +8,7 @@ import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/image_file_service.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/utils/durations.dart';
import 'package:aves/widgets/album/app_bar.dart';
import 'package:aves/widgets/album/empty.dart';
@ -200,6 +201,8 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
child: Text('Delete'.toUpperCase()),
),
],
actionsPadding: Constants.dialogActionsPadding,
shape: Constants.dialogShape,
);
},
);

View file

@ -0,0 +1,60 @@
import 'package:aves/model/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/utils/constants.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class SortCollectionDialog extends StatefulWidget {
@override
_SortCollectionDialogState createState() => _SortCollectionDialogState();
}
class _SortCollectionDialogState extends State<SortCollectionDialog> {
SortFactor _selectedSort;
@override
void initState() {
super.initState();
_selectedSort = settings.collectionSortFactor;
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Sort'),
content: ListView(
shrinkWrap: true,
children: [
_buildRadioListTile(SortFactor.date, 'By date'),
_buildRadioListTile(SortFactor.size, 'By size'),
_buildRadioListTile(SortFactor.name, 'By album & file name'),
],
),
contentPadding: EdgeInsets.only(top: 20),
actions: [
FlatButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'.toUpperCase()),
),
FlatButton(
onPressed: () => Navigator.pop(context, _selectedSort),
child: Text('Apply'.toUpperCase()),
),
],
actionsPadding: Constants.dialogActionsPadding,
shape: Constants.dialogShape,
);
}
Widget _buildRadioListTile(SortFactor sort, String title) => RadioListTile<SortFactor>(
value: sort,
groupValue: _selectedSort,
onChanged: (sort) => setState(() => _selectedSort = sort),
title: Text(
title,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
);
}

View file

@ -32,6 +32,7 @@ class AIcons {
static const IconData favourite = OMIcons.favoriteBorder;
static const IconData favouriteActive = OMIcons.favorite;
static const IconData goUp = OMIcons.arrowUpward;
static const IconData group = OMIcons.groupWork;
static const IconData info = OMIcons.info;
static const IconData openInNew = OMIcons.openInNew;
static const IconData print = OMIcons.print;

View file

@ -11,7 +11,7 @@ description: A new Flutter application.
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.1.1+13
version: 1.1.2+14
# video_player (as of v0.10.8+2, backed by ExoPlayer):
# - does not support content URIs (by default, but trivial by fork)