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
- Collection: view settings allow changing the sort order (aka ascending/descending)
- Collection / Info: edit title via IPTC / XMP
- Albums / Countries / Tags: size displayed in list view details, sort by size
- Search: `undated` and `untitled` filters

View file

@ -417,6 +417,7 @@
"viewDialogTabSort": "Sort",
"viewDialogTabGroup": "Group",
"viewDialogTabLayout": "Layout",
"viewDialogReverseSortOrder": "Reverse sort order",
"tileLayoutGrid": "Grid",
"tileLayoutList": "List",
@ -570,6 +571,15 @@
"sortByAlbumFileName": "By album & file name",
"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",
"albumGroupVolume": "By storage volume",
"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/naming_pattern.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/countries_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/enums/enums.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/services.dart';
import 'package:aves_map/aves_map.dart';
@ -75,6 +75,7 @@ class Settings extends ChangeNotifier {
// collection
static const collectionGroupFactorKey = 'collection_group_factor';
static const collectionSortFactorKey = 'collection_sort_factor';
static const collectionSortReverseKey = 'collection_sort_reverse';
static const collectionBrowsingQuickActionsKey = 'collection_browsing_quick_actions';
static const collectionSelectionQuickActionsKey = 'collection_selection_quick_actions';
static const showThumbnailFavouriteKey = 'show_thumbnail_favourite';
@ -90,6 +91,9 @@ class Settings extends ChangeNotifier {
static const albumSortFactorKey = 'album_sort_factor';
static const countrySortFactorKey = 'country_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 hiddenFiltersKey = 'hidden_filters';
@ -388,6 +392,10 @@ class Settings extends ChangeNotifier {
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);
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());
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 pinnedFilters(Set<CollectionFilter> newValue) => setAndNotify(pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList());
@ -828,6 +848,7 @@ class Settings extends ChangeNotifier {
case confirmMoveUndatedItemsKey:
case confirmAfterMoveToBinKey:
case setMetadataDateBeforeFileOpKey:
case collectionSortReverseKey:
case showThumbnailFavouriteKey:
case showThumbnailTagKey:
case showThumbnailLocationKey:
@ -835,6 +856,9 @@ class Settings extends ChangeNotifier {
case showThumbnailRatingKey:
case showThumbnailRawKey:
case showThumbnailVideoDurationKey:
case albumSortReverseKey:
case countrySortReverseKey:
case tagSortReverseKey:
case showOverlayOnOpeningKey:
case showOverlayMinimapKey:
case showOverlayInfoKey:

View file

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

View file

@ -1,5 +1,5 @@
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 {
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/source/analysis_controller.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/utils/collection_utils.dart';
import 'package:collection/collection.dart';

View file

@ -4,7 +4,7 @@ import 'dart:ui';
import 'package:aves/l10n/l10n.dart';
import 'package:aves/model/settings/settings.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/source_state.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 layout = Icons.grid_view_outlined;
static const IconData sort = Icons.sort_outlined;
static const IconData sortOrder = Icons.swap_vert_outlined;
// actions
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/source/collection_lens.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/icons.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;
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
void initState() {
super.initState();
@ -506,33 +526,22 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
}
Future<void> _configureView() async {
final initialValue = Tuple3(
final initialValue = Tuple4(
settings.collectionSortFactor,
settings.collectionSectionFactor,
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,
builder: (context) {
final l10n = context.l10n;
return TileViewDialog<EntrySortFactor, EntryGroupFactor, TileLayout>(
initialValue: initialValue,
sortOptions: {
EntrySortFactor.date: l10n.sortByDate,
EntrySortFactor.size: l10n.sortBySize,
EntrySortFactor.name: l10n.sortByAlbumFileName,
EntrySortFactor.rating: l10n.sortByRating,
},
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,
},
sortOptions: Map.fromEntries(_sortOptions.map((v) => MapEntry(v, v.getName(context)))),
groupOptions: Map.fromEntries(_groupOptions.map((v) => MapEntry(v, v.getName(context)))),
layoutOptions: Map.fromEntries(_layoutOptions.map((v) => MapEntry(v, v.getName(context)))),
sortOrder: (factor, reverse) => factor.getOrderName(context, reverse),
canGroup: (s, g, l) => s == EntrySortFactor.date,
);
},
);
@ -542,6 +551,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
settings.collectionSortFactor = value.item1!;
settings.collectionSectionFactor = value.item2!;
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/settings/settings.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/ref/mime_types.dart';
import 'package:aves/theme/durations.dart';
@ -542,7 +542,7 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
final oldest = lastKey.date;
if (newest != null && oldest != null) {
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;
sectionLayouts.forEach((section) {
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/source/collection_lens.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/widgets/common/extensions/build_context.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/source/collection_lens.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/widgets/collection/grid/headers/album.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/selection.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/widgets/collection/grid/list_details.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart';

View file

@ -1,7 +1,7 @@
import 'dart:ui';
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/source_state.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 '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/widgets/common/grid/section_layout.dart';
import 'package:collection/collection.dart';

View file

@ -1,7 +1,7 @@
import 'dart:ui' as ui;
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/widgets/common/behaviour/eager_scale_gesture_recognizer.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';

View file

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

View file

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

View file

@ -1,19 +1,22 @@
import 'dart:math';
import 'package:aves/model/source/enums.dart';
import 'package:aves/theme/durations.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/identity/highlight_title.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
import 'aves_dialog.dart';
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<G, String> groupOptions;
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({
super.key,
@ -21,6 +24,8 @@ class TileViewDialog<S, G, L> extends StatefulWidget {
this.sortOptions = const {},
this.groupOptions = const {},
this.layoutOptions = const {},
required this.sortOrder,
this.canGroup,
});
@override
@ -31,8 +36,7 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
late S? _selectedSort;
late G? _selectedGroup;
late L? _selectedLayout;
late final TabController _tabController;
late final String _optionLines;
late bool _reverseSort;
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;
static const int groupTabIndex = 1;
double tabBarHeight(BuildContext context) => 64 * max(1, MediaQuery.textScaleFactorOf(context));
static const double tabIndicatorWeight = 2;
bool get canGroup => (widget.canGroup ?? (s, g, l) => true).call(_selectedSort, _selectedGroup, _selectedLayout);
@override
void initState() {
@ -53,259 +53,140 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
_selectedSort = initialValue.item1;
_selectedGroup = initialValue.item2;
_selectedLayout = initialValue.item3;
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();
_reverseSort = initialValue.item4;
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final tabs = <Tuple2<Tab, Widget>>[
if (sortOptions.isNotEmpty)
Tuple2(
_buildTab(
context,
const Key('tab-sort'),
AIcons.sort,
l10n.viewDialogTabSort,
),
Column(
children: sortOptions.entries
.map((kv) => _buildRadioListTile<S>(
kv.key,
kv.value,
() => _selectedSort,
(v) => _selectedSort = v,
))
.toList(),
),
),
if (groupOptions.isNotEmpty)
Tuple2(
_buildTab(
context,
const Key('tab-group'),
AIcons.group,
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(
_buildTab(
context,
const Key('tab-layout'),
AIcons.layout,
l10n.viewDialogTabLayout,
),
Column(
children: layoutOptions.entries
.map((kv) => _buildRadioListTile<L>(
kv.key,
kv.value,
() => _selectedLayout,
(v) => _selectedLayout = v,
))
.toList(),
),
),
];
final contentWidget = DecoratedBox(
decoration: AvesDialog.contentDecoration(context),
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,
return AvesDialog(
scrollableContent: [
_buildSection(
icon: AIcons.sort,
title: l10n.viewDialogTabSort,
trailing: IconButton(
icon: const Icon(AIcons.sortOrder),
onPressed: () => setState(() => _reverseSort = !_reverseSort),
tooltip: l10n.viewDialogReverseSortOrder,
),
clipBehavior: Clip.antiAlias,
child: TabBar(
indicatorWeight: tabIndicatorWeight,
tabs: tabs.map((t) => t.item1).toList(),
controller: _tabController,
options: sortOptions,
value: _selectedSort,
onChanged: (v) {
_selectedSort = v;
_reverseSort = false;
},
bottom: _selectedSort != null
? Text(
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,
),
),
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: maxHeight,
child: _buildSection(
show: canGroup,
icon: AIcons.group,
title: l10n.viewDialogTabGroup,
options: groupOptions,
value: _selectedGroup,
onChanged: (v) => _selectedGroup = v,
),
child: TabBarView(
controller: _tabController,
physics: const NeverScrollableScrollPhysics(),
children: tabs
.map((t) => SingleChildScrollView(
child: t.item2,
))
.toList(),
),
_buildSection(
icon: AIcons.layout,
title: l10n.viewDialogTabLayout,
options: layoutOptions,
value: _selectedLayout,
onChanged: (v) => _selectedLayout = v,
),
],
);
},
),
);
final actionsWidget = Padding(
padding: AvesDialog.actionsPadding,
child: OverflowBar(
alignment: MainAxisAlignment.end,
spacing: AvesDialog.buttonPadding.horizontal / 2,
overflowAlignment: OverflowBarAlignment.end,
children: [
actions: [
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)),
onPressed: () => Navigator.pop(context, Tuple4(_selectedSort, _selectedGroup, _selectedLayout, _reverseSort)),
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) {
return RadioListTile<T>(
// key is expected by test driver
key: Key(value.toString()),
value: value,
groupValue: get(),
onChanged: (v) => setState(() => set(v as T)),
title: Text(
title,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
);
}
// tabs
Tab _buildTab(
BuildContext context,
Key key,
IconData icon,
String text, {
Color? color,
Widget _buildSection<T>({
bool show = true,
required IconData icon,
required String title,
Widget? trailing,
required Map<T, String> options,
required T value,
required ValueChanged<T?> onChanged,
Widget? bottom,
}) {
// cannot use `IconTheme` over `TabBar` to change size,
// because `TabBar` does so internally
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
final iconSize = IconTheme.of(context).size! * textScaleFactor;
return Tab(
key: key,
height: tabBarHeight(context),
if (options.isEmpty || !show) return const SizedBox();
final iconSize = IconTheme.of(context).size! * MediaQuery.textScaleFactorOf(context);
return TooltipTheme(
data: TooltipTheme.of(context).copyWith(
preferBelow: false,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
icon,
size: iconSize,
color: color,
const SizedBox(height: 8),
ConstrainedBox(
constraints: const BoxConstraints(
minHeight: kMinInteractiveDimension,
),
const SizedBox(height: 4),
Text(
text,
style: TextStyle(color: color),
softWrap: false,
overflow: TextOverflow.fade,
child: Row(
children: [
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/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/viewer/video/controller.dart';
import 'package:collection/collection.dart';
@ -123,25 +124,6 @@ class _VideoStreamSelectionDialogState extends State<VideoStreamSelectionDialog>
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({
required IconData icon,
required String title,
@ -162,9 +144,9 @@ class _VideoStreamSelectionDialogState extends State<VideoStreamSelectionDialog>
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: DropdownButton<StreamSummary>(
items: streams.map(_buildMenuItem).toList(),
selectedItemBuilder: (context) => streams.map(_buildSelectedItem).toList(),
child: TextDropdownButton<StreamSummary>(
values: streams.whereNotNull().toList(),
valueText: _streamName,
value: current,
onChanged: streams.length > 1 ? (newValue) => setState(() => setter(newValue)) : null,
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/source/album.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/icons.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/source/album.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/utils/android_file_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
@ -25,12 +25,12 @@ class AlbumListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final source = context.read<CollectionSource>();
return Selector<Settings, Tuple3<AlbumChipGroupFactor, ChipSortFactor, Set<CollectionFilter>>>(
selector: (context, s) => Tuple3(s.albumGroupFactor, s.albumSortFactor, s.pinnedFilters),
return Selector<Settings, Tuple4<AlbumChipGroupFactor, ChipSortFactor, bool, Set<CollectionFilter>>>(
selector: (context, s) => Tuple4(s.albumGroupFactor, s.albumSortFactor, s.albumSortReverse, s.pinnedFilters),
shouldRebuild: (t1, t2) {
// `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within `TupleN`
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) {
return ValueListenableBuilder<bool>(
@ -75,7 +75,7 @@ class AlbumListPage extends StatelessWidget {
static List<FilterGridItem<AlbumFilter>> getAlbumGridItems(BuildContext context, CollectionSource source) {
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) {

View file

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

View file

@ -1,7 +1,7 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/location.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/countries_page.dart';
@ -19,6 +19,12 @@ class CountryChipSetActionDelegate extends ChipSetActionDelegate<LocationFilter>
@override
set sortFactor(ChipSortFactor factor) => settings.countrySortFactor = factor;
@override
bool get sortReverse => settings.countrySortReverse;
@override
set sortReverse(bool value) => settings.countrySortReverse = value;
@override
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/tag.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/tags_page.dart';
@ -19,6 +19,12 @@ class TagChipSetActionDelegate extends ChipSetActionDelegate<TagFilter> {
@override
set sortFactor(ChipSortFactor factor) => settings.tagSortFactor = factor;
@override
bool get sortReverse => settings.tagSortReverse;
@override
set sortReverse(bool value) => settings.tagSortReverse = value;
@override
TileLayout get tileLayout => settings.getTileLayout(TagListPage.routeName);

View file

@ -1,6 +1,6 @@
import 'package:aves/model/filters/filters.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/widgets/common/extensions/build_context.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/selection.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/widgets/common/basic/draggable_scrollbar.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/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/widgets/common/identity/aves_filter_chip.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);
}
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) {
return filters
.map((filter) => FilterGridItem(
@ -87,6 +92,9 @@ class FilterNavigationPage<T extends CollectionFilter, CSAD extends ChipSetActio
allMapEntries = toGridItem(source, filters);
break;
}
if (reverse) {
allMapEntries = allMapEntries.reversed.toList();
}
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/settings/settings.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/common/extensions/build_context.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/settings/settings.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/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
@ -23,12 +23,12 @@ class CountryListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final source = context.read<CollectionSource>();
return Selector<Settings, Tuple2<ChipSortFactor, Set<CollectionFilter>>>(
selector: (context, s) => Tuple2(s.countrySortFactor, s.pinnedFilters),
return Selector<Settings, Tuple3<ChipSortFactor, bool, Set<CollectionFilter>>>(
selector: (context, s) => Tuple3(s.countrySortFactor, s.countrySortReverse, s.pinnedFilters),
shouldRebuild: (t1, t2) {
// `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within `TupleN`
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) {
return StreamBuilder(
@ -60,7 +60,7 @@ class CountryListPage extends StatelessWidget {
List<FilterGridItem<LocationFilter>> _getGridItems(CollectionSource source) {
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) {

View file

@ -2,7 +2,7 @@ import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/tag.dart';
import 'package:aves/model/settings/settings.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/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
@ -23,12 +23,12 @@ class TagListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final source = context.read<CollectionSource>();
return Selector<Settings, Tuple2<ChipSortFactor, Set<CollectionFilter>>>(
selector: (context, s) => Tuple2(s.tagSortFactor, s.pinnedFilters),
return Selector<Settings, Tuple3<ChipSortFactor, bool, Set<CollectionFilter>>>(
selector: (context, s) => Tuple3(s.tagSortFactor, s.tagSortReverse, s.pinnedFilters),
shouldRebuild: (t1, t2) {
// `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within `TupleN`
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) {
return StreamBuilder(
@ -60,7 +60,7 @@ class TagListPage extends StatelessWidget {
List<FilterGridItem<TagFilter>> _getGridItems(CollectionSource source) {
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) {

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/source/collection_lens.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/common/services.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/enums/enums.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:flutter_driver/driver_extension.dart';
import 'package:flutter_test/flutter_test.dart';

View file

@ -1,7 +1,7 @@
// ignore_for_file: avoid_print
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:path/path.dart' as p;
import 'package:test/test.dart';

View file

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