From 341f0058dc877963ce40b402861ecd6607672053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magazz=C3=B9=20Giuseppe?= Date: Tue, 27 Oct 2020 10:16:00 +0100 Subject: [PATCH 01/20] [skip ci] update README.md --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d2d9bce..c034122 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Jap Vocabs -![deploy](https://github.com/Darklod/jap-vocabs/workflows/deploy/badge.svg?branch=master) -![dev](https://github.com/Darklod/jap-vocabs/workflows/dev/badge.svg?branch=develop) -![codecov](https://codecov.io/gh/Darklod/jap-vocabs/branch/develop/graph/badge.svg?token=C4RT80DY1S) -![Flutter version](https://img.shields.io/badge/flutter-v1.22.2-blue?logo=flutter) -![style: pedantic](https://img.shields.io/badge/style-pedantic-00b4ab.svg) +[![deploy](https://github.com/Darklod/jap-vocabs/workflows/deploy/badge.svg?branch=master)](https://github.com/Darklod/jap-vocabs/actions?query=workflow%release) +[![dev](https://github.com/Darklod/jap-vocabs/workflows/dev/badge.svg?branch=develop)](https://github.com/Darklod/jap-vocabs/actions?query=workflow%3Adevelop) +[![codecov](https://codecov.io/gh/Darklod/jap-vocabs/branch/develop/graph/badge.svg?token=C4RT80DY1S)](https://codecov.io/gh/Darklod/jap-vocabs) +[![Flutter version](https://img.shields.io/badge/flutter-v1.22.2-blue?logo=flutter)](https://flutter.dev/docs/development/tools/sdk/releases) +[![style: pedantic](https://img.shields.io/badge/style-pedantic-00b4ab.svg)](https://pub.dev/packages/pedantic) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FDarklod%2Fjap-vocabs.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FDarklod%2Fjap-vocabs?ref=badge_shield) From 7c21ba46506526ca6f436258785827b326fca103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magazz=C3=B9=20Giuseppe?= Date: Tue, 27 Oct 2020 10:18:08 +0100 Subject: [PATCH 02/20] [skip ci] update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c034122..cbcbefd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Jap Vocabs -[![deploy](https://github.com/Darklod/jap-vocabs/workflows/deploy/badge.svg?branch=master)](https://github.com/Darklod/jap-vocabs/actions?query=workflow%release) -[![dev](https://github.com/Darklod/jap-vocabs/workflows/dev/badge.svg?branch=develop)](https://github.com/Darklod/jap-vocabs/actions?query=workflow%3Adevelop) +[![release](https://github.com/Darklod/jap-vocabs/workflows/release/badge.svg?branch=master)](https://github.com/Darklod/jap-vocabs/actions?query=workflow%release) +[![dev](https://github.com/Darklod/jap-vocabs/workflows/dev/badge.svg?branch=develop)](https://github.com/Darklod/jap-vocabs/actions?query=workflow%3Adev) [![codecov](https://codecov.io/gh/Darklod/jap-vocabs/branch/develop/graph/badge.svg?token=C4RT80DY1S)](https://codecov.io/gh/Darklod/jap-vocabs) [![Flutter version](https://img.shields.io/badge/flutter-v1.22.2-blue?logo=flutter)](https://flutter.dev/docs/development/tools/sdk/releases) [![style: pedantic](https://img.shields.io/badge/style-pedantic-00b4ab.svg)](https://pub.dev/packages/pedantic) From 53da0501c39d9ab3cc7aca7e1306873e777eae12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magazz=C3=B9=20Giuseppe?= Date: Tue, 27 Oct 2020 10:19:17 +0100 Subject: [PATCH 03/20] [skip ci] update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cbcbefd..393bc8c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Jap Vocabs -[![release](https://github.com/Darklod/jap-vocabs/workflows/release/badge.svg?branch=master)](https://github.com/Darklod/jap-vocabs/actions?query=workflow%release) +[![release](https://github.com/Darklod/jap-vocabs/workflows/release/badge.svg?branch=master)](https://github.com/Darklod/jap-vocabs/actions?query=workflow%3Arelease) [![dev](https://github.com/Darklod/jap-vocabs/workflows/dev/badge.svg?branch=develop)](https://github.com/Darklod/jap-vocabs/actions?query=workflow%3Adev) [![codecov](https://codecov.io/gh/Darklod/jap-vocabs/branch/develop/graph/badge.svg?token=C4RT80DY1S)](https://codecov.io/gh/Darklod/jap-vocabs) [![Flutter version](https://img.shields.io/badge/flutter-v1.22.2-blue?logo=flutter)](https://flutter.dev/docs/development/tools/sdk/releases) From 5772222fe8e942fe44b11067ebadbea87a9122a1 Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Tue, 27 Oct 2020 12:38:56 +0100 Subject: [PATCH 04/20] [skip ci] update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index cd340fc..e1b2613 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ .pub/ /build/ /ios/ +/coverage/ # Web related lib/generated_plugin_registrant.dart From 896380e137499d9632020733383c1027008e97fb Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Tue, 27 Oct 2020 13:40:28 +0100 Subject: [PATCH 05/20] [skip ci] update utils/date.dart --- lib/utils/date.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/date.dart b/lib/utils/date.dart index 7fb150a..2a22fa9 100644 --- a/lib/utils/date.dart +++ b/lib/utils/date.dart @@ -14,7 +14,7 @@ class Date { final millis = date.millisecondsSinceEpoch - DateTime.now().millisecondsSinceEpoch; - + if (millis <= 0) return '今'; final seconds = millis / 1000; From 3e28e37afc5bd8b622ef4cb9c26bff6ab97a0060 Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Tue, 27 Oct 2020 13:41:24 +0100 Subject: [PATCH 06/20] [skip ci] add tests for date utils --- test/date_test.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/date_test.dart b/test/date_test.dart index 42101dc..1b1531b 100644 --- a/test/date_test.dart +++ b/test/date_test.dart @@ -5,6 +5,22 @@ void main() { group('format', () { final now = DateTime.now(); + test('date and time', () { + final date = DateTime(now.year, now.month, now.day, 14, 30); + + expect( + Date.format(date, full: true), + '${date.year}年${date.month}月${date.day}日', + reason: 'It must be ${date.year}/${date.month}/${date.day}', + ); + + expect( + Date.format(date, full: true, time: true), + '${date.year}年${date.month}月${date.day}日 14時30分', + reason: 'It must be ${date.year}/${date.month}/${date.day} 14:30', + ); + }); + test('today', () { final date = DateTime(now.year, now.month, now.day, 7); From 572c34972e90fa948b29cfd68a505f68a00fcb0a Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Tue, 27 Oct 2020 13:42:53 +0100 Subject: [PATCH 07/20] [skip ci] add tests for review --- test/review_test.dart | 132 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 114 insertions(+), 18 deletions(-) diff --git a/test/review_test.dart b/test/review_test.dart index c6f6b0e..e27f5a5 100644 --- a/test/review_test.dart +++ b/test/review_test.dart @@ -1,9 +1,63 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:jap_vocab/models/review.dart'; +import 'package:sembast/timestamp.dart'; void main() { + group('accuracy', () { + test('It must be 0.5', () { + final r = Review(timesCorrect: 5, timesIncorrect: 5); + expect(r.accuracy, 0.5); + }); + + test('It must be 0', () { + final r1 = Review(timesCorrect: 0, timesIncorrect: 5); + final r2 = Review(timesCorrect: 0, timesIncorrect: 0); + expect(r1.accuracy, 0.0); + expect(r2.accuracy, 0.0); + }); + + test('It must be 1', () { + final r = Review(timesCorrect: 5, timesIncorrect: 0); + expect(r.accuracy, 1.0); + }); + }); + + test('toMap', () { + final r = Review(last: DateTime.now(), next: null); + + expect( + r.toMap()['last'], + isInstanceOf(), + reason: 'It must be a Timestamp', + ); + expect(r.toMap()['next'], null); + }); + + group('fromMap', () { + test('Timestamp argument', () { + final map1 = {'last': null, 'next': Timestamp.now()}; + final r1 = Review.fromMap(map1); + + expect( + r1.next, + isInstanceOf(), + reason: 'It must be a DateTime', + ); + expect(r1.last, null); + }); + + test('Other types argument', () { + final map2 = {'last': 'test', 'next': DateTime.now()}; + final r2 = Review.fromMap(map2); + + expect(r2.last, null); + expect(r2.next, null); + }); + }); + group('copyWith', () { final review = Review( + id: '2345u334udfsnfb347', ef: 2.6, interval: 6, reviewType: 'meaning', @@ -35,24 +89,66 @@ void main() { itemId: 'itemId', ); - expect(review.id == copy.id, false, reason: 'id must be different'); - expect(review.ef == copy.ef, false, reason: 'ef must be different'); - expect(review.interval == copy.interval, false, - reason: 'interval must be different'); - expect(review.last == copy.last, false, reason: 'last must be different'); - expect(review.next == copy.next, false, reason: 'next must be different'); - expect(review.reviewType == copy.reviewType, false, - reason: 'reviewType must be different'); - expect(review.type == copy.type, false, reason: 'type must be different'); - expect(review.streak == copy.streak, false, - reason: 'streak must be different'); - expect(review.type == copy.type, false, reason: 'type must be different'); - expect(review.timesCorrect == copy.timesCorrect, false, - reason: 'timesCorrect must be different'); - expect(review.timesIncorrect == copy.timesIncorrect, false, - reason: 'timesIncorrect must be different'); - expect(review.itemId == copy.itemId, false, - reason: 'itemId must be different'); + expect( + review.id == copy.id, + false, + reason: 'id must be different', + ); + expect( + review.ef == copy.ef, + false, + reason: 'ef must be different', + ); + expect( + review.interval == copy.interval, + false, + reason: 'interval must be different', + ); + expect( + review.last == copy.last, + false, + reason: 'last must be different', + ); + expect( + review.next == copy.next, + false, + reason: 'next must be different', + ); + expect( + review.reviewType == copy.reviewType, + false, + reason: 'reviewType must be different', + ); + expect( + review.type == copy.type, + false, + reason: 'type must be different', + ); + expect( + review.streak == copy.streak, + false, + reason: 'streak must be different', + ); + expect( + review.type == copy.type, + false, + reason: 'type must be different', + ); + expect( + review.timesCorrect == copy.timesCorrect, + false, + reason: 'timesCorrect must be different', + ); + expect( + review.timesIncorrect == copy.timesIncorrect, + false, + reason: 'timesIncorrect must be different', + ); + expect( + review.itemId == copy.itemId, + false, + reason: 'itemId must be different', + ); expect(copy == review, false, reason: 'The copy must be not equal'); }); From 78562586dd4010e650040ce2f960a22fd74fda17 Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Tue, 27 Oct 2020 13:46:39 +0100 Subject: [PATCH 08/20] fixed unhandled exception --- lib/models/review.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/models/review.dart b/lib/models/review.dart index 6cc03cc..76197e4 100644 --- a/lib/models/review.dart +++ b/lib/models/review.dart @@ -56,8 +56,8 @@ class Review { ef: map['EF'], interval: map['interval'], streak: map['streak'], - last: map['last'] != null ? map['last'].toDateTime() : null, - next: map['next'] != null ? map['next'].toDateTime() : null, + last: map['last'] is Timestamp ? map['last'].toDateTime() : null, + next: map['next'] is Timestamp ? map['next'].toDateTime() : null, timesCorrect: map['times_correct'], timesIncorrect: map['times_incorrect'], reviewType: map['review_type'], From 47fa75e9c9afb2236289a2c5445be607b94ff9b7 Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Tue, 27 Oct 2020 17:00:52 +0100 Subject: [PATCH 09/20] [skip ci] refactor sm2_test.dart --- test/sm2_test.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/sm2_test.dart b/test/sm2_test.dart index c1a568a..59dd89b 100644 --- a/test/sm2_test.dart +++ b/test/sm2_test.dart @@ -46,14 +46,17 @@ void main() { var oldR = Review(); for (var i = 0; i < 100; i++) { final q = Random().nextInt(6); - var newR = SM2.newIteration(oldR, q); + final newR = SM2.newIteration(oldR, q); + + final newEF = newR.ef.toStringAsPrecision(2); + final oldEF = oldR.ef.toStringAsPrecision(2); if (q == 4) { - expect(newR.ef.toStringAsPrecision(2), oldR.ef.toStringAsPrecision(2)); + expect(newEF, oldEF); } if (q < 3) { - expect(newR.ef.toStringAsPrecision(2), oldR.ef.toStringAsPrecision(2)); + expect(newEF, oldEF); expect(newR.streak, 0); } From 4a6abf56b9eee06cc089e452748eb6e98960b848 Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Tue, 27 Oct 2020 17:02:41 +0100 Subject: [PATCH 10/20] [skip ci] add tests for item --- lib/models/item.dart | 29 ++++++-------- test/item_test.dart | 94 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 16 deletions(-) diff --git a/lib/models/item.dart b/lib/models/item.dart index f69fafe..b039e4f 100644 --- a/lib/models/item.dart +++ b/lib/models/item.dart @@ -35,30 +35,19 @@ class Item { this.reviewId2, }); - int _checkJLPT(int jlpt) { - if (jlpt < 0 || jlpt > 5) { - throw RangeError.range(jlpt, 0, 5); - } - - return jlpt; - } - Map toMap() { final map = { 'id': id, 'text': text, 'type': type, - 'jlpt': jlpt, - 'favorite': favorite, + 'meaning': meaning, 'examples': examples == null ? [] : examples.map((e) => e.toMap()).toList(), 'reviewId1': reviewId1, 'reviewId2': reviewId2, }; - if (meaning != null && meaning.isNotEmpty) { - map['meaning'] = meaning; - } + // Don't persist empty fields that are not required if (reading != null && reading.isNotEmpty) { map['reading'] = reading; @@ -68,7 +57,15 @@ class Item { map['part_of_speech'] = partOfSpeech; } - if (numberOfStrokes != null) { + if (favorite) { + map['favorite'] = favorite; + } + + if (jlpt != null && jlpt > 0) { + map['jlpt'] = jlpt; + } + + if (numberOfStrokes != null && numberOfStrokes > 0) { map['number_of_strokes'] = numberOfStrokes; } @@ -114,7 +111,7 @@ class Item { type: type ?? this.type, reading: reading ?? this.reading, meaning: meaning ?? this.meaning, - jlpt: jlpt ?? _checkJLPT(this.jlpt), + jlpt: jlpt ?? this.jlpt, numberOfStrokes: numberOfStrokes ?? this.numberOfStrokes, partOfSpeech: partOfSpeech ?? this.partOfSpeech, favorite: favorite ?? this.favorite, @@ -126,7 +123,7 @@ class Item { DateTime get nextReview { if (review1?.next == null && review2?.next == null) return null; - + var r1 = 8640000000000000; // maxMillisecondsSinceEpoch var r2 = 8640000000000000; // maxMillisecondsSinceEpoch diff --git a/test/item_test.dart b/test/item_test.dart index 5e7b891..f8d3b61 100644 --- a/test/item_test.dart +++ b/test/item_test.dart @@ -1,4 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:jap_vocab/models/example.dart'; import 'package:jap_vocab/models/item.dart'; import 'package:jap_vocab/models/review.dart'; import 'package:uuid/uuid.dart'; @@ -135,4 +136,97 @@ void main() { expect(item.nextReview == r2.next, true); }); }); + + group('toMap', () { + final item = Item( + text: '溶岩', + meaning: 'lava', + type: 'word', + reading: 'ようがん', + jlpt: 3, + partOfSpeech: 'sostantivo', + ); + + test('example', () { + final item1 = item.toMap(); + final item2 = item.copyWith(examples: []).toMap(); + final item3 = item.copyWith(examples: [Example(), Example()]).toMap(); + + expect(item1['examples'], []); + expect(item2['examples'], isInstanceOf>>()); + expect(item3['examples'], isInstanceOf>>()); + }); + + test('empty fields', () { + final item1 = Item( + text: '水', + meaning: 'acqua', + type: 'word', + ).toMap(); + + final item2 = Item( + text: '水', + meaning: 'acqua', + type: 'word', + reading: 'mizu', + favorite: true, + partOfSpeech: 'sostantivo', + jlpt: 3, + numberOfStrokes: 12, + ).toMap(); + + final fieldsToCheck = [ + 'reading', + 'part_of_speech', + 'favorite', + 'jlpt', + 'number_of_strokes', + ]; + + for (var i = 0; i < fieldsToCheck.length; i++) { + expect( + item1.containsKey(fieldsToCheck[i]), + false, + reason: 'Field \'${fieldsToCheck[i]}\' must be absent', + ); + expect( + item2.containsKey(fieldsToCheck[i]), + true, + reason: 'Field \'${fieldsToCheck[i]}\' must be present', + ); + } + }); + }); + + group('fromMap', () { + test('examples argument', () { + final item1 = Item.fromMap({ + 'examples': null, + }); + final item2 = Item.fromMap({ + 'examples': >[], + }); + final item3 = Item.fromMap({ + 'examples': >[{}], + }); + + expect( + item1.examples, + [], + reason: 'It must be an empty list', + ); + + expect( + item2.examples, + isInstanceOf>(), + reason: 'It must be an instance of List', + ); + + expect( + item3.examples, + isInstanceOf>(), + reason: 'It must be an instance of List', + ); + }); + }); } From 8de6c64855108d9e47702f244fa415e6b2a7fd98 Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Wed, 28 Oct 2020 21:12:17 +0100 Subject: [PATCH 11/20] [skip ci] update README.md --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 393bc8c..b9dc001 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,12 @@ [![Flutter version](https://img.shields.io/badge/flutter-v1.22.2-blue?logo=flutter)](https://flutter.dev/docs/development/tools/sdk/releases) [![style: pedantic](https://img.shields.io/badge/style-pedantic-00b4ab.svg)](https://pub.dev/packages/pedantic) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FDarklod%2Fjap-vocabs.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FDarklod%2Fjap-vocabs?ref=badge_shield) + +### To Do +- [ ] Notifications +- [ ] Filter, Sorting +- [ ] Statistics Page +- [ ] Remove, Update examples +- [ ] Automatic Backups +- [ ] Language localization +- [ ] Redesign Add/Edit Page From 9319e522ceed4e2a8ee5b31aa4d648ec798416be Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Thu, 29 Oct 2020 01:52:43 +0100 Subject: [PATCH 12/20] [skip ci] add sorting status --- lib/redux/actions/order_actions.dart | 9 ++++++ lib/redux/reducers/app_reducers.dart | 2 ++ lib/redux/reducers/order_reducers.dart | 16 ++++++++++ lib/redux/state/app_state.dart | 10 +++++- lib/redux/state/order_state.dart | 43 ++++++++++++++++++++++++++ lib/redux/thunk/order.dart | 23 ++++++++++++++ 6 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 lib/redux/actions/order_actions.dart create mode 100644 lib/redux/reducers/order_reducers.dart create mode 100644 lib/redux/state/order_state.dart create mode 100644 lib/redux/thunk/order.dart diff --git a/lib/redux/actions/order_actions.dart b/lib/redux/actions/order_actions.dart new file mode 100644 index 0000000..44d15fa --- /dev/null +++ b/lib/redux/actions/order_actions.dart @@ -0,0 +1,9 @@ +class ChangeSortFieldAction { + final String field; + const ChangeSortFieldAction(this.field); +} + +class ChangeSortModeAction { + final String mode; + const ChangeSortModeAction(this.mode); +} diff --git a/lib/redux/reducers/app_reducers.dart b/lib/redux/reducers/app_reducers.dart index a94b15f..40ba000 100644 --- a/lib/redux/reducers/app_reducers.dart +++ b/lib/redux/reducers/app_reducers.dart @@ -1,5 +1,6 @@ import 'package:jap_vocab/redux/reducers/filter_reducers.dart'; import 'package:jap_vocab/redux/reducers/item_reducers.dart'; +import 'package:jap_vocab/redux/reducers/order_reducers.dart'; import 'package:jap_vocab/redux/reducers/reviews_reducers.dart'; import 'package:jap_vocab/redux/reducers/settings_reducers.dart'; import 'package:jap_vocab/redux/state/app_state.dart'; @@ -7,6 +8,7 @@ import 'package:jap_vocab/redux/state/app_state.dart'; AppState appStateReducer(AppState state, action) { return AppState( filterState: filterReducer(state.filterState, action), + orderState: orderReducer(state.orderState, action), itemsState: itemsReducer(state.itemsState, action), reviewsState: reviewsReducer(state.reviewsState, action), settingsState: settingsReducer(state.settingsState, action), diff --git a/lib/redux/reducers/order_reducers.dart b/lib/redux/reducers/order_reducers.dart new file mode 100644 index 0000000..1f7da14 --- /dev/null +++ b/lib/redux/reducers/order_reducers.dart @@ -0,0 +1,16 @@ +import 'package:jap_vocab/redux/actions/order_actions.dart'; +import 'package:jap_vocab/redux/state/order_state.dart'; +import 'package:redux/redux.dart'; + +Reducer orderReducer = combineReducers([ + TypedReducer(_changeField), + TypedReducer(_changeMode), +]); + +OrderState _changeField(OrderState state, ChangeSortFieldAction action) { + return state.copyWith(field: action.field); +} + +OrderState _changeMode(OrderState state, ChangeSortModeAction action) { + return state.copyWith(mode: action.mode); +} diff --git a/lib/redux/state/app_state.dart b/lib/redux/state/app_state.dart index d17954e..1ad10d7 100644 --- a/lib/redux/state/app_state.dart +++ b/lib/redux/state/app_state.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:jap_vocab/redux/state/filter_state.dart'; import 'package:jap_vocab/redux/state/items_state.dart'; +import 'package:jap_vocab/redux/state/order_state.dart'; import 'package:jap_vocab/redux/state/reviews_state.dart'; import 'package:jap_vocab/redux/state/settings_state.dart'; @@ -9,30 +10,35 @@ class AppState { final ItemsState itemsState; final ReviewsState reviewsState; final FilterState filterState; + final OrderState orderState; final SettingsState settingsState; const AppState({ @required this.itemsState, @required this.reviewsState, @required this.filterState, + @required this.orderState, @required this.settingsState, }); - AppState.initialState({SettingsState settings}) + AppState.initialState({SettingsState settings, OrderState order}) : itemsState = ItemsState.initial(), reviewsState = ReviewsState.initial(), filterState = FilterState.initial(), + orderState = order ?? OrderState.initial(), settingsState = settings ?? SettingsState.initial(); static AppState fromJson(dynamic json) { return AppState.initialState( settings: SettingsState.fromJson(json['settingsState']), + order: OrderState.fromJson(json['orderState']), ); } dynamic toJson() { return { 'settingsState': settingsState.toJson(), + 'orderState': orderState.toJson(), }; } @@ -40,12 +46,14 @@ class AppState { ItemsState itemsState, ReviewsState reviewsState, FilterState filterState, + OrderState orderState, SettingsState settingsState, }) { return AppState( itemsState: itemsState ?? this.itemsState, reviewsState: reviewsState ?? this.reviewsState, filterState: filterState ?? this.filterState, + orderState: orderState ?? this.orderState, settingsState: settingsState ?? this.settingsState, ); } diff --git a/lib/redux/state/order_state.dart b/lib/redux/state/order_state.dart new file mode 100644 index 0000000..6269a09 --- /dev/null +++ b/lib/redux/state/order_state.dart @@ -0,0 +1,43 @@ +import 'package:flutter/foundation.dart'; + +@immutable +class OrderState { + final String field; + final String mode; + const OrderState({this.field, this.mode}); + + OrderState.initial() + : field = 'Next Review', + mode = 'DESC'; + + OrderState copyWith({field, mode}) { + return OrderState( + field: field ?? this.field, + mode: mode ?? this.mode, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is OrderState && field == other.field && mode == other.mode; + + @override + int get hashCode => field.hashCode ^ mode.hashCode; + + static OrderState fromJson(dynamic json) { + if (json == null) return null; + + return OrderState( + field: json['field'] ?? 'Next Review', + mode: json['mode'] ?? 'DESC', + ); + } + + dynamic toJson() { + return { + 'field': field, + 'mode': mode, + }; + } +} diff --git a/lib/redux/thunk/order.dart b/lib/redux/thunk/order.dart new file mode 100644 index 0000000..bab9e57 --- /dev/null +++ b/lib/redux/thunk/order.dart @@ -0,0 +1,23 @@ +import 'package:jap_vocab/redux/actions/order_actions.dart'; +import 'package:jap_vocab/redux/state/app_state.dart'; +import 'package:jap_vocab/redux/thunk/items.dart'; +import 'package:redux/redux.dart'; +import 'package:redux_thunk/redux_thunk.dart'; + +ThunkAction changeSortField(String field) { + return (Store store) async { + if (field != null && field.isNotEmpty) { + await store.dispatch(ChangeSortFieldAction(field)); + await store.dispatch(getItems()); + } + }; +} + +ThunkAction changeSortMode(String mode) { + return (Store store) async { + if (mode != null && mode.isNotEmpty) { + await store.dispatch(ChangeSortModeAction(mode)); + await store.dispatch(getItems()); + } + }; +} From 8323ae32c29584b64c54028faeac80321ef17d99 Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Thu, 29 Oct 2020 11:15:11 +0100 Subject: [PATCH 13/20] new feature: sorting --- .../components/md2_indicator.dart | 0 lib/database/item_dao.dart | 13 +- lib/models/item.dart | 44 +++ .../details/components/details_appbar.dart | 2 +- lib/pages/home/components/filter_sheet.dart | 283 +++++++++++------- lib/pages/home/components/list_item.dart | 19 +- lib/pages/home/home.dart | 9 +- lib/pages/reviews/reviews.dart | 2 - lib/redux/thunk/items.dart | 11 +- 9 files changed, 248 insertions(+), 135 deletions(-) rename lib/{pages/details => }/components/md2_indicator.dart (100%) diff --git a/lib/pages/details/components/md2_indicator.dart b/lib/components/md2_indicator.dart similarity index 100% rename from lib/pages/details/components/md2_indicator.dart rename to lib/components/md2_indicator.dart diff --git a/lib/database/item_dao.dart b/lib/database/item_dao.dart index baf7e8e..322b7e0 100644 --- a/lib/database/item_dao.dart +++ b/lib/database/item_dao.dart @@ -34,7 +34,11 @@ class ItemDao { await _store.delete(await _db, finder: finder); } - Future> getAllItems({@required String type, String search}) async { + Future> getAllItems( + {@required String type, + String search, + String sortField, + String sortMode}) async { var finder = Finder(filter: Filter.equals('type', type)); if (search != null && search.isNotEmpty) { @@ -56,13 +60,16 @@ class ItemDao { final recordSnapshot = await _store.find(await _db, finder: finder); if (recordSnapshot != null) { - return Future.wait(recordSnapshot.map((snapshot) async { + final list = recordSnapshot.map((snapshot) async { final item = Item.fromMap(snapshot.value); item.review1 = await ReviewDao().getReviewById(item.reviewId1); item.review2 = await ReviewDao().getReviewById(item.reviewId2); return item; - }).toList()); + }).toList(); + + final sorted = await Future.wait(list); + return sorted..sort(Item.comparator(sortField, sortMode)); } return null; } diff --git a/lib/models/item.dart b/lib/models/item.dart index b039e4f..81813c1 100644 --- a/lib/models/item.dart +++ b/lib/models/item.dart @@ -121,6 +121,34 @@ class Item { ); } + // Compute total accuracy + double get accuracy { + if (review1 == null && review2 == null) return 0.0; + if (review1 != null && review2 != null) { + return review1.accuracy * review2.accuracy; + } + + if (review1 != null) { + return review1.accuracy; + } else { + return review2.accuracy; + } + } + + // Compute mean streak + double get streak { + if (review1 == null && review2 == null) return 0.0; + if (review1 != null && review2 != null) { + return review1.streak * review2.streak * 0.5; + } + + if (review1 != null) { + return review1.streak.toDouble(); + } else { + return review2.streak.toDouble(); + } + } + DateTime get nextReview { if (review1?.next == null && review2?.next == null) return null; @@ -145,4 +173,20 @@ class Item { @override int get hashCode => id.hashCode ^ text.hashCode ^ type.hashCode; + + static Comparator comparator(String field, String mode) { + final mult = mode == 'ASC' ? -1 : 1; + + switch(field) { + case 'Alphabetical': + return (a, b) => a.text.compareTo(b.text) * mult; + case 'Streak': + return (a, b) => a.streak.compareTo(b.streak) * mult; + case 'Accuracy': + return (a, b) => a.accuracy.compareTo(b.accuracy) * mult; + case 'Next Review': + default: + return (a, b) => a.nextReview.compareTo(b.nextReview) * mult; + } + } } diff --git a/lib/pages/details/components/details_appbar.dart b/lib/pages/details/components/details_appbar.dart index a137afe..93a3e96 100644 --- a/lib/pages/details/components/details_appbar.dart +++ b/lib/pages/details/components/details_appbar.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:jap_vocab/models/item.dart'; import 'package:jap_vocab/pages/details/components/confirm_dialog.dart'; -import 'package:jap_vocab/pages/details/components/md2_indicator.dart'; +import 'package:jap_vocab/components/md2_indicator.dart'; import 'package:jap_vocab/redux/state/app_state.dart'; import 'package:jap_vocab/redux/thunk/items.dart'; import 'package:redux/redux.dart'; diff --git a/lib/pages/home/components/filter_sheet.dart b/lib/pages/home/components/filter_sheet.dart index a48d2a1..19f4cbb 100644 --- a/lib/pages/home/components/filter_sheet.dart +++ b/lib/pages/home/components/filter_sheet.dart @@ -1,23 +1,61 @@ import 'package:chips_choice/chips_choice.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:jap_vocab/components/md2_indicator.dart'; +import 'package:jap_vocab/redux/state/app_state.dart'; +import 'package:jap_vocab/redux/thunk/order.dart'; +import 'package:redux/redux.dart'; class FilterSheet extends StatelessWidget { FilterSheet({Key key}) : super(key: key); @override Widget build(BuildContext context) { - return FractionallySizedBox( - heightFactor: 0.8, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SortSection(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Divider(), - ), - FilterSection(), - ], + return DefaultTabController( + initialIndex: 0, + length: 2, + child: Container( + height: MediaQuery.of(context).size.height * 0.4, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16.0), + topRight: Radius.circular(16.0), + ), + ), + padding: const EdgeInsets.only(top: 4), + child: TabBar( + tabs: [ + Tab(text: '並べ替え'), + Tab(text: 'フィルター'), + ], + unselectedLabelColor: Colors.white30, + labelColor: Colors.white, + indicatorSize: TabBarIndicatorSize.label, + indicator: MD2Indicator( + indicatorHeight: 4.0, + indicatorColor: Colors.white, + horizontalPadding: 16.0, + ), + ), + ), + Expanded( + child: Container( + color: Theme.of(context).primaryColorLight, + child: TabBarView( + children: [ + SortSection(), + FilterSection(), + ], + ), + ), + ) + ], + ), ), ); } @@ -37,68 +75,71 @@ class _FilterSectionState extends State { @override Widget build(BuildContext context) { - final style = Theme.of(context).textTheme.subtitle1.copyWith( + /*final style = Theme.of(context).textTheme.subtitle1.copyWith( // color: Theme.of(context).accentColor, fontWeight: FontWeight.bold, - ); + );*/ - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.all(16.0), - child: Text('フィルター', style: style), - ), - ChipsChoice.multiple( - value: _partOfSpeech, - options: [ - ChipsChoiceOption(value: '動詞', label: '動詞'), - ChipsChoiceOption(value: '名詞', label: '名詞'), - ChipsChoiceOption(value: '形容詞', label: '形容詞'), - ChipsChoiceOption(value: '名形容動詞', label: '名形容動詞'), - ], - onChanged: (value) => setState(() => _partOfSpeech = value), - itemConfig: ChipsChoiceItemConfig( - showCheckmark: false, - unselectedBrightness: Brightness.dark, - selectedBrightness: Brightness.dark, + return Container( + height: 700, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + /*Container( + padding: const EdgeInsets.all(16.0), + child: Text('フィルター', style: style), + ),*/ + ChipsChoice.multiple( + value: _partOfSpeech, + options: [ + ChipsChoiceOption(value: '動詞', label: '動詞'), + ChipsChoiceOption(value: '名詞', label: '名詞'), + ChipsChoiceOption(value: '形容詞', label: '形容詞'), + ChipsChoiceOption(value: '名形容動詞', label: '名形容動詞'), + ], + onChanged: (value) => setState(() => _partOfSpeech = value), + itemConfig: ChipsChoiceItemConfig( + showCheckmark: false, + unselectedBrightness: Brightness.dark, + selectedBrightness: Brightness.dark, + ), + isWrapped: true, ), - isWrapped: true, - ), - ChipsChoice.multiple( - value: _jlpt, - options: [ - ChipsChoiceOption(value: 5, label: 'N5'), - ChipsChoiceOption(value: 4, label: 'N4'), - ChipsChoiceOption(value: 3, label: 'N3'), - ChipsChoiceOption(value: 2, label: 'N2'), - ChipsChoiceOption(value: 1, label: 'N1'), - ], - onChanged: (value) => setState(() => _jlpt = value), - itemConfig: ChipsChoiceItemConfig( - showCheckmark: false, - unselectedBrightness: Brightness.dark, - selectedBrightness: Brightness.dark, + ChipsChoice.multiple( + value: _jlpt, + options: [ + ChipsChoiceOption(value: 5, label: 'N5'), + ChipsChoiceOption(value: 4, label: 'N4'), + ChipsChoiceOption(value: 3, label: 'N3'), + ChipsChoiceOption(value: 2, label: 'N2'), + ChipsChoiceOption(value: 1, label: 'N1'), + ], + onChanged: (value) => setState(() => _jlpt = value), + itemConfig: ChipsChoiceItemConfig( + showCheckmark: false, + unselectedBrightness: Brightness.dark, + selectedBrightness: Brightness.dark, + ), + isWrapped: true, ), - isWrapped: true, - ), - ChipsChoice.multiple( - value: _level, - options: [ - ChipsChoiceOption(value: 'weak', label: 'Weak'), - ChipsChoiceOption(value: 'medium', label: 'Medium'), - ChipsChoiceOption(value: 'strong', label: 'Strong'), - ], - onChanged: (value) => setState(() => _level = value), - itemConfig: ChipsChoiceItemConfig( - showCheckmark: false, - unselectedBrightness: Brightness.dark, - selectedBrightness: Brightness.dark, + ChipsChoice.multiple( + value: _level, + options: [ + ChipsChoiceOption(value: 'weak', label: 'Weak'), + ChipsChoiceOption(value: 'medium', label: 'Medium'), + ChipsChoiceOption(value: 'strong', label: 'Strong'), + ], + onChanged: (value) => setState(() => _level = value), + itemConfig: ChipsChoiceItemConfig( + showCheckmark: false, + unselectedBrightness: Brightness.dark, + selectedBrightness: Brightness.dark, + ), + isWrapped: true, ), - isWrapped: true, - ), - ], + ], + ), ); } } @@ -109,48 +150,84 @@ class SortSection extends StatelessWidget { final sorts = [ 'Alphabetical', 'Next Review', - 'Last Review', 'Streak', - 'Accuracy' + 'Accuracy', ]; @override Widget build(BuildContext context) { - final style = Theme.of(context).textTheme.subtitle1.copyWith( - // color: Theme.of(context).accentColor, - fontWeight: FontWeight.bold, + return StoreConnector( + converter: (Store state) => _ViewModel.create(state), + builder: (context, _ViewModel vm) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + /*Container( + padding: const EdgeInsets.all(16.0), + child: Text('並べ替え', style: style), + ),*/ + ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: NeverScrollableScrollPhysics(), + itemCount: sorts.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(sorts[index], + style: sorts[index] == vm.field + ? TextStyle( + fontWeight: FontWeight.bold, + color: Colors.orangeAccent, + ) + : null), + selected: sorts[index] == vm.field, + // selectedTileColor: Colors.red, + trailing: sorts[index] == vm.field + ? Icon(vm.mode == 'ASC' ? Icons.arrow_upward : Icons.arrow_downward, color: Colors.orangeAccent) + : null, + onTap: () { + if (vm.field != sorts[index]) { + vm.setField(sorts[index]); + } else { + vm.setMode(vm.mode == 'ASC' ? 'DESC' : 'ASC'); + } + }, + ); + }, + ), + ], ); + }, + ); + } +} - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.all(16.0), - child: Text('並べ替え', style: style), - ), - ListView.builder( - shrinkWrap: true, - padding: EdgeInsets.zero, - physics: NeverScrollableScrollPhysics(), - itemCount: sorts.length, - itemBuilder: (context, index) { - return ListTile( - title: Text( - sorts[index], - style: - TextStyle(color: index == 2 ? Colors.orangeAccent : null), - ), - selected: index == 2, - // selectedTileColor: Colors.red, - trailing: index == 2 - ? Icon(Icons.arrow_upward, color: Colors.orangeAccent) - : null, - onTap: () {}, - ); - }, - ), - ], +class _ViewModel { + final String field; + final String mode; + final Function(String) setField; + final Function(String) setMode; + + _ViewModel({this.field, this.mode, this.setField, this.setMode}); + + factory _ViewModel.create(Store store) { + final _field = store.state.orderState.field; + final _mode = store.state.orderState.mode; + + void _setField(String field) { + store.dispatch(changeSortField(field)); + } + + void _setMode(String mode) { + store.dispatch(changeSortMode(mode)); + } + + return _ViewModel( + field: _field, + mode: _mode, + setField: _setField, + setMode: _setMode, ); } } diff --git a/lib/pages/home/components/list_item.dart b/lib/pages/home/components/list_item.dart index e97182e..2eaabf6 100644 --- a/lib/pages/home/components/list_item.dart +++ b/lib/pages/home/components/list_item.dart @@ -14,19 +14,6 @@ class ListItem extends StatelessWidget { const ListItem({this.item, this.onTap}); - double get _percent { - if (item.review1 == null && item.review2 == null) return 0.0; - if (item.review1 != null && item.review2 != null) { - return item.review1.accuracy * item.review2.accuracy; - } - - if (item.review1 != null) { - return item.review1.accuracy; - } else { - return item.review2.accuracy; - } - } - String get _date => Date.format(item.nextReview) ?? 'New'; String get _example { @@ -87,10 +74,10 @@ class ListItem extends StatelessWidget { CircularPercentIndicator( radius: 40.0, lineWidth: 4.0, - percent: _percent, + percent: item.accuracy, circularStrokeCap: CircularStrokeCap.round, - center: Text('${(_percent * 100).toInt()}'), - progressColor: colorPercent(_percent), + center: Text('${(item.accuracy * 100).toInt()}'), + progressColor: colorPercent(item.accuracy), ), ], ), diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index 63adbfb..b922c6b 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -55,20 +55,13 @@ class _ViewModel { factory _ViewModel.create(Store store) { final _items = store.state.itemsState.items; - final _sorted = List.from(_items) - ..sort((a, b) { - final dateA = a.nextReview ?? DateTime.now(); - final dateB = b.nextReview ?? DateTime.now(); - - return dateA.compareTo(dateB); - }); void _getItems() { store.dispatch(getItems()); } return _ViewModel( - items: _sorted, + items: _items, getAllItems: _getItems, ); } diff --git a/lib/pages/reviews/reviews.dart b/lib/pages/reviews/reviews.dart index d0c0ce4..35d5d77 100644 --- a/lib/pages/reviews/reviews.dart +++ b/lib/pages/reviews/reviews.dart @@ -54,8 +54,6 @@ class _ReviewPageState extends State { bool get _enable => _quality != -1; - // TODO: tasto ? per info sulla qualità - void _showHelp() { showDialog( context: context, diff --git a/lib/redux/thunk/items.dart b/lib/redux/thunk/items.dart index 89b9867..3028e19 100644 --- a/lib/redux/thunk/items.dart +++ b/lib/redux/thunk/items.dart @@ -16,8 +16,15 @@ ThunkAction getItems() { var type = store.state.filterState.type; var search = store.state.filterState.search; - - final items = await ItemDao().getAllItems(type: type, search: search); + var sortField = store.state.orderState.field; + var sortMode = store.state.orderState.mode; + + final items = await ItemDao().getAllItems( + type: type, + search: search, + sortField: sortField, + sortMode: sortMode, + ); await store.dispatch(LoadedItemsAction(items)); }; } From b8dc0a01e8bd2056eae106be7d19b16625387ff1 Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Thu, 29 Oct 2020 11:17:56 +0100 Subject: [PATCH 14/20] [skip ci] update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b9dc001..443052d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ ### To Do - [ ] Notifications -- [ ] Filter, Sorting +- [x] Sorting +- [ ] Filter - [ ] Statistics Page - [ ] Remove, Update examples - [ ] Automatic Backups From 24720a48372f51fd8b667c16d89c5fe403e3208f Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Thu, 29 Oct 2020 14:41:57 +0100 Subject: [PATCH 15/20] [skip ci] edit filter state --- lib/redux/actions/filter_actions.dart | 15 ++++++++++ lib/redux/reducers/filter_reducers.dart | 18 ++++++++++++ lib/redux/state/filter_state.dart | 37 +++++++++++++++++++++---- lib/redux/thunk/filter.dart | 24 ++++++++++++++++ 4 files changed, 89 insertions(+), 5 deletions(-) diff --git a/lib/redux/actions/filter_actions.dart b/lib/redux/actions/filter_actions.dart index b103e81..2df3f37 100644 --- a/lib/redux/actions/filter_actions.dart +++ b/lib/redux/actions/filter_actions.dart @@ -7,3 +7,18 @@ class ChangeTypeAction { final String type; const ChangeTypeAction(this.type); } + +class ChangeJLTPAction { + final List jlpt; + const ChangeJLTPAction(this.jlpt); +} + +class ChangeLevelAction { + final List level; + const ChangeLevelAction(this.level); +} + +class ChangePartOfSpeechAction { + final List partOfSpeech; + const ChangePartOfSpeechAction(this.partOfSpeech); +} diff --git a/lib/redux/reducers/filter_reducers.dart b/lib/redux/reducers/filter_reducers.dart index 9faf94c..91da2ef 100644 --- a/lib/redux/reducers/filter_reducers.dart +++ b/lib/redux/reducers/filter_reducers.dart @@ -5,6 +5,9 @@ import 'package:redux/redux.dart'; Reducer filterReducer = combineReducers([ TypedReducer(_changeSearch), TypedReducer(_changeType), + TypedReducer(_changeJLPT), + TypedReducer(_changeLevel), + TypedReducer(_changePartOfSpeech), ]); FilterState _changeSearch(FilterState state, ChangeSearchAction action) { @@ -14,3 +17,18 @@ FilterState _changeSearch(FilterState state, ChangeSearchAction action) { FilterState _changeType(FilterState state, ChangeTypeAction action) { return state.copyWith(type: action.type); } + +FilterState _changeJLPT(FilterState state, ChangeJLTPAction action) { + return state.copyWith(type: action.jlpt); +} + +FilterState _changeLevel(FilterState state, ChangeLevelAction action) { + return state.copyWith(type: action.level); +} + +FilterState _changePartOfSpeech( + FilterState state, + ChangePartOfSpeechAction action, +) { + return state.copyWith(type: action.partOfSpeech); +} diff --git a/lib/redux/state/filter_state.dart b/lib/redux/state/filter_state.dart index e025f75..c0457f7 100644 --- a/lib/redux/state/filter_state.dart +++ b/lib/redux/state/filter_state.dart @@ -4,24 +4,51 @@ import 'package:flutter/foundation.dart'; class FilterState { final String type; final String search; - const FilterState({@required this.type, this.search}); + + final List jlpt; + final List level; + final List partOfSpeech; + + const FilterState({ + @required this.type, + this.search, + this.jlpt, + this.level, + this.partOfSpeech, + }); FilterState.initial() : type = 'word', - search = null; + search = null, + jlpt = null, + level = null, + partOfSpeech = null; - FilterState copyWith({type, search}) { + FilterState copyWith({type, search, jlpt, level, partOfSpeech}) { return FilterState( type: type ?? this.type, search: search ?? this.search, + jlpt: jlpt ?? this.jlpt, + level: level ?? this.level, + partOfSpeech: partOfSpeech ?? this.level, ); } @override bool operator ==(Object other) => identical(this, other) || - other is FilterState && type == other.type && search == other.search; + other is FilterState && + type == other.type && + search == other.search && + jlpt == other.jlpt && + level == other.level && + partOfSpeech == other.partOfSpeech; @override - int get hashCode => type.hashCode ^ search.hashCode; + int get hashCode => + type.hashCode ^ + search.hashCode ^ + jlpt.hashCode ^ + level.hashCode ^ + partOfSpeech.hashCode; } diff --git a/lib/redux/thunk/filter.dart b/lib/redux/thunk/filter.dart index 38f418a..54edf69 100644 --- a/lib/redux/thunk/filter.dart +++ b/lib/redux/thunk/filter.dart @@ -12,3 +12,27 @@ ThunkAction changeType(String type) { await store.dispatch(getReviews()); }; } + +ThunkAction changeJLPT(List jlpt) { + return (Store store) async { + await store.dispatch(ChangeJLTPAction(jlpt)); + await store.dispatch(getItems()); + await store.dispatch(getReviews()); + }; +} + +ThunkAction changeLevel(List level) { + return (Store store) async { + await store.dispatch(ChangeLevelAction(level)); + await store.dispatch(getItems()); + await store.dispatch(getReviews()); + }; +} + +ThunkAction changePartOfSpeech(List partOfSpeech) { + return (Store store) async { + await store.dispatch(ChangePartOfSpeechAction(partOfSpeech)); + await store.dispatch(getItems()); + await store.dispatch(getReviews()); + }; +} From dc39909f759211188c0aedb1c393c1d46c624263 Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Thu, 29 Oct 2020 15:09:16 +0100 Subject: [PATCH 16/20] [skip ci] fix filter state --- lib/redux/actions/filter_actions.dart | 2 +- lib/redux/reducers/filter_reducers.dart | 6 +++--- lib/redux/state/filter_state.dart | 10 ++++++++-- lib/redux/thunk/filter.dart | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/redux/actions/filter_actions.dart b/lib/redux/actions/filter_actions.dart index 2df3f37..1b74b12 100644 --- a/lib/redux/actions/filter_actions.dart +++ b/lib/redux/actions/filter_actions.dart @@ -14,7 +14,7 @@ class ChangeJLTPAction { } class ChangeLevelAction { - final List level; + final List level; const ChangeLevelAction(this.level); } diff --git a/lib/redux/reducers/filter_reducers.dart b/lib/redux/reducers/filter_reducers.dart index 91da2ef..2220eac 100644 --- a/lib/redux/reducers/filter_reducers.dart +++ b/lib/redux/reducers/filter_reducers.dart @@ -19,16 +19,16 @@ FilterState _changeType(FilterState state, ChangeTypeAction action) { } FilterState _changeJLPT(FilterState state, ChangeJLTPAction action) { - return state.copyWith(type: action.jlpt); + return state.copyWith(jlpt: action.jlpt); } FilterState _changeLevel(FilterState state, ChangeLevelAction action) { - return state.copyWith(type: action.level); + return state.copyWith(level: action.level); } FilterState _changePartOfSpeech( FilterState state, ChangePartOfSpeechAction action, ) { - return state.copyWith(type: action.partOfSpeech); + return state.copyWith(partOfSpeech: action.partOfSpeech); } diff --git a/lib/redux/state/filter_state.dart b/lib/redux/state/filter_state.dart index c0457f7..f20e6dd 100644 --- a/lib/redux/state/filter_state.dart +++ b/lib/redux/state/filter_state.dart @@ -6,7 +6,7 @@ class FilterState { final String search; final List jlpt; - final List level; + final List level; final List partOfSpeech; const FilterState({ @@ -24,7 +24,13 @@ class FilterState { level = null, partOfSpeech = null; - FilterState copyWith({type, search, jlpt, level, partOfSpeech}) { + FilterState copyWith({ + String type, + String search, + List jlpt, + List level, + List partOfSpeech, + }) { return FilterState( type: type ?? this.type, search: search ?? this.search, diff --git a/lib/redux/thunk/filter.dart b/lib/redux/thunk/filter.dart index 54edf69..3e5fd93 100644 --- a/lib/redux/thunk/filter.dart +++ b/lib/redux/thunk/filter.dart @@ -21,7 +21,7 @@ ThunkAction changeJLPT(List jlpt) { }; } -ThunkAction changeLevel(List level) { +ThunkAction changeLevel(List level) { return (Store store) async { await store.dispatch(ChangeLevelAction(level)); await store.dispatch(getItems()); From ed4c4f2faa1f8a24deb6840acd95669484d492ce Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Thu, 29 Oct 2020 15:37:41 +0100 Subject: [PATCH 17/20] [skip ci] fix filter state --- lib/redux/state/filter_state.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/redux/state/filter_state.dart b/lib/redux/state/filter_state.dart index f20e6dd..fdbc79b 100644 --- a/lib/redux/state/filter_state.dart +++ b/lib/redux/state/filter_state.dart @@ -36,7 +36,7 @@ class FilterState { search: search ?? this.search, jlpt: jlpt ?? this.jlpt, level: level ?? this.level, - partOfSpeech: partOfSpeech ?? this.level, + partOfSpeech: partOfSpeech ?? this.partOfSpeech, ); } From b9e07c40842cdf115aca8e92a3e5d71a6b4913f2 Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Sat, 31 Oct 2020 01:13:00 +0100 Subject: [PATCH 18/20] [skip ci] fixed item class --- lib/models/item.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/models/item.dart b/lib/models/item.dart index 81813c1..a9f3b1e 100644 --- a/lib/models/item.dart +++ b/lib/models/item.dart @@ -77,7 +77,7 @@ class Item { id: map['id'], text: map['text'], type: map['type'], - favorite: map['favorite'], + favorite: map['favorite'] ?? false, reading: map['reading'], meaning: map['meaning'], examples: map['examples'] == null @@ -139,7 +139,7 @@ class Item { double get streak { if (review1 == null && review2 == null) return 0.0; if (review1 != null && review2 != null) { - return review1.streak * review2.streak * 0.5; + return (review1.streak + review2.streak) * 0.5; } if (review1 != null) { @@ -177,7 +177,7 @@ class Item { static Comparator comparator(String field, String mode) { final mult = mode == 'ASC' ? -1 : 1; - switch(field) { + switch (field) { case 'Alphabetical': return (a, b) => a.text.compareTo(b.text) * mult; case 'Streak': @@ -186,7 +186,11 @@ class Item { return (a, b) => a.accuracy.compareTo(b.accuracy) * mult; case 'Next Review': default: - return (a, b) => a.nextReview.compareTo(b.nextReview) * mult; + return (a, b) { + final dateA = a.nextReview ?? DateTime.now(); + final dateB = b.nextReview ?? DateTime.now(); + return dateA.compareTo(dateB) * mult; + }; } - } + } } From 59a3d391fb15bdc8a7b551759a807bc9d561987b Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Sat, 31 Oct 2020 01:14:01 +0100 Subject: [PATCH 19/20] [skip ci] formatting & refactoring --- lib/pages/home/components/home_appbar.dart | 2 +- lib/pages/home/components/seachbar.dart | 12 +++++------- lib/pages/home/home.dart | 5 +---- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/lib/pages/home/components/home_appbar.dart b/lib/pages/home/components/home_appbar.dart index c48f019..e7bb0cb 100644 --- a/lib/pages/home/components/home_appbar.dart +++ b/lib/pages/home/components/home_appbar.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:jap_vocab/pages/home/components/filter_sheet.dart'; +import 'package:jap_vocab/pages/home/components/filter_sheet/filter_sheet.dart'; import 'package:jap_vocab/pages/home/components/reivew_type_switch.dart'; import 'package:jap_vocab/pages/home/components/seachbar.dart'; diff --git a/lib/pages/home/components/seachbar.dart b/lib/pages/home/components/seachbar.dart index 82ee424..f514bc6 100644 --- a/lib/pages/home/components/seachbar.dart +++ b/lib/pages/home/components/seachbar.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; -import 'package:jap_vocab/redux/actions/filter_actions.dart'; import 'package:jap_vocab/redux/state/app_state.dart'; -import 'package:jap_vocab/redux/thunk/items.dart'; +import 'package:jap_vocab/redux/thunk/filter.dart'; import 'package:redux/redux.dart'; class SearchBar extends StatefulWidget with PreferredSizeWidget { @@ -90,13 +89,12 @@ class _ViewModel { _ViewModel({this.setSearch, this.resetSearch}); factory _ViewModel.create(Store store) { - void _setSearch(String value) async { - await store.dispatch(ChangeSearchAction(value)); - await store.dispatch(getItems()); + void _setSearch(String value) { + store.dispatch(changeSearch(value)); } - void _resetSearch() async { - await _setSearch(''); + void _resetSearch() { + _setSearch(''); } return _ViewModel( diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index b922c6b..24ac9b7 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -16,11 +16,8 @@ class HomePage extends StatelessWidget { appBar: HomeAppBar(), body: StoreConnector( converter: (Store store) => _ViewModel.create(store), - onInit: (Store store) => - store.dispatch(getItems()), // TODO: CHECK + onInit: (Store store) => store.dispatch(getItems()), builder: (context, _ViewModel vm) { - print('HomePage build) ${vm.items.length}'); - return ListView.builder( padding: const EdgeInsets.only(bottom: 80), itemCount: vm.items?.length ?? 0, From 5d87a2cac73816c2ac32b19cbbc2a6acc94fd492 Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Sat, 31 Oct 2020 01:14:57 +0100 Subject: [PATCH 20/20] new feature: filter --- README.md | 27 +- lib/database/item_dao.dart | 69 ++++-- lib/pages/home/components/filter_sheet.dart | 233 ------------------ .../components/filter_section.dart | 136 ++++++++++ .../filter_sheet/components/sort_section.dart | 97 ++++++++ .../components/filter_sheet/filter_sheet.dart | 59 +++++ lib/redux/thunk/filter.dart | 10 +- lib/redux/thunk/items.dart | 15 +- 8 files changed, 377 insertions(+), 269 deletions(-) delete mode 100644 lib/pages/home/components/filter_sheet.dart create mode 100644 lib/pages/home/components/filter_sheet/components/filter_section.dart create mode 100644 lib/pages/home/components/filter_sheet/components/sort_section.dart create mode 100644 lib/pages/home/components/filter_sheet/filter_sheet.dart diff --git a/README.md b/README.md index 443052d..a7b0894 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,34 @@ ### To Do - [ ] Notifications - [x] Sorting -- [ ] Filter +- [x] Filter - [ ] Statistics Page - [ ] Remove, Update examples - [ ] Automatic Backups - [ ] Language localization - [ ] Redesign Add/Edit Page + +### Folder Structure + +``` +lib +├── components # Common widgets shared between pages +├── database # Database configuration and Dao classes +├── models # Models +├── pages # Page widgets +│ ├─ home +│ │ ├── components # Local widgets used only in the home page +│ │ └── home.dart +│ ├─ details +│ └─ ... +├── redux # Redux folders +│ ├─ actions +│ ├─ reducers +│ ├─ state +│ ├─ thunk +│ └─ store.dart +├── utils # Common functions +├── main.dart +└── routes.dart # Contains the routes and imports all pages. +``` + diff --git a/lib/database/item_dao.dart b/lib/database/item_dao.dart index 322b7e0..b52781d 100644 --- a/lib/database/item_dao.dart +++ b/lib/database/item_dao.dart @@ -1,8 +1,9 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:jap_vocab/models/item.dart'; import 'package:jap_vocab/database/app_database.dart'; import 'package:jap_vocab/database/review_dao.dart'; +import 'package:jap_vocab/redux/state/filter_state.dart'; +import 'package:jap_vocab/redux/state/order_state.dart'; import 'package:sembast/sembast.dart'; class ItemDao { @@ -34,33 +35,44 @@ class ItemDao { await _store.delete(await _db, finder: finder); } - Future> getAllItems( - {@required String type, - String search, - String sortField, - String sortMode}) async { - var finder = Finder(filter: Filter.equals('type', type)); + Future> getAllItems({FilterState filter, OrderState order}) async { + var filters = [Filter.equals('type', filter.type)]; - if (search != null && search.isNotEmpty) { - finder = Finder( - filter: Filter.and( + if (filter.search != null && filter.search.isNotEmpty) { + filters.add( + Filter.or( [ - Filter.equals('type', type), - Filter.or( - [ - Filter.matches('text', search), - Filter.matches('reading', search) - ], - ), + Filter.matches('text', filter.search), + Filter.matches('reading', filter.search), ], ), ); } - final recordSnapshot = await _store.find(await _db, finder: finder); + if (filter.jlpt != null && filter.jlpt.isNotEmpty) { + filters.add(Filter.inList('jlpt', filter.jlpt)); + } + + if (filter.type == 'word' && + filter.partOfSpeech != null && + filter.partOfSpeech.isNotEmpty) { + filters.add(Filter.custom((record) { + for (var i = 0; i < filter.partOfSpeech.length; i++) { + if (record.value['part_of_speech'].contains(filter.partOfSpeech[i])) { + return true; + } + } + return false; + })); + } + + final recordSnapshot = await _store.find( + await _db, + finder: Finder(filter: Filter.and(filters)), + ); if (recordSnapshot != null) { - final list = recordSnapshot.map((snapshot) async { + final futureList = recordSnapshot.map((snapshot) async { final item = Item.fromMap(snapshot.value); item.review1 = await ReviewDao().getReviewById(item.reviewId1); item.review2 = await ReviewDao().getReviewById(item.reviewId2); @@ -68,8 +80,23 @@ class ItemDao { return item; }).toList(); - final sorted = await Future.wait(list); - return sorted..sort(Item.comparator(sortField, sortMode)); + var list = await Future.wait(futureList); + + if (list == null || list.isEmpty) return []; + + if (filter.level != null && filter.level.isNotEmpty) { + list = list.where((Item item) { + if (item.streak >= 6.0 && filter.level.contains('strong')) { + return true; + } else if (item.streak >= 4.0 && filter.level.contains('medium')) { + return true; + } else { + return filter.level.contains('weak'); + } + }).toList(); + } + + return list..sort(Item.comparator(order.field, order.mode)); } return null; } diff --git a/lib/pages/home/components/filter_sheet.dart b/lib/pages/home/components/filter_sheet.dart deleted file mode 100644 index 19f4cbb..0000000 --- a/lib/pages/home/components/filter_sheet.dart +++ /dev/null @@ -1,233 +0,0 @@ -import 'package:chips_choice/chips_choice.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import 'package:jap_vocab/components/md2_indicator.dart'; -import 'package:jap_vocab/redux/state/app_state.dart'; -import 'package:jap_vocab/redux/thunk/order.dart'; -import 'package:redux/redux.dart'; - -class FilterSheet extends StatelessWidget { - FilterSheet({Key key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return DefaultTabController( - initialIndex: 0, - length: 2, - child: Container( - height: MediaQuery.of(context).size.height * 0.4, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16.0), - topRight: Radius.circular(16.0), - ), - ), - padding: const EdgeInsets.only(top: 4), - child: TabBar( - tabs: [ - Tab(text: '並べ替え'), - Tab(text: 'フィルター'), - ], - unselectedLabelColor: Colors.white30, - labelColor: Colors.white, - indicatorSize: TabBarIndicatorSize.label, - indicator: MD2Indicator( - indicatorHeight: 4.0, - indicatorColor: Colors.white, - horizontalPadding: 16.0, - ), - ), - ), - Expanded( - child: Container( - color: Theme.of(context).primaryColorLight, - child: TabBarView( - children: [ - SortSection(), - FilterSection(), - ], - ), - ), - ) - ], - ), - ), - ); - } -} - -class FilterSection extends StatefulWidget { - FilterSection({Key key}) : super(key: key); - - @override - _FilterSectionState createState() => _FilterSectionState(); -} - -class _FilterSectionState extends State { - var _partOfSpeech = []; - var _jlpt = []; - var _level = []; - - @override - Widget build(BuildContext context) { - /*final style = Theme.of(context).textTheme.subtitle1.copyWith( - // color: Theme.of(context).accentColor, - fontWeight: FontWeight.bold, - );*/ - - return Container( - height: 700, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - /*Container( - padding: const EdgeInsets.all(16.0), - child: Text('フィルター', style: style), - ),*/ - ChipsChoice.multiple( - value: _partOfSpeech, - options: [ - ChipsChoiceOption(value: '動詞', label: '動詞'), - ChipsChoiceOption(value: '名詞', label: '名詞'), - ChipsChoiceOption(value: '形容詞', label: '形容詞'), - ChipsChoiceOption(value: '名形容動詞', label: '名形容動詞'), - ], - onChanged: (value) => setState(() => _partOfSpeech = value), - itemConfig: ChipsChoiceItemConfig( - showCheckmark: false, - unselectedBrightness: Brightness.dark, - selectedBrightness: Brightness.dark, - ), - isWrapped: true, - ), - ChipsChoice.multiple( - value: _jlpt, - options: [ - ChipsChoiceOption(value: 5, label: 'N5'), - ChipsChoiceOption(value: 4, label: 'N4'), - ChipsChoiceOption(value: 3, label: 'N3'), - ChipsChoiceOption(value: 2, label: 'N2'), - ChipsChoiceOption(value: 1, label: 'N1'), - ], - onChanged: (value) => setState(() => _jlpt = value), - itemConfig: ChipsChoiceItemConfig( - showCheckmark: false, - unselectedBrightness: Brightness.dark, - selectedBrightness: Brightness.dark, - ), - isWrapped: true, - ), - ChipsChoice.multiple( - value: _level, - options: [ - ChipsChoiceOption(value: 'weak', label: 'Weak'), - ChipsChoiceOption(value: 'medium', label: 'Medium'), - ChipsChoiceOption(value: 'strong', label: 'Strong'), - ], - onChanged: (value) => setState(() => _level = value), - itemConfig: ChipsChoiceItemConfig( - showCheckmark: false, - unselectedBrightness: Brightness.dark, - selectedBrightness: Brightness.dark, - ), - isWrapped: true, - ), - ], - ), - ); - } -} - -class SortSection extends StatelessWidget { - SortSection({Key key}) : super(key: key); - - final sorts = [ - 'Alphabetical', - 'Next Review', - 'Streak', - 'Accuracy', - ]; - - @override - Widget build(BuildContext context) { - return StoreConnector( - converter: (Store state) => _ViewModel.create(state), - builder: (context, _ViewModel vm) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - /*Container( - padding: const EdgeInsets.all(16.0), - child: Text('並べ替え', style: style), - ),*/ - ListView.builder( - shrinkWrap: true, - padding: EdgeInsets.zero, - physics: NeverScrollableScrollPhysics(), - itemCount: sorts.length, - itemBuilder: (context, index) { - return ListTile( - title: Text(sorts[index], - style: sorts[index] == vm.field - ? TextStyle( - fontWeight: FontWeight.bold, - color: Colors.orangeAccent, - ) - : null), - selected: sorts[index] == vm.field, - // selectedTileColor: Colors.red, - trailing: sorts[index] == vm.field - ? Icon(vm.mode == 'ASC' ? Icons.arrow_upward : Icons.arrow_downward, color: Colors.orangeAccent) - : null, - onTap: () { - if (vm.field != sorts[index]) { - vm.setField(sorts[index]); - } else { - vm.setMode(vm.mode == 'ASC' ? 'DESC' : 'ASC'); - } - }, - ); - }, - ), - ], - ); - }, - ); - } -} - -class _ViewModel { - final String field; - final String mode; - final Function(String) setField; - final Function(String) setMode; - - _ViewModel({this.field, this.mode, this.setField, this.setMode}); - - factory _ViewModel.create(Store store) { - final _field = store.state.orderState.field; - final _mode = store.state.orderState.mode; - - void _setField(String field) { - store.dispatch(changeSortField(field)); - } - - void _setMode(String mode) { - store.dispatch(changeSortMode(mode)); - } - - return _ViewModel( - field: _field, - mode: _mode, - setField: _setField, - setMode: _setMode, - ); - } -} diff --git a/lib/pages/home/components/filter_sheet/components/filter_section.dart b/lib/pages/home/components/filter_sheet/components/filter_section.dart new file mode 100644 index 0000000..2300762 --- /dev/null +++ b/lib/pages/home/components/filter_sheet/components/filter_section.dart @@ -0,0 +1,136 @@ +import 'package:chips_choice/chips_choice.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:jap_vocab/redux/state/app_state.dart'; +import 'package:jap_vocab/redux/thunk/filter.dart'; +import 'package:redux/redux.dart'; + +class FilterSection extends StatelessWidget { + @override + Widget build(BuildContext context) { + final _chipConfig = ChipsChoiceItemConfig( + showCheckmark: false, + unselectedBrightness: Brightness.dark, + selectedBrightness: Brightness.dark, + selectedColor: Theme.of(context).accentColor, + margin: EdgeInsets.zero, + spacing: 8.0, + labelStyle: Theme.of(context).textTheme.caption, + ); + + final _style = Theme.of(context).textTheme.subtitle1; + + // TODO: add select all / deselect all button + return StoreConnector( + converter: (Store store) => _ViewModel.create(store), + builder: (context, _ViewModel vm) { + return Container( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisSize: MainAxisSize.min, + children: [ + Text('JLPT', style: _style), + ChipsChoice.multiple( + value: vm.jlpt, + options: [ + ChipsChoiceOption(value: 5, label: 'N5'), + ChipsChoiceOption(value: 4, label: 'N4'), + ChipsChoiceOption(value: 3, label: 'N3'), + ChipsChoiceOption(value: 2, label: 'N2'), + ChipsChoiceOption(value: 1, label: 'N1'), + ], + onChanged: (value) => vm.setJLPT(value), + itemConfig: _chipConfig, + padding: EdgeInsets.zero, + isWrapped: true, + ), + Text('Level', style: _style), + ChipsChoice.multiple( + value: vm.level, + options: [ + ChipsChoiceOption(value: 'weak', label: 'Weak'), + ChipsChoiceOption(value: 'medium', label: 'Medium'), + ChipsChoiceOption(value: 'strong', label: 'Strong'), + ], + onChanged: (value) => vm.setLevel(value), + itemConfig: _chipConfig, + padding: EdgeInsets.zero, + isWrapped: true, + ), + Text('Part Of Speech', style: _style), + ChipsChoice.multiple( + value: vm.partOfSpeech, + options: [ + ChipsChoiceOption(value: 'avverbio', label: 'avverbio'), + ChipsChoiceOption( + value: 'aggettivo-no', label: 'aggettivo-no'), + ChipsChoiceOption( + value: 'aggettivo-na', label: 'aggettivo-na'), + ChipsChoiceOption(value: 'verbo', label: 'verbo'), + ChipsChoiceOption( + value: 'verbo-transitivo', label: 'verbo transitivo'), + ChipsChoiceOption( + value: 'verbo-intransitivo', label: 'verbo intransitivo'), + ChipsChoiceOption(value: 'verbo-suru', label: 'verbo suru'), + ChipsChoiceOption(value: 'sostantivo', label: 'sostantivo'), + ], + onChanged: (value) => vm.setPartOfSpeech(value), + itemConfig: _chipConfig, + padding: EdgeInsets.zero, + isWrapped: true, + ), + ], + ), + ); + }, + ); + } +} + +class _ViewModel { + final List jlpt; + final List level; + final List partOfSpeech; + + final Function(List) setJLPT; + final Function(List) setLevel; + final Function(List) setPartOfSpeech; + + _ViewModel({ + this.jlpt, + this.level, + this.partOfSpeech, + this.setJLPT, + this.setLevel, + this.setPartOfSpeech, + }); + + factory _ViewModel.create(Store store) { + final _jlpt = store.state.filterState.jlpt; + final _level = store.state.filterState.level; + final _partOfSpeech = store.state.filterState.partOfSpeech; + + void _setJLPT(List jlpt) { + store.dispatch(changeJLPT(jlpt)); + } + + void _setLevel(List level) { + store.dispatch(changeLevel(level)); + } + + void _setPartOfSpeech(List partOfSpeech) { + store.dispatch(changePartOfSpeech(partOfSpeech)); + } + + return _ViewModel( + jlpt: _jlpt, + level: _level, + partOfSpeech: _partOfSpeech, + setJLPT: _setJLPT, + setLevel: _setLevel, + setPartOfSpeech: _setPartOfSpeech, + ); + } +} diff --git a/lib/pages/home/components/filter_sheet/components/sort_section.dart b/lib/pages/home/components/filter_sheet/components/sort_section.dart new file mode 100644 index 0000000..08222ff --- /dev/null +++ b/lib/pages/home/components/filter_sheet/components/sort_section.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:jap_vocab/redux/state/app_state.dart'; +import 'package:jap_vocab/redux/thunk/order.dart'; +import 'package:redux/redux.dart'; + +class SortSection extends StatelessWidget { + SortSection({Key key}) : super(key: key); + + final sorts = [ + 'Alphabetical', + 'Next Review', + 'Streak', + 'Accuracy', + ]; + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: (Store state) => _ViewModel.create(state), + builder: (context, _ViewModel vm) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + /*Container( + padding: const EdgeInsets.all(16.0), + child: Text('並べ替え', style: style), + ),*/ + ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: NeverScrollableScrollPhysics(), + itemCount: sorts.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(sorts[index], + style: sorts[index] == vm.field + ? TextStyle( + fontWeight: FontWeight.bold, + color: Colors.orangeAccent, + ) + : null), + selected: sorts[index] == vm.field, + trailing: sorts[index] == vm.field + ? Icon( + vm.mode == 'ASC' + ? Icons.arrow_upward + : Icons.arrow_downward, + color: Colors.orangeAccent, + ) + : null, + onTap: () { + if (vm.field != sorts[index]) { + vm.setField(sorts[index]); + } else { + vm.setMode(vm.mode == 'ASC' ? 'DESC' : 'ASC'); + } + }, + ); + }, + ), + ], + ); + }, + ); + } +} + +class _ViewModel { + final String field; + final String mode; + final Function(String) setField; + final Function(String) setMode; + + _ViewModel({this.field, this.mode, this.setField, this.setMode}); + + factory _ViewModel.create(Store store) { + final _field = store.state.orderState.field; + final _mode = store.state.orderState.mode; + + void _setField(String field) { + store.dispatch(changeSortField(field)); + } + + void _setMode(String mode) { + store.dispatch(changeSortMode(mode)); + } + + return _ViewModel( + field: _field, + mode: _mode, + setField: _setField, + setMode: _setMode, + ); + } +} diff --git a/lib/pages/home/components/filter_sheet/filter_sheet.dart b/lib/pages/home/components/filter_sheet/filter_sheet.dart new file mode 100644 index 0000000..0a0fa57 --- /dev/null +++ b/lib/pages/home/components/filter_sheet/filter_sheet.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:jap_vocab/components/md2_indicator.dart'; +import 'package:jap_vocab/pages/home/components/filter_sheet/components/filter_section.dart'; +import 'package:jap_vocab/pages/home/components/filter_sheet/components/sort_section.dart'; + +class FilterSheet extends StatelessWidget { + FilterSheet({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return DefaultTabController( + initialIndex: 0, + length: 2, + child: Container( + height: MediaQuery.of(context).size.height * 0.54, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16.0), + topRight: Radius.circular(16.0), + ), + ), + padding: const EdgeInsets.only(top: 4), + child: TabBar( + tabs: [ + Tab(text: '並べ替え'), + Tab(text: 'フィルター'), + ], + unselectedLabelColor: Colors.white30, + labelColor: Colors.white, + indicatorSize: TabBarIndicatorSize.label, + indicator: MD2Indicator( + indicatorHeight: 4.0, + indicatorColor: Colors.white, + horizontalPadding: 16.0, + ), + ), + ), + Expanded( + child: Container( + color: Theme.of(context).primaryColorLight, + child: TabBarView( + children: [ + SortSection(), + FilterSection(), + ], + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/redux/thunk/filter.dart b/lib/redux/thunk/filter.dart index 3e5fd93..a32f199 100644 --- a/lib/redux/thunk/filter.dart +++ b/lib/redux/thunk/filter.dart @@ -5,6 +5,13 @@ import 'package:jap_vocab/redux/thunk/reviews.dart'; import 'package:redux/redux.dart'; import 'package:redux_thunk/redux_thunk.dart'; +ThunkAction changeSearch(String search) { + return (Store store) async { + await store.dispatch(ChangeSearchAction(search)); + await store.dispatch(getItems()); + }; +} + ThunkAction changeType(String type) { return (Store store) async { await store.dispatch(ChangeTypeAction(type)); @@ -17,7 +24,6 @@ ThunkAction changeJLPT(List jlpt) { return (Store store) async { await store.dispatch(ChangeJLTPAction(jlpt)); await store.dispatch(getItems()); - await store.dispatch(getReviews()); }; } @@ -25,7 +31,6 @@ ThunkAction changeLevel(List level) { return (Store store) async { await store.dispatch(ChangeLevelAction(level)); await store.dispatch(getItems()); - await store.dispatch(getReviews()); }; } @@ -33,6 +38,5 @@ ThunkAction changePartOfSpeech(List partOfSpeech) { return (Store store) async { await store.dispatch(ChangePartOfSpeechAction(partOfSpeech)); await store.dispatch(getItems()); - await store.dispatch(getReviews()); }; } diff --git a/lib/redux/thunk/items.dart b/lib/redux/thunk/items.dart index 3028e19..1efdd98 100644 --- a/lib/redux/thunk/items.dart +++ b/lib/redux/thunk/items.dart @@ -14,17 +14,10 @@ ThunkAction getItems() { return (Store store) async { await store.dispatch(LoadingItemsAction()); - var type = store.state.filterState.type; - var search = store.state.filterState.search; - var sortField = store.state.orderState.field; - var sortMode = store.state.orderState.mode; - - final items = await ItemDao().getAllItems( - type: type, - search: search, - sortField: sortField, - sortMode: sortMode, - ); + var filter = store.state.filterState; + var order = store.state.orderState; + + final items = await ItemDao().getAllItems(filter: filter, order: order); await store.dispatch(LoadedItemsAction(items)); }; }