diff --git a/CHANGELOG.md b/CHANGELOG.md index bcd801fb6..b4e679f4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index de316fb95..1ac5b948c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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", diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index 9a0c27f6e..4e21d4ba8 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -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'; diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 1c59522ba..fd21a2409 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -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 get collectionBrowsingQuickActions => getEnumListOrDefault(collectionBrowsingQuickActionsKey, SettingsDefaults.collectionBrowsingQuickActions, EntrySetAction.values); set collectionBrowsingQuickActions(List 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 get pinnedFilters => (getStringList(pinnedFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet(); set pinnedFilters(Set 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: diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index 71c94c239..fbeec1e64 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -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 filters; EntryGroupFactor sectionFactor; EntrySortFactor sortFactor; + bool sortReverse; final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier(); final List _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(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory)); - sections = SplayTreeMap>.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>.of(byAlbum, compare); break; case EntrySortFactor.rating: sections = groupBy(_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) { diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index 79dfc825f..7e182b0d2 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -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'; diff --git a/lib/model/source/enums.dart b/lib/model/source/enums/enums.dart similarity index 100% rename from lib/model/source/enums.dart rename to lib/model/source/enums/enums.dart diff --git a/lib/model/source/enums/view.dart b/lib/model/source/enums/view.dart new file mode 100644 index 000000000..a80c116c9 --- /dev/null +++ b/lib/model/source/enums/view.dart @@ -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; + } + } +} diff --git a/lib/model/source/location.dart b/lib/model/source/location.dart index 83d3439d1..f063aab80 100644 --- a/lib/model/source/location.dart +++ b/lib/model/source/location.dart @@ -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'; diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart index 80318c595..18e6d4d44 100644 --- a/lib/model/source/media_store_source.dart +++ b/lib/model/source/media_store_source.dart @@ -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'; diff --git a/lib/model/source/source_state.dart b/lib/model/source/source_state.dart index 5b5ff3a86..35d57bd7d 100644 --- a/lib/model/source/source_state.dart +++ b/lib/model/source/source_state.dart @@ -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) { diff --git a/lib/model/source/tag.dart b/lib/model/source/tag.dart index 56cb6c2d1..e1ebd33e6 100644 --- a/lib/model/source/tag.dart +++ b/lib/model/source/tag.dart @@ -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'; diff --git a/lib/services/analysis_service.dart b/lib/services/analysis_service.dart index 1fb764e07..3118e2b44 100644 --- a/lib/services/analysis_service.dart +++ b/lib/services/analysis_service.dart @@ -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'; diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index af817ffde..6dcdc8a03 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -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; diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index e9ba07921..4a2d6ccdf 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -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 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 with SingleTickerPr } Future _configureView() async { - final initialValue = Tuple3( + final initialValue = Tuple4( settings.collectionSortFactor, settings.collectionSectionFactor, settings.getTileLayout(CollectionPage.routeName), + settings.collectionSortReverse, ); - final value = await showDialog>( + final value = await showDialog>( context: context, builder: (context) { - final l10n = context.l10n; return TileViewDialog( 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 with SingleTickerPr settings.collectionSortFactor = value.item1!; settings.collectionSectionFactor = value.item2!; settings.setTileLayout(CollectionPage.routeName, value.item3!); + settings.collectionSortReverse = value.item4; } } diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index 1e262055e..bc9eb6b34 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -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; diff --git a/lib/widgets/collection/draggable_thumb_label.dart b/lib/widgets/collection/draggable_thumb_label.dart index f759854b9..145a00607 100644 --- a/lib/widgets/collection/draggable_thumb_label.dart +++ b/lib/widgets/collection/draggable_thumb_label.dart @@ -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'; diff --git a/lib/widgets/collection/grid/headers/any.dart b/lib/widgets/collection/grid/headers/any.dart index d88929c68..29cbbd0b5 100644 --- a/lib/widgets/collection/grid/headers/any.dart +++ b/lib/widgets/collection/grid/headers/any.dart @@ -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'; diff --git a/lib/widgets/collection/grid/tile.dart b/lib/widgets/collection/grid/tile.dart index 5f3f3c20a..3b4ed5225 100644 --- a/lib/widgets/collection/grid/tile.dart +++ b/lib/widgets/collection/grid/tile.dart @@ -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'; diff --git a/lib/widgets/common/app_bar/app_bar_subtitle.dart b/lib/widgets/common/app_bar/app_bar_subtitle.dart index bc33ac08f..2e09fe94c 100644 --- a/lib/widgets/common/app_bar/app_bar_subtitle.dart +++ b/lib/widgets/common/app_bar/app_bar_subtitle.dart @@ -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'; diff --git a/lib/widgets/common/basic/text_dropdown_button.dart b/lib/widgets/common/basic/text_dropdown_button.dart new file mode 100644 index 000000000..4f60ef164 --- /dev/null +++ b/lib/widgets/common/basic/text_dropdown_button.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +class TextDropdownButton extends DropdownButton { + TextDropdownButton({ + super.key, + required List 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( + value: v, + child: Text(valueText(v)), + )) + .toList(), + selectedItemBuilder: (context) => values + .map((v) => DropdownMenuItem( + value: v, + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Text( + valueText(v), + softWrap: false, + overflow: TextOverflow.fade, + ), + ), + )) + .toList(), + ); +} diff --git a/lib/widgets/common/grid/item_tracker.dart b/lib/widgets/common/grid/item_tracker.dart index f9b07c896..abaa6e063 100644 --- a/lib/widgets/common/grid/item_tracker.dart +++ b/lib/widgets/common/grid/item_tracker.dart @@ -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'; diff --git a/lib/widgets/common/grid/scaling.dart b/lib/widgets/common/grid/scaling.dart index e24646e04..96988cbff 100644 --- a/lib/widgets/common/grid/scaling.dart +++ b/lib/widgets/common/grid/scaling.dart @@ -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'; diff --git a/lib/widgets/common/grid/section_layout.dart b/lib/widgets/common/grid/section_layout.dart index cb6ef28b5..040304d67 100644 --- a/lib/widgets/common/grid/section_layout.dart +++ b/lib/widgets/common/grid/section_layout.dart @@ -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'; diff --git a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart index 619b89bab..c83656741 100644 --- a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart @@ -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 { scrollableContent: [ Padding( padding: const EdgeInsets.only(left: 16, top: 8, right: 16), - child: DropdownButton( - items: DateEditAction.values - .map((v) => DropdownMenuItem( - value: v, - child: Text(_actionText(context, v)), - )) - .toList(), + child: TextDropdownButton( + 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 { Widget _buildCopyFieldContent(BuildContext context) { return Padding( padding: const EdgeInsets.only(left: 16, top: 0, right: 16), - child: DropdownButton( - items: DateFieldSource.values - .map((v) => DropdownMenuItem( - value: v, - child: Text(_setSourceText(context, v)), - )) - .toList(), - selectedItemBuilder: (context) => DateFieldSource.values - .map((v) => DropdownMenuItem( - value: v, - child: Text( - _setSourceText(context, v), - softWrap: false, - overflow: TextOverflow.fade, - ), - )) - .toList(), + child: TextDropdownButton( + values: DateFieldSource.values, + valueText: (v) => _setSourceText(context, v), value: _copyFieldSource, onChanged: (v) => setState(() => _copyFieldSource = v!), isExpanded: true, diff --git a/lib/widgets/dialogs/export_entry_dialog.dart b/lib/widgets/dialogs/export_entry_dialog.dart index 7482b887b..5b59c90d2 100644 --- a/lib/widgets/dialogs/export_entry_dialog.dart +++ b/lib/widgets/dialogs/export_entry_dialog.dart @@ -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 { children: [ Text(l10n.exportEntryDialogFormat), const SizedBox(width: AvesDialog.controlCaptionPadding), - DropdownButton( - items: imageExportFormats.map((mimeType) { - return DropdownMenuItem( - value: mimeType, - child: Text(MimeUtils.displayType(mimeType)), - ); - }).toList(), + TextDropdownButton( + values: imageExportFormats, + valueText: MimeUtils.displayType, value: _mimeType, onChanged: (selected) { if (selected != null) { diff --git a/lib/widgets/dialogs/tile_view_dialog.dart b/lib/widgets/dialogs/tile_view_dialog.dart index 490d632dc..b47425ae6 100644 --- a/lib/widgets/dialogs/tile_view_dialog.dart +++ b/lib/widgets/dialogs/tile_view_dialog.dart @@ -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 extends StatefulWidget { - final Tuple3 initialValue; + final Tuple4 initialValue; final Map sortOptions; final Map groupOptions; final Map 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 extends StatefulWidget { this.sortOptions = const {}, this.groupOptions = const {}, this.layoutOptions = const {}, + required this.sortOrder, + this.canGroup, }); @override @@ -31,8 +36,7 @@ class _TileViewDialogState extends State> with late S? _selectedSort; late G? _selectedGroup; late L? _selectedLayout; - late final TabController _tabController; - late final String _optionLines; + late bool _reverseSort; Map get sortOptions => widget.sortOptions; @@ -40,11 +44,7 @@ class _TileViewDialogState extends State> with Map 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 extends State> 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 = >[ - if (sortOptions.isNotEmpty) - Tuple2( - _buildTab( - context, - const Key('tab-sort'), - AIcons.sort, - l10n.viewDialogTabSort, + + return AvesDialog( + scrollableContent: [ + _buildSection( + icon: AIcons.sort, + title: l10n.viewDialogTabSort, + trailing: IconButton( + icon: const Icon(AIcons.sortOrder), + onPressed: () => setState(() => _reverseSort = !_reverseSort), + tooltip: l10n.viewDialogReverseSortOrder, ), - Column( - children: sortOptions.entries - .map((kv) => _buildRadioListTile( - kv.key, - kv.value, - () => _selectedSort, - (v) => _selectedSort = v, - )) - .toList(), + 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().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) - 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( - kv.key, - kv.value, - () => _selectedGroup, - (v) => _selectedGroup = v, - )) - .toList(), - ), + _buildSection( + icon: AIcons.layout, + title: l10n.viewDialogTabLayout, + options: layoutOptions, + value: _selectedLayout, + onChanged: (v) => _selectedLayout = v, ), - if (layoutOptions.isNotEmpty) - Tuple2( - _buildTab( - context, - const Key('tab-layout'), - AIcons.layout, - l10n.viewDialogTabLayout, - ), - Column( - children: layoutOptions.entries - .map((kv) => _buildRadioListTile( - kv.key, - kv.value, - () => _selectedLayout, - (v) => _selectedLayout = v, - )) - .toList(), - ), + ], + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), ), - ]; - - 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, - ), - 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, + TextButton( + key: const Key('button-apply'), + onPressed: () => Navigator.pop(context, Tuple4(_selectedSort, _selectedGroup, _selectedLayout, _reverseSort)), + child: Text(l10n.applyButtonLabel), + ) + ], ); } - Widget _buildRadioListTile(T value, String title, T? Function() get, void Function(T value) set) { - return RadioListTile( - // 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({ + bool show = true, + required IconData icon, + required String title, + Widget? trailing, + required Map options, + required T value, + required ValueChanged 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), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - icon, - size: iconSize, - color: color, - ), - const SizedBox(height: 4), - Text( - text, - style: TextStyle(color: color), - softWrap: false, - overflow: TextOverflow.fade, - ), - ], + 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: [ + const SizedBox(height: 8), + ConstrainedBox( + constraints: const BoxConstraints( + minHeight: kMinInteractiveDimension, + ), + 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( + 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); } diff --git a/lib/widgets/dialogs/video_stream_selection_dialog.dart b/lib/widgets/dialogs/video_stream_selection_dialog.dart index 87f5e8435..b89950b49 100644 --- a/lib/widgets/dialogs/video_stream_selection_dialog.dart +++ b/lib/widgets/dialogs/video_stream_selection_dialog.dart @@ -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 return common; } - DropdownMenuItem _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 _buildSection({ required IconData icon, required String title, @@ -162,9 +144,9 @@ class _VideoStreamSelectionDialogState extends State ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: DropdownButton( - items: streams.map(_buildMenuItem).toList(), - selectedItemBuilder: (context) => streams.map(_buildSelectedItem).toList(), + child: TextDropdownButton( + values: streams.whereNotNull().toList(), + valueText: _streamName, value: current, onChanged: streams.length > 1 ? (newValue) => setState(() => setter(newValue)) : null, isExpanded: true, diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart index a2f040eda..3a19327cd 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/filter_grids/album_pick.dart @@ -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'; diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index fb5a47786..39160aee4 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -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(); - return Selector>>( - selector: (context, s) => Tuple3(s.albumGroupFactor, s.albumSortFactor, s.pinnedFilters), + return Selector>>( + 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( @@ -75,7 +75,7 @@ class AlbumListPage extends StatelessWidget { static List> 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>> groupToSections(BuildContext context, CollectionSource source, Iterable> sortedMapEntries) { diff --git a/lib/widgets/filter_grids/common/action_delegates/album_set.dart b/lib/widgets/filter_grids/common/action_delegates/album_set.dart index 76feb189b..c2945a12e 100644 --- a/lib/widgets/filter_grids/common/action_delegates/album_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/album_set.dart @@ -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 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 with @override Future configureView(BuildContext context) async { - final initialValue = Tuple3( + final initialValue = Tuple4( sortFactor, settings.albumGroupFactor, tileLayout, + sortReverse, ); - final value = await showDialog>( + final value = await showDialog>( context: context, builder: (context) { - final l10n = context.l10n; return TileViewDialog( 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 with sortFactor = value.item1!; settings.albumGroupFactor = value.item2!; tileLayout = value.item3!; + sortReverse = value.item4; } } diff --git a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart index 8b6a34925..5455c9a7b 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart @@ -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 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 with FeedbackMi } Future configureView(BuildContext context) async { - final initialValue = Tuple3( + final initialValue = Tuple4( sortFactor, null, tileLayout, + sortReverse, ); - final value = await showDialog>( + final value = await showDialog>( context: context, builder: (context) { - final l10n = context.l10n; return TileViewDialog( 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 with FeedbackMi if (value != null && initialValue != value) { sortFactor = value.item1!; tileLayout = value.item3!; + sortReverse = value.item4; } } diff --git a/lib/widgets/filter_grids/common/action_delegates/country_set.dart b/lib/widgets/filter_grids/common/action_delegates/country_set.dart index 7ae5e9baf..1e2ea6a31 100644 --- a/lib/widgets/filter_grids/common/action_delegates/country_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/country_set.dart @@ -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 @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); diff --git a/lib/widgets/filter_grids/common/action_delegates/tag_set.dart b/lib/widgets/filter_grids/common/action_delegates/tag_set.dart index 87d7c3b07..e6403fd04 100644 --- a/lib/widgets/filter_grids/common/action_delegates/tag_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/tag_set.dart @@ -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 { @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); diff --git a/lib/widgets/filter_grids/common/draggable_thumb_label.dart b/lib/widgets/filter_grids/common/draggable_thumb_label.dart index 6eb55a823..75ace8365 100644 --- a/lib/widgets/filter_grids/common/draggable_thumb_label.dart +++ b/lib/widgets/filter_grids/common/draggable_thumb_label.dart @@ -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'; diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index ab4c9a984..b2e851dfc 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -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'; diff --git a/lib/widgets/filter_grids/common/filter_nav_page.dart b/lib/widgets/filter_grids/common/filter_nav_page.dart index a9cda6ea4..c8fde49e0 100644 --- a/lib/widgets/filter_grids/common/filter_nav_page.dart +++ b/lib/widgets/filter_grids/common/filter_nav_page.dart @@ -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> sort>(ChipSortFactor sortFactor, CollectionSource source, Set filters) { + static List> sort>( + ChipSortFactor sortFactor, + bool reverse, + CollectionSource source, + Set filters, + ) { List> toGridItem(CollectionSource source, Set filters) { return filters .map((filter) => FilterGridItem( @@ -87,6 +92,9 @@ class FilterNavigationPage(); - return Selector>>( - selector: (context, s) => Tuple2(s.countrySortFactor, s.pinnedFilters), + return Selector>>( + 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> _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>> _groupToSections(Iterable> sortedMapEntries) { diff --git a/lib/widgets/filter_grids/tags_page.dart b/lib/widgets/filter_grids/tags_page.dart index 5eae618ac..23520b47f 100644 --- a/lib/widgets/filter_grids/tags_page.dart +++ b/lib/widgets/filter_grids/tags_page.dart @@ -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(); - return Selector>>( - selector: (context, s) => Tuple2(s.tagSortFactor, s.pinnedFilters), + return Selector>>( + 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> _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>> _groupToSections(Iterable> sortedMapEntries) { diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 9ebbe0b03..61b363e7f 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -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'; diff --git a/test_driver/driver_screenshots.dart b/test_driver/driver_screenshots.dart index f4de8d3f8..2c2e11050 100644 --- a/test_driver/driver_screenshots.dart +++ b/test_driver/driver_screenshots.dart @@ -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'; diff --git a/test_driver/driver_shaders_test.dart b/test_driver/driver_shaders_test.dart index 9abd1fb42..ccdbbf635 100644 --- a/test_driver/driver_shaders_test.dart +++ b/test_driver/driver_shaders_test.dart @@ -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'; diff --git a/untranslated.json b/untranslated.json index 2083a68b3..0b496c324 100644 --- a/untranslated.json +++ b/untranslated.json @@ -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" ] }