albums: pin to top

This commit is contained in:
Thibault Deckers 2020-09-20 17:02:50 +09:00
parent 15c50d5cef
commit 055cad333f
5 changed files with 137 additions and 39 deletions

View file

@ -1,3 +1,4 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/coordinate_format.dart'; import 'package:aves/model/settings/coordinate_format.dart';
import 'package:aves/model/settings/home_page.dart'; import 'package:aves/model/settings/home_page.dart';
import 'package:aves/model/settings/screen_on.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 albumSortFactorKey = 'album_sort_factor';
static const countrySortFactorKey = 'country_sort_factor'; static const countrySortFactorKey = 'country_sort_factor';
static const tagSortFactorKey = 'tag_sort_factor'; static const tagSortFactorKey = 'tag_sort_factor';
static const pinnedFiltersKey = 'pinned_filters';
// info // info
static const infoMapStyleKey = 'info_map_style'; static const infoMapStyleKey = 'info_map_style';
@ -120,6 +122,10 @@ class Settings extends ChangeNotifier {
set tagSortFactor(ChipSortFactor newValue) => setAndNotify(tagSortFactorKey, newValue.toString()); set tagSortFactor(ChipSortFactor newValue) => setAndNotify(tagSortFactorKey, newValue.toString());
Set<CollectionFilter> get pinnedFilters => (_prefs.getStringList(pinnedFiltersKey) ?? []).map(CollectionFilter.fromJson).toSet();
set pinnedFilters(Set<CollectionFilter> newValue) => setAndNotify(pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList());
// info // info
EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values); EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values);

View file

@ -1,4 +1,5 @@
import 'package:aves/model/filters/album.dart'; 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/image_entry.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/album.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/utils/android_file_utils.dart';
import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/collection/empty.dart';
import 'package:aves/widgets/common/icons.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_action_delegate.dart';
import 'package:aves/widgets/filter_grids/chip_actions.dart';
import 'package:aves/widgets/filter_grids/filter_grid_page.dart'; import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
class AlbumListPage extends StatelessWidget { class AlbumListPage extends StatelessWidget {
static const routeName = '/albums'; static const routeName = '/albums';
@ -23,9 +28,9 @@ class AlbumListPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<Settings, ChipSortFactor>( return Selector<Settings, Tuple2<ChipSortFactor, Set<CollectionFilter>>>(
selector: (context, s) => s.albumSortFactor, selector: (context, s) => Tuple2(s.albumSortFactor, s.pinnedFilters),
builder: (context, sortFactor, child) { builder: (context, s, child) {
return AnimatedBuilder( return AnimatedBuilder(
animation: androidFileUtils.appNameChangeNotifier, animation: androidFileUtils.appNameChangeNotifier,
builder: (context, child) => StreamBuilder( builder: (context, child) => StreamBuilder(
@ -35,11 +40,12 @@ class AlbumListPage extends StatelessWidget {
title: 'Albums', title: 'Albums',
actionDelegate: actionDelegate, actionDelegate: actionDelegate,
filterEntries: getAlbumEntries(source), filterEntries: getAlbumEntries(source),
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)), filterBuilder: (album) => AlbumFilter(album, source.getUniqueAlbumName(album)),
emptyBuilder: () => EmptyContent( emptyBuilder: () => EmptyContent(
icon: AIcons.album, icon: AIcons.album,
text: 'No albums', text: 'No albums',
), ),
onLongPress: (filter, tapPosition) => _showMenu(context, filter, tapPosition),
), ),
), ),
); );
@ -47,38 +53,75 @@ class AlbumListPage extends StatelessWidget {
); );
} }
Future<void> _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<AlbumAction>(
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 // common with album selection page to move/copy entries
static Map<String, ImageEntry> getAlbumEntries(CollectionSource source) { static Map<String, ImageEntry> getAlbumEntries(CollectionSource source) {
final pinned = settings.pinnedFilters.whereType<AlbumFilter>().map((f) => f.album);
final entriesByDate = source.sortedEntriesForFilterList; final entriesByDate = source.sortedEntriesForFilterList;
final albums = source.sortedAlbums
.map((album) => MapEntry(
album,
entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null),
))
.toList();
switch (settings.albumSortFactor) { switch (settings.albumSortFactor) {
case ChipSortFactor.date: case ChipSortFactor.date:
albums.sort(FilterNavigationPage.compareChipByDate); final allAlbumMapEntries = source.sortedAlbums.map((album) => MapEntry(
return Map.fromEntries(albums); album,
entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null),
));
final byPin = groupBy<MapEntry<String, ImageEntry>, 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: case ChipSortFactor.name:
default: default:
final regularAlbums = <String>[], appAlbums = <String>[], specialAlbums = <String>[]; final pinnedAlbums = <String>[], regularAlbums = <String>[], appAlbums = <String>[], specialAlbums = <String>[];
for (var album in source.sortedAlbums) { for (var album in source.sortedAlbums) {
switch (androidFileUtils.getAlbumType(album)) { if (pinned.contains(album)) {
case AlbumType.regular: pinnedAlbums.add(album);
regularAlbums.add(album); } else {
break; switch (androidFileUtils.getAlbumType(album)) {
case AlbumType.app: case AlbumType.regular:
appAlbums.add(album); regularAlbums.add(album);
break; break;
default: case AlbumType.app:
specialAlbums.add(album); appAlbums.add(album);
break; 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( return MapEntry(
album, album,
entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null), entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null),

View file

@ -1,3 +1,37 @@
import 'package:aves/widgets/common/icons.dart';
import 'package:flutter/widgets.dart';
enum ChipAction { enum ChipAction {
sort, 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;
}
}

View file

@ -16,6 +16,7 @@ class DecoratedFilterChip extends StatelessWidget {
final CollectionSource source; final CollectionSource source;
final CollectionFilter filter; final CollectionFilter filter;
final ImageEntry entry; final ImageEntry entry;
final bool pinned;
final FilterCallback onTap; final FilterCallback onTap;
final OffsetFilterCallback onLongPress; final OffsetFilterCallback onLongPress;
@ -24,6 +25,7 @@ class DecoratedFilterChip extends StatelessWidget {
@required this.source, @required this.source,
@required this.filter, @required this.filter,
@required this.entry, @required this.entry,
this.pinned = false,
@required this.onTap, @required this.onTap,
this.onLongPress, this.onLongPress,
}) : super(key: key); }) : super(key: key);
@ -57,19 +59,29 @@ class DecoratedFilterChip extends StatelessWidget {
'${source.count(filter)}', '${source.count(filter)}',
style: TextStyle(color: FilterGridPage.detailColor), style: TextStyle(color: FilterGridPage.detailColor),
); );
return filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album) return Row(
? Row( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ if (pinned)
Icon( Padding(
AIcons.removableStorage, padding: EdgeInsets.only(right: 8),
size: 16, child: Icon(
color: FilterGridPage.detailColor, AIcons.pin,
), size: 16,
SizedBox(width: 8), color: FilterGridPage.detailColor,
count, ),
], ),
) if (filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album))
: count; Padding(
padding: EdgeInsets.only(right: 8),
child: Icon(
AIcons.removableStorage,
size: 16,
color: FilterGridPage.detailColor,
),
),
count,
],
);
} }
} }

View file

@ -146,6 +146,7 @@ class FilterGridPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final pinnedFilters = settings.pinnedFilters;
return MediaQueryDataProvider( return MediaQueryDataProvider(
child: Scaffold( child: Scaffold(
body: DoubleBackPopScope( body: DoubleBackPopScope(
@ -169,11 +170,13 @@ class FilterGridPage extends StatelessWidget {
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(context, i) { (context, i) {
final key = filterKeys[i]; final key = filterKeys[i];
final filter = filterBuilder(key);
final child = DecoratedFilterChip( final child = DecoratedFilterChip(
key: Key(key), key: Key(key),
source: source, source: source,
filter: filterBuilder(key), filter: filter,
entry: filterEntries[key], entry: filterEntries[key],
pinned: pinnedFilters.contains(filter),
onTap: onTap, onTap: onTap,
onLongPress: onLongPress, onLongPress: onLongPress,
); );