view settings allow changing the sort order

This commit is contained in:
Thibault Deckers 2022-09-07 14:13:47 +02:00
parent d926d943fd
commit f6d8680a17
44 changed files with 615 additions and 403 deletions

View file

@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
### Added ### Added
- Collection: view settings allow changing the sort order (aka ascending/descending)
- Collection / Info: edit title via IPTC / XMP - Collection / Info: edit title via IPTC / XMP
- Albums / Countries / Tags: size displayed in list view details, sort by size - Albums / Countries / Tags: size displayed in list view details, sort by size
- Search: `undated` and `untitled` filters - Search: `undated` and `untitled` filters

View file

@ -417,6 +417,7 @@
"viewDialogTabSort": "Sort", "viewDialogTabSort": "Sort",
"viewDialogTabGroup": "Group", "viewDialogTabGroup": "Group",
"viewDialogTabLayout": "Layout", "viewDialogTabLayout": "Layout",
"viewDialogReverseSortOrder": "Reverse sort order",
"tileLayoutGrid": "Grid", "tileLayoutGrid": "Grid",
"tileLayoutList": "List", "tileLayoutList": "List",
@ -570,6 +571,15 @@
"sortByAlbumFileName": "By album & file name", "sortByAlbumFileName": "By album & file name",
"sortByRating": "By rating", "sortByRating": "By rating",
"sortOrderNewestFirst": "Newest first",
"sortOrderOldestFirst": "Oldest first",
"sortOrderAtoZ": "A to Z",
"sortOrderZtoA": "Z to A",
"sortOrderHighestFirst": "Highest first",
"sortOrderLowestFirst": "Lowest first",
"sortOrderLargestFirst": "Largest first",
"sortOrderSmallestFirst": "Smallest first",
"albumGroupTier": "By tier", "albumGroupTier": "By tier",
"albumGroupVolume": "By storage volume", "albumGroupVolume": "By storage volume",
"albumGroupNone": "Do not group", "albumGroupNone": "Do not group",

View file

@ -5,7 +5,7 @@ import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/filters/recent.dart'; import 'package:aves/model/filters/recent.dart';
import 'package:aves/model/naming_pattern.dart'; import 'package:aves/model/naming_pattern.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart';

View file

