diff --git a/packages/neon/neon/lib/src/sort_box/sort_box_builder.dart b/packages/neon/neon/lib/src/sort_box/sort_box_builder.dart index 276338fb..afa7227b 100644 --- a/packages/neon/neon/lib/src/sort_box/sort_box_builder.dart +++ b/packages/neon/neon/lib/src/sort_box/sort_box_builder.dart @@ -2,26 +2,43 @@ import 'package:flutter/widgets.dart'; import 'package:neon/src/settings/models/select_option.dart'; import 'package:sort_box/sort_box.dart'; +/// Signature for a function that creates a widget for a given sorted list. +/// +/// Used by [SortBoxBuilder] to display a sorted list of items. +typedef SortBoxWidgetBuilder = Widget Function(BuildContext context, List sorted); + +/// Sorted list builder. +/// +/// Used together with a [SortBox] to sort a given list. class SortBoxBuilder extends StatelessWidget { - const SortBoxBuilder({ + SortBoxBuilder({ required this.sortBox, required this.sortPropertyOption, required this.sortBoxOrderOption, - required this.input, + required final List? input, required this.builder, + this.presort, super.key, - }); + }) : input = input ?? []; final SortBox sortBox; final SelectOption sortPropertyOption; final SelectOption sortBoxOrderOption; - final List? input; - final Widget Function(BuildContext, List?) builder; + + /// Input list to sort. + final List input; + + /// Child builder using the sorted list. + final SortBoxWidgetBuilder builder; + + /// Pre sorts input. + final Set<(T property, SortBoxOrder order)>? presort; @override Widget build(final BuildContext context) { - if (input == null || (input?.isEmpty ?? false)) { - return builder(context, null); + if (input.length <= 1) { + // input is already sorted. + return builder(context, input); } return ValueListenableBuilder( @@ -29,9 +46,9 @@ class SortBoxBuilder extends StatelessWidget { builder: (final context, final property, final _) => ValueListenableBuilder( valueListenable: sortBoxOrderOption, builder: (final context, final order, final _) { - final box = Box(property, order); + final box = (property, order); - return builder(context, sortBox.sort(input!, box)); + return builder(context, sortBox.sort(input, box, presort)); }, ), ); diff --git a/packages/neon/neon_files/lib/sort/files.dart b/packages/neon/neon_files/lib/sort/files.dart index 1f65ac7c..d0d7d96e 100644 --- a/packages/neon/neon_files/lib/sort/files.dart +++ b/packages/neon/neon_files/lib/sort/files.dart @@ -7,7 +7,11 @@ final filesSortBox = SortBox( FilesSortProperty.size: (final file) => file.size ?? 0, }, { - FilesSortProperty.modifiedDate: Box(FilesSortProperty.name, SortBoxOrder.ascending), - FilesSortProperty.size: Box(FilesSortProperty.name, SortBoxOrder.ascending), + FilesSortProperty.modifiedDate: { + (FilesSortProperty.name, SortBoxOrder.ascending), + }, + FilesSortProperty.size: { + (FilesSortProperty.name, SortBoxOrder.ascending), + }, }, ); diff --git a/packages/neon/neon_files/lib/widgets/browser_view.dart b/packages/neon/neon_files/lib/widgets/browser_view.dart index 912daeac..2d06b8aa 100644 --- a/packages/neon/neon_files/lib/widgets/browser_view.dart +++ b/packages/neon/neon_files/lib/widgets/browser_view.dart @@ -60,8 +60,7 @@ class _FilesBrowserViewState extends State { items: [ for (final uploadTask in tasksSnapshot.requireData.whereType().where( (final task) => - sorted?.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty ?? - false, + sorted.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty, )) ...[ FileListTile( bloc: widget.filesBloc, @@ -73,34 +72,32 @@ class _FilesBrowserViewState extends State { onPickFile: widget.onPickFile, ), ], - if (sorted != null) ...[ - for (final file in sorted) ...[ - if (!widget.onlyShowDirectories || file.isDirectory) ...[ - Builder( - builder: (final context) { - final matchingTask = tasksSnapshot.requireData - .firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name)); + for (final file in sorted) ...[ + if (!widget.onlyShowDirectories || file.isDirectory) ...[ + Builder( + builder: (final context) { + final matchingTask = tasksSnapshot.requireData + .firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name)); - final details = matchingTask != null - ? FileDetails.fromTask( - task: matchingTask, - file: file, - ) - : FileDetails.fromWebDav( - file: file, - path: widget.bloc.path.value, - ); + final details = matchingTask != null + ? FileDetails.fromTask( + task: matchingTask, + file: file, + ) + : FileDetails.fromWebDav( + file: file, + path: widget.bloc.path.value, + ); - return FileListTile( - bloc: widget.filesBloc, - browserBloc: widget.bloc, - details: details, - enableFileActions: widget.enableFileActions, - onPickFile: widget.onPickFile, - ); - }, - ), - ], + return FileListTile( + bloc: widget.filesBloc, + browserBloc: widget.bloc, + details: details, + enableFileActions: widget.enableFileActions, + onPickFile: widget.onPickFile, + ); + }, + ), ], ], ], diff --git a/packages/neon/neon_news/lib/sort/articles.dart b/packages/neon/neon_news/lib/sort/articles.dart index ff1087ba..4a082afb 100644 --- a/packages/neon/neon_news/lib/sort/articles.dart +++ b/packages/neon/neon_news/lib/sort/articles.dart @@ -7,7 +7,11 @@ final articlesSortBox = SortBox( ArticlesSortProperty.byFeed: (final article) => article.feedId, }, { - ArticlesSortProperty.alphabetical: Box(ArticlesSortProperty.publishDate, SortBoxOrder.descending), - ArticlesSortProperty.byFeed: Box(ArticlesSortProperty.alphabetical, SortBoxOrder.ascending), + ArticlesSortProperty.alphabetical: { + (ArticlesSortProperty.publishDate, SortBoxOrder.descending), + }, + ArticlesSortProperty.byFeed: { + (ArticlesSortProperty.alphabetical, SortBoxOrder.ascending), + }, }, ); diff --git a/packages/neon/neon_news/lib/sort/feeds.dart b/packages/neon/neon_news/lib/sort/feeds.dart index 7425e875..c25d3596 100644 --- a/packages/neon/neon_news/lib/sort/feeds.dart +++ b/packages/neon/neon_news/lib/sort/feeds.dart @@ -6,7 +6,11 @@ final feedsSortBox = SortBox( FeedsSortProperty.unreadCount: (final feed) => feed.unreadCount ?? 0, }, { - FeedsSortProperty.alphabetical: Box(FeedsSortProperty.unreadCount, SortBoxOrder.descending), - FeedsSortProperty.unreadCount: Box(FeedsSortProperty.alphabetical, SortBoxOrder.ascending), + FeedsSortProperty.alphabetical: { + (FeedsSortProperty.unreadCount, SortBoxOrder.descending), + }, + FeedsSortProperty.unreadCount: { + (FeedsSortProperty.alphabetical, SortBoxOrder.ascending), + }, }, ); diff --git a/packages/neon/neon_news/lib/sort/folders.dart b/packages/neon/neon_news/lib/sort/folders.dart index 633c5882..cfc1bc9d 100644 --- a/packages/neon/neon_news/lib/sort/folders.dart +++ b/packages/neon/neon_news/lib/sort/folders.dart @@ -6,8 +6,12 @@ final foldersSortBox = SortBox( FoldersSortProperty.unreadCount: (final folderFeedsWrapper) => folderFeedsWrapper.$3, }, { - FoldersSortProperty.alphabetical: Box(FoldersSortProperty.unreadCount, SortBoxOrder.descending), - FoldersSortProperty.unreadCount: Box(FoldersSortProperty.alphabetical, SortBoxOrder.ascending), + FoldersSortProperty.alphabetical: { + (FoldersSortProperty.unreadCount, SortBoxOrder.descending), + }, + FoldersSortProperty.unreadCount: { + (FoldersSortProperty.alphabetical, SortBoxOrder.ascending), + }, }, ); diff --git a/packages/neon/neon_notes/lib/options.dart b/packages/neon/neon_notes/lib/options.dart index 72c7cb26..f7ac1957 100644 --- a/packages/neon/neon_notes/lib/options.dart +++ b/packages/neon/neon_notes/lib/options.dart @@ -108,6 +108,7 @@ enum DefaultNoteViewType { enum NotesSortProperty { lastModified, alphabetical, + favorite, } enum CategoriesSortProperty { diff --git a/packages/neon/neon_notes/lib/sort/categories.dart b/packages/neon/neon_notes/lib/sort/categories.dart index 42f461c4..32c16df8 100644 --- a/packages/neon/neon_notes/lib/sort/categories.dart +++ b/packages/neon/neon_notes/lib/sort/categories.dart @@ -6,7 +6,9 @@ final categoriesSortBox = SortBox( CategoriesSortProperty.notesCount: (final category) => category.count, }, { - CategoriesSortProperty.notesCount: Box(CategoriesSortProperty.alphabetical, SortBoxOrder.ascending), + CategoriesSortProperty.notesCount: { + (CategoriesSortProperty.alphabetical, SortBoxOrder.ascending), + }, }, ); diff --git a/packages/neon/neon_notes/lib/sort/notes.dart b/packages/neon/neon_notes/lib/sort/notes.dart index 137dcdfb..bed700b2 100644 --- a/packages/neon/neon_notes/lib/sort/notes.dart +++ b/packages/neon/neon_notes/lib/sort/notes.dart @@ -4,8 +4,14 @@ final notesSortBox = SortBox( { NotesSortProperty.alphabetical: (final note) => note.title.toLowerCase(), NotesSortProperty.lastModified: (final note) => note.modified, + NotesSortProperty.favorite: (final note) => note.favorite ? 0 : 1, }, { - NotesSortProperty.alphabetical: Box(NotesSortProperty.lastModified, SortBoxOrder.descending), + NotesSortProperty.alphabetical: { + (NotesSortProperty.lastModified, SortBoxOrder.descending), + }, + NotesSortProperty.lastModified: { + (NotesSortProperty.alphabetical, SortBoxOrder.ascending), + }, }, ); diff --git a/packages/neon/neon_notes/lib/widgets/notes_view.dart b/packages/neon/neon_notes/lib/widgets/notes_view.dart index 8042fe5b..03ef7266 100644 --- a/packages/neon/neon_notes/lib/widgets/notes_view.dart +++ b/packages/neon/neon_notes/lib/widgets/notes_view.dart @@ -15,30 +15,20 @@ class NotesView extends StatelessWidget { stream: bloc.notes, builder: (final context, final notes) => SortBoxBuilder( sortBox: notesSortBox, + presort: const { + (NotesSortProperty.favorite, SortBoxOrder.ascending), + }, sortPropertyOption: bloc.options.notesSortPropertyOption, sortBoxOrderOption: bloc.options.notesSortBoxOrderOption, - input: category != null - ? notes.data?.where((final note) => note.favorite && note.category == category).toList() - : notes.data?.where((final note) => note.favorite).toList(), - builder: (final context, final sortedFavorites) => SortBoxBuilder( - sortBox: notesSortBox, - sortPropertyOption: bloc.options.notesSortPropertyOption, - sortBoxOrderOption: bloc.options.notesSortBoxOrderOption, - input: category != null - ? notes.data?.where((final note) => !note.favorite && note.category == category).toList() - : notes.data?.where((final note) => !note.favorite).toList(), - builder: (final context, final sortedNonFavorites) => NeonListView( - scrollKey: 'notes-notes', - withFloatingActionButton: true, - items: [ - ...?sortedFavorites, - ...?sortedNonFavorites, - ], - isLoading: notes.isLoading, - error: notes.error, - onRefresh: bloc.refresh, - builder: _buildNote, - ), + input: category != null ? notes.data?.where((final note) => note.category == category).toList() : notes.data, + builder: (final context, final sorted) => NeonListView( + scrollKey: 'notes-notes', + withFloatingActionButton: true, + items: sorted, + isLoading: notes.isLoading, + error: notes.error, + onRefresh: bloc.refresh, + builder: _buildNote, ), ), ); diff --git a/packages/sort_box/lib/sort_box.dart b/packages/sort_box/lib/sort_box.dart index 6c174810..c7e4eca4 100644 --- a/packages/sort_box/lib/sort_box.dart +++ b/packages/sort_box/lib/sort_box.dart @@ -1,58 +1,81 @@ -// ignore_for_file: public_member_api_docs - +/// Signature of a function returning a [Comparable]. typedef ComparableGetter = Comparable Function(T); +/// Sorting Box to sort [List]s on multiple properties. class SortBox { + /// Constructs a new SortBox. + /// + /// A *Box* is a record of a property and how to order it. SortBox( this._properties, - this._secondaryBoxes, + this._boxes, ); + /// A mapping of all values [T] to their [ComparableGetter]. final Map> _properties; - final Map> _secondaryBoxes; - List sort(final List input, final Box box) { + /// A mapping of values [T] to their *Boxes*. + /// + /// The Boxes are applied if two elements are considered equal regarding their property [T]. + final Map> _boxes; + + /// Sorts the [input] list according to their [box]. + /// + /// A box contains the property and [SortBoxOrder] how the list should be sorted. + /// In case the property of two elements is considered equal all following boxes specified at `_boxes[property]` are applied. + /// If specified [presort] will be applied before [box] and [_boxes]. + /// + /// This function sorts the input in place and a reference to it mutating the provided list. + List sort( + final List input, + final (T property, SortBoxOrder order) box, [ + final Set<(T property, SortBoxOrder order)>? presort, + ]) { if (input.length <= 1) { return input; } - final comparableGetter = _properties[box.property]!; - final secondaryBox = _secondaryBoxes[box.property]; - final comparableGetter2 = _properties[secondaryBox?.property]; - - return input - ..sort( - (final item1, final item2) { - final first = _compare(item1, item2, box.order, comparableGetter); + final boxes = { + ...?presort, + box, + ...?_boxes[box.$1], + }; - if (first == 0 && secondaryBox != null) { - return _compare(item1, item2, secondaryBox.order, comparableGetter2!); - } + final sorted = input..sort((final item1, final item2) => _compare(item1, item2, boxes.iterator..moveNext())); - return first; - }, - ); + return sorted; } - int _compare(final R item1, final R item2, final SortBoxOrder order, final ComparableGetter getter) { - final comparable1 = getter(item1); - final comparable2 = getter(item2); + int _compare( + final R item1, + final R item2, + final Iterator<(T property, SortBoxOrder order)> iterator, + ) { + final box = iterator.current; + final (property, sortBoxOrder) = box; + final comparableGetter = _properties[property]!; + + final comparable1 = comparableGetter(item1); + final comparable2 = comparableGetter(item2); + + final order = switch (sortBoxOrder) { + SortBoxOrder.ascending => comparable1.compareTo(comparable2), + SortBoxOrder.descending => comparable2.compareTo(comparable1), + }; + + if (order == 0 && iterator.moveNext()) { + return _compare(item1, item2, iterator); + } - return order == SortBoxOrder.ascending ? comparable1.compareTo(comparable2) : comparable2.compareTo(comparable1); + return order; } } +/// Sorting order used by [SortBox]. enum SortBoxOrder { + /// Ascending sorting order. ascending, - descending, -} -class Box { - Box( - this.property, - this.order, - ); - - final T property; - final SortBoxOrder order; + /// Descending sorting order. + descending, } diff --git a/packages/sort_box/test/sort_box_test.dart b/packages/sort_box/test/sort_box_test.dart index 2ea10bfc..01fa842b 100644 --- a/packages/sort_box/test/sort_box_test.dart +++ b/packages/sort_box/test/sort_box_test.dart @@ -4,16 +4,19 @@ import 'package:test/test.dart'; enum FruitSort { alphabetical, count, + price, } class Fruit { - Fruit( + const Fruit( this.name, - this.count, - ); + this.count, [ + this.price, + ]); final String name; final int count; + final int? price; @override String toString() => 'Fruit(name: $name, count: $count)'; @@ -24,23 +27,32 @@ void main() { { FruitSort.alphabetical: (final fruit) => fruit.name.toLowerCase(), FruitSort.count: (final fruit) => fruit.count, + FruitSort.price: (final fruit) => fruit.price!, }, { - FruitSort.alphabetical: Box(FruitSort.count, SortBoxOrder.ascending), - FruitSort.count: Box(FruitSort.alphabetical, SortBoxOrder.ascending), + FruitSort.alphabetical: { + (FruitSort.count, SortBoxOrder.ascending), + }, + FruitSort.count: { + (FruitSort.alphabetical, SortBoxOrder.ascending), + }, + FruitSort.price: { + (FruitSort.alphabetical, SortBoxOrder.descending), + (FruitSort.count, SortBoxOrder.ascending), + }, }, ); group('Primary', () { test('Alphabetical', () { final fruits = [ - Fruit('Apple', 1), - Fruit('Banana', 2), - Fruit('Apple', 3), - Fruit('Banana', 4), - Fruit('Apple', 5), + const Fruit('Apple', 1), + const Fruit('Banana', 2), + const Fruit('Apple', 3), + const Fruit('Banana', 4), + const Fruit('Apple', 5), ]; - final sorted = sortBox.sort(fruits, Box(FruitSort.alphabetical, SortBoxOrder.ascending)); + final sorted = sortBox.sort(fruits, (FruitSort.alphabetical, SortBoxOrder.ascending)); for (var i = 0; i < 3; i++) { expect(sorted[i].name, 'Apple'); @@ -52,13 +64,13 @@ void main() { test('Count', () { final fruits = [ - Fruit('Apple', 1), - Fruit('Banana', 5), - Fruit('Apple', 4), - Fruit('Banana', 2), - Fruit('Apple', 3), + const Fruit('Apple', 1), + const Fruit('Banana', 5), + const Fruit('Apple', 4), + const Fruit('Banana', 2), + const Fruit('Apple', 3), ]; - final sorted = sortBox.sort(fruits, Box(FruitSort.count, SortBoxOrder.ascending)); + final sorted = sortBox.sort(fruits, (FruitSort.count, SortBoxOrder.ascending)); final names = ['Apple', 'Banana', 'Apple', 'Apple', 'Banana']; for (var i = 0; i < 5; i++) { @@ -73,13 +85,13 @@ void main() { group('Secondary', () { test('Alphabetical', () { final fruits = [ - Fruit('Apple', 1), - Fruit('Banana', 2), - Fruit('Apple', 2), - Fruit('Banana', 1), - Fruit('Apple', 2), + const Fruit('Apple', 1), + const Fruit('Banana', 2), + const Fruit('Apple', 2), + const Fruit('Banana', 1), + const Fruit('Apple', 2), ]; - final sorted = sortBox.sort(fruits, Box(FruitSort.count, SortBoxOrder.ascending)); + final sorted = sortBox.sort(fruits, (FruitSort.count, SortBoxOrder.ascending)); final names = ['Apple', 'Banana', 'Apple', 'Apple', 'Banana']; for (var i = 0; i < 5; i++) { @@ -94,13 +106,13 @@ void main() { test('Count', () { final fruits = [ - Fruit('Apple', 3), - Fruit('Banana', 4), - Fruit('Apple', 1), - Fruit('Banana', 2), - Fruit('Apple', 5), + const Fruit('Apple', 3), + const Fruit('Banana', 4), + const Fruit('Apple', 1), + const Fruit('Banana', 2), + const Fruit('Apple', 5), ]; - final sorted = sortBox.sort(fruits, Box(FruitSort.alphabetical, SortBoxOrder.ascending)); + final sorted = sortBox.sort(fruits, (FruitSort.alphabetical, SortBoxOrder.ascending)); for (var i = 0; i < 3; i++) { expect(sorted[i].name, 'Apple'); @@ -116,13 +128,13 @@ void main() { test('Primary all equal', () { final fruits = [ - Fruit('Coconut', 1), - Fruit('Banana', 1), - Fruit('Apple', 1), - Fruit('Elderberry', 1), - Fruit('Damson', 1), + const Fruit('Coconut', 1), + const Fruit('Banana', 1), + const Fruit('Apple', 1), + const Fruit('Elderberry', 1), + const Fruit('Damson', 1), ]; - final sorted = sortBox.sort(fruits, Box(FruitSort.count, SortBoxOrder.ascending)); + final sorted = sortBox.sort(fruits, (FruitSort.count, SortBoxOrder.ascending)); final names = ['Apple', 'Banana', 'Coconut', 'Damson', 'Elderberry']; for (var i = 0; i < 5; i++) { @@ -130,4 +142,32 @@ void main() { } }); }); + + group('Third', () { + test('Count', () { + final fruits = [ + const Fruit('Apple', 1, 3), + const Fruit('Banana', 2, 2), + const Fruit('Apple', 2, 0), + const Fruit('Banana', 1, 3), + const Fruit('Apple', 2, 3), + ]; + final sorted = sortBox.sort(fruits, (FruitSort.price, SortBoxOrder.ascending)); + + final price = [0, 2, 3, 3, 3]; + for (var i = 0; i < 5; i++) { + expect(sorted[i].price, price[i]); + } + + final names = ['Apple', 'Banana', 'Banana', 'Apple', 'Apple']; + for (var i = 0; i < 5; i++) { + expect(sorted[i].name, names[i]); + } + + final counts = [2, 2, 1, 1, 2]; + for (var i = 0; i < 5; i++) { + expect(sorted[i].count, counts[i]); + } + }); + }); }