Browse Source

Merge pull request #591 from nextcloud/feat/sort_box/multi_sorting

Feat/sort box/multi sorting
pull/581/head
Nikolas Rimikis 1 year ago committed by GitHub
parent
commit
2ed626870d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 35
      packages/neon/neon/lib/src/sort_box/sort_box_builder.dart
  2. 8
      packages/neon/neon_files/lib/sort/files.dart
  3. 53
      packages/neon/neon_files/lib/widgets/browser_view.dart
  4. 8
      packages/neon/neon_news/lib/sort/articles.dart
  5. 8
      packages/neon/neon_news/lib/sort/feeds.dart
  6. 8
      packages/neon/neon_news/lib/sort/folders.dart
  7. 1
      packages/neon/neon_notes/lib/options.dart
  8. 4
      packages/neon/neon_notes/lib/sort/categories.dart
  9. 8
      packages/neon/neon_notes/lib/sort/notes.dart
  10. 34
      packages/neon/neon_notes/lib/widgets/notes_view.dart
  11. 89
      packages/sort_box/lib/sort_box.dart
  12. 110
      packages/sort_box/test/sort_box_test.dart

35
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:neon/src/settings/models/select_option.dart';
import 'package:sort_box/sort_box.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<T> = Widget Function(BuildContext context, List<T> sorted);
/// Sorted list builder.
///
/// Used together with a [SortBox] to sort a given list.
class SortBoxBuilder<T extends Enum, R> extends StatelessWidget { class SortBoxBuilder<T extends Enum, R> extends StatelessWidget {
const SortBoxBuilder({ SortBoxBuilder({
required this.sortBox, required this.sortBox,
required this.sortPropertyOption, required this.sortPropertyOption,
required this.sortBoxOrderOption, required this.sortBoxOrderOption,
required this.input, required final List<R>? input,
required this.builder, required this.builder,
this.presort,
super.key, super.key,
}); }) : input = input ?? [];
final SortBox<T, R> sortBox; final SortBox<T, R> sortBox;
final SelectOption<T> sortPropertyOption; final SelectOption<T> sortPropertyOption;
final SelectOption<SortBoxOrder> sortBoxOrderOption; final SelectOption<SortBoxOrder> sortBoxOrderOption;
final List<R>? input;
final Widget Function(BuildContext, List<R>?) builder; /// Input list to sort.
final List<R> input;
/// Child builder using the sorted list.
final SortBoxWidgetBuilder<R> builder;
/// Pre sorts input.
final Set<(T property, SortBoxOrder order)>? presort;
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
if (input == null || (input?.isEmpty ?? false)) { if (input.length <= 1) {
return builder(context, null); // input is already sorted.
return builder(context, input);
} }
return ValueListenableBuilder<T>( return ValueListenableBuilder<T>(
@ -29,9 +46,9 @@ class SortBoxBuilder<T extends Enum, R> extends StatelessWidget {
builder: (final context, final property, final _) => ValueListenableBuilder<SortBoxOrder>( builder: (final context, final property, final _) => ValueListenableBuilder<SortBoxOrder>(
valueListenable: sortBoxOrderOption, valueListenable: sortBoxOrderOption,
builder: (final context, final order, final _) { 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));
}, },
), ),
); );

8
packages/neon/neon_files/lib/sort/files.dart

@ -7,7 +7,11 @@ final filesSortBox = SortBox<FilesSortProperty, WebDavFile>(
FilesSortProperty.size: (final file) => file.size ?? 0, FilesSortProperty.size: (final file) => file.size ?? 0,
}, },
{ {
FilesSortProperty.modifiedDate: Box(FilesSortProperty.name, SortBoxOrder.ascending), FilesSortProperty.modifiedDate: {
FilesSortProperty.size: Box(FilesSortProperty.name, SortBoxOrder.ascending), (FilesSortProperty.name, SortBoxOrder.ascending),
},
FilesSortProperty.size: {
(FilesSortProperty.name, SortBoxOrder.ascending),
},
}, },
); );

53
packages/neon/neon_files/lib/widgets/browser_view.dart