@ -9,7 +9,7 @@ import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/defaults.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/enums/map_style.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/services/common/optional_event_channel.dart'; import 'package:aves/services/common/optional_event_channel.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves_map/aves_map.dart'; import 'package:aves_map/aves_map.dart';
@ -75,6 +75,7 @@ class Settings extends ChangeNotifier {
// collection // collection
static const collectionGroupFactorKey = 'collection_group_factor'; static const collectionGroupFactorKey = 'collection_group_factor';
static const collectionSortFactorKey = 'collection_sort_factor'; static const collectionSortFactorKey = 'collection_sort_factor';
static const collectionSortReverseKey = 'collection_sort_reverse';
static const collectionBrowsingQuickActionsKey = 'collection_browsing_quick_actions'; static const collectionBrowsingQuickActionsKey = 'collection_browsing_quick_actions';
static const collectionSelectionQuickActionsKey = 'collection_selection_quick_actions'; static const collectionSelectionQuickActionsKey = 'collection_selection_quick_actions';
static const showThumbnailFavouriteKey = 'show_thumbnail_favourite'; static const showThumbnailFavouriteKey = 'show_thumbnail_favourite';
@ -90,6 +91,9 @@ 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 albumSortReverseKey = 'album_sort_reverse';
static const countrySortReverseKey = 'country_sort_reverse';
static const tagSortReverseKey = 'tag_sort_reverse';
static const pinnedFiltersKey = 'pinned_filters'; static const pinnedFiltersKey = 'pinned_filters';
static const hiddenFiltersKey = 'hidden_filters'; static const hiddenFiltersKey = 'hidden_filters';
@ -388,6 +392,10 @@ class Settings extends ChangeNotifier {
set collectionSortFactor(EntrySortFactor newValue) => setAndNotify(collectionSortFactorKey, newValue.toString()); set collectionSortFactor(EntrySortFactor newValue) => setAndNotify(collectionSortFactorKey, newValue.toString());
bool get collectionSortReverse => getBoolOrDefault(collectionSortReverseKey, false);
set collectionSortReverse(bool newValue) => setAndNotify(collectionSortReverseKey, newValue);
List<EntrySetAction> get collectionBrowsingQuickActions => getEnumListOrDefault(collectionBrowsingQuickActionsKey, SettingsDefaults.collectionBrowsingQuickActions, EntrySetAction.values); List<EntrySetAction> get collectionBrowsingQuickActions => getEnumListOrDefault(collectionBrowsingQuickActionsKey, SettingsDefaults.collectionBrowsingQuickActions, EntrySetAction.values);
set collectionBrowsingQuickActions(List<EntrySetAction> newValue) => setAndNotify(collectionBrowsingQuickActionsKey, newValue.map((v) => v.toString()).toList()); set collectionBrowsingQuickActions(List<EntrySetAction> newValue) => setAndNotify(collectionBrowsingQuickActionsKey, newValue.map((v) => v.toString()).toList());
@ -442,6 +450,18 @@ class Settings extends ChangeNotifier {
set tagSortFactor(ChipSortFactor newValue) => setAndNotify(tagSortFactorKey, newValue.toString()); set tagSortFactor(ChipSortFactor newValue) => setAndNotify(tagSortFactorKey, newValue.toString());
bool get albumSortReverse => getBoolOrDefault(albumSortReverseKey, false);
set albumSortReverse(bool newValue) => setAndNotify(albumSortReverseKey, newValue);
bool get countrySortReverse => getBoolOrDefault(countrySortReverseKey, false);
set countrySortReverse(bool newValue) => setAndNotify(countrySortReverseKey, newValue);
bool get tagSortReverse => getBoolOrDefault(tagSortReverseKey, false);
set tagSortReverse(bool newValue) => setAndNotify(tagSortReverseKey, newValue);
Set<CollectionFilter> get pinnedFilters => (getStringList(pinnedFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet(); Set<CollectionFilter> get pinnedFilters => (getStringList(pinnedFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet();
set pinnedFilters(Set<CollectionFilter> newValue) => setAndNotify(pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList()); set pinnedFilters(Set<CollectionFilter> newValue) => setAndNotify(pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList());
@ -828,6 +848,7 @@ class Settings extends ChangeNotifier {
case confirmMoveUndatedItemsKey: case confirmMoveUndatedItemsKey:
case confirmAfterMoveToBinKey: case confirmAfterMoveToBinKey:
case setMetadataDateBeforeFileOpKey: case setMetadataDateBeforeFileOpKey:
case collectionSortReverseKey:
case showThumbnailFavouriteKey: case showThumbnailFavouriteKey:
case showThumbnailTagKey: case showThumbnailTagKey:
case showThumbnailLocationKey: case showThumbnailLocationKey:
@ -835,6 +856,9 @@ class Settings extends ChangeNotifier {
case showThumbnailRatingKey: case showThumbnailRatingKey:
case showThumbnailRawKey: case showThumbnailRawKey:
case showThumbnailVideoDurationKey: case showThumbnailVideoDurationKey:
case albumSortReverseKey:
case countrySortReverseKey:
case tagSortReverseKey:
case showOverlayOnOpeningKey: case showOverlayOnOpeningKey:
case showOverlayMinimapKey: case showOverlayMinimapKey:
case showOverlayInfoKey: case showOverlayInfoKey:

View file

@ -22,13 +22,14 @@ import 'package:aves/utils/collection_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'enums.dart'; import 'enums/enums.dart';
class CollectionLens with ChangeNotifier { class CollectionLens with ChangeNotifier {
final CollectionSource source; final CollectionSource source;
final Set<CollectionFilter> filters; final Set<CollectionFilter> filters;
EntryGroupFactor sectionFactor; EntryGroupFactor sectionFactor;
EntrySortFactor sortFactor; EntrySortFactor sortFactor;
bool sortReverse;
final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier(); final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier();
final List<StreamSubscription> _subscriptions = []; final List<StreamSubscription> _subscriptions = [];
int? id; int? id;
@ -49,7 +50,8 @@ class CollectionLens with ChangeNotifier {
this.fixedSelection, this.fixedSelection,
}) : filters = (filters ?? {}).whereNotNull().toSet(), }) : filters = (filters ?? {}).whereNotNull().toSet(),
sectionFactor = settings.collectionSectionFactor, sectionFactor = settings.collectionSectionFactor,
sortFactor = settings.collectionSortFactor { sortFactor = settings.collectionSortFactor,
sortReverse = settings.collectionSortReverse {
id ??= hashCode; id ??= hashCode;
if (listenToSource) { if (listenToSource) {
final sourceEvents = source.eventBus; final sourceEvents = source.eventBus;
@ -84,6 +86,7 @@ class CollectionLens with ChangeNotifier {
.where((event) => [ .where((event) => [
Settings.collectionSortFactorKey, Settings.collectionSortFactorKey,
Settings.collectionGroupFactorKey, Settings.collectionGroupFactorKey,
Settings.collectionSortReverseKey,
].contains(event.key)) ].contains(event.key))
.listen((_) => _onSettingsChanged())); .listen((_) => _onSettingsChanged()));
refresh(); refresh();
@ -218,6 +221,9 @@ class CollectionLens with ChangeNotifier {
_filteredSortedEntries.sort(AvesEntry.compareBySize); _filteredSortedEntries.sort(AvesEntry.compareBySize);
break; break;
} }
if (sortReverse) {
_filteredSortedEntries = _filteredSortedEntries.reversed.toList();
}
} }
void _applySection() { void _applySection() {
@ -247,7 +253,8 @@ class CollectionLens with ChangeNotifier {
break; break;
case EntrySortFactor.name: case EntrySortFactor.name:
final byAlbum = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory)); final byAlbum = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
sections = SplayTreeMap<EntryAlbumSectionKey, List<AvesEntry>>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.directory!, b.directory!)); final compare = sortReverse ? (a, b) => source.compareAlbumsByName(b.directory!, a.directory!) : (a, b) => source.compareAlbumsByName(a.directory!, b.directory!);
sections = SplayTreeMap<EntryAlbumSectionKey, List<AvesEntry>>.of(byAlbum, compare);
break; break;
case EntrySortFactor.rating: case EntrySortFactor.rating:
sections = groupBy<AvesEntry, EntryRatingSectionKey>(_filteredSortedEntries, (entry) => EntryRatingSectionKey(entry.rating)); sections = groupBy<AvesEntry, EntryRatingSectionKey>(_filteredSortedEntries, (entry) => EntryRatingSectionKey(entry.rating));
@ -281,12 +288,14 @@ class CollectionLens with ChangeNotifier {
void _onSettingsChanged() { void _onSettingsChanged() {
final newSortFactor = settings.collectionSortFactor; final newSortFactor = settings.collectionSortFactor;
final newSectionFactor = settings.collectionSectionFactor; final newSectionFactor = settings.collectionSectionFactor;
final newSortReverse = settings.collectionSortReverse;
final needSort = sortFactor != newSortFactor; final needSort = sortFactor != newSortFactor || sortReverse != newSortReverse;
final needSection = needSort || sectionFactor != newSectionFactor; final needSection = needSort || sectionFactor != newSectionFactor;
if (needSort) { if (needSort) {
sortFactor = newSortFactor; sortFactor = newSortFactor;
sortReverse = newSortReverse;
_applySort(); _applySort();
} }
if (needSection) { if (needSection) {

View file

@ -13,7 +13,7 @@ import 'package:aves/model/metadata/trash.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';
import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/analysis_controller.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/source/events.dart'; import 'package:aves/model/source/events.dart';
import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/location.dart';
import 'package:aves/model/source/tag.dart'; import 'package:aves/model/source/tag.dart';

View file

@ -0,0 +1,105 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraEntrySortFactor on EntrySortFactor {
String getName(BuildContext context) {
final l10n = context.l10n;
switch (this) {
case EntrySortFactor.date:
return l10n.sortByDate;
case EntrySortFactor.name:
return l10n.sortByAlbumFileName;
case EntrySortFactor.rating:
return l10n.sortByRating;
case EntrySortFactor.size:
return l10n.sortBySize;
}
}
String getOrderName(BuildContext context, bool reverse) {
final l10n = context.l10n;
switch (this) {
case EntrySortFactor.date:
return reverse ? l10n.sortOrderOldestFirst : l10n.sortOrderNewestFirst;
case EntrySortFactor.name:
return reverse ? l10n.sortOrderZtoA : l10n.sortOrderAtoZ;
case EntrySortFactor.rating:
return reverse ? l10n.sortOrderLowestFirst : l10n.sortOrderHighestFirst;
case EntrySortFactor.size:
return reverse ? l10n.sortOrderSmallestFirst : l10n.sortOrderLargestFirst;
}
}
}
extension ExtraChipSortFactor on ChipSortFactor {
String getName(BuildContext context) {
final l10n = context.l10n;
switch (this) {
case ChipSortFactor.date:
return l10n.sortByDate;
case ChipSortFactor.name:
return l10n.sortByName;
case ChipSortFactor.count:
return l10n.sortByItemCount;
case ChipSortFactor.size:
return l10n.sortBySize;
}
}
String getOrderName(BuildContext context, bool reverse) {
final l10n = context.l10n;
switch (this) {
case ChipSortFactor.date:
return reverse ? l10n.sortOrderOldestFirst : l10n.sortOrderNewestFirst;
case ChipSortFactor.name:
return reverse ? l10n.sortOrderZtoA : l10n.sortOrderAtoZ;
case ChipSortFactor.count:
case ChipSortFactor.size:
return reverse ? l10n.sortOrderSmallestFirst : l10n.sortOrderLargestFirst;
}
}
}
extension ExtraEntryGroupFactor on EntryGroupFactor {
String getName(BuildContext context) {
final l10n = context.l10n;
switch (this) {
case EntryGroupFactor.album:
return l10n.collectionGroupAlbum;
case EntryGroupFactor.month:
return l10n.collectionGroupMonth;
case EntryGroupFactor.day:
return l10n.collectionGroupDay;
case EntryGroupFactor.none:
return l10n.collectionGroupNone;
}
}
}
extension ExtraAlbumChipGroupFactor on AlbumChipGroupFactor {
String getName(BuildContext context) {
final l10n = context.l10n;
switch (this) {
case AlbumChipGroupFactor.importance:
return l10n.albumGroupTier;
case AlbumChipGroupFactor.volume:
return l10n.albumGroupVolume;
case AlbumChipGroupFactor.none:
return l10n.albumGroupNone;
}
}
}
extension ExtraTileLayout on TileLayout {
String getName(BuildContext context) {
final l10n = context.l10n;
switch (this) {
case TileLayout.grid:
return l10n.tileLayoutGrid;
case TileLayout.list:
return l10n.tileLayoutList;
}
}
}

View file

@ -7,7 +7,7 @@ import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/analysis_controller.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/collection_utils.dart'; import 'package:aves/utils/collection_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';

View file

@ -7,7 +7,7 @@ import 'package:aves/model/favourites.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/analysis_controller.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';

View file

@ -1,5 +1,5 @@
import 'package:aves/l10n/l10n.dart'; import 'package:aves/l10n/l10n.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
extension ExtraSourceState on SourceState { extension ExtraSourceState on SourceState {
String? getName(AppLocalizations l10n) { String? getName(AppLocalizations l10n) {

View file

@ -3,7 +3,7 @@ import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/analysis_controller.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/collection_utils.dart'; import 'package:aves/utils/collection_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';

View file

@ -4,7 +4,7 @@ import 'dart:ui';
import 'package:aves/l10n/l10n.dart'; import 'package:aves/l10n/l10n.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/analysis_controller.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/source/media_store_source.dart'; import 'package:aves/model/source/media_store_source.dart';
import 'package:aves/model/source/source_state.dart'; import 'package:aves/model/source/source_state.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';

View file

@ -51,6 +51,7 @@ class AIcons {
static const IconData group = Icons.group_work_outlined; static const IconData group = Icons.group_work_outlined;
static const IconData layout = Icons.grid_view_outlined; static const IconData layout = Icons.grid_view_outlined;
static const IconData sort = Icons.sort_outlined; static const IconData sort = Icons.sort_outlined;
static const IconData sortOrder = Icons.swap_vert_outlined;
// actions // actions
static const IconData add = Icons.add_circle_outline; static const IconData add = Icons.add_circle_outline;

View file

@ -12,7 +12,8 @@ import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/source/enums/view.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/collection_page.dart';
@ -67,6 +68,25 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
bool get showFilterBar => visibleFilters.isNotEmpty; bool get showFilterBar => visibleFilters.isNotEmpty;
static const _sortOptions = [
EntrySortFactor.date,
EntrySortFactor.size,
EntrySortFactor.name,
EntrySortFactor.rating,
];
static const _groupOptions = [
EntryGroupFactor.album,
EntryGroupFactor.month,
EntryGroupFactor.day,
EntryGroupFactor.none,
];
static const _layoutOptions = [
TileLayout.grid,
TileLayout.list,
];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -506,33 +526,22 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
} }
Future<void> _configureView() async { Future<void> _configureView() async {
final initialValue = Tuple3( final initialValue = Tuple4(
settings.collectionSortFactor, settings.collectionSortFactor,
settings.collectionSectionFactor, settings.collectionSectionFactor,
settings.getTileLayout(CollectionPage.routeName), settings.getTileLayout(CollectionPage.routeName),
settings.collectionSortReverse,
); );
final value = await showDialog<Tuple3<EntrySortFactor?, EntryGroupFactor?, TileLayout?>>( final value = await showDialog<Tuple4<EntrySortFactor?, EntryGroupFactor?, TileLayout?, bool>>(
context: context, context: context,
builder: (context) { builder: (context) {
final l10n = context.l10n;
return TileViewDialog<EntrySortFactor, EntryGroupFactor, TileLayout>( return TileViewDialog<EntrySortFactor, EntryGroupFactor, TileLayout>(
initialValue: initialValue, initialValue: initialValue,
sortOptions: { sortOptions: Map.fromEntries(_sortOptions.map((v) => MapEntry(v, v.getName(context)))),
EntrySortFactor.date: l10n.sortByDate, groupOptions: Map.fromEntries(_groupOptions.map((v) => MapEntry(v, v.getName(context)))),
EntrySortFactor.size: l10n.sortBySize, layoutOptions: Map.fromEntries(_layoutOptions.map((v) => MapEntry(v, v.getName(context)))),
EntrySortFactor.name: l10n.sortByAlbumFileName, sortOrder: (factor, reverse) => factor.getOrderName(context, reverse),
EntrySortFactor.rating: l10n.sortByRating, canGroup: (s, g, l) => s == EntrySortFactor.date,
},
groupOptions: {
EntryGroupFactor.album: l10n.collectionGroupAlbum,
EntryGroupFactor.month: l10n.collectionGroupMonth,
EntryGroupFactor.day: l10n.collectionGroupDay,
EntryGroupFactor.none: l10n.collectionGroupNone,
},
layoutOptions: {
TileLayout.grid: l10n.tileLayoutGrid,
TileLayout.list: l10n.tileLayoutList,
},
); );
}, },
); );
@ -542,6 +551,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
settings.collectionSortFactor = value.item1!; settings.collectionSortFactor = value.item1!;
settings.collectionSectionFactor = value.item2!; settings.collectionSectionFactor = value.item2!;
settings.setTileLayout(CollectionPage.routeName, value.item3!); settings.setTileLayout(CollectionPage.routeName, value.item3!);
settings.collectionSortReverse = value.item4;
} }
} }

View file

@ -7,7 +7,7 @@ import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/section_keys.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
@ -542,7 +542,7 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
final oldest = lastKey.date; final oldest = lastKey.date;
if (newest != null && oldest != null) { if (newest != null && oldest != null) {
final localeName = context.l10n.localeName; final localeName = context.l10n.localeName;
final dateFormat = newest.difference(oldest).inDays > 365 ? DateFormat.y(localeName) : DateFormat.MMM(localeName); final dateFormat = (newest.difference(oldest).inDays).abs() > 365 ? DateFormat.y(localeName) : DateFormat.MMM(localeName);
String? lastLabel; String? lastLabel;
sectionLayouts.forEach((section) { sectionLayouts.forEach((section) {
final date = (section.sectionKey as EntryDateSectionKey).date; final date = (section.sectionKey as EntryDateSectionKey).date;

View file

@ -2,7 +2,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/rating.dart'; import 'package:aves/model/filters/rating.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/utils/file_utils.dart'; import 'package:aves/utils/file_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/grid/draggable_thumb_label.dart'; import 'package:aves/widgets/common/grid/draggable_thumb_label.dart';

View file

@ -3,7 +3,7 @@ import 'dart:math';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/section_keys.dart';
import 'package:aves/widgets/collection/grid/headers/album.dart'; import 'package:aves/widgets/collection/grid/headers/album.dart';
import 'package:aves/widgets/collection/grid/headers/date.dart'; import 'package:aves/widgets/collection/grid/headers/date.dart';

View file

@ -2,7 +2,7 @@ import 'package:aves/app_mode.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/selection.dart'; import 'package:aves/model/selection.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/services/intent_service.dart'; import 'package:aves/services/intent_service.dart';
import 'package:aves/widgets/collection/grid/list_details.dart'; import 'package:aves/widgets/collection/grid/list_details.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart'; import 'package:aves/widgets/collection/grid/list_details_theme.dart';

View file

@ -1,7 +1,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/source/events.dart'; import 'package:aves/model/source/events.dart';
import 'package:aves/model/source/source_state.dart'; import 'package:aves/model/source/source_state.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';

View file

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
class TextDropdownButton<T> extends DropdownButton<T> {
TextDropdownButton({
super.key,
required List<T> values,
required String Function(T value) valueText,
super.value,
super.hint,
super.disabledHint,
required super.onChanged,
super.onTap,
super.elevation,
super.style,
super.underline,
super.icon,
super.iconDisabledColor,
super.iconEnabledColor,
super.iconSize,
super.isDense,
super.isExpanded,
super.itemHeight,
super.focusColor,
super.focusNode,
super.autofocus,
super.dropdownColor,
super.menuMaxHeight,
super.enableFeedback,
super.alignment,
super.borderRadius,
}) : super(
items: values
.map((v) => DropdownMenuItem<T>(
value: v,
child: Text(valueText(v)),
))
.toList(),
selectedItemBuilder: (context) => values
.map((v) => DropdownMenuItem<T>(
value: v,
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
valueText(v),
softWrap: false,
overflow: TextOverflow.fade,
),
),
))
.toList(),
);
}

View file

@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/common/grid/section_layout.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';

View file

@ -1,7 +1,7 @@
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/behaviour/eager_scale_gesture_recognizer.dart'; import 'package:aves/widgets/common/behaviour/eager_scale_gesture_recognizer.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';

View file

@ -1,6 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/section_keys.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';

View file

@ -7,6 +7,7 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/theme/format.dart'; import 'package:aves/theme/format.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/theme/themes.dart'; import 'package:aves/theme/themes.dart';
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
import 'package:aves/widgets/common/basic/wheel.dart'; import 'package:aves/widgets/common/basic/wheel.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
@ -80,13 +81,9 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
scrollableContent: [ scrollableContent: [
Padding( Padding(
padding: const EdgeInsets.only(left: 16, top: 8, right: 16), padding: const EdgeInsets.only(left: 16, top: 8, right: 16),
child: DropdownButton<DateEditAction>( child: TextDropdownButton<DateEditAction>(
items: DateEditAction.values values: DateEditAction.values,
.map((v) => DropdownMenuItem<DateEditAction>( valueText: (v) => _actionText(context, v),
value: v,
child: Text(_actionText(context, v)),
))
.toList(),
value: _action, value: _action,
onChanged: (v) => setState(() => _action = v!), onChanged: (v) => setState(() => _action = v!),
isExpanded: true, isExpanded: true,
@ -159,23 +156,9 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
Widget _buildCopyFieldContent(BuildContext context) { Widget _buildCopyFieldContent(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.only(left: 16, top: 0, right: 16), padding: const EdgeInsets.only(left: 16, top: 0, right: 16),
child: DropdownButton<DateFieldSource>( child: TextDropdownButton<DateFieldSource>(
items: DateFieldSource.values values: DateFieldSource.values,
.map((v) => DropdownMenuItem<DateFieldSource>( valueText: (v) => _setSourceText(context, v),
value: v,
child: Text(_setSourceText(context, v)),
))
.toList(),
selectedItemBuilder: (context) => DateFieldSource.values
.map((v) => DropdownMenuItem<DateFieldSource>(
value: v,
child: Text(
_setSourceText(context, v),
softWrap: false,
overflow: TextOverflow.fade,
),
))
.toList(),
value: _copyFieldSource, value: _copyFieldSource,
onChanged: (v) => setState(() => _copyFieldSource = v!), onChanged: (v) => setState(() => _copyFieldSource = v!),
isExpanded: true, isExpanded: true,

View file

@ -2,6 +2,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/media/media_edit_service.dart'; import 'package:aves/services/media/media_edit_service.dart';
import 'package:aves/utils/mime_utils.dart'; import 'package:aves/utils/mime_utils.dart';
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -63,13 +64,9 @@ class _ExportEntryDialogState extends State<ExportEntryDialog> {
children: [ children: [
Text(l10n.exportEntryDialogFormat), Text(l10n.exportEntryDialogFormat),
const SizedBox(width: AvesDialog.controlCaptionPadding), const SizedBox(width: AvesDialog.controlCaptionPadding),
DropdownButton<String>( TextDropdownButton<String>(
items: imageExportFormats.map((mimeType) { values: imageExportFormats,
return DropdownMenuItem<String>( valueText: MimeUtils.displayType,
value: mimeType,
child: Text(MimeUtils.displayType(mimeType)),
);
}).toList(),
value: _mimeType, value: _mimeType,
onChanged: (selected) { onChanged: (selected) {
if (selected != null) { if (selected != null) {

View file

@ -1,19 +1,22 @@
import 'dart:math'; import 'package:aves/theme/durations.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/highlight_title.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'aves_dialog.dart'; import 'aves_dialog.dart';
class TileViewDialog<S, G, L> extends StatefulWidget { class TileViewDialog<S, G, L> extends StatefulWidget {
final Tuple3<S?, G?, L?> initialValue; final Tuple4<S?, G?, L?, bool> initialValue;
final Map<S, String> sortOptions; final Map<S, String> sortOptions;
final Map<G, String> groupOptions; final Map<G, String> groupOptions;
final Map<L, String> layoutOptions; final Map<L, String> layoutOptions;
final String Function(S sort, bool reverse) sortOrder;
final bool Function(S? sort, G? group, L? layout)? canGroup;
const TileViewDialog({ const TileViewDialog({
super.key, super.key,
@ -21,6 +24,8 @@ class TileViewDialog<S, G, L> extends StatefulWidget {
this.sortOptions = const {}, this.sortOptions = const {},
this.groupOptions = const {}, this.groupOptions = const {},
this.layoutOptions = const {}, this.layoutOptions = const {},
required this.sortOrder,
this.canGroup,
}); });
@override @override
@ -31,8 +36,7 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
late S? _selectedSort; late S? _selectedSort;
late G? _selectedGroup; late G? _selectedGroup;
late L? _selectedLayout; late L? _selectedLayout;
late final TabController _tabController; late bool _reverseSort;
late final String _optionLines;
Map<S, String> get sortOptions => widget.sortOptions; Map<S, String> get sortOptions => widget.sortOptions;
@ -40,11 +44,7 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
Map<L, String> get layoutOptions => widget.layoutOptions; Map<L, String> get layoutOptions => widget.layoutOptions;
static const int groupTabIndex = 1; bool get canGroup => (widget.canGroup ?? (s, g, l) => true).call(_selectedSort, _selectedGroup, _selectedLayout);
double tabBarHeight(BuildContext context) => 64 * max(1, MediaQuery.textScaleFactorOf(context));
static const double tabIndicatorWeight = 2;
@override @override
void initState() { void initState() {
@ -53,259 +53,140 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
_selectedSort = initialValue.item1; _selectedSort = initialValue.item1;
_selectedGroup = initialValue.item2; _selectedGroup = initialValue.item2;
_selectedLayout = initialValue.item3; _selectedLayout = initialValue.item3;
_reverseSort = initialValue.item4;
final allOptions = [
sortOptions,
groupOptions,
layoutOptions,
];
final tabCount = allOptions.where((options) => options.isNotEmpty).length;
_tabController = TabController(length: tabCount, vsync: this);
_tabController.addListener(_onTabChange);
_optionLines = allOptions.expand((v) => v.values).fold('', (previousValue, element) => '$previousValue\n$element');
}
@override
void dispose() {
_tabController.removeListener(_onTabChange);
_tabController.dispose();
super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
final tabs = <Tuple2<Tab, Widget>>[
if (sortOptions.isNotEmpty) return AvesDialog(
Tuple2( scrollableContent: [
_buildTab( _buildSection(
context, icon: AIcons.sort,
const Key('tab-sort'), title: l10n.viewDialogTabSort,
AIcons.sort, trailing: IconButton(
l10n.viewDialogTabSort, icon: const Icon(AIcons.sortOrder),
onPressed: () => setState(() => _reverseSort = !_reverseSort),
tooltip: l10n.viewDialogReverseSortOrder,
), ),
Column( options: sortOptions,
children: sortOptions.entries value: _selectedSort,
.map((kv) => _buildRadioListTile<S>( onChanged: (v) {
kv.key, _selectedSort = v;
kv.value, _reverseSort = false;
() => _selectedSort, },
(v) => _selectedSort = v, bottom: _selectedSort != null
)) ? Text(
.toList(), widget.sortOrder(_selectedSort as S, _reverseSort),
style: Theme.of(context).textTheme.caption,
)
: null,
),
AnimatedSwitcher(
duration: context.read<DurationsData>().formTransition,
switchInCurve: Curves.easeInOutCubic,
switchOutCurve: Curves.easeInOutCubic,
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axisAlignment: -1,
child: child,
),
),
child: _buildSection(
show: canGroup,
icon: AIcons.group,
title: l10n.viewDialogTabGroup,
options: groupOptions,
value: _selectedGroup,
onChanged: (v) => _selectedGroup = v,
), ),
), ),
if (groupOptions.isNotEmpty) _buildSection(
Tuple2( icon: AIcons.layout,
_buildTab( title: l10n.viewDialogTabLayout,
context, options: layoutOptions,
const Key('tab-group'), value: _selectedLayout,
AIcons.group, onChanged: (v) => _selectedLayout = v,
l10n.viewDialogTabGroup,
color: canGroup ? null : Theme.of(context).disabledColor,
),
Column(
children: groupOptions.entries
.map((kv) => _buildRadioListTile<G>(
kv.key,
kv.value,
() => _selectedGroup,
(v) => _selectedGroup = v,
))
.toList(),
),
), ),
if (layoutOptions.isNotEmpty) ],
Tuple2( actions: [
_buildTab( TextButton(
context, onPressed: () => Navigator.pop(context),
const Key('tab-layout'), child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
AIcons.layout,
l10n.viewDialogTabLayout,
),
Column(
children: layoutOptions.entries
.map((kv) => _buildRadioListTile<L>(
kv.key,
kv.value,
() => _selectedLayout,
(v) => _selectedLayout = v,
))
.toList(),
),
), ),
]; TextButton(
key: const Key('button-apply'),
final contentWidget = DecoratedBox( onPressed: () => Navigator.pop(context, Tuple4(_selectedSort, _selectedGroup, _selectedLayout, _reverseSort)),
decoration: AvesDialog.contentDecoration(context), child: Text(l10n.applyButtonLabel),
child: LayoutBuilder( )
builder: (context, constraints) { ],
final availableBodyHeight = constraints.maxHeight - tabBarHeight(context) - tabIndicatorWeight;
final maxHeight = min(availableBodyHeight, tabBodyMaxHeight(context));
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Material(
borderRadius: const BorderRadius.vertical(
top: AvesDialog.cornerRadius,
),
clipBehavior: Clip.antiAlias,
child: TabBar(
indicatorWeight: tabIndicatorWeight,
tabs: tabs.map((t) => t.item1).toList(),
controller: _tabController,
),
),
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: maxHeight,
),
child: TabBarView(
controller: _tabController,
physics: const NeverScrollableScrollPhysics(),
children: tabs
.map((t) => SingleChildScrollView(
child: t.item2,
))
.toList(),
),
),
],
);
},
),
);
final actionsWidget = Padding(
padding: AvesDialog.actionsPadding,
child: OverflowBar(
alignment: MainAxisAlignment.end,
spacing: AvesDialog.buttonPadding.horizontal / 2,
overflowAlignment: OverflowBarAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
),
TextButton(
key: const Key('button-apply'),
onPressed: () => Navigator.pop(context, Tuple3(_selectedSort, _selectedGroup, _selectedLayout)),
child: Text(l10n.applyButtonLabel),
)
],
),
);
Widget dialogChild = LayoutBuilder(
builder: (context, constraints) {
final availableBodyWidth = constraints.maxWidth;
final maxWidth = min(availableBodyWidth, tabBodyMaxWidth(context));
return ConstrainedBox(
constraints: BoxConstraints(
maxWidth: maxWidth,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Flexible(child: contentWidget),
actionsWidget,
],
),
);
},
);
return Dialog(
shape: AvesDialog.shape(context),
child: dialogChild,
); );
} }
Widget _buildRadioListTile<T>(T value, String title, T? Function() get, void Function(T value) set) { Widget _buildSection<T>({
return RadioListTile<T>( bool show = true,
// key is expected by test driver required IconData icon,
key: Key(value.toString()), required String title,
value: value, Widget? trailing,
groupValue: get(), required Map<T, String> options,
onChanged: (v) => setState(() => set(v as T)), required T value,
title: Text( required ValueChanged<T?> onChanged,
title, Widget? bottom,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
);
}
// tabs
Tab _buildTab(
BuildContext context,
Key key,
IconData icon,
String text, {
Color? color,
}) { }) {
// cannot use `IconTheme` over `TabBar` to change size, if (options.isEmpty || !show) return const SizedBox();
// because `TabBar` does so internally
final textScaleFactor = MediaQuery.textScaleFactorOf(context); final iconSize = IconTheme.of(context).size! * MediaQuery.textScaleFactorOf(context);
final iconSize = IconTheme.of(context).size! * textScaleFactor; return TooltipTheme(
return Tab( data: TooltipTheme.of(context).copyWith(
key: key, preferBelow: false,
height: tabBarHeight(context), ),
child: Column( child: Padding(
mainAxisSize: MainAxisSize.min, padding: const EdgeInsets.symmetric(horizontal: 16),
children: [ child: Column(
Icon( mainAxisSize: MainAxisSize.min,
icon, crossAxisAlignment: CrossAxisAlignment.start,
size: iconSize, children: [
color: color, const SizedBox(height: 8),
), ConstrainedBox(
const SizedBox(height: 4), constraints: const BoxConstraints(
Text( minHeight: kMinInteractiveDimension,
text, ),
style: TextStyle(color: color), child: Row(
softWrap: false, children: [
overflow: TextOverflow.fade, Icon(icon),
), const SizedBox(width: 16),
], Expanded(
child: HighlightTitle(
title: title,
showHighlight: false,
),
),
if (trailing != null) trailing,
],
),
),
Padding(
padding: EdgeInsetsDirectional.only(start: iconSize + 16, end: 12),
child: TextDropdownButton<T>(
values: options.keys.toList(),
valueText: (v) => options[v] ?? v.toString(),
value: value,
onChanged: (v) => setState(() => onChanged(v)),
isExpanded: true,
dropdownColor: Themes.thirdLayerColor(context),
),
),
if (bottom != null)
Padding(
padding: EdgeInsetsDirectional.only(start: iconSize + 16),
child: bottom,
),
],
),
), ),
); );
} }
bool get canGroup => _selectedSort == EntrySortFactor.date || _selectedSort is ChipSortFactor;
void _onTabChange() {
if (!canGroup && _tabController.index == groupTabIndex) {
_tabController.index = _tabController.previousIndex;
}
}
// based on `ListTile` height computation (one line, no subtitle, not dense)
double singleOptionTileHeight(BuildContext context) => 56.0 + Theme.of(context).visualDensity.baseSizeAdjustment.dy;
double tabBodyMaxWidth(BuildContext context) {
final para = RenderParagraph(
TextSpan(text: _optionLines, style: Theme.of(context).textTheme.subtitle1!),
textDirection: TextDirection.ltr,
textScaleFactor: MediaQuery.textScaleFactorOf(context),
)..layout(const BoxConstraints(), parentUsesSize: true);
final textWidth = para.getMaxIntrinsicWidth(double.infinity);
// from `RadioListTile` layout
const contentPadding = 32;
const leadingWidth = kMinInteractiveDimension + 8;
return contentPadding + leadingWidth + textWidth;
}
double tabBodyMaxHeight(BuildContext context) =>
[
sortOptions,
groupOptions,
layoutOptions,
].map((v) => v.length).fold(0, max) *
singleOptionTileHeight(context);
} }

View file

@ -2,6 +2,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/ref/languages.dart'; import 'package:aves/ref/languages.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/theme/themes.dart'; import 'package:aves/theme/themes.dart';
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves/widgets/viewer/video/controller.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -123,25 +124,6 @@ class _VideoStreamSelectionDialogState extends State<VideoStreamSelectionDialog>
return common; return common;
} }
DropdownMenuItem<StreamSummary> _buildMenuItem(StreamSummary? value) {
return DropdownMenuItem(
value: value,
child: Text(_streamName(value)),
);
}
Widget _buildSelectedItem(StreamSummary? v) {
return Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
_streamName(v),
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
);
}
List<Widget> _buildSection({ List<Widget> _buildSection({
required IconData icon, required IconData icon,
required String title, required String title,
@ -162,9 +144,9 @@ class _VideoStreamSelectionDialogState extends State<VideoStreamSelectionDialog>
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: DropdownButton<StreamSummary>( child: TextDropdownButton<StreamSummary>(
items: streams.map(_buildMenuItem).toList(), values: streams.whereNotNull().toList(),
selectedItemBuilder: (context) => streams.map(_buildSelectedItem).toList(), valueText: _streamName,
value: current, value: current,
onChanged: streams.length > 1 ? (newValue) => setState(() => setter(newValue)) : null, onChanged: streams.length > 1 ? (newValue) => setState(() => setter(newValue)) : null,
isExpanded: true, isExpanded: true,

View file

@ -7,7 +7,7 @@ import 'package:aves/model/selection.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';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/menu.dart';

View file

@ -4,7 +4,7 @@ import 'package:aves/model/filters/filters.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';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
@ -25,12 +25,12 @@ class AlbumListPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
return Selector<Settings, Tuple3<AlbumChipGroupFactor, ChipSortFactor, Set<CollectionFilter>>>( return Selector<Settings, Tuple4<AlbumChipGroupFactor, ChipSortFactor, bool, Set<CollectionFilter>>>(
selector: (context, s) => Tuple3(s.albumGroupFactor, s.albumSortFactor, s.pinnedFilters), selector: (context, s) => Tuple4(s.albumGroupFactor, s.albumSortFactor, s.albumSortReverse, s.pinnedFilters),
shouldRebuild: (t1, t2) { shouldRebuild: (t1, t2) {
// `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within `TupleN` // `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within `TupleN`
const eq = DeepCollectionEquality(); const eq = DeepCollectionEquality();
return !(eq.equals(t1.item1, t2.item1) && eq.equals(t1.item2, t2.item2) && eq.equals(t1.item3, t2.item3)); return !(eq.equals(t1.item1, t2.item1) && eq.equals(t1.item2, t2.item2) && eq.equals(t1.item3, t2.item3) && eq.equals(t1.item4, t2.item4));
}, },
builder: (context, s, child) { builder: (context, s, child) {
return ValueListenableBuilder<bool>( return ValueListenableBuilder<bool>(
@ -75,7 +75,7 @@ class AlbumListPage extends StatelessWidget {
static List<FilterGridItem<AlbumFilter>> getAlbumGridItems(BuildContext context, CollectionSource source) { static List<FilterGridItem<AlbumFilter>> getAlbumGridItems(BuildContext context, CollectionSource source) {
final filters = source.rawAlbums.map((album) => AlbumFilter(album, source.getAlbumDisplayName(context, album))).toSet(); final filters = source.rawAlbums.map((album) => AlbumFilter(album, source.getAlbumDisplayName(context, album))).toSet();
return FilterNavigationPage.sort(settings.albumSortFactor, source, filters); return FilterNavigationPage.sort(settings.albumSortFactor, settings.albumSortReverse, source, filters);
} }
static Map<ChipSectionKey, List<FilterGridItem<AlbumFilter>>> groupToSections(BuildContext context, CollectionSource source, Iterable<FilterGridItem<AlbumFilter>> sortedMapEntries) { static Map<ChipSectionKey, List<FilterGridItem<AlbumFilter>>> groupToSections(BuildContext context, CollectionSource source, Iterable<FilterGridItem<AlbumFilter>> sortedMapEntries) {

View file

@ -9,7 +9,8 @@ import 'package:aves/model/highlight.dart';
import 'package:aves/model/selection.dart'; import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/source/enums/view.dart';
import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/image_op_events.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/services/media/enums.dart'; import 'package:aves/services/media/enums.dart';
@ -44,12 +45,24 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
@override @override
set sortFactor(ChipSortFactor factor) => settings.albumSortFactor = factor; set sortFactor(ChipSortFactor factor) => settings.albumSortFactor = factor;
@override
bool get sortReverse => settings.albumSortReverse;
@override
set sortReverse(bool value) => settings.albumSortReverse = value;
@override @override
TileLayout get tileLayout => settings.getTileLayout(AlbumListPage.routeName); TileLayout get tileLayout => settings.getTileLayout(AlbumListPage.routeName);
@override @override
set tileLayout(TileLayout tileLayout) => settings.setTileLayout(AlbumListPage.routeName, tileLayout); set tileLayout(TileLayout tileLayout) => settings.setTileLayout(AlbumListPage.routeName, tileLayout);
static const _groupOptions = [
AlbumChipGroupFactor.importance,
AlbumChipGroupFactor.volume,
AlbumChipGroupFactor.none,
];
@override @override
bool isVisible( bool isVisible(
ChipSetAction action, { ChipSetAction action, {
@ -125,32 +138,21 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
@override @override
Future<void> configureView(BuildContext context) async { Future<void> configureView(BuildContext context) async {
final initialValue = Tuple3( final initialValue = Tuple4(
sortFactor, sortFactor,
settings.albumGroupFactor, settings.albumGroupFactor,
tileLayout, tileLayout,
sortReverse,
); );
final value = await showDialog<Tuple3<ChipSortFactor?, AlbumChipGroupFactor?, TileLayout?>>( final value = await showDialog<Tuple4<ChipSortFactor?, AlbumChipGroupFactor?, TileLayout?, bool>>(
context: context, context: context,
builder: (context) { builder: (context) {
final l10n = context.l10n;
return TileViewDialog<ChipSortFactor, AlbumChipGroupFactor, TileLayout>( return TileViewDialog<ChipSortFactor, AlbumChipGroupFactor, TileLayout>(
initialValue: initialValue, initialValue: initialValue,
sortOptions: { sortOptions: Map.fromEntries(ChipSetActionDelegate.sortOptions.map((v) => MapEntry(v, v.getName(context)))),
ChipSortFactor.date: l10n.sortByDate, groupOptions: Map.fromEntries(_groupOptions.map((v) => MapEntry(v, v.getName(context)))),
ChipSortFactor.name: l10n.sortByName, layoutOptions: Map.fromEntries(ChipSetActionDelegate.layoutOptions.map((v) => MapEntry(v, v.getName(context)))),
ChipSortFactor.count: l10n.sortByItemCount, sortOrder: (factor, reverse) => factor.getOrderName(context, reverse),
ChipSortFactor.size: l10n.sortBySize,
},
groupOptions: {
AlbumChipGroupFactor.importance: l10n.albumGroupTier,
AlbumChipGroupFactor.volume: l10n.albumGroupVolume,
AlbumChipGroupFactor.none: l10n.albumGroupNone,
},
layoutOptions: {
TileLayout.grid: l10n.tileLayoutGrid,
TileLayout.list: l10n.tileLayoutList,
},
); );
}, },
); );
@ -160,6 +162,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
sortFactor = value.item1!; sortFactor = value.item1!;
settings.albumGroupFactor = value.item2!; settings.albumGroupFactor = value.item2!;
tileLayout = value.item3!; tileLayout = value.item3!;
sortReverse = value.item4;
} }
} }

View file

@ -9,7 +9,8 @@ import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/source/enums/view.dart';
import 'package:aves/theme/colors.dart'; import 'package:aves/theme/colors.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart';
@ -37,10 +38,26 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
set sortFactor(ChipSortFactor factor); set sortFactor(ChipSortFactor factor);
bool get sortReverse;
set sortReverse(bool value);
TileLayout get tileLayout; TileLayout get tileLayout;
set tileLayout(TileLayout tileLayout); set tileLayout(TileLayout tileLayout);
static const sortOptions = [
ChipSortFactor.date,
ChipSortFactor.name,
ChipSortFactor.count,
ChipSortFactor.size,
];
static const layoutOptions = [
TileLayout.grid,
TileLayout.list,
];
bool isVisible( bool isVisible(
ChipSetAction action, { ChipSetAction action, {
required AppMode appMode, required AppMode appMode,
@ -194,27 +211,20 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
} }
Future<void> configureView(BuildContext context) async { Future<void> configureView(BuildContext context) async {
final initialValue = Tuple3( final initialValue = Tuple4(
sortFactor, sortFactor,
null, null,
tileLayout, tileLayout,
sortReverse,
); );
final value = await showDialog<Tuple3<ChipSortFactor?, void, TileLayout?>>( final value = await showDialog<Tuple4<ChipSortFactor?, void, TileLayout?, bool>>(
context: context, context: context,
builder: (context) { builder: (context) {
final l10n = context.l10n;
return TileViewDialog<ChipSortFactor, void, TileLayout>( return TileViewDialog<ChipSortFactor, void, TileLayout>(
initialValue: initialValue, initialValue: initialValue,
sortOptions: { sortOptions: Map.fromEntries(sortOptions.map((v) => MapEntry(v, v.getName(context)))),
ChipSortFactor.date: l10n.sortByDate, layoutOptions: Map.fromEntries(layoutOptions.map((v) => MapEntry(v, v.getName(context)))),
ChipSortFactor.name: l10n.sortByName, sortOrder: (factor, reverse) => factor.getOrderName(context, reverse),
ChipSortFactor.count: l10n.sortByItemCount,
ChipSortFactor.size: l10n.sortBySize,
},
layoutOptions: {
TileLayout.grid: l10n.tileLayoutGrid,
TileLayout.list: l10n.tileLayoutList,
},
); );
}, },
); );
@ -223,6 +233,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
if (value != null && initialValue != value) { if (value != null && initialValue != value) {
sortFactor = value.item1!; sortFactor = value.item1!;
tileLayout = value.item3!; tileLayout = value.item3!;
sortReverse = value.item4;
} }
} }

View file

@ -1,7 +1,7 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/location.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart';
@ -19,6 +19,12 @@ class CountryChipSetActionDelegate extends ChipSetActionDelegate<LocationFilter>
@override @override
set sortFactor(ChipSortFactor factor) => settings.countrySortFactor = factor; set sortFactor(ChipSortFactor factor) => settings.countrySortFactor = factor;
@override
bool get sortReverse => settings.countrySortReverse;
@override
set sortReverse(bool value) => settings.countrySortReverse = value;
@override @override
TileLayout get tileLayout => settings.getTileLayout(CountryListPage.routeName); TileLayout get tileLayout => settings.getTileLayout(CountryListPage.routeName);

View file

@ -1,7 +1,7 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart';
@ -19,6 +19,12 @@ class TagChipSetActionDelegate extends ChipSetActionDelegate<TagFilter> {
@override @override
set sortFactor(ChipSortFactor factor) => settings.tagSortFactor = factor; set sortFactor(ChipSortFactor factor) => settings.tagSortFactor = factor;
@override
bool get sortReverse => settings.tagSortReverse;
@override
set sortReverse(bool value) => settings.tagSortReverse = value;
@override @override
TileLayout get tileLayout => settings.getTileLayout(TagListPage.routeName); TileLayout get tileLayout => settings.getTileLayout(TagListPage.routeName);

View file

@ -1,6 +1,6 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/utils/file_utils.dart'; import 'package:aves/utils/file_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/grid/draggable_thumb_label.dart'; import 'package:aves/widgets/common/grid/draggable_thumb_label.dart';

View file

@ -6,7 +6,7 @@ import 'package:aves/model/highlight.dart';
import 'package:aves/model/query.dart'; import 'package:aves/model/query.dart';
import 'package:aves/model/selection.dart'; import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/insets.dart';

View file

@ -1,6 +1,6 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/utils/time_utils.dart'; import 'package:aves/utils/time_utils.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/providers/selection_provider.dart'; import 'package:aves/widgets/common/providers/selection_provider.dart';
@ -56,7 +56,12 @@ class FilterNavigationPage<T extends CollectionFilter, CSAD extends ChipSetActio
return a.filter.compareTo(b.filter); return a.filter.compareTo(b.filter);
} }
static List<FilterGridItem<T>> sort<T extends CollectionFilter, CSAD extends ChipSetActionDelegate<T>>(ChipSortFactor sortFactor, CollectionSource source, Set<T> filters) { static List<FilterGridItem<T>> sort<T extends CollectionFilter, CSAD extends ChipSetActionDelegate<T>>(
ChipSortFactor sortFactor,
bool reverse,
CollectionSource source,
Set<T> filters,
) {
List<FilterGridItem<T>> toGridItem(CollectionSource source, Set<T> filters) { List<FilterGridItem<T>> toGridItem(CollectionSource source, Set<T> filters) {
return filters return filters
.map((filter) => FilterGridItem( .map((filter) => FilterGridItem(
@ -87,6 +92,9 @@ class FilterNavigationPage<T extends CollectionFilter, CSAD extends ChipSetActio
allMapEntries = toGridItem(source, filters); allMapEntries = toGridItem(source, filters);
break; break;
} }
if (reverse) {
allMapEntries = allMapEntries.reversed.toList();
}
return allMapEntries; return allMapEntries;
} }
} }

View file

@ -3,7 +3,7 @@ import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/selection.dart'; import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/grid/scaling.dart'; import 'package:aves/widgets/common/grid/scaling.dart';

View file

@ -2,7 +2,7 @@ import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/location.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/location.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
@ -23,12 +23,12 @@ class CountryListPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
return Selector<Settings, Tuple2<ChipSortFactor, Set<CollectionFilter>>>( return Selector<Settings, Tuple3<ChipSortFactor, bool, Set<CollectionFilter>>>(
selector: (context, s) => Tuple2(s.countrySortFactor, s.pinnedFilters), selector: (context, s) => Tuple3(s.countrySortFactor, s.countrySortReverse, s.pinnedFilters),
shouldRebuild: (t1, t2) { shouldRebuild: (t1, t2) {
// `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within `TupleN` // `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within `TupleN`
const eq = DeepCollectionEquality(); const eq = DeepCollectionEquality();
return !(eq.equals(t1.item1, t2.item1) && eq.equals(t1.item2, t2.item2)); return !(eq.equals(t1.item1, t2.item1) && eq.equals(t1.item2, t2.item2) && eq.equals(t1.item3, t2.item3));
}, },
builder: (context, s, child) { builder: (context, s, child) {
return StreamBuilder( return StreamBuilder(
@ -60,7 +60,7 @@ class CountryListPage extends StatelessWidget {
List<FilterGridItem<LocationFilter>> _getGridItems(CollectionSource source) { List<FilterGridItem<LocationFilter>> _getGridItems(CollectionSource source) {
final filters = source.sortedCountries.map((location) => LocationFilter(LocationLevel.country, location)).toSet(); final filters = source.sortedCountries.map((location) => LocationFilter(LocationLevel.country, location)).toSet();
return FilterNavigationPage.sort(settings.countrySortFactor, source, filters); return FilterNavigationPage.sort(settings.countrySortFactor, settings.countrySortReverse, source, filters);
} }
static Map<ChipSectionKey, List<FilterGridItem<LocationFilter>>> _groupToSections(Iterable<FilterGridItem<LocationFilter>> sortedMapEntries) { static Map<ChipSectionKey, List<FilterGridItem<LocationFilter>>> _groupToSections(Iterable<FilterGridItem<LocationFilter>> sortedMapEntries) {

View file

@ -2,7 +2,7 @@ import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/model/source/tag.dart'; import 'package:aves/model/source/tag.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
@ -23,12 +23,12 @@ class TagListPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
return Selector<Settings, Tuple2<ChipSortFactor, Set<CollectionFilter>>>( return Selector<Settings, Tuple3<ChipSortFactor, bool, Set<CollectionFilter>>>(
selector: (context, s) => Tuple2(s.tagSortFactor, s.pinnedFilters), selector: (context, s) => Tuple3(s.tagSortFactor, s.tagSortReverse, s.pinnedFilters),
shouldRebuild: (t1, t2) { shouldRebuild: (t1, t2) {
// `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within `TupleN` // `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within `TupleN`
const eq = DeepCollectionEquality(); const eq = DeepCollectionEquality();
return !(eq.equals(t1.item1, t2.item1) && eq.equals(t1.item2, t2.item2)); return !(eq.equals(t1.item1, t2.item1) && eq.equals(t1.item2, t2.item2) && eq.equals(t1.item3, t2.item3));
}, },
builder: (context, s, child) { builder: (context, s, child) {
return StreamBuilder( return StreamBuilder(
@ -60,7 +60,7 @@ class TagListPage extends StatelessWidget {
List<FilterGridItem<TagFilter>> _getGridItems(CollectionSource source) { List<FilterGridItem<TagFilter>> _getGridItems(CollectionSource source) {
final filters = source.sortedTags.map(TagFilter.new).toSet(); final filters = source.sortedTags.map(TagFilter.new).toSet();
return FilterNavigationPage.sort(settings.tagSortFactor, source, filters); return FilterNavigationPage.sort(settings.tagSortFactor, settings.tagSortReverse, source, filters);
} }
static Map<ChipSectionKey, List<FilterGridItem<TagFilter>>> _groupToSections(Iterable<FilterGridItem<TagFilter>> sortedMapEntries) { static Map<ChipSectionKey, List<FilterGridItem<TagFilter>>> _groupToSections(Iterable<FilterGridItem<TagFilter>> sortedMapEntries) {

View file

@ -9,7 +9,7 @@ import 'package:aves/model/settings/enums/home_page.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/services/analysis_service.dart'; import 'package:aves/services/analysis_service.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/services/global_search.dart'; import 'package:aves/services/global_search.dart';

View file

@ -2,7 +2,7 @@ import 'package:aves/main_play.dart' as app;
import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/defaults.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';

View file

@ -1,7 +1,7 @@
// ignore_for_file: avoid_print // ignore_for_file: avoid_print
import 'dart:async'; import 'dart:async';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums/enums.dart';
import 'package:flutter_driver/flutter_driver.dart'; import 'package:flutter_driver/flutter_driver.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:test/test.dart'; import 'package:test/test.dart';

View file

@ -3,6 +3,15 @@
"entryInfoActionEditTitleDescription", "entryInfoActionEditTitleDescription",
"filterNoDateLabel", "filterNoDateLabel",
"filterNoTitleLabel", "filterNoTitleLabel",
"viewDialogReverseSortOrder",
"sortOrderNewestFirst",
"sortOrderOldestFirst",
"sortOrderAtoZ",
"sortOrderZtoA",
"sortOrderHighestFirst",
"sortOrderLowestFirst",
"sortOrderLargestFirst",
"sortOrderSmallestFirst",
"searchMetadataSectionTitle" "searchMetadataSectionTitle"
], ],
@ -10,6 +19,15 @@
"entryInfoActionEditTitleDescription", "entryInfoActionEditTitleDescription",
"filterNoDateLabel", "filterNoDateLabel",
"filterNoTitleLabel", "filterNoTitleLabel",
"viewDialogReverseSortOrder",
"sortOrderNewestFirst",
"sortOrderOldestFirst",
"sortOrderAtoZ",
"sortOrderZtoA",
"sortOrderHighestFirst",
"sortOrderLowestFirst",
"sortOrderLargestFirst",
"sortOrderSmallestFirst",
"searchMetadataSectionTitle" "searchMetadataSectionTitle"
], ],
@ -18,16 +36,46 @@
"filterNoDateLabel", "filterNoDateLabel",
"filterNoTitleLabel", "filterNoTitleLabel",
"filterRecentlyAddedLabel", "filterRecentlyAddedLabel",
"viewDialogReverseSortOrder",
"sortOrderNewestFirst",
"sortOrderOldestFirst",
"sortOrderAtoZ",
"sortOrderZtoA",
"sortOrderHighestFirst",
"sortOrderLowestFirst",
"sortOrderLargestFirst",
"sortOrderSmallestFirst",
"searchMetadataSectionTitle", "searchMetadataSectionTitle",
"settingsConfirmationAfterMoveToBinItems", "settingsConfirmationAfterMoveToBinItems",
"viewerInfoLabelDescription" "viewerInfoLabelDescription"
], ],
"fr": [
"viewDialogReverseSortOrder",
"sortOrderNewestFirst",
"sortOrderOldestFirst",
"sortOrderAtoZ",
"sortOrderZtoA",
"sortOrderHighestFirst",
"sortOrderLowestFirst",
"sortOrderLargestFirst",
"sortOrderSmallestFirst"
],
"id": [ "id": [
"entryInfoActionEditTitleDescription", "entryInfoActionEditTitleDescription",
"filterNoDateLabel", "filterNoDateLabel",
"filterNoTitleLabel", "filterNoTitleLabel",
"filterRecentlyAddedLabel", "filterRecentlyAddedLabel",
"viewDialogReverseSortOrder",
"sortOrderNewestFirst",
"sortOrderOldestFirst",
"sortOrderAtoZ",
"sortOrderZtoA",
"sortOrderHighestFirst",
"sortOrderLowestFirst",
"sortOrderLargestFirst",
"sortOrderSmallestFirst",
"searchMetadataSectionTitle", "searchMetadataSectionTitle",
"settingsConfirmationAfterMoveToBinItems", "settingsConfirmationAfterMoveToBinItems",
"settingsViewerGestureSideTapNext", "settingsViewerGestureSideTapNext",
@ -38,6 +86,15 @@
"entryInfoActionEditTitleDescription", "entryInfoActionEditTitleDescription",
"filterNoDateLabel", "filterNoDateLabel",
"filterNoTitleLabel", "filterNoTitleLabel",
"viewDialogReverseSortOrder",
"sortOrderNewestFirst",
"sortOrderOldestFirst",
"sortOrderAtoZ",
"sortOrderZtoA",
"sortOrderHighestFirst",
"sortOrderLowestFirst",
"sortOrderLargestFirst",
"sortOrderSmallestFirst",
"searchMetadataSectionTitle" "searchMetadataSectionTitle"
], ],
@ -46,16 +103,46 @@
"filterNoDateLabel", "filterNoDateLabel",
"filterNoTitleLabel", "filterNoTitleLabel",
"filterRecentlyAddedLabel", "filterRecentlyAddedLabel",
"viewDialogReverseSortOrder",
"sortOrderNewestFirst",
"sortOrderOldestFirst",
"sortOrderAtoZ",
"sortOrderZtoA",
"sortOrderHighestFirst",
"sortOrderLowestFirst",
"sortOrderLargestFirst",
"sortOrderSmallestFirst",
"searchMetadataSectionTitle", "searchMetadataSectionTitle",
"settingsConfirmationAfterMoveToBinItems", "settingsConfirmationAfterMoveToBinItems",
"settingsViewerGestureSideTapNext", "settingsViewerGestureSideTapNext",
"viewerInfoLabelDescription" "viewerInfoLabelDescription"
], ],
"ko": [
"viewDialogReverseSortOrder",
"sortOrderNewestFirst",
"sortOrderOldestFirst",
"sortOrderAtoZ",
"sortOrderZtoA",
"sortOrderHighestFirst",
"sortOrderLowestFirst",
"sortOrderLargestFirst",
"sortOrderSmallestFirst"
],
"nl": [ "nl": [
"entryInfoActionEditTitleDescription", "entryInfoActionEditTitleDescription",
"filterNoDateLabel", "filterNoDateLabel",
"filterNoTitleLabel", "filterNoTitleLabel",
"viewDialogReverseSortOrder",
"sortOrderNewestFirst",
"sortOrderOldestFirst",
"sortOrderAtoZ",
"sortOrderZtoA",
"sortOrderHighestFirst",
"sortOrderLowestFirst",
"sortOrderLargestFirst",
"sortOrderSmallestFirst",
"searchMetadataSectionTitle" "searchMetadataSectionTitle"
], ],
@ -63,6 +150,15 @@
"entryInfoActionEditTitleDescription", "entryInfoActionEditTitleDescription",
"filterNoDateLabel", "filterNoDateLabel",
"filterNoTitleLabel", "filterNoTitleLabel",
"viewDialogReverseSortOrder",
"sortOrderNewestFirst",
"sortOrderOldestFirst",
"sortOrderAtoZ",
"sortOrderZtoA",
"sortOrderHighestFirst",
"sortOrderLowestFirst",
"sortOrderLargestFirst",
"sortOrderSmallestFirst",
"searchMetadataSectionTitle" "searchMetadataSectionTitle"
], ],
@ -72,6 +168,15 @@
"filterNoTitleLabel", "filterNoTitleLabel",
"filterOnThisDayLabel", "filterOnThisDayLabel",
"filterRecentlyAddedLabel", "filterRecentlyAddedLabel",
"viewDialogReverseSortOrder",
"sortOrderNewestFirst",
"sortOrderOldestFirst",
"sortOrderAtoZ",
"sortOrderZtoA",
"sortOrderHighestFirst",
"sortOrderLowestFirst",
"sortOrderLargestFirst",
"sortOrderSmallestFirst",
"searchMetadataSectionTitle", "searchMetadataSectionTitle",
"settingsConfirmationAfterMoveToBinItems", "settingsConfirmationAfterMoveToBinItems",
"settingsViewerGestureSideTapNext", "settingsViewerGestureSideTapNext",
@ -100,6 +205,15 @@
"wallpaperTargetLock", "wallpaperTargetLock",
"wallpaperTargetHomeLock", "wallpaperTargetHomeLock",
"menuActionSlideshow", "menuActionSlideshow",
"viewDialogReverseSortOrder",
"sortOrderNewestFirst",
"sortOrderOldestFirst",
"sortOrderAtoZ",
"sortOrderZtoA",
"sortOrderHighestFirst",
"sortOrderLowestFirst",
"sortOrderLargestFirst",
"sortOrderSmallestFirst",
"searchMetadataSectionTitle", "searchMetadataSectionTitle",
"settingsConfirmationAfterMoveToBinItems", "settingsConfirmationAfterMoveToBinItems",
"settingsViewerGestureSideTapNext", "settingsViewerGestureSideTapNext",
@ -124,6 +238,15 @@
"entryInfoActionEditTitleDescription", "entryInfoActionEditTitleDescription",
"filterNoDateLabel", "filterNoDateLabel",
"filterNoTitleLabel", "filterNoTitleLabel",
"viewDialogReverseSortOrder",
"sortOrderNewestFirst",
"sortOrderOldestFirst",
"sortOrderAtoZ",
"sortOrderZtoA",
"sortOrderHighestFirst",
"sortOrderLowestFirst",
"sortOrderLargestFirst",
"sortOrderSmallestFirst",
"searchMetadataSectionTitle" "searchMetadataSectionTitle"
] ]
} }