From 055cad333f68b2a3620e6a7c68f2ccc94ac1ca6f Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 20 Sep 2020 17:02:50 +0900 Subject: [PATCH] albums: pin to top --- lib/model/settings/settings.dart | 6 ++ lib/widgets/filter_grids/albums_page.dart | 91 ++++++++++++++----- lib/widgets/filter_grids/chip_actions.dart | 34 +++++++ .../filter_grids/decorated_filter_chip.dart | 40 +++++--- .../filter_grids/filter_grid_page.dart | 5 +- 5 files changed, 137 insertions(+), 39 deletions(-) diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index d6766c0bc..1d5091084 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/coordinate_format.dart'; import 'package:aves/model/settings/home_page.dart'; import 'package:aves/model/settings/screen_on.dart'; @@ -37,6 +38,7 @@ class Settings extends ChangeNotifier { static const albumSortFactorKey = 'album_sort_factor'; static const countrySortFactorKey = 'country_sort_factor'; static const tagSortFactorKey = 'tag_sort_factor'; + static const pinnedFiltersKey = 'pinned_filters'; // info static const infoMapStyleKey = 'info_map_style'; @@ -120,6 +122,10 @@ class Settings extends ChangeNotifier { set tagSortFactor(ChipSortFactor newValue) => setAndNotify(tagSortFactorKey, newValue.toString()); + Set get pinnedFilters => (_prefs.getStringList(pinnedFiltersKey) ?? []).map(CollectionFilter.fromJson).toSet(); + + set pinnedFilters(Set newValue) => setAndNotify(pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList()); + // info EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values); diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index 42cc65638..14ac5ccd1 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -1,4 +1,5 @@ import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/album.dart'; @@ -7,10 +8,14 @@ import 'package:aves/model/source/enums.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/common/menu_row.dart'; import 'package:aves/widgets/filter_grids/chip_action_delegate.dart'; +import 'package:aves/widgets/filter_grids/chip_actions.dart'; import 'package:aves/widgets/filter_grids/filter_grid_page.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; class AlbumListPage extends StatelessWidget { static const routeName = '/albums'; @@ -23,9 +28,9 @@ class AlbumListPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Selector( - selector: (context, s) => s.albumSortFactor, - builder: (context, sortFactor, child) { + return Selector>>( + selector: (context, s) => Tuple2(s.albumSortFactor, s.pinnedFilters), + builder: (context, s, child) { return AnimatedBuilder( animation: androidFileUtils.appNameChangeNotifier, builder: (context, child) => StreamBuilder( @@ -35,11 +40,12 @@ class AlbumListPage extends StatelessWidget { title: 'Albums', actionDelegate: actionDelegate, filterEntries: getAlbumEntries(source), - filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)), + filterBuilder: (album) => AlbumFilter(album, source.getUniqueAlbumName(album)), emptyBuilder: () => EmptyContent( icon: AIcons.album, text: 'No albums', ), + onLongPress: (filter, tapPosition) => _showMenu(context, filter, tapPosition), ), ), ); @@ -47,38 +53,75 @@ class AlbumListPage extends StatelessWidget { ); } + Future _showMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async { + final RenderBox overlay = Overlay.of(context).context.findRenderObject(); + final touchArea = Size(40, 40); + final selectedAction = await showMenu( + context: context, + position: RelativeRect.fromRect(tapPosition & touchArea, Offset.zero & overlay.size), + items: [settings.pinnedFilters.contains(filter) ? AlbumAction.unpin : AlbumAction.pin, AlbumAction.rename] + .map((action) => PopupMenuItem( + value: action, + child: MenuRow(text: action.getText(), icon: action.getIcon()), + )) + .toList(), + ); + if (selectedAction != null) { + switch (selectedAction) { + case AlbumAction.pin: + final pinnedFilters = settings.pinnedFilters..add(filter); + settings.pinnedFilters = pinnedFilters; + break; + case AlbumAction.unpin: + final pinnedFilters = settings.pinnedFilters..remove(filter); + settings.pinnedFilters = pinnedFilters; + break; + case AlbumAction.rename: + // TODO TLAD rename album + break; + default: + break; + } + } + } + // common with album selection page to move/copy entries static Map getAlbumEntries(CollectionSource source) { + final pinned = settings.pinnedFilters.whereType().map((f) => f.album); final entriesByDate = source.sortedEntriesForFilterList; - final albums = source.sortedAlbums - .map((album) => MapEntry( - album, - entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null), - )) - .toList(); switch (settings.albumSortFactor) { case ChipSortFactor.date: - albums.sort(FilterNavigationPage.compareChipByDate); - return Map.fromEntries(albums); + final allAlbumMapEntries = source.sortedAlbums.map((album) => MapEntry( + album, + entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null), + )); + final byPin = groupBy, bool>(allAlbumMapEntries, (e) => pinned.contains(e.key)); + final pinnedAlbumMapEntries = (byPin[true] ?? [])..sort(FilterNavigationPage.compareChipByDate); + final unpinnedAlbumMapEntries = (byPin[false] ?? [])..sort(FilterNavigationPage.compareChipByDate); + return Map.fromEntries([...pinnedAlbumMapEntries, ...unpinnedAlbumMapEntries]); case ChipSortFactor.name: default: - final regularAlbums = [], appAlbums = [], specialAlbums = []; + final pinnedAlbums = [], regularAlbums = [], appAlbums = [], specialAlbums = []; for (var album in source.sortedAlbums) { - switch (androidFileUtils.getAlbumType(album)) { - case AlbumType.regular: - regularAlbums.add(album); - break; - case AlbumType.app: - appAlbums.add(album); - break; - default: - specialAlbums.add(album); - break; + if (pinned.contains(album)) { + pinnedAlbums.add(album); + } else { + switch (androidFileUtils.getAlbumType(album)) { + case AlbumType.regular: + regularAlbums.add(album); + break; + case AlbumType.app: + appAlbums.add(album); + break; + default: + specialAlbums.add(album); + break; + } } } - return Map.fromEntries([...specialAlbums, ...appAlbums, ...regularAlbums].map((album) { + return Map.fromEntries([...pinnedAlbums, ...specialAlbums, ...appAlbums, ...regularAlbums].map((album) { return MapEntry( album, entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null), diff --git a/lib/widgets/filter_grids/chip_actions.dart b/lib/widgets/filter_grids/chip_actions.dart index 5e0984543..0b0ae1de0 100644 --- a/lib/widgets/filter_grids/chip_actions.dart +++ b/lib/widgets/filter_grids/chip_actions.dart @@ -1,3 +1,37 @@ +import 'package:aves/widgets/common/icons.dart'; +import 'package:flutter/widgets.dart'; + enum ChipAction { sort, } + +enum AlbumAction { + pin, + unpin, + rename, +} + +extension ExtraAlbumAction on AlbumAction { + String getText() { + switch (this) { + case AlbumAction.pin: + return 'Pin to top'; + case AlbumAction.unpin: + return 'Unpin from top'; + case AlbumAction.rename: + return 'Rename'; + } + return null; + } + + IconData getIcon() { + switch (this) { + case AlbumAction.pin: + case AlbumAction.unpin: + return AIcons.pin; + case AlbumAction.rename: + return AIcons.rename; + } + return null; + } +} diff --git a/lib/widgets/filter_grids/decorated_filter_chip.dart b/lib/widgets/filter_grids/decorated_filter_chip.dart index 6ed7ec8ae..228650c2d 100644 --- a/lib/widgets/filter_grids/decorated_filter_chip.dart +++ b/lib/widgets/filter_grids/decorated_filter_chip.dart @@ -16,6 +16,7 @@ class DecoratedFilterChip extends StatelessWidget { final CollectionSource source; final CollectionFilter filter; final ImageEntry entry; + final bool pinned; final FilterCallback onTap; final OffsetFilterCallback onLongPress; @@ -24,6 +25,7 @@ class DecoratedFilterChip extends StatelessWidget { @required this.source, @required this.filter, @required this.entry, + this.pinned = false, @required this.onTap, this.onLongPress, }) : super(key: key); @@ -57,19 +59,29 @@ class DecoratedFilterChip extends StatelessWidget { '${source.count(filter)}', style: TextStyle(color: FilterGridPage.detailColor), ); - return filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album) - ? Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - AIcons.removableStorage, - size: 16, - color: FilterGridPage.detailColor, - ), - SizedBox(width: 8), - count, - ], - ) - : count; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (pinned) + Padding( + padding: EdgeInsets.only(right: 8), + child: Icon( + AIcons.pin, + size: 16, + color: FilterGridPage.detailColor, + ), + ), + if (filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album)) + Padding( + padding: EdgeInsets.only(right: 8), + child: Icon( + AIcons.removableStorage, + size: 16, + color: FilterGridPage.detailColor, + ), + ), + count, + ], + ); } } diff --git a/lib/widgets/filter_grids/filter_grid_page.dart b/lib/widgets/filter_grids/filter_grid_page.dart index 47ea99a23..908eaa6ee 100644 --- a/lib/widgets/filter_grids/filter_grid_page.dart +++ b/lib/widgets/filter_grids/filter_grid_page.dart @@ -146,6 +146,7 @@ class FilterGridPage extends StatelessWidget { @override Widget build(BuildContext context) { + final pinnedFilters = settings.pinnedFilters; return MediaQueryDataProvider( child: Scaffold( body: DoubleBackPopScope( @@ -169,11 +170,13 @@ class FilterGridPage extends StatelessWidget { delegate: SliverChildBuilderDelegate( (context, i) { final key = filterKeys[i]; + final filter = filterBuilder(key); final child = DecoratedFilterChip( key: Key(key), source: source, - filter: filterBuilder(key), + filter: filter, entry: filterEntries[key], + pinned: pinnedFilters.contains(filter), onTap: onTap, onLongPress: onLongPress, );