@ -60,8 +60,7 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
items: [ items: [
for (final uploadTask in tasksSnapshot.requireData.whereType<FilesUploadTask>().where( for (final uploadTask in tasksSnapshot.requireData.whereType<FilesUploadTask>().where(
(final task) => (final task) =>
sorted?.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty ?? sorted.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty,
false,
)) ...[ )) ...[
FileListTile( FileListTile(
bloc: widget.filesBloc, bloc: widget.filesBloc,
@ -73,34 +72,32 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
onPickFile: widget.onPickFile, onPickFile: widget.onPickFile,
), ),
], ],
if (sorted != null) ...[ for (final file in sorted) ...[
for (final file in sorted) ...[ if (!widget.onlyShowDirectories || file.isDirectory) ...[
if (!widget.onlyShowDirectories || file.isDirectory) ...[ Builder(
Builder( builder: (final context) {
builder: (final context) { final matchingTask = tasksSnapshot.requireData
final matchingTask = tasksSnapshot.requireData .firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name));
.firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name));
final details = matchingTask != null final details = matchingTask != null
? FileDetails.fromTask( ? FileDetails.fromTask(
task: matchingTask, task: matchingTask,
file: file, file: file,
) )
: FileDetails.fromWebDav( : FileDetails.fromWebDav(
file: file, file: file,
path: widget.bloc.path.value, path: widget.bloc.path.value,
); );
return FileListTile( return FileListTile(
bloc: widget.filesBloc, bloc: widget.filesBloc,
browserBloc: widget.bloc, browserBloc: widget.bloc,
details: details, details: details,
enableFileActions: widget.enableFileActions, enableFileActions: widget.enableFileActions,
onPickFile: widget.onPickFile, onPickFile: widget.onPickFile,
); );
}, },
), ),
],
], ],
], ],
], ],

8
packages/neon/neon_news/lib/sort/articles.dart

@ -7,7 +7,11 @@ final articlesSortBox = SortBox<ArticlesSortProperty, NewsArticle>(
ArticlesSortProperty.byFeed: (final article) => article.feedId, ArticlesSortProperty.byFeed: (final article) => article.feedId,
}, },
{ {
ArticlesSortProperty.alphabetical: Box(ArticlesSortProperty.publishDate, SortBoxOrder.descending), ArticlesSortProperty.alphabetical: {
ArticlesSortProperty.byFeed: Box(ArticlesSortProperty.alphabetical, SortBoxOrder.ascending), (ArticlesSortProperty.publishDate, SortBoxOrder.descending),
},
ArticlesSortProperty.byFeed: {
(ArticlesSortProperty.alphabetical, SortBoxOrder.ascending),
},
}, },
); );

8
packages/neon/neon_news/lib/sort/feeds.dart

@ -6,7 +6,11 @@ final feedsSortBox = SortBox<FeedsSortProperty, NewsFeed>(
FeedsSortProperty.unreadCount: (final feed) => feed.unreadCount ?? 0, FeedsSortProperty.unreadCount: (final feed) => feed.unreadCount ?? 0,
}, },
{ {
FeedsSortProperty.alphabetical: Box(FeedsSortProperty.unreadCount, SortBoxOrder.descending), FeedsSortProperty.alphabetical: {
FeedsSortProperty.unreadCount: Box(FeedsSortProperty.alphabetical, SortBoxOrder.ascending), (FeedsSortProperty.unreadCount, SortBoxOrder.descending),
},
FeedsSortProperty.unreadCount: {
(FeedsSortProperty.alphabetical, SortBoxOrder.ascending),
},
}, },
); );

8
packages/neon/neon_news/lib/sort/folders.dart

@ -6,8 +6,12 @@ final foldersSortBox = SortBox<FoldersSortProperty, FolderFeedsWrapper>(
FoldersSortProperty.unreadCount: (final folderFeedsWrapper) => folderFeedsWrapper.$3, FoldersSortProperty.unreadCount: (final folderFeedsWrapper) => folderFeedsWrapper.$3,
}, },
{ {
FoldersSortProperty.alphabetical: Box(FoldersSortProperty.unreadCount, SortBoxOrder.descending), FoldersSortProperty.alphabetical: {
FoldersSortProperty.unreadCount: Box(FoldersSortProperty.alphabetical, SortBoxOrder.ascending), (FoldersSortProperty.unreadCount, SortBoxOrder.descending),
},
FoldersSortProperty.unreadCount: {
(FoldersSortProperty.alphabetical, SortBoxOrder.ascending),
},
}, },
); );

1
packages/neon/neon_notes/lib/options.dart

@ -108,6 +108,7 @@ enum DefaultNoteViewType {
enum NotesSortProperty { enum NotesSortProperty {
lastModified, lastModified,
alphabetical, alphabetical,
favorite,
} }
enum CategoriesSortProperty { enum CategoriesSortProperty {

4
packages/neon/neon_notes/lib/sort/categories.dart

@ -6,7 +6,9 @@ final categoriesSortBox = SortBox<CategoriesSortProperty, NoteCategory>(
CategoriesSortProperty.notesCount: (final category) => category.count, CategoriesSortProperty.notesCount: (final category) => category.count,
}, },
{ {
CategoriesSortProperty.notesCount: Box(CategoriesSortProperty.alphabetical, SortBoxOrder.ascending), CategoriesSortProperty.notesCount: {
(CategoriesSortProperty.alphabetical, SortBoxOrder.ascending),
},
}, },
); );

8
packages/neon/neon_notes/lib/sort/notes.dart

@ -4,8 +4,14 @@ final notesSortBox = SortBox<NotesSortProperty, NotesNote>(
{ {
NotesSortProperty.alphabetical: (final note) => note.title.toLowerCase(), NotesSortProperty.alphabetical: (final note) => note.title.toLowerCase(),
NotesSortProperty.lastModified: (final note) => note.modified, 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),
},
}, },
); );

34
packages/neon/neon_notes/lib/widgets/notes_view.dart

@ -15,30 +15,20 @@ class NotesView extends StatelessWidget {
stream: bloc.notes, stream: bloc.notes,
builder: (final context, final notes) => SortBoxBuilder<NotesSortProperty, NotesNote>( builder: (final context, final notes) => SortBoxBuilder<NotesSortProperty, NotesNote>(
sortBox: notesSortBox, sortBox: notesSortBox,
presort: const {
(NotesSortProperty.favorite, SortBoxOrder.ascending),
},
sortPropertyOption: bloc.options.notesSortPropertyOption, sortPropertyOption: bloc.options.notesSortPropertyOption,
sortBoxOrderOption: bloc.options.notesSortBoxOrderOption, sortBoxOrderOption: bloc.options.notesSortBoxOrderOption,
input: category != null input: category != null ? notes.data?.where((final note) => note.category == category).toList() : notes.data,
? notes.data?.where((final note) => note.favorite && note.category == category).toList() builder: (final context, final sorted) => NeonListView<NotesNote>(
: notes.data?.where((final note) => note.favorite).toList(), scrollKey: 'notes-notes',
builder: (final context, final sortedFavorites) => SortBoxBuilder<NotesSortProperty, NotesNote>( withFloatingActionButton: true,
sortBox: notesSortBox, items: sorted,
sortPropertyOption: bloc.options.notesSortPropertyOption, isLoading: notes.isLoading,
sortBoxOrderOption: bloc.options.notesSortBoxOrderOption, error: notes.error,
input: category != null onRefresh: bloc.refresh,
? notes.data?.where((final note) => !note.favorite && note.category == category).toList() builder: _buildNote,
: notes.data?.where((final note) => !note.favorite).toList(),
builder: (final context, final sortedNonFavorites) => NeonListView<NotesNote>(
scrollKey: 'notes-notes',
withFloatingActionButton: true,
items: [
...?sortedFavorites,
...?sortedNonFavorites,
],
isLoading: notes.isLoading,
error: notes.error,
onRefresh: bloc.refresh,
builder: _buildNote,
),
), ),
), ),
); );

89
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<T> = Comparable Function(T); typedef ComparableGetter<T> = Comparable Function(T);
/// Sorting Box to sort [List]s on multiple properties.
class SortBox<T extends Enum, R> { class SortBox<T extends Enum, R> {
/// Constructs a new SortBox.
///
/// A *Box* is a record of a property and how to order it.
SortBox( SortBox(
this._properties, this._properties,
this._secondaryBoxes, this._boxes,
); );
/// A mapping of all values [T] to their [ComparableGetter].
final Map<T, ComparableGetter<R>> _properties; final Map<T, ComparableGetter<R>> _properties;
final Map<T, Box<T>> _secondaryBoxes;
List<R> sort(final List<R> input, final Box<T> 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<T, Set<(T property, SortBoxOrder order)>> _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<R> sort(
final List<R> input,
final (T property, SortBoxOrder order) box, [
final Set<(T property, SortBoxOrder order)>? presort,
]) {
if (input.length <= 1) { if (input.length <= 1) {
return input; return input;
} }
final comparableGetter = _properties[box.property]!; final boxes = {
final secondaryBox = _secondaryBoxes[box.property]; ...?presort,
final comparableGetter2 = _properties[secondaryBox?.property]; box,
...?_boxes[box.$1],
return input };
..sort(
(final item1, final item2) {
final first = _compare(item1, item2, box.order, comparableGetter);
if (first == 0 && secondaryBox != null) { final sorted = input..sort((final item1, final item2) => _compare(item1, item2, boxes.iterator..moveNext()));
return _compare(item1, item2, secondaryBox.order, comparableGetter2!);
}
return first; return sorted;
},
);
} }
int _compare(final R item1, final R item2, final SortBoxOrder order, final ComparableGetter<R> getter) { int _compare(
final comparable1 = getter(item1); final R item1,
final comparable2 = getter(item2); 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 { enum SortBoxOrder {
/// Ascending sorting order.
ascending, ascending,
descending,
}
class Box<T extends Enum> { /// Descending sorting order.
Box( descending,
this.property,
this.order,
);
final T property;
final SortBoxOrder order;
} }

110
packages/sort_box/test/sort_box_test.dart

@ -4,16 +4,19 @@ import 'package:test/test.dart';
enum FruitSort { enum FruitSort {
alphabetical, alphabetical,
count, count,
price,
} }
class Fruit { class Fruit {
Fruit( const Fruit(
this.name, this.name,
this.count, this.count, [
); this.price,
]);
final String name; final String name;
final int count; final int count;
final int? price;
@override @override
String toString() => 'Fruit(name: $name, count: $count)'; String toString() => 'Fruit(name: $name, count: $count)';
@ -24,23 +27,32 @@ void main() {
{ {
FruitSort.alphabetical: (final fruit) => fruit.name.toLowerCase(), FruitSort.alphabetical: (final fruit) => fruit.name.toLowerCase(),
FruitSort.count: (final fruit) => fruit.count, FruitSort.count: (final fruit) => fruit.count,
FruitSort.price: (final fruit) => fruit.price!,
}, },
{ {
FruitSort.alphabetical: Box(FruitSort.count, SortBoxOrder.ascending), FruitSort.alphabetical: {
FruitSort.count: Box(FruitSort.alphabetical, SortBoxOrder.ascending), (FruitSort.count, SortBoxOrder.ascending),
},
FruitSort.count: {
(FruitSort.alphabetical, SortBoxOrder.ascending),
},
FruitSort.price: {
(FruitSort.alphabetical, SortBoxOrder.descending),
(FruitSort.count, SortBoxOrder.ascending),
},
}, },
); );
group('Primary', () { group('Primary', () {
test('Alphabetical', () { test('Alphabetical', () {
final fruits = [ final fruits = [
Fruit('Apple', 1), const Fruit('Apple', 1),
Fruit('Banana', 2), const Fruit('Banana', 2),
Fruit('Apple', 3), const Fruit('Apple', 3),
Fruit('Banana', 4), const Fruit('Banana', 4),
Fruit('Apple', 5), 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++) { for (var i = 0; i < 3; i++) {
expect(sorted[i].name, 'Apple'); expect(sorted[i].name, 'Apple');
@ -52,13 +64,13 @@ void main() {
test('Count', () { test('Count', () {
final fruits = [ final fruits = [
Fruit('Apple', 1), const Fruit('Apple', 1),
Fruit('Banana', 5), const Fruit('Banana', 5),
Fruit('Apple', 4), const Fruit('Apple', 4),
Fruit('Banana', 2), const Fruit('Banana', 2),
Fruit('Apple', 3), 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']; final names = ['Apple', 'Banana', 'Apple', 'Apple', 'Banana'];
for (var i = 0; i < 5; i++) { for (var i = 0; i < 5; i++) {
@ -73,13 +85,13 @@ void main() {
group('Secondary', () { group('Secondary', () {
test('Alphabetical', () { test('Alphabetical', () {
final fruits = [ final fruits = [
Fruit('Apple', 1), const Fruit('Apple', 1),
Fruit('Banana', 2), const Fruit('Banana', 2),
Fruit('Apple', 2), const Fruit('Apple', 2),
Fruit('Banana', 1), const Fruit('Banana', 1),
Fruit('Apple', 2), 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']; final names = ['Apple', 'Banana', 'Apple', 'Apple', 'Banana'];
for (var i = 0; i < 5; i++) { for (var i = 0; i < 5; i++) {
@ -94,13 +106,13 @@ void main() {
test('Count', () { test('Count', () {
final fruits = [ final fruits = [
Fruit('Apple', 3), const Fruit('Apple', 3),
Fruit('Banana', 4), const Fruit('Banana', 4),
Fruit('Apple', 1), const Fruit('Apple', 1),
Fruit('Banana', 2), const Fruit('Banana', 2),
Fruit('Apple', 5), 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++) { for (var i = 0; i < 3; i++) {
expect(sorted[i].name, 'Apple'); expect(sorted[i].name, 'Apple');
@ -116,13 +128,13 @@ void main() {
test('Primary all equal', () { test('Primary all equal', () {
final fruits = [ final fruits = [
Fruit('Coconut', 1), const Fruit('Coconut', 1),
Fruit('Banana', 1), const Fruit('Banana', 1),
Fruit('Apple', 1), const Fruit('Apple', 1),
Fruit('Elderberry', 1), const Fruit('Elderberry', 1),
Fruit('Damson', 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']; final names = ['Apple', 'Banana', 'Coconut', 'Damson', 'Elderberry'];
for (var i = 0; i < 5; i++) { 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]);
}
});
});
} }

Loading…
Cancel
Save