albums: pin to top
This commit is contained in:
parent
15c50d5cef
commit
055cad333f
5 changed files with 137 additions and 39 deletions
|
@ -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);
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue