diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 03ee79fbf..7460a89bf 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -29,6 +29,8 @@ class Settings extends ChangeNotifier { // filter grids static const albumSortFactorKey = 'album_sort_factor'; + static const countrySortFactorKey = 'country_sort_factor'; + static const tagSortFactorKey = 'tag_sort_factor'; // info static const infoMapStyleKey = 'info_map_style'; @@ -84,6 +86,14 @@ class Settings extends ChangeNotifier { set albumSortFactor(ChipSortFactor newValue) => setAndNotify(albumSortFactorKey, newValue.toString()); + ChipSortFactor get countrySortFactor => getEnumOrDefault(countrySortFactorKey, ChipSortFactor.name, ChipSortFactor.values); + + set countrySortFactor(ChipSortFactor newValue) => setAndNotify(countrySortFactorKey, newValue.toString()); + + ChipSortFactor get tagSortFactor => getEnumOrDefault(tagSortFactorKey, ChipSortFactor.name, ChipSortFactor.values); + + set tagSortFactor(ChipSortFactor newValue) => setAndNotify(tagSortFactorKey, newValue.toString()); + // info EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values); diff --git a/lib/model/source/location.dart b/lib/model/source/location.dart index 74fe62f5d..f5cd1f125 100644 --- a/lib/model/source/location.dart +++ b/lib/model/source/location.dart @@ -83,19 +83,6 @@ mixin LocationMixin on SourceBase { invalidateFilterEntryCounts(); eventBus.fire(LocationsChangedEvent()); } - - Map getCountryEntries() { - final locatedEntries = sortedEntriesForFilterList.where((entry) => entry.isLocated); - return Map.fromEntries(sortedCountries.map((countryNameAndCode) { - final split = countryNameAndCode.split(LocationFilter.locationSeparator); - ImageEntry entry; - if (split.length > 1) { - final countryCode = split[1]; - entry = locatedEntries.firstWhere((entry) => entry.addressDetails.countryCode == countryCode, orElse: () => null); - } - return MapEntry(countryNameAndCode, entry); - })); - } } class AddressMetadataChangedEvent {} diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart index 5b2c5a1c4..28a879ab3 100644 --- a/lib/model/source/tag.dart +++ b/lib/model/source/tag.dart @@ -59,14 +59,6 @@ mixin TagMixin on SourceBase { invalidateFilterEntryCounts(); eventBus.fire(TagsChangedEvent()); } - - Map getTagEntries() { - final entries = sortedEntriesForFilterList; - return Map.fromEntries(sortedTags.map((tag) => MapEntry( - tag, - entries.firstWhere((entry) => entry.xmpSubjects.contains(tag), orElse: () => null), - ))); - } } class CatalogMetadataChangedEvent {} diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index 495b3cdf9..86300f3f4 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -5,15 +5,11 @@ import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/utils/android_file_utils.dart'; -import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/common/aves_selection_dialog.dart'; import 'package:aves/widgets/common/icons.dart'; -import 'package:aves/widgets/common/menu_row.dart'; import 'package:aves/widgets/filter_grids/filter_grid_page.dart'; -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; class AlbumListPage extends StatelessWidget { @@ -27,51 +23,29 @@ class AlbumListPage extends StatelessWidget { Widget build(BuildContext context) { return Selector( selector: (context, s) => s.albumSortFactor, - builder: (context, albumSortFactor, child) { + builder: (context, sortFactor, child) { return AnimatedBuilder( animation: androidFileUtils.appNameChangeNotifier, builder: (context, child) => StreamBuilder( stream: source.eventBus.on(), - builder: (context, snapshot) { - return FilterNavigationPage( - source: source, - title: 'Albums', - actions: _buildActions(context), - filterEntries: getAlbumEntries(source), - filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)), - emptyBuilder: () => EmptyContent( - icon: AIcons.album, - text: 'No albums', - ), - ); - }, + builder: (context, snapshot) => FilterNavigationPage( + source: source, + title: 'Albums', + onChipActionSelected: _onChipActionSelected, + filterEntries: getAlbumEntries(source), + filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)), + emptyBuilder: () => EmptyContent( + icon: AIcons.album, + text: 'No albums', + ), + ), ), ); }, ); } - List _buildActions(BuildContext context) { - return [ - PopupMenuButton( - key: Key('appbar-menu-button'), - itemBuilder: (context) { - return [ - PopupMenuItem( - key: Key('menu-sort'), - value: ChipAction.sort, - child: MenuRow(text: 'Sort...', icon: AIcons.sort), - ), - ]; - }, - onSelected: (action) => _onChipActionSelected(context, action), - ), - ]; - } - void _onChipActionSelected(BuildContext context, ChipAction action) async { - // wait for the popup menu to hide before proceeding with the action - await Future.delayed(Durations.popupMenuAnimation * timeDilation); switch (action) { case ChipAction.sort: final factor = await showDialog( @@ -96,20 +70,17 @@ class AlbumListPage extends StatelessWidget { static Map getAlbumEntries(CollectionSource source) { final entriesByDate = source.sortedEntriesForFilterList; - final albumEntries = source.sortedAlbums.map((album) { - return MapEntry( - album, - entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null), - ); - }).toList(); + final albums = source.sortedAlbums + .map((album) => MapEntry( + album, + entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null), + )) + .toList(); switch (settings.albumSortFactor) { case ChipSortFactor.date: - albumEntries.sort((a, b) { - final c = b.value.bestDate?.compareTo(a.value.bestDate) ?? -1; - return c != 0 ? c : compareAsciiUpperCase(a.key, b.key); - }); - return Map.fromEntries(albumEntries); + albums.sort(FilterNavigationPage.compareChipByDate); + return Map.fromEntries(albums); case ChipSortFactor.name: default: final regularAlbums = [], appAlbums = [], specialAlbums = []; diff --git a/lib/widgets/filter_grids/countries_page.dart b/lib/widgets/filter_grids/countries_page.dart index d580f51d4..032c50bbb 100644 --- a/lib/widgets/filter_grids/countries_page.dart +++ b/lib/widgets/filter_grids/countries_page.dart @@ -1,10 +1,15 @@ import 'package:aves/model/filters/location.dart'; +import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/location.dart'; import 'package:aves/widgets/collection/empty.dart'; +import 'package:aves/widgets/common/aves_selection_dialog.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/filter_grids/filter_grid_page.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class CountryListPage extends StatelessWidget { static const routeName = '/countries'; @@ -15,18 +20,67 @@ class CountryListPage extends StatelessWidget { @override Widget build(BuildContext context) { - return StreamBuilder( - stream: source.eventBus.on(), - builder: (context, snapshot) => FilterNavigationPage( - source: source, - title: 'Countries', - filterEntries: source.getCountryEntries(), - filterBuilder: (s) => LocationFilter(LocationLevel.country, s), - emptyBuilder: () => EmptyContent( - icon: AIcons.location, - text: 'No countries', - ), - ), + return Selector( + selector: (context, s) => s.countrySortFactor, + builder: (context, sortFactor, child) { + return StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) => FilterNavigationPage( + source: source, + title: 'Countries', + onChipActionSelected: _onChipActionSelected, + filterEntries: _getCountryEntries(), + filterBuilder: (s) => LocationFilter(LocationLevel.country, s), + emptyBuilder: () => EmptyContent( + icon: AIcons.location, + text: 'No countries', + ), + ), + ); + }, ); } + + void _onChipActionSelected(BuildContext context, ChipAction action) async { + switch (action) { + case ChipAction.sort: + final factor = await showDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: settings.countrySortFactor, + options: { + ChipSortFactor.date: 'By date', + ChipSortFactor.name: 'By name', + }, + title: 'Sort', + ), + ); + if (factor != null) { + settings.countrySortFactor = factor; + } + break; + } + } + + Map _getCountryEntries() { + final entriesByDate = source.sortedEntriesForFilterList; + final locatedEntries = entriesByDate.where((entry) => entry.isLocated); + final countries = source.sortedCountries.map((countryNameAndCode) { + final split = countryNameAndCode.split(LocationFilter.locationSeparator); + ImageEntry entry; + if (split.length > 1) { + final countryCode = split[1]; + entry = locatedEntries.firstWhere((entry) => entry.addressDetails.countryCode == countryCode, orElse: () => null); + } + return MapEntry(countryNameAndCode, entry); + }).toList(); + + switch (settings.countrySortFactor) { + case ChipSortFactor.date: + countries.sort(FilterNavigationPage.compareChipByDate); + break; + case ChipSortFactor.name: + } + return Map.fromEntries(countries); + } } diff --git a/lib/widgets/filter_grids/filter_grid_page.dart b/lib/widgets/filter_grids/filter_grid_page.dart index ced774c4b..4ef83fc00 100644 --- a/lib/widgets/filter_grids/filter_grid_page.dart +++ b/lib/widgets/filter_grids/filter_grid_page.dart @@ -13,17 +13,21 @@ import 'package:aves/widgets/common/app_bar_title.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/double_back_pop.dart'; +import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/common/menu_row.dart'; import 'package:aves/widgets/drawer/app_drawer.dart'; import 'package:aves/widgets/filter_grids/decorated_filter_chip.dart'; import 'package:aves/widgets/filter_grids/search_button.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:provider/provider.dart'; class FilterNavigationPage extends StatelessWidget { final CollectionSource source; final String title; - final List actions; + final void Function(BuildContext context, ChipAction action) onChipActionSelected; final Map filterEntries; final CollectionFilter Function(String key) filterBuilder; final Widget Function() emptyBuilder; @@ -31,7 +35,7 @@ class FilterNavigationPage extends StatelessWidget { const FilterNavigationPage({ @required this.source, @required this.title, - this.actions, + @required this.onChipActionSelected, @required this.filterEntries, @required this.filterBuilder, @required this.emptyBuilder, @@ -49,10 +53,7 @@ class FilterNavigationPage extends StatelessWidget { source: source, ), ), - actions: [ - SearchButton(source), - ...(actions ?? []), - ], + actions: _buildActions(context), titleSpacing: 0, floating: true, ), @@ -80,6 +81,29 @@ class FilterNavigationPage extends StatelessWidget { ); } + List _buildActions(BuildContext context) { + return [ + SearchButton(source), + PopupMenuButton( + key: Key('appbar-menu-button'), + itemBuilder: (context) { + return [ + PopupMenuItem( + key: Key('menu-sort'), + value: ChipAction.sort, + child: MenuRow(text: 'Sort...', icon: AIcons.sort), + ), + ]; + }, + onSelected: (action) async { + // wait for the popup menu to hide before proceeding with the action + await Future.delayed(Durations.popupMenuAnimation * timeDilation); + onChipActionSelected(context, action); + }, + ), + ]; + } + void _goToSearch(BuildContext context) { Navigator.push( context, @@ -89,6 +113,11 @@ class FilterNavigationPage extends StatelessWidget { ), )); } + + static int compareChipByDate(MapEntry a, MapEntry b) { + final c = b.value.bestDate?.compareTo(a.value.bestDate) ?? -1; + return c != 0 ? c : compareAsciiUpperCase(a.key, b.key); + } } class FilterGridPage extends StatelessWidget { diff --git a/lib/widgets/filter_grids/tags_page.dart b/lib/widgets/filter_grids/tags_page.dart index be5bfd8f8..d67cb551e 100644 --- a/lib/widgets/filter_grids/tags_page.dart +++ b/lib/widgets/filter_grids/tags_page.dart @@ -1,10 +1,15 @@ import 'package:aves/model/filters/tag.dart'; +import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/widgets/collection/empty.dart'; +import 'package:aves/widgets/common/aves_selection_dialog.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/filter_grids/filter_grid_page.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class TagListPage extends StatelessWidget { static const routeName = '/tags'; @@ -15,18 +20,63 @@ class TagListPage extends StatelessWidget { @override Widget build(BuildContext context) { - return StreamBuilder( - stream: source.eventBus.on(), - builder: (context, snapshot) => FilterNavigationPage( - source: source, - title: 'Tags', - filterEntries: source.getTagEntries(), - filterBuilder: (s) => TagFilter(s), - emptyBuilder: () => EmptyContent( - icon: AIcons.tag, - text: 'No tags', - ), - ), + return Selector( + selector: (context, s) => s.tagSortFactor, + builder: (context, sortFactor, child) { + return StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) => FilterNavigationPage( + source: source, + title: 'Tags', + onChipActionSelected: _onChipActionSelected, + filterEntries: _getTagEntries(), + filterBuilder: (s) => TagFilter(s), + emptyBuilder: () => EmptyContent( + icon: AIcons.tag, + text: 'No tags', + ), + ), + ); + }, ); } + + void _onChipActionSelected(BuildContext context, ChipAction action) async { + switch (action) { + case ChipAction.sort: + final factor = await showDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: settings.tagSortFactor, + options: { + ChipSortFactor.date: 'By date', + ChipSortFactor.name: 'By name', + }, + title: 'Sort', + ), + ); + if (factor != null) { + settings.tagSortFactor = factor; + } + break; + } + } + + Map _getTagEntries() { + final entriesByDate = source.sortedEntriesForFilterList; + final tags = source.sortedTags + .map((tag) => MapEntry( + tag, + entriesByDate.firstWhere((entry) => entry.xmpSubjects.contains(tag), orElse: () => null), + )) + .toList(); + + switch (settings.tagSortFactor) { + case ChipSortFactor.date: + tags.sort(FilterNavigationPage.compareChipByDate); + break; + case ChipSortFactor.name: + } + return Map.fromEntries(tags); + } }