sort chips on countries/tags pages

This commit is contained in:
Thibault Deckers 2020-09-10 14:59:50 +09:00
parent 78ba136a21
commit 7e530aed74
7 changed files with 193 additions and 100 deletions

View file

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

View file

@ -83,19 +83,6 @@ mixin LocationMixin on SourceBase {
invalidateFilterEntryCounts();
eventBus.fire(LocationsChangedEvent());
}
Map<String, ImageEntry> 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 {}

View file

@ -59,14 +59,6 @@ mixin TagMixin on SourceBase {
invalidateFilterEntryCounts();
eventBus.fire(TagsChangedEvent());
}
Map<String, ImageEntry> getTagEntries() {
final entries = sortedEntriesForFilterList;
return Map.fromEntries(sortedTags.map((tag) => MapEntry(
tag,
entries.firstWhere((entry) => entry.xmpSubjects.contains(tag), orElse: () => null),
)));
}
}
class CatalogMetadataChangedEvent {}

View file

@ -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<Settings, ChipSortFactor>(
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<AlbumsChangedEvent>(),
builder: (context, snapshot) {
return FilterNavigationPage(
builder: (context, snapshot) => FilterNavigationPage(
source: source,
title: 'Albums',
actions: _buildActions(context),
onChipActionSelected: _onChipActionSelected,
filterEntries: getAlbumEntries(source),
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)),
emptyBuilder: () => EmptyContent(
icon: AIcons.album,
text: 'No albums',
),
);
},
),
),
);
},
);
}
List<Widget> _buildActions(BuildContext context) {
return [
PopupMenuButton<ChipAction>(
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<ChipSortFactor>(
@ -96,20 +70,17 @@ class AlbumListPage extends StatelessWidget {
static Map<String, ImageEntry> getAlbumEntries(CollectionSource source) {
final entriesByDate = source.sortedEntriesForFilterList;
final albumEntries = source.sortedAlbums.map((album) {
return MapEntry(
final albums = source.sortedAlbums
.map((album) => MapEntry(
album,
entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null),
);
}).toList();
))
.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 = <String>[], appAlbums = <String>[], specialAlbums = <String>[];

View file

@ -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,12 +20,16 @@ class CountryListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Selector<Settings, ChipSortFactor>(
selector: (context, s) => s.countrySortFactor,
builder: (context, sortFactor, child) {
return StreamBuilder(
stream: source.eventBus.on<LocationsChangedEvent>(),
builder: (context, snapshot) => FilterNavigationPage(
source: source,
title: 'Countries',
filterEntries: source.getCountryEntries(),
onChipActionSelected: _onChipActionSelected,
filterEntries: _getCountryEntries(),
filterBuilder: (s) => LocationFilter(LocationLevel.country, s),
emptyBuilder: () => EmptyContent(
icon: AIcons.location,
@ -28,5 +37,50 @@ class CountryListPage extends StatelessWidget {
),
),
);
},
);
}
void _onChipActionSelected(BuildContext context, ChipAction action) async {
switch (action) {
case ChipAction.sort:
final factor = await showDialog<ChipSortFactor>(
context: context,
builder: (context) => AvesSelectionDialog<ChipSortFactor>(
initialValue: settings.countrySortFactor,
options: {
ChipSortFactor.date: 'By date',
ChipSortFactor.name: 'By name',
},
title: 'Sort',
),
);
if (factor != null) {
settings.countrySortFactor = factor;
}
break;
}
}
Map<String, ImageEntry> _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);
}
}

View file

@ -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<Widget> actions;
final void Function(BuildContext context, ChipAction action) onChipActionSelected;
final Map<String, ImageEntry> 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<Widget> _buildActions(BuildContext context) {
return [
SearchButton(source),
PopupMenuButton<ChipAction>(
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<String, ImageEntry> a, MapEntry<String, ImageEntry> b) {
final c = b.value.bestDate?.compareTo(a.value.bestDate) ?? -1;
return c != 0 ? c : compareAsciiUpperCase(a.key, b.key);
}
}
class FilterGridPage extends StatelessWidget {

View file

@ -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,12 +20,16 @@ class TagListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Selector<Settings, ChipSortFactor>(
selector: (context, s) => s.tagSortFactor,
builder: (context, sortFactor, child) {
return StreamBuilder(
stream: source.eventBus.on<TagsChangedEvent>(),
builder: (context, snapshot) => FilterNavigationPage(
source: source,
title: 'Tags',
filterEntries: source.getTagEntries(),
onChipActionSelected: _onChipActionSelected,
filterEntries: _getTagEntries(),
filterBuilder: (s) => TagFilter(s),
emptyBuilder: () => EmptyContent(
icon: AIcons.tag,
@ -28,5 +37,46 @@ class TagListPage extends StatelessWidget {
),
),
);
},
);
}
void _onChipActionSelected(BuildContext context, ChipAction action) async {
switch (action) {
case ChipAction.sort:
final factor = await showDialog<ChipSortFactor>(
context: context,
builder: (context) => AvesSelectionDialog<ChipSortFactor>(
initialValue: settings.tagSortFactor,
options: {
ChipSortFactor.date: 'By date',
ChipSortFactor.name: 'By name',
},
title: 'Sort',
),
);
if (factor != null) {
settings.tagSortFactor = factor;
}
break;
}
}
Map<String, ImageEntry> _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);
}
}