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: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 {
const SortBoxBuilder({
SortBoxBuilder({
required this.sortBox,
required this.sortPropertyOption,
required this.sortBoxOrderOption,
required this.input,
required final List<R>? input,
required this.builder,
this.presort,
super.key,
});
}) : input = input ?? [];
final SortBox<T, R> sortBox;
final SelectOption<T> sortPropertyOption;
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
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<T>(
@ -29,9 +46,9 @@ class SortBoxBuilder<T extends Enum, R> extends StatelessWidget {
builder: (final context, final property, final _) => ValueListenableBuilder<SortBoxOrder>(
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));
},
),
);

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.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),
},
},
);

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

@ -60,8 +60,7 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
items: [
for (final uploadTask in tasksSnapshot.requireData.whereType<FilesUploadTask>().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<FilesBrowserView> {
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,
);
},
),
],
],
],

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.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),
},
},
);

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.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),
},
},
);

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.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),
},
},
);

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

@ -108,6 +108,7 @@ enum DefaultNoteViewType {
enum NotesSortProperty {
lastModified,
alphabetical,
favorite,
}
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: 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.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,
builder: (final context, final notes) => SortBoxBuilder<NotesSortProperty, NotesNote>(
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<NotesSortProperty, NotesNote>(
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<NotesNote>(
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<NotesNote>(
scrollKey: 'notes-notes',
withFloatingActionButton: true,
items: sorted,
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);
/// Sorting Box to sort [List]s on multiple properties.
class SortBox<T extends Enum, R> {
/// 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<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) {
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<R> 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<T extends Enum> {
Box(
this.property,
this.order,
);
final T property;
final SortBoxOrder order;
/// Descending sorting order.
descending,
}

110
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]);
}
});
});
}

Loading…
Cancel
Save