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/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<CollectionFilter> get pinnedFilters => (_prefs.getStringList(pinnedFiltersKey) ?? []).map(CollectionFilter.fromJson).toSet();
set pinnedFilters(Set<CollectionFilter> newValue) => setAndNotify(pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList());
// info
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/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<Settings, ChipSortFactor>(
selector: (context, s) => s.albumSortFactor,
builder: (context, sortFactor, child) {
return Selector<Settings, Tuple2<ChipSortFactor, Set<CollectionFilter>>>(
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<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
static Map<String, ImageEntry> getAlbumEntries(CollectionSource source) {
final pinned = settings.pinnedFilters.whereType<AlbumFilter>().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<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:
default:
final regularAlbums = <String>[], appAlbums = <String>[], specialAlbums = <String>[];
final pinnedAlbums = <String>[], regularAlbums = <String>[], appAlbums = <String>[], specialAlbums = <String>[];
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),

View file

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

View file

@ -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,
],
);
}
}

View file

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