From 247af48af702717846a3e0aff707008978d064f1 Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Fri, 16 Jul 2021 21:24:31 +0300 Subject: [PATCH 01/60] Prevent use of S.of(context) via automated review. (#225) * Prevent use of S.of(context). * Add test code. * Fix dangerfile. * Check only dart files. * Revert "Add test code." This reverts commit bd975da2 * Move remote_config import back --- .github/linter/Dangerfile | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/linter/Dangerfile b/.github/linter/Dangerfile index a523b3a32..ae8e9040a 100644 --- a/.github/linter/Dangerfile +++ b/.github/linter/Dangerfile @@ -13,6 +13,19 @@ flutter_lint.only_modified_files = true flutter_lint.report_path = "flutter_analyze_report.txt" flutter_lint.lint(inline_mode: true) +files = git.added_files + git.modified_files +files.each do |f| + diff = git.diff_for_file(f) + # Check for uses of S.of(context) or similar + if f =~ /.*\.dart/ and diff.patch =~ /^\+.*S\.of\(.+\)/m + File.readlines(f).each_with_index do |line, index| + if line =~ /S\.of\(.+\)/ + warn("Use S.current instead of S.of(context)", file: f, line: index+1) + end + end + end +end + # Analyze documentation textlint.config_file = '.github/linter/.textlintrc' textlint.max_severity = "warn" From 25a8e0123627788858dba401388159ce7790c6d3 Mon Sep 17 00:00:00 2001 From: AlexandraPavel <72041403+AlexandraPavel@users.noreply.github.com> Date: Fri, 16 Jul 2021 23:24:26 +0300 Subject: [PATCH 02/60] Updated README.md, added a new contributor (#216) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f178e9f0c..dd80187fc 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ https://user-images.githubusercontent.com/25504811/120929790-1bc24080-c6f3-11eb- * [Bogdan Piele](https://github.com/bogpie) * [Bogdan Iuga](https://github.com/iugabogdan98) * [Andreea-Giorgiana Adăscăliței](https://github.com/AndreeaAdascalitei) +* [Alexandra Pavel](https://github.com/AlexandraPavel) ## Building from source with Android Studio From b4811f340b14c630ef790f715bf9ee85c02bda6a Mon Sep 17 00:00:00 2001 From: Badea Dragos <31243571+GhiaraD@users.noreply.github.com> Date: Sat, 17 Jul 2021 18:26:41 +0300 Subject: [PATCH 03/60] Add Badea Dragos to contributors list (#226) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dd80187fc..74ce45a62 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ https://user-images.githubusercontent.com/25504811/120929790-1bc24080-c6f3-11eb- * [Bogdan Iuga](https://github.com/iugabogdan98) * [Andreea-Giorgiana Adăscăliței](https://github.com/AndreeaAdascalitei) * [Alexandra Pavel](https://github.com/AlexandraPavel) +* [Ștefan-Dragoș Badea](https://github.com/GhiaraD) ## Building from source with Android Studio From cc7a211ab021dd6e84fbd4543663bcf8f5164c2e Mon Sep 17 00:00:00 2001 From: stafy2912 <56953855+stafy2912@users.noreply.github.com> Date: Sun, 18 Jul 2021 11:32:27 +0300 Subject: [PATCH 04/60] Add Alin Pahontu to Contributors List (#221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added myself to contributers * Added myself to contributers * Added myself to contributers * Added myself to contributers * Added myself to contributers * Added myself to contributers(+ Ș/ț) Co-authored-by: stafy2912 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 74ce45a62..00f38f53f 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ https://user-images.githubusercontent.com/25504811/120929790-1bc24080-c6f3-11eb- * [Bogdan Piele](https://github.com/bogpie) * [Bogdan Iuga](https://github.com/iugabogdan98) * [Andreea-Giorgiana Adăscăliței](https://github.com/AndreeaAdascalitei) +* [Ștefan-Alin Pahonțu](https://github.com/stafy2912) * [Alexandra Pavel](https://github.com/AlexandraPavel) * [Ștefan-Dragoș Badea](https://github.com/GhiaraD) From 540e4e4a52e5a5bc8036cadb7982b4171ab64065 Mon Sep 17 00:00:00 2001 From: StefanP-EQ Date: Mon, 19 Jul 2021 00:29:38 +0300 Subject: [PATCH 05/60] Stefan Popa - Update Contributors --- README.md | 1 + pubspec.lock | 88 ++++++++++++++-------------------------------------- 2 files changed, 24 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 00f38f53f..4df651c8b 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ https://user-images.githubusercontent.com/25504811/120929790-1bc24080-c6f3-11eb- * [Ștefan-Alin Pahonțu](https://github.com/stafy2912) * [Alexandra Pavel](https://github.com/AlexandraPavel) * [Ștefan-Dragoș Badea](https://github.com/GhiaraD) +* [Stefan-Andrei Popa](https://github.com/AndreiPopa21) ## Building from source with Android Studio diff --git a/pubspec.lock b/pubspec.lock index 713b2771e..3d75690c2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,7 +42,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0-nullsafety.3" + version: "2.6.1" auto_size_text: dependency: "direct main" description: @@ -77,28 +77,7 @@ packages: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.3" - build: - dependency: transitive - description: - name: build - url: "https://pub.dartlang.org" - source: hosted - version: "1.6.2" - built_collection: - dependency: transitive - description: - name: built_collection - url: "https://pub.dartlang.org" - source: hosted - version: "4.3.2" - built_value: - dependency: transitive - description: - name: built_value - url: "https://pub.dartlang.org" - source: hosted - version: "7.1.0" + version: "2.1.0" cached_network_image: dependency: "direct main" description: @@ -112,14 +91,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.5" + version: "1.1.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.3" + version: "1.2.0" cli_util: dependency: transitive description: @@ -133,7 +112,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.3" + version: "1.1.0" cloud_firestore: dependency: "direct main" description: @@ -155,20 +134,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.1+2" - code_builder: - dependency: transitive - description: - name: code_builder - url: "https://pub.dartlang.org" - source: hosted - version: "3.7.0" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0-nullsafety.5" + version: "1.15.0" color: dependency: transitive description: @@ -259,7 +231,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.3" + version: "1.2.0" ffi: dependency: transitive description: @@ -372,13 +344,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.1+1" - fixnum: - dependency: transitive - description: - name: fixnum - url: "https://pub.dartlang.org" - source: hosted - version: "0.10.11" flutter: dependency: "direct main" description: flutter @@ -566,7 +531,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3-nullsafety.3" + version: "0.6.3" json_annotation: dependency: transitive description: @@ -594,21 +559,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10-nullsafety.3" + version: "0.12.10" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.6" + version: "1.3.0" mockito: dependency: "direct dev" description: name: mockito url: "https://pub.dartlang.org" source: hosted - version: "4.1.4" + version: "4.1.1+1" nested: dependency: transitive description: @@ -622,7 +587,7 @@ packages: name: network_image_mock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.0.2" node_interop: dependency: transitive description: @@ -706,7 +671,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.3" + version: "1.8.0" path_provider: dependency: transitive description: @@ -894,20 +859,13 @@ packages: description: flutter source: sdk version: "0.0.99" - source_gen: - dependency: transitive - description: - name: source_gen - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.10+2" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.4" + version: "1.8.1" sqflite: dependency: transitive description: @@ -928,21 +886,21 @@ packages: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.10.0-nullsafety.6" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.3" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.3" + version: "1.1.0" synchronized: dependency: "direct main" description: @@ -956,14 +914,14 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.3" + version: "1.2.0" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19-nullsafety.6" + version: "0.3.0" time: dependency: transitive description: @@ -991,7 +949,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.5" + version: "1.3.0" url_launcher: dependency: "direct main" description: @@ -1054,7 +1012,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.5" + version: "2.1.0" watcher: dependency: transitive description: @@ -1098,5 +1056,5 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=2.12.0-0.0 <3.0.0" - flutter: ">=1.24.0-7.0.pre <2.0.0" + dart: ">=2.12.0 <3.0.0" + flutter: ">=1.24.0-7.0.pre" From 43a88eb8510c8bf905f0eb53d09daddf7cf2f778 Mon Sep 17 00:00:00 2001 From: Stefan Popa Date: Mon, 19 Jul 2021 10:45:56 +0300 Subject: [PATCH 06/60] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4df651c8b..a9cf22429 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ https://user-images.githubusercontent.com/25504811/120929790-1bc24080-c6f3-11eb- * [Ștefan-Alin Pahonțu](https://github.com/stafy2912) * [Alexandra Pavel](https://github.com/AlexandraPavel) * [Ștefan-Dragoș Badea](https://github.com/GhiaraD) -* [Stefan-Andrei Popa](https://github.com/AndreiPopa21) +* [Ștefan-Andrei Popa](https://github.com/AndreiPopa21) ## Building from source with Android Studio From 174c7ae4942378719536d3a3bf8611e43332c355 Mon Sep 17 00:00:00 2001 From: StefanP-EQ Date: Mon, 19 Jul 2021 10:48:37 +0300 Subject: [PATCH 07/60] Revert pubspec.lock --- pubspec.lock | 88 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 23 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 3d75690c2..35cfe6a95 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,7 +42,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.5.0-nullsafety.3" auto_size_text: dependency: "direct main" description: @@ -77,7 +77,28 @@ packages: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.0-nullsafety.3" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.2" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.2" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "7.1.0" cached_network_image: dependency: "direct main" description: @@ -91,14 +112,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.0-nullsafety.5" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.0-nullsafety.3" cli_util: dependency: transitive description: @@ -112,7 +133,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.0-nullsafety.3" cloud_firestore: dependency: "direct main" description: @@ -134,13 +155,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.1+2" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.7.0" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.15.0-nullsafety.5" color: dependency: transitive description: @@ -231,7 +259,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.0-nullsafety.3" ffi: dependency: transitive description: @@ -344,6 +372,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.1+1" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.11" flutter: dependency: "direct main" description: flutter @@ -531,7 +566,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.3-nullsafety.3" json_annotation: dependency: transitive description: @@ -559,21 +594,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.10-nullsafety.3" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.0-nullsafety.6" mockito: dependency: "direct dev" description: name: mockito url: "https://pub.dartlang.org" source: hosted - version: "4.1.1+1" + version: "4.1.4" nested: dependency: transitive description: @@ -587,7 +622,7 @@ packages: name: network_image_mock url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.1.0" node_interop: dependency: transitive description: @@ -671,7 +706,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.0-nullsafety.3" path_provider: dependency: transitive description: @@ -859,13 +894,20 @@ packages: description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.10+2" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.0-nullsafety.4" sqflite: dependency: transitive description: @@ -886,21 +928,21 @@ packages: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.10.0" + version: "1.10.0-nullsafety.6" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.0-nullsafety.3" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.0-nullsafety.3" synchronized: dependency: "direct main" description: @@ -914,14 +956,14 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.0-nullsafety.3" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.2.19-nullsafety.6" time: dependency: transitive description: @@ -949,7 +991,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.0-nullsafety.5" url_launcher: dependency: "direct main" description: @@ -1012,7 +1054,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.0-nullsafety.5" watcher: dependency: transitive description: @@ -1056,5 +1098,5 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=1.24.0-7.0.pre" + dart: ">=2.12.0-0.0 <3.0.0" + flutter: ">=1.24.0-7.0.pre <2.0.0" \ No newline at end of file From c0c5dd6fc86a63184c124909772e723185469cc6 Mon Sep 17 00:00:00 2001 From: Stefan Popa Date: Mon, 19 Jul 2021 10:50:32 +0300 Subject: [PATCH 08/60] Update pubspec.lock --- pubspec.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index 35cfe6a95..95b7b5228 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1099,4 +1099,5 @@ packages: version: "2.2.1" sdks: dart: ">=2.12.0-0.0 <3.0.0" - flutter: ">=1.24.0-7.0.pre <2.0.0" \ No newline at end of file + flutter: ">=1.24.0-7.0.pre <2.0.0" + From 857f5c7acab1aa1cf8ed05665fd0f77e916f04a3 Mon Sep 17 00:00:00 2001 From: Stefan Popa Date: Mon, 19 Jul 2021 10:50:58 +0300 Subject: [PATCH 09/60] Update pubspec.lock --- pubspec.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index 95b7b5228..713b2771e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1100,4 +1100,3 @@ packages: sdks: dart: ">=2.12.0-0.0 <3.0.0" flutter: ">=1.24.0-7.0.pre <2.0.0" - From d815af2694d7c80269dcc24761db61d806dbce69 Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Mon, 19 Jul 2021 11:02:57 +0300 Subject: [PATCH 10/60] Sort people by last name. #trivial (#228) * Sort people by last name. * Bump build number. Co-authored-by: Andrei-Constantin Mirciu <38398944+andreicmirciu@users.noreply.github.com> --- lib/pages/people/model/person.dart | 2 ++ lib/pages/people/view/people_page.dart | 45 ++++++++++++-------------- pubspec.yaml | 2 +- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/lib/pages/people/model/person.dart b/lib/pages/people/model/person.dart index e178a79c4..decd5d945 100644 --- a/lib/pages/people/model/person.dart +++ b/lib/pages/people/model/person.dart @@ -14,6 +14,8 @@ class Person { final String position; final String photo; + String get lastName => name.trim().split(' ').last; + @override int get hashCode => name.hashCode; diff --git a/lib/pages/people/view/people_page.dart b/lib/pages/people/view/people_page.dart index 1c9e2ca09..49050efa8 100644 --- a/lib/pages/people/view/people_page.dart +++ b/lib/pages/people/view/people_page.dart @@ -87,59 +87,56 @@ class _PeoplePageState extends State { .toList(); } -class PeopleList extends StatefulWidget { +class PeopleList extends StatelessWidget { const PeopleList({this.people, this.filter}); final List people; final String filter; - @override - _PeopleListState createState() => _PeopleListState(); -} - -class _PeopleListState extends State { @override Widget build(BuildContext context) { - final List filteredWords = widget.filter + final List filteredWords = filter .toLowerCase() .split(' ') .where((element) => element != '') .toList(); + people.sort((p1, p2) { + final cmpLast = p1.lastName.compareTo(p2.lastName); + if (cmpLast != 0) { + return cmpLast; + } + return p1.name.compareTo(p2.name); + }); return ListView.builder( shrinkWrap: true, - itemCount: widget.people.length, + itemCount: people.length, itemBuilder: (context, index) { return ListTile( - key: ValueKey(widget.people[index].name), + key: ValueKey(people[index].name), leading: CircleAvatar( - backgroundImage: NetworkImage(widget.people[index].photo), + backgroundImage: NetworkImage(people[index].photo), ), title: filteredWords.isNotEmpty ? DynamicTextHighlighting( - text: widget.people[index].name, + text: people[index].name, style: Theme.of(context).textTheme.subtitle1, highlights: filteredWords, color: Theme.of(context).accentColor, caseSensitive: false, ) - : Text( - widget.people[index].name, - ), - subtitle: Text(widget.people[index].email), - onTap: () => showPersonInfo(widget.people[index]), + : Text(people[index].name), + subtitle: Text(people[index].email), + onTap: () => showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (BuildContext buildContext) => + PersonView(person: people[index]), + ), ); }, ); } - - void showPersonInfo(Person person) { - showModalBottomSheet( - isScrollControlled: true, - context: context, - builder: (BuildContext buildContext) => PersonView(person: person), - ); - } } class AutocompletePerson extends StatefulWidget { diff --git a/pubspec.yaml b/pubspec.yaml index b4e05745e..938f2fd48 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ description: A mobile application for students at ACS UPB. # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # # ACS UPB Mobile uses semantic versioning. You can read more in the CONTRIBUTING.md file. -version: 1.2.12+16 +version: 1.2.12+17 environment: sdk: ">=2.7.0 <3.0.0" From 573068799cc7f24128edb086bd200672882700b3 Mon Sep 17 00:00:00 2001 From: AndreiMirica <64529696+AndreiMirica19@users.noreply.github.com> Date: Mon, 19 Jul 2021 13:27:28 +0300 Subject: [PATCH 11/60] Add Andrei Mirica to contributors (#212) * update_contributors * Update pubspec.lock * Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 00f38f53f..b3380b331 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ https://user-images.githubusercontent.com/25504811/120929790-1bc24080-c6f3-11eb- * [Anghel Andrei](https://github.com/AnghelAndrei28) * [Bogdan Piele](https://github.com/bogpie) * [Bogdan Iuga](https://github.com/iugabogdan98) +* [Andrei Mirică](https://github.com/AndreiMirica19) * [Andreea-Giorgiana Adăscăliței](https://github.com/AndreeaAdascalitei) * [Ștefan-Alin Pahonțu](https://github.com/stafy2912) * [Alexandra Pavel](https://github.com/AlexandraPavel) From 43bc03d08d202deea485a6632b74d3150bd6f15f Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Thu, 22 Jul 2021 23:19:20 +0300 Subject: [PATCH 12/60] Allow aii.pub.ro unsafe connections. #trivial (#227) * Allow unsafe traffic from aii.pub.ro. * Only allow HTTP fo aii.pub.ro. * Fix some whitespace. * Bump version. * Allow HTTP on iOS as well. * Indent Info.plist using tabs. * Missed a tab. --- android/app/src/main/AndroidManifest.xml | 5 ++++ .../main/res/xml/network_security_config.xml | 6 ++++ ios/Podfile.lock | 30 +++++++++++++++++++ ios/Runner.xcodeproj/project.pbxproj | 2 ++ ios/Runner/Info.plist | 6 ++++ lib/main.dart | 12 ++++---- lib/pages/people/view/people_page.dart | 5 +++- pubspec.yaml | 2 +- 8 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 android/app/src/main/res/xml/network_security_config.xml diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 38bfc21ac..fa63c6215 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -43,5 +43,10 @@ + + + diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 000000000..7bcc3fd4c --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,6 @@ + + + + aii.pub.ro + + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 670851ebc..2c6dd7c95 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -236,6 +236,9 @@ PODS: - Firebase/Firestore (6.33.0): - Firebase/CoreOnly - FirebaseFirestore (~> 1.18.0) + - Firebase/RemoteConfig (6.33.0): + - Firebase/CoreOnly + - FirebaseRemoteConfig (~> 4.9.0) - Firebase/Storage (6.33.0): - Firebase/CoreOnly - FirebaseStorage (~> 3.9.0) @@ -251,11 +254,18 @@ PODS: - firebase_core (0.5.3): - Firebase/CoreOnly (~> 6.33.0) - Flutter + - firebase_remote_config (0.4.3): + - Firebase/CoreOnly (~> 6.33.0) + - Firebase/RemoteConfig (~> 6.33.0) + - firebase_core + - Flutter - firebase_storage (5.2.0): - Firebase/CoreOnly (~> 6.33.0) - Firebase/Storage (~> 6.33.0) - firebase_core - Flutter + - FirebaseABTesting (4.2.0): + - FirebaseCore (~> 6.10) - FirebaseAnalytics (6.8.3): - FirebaseCore (~> 6.10) - FirebaseInstallations (~> 1.6) @@ -296,10 +306,18 @@ PODS: - GoogleUtilities/Environment (~> 6.7) - GoogleUtilities/UserDefaults (~> 6.7) - PromisesObjC (~> 1.2) + - FirebaseRemoteConfig (4.9.1): + - FirebaseABTesting (~> 4.2) + - FirebaseCore (~> 6.10) + - FirebaseInstallations (~> 1.6) + - GoogleUtilities/Environment (~> 6.7) + - "GoogleUtilities/NSData+zlib (~> 6.7)" - FirebaseStorage (3.9.1): - FirebaseCore (~> 6.10) - GTMSessionFetcher/Core (~> 1.1) - Flutter (1.0.0) + - flutter_web_browser (0.13.1): + - Flutter - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) @@ -383,8 +401,10 @@ DEPENDENCIES: - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_remote_config (from `.symlinks/plugins/firebase_remote_config/ios`) - firebase_storage (from `.symlinks/plugins/firebase_storage/ios`) - Flutter (from `Flutter`) + - flutter_web_browser (from `.symlinks/plugins/flutter_web_browser/ios`) - image_picker (from `.symlinks/plugins/image_picker/ios`) - package_info (from `.symlinks/plugins/package_info/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) @@ -398,12 +418,14 @@ SPEC REPOS: - abseil - BoringSSL-GRPC - Firebase + - FirebaseABTesting - FirebaseAnalytics - FirebaseAuth - FirebaseCore - FirebaseCoreDiagnostics - FirebaseFirestore - FirebaseInstallations + - FirebaseRemoteConfig - FirebaseStorage - FMDB - GoogleAppMeasurement @@ -425,10 +447,14 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/firebase_auth/ios" firebase_core: :path: ".symlinks/plugins/firebase_core/ios" + firebase_remote_config: + :path: ".symlinks/plugins/firebase_remote_config/ios" firebase_storage: :path: ".symlinks/plugins/firebase_storage/ios" Flutter: :path: Flutter + flutter_web_browser: + :path: ".symlinks/plugins/flutter_web_browser/ios" image_picker: :path: ".symlinks/plugins/image_picker/ios" package_info: @@ -452,15 +478,19 @@ SPEC CHECKSUMS: firebase_analytics: 9118044ffb98bee71d84733fc594f5134fe4bc1b firebase_auth: d5159db3873478d1ac839af7b10d2f831516136a firebase_core: 5d6a02f3d85acd5f8321c2d6d62877626a670659 + firebase_remote_config: 259817aa1d7db2d84f01d1536b4f847dd2058e47 firebase_storage: a023e199edb807d8481c8aa722c8516f462ffab2 + FirebaseABTesting: 8a9d8df3acc2b43f4a22014ddf9f601bca6af699 FirebaseAnalytics: 5dd088bd2e67bb9d13dbf792d1164ceaf3052193 FirebaseAuth: c92d49ada7948d1a23466e3db17bc4c2039dddc3 FirebaseCore: d889d9e12535b7f36ac8bfbf1713a0836a3012cd FirebaseCoreDiagnostics: 770ac5958e1372ce67959ae4b4f31d8e127c3ac1 FirebaseFirestore: adff4877869ca91a11250cc0989a6cd56bad163f FirebaseInstallations: 466c7b4d1f58fe16707693091da253726a731ed2 + FirebaseRemoteConfig: 35a729305f254fb15a2e541d4b36f3a379da7fdc FirebaseStorage: 15e0f15ef3c7fec3d1899d68623e47d4447066b4 Flutter: 0e3d915762c693b495b44d77113d4970485de6ec + flutter_web_browser: cf735f704b5d72449e6ea1cb65a7da102aa9123a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a GoogleAppMeasurement: 966e88df9d19c15715137bb2ddaf52373f111436 GoogleDataTransport: f56af7caa4ed338dc8e138a5d7c5973e66440833 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 082c6ea32..b4362c536 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -261,6 +261,7 @@ "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", "${BUILT_PRODUCTS_DIR}/abseil/absl.framework", + "${BUILT_PRODUCTS_DIR}/flutter_web_browser/flutter_web_browser.framework", "${BUILT_PRODUCTS_DIR}/gRPC-C++/grpcpp.framework", "${BUILT_PRODUCTS_DIR}/gRPC-Core/grpc.framework", "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework", @@ -282,6 +283,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/absl.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_web_browser.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpcpp.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpc.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework", diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 67d7a4a69..4670b95b4 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -45,5 +45,11 @@ UIViewControllerBasedStatusBarAppearance + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + diff --git a/lib/main.dart b/lib/main.dart index 97b6eaccc..994bbd9d0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,6 +21,7 @@ import 'package:acs_upb_mobile/pages/settings/view/request_permissions.dart'; import 'package:acs_upb_mobile/pages/settings/view/settings_page.dart'; import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; import 'package:acs_upb_mobile/resources/locale_provider.dart'; +import 'package:acs_upb_mobile/resources/remote_config.dart'; import 'package:acs_upb_mobile/resources/utils.dart'; import 'package:acs_upb_mobile/widgets/loading_screen.dart'; import 'package:dynamic_theme/dynamic_theme.dart'; @@ -37,19 +38,18 @@ import 'package:preferences/preferences.dart'; import 'package:provider/provider.dart'; import 'package:rrule/rrule.dart'; import 'package:time_machine/time_machine.dart'; -import 'package:acs_upb_mobile/resources/remote_config.dart'; -// FIXME: acs.pub.ro has some bad certificate configuration right now, and the -// cs.pub.ro certificate is expired. -// We get around this by accepting any certificate if the host is either -// acs.pub.ro or cs.pub.ro. +// FIXME: Our university website certificates have some issues, so we say we +// trust them regardless. // Remove this in the future. class MyHttpOverrides extends HttpOverrides { @override HttpClient createHttpClient(SecurityContext context) { return super.createHttpClient(context) ..badCertificateCallback = (X509Certificate cert, String host, int port) { - return host == 'acs.pub.ro' || host == 'cs.pub.ro'; + return host == 'acs.pub.ro' || + host == 'cs.pub.ro' || + host == 'aii.pub.ro'; }; } } diff --git a/lib/pages/people/view/people_page.dart b/lib/pages/people/view/people_page.dart index 49050efa8..dde87cbae 100644 --- a/lib/pages/people/view/people_page.dart +++ b/lib/pages/people/view/people_page.dart @@ -5,6 +5,7 @@ import 'package:acs_upb_mobile/pages/people/view/person_view.dart'; import 'package:acs_upb_mobile/widgets/autocomplete.dart'; import 'package:acs_upb_mobile/widgets/scaffold.dart'; import 'package:acs_upb_mobile/widgets/search_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:dynamic_text_highlighting/dynamic_text_highlighting.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -115,7 +116,9 @@ class PeopleList extends StatelessWidget { return ListTile( key: ValueKey(people[index].name), leading: CircleAvatar( - backgroundImage: NetworkImage(people[index].photo), + backgroundImage: CachedNetworkImageProvider( + people[index].photo, + ), ), title: filteredWords.isNotEmpty ? DynamicTextHighlighting( diff --git a/pubspec.yaml b/pubspec.yaml index 938f2fd48..b3d8cce0c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ description: A mobile application for students at ACS UPB. # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # # ACS UPB Mobile uses semantic versioning. You can read more in the CONTRIBUTING.md file. -version: 1.2.12+17 +version: 1.2.12+18 environment: sdk: ">=2.7.0 <3.0.0" From 7a8f8709afea900cacbe202ade9fecabb66e32c5 Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Fri, 23 Jul 2021 11:19:51 +0300 Subject: [PATCH 13/60] UI improvements (#178) * Remove bottom blue line in tab bar. * Change SelectableFormField to use Chips. * Improve Chip theme. * Make dividers more consistent. Couldn't find the exact setting but this looks decent. * Add custom theme for dark mode. * First attempt at fixing relevance picker. * Fix logic for relevance picker. * Select "Anyone" when no custom node is selected. * Call relevancePicker.onChanged whenever it changes. * Disable public filter options if user doesn't have permission. * Fix ChoiceChip theme. * Make RelevancePicker a FormField everywhere. * Reuse ChipFormField in RelevanceFormField. * Move filterIconColor to resources/theme.dart. * Remove some duplicate code. * Remove S.of(context). * Fix default relevance in picker. * Use chips on filter page. This also fixes #5. * Use chips in FAQ page. * Fix tests. * Make flutter version in workflows more specific. Workflows now fail on GH but work locally, so I think it's because the 1.24.x version somehow gets matched to 2.3.0-24.1.pre-beta instead of 1.24.0-10.2.pre. * Revert workflow changes. No longer seems to be necessary and the previous version didn't work well anyway. * Prep for release. * Bump version and update changelog. * Add comma for formatting. Co-authored-by: Andrei-Constantin Mirciu <38398944+andreicmirciu@users.noreply.github.com> --- .../android/en-GB/changelogs/10019.txt | 8 + .../android/en-US/changelogs/10019.txt | 8 + .../metadata/android/ro/changelogs/10019.txt | 8 + ios/Podfile.lock | 6 - ios/Runner.xcodeproj/project.pbxproj | 2 - lib/main.dart | 34 +- lib/navigation/bottom_navigation_bar.dart | 16 +- lib/pages/faq/view/faq_page.dart | 13 +- lib/pages/filter/service/filter_provider.dart | 39 +- lib/pages/filter/view/filter_page.dart | 53 +- lib/pages/filter/view/relevance_picker.dart | 533 ++++++++++-------- lib/pages/portal/view/portal_page.dart | 32 +- lib/pages/portal/view/website_view.dart | 14 +- .../timetable/view/events/add_event_view.dart | 186 +----- lib/resources/custom_icons.dart | 11 - lib/resources/theme.dart | 23 + lib/widgets/chip_form_field.dart | 123 ++++ pubspec.lock | 46 +- pubspec.yaml | 2 +- 19 files changed, 591 insertions(+), 566 deletions(-) create mode 100644 android/fastlane/metadata/android/en-GB/changelogs/10019.txt create mode 100644 android/fastlane/metadata/android/en-US/changelogs/10019.txt create mode 100644 android/fastlane/metadata/android/ro/changelogs/10019.txt create mode 100644 lib/resources/theme.dart create mode 100644 lib/widgets/chip_form_field.dart diff --git a/android/fastlane/metadata/android/en-GB/changelogs/10019.txt b/android/fastlane/metadata/android/en-GB/changelogs/10019.txt new file mode 100644 index 000000000..496e45246 --- /dev/null +++ b/android/fastlane/metadata/android/en-GB/changelogs/10019.txt @@ -0,0 +1,8 @@ +Improved + - Filtering UI and bottom navigation bar are now nicer⭐ + - Teachers on the People page are now sorted by last name + - Did some internal ✨magic✨ to improve the app + +Fixed + - Bug where some filter options would show as selected even though they were not + - Default filter options when adding a new event were sometimes empty \ No newline at end of file diff --git a/android/fastlane/metadata/android/en-US/changelogs/10019.txt b/android/fastlane/metadata/android/en-US/changelogs/10019.txt new file mode 100644 index 000000000..496e45246 --- /dev/null +++ b/android/fastlane/metadata/android/en-US/changelogs/10019.txt @@ -0,0 +1,8 @@ +Improved + - Filtering UI and bottom navigation bar are now nicer⭐ + - Teachers on the People page are now sorted by last name + - Did some internal ✨magic✨ to improve the app + +Fixed + - Bug where some filter options would show as selected even though they were not + - Default filter options when adding a new event were sometimes empty \ No newline at end of file diff --git a/android/fastlane/metadata/android/ro/changelogs/10019.txt b/android/fastlane/metadata/android/ro/changelogs/10019.txt new file mode 100644 index 000000000..b7e6d3db2 --- /dev/null +++ b/android/fastlane/metadata/android/ro/changelogs/10019.txt @@ -0,0 +1,8 @@ +Îmbunătățit + - Paginile de filtrare și bara de navigare arată mai frumos⭐ + - Profesorii de pe pagina Persoane sunt acum sortați după numele de familie + - Puțină ✨magie✨ internă ca să îmbunătățim aplicația + +Rezolvat + - Uneori apăreau selectate opțiuni din filtru fără să fi fost apăsate + - Opțiunile de filtru default uneori erau goale la adăugarea de evenimente \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2c6dd7c95..df5e824e4 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -381,8 +381,6 @@ PODS: - nanopb/encode (= 1.30906.0) - nanopb/decode (1.30906.0) - nanopb/encode (1.30906.0) - - package_info (0.0.1): - - Flutter - package_info_plus (0.4.5): - Flutter - path_provider (0.0.1): @@ -406,7 +404,6 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_web_browser (from `.symlinks/plugins/flutter_web_browser/ios`) - image_picker (from `.symlinks/plugins/image_picker/ios`) - - package_info (from `.symlinks/plugins/package_info/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) @@ -457,8 +454,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_web_browser/ios" image_picker: :path: ".symlinks/plugins/image_picker/ios" - package_info: - :path: ".symlinks/plugins/package_info/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider: @@ -501,7 +496,6 @@ SPEC CHECKSUMS: image_picker: 9c3312491f862b28d21ecd8fdf0ee14e601b3f09 leveldb-library: 50c7b45cbd7bf543c81a468fe557a16ae3db8729 nanopb: 59317e09cf1f1a0af72f12af412d54edf52603fc - package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c PromisesObjC: b14b1c6b68e306650688599de8a45e49fae81151 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b4362c536..806a8a4b2 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -267,7 +267,6 @@ "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework", "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", - "${BUILT_PRODUCTS_DIR}/package_info/package_info.framework", "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework", "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", "${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework", @@ -289,7 +288,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework", diff --git a/lib/main.dart b/lib/main.dart index 994bbd9d0..30c5081dc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -138,6 +138,24 @@ class _MyAppState extends State { ); } + ChipThemeData defaultChipThemeData(Brightness brightness) => + ChipThemeData.fromDefaults( + brightness: brightness, + secondaryColor: _accentColor, + labelStyle: ThemeData() + .accentTextTheme + .apply( + fontFamily: 'Montserrat', + bodyColor: _accentColor, + displayColor: _accentColor) + .bodyText2, + ); + + Color chipSelectedColor(Brightness brightness) => + brightness == Brightness.light + ? _accentColor.withOpacity(0.3) + : _accentColor; + @override Widget build(BuildContext context) { return DynamicTheme( @@ -154,7 +172,21 @@ class _MyAppState extends State { displayColor: _accentColor), toggleableActiveColor: _accentColor, fontFamily: 'Montserrat', - primaryColor: const Color(0xFF4DB5E4), + primaryColor: _accentColor, + chipTheme: ChipThemeData( + brightness: brightness, + selectedColor: chipSelectedColor(brightness), + secondarySelectedColor: chipSelectedColor(brightness), + backgroundColor: + defaultChipThemeData(brightness).backgroundColor.withOpacity(0.1), + disabledColor: defaultChipThemeData(brightness).disabledColor, + padding: defaultChipThemeData(brightness).padding, + labelStyle: defaultChipThemeData(brightness).labelStyle, + secondaryLabelStyle: + defaultChipThemeData(brightness).secondaryLabelStyle, + checkmarkColor: + brightness == Brightness.light ? _accentColor : Colors.white, + ), ), themedWidgetBuilder: (context, theme) { return OKToast( diff --git a/lib/navigation/bottom_navigation_bar.dart b/lib/navigation/bottom_navigation_bar.dart index 4a833488f..482ed3ebb 100644 --- a/lib/navigation/bottom_navigation_bar.dart +++ b/lib/navigation/bottom_navigation_bar.dart @@ -59,7 +59,7 @@ class _AppBottomNavigationBarState extends State ), bottomNavigationBar: SafeArea( child: SizedBox( - height: 50, + height: 52, child: Column( children: [ const Divider(indent: 0, endIndent: 0, height: 1), @@ -72,36 +72,36 @@ class _AppBottomNavigationBarState extends State ? const Icon(Icons.home) : const Icon(Icons.home_outlined), text: S.current.navigationHome, - iconMargin: const EdgeInsets.only(top: 5), + iconMargin: EdgeInsets.zero, ), Tab( icon: currentTab == 1 ? const Icon(Icons.calendar_today) : const Icon(Icons.calendar_today_outlined), text: S.current.navigationTimetable, - iconMargin: const EdgeInsets.only(top: 5), + iconMargin: EdgeInsets.zero, ), Tab( icon: const Icon(FeatherIcons.globe), text: S.current.navigationPortal, - iconMargin: const EdgeInsets.only(top: 5), + iconMargin: EdgeInsets.zero, ), Tab( icon: currentTab == 3 ? const Icon(Icons.people) : const Icon(Icons.people_outlined), text: S.current.navigationPeople, - iconMargin: const EdgeInsets.only(top: 5), + iconMargin: EdgeInsets.zero, ), ], labelColor: Theme.of(context).accentColor, - labelPadding: EdgeInsets.zero, - indicatorPadding: EdgeInsets.zero, + labelPadding: const EdgeInsets.only(top: 4), unselectedLabelColor: Theme.of(context).unselectedWidgetColor, - indicatorColor: Theme.of(context).accentColor, + indicatorColor: Colors.transparent, ), ), + const SizedBox(height: 2) ], ), ), diff --git a/lib/pages/faq/view/faq_page.dart b/lib/pages/faq/view/faq_page.dart index 3fadb6ef7..0f7f6e2dc 100644 --- a/lib/pages/faq/view/faq_page.dart +++ b/lib/pages/faq/view/faq_page.dart @@ -4,7 +4,6 @@ import 'package:acs_upb_mobile/pages/faq/service/question_provider.dart'; import 'package:acs_upb_mobile/resources/utils.dart'; import 'package:acs_upb_mobile/widgets/scaffold.dart'; import 'package:acs_upb_mobile/widgets/search_bar.dart'; -import 'package:acs_upb_mobile/widgets/selectable.dart'; import 'package:dynamic_text_highlighting/dynamic_text_highlighting.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -21,7 +20,7 @@ class FaqPage extends StatefulWidget { class _FaqPageState extends State { List questions = []; - List categories; + List tags; String filter = ''; bool searchClosed = true; List activeTags = []; @@ -40,12 +39,12 @@ class _FaqPageState extends State { child: ListView( scrollDirection: Axis.horizontal, children: [const SizedBox(width: 10)] + - categories + tags .map((category) => Padding( padding: const EdgeInsets.symmetric(horizontal: 3), - child: Selectable( - label: category, - initiallySelected: false, + child: FilterChip( + label: Text(category), + selected: activeTags.contains(category), onSelected: (selection) { setState(() { if (selection) { @@ -83,7 +82,7 @@ class _FaqPageState extends State { return const Center(child: CircularProgressIndicator()); } questions = snapshot.data; - categories = questions.expand((e) => e.tags).toSet().toList(); + tags = questions.expand((e) => e.tags).toSet().toList(); return ListView( children: [ SearchWidget( diff --git a/lib/pages/filter/service/filter_provider.dart b/lib/pages/filter/service/filter_provider.dart index 7f0a8ad46..21b047884 100644 --- a/lib/pages/filter/service/filter_provider.dart +++ b/lib/pages/filter/service/filter_provider.dart @@ -38,7 +38,9 @@ class FilterProvider with ChangeNotifier { final FirebaseFirestore _db = FirebaseFirestore.instance; Filter _relevanceFilter; // filter cache - /// Whether this is the global filter instance and should update shared preferences + /// Whether this is the global filter instance and should update shared preferences. + /// If false, this is probably a local filter setting (e.g. for an event or website) + /// and should be the user's class by default. final bool global; final String defaultDegree; @@ -138,18 +140,29 @@ class FilterProvider with ChangeNotifier { _relevanceFilter.setRelevantUpToRoot(node, defaultDegree); } _relevantNodes = _relevanceFilter.relevantNodes; - } - - // Check if there is an existing setting already - if (_authProvider != null && - _authProvider.isAuthenticated && - !_authProvider.isAnonymous) { - final userSnap = - await _db.collection('users').doc(_authProvider.uid).get(); - - //Load filter_nodes from Firestore - _relevantNodes = List.from(userSnap['filter_nodes']); - _relevanceFilter?.setRelevantNodes(_relevantNodes); + } else if (!global) { + if (_authProvider != null && + _authProvider.isAuthenticated && + !_authProvider.isAnonymous) { + final userSnap = + await _db.collection('users').doc(_authProvider.uid).get(); + + // Load class information from Firestore + _relevantNodes = List.from(userSnap['class']); + _relevanceFilter?.setRelevantNodes(_relevantNodes); + } + } else { + // Check if there is an existing setting already + if (_authProvider != null && + _authProvider.isAuthenticated && + !_authProvider.isAnonymous) { + final userSnap = + await _db.collection('users').doc(_authProvider.uid).get(); + + // Load filter_nodes from Firestore + _relevantNodes = List.from(userSnap['filter_nodes']); + _relevanceFilter?.setRelevantNodes(_relevantNodes); + } } notifyListeners(); diff --git a/lib/pages/filter/view/filter_page.dart b/lib/pages/filter/view/filter_page.dart index 97bd3d120..6d4173cca 100644 --- a/lib/pages/filter/view/filter_page.dart +++ b/lib/pages/filter/view/filter_page.dart @@ -4,7 +4,6 @@ import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; import 'package:acs_upb_mobile/resources/locale_provider.dart'; import 'package:acs_upb_mobile/widgets/icon_text.dart'; import 'package:acs_upb_mobile/widgets/scaffold.dart'; -import 'package:acs_upb_mobile/widgets/selectable.dart'; import 'package:acs_upb_mobile/widgets/toast.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -46,7 +45,6 @@ class FilterPage extends StatefulWidget { class FilterPageState extends State { Filter filter; - Map nodeControllers = {}; int selectedNodes = 0; final int maxSelectedNodes = 10; @@ -92,37 +90,26 @@ class FilterPageState extends State { final listItems = [const SizedBox(width: 10)]; for (final child in node.children) { - // Add option - nodeControllers.putIfAbsent(child, () => SelectableController()); - final controller = nodeControllers[child]; - listItems.add(Selectable( - label: child.localizedName(context), - initiallySelected: child.value, - controller: controller, - onSelected: (selection) { - if (selection && selectedNodes >= maxSelectedNodes) { - AppToast.show( - S.current.warningOnlyNOptionsAtATime(maxSelectedNodes)); - controller.deselect(); - return; - } - - level != 0 - ? _onSelected(selection, child) - : _onSelectedExclusive(selection, child, node.children); - }, - )); - child.addListener(() { - if (child.value) { - controller.select(); - } else { - controller.deselect(); - } - setState(() {}); - }); + listItems + // Add option + ..add(FilterChip( + label: Text(child.localizedName(context)), + selected: child.value, + showCheckmark: level != 0, + onSelected: (selection) { + if (selection && selectedNodes >= maxSelectedNodes && level != 0) { + AppToast.show( + S.current.warningOnlyNOptionsAtATime(maxSelectedNodes)); + return; + } - // Add padding - listItems.add(const SizedBox(width: 10)); + level != 0 + ? _onSelected(selection, child) + : _onSelectedExclusive(selection, child, node.children); + }, + )) + // Add padding + ..add(const SizedBox(width: 10)); } optionsByLevel[level].add( @@ -194,7 +181,7 @@ class FilterPageState extends State { child: Text( filter.localizedLevelNames[i] [LocaleProvider.localeString], - style: Theme.of(context).textTheme.headline6), + style: Theme.of(context).textTheme.subtitle1), )) // Level options ..addAll(optionsByLevel[i]); diff --git a/lib/pages/filter/view/relevance_picker.dart b/lib/pages/filter/view/relevance_picker.dart index 804f8e6c7..7142ca81f 100644 --- a/lib/pages/filter/view/relevance_picker.dart +++ b/lib/pages/filter/view/relevance_picker.dart @@ -4,13 +4,125 @@ import 'package:acs_upb_mobile/generated/l10n.dart'; import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; import 'package:acs_upb_mobile/pages/filter/view/filter_page.dart'; -import 'package:acs_upb_mobile/resources/custom_icons.dart'; -import 'package:acs_upb_mobile/widgets/selectable.dart'; +import 'package:acs_upb_mobile/resources/theme.dart'; +import 'package:acs_upb_mobile/widgets/chip_form_field.dart'; import 'package:acs_upb_mobile/widgets/toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; +class RelevanceFormField extends ChipFormField> { + RelevanceFormField({ + @required this.controller, + this.canBePrivate = true, + this.canBeForEveryone = true, + this.defaultPrivate = false, + Key key, + }) : super( + key: key, + icon: FeatherIcons.filter, + label: S.current.labelRelevance, + validator: (_) { + if (canBeForEveryone) { + // When the relevance can be for everyone, it's selected automatically + // if no custom relevance is selected; no error is possible. + return null; + } + if (controller.customRelevance?.isEmpty ?? true) { + return S.current.warningYouNeedToSelectAtLeastOne; + } + return null; + }, + trailingBuilder: (FormFieldState> state) { + return _customRelevanceButton( + state.context, controller, canBeForEveryone); + }, + contentBuilder: (FormFieldState> state) { + controller.onChanged = () { + state.didChange(controller.customRelevance); + }; + return _RelevancePicker( + defaultPrivate: defaultPrivate, + canBePrivate: canBePrivate, + canBeForEveryone: canBeForEveryone, + controller: controller, + ); + }, + ); + + final RelevanceController controller; + final bool canBePrivate; + final bool canBeForEveryone; + final bool defaultPrivate; + + static Widget _customRelevanceButton(BuildContext context, + RelevanceController controller, bool canBeForEveryone) { + final User user = Provider.of(context).currentUserFromCache; + final buttonColor = user?.canAddPublicInfo ?? false + ? Theme.of(context).accentColor + : Theme.of(context).hintColor; + + return IntrinsicWidth( + child: GestureDetector( + onTap: () { + if (user?.canAddPublicInfo ?? false) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ChangeNotifierProvider.value( + value: Provider.of(context), + child: FilterPage( + title: S.current.labelRelevance, + buttonText: S.current.buttonSet, + canBeForEveryone: canBeForEveryone, + info: + '${S.current.infoRelevanceNothingSelected} ${S.current.infoRelevance}', + hint: S.current.infoRelevanceExample, + onSubmit: () async { + // Deselect all other options + controller._state._onlyMeSelected = false; + controller._state._anyoneSelected = false; + + // Select the new options + await controller._state._fetchFilter(); + if (controller._state._filter.relevantLeaves + .contains('All')) { + controller._state._anyoneSelected = true; + } else { + for (final node + in controller._state._customSelected.keys) { + controller._state._customSelected[node] = true; + } + } + }, + ), + ), + ), + ); + } else { + AppToast.show(S.current.warningNoPermissionToAddPublicWebsite); + } + }, + child: Row( + children: [ + Text( + S.current.labelCustom, + style: Theme.of(context) + .accentTextTheme + .subtitle2 + .copyWith(color: buttonColor), + ), + Icon( + Icons.arrow_forward_ios_outlined, + color: buttonColor, + size: Theme.of(context).textTheme.subtitle2.fontSize, + ) + ], + ), + ), + ); + } +} + class RelevanceController { RelevanceController({this.onChanged}); @@ -20,20 +132,18 @@ class RelevanceController { String get degree => _state?._filter?.baseNode; bool get private => - _state?._onlyMeController?.isSelected ?? - _state?.widget?.defaultPrivate ?? - true; + _state?._onlyMeSelected ?? _state?.widget?.defaultPrivate ?? true; bool get anyone => - _state?._anyoneController?.isSelected ?? + _state?._anyoneSelected ?? _state?.widget != null && - _state.widget.filterProvider.defaultRelevance == null; + Provider.of(_state.context).defaultRelevance == null; List get customRelevance { final relevance = []; - if (_state?._customControllers != null) { - _state._customControllers.forEach((node, controller) { - if (controller.isSelected) { + if (_state?._customSelected != null) { + _state._customSelected.forEach((node, selected) { + if (selected) { relevance.add(node); } }); @@ -43,17 +153,14 @@ class RelevanceController { } } -class RelevancePicker extends StatefulWidget { - const RelevancePicker({ - @required this.filterProvider, +class _RelevancePicker extends StatefulWidget { + const _RelevancePicker({ this.canBePrivate = true, this.canBeForEveryone = true, - bool defaultPrivate, + bool defaultPrivate = false, this.controller, }) : defaultPrivate = (defaultPrivate ?? true) && canBePrivate; - final FilterProvider filterProvider; - /// Whether 'Only me' is an option (this overrides [defaultPrivate]) final bool canBePrivate; @@ -69,11 +176,10 @@ class RelevancePicker extends StatefulWidget { _RelevancePickerState createState() => _RelevancePickerState(); } -class _RelevancePickerState extends State { +class _RelevancePickerState extends State<_RelevancePicker> { // The three relevance options ("Only me", "Anyone" or an arbitrary list of nodes) are mutually exclusive - final _onlyMeController = SelectableController(); - final _anyoneController = SelectableController(); - Map _customControllers = {}; + bool _onlyMeSelected, _anyoneSelected; + Map _customSelected; User _user; Filter _filter; @@ -87,7 +193,8 @@ class _RelevancePickerState extends State { } Future _fetchFilter() async { - _filter = await widget.filterProvider.fetchFilter(); + _filter = + await Provider.of(context, listen: false).fetchFilter(); if (mounted) { setState(() {}); } @@ -98,115 +205,70 @@ class _RelevancePickerState extends State { super.initState(); _fetchUser(); _fetchFilter(); + _onlyMeSelected = widget.defaultPrivate ?? true; + _anyoneSelected = !widget.defaultPrivate && + Provider.of(context, listen: false).defaultRelevance == + null; + _customSelected = {}; } - Widget _customRelevanceButton() { - final buttonColor = _user?.canAddPublicInfo ?? false - ? Theme.of(context).accentColor - : Theme.of(context).hintColor; + bool get _canAddPublicInfo => _user?.canAddPublicInfo ?? false; - return IntrinsicWidth( - child: GestureDetector( - onTap: () { - if (_user?.canAddPublicInfo ?? false) { - Navigator.of(context) - .push(MaterialPageRoute( - builder: (_) => ChangeNotifierProvider.value( - value: widget.filterProvider, - child: FilterPage( - title: S.current.labelRelevance, - buttonText: S.current.buttonSet, - canBeForEveryone: widget.canBeForEveryone, - info: - '${S.current.infoRelevanceNothingSelected} ${S.current.infoRelevance}', - hint: S.current.infoRelevanceExample, - onSubmit: () async { - // Deselect all options - _onlyMeController.deselect(); - _anyoneController.deselect(); - - // Select the new options - await _fetchFilter(); - if (_filter.relevantLeaves.contains('All')) { - _anyoneController.select(); - } else { - for (final controller in _customControllers.values) { - controller.select(); - } - } - }, - ), - ), - )); - } else { - AppToast.show(S.current.warningNoPermissionToAddPublicWebsite); - } - }, - child: Row( - children: [ - Text( - S.current.labelCustom, - style: Theme.of(context) - .accentTextTheme - .subtitle2 - .copyWith(color: buttonColor), - ), - Icon( - Icons.arrow_forward_ios_outlined, - color: buttonColor, - size: Theme.of(context).textTheme.subtitle2.fontSize, - ) - ], - ), - ), - ); - } + bool get _somethingSelected { + if (_onlyMeSelected || _anyoneSelected) { + return true; + } - void _onCustomSelected(bool selected) => setState(() { - if (_user?.canAddPublicInfo ?? false) { - if (selected) { - _onlyMeController.deselect(); - _anyoneController.deselect(); - widget.controller?.onChanged(); - } - } else { - AppToast.show(S.current.warningNoPermissionToAddPublicWebsite); - } - }); + for (final node in _customSelected.keys) { + if (_customSelected[node]) { + return true; + } + } + return false; + } - Widget _customRelevanceSelectables() { + Widget _customRelevanceChips() { final widgets = []; - _customControllers = {}; // Add strings from the filter options for (final node in _filter?.relevantLocalizedLeaves(context) ?? []) { + // The "All" case (when nothing is selected in the filter) is handled + // separately using [_anyoneSelected] if (node != 'All') { - // The "All" case (when nothing is selected in the filter) is handled - // separately using [_anyoneController] - final controller = SelectableController(); - _customControllers[node] = controller; + if (!_customSelected.containsKey(node)) { + _customSelected[node] = + (!(_onlyMeSelected ?? false) && !(_anyoneSelected ?? false)) || + (!widget.canBePrivate && !widget.canBeForEveryone); + } widgets - ..add(Selectable( - label: node, - controller: controller, - initiallySelected: (!(_onlyMeController?.isSelected ?? false) && - !(_anyoneController?.isSelected ?? false)) || - (!widget.canBePrivate && !widget.canBeForEveryone), - onSelected: (selected) => setState(() { - if (_user?.canAddPublicInfo ?? false) { - if (selected) { - _onlyMeController.deselect(); - _anyoneController.deselect(); - } - if (widget.controller?.onChanged != null) { - widget.controller.onChanged(); - } - } else { + ..add(GestureDetector( + onTap: () { + if (!_canAddPublicInfo) { AppToast.show(S.current.warningNoPermissionToAddPublicWebsite); } - }), - disabled: !(_user?.canAddPublicInfo ?? false), + }, + child: FilterChip( + label: Text( + node, + style: Theme.of(context) + .chipTextStyle(selected: _customSelected[node]), + ), + selected: _customSelected[node], + onSelected: !_canAddPublicInfo + ? null + : (selected) => setState(() { + _customSelected[node] = selected; + if (selected) { + _onlyMeSelected = false; + _anyoneSelected = false; + } + + if (widget.controller?.onChanged != null) { + widget.controller.onChanged(); + } + }), + ), )) ..add(const SizedBox(width: 10)); } @@ -214,20 +276,45 @@ class _RelevancePickerState extends State { // Add the provided website relevance strings, if applicable // These are selected by default - for (final node in widget.filterProvider.defaultRelevance ?? []) { - if (!_customControllers.containsKey(node)) { - final controller = SelectableController(); - _customControllers[node] = controller; + final filterProvider = Provider.of(context); + for (final node in filterProvider.defaultRelevance ?? []) { + if (!_customSelected.containsKey(node)) { + _customSelected[node] = true; widgets ..add(const SizedBox(width: 10)) - ..add(Selectable( - label: node, - controller: controller, - initiallySelected: true, - onSelected: _onCustomSelected, - disabled: !(_user?.canAddPublicInfo ?? false), - )); + ..add( + GestureDetector( + onTap: () { + if (!_canAddPublicInfo) { + AppToast.show( + S.current.warningNoPermissionToAddPublicWebsite); + } + }, + child: FilterChip( + label: Text( + node, + style: Theme.of(context) + .chipTextStyle(selected: _customSelected[node]), + ), + selected: _customSelected[node], + onSelected: !_canAddPublicInfo + ? null + : (bool selected) => setState(() { + _customSelected[node] = selected; + if (selected) { + _onlyMeSelected = false; + _anyoneSelected = false; + widget.controller?.onChanged(); + } + + if (widget.controller?.onChanged != null) { + widget.controller.onChanged(); + } + }), + ), + ), + ); } } @@ -240,124 +327,84 @@ class _RelevancePickerState extends State { widget.controller._state = this; } - return Padding( - padding: const EdgeInsets.only(top: 12, left: 12), - child: Row( - children: [ - Icon(FeatherIcons.filter, - color: CustomIcons.formIconColor(Theme.of(context))), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - S.current.labelRelevance, - style: Theme.of(context) - .textTheme - .caption - .apply(color: Theme.of(context).hintColor), - ), - ), - _customRelevanceButton(), - ], - ), - const SizedBox(height: 10), - Row( - children: [ - Expanded( - child: Container( - height: 40, - child: ListView( - scrollDirection: Axis.horizontal, - children: [ - if (widget.canBePrivate) - Row( - children: [ - Selectable( - label: S.current.relevanceOnlyMe, - initiallySelected: - widget.defaultPrivate ?? true, - onSelected: (selected) => setState(() { - if (_user?.canAddPublicInfo ?? - false) { - if (selected) { - _anyoneController.deselect(); - for (final controller - in _customControllers - .values) { - controller.deselect(); - } - } else { - _anyoneController.select(); - } - } else { - _onlyMeController.select(); - } - widget.controller?.onChanged(); - }), - controller: _onlyMeController, - ), - const SizedBox(width: 10), - ], - ), - if (widget.canBeForEveryone) - Row( - children: [ - Selectable( - label: S.current.relevanceAnyone, - initiallySelected: - !widget.defaultPrivate && - widget.filterProvider - .defaultRelevance == - null, - onSelected: (selected) => setState(() { - if (_user?.canAddPublicInfo ?? - false) { - if (selected) { - // Deselect all controllers - _onlyMeController.deselect(); - for (final controller - in _customControllers - .values) { - controller.deselect(); - } - } else { - _onlyMeController.select(); - } - } else { - AppToast.show(S - .of(context) - .warningNoPermissionToAddPublicWebsite); - } - }), - controller: _anyoneController, - disabled: - !(_user?.canAddPublicInfo ?? false), - ), - const SizedBox(width: 10), - ], - ), - _customRelevanceSelectables(), - ], - ), - ), - ), - ], - ), - ], + return ListView( + scrollDirection: Axis.horizontal, + children: [ + if (widget.canBePrivate) + Row( + children: [ + ChoiceChip( + label: Text( + S.current.relevanceOnlyMe, + style: Theme.of(context) + .chipTextStyle(selected: _onlyMeSelected), ), - ], - ), + selected: _onlyMeSelected, + onSelected: (selected) => setState(() { + if (_user?.canAddPublicInfo ?? false) { + _onlyMeSelected = selected; + if (selected) { + _anyoneSelected = false; + for (final node in _customSelected.keys) { + _customSelected[node] = false; + } + } else { + _anyoneSelected = true; + } + } else { + _onlyMeSelected = true; + } + + if (widget.controller?.onChanged != null) { + widget.controller.onChanged(); + } + }), + ), + const SizedBox(width: 10), + ], ), - ], - ), + if (widget.canBeForEveryone) + Row( + children: [ + GestureDetector( + onTap: () { + if (!_canAddPublicInfo) { + AppToast.show( + S.current.warningNoPermissionToAddPublicWebsite); + } + }, + child: ChoiceChip( + label: Text( + S.current.relevanceAnyone, + style: Theme.of(context).chipTextStyle( + selected: !_somethingSelected || _anyoneSelected), + ), + selected: !_somethingSelected || _anyoneSelected, + onSelected: !_canAddPublicInfo + ? null + : (selected) => setState(() { + _anyoneSelected = selected; + if (selected) { + // Deselect all other options + _onlyMeSelected = false; + for (final node in _customSelected.keys) { + _customSelected[node] = false; + } + } else { + _onlyMeSelected = true; + } + + if (widget.controller?.onChanged != null) { + widget.controller.onChanged(); + } + }), + ), + ), + const SizedBox(width: 10), + ], + ), + _customRelevanceChips(), + ], ); } } diff --git a/lib/pages/portal/view/portal_page.dart b/lib/pages/portal/view/portal_page.dart index f3baa6dda..9259964be 100644 --- a/lib/pages/portal/view/portal_page.dart +++ b/lib/pages/portal/view/portal_page.dart @@ -87,14 +87,15 @@ class _PortalPageState extends State { if (canEdit) { Navigator.of(context) .push(MaterialPageRoute( - builder: (_) => ChangeNotifierProvider( - create: (_) => - Platform.environment.containsKey('FLUTTER_TEST') - ? Provider.of(context) - : FilterProvider( - defaultDegree: website.degree, - defaultRelevance: website.relevance, - ), + builder: (_) => ChangeNotifierProvider.value( + // If testing, use the global (mocked) provider; otherwise instantiate a new local provider + value: Platform.environment.containsKey('FLUTTER_TEST') + ? Provider.of(context) + : FilterProvider( + defaultDegree: website.degree, + defaultRelevance: website.relevance, + ) + ..updateAuth(Provider.of(context)), child: WebsiteView( website: website, updateExisting: true, @@ -348,15 +349,12 @@ class _AddWebsiteButton extends StatelessWidget { if (authProvider.isAuthenticated && !authProvider.isAnonymous) { Navigator.of(context) .push(MaterialPageRoute( - builder: (_) => - ChangeNotifierProxyProvider( - create: (_) => - Platform.environment.containsKey('FLUTTER_TEST') - ? Provider.of(context) - : FilterProvider(), - update: (context, authProvider, filterProvider) { - return filterProvider..updateAuth(authProvider); - }, + builder: (_) => ChangeNotifierProvider.value( + // If testing, use the global (mocked) provider; otherwise instantiate a new local provider + value: Platform.environment.containsKey('FLUTTER_TEST') + ? Provider.of(context) + : FilterProvider() + ..updateAuth(authProvider), child: WebsiteView( website: Website( relevance: null, diff --git a/lib/pages/portal/view/website_view.dart b/lib/pages/portal/view/website_view.dart index 1817a8b8a..bbe0c045d 100644 --- a/lib/pages/portal/view/website_view.dart +++ b/lib/pages/portal/view/website_view.dart @@ -1,13 +1,12 @@ import 'package:acs_upb_mobile/authentication/model/user.dart'; import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; import 'package:acs_upb_mobile/generated/l10n.dart'; -import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; import 'package:acs_upb_mobile/pages/filter/view/relevance_picker.dart'; import 'package:acs_upb_mobile/pages/portal/model/website.dart'; import 'package:acs_upb_mobile/pages/portal/service/website_provider.dart'; -import 'package:acs_upb_mobile/resources/custom_icons.dart'; import 'package:acs_upb_mobile/resources/locale_provider.dart'; import 'package:acs_upb_mobile/resources/storage/storage_provider.dart'; +import 'package:acs_upb_mobile/resources/theme.dart'; import 'package:acs_upb_mobile/resources/utils.dart'; import 'package:acs_upb_mobile/widgets/button.dart'; import 'package:acs_upb_mobile/widgets/circle_image.dart'; @@ -120,8 +119,10 @@ class _WebsiteViewState extends State { padding: const EdgeInsets.all(12), child: Row( children: [ - Icon(Icons.remove_red_eye_outlined, - color: CustomIcons.formIconColor(Theme.of(context))), + Icon( + Icons.remove_red_eye_outlined, + color: Theme.of(context).formIconColor, + ), const SizedBox(width: 12), AutoSizeText( '${S.current.labelPreview}:', @@ -282,8 +283,9 @@ class _WebsiteViewState extends State { }, onChanged: (_) => setState(() {}), ), - RelevancePicker( - filterProvider: Provider.of(context), + RelevanceFormField( + canBePrivate: true, + canBeForEveryone: true, defaultPrivate: widget.website?.isPrivate ?? true, controller: _relevanceController, ), diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index a61ed64d1..a7608f0ac 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -4,7 +4,6 @@ import 'package:acs_upb_mobile/generated/l10n.dart'; import 'package:acs_upb_mobile/navigation/routes.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; -import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; import 'package:acs_upb_mobile/pages/filter/view/relevance_picker.dart'; import 'package:acs_upb_mobile/pages/people/model/person.dart'; import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; @@ -15,12 +14,12 @@ import 'package:acs_upb_mobile/pages/timetable/model/events/class_event.dart'; import 'package:acs_upb_mobile/pages/timetable/model/events/recurring_event.dart'; import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; -import 'package:acs_upb_mobile/resources/custom_icons.dart'; import 'package:acs_upb_mobile/resources/locale_provider.dart'; +import 'package:acs_upb_mobile/resources/theme.dart'; import 'package:acs_upb_mobile/widgets/button.dart'; +import 'package:acs_upb_mobile/widgets/chip_form_field.dart'; import 'package:acs_upb_mobile/widgets/dialog.dart'; import 'package:acs_upb_mobile/widgets/scaffold.dart'; -import 'package:acs_upb_mobile/widgets/selectable.dart'; import 'package:acs_upb_mobile/widgets/toast.dart'; import 'package:dotted_line/dotted_line.dart'; import 'package:flutter/material.dart'; @@ -254,14 +253,9 @@ class _AddEventViewState extends State { ], ), RelevanceFormField( + canBePrivate: false, + canBeForEveryone: false, controller: relevanceController, - validator: (_) { - if (relevanceController.customRelevance?.isEmpty ?? - true) { - return S.current.warningYouNeedToSelectAtLeastOne; - } - return null; - }, ), DropdownButtonFormField( decoration: InputDecoration( @@ -334,40 +328,25 @@ class _AddEventViewState extends State { onChanged: (_) => setState(() {}), ), timeIntervalPicker(), + Divider( + thickness: 0.7, + color: Theme.of(context).hintColor, + ), if (weekSelected[WeekType.odd] != null && weekSelected[WeekType.even] != null) - SelectableFormField( + FilterChipFormField( key: const ValueKey('week_picker'), icon: FeatherIcons.calendar, label: S.current.labelWeek, initialValues: weekSelected, - validator: (selection) { - if (selection.values - .where((e) => e != false) - .isEmpty) { - return S - .of(context) - .warningYouNeedToSelectAtLeastOne; - } - return null; - }, ), - SelectableFormField( + FilterChipFormField( key: const ValueKey('day_picker'), icon: Icons.today_outlined, label: S.current.labelDay, initialValues: weekDaySelected, - validator: (selection) { - if (selection.values - .where((e) => e != false) - .isEmpty) { - return S - .of(context) - .warningYouNeedToSelectAtLeastOne; - } - return null; - }, ), + const SizedBox(height: 16), ], ), const SizedBox(width: 16), @@ -484,7 +463,7 @@ class _AddEventViewState extends State { padding: const EdgeInsets.all(12), child: Icon( FeatherIcons.clock, - color: CustomIcons.formIconColor(Theme.of(context)), + color: Theme.of(context).formIconColor, ), ), TextButton( @@ -551,147 +530,6 @@ class _AddEventViewState extends State { } } -class RelevanceFormField extends FormField> { - RelevanceFormField({ - @required this.controller, - String Function(List) validator, - Key key, - }) : super( - key: key, - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: validator, - builder: (FormFieldState> state) { - controller.onChanged = () { - state.didChange(controller.customRelevance); - }; - final context = state.context; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RelevancePicker( - canBePrivate: false, - canBeForEveryone: false, - filterProvider: Provider.of(context), - controller: controller, - ), - if (state.hasError) - Padding( - padding: const EdgeInsets.only(top: 10), - child: Text( - state.errorText, - style: Theme.of(context) - .textTheme - .caption - .copyWith(color: Theme.of(context).errorColor), - ), - ), - ], - ); - }, - ); - - final RelevanceController controller; -} - -class SelectableFormField extends FormField> { - SelectableFormField({ - @required Map initialValues, - @required IconData icon, - @required String label, - String Function(Map) validator, - Key key, - }) : super( - autovalidateMode: AutovalidateMode.onUserInteraction, - initialValue: initialValues, - key: key, - validator: validator, - builder: (state) { - final context = state.context; - final labels = state.value.keys.toList(); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.only(top: 12, left: 12), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, - color: - CustomIcons.formIconColor(Theme.of(context))), - const SizedBox(width: 12), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - label, - style: Theme.of(context) - .textTheme - .caption - .apply( - color: Theme.of(context).hintColor), - ), - ), - const SizedBox(height: 10), - Row( - children: [ - Expanded( - child: Container( - height: 40, - child: ListView.builder( - itemCount: labels.length, - scrollDirection: Axis.horizontal, - itemBuilder: (context, index) { - return Row( - children: [ - Selectable( - label: labels[index] - .toLocalizedString(), - initiallySelected: - state.value[labels[index]], - onSelected: (selected) { - state.value[labels[index]] = - selected; - state.didChange(state.value); - }, - ), - const SizedBox(width: 10), - ], - ); - }, - ), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ), - if (state.hasError) - Padding( - padding: const EdgeInsets.only(top: 10), - child: Text( - state.errorText, - style: Theme.of(context) - .textTheme - .caption - .copyWith(color: Theme.of(context).errorColor), - ), - ), - ], - ); - }, - ); -} - class _DayOfWeek extends time_machine.DayOfWeek with Localizable { const _DayOfWeek(int value) : super(value); diff --git a/lib/resources/custom_icons.dart b/lib/resources/custom_icons.dart index a46c90912..d43d17eed 100644 --- a/lib/resources/custom_icons.dart +++ b/lib/resources/custom_icons.dart @@ -32,15 +32,4 @@ class CustomIcons { // Transparent icon to be used as a placeholder static const Icon empty = Icon(Icons.cancel_outlined, color: Color(0x00000000)); - - static Color formIconColor(ThemeData themeData) { - switch (themeData.brightness) { - case Brightness.dark: - return Colors.white70; - case Brightness.light: - return Colors.black45; - default: - return themeData.iconTheme.color; - } - } } diff --git a/lib/resources/theme.dart b/lib/resources/theme.dart new file mode 100644 index 000000000..7ba653a93 --- /dev/null +++ b/lib/resources/theme.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +extension ThemeExtension on ThemeData { + TextStyle chipTextStyle({@required bool selected}) => TextStyle( + color: selected + ? brightness == Brightness.light + ? accentColor + : Colors.white + : textTheme.bodyText2.color, + fontWeight: selected ? FontWeight.bold : FontWeight.normal, + ); + + Color get formIconColor { + switch (brightness) { + case Brightness.dark: + return Colors.white70; + case Brightness.light: + return Colors.black45; + default: + return iconTheme.color; + } + } +} diff --git a/lib/widgets/chip_form_field.dart b/lib/widgets/chip_form_field.dart new file mode 100644 index 000000000..a45a4f455 --- /dev/null +++ b/lib/widgets/chip_form_field.dart @@ -0,0 +1,123 @@ +import 'package:acs_upb_mobile/generated/l10n.dart'; +import 'package:acs_upb_mobile/resources/locale_provider.dart'; +import 'package:acs_upb_mobile/resources/theme.dart'; +import 'package:flutter/material.dart'; + +class FilterChipFormField extends ChipFormField> { + FilterChipFormField({ + @required Map initialValues, + @required IconData icon, + @required String label, + Key key, + }) : super( + key: key, + icon: icon, + label: label, + initialValues: initialValues, + validator: (selection) { + if (selection.values.where((e) => e != false).isEmpty) { + return S.current.warningYouNeedToSelectAtLeastOne; + } + return null; + }, + contentBuilder: (state) { + final labels = state.value.keys.toList(); + return ListView.builder( + itemCount: labels.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + return Row( + children: [ + FilterChip( + label: Text( + labels[index].toLocalizedString(), + style: Theme.of(context).chipTextStyle( + selected: state.value[labels[index]]), + ), + selected: state.value[labels[index]], + onSelected: (selected) { + state.value[labels[index]] = selected; + state.didChange(state.value); + }, + ), + const SizedBox(width: 10), + ], + ); + }, + ); + }, + ); +} + +class ChipFormField extends FormField { + ChipFormField({ + @required IconData icon, + @required String label, + @required Widget Function(FormFieldState state) contentBuilder, + Widget Function(FormFieldState) trailingBuilder, + T initialValues, + String Function(T) validator, + Key key, + }) : super( + autovalidateMode: AutovalidateMode.onUserInteraction, + initialValue: initialValues, + key: key, + validator: validator, + builder: (state) { + final context = state.context; + return Padding( + padding: const EdgeInsets.only(top: 12), + child: IntrinsicHeight( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Row( + children: [ + const SizedBox(width: 12), + Icon(icon, color: Theme.of(context).formIconColor), + const SizedBox(width: 12), + Text( + label, + style: Theme.of(context) + .textTheme + .subtitle1 + .copyWith(fontWeight: FontWeight.w400), + ), + Expanded(child: Container()), + if (trailingBuilder != null) trailingBuilder(state), + ], + ), + ), + const SizedBox(height: 10), + Row( + children: [ + const SizedBox(width: 12), + Expanded( + child: Container( + height: 40, + child: contentBuilder(state), + ), + ), + ], + ), + const SizedBox(height: 8), + Divider( + thickness: 0.7, + color: state.hasError + ? Theme.of(context).errorColor + : Theme.of(context).hintColor), + if (state.hasError) + Text( + state.errorText, + style: Theme.of(context).textTheme.caption.copyWith( + color: Theme.of(context).errorColor.withOpacity(1)), + ), + ], + ), + ), + ); + }, + ); +} diff --git a/pubspec.lock b/pubspec.lock index 713b2771e..78a3a8f7d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -78,27 +78,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0-nullsafety.3" - build: - dependency: transitive - description: - name: build - url: "https://pub.dartlang.org" - source: hosted - version: "1.6.2" - built_collection: - dependency: transitive - description: - name: built_collection - url: "https://pub.dartlang.org" - source: hosted - version: "4.3.2" - built_value: - dependency: transitive - description: - name: built_value - url: "https://pub.dartlang.org" - source: hosted - version: "7.1.0" cached_network_image: dependency: "direct main" description: @@ -155,13 +134,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.1+2" - code_builder: - dependency: transitive - description: - name: code_builder - url: "https://pub.dartlang.org" - source: hosted - version: "3.7.0" collection: dependency: transitive description: @@ -372,13 +344,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.1+1" - fixnum: - dependency: transitive - description: - name: fixnum - url: "https://pub.dartlang.org" - source: hosted - version: "0.10.11" flutter: dependency: "direct main" description: flutter @@ -608,7 +573,7 @@ packages: name: mockito url: "https://pub.dartlang.org" source: hosted - version: "4.1.4" + version: "4.1.1+1" nested: dependency: transitive description: @@ -622,7 +587,7 @@ packages: name: network_image_mock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.0.2" node_interop: dependency: transitive description: @@ -894,13 +859,6 @@ packages: description: flutter source: sdk version: "0.0.99" - source_gen: - dependency: transitive - description: - name: source_gen - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.10+2" source_span: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b3d8cce0c..77b50a17f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ description: A mobile application for students at ACS UPB. # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # # ACS UPB Mobile uses semantic versioning. You can read more in the CONTRIBUTING.md file. -version: 1.2.12+18 +version: 1.2.12+19 environment: sdk: ">=2.7.0 <3.0.0" From 8c38b7c069dc4129629cc143dfdf33feaa00d8ac Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Fri, 23 Jul 2021 11:21:34 +0300 Subject: [PATCH 14/60] Bump patch version for release. --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 77b50a17f..67cbb0458 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ description: A mobile application for students at ACS UPB. # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # # ACS UPB Mobile uses semantic versioning. You can read more in the CONTRIBUTING.md file. -version: 1.2.12+19 +version: 1.2.13+19 environment: sdk: ">=2.7.0 <3.0.0" From dd07aea1239d7d01af6f45d3841d90f8fbebc4ba Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Fri, 23 Jul 2021 11:36:22 +0300 Subject: [PATCH 15/60] Update gradle to fix failing release. https://stackoverflow.com/questions/68255521/could-not-get-https-google-bintray-com-maven-metadata-xml-received-stat --- android/build.gradle | 2 +- android/gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 93db55e10..a407870dc 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:4.2.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.gms:google-services:4.3.3' // Google Services plugin } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 296b146b7..939efa295 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip From aca8e977d05e34043866f1f9018b4566ae00a4b6 Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Fri, 23 Jul 2021 14:48:18 +0300 Subject: [PATCH 16/60] Fix GH release failing. --- .github/workflows/deploy.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c3cf38af1..4820e1088 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -73,7 +73,12 @@ jobs: env: ANDROID_KEYS_SECRET_PASSPHRASE: ${{ secrets.ANDROID_KEYS_SECRET_PASSPHRASE }} - - name: Build APK + # Apparently there's a known issue where there are some missing files if you don't build the debug version first + # https://techshits.com/flutter-error-transforms-input-file-does-not-exist/ + - name: Build debug APK + run: flutter build apk --debug + + - name: Build release APK run: flutter build apk --release - name: Create a Release APK From 50966012a8eef8b194feb214a405f8b7a8688f4d Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Fri, 23 Jul 2021 15:12:35 +0300 Subject: [PATCH 17/60] Fix GH release failing (take #2). --- .github/workflows/deploy.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4820e1088..4291ba738 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -73,10 +73,12 @@ jobs: env: ANDROID_KEYS_SECRET_PASSPHRASE: ${{ secrets.ANDROID_KEYS_SECRET_PASSPHRASE }} - # Apparently there's a known issue where there are some missing files if you don't build the debug version first + # Apparently there's a known issue where there are some missing files if you don't build the debug/profile version first # https://techshits.com/flutter-error-transforms-input-file-does-not-exist/ - name: Build debug APK run: flutter build apk --debug + - name: Build profile APK + run: flutter build apk --profile - name: Build release APK run: flutter build apk --release From 9e47db5e45840f9c3a9e5d5b8b07e63d4f783e18 Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Fri, 23 Jul 2021 15:19:18 +0300 Subject: [PATCH 18/60] Fix some typos in CONTRIBUTING.md. --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fa2bee845..145f6a636 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,9 +56,9 @@ auditore/fix_md_typo torvalds/android_speedups ``` ### Merging -When developing a new feature or working on a bug, your pull request will end up containing fixup commits (commits that change the same line of code repeatedly) or too fine-grained commits. An issue that can arise from this is that the main branch history will become poluted with unnecessary commits. To avoid it, we implement and enforce a squash policy. +When developing a new feature or working on a bug, your pull request will end up containing fix-up commits (commits that change the same line of code repeatedly) or too fine-grained commits. An issue that can arise from this is that the main branch history will become polluted with unnecessary commits. To avoid it, we implement and enforce a squash policy. All commits that are merged into the main development branch have to be squashed ahead of the merge. -You can do so by pressing "squash and merge" in GitHub (_recommended_), or, alternetively, following the generic local squash routine outlined bellow: +You can do so by pressing "squash and merge" in GitHub (_recommended_), or, alternatively, following the generic local squash routine outlined bellow: ``` git checkout your_branch_name git rebase -i HEAD~n @@ -239,7 +239,7 @@ A user can define their own websites, that only they have access to. These will Anyone can **create** a new user (a new document in this collection) _if the `permissionLevel` of the created user is 0, null or not set at all_. -Authenticated users can only **read**, **delete** and **update** their own document (including its subcollections) and no one else's. However, they cannot modify the `permissionLevel` field. +Authenticated users can only **read**, **delete** and **update** their own document (including its sub-collections) and no one else's. However, they cannot modify the `permissionLevel` field. From 18cfe597b6860cf36ccfc4d0d3467435fcc8663d Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Tue, 17 Aug 2021 17:16:22 +0300 Subject: [PATCH 19/60] Uncommented code, fixed warnings --- .../timetable/model/academic_calendar.dart | 160 ++-- .../timetable/model/events/all_day_event.dart | 110 +-- .../timetable/model/events/class_event.dart | 89 +- .../model/events/recurring_event.dart | 255 +++--- .../timetable/model/events/uni_event.dart | 426 +++++----- .../service/google_calendar_services.dart | 329 +++---- .../timetable/service/uni_event_provider.dart | 802 +++++++++--------- lib/pages/timetable/view/date_header.dart | 144 ++-- lib/pages/timetable/view/timetable_page.dart | 665 +++++++-------- 9 files changed, 1493 insertions(+), 1487 deletions(-) diff --git a/lib/pages/timetable/model/academic_calendar.dart b/lib/pages/timetable/model/academic_calendar.dart index e2690ac22..a39cc1f5d 100644 --- a/lib/pages/timetable/model/academic_calendar.dart +++ b/lib/pages/timetable/model/academic_calendar.dart @@ -1,80 +1,80 @@ -//import 'package:acs_upb_mobile/pages/timetable/model/events/all_day_event.dart'; -//import 'package:acs_upb_mobile/resources/utils.dart'; -//import 'package:flutter/foundation.dart'; -//import 'package:time_machine/time_machine.dart'; -// -//class AcademicCalendar { -// AcademicCalendar( -// {@required this.id, -// this.semesters = const [], -// this.holidays = const [], -// this.exams = const []}); -// -// String id; -// List semesters; -// List holidays; -// List exams; -// -// Map> _getWeeksByYearInInterval(DateInterval interval) { -// final Map> weeksByYear = {}; -// final rule = WeekYearRules.iso; -// -// final int firstWeek = rule.getWeekOfWeekYear(interval.start); -// final int lastWeek = rule.getWeekOfWeekYear(interval.end); -// -// if (interval.start.year == interval.end.year) { -// weeksByYear[interval.start.year] = range(firstWeek, lastWeek + 1).toSet(); -// } else { -// weeksByYear[interval.start.year] = range( -// firstWeek, -// rule.getWeeksInWeekYear(interval.start.year, CalendarSystem.iso) + -// 1) -// .toSet(); -// weeksByYear[interval.end.year] = range(1, lastWeek + 1).toSet(); -// } -// -// return weeksByYear; -// } -// -// Set get nonHolidayWeeks { -// final Map> weeksByYear = {}; -// final rule = WeekYearRules.iso; -// -// for (final semester in semesters) { -// for (final entry in _getWeeksByYearInInterval( -// DateInterval(semester.startDate, semester.endDate)) -// .entries) { -// weeksByYear[entry.key] ??= {}; -// weeksByYear[entry.key].addAll(entry.value); -// } -// } -// -// for (final holiday in holidays) { -// final DateInterval holidayInterval = -// DateInterval(holiday.startDate, holiday.endDate); -// final Map> holidayWeeksByYear = -// _getWeeksByYearInInterval(holidayInterval); -// -// for (final entry in holidayWeeksByYear.entries) { -// final int year = entry.key; -// final Set weeks = entry.value; -// -// for (final week in weeks) { -// final LocalDate monday = rule.getLocalDate( -// year, week, DayOfWeek.monday, CalendarSystem.iso); -// final LocalDate friday = rule.getLocalDate( -// year, week, DayOfWeek.friday, CalendarSystem.iso); -// -// // If the holiday includes Monday to Friday in a week, exclude week -// // number from [nonHolidayWeeks]. -// if (holidayInterval.contains(monday) && -// holidayInterval.contains(friday)) { -// weeksByYear[year].remove(week); -// } -// } -// } -// } -// -// return weeksByYear.values.expand((e) => e).toSet(); -// } -//} +import 'package:acs_upb_mobile/pages/timetable/model/events/all_day_event.dart'; +import 'package:acs_upb_mobile/resources/utils.dart'; +import 'package:flutter/foundation.dart'; +import 'package:time_machine/time_machine.dart'; + +class AcademicCalendar { + AcademicCalendar( + {@required this.id, + this.semesters = const [], + this.holidays = const [], + this.exams = const []}); + + String id; + List semesters; + List holidays; + List exams; + + Map> _getWeeksByYearInInterval(DateInterval interval) { + final Map> weeksByYear = {}; + final rule = WeekYearRules.iso; + + final int firstWeek = rule.getWeekOfWeekYear(interval.start); + final int lastWeek = rule.getWeekOfWeekYear(interval.end); + + if (interval.start.year == interval.end.year) { + weeksByYear[interval.start.year] = range(firstWeek, lastWeek + 1).toSet(); + } else { + weeksByYear[interval.start.year] = range( + firstWeek, + rule.getWeeksInWeekYear(interval.start.year, CalendarSystem.iso) + + 1) + .toSet(); + weeksByYear[interval.end.year] = range(1, lastWeek + 1).toSet(); + } + + return weeksByYear; + } + + Set get nonHolidayWeeks { + final Map> weeksByYear = {}; + final rule = WeekYearRules.iso; + + for (final semester in semesters) { + for (final entry in _getWeeksByYearInInterval( + DateInterval(semester.startDate, semester.endDate)) + .entries) { + weeksByYear[entry.key] ??= {}; + weeksByYear[entry.key].addAll(entry.value); + } + } + + for (final holiday in holidays) { + final DateInterval holidayInterval = + DateInterval(holiday.startDate, holiday.endDate); + final Map> holidayWeeksByYear = + _getWeeksByYearInInterval(holidayInterval); + + for (final entry in holidayWeeksByYear.entries) { + final int year = entry.key; + final Set weeks = entry.value; + + for (final week in weeks) { + final LocalDate monday = rule.getLocalDate( + year, week, DayOfWeek.monday, CalendarSystem.iso); + final LocalDate friday = rule.getLocalDate( + year, week, DayOfWeek.friday, CalendarSystem.iso); + + // If the holiday includes Monday to Friday in a week, exclude week + // number from [nonHolidayWeeks]. + if (holidayInterval.contains(monday) && + holidayInterval.contains(friday)) { + weeksByYear[year].remove(week); + } + } + } + } + + return weeksByYear.values.expand((e) => e).toSet(); + } +} diff --git a/lib/pages/timetable/model/events/all_day_event.dart b/lib/pages/timetable/model/events/all_day_event.dart index 023b8adb1..e2bfb3a72 100644 --- a/lib/pages/timetable/model/events/all_day_event.dart +++ b/lib/pages/timetable/model/events/all_day_event.dart @@ -1,54 +1,56 @@ -//import 'package:acs_upb_mobile/pages/classes/model/class.dart'; -//import 'package:acs_upb_mobile/pages/timetable/model/academic_calendar.dart'; -//import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -//import 'package:flutter/material.dart'; -//import 'package:time_machine/time_machine.dart'; -// -//class AllDayUniEvent extends UniEvent { -// AllDayUniEvent({ -// @required LocalDate start, -// @required LocalDate end, -// @required String id, -// String name, -// String location, -// Color color, -// UniEventType type, -// ClassHeader classHeader, -// AcademicCalendar calendar, -// List relevance, -// String degree, -// String addedBy, -// bool editable, -// }) : startDate = start, -// endDate = end, -// super( -// name: name, -// location: location, -// start: start.atMidnight(), -// duration: Period.differenceBetweenDates(start, end.addDays(1)), -// id: id, -// color: color, -// type: type, -// classHeader: classHeader, -// calendar: calendar, -// relevance: relevance, -// degree: degree, -// addedBy: addedBy, -// editable: editable); -// -// LocalDate startDate; -// LocalDate endDate; -// -// @override -// Iterable generateInstances( -// {DateInterval intersectingInterval}) sync* { -// yield UniEventInstance( -// id: id, -// title: name, -// mainEvent: this, -// start: startDate.atMidnight(), -// end: endDate.addDays(1).atMidnight(), -// color: color, -// ); -// } -//} +import 'package:flutter/material.dart'; +import 'package:time_machine/time_machine.dart'; + +import '../../../classes/model/class.dart'; +import '../academic_calendar.dart'; +import 'uni_event.dart'; + +class AllDayUniEvent extends UniEvent { + AllDayUniEvent({ + @required LocalDate start, + @required LocalDate end, + @required String id, + String name, + String location, + Color color, + UniEventType type, + ClassHeader classHeader, + AcademicCalendar calendar, + List relevance, + String degree, + String addedBy, + bool editable, + }) + : startDate = start, + endDate = end, + super( + name: name, + location: location, + start: start.atMidnight(), + duration: Period.differenceBetweenDates(start, end.addDays(1)), + id: id, + color: color, + type: type, + classHeader: classHeader, + calendar: calendar, + relevance: relevance, + degree: degree, + addedBy: addedBy, + editable: editable); + + LocalDate startDate; + LocalDate endDate; + + @override + Iterable generateInstances( + {DateInterval intersectingInterval}) sync* { + yield UniEventInstance( + id: id, + title: name, + mainEvent: this, + start: startDate.atMidnight(), + end: endDate.addDays(1).atMidnight(), + color: color, + ); + } +} diff --git a/lib/pages/timetable/model/events/class_event.dart b/lib/pages/timetable/model/events/class_event.dart index f5d8f0acf..4e1a1e175 100644 --- a/lib/pages/timetable/model/events/class_event.dart +++ b/lib/pages/timetable/model/events/class_event.dart @@ -1,44 +1,45 @@ -//import 'package:acs_upb_mobile/pages/classes/model/class.dart'; -//import 'package:acs_upb_mobile/pages/people/model/person.dart'; -//import 'package:acs_upb_mobile/pages/timetable/model/academic_calendar.dart'; -//import 'package:acs_upb_mobile/pages/timetable/model/events/recurring_event.dart'; -//import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -//import 'package:flutter/cupertino.dart'; -//import 'package:rrule/rrule.dart'; -//import 'package:time_machine/time_machine.dart'; -// -//class ClassEvent extends RecurringUniEvent { -// const ClassEvent({ -// @required this.teacher, -// @required RecurrenceRule rrule, -// @required LocalDateTime start, -// @required Period duration, -// @required String id, -// List relevance, -// String degree, -// String name, -// String location, -// Color color, -// UniEventType type, -// ClassHeader classHeader, -// AcademicCalendar calendar, -// String addedBy, -// bool editable, -// }) : super( -// rrule: rrule, -// name: name, -// location: location, -// start: start, -// duration: duration, -// degree: degree, -// relevance: relevance, -// id: id, -// color: color, -// type: type, -// classHeader: classHeader, -// calendar: calendar, -// addedBy: addedBy, -// editable: editable); -// -// final Person teacher; -//} +import 'package:flutter/cupertino.dart'; +import 'package:rrule/rrule.dart'; +import 'package:time_machine/time_machine.dart'; + +import '../../../classes/model/class.dart'; +import '../../../people/model/person.dart'; +import '../academic_calendar.dart'; +import 'recurring_event.dart'; +import 'uni_event.dart'; + +class ClassEvent extends RecurringUniEvent { + const ClassEvent({ + @required this.teacher, + @required RecurrenceRule rrule, + @required LocalDateTime start, + @required Period duration, + @required String id, + List relevance, + String degree, + String name, + String location, + Color color, + UniEventType type, + ClassHeader classHeader, + AcademicCalendar calendar, + String addedBy, + bool editable, + }) : super( + rrule: rrule, + name: name, + location: location, + start: start, + duration: duration, + degree: degree, + relevance: relevance, + id: id, + color: color, + type: type, + classHeader: classHeader, + calendar: calendar, + addedBy: addedBy, + editable: editable); + + final Person teacher; +} diff --git a/lib/pages/timetable/model/events/recurring_event.dart b/lib/pages/timetable/model/events/recurring_event.dart index 8bc902919..8f3baa520 100644 --- a/lib/pages/timetable/model/events/recurring_event.dart +++ b/lib/pages/timetable/model/events/recurring_event.dart @@ -1,127 +1,128 @@ -//import 'package:acs_upb_mobile/pages/classes/model/class.dart'; -//import 'package:acs_upb_mobile/pages/timetable/model/academic_calendar.dart'; -//import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -//import 'package:acs_upb_mobile/resources/locale_provider.dart'; -//import 'package:acs_upb_mobile/resources/utils.dart'; -//import 'package:flutter/material.dart'; -//import 'package:rrule/rrule.dart'; -//import 'package:time_machine/time_machine.dart'; -// -//class RecurringUniEvent extends UniEvent { -// const RecurringUniEvent({ -// @required this.rrule, -// @required LocalDateTime start, -// @required Period duration, -// @required String id, -// List relevance, -// String degree, -// String name, -// String location, -// Color color, -// UniEventType type, -// ClassHeader classHeader, -// AcademicCalendar calendar, -// String addedBy, -// bool editable, -// }) : assert(rrule != null), -// super( -// name: name, -// location: location, -// start: start, -// duration: duration, -// degree: degree, -// relevance: relevance, -// id: id, -// color: color, -// type: type, -// classHeader: classHeader, -// calendar: calendar, -// addedBy: addedBy, -// editable: editable); -// -// final RecurrenceRule rrule; -// -// @override -// String get info { -// if (LocaleProvider.rruleL10n != null) { -// return rrule.toText(l10n: LocaleProvider.rruleL10n); -// } -// return ''; -// } -// -// RecurrenceRule get rruleBasedOnCalendar { -// final RecurrenceRule rrule = this.rrule; -// if (calendar != null && rrule.frequency == Frequency.weekly) { -// var weeks = calendar.nonHolidayWeeks; -// -// // Get the correct sequence of weeks for this event. -// // -// // For example, if the first academic calendar week is 40 and the event -// // starts on week 41 and repeats every two weeks - get every odd-index -// // week in the non holiday weeks. -// // This is necessary because if an "even" week is followed by a one-week -// // holiday, the week that comes after the holiday should be considered -// // an "odd" week, even though its number in the calendar would have the -// // same parity as the week before the holiday. -// if (rrule.interval != 1) { -// // Check whether the first calendar week is odd -// final bool startOdd = weeks.first % 2 == 1; -// weeks = weeks -// .whereIndex((index) => -// (startOdd ? index : index + 1) % rrule.interval != -// weeks.lookup(WeekYearRules.iso -// .getWeekOfWeekYear(start.calendarDate)) % -// rrule.interval) -// .toSet(); -// } -// return rrule.copyWith( -// frequency: Frequency.yearly, -// interval: 1, -// byWeekDays: rrule.byWeekDays.isNotEmpty -// ? rrule.byWeekDays -// : {ByWeekDayEntry(start.dayOfWeek)}, -// byWeeks: weeks); -// } -// return rrule; -// } -// -// @override -// Iterable generateInstances( -// {DateInterval intersectingInterval}) sync* { -// final RecurrenceRule rrule = rruleBasedOnCalendar; -// -// // Calculate recurrences -// int i = 0; -// for (final start in rrule.getInstances(start: start)) { -// final LocalDateTime end = start.add(duration); -// if (intersectingInterval != null) { -// if (end.calendarDate < intersectingInterval.start) continue; -// if (start.calendarDate > intersectingInterval.end) break; -// } -// -// bool skip = false; -// for (final holiday in calendar?.holidays ?? []) { -// final holidayInterval = -// DateInterval(holiday.startDate, holiday.endDate); -// if (holidayInterval.contains(start.calendarDate)) { -// // Skip holidays -// skip = true; -// } -// } -// -// if (!skip) { -// yield UniEventInstance( -// id: '$id-$i', -// title: name, -// mainEvent: this, -// color: color, -// start: start, -// end: end, -// location: location, -// ); -// } -// -// i++; -// } -// } -//} +import 'package:flutter/material.dart'; +import 'package:rrule/rrule.dart'; +import 'package:time_machine/time_machine.dart'; + +import '../../../../resources/locale_provider.dart'; +import '../../../../resources/utils.dart'; +import '../../../classes/model/class.dart'; +import '../academic_calendar.dart'; +import 'uni_event.dart'; + +class RecurringUniEvent extends UniEvent { + const RecurringUniEvent({ + @required this.rrule, + @required LocalDateTime start, + @required Period duration, + @required String id, + List relevance, + String degree, + String name, + String location, + Color color, + UniEventType type, + ClassHeader classHeader, + AcademicCalendar calendar, + String addedBy, + bool editable, + }) : assert(rrule != null, 'rrule is null'), + super( + name: name, + location: location, + start: start, + duration: duration, + degree: degree, + relevance: relevance, + id: id, + color: color, + type: type, + classHeader: classHeader, + calendar: calendar, + addedBy: addedBy, + editable: editable); + + final RecurrenceRule rrule; + + @override + String get info { + if (LocaleProvider.rruleL10n != null) { + return rrule.toText(l10n: LocaleProvider.rruleL10n); + } + return ''; + } + + RecurrenceRule get rruleBasedOnCalendar { + final RecurrenceRule rrule = this.rrule; + if (calendar != null && rrule.frequency == Frequency.weekly) { + var weeks = calendar.nonHolidayWeeks; + + // Get the correct sequence of weeks for this event. + // + // For example, if the first academic calendar week is 40 and the event + // starts on week 41 and repeats every two weeks - get every odd-index + // week in the non holiday weeks. + // This is necessary because if an "even" week is followed by a one-week + // holiday, the week that comes after the holiday should be considered + // an "odd" week, even though its number in the calendar would have the + // same parity as the week before the holiday. + if (rrule.interval != 1) { + // Check whether the first calendar week is odd + final bool startOdd = weeks.first.isOdd; + weeks = weeks + .whereIndex((index) => + (startOdd ? index : index + 1) % rrule.interval != + weeks.lookup(WeekYearRules.iso + .getWeekOfWeekYear(start.calendarDate)) % + rrule.interval) + .toSet(); + } + return rrule.copyWith( + frequency: Frequency.yearly, + interval: 1, + byWeekDays: rrule.byWeekDays.isNotEmpty + ? rrule.byWeekDays + : {ByWeekDayEntry(start.dayOfWeek)}, + byWeeks: weeks); + } + return rrule; + } + + @override + Iterable generateInstances( + {DateInterval intersectingInterval}) sync* { + final RecurrenceRule rrule = rruleBasedOnCalendar; + + // Calculate recurrences + int i = 0; + for (final start in rrule.getInstances(start: start)) { + final LocalDateTime end = start.add(duration); + if (intersectingInterval != null) { + if (end.calendarDate < intersectingInterval.start) continue; + if (start.calendarDate > intersectingInterval.end) break; + } + + bool skip = false; + for (final holiday in calendar?.holidays ?? []) { + final holidayInterval = + DateInterval(holiday.startDate, holiday.endDate); + if (holidayInterval.contains(start.calendarDate)) { + // Skip holidays + skip = true; + } + } + + if (!skip) { + yield UniEventInstance( + id: '$id-$i', + title: name, + mainEvent: this, + color: color, + start: start, + end: end, + location: location, + ); + } + + i++; + } + } +} diff --git a/lib/pages/timetable/model/events/uni_event.dart b/lib/pages/timetable/model/events/uni_event.dart index 775cdb59e..ebaf1a98a 100644 --- a/lib/pages/timetable/model/events/uni_event.dart +++ b/lib/pages/timetable/model/events/uni_event.dart @@ -1,213 +1,213 @@ -//import 'dart:core'; -// -//import 'package:acs_upb_mobile/generated/l10n.dart'; -//import 'package:acs_upb_mobile/pages/classes/model/class.dart'; -//import 'package:acs_upb_mobile/pages/timetable/model/academic_calendar.dart'; -//import 'package:flutter/material.dart'; -//import 'package:time_machine/time_machine.dart'; -//import 'package:timetable/timetable.dart'; -// -//enum UniEventType { -// lecture, -// lab, -// seminar, -// sports, -// semester, -// holiday, -// examSession, -// other -//} -// -//extension UniEventTypeExtension on UniEventType { -// String toLocalizedString() { -// switch (this) { -// case UniEventType.lecture: -// return S.current.uniEventTypeLecture; -// case UniEventType.lab: -// return S.current.uniEventTypeLab; -// case UniEventType.seminar: -// return S.current.uniEventTypeSeminar; -// case UniEventType.sports: -// return S.current.uniEventTypeSports; -// case UniEventType.semester: -// return S.current.uniEventTypeSemester; -// case UniEventType.holiday: -// return S.current.uniEventTypeHoliday; -// case UniEventType.examSession: -// return S.current.uniEventTypeExamSession; -// default: -// return S.current.uniEventTypeOther; -// } -// } -// -// static List get classTypes => [ -// UniEventType.lecture, -// UniEventType.lab, -// UniEventType.seminar, -// UniEventType.sports -// ]; -// -// static UniEventType fromString(String string) { -// switch (string) { -// case 'lab': -// return UniEventType.lab; -// case 'lecture': -// return UniEventType.lecture; -// case 'seminar': -// return UniEventType.seminar; -// case 'sports': -// return UniEventType.sports; -// case 'semester': -// return UniEventType.semester; -// case 'holiday': -// return UniEventType.holiday; -// case 'examSession': -// return UniEventType.examSession; -// default: -// return UniEventType.other; -// } -// } -// -// Color get color { -// switch (this) { -// case UniEventType.lecture: -// return Colors.pinkAccent; -// case UniEventType.lab: -// return Colors.blueAccent; -// case UniEventType.seminar: -// return Colors.orangeAccent; -// case UniEventType.sports: -// return Colors.greenAccent; -// case UniEventType.semester: -// return Colors.transparent; -// case UniEventType.holiday: -// return Colors.yellow; -// case UniEventType.examSession: -// return Colors.red; -// default: -// return Colors.white; -// } -// } -//} -// -//class UniEvent { -// const UniEvent({ -// @required this.start, -// @required this.duration, -// @required this.id, -// this.name, -// this.location, -// this.color, -// this.type, -// this.classHeader, -// this.calendar, -// this.relevance, -// this.degree, -// this.addedBy, -// bool editable, -// }) : editable = editable ?? true; -// -// final String id; -// final Color color; -// final UniEventType type; -// final LocalDateTime start; -// final Period duration; -// final String name; -// final String location; -// final ClassHeader classHeader; -// final AcademicCalendar calendar; -// final String degree; -// final List relevance; -// final String addedBy; -// final bool editable; -// -// String get info { -// return generateInstances().first.dateString; -// } -// -// Iterable generateInstances( -// {DateInterval intersectingInterval}) sync* { -// final LocalDateTime end = start.add(duration); -// if (intersectingInterval != null) { -// if (end.calendarDate < intersectingInterval.start || -// start.calendarDate > intersectingInterval.end) return; -// } -// -// yield UniEventInstance( -// id: id, -// title: name, -// mainEvent: this, -// color: color, -// start: start, -// end: start.add(duration), -// location: location, -// ); -// } -//} -// -//class UniEventInstance extends Event { -// UniEventInstance({ -// @required String id, -// @required this.title, -// @required this.mainEvent, -// @required LocalDateTime start, -// @required LocalDateTime end, -// Color color, -// this.location, -// this.info, -// }) : color = color ?? mainEvent?.color, -// super(id: id, start: start, end: end); -// -// final UniEvent mainEvent; -// final String title; -// -// final Color color; -// final String location; -// final String info; -// -// @override -// bool operator ==(dynamic other) => -// super == other && -// color == other.color && -// location == other.location && -// mainEvent == other.mainEvent && -// title == other.title; -// -// @override -// int get hashCode => -// hashList([super.hashCode, color, location, mainEvent, title]); -// -// String get dateString => getDateString(useRelativeDayFormat: false); -// -// String get relativeDateString => getDateString(useRelativeDayFormat: true); -// -// String getDateString({bool useRelativeDayFormat}) { -// final LocalDateTime end = this.end.clockTime.equals(LocalTime(00, 00, 00)) -// ? this.end.subtractDays(1) -// : this.end; -// -// String string = -// useRelativeDayFormat && start.calendarDate.equals(LocalDate.today()) -// ? S.current.labelToday -// : useRelativeDayFormat && -// start.calendarDate.subtractDays(1).equals(LocalDate.today()) -// ? S.current.labelTomorrow -// : start.calendarDate.toString('dddd, dd MMMM'); -// -// if (!start.clockTime.equals(LocalTime(00, 00, 00))) { -// string += ' • ${start.clockTime.toString('HH:mm')}'; -// } -// if (start.calendarDate != end.calendarDate) { -// string += ' - ${end.calendarDate.toString('dddd, dd MMMM')}'; -// } -// if (!end.clockTime.equals(LocalTime(00, 00, 00))) { -// if (start.calendarDate != end.calendarDate) { -// string += ' • '; -// } else { -// string += '-'; -// } -// string += end.clockTime.toString('HH:mm'); -// } -// return string; -// } -//} +import 'dart:core'; + +import 'package:acs_upb_mobile/generated/l10n.dart'; +import 'package:acs_upb_mobile/pages/classes/model/class.dart'; +import 'package:acs_upb_mobile/pages/timetable/model/academic_calendar.dart'; +import 'package:flutter/material.dart'; +import 'package:time_machine/time_machine.dart'; +import 'package:timetable/timetable.dart'; + +enum UniEventType { + lecture, + lab, + seminar, + sports, + semester, + holiday, + examSession, + other +} + +extension UniEventTypeExtension on UniEventType { + String toLocalizedString() { + switch (this) { + case UniEventType.lecture: + return S.current.uniEventTypeLecture; + case UniEventType.lab: + return S.current.uniEventTypeLab; + case UniEventType.seminar: + return S.current.uniEventTypeSeminar; + case UniEventType.sports: + return S.current.uniEventTypeSports; + case UniEventType.semester: + return S.current.uniEventTypeSemester; + case UniEventType.holiday: + return S.current.uniEventTypeHoliday; + case UniEventType.examSession: + return S.current.uniEventTypeExamSession; + default: + return S.current.uniEventTypeOther; + } + } + + static List get classTypes => [ + UniEventType.lecture, + UniEventType.lab, + UniEventType.seminar, + UniEventType.sports + ]; + + static UniEventType fromString(String string) { + switch (string) { + case 'lab': + return UniEventType.lab; + case 'lecture': + return UniEventType.lecture; + case 'seminar': + return UniEventType.seminar; + case 'sports': + return UniEventType.sports; + case 'semester': + return UniEventType.semester; + case 'holiday': + return UniEventType.holiday; + case 'examSession': + return UniEventType.examSession; + default: + return UniEventType.other; + } + } + + Color get color { + switch (this) { + case UniEventType.lecture: + return Colors.pinkAccent; + case UniEventType.lab: + return Colors.blueAccent; + case UniEventType.seminar: + return Colors.orangeAccent; + case UniEventType.sports: + return Colors.greenAccent; + case UniEventType.semester: + return Colors.transparent; + case UniEventType.holiday: + return Colors.yellow; + case UniEventType.examSession: + return Colors.red; + default: + return Colors.white; + } + } +} + +class UniEvent { + const UniEvent({ + @required this.start, + @required this.duration, + @required this.id, + this.name, + this.location, + this.color, + this.type, + this.classHeader, + this.calendar, + this.relevance, + this.degree, + this.addedBy, + bool editable, + }) : editable = editable ?? true; + + final String id; + final Color color; + final UniEventType type; + final LocalDateTime start; + final Period duration; + final String name; + final String location; + final ClassHeader classHeader; + final AcademicCalendar calendar; + final String degree; + final List relevance; + final String addedBy; + final bool editable; + + String get info { + return generateInstances().first.dateString; + } + + Iterable generateInstances( + {DateInterval intersectingInterval}) sync* { + final LocalDateTime end = start.add(duration); + if (intersectingInterval != null) { + if (end.calendarDate < intersectingInterval.start || + start.calendarDate > intersectingInterval.end) return; + } + + yield UniEventInstance( + id: id, + title: name, + mainEvent: this, + color: color, + start: start, + end: start.add(duration), + location: location, + ); + } +} + +class UniEventInstance extends Event { + UniEventInstance({ + @required String id, + @required this.title, + @required this.mainEvent, + @required LocalDateTime start, + @required LocalDateTime end, + Color color, + this.location, + this.info, + }) : color = color ?? mainEvent?.color, + super(id: id, start: start, end: end); + + final UniEvent mainEvent; + final String title; + + final Color color; + final String location; + final String info; + + @override + bool operator ==(dynamic other) => + super == other && + color == other.color && + location == other.location && + mainEvent == other.mainEvent && + title == other.title; + + @override + int get hashCode => + hashList([super.hashCode, color, location, mainEvent, title]); + + String get dateString => getDateString(useRelativeDayFormat: false); + + String get relativeDateString => getDateString(useRelativeDayFormat: true); + + String getDateString({bool useRelativeDayFormat}) { + final LocalDateTime end = this.end.clockTime.equals(LocalTime(00, 00, 00)) + ? this.end.subtractDays(1) + : this.end; + + String string = + useRelativeDayFormat && start.calendarDate.equals(LocalDate.today()) + ? S.current.labelToday + : useRelativeDayFormat && + start.calendarDate.subtractDays(1).equals(LocalDate.today()) + ? S.current.labelTomorrow + : start.calendarDate.toString('dddd, dd MMMM'); + + if (!start.clockTime.equals(LocalTime(00, 00, 00))) { + string += ' • ${start.clockTime.toString('HH:mm')}'; + } + if (start.calendarDate != end.calendarDate) { + string += ' - ${end.calendarDate.toString('dddd, dd MMMM')}'; + } + if (!end.clockTime.equals(LocalTime(00, 00, 00))) { + if (start.calendarDate != end.calendarDate) { + string += ' • '; + } else { + string += '-'; + } + string += end.clockTime.toString('HH:mm'); + } + return string; + } +} diff --git a/lib/pages/timetable/service/google_calendar_services.dart b/lib/pages/timetable/service/google_calendar_services.dart index cc54f7123..dbe4c677a 100644 --- a/lib/pages/timetable/service/google_calendar_services.dart +++ b/lib/pages/timetable/service/google_calendar_services.dart @@ -1,164 +1,165 @@ -//import 'package:acs_upb_mobile/pages/classes/model/class.dart'; -//import 'package:acs_upb_mobile/pages/timetable/model/events/recurring_event.dart'; -//import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -//import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; -//import 'package:acs_upb_mobile/resources/utils.dart'; -//import 'package:acs_upb_mobile/widgets/toast.dart'; -//import 'package:googleapis/calendar/v3.dart' as g_cal; -//import 'package:googleapis/calendar/v3.dart'; -//import 'package:googleapis_auth/auth_io.dart'; -//import 'package:acs_upb_mobile/generated/l10n.dart'; -// -//class GoogleCalendarServices { -// GoogleCalendarServices(); -// -// // allows us to see, edit, share, and permanently delete all the calendars you can access using GCal -// static const List _scopes = [CalendarApi.calendarScope]; -// -// static List get scopes => _scopes; -// -// // Our project IDs, used to identify an app to Google's OAuth servers. -// static ClientId get credentials { -// String _clientIdString; -// if (Platform.isAndroid) { -// _clientIdString = -// '611150208061-4ftun8ln4v9hm1mocqs1vqcftaanj8sj.apps.googleusercontent.com'; -// } else if (Platform.isIOS) { -// _clientIdString = -// '611150208061-4ftun8ln4v9hm1mocqs1vqcftaanj8sj.apps.googleusercontent.com'; -// } else { -// _clientIdString = -// '611150208061-ljqdu5mfmjisdi1h3ics3l2sirvtpljk.apps.googleusercontent.com'; -// } -// return ClientId(_clientIdString, ''); -// } -//} -// -//extension UniEventProviderGoogleCalendar on UniEventProvider { -// g_cal.Event convertEvent(UniEvent uniEvent) { -// final g_cal.Event googleCalendarEvent = g_cal.Event(); -// -// final g_cal.EventDateTime start = g_cal.EventDateTime(); -// final DateTime startDateTime = uniEvent.start.toDateTimeLocal(); -// -// start -// ..timeZone = 'Europe/Bucharest' -// // Google Calendar uses the IANA timezone format, but the native Dart `DateTime` uses an abbreviation provided by the operating system. -// ..dateTime = startDateTime; -// -// final Duration duration = uniEvent.duration.toDuration(); -// -// final g_cal.EventDateTime end = g_cal.EventDateTime(); -// final DateTime endDateTime = startDateTime.add(duration); -// end -// ..timeZone = 'Europe/Bucharest' -// ..dateTime = endDateTime; -// -// final ClassHeader classHeader = uniEvent.classHeader; -// -// googleCalendarEvent -// ..start = start -// ..end = end -// ..summary = classHeader.acronym -// ..colorId = (uniEvent.type.googleCalendarColor.index).toString() -// ..location = uniEvent.location; -// -// if (uniEvent is RecurringUniEvent) { -// final String rruleBasedOnCalendarString = uniEvent.rruleBasedOnCalendar -// .toString() -// .replaceAll(RegExp(r'T000000'), 'T000000Z'); -// googleCalendarEvent.recurrence = [rruleBasedOnCalendarString]; -// } -// -// return googleCalendarEvent; -// } -// -// // This opens a browser window asking the user to authenticate and allow access to edit their calendar -// Future insertGoogleEvents( -// List googleCalendarEvents) async { -// AutoRefreshingAuthClient client; -// try { -// client = await clientViaUserConsent(GoogleCalendarServices.credentials, -// GoogleCalendarServices.scopes, Utils.launchURL); -// final g_cal.CalendarApi calendarApi = g_cal.CalendarApi(client); -// final g_cal.Calendar calendar = g_cal.Calendar() -// ..timeZone = 'Europe/Bucharest' -// ..summary = 'ACS UPB Mobile' -// ..description = 'Timetable imported from ACS UPB Mobile'; -// -// g_cal.CalendarList calendarListNonIterable; -// calendarListNonIterable = await calendarApi.calendarList.list(); -// -// final List calendarList = -// calendarListNonIterable.items; -// for (final g_cal.CalendarListEntry calendar in calendarList) { -// if (calendar.summary == 'ACS UPB Mobile') { -// await calendarApi.calendars.delete(calendar.id); -// break; -// } -// } -// -// final g_cal.Calendar returnedCalendar = -// await calendarApi.calendars.insert(calendar); -// -// if (returnedCalendar is g_cal.Calendar) { -// final String calendarId = returnedCalendar.id; -// for (final g_cal.Event event in googleCalendarEvents) { -// await calendarApi.events.insert(event, calendarId).then( -// (value) { -// print('Added event status: ${value.status}'); -// if (value.status == 'confirmed') { -// print('Event named ${event.summary} added in Google Calendar'); -// } else { -// print( -// 'Unable to add event named ${event.summary} in Google Calendar'); -// } -// }, -// ); -// } -// } -// } catch (e) { -// AppToast.show(S.current.errorInsertGoogleEvents); -// print('Error $e when inserting GCal events.'); -// return; -// } -// } -//} -// -//enum GoogleCalendarColorNames { -// undefined, -// lavender, -// sage, -// grape, -// flamingo, -// banana, -// tangerine, -// peacock, -// graphite, -// blueberry, -// basil, -// tomato -//} -// -//extension UniEventTypeGCalColor on UniEventType { -// GoogleCalendarColorNames get googleCalendarColor { -// switch (this) { -// case UniEventType.lecture: -// return GoogleCalendarColorNames.flamingo; -// case UniEventType.lab: -// return GoogleCalendarColorNames.peacock; -// case UniEventType.seminar: -// return GoogleCalendarColorNames.banana; -// case UniEventType.sports: -// return GoogleCalendarColorNames.basil; -// case UniEventType.semester: -// return GoogleCalendarColorNames.undefined; -// case UniEventType.holiday: -// return GoogleCalendarColorNames.grape; -// case UniEventType.examSession: -// return GoogleCalendarColorNames.tomato; -// default: -// return GoogleCalendarColorNames.undefined; -// } -// } -//} +import 'package:googleapis/calendar/v3.dart' as g_cal; +import 'package:googleapis/calendar/v3.dart'; +import 'package:googleapis_auth/auth_io.dart'; + +import '../../../generated/l10n.dart'; +import '../../../resources/utils.dart'; +import '../../../widgets/toast.dart'; +import '../../classes/model/class.dart'; +import '../model/events/recurring_event.dart'; +import '../model/events/uni_event.dart'; +import 'uni_event_provider.dart'; + +class GoogleCalendarServices { + GoogleCalendarServices(); + + // allows us to see, edit, share, and permanently delete all the calendars you can access using GCal + static const List _scopes = [CalendarApi.calendarScope]; + + static List get scopes => _scopes; + + // Our project IDs, used to identify an app to Google's OAuth servers. + static ClientId get credentials { + String _clientIdString; + if (Platform.isAndroid) { + _clientIdString = + '611150208061-4ftun8ln4v9hm1mocqs1vqcftaanj8sj.apps.googleusercontent.com'; + } else if (Platform.isIOS) { + _clientIdString = + '611150208061-4ftun8ln4v9hm1mocqs1vqcftaanj8sj.apps.googleusercontent.com'; + } else { + _clientIdString = + '611150208061-ljqdu5mfmjisdi1h3ics3l2sirvtpljk.apps.googleusercontent.com'; + } + return ClientId(_clientIdString, ''); + } +} + +extension UniEventProviderGoogleCalendar on UniEventProvider { + g_cal.Event convertEvent(UniEvent uniEvent) { + final g_cal.Event googleCalendarEvent = g_cal.Event(); + + final g_cal.EventDateTime start = g_cal.EventDateTime(); + final DateTime startDateTime = uniEvent.start.toDateTimeLocal(); + + start + ..timeZone = 'Europe/Bucharest' + // Google Calendar uses the IANA timezone format, but the native Dart `DateTime` uses an abbreviation provided by the operating system. + ..dateTime = startDateTime; + + final Duration duration = uniEvent.duration.toDuration(); + + final g_cal.EventDateTime end = g_cal.EventDateTime(); + final DateTime endDateTime = startDateTime.add(duration); + end + ..timeZone = 'Europe/Bucharest' + ..dateTime = endDateTime; + + final ClassHeader classHeader = uniEvent.classHeader; + + googleCalendarEvent + ..start = start + ..end = end + ..summary = classHeader.acronym + ..colorId = (uniEvent.type.googleCalendarColor.index).toString() + ..location = uniEvent.location; + + if (uniEvent is RecurringUniEvent) { + final String rruleBasedOnCalendarString = uniEvent.rruleBasedOnCalendar + .toString() + .replaceAll(RegExp(r'T000000'), 'T000000Z'); + googleCalendarEvent.recurrence = [rruleBasedOnCalendarString]; + } + + return googleCalendarEvent; + } + + // This opens a browser window asking the user to authenticate and allow access to edit their calendar + Future insertGoogleEvents( + List googleCalendarEvents) async { + AutoRefreshingAuthClient client; + try { + client = await clientViaUserConsent(GoogleCalendarServices.credentials, + GoogleCalendarServices.scopes, Utils.launchURL); + final g_cal.CalendarApi calendarApi = g_cal.CalendarApi(client); + final g_cal.Calendar calendar = g_cal.Calendar() + ..timeZone = 'Europe/Bucharest' + ..summary = 'ACS UPB Mobile' + ..description = 'Timetable imported from ACS UPB Mobile'; + + g_cal.CalendarList calendarListNonIterable; + calendarListNonIterable = await calendarApi.calendarList.list(); + + final List calendarList = + calendarListNonIterable.items; + for (final g_cal.CalendarListEntry calendar in calendarList) { + if (calendar.summary == 'ACS UPB Mobile') { + await calendarApi.calendars.delete(calendar.id); + break; + } + } + + final g_cal.Calendar returnedCalendar = + await calendarApi.calendars.insert(calendar); + + if (returnedCalendar is g_cal.Calendar) { + final String calendarId = returnedCalendar.id; + for (final g_cal.Event event in googleCalendarEvents) { + await calendarApi.events.insert(event, calendarId).then( + (value) { + print('Added event status: ${value.status}'); + if (value.status == 'confirmed') { + print('Event named ${event.summary} added in Google Calendar'); + } else { + print( + 'Unable to add event named ${event.summary} in Google Calendar'); + } + }, + ); + } + } + } catch (e) { + AppToast.show(S.current.errorInsertGoogleEvents); + print('Error $e when inserting GCal events.'); + return; + } + } +} + +enum GoogleCalendarColorNames { + undefined, + lavender, + sage, + grape, + flamingo, + banana, + tangerine, + peacock, + graphite, + blueberry, + basil, + tomato +} + +extension UniEventTypeGCalColor on UniEventType { + GoogleCalendarColorNames get googleCalendarColor { + switch (this) { + case UniEventType.lecture: + return GoogleCalendarColorNames.flamingo; + case UniEventType.lab: + return GoogleCalendarColorNames.peacock; + case UniEventType.seminar: + return GoogleCalendarColorNames.banana; + case UniEventType.sports: + return GoogleCalendarColorNames.basil; + case UniEventType.semester: + return GoogleCalendarColorNames.undefined; + case UniEventType.holiday: + return GoogleCalendarColorNames.grape; + case UniEventType.examSession: + return GoogleCalendarColorNames.tomato; + default: + return GoogleCalendarColorNames.undefined; + } + } +} diff --git a/lib/pages/timetable/service/uni_event_provider.dart b/lib/pages/timetable/service/uni_event_provider.dart index 18b290605..aba191842 100644 --- a/lib/pages/timetable/service/uni_event_provider.dart +++ b/lib/pages/timetable/service/uni_event_provider.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:time_machine/time_machine.dart'; import '../../../authentication/service/auth_provider.dart'; import '../../classes/service/class_provider.dart'; @@ -6,201 +7,201 @@ import '../../filter/model/filter.dart'; import '../../filter/service/filter_provider.dart'; import '../../people/service/person_provider.dart'; -//extension PeriodExtension on Period { -// static Period fromJSON(Map json) { -// return Period( -// years: json['years'] ?? 0, -// months: json['months'] ?? 0, -// weeks: json['weeks'] ?? 0, -// days: json['days'] ?? 0, -// hours: json['hours'] ?? 0, -// minutes: json['minutes'] ?? 0, -// seconds: json['seconds'] ?? 0, -// milliseconds: json['milliseconds'] ?? 0, -// microseconds: json['microseconds'] ?? 0, -// nanoseconds: json['nanoseconds'] ?? 0, -// ); -// } -// -// Map toJSON() { -// final json = { -// 'years': years, -// 'months': months, -// 'weeks': weeks, -// 'days': days, -// 'hours': hours, -// 'minutes': minutes, -// 'seconds': seconds, -// 'milliseconds': milliseconds, -// 'microseconds': microseconds, -// 'nanoseconds': nanoseconds -// }; -// -// return json..removeWhere((key, value) => value == 0); -// } -//} -// -//extension LocalDateTimeExtension on LocalDateTime { -// Timestamp toTimestamp() => Timestamp.fromDate(toDateTimeLocal()); -//} -// -//extension UniEventExtension on UniEvent { -// static UniEvent fromJSON(String id, Map json, -// {ClassHeader classHeader, -// Person teacher, -// Map calendars = const {}}) { -// if (json['start'] == null || -// (json['duration'] == null && json['end'] == null)) return null; -// -// final type = UniEventTypeExtension.fromString(json['type']); -// -// if (json['end'] != null) { -// return AllDayUniEvent( -// id: id, -// type: type, -// name: json['name'], -// // Convert time to UTC and then to local time -// start: (json['start'] as Timestamp).toLocalDateTime().calendarDate, -// end: (json['end'] as Timestamp).toLocalDateTime().calendarDate, -// location: json['location'], -// // TODO(IoanaAlexandru): Allow users to set event colours in settings -// color: type.color, -// classHeader: classHeader, -// calendar: calendars[json['calendar']], -// degree: json['degree'], -// relevance: json['relevance'] == null -// ? null -// : List.from(json['relevance']), -// addedBy: json['addedBy'], -// editable: -// json['editable'] ?? false, // Holidays are read-only by default -// ); -// } else if (json['rrule'] != null && json['teacher'] == null) { -// return RecurringUniEvent( -// rrule: RecurrenceRule.fromString(json['rrule']), -// id: id, -// type: type, -// name: json['name'], -// // Convert time to UTC and then to local time -// start: (json['start'] as Timestamp).toLocalDateTime(), -// duration: PeriodExtension.fromJSON(json['duration']), -// location: json['location'], -// // TODO(IoanaAlexandru): Allow users to set event colours in settings -// color: type.color, -// classHeader: classHeader, -// calendar: calendars[json['calendar']], -// degree: json['degree'], -// relevance: json['relevance'] == null -// ? null -// : List.from(json['relevance']), -// addedBy: json['addedBy'], -// editable: json['editable'] ?? true, -// ); -// } else if (json['rrule'] != null && json['teacher'] != null) { -// return ClassEvent( -// teacher: teacher, -// rrule: RecurrenceRule.fromString(json['rrule']), -// id: id, -// type: type, -// name: json['name'], -// start: (json['start'] as Timestamp).toLocalDateTime(), -// duration: PeriodExtension.fromJSON(json['duration']), -// location: json['location'], -// color: type.color, -// classHeader: classHeader, -// calendar: calendars[json['calendar']], -// degree: json['degree'], -// relevance: json['relevance'] == null -// ? null -// : List.from(json['relevance']), -// addedBy: json['addedBy'], -// editable: json['editable'] ?? true, -// ); -// } else { -// return UniEvent( -// id: id, -// type: type, -// name: json['name'], -// // Convert time to UTC and then to local time -// start: (json['start'] as Timestamp).toLocalDateTime(), -// duration: PeriodExtension.fromJSON(json['duration']), -// location: json['location'], -// // TODO(IoanaAlexandru): Allow users to set event colours in settings -// color: type.color, -// classHeader: classHeader, -// calendar: calendars[json['calendar']], -// degree: json['degree'], -// relevance: json['relevance'] == null -// ? null -// : List.from(json['relevance']), -// addedBy: json['addedBy'], -// editable: json['editable'] ?? true, -// ); -// } -// } -// -// Map toData() { -// final type = this.type.toShortString(); -// -// final json = { -// 'type': type, -// 'name': name, -// 'start': start.toTimestamp(), -// 'duration': duration.toJSON(), -// 'location': location, -// 'class': classHeader.id, -// 'degree': degree, -// 'relevance': relevance, -// 'calendar': calendar.id, -// 'addedBy': addedBy, -// }; -// -// if (this is RecurringUniEvent) { -// json['rrule'] = (this as RecurringUniEvent).rrule.toString(); -// } -// -// if (this is AllDayUniEvent) { -// json['end'] = (this as AllDayUniEvent).endDate.atMidnight().toTimestamp(); -// } -// -// if (this is ClassEvent) { -// json['teacher'] = (this as ClassEvent).teacher?.name; -// } -// -// return json; -// } -//} -// -//extension AcademicCalendarExtension on AcademicCalendar { -// static List _eventsFromMapList( -// List list, String type) => -// List.from((list ?? []).asMap().map((index, e) { -// e['type'] = type; -// return MapEntry( -// index, UniEventExtension.fromJSON(type + index.toString(), e)); -// }).values); -// -// static AcademicCalendar fromSnap(DocumentSnapshot> snap) { -// final data = snap.data(); -// return AcademicCalendar( -// id: snap.id, -// semesters: _eventsFromMapList(data['semesters'], 'semester'), -// holidays: _eventsFromMapList(data['holidays'], 'holiday'), -// exams: _eventsFromMapList(data['exams'], 'examSession'), -// ); -// } -//} - -class UniEventProvider // extends EventProvider - with - ChangeNotifier { +extension PeriodExtension on Period { + static Period fromJSON(Map json) { + return Period( + years: json['years'] ?? 0, + months: json['months'] ?? 0, + weeks: json['weeks'] ?? 0, + days: json['days'] ?? 0, + hours: json['hours'] ?? 0, + minutes: json['minutes'] ?? 0, + seconds: json['seconds'] ?? 0, + milliseconds: json['milliseconds'] ?? 0, + microseconds: json['microseconds'] ?? 0, + nanoseconds: json['nanoseconds'] ?? 0, + ); + } + + Map toJSON() { + final json = { + 'years': years, + 'months': months, + 'weeks': weeks, + 'days': days, + 'hours': hours, + 'minutes': minutes, + 'seconds': seconds, + 'milliseconds': milliseconds, + 'microseconds': microseconds, + 'nanoseconds': nanoseconds + }; + + return json..removeWhere((key, value) => value == 0); + } +} + +extension LocalDateTimeExtension on LocalDateTime { + Timestamp toTimestamp() => Timestamp.fromDate(toDateTimeLocal()); +} + +extension UniEventExtension on UniEvent { + static UniEvent fromJSON(String id, Map json, + {ClassHeader classHeader, + Person teacher, + Map calendars = const {}}) { + if (json['start'] == null || + (json['duration'] == null && json['end'] == null)) return null; + + final type = UniEventTypeExtension.fromString(json['type']); + + if (json['end'] != null) { + return AllDayUniEvent( + id: id, + type: type, + name: json['name'], + // Convert time to UTC and then to local time + start: (json['start'] as Timestamp).toLocalDateTime().calendarDate, + end: (json['end'] as Timestamp).toLocalDateTime().calendarDate, + location: json['location'], + // TODO(IoanaAlexandru): Allow users to set event colours in settings + color: type.color, + classHeader: classHeader, + calendar: calendars[json['calendar']], + degree: json['degree'], + relevance: json['relevance'] == null + ? null + : List.from(json['relevance']), + addedBy: json['addedBy'], + editable: + json['editable'] ?? false, // Holidays are read-only by default + ); + } else if (json['rrule'] != null && json['teacher'] == null) { + return RecurringUniEvent( + rrule: RecurrenceRule.fromString(json['rrule']), + id: id, + type: type, + name: json['name'], + // Convert time to UTC and then to local time + start: (json['start'] as Timestamp).toLocalDateTime(), + duration: PeriodExtension.fromJSON(json['duration']), + location: json['location'], + // TODO(IoanaAlexandru): Allow users to set event colours in settings + color: type.color, + classHeader: classHeader, + calendar: calendars[json['calendar']], + degree: json['degree'], + relevance: json['relevance'] == null + ? null + : List.from(json['relevance']), + addedBy: json['addedBy'], + editable: json['editable'] ?? true, + ); + } else if (json['rrule'] != null && json['teacher'] != null) { + return ClassEvent( + teacher: teacher, + rrule: RecurrenceRule.fromString(json['rrule']), + id: id, + type: type, + name: json['name'], + start: (json['start'] as Timestamp).toLocalDateTime(), + duration: PeriodExtension.fromJSON(json['duration']), + location: json['location'], + color: type.color, + classHeader: classHeader, + calendar: calendars[json['calendar']], + degree: json['degree'], + relevance: json['relevance'] == null + ? null + : List.from(json['relevance']), + addedBy: json['addedBy'], + editable: json['editable'] ?? true, + ); + } else { + return UniEvent( + id: id, + type: type, + name: json['name'], + // Convert time to UTC and then to local time + start: (json['start'] as Timestamp).toLocalDateTime(), + duration: PeriodExtension.fromJSON(json['duration']), + location: json['location'], + // TODO(IoanaAlexandru): Allow users to set event colours in settings + color: type.color, + classHeader: classHeader, + calendar: calendars[json['calendar']], + degree: json['degree'], + relevance: json['relevance'] == null + ? null + : List.from(json['relevance']), + addedBy: json['addedBy'], + editable: json['editable'] ?? true, + ); + } + } + + Map toData() { + final type = this.type.toShortString(); + + final json = { + 'type': type, + 'name': name, + 'start': start.toTimestamp(), + 'duration': duration.toJSON(), + 'location': location, + 'class': classHeader.id, + 'degree': degree, + 'relevance': relevance, + 'calendar': calendar.id, + 'addedBy': addedBy, + }; + + if (this is RecurringUniEvent) { + json['rrule'] = (this as RecurringUniEvent).rrule.toString(); + } + + if (this is AllDayUniEvent) { + json['end'] = (this as AllDayUniEvent).endDate.atMidnight().toTimestamp(); + } + + if (this is ClassEvent) { + json['teacher'] = (this as ClassEvent).teacher?.name; + } + + return json; + } +} + +extension AcademicCalendarExtension on AcademicCalendar { + static List _eventsFromMapList( + List list, String type) => + List.from((list ?? []).asMap().map((index, e) { + e['type'] = type; + return MapEntry( + index, UniEventExtension.fromJSON(type + index.toString(), e)); + }).values); + + static AcademicCalendar fromSnap( + DocumentSnapshot> snap) { + final data = snap.data(); + return AcademicCalendar( + id: snap.id, + semesters: _eventsFromMapList(data['semesters'], 'semester'), + holidays: _eventsFromMapList(data['holidays'], 'holiday'), + exams: _eventsFromMapList(data['exams'], 'examSession'), + ); + } +} + +class UniEventProvider extends EventProvider + with ChangeNotifier { UniEventProvider({AuthProvider authProvider, PersonProvider personProvider}) : _authProvider = authProvider ?? AuthProvider(), _personProvider = personProvider ?? PersonProvider() { -// fetchCalendars(); + fetchCalendars(); } -// final Map _calendars = {}; + final Map _calendars = {}; ClassProvider _classProvider; FilterProvider _filterProvider; final AuthProvider _authProvider; @@ -209,220 +210,219 @@ class UniEventProvider // extends EventProvider Filter _filter; bool empty; -// Future> fetchCalendars() async { -// final QuerySnapshot> query = -// await FirebaseFirestore.instance.collection('calendars').get(); -// for (final doc in query.docs) { -// _calendars[doc.id] = AcademicCalendarExtension.fromSnap(doc); -// } -// -// notifyListeners(); -// return _calendars; -// } -// -// Future checkIfEmpty(List>> streams) async { -// for (final stream in streams) { -// if ((await stream.first)?.isNotEmpty ?? false) { -// empty = false; -// return; -// } -// } -// empty = true; -// } -// -// Stream> get _events { -// if (!_authProvider.isAuthenticated || -// _filter == null || -// _calendars == null) { -// return Stream.value([]); -// } -// -// final streams = >>[]; -// -// if (_filter.relevantNodes.length > 1) { -// for (final classId in _classIds ?? []) { -// final Stream> stream = FirebaseFirestore.instance -// .collection('events') -// .where('class', isEqualTo: classId) -// .where('degree', isEqualTo: _filter.baseNode) -// .where('relevance', -// arrayContainsAny: _filter.relevantNodes..remove('All')) -// .snapshots() -// .asyncMap((snapshot) async { -// final events = []; -// -// try { -// for (final doc in snapshot.docs) { -// ClassHeader classHeader; -// Person teacher; -// final data = doc.data(); -// if (data['class'] != null) { -// classHeader = -// await _classProvider.fetchClassHeader(data['class']); -// } -// if (data['teacher'] != null) { -// teacher = await _personProvider.fetchPerson(data['teacher']); -// } -// -// events.add(UniEventExtension.fromJSON(doc.id, data, -// classHeader: classHeader, -// teacher: teacher, -// calendars: _calendars)); -// } -// return events.where((element) => element != null).toList(); -// } catch (e) { -// print(e); -// return events; -// } -// }); -// streams.add(stream); -// } -// } -// -// checkIfEmpty(streams); -// -// final stream = StreamZip(streams); -// -// // Flatten zipped streams -// return stream.map((events) => events.expand((i) => i).toList()); -// } -// -// Future exportToGoogleCalendar() async { -// final Stream> eventsStream = _events; -// final List streamElement = await eventsStream.first; -// final List googleCalendarEvents = []; -// for (final UniEvent eventInstance in streamElement) { -// final g_cal.Event googleCalendarEvent = convertEvent(eventInstance); -// googleCalendarEvents.add(googleCalendarEvent); -// } -// await insertGoogleEvents(googleCalendarEvents); -// } -// -// @override -// Stream> getAllDayEventsIntersecting( -// DateInterval interval) { -// return _events.map((events) => events -// .map((event) => event.generateInstances(intersectingInterval: interval)) -// .expand((i) => i) -// .allDayEvents -// .followedBy(_calendars.values.map((cal) { -// final List events = cal.holidays + cal.exams; -// return events -// .where((event) => -// event.relevance == null || -// (_filter != null && -// event.degree == _filter.baseNode && -// event.relevance.any(_filter.relevantNodes.contains))) -// .map((e) => e.generateInstances(intersectingInterval: interval)) -// .expand((e) => e); -// }).expand((e) => e))); -// } -// -// @override -// Stream> getPartDayEventsIntersecting( -// LocalDate date) { -// return _events.map((events) => events -// .map((event) => event.generateInstances( -// intersectingInterval: DateInterval(date, date))) -// .expand((i) => i) -// .partDayEvents); -// } -// -// Future> getUpcomingEvents(LocalDate date, -// {int limit = 3}) async { -// return _events -// .map((events) => events -// .where((event) => !(event is AllDayUniEvent)) -// .map((event) => event.generateInstances( -// intersectingInterval: DateInterval(date, date.addDays(6)))) -// .expand((i) => i) -// .sortedByStartLength() -// .where((element) => -// element.end.toDateTimeLocal().isAfter(DateTime.now())) -// .take(limit)) -// .first; -// } -// -// Future> getAllEventsOfClass(String classId) async { -// return _events -// .map((events) => -// events.where((event) => event.classHeader.id == classId)) -// .first; -// } -// + Future> fetchCalendars() async { + final QuerySnapshot> query = + await FirebaseFirestore.instance.collection('calendars').get(); + for (final doc in query.docs) { + _calendars[doc.id] = AcademicCalendarExtension.fromSnap(doc); + } + + notifyListeners(); + return _calendars; + } + + Future checkIfEmpty(List>> streams) async { + for (final stream in streams) { + if ((await stream.first)?.isNotEmpty ?? false) { + empty = false; + return; + } + } + empty = true; + } + + Stream> get _events { + if (!_authProvider.isAuthenticated || + _filter == null || + _calendars == null) { + return Stream.value([]); + } + + final streams = >>[]; + + if (_filter.relevantNodes.length > 1) { + for (final classId in _classIds ?? []) { + final Stream> stream = FirebaseFirestore.instance + .collection('events') + .where('class', isEqualTo: classId) + .where('degree', isEqualTo: _filter.baseNode) + .where('relevance', + arrayContainsAny: _filter.relevantNodes..remove('All')) + .snapshots() + .asyncMap((snapshot) async { + final events = []; + + try { + for (final doc in snapshot.docs) { + ClassHeader classHeader; + Person teacher; + final data = doc.data(); + if (data['class'] != null) { + classHeader = + await _classProvider.fetchClassHeader(data['class']); + } + if (data['teacher'] != null) { + teacher = await _personProvider.fetchPerson(data['teacher']); + } + + events.add(UniEventExtension.fromJSON(doc.id, data, + classHeader: classHeader, + teacher: teacher, + calendars: _calendars)); + } + return events.where((element) => element != null).toList(); + } catch (e) { + print(e); + return events; + } + }); + streams.add(stream); + } + } + + checkIfEmpty(streams); + + final stream = StreamZip(streams); + + // Flatten zipped streams + return stream.map((events) => events.expand((i) => i).toList()); + } + + Future exportToGoogleCalendar() async { + final Stream> eventsStream = _events; + final List streamElement = await eventsStream.first; + final List googleCalendarEvents = []; + for (final UniEvent eventInstance in streamElement) { + final g_cal.Event googleCalendarEvent = convertEvent(eventInstance); + googleCalendarEvents.add(googleCalendarEvent); + } + await insertGoogleEvents(googleCalendarEvents); + } + + @override + Stream> getAllDayEventsIntersecting( + DateInterval interval) { + return _events.map((events) => events + .map((event) => event.generateInstances(intersectingInterval: interval)) + .expand((i) => i) + .allDayEvents + .followedBy(_calendars.values.map((cal) { + final List events = cal.holidays + cal.exams; + return events + .where((event) => + event.relevance == null || + (_filter != null && + event.degree == _filter.baseNode && + event.relevance.any(_filter.relevantNodes.contains))) + .map((e) => e.generateInstances(intersectingInterval: interval)) + .expand((e) => e); + }).expand((e) => e))); + } + + @override + Stream> getPartDayEventsIntersecting( + LocalDate date) { + return _events.map((events) => events + .map((event) => event.generateInstances( + intersectingInterval: DateInterval(date, date))) + .expand((i) => i) + .partDayEvents); + } + + Future> getUpcomingEvents(LocalDate date, + {int limit = 3}) async { + return _events + .map((events) => events + .where((event) => !(event is AllDayUniEvent)) + .map((event) => event.generateInstances( + intersectingInterval: DateInterval(date, date.addDays(6)))) + .expand((i) => i) + .sortedByStartLength() + .where((element) => + element.end.toDateTimeLocal().isAfter(DateTime.now())) + .take(limit)) + .first; + } + + Future> getAllEventsOfClass(String classId) async { + return _events + .map((events) => + events.where((event) => event.classHeader.id == classId)) + .first; + } + void updateClasses(ClassProvider classProvider) { -// _classProvider = classProvider; -// _classProvider.fetchUserClassIds(_authProvider.uid).then((classIds) { -// _classIds = classIds; -// notifyListeners(); -// }); + _classProvider = classProvider; + _classProvider.fetchUserClassIds(_authProvider.uid).then((classIds) { + _classIds = classIds; + notifyListeners(); + }); } -// void updateFilter(FilterProvider filterProvider) { -// _filterProvider = filterProvider; -// _filterProvider.fetchFilter().then((filter) { -// _filter = filter; -// notifyListeners(); -// }); + _filterProvider = filterProvider; + _filterProvider.fetchFilter().then((filter) { + _filter = filter; + notifyListeners(); + }); + } + + Future addEvent(UniEvent event) async { + try { + await FirebaseFirestore.instance.collection('events').add(event.toData()); + notifyListeners(); + return true; + } catch (e) { + _errorHandler(e); + return false; + } + } + + Future updateEvent(UniEvent event) async { + try { + final ref = FirebaseFirestore.instance.collection('events').doc(event.id); + + if ((await ref.get()).data == null) { + print('Event not found.'); + return false; + } + + await ref.update(event.toData()); + notifyListeners(); + return true; + } catch (e) { + _errorHandler(e); + return false; + } + } + + Future deleteEvent(UniEvent event) async { + try { + DocumentReference ref; + ref = FirebaseFirestore.instance.collection('events').doc(event.id); + await ref.delete(); + notifyListeners(); + return true; + } catch (e) { + _errorHandler(e); + return false; + } + } + + @override + // ignore: must_call_super + void dispose() { + // TODO(IoanaAlexandru): Find a better way to prevent Timetable from calling dispose on this provider + } + + void _errorHandler(dynamic e, {bool showToast = true}) { + print(e.message); + if (showToast) { + if (e.message.contains('PERMISSION_DENIED')) { + AppToast.show(S.current.errorPermissionDenied); + } else { + AppToast.show(S.current.errorSomethingWentWrong); + } + } } -// -// Future addEvent(UniEvent event) async { -// try { -// await FirebaseFirestore.instance.collection('events').add(event.toData()); -// notifyListeners(); -// return true; -// } catch (e) { -// _errorHandler(e); -// return false; -// } -// } -// -// Future updateEvent(UniEvent event) async { -// try { -// final ref = FirebaseFirestore.instance.collection('events').doc(event.id); -// -// if ((await ref.get()).data == null) { -// print('Event not found.'); -// return false; -// } -// -// await ref.update(event.toData()); -// notifyListeners(); -// return true; -// } catch (e) { -// _errorHandler(e); -// return false; -// } -// } -// -// Future deleteEvent(UniEvent event) async { -// try { -// DocumentReference ref; -// ref = FirebaseFirestore.instance.collection('events').doc(event.id); -// await ref.delete(); -// notifyListeners(); -// return true; -// } catch (e) { -// _errorHandler(e); -// return false; -// } -// } -// -// @override -// // ignore: must_call_super -// void dispose() { -// // TODO(IoanaAlexandru): Find a better way to prevent Timetable from calling dispose on this provider -// } -// -// void _errorHandler(dynamic e, {bool showToast = true}) { -// print(e.message); -// if (showToast) { -// if (e.message.contains('PERMISSION_DENIED')) { -// AppToast.show(S.current.errorPermissionDenied); -// } else { -// AppToast.show(S.current.errorSomethingWentWrong); -// } -// } -// } } diff --git a/lib/pages/timetable/view/date_header.dart b/lib/pages/timetable/view/date_header.dart index f28fff8db..f29725748 100644 --- a/lib/pages/timetable/view/date_header.dart +++ b/lib/pages/timetable/view/date_header.dart @@ -1,72 +1,72 @@ -//import 'package:auto_size_text/auto_size_text.dart'; -//import 'package:black_hole_flutter/black_hole_flutter.dart'; -//import 'package:flutter/material.dart'; -//import 'package:time_machine/time_machine.dart'; -//import 'package:time_machine/time_machine_text_patterns.dart'; -//// ignore: implementation_imports -//import 'package:timetable/src/header/date_indicator.dart'; -//// ignore: implementation_imports -//import 'package:timetable/src/theme.dart'; -//// ignore: implementation_imports -//import 'package:timetable/src/utils/utils.dart'; -// -//// TODO(IoanaAlexandru): This is a temporary fix because the default -//// [DateHeader] from the timetable package has an overflow when the culture -//// is set to Romanian. We copied it here with minor changes and it can be -//// removed once the timetable package has it fixed. -//class DateHeader extends StatelessWidget { -// const DateHeader(this.date, {Key key}) : super(key: key); -// -// final LocalDate date; -// -// @override -// Widget build(BuildContext context) { -// return Column( -// mainAxisSize: MainAxisSize.min, -// crossAxisAlignment: CrossAxisAlignment.center, -// children: [ -// WeekdayIndicator(date), -// const SizedBox(height: 4), -// DateIndicator(date), -// ], -// ); -// } -//} -// -//class WeekdayIndicator extends StatelessWidget { -// const WeekdayIndicator(this.date, {Key key}) : super(key: key); -// -// final LocalDate date; -// -// @override -// Widget build(BuildContext context) { -// final theme = context.theme; -// final timetableTheme = context.timetableTheme; -// -// final states = DateIndicator.statesFor(date); -// final pattern = timetableTheme?.weekDayIndicatorPattern?.resolve(states) ?? -// LocalDatePattern.createWithCurrentCulture('ddd'); -// final decoration = -// timetableTheme?.weekDayIndicatorDecoration?.resolve(states) ?? -// const BoxDecoration(); -// final textStyle = -// timetableTheme?.weekDayIndicatorTextStyle?.resolve(states) ?? -// TextStyle( -// color: date.isToday -// ? timetableTheme?.primaryColor ?? theme.primaryColor -// : theme.highEmphasisOnBackground, -// ); -// -// return DecoratedBox( -// decoration: decoration, -// child: Padding( -// padding: const EdgeInsets.all(4), -// child: AutoSizeText( -// pattern.format(date), -// style: textStyle, -// maxLines: 1, -// ), -// ), -// ); -// } -//} +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:black_hole_flutter/black_hole_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:time_machine/time_machine.dart'; +import 'package:time_machine/time_machine_text_patterns.dart'; +// ignore: implementation_imports +import 'package:timetable/src/header/date_indicator.dart'; +// ignore: implementation_imports +import 'package:timetable/src/theme.dart'; +// ignore: implementation_imports +import 'package:timetable/src/utils/utils.dart'; + +// TODO(IoanaAlexandru): This is a temporary fix because the default +// [DateHeader] from the timetable package has an overflow when the culture +// is set to Romanian. We copied it here with minor changes and it can be +// removed once the timetable package has it fixed. +class DateHeader extends StatelessWidget { + const DateHeader(this.date, {Key key}) : super(key: key); + + final LocalDate date; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + WeekdayIndicator(date), + const SizedBox(height: 4), + DateIndicator(date), + ], + ); + } +} + +class WeekdayIndicator extends StatelessWidget { + const WeekdayIndicator(this.date, {Key key}) : super(key: key); + + final LocalDate date; + + @override + Widget build(BuildContext context) { + final theme = context.theme; + final timetableTheme = context.timetableTheme; + + final states = DateIndicator.statesFor(date); + final pattern = timetableTheme?.weekDayIndicatorPattern?.resolve(states) ?? + LocalDatePattern.createWithCurrentCulture('ddd'); + final decoration = + timetableTheme?.weekDayIndicatorDecoration?.resolve(states) ?? + const BoxDecoration(); + final textStyle = + timetableTheme?.weekDayIndicatorTextStyle?.resolve(states) ?? + TextStyle( + color: date.isToday + ? timetableTheme?.primaryColor ?? theme.primaryColor + : theme.highEmphasisOnBackground, + ); + + return DecoratedBox( + decoration: decoration, + child: Padding( + padding: const EdgeInsets.all(4), + child: AutoSizeText( + pattern.format(date), + style: textStyle, + maxLines: 1, + ), + ), + ); + } +} diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index 4132dd5f1..b121bf589 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -1,332 +1,333 @@ -//import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; -//import 'package:acs_upb_mobile/generated/l10n.dart'; -//import 'package:acs_upb_mobile/navigation/routes.dart'; -//import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; -//import 'package:acs_upb_mobile/pages/classes/view/classes_page.dart'; -//import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; -//import 'package:acs_upb_mobile/pages/filter/view/filter_page.dart'; -//import 'package:acs_upb_mobile/pages/settings/service/request_provider.dart'; -//import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -//import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; -//import 'package:acs_upb_mobile/pages/timetable/view/date_header.dart'; -//import 'package:acs_upb_mobile/pages/timetable/view/events/add_event_view.dart'; -//import 'package:acs_upb_mobile/pages/timetable/view/events/all_day_event_widget.dart'; -//import 'package:acs_upb_mobile/pages/timetable/view/events/event_widget.dart'; -//import 'package:acs_upb_mobile/widgets/button.dart'; -//import 'package:acs_upb_mobile/widgets/dialog.dart'; -//import 'package:acs_upb_mobile/widgets/scaffold.dart'; -//import 'package:acs_upb_mobile/widgets/toast.dart'; -//import 'package:flutter/material.dart'; -//import 'package:flutter_feather_icons/flutter_feather_icons.dart'; -//import 'package:provider/provider.dart'; -//import 'package:recase/recase.dart'; -//import 'package:time_machine/time_machine.dart'; -//import 'package:timetable/timetable.dart'; -// -//class TimetablePage extends StatefulWidget { -// const TimetablePage({Key key}) : super(key: key); -// -// @override -// _TimetablePageState createState() => _TimetablePageState(); -//} -// -//class _TimetablePageState extends State { -// TimetableController _controller; -// -// @override -// void dispose() { -// _controller?.dispose(); -// super.dispose(); -// } -// -// @override -// Widget build(BuildContext context) { -// final authProvider = Provider.of(context); -// final eventProvider = Provider.of(context); -// if (_controller == null) { -// _controller = TimetableController( -// // TODO(IoanaAlexandru): Make initialTimeRange customizable in settings -// initialTimeRange: InitialTimeRange.range( -// startTime: LocalTime(7, 55, 0), endTime: LocalTime(20, 5, 0)), -// eventProvider: eventProvider); -// -// if (authProvider.isAuthenticated && !authProvider.isAnonymous) { -// scheduleDialog(context); -// } -// } -// -// return AppScaffold( -// title: AnimatedBuilder( -// animation: _controller.dateListenable, -// builder: (context, child) => Text( -// authProvider.isAuthenticated && !authProvider.isAnonymous -// ? S.current.navigationTimetable -// : _controller.currentMonth.titleCase), -// ), -// needsToBeAuthenticated: true, -// leading: AppScaffoldAction( -// icon: Icons.today_outlined, -// onPressed: () => _controller.animateToToday(), -// tooltip: S.current.actionJumpToToday, -// ), -// actions: [ -// AppScaffoldAction( -// icon: FeatherIcons.bookOpen, -// tooltip: S.current.navigationClasses, -// onPressed: () => Navigator.of(context).push( -// MaterialPageRoute( -// builder: (_) => ChangeNotifierProvider.value( -// value: Provider.of(context), -// child: const ClassesPage()), -// ), -// ), -// ), -// AppScaffoldAction( -// icon: FeatherIcons.filter, -// tooltip: S.current.navigationFilter, -// onPressed: () => Navigator.push( -// context, -// MaterialPageRoute(builder: (_) => const FilterPage()), -// ), -// ), -// ], -// body: Padding( -// padding: const EdgeInsets.all(10), -// child: Stack( -// children: [ -// Timetable( -// controller: _controller, -// dateHeaderBuilder: (_, date) => DateHeader(date), -// eventBuilder: (event) => UniEventWidget(event), -// allDayEventBuilder: (context, event, info) => -// UniAllDayEventWidget( -// event, -// info: info, -// ), -// onEventBackgroundTap: (dateTime, isAllDay) { -// if (!isAllDay) { -// final user = Provider.of(context, listen: false) -// .currentUserFromCache; -// if (user.canAddPublicInfo) { -// Navigator.of(context).push(MaterialPageRoute( -// builder: (_) => ChangeNotifierProxyProvider( -// create: (_) => FilterProvider(), -// update: (context, authProvider, filterProvider) { -// return filterProvider..updateAuth(authProvider); -// }, -// child: AddEventView( -// initialEvent: UniEvent( -// start: dateTime, -// duration: const Period(hours: 2), -// id: null), -// ), -// ), -// )); -// } else { -// AppToast.show(S.current.errorPermissionDenied); -// } -// } -// }, -// ), -// ], -// ), -// ), -// ); -// } -// -// Future scheduleDialog(BuildContext context) async { -// WidgetsBinding.instance.addPostFrameCallback((_) async { -// if (!mounted) { -// return; -// } -// -// // Fetch user classes, request necessary info from providers so it's -// // cached when we check in the dialog -// final user = Provider.of(context, listen: false) -// .currentUserFromCache; -// await Provider.of(context, listen: false) -// .fetchClassHeaders(uid: user.uid); -// await Provider.of(context, listen: false).fetchFilter(); -// await Provider.of(context, listen: false) -// .userAlreadyRequested(user.uid); -// -// // Slight delay between last frame and dialog -// await Future.delayed(const Duration(milliseconds: 100)); -// -// // Show dialog if there are no events -// final eventProvider = -// Provider.of(context, listen: false); -// if (eventProvider.empty) { -// await showDialog( -// context: context, -// builder: buildDialog, -// ); -// } -// }); -// } -// -// Widget buildDialog(BuildContext context) { -// final classProvider = Provider.of(context); -// final authProvider = Provider.of(context); -// final filterProvider = Provider.of(context); -// final user = authProvider.currentUserFromCache; -// -// if (classProvider.userClassHeadersCache?.isEmpty ?? true) { -// return AppDialog( -// title: S.current.warningNoEvents, -// content: [ -// RichText( -// text: TextSpan( -// style: Theme.of(context).textTheme.subtitle1, -// children: [ -// TextSpan(text: '${S.current.infoYouNeedToSelect} '), -// WidgetSpan( -// alignment: PlaceholderAlignment.top, -// child: Icon( -// FeatherIcons.bookOpen, -// size: Theme.of(context).textTheme.subtitle1.fontSize + 2, -// ), -// ), -// TextSpan(text: ' ${S.current.infoClasses}.'), -// ], -// ), -// ), -// ], -// actions: [ -// AppButton( -// text: S.current.actionChooseClasses, -// width: 130, -// onTap: () async { -// // Pop the dialog -// Navigator.of(context).pop(); -// // Push the Add classes page -// await Navigator.of(context) -// .push(MaterialPageRoute( -// builder: (_) => ChangeNotifierProvider.value( -// value: classProvider, -// child: FutureBuilder( -// future: classProvider.fetchUserClassIds(user.uid), -// builder: (context, snap) { -// if (snap.hasData) { -// return AddClassesPage( -// initialClassIds: snap.data, -// onSave: (classIds) async { -// await classProvider.setUserClassIds( -// classIds, authProvider.uid); -// Navigator.pop(context); -// }); -// } else { -// return const Center( -// child: CircularProgressIndicator()); -// } -// }, -// )), -// )); -// }, -// ) -// ], -// ); -// } else if ((filterProvider.cachedFilter?.relevantNodes?.length ?? 0) < 6) { -// return AppDialog( -// title: S.current.warningNoEvents, -// content: [ -// RichText( -// text: TextSpan( -// style: Theme.of(context).textTheme.subtitle1, -// children: [ -// TextSpan(text: '${S.current.infoMakeSureGroupIsSelected} '), -// WidgetSpan( -// alignment: PlaceholderAlignment.top, -// child: Icon( -// FeatherIcons.filter, -// size: Theme.of(context).textTheme.subtitle1.fontSize + 2, -// ), -// ), -// TextSpan(text: ' ${S.current.navigationFilter.toLowerCase()}.'), -// ], -// ), -// ), -// ], -// actions: [ -// AppButton( -// text: S.current.actionOpenFilter, -// width: 130, -// onTap: () async { -// // Pop the dialog -// Navigator.of(context).pop(); -// // Push the Filter page -// await Navigator.pushNamed(context, Routes.filter); -// }, -// ) -// ], -// ); -// } else if (user.permissionLevel < 3) { -// // TODO(IoanaAlexandru): Check if user already requested and show a different message -// return AppDialog( -// title: S.current.warningNoEvents, -// content: [Text(S.current.messageYouCanContribute)], -// actions: [ -// AppButton( -// text: S.current.actionRequestPermissions, -// width: 130, -// onTap: () async { -// // Check if user is verified -// final bool isVerified = await authProvider.isVerified; -// // Pop the dialog -// Navigator.of(context).pop(); -// // Push the Permissions page -// if (authProvider.isAnonymous) { -// AppToast.show(S.current.messageNotLoggedIn); -// } else if (!isVerified) { -// AppToast.show(S.current.messageEmailNotVerifiedToPerformAction); -// } else { -// await Navigator.of(context) -// .pushNamed(Routes.requestPermissions); -// } -// }, -// ) -// ], -// ); -// } else { -// return AppDialog( -// title: S.current.warningNoEvents, -// content: [ -// RichText( -// key: const ValueKey('no_events_message'), -// text: TextSpan( -// style: Theme.of(context).textTheme.subtitle1, -// children: [ -// TextSpan(text: S.current.messageThereAreNoEventsForSelected), -// WidgetSpan( -// alignment: PlaceholderAlignment.top, -// child: Icon( -// FeatherIcons.bookOpen, -// size: Theme.of(context).textTheme.subtitle1.fontSize + 2, -// ), -// ), -// TextSpan( -// text: -// '${S.current.navigationClasses.toLowerCase()} ${S.current.stringAnd} '), -// WidgetSpan( -// alignment: PlaceholderAlignment.top, -// child: Icon( -// FeatherIcons.filter, -// size: Theme.of(context).textTheme.subtitle1.fontSize + 2, -// ), -// ), -// TextSpan(text: ' ${S.current.navigationFilter.toLowerCase()}.'), -// ], -// ), -// ), -// ], -// ); -// } -// } -//} -// -//extension MonthController on TimetableController { -// String get currentMonth => -// LocalDateTime(2020, dateListenable.value.monthOfYear, 1, 1, 1, 1) -// .toString('MMMM'); -//} +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:provider/provider.dart'; +import 'package:recase/recase.dart'; +import 'package:time_machine/time_machine.dart'; +import 'package:timetable/timetable.dart'; + +import '../../../authentication/service/auth_provider.dart'; +import '../../../generated/l10n.dart'; +import '../../../navigation/routes.dart'; +import '../../../widgets/button.dart'; +import '../../../widgets/dialog.dart'; +import '../../../widgets/scaffold.dart'; +import '../../../widgets/toast.dart'; +import '../../classes/service/class_provider.dart'; +import '../../classes/view/classes_page.dart'; +import '../../filter/service/filter_provider.dart'; +import '../../filter/view/filter_page.dart'; +import '../../settings/service/request_provider.dart'; +import '../model/events/uni_event.dart'; +import '../service/uni_event_provider.dart'; +import 'date_header.dart'; +import 'events/add_event_view.dart'; +import 'events/all_day_event_widget.dart'; +import 'events/event_widget.dart'; + +class TimetablePage extends StatefulWidget { + const TimetablePage({Key key}) : super(key: key); + + @override + _TimetablePageState createState() => _TimetablePageState(); +} + +class _TimetablePageState extends State { + TimetableController _controller; + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final authProvider = Provider.of(context); + final eventProvider = Provider.of(context); + if (_controller == null) { + _controller = TimetableController( + // TODO(IoanaAlexandru): Make initialTimeRange customizable in settings + initialTimeRange: InitialTimeRange.range( + startTime: LocalTime(7, 55, 0), endTime: LocalTime(20, 5, 0)), + eventProvider: eventProvider); + + if (authProvider.isAuthenticated && !authProvider.isAnonymous) { + scheduleDialog(context); + } + } + + return AppScaffold( + title: AnimatedBuilder( + animation: _controller.dateListenable, + builder: (context, child) => Text( + authProvider.isAuthenticated && !authProvider.isAnonymous + ? S.current.navigationTimetable + : _controller.currentMonth.titleCase), + ), + needsToBeAuthenticated: true, + leading: AppScaffoldAction( + icon: Icons.today_outlined, + onPressed: () => _controller.animateToToday(), + tooltip: S.current.actionJumpToToday, + ), + actions: [ + AppScaffoldAction( + icon: FeatherIcons.bookOpen, + tooltip: S.current.navigationClasses, + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ChangeNotifierProvider.value( + value: Provider.of(context), + child: const ClassesPage()), + ), + ), + ), + AppScaffoldAction( + icon: FeatherIcons.filter, + tooltip: S.current.navigationFilter, + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const FilterPage()), + ), + ), + ], + body: Padding( + padding: const EdgeInsets.all(10), + child: Stack( + children: [ + Timetable( + controller: _controller, + dateHeaderBuilder: (_, date) => DateHeader(date), + eventBuilder: (event) => UniEventWidget(event), + allDayEventBuilder: (context, event, info) => + UniAllDayEventWidget( + event, + info: info, + ), + onEventBackgroundTap: (dateTime, isAllDay) { + if (!isAllDay) { + final user = Provider.of(context, listen: false) + .currentUserFromCache; + if (user.canAddPublicInfo) { + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => ChangeNotifierProxyProvider( + create: (_) => FilterProvider(), + update: (context, authProvider, filterProvider) { + return filterProvider..updateAuth(authProvider); + }, + child: AddEventView( + initialEvent: UniEvent( + start: dateTime, + duration: const Period(hours: 2), + id: null), + ), + ), + )); + } else { + AppToast.show(S.current.errorPermissionDenied); + } + } + }, + ), + ], + ), + ), + ); + } + + Future scheduleDialog(BuildContext context) async { + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!mounted) { + return; + } + + // Fetch user classes, request necessary info from providers so it's + // cached when we check in the dialog + final user = Provider.of(context, listen: false) + .currentUserFromCache; + await Provider.of(context, listen: false) + .fetchClassHeaders(uid: user.uid); + await Provider.of(context, listen: false).fetchFilter(); + await Provider.of(context, listen: false) + .userAlreadyRequested(user.uid); + + // Slight delay between last frame and dialog + await Future.delayed(const Duration(milliseconds: 100)); + + // Show dialog if there are no events + final eventProvider = + Provider.of(context, listen: false); + if (eventProvider.empty) { + await showDialog( + context: context, + builder: buildDialog, + ); + } + }); + } + + Widget buildDialog(BuildContext context) { + final classProvider = Provider.of(context); + final authProvider = Provider.of(context); + final filterProvider = Provider.of(context); + final user = authProvider.currentUserFromCache; + + if (classProvider.userClassHeadersCache?.isEmpty ?? true) { + return AppDialog( + title: S.current.warningNoEvents, + content: [ + RichText( + text: TextSpan( + style: Theme.of(context).textTheme.subtitle1, + children: [ + TextSpan(text: '${S.current.infoYouNeedToSelect} '), + WidgetSpan( + alignment: PlaceholderAlignment.top, + child: Icon( + FeatherIcons.bookOpen, + size: Theme.of(context).textTheme.subtitle1.fontSize + 2, + ), + ), + TextSpan(text: ' ${S.current.infoClasses}.'), + ], + ), + ), + ], + actions: [ + AppButton( + text: S.current.actionChooseClasses, + width: 130, + onTap: () async { + // Pop the dialog + Navigator.of(context).pop(); + // Push the Add classes page + await Navigator.of(context) + .push(MaterialPageRoute( + builder: (_) => ChangeNotifierProvider.value( + value: classProvider, + child: FutureBuilder( + future: classProvider.fetchUserClassIds(user.uid), + builder: (context, snap) { + if (snap.hasData) { + return AddClassesPage( + initialClassIds: snap.data, + onSave: (classIds) async { + await classProvider.setUserClassIds( + classIds, authProvider.uid); + Navigator.pop(context); + }); + } else { + return const Center( + child: CircularProgressIndicator()); + } + }, + )), + )); + }, + ) + ], + ); + } else if ((filterProvider.cachedFilter?.relevantNodes?.length ?? 0) < 6) { + return AppDialog( + title: S.current.warningNoEvents, + content: [ + RichText( + text: TextSpan( + style: Theme.of(context).textTheme.subtitle1, + children: [ + TextSpan(text: '${S.current.infoMakeSureGroupIsSelected} '), + WidgetSpan( + alignment: PlaceholderAlignment.top, + child: Icon( + FeatherIcons.filter, + size: Theme.of(context).textTheme.subtitle1.fontSize + 2, + ), + ), + TextSpan(text: ' ${S.current.navigationFilter.toLowerCase()}.'), + ], + ), + ), + ], + actions: [ + AppButton( + text: S.current.actionOpenFilter, + width: 130, + onTap: () async { + // Pop the dialog + Navigator.of(context).pop(); + // Push the Filter page + await Navigator.pushNamed(context, Routes.filter); + }, + ) + ], + ); + } else if (user.permissionLevel < 3) { + // TODO(IoanaAlexandru): Check if user already requested and show a different message + return AppDialog( + title: S.current.warningNoEvents, + content: [Text(S.current.messageYouCanContribute)], + actions: [ + AppButton( + text: S.current.actionRequestPermissions, + width: 130, + onTap: () async { + // Check if user is verified + final bool isVerified = await authProvider.isVerified; + // Pop the dialog + Navigator.of(context).pop(); + // Push the Permissions page + if (authProvider.isAnonymous) { + AppToast.show(S.current.messageNotLoggedIn); + } else if (!isVerified) { + AppToast.show(S.current.messageEmailNotVerifiedToPerformAction); + } else { + await Navigator.of(context) + .pushNamed(Routes.requestPermissions); + } + }, + ) + ], + ); + } else { + return AppDialog( + title: S.current.warningNoEvents, + content: [ + RichText( + key: const ValueKey('no_events_message'), + text: TextSpan( + style: Theme.of(context).textTheme.subtitle1, + children: [ + TextSpan(text: S.current.messageThereAreNoEventsForSelected), + WidgetSpan( + alignment: PlaceholderAlignment.top, + child: Icon( + FeatherIcons.bookOpen, + size: Theme.of(context).textTheme.subtitle1.fontSize + 2, + ), + ), + TextSpan( + text: + '${S.current.navigationClasses.toLowerCase()} ${S.current.stringAnd} '), + WidgetSpan( + alignment: PlaceholderAlignment.top, + child: Icon( + FeatherIcons.filter, + size: Theme.of(context).textTheme.subtitle1.fontSize + 2, + ), + ), + TextSpan(text: ' ${S.current.navigationFilter.toLowerCase()}.'), + ], + ), + ), + ], + ); + } + } +} + +extension MonthController on TimetableController { + String get currentMonth => + LocalDateTime(2020, dateListenable.value.monthOfYear, 1, 1, 1, 1) + .toString('MMMM'); +} From 216a0be2d74e8e8e4869abce659d0fd2dd0ad286 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Wed, 18 Aug 2021 16:36:09 +0300 Subject: [PATCH 20/60] modified pubspec.yaml --- pubspec.yaml | 123 ++++++++++++++++++++++++++------------------------- 1 file changed, 63 insertions(+), 60 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index b3a8621b3..380555388 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,10 +45,10 @@ dependencies: dotted_line: ^3.0.0 # Highlight words -# dynamic_text_highlighting: ^2.2.0 + # dynamic_text_highlighting: ^2.2.0 # Package for dynamically changing the app theme -# dynamic_theme: ^1.0.1 + # dynamic_theme: ^1.0.1 easy_dynamic_theme: ^2.2.0 # Firebase products @@ -94,7 +94,7 @@ dependencies: image_picker_web: ^2.0.2 # Interval picker widget -# interval_time_picker: ^0.1.0 + # interval_time_picker: ^0.1.0 # Displays a custom toast oktoast: ^3.0.0 @@ -127,7 +127,10 @@ dependencies: synchronized: ^3.0.0 # DateTime utilities -# time_machine: ^0.9.16 + time_machine: + git: + url: https://github.com/Dana-Ferguson/time_machine + ref: master # Time interval picker time_range_picker: ^2.0.1 @@ -159,7 +162,7 @@ dev_dependencies: sdk: flutter # Helps with generating Dart code with the messages from `.arb` files -# intl_translation: ^0.17.1 + # intl_translation: ^0.17.1 # Mocking utility used for testing mockito: ^5.0.9 @@ -182,63 +185,63 @@ flutter: uses-material-design: true assets: - - assets/icons/ - - assets/illustrations/ - - assets/images/ -# - packages/time_machine/data/cultures/cultures.bin -# - packages/time_machine/data/tzdb/tzdb.bin + - assets/icons/ + - assets/illustrations/ + - assets/images/ + - packages/time_machine/data/cultures/cultures.bin + - packages/time_machine/data/tzdb/tzdb.bin fonts: - - family: CustomIcons - fonts: - - asset: assets/fonts/CustomIcons/CustomIcons.ttf - - family: Montserrat - fonts: - - asset: assets/fonts/Montserrat/Montserrat-Thin.otf - weight: 100 - - asset: assets/fonts/Montserrat/Montserrat-ThinItalic.otf - weight: 100 - style: italic - - asset: assets/fonts/Montserrat/Montserrat-ExtraLight.otf - weight: 200 - - asset: assets/fonts/Montserrat/Montserrat-ExtraLightItalic.otf - weight: 200 - style: italic - - asset: assets/fonts/Montserrat/Montserrat-Light.otf - weight: 300 - - asset: assets/fonts/Montserrat/Montserrat-LightItalic.otf - weight: 300 - style: italic - - asset: assets/fonts/Montserrat/Montserrat-Regular.otf - weight: 400 - - asset: assets/fonts/Montserrat/Montserrat-Italic.otf - weight: 400 - style: italic - - asset: assets/fonts/Montserrat/Montserrat-Medium.otf - weight: 500 - - asset: assets/fonts/Montserrat/Montserrat-MediumItalic.otf - weight: 500 - style: italic - - asset: assets/fonts/Montserrat/Montserrat-SemiBold.otf - weight: 600 - - asset: assets/fonts/Montserrat/Montserrat-SemiBoldItalic.otf - weight: 600 - style: italic - - asset: assets/fonts/Montserrat/Montserrat-Bold.otf - weight: 700 - - asset: assets/fonts/Montserrat/Montserrat-BoldItalic.otf - weight: 700 - style: italic - - asset: assets/fonts/Montserrat/Montserrat-ExtraBold.otf - weight: 800 - - asset: assets/fonts/Montserrat/Montserrat-ExtraBoldItalic.otf - weight: 800 - style: italic - - asset: assets/fonts/Montserrat/Montserrat-Black.otf - weight: 900 - - asset: assets/fonts/Montserrat/Montserrat-BlackItalic.otf - weight: 900 - style: italic + - family: CustomIcons + fonts: + - asset: assets/fonts/CustomIcons/CustomIcons.ttf + - family: Montserrat + fonts: + - asset: assets/fonts/Montserrat/Montserrat-Thin.otf + weight: 100 + - asset: assets/fonts/Montserrat/Montserrat-ThinItalic.otf + weight: 100 + style: italic + - asset: assets/fonts/Montserrat/Montserrat-ExtraLight.otf + weight: 200 + - asset: assets/fonts/Montserrat/Montserrat-ExtraLightItalic.otf + weight: 200 + style: italic + - asset: assets/fonts/Montserrat/Montserrat-Light.otf + weight: 300 + - asset: assets/fonts/Montserrat/Montserrat-LightItalic.otf + weight: 300 + style: italic + - asset: assets/fonts/Montserrat/Montserrat-Regular.otf + weight: 400 + - asset: assets/fonts/Montserrat/Montserrat-Italic.otf + weight: 400 + style: italic + - asset: assets/fonts/Montserrat/Montserrat-Medium.otf + weight: 500 + - asset: assets/fonts/Montserrat/Montserrat-MediumItalic.otf + weight: 500 + style: italic + - asset: assets/fonts/Montserrat/Montserrat-SemiBold.otf + weight: 600 + - asset: assets/fonts/Montserrat/Montserrat-SemiBoldItalic.otf + weight: 600 + style: italic + - asset: assets/fonts/Montserrat/Montserrat-Bold.otf + weight: 700 + - asset: assets/fonts/Montserrat/Montserrat-BoldItalic.otf + weight: 700 + style: italic + - asset: assets/fonts/Montserrat/Montserrat-ExtraBold.otf + weight: 800 + - asset: assets/fonts/Montserrat/Montserrat-ExtraBoldItalic.otf + weight: 800 + style: italic + - asset: assets/fonts/Montserrat/Montserrat-Black.otf + weight: 900 + - asset: assets/fonts/Montserrat/Montserrat-BlackItalic.otf + weight: 900 + style: italic flutter_intl: enabled: true From 1f073cfa886f629f0396a220db69b21e97534eed Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Wed, 18 Aug 2021 18:17:10 +0300 Subject: [PATCH 21/60] Fixed compatibility issues Solved warnings Updated related packages --- .../model/events/recurring_event.dart | 9 ++-- .../timetable/model/events/uni_event.dart | 42 +++++++++++-------- .../service/google_calendar_services.dart | 5 ++- pubspec.lock | 11 ++++- pubspec.yaml | 2 +- 5 files changed, 44 insertions(+), 25 deletions(-) diff --git a/lib/pages/timetable/model/events/recurring_event.dart b/lib/pages/timetable/model/events/recurring_event.dart index 8f3baa520..8a130af24 100644 --- a/lib/pages/timetable/model/events/recurring_event.dart +++ b/lib/pages/timetable/model/events/recurring_event.dart @@ -80,7 +80,7 @@ class RecurringUniEvent extends UniEvent { interval: 1, byWeekDays: rrule.byWeekDays.isNotEmpty ? rrule.byWeekDays - : {ByWeekDayEntry(start.dayOfWeek)}, + : {ByWeekDayEntry(start.dayOfWeek.value)}, byWeeks: weeks); } return rrule; @@ -93,13 +93,16 @@ class RecurringUniEvent extends UniEvent { // Calculate recurrences int i = 0; - for (final start in rrule.getInstances(start: start)) { + final Iterable instances = rrule + .getInstances(start: start.toDateTimeLocal()) + .map((dateTime) => LocalDateTime.dateTime(dateTime)); + + for (final start in instances) { final LocalDateTime end = start.add(duration); if (intersectingInterval != null) { if (end.calendarDate < intersectingInterval.start) continue; if (start.calendarDate > intersectingInterval.end) break; } - bool skip = false; for (final holiday in calendar?.holidays ?? []) { final holidayInterval = diff --git a/lib/pages/timetable/model/events/uni_event.dart b/lib/pages/timetable/model/events/uni_event.dart index ebaf1a98a..41829b9c1 100644 --- a/lib/pages/timetable/model/events/uni_event.dart +++ b/lib/pages/timetable/model/events/uni_event.dart @@ -35,9 +35,10 @@ extension UniEventTypeExtension on UniEventType { return S.current.uniEventTypeHoliday; case UniEventType.examSession: return S.current.uniEventTypeExamSession; - default: + case UniEventType.other: return S.current.uniEventTypeOther; } + return S.current.uniEventTypeOther; } static List get classTypes => [ @@ -84,9 +85,10 @@ extension UniEventTypeExtension on UniEventType { return Colors.yellow; case UniEventType.examSession: return Colors.red; - default: + case UniEventType.other: return Colors.white; } + return Colors.white; } } @@ -182,26 +184,30 @@ class UniEventInstance extends Event { String get relativeDateString => getDateString(useRelativeDayFormat: true); String getDateString({bool useRelativeDayFormat}) { - final LocalDateTime end = this.end.clockTime.equals(LocalTime(00, 00, 00)) - ? this.end.subtractDays(1) - : this.end; - - String string = - useRelativeDayFormat && start.calendarDate.equals(LocalDate.today()) - ? S.current.labelToday - : useRelativeDayFormat && - start.calendarDate.subtractDays(1).equals(LocalDate.today()) - ? S.current.labelTomorrow - : start.calendarDate.toString('dddd, dd MMMM'); - - if (!start.clockTime.equals(LocalTime(00, 00, 00))) { - string += ' • ${start.clockTime.toString('HH:mm')}'; + final LocalDateTime defaultStart = LocalDateTime.dateTime(start); + final LocalDateTime defaultEnd = LocalDateTime.dateTime(this.end); + final LocalDateTime end = defaultEnd.clockTime.equals(LocalTime(00, 00, 00)) + ? defaultEnd.subtractDays(1) + : defaultEnd; + + String string = useRelativeDayFormat && + defaultStart.calendarDate.equals(LocalDate.today()) + ? S.current.labelToday + : useRelativeDayFormat && + defaultStart.calendarDate + .subtractDays(1) + .equals(LocalDate.today()) + ? S.current.labelTomorrow + : defaultStart.calendarDate.toString('dddd, dd MMMM'); + + if (!defaultStart.clockTime.equals(LocalTime(00, 00, 00))) { + string += ' • ${defaultStart.clockTime.toString('HH:mm')}'; } - if (start.calendarDate != end.calendarDate) { + if (defaultStart.calendarDate != defaultEnd.calendarDate) { string += ' - ${end.calendarDate.toString('dddd, dd MMMM')}'; } if (!end.clockTime.equals(LocalTime(00, 00, 00))) { - if (start.calendarDate != end.calendarDate) { + if (defaultStart.calendarDate != defaultEnd.calendarDate) { string += ' • '; } else { string += '-'; diff --git a/lib/pages/timetable/service/google_calendar_services.dart b/lib/pages/timetable/service/google_calendar_services.dart index dbe4c677a..1bf763086 100644 --- a/lib/pages/timetable/service/google_calendar_services.dart +++ b/lib/pages/timetable/service/google_calendar_services.dart @@ -47,7 +47,7 @@ extension UniEventProviderGoogleCalendar on UniEventProvider { // Google Calendar uses the IANA timezone format, but the native Dart `DateTime` uses an abbreviation provided by the operating system. ..dateTime = startDateTime; - final Duration duration = uniEvent.duration.toDuration(); + final Duration duration = uniEvent.duration.toTime().toDuration; final g_cal.EventDateTime end = g_cal.EventDateTime(); final DateTime endDateTime = startDateTime.add(duration); @@ -158,8 +158,9 @@ extension UniEventTypeGCalColor on UniEventType { return GoogleCalendarColorNames.grape; case UniEventType.examSession: return GoogleCalendarColorNames.tomato; - default: + case UniEventType.other: return GoogleCalendarColorNames.undefined; } + return GoogleCalendarColorNames.undefined; } } diff --git a/pubspec.lock b/pubspec.lock index ca12464ed..6e36c760a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -943,6 +943,15 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.3.0" + time_machine: + dependency: "direct main" + description: + path: "." + ref: master + resolved-ref: "040de1a261df442538ed97f6de5895465d7ca4dd" + url: "https://github.com/Dana-Ferguson/time_machine" + source: git + version: "0.9.17" time_range_picker: dependency: "direct main" description: @@ -963,7 +972,7 @@ packages: name: timetable url: "https://pub.dartlang.org" source: hosted - version: "1.0.0-alpha.0" + version: "1.0.0-alpha.5" tuple: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 380555388..b479e3a29 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -136,7 +136,7 @@ dependencies: time_range_picker: ^2.0.1 # Timetable widget - timetable: ^1.0.0-alpha.0 + timetable: ^1.0.0-alpha.5 # URL opener url_launcher: ^6.0.6 From 5b87de81ce621bec46a0af5bdc6b647adb6e6f80 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Wed, 18 Aug 2021 18:53:54 +0300 Subject: [PATCH 22/60] Fixed compatibility issues Solved warnings Updated related packages --- lib/pages/timetable/model/events/uni_event.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/pages/timetable/model/events/uni_event.dart b/lib/pages/timetable/model/events/uni_event.dart index 41829b9c1..289ed1c14 100644 --- a/lib/pages/timetable/model/events/uni_event.dart +++ b/lib/pages/timetable/model/events/uni_event.dart @@ -149,7 +149,6 @@ class UniEvent { class UniEventInstance extends Event { UniEventInstance({ - @required String id, @required this.title, @required this.mainEvent, @required LocalDateTime start, @@ -158,11 +157,11 @@ class UniEventInstance extends Event { this.location, this.info, }) : color = color ?? mainEvent?.color, - super(id: id, start: start, end: end); + super(start: start.toDateTimeLocal(), end: end.toDateTimeLocal()); final UniEvent mainEvent; - final String title; + final String title; final Color color; final String location; final String info; From 111c0aba20898088eeb0c014804cc8ce29b1377f Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Thu, 19 Aug 2021 15:05:22 +0300 Subject: [PATCH 23/60] Further migration to DateTime and DateTimeRange (in timetable's model classes) --- .../timetable/model/academic_calendar.dart | 31 ++++++----- .../timetable/model/events/all_day_event.dart | 42 +++++++-------- .../timetable/model/events/class_event.dart | 2 +- .../model/events/recurring_event.dart | 29 +++++------ .../timetable/model/events/uni_event.dart | 27 +++++----- lib/pages/timetable/timetable_utils.dart | 51 +++++++++++++++++++ 6 files changed, 120 insertions(+), 62 deletions(-) create mode 100644 lib/pages/timetable/timetable_utils.dart diff --git a/lib/pages/timetable/model/academic_calendar.dart b/lib/pages/timetable/model/academic_calendar.dart index a39cc1f5d..d1095f98b 100644 --- a/lib/pages/timetable/model/academic_calendar.dart +++ b/lib/pages/timetable/model/academic_calendar.dart @@ -1,8 +1,11 @@ -import 'package:acs_upb_mobile/pages/timetable/model/events/all_day_event.dart'; -import 'package:acs_upb_mobile/resources/utils.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:time_machine/time_machine.dart'; +import '../../../resources/utils.dart'; +import '../timetable_utils.dart'; +import 'events/all_day_event.dart'; + class AcademicCalendar { AcademicCalendar( {@required this.id, @@ -15,12 +18,14 @@ class AcademicCalendar { List holidays; List exams; - Map> _getWeeksByYearInInterval(DateInterval interval) { + Map> _getWeeksByYearInInterval(DateTimeRange interval) { final Map> weeksByYear = {}; final rule = WeekYearRules.iso; - final int firstWeek = rule.getWeekOfWeekYear(interval.start); - final int lastWeek = rule.getWeekOfWeekYear(interval.end); + final int firstWeek = + rule.getWeekOfWeekYear(LocalDate.dateTime(interval.start)); + final int lastWeek = + rule.getWeekOfWeekYear(LocalDate.dateTime(interval.end)); if (interval.start.year == interval.end.year) { weeksByYear[interval.start.year] = range(firstWeek, lastWeek + 1).toSet(); @@ -42,7 +47,7 @@ class AcademicCalendar { for (final semester in semesters) { for (final entry in _getWeeksByYearInInterval( - DateInterval(semester.startDate, semester.endDate)) + DateTimeRange(start: semester.startDate, end: semester.endDate)) .entries) { weeksByYear[entry.key] ??= {}; weeksByYear[entry.key].addAll(entry.value); @@ -50,8 +55,8 @@ class AcademicCalendar { } for (final holiday in holidays) { - final DateInterval holidayInterval = - DateInterval(holiday.startDate, holiday.endDate); + final DateTimeRange holidayInterval = + DateTimeRange(start: holiday.startDate, end: holiday.endDate); final Map> holidayWeeksByYear = _getWeeksByYearInInterval(holidayInterval); @@ -60,10 +65,12 @@ class AcademicCalendar { final Set weeks = entry.value; for (final week in weeks) { - final LocalDate monday = rule.getLocalDate( - year, week, DayOfWeek.monday, CalendarSystem.iso); - final LocalDate friday = rule.getLocalDate( - year, week, DayOfWeek.friday, CalendarSystem.iso); + final DateTime monday = rule + .getLocalDate(year, week, DayOfWeek.monday, CalendarSystem.iso) + .toDateTimeUnspecified(); + final DateTime friday = rule + .getLocalDate(year, week, DayOfWeek.friday, CalendarSystem.iso) + .toDateTimeUnspecified(); // If the holiday includes Monday to Friday in a week, exclude week // number from [nonHolidayWeeks]. diff --git a/lib/pages/timetable/model/events/all_day_event.dart b/lib/pages/timetable/model/events/all_day_event.dart index e2bfb3a72..e775a333b 100644 --- a/lib/pages/timetable/model/events/all_day_event.dart +++ b/lib/pages/timetable/model/events/all_day_event.dart @@ -2,13 +2,14 @@ import 'package:flutter/material.dart'; import 'package:time_machine/time_machine.dart'; import '../../../classes/model/class.dart'; +import '../../timetable_utils.dart'; import '../academic_calendar.dart'; import 'uni_event.dart'; class AllDayUniEvent extends UniEvent { AllDayUniEvent({ - @required LocalDate start, - @required LocalDate end, + @required DateTime start, + @required DateTime end, @required String id, String name, String location, @@ -20,32 +21,31 @@ class AllDayUniEvent extends UniEvent { String degree, String addedBy, bool editable, - }) - : startDate = start, + }) : startDate = start, endDate = end, super( - name: name, - location: location, - start: start.atMidnight(), - duration: Period.differenceBetweenDates(start, end.addDays(1)), - id: id, - color: color, - type: type, - classHeader: classHeader, - calendar: calendar, - relevance: relevance, - degree: degree, - addedBy: addedBy, - editable: editable); + name: name, + location: location, + start: start.atMidnight(), + duration: Period.differenceBetweenDates( + LocalDate.dateTime(start), LocalDate.dateTime(end.addDays(1))), + id: id, + color: color, + type: type, + classHeader: classHeader, + calendar: calendar, + relevance: relevance, + degree: degree, + addedBy: addedBy, + editable: editable); - LocalDate startDate; - LocalDate endDate; + DateTime startDate; + DateTime endDate; @override Iterable generateInstances( - {DateInterval intersectingInterval}) sync* { + {DateTimeRange intersectingInterval}) sync* { yield UniEventInstance( - id: id, title: name, mainEvent: this, start: startDate.atMidnight(), diff --git a/lib/pages/timetable/model/events/class_event.dart b/lib/pages/timetable/model/events/class_event.dart index 4e1a1e175..1caf0fc41 100644 --- a/lib/pages/timetable/model/events/class_event.dart +++ b/lib/pages/timetable/model/events/class_event.dart @@ -12,7 +12,7 @@ class ClassEvent extends RecurringUniEvent { const ClassEvent({ @required this.teacher, @required RecurrenceRule rrule, - @required LocalDateTime start, + @required DateTime start, @required Period duration, @required String id, List relevance, diff --git a/lib/pages/timetable/model/events/recurring_event.dart b/lib/pages/timetable/model/events/recurring_event.dart index 8a130af24..7c6cd8cb7 100644 --- a/lib/pages/timetable/model/events/recurring_event.dart +++ b/lib/pages/timetable/model/events/recurring_event.dart @@ -5,13 +5,14 @@ import 'package:time_machine/time_machine.dart'; import '../../../../resources/locale_provider.dart'; import '../../../../resources/utils.dart'; import '../../../classes/model/class.dart'; +import '../../timetable_utils.dart'; import '../academic_calendar.dart'; import 'uni_event.dart'; class RecurringUniEvent extends UniEvent { const RecurringUniEvent({ @required this.rrule, - @required LocalDateTime start, + @required DateTime start, @required Period duration, @required String id, List relevance, @@ -71,7 +72,7 @@ class RecurringUniEvent extends UniEvent { .whereIndex((index) => (startOdd ? index : index + 1) % rrule.interval != weeks.lookup(WeekYearRules.iso - .getWeekOfWeekYear(start.calendarDate)) % + .getWeekOfWeekYear(LocalDate.dateTime(start))) % rrule.interval) .toSet(); } @@ -80,7 +81,7 @@ class RecurringUniEvent extends UniEvent { interval: 1, byWeekDays: rrule.byWeekDays.isNotEmpty ? rrule.byWeekDays - : {ByWeekDayEntry(start.dayOfWeek.value)}, + : {ByWeekDayEntry(start.weekday)}, byWeeks: weeks); } return rrule; @@ -88,26 +89,24 @@ class RecurringUniEvent extends UniEvent { @override Iterable generateInstances( - {DateInterval intersectingInterval}) sync* { + {DateTimeRange intersectingInterval}) sync* { final RecurrenceRule rrule = rruleBasedOnCalendar; // Calculate recurrences int i = 0; - final Iterable instances = rrule - .getInstances(start: start.toDateTimeLocal()) - .map((dateTime) => LocalDateTime.dateTime(dateTime)); - - for (final start in instances) { - final LocalDateTime end = start.add(duration); + for (final start in rrule.getInstances(start: start)) { + final DateTime end = start.add(duration.toTime().toDuration); if (intersectingInterval != null) { - if (end.calendarDate < intersectingInterval.start) continue; - if (start.calendarDate > intersectingInterval.end) break; + if (end < intersectingInterval.start) continue; + if (start > intersectingInterval.end) break; } + bool skip = false; for (final holiday in calendar?.holidays ?? []) { final holidayInterval = - DateInterval(holiday.startDate, holiday.endDate); - if (holidayInterval.contains(start.calendarDate)) { + DateTimeRange(start: holiday.startDate, end: holiday.endDate); + // DateInterval(holiday.startDate, holiday.endDate); + if (holidayInterval.contains(start)) { // Skip holidays skip = true; } @@ -115,7 +114,7 @@ class RecurringUniEvent extends UniEvent { if (!skip) { yield UniEventInstance( - id: '$id-$i', + // id: '$id-$i', title: name, mainEvent: this, color: color, diff --git a/lib/pages/timetable/model/events/uni_event.dart b/lib/pages/timetable/model/events/uni_event.dart index 289ed1c14..b470fa66b 100644 --- a/lib/pages/timetable/model/events/uni_event.dart +++ b/lib/pages/timetable/model/events/uni_event.dart @@ -1,11 +1,12 @@ import 'dart:core'; -import 'package:acs_upb_mobile/generated/l10n.dart'; -import 'package:acs_upb_mobile/pages/classes/model/class.dart'; -import 'package:acs_upb_mobile/pages/timetable/model/academic_calendar.dart'; import 'package:flutter/material.dart'; import 'package:time_machine/time_machine.dart'; import 'package:timetable/timetable.dart'; +import '../../../../generated/l10n.dart'; +import '../../../classes/model/class.dart'; +import '../../timetable_utils.dart'; +import '../academic_calendar.dart'; enum UniEventType { lecture, @@ -112,7 +113,7 @@ class UniEvent { final String id; final Color color; final UniEventType type; - final LocalDateTime start; + final DateTime start; final Period duration; final String name; final String location; @@ -128,20 +129,20 @@ class UniEvent { } Iterable generateInstances( - {DateInterval intersectingInterval}) sync* { - final LocalDateTime end = start.add(duration); + {DateTimeRange intersectingInterval}) sync* { + final DateTime end = start.add(duration.toTime().toDuration); if (intersectingInterval != null) { - if (end.calendarDate < intersectingInterval.start || - start.calendarDate > intersectingInterval.end) return; + if (end < intersectingInterval.start || start > intersectingInterval.end) + return; } yield UniEventInstance( - id: id, + // id: id, title: name, mainEvent: this, color: color, start: start, - end: start.add(duration), + end: start.add(duration.toTime().toDuration), location: location, ); } @@ -151,13 +152,13 @@ class UniEventInstance extends Event { UniEventInstance({ @required this.title, @required this.mainEvent, - @required LocalDateTime start, - @required LocalDateTime end, + @required DateTime start, + @required DateTime end, Color color, this.location, this.info, }) : color = color ?? mainEvent?.color, - super(start: start.toDateTimeLocal(), end: end.toDateTimeLocal()); + super(start: start, end: end); final UniEvent mainEvent; diff --git a/lib/pages/timetable/timetable_utils.dart b/lib/pages/timetable/timetable_utils.dart new file mode 100644 index 000000000..b1c5c4b81 --- /dev/null +++ b/lib/pages/timetable/timetable_utils.dart @@ -0,0 +1,51 @@ +// import 'package:flutter/material.dart'; +// import 'package:intl/intl.dart'; + +import 'package:flutter/material.dart'; + +extension DateTimeExtension on DateTime { + DateTime atMidnight() => DateTime(year, month, day, 0, 0, 0); + DateTime addDays(int noDays) => add(Duration(days: noDays)); + + // from https://github.com/JonasWanke/timetable/blob/a09be6e1e64457e93a448c60ab6851230a3f2a4b/lib/src/utils.dart + bool operator <(DateTime other) => isBefore(other); + bool operator <=(DateTime other) => + isBefore(other) || isAtSameMomentAs(other); + bool operator >(DateTime other) => isAfter(other); + bool operator >=(DateTime other) => isAfter(other) || isAtSameMomentAs(other); + +} + +extension DateTimeRangeExtension on DateTimeRange { + bool contains(DateTime dateTime) { + return dateTime >= start && dateTime <= end; + } +} + + +// +// class CalendarDate { +// CalendarDate(this.year, this.month, this.day); +// +// int year; +// int month; +// int day; +// +// bool equals(CalendarDate other) { +// return year == other.year && month == other.month && day == other.day; +// } +// } +// +// class ClockTime { +// ClockTime(this.hour, this.minute, this.second); +// +// int hour; +// int minute; +// int second; +// +// bool equals(ClockTime other) { +// return hour == other.hour && +// minute == other.minute && +// second == other.second; +// } +// } From d3f92de900cd9e8ca54d258e578cfe1fdce7433e Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Fri, 20 Aug 2021 17:48:06 +0300 Subject: [PATCH 24/60] Compatibility fixes --- .../timetable/model/events/uni_event.dart | 4 +- .../service/google_calendar_services.dart | 2 +- .../timetable/service/uni_event_provider.dart | 53 +- .../timetable/view/events/add_event_view.dart | 1533 +++++++++-------- 4 files changed, 808 insertions(+), 784 deletions(-) diff --git a/lib/pages/timetable/model/events/uni_event.dart b/lib/pages/timetable/model/events/uni_event.dart index b470fa66b..0c33b346e 100644 --- a/lib/pages/timetable/model/events/uni_event.dart +++ b/lib/pages/timetable/model/events/uni_event.dart @@ -132,8 +132,10 @@ class UniEvent { {DateTimeRange intersectingInterval}) sync* { final DateTime end = start.add(duration.toTime().toDuration); if (intersectingInterval != null) { - if (end < intersectingInterval.start || start > intersectingInterval.end) + if (end < intersectingInterval.start || + start > intersectingInterval.end) { return; + } } yield UniEventInstance( diff --git a/lib/pages/timetable/service/google_calendar_services.dart b/lib/pages/timetable/service/google_calendar_services.dart index 1bf763086..957739f24 100644 --- a/lib/pages/timetable/service/google_calendar_services.dart +++ b/lib/pages/timetable/service/google_calendar_services.dart @@ -40,7 +40,7 @@ extension UniEventProviderGoogleCalendar on UniEventProvider { final g_cal.Event googleCalendarEvent = g_cal.Event(); final g_cal.EventDateTime start = g_cal.EventDateTime(); - final DateTime startDateTime = uniEvent.start.toDateTimeLocal(); + final DateTime startDateTime = uniEvent.start; start ..timeZone = 'Europe/Bucharest' diff --git a/lib/pages/timetable/service/uni_event_provider.dart b/lib/pages/timetable/service/uni_event_provider.dart index aba191842..e442cb672 100644 --- a/lib/pages/timetable/service/uni_event_provider.dart +++ b/lib/pages/timetable/service/uni_event_provider.dart @@ -1,11 +1,30 @@ +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; +import 'package:googleapis/calendar/v3.dart' as g_cal; +import 'package:rrule/rrule.dart'; import 'package:time_machine/time_machine.dart'; +import 'package:timetable/timetable.dart'; import '../../../authentication/service/auth_provider.dart'; +import '../../../generated/l10n.dart'; +import '../../../resources/utils.dart'; +import '../../../widgets/toast.dart'; +import '../../classes/model/class.dart'; import '../../classes/service/class_provider.dart'; import '../../filter/model/filter.dart'; import '../../filter/service/filter_provider.dart'; +import '../../people/model/person.dart'; import '../../people/service/person_provider.dart'; +import '../model/academic_calendar.dart'; +import '../model/events/all_day_event.dart'; +import '../model/events/class_event.dart'; +import '../model/events/recurring_event.dart'; +import '../model/events/uni_event.dart'; +import '../timetable_utils.dart'; +import 'google_calendar_services.dart'; extension PeriodExtension on Period { static Period fromJSON(Map json) { @@ -41,8 +60,8 @@ extension PeriodExtension on Period { } } -extension LocalDateTimeExtension on LocalDateTime { - Timestamp toTimestamp() => Timestamp.fromDate(toDateTimeLocal()); +extension DateTimeExtension on DateTime { + Timestamp toTimestamp() => Timestamp.fromDate(this); } extension UniEventExtension on UniEvent { @@ -61,8 +80,8 @@ extension UniEventExtension on UniEvent { type: type, name: json['name'], // Convert time to UTC and then to local time - start: (json['start'] as Timestamp).toLocalDateTime().calendarDate, - end: (json['end'] as Timestamp).toLocalDateTime().calendarDate, + start: (json['start'] as Timestamp).toDate(), + end: (json['end'] as Timestamp).toDate(), location: json['location'], // TODO(IoanaAlexandru): Allow users to set event colours in settings color: type.color, @@ -83,7 +102,7 @@ extension UniEventExtension on UniEvent { type: type, name: json['name'], // Convert time to UTC and then to local time - start: (json['start'] as Timestamp).toLocalDateTime(), + start: (json['start'] as Timestamp).toDate(), duration: PeriodExtension.fromJSON(json['duration']), location: json['location'], // TODO(IoanaAlexandru): Allow users to set event colours in settings @@ -104,7 +123,7 @@ extension UniEventExtension on UniEvent { id: id, type: type, name: json['name'], - start: (json['start'] as Timestamp).toLocalDateTime(), + start: (json['start'] as Timestamp).toDate(), duration: PeriodExtension.fromJSON(json['duration']), location: json['location'], color: type.color, @@ -123,7 +142,7 @@ extension UniEventExtension on UniEvent { type: type, name: json['name'], // Convert time to UTC and then to local time - start: (json['start'] as Timestamp).toLocalDateTime(), + start: (json['start'] as Timestamp).toDate(), duration: PeriodExtension.fromJSON(json['duration']), location: json['location'], // TODO(IoanaAlexandru): Allow users to set event colours in settings @@ -193,7 +212,7 @@ extension AcademicCalendarExtension on AcademicCalendar { } } -class UniEventProvider extends EventProvider +class UniEventProvider extends DefaultEventProvider with ChangeNotifier { UniEventProvider({AuthProvider authProvider, PersonProvider personProvider}) : _authProvider = authProvider ?? AuthProvider(), @@ -301,11 +320,11 @@ class UniEventProvider extends EventProvider @override Stream> getAllDayEventsIntersecting( - DateInterval interval) { + DateTimeRange interval) { return _events.map((events) => events .map((event) => event.generateInstances(intersectingInterval: interval)) .expand((i) => i) - .allDayEvents + .where((event) => event.isAllDay) .followedBy(_calendars.values.map((cal) { final List events = cal.holidays + cal.exams; return events @@ -321,25 +340,25 @@ class UniEventProvider extends EventProvider @override Stream> getPartDayEventsIntersecting( - LocalDate date) { + DateTime date) { return _events.map((events) => events .map((event) => event.generateInstances( - intersectingInterval: DateInterval(date, date))) + intersectingInterval: DateTimeRange(start: date, end: date))) .expand((i) => i) - .partDayEvents); + .where((event) => event.isPartDay)); } - Future> getUpcomingEvents(LocalDate date, + Future> getUpcomingEvents(DateTime date, {int limit = 3}) async { return _events .map((events) => events .where((event) => !(event is AllDayUniEvent)) .map((event) => event.generateInstances( - intersectingInterval: DateInterval(date, date.addDays(6)))) + intersectingInterval: + DateTimeRange(start: date, end: date.addDays(6)))) .expand((i) => i) .sortedByStartLength() - .where((element) => - element.end.toDateTimeLocal().isAfter(DateTime.now())) + .where((element) => element.end.isAfter(DateTime.now())) .take(limit)) .first; } diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index ab3675547..7e467e004 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -1,765 +1,768 @@ -// import 'package:acs_upb_mobile/authentication/model/user.dart'; -// import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; -// import 'package:acs_upb_mobile/generated/l10n.dart'; -// import 'package:acs_upb_mobile/navigation/routes.dart'; -// import 'package:acs_upb_mobile/pages/classes/model/class.dart'; -// import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; -// import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; -// import 'package:acs_upb_mobile/pages/filter/view/relevance_picker.dart'; -// import 'package:acs_upb_mobile/pages/people/model/person.dart'; -// import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; -// import 'package:acs_upb_mobile/pages/people/view/people_page.dart'; -// import 'package:acs_upb_mobile/pages/timetable/model/academic_calendar.dart'; -// import 'package:acs_upb_mobile/pages/timetable/model/events/all_day_event.dart'; -// import 'package:acs_upb_mobile/pages/timetable/model/events/class_event.dart'; -// import 'package:acs_upb_mobile/pages/timetable/model/events/recurring_event.dart'; -// import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -// import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; -// import 'package:acs_upb_mobile/resources/custom_icons.dart'; -// import 'package:acs_upb_mobile/resources/locale_provider.dart'; -// import 'package:acs_upb_mobile/widgets/button.dart'; -// import 'package:acs_upb_mobile/widgets/dialog.dart'; -// import 'package:acs_upb_mobile/widgets/scaffold.dart'; -// import 'package:acs_upb_mobile/widgets/selectable.dart'; -// import 'package:acs_upb_mobile/widgets/toast.dart'; -// import 'package:dotted_line/dotted_line.dart'; -// import 'package:flutter/material.dart'; -// import 'package:flutter/rendering.dart'; -// import 'package:flutter_feather_icons/flutter_feather_icons.dart'; -// import 'package:provider/provider.dart'; -// import 'package:rrule/rrule.dart'; -// import 'package:time_machine/time_machine.dart' as time_machine show DayOfWeek; -// import 'package:time_machine/time_machine.dart' hide DayOfWeek; -// import 'package:time_machine/time_machine_text_patterns.dart'; -// -// class AddEventView extends StatefulWidget { -// /// If the `id` of [initialEvent] is not null, this acts like an "Edit event" -// /// page starting from the info in [initialEvent]. Otherwise, it acts like an -// /// "Add event" page with optional default values based on [initialEvent]. -// const AddEventView({Key key, this.initialEvent}) : super(key: key); -// -// final UniEvent initialEvent; -// -// @override -// _AddEventViewState createState() => _AddEventViewState(); -// } -// -// class _AddEventViewState extends State { -// final formKey = GlobalKey(); -// -// TextEditingController locationController; -// RelevanceController relevanceController = RelevanceController(); -// -// UniEventType selectedEventType; -// ClassHeader selectedClass; -// Person selectedTeacher; -// String selectedCalendar; -// LocalTime startTime; -// Period duration; -// Map weekSelected = { -// WeekType.odd: null, -// WeekType.even: null, -// }; -// Map<_DayOfWeek, bool> weekDaySelected = { -// _DayOfWeek.monday: false, -// _DayOfWeek.tuesday: false, -// _DayOfWeek.wednesday: false, -// _DayOfWeek.thursday: false, -// _DayOfWeek.friday: false, -// _DayOfWeek.saturday: false, -// _DayOfWeek.sunday: false, -// }; -// -// int selectedSemester = 1; -// -// AllDayUniEvent get semester => -// calendars[selectedCalendar]?.semesters?.elementAt(selectedSemester - 1); -// -// List classHeaders = []; -// List classTeachers = []; -// User user; -// Map calendars = {}; -// -// @override -// void initState() { -// super.initState(); -// -// user = -// Provider.of(context, listen: false).currentUserFromCache; -// Provider.of(context, listen: false) -// .fetchClassHeaders(uid: user.uid) -// .then((headers) => setState(() => classHeaders = headers)); -// Provider.of(context, listen: false) -// .fetchPeople() -// .then((teachers) => setState(() => classTeachers = teachers)); -// Provider.of(context, listen: false) -// .fetchCalendars() -// .then((calendars) { -// setState(() { -// this.calendars = calendars; -// selectedCalendar = calendars.keys.first; -// }); -// -// if (widget.initialEvent?.id != null) { -// selectedCalendar = widget.initialEvent.calendar.id; -// final AllDayUniEvent secondSemester = -// widget.initialEvent.calendar.semesters.last; -// selectedSemester = -// DateInterval(secondSemester.startDate, secondSemester.endDate) -// .contains(widget.initialEvent.start.calendarDate) -// ? 2 -// : 1; -// } else { -// bool foundSemester = false; -// for (final calendar in calendars.entries) { -// for (final semester in calendar.value.semesters) { -// final LocalDate date = -// widget.initialEvent.start.calendarDate ?? LocalDate.today(); -// if (date.isBeforeOrDuring(semester)) { -// // semester.id is represented as "semesterN", where "semester0" is the first semester -// selectedSemester = -// 1 + int.tryParse(semester.id[semester.id.length - 1]); -// selectedCalendar = calendar.key; -// foundSemester = true; -// break; -// } -// } -// if (foundSemester) break; -// } -// if (!foundSemester) { -// selectedCalendar = calendars.entries.last.value.id; -// selectedSemester = 2; -// } -// } -// -// if (widget.initialEvent != null && -// widget.initialEvent is RecurringUniEvent) { -// final RecurringUniEvent event = widget.initialEvent; -// if (event.rrule.interval != 1) { -// final rule = WeekYearRules.iso; -// if (rule.getWeekOfWeekYear(semester.start.calendarDate) == -// rule.getWeekOfWeekYear(event.start.calendarDate)) { -// // Week is odd -// weekSelected[WeekType.even] = false; -// weekSelected[WeekType.odd] = true; -// } else { -// // Week is even -// weekSelected[WeekType.even] = true; -// weekSelected[WeekType.odd] = false; -// } -// } -// } -// -// setState(() { -// weekSelected[WeekType.even] ??= true; -// weekSelected[WeekType.odd] ??= true; -// }); -// }); -// -// selectedEventType = widget.initialEvent?.type; -// selectedClass = widget.initialEvent?.classHeader; -// selectedTeacher = widget.initialEvent is ClassEvent -// ? (widget.initialEvent as ClassEvent).teacher -// : null; -// locationController = -// TextEditingController(text: widget.initialEvent?.location ?? ''); -// -// final startHour = widget.initialEvent?.start?.hourOfDay ?? 8; -// duration = widget.initialEvent?.duration ?? const Period(hours: 2); -// startTime = LocalTime(startHour, 0, 0); -// -// var initialWeekDays = [ -// _DayOfWeek.from(widget.initialEvent?.start?.dayOfWeek) ?? -// _DayOfWeek.monday -// ]; -// if (widget.initialEvent != null && -// widget.initialEvent is RecurringUniEvent && -// (widget.initialEvent as RecurringUniEvent) -// .rrule -// .byWeekDays -// .isNotEmpty) { -// initialWeekDays = (widget.initialEvent as RecurringUniEvent) -// .rrule -// .byWeekDays -// .map((entry) => _DayOfWeek.from(entry.day)) -// .toList(); -// } -// for (final initialWeekDay in initialWeekDays) { -// weekDaySelected[initialWeekDay] = true; -// } -// } -// -// @override -// Widget build(BuildContext context) { -// return AppScaffold( -// title: Text(widget.initialEvent?.id == null -// ? S.current.actionAddEvent -// : S.current.actionEditEvent), -// actions: widget.initialEvent?.id == null -// ? [_saveButton()] -// : [ -// _saveButton(), -// _deleteButton(), -// ], -// body: ListView( -// children: [ -// Padding( -// padding: const EdgeInsets.only(left: 16, right: 16), -// child: Form( -// key: formKey, -// child: Column( -// mainAxisSize: MainAxisSize.min, -// children: [ -// Row( -// mainAxisSize: MainAxisSize.min, -// children: [ -// Expanded( -// child: DropdownButtonFormField( -// decoration: InputDecoration( -// labelText: S.current.labelUniversityYear, -// prefixIcon: -// const Icon(Icons.calendar_today_outlined), -// ), -// value: selectedCalendar, -// items: calendars.keys.map((key) { -// final year = int.tryParse(key); -// return DropdownMenuItem( -// value: key, -// child: Text( -// year != null ? '$year-${year + 1}' : key), -// ); -// }).toList(), -// onChanged: (selection) => -// selectedCalendar = selection, -// ), -// ), -// const SizedBox(width: 16), -// Expanded( -// child: DropdownButtonFormField( -// decoration: InputDecoration( -// labelText: S.current.labelSemester, -// prefixIcon: const Icon(FeatherIcons.columns), -// ), -// value: selectedSemester, -// items: [1, 2] -// .map((semester) => DropdownMenuItem( -// value: semester, -// child: Text(semester.toString()), -// )) -// .toList(), -// onChanged: (selection) => -// selectedSemester = selection, -// ), -// ), -// ], -// ), -// RelevanceFormField( -// controller: relevanceController, -// validator: (_) { -// if (relevanceController.customRelevance?.isEmpty ?? -// true) { -// return S.current.warningYouNeedToSelectAtLeastOne; -// } -// return null; -// }, -// ), -// DropdownButtonFormField( -// decoration: InputDecoration( -// labelText: S.current.labelType, -// prefixIcon: const Icon(Icons.category_outlined), -// ), -// value: selectedEventType, -// items: UniEventTypeExtension.classTypes -// .map( -// (type) => DropdownMenuItem( -// value: type, -// child: Text(type.toLocalizedString()), -// ), -// ) -// .toList(), -// onChanged: (selection) { -// formKey.currentState.validate(); -// setState(() => selectedEventType = selection); -// }, -// validator: (selection) { -// if (selection == null) { -// return S.current.errorEventTypeCannotBeEmpty; -// } -// return null; -// }, -// ), -// if (selectedEventType != null) -// Column( -// children: [ -// if (classHeaders.isNotEmpty) -// DropdownButtonFormField( -// isExpanded: true, -// decoration: InputDecoration( -// labelText: S.current.labelClass, -// prefixIcon: const Icon(FeatherIcons.bookOpen), -// ), -// value: selectedClass, -// items: classHeaders -// .map( -// (header) => DropdownMenuItem( -// value: header, child: Text(header.name)), -// ) -// .toList(), -// onChanged: (selection) { -// formKey.currentState.validate(); -// setState(() => selectedClass = selection); -// }, -// validator: (selection) { -// if (selection == null) { -// return S.current.errorClassCannotBeEmpty; -// } -// return null; -// }, -// ), -// if ([UniEventType.lecture].contains(selectedEventType)) -// AutocompletePerson( -// key: const Key('AutocompleteLecturer'), -// labelText: S.current.labelLecturer, -// formKey: formKey, -// onSaved: (value) => selectedTeacher = value, -// classTeachers: classTeachers, -// personDisplayed: selectedTeacher, -// ), -// TextFormField( -// controller: locationController, -// decoration: InputDecoration( -// labelText: S.current.labelLocation, -// prefixIcon: const Icon(FeatherIcons.mapPin), -// ), -// onChanged: (_) => setState(() {}), -// ), -// timeIntervalPicker(), -// if (weekSelected[WeekType.odd] != null && -// weekSelected[WeekType.even] != null) -// SelectableFormField( -// key: const ValueKey('week_picker'), -// icon: FeatherIcons.calendar, -// label: S.current.labelWeek, -// initialValues: weekSelected, -// validator: (selection) { -// if (selection.values -// .where((e) => e != false) -// .isEmpty) { -// return S -// .of(context) -// .warningYouNeedToSelectAtLeastOne; -// } -// return null; -// }, -// ), -// SelectableFormField( -// key: const ValueKey('day_picker'), -// icon: Icons.today_outlined, -// label: S.current.labelDay, -// initialValues: weekDaySelected, -// validator: (selection) { -// if (selection.values -// .where((e) => e != false) -// .isEmpty) { -// return S -// .of(context) -// .warningYouNeedToSelectAtLeastOne; -// } -// return null; -// }, -// ), -// ], -// ), -// const SizedBox(width: 16), -// ], -// ), -// ), -// ), -// ], -// ), -// ); -// } -// -// AppDialog _deletionConfirmationDialog(BuildContext context) => AppDialog( -// icon: const Icon(Icons.delete_outlined), -// title: S.current.actionDeleteEvent, -// info: S.current.messageThisCouldAffectOtherStudents, -// message: S.current.messageDeleteEvent, -// actions: [ -// AppButton( -// text: S.current.actionDeleteEvent, -// width: 130, -// onTap: () async { -// final res = -// await Provider.of(context, listen: false) -// .deleteEvent(widget.initialEvent); -// if (res) { -// Navigator.of(context) -// .popUntil(ModalRoute.withName(Routes.home)); -// AppToast.show(S.current.messageEventDeleted); -// } -// }, -// ) -// ], -// ); -// -// AppScaffoldAction _saveButton() => AppScaffoldAction( -// text: S.current.buttonSave, -// onPressed: () async { -// if (!formKey.currentState.validate()) return; -// -// LocalDateTime start = semester.startDate.at(startTime); -// if (weekSelected[WeekType.even] && !weekSelected[WeekType.odd]) { -// // Event is every even week, add a week to start date -// start = start.add(const Period(weeks: 1)); -// } -// -// final rrule = RecurrenceRule( -// frequency: Frequency.weekly, -// byWeekDays: (Map<_DayOfWeek, bool>.from(weekDaySelected) -// ..removeWhere((key, value) => !value)) -// .keys -// .map((weekDay) => ByWeekDayEntry(weekDay)) -// .toSet(), -// interval: -// weekSelected[WeekType.odd] != weekSelected[WeekType.even] -// ? 2 -// : 1, -// until: semester.endDate.add(const Period(days: 1)).atMidnight()); -// -// final event = ClassEvent( -// teacher: selectedTeacher, -// rrule: rrule, -// start: start, -// duration: duration, -// id: widget.initialEvent?.id, -// relevance: relevanceController.customRelevance, -// degree: relevanceController.degree, -// location: locationController.text, -// type: selectedEventType, -// classHeader: selectedClass, -// calendar: calendars[selectedCalendar], -// addedBy: Provider.of(context, listen: false) -// .currentUserFromCache -// .uid); -// -// if (widget.initialEvent?.id == null) { -// final res = -// await Provider.of(context, listen: false) -// .addEvent(event); -// if (res) { -// Navigator.of(context).pop(); -// AppToast.show(S.current.messageEventAdded); -// } -// } else { -// final res = -// await Provider.of(context, listen: false) -// .updateEvent(event); -// if (res) { -// Navigator.of(context).popUntil(ModalRoute.withName(Routes.home)); -// AppToast.show(S.current.messageEventEdited); -// } -// } -// }, -// ); -// -// AppScaffoldAction _deleteButton() => AppScaffoldAction( -// icon: Icons.more_vert_outlined, -// items: { -// S.current.actionDeleteEvent: () => -// showDialog(context: context, builder: _deletionConfirmationDialog) -// }, -// onPressed: () => -// showDialog(context: context, builder: _deletionConfirmationDialog), -// ); -// -// Widget timeIntervalPicker() { -// final endTime = startTime.add(duration); -// final textColor = Theme.of(context).textTheme.headline4.color; -// return Padding( -// padding: const EdgeInsets.only(top: 10), -// child: Row( -// children: [ -// Padding( -// padding: const EdgeInsets.all(12), -// child: Icon( -// FeatherIcons.clock, -// color: CustomIcons.formIconColor(Theme.of(context)), -// ), -// ), -// TextButton( -// style: ButtonStyle( -// padding: MaterialStateProperty.all(EdgeInsets.zero), -// ), -// onPressed: () async { -// final TimeOfDay start = await showTimePicker( -// context: context, -// initialTime: startTime.toTimeOfDay(), -// ); -// setState(() => startTime = start.toLocalTime()); -// }, -// child: Text( -// startTime.toString('HH:mm'), -// style: Theme.of(context).textTheme.headline4, -// ), -// ), -// Expanded( -// child: Padding( -// padding: const EdgeInsets.symmetric(horizontal: 12), -// child: Column( -// children: [ -// Text( -// duration.toString().replaceAll(RegExp(r'[PT]'), ''), -// style: Theme.of(context) -// .textTheme -// .bodyText1 -// .copyWith(color: textColor), -// ), -// DottedLine( -// lineThickness: 4, -// dashRadius: 2, -// dashColor: textColor, -// ), -// // Text-sized box so that the line is centered -// SizedBox( -// height: Theme.of(context).textTheme.bodyText1.fontSize), -// ], -// ), -// ), -// ), -// TextButton( -// style: ButtonStyle( -// padding: MaterialStateProperty.all(EdgeInsets.zero), -// ), -// onPressed: () async { -// final TimeOfDay end = await showTimePicker( -// context: context, -// initialTime: startTime.add(duration).toTimeOfDay(), -// ); -// setState(() => duration = -// Period.differenceBetweenTimes(startTime, end.toLocalTime())); -// }, -// child: Text( -// endTime.toString('HH:mm'), -// style: Theme.of(context).textTheme.headline4, -// ), -// ), -// const SizedBox(width: 12), -// ], -// ), -// ); -// } -// } -// -// class RelevanceFormField extends FormField> { -// RelevanceFormField({ -// @required this.controller, -// String Function(List) validator, -// Key key, -// }) : super( -// key: key, -// autovalidateMode: AutovalidateMode.onUserInteraction, -// validator: validator, -// builder: (FormFieldState> state) { -// controller.onChanged = () { -// state.didChange(controller.customRelevance); -// }; -// final context = state.context; -// return Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// RelevancePicker( -// canBePrivate: false, -// canBeForEveryone: false, -// filterProvider: Provider.of(context), -// controller: controller, -// ), -// if (state.hasError) -// Padding( -// padding: const EdgeInsets.only(top: 10), -// child: Text( -// state.errorText, -// style: Theme.of(context) -// .textTheme -// .caption -// .copyWith(color: Theme.of(context).errorColor), -// ), -// ), -// ], -// ); -// }, -// ); -// -// final RelevanceController controller; -// } -// -// class SelectableFormField extends FormField> { -// SelectableFormField({ -// @required Map initialValues, -// @required IconData icon, -// @required String label, -// String Function(Map) validator, -// Key key, -// }) : super( -// autovalidateMode: AutovalidateMode.onUserInteraction, -// initialValue: initialValues, -// key: key, -// validator: validator, -// builder: (state) { -// final context = state.context; -// final labels = state.value.keys.toList(); -// return Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// IntrinsicHeight( -// child: Padding( -// padding: const EdgeInsets.only(top: 12, left: 12), -// child: Row( -// mainAxisSize: MainAxisSize.min, -// children: [ -// Icon(icon, -// color: -// CustomIcons.formIconColor(Theme.of(context))), -// const SizedBox(width: 12), -// Expanded( -// child: Column( -// mainAxisSize: MainAxisSize.min, -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Expanded( -// child: Text( -// label, -// style: Theme.of(context) -// .textTheme -// .caption -// .apply( -// color: Theme.of(context).hintColor), -// ), -// ), -// const SizedBox(height: 10), -// Row( -// children: [ -// Expanded( -// child: Container( -// height: 40, -// child: ListView.builder( -// itemCount: labels.length, -// scrollDirection: Axis.horizontal, -// itemBuilder: (context, index) { -// return Row( -// children: [ -// Selectable( -// label: labels[index] -// .toLocalizedString(), -// initiallySelected: -// state.value[labels[index]], -// onSelected: (selected) { -// state.value[labels[index]] = -// selected; -// state.didChange(state.value); -// }, -// ), -// const SizedBox(width: 10), -// ], -// ); -// }, -// ), -// ), -// ), -// ], -// ), -// ], -// ), -// ), -// ], -// ), -// ), -// ), -// if (state.hasError) -// Padding( -// padding: const EdgeInsets.only(top: 10), -// child: Text( -// state.errorText, -// style: Theme.of(context) -// .textTheme -// .caption -// .copyWith(color: Theme.of(context).errorColor), -// ), -// ), -// ], -// ); -// }, -// ); -// } -// -// class _DayOfWeek extends time_machine.DayOfWeek with Localizable { -// const _DayOfWeek(int value) : super(value); -// -// _DayOfWeek.from(time_machine.DayOfWeek dayOfWeek) : super(dayOfWeek.value); -// -// @override -// String toLocalizedString() { -// final helperDate = LocalDate.today().next(this); -// return LocalDatePattern.createWithCurrentCulture('ddd') -// .format(helperDate) -// .substring(0, 3); -// } -// -// static const _DayOfWeek monday = _DayOfWeek(1); -// static const _DayOfWeek tuesday = _DayOfWeek(2); -// static const _DayOfWeek wednesday = _DayOfWeek(3); -// static const _DayOfWeek thursday = _DayOfWeek(4); -// static const _DayOfWeek friday = _DayOfWeek(5); -// static const _DayOfWeek saturday = _DayOfWeek(6); -// static const _DayOfWeek sunday = _DayOfWeek(7); -// } -// -// class WeekType with Localizable { -// const WeekType(this._value); -// -// final int _value; -// -// int get value => _value; -// -// static const WeekType odd = WeekType(0); -// static const WeekType even = WeekType(1); -// -// @override -// int get hashCode => _value.hashCode; -// -// @override -// bool operator ==(dynamic other) => -// other is WeekType && other._value == _value || -// other is int && other == _value; -// -// @override -// String toLocalizedString() { -// switch (_value) { -// case 0: -// return S.current.labelOdd; -// case 1: -// return S.current.labelEven; -// default: -// return ''; -// } -// } -// } -// -// extension LocalTimeConversion on LocalTime { -// TimeOfDay toTimeOfDay() => TimeOfDay(hour: hourOfDay, minute: minuteOfHour); -// } -// -// extension TimeOfDayConversion on TimeOfDay { -// LocalTime toLocalTime() => LocalTime(hour, minute, 0); -// } -// -// extension LocalDateComparisons on LocalDate { -// bool isDuring(AllDayUniEvent semester) { -// return DateInterval(semester.startDate, semester.endDate).contains(this); -// } -// -// bool isBeforeOrDuring(AllDayUniEvent semester) { -// if (compareTo(semester.startDate) < 0) return true; -// return isDuring(semester); -// } -// } +import 'package:dotted_line/dotted_line.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:provider/provider.dart'; +import 'package:rrule/rrule.dart'; +import 'package:time_machine/time_machine.dart' as time_machine show DayOfWeek; +import 'package:time_machine/time_machine.dart' hide DayOfWeek; +import 'package:time_machine/time_machine_text_patterns.dart'; + +import '../../../../authentication/model/user.dart'; +import '../../../../authentication/service/auth_provider.dart'; +import '../../../../generated/l10n.dart'; +import '../../../../navigation/routes.dart'; +import '../../../../resources/custom_icons.dart'; +import '../../../../resources/locale_provider.dart'; +import '../../../../widgets/button.dart'; +import '../../../../widgets/dialog.dart'; +import '../../../../widgets/scaffold.dart'; +import '../../../../widgets/selectable.dart'; +import '../../../../widgets/toast.dart'; +import '../../../classes/model/class.dart'; +import '../../../classes/service/class_provider.dart'; +import '../../../filter/service/filter_provider.dart'; +import '../../../filter/view/relevance_picker.dart'; +import '../../../people/model/person.dart'; +import '../../../people/service/person_provider.dart'; +import '../../../people/view/people_page.dart'; +import '../../model/academic_calendar.dart'; +import '../../model/events/all_day_event.dart'; +import '../../model/events/class_event.dart'; +import '../../model/events/recurring_event.dart'; +import '../../model/events/uni_event.dart'; +import '../../service/uni_event_provider.dart'; +import '../../timetable_utils.dart'; + +class AddEventView extends StatefulWidget { + /// If the `id` of [initialEvent] is not null, this acts like an "Edit event" + /// page starting from the info in [initialEvent]. Otherwise, it acts like an + /// "Add event" page with optional default values based on [initialEvent]. + const AddEventView({Key key, this.initialEvent}) : super(key: key); + + final UniEvent initialEvent; + + @override + _AddEventViewState createState() => _AddEventViewState(); +} + +class _AddEventViewState extends State { + final formKey = GlobalKey(); + + TextEditingController locationController; + RelevanceController relevanceController = RelevanceController(); + + UniEventType selectedEventType; + ClassHeader selectedClass; + Person selectedTeacher; + String selectedCalendar; + LocalTime startTime; + Period duration; + Map weekSelected = { + WeekType.odd: null, + WeekType.even: null, + }; + Map<_DayOfWeek, bool> weekDaySelected = { + _DayOfWeek.monday: false, + _DayOfWeek.tuesday: false, + _DayOfWeek.wednesday: false, + _DayOfWeek.thursday: false, + _DayOfWeek.friday: false, + _DayOfWeek.saturday: false, + _DayOfWeek.sunday: false, + }; + + int selectedSemester = 1; + + AllDayUniEvent get semester => + calendars[selectedCalendar]?.semesters?.elementAt(selectedSemester - 1); + + List classHeaders = []; + List classTeachers = []; + User user; + Map calendars = {}; + + @override + void initState() { + super.initState(); + + user = + Provider.of(context, listen: false).currentUserFromCache; + Provider.of(context, listen: false) + .fetchClassHeaders(uid: user.uid) + .then((headers) => setState(() => classHeaders = headers)); + Provider.of(context, listen: false) + .fetchPeople() + .then((teachers) => setState(() => classTeachers = teachers)); + Provider.of(context, listen: false) + .fetchCalendars() + .then((calendars) { + setState(() { + this.calendars = calendars; + selectedCalendar = calendars.keys.first; + }); + + if (widget.initialEvent?.id != null) { + selectedCalendar = widget.initialEvent.calendar.id; + final AllDayUniEvent secondSemester = + widget.initialEvent.calendar.semesters.last; + selectedSemester = DateTimeRange( + start: secondSemester.startDate, + end: secondSemester.endDate) + .contains(widget.initialEvent.start) + ? 2 + : 1; + } else { + bool foundSemester = false; + for (final calendar in calendars.entries) { + for (final semester in calendar.value.semesters) { + final DateTime date = widget.initialEvent.start ?? DateTime.now(); + if (date.isBeforeOrDuring(semester)) { + // semester.id is represented as "semesterN", where "semester0" is the first semester + selectedSemester = + 1 + int.tryParse(semester.id[semester.id.length - 1]); + selectedCalendar = calendar.key; + foundSemester = true; + break; + } + } + if (foundSemester) break; + } + if (!foundSemester) { + selectedCalendar = calendars.entries.last.value.id; + selectedSemester = 2; + } + } + + if (widget.initialEvent != null && + widget.initialEvent is RecurringUniEvent) { + final RecurringUniEvent event = widget.initialEvent; + if (event.rrule.interval != 1) { + final rule = WeekYearRules.iso; + if (rule.getWeekOfWeekYear(LocalDate.dateTime(semester.start)) == + rule.getWeekOfWeekYear(LocalDate.dateTime(event.start))) { + // Week is odd + weekSelected[WeekType.even] = false; + weekSelected[WeekType.odd] = true; + } else { + // Week is even + weekSelected[WeekType.even] = true; + weekSelected[WeekType.odd] = false; + } + } + } + + setState(() { + weekSelected[WeekType.even] ??= true; + weekSelected[WeekType.odd] ??= true; + }); + }); + + selectedEventType = widget.initialEvent?.type; + selectedClass = widget.initialEvent?.classHeader; + selectedTeacher = widget.initialEvent is ClassEvent + ? (widget.initialEvent as ClassEvent).teacher + : null; + locationController = + TextEditingController(text: widget.initialEvent?.location ?? ''); + + final startHour = widget.initialEvent?.start?.hour ?? 8; + duration = widget.initialEvent?.duration ?? const Period(hours: 2); + startTime = LocalTime(startHour, 0, 0); + + var initialWeekDays = [ + _DayOfWeek.from(widget.initialEvent?.start?.dayOfWeek) ?? + _DayOfWeek.monday + ]; + if (widget.initialEvent != null && + widget.initialEvent is RecurringUniEvent && + (widget.initialEvent as RecurringUniEvent) + .rrule + .byWeekDays + .isNotEmpty) { + initialWeekDays = (widget.initialEvent as RecurringUniEvent) + .rrule + .byWeekDays + .map((entry) => _DayOfWeek.from(entry.day)) + .toList(); + } + for (final initialWeekDay in initialWeekDays) { + weekDaySelected[initialWeekDay] = true; + } + } + + @override + Widget build(BuildContext context) { + return AppScaffold( + title: Text(widget.initialEvent?.id == null + ? S.current.actionAddEvent + : S.current.actionEditEvent), + actions: widget.initialEvent?.id == null + ? [_saveButton()] + : [ + _saveButton(), + _deleteButton(), + ], + body: ListView( + children: [ + Padding( + padding: const EdgeInsets.only(left: 16, right: 16), + child: Form( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: DropdownButtonFormField( + decoration: InputDecoration( + labelText: S.current.labelUniversityYear, + prefixIcon: + const Icon(Icons.calendar_today_outlined), + ), + value: selectedCalendar, + items: calendars.keys.map((key) { + final year = int.tryParse(key); + return DropdownMenuItem( + value: key, + child: Text( + year != null ? '$year-${year + 1}' : key), + ); + }).toList(), + onChanged: (selection) => + selectedCalendar = selection, + ), + ), + const SizedBox(width: 16), + Expanded( + child: DropdownButtonFormField( + decoration: InputDecoration( + labelText: S.current.labelSemester, + prefixIcon: const Icon(FeatherIcons.columns), + ), + value: selectedSemester, + items: [1, 2] + .map((semester) => DropdownMenuItem( + value: semester, + child: Text(semester.toString()), + )) + .toList(), + onChanged: (selection) => + selectedSemester = selection, + ), + ), + ], + ), + RelevanceFormField( + controller: relevanceController, + validator: (_) { + if (relevanceController.customRelevance?.isEmpty ?? + true) { + return S.current.warningYouNeedToSelectAtLeastOne; + } + return null; + }, + ), + DropdownButtonFormField( + decoration: InputDecoration( + labelText: S.current.labelType, + prefixIcon: const Icon(Icons.category_outlined), + ), + value: selectedEventType, + items: UniEventTypeExtension.classTypes + .map( + (type) => DropdownMenuItem( + value: type, + child: Text(type.toLocalizedString()), + ), + ) + .toList(), + onChanged: (selection) { + formKey.currentState.validate(); + setState(() => selectedEventType = selection); + }, + validator: (selection) { + if (selection == null) { + return S.current.errorEventTypeCannotBeEmpty; + } + return null; + }, + ), + if (selectedEventType != null) + Column( + children: [ + if (classHeaders.isNotEmpty) + DropdownButtonFormField( + isExpanded: true, + decoration: InputDecoration( + labelText: S.current.labelClass, + prefixIcon: const Icon(FeatherIcons.bookOpen), + ), + value: selectedClass, + items: classHeaders + .map( + (header) => DropdownMenuItem( + value: header, child: Text(header.name)), + ) + .toList(), + onChanged: (selection) { + formKey.currentState.validate(); + setState(() => selectedClass = selection); + }, + validator: (selection) { + if (selection == null) { + return S.current.errorClassCannotBeEmpty; + } + return null; + }, + ), + if ([UniEventType.lecture].contains(selectedEventType)) + AutocompletePerson( + key: const Key('AutocompleteLecturer'), + labelText: S.current.labelLecturer, + formKey: formKey, + onSaved: (value) => selectedTeacher = value, + classTeachers: classTeachers, + personDisplayed: selectedTeacher, + ), + TextFormField( + controller: locationController, + decoration: InputDecoration( + labelText: S.current.labelLocation, + prefixIcon: const Icon(FeatherIcons.mapPin), + ), + onChanged: (_) => setState(() {}), + ), + timeIntervalPicker(), + if (weekSelected[WeekType.odd] != null && + weekSelected[WeekType.even] != null) + SelectableFormField( + key: const ValueKey('week_picker'), + icon: FeatherIcons.calendar, + label: S.current.labelWeek, + initialValues: weekSelected, + validator: (selection) { + if (selection.values + .where((e) => e != false) + .isEmpty) { + return S + .of(context) + .warningYouNeedToSelectAtLeastOne; + } + return null; + }, + ), + SelectableFormField( + key: const ValueKey('day_picker'), + icon: Icons.today_outlined, + label: S.current.labelDay, + initialValues: weekDaySelected, + validator: (selection) { + if (selection.values + .where((e) => e != false) + .isEmpty) { + return S + .of(context) + .warningYouNeedToSelectAtLeastOne; + } + return null; + }, + ), + ], + ), + const SizedBox(width: 16), + ], + ), + ), + ), + ], + ), + ); + } + + AppDialog _deletionConfirmationDialog(BuildContext context) => AppDialog( + icon: const Icon(Icons.delete_outlined), + title: S.current.actionDeleteEvent, + info: S.current.messageThisCouldAffectOtherStudents, + message: S.current.messageDeleteEvent, + actions: [ + AppButton( + text: S.current.actionDeleteEvent, + width: 130, + onTap: () async { + final res = + await Provider.of(context, listen: false) + .deleteEvent(widget.initialEvent); + if (res) { + Navigator.of(context) + .popUntil(ModalRoute.withName(Routes.home)); + AppToast.show(S.current.messageEventDeleted); + } + }, + ) + ], + ); + + AppScaffoldAction _saveButton() => AppScaffoldAction( + text: S.current.buttonSave, + onPressed: () async { + if (!formKey.currentState.validate()) return; + + DateTime start = semester.startDate.at(startTime); + if (weekSelected[WeekType.even] && !weekSelected[WeekType.odd]) { + // Event is every even week, add a week to start date + start = start.add(const Period(weeks: 1)); + } + + final rrule = RecurrenceRule( + frequency: Frequency.weekly, + byWeekDays: (Map<_DayOfWeek, bool>.from(weekDaySelected) + ..removeWhere((key, value) => !value)) + .keys + .map((weekDay) => ByWeekDayEntry(weekDay)) + .toSet(), + interval: + weekSelected[WeekType.odd] != weekSelected[WeekType.even] + ? 2 + : 1, + until: semester.endDate.add(const Period(days: 1)).atMidnight()); + + final event = ClassEvent( + teacher: selectedTeacher, + rrule: rrule, + start: start, + duration: duration, + id: widget.initialEvent?.id, + relevance: relevanceController.customRelevance, + degree: relevanceController.degree, + location: locationController.text, + type: selectedEventType, + classHeader: selectedClass, + calendar: calendars[selectedCalendar], + addedBy: Provider.of(context, listen: false) + .currentUserFromCache + .uid); + + if (widget.initialEvent?.id == null) { + final res = + await Provider.of(context, listen: false) + .addEvent(event); + if (res) { + Navigator.of(context).pop(); + AppToast.show(S.current.messageEventAdded); + } + } else { + final res = + await Provider.of(context, listen: false) + .updateEvent(event); + if (res) { + Navigator.of(context).popUntil(ModalRoute.withName(Routes.home)); + AppToast.show(S.current.messageEventEdited); + } + } + }, + ); + + AppScaffoldAction _deleteButton() => AppScaffoldAction( + icon: Icons.more_vert_outlined, + items: { + S.current.actionDeleteEvent: () => + showDialog(context: context, builder: _deletionConfirmationDialog) + }, + onPressed: () => + showDialog(context: context, builder: _deletionConfirmationDialog), + ); + + Widget timeIntervalPicker() { + final endTime = startTime.add(duration); + final textColor = Theme.of(context).textTheme.headline4.color; + return Padding( + padding: const EdgeInsets.only(top: 10), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Icon( + FeatherIcons.clock, + color: CustomIcons.formIconColor(Theme.of(context)), + ), + ), + TextButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () async { + final TimeOfDay start = await showTimePicker( + context: context, + initialTime: startTime.toTimeOfDay(), + ); + setState(() => startTime = start.toLocalTime()); + }, + child: Text( + startTime.toString('HH:mm'), + style: Theme.of(context).textTheme.headline4, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( + children: [ + Text( + duration.toString().replaceAll(RegExp(r'[PT]'), ''), + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith(color: textColor), + ), + DottedLine( + lineThickness: 4, + dashRadius: 2, + dashColor: textColor, + ), + // Text-sized box so that the line is centered + SizedBox( + height: Theme.of(context).textTheme.bodyText1.fontSize), + ], + ), + ), + ), + TextButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () async { + final TimeOfDay end = await showTimePicker( + context: context, + initialTime: startTime.add(duration).toTimeOfDay(), + ); + setState(() => duration = + Period.differenceBetweenTimes(startTime, end.toLocalTime())); + }, + child: Text( + endTime.toString('HH:mm'), + style: Theme.of(context).textTheme.headline4, + ), + ), + const SizedBox(width: 12), + ], + ), + ); + } +} + +class RelevanceFormField extends FormField> { + RelevanceFormField({ + @required this.controller, + String Function(List) validator, + Key key, + }) : super( + key: key, + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: validator, + builder: (FormFieldState> state) { + controller.onChanged = () { + state.didChange(controller.customRelevance); + }; + final context = state.context; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RelevancePicker( + canBePrivate: false, + canBeForEveryone: false, + filterProvider: Provider.of(context), + controller: controller, + ), + if (state.hasError) + Padding( + padding: const EdgeInsets.only(top: 10), + child: Text( + state.errorText, + style: Theme.of(context) + .textTheme + .caption + .copyWith(color: Theme.of(context).errorColor), + ), + ), + ], + ); + }, + ); + + final RelevanceController controller; +} + +class SelectableFormField extends FormField> { + SelectableFormField({ + @required Map initialValues, + @required IconData icon, + @required String label, + String Function(Map) validator, + Key key, + }) : super( + autovalidateMode: AutovalidateMode.onUserInteraction, + initialValue: initialValues, + key: key, + validator: validator, + builder: (state) { + final context = state.context; + final labels = state.value.keys.toList(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.only(top: 12, left: 12), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, + color: + CustomIcons.formIconColor(Theme.of(context))), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + label, + style: Theme.of(context) + .textTheme + .caption + .apply( + color: Theme.of(context).hintColor), + ), + ), + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: Container( + height: 40, + child: ListView.builder( + itemCount: labels.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + return Row( + children: [ + Selectable( + label: labels[index] + .toLocalizedString(), + initiallySelected: + state.value[labels[index]], + onSelected: (selected) { + state.value[labels[index]] = + selected; + state.didChange(state.value); + }, + ), + const SizedBox(width: 10), + ], + ); + }, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + if (state.hasError) + Padding( + padding: const EdgeInsets.only(top: 10), + child: Text( + state.errorText, + style: Theme.of(context) + .textTheme + .caption + .copyWith(color: Theme.of(context).errorColor), + ), + ), + ], + ); + }, + ); +} + +class _DayOfWeek extends time_machine.DayOfWeek with Localizable { + const _DayOfWeek(int value) : super(value); + + _DayOfWeek.from(time_machine.DayOfWeek dayOfWeek) : super(dayOfWeek.value); + + @override + String toLocalizedString() { + final helperDate = DateTime.now().next(this); + return LocalDatePattern.createWithCurrentCulture('ddd') + .format(helperDate) + .substring(0, 3); + } + + static const _DayOfWeek monday = _DayOfWeek(1); + static const _DayOfWeek tuesday = _DayOfWeek(2); + static const _DayOfWeek wednesday = _DayOfWeek(3); + static const _DayOfWeek thursday = _DayOfWeek(4); + static const _DayOfWeek friday = _DayOfWeek(5); + static const _DayOfWeek saturday = _DayOfWeek(6); + static const _DayOfWeek sunday = _DayOfWeek(7); +} + +class WeekType with Localizable { + const WeekType(this._value); + + final int _value; + + int get value => _value; + + static const WeekType odd = WeekType(0); + static const WeekType even = WeekType(1); + + @override + int get hashCode => _value.hashCode; + + @override + bool operator ==(dynamic other) => + other is WeekType && other._value == _value || + other is int && other == _value; + + @override + String toLocalizedString() { + switch (_value) { + case 0: + return S.current.labelOdd; + case 1: + return S.current.labelEven; + default: + return ''; + } + } +} + +extension LocalTimeConversion on LocalTime { + TimeOfDay toTimeOfDay() => TimeOfDay(hour: hourOfDay, minute: minuteOfHour); +} + +extension TimeOfDayConversion on TimeOfDay { + LocalTime toLocalTime() => LocalTime(hour, minute, 0); +} + +extension DateTimeComparisons on DateTime { + bool isDuring(AllDayUniEvent semester) { + return DateTimeRange(start: semester.startDate, end: semester.endDate) + .contains(this); + } + + bool isBeforeOrDuring(AllDayUniEvent semester) { + if (compareTo(semester.startDate) < 0) return true; + return isDuring(semester); + } +} From 92578280bdd1178013c057d6eae31e7bffa6336f Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Sat, 21 Aug 2021 00:02:39 +0300 Subject: [PATCH 25/60] Compatibility fixes --- .../timetable/model/events/uni_event.dart | 1 - lib/pages/timetable/timetable_utils.dart | 46 +- lib/pages/timetable/view/date_header.dart | 106 ++-- .../timetable/view/events/add_event_view.dart | 2 +- .../view/events/all_day_event_widget.dart | 497 +++++++++--------- .../timetable/view/events/event_view.dart | 383 +++++++------- .../timetable/view/events/event_widget.dart | 203 +++---- pubspec.lock | 14 + pubspec.yaml | 2 + 9 files changed, 624 insertions(+), 630 deletions(-) diff --git a/lib/pages/timetable/model/events/uni_event.dart b/lib/pages/timetable/model/events/uni_event.dart index 0c33b346e..8b0f30b77 100644 --- a/lib/pages/timetable/model/events/uni_event.dart +++ b/lib/pages/timetable/model/events/uni_event.dart @@ -163,7 +163,6 @@ class UniEventInstance extends Event { super(start: start, end: end); final UniEvent mainEvent; - final String title; final Color color; final String location; diff --git a/lib/pages/timetable/timetable_utils.dart b/lib/pages/timetable/timetable_utils.dart index b1c5c4b81..702fe8721 100644 --- a/lib/pages/timetable/timetable_utils.dart +++ b/lib/pages/timetable/timetable_utils.dart @@ -1,19 +1,15 @@ -// import 'package:flutter/material.dart'; -// import 'package:intl/intl.dart'; +library timetable_utils; +// ignore: implementation_imports +import 'package:timetable/src/utils.dart'; +import 'package:timetable/timetable.dart'; import 'package:flutter/material.dart'; +export 'package:timetable/src/utils.dart'; extension DateTimeExtension on DateTime { - DateTime atMidnight() => DateTime(year, month, day, 0, 0, 0); - DateTime addDays(int noDays) => add(Duration(days: noDays)); - - // from https://github.com/JonasWanke/timetable/blob/a09be6e1e64457e93a448c60ab6851230a3f2a4b/lib/src/utils.dart - bool operator <(DateTime other) => isBefore(other); - bool operator <=(DateTime other) => - isBefore(other) || isAtSameMomentAs(other); - bool operator >(DateTime other) => isAfter(other); - bool operator >=(DateTime other) => isAfter(other) || isAtSameMomentAs(other); + DateTime atMidnight() => DateTime(year, month, day, 0, 0, 0, 0, 0); + DateTime addDays(int noDays) => add(Duration(days: noDays)); } extension DateTimeRangeExtension on DateTimeRange { @@ -21,31 +17,3 @@ extension DateTimeRangeExtension on DateTimeRange { return dateTime >= start && dateTime <= end; } } - - -// -// class CalendarDate { -// CalendarDate(this.year, this.month, this.day); -// -// int year; -// int month; -// int day; -// -// bool equals(CalendarDate other) { -// return year == other.year && month == other.month && day == other.day; -// } -// } -// -// class ClockTime { -// ClockTime(this.hour, this.minute, this.second); -// -// int hour; -// int minute; -// int second; -// -// bool equals(ClockTime other) { -// return hour == other.hour && -// minute == other.minute && -// second == other.second; -// } -// } diff --git a/lib/pages/timetable/view/date_header.dart b/lib/pages/timetable/view/date_header.dart index f29725748..34ae691d3 100644 --- a/lib/pages/timetable/view/date_header.dart +++ b/lib/pages/timetable/view/date_header.dart @@ -3,70 +3,78 @@ import 'package:black_hole_flutter/black_hole_flutter.dart'; import 'package:flutter/material.dart'; import 'package:time_machine/time_machine.dart'; import 'package:time_machine/time_machine_text_patterns.dart'; + // ignore: implementation_imports -import 'package:timetable/src/header/date_indicator.dart'; +import 'package:timetable/src/components/date_indicator.dart'; + // ignore: implementation_imports import 'package:timetable/src/theme.dart'; -// ignore: implementation_imports -import 'package:timetable/src/utils/utils.dart'; + +import '../timetable_utils.dart'; // TODO(IoanaAlexandru): This is a temporary fix because the default // [DateHeader] from the timetable package has an overflow when the culture // is set to Romanian. We copied it here with minor changes and it can be // removed once the timetable package has it fixed. class DateHeader extends StatelessWidget { - const DateHeader(this.date, {Key key}) : super(key: key); - - final LocalDate date; + const DateHeader(this.date, {Key key}) : super(key: key); + final DateTime date; - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - WeekdayIndicator(date), - const SizedBox(height: 4), - DateIndicator(date), - ], - ); - } + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + WeekdayIndicator(date), + const SizedBox(height: 4), + DateIndicator(date), + ], + ); + } } class WeekdayIndicator extends StatelessWidget { - const WeekdayIndicator(this.date, {Key key}) : super(key: key); + const WeekdayIndicator(this.date, {Key key}) : super(key: key); + + final DateTime date; - final LocalDate date; + @override + Widget build(BuildContext context) { + final theme = context.theme; + final timetableTheme = TimetableTheme.of(context); - @override - Widget build(BuildContext context) { - final theme = context.theme; - final timetableTheme = context.timetableTheme; + final states = statesFor(date); + final pattern = timetableTheme?.weekDayIndicatorPattern?.resolve(states) ?? + LocalDatePattern.createWithCurrentCulture('ddd'); + final decoration = + timetableTheme?.weekDayIndicatorDecoration?.resolve(states) ?? + const BoxDecoration(); + final textStyle = + timetableTheme?.weekDayIndicatorTextStyle?.resolve(states) ?? + TextStyle( + color: date.isToday + ? timetableTheme?.primaryColor ?? theme.primaryColor + : theme.highEmphasisOnBackground, + ); - final states = DateIndicator.statesFor(date); - final pattern = timetableTheme?.weekDayIndicatorPattern?.resolve(states) ?? - LocalDatePattern.createWithCurrentCulture('ddd'); - final decoration = - timetableTheme?.weekDayIndicatorDecoration?.resolve(states) ?? - const BoxDecoration(); - final textStyle = - timetableTheme?.weekDayIndicatorTextStyle?.resolve(states) ?? - TextStyle( - color: date.isToday - ? timetableTheme?.primaryColor ?? theme.primaryColor - : theme.highEmphasisOnBackground, - ); + return DecoratedBox( + decoration: decoration, + child: Padding( + padding: const EdgeInsets.all(4), + child: AutoSizeText( + pattern.format(date), + style: textStyle, + maxLines: 1, + ), + ), + ); + } - return DecoratedBox( - decoration: decoration, - child: Padding( - padding: const EdgeInsets.all(4), - child: AutoSizeText( - pattern.format(date), - style: textStyle, - maxLines: 1, - ), - ), - ); - } + static Set statesFor(DateTime date) { + return { + if (date < DateTime.now()) MaterialState.disabled, + if (date.isToday) MaterialState.selected, + }; + } } diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index 7e467e004..60e131f29 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -413,7 +413,7 @@ class _AddEventViewState extends State { DateTime start = semester.startDate.at(startTime); if (weekSelected[WeekType.even] && !weekSelected[WeekType.odd]) { // Event is every even week, add a week to start date - start = start.add(const Period(weeks: 1)); + start = start.addDays(7); } final rrule = RecurrenceRule( diff --git a/lib/pages/timetable/view/events/all_day_event_widget.dart b/lib/pages/timetable/view/events/all_day_event_widget.dart index d4fd99ecc..c0fe41afa 100644 --- a/lib/pages/timetable/view/events/all_day_event_widget.dart +++ b/lib/pages/timetable/view/events/all_day_event_widget.dart @@ -1,248 +1,249 @@ -//import 'dart:math' as math; -// -//import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -//import 'package:acs_upb_mobile/pages/timetable/view/events/event_view.dart'; -//import 'package:black_hole_flutter/black_hole_flutter.dart'; -//import 'package:dartx/dartx.dart'; -//import 'package:flutter/foundation.dart'; -//import 'package:flutter/material.dart'; -//import 'package:timetable/timetable.dart'; -// -///// Widget to display all day events in the timetable, based on -///// [BasicAllDayEventWidget] from the timetable API. -//class UniAllDayEventWidget extends StatelessWidget { -// const UniAllDayEventWidget( -// this.event, { -// @required this.info, -// Key key, -// this.borderRadius = 4, -// }) : assert(event != null), -// assert(info != null), -// assert(borderRadius != null), -// super(key: key); -// -// /// The event to be displayed. -// final UniEventInstance event; -// final AllDayEventLayoutInfo info; -// final double borderRadius; -// -// @override -// Widget build(BuildContext context) { -// final color = event.color ?? -// event?.mainEvent?.color ?? -// Theme.of(context).primaryColor; -// -// return Padding( -// padding: const EdgeInsets.all(2), -// child: CustomPaint( -// painter: AllDayEventBackgroundPainter( -// info: info, -// color: color, -// borderRadius: borderRadius, -// ), -// child: Material( -// shape: AllDayEventBorder( -// info: info, -// side: BorderSide.none, -// borderRadius: borderRadius, -// ), -// clipBehavior: Clip.antiAlias, -// color: Colors.transparent, -// child: InkWell( -// onTap: () => -// Navigator.of(context).push(MaterialPageRoute( -// builder: (_) => EventView(eventInstance: event), -// )), -// child: _buildContent(context), -// ), -// ), -// ), -// ); -// } -// -// Widget _buildContent(BuildContext context) { -// final color = event.color ?? Theme.of(context).primaryColor; -// -// return Padding( -// padding: const EdgeInsets.fromLTRB(4, 2, 0, 2), -// child: Align( -// alignment: AlignmentDirectional.centerStart, -// child: DefaultTextStyle( -// style: context.textTheme.bodyText2.copyWith( -// fontSize: 14, -// color: color.highEmphasisOnColor, -// ), -// child: Text( -// event.title ?? event.mainEvent?.classHeader?.acronym, -// maxLines: 1, -// ), -// ), -// ), -// ); -// } -//} -// -//class AllDayEventBackgroundPainter extends CustomPainter { -// const AllDayEventBackgroundPainter({ -// @required this.info, -// @required this.color, -// this.borderRadius = 0, -// }) : assert(info != null), -// assert(color != null), -// assert(borderRadius != null); -// -// final AllDayEventLayoutInfo info; -// final Color color; -// final double borderRadius; -// -// @override -// void paint(Canvas canvas, Size size) { -// canvas.drawPath( -// _getPath(size, info, borderRadius), -// Paint()..color = color, -// ); -// } -// -// @override -// bool shouldRepaint(covariant AllDayEventBackgroundPainter oldDelegate) { -// return info != oldDelegate.info || -// color != oldDelegate.color || -// borderRadius != oldDelegate.borderRadius; -// } -//} -// -///// A modified [RoundedRectangleBorder] that morphs to triangular left and/or -///// right borders if not all of the event is currently visible. -//class AllDayEventBorder extends ShapeBorder { -// const AllDayEventBorder({ -// @required this.info, -// this.side = BorderSide.none, -// this.borderRadius = 0, -// }) : assert(info != null), -// assert(side != null), -// assert(borderRadius != null); -// -// final AllDayEventLayoutInfo info; -// final BorderSide side; -// final double borderRadius; -// -// @override -// EdgeInsetsGeometry get dimensions => EdgeInsets.all(side.width); -// -// @override -// ShapeBorder scale(double t) { -// return AllDayEventBorder( -// info: info, -// side: side.scale(t), -// borderRadius: borderRadius * t, -// ); -// } -// -// @override -// Path getInnerPath(Rect rect, {TextDirection textDirection}) { -// return null; -// } -// -// @override -// Path getOuterPath(Rect rect, {TextDirection textDirection}) { -// return _getPath(rect.size, info, borderRadius); -// } -// -// @override -// void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) { -// // For some reason, when we paint the background in this shape directly, it -// // lags while scrolling. Hence, we only use it to provide the outer path -// // used for clipping. -// } -// -// @override -// bool operator ==(Object other) { -// if (other.runtimeType != runtimeType) { -// return false; -// } -// return other is AllDayEventBorder && -// other.info == info && -// other.side == side && -// other.borderRadius == borderRadius; -// } -// -// @override -// int get hashCode => hashValues(info, side, borderRadius); -// -// @override -// String toString() => -// '${objectRuntimeType(this, 'RoundedRectangleBorder')}($side, $borderRadius)'; -//} -// -//Path _getPath(Size size, AllDayEventLayoutInfo info, double radius) { -// final height = size.height; -// // final radius = borderRadius.coerceAtMost(width / 2); -// -// final maxTipWidth = height / 4; -// final leftTipWidth = info.hiddenStartDays.coerceAtMost(1) * maxTipWidth; -// final rightTipWidth = info.hiddenEndDays.coerceAtMost(1) * maxTipWidth; -// -// final width = size.width; -// // final leftTipBase = math.min(leftTipWidth + radius, width - radius); -// // final rightTipBase = math.max(width - rightTipWidth - radius, radius); -// final leftTipBase = info.hiddenStartDays > 0 -// ? math.min(leftTipWidth + radius, width - radius) -// : leftTipWidth + radius; -// final rightTipBase = info.hiddenEndDays > 0 -// ? math.max(width - rightTipWidth - radius, radius) -// : width - rightTipWidth - radius; -// -// final tipSize = Size.square(radius * 2); -// -// // no tip: 0 ≈ 0° -// // full tip: PI / 4 ≈ 45° -// final leftTipAngle = math.pi / 2 - math.atan2(height / 2, leftTipWidth); -// final rightTipAngle = math.pi / 2 - math.atan2(height / 2, rightTipWidth); -// -// return Path() -// ..moveTo(leftTipBase, 0) -// // Right top -// ..arcTo( -// Offset(rightTipBase - radius, 0) & tipSize, -// math.pi * 3 / 2, -// math.pi / 2 - rightTipAngle, -// false, -// ) -// // Right tip -// ..arcTo( -// Offset(rightTipBase + rightTipWidth - radius, height / 2 - radius) & -// tipSize, -// -rightTipAngle, -// 2 * rightTipAngle, -// false, -// ) -// // Right bottom -// ..arcTo( -// Offset(rightTipBase - radius, height - radius * 2) & tipSize, -// rightTipAngle, -// math.pi / 2 - rightTipAngle, -// false, -// ) -// // Left bottom -// ..arcTo( -// Offset(leftTipBase - radius, height - radius * 2) & tipSize, -// math.pi / 2, -// math.pi / 2 - leftTipAngle, -// false, -// ) -// // Left tip -// ..arcTo( -// Offset(leftTipBase - leftTipWidth - radius, height / 2 - radius) & -// tipSize, -// math.pi - leftTipAngle, -// 2 * leftTipAngle, -// false, -// ) -// // Left top -// ..arcTo( -// Offset(leftTipBase - radius, 0) & tipSize, -// math.pi + leftTipAngle, -// math.pi / 2 - leftTipAngle, -// false, -// ); -//} +import 'dart:math' as math; + +import 'package:black_hole_flutter/black_hole_flutter.dart'; +import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:timetable/timetable.dart'; + +import '../../model/events/uni_event.dart'; +import 'event_view.dart'; + +/// Widget to display all day events in the timetable, based on +/// [BasicAllDayEventWidget] from the timetable API. +class UniAllDayEventWidget extends StatelessWidget { + const UniAllDayEventWidget( + this.event, { + @required this.info, + Key key, + this.borderRadius = 4, + }) : assert(event != null), + assert(info != null), + assert(borderRadius != null), + super(key: key); + + /// The event to be displayed. + final UniEventInstance event; + final AllDayEventLayoutInfo info; + final double borderRadius; + + @override + Widget build(BuildContext context) { + final color = event.color ?? + event?.mainEvent?.color ?? + Theme.of(context).primaryColor; + + return Padding( + padding: const EdgeInsets.all(2), + child: CustomPaint( + painter: AllDayEventBackgroundPainter( + info: info, + color: color, + borderRadius: borderRadius, + ), + child: Material( + shape: AllDayEventBorder( + info: info, + side: BorderSide.none, + borderRadius: borderRadius, + ), + clipBehavior: Clip.antiAlias, + color: Colors.transparent, + child: InkWell( + onTap: () => + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => EventView(eventInstance: event), + )), + child: _buildContent(context), + ), + ), + ), + ); + } + + Widget _buildContent(BuildContext context) { + final color = event.color ?? Theme.of(context).primaryColor; + + return Padding( + padding: const EdgeInsets.fromLTRB(4, 2, 0, 2), + child: Align( + alignment: AlignmentDirectional.centerStart, + child: DefaultTextStyle( + style: context.textTheme.bodyText2.copyWith( + fontSize: 14, + color: color.highEmphasisOnColor, + ), + child: Text( + event.title ?? event.mainEvent?.classHeader?.acronym, + maxLines: 1, + ), + ), + ), + ); + } +} + +class AllDayEventBackgroundPainter extends CustomPainter { + const AllDayEventBackgroundPainter({ + @required this.info, + @required this.color, + this.borderRadius = 0, + }) : assert(info != null), + assert(color != null), + assert(borderRadius != null); + + final AllDayEventLayoutInfo info; + final Color color; + final double borderRadius; + + @override + void paint(Canvas canvas, Size size) { + canvas.drawPath( + _getPath(size, info, borderRadius), + Paint()..color = color, + ); + } + + @override + bool shouldRepaint(covariant AllDayEventBackgroundPainter oldDelegate) { + return info != oldDelegate.info || + color != oldDelegate.color || + borderRadius != oldDelegate.borderRadius; + } +} + +/// A modified [RoundedRectangleBorder] that morphs to triangular left and/or +/// right borders if not all of the event is currently visible. +class AllDayEventBorder extends ShapeBorder { + const AllDayEventBorder({ + @required this.info, + this.side = BorderSide.none, + this.borderRadius = 0, + }) : assert(info != null, 'info is null'), + assert(side != null, 'side is null'), + assert(borderRadius != null, 'borderRadius is null'); + + final AllDayEventLayoutInfo info; + final BorderSide side; + final double borderRadius; + + @override + EdgeInsetsGeometry get dimensions => EdgeInsets.all(side.width); + + @override + ShapeBorder scale(double t) { + return AllDayEventBorder( + info: info, + side: side.scale(t), + borderRadius: borderRadius * t, + ); + } + + @override + Path getInnerPath(Rect rect, {TextDirection textDirection}) { + return null; + } + + @override + Path getOuterPath(Rect rect, {TextDirection textDirection}) { + return _getPath(rect.size, info, borderRadius); + } + + @override + void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) { + // For some reason, when we paint the background in this shape directly, it + // lags while scrolling. Hence, we only use it to provide the outer path + // used for clipping. + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is AllDayEventBorder && + other.info == info && + other.side == side && + other.borderRadius == borderRadius; + } + + @override + int get hashCode => hashValues(info, side, borderRadius); + + @override + String toString() => + '${objectRuntimeType(this, 'RoundedRectangleBorder')}($side, $borderRadius)'; +} + +Path _getPath(Size size, AllDayEventLayoutInfo info, double radius) { + final height = size.height; + // final radius = borderRadius.coerceAtMost(width / 2); + + final maxTipWidth = height / 4; + final leftTipWidth = info.hiddenStartDays.coerceAtMost(1) * maxTipWidth; + final rightTipWidth = info.hiddenEndDays.coerceAtMost(1) * maxTipWidth; + + final width = size.width; + // final leftTipBase = math.min(leftTipWidth + radius, width - radius); + // final rightTipBase = math.max(width - rightTipWidth - radius, radius); + final leftTipBase = info.hiddenStartDays > 0 + ? math.min(leftTipWidth + radius, width - radius) + : leftTipWidth + radius; + final rightTipBase = info.hiddenEndDays > 0 + ? math.max(width - rightTipWidth - radius, radius) + : width - rightTipWidth - radius; + + final tipSize = Size.square(radius * 2); + + // no tip: 0 ≈ 0° + // full tip: PI / 4 ≈ 45° + final leftTipAngle = math.pi / 2 - math.atan2(height / 2, leftTipWidth); + final rightTipAngle = math.pi / 2 - math.atan2(height / 2, rightTipWidth); + + return Path() + ..moveTo(leftTipBase, 0) + // Right top + ..arcTo( + Offset(rightTipBase - radius, 0) & tipSize, + math.pi * 3 / 2, + math.pi / 2 - rightTipAngle, + false, + ) + // Right tip + ..arcTo( + Offset(rightTipBase + rightTipWidth - radius, height / 2 - radius) & + tipSize, + -rightTipAngle, + 2 * rightTipAngle, + false, + ) + // Right bottom + ..arcTo( + Offset(rightTipBase - radius, height - radius * 2) & tipSize, + rightTipAngle, + math.pi / 2 - rightTipAngle, + false, + ) + // Left bottom + ..arcTo( + Offset(leftTipBase - radius, height - radius * 2) & tipSize, + math.pi / 2, + math.pi / 2 - leftTipAngle, + false, + ) + // Left tip + ..arcTo( + Offset(leftTipBase - leftTipWidth - radius, height / 2 - radius) & + tipSize, + math.pi - leftTipAngle, + 2 * leftTipAngle, + false, + ) + // Left top + ..arcTo( + Offset(leftTipBase - radius, 0) & tipSize, + math.pi + leftTipAngle, + math.pi / 2 - leftTipAngle, + false, + ); +} diff --git a/lib/pages/timetable/view/events/event_view.dart b/lib/pages/timetable/view/events/event_view.dart index 96b727f37..c14f84783 100644 --- a/lib/pages/timetable/view/events/event_view.dart +++ b/lib/pages/timetable/view/events/event_view.dart @@ -1,191 +1,192 @@ -// import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; -// import 'package:acs_upb_mobile/generated/l10n.dart'; -// import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; -// import 'package:acs_upb_mobile/pages/classes/view/class_view.dart'; -// import 'package:acs_upb_mobile/pages/classes/view/classes_page.dart'; -// import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; -// import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; -// import 'package:acs_upb_mobile/pages/people/view/person_view.dart'; -// import 'package:acs_upb_mobile/pages/timetable/model/events/class_event.dart'; -// import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -// import 'package:acs_upb_mobile/pages/timetable/view/events/add_event_view.dart'; -// import 'package:acs_upb_mobile/widgets/scaffold.dart'; -// import 'package:acs_upb_mobile/widgets/toast.dart'; -// import 'package:flutter/material.dart'; -// import 'package:flutter/rendering.dart'; -// import 'package:flutter_feather_icons/flutter_feather_icons.dart'; -// import 'package:provider/provider.dart'; -// -// class EventView extends StatefulWidget { -// const EventView({Key key, this.eventInstance, this.uniEvent}) -// : assert( -// (eventInstance != null && uniEvent == null) || -// (eventInstance == null && uniEvent != null), -// 'Only one of the parameters must be provided'), -// super(key: key); -// final UniEventInstance eventInstance; -// final UniEvent uniEvent; -// -// @override -// _EventViewState createState() => _EventViewState(); -// } -// -// class _EventViewState extends State { -// Padding _colorIcon() => Padding( -// padding: const EdgeInsets.all(10), -// child: Container( -// width: 20, -// height: 20, -// decoration: BoxDecoration( -// borderRadius: const BorderRadius.all(Radius.circular(4)), -// color: widget.uniEvent?.color ?? widget.eventInstance.color), -// ), -// ); -// -// @override -// Widget build(BuildContext context) { -// final user = Provider.of(context).currentUserFromCache; -// final UniEvent mainEvent = -// widget.eventInstance?.mainEvent ?? widget.uniEvent; -// return AppScaffold( -// title: Text(S.current.navigationEventDetails), -// actions: [ -// AppScaffoldAction( -// icon: Icons.edit_outlined, -// disabled: !mainEvent.editable || !user.canAddPublicInfo, -// onPressed: () { -// if (!mainEvent.editable) { -// AppToast.show(S.current.warningEventNotEditable); -// } else if (!user.canAddPublicInfo) { -// AppToast.show(S.current.errorPermissionDenied); -// } else { -// Navigator.of(context).push(MaterialPageRoute( -// builder: (_) => ChangeNotifierProvider( -// create: (_) => FilterProvider( -// defaultDegree: mainEvent.degree, -// defaultRelevance: mainEvent.relevance, -// ), -// child: AddEventView( -// initialEvent: mainEvent, -// ), -// ), -// )); -// } -// }, -// ) -// ], -// body: SafeArea( -// child: ListView(children: [ -// Padding( -// padding: const EdgeInsets.all(16), -// child: Row( -// children: [ -// _colorIcon(), -// const SizedBox(width: 16), -// Expanded( -// child: Column( -// mainAxisSize: MainAxisSize.min, -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Text( -// widget.eventInstance?.title ?? -// mainEvent.type.toLocalizedString(), -// style: Theme.of(context).textTheme.headline6), -// const SizedBox(height: 4), -// if (widget.eventInstance != null) -// Text(widget.eventInstance.dateString), -// if (mainEvent.info != widget.eventInstance?.dateString) -// Padding( -// padding: const EdgeInsets.only(top: 2), -// child: Text( -// mainEvent.info, -// style: Theme.of(context) -// .textTheme -// .bodyText2 -// .copyWith(color: Theme.of(context).hintColor), -// ), -// ), -// ], -// ), -// ), -// ], -// ), -// ), -// if (mainEvent?.classHeader != null) -// ClassListItem( -// classHeader: mainEvent.classHeader, -// hint: S.current.messageTapForMoreInfo, -// onTap: () => Navigator.of(context).push( -// MaterialPageRoute( -// builder: (context) => ChangeNotifierProvider.value( -// value: Provider.of(context), -// child: ClassView( -// classHeader: mainEvent.classHeader, -// ), -// ), -// ), -// ), -// ), -// if (widget.eventInstance?.location?.isNotEmpty ?? false) -// Padding( -// padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), -// child: Row( -// children: [ -// const Padding( -// padding: EdgeInsets.all(10), -// child: Icon(FeatherIcons.mapPin), -// ), -// const SizedBox(width: 16), -// Text(widget.eventInstance?.location, -// style: Theme.of(context).textTheme.subtitle1), -// ], -// ), -// ), -// Padding( -// padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), -// child: Row( -// children: [ -// const Padding( -// padding: EdgeInsets.all(10), -// child: Icon(FeatherIcons.users), -// ), -// const SizedBox(width: 16), -// Text( -// mainEvent.relevance == null -// ? S.current.relevanceAnyone -// : '${FilterNode.localizeName(mainEvent.degree, context)}: ${mainEvent.relevance.join(', ')}', -// style: Theme.of(context).textTheme.subtitle1), -// ], -// ), -// ), -// if (mainEvent is ClassEvent) -// Padding( -// padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), -// child: GestureDetector( -// onTap: () { -// if (mainEvent.teacher != null) { -// showModalBottomSheet( -// isScrollControlled: true, -// context: context, -// builder: (BuildContext buildContext) => -// PersonView(person: mainEvent.teacher)); -// } -// }, -// child: Row( -// children: [ -// const Padding( -// padding: EdgeInsets.all(10), -// child: Icon(FeatherIcons.user), -// ), -// const SizedBox(width: 16), -// Text(mainEvent.teacher.name ?? S.current.labelUnknown, -// style: Theme.of(context).textTheme.subtitle1), -// ], -// ), -// ), -// ), -// ]), -// ), -// ); -// } -// } +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:provider/provider.dart'; + +import '../../../../authentication/service/auth_provider.dart'; +import '../../../../generated/l10n.dart'; +import '../../../../widgets/scaffold.dart'; +import '../../../../widgets/toast.dart'; +import '../../../classes/service/class_provider.dart'; +import '../../../classes/view/class_view.dart'; +import '../../../classes/view/classes_page.dart'; +import '../../../filter/model/filter.dart'; +import '../../../filter/service/filter_provider.dart'; +import '../../../people/view/person_view.dart'; +import '../../model/events/class_event.dart'; +import '../../model/events/uni_event.dart'; +import 'add_event_view.dart'; + +class EventView extends StatefulWidget { + const EventView({Key key, this.eventInstance, this.uniEvent}) + : assert( + (eventInstance != null && uniEvent == null) || + (eventInstance == null && uniEvent != null), + 'Only one of the parameters must be provided'), + super(key: key); + final UniEventInstance eventInstance; + final UniEvent uniEvent; + + @override + _EventViewState createState() => _EventViewState(); +} + +class _EventViewState extends State { + Padding _colorIcon() => Padding( + padding: const EdgeInsets.all(10), + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(4)), + color: widget.uniEvent?.color ?? widget.eventInstance.color), + ), + ); + + @override + Widget build(BuildContext context) { + final user = Provider.of(context).currentUserFromCache; + final UniEvent mainEvent = + widget.eventInstance?.mainEvent ?? widget.uniEvent; + return AppScaffold( + title: Text(S.current.navigationEventDetails), + actions: [ + AppScaffoldAction( + icon: Icons.edit_outlined, + disabled: !mainEvent.editable || !user.canAddPublicInfo, + onPressed: () { + if (!mainEvent.editable) { + AppToast.show(S.current.warningEventNotEditable); + } else if (!user.canAddPublicInfo) { + AppToast.show(S.current.errorPermissionDenied); + } else { + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => ChangeNotifierProvider( + create: (_) => FilterProvider( + defaultDegree: mainEvent.degree, + defaultRelevance: mainEvent.relevance, + ), + child: AddEventView( + initialEvent: mainEvent, + ), + ), + )); + } + }, + ) + ], + body: SafeArea( + child: ListView(children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + _colorIcon(), + const SizedBox(width: 16), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.eventInstance?.title ?? + mainEvent.type.toLocalizedString(), + style: Theme.of(context).textTheme.headline6), + const SizedBox(height: 4), + if (widget.eventInstance != null) + Text(widget.eventInstance.dateString), + if (mainEvent.info != widget.eventInstance?.dateString) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + mainEvent.info, + style: Theme.of(context) + .textTheme + .bodyText2 + .copyWith(color: Theme.of(context).hintColor), + ), + ), + ], + ), + ), + ], + ), + ), + if (mainEvent?.classHeader != null) + ClassListItem( + classHeader: mainEvent.classHeader, + hint: S.current.messageTapForMoreInfo, + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ChangeNotifierProvider.value( + value: Provider.of(context), + child: ClassView( + classHeader: mainEvent.classHeader, + ), + ), + ), + ), + ), + if (widget.eventInstance?.location?.isNotEmpty ?? false) + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Row( + children: [ + const Padding( + padding: EdgeInsets.all(10), + child: Icon(FeatherIcons.mapPin), + ), + const SizedBox(width: 16), + Text(widget.eventInstance?.location, + style: Theme.of(context).textTheme.subtitle1), + ], + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Row( + children: [ + const Padding( + padding: EdgeInsets.all(10), + child: Icon(FeatherIcons.users), + ), + const SizedBox(width: 16), + Text( + mainEvent.relevance == null + ? S.current.relevanceAnyone + : '${FilterNode.localizeName(mainEvent.degree, context)}: ${mainEvent.relevance.join(', ')}', + style: Theme.of(context).textTheme.subtitle1), + ], + ), + ), + if (mainEvent is ClassEvent) + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: GestureDetector( + onTap: () { + if (mainEvent.teacher != null) { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (BuildContext buildContext) => + PersonView(person: mainEvent.teacher)); + } + }, + child: Row( + children: [ + const Padding( + padding: EdgeInsets.all(10), + child: Icon(FeatherIcons.user), + ), + const SizedBox(width: 16), + Text(mainEvent.teacher.name ?? S.current.labelUnknown, + style: Theme.of(context).textTheme.subtitle1), + ], + ), + ), + ), + ]), + ), + ); + } +} diff --git a/lib/pages/timetable/view/events/event_widget.dart b/lib/pages/timetable/view/events/event_widget.dart index 95e9a5fc2..e28c885dc 100644 --- a/lib/pages/timetable/view/events/event_widget.dart +++ b/lib/pages/timetable/view/events/event_widget.dart @@ -1,101 +1,102 @@ -//import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -//import 'package:acs_upb_mobile/pages/timetable/view/events/event_view.dart'; -//import 'package:auto_size_text/auto_size_text.dart'; -//import 'package:black_hole_flutter/black_hole_flutter.dart'; -//import 'package:flutter/foundation.dart'; -//import 'package:flutter/material.dart'; -//import 'package:timetable/timetable.dart'; -// -///// Widget to display all day events in the timetable, based on -///// [BasicEventWidget] from the timetable API. -//class UniEventWidget extends StatelessWidget { -// const UniEventWidget(this.event, {Key key}) -// : assert(event != null), -// super(key: key); -// -// final UniEventInstance event; -// -// @override -// Widget build(BuildContext context) { -// final color = event.color ?? -// event?.mainEvent?.color ?? -// Theme.of(context).primaryColor; -// final footer = -// (event.location?.isNotEmpty ?? false) ? event.location : event.info; -// -// return GestureDetector( -// onTap: () => Navigator.of(context).push(MaterialPageRoute( -// builder: (_) => EventView(eventInstance: event), -// )), -// child: Material( -// shape: RoundedRectangleBorder( -// side: BorderSide( -// color: Theme.of(context).scaffoldBackgroundColor, -// width: 0.75, -// ), -// borderRadius: BorderRadius.circular(4), -// ), -// color: color, -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// mainAxisSize: MainAxisSize.min, -// mainAxisAlignment: MainAxisAlignment.end, -// children: [ -// Padding( -// padding: const EdgeInsets.fromLTRB(4, 2, 4, 0), -// child: AutoSizeText( -// event.title ?? event.mainEvent?.classHeader?.acronym ?? '', -// maxLines: 2, -// minFontSize: 4, -// maxFontSize: 12, -// style: Theme.of(context).textTheme.bodyText2.copyWith( -// fontSize: 12, -// fontWeight: FontWeight.w600, -// color: color.highEmphasisOnColor, -// ), -// ), -// ), -// if (event.mainEvent.type != null) -// Expanded( -// child: Padding( -// padding: const EdgeInsets.fromLTRB(4, 2, 4, 2), -// child: AutoSizeText( -// event.mainEvent.type.toLocalizedString(), -// wrapWords: false, -// minFontSize: 10, -// maxFontSize: 10, -// maxLines: 1, -// overflow: TextOverflow.ellipsis, -// style: Theme.of(context).textTheme.bodyText2.copyWith( -// fontSize: 10, -// color: color.highEmphasisOnColor, -// ), -// ), -// ), -// ), -// Expanded( -// child: footer?.isNotEmpty ?? false -// ? Align( -// alignment: Alignment.bottomRight, -// child: Padding( -// padding: const EdgeInsets.fromLTRB(4, 2, 4, 2), -// child: AutoSizeText( -// footer, -// maxLines: 1, -// minFontSize: 10, -// overflow: TextOverflow.ellipsis, -// style: Theme.of(context).textTheme.bodyText2.copyWith( -// fontSize: 12, -// color: color.mediumEmphasisOnColor, -// ), -// ), -// ), -// ) -// : Container(), -// ), -// ], -// ), -// ), -// ); -// } -//} +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:black_hole_flutter/black_hole_flutter.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:timetable/timetable.dart'; + +import '../../model/events/uni_event.dart'; +import 'event_view.dart'; + +/// Widget to display all day events in the timetable, based on +/// [BasicEventWidget] from the timetable API. +class UniEventWidget extends StatelessWidget { + const UniEventWidget(this.event, {Key key}) + : assert(event != null), + super(key: key); + + final UniEventInstance event; + + @override + Widget build(BuildContext context) { + final color = event.color ?? + event?.mainEvent?.color ?? + Theme.of(context).primaryColor; + final footer = + (event.location?.isNotEmpty ?? false) ? event.location : event.info; + + return GestureDetector( + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (_) => EventView(eventInstance: event), + )), + child: Material( + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).scaffoldBackgroundColor, + width: 0.75, + ), + borderRadius: BorderRadius.circular(4), + ), + color: color, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(4, 2, 4, 0), + child: AutoSizeText( + event.title ?? event.mainEvent?.classHeader?.acronym ?? '', + maxLines: 2, + minFontSize: 4, + maxFontSize: 12, + style: Theme.of(context).textTheme.bodyText2.copyWith( + fontSize: 12, + fontWeight: FontWeight.w600, + color: color.highEmphasisOnColor, + ), + ), + ), + if (event.mainEvent.type != null) + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 2, 4, 2), + child: AutoSizeText( + event.mainEvent.type.toLocalizedString(), + wrapWords: false, + minFontSize: 10, + maxFontSize: 10, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyText2.copyWith( + fontSize: 10, + color: color.highEmphasisOnColor, + ), + ), + ), + ), + Expanded( + child: footer?.isNotEmpty ?? false + ? Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 2, 4, 2), + child: AutoSizeText( + footer, + maxLines: 1, + minFontSize: 10, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyText2.copyWith( + fontSize: 12, + color: color.mediumEmphasisOnColor, + ), + ), + ), + ) + : Container(), + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 6e36c760a..915543118 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -204,6 +204,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + dartx: + dependency: "direct main" + description: + name: dartx + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.1" datetime_picker_formfield: dependency: "direct main" description: @@ -943,6 +950,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.3.0" + time: + dependency: transitive + description: + name: time + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" time_machine: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b479e3a29..05cb1356b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,8 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.3 + dartx: any + # Date & time picker widget datetime_picker_formfield: ^2.0.0 From 1fe2097ead63a6e6aa0672f6104ee3087f97bcc9 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Sat, 21 Aug 2021 01:24:54 +0300 Subject: [PATCH 26/60] Started to migrate to the new API of Timetable --- lib/pages/timetable/view/timetable_page.dart | 578 ++++++++++--------- 1 file changed, 290 insertions(+), 288 deletions(-) diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index b121bf589..68b16c2a1 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; import 'package:recase/recase.dart'; +import 'package:supercharged/supercharged.dart'; import 'package:time_machine/time_machine.dart'; import 'package:timetable/timetable.dart'; @@ -25,309 +26,310 @@ import 'events/all_day_event_widget.dart'; import 'events/event_widget.dart'; class TimetablePage extends StatefulWidget { - const TimetablePage({Key key}) : super(key: key); + const TimetablePage({Key key}) : super(key: key); - @override - _TimetablePageState createState() => _TimetablePageState(); + @override + _TimetablePageState createState() => _TimetablePageState(); } class _TimetablePageState extends State { - TimetableController _controller; + TimeController _controller; - @override - void dispose() { - _controller?.dispose(); - super.dispose(); - } + /// UniEventInstance? - @override - Widget build(BuildContext context) { - final authProvider = Provider.of(context); - final eventProvider = Provider.of(context); - if (_controller == null) { - _controller = TimetableController( - // TODO(IoanaAlexandru): Make initialTimeRange customizable in settings - initialTimeRange: InitialTimeRange.range( - startTime: LocalTime(7, 55, 0), endTime: LocalTime(20, 5, 0)), - eventProvider: eventProvider); + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } - if (authProvider.isAuthenticated && !authProvider.isAnonymous) { - scheduleDialog(context); - } - } + @override + Widget build(BuildContext context) { + final authProvider = Provider.of(context); + final eventProvider = Provider.of(context); + if (_controller == null) { + _controller = TimeController( + initialRange: TimeRange(7.hours + 55.minutes, 20.hours + 5.minutes), + // TODO(IoanaAlexandru): Make initialTimeRange customizable in settings + ); - return AppScaffold( - title: AnimatedBuilder( - animation: _controller.dateListenable, - builder: (context, child) => Text( - authProvider.isAuthenticated && !authProvider.isAnonymous - ? S.current.navigationTimetable - : _controller.currentMonth.titleCase), - ), - needsToBeAuthenticated: true, - leading: AppScaffoldAction( - icon: Icons.today_outlined, - onPressed: () => _controller.animateToToday(), - tooltip: S.current.actionJumpToToday, - ), - actions: [ - AppScaffoldAction( - icon: FeatherIcons.bookOpen, - tooltip: S.current.navigationClasses, - onPressed: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => ChangeNotifierProvider.value( - value: Provider.of(context), - child: const ClassesPage()), - ), - ), - ), - AppScaffoldAction( - icon: FeatherIcons.filter, - tooltip: S.current.navigationFilter, - onPressed: () => Navigator.push( - context, - MaterialPageRoute(builder: (_) => const FilterPage()), - ), - ), - ], - body: Padding( - padding: const EdgeInsets.all(10), - child: Stack( - children: [ - Timetable( - controller: _controller, - dateHeaderBuilder: (_, date) => DateHeader(date), - eventBuilder: (event) => UniEventWidget(event), - allDayEventBuilder: (context, event, info) => - UniAllDayEventWidget( - event, - info: info, - ), - onEventBackgroundTap: (dateTime, isAllDay) { - if (!isAllDay) { - final user = Provider.of(context, listen: false) - .currentUserFromCache; - if (user.canAddPublicInfo) { - Navigator.of(context).push(MaterialPageRoute( - builder: (_) => ChangeNotifierProxyProvider( - create: (_) => FilterProvider(), - update: (context, authProvider, filterProvider) { - return filterProvider..updateAuth(authProvider); - }, - child: AddEventView( - initialEvent: UniEvent( - start: dateTime, - duration: const Period(hours: 2), - id: null), - ), - ), - )); - } else { - AppToast.show(S.current.errorPermissionDenied); - } - } - }, - ), - ], - ), - ), - ); - } + if (authProvider.isAuthenticated && !authProvider.isAnonymous) { + scheduleDialog(context); + } + } - Future scheduleDialog(BuildContext context) async { - WidgetsBinding.instance.addPostFrameCallback((_) async { - if (!mounted) { - return; - } + return AppScaffold( + title: AnimatedBuilder( + animation: _controller.dateListenable, + builder: (context, child) => Text( + authProvider.isAuthenticated && !authProvider.isAnonymous + ? S.current.navigationTimetable + : _controller.currentMonth.titleCase), + ), + needsToBeAuthenticated: true, + leading: AppScaffoldAction( + icon: Icons.today_outlined, + onPressed: () => _controller.animateToToday(), + tooltip: S.current.actionJumpToToday, + ), + actions: [ + AppScaffoldAction( + icon: FeatherIcons.bookOpen, + tooltip: S.current.navigationClasses, + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ChangeNotifierProvider.value( + value: Provider.of(context), + child: const ClassesPage()), + ), + ), + ), + AppScaffoldAction( + icon: FeatherIcons.filter, + tooltip: S.current.navigationFilter, + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const FilterPage()), + ), + ), + ], + body: Padding( + padding: const EdgeInsets.all(10), + child: Stack( + children: [ + Timetable( + controller: _controller, + dateHeaderBuilder: (_, date) => DateHeader(date), + eventBuilder: (event) => UniEventWidget(event), + allDayEventBuilder: (context, event, info) => + UniAllDayEventWidget( + event, + info: info, + ), + onEventBackgroundTap: (dateTime, isAllDay) { + if (!isAllDay) { + final user = Provider.of(context, listen: false) + .currentUserFromCache; + if (user.canAddPublicInfo) { + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => ChangeNotifierProxyProvider( + create: (_) => FilterProvider(), + update: (context, authProvider, filterProvider) { + return filterProvider..updateAuth(authProvider); + }, + child: AddEventView( + initialEvent: UniEvent( + start: dateTime, + duration: const Period(hours: 2), + id: null), + ), + ), + )); + } else { + AppToast.show(S.current.errorPermissionDenied); + } + } + }, + ), + ], + ), + ), + ); + } - // Fetch user classes, request necessary info from providers so it's - // cached when we check in the dialog - final user = Provider.of(context, listen: false) - .currentUserFromCache; - await Provider.of(context, listen: false) - .fetchClassHeaders(uid: user.uid); - await Provider.of(context, listen: false).fetchFilter(); - await Provider.of(context, listen: false) - .userAlreadyRequested(user.uid); + Future scheduleDialog(BuildContext context) async { + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!mounted) { + return; + } - // Slight delay between last frame and dialog - await Future.delayed(const Duration(milliseconds: 100)); + // Fetch user classes, request necessary info from providers so it's + // cached when we check in the dialog + final user = Provider.of(context, listen: false) + .currentUserFromCache; + await Provider.of(context, listen: false) + .fetchClassHeaders(uid: user.uid); + await Provider.of(context, listen: false).fetchFilter(); + await Provider.of(context, listen: false) + .userAlreadyRequested(user.uid); - // Show dialog if there are no events - final eventProvider = - Provider.of(context, listen: false); - if (eventProvider.empty) { - await showDialog( - context: context, - builder: buildDialog, - ); - } - }); - } + // Slight delay between last frame and dialog + await Future.delayed(const Duration(milliseconds: 100)); - Widget buildDialog(BuildContext context) { - final classProvider = Provider.of(context); - final authProvider = Provider.of(context); - final filterProvider = Provider.of(context); - final user = authProvider.currentUserFromCache; + // Show dialog if there are no events + final eventProvider = + Provider.of(context, listen: false); + if (eventProvider.empty) { + await showDialog( + context: context, + builder: buildDialog, + ); + } + }); + } - if (classProvider.userClassHeadersCache?.isEmpty ?? true) { - return AppDialog( - title: S.current.warningNoEvents, - content: [ - RichText( - text: TextSpan( - style: Theme.of(context).textTheme.subtitle1, - children: [ - TextSpan(text: '${S.current.infoYouNeedToSelect} '), - WidgetSpan( - alignment: PlaceholderAlignment.top, - child: Icon( - FeatherIcons.bookOpen, - size: Theme.of(context).textTheme.subtitle1.fontSize + 2, - ), - ), - TextSpan(text: ' ${S.current.infoClasses}.'), - ], - ), - ), - ], - actions: [ - AppButton( - text: S.current.actionChooseClasses, - width: 130, - onTap: () async { - // Pop the dialog - Navigator.of(context).pop(); - // Push the Add classes page - await Navigator.of(context) - .push(MaterialPageRoute( - builder: (_) => ChangeNotifierProvider.value( - value: classProvider, - child: FutureBuilder( - future: classProvider.fetchUserClassIds(user.uid), - builder: (context, snap) { - if (snap.hasData) { - return AddClassesPage( - initialClassIds: snap.data, - onSave: (classIds) async { - await classProvider.setUserClassIds( - classIds, authProvider.uid); - Navigator.pop(context); - }); - } else { - return const Center( - child: CircularProgressIndicator()); - } - }, - )), - )); - }, - ) - ], - ); - } else if ((filterProvider.cachedFilter?.relevantNodes?.length ?? 0) < 6) { - return AppDialog( - title: S.current.warningNoEvents, - content: [ - RichText( - text: TextSpan( - style: Theme.of(context).textTheme.subtitle1, - children: [ - TextSpan(text: '${S.current.infoMakeSureGroupIsSelected} '), - WidgetSpan( - alignment: PlaceholderAlignment.top, - child: Icon( - FeatherIcons.filter, - size: Theme.of(context).textTheme.subtitle1.fontSize + 2, - ), - ), - TextSpan(text: ' ${S.current.navigationFilter.toLowerCase()}.'), - ], - ), - ), - ], - actions: [ - AppButton( - text: S.current.actionOpenFilter, - width: 130, - onTap: () async { - // Pop the dialog - Navigator.of(context).pop(); - // Push the Filter page - await Navigator.pushNamed(context, Routes.filter); - }, - ) - ], - ); - } else if (user.permissionLevel < 3) { - // TODO(IoanaAlexandru): Check if user already requested and show a different message - return AppDialog( - title: S.current.warningNoEvents, - content: [Text(S.current.messageYouCanContribute)], - actions: [ - AppButton( - text: S.current.actionRequestPermissions, - width: 130, - onTap: () async { - // Check if user is verified - final bool isVerified = await authProvider.isVerified; - // Pop the dialog - Navigator.of(context).pop(); - // Push the Permissions page - if (authProvider.isAnonymous) { - AppToast.show(S.current.messageNotLoggedIn); - } else if (!isVerified) { - AppToast.show(S.current.messageEmailNotVerifiedToPerformAction); - } else { - await Navigator.of(context) - .pushNamed(Routes.requestPermissions); - } - }, - ) - ], - ); - } else { - return AppDialog( - title: S.current.warningNoEvents, - content: [ - RichText( - key: const ValueKey('no_events_message'), - text: TextSpan( - style: Theme.of(context).textTheme.subtitle1, - children: [ - TextSpan(text: S.current.messageThereAreNoEventsForSelected), - WidgetSpan( - alignment: PlaceholderAlignment.top, - child: Icon( - FeatherIcons.bookOpen, - size: Theme.of(context).textTheme.subtitle1.fontSize + 2, - ), - ), - TextSpan( - text: - '${S.current.navigationClasses.toLowerCase()} ${S.current.stringAnd} '), - WidgetSpan( - alignment: PlaceholderAlignment.top, - child: Icon( - FeatherIcons.filter, - size: Theme.of(context).textTheme.subtitle1.fontSize + 2, - ), - ), - TextSpan(text: ' ${S.current.navigationFilter.toLowerCase()}.'), - ], - ), - ), - ], - ); - } - } + Widget buildDialog(BuildContext context) { + final classProvider = Provider.of(context); + final authProvider = Provider.of(context); + final filterProvider = Provider.of(context); + final user = authProvider.currentUserFromCache; + + if (classProvider.userClassHeadersCache?.isEmpty ?? true) { + return AppDialog( + title: S.current.warningNoEvents, + content: [ + RichText( + text: TextSpan( + style: Theme.of(context).textTheme.subtitle1, + children: [ + TextSpan(text: '${S.current.infoYouNeedToSelect} '), + WidgetSpan( + alignment: PlaceholderAlignment.top, + child: Icon( + FeatherIcons.bookOpen, + size: Theme.of(context).textTheme.subtitle1.fontSize + 2, + ), + ), + TextSpan(text: ' ${S.current.infoClasses}.'), + ], + ), + ), + ], + actions: [ + AppButton( + text: S.current.actionChooseClasses, + width: 130, + onTap: () async { + // Pop the dialog + Navigator.of(context).pop(); + // Push the Add classes page + await Navigator.of(context) + .push(MaterialPageRoute( + builder: (_) => ChangeNotifierProvider.value( + value: classProvider, + child: FutureBuilder( + future: classProvider.fetchUserClassIds(user.uid), + builder: (context, snap) { + if (snap.hasData) { + return AddClassesPage( + initialClassIds: snap.data, + onSave: (classIds) async { + await classProvider.setUserClassIds( + classIds, authProvider.uid); + Navigator.pop(context); + }); + } else { + return const Center( + child: CircularProgressIndicator()); + } + }, + )), + )); + }, + ) + ], + ); + } else if ((filterProvider.cachedFilter?.relevantNodes?.length ?? 0) < 6) { + return AppDialog( + title: S.current.warningNoEvents, + content: [ + RichText( + text: TextSpan( + style: Theme.of(context).textTheme.subtitle1, + children: [ + TextSpan(text: '${S.current.infoMakeSureGroupIsSelected} '), + WidgetSpan( + alignment: PlaceholderAlignment.top, + child: Icon( + FeatherIcons.filter, + size: Theme.of(context).textTheme.subtitle1.fontSize + 2, + ), + ), + TextSpan(text: ' ${S.current.navigationFilter.toLowerCase()}.'), + ], + ), + ), + ], + actions: [ + AppButton( + text: S.current.actionOpenFilter, + width: 130, + onTap: () async { + // Pop the dialog + Navigator.of(context).pop(); + // Push the Filter page + await Navigator.pushNamed(context, Routes.filter); + }, + ) + ], + ); + } else if (user.permissionLevel < 3) { + // TODO(IoanaAlexandru): Check if user already requested and show a different message + return AppDialog( + title: S.current.warningNoEvents, + content: [Text(S.current.messageYouCanContribute)], + actions: [ + AppButton( + text: S.current.actionRequestPermissions, + width: 130, + onTap: () async { + // Check if user is verified + final bool isVerified = await authProvider.isVerified; + // Pop the dialog + Navigator.of(context).pop(); + // Push the Permissions page + if (authProvider.isAnonymous) { + AppToast.show(S.current.messageNotLoggedIn); + } else if (!isVerified) { + AppToast.show(S.current.messageEmailNotVerifiedToPerformAction); + } else { + await Navigator.of(context) + .pushNamed(Routes.requestPermissions); + } + }, + ) + ], + ); + } else { + return AppDialog( + title: S.current.warningNoEvents, + content: [ + RichText( + key: const ValueKey('no_events_message'), + text: TextSpan( + style: Theme.of(context).textTheme.subtitle1, + children: [ + TextSpan(text: S.current.messageThereAreNoEventsForSelected), + WidgetSpan( + alignment: PlaceholderAlignment.top, + child: Icon( + FeatherIcons.bookOpen, + size: Theme.of(context).textTheme.subtitle1.fontSize + 2, + ), + ), + TextSpan( + text: + '${S.current.navigationClasses.toLowerCase()} ${S.current.stringAnd} '), + WidgetSpan( + alignment: PlaceholderAlignment.top, + child: Icon( + FeatherIcons.filter, + size: Theme.of(context).textTheme.subtitle1.fontSize + 2, + ), + ), + TextSpan(text: ' ${S.current.navigationFilter.toLowerCase()}.'), + ], + ), + ), + ], + ); + } + } } extension MonthController on TimetableController { - String get currentMonth => - LocalDateTime(2020, dateListenable.value.monthOfYear, 1, 1, 1, 1) - .toString('MMMM'); + String get currentMonth => + LocalDateTime(2020, dateListenable.value.monthOfYear, 1, 1, 1, 1) + .toString('MMMM'); } From 865a3e0e1f0fcece478e5894f5523c273400c13f Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Sat, 21 Aug 2021 18:18:49 +0300 Subject: [PATCH 27/60] Migrating timetable widget using new time and date controllers from timetable API --- lib/generated/l10n.dart | 20 +++--- lib/pages/timetable/view/timetable_page.dart | 72 +++++++++++--------- 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index ac904423e..f5bd70a8b 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -1,7 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; - import 'intl/messages_all.dart'; // ************************************************************************** @@ -15,23 +14,22 @@ import 'intl/messages_all.dart'; class S { S(); - + static S current; - - static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); + + static const AppLocalizationDelegate delegate = + AppLocalizationDelegate(); static Future load(Locale locale) { - final name = (locale.countryCode?.isEmpty ?? false) - ? locale.languageCode - : locale.toString(); - final localeName = Intl.canonicalizedLocale(name); + final name = (locale.countryCode?.isEmpty ?? false) ? locale.languageCode : locale.toString(); + final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; S.current = S(); - + return S.current; }); - } + } static S of(BuildContext context) { return Localizations.of(context, S); @@ -2875,4 +2873,4 @@ class AppLocalizationDelegate extends LocalizationsDelegate { } return false; } -} +} \ No newline at end of file diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index 68b16c2a1..370577996 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -33,13 +33,15 @@ class TimetablePage extends StatefulWidget { } class _TimetablePageState extends State { - TimeController _controller; + TimeController _timeController; + DateController _dateController; /// UniEventInstance? @override void dispose() { - _controller?.dispose(); + _timeController?.dispose(); + _dateController?.dispose(); super.dispose(); } @@ -47,8 +49,11 @@ class _TimetablePageState extends State { Widget build(BuildContext context) { final authProvider = Provider.of(context); final eventProvider = Provider.of(context); - if (_controller == null) { - _controller = TimeController( + + _dateController ??= DateController(); + + if (_timeController == null) { + _timeController = TimeController( initialRange: TimeRange(7.hours + 55.minutes, 20.hours + 5.minutes), // TODO(IoanaAlexandru): Make initialTimeRange customizable in settings ); @@ -60,16 +65,17 @@ class _TimetablePageState extends State { return AppScaffold( title: AnimatedBuilder( - animation: _controller.dateListenable, + animation: _timeController.dateListenable, builder: (context, child) => Text( authProvider.isAuthenticated && !authProvider.isAnonymous ? S.current.navigationTimetable - : _controller.currentMonth.titleCase), + : _timeController.currentMonth.titleCase), ), needsToBeAuthenticated: true, leading: AppScaffoldAction( icon: Icons.today_outlined, - onPressed: () => _controller.animateToToday(), + onPressed: () => _dateController.animateTo(DateTime.now(), vsync: null), + //.animateToToday(), tooltip: S.current.actionJumpToToday, ), actions: [ @@ -97,40 +103,42 @@ class _TimetablePageState extends State { padding: const EdgeInsets.all(10), child: Stack( children: [ - Timetable( - controller: _controller, - dateHeaderBuilder: (_, date) => DateHeader(date), - eventBuilder: (event) => UniEventWidget(event), + TimetableConfig( + dateController: _dateController, + timeController: _timeController, + eventBuilder: (context, event) => UniEventWidget(event), + child: MultiDateTimetable(), + eventProvider: (date) => someListOfEvents, allDayEventBuilder: (context, event, info) => - UniAllDayEventWidget( - event, - info: info, - ), - onEventBackgroundTap: (dateTime, isAllDay) { - if (!isAllDay) { + UniAllDayEventWidget(event, info: info), + callbacks: TimetableCallbacks( + // TODO(bogpie): Typing on an all day event (e.g.: holiday). + onDateTimeBackgroundTap: (dateTime) { final user = Provider.of(context, listen: false) .currentUserFromCache; if (user.canAddPublicInfo) { - Navigator.of(context).push(MaterialPageRoute( - builder: (_) => ChangeNotifierProxyProvider( - create: (_) => FilterProvider(), - update: (context, authProvider, filterProvider) { - return filterProvider..updateAuth(authProvider); - }, - child: AddEventView( - initialEvent: UniEvent( - start: dateTime, - duration: const Period(hours: 2), - id: null), + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ChangeNotifierProxyProvider< + AuthProvider, FilterProvider>( + create: (_) => FilterProvider(), + update: (context, authProvider, filterProvider) { + return filterProvider..updateAuth(authProvider); + }, + child: AddEventView( + initialEvent: UniEvent( + start: dateTime, + duration: const Period(hours: 2), + id: null), + ), ), ), - )); + ); } else { AppToast.show(S.current.errorPermissionDenied); } - } - }, + }, + ), ), ], ), From 5df5f02a568d3ee5eaedc305de3313527490e2c5 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Sun, 22 Aug 2021 15:16:25 +0300 Subject: [PATCH 28/60] Compatibility fixes Improved migration to DateTime. Improved migration to new timetable API Renamed some instances of "duration" to "period" to avoid confusion, until further full migration to Duration & DateTimeRange. --- .../timetable/model/events/all_day_event.dart | 2 +- .../timetable/model/events/class_event.dart | 4 +- .../model/events/recurring_event.dart | 6 +-- .../timetable/model/events/uni_event.dart | 41 ++++++++----------- .../service/google_calendar_services.dart | 2 +- .../timetable/service/uni_event_provider.dart | 8 ++-- lib/pages/timetable/timetable_utils.dart | 37 ++++++++++++++++- .../timetable/view/events/add_event_view.dart | 30 +++++++------- lib/pages/timetable/view/timetable_page.dart | 32 +++++++++++---- test/integration_test.dart | 22 +++++----- 10 files changed, 113 insertions(+), 71 deletions(-) diff --git a/lib/pages/timetable/model/events/all_day_event.dart b/lib/pages/timetable/model/events/all_day_event.dart index e775a333b..3c57f7b88 100644 --- a/lib/pages/timetable/model/events/all_day_event.dart +++ b/lib/pages/timetable/model/events/all_day_event.dart @@ -27,7 +27,7 @@ class AllDayUniEvent extends UniEvent { name: name, location: location, start: start.atMidnight(), - duration: Period.differenceBetweenDates( + period: Period.differenceBetweenDates( LocalDate.dateTime(start), LocalDate.dateTime(end.addDays(1))), id: id, color: color, diff --git a/lib/pages/timetable/model/events/class_event.dart b/lib/pages/timetable/model/events/class_event.dart index 1caf0fc41..dc683ca6e 100644 --- a/lib/pages/timetable/model/events/class_event.dart +++ b/lib/pages/timetable/model/events/class_event.dart @@ -13,7 +13,7 @@ class ClassEvent extends RecurringUniEvent { @required this.teacher, @required RecurrenceRule rrule, @required DateTime start, - @required Period duration, + @required Period period, @required String id, List relevance, String degree, @@ -30,7 +30,7 @@ class ClassEvent extends RecurringUniEvent { name: name, location: location, start: start, - duration: duration, + period: period, degree: degree, relevance: relevance, id: id, diff --git a/lib/pages/timetable/model/events/recurring_event.dart b/lib/pages/timetable/model/events/recurring_event.dart index 7c6cd8cb7..48da22623 100644 --- a/lib/pages/timetable/model/events/recurring_event.dart +++ b/lib/pages/timetable/model/events/recurring_event.dart @@ -13,7 +13,7 @@ class RecurringUniEvent extends UniEvent { const RecurringUniEvent({ @required this.rrule, @required DateTime start, - @required Period duration, + @required Period period, @required String id, List relevance, String degree, @@ -30,7 +30,7 @@ class RecurringUniEvent extends UniEvent { name: name, location: location, start: start, - duration: duration, + period: period, degree: degree, relevance: relevance, id: id, @@ -95,7 +95,7 @@ class RecurringUniEvent extends UniEvent { // Calculate recurrences int i = 0; for (final start in rrule.getInstances(start: start)) { - final DateTime end = start.add(duration.toTime().toDuration); + final DateTime end = start.add(period.toTime().toDuration); if (intersectingInterval != null) { if (end < intersectingInterval.start) continue; if (start > intersectingInterval.end) break; diff --git a/lib/pages/timetable/model/events/uni_event.dart b/lib/pages/timetable/model/events/uni_event.dart index 8b0f30b77..d9e0a9a79 100644 --- a/lib/pages/timetable/model/events/uni_event.dart +++ b/lib/pages/timetable/model/events/uni_event.dart @@ -96,7 +96,7 @@ extension UniEventTypeExtension on UniEventType { class UniEvent { const UniEvent({ @required this.start, - @required this.duration, + @required this.period, @required this.id, this.name, this.location, @@ -114,7 +114,7 @@ class UniEvent { final Color color; final UniEventType type; final DateTime start; - final Period duration; + final Period period; final String name; final String location; final ClassHeader classHeader; @@ -130,7 +130,7 @@ class UniEvent { Iterable generateInstances( {DateTimeRange intersectingInterval}) sync* { - final DateTime end = start.add(duration.toTime().toDuration); + final DateTime end = start.add(period.toTime().toDuration); if (intersectingInterval != null) { if (end < intersectingInterval.start || start > intersectingInterval.end) { @@ -144,7 +144,7 @@ class UniEvent { mainEvent: this, color: color, start: start, - end: start.add(duration.toTime().toDuration), + end: start.add(period.toTime().toDuration), location: location, ); } @@ -185,35 +185,28 @@ class UniEventInstance extends Event { String get relativeDateString => getDateString(useRelativeDayFormat: true); String getDateString({bool useRelativeDayFormat}) { - final LocalDateTime defaultStart = LocalDateTime.dateTime(start); - final LocalDateTime defaultEnd = LocalDateTime.dateTime(this.end); - final LocalDateTime end = defaultEnd.clockTime.equals(LocalTime(00, 00, 00)) - ? defaultEnd.subtractDays(1) - : defaultEnd; - - String string = useRelativeDayFormat && - defaultStart.calendarDate.equals(LocalDate.today()) + final DateTime end = + this.end.isMidnight() ? this.end.subtractDays(1) : this.end; + + String string = useRelativeDayFormat && start.isToday ? S.current.labelToday - : useRelativeDayFormat && - defaultStart.calendarDate - .subtractDays(1) - .equals(LocalDate.today()) + : useRelativeDayFormat && start.subtractDays(1).isToday ? S.current.labelTomorrow - : defaultStart.calendarDate.toString('dddd, dd MMMM'); + : start.toStringWithFormat('dddd, dd MMMM'); - if (!defaultStart.clockTime.equals(LocalTime(00, 00, 00))) { - string += ' • ${defaultStart.clockTime.toString('HH:mm')}'; + if (!start.isMidnight()) { + string += ' • ${start.toStringWithFormat('HH:mm')}'; } - if (defaultStart.calendarDate != defaultEnd.calendarDate) { - string += ' - ${end.calendarDate.toString('dddd, dd MMMM')}'; + if (start.atStartOfDay != end.atStartOfDay) { + string += ' - ${end.toStringWithFormat('dddd, dd MMMM')}'; } - if (!end.clockTime.equals(LocalTime(00, 00, 00))) { - if (defaultStart.calendarDate != defaultEnd.calendarDate) { + if (!end.isMidnight()) { + if (start.atStartOfDay != end.atStartOfDay) { string += ' • '; } else { string += '-'; } - string += end.clockTime.toString('HH:mm'); + string += end.toStringWithFormat('HH:mm'); } return string; } diff --git a/lib/pages/timetable/service/google_calendar_services.dart b/lib/pages/timetable/service/google_calendar_services.dart index 957739f24..6581c6e41 100644 --- a/lib/pages/timetable/service/google_calendar_services.dart +++ b/lib/pages/timetable/service/google_calendar_services.dart @@ -47,7 +47,7 @@ extension UniEventProviderGoogleCalendar on UniEventProvider { // Google Calendar uses the IANA timezone format, but the native Dart `DateTime` uses an abbreviation provided by the operating system. ..dateTime = startDateTime; - final Duration duration = uniEvent.duration.toTime().toDuration; + final Duration duration = uniEvent.period.toTime().toDuration; final g_cal.EventDateTime end = g_cal.EventDateTime(); final DateTime endDateTime = startDateTime.add(duration); diff --git a/lib/pages/timetable/service/uni_event_provider.dart b/lib/pages/timetable/service/uni_event_provider.dart index e442cb672..90cbb83a6 100644 --- a/lib/pages/timetable/service/uni_event_provider.dart +++ b/lib/pages/timetable/service/uni_event_provider.dart @@ -103,7 +103,7 @@ extension UniEventExtension on UniEvent { name: json['name'], // Convert time to UTC and then to local time start: (json['start'] as Timestamp).toDate(), - duration: PeriodExtension.fromJSON(json['duration']), + period: PeriodExtension.fromJSON(json['duration']), location: json['location'], // TODO(IoanaAlexandru): Allow users to set event colours in settings color: type.color, @@ -124,7 +124,7 @@ extension UniEventExtension on UniEvent { type: type, name: json['name'], start: (json['start'] as Timestamp).toDate(), - duration: PeriodExtension.fromJSON(json['duration']), + period: PeriodExtension.fromJSON(json['duration']), location: json['location'], color: type.color, classHeader: classHeader, @@ -143,7 +143,7 @@ extension UniEventExtension on UniEvent { name: json['name'], // Convert time to UTC and then to local time start: (json['start'] as Timestamp).toDate(), - duration: PeriodExtension.fromJSON(json['duration']), + period: PeriodExtension.fromJSON(json['duration']), location: json['location'], // TODO(IoanaAlexandru): Allow users to set event colours in settings color: type.color, @@ -166,7 +166,7 @@ extension UniEventExtension on UniEvent { 'type': type, 'name': name, 'start': start.toTimestamp(), - 'duration': duration.toJSON(), + 'duration': period.toJSON(), 'location': location, 'class': classHeader.id, 'degree': degree, diff --git a/lib/pages/timetable/timetable_utils.dart b/lib/pages/timetable/timetable_utils.dart index 702fe8721..c0eeeaca1 100644 --- a/lib/pages/timetable/timetable_utils.dart +++ b/lib/pages/timetable/timetable_utils.dart @@ -1,15 +1,42 @@ library timetable_utils; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:time_machine/time_machine.dart'; + // ignore: implementation_imports import 'package:timetable/src/utils.dart'; -import 'package:timetable/timetable.dart'; -import 'package:flutter/material.dart'; + export 'package:timetable/src/utils.dart'; extension DateTimeExtension on DateTime { + bool isMidnight() => hour == 0 && minute == 0 && second == 0; + DateTime atMidnight() => DateTime(year, month, day, 0, 0, 0, 0, 0); DateTime addDays(int noDays) => add(Duration(days: noDays)); + + DateTime subtractDays(int noDays) => subtract(Duration(days: noDays)); + + String toStringWithFormat(String format) { + return DateFormat(format).format(this); + } + + DateTime at(DateTime time) { + return copyWith( + hour: time.hour, + minute: time.minute, + second: time.second, + millisecond: time.millisecond, + ); + } + + TimeOfDay toTimeOfDay() { + return TimeOfDay( + hour: hour, + minute: minute, + ); + } } extension DateTimeRangeExtension on DateTimeRange { @@ -17,3 +44,9 @@ extension DateTimeRangeExtension on DateTimeRange { return dateTime >= start && dateTime <= end; } } + +extension DurationExtension on Duration { + Period toPeriod() { + return Period(minutes: inMinutes).normalize(); + } +} diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index 60e131f29..e7810a3ab 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -56,8 +56,8 @@ class _AddEventViewState extends State { ClassHeader selectedClass; Person selectedTeacher; String selectedCalendar; - LocalTime startTime; - Period duration; + DateTime startTime; + Duration duration; Map weekSelected = { WeekType.odd: null, WeekType.even: null, @@ -167,10 +167,10 @@ class _AddEventViewState extends State { TextEditingController(text: widget.initialEvent?.location ?? ''); final startHour = widget.initialEvent?.start?.hour ?? 8; - duration = widget.initialEvent?.duration ?? const Period(hours: 2); - startTime = LocalTime(startHour, 0, 0); + duration = widget.initialEvent?.period ?? const Duration(hours: 2); + startTime = DateTime(startHour, 0, 0); - var initialWeekDays = [ + List<_DayOfWeek> initialWeekDays = [ _DayOfWeek.from(widget.initialEvent?.start?.dayOfWeek) ?? _DayOfWeek.monday ]; @@ -427,13 +427,14 @@ class _AddEventViewState extends State { weekSelected[WeekType.odd] != weekSelected[WeekType.even] ? 2 : 1, - until: semester.endDate.add(const Period(days: 1)).atMidnight()); + until: + semester.endDate.add(const Duration(days: 1)).atMidnight()); final event = ClassEvent( teacher: selectedTeacher, rrule: rrule, start: start, - duration: duration, + period: duration.toPeriod(), id: widget.initialEvent?.id, relevance: relevanceController.customRelevance, degree: relevanceController.degree, @@ -496,12 +497,13 @@ class _AddEventViewState extends State { onPressed: () async { final TimeOfDay start = await showTimePicker( context: context, - initialTime: startTime.toTimeOfDay(), + initialTime: + TimeOfDay(hour: startTime.hour, minute: startTime.minute), ); - setState(() => startTime = start.toLocalTime()); + setState(() => startTime = start.toDateTime()); }, child: Text( - startTime.toString('HH:mm'), + startTime.toStringWithFormat('HH:mm'), style: Theme.of(context).textTheme.headline4, ), ), @@ -538,11 +540,10 @@ class _AddEventViewState extends State { context: context, initialTime: startTime.add(duration).toTimeOfDay(), ); - setState(() => duration = - Period.differenceBetweenTimes(startTime, end.toLocalTime())); + setState(() => duration = end.toDateTime().difference(startTime)); }, child: Text( - endTime.toString('HH:mm'), + endTime.toStringWithFormat('HH:mm'), style: Theme.of(context).textTheme.headline4, ), ), @@ -752,7 +753,8 @@ extension LocalTimeConversion on LocalTime { } extension TimeOfDayConversion on TimeOfDay { - LocalTime toLocalTime() => LocalTime(hour, minute, 0); + DateTime toDateTime() => DateTime(0, 1, 1, hour, minute, 0); +// LocalTime toLocalTime() => LocalTime(hour, minute, 0); } extension DateTimeComparisons on DateTime { diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index 370577996..328637ab2 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -5,6 +5,7 @@ import 'package:recase/recase.dart'; import 'package:supercharged/supercharged.dart'; import 'package:time_machine/time_machine.dart'; import 'package:timetable/timetable.dart'; +import 'package:intl/intl.dart'; import '../../../authentication/service/auth_provider.dart'; import '../../../generated/l10n.dart'; @@ -50,12 +51,16 @@ class _TimetablePageState extends State { final authProvider = Provider.of(context); final eventProvider = Provider.of(context); - _dateController ??= DateController(); + _dateController ??= DateController( + initialDate: DateTimeTimetable.today(), + visibleRange: VisibleDateRange.week(startOfWeek: DateTime.monday), + ); if (_timeController == null) { _timeController = TimeController( initialRange: TimeRange(7.hours + 55.minutes, 20.hours + 5.minutes), // TODO(IoanaAlexandru): Make initialTimeRange customizable in settings + maxRange: TimeRange(0.hours, 24.hours), ); if (authProvider.isAuthenticated && !authProvider.isAnonymous) { @@ -65,11 +70,11 @@ class _TimetablePageState extends State { return AppScaffold( title: AnimatedBuilder( - animation: _timeController.dateListenable, + animation: null, // animation: _timeController.dateListenable, builder: (context, child) => Text( authProvider.isAuthenticated && !authProvider.isAnonymous ? S.current.navigationTimetable - : _timeController.currentMonth.titleCase), + : _dateController.currentMonth.titleCase), ), needsToBeAuthenticated: true, leading: AppScaffoldAction( @@ -108,7 +113,8 @@ class _TimetablePageState extends State { timeController: _timeController, eventBuilder: (context, event) => UniEventWidget(event), child: MultiDateTimetable(), - eventProvider: (date) => someListOfEvents, + eventProvider: (date) => null, + // ?? allDayEventBuilder: (context, event, info) => UniAllDayEventWidget(event, info: info), callbacks: TimetableCallbacks( @@ -128,7 +134,7 @@ class _TimetablePageState extends State { child: AddEventView( initialEvent: UniEvent( start: dateTime, - duration: const Period(hours: 2), + period: const Period(hours: 2), id: null), ), ), @@ -336,8 +342,16 @@ class _TimetablePageState extends State { } } -extension MonthController on TimetableController { - String get currentMonth => - LocalDateTime(2020, dateListenable.value.monthOfYear, 1, 1, 1, 1) - .toString('MMMM'); +extension MonthController on DateController { + String get currentMonth => DateFormat('MMMM').format( + DateTime( + value.date.year, + value.date.month, + 1, + 0, + 0, + 0, + ), + ); +// LocalDateTime(2020, this.value.monthOfYear, 1, 1, 1, 1).toString('MMMM'); } diff --git a/test/integration_test.dart b/test/integration_test.dart index 4fe811def..0120ad623 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -592,7 +592,7 @@ Future main() async { calendar: calendar, rrule: rruleEveryWeekFirstSem, start: weekStart.at(LocalTime(8, 0, 0)), - duration: duration, + period: duration, id: '0', ), RecurringUniEvent( @@ -600,7 +600,7 @@ Future main() async { calendar: calendar, rrule: rruleEveryTwoWeeksFirstSem, start: weekStart.at(LocalTime(10, 0, 0)), - duration: duration, + period: duration, id: '1', ), RecurringUniEvent( @@ -608,7 +608,7 @@ Future main() async { calendar: calendar, rrule: rruleEveryWeekFirstSem, start: weekStart.addDays(1).at(LocalTime(8, 0, 0)), - duration: duration, + period: duration, id: '2', ), RecurringUniEvent( @@ -616,7 +616,7 @@ Future main() async { calendar: calendar, rrule: rruleEveryTwoWeeksFirstSem, start: weekStart.addDays(1).at(LocalTime(9, 0, 0)), - duration: duration, + period: duration, id: '3', ), RecurringUniEvent( @@ -624,21 +624,21 @@ Future main() async { calendar: calendar, rrule: rruleEveryWeekFirstSem, start: weekStart.addDays(2).at(LocalTime(8, 0, 0)), - duration: duration, + period: duration, id: '4', ), RecurringUniEvent( name: 'W2', rrule: rruleEveryWeek, start: weekStart.addDays(2).at(LocalTime(10, 0, 0)), - duration: duration, + period: duration, id: '5', ), RecurringUniEvent( name: 'W3', rrule: rruleEveryTwoWeeks, start: weekStart.addDays(2).at(LocalTime(12, 0, 0)), - duration: duration, + period: duration, id: '6', ), ClassEvent( @@ -651,7 +651,7 @@ Future main() async { calendar: calendar, rrule: rruleEveryWeek, start: weekStart.addDays(3).at(LocalTime(10, 0, 0)), - duration: duration, + period: duration, id: '7', ), RecurringUniEvent( @@ -663,7 +663,7 @@ Future main() async { calendar: calendar, rrule: rruleEveryTwoWeeks, start: weekStart.addDays(3).at(LocalTime(12, 0, 0)), - duration: duration, + period: duration, id: '8', ), RecurringUniEvent( @@ -671,7 +671,7 @@ Future main() async { calendar: calendar, rrule: rruleEveryWeek, start: weekStart.addDays(4).at(LocalTime(10, 0, 0)), - duration: duration, + period: duration, id: '9', ), RecurringUniEvent( @@ -679,7 +679,7 @@ Future main() async { calendar: calendar, rrule: rruleEveryTwoWeeks, start: weekStart.addDays(4).at(LocalTime(12, 0, 0)), - duration: duration, + period: duration, id: '10', ), ]; From ae96e6d7aee629797c574c4147e5c9d521830b8c Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Sun, 22 Aug 2021 20:14:12 +0300 Subject: [PATCH 29/60] Temporary fix in order to solve errors regarding rrule package update --- lib/pages/timetable/view/events/add_event_view.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index e7810a3ab..b8eff1741 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -171,7 +171,8 @@ class _AddEventViewState extends State { startTime = DateTime(startHour, 0, 0); List<_DayOfWeek> initialWeekDays = [ - _DayOfWeek.from(widget.initialEvent?.start?.dayOfWeek) ?? + _DayOfWeek.from( + LocalDate.dateTime(widget.initialEvent?.start)?.dayOfWeek) ?? _DayOfWeek.monday ]; if (widget.initialEvent != null && @@ -183,7 +184,7 @@ class _AddEventViewState extends State { initialWeekDays = (widget.initialEvent as RecurringUniEvent) .rrule .byWeekDays - .map((entry) => _DayOfWeek.from(entry.day)) + .map((entry) => _DayOfWeek.from(time_machine.DayOfWeek(entry.day))) .toList(); } for (final initialWeekDay in initialWeekDays) { @@ -421,7 +422,7 @@ class _AddEventViewState extends State { byWeekDays: (Map<_DayOfWeek, bool>.from(weekDaySelected) ..removeWhere((key, value) => !value)) .keys - .map((weekDay) => ByWeekDayEntry(weekDay)) + .map((weekDay) => ByWeekDayEntry(weekDay.value)) .toSet(), interval: weekSelected[WeekType.odd] != weekSelected[WeekType.even] @@ -702,7 +703,7 @@ class _DayOfWeek extends time_machine.DayOfWeek with Localizable { @override String toLocalizedString() { - final helperDate = DateTime.now().next(this); + final helperDate = LocalDate.today().next(this); return LocalDatePattern.createWithCurrentCulture('ddd') .format(helperDate) .substring(0, 3); From 5c7fe5032e03d144135c4720a2b9743249327877 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Sun, 22 Aug 2021 20:18:07 +0300 Subject: [PATCH 30/60] Marked some comments so I can return to them later in order to solve associated issues. --- lib/pages/timetable/view/date_header.dart | 5 ++++- lib/pages/timetable/view/timetable_page.dart | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/pages/timetable/view/date_header.dart b/lib/pages/timetable/view/date_header.dart index 34ae691d3..a9af4bdc4 100644 --- a/lib/pages/timetable/view/date_header.dart +++ b/lib/pages/timetable/view/date_header.dart @@ -1,7 +1,6 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:black_hole_flutter/black_hole_flutter.dart'; import 'package:flutter/material.dart'; -import 'package:time_machine/time_machine.dart'; import 'package:time_machine/time_machine_text_patterns.dart'; // ignore: implementation_imports @@ -45,6 +44,10 @@ class WeekdayIndicator extends StatelessWidget { final timetableTheme = TimetableTheme.of(context); final states = statesFor(date); + + final style = + TimetableTheme.of(context).weekdayIndicatorStyleProvider(date); + final pattern = timetableTheme?.weekDayIndicatorPattern?.resolve(states) ?? LocalDatePattern.createWithCurrentCulture('ddd'); final decoration = diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index 328637ab2..a78ccc5b1 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -37,7 +37,7 @@ class _TimetablePageState extends State { TimeController _timeController; DateController _dateController; - /// UniEventInstance? + // ? UniEventInstance @override void dispose() { @@ -70,7 +70,7 @@ class _TimetablePageState extends State { return AppScaffold( title: AnimatedBuilder( - animation: null, // animation: _timeController.dateListenable, + animation: null, // ? animation: _timeController.dateListenable, builder: (context, child) => Text( authProvider.isAuthenticated && !authProvider.isAnonymous ? S.current.navigationTimetable @@ -80,7 +80,7 @@ class _TimetablePageState extends State { leading: AppScaffoldAction( icon: Icons.today_outlined, onPressed: () => _dateController.animateTo(DateTime.now(), vsync: null), - //.animateToToday(), + // ? .animateToToday(), tooltip: S.current.actionJumpToToday, ), actions: [ @@ -114,7 +114,7 @@ class _TimetablePageState extends State { eventBuilder: (context, event) => UniEventWidget(event), child: MultiDateTimetable(), eventProvider: (date) => null, - // ?? + // ? allDayEventBuilder: (context, event, info) => UniAllDayEventWidget(event, info: info), callbacks: TimetableCallbacks( From 2792e125d79b971e7e170903d03e3d7bd92a9ff5 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Sun, 22 Aug 2021 20:30:40 +0300 Subject: [PATCH 31/60] Copied bottom navigation bar implementation from master to help with debugging. --- lib/navigation/bottom_navigation_bar.dart | 33 ++++++++-------- lib/pages/timetable/view/date_header.dart | 48 ++++++++++++----------- 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/lib/navigation/bottom_navigation_bar.dart b/lib/navigation/bottom_navigation_bar.dart index 0d93cf4ef..9754e9c5e 100644 --- a/lib/navigation/bottom_navigation_bar.dart +++ b/lib/navigation/bottom_navigation_bar.dart @@ -5,6 +5,7 @@ import '../generated/l10n.dart'; import '../pages/home/home_page.dart'; import '../pages/people/view/people_page.dart'; import '../pages/portal/view/portal_page.dart'; +import '../pages/timetable/view/timetable_page.dart'; class AppBottomNavigationBar extends StatefulWidget { const AppBottomNavigationBar({this.tabIndex = 0}); @@ -25,7 +26,7 @@ class _AppBottomNavigationBarState extends State @override void initState() { super.initState(); - tabController = TabController(vsync: this, length: 3); + tabController = TabController(vsync: this, length: 4); tabController.addListener(() { if (!tabController.indexIsChanging) { setState(() { @@ -35,7 +36,7 @@ class _AppBottomNavigationBarState extends State }); tabs = [ HomePage(key: const PageStorageKey('Home'), tabController: tabController), -// const TimetablePage(), // Cannot preserve state with PageStorageKey + const TimetablePage(), // Cannot preserve state with PageStorageKey const PortalPage(key: PageStorageKey('Portal')), const PeoplePage(key: PageStorageKey('People')), ]; @@ -59,7 +60,7 @@ class _AppBottomNavigationBarState extends State ), bottomNavigationBar: SafeArea( child: SizedBox( - height: 50, + height: 52, child: Column( children: [ const Divider(indent: 0, endIndent: 0, height: 1), @@ -72,36 +73,36 @@ class _AppBottomNavigationBarState extends State ? const Icon(Icons.home) : const Icon(Icons.home_outlined), text: S.current.navigationHome, - iconMargin: const EdgeInsets.only(top: 5), + iconMargin: EdgeInsets.zero, + ), + Tab( + icon: currentTab == 1 + ? const Icon(Icons.calendar_today) + : const Icon(Icons.calendar_today_outlined), + text: S.current.navigationTimetable, + iconMargin: EdgeInsets.zero, ), -// Tab( -// icon: currentTab == 1 -// ? const Icon(Icons.calendar_today) -// : const Icon(Icons.calendar_today_outlined), -// text: S.current.navigationTimetable, -// iconMargin: const EdgeInsets.only(top: 5), -// ), Tab( icon: const Icon(FeatherIcons.globe), text: S.current.navigationPortal, - iconMargin: const EdgeInsets.only(top: 5), + iconMargin: EdgeInsets.zero, ), Tab( icon: currentTab == 3 ? const Icon(Icons.people) : const Icon(Icons.people_outlined), text: S.current.navigationPeople, - iconMargin: const EdgeInsets.only(top: 5), + iconMargin: EdgeInsets.zero, ), ], labelColor: Theme.of(context).accentColor, - labelPadding: EdgeInsets.zero, - indicatorPadding: EdgeInsets.zero, + labelPadding: const EdgeInsets.only(top: 4), unselectedLabelColor: - Theme.of(context).unselectedWidgetColor, + Theme.of(context).unselectedWidgetColor, indicatorColor: Theme.of(context).accentColor, ), ), + const SizedBox(height: 2) ], ), ), diff --git a/lib/pages/timetable/view/date_header.dart b/lib/pages/timetable/view/date_header.dart index a9af4bdc4..87840d766 100644 --- a/lib/pages/timetable/view/date_header.dart +++ b/lib/pages/timetable/view/date_header.dart @@ -47,31 +47,33 @@ class WeekdayIndicator extends StatelessWidget { final style = TimetableTheme.of(context).weekdayIndicatorStyleProvider(date); + // + // final pattern = timetableTheme?.weekDayIndicatorPattern?.resolve(states) ?? + // LocalDatePattern.createWithCurrentCulture('ddd'); + // final decoration = + // timetableTheme?.weekDayIndicatorDecoration?.resolve(states) ?? + // const BoxDecoration(); + // final textStyle = + // timetableTheme?.weekDayIndicatorTextStyle?.resolve(states) ?? + // TextStyle( + // color: date.isToday + // ? timetableTheme?.primaryColor ?? theme.primaryColor + // : theme.highEmphasisOnBackground, + // ); - final pattern = timetableTheme?.weekDayIndicatorPattern?.resolve(states) ?? - LocalDatePattern.createWithCurrentCulture('ddd'); - final decoration = - timetableTheme?.weekDayIndicatorDecoration?.resolve(states) ?? - const BoxDecoration(); - final textStyle = - timetableTheme?.weekDayIndicatorTextStyle?.resolve(states) ?? - TextStyle( - color: date.isToday - ? timetableTheme?.primaryColor ?? theme.primaryColor - : theme.highEmphasisOnBackground, - ); + return const AutoSizeText('Placeholder'); - return DecoratedBox( - decoration: decoration, - child: Padding( - padding: const EdgeInsets.all(4), - child: AutoSizeText( - pattern.format(date), - style: textStyle, - maxLines: 1, - ), - ), - ); + // return DecoratedBox( + // decoration: decoration, + // child: Padding( + // padding: const EdgeInsets.all(4), + // child: AutoSizeText( + // pattern.format(date), + // style: textStyle, + // maxLines: 1, + // ), + // ), + // ); } static Set statesFor(DateTime date) { From d32a778f615419356536a7d53c2b231b98efd112 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Sun, 22 Aug 2021 21:01:54 +0300 Subject: [PATCH 32/60] Updated event provider --- lib/pages/timetable/view/timetable_page.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index a78ccc5b1..d9936422b 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -37,8 +37,6 @@ class _TimetablePageState extends State { TimeController _timeController; DateController _dateController; - // ? UniEventInstance - @override void dispose() { _timeController?.dispose(); @@ -69,9 +67,8 @@ class _TimetablePageState extends State { } return AppScaffold( - title: AnimatedBuilder( - animation: null, // ? animation: _timeController.dateListenable, - builder: (context, child) => Text( + title: Builder( // ? AnimatedBuilder + builder: (context) => Text( authProvider.isAuthenticated && !authProvider.isAnonymous ? S.current.navigationTimetable : _dateController.currentMonth.titleCase), @@ -113,8 +110,8 @@ class _TimetablePageState extends State { timeController: _timeController, eventBuilder: (context, event) => UniEventWidget(event), child: MultiDateTimetable(), - eventProvider: (date) => null, - // ? + eventProvider: + Provider.of(context).eventProvider, allDayEventBuilder: (context, event, info) => UniAllDayEventWidget(event, info: info), callbacks: TimetableCallbacks( From d977ec4894c7a4cb67fb1eff9745b7ae0481cd26 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Mon, 23 Aug 2021 13:59:07 +0300 Subject: [PATCH 33/60] Compatibility fixes --- lib/main.dart | 4 +++- lib/pages/timetable/view/date_header.dart | 2 +- lib/pages/timetable/view/events/add_event_view.dart | 3 ++- lib/pages/timetable/view/timetable_page.dart | 11 ++++++++--- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index c6bbc978e..5ee78fff6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:pref/pref.dart'; import 'package:provider/provider.dart'; import 'package:rrule/rrule.dart'; +import 'package:timetable/timetable.dart'; import 'authentication/service/auth_provider.dart'; import 'authentication/view/login_view.dart'; @@ -148,7 +149,8 @@ class _MyAppState extends State { localizationsDelegates: [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, - S.delegate + S.delegate, + TimetableLocalizationsDelegate(), ], supportedLocales: S.delegate.supportedLocales, initialRoute: Routes.root, diff --git a/lib/pages/timetable/view/date_header.dart b/lib/pages/timetable/view/date_header.dart index 87840d766..9928600af 100644 --- a/lib/pages/timetable/view/date_header.dart +++ b/lib/pages/timetable/view/date_header.dart @@ -47,7 +47,7 @@ class WeekdayIndicator extends StatelessWidget { final style = TimetableTheme.of(context).weekdayIndicatorStyleProvider(date); - // + // ? // final pattern = timetableTheme?.weekDayIndicatorPattern?.resolve(states) ?? // LocalDatePattern.createWithCurrentCulture('ddd'); // final decoration = diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index b8eff1741..984eacf75 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -167,7 +167,8 @@ class _AddEventViewState extends State { TextEditingController(text: widget.initialEvent?.location ?? ''); final startHour = widget.initialEvent?.start?.hour ?? 8; - duration = widget.initialEvent?.period ?? const Duration(hours: 2); + duration = widget.initialEvent?.period?.toTime()?.toDuration ?? + const Duration(hours: 2); startTime = DateTime(startHour, 0, 0); List<_DayOfWeek> initialWeekDays = [ diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index d9936422b..4e703f3df 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -33,7 +33,8 @@ class TimetablePage extends StatefulWidget { _TimetablePageState createState() => _TimetablePageState(); } -class _TimetablePageState extends State { +class _TimetablePageState extends State + with TickerProviderStateMixin { TimeController _timeController; DateController _dateController; @@ -67,7 +68,8 @@ class _TimetablePageState extends State { } return AppScaffold( - title: Builder( // ? AnimatedBuilder + title: Builder( + // ? AnimatedBuilder builder: (context) => Text( authProvider.isAuthenticated && !authProvider.isAnonymous ? S.current.navigationTimetable @@ -76,7 +78,10 @@ class _TimetablePageState extends State { needsToBeAuthenticated: true, leading: AppScaffoldAction( icon: Icons.today_outlined, - onPressed: () => _dateController.animateTo(DateTime.now(), vsync: null), + onPressed: () { + _dateController.animateToToday(vsync: this); + _timeController.animateToShowFullDay(vsync: this); + }, // ? .animateToToday(), tooltip: S.current.actionJumpToToday, ), From 5269e7c700f123e75e69a552dcef7b39294b5e25 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Thu, 26 Aug 2021 22:58:43 +0300 Subject: [PATCH 34/60] Started to supply recurrent and all-day events asynchronously via eventProvider --- lib/pages/settings/view/settings_page.dart | 2 +- .../timetable/model/events/uni_event.dart | 10 ++ .../timetable/service/uni_event_provider.dart | 21 ++- lib/pages/timetable/view/timetable_page.dart | 157 +++++++++++------- 4 files changed, 124 insertions(+), 66 deletions(-) diff --git a/lib/pages/settings/view/settings_page.dart b/lib/pages/settings/view/settings_page.dart index 07fd7c070..3723e14d5 100644 --- a/lib/pages/settings/view/settings_page.dart +++ b/lib/pages/settings/view/settings_page.dart @@ -131,7 +131,7 @@ class _SettingsPageState extends State { final eventProvider = Provider.of( context, listen: false); -// await eventProvider.exportToGoogleCalendar(); + await eventProvider.exportToGoogleCalendar(); } }, title: Text(S.current.settingsExportToGoogleCalendar), diff --git a/lib/pages/timetable/model/events/uni_event.dart b/lib/pages/timetable/model/events/uni_event.dart index d9e0a9a79..ba901669c 100644 --- a/lib/pages/timetable/model/events/uni_event.dart +++ b/lib/pages/timetable/model/events/uni_event.dart @@ -128,6 +128,16 @@ class UniEvent { return generateInstances().first.dateString; } + // UniEventInstance toUniEventInstance() { + // return UniEventInstance( + // title: name, + // mainEvent: this, + // start: start, + // end: start.add(period.toTime().toDuration), + // location: location, + // ); + // } + Iterable generateInstances( {DateTimeRange intersectingInterval}) sync* { final DateTime end = start.add(period.toTime().toDuration); diff --git a/lib/pages/timetable/service/uni_event_provider.dart b/lib/pages/timetable/service/uni_event_provider.dart index 90cbb83a6..8353fecc1 100644 --- a/lib/pages/timetable/service/uni_event_provider.dart +++ b/lib/pages/timetable/service/uni_event_provider.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:async/async.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:dart_date/dart_date.dart' as DartDate show Interval; import 'package:flutter/material.dart'; import 'package:googleapis/calendar/v3.dart' as g_cal; import 'package:rrule/rrule.dart'; @@ -212,8 +213,8 @@ extension AcademicCalendarExtension on AcademicCalendar { } } -class UniEventProvider extends DefaultEventProvider - with ChangeNotifier { +// extends DefaultEventProvider +class UniEventProvider with ChangeNotifier { UniEventProvider({AuthProvider authProvider, PersonProvider personProvider}) : _authProvider = authProvider ?? AuthProvider(), _personProvider = personProvider ?? PersonProvider() { @@ -307,11 +308,23 @@ class UniEventProvider extends DefaultEventProvider return stream.map((events) => events.expand((i) => i).toList()); } + Future> loadEventsForRange(DateTimeRange range) async { + final List events = await _events.first; + final List eventsInRange = events + .where((event) => range.contains(event.start)) + .map((event) => event.generateInstances( + intersectingInterval: + DateTimeRange(start: event.start, end: event.start))) + .expand((i) => i) + .toList(); + return eventsInRange; + } + Future exportToGoogleCalendar() async { final Stream> eventsStream = _events; - final List streamElement = await eventsStream.first; + final List events = await eventsStream.first; final List googleCalendarEvents = []; - for (final UniEvent eventInstance in streamElement) { + for (final UniEvent eventInstance in events) { final g_cal.Event googleCalendarEvent = convertEvent(eventInstance); googleCalendarEvents.add(googleCalendarEvent); } diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index 4e703f3df..6b3e70d1c 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -110,44 +110,77 @@ class _TimetablePageState extends State padding: const EdgeInsets.all(10), child: Stack( children: [ - TimetableConfig( - dateController: _dateController, - timeController: _timeController, - eventBuilder: (context, event) => UniEventWidget(event), - child: MultiDateTimetable(), - eventProvider: - Provider.of(context).eventProvider, - allDayEventBuilder: (context, event, info) => - UniAllDayEventWidget(event, info: info), - callbacks: TimetableCallbacks( - // TODO(bogpie): Typing on an all day event (e.g.: holiday). - onDateTimeBackgroundTap: (dateTime) { - final user = Provider.of(context, listen: false) - .currentUserFromCache; - if (user.canAddPublicInfo) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => ChangeNotifierProxyProvider< - AuthProvider, FilterProvider>( - create: (_) => FilterProvider(), - update: (context, authProvider, filterProvider) { - return filterProvider..updateAuth(authProvider); - }, - child: AddEventView( - initialEvent: UniEvent( - start: dateTime, - period: const Period(hours: 2), - id: null), - ), - ), + ValueListenableBuilder( + valueListenable: _dateController, + builder: (context, value, child) { + final Future> events = + Provider.of(context, listen: false) + .loadEventsForRange( + DateTimeRange( + start: DateTimeTimetable.dateFromPage(value.page.floor()), + end: DateTimeTimetable.dateFromPage( + value.page.ceil() + value.visibleDayCount, + ), + ), + ); + // Or probably preload events outside this range, maybe + // for the previous and next page. + + return FutureBuilder>( + future: events, + builder: (context, snapshot) { + if (snapshot.data == null || snapshot.hasError) { + // Handle loading and error states + return Container(); + } + + return TimetableConfig( + eventProvider: + eventProviderFromFixedList(snapshot.data ?? []), + child: child, + // !, + dateController: _dateController, + timeController: _timeController, + eventBuilder: (context, event) => UniEventWidget(event), + allDayEventBuilder: (context, event, info) => + UniAllDayEventWidget(event, info: info), + callbacks: TimetableCallbacks( + // TODO(bogpie): Typing on an all day event (e.g.: holiday). + onDateTimeBackgroundTap: (dateTime) { + final user = + Provider.of(context, listen: false) + .currentUserFromCache; + if (user.canAddPublicInfo) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ChangeNotifierProxyProvider< + AuthProvider, FilterProvider>( + create: (_) => FilterProvider(), + update: + (context, authProvider, filterProvider) { + return filterProvider + ..updateAuth(authProvider); + }, + child: AddEventView( + initialEvent: UniEvent( + start: dateTime, + period: const Period(hours: 2), + id: null), + ), + ), + ), + ); + } else { + AppToast.show(S.current.errorPermissionDenied); + } + }, ), ); - } else { - AppToast.show(S.current.errorPermissionDenied); - } - }, - ), - ), + }, + ); + }, + child: MultiDateTimetable(), + ) ], ), ), @@ -155,34 +188,36 @@ class _TimetablePageState extends State } Future scheduleDialog(BuildContext context) async { - WidgetsBinding.instance.addPostFrameCallback((_) async { - if (!mounted) { - return; - } + WidgetsBinding.instance.addPostFrameCallback( + (_) async { + if (!mounted) { + return; + } - // Fetch user classes, request necessary info from providers so it's - // cached when we check in the dialog - final user = Provider.of(context, listen: false) - .currentUserFromCache; - await Provider.of(context, listen: false) - .fetchClassHeaders(uid: user.uid); - await Provider.of(context, listen: false).fetchFilter(); - await Provider.of(context, listen: false) - .userAlreadyRequested(user.uid); + // Fetch user classes, request necessary info from providers so it's + // cached when we check in the dialog + final user = Provider.of(context, listen: false) + .currentUserFromCache; + await Provider.of(context, listen: false) + .fetchClassHeaders(uid: user.uid); + await Provider.of(context, listen: false).fetchFilter(); + await Provider.of(context, listen: false) + .userAlreadyRequested(user.uid); - // Slight delay between last frame and dialog - await Future.delayed(const Duration(milliseconds: 100)); + // Slight delay between last frame and dialog + await Future.delayed(const Duration(milliseconds: 100)); - // Show dialog if there are no events - final eventProvider = - Provider.of(context, listen: false); - if (eventProvider.empty) { - await showDialog( - context: context, - builder: buildDialog, - ); - } - }); + // Show dialog if there are no events + final eventProvider = + Provider.of(context, listen: false); + if (eventProvider.empty) { + await showDialog( + context: context, + builder: buildDialog, + ); + } + }, + ); } Widget buildDialog(BuildContext context) { From 5a2ab61e26bf7e649e4b517a1eefc1d60fdab59c Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Tue, 31 Aug 2021 20:00:53 +0300 Subject: [PATCH 35/60] Improved StreamBuilder, events can now be seen on timetable. Created event provider function (getEventsIntersecting), currently from already existing methods. Made necessary conversions to UTC time zone. --- .../timetable/model/events/all_day_event.dart | 4 +-- .../model/events/recurring_event.dart | 5 +-- .../timetable/model/events/uni_event.dart | 4 +-- .../timetable/service/uni_event_provider.dart | 33 +++++++++---------- lib/pages/timetable/view/timetable_page.dart | 12 +++---- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/lib/pages/timetable/model/events/all_day_event.dart b/lib/pages/timetable/model/events/all_day_event.dart index 3c57f7b88..94d894e21 100644 --- a/lib/pages/timetable/model/events/all_day_event.dart +++ b/lib/pages/timetable/model/events/all_day_event.dart @@ -48,8 +48,8 @@ class AllDayUniEvent extends UniEvent { yield UniEventInstance( title: name, mainEvent: this, - start: startDate.atMidnight(), - end: endDate.addDays(1).atMidnight(), + start: startDate.atMidnight().toUtc(), + end: endDate.addDays(1).atMidnight().toUtc(), color: color, ); } diff --git a/lib/pages/timetable/model/events/recurring_event.dart b/lib/pages/timetable/model/events/recurring_event.dart index 48da22623..5b7e721ac 100644 --- a/lib/pages/timetable/model/events/recurring_event.dart +++ b/lib/pages/timetable/model/events/recurring_event.dart @@ -94,7 +94,8 @@ class RecurringUniEvent extends UniEvent { // Calculate recurrences int i = 0; - for (final start in rrule.getInstances(start: start)) { + + for (final start in rrule.getInstances(start: start.toUtc())) { final DateTime end = start.add(period.toTime().toDuration); if (intersectingInterval != null) { if (end < intersectingInterval.start) continue; @@ -105,7 +106,7 @@ class RecurringUniEvent extends UniEvent { for (final holiday in calendar?.holidays ?? []) { final holidayInterval = DateTimeRange(start: holiday.startDate, end: holiday.endDate); - // DateInterval(holiday.startDate, holiday.endDate); + // DateInterval(holiday.startDate, holiday.endDate); if (holidayInterval.contains(start)) { // Skip holidays skip = true; diff --git a/lib/pages/timetable/model/events/uni_event.dart b/lib/pages/timetable/model/events/uni_event.dart index ba901669c..b7306a1d1 100644 --- a/lib/pages/timetable/model/events/uni_event.dart +++ b/lib/pages/timetable/model/events/uni_event.dart @@ -153,8 +153,8 @@ class UniEvent { title: name, mainEvent: this, color: color, - start: start, - end: start.add(period.toTime().toDuration), + start: start.toUtc(), + end: start.add(period.toTime().toDuration).toUtc(), location: location, ); } diff --git a/lib/pages/timetable/service/uni_event_provider.dart b/lib/pages/timetable/service/uni_event_provider.dart index 8353fecc1..61af5d2dc 100644 --- a/lib/pages/timetable/service/uni_event_provider.dart +++ b/lib/pages/timetable/service/uni_event_provider.dart @@ -271,7 +271,6 @@ class UniEventProvider with ChangeNotifier { .snapshots() .asyncMap((snapshot) async { final events = []; - try { for (final doc in snapshot.docs) { ClassHeader classHeader; @@ -308,18 +307,6 @@ class UniEventProvider with ChangeNotifier { return stream.map((events) => events.expand((i) => i).toList()); } - Future> loadEventsForRange(DateTimeRange range) async { - final List events = await _events.first; - final List eventsInRange = events - .where((event) => range.contains(event.start)) - .map((event) => event.generateInstances( - intersectingInterval: - DateTimeRange(start: event.start, end: event.start))) - .expand((i) => i) - .toList(); - return eventsInRange; - } - Future exportToGoogleCalendar() async { final Stream> eventsStream = _events; final List events = await eventsStream.first; @@ -331,7 +318,19 @@ class UniEventProvider with ChangeNotifier { await insertGoogleEvents(googleCalendarEvents); } - @override + Stream> getEventsIntersecting(DateTimeRange interval) { + final streams = >>[]; + final Stream> allDay = + getAllDayEventsIntersecting(interval); + final Stream> partDay = + getPartDayEventsIntersecting(interval); + streams..add(allDay)..add(partDay); + final stream = StreamZip(streams); + + // Flatten zipped streams + return stream.map((events) => events.expand((i) => i).toList()); + } + Stream> getAllDayEventsIntersecting( DateTimeRange interval) { return _events.map((events) => events @@ -351,12 +350,12 @@ class UniEventProvider with ChangeNotifier { }).expand((e) => e))); } - @override Stream> getPartDayEventsIntersecting( - DateTime date) { + DateTimeRange interval) { return _events.map((events) => events .map((event) => event.generateInstances( - intersectingInterval: DateTimeRange(start: date, end: date))) + intersectingInterval: + DateTimeRange(start: interval.start, end: interval.end))) .expand((i) => i) .where((event) => event.isPartDay)); } diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index 6b3e70d1c..7465a2793 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -48,7 +48,6 @@ class _TimetablePageState extends State @override Widget build(BuildContext context) { final authProvider = Provider.of(context); - final eventProvider = Provider.of(context); _dateController ??= DateController( initialDate: DateTimeTimetable.today(), @@ -113,9 +112,9 @@ class _TimetablePageState extends State ValueListenableBuilder( valueListenable: _dateController, builder: (context, value, child) { - final Future> events = + final Stream> events = Provider.of(context, listen: false) - .loadEventsForRange( + .getEventsIntersecting( DateTimeRange( start: DateTimeTimetable.dateFromPage(value.page.floor()), end: DateTimeTimetable.dateFromPage( @@ -126,9 +125,10 @@ class _TimetablePageState extends State // Or probably preload events outside this range, maybe // for the previous and next page. - return FutureBuilder>( - future: events, - builder: (context, snapshot) { + return StreamBuilder>( + stream: events, + builder: (context, + AsyncSnapshot> snapshot) { if (snapshot.data == null || snapshot.hasError) { // Handle loading and error states return Container(); From b2f0d0a867f2a345eef790db81620afced23e2bc Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Tue, 31 Aug 2021 22:37:24 +0300 Subject: [PATCH 36/60] Commented code for testing --- test/authentication_test.dart | 1398 ++++++------ test/integration_test.dart | 3852 ++++++++++++++++----------------- test/portal_test.dart | 354 +-- test/settings_test.dart | 772 +++---- test/test_utils.dart | 566 ++--- 5 files changed, 3471 insertions(+), 3471 deletions(-) diff --git a/test/authentication_test.dart b/test/authentication_test.dart index dcbc2baf3..5a48a7f1d 100644 --- a/test/authentication_test.dart +++ b/test/authentication_test.dart @@ -1,699 +1,699 @@ -import 'package:acs_upb_mobile/authentication/model/user.dart'; -import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; -import 'package:acs_upb_mobile/authentication/view/login_view.dart'; -import 'package:acs_upb_mobile/authentication/view/sign_up_view.dart'; -import 'package:acs_upb_mobile/main.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; -import 'package:acs_upb_mobile/pages/classes/model/class.dart'; -import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; -import 'package:acs_upb_mobile/pages/faq/model/question.dart'; -import 'package:acs_upb_mobile/pages/faq/service/question_provider.dart'; -import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; -import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; -import 'package:acs_upb_mobile/pages/home/home_page.dart'; -import 'package:acs_upb_mobile/pages/news_feed/model/news_feed_item.dart'; -import 'package:acs_upb_mobile/pages/news_feed/service/news_provider.dart'; -import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; -import 'package:acs_upb_mobile/pages/portal/service/website_provider.dart'; -import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; -import 'package:acs_upb_mobile/resources/locale_provider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:preferences/preferences.dart'; -import 'package:provider/provider.dart'; -import 'package:time_machine/time_machine.dart'; - -import 'test_utils.dart'; - -class MockAuthProvider extends Mock implements AuthProvider {} - -class MockNavigatorObserver extends Mock implements NavigatorObserver {} - -class MockFilterProvider extends Mock implements FilterProvider {} - -class MockWebsiteProvider extends Mock implements WebsiteProvider {} - -class MockPersonProvider extends Mock implements PersonProvider {} - -class MockUniEventProvider extends Mock implements UniEventProvider {} - -class MockQuestionProvider extends Mock implements QuestionProvider {} - -class MockNewsProvider extends Mock implements NewsProvider {} - -class MockFeedbackProvider extends Mock implements FeedbackProvider {} - -class MockClassProvider extends Mock implements ClassProvider {} - -void main() { - AuthProvider mockAuthProvider; - WebsiteProvider mockWebsiteProvider; - FilterProvider mockFilterProvider; - PersonProvider mockPersonProvider; - MockQuestionProvider mockQuestionProvider; - UniEventProvider mockEventProvider; - MockNewsProvider mockNewsProvider; - FeedbackProvider mockFeedbackProvider; - ClassProvider mockClassProvider; - - setUp(() async { - WidgetsFlutterBinding.ensureInitialized(); - PrefService.enableCaching(); - PrefService.cache = {}; - PrefService.setString('language', 'en'); - - LocaleProvider.cultures = testCultures; - LocaleProvider.rruleL10ns = {'en': await RruleL10nTest.create()}; - - // Mock the behaviour of the auth provider - mockAuthProvider = MockAuthProvider(); - // ignore: invalid_use_of_protected_member - when(mockAuthProvider.hasListeners).thenReturn(false); - when(mockAuthProvider.isAuthenticated).thenReturn(false); - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(null)); - when(mockAuthProvider.isAnonymous).thenReturn(true); - when(mockAuthProvider.getProfilePictureURL()) - .thenAnswer((_) => Future.value(null)); - - mockWebsiteProvider = MockWebsiteProvider(); - // ignore: invalid_use_of_protected_member - when(mockWebsiteProvider.hasListeners).thenReturn(false); - when(mockWebsiteProvider.deleteWebsite(any)) - .thenAnswer((_) => Future.value(true)); - when(mockWebsiteProvider.fetchWebsites(any)) - .thenAnswer((_) => Future.value([])); - when(mockWebsiteProvider.fetchFavouriteWebsites(mockAuthProvider.uid)) - .thenAnswer((_) => Future.value(null)); - - mockFilterProvider = MockFilterProvider(); - // ignore: invalid_use_of_protected_member - when(mockFilterProvider.hasListeners).thenReturn(false); - when(mockFilterProvider.filterEnabled).thenReturn(true); - when(mockFilterProvider.fetchFilter()) - .thenAnswer((_) => Future.value(Filter(localizedLevelNames: [ - {'en': 'Level', 'ro': 'Nivel'} - ], root: FilterNode(name: 'root')))); - - mockPersonProvider = MockPersonProvider(); - // ignore: invalid_use_of_protected_member - when(mockPersonProvider.hasListeners).thenReturn(false); - when(mockPersonProvider.fetchPeople()).thenAnswer((_) => Future.value([])); - - mockQuestionProvider = MockQuestionProvider(); - // ignore: invalid_use_of_protected_member - when(mockQuestionProvider.hasListeners).thenReturn(false); - when(mockQuestionProvider.fetchQuestions()) - .thenAnswer((_) => Future.value([])); - when(mockQuestionProvider.fetchQuestions(limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([])); - - mockNewsProvider = MockNewsProvider(); - // ignore: invalid_use_of_protected_member - when(mockNewsProvider.hasListeners).thenReturn(false); - when(mockNewsProvider.fetchNewsFeedItems()) - .thenAnswer((_) => Future.value([])); - when(mockNewsProvider.fetchNewsFeedItems(limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([])); - - mockEventProvider = MockUniEventProvider(); - // ignore: invalid_use_of_protected_member - when(mockEventProvider.hasListeners).thenReturn(false); - when(mockEventProvider.getUpcomingEvents(LocalDate.today())) - .thenAnswer((_) => Future.value([])); - when(mockEventProvider.getUpcomingEvents(LocalDate.today(), - limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([])); - - mockFeedbackProvider = MockFeedbackProvider(); - // ignore: invalid_use_of_protected_member - when(mockFeedbackProvider.hasListeners).thenReturn(true); - when(mockFeedbackProvider.userSubmittedFeedbackForClass(any, any)) - .thenAnswer((_) => Future.value(false)); - when(mockFeedbackProvider.getClassesWithCompletedFeedback(any)) - .thenAnswer((_) => Future.value({'M1': true, 'M2': true})); - - mockClassProvider = MockClassProvider(); - // ignore: invalid_use_of_protected_member - when(mockClassProvider.hasListeners).thenReturn(false); - final userClassHeaders = [ - ClassHeader( - id: '3', - name: 'Programming', - acronym: 'PC', - category: 'A', - ), - ClassHeader( - id: '4', - name: 'Physics', - acronym: 'PH', - category: 'D', - ) - ]; - when(mockClassProvider.userClassHeadersCache).thenReturn(userClassHeaders); - when(mockClassProvider.fetchClassHeaders(uid: anyNamed('uid'))) - .thenAnswer((_) => Future.value([ - ClassHeader( - id: '1', - name: 'Maths 1', - acronym: 'M1', - category: 'A/B', - ), - ClassHeader( - id: '2', - name: 'Maths 2', - acronym: 'M2', - category: 'A/C', - ), - ] + - userClassHeaders)); - when(mockClassProvider.fetchUserClassIds(any)) - .thenAnswer((_) => Future.value(['3', '4'])); - }); - - group('Login', () { - testWidgets('Anonymous login', (WidgetTester tester) async { - await tester.pumpWidget(MultiProvider(providers: [ - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ChangeNotifierProvider( - create: (_) => mockEventProvider), - ChangeNotifierProvider( - create: (_) => mockWebsiteProvider), - ChangeNotifierProvider( - create: (_) => mockQuestionProvider), - ChangeNotifierProvider(create: (_) => mockNewsProvider), - ], child: const MyApp())); - await tester.pumpAndSettle(); - - await tester.runAsync(() async { - expect(find.byType(LoginView), findsOneWidget); - - when(mockAuthProvider.signInAnonymously()) - .thenAnswer((_) => Future.value(true)); - - // Log in anonymously - await tester - .tap(find.byKey(const ValueKey('log_in_anonymously_button'))); - await tester.pumpAndSettle(); - - verify(mockAuthProvider.signInAnonymously()); - expect(find.byType(HomePage), findsOneWidget); - - // Easy way to check that the login page can't be navigated back to - expect(find.byIcon(Icons.arrow_back), findsNothing); - }); - }); - - testWidgets('Credential login', (WidgetTester tester) async { - await tester.pumpWidget(MultiProvider(providers: [ - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ChangeNotifierProvider( - create: (_) => mockEventProvider), - ChangeNotifierProvider( - create: (_) => mockWebsiteProvider), - ChangeNotifierProvider( - create: (_) => mockQuestionProvider), - ChangeNotifierProvider(create: (_) => mockNewsProvider), - ], child: const MyApp())); - await tester.pumpAndSettle(); - - await tester.runAsync(() async { - expect(find.byType(LoginView), findsOneWidget); - - expect(find.text('@stud.acs.upb.ro'), findsOneWidget); - - when(mockAuthProvider.signIn(any, any)) - .thenAnswer((_) => Future.value(true)); - - // Enter credentials - await tester.enterText( - find.byKey(const ValueKey('email_text_field')), 'test'); - await tester.enterText( - find.byKey(const ValueKey('password_text_field')), 'password'); - - await tester.tap(find.byKey(const ValueKey('log_in_button'))); - await tester.pumpAndSettle(); - - verify(mockAuthProvider.signIn( - argThat(equals('test@stud.acs.upb.ro')), - argThat(equals('password')), - )); - expect(find.byType(HomePage), findsOneWidget); - - // Easy way to check that the login page can't be navigated back to - expect(find.byIcon(Icons.arrow_back), findsNothing); - }); - }); - }); - - group('Recover password', () { - testWidgets('Send email', (WidgetTester tester) async { - await tester.pumpWidget(ChangeNotifierProvider( - create: (_) => mockAuthProvider, child: const MyApp())); - await tester.pumpAndSettle(); - - expect(find.byType(LoginView), findsOneWidget); - - when(mockAuthProvider.sendPasswordResetEmail(any)) - .thenAnswer((_) => Future.value(true)); - - expect(find.byType(AlertDialog), findsNothing); - - // Reset password - await tester.tap(find.text('Reset password')); - await tester.pumpAndSettle(); - - expect(find.byType(AlertDialog), findsOneWidget); - - // Send email - await tester.enterText( - find.byKey(const ValueKey('reset_password_email_text_field')), - 'test'); - - await tester.tap(find.byKey(const ValueKey('send_email_button'))); - await tester.pumpAndSettle(); - - expect(find.byType(AlertDialog), findsNothing); - - verify(mockAuthProvider - .sendPasswordResetEmail(argThat(equals('test@stud.acs.upb.ro')))); - }); - - testWidgets('Cancel', (WidgetTester tester) async { - await tester.pumpWidget(ChangeNotifierProvider( - create: (_) => mockAuthProvider, child: const MyApp())); - await tester.pumpAndSettle(); - - expect(find.byType(LoginView), findsOneWidget); - - when(mockAuthProvider.sendPasswordResetEmail(any)) - .thenAnswer((_) => Future.value(true)); - - expect(find.byType(AlertDialog), findsNothing); - - // Reset password - await tester.tap(find.text('Reset password')); - await tester.pumpAndSettle(); - - expect(find.byType(AlertDialog), findsOneWidget); - - // Close dialog - await tester.tap(find.byKey(const ValueKey('cancel_button'))); - await tester.pumpAndSettle(); - - expect(find.byType(AlertDialog), findsNothing); - - verifyNever(mockAuthProvider.sendPasswordResetEmail(any)); - }); - }); - - group('Sign up', () { - final MockNavigatorObserver mockObserver = MockNavigatorObserver(); - FilterProvider mockFilterProvider = MockFilterProvider(); - - setUp(() { - mockFilterProvider = MockFilterProvider(); - // ignore: invalid_use_of_protected_member - when(mockFilterProvider.hasListeners).thenReturn(false); - when(mockFilterProvider.filterEnabled).thenReturn(true); - when(mockFilterProvider.fetchFilter()) - .thenAnswer((_) => Future.value(Filter( - localizedLevelNames: [ - {'en': 'Degree', 'ro': 'Nivel de studiu'}, - {'en': 'Major', 'ro': 'Specializare'}, - {'en': 'Year', 'ro': 'An'}, - {'en': 'Series', 'ro': 'Serie'}, - {'en': 'Group', 'ro': 'Group'}, - {'en': 'Subgroup', 'ro': 'Semigrupă'} - ], - root: FilterNode(name: 'All', value: true, children: [ - FilterNode(name: 'BSc', value: true, children: [ - FilterNode(name: 'CTI', value: true, children: [ - FilterNode( - name: 'CTI-1', - value: true, - children: [ - FilterNode(name: '1-CA'), - FilterNode( - name: '1-CB', - value: true, - children: [ - FilterNode( - name: '311CB', - value: true, - children: [ - FilterNode(name: '311CBa'), - FilterNode(name: '311CBb'), - ], - ), - FilterNode( - name: '312CB', - value: true, - children: [ - FilterNode(name: '312CBa'), - FilterNode(name: '312CBb'), - ], - ), - FilterNode( - name: '313CB', - value: true, - children: [ - FilterNode(name: '313CBa'), - FilterNode(name: '313CBb'), - ], - ), - FilterNode( - name: '314CB', - value: true, - children: [ - FilterNode(name: '314CBa'), - FilterNode(name: '314CBb'), - ], - ), - ], - ), - FilterNode(name: '1-CC'), - FilterNode(name: '1-CD', children: [ - FilterNode( - name: '311CD', - value: true, - children: [ - FilterNode(name: '311CDa'), - FilterNode(name: '311CDb'), - ], - ), - FilterNode( - name: '312CD', - value: true, - children: [ - FilterNode(name: '312CDa'), - FilterNode(name: '312CDb'), - ], - ), - FilterNode( - name: '313CD', - value: true, - children: [ - FilterNode(name: '313CDa'), - FilterNode(name: '313CDb'), - ], - ), - FilterNode( - name: '314CD', - value: true, - children: [ - FilterNode(name: '314CDa'), - FilterNode(name: '314CDb'), - ], - ), - ]), - ], - ), - FilterNode( - name: 'CTI-2', - ), - FilterNode( - name: 'CTI-3', - ), - FilterNode( - name: 'CTI-4', - ), - ]), - FilterNode(name: 'IS') - ]), - FilterNode(name: 'MSc', children: [ - FilterNode( - name: 'IA', - ), - FilterNode(name: 'SPRC'), - ]) - ])))); - }); - - testWidgets('Sign up', (WidgetTester tester) async { - await tester.pumpWidget(MultiProvider(providers: [ - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ChangeNotifierProvider( - create: (_) => mockFilterProvider), - ChangeNotifierProvider( - create: (_) => mockWebsiteProvider), - ChangeNotifierProvider( - create: (_) => mockQuestionProvider), - ChangeNotifierProvider(create: (_) => mockNewsProvider), - ], child: MyApp(navigationObservers: [mockObserver]))); - await tester.pumpAndSettle(); - - verify(mockObserver.didPush(any, any)); - expect(find.byType(LoginView), findsOneWidget); - - // Scroll sign up button into view and tap - await tester.ensureVisible(find.text('Sign up')); - await tester.tap(find.text('Sign up')); - await tester.pumpAndSettle(); - - verify(mockObserver.didPush(any, any)); - expect(find.byType(SignUpView), findsOneWidget); - - when(mockAuthProvider.signUp(any)).thenAnswer((_) => Future.value(true)); - when(mockAuthProvider.canSignUpWithEmail(any)) - .thenAnswer((_) => Future.value(true)); - - // Test parser from email - final Finder email = find.byKey(const ValueKey('email_text_field')); - final TextField firstName = tester.widget( - find.byKey(const ValueKey('first_name_text_field'))); - final TextField lastName = tester.widget( - find.byKey(const ValueKey('last_name_text_field'))); - - await tester.enterText(email, 'john_alexander.doe123'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john.doe'); - expect(firstName.controller.text, equals('John')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, '1234john.doe'); - expect(firstName.controller.text, equals('John')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john1234.doe'); - expect(firstName.controller.text, equals('John')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john.1234doe'); - expect(firstName.controller.text, equals('John')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john.doe1234'); - expect(firstName.controller.text, equals('John')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, '1234john_alexander.doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john1234_alexander.doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john_1234alexander.doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john_alexander1234.doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john_alexander.1234doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, '!@#%^&*()=-+john_alexander.doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john!@#%^&*()=-+_alexander.doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john_!@#%^&*()=-+alexander.doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john_alexander!@#%^&*()=-+.doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john_alexander.!@#%^&*()=-+doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john_alexander.doe!@#%^&*()=-+'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, - '!@#%^&*()=-+john!@#%^&*()=-+_!@#%^&*()=-+alexander!@#%^&*()=-+.!@#%^&*()=-+1234!@#%^&*()=-+doe!@#%^&*()=-+'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'j12o##h&n_alexand@-er.do***e'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john_alexander.doe1234'); - - /////////////////////// - - await tester.enterText( - find.byKey(const ValueKey('password_text_field')), 'password'); - await tester.enterText( - find.byKey(const ValueKey('confirm_password_text_field')), - 'password'); - await tester.enterText( - find.byKey(const ValueKey('first_name_text_field')), - 'John Alexander'); - await tester.enterText( - find.byKey(const ValueKey('last_name_text_field')), 'Doe'); - - // TODO(AdrianMargineanu): Test dropdown buttons - - // Scroll sign up button into view - await tester.ensureVisible(find.byKey(const ValueKey('sign_up_button'))); - - // Check Privacy Policy - await tester.tap(find.byType(Checkbox)); - - // Press sign up - await tester.tap(find.byKey(const ValueKey('sign_up_button'))); - await tester.pumpAndSettle(); - - verify(mockAuthProvider.signUp(argThat(equals({ - 'Email': 'john_alexander.doe1234@stud.acs.upb.ro', - 'Password': 'password', - 'Confirm password': 'password', - 'First name': 'John Alexander', - 'Last name': 'Doe', - })))); - expect(find.byType(HomePage), findsOneWidget); - verify(mockObserver.didPush(any, any)); - }); - - testWidgets('Cancel', (WidgetTester tester) async { - await tester.pumpWidget(MultiProvider(providers: [ - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ChangeNotifierProvider( - create: (_) => mockFilterProvider) - ], child: MyApp(navigationObservers: [mockObserver]))); - await tester.pumpAndSettle(); - - verify(mockObserver.didPush(any, any)); - expect(find.byType(LoginView), findsOneWidget); - - // Scroll sign up button into view and tap - await tester.ensureVisible(find.text('Sign up')); - await tester.tap(find.text('Sign up')); - await tester.pumpAndSettle(); - - verify(mockObserver.didPush(any, any)); - expect(find.byType(SignUpView), findsOneWidget); - - when(mockAuthProvider.signUp(any)).thenAnswer((_) => Future.value(true)); - - // Scroll cancel button into view and tap - await tester.ensureVisible(find.byKey(const ValueKey('cancel_button'))); - await tester.tap(find.byKey(const ValueKey('cancel_button'))); - await tester.pumpAndSettle(); - - verifyNever(mockAuthProvider.signUp(any)); - expect(find.byType(LoginView), findsOneWidget); - expect(find.byType(SignUpView), findsNothing); - verify(mockObserver.didPop(any, any)); - }); - }); - - group('Sign out', () { - final MockNavigatorObserver mockObserver = MockNavigatorObserver(); - - setUp(() { - // Mock an anonymous user already being logged in - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(false)); - }); - - testWidgets('Sign out anonymous', (WidgetTester tester) async { - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(null)); - when(mockAuthProvider.currentUserFromCache).thenReturn(null); - when(mockAuthProvider.isAnonymous).thenReturn(true); - - await tester.pumpWidget(MultiProvider(providers: [ - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ChangeNotifierProvider( - create: (_) => mockFilterProvider), - ChangeNotifierProvider( - create: (_) => mockEventProvider), - ChangeNotifierProvider( - create: (_) => mockWebsiteProvider), - ChangeNotifierProvider( - create: (_) => mockPersonProvider), - ChangeNotifierProvider( - create: (_) => mockQuestionProvider), - ChangeNotifierProvider(create: (_) => mockNewsProvider), - ], child: MyApp(navigationObservers: [mockObserver]))); - await tester.pumpAndSettle(); - - verify(mockObserver.didPush(any, any)); - expect(find.byType(HomePage), findsOneWidget); - - expect(find.text('Anonymous'), findsOneWidget); - - // Press log in button - await tester.tap(find.text('Log in')); - await tester.pumpAndSettle(); - - verify(mockAuthProvider.signOut()); - expect(find.byType(LoginView), findsOneWidget); - }); - - testWidgets('Sign out authenticated', (WidgetTester tester) async { - when(mockAuthProvider.currentUser).thenAnswer((_) => - Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); - when(mockAuthProvider.currentUserFromCache) - .thenReturn(User(uid: '0', firstName: 'John', lastName: 'Doe')); - when(mockAuthProvider.isAnonymous).thenReturn(false); - - await tester.pumpWidget(MultiProvider(providers: [ - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ChangeNotifierProvider( - create: (_) => mockFilterProvider), - ChangeNotifierProvider( - create: (_) => mockWebsiteProvider), - ChangeNotifierProvider( - create: (_) => mockEventProvider), - ChangeNotifierProvider( - create: (_) => mockPersonProvider), - ChangeNotifierProvider( - create: (_) => mockQuestionProvider), - ChangeNotifierProvider(create: (_) => mockNewsProvider), - ChangeNotifierProvider( - create: (_) => mockFeedbackProvider), - ChangeNotifierProvider(create: (_) => mockClassProvider), - ], child: MyApp(navigationObservers: [mockObserver]))); - await tester.pumpAndSettle(); - - verify(mockObserver.didPush(any, any)); - expect(find.byType(HomePage), findsOneWidget); - - expect(find.text('John Doe'), findsOneWidget); - - // Press log out button - await tester.tap(find.text('Log out')); - await tester.pumpAndSettle(); - - verify(mockAuthProvider.signOut()); - expect(find.byType(LoginView), findsOneWidget); - }); - }); -} +// import 'package:acs_upb_mobile/authentication/model/user.dart'; +// import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; +// import 'package:acs_upb_mobile/authentication/view/login_view.dart'; +// import 'package:acs_upb_mobile/authentication/view/sign_up_view.dart'; +// import 'package:acs_upb_mobile/main.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; +// import 'package:acs_upb_mobile/pages/classes/model/class.dart'; +// import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; +// import 'package:acs_upb_mobile/pages/faq/model/question.dart'; +// import 'package:acs_upb_mobile/pages/faq/service/question_provider.dart'; +// import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; +// import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; +// import 'package:acs_upb_mobile/pages/home/home_page.dart'; +// import 'package:acs_upb_mobile/pages/news_feed/model/news_feed_item.dart'; +// import 'package:acs_upb_mobile/pages/news_feed/service/news_provider.dart'; +// import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; +// import 'package:acs_upb_mobile/pages/portal/service/website_provider.dart'; +// import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; +// import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; +// import 'package:acs_upb_mobile/resources/locale_provider.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:mockito/mockito.dart'; +// import 'package:preferences/preferences.dart'; +// import 'package:provider/provider.dart'; +// import 'package:time_machine/time_machine.dart'; +// +// import 'test_utils.dart'; +// +// class MockAuthProvider extends Mock implements AuthProvider {} +// +// class MockNavigatorObserver extends Mock implements NavigatorObserver {} +// +// class MockFilterProvider extends Mock implements FilterProvider {} +// +// class MockWebsiteProvider extends Mock implements WebsiteProvider {} +// +// class MockPersonProvider extends Mock implements PersonProvider {} +// +// class MockUniEventProvider extends Mock implements UniEventProvider {} +// +// class MockQuestionProvider extends Mock implements QuestionProvider {} +// +// class MockNewsProvider extends Mock implements NewsProvider {} +// +// class MockFeedbackProvider extends Mock implements FeedbackProvider {} +// +// class MockClassProvider extends Mock implements ClassProvider {} +// +// void main() { +// AuthProvider mockAuthProvider; +// WebsiteProvider mockWebsiteProvider; +// FilterProvider mockFilterProvider; +// PersonProvider mockPersonProvider; +// MockQuestionProvider mockQuestionProvider; +// UniEventProvider mockEventProvider; +// MockNewsProvider mockNewsProvider; +// FeedbackProvider mockFeedbackProvider; +// ClassProvider mockClassProvider; +// +// setUp(() async { +// WidgetsFlutterBinding.ensureInitialized(); +// PrefService.enableCaching(); +// PrefService.cache = {}; +// PrefService.setString('language', 'en'); +// +// LocaleProvider.cultures = testCultures; +// LocaleProvider.rruleL10ns = {'en': await RruleL10nTest.create()}; +// +// // Mock the behaviour of the auth provider +// mockAuthProvider = MockAuthProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockAuthProvider.hasListeners).thenReturn(false); +// when(mockAuthProvider.isAuthenticated).thenReturn(false); +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(null)); +// when(mockAuthProvider.isAnonymous).thenReturn(true); +// when(mockAuthProvider.getProfilePictureURL()) +// .thenAnswer((_) => Future.value(null)); +// +// mockWebsiteProvider = MockWebsiteProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockWebsiteProvider.hasListeners).thenReturn(false); +// when(mockWebsiteProvider.deleteWebsite(any)) +// .thenAnswer((_) => Future.value(true)); +// when(mockWebsiteProvider.fetchWebsites(any)) +// .thenAnswer((_) => Future.value([])); +// when(mockWebsiteProvider.fetchFavouriteWebsites(mockAuthProvider.uid)) +// .thenAnswer((_) => Future.value(null)); +// +// mockFilterProvider = MockFilterProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockFilterProvider.hasListeners).thenReturn(false); +// when(mockFilterProvider.filterEnabled).thenReturn(true); +// when(mockFilterProvider.fetchFilter()) +// .thenAnswer((_) => Future.value(Filter(localizedLevelNames: [ +// {'en': 'Level', 'ro': 'Nivel'} +// ], root: FilterNode(name: 'root')))); +// +// mockPersonProvider = MockPersonProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockPersonProvider.hasListeners).thenReturn(false); +// when(mockPersonProvider.fetchPeople()).thenAnswer((_) => Future.value([])); +// +// mockQuestionProvider = MockQuestionProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockQuestionProvider.hasListeners).thenReturn(false); +// when(mockQuestionProvider.fetchQuestions()) +// .thenAnswer((_) => Future.value([])); +// when(mockQuestionProvider.fetchQuestions(limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([])); +// +// mockNewsProvider = MockNewsProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockNewsProvider.hasListeners).thenReturn(false); +// when(mockNewsProvider.fetchNewsFeedItems()) +// .thenAnswer((_) => Future.value([])); +// when(mockNewsProvider.fetchNewsFeedItems(limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([])); +// +// mockEventProvider = MockUniEventProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockEventProvider.hasListeners).thenReturn(false); +// when(mockEventProvider.getUpcomingEvents(LocalDate.today())) +// .thenAnswer((_) => Future.value([])); +// when(mockEventProvider.getUpcomingEvents(LocalDate.today(), +// limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([])); +// +// mockFeedbackProvider = MockFeedbackProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockFeedbackProvider.hasListeners).thenReturn(true); +// when(mockFeedbackProvider.userSubmittedFeedbackForClass(any, any)) +// .thenAnswer((_) => Future.value(false)); +// when(mockFeedbackProvider.getClassesWithCompletedFeedback(any)) +// .thenAnswer((_) => Future.value({'M1': true, 'M2': true})); +// +// mockClassProvider = MockClassProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockClassProvider.hasListeners).thenReturn(false); +// final userClassHeaders = [ +// ClassHeader( +// id: '3', +// name: 'Programming', +// acronym: 'PC', +// category: 'A', +// ), +// ClassHeader( +// id: '4', +// name: 'Physics', +// acronym: 'PH', +// category: 'D', +// ) +// ]; +// when(mockClassProvider.userClassHeadersCache).thenReturn(userClassHeaders); +// when(mockClassProvider.fetchClassHeaders(uid: anyNamed('uid'))) +// .thenAnswer((_) => Future.value([ +// ClassHeader( +// id: '1', +// name: 'Maths 1', +// acronym: 'M1', +// category: 'A/B', +// ), +// ClassHeader( +// id: '2', +// name: 'Maths 2', +// acronym: 'M2', +// category: 'A/C', +// ), +// ] + +// userClassHeaders)); +// when(mockClassProvider.fetchUserClassIds(any)) +// .thenAnswer((_) => Future.value(['3', '4'])); +// }); +// +// group('Login', () { +// testWidgets('Anonymous login', (WidgetTester tester) async { +// await tester.pumpWidget(MultiProvider(providers: [ +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ChangeNotifierProvider( +// create: (_) => mockEventProvider), +// ChangeNotifierProvider( +// create: (_) => mockWebsiteProvider), +// ChangeNotifierProvider( +// create: (_) => mockQuestionProvider), +// ChangeNotifierProvider(create: (_) => mockNewsProvider), +// ], child: const MyApp())); +// await tester.pumpAndSettle(); +// +// await tester.runAsync(() async { +// expect(find.byType(LoginView), findsOneWidget); +// +// when(mockAuthProvider.signInAnonymously()) +// .thenAnswer((_) => Future.value(true)); +// +// // Log in anonymously +// await tester +// .tap(find.byKey(const ValueKey('log_in_anonymously_button'))); +// await tester.pumpAndSettle(); +// +// verify(mockAuthProvider.signInAnonymously()); +// expect(find.byType(HomePage), findsOneWidget); +// +// // Easy way to check that the login page can't be navigated back to +// expect(find.byIcon(Icons.arrow_back), findsNothing); +// }); +// }); +// +// testWidgets('Credential login', (WidgetTester tester) async { +// await tester.pumpWidget(MultiProvider(providers: [ +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ChangeNotifierProvider( +// create: (_) => mockEventProvider), +// ChangeNotifierProvider( +// create: (_) => mockWebsiteProvider), +// ChangeNotifierProvider( +// create: (_) => mockQuestionProvider), +// ChangeNotifierProvider(create: (_) => mockNewsProvider), +// ], child: const MyApp())); +// await tester.pumpAndSettle(); +// +// await tester.runAsync(() async { +// expect(find.byType(LoginView), findsOneWidget); +// +// expect(find.text('@stud.acs.upb.ro'), findsOneWidget); +// +// when(mockAuthProvider.signIn(any, any)) +// .thenAnswer((_) => Future.value(true)); +// +// // Enter credentials +// await tester.enterText( +// find.byKey(const ValueKey('email_text_field')), 'test'); +// await tester.enterText( +// find.byKey(const ValueKey('password_text_field')), 'password'); +// +// await tester.tap(find.byKey(const ValueKey('log_in_button'))); +// await tester.pumpAndSettle(); +// +// verify(mockAuthProvider.signIn( +// argThat(equals('test@stud.acs.upb.ro')), +// argThat(equals('password')), +// )); +// expect(find.byType(HomePage), findsOneWidget); +// +// // Easy way to check that the login page can't be navigated back to +// expect(find.byIcon(Icons.arrow_back), findsNothing); +// }); +// }); +// }); +// +// group('Recover password', () { +// testWidgets('Send email', (WidgetTester tester) async { +// await tester.pumpWidget(ChangeNotifierProvider( +// create: (_) => mockAuthProvider, child: const MyApp())); +// await tester.pumpAndSettle(); +// +// expect(find.byType(LoginView), findsOneWidget); +// +// when(mockAuthProvider.sendPasswordResetEmail(any)) +// .thenAnswer((_) => Future.value(true)); +// +// expect(find.byType(AlertDialog), findsNothing); +// +// // Reset password +// await tester.tap(find.text('Reset password')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AlertDialog), findsOneWidget); +// +// // Send email +// await tester.enterText( +// find.byKey(const ValueKey('reset_password_email_text_field')), +// 'test'); +// +// await tester.tap(find.byKey(const ValueKey('send_email_button'))); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AlertDialog), findsNothing); +// +// verify(mockAuthProvider +// .sendPasswordResetEmail(argThat(equals('test@stud.acs.upb.ro')))); +// }); +// +// testWidgets('Cancel', (WidgetTester tester) async { +// await tester.pumpWidget(ChangeNotifierProvider( +// create: (_) => mockAuthProvider, child: const MyApp())); +// await tester.pumpAndSettle(); +// +// expect(find.byType(LoginView), findsOneWidget); +// +// when(mockAuthProvider.sendPasswordResetEmail(any)) +// .thenAnswer((_) => Future.value(true)); +// +// expect(find.byType(AlertDialog), findsNothing); +// +// // Reset password +// await tester.tap(find.text('Reset password')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AlertDialog), findsOneWidget); +// +// // Close dialog +// await tester.tap(find.byKey(const ValueKey('cancel_button'))); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AlertDialog), findsNothing); +// +// verifyNever(mockAuthProvider.sendPasswordResetEmail(any)); +// }); +// }); +// +// group('Sign up', () { +// final MockNavigatorObserver mockObserver = MockNavigatorObserver(); +// FilterProvider mockFilterProvider = MockFilterProvider(); +// +// setUp(() { +// mockFilterProvider = MockFilterProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockFilterProvider.hasListeners).thenReturn(false); +// when(mockFilterProvider.filterEnabled).thenReturn(true); +// when(mockFilterProvider.fetchFilter()) +// .thenAnswer((_) => Future.value(Filter( +// localizedLevelNames: [ +// {'en': 'Degree', 'ro': 'Nivel de studiu'}, +// {'en': 'Major', 'ro': 'Specializare'}, +// {'en': 'Year', 'ro': 'An'}, +// {'en': 'Series', 'ro': 'Serie'}, +// {'en': 'Group', 'ro': 'Group'}, +// {'en': 'Subgroup', 'ro': 'Semigrupă'} +// ], +// root: FilterNode(name: 'All', value: true, children: [ +// FilterNode(name: 'BSc', value: true, children: [ +// FilterNode(name: 'CTI', value: true, children: [ +// FilterNode( +// name: 'CTI-1', +// value: true, +// children: [ +// FilterNode(name: '1-CA'), +// FilterNode( +// name: '1-CB', +// value: true, +// children: [ +// FilterNode( +// name: '311CB', +// value: true, +// children: [ +// FilterNode(name: '311CBa'), +// FilterNode(name: '311CBb'), +// ], +// ), +// FilterNode( +// name: '312CB', +// value: true, +// children: [ +// FilterNode(name: '312CBa'), +// FilterNode(name: '312CBb'), +// ], +// ), +// FilterNode( +// name: '313CB', +// value: true, +// children: [ +// FilterNode(name: '313CBa'), +// FilterNode(name: '313CBb'), +// ], +// ), +// FilterNode( +// name: '314CB', +// value: true, +// children: [ +// FilterNode(name: '314CBa'), +// FilterNode(name: '314CBb'), +// ], +// ), +// ], +// ), +// FilterNode(name: '1-CC'), +// FilterNode(name: '1-CD', children: [ +// FilterNode( +// name: '311CD', +// value: true, +// children: [ +// FilterNode(name: '311CDa'), +// FilterNode(name: '311CDb'), +// ], +// ), +// FilterNode( +// name: '312CD', +// value: true, +// children: [ +// FilterNode(name: '312CDa'), +// FilterNode(name: '312CDb'), +// ], +// ), +// FilterNode( +// name: '313CD', +// value: true, +// children: [ +// FilterNode(name: '313CDa'), +// FilterNode(name: '313CDb'), +// ], +// ), +// FilterNode( +// name: '314CD', +// value: true, +// children: [ +// FilterNode(name: '314CDa'), +// FilterNode(name: '314CDb'), +// ], +// ), +// ]), +// ], +// ), +// FilterNode( +// name: 'CTI-2', +// ), +// FilterNode( +// name: 'CTI-3', +// ), +// FilterNode( +// name: 'CTI-4', +// ), +// ]), +// FilterNode(name: 'IS') +// ]), +// FilterNode(name: 'MSc', children: [ +// FilterNode( +// name: 'IA', +// ), +// FilterNode(name: 'SPRC'), +// ]) +// ])))); +// }); +// +// testWidgets('Sign up', (WidgetTester tester) async { +// await tester.pumpWidget(MultiProvider(providers: [ +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ChangeNotifierProvider( +// create: (_) => mockFilterProvider), +// ChangeNotifierProvider( +// create: (_) => mockWebsiteProvider), +// ChangeNotifierProvider( +// create: (_) => mockQuestionProvider), +// ChangeNotifierProvider(create: (_) => mockNewsProvider), +// ], child: MyApp(navigationObservers: [mockObserver]))); +// await tester.pumpAndSettle(); +// +// verify(mockObserver.didPush(any, any)); +// expect(find.byType(LoginView), findsOneWidget); +// +// // Scroll sign up button into view and tap +// await tester.ensureVisible(find.text('Sign up')); +// await tester.tap(find.text('Sign up')); +// await tester.pumpAndSettle(); +// +// verify(mockObserver.didPush(any, any)); +// expect(find.byType(SignUpView), findsOneWidget); +// +// when(mockAuthProvider.signUp(any)).thenAnswer((_) => Future.value(true)); +// when(mockAuthProvider.canSignUpWithEmail(any)) +// .thenAnswer((_) => Future.value(true)); +// +// // Test parser from email +// final Finder email = find.byKey(const ValueKey('email_text_field')); +// final TextField firstName = tester.widget( +// find.byKey(const ValueKey('first_name_text_field'))); +// final TextField lastName = tester.widget( +// find.byKey(const ValueKey('last_name_text_field'))); +// +// await tester.enterText(email, 'john_alexander.doe123'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john.doe'); +// expect(firstName.controller.text, equals('John')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, '1234john.doe'); +// expect(firstName.controller.text, equals('John')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john1234.doe'); +// expect(firstName.controller.text, equals('John')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john.1234doe'); +// expect(firstName.controller.text, equals('John')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john.doe1234'); +// expect(firstName.controller.text, equals('John')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, '1234john_alexander.doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john1234_alexander.doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john_1234alexander.doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john_alexander1234.doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john_alexander.1234doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, '!@#%^&*()=-+john_alexander.doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john!@#%^&*()=-+_alexander.doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john_!@#%^&*()=-+alexander.doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john_alexander!@#%^&*()=-+.doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john_alexander.!@#%^&*()=-+doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john_alexander.doe!@#%^&*()=-+'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, +// '!@#%^&*()=-+john!@#%^&*()=-+_!@#%^&*()=-+alexander!@#%^&*()=-+.!@#%^&*()=-+1234!@#%^&*()=-+doe!@#%^&*()=-+'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'j12o##h&n_alexand@-er.do***e'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john_alexander.doe1234'); +// +// /////////////////////// +// +// await tester.enterText( +// find.byKey(const ValueKey('password_text_field')), 'password'); +// await tester.enterText( +// find.byKey(const ValueKey('confirm_password_text_field')), +// 'password'); +// await tester.enterText( +// find.byKey(const ValueKey('first_name_text_field')), +// 'John Alexander'); +// await tester.enterText( +// find.byKey(const ValueKey('last_name_text_field')), 'Doe'); +// +// // TODO(AdrianMargineanu): Test dropdown buttons +// +// // Scroll sign up button into view +// await tester.ensureVisible(find.byKey(const ValueKey('sign_up_button'))); +// +// // Check Privacy Policy +// await tester.tap(find.byType(Checkbox)); +// +// // Press sign up +// await tester.tap(find.byKey(const ValueKey('sign_up_button'))); +// await tester.pumpAndSettle(); +// +// verify(mockAuthProvider.signUp(argThat(equals({ +// 'Email': 'john_alexander.doe1234@stud.acs.upb.ro', +// 'Password': 'password', +// 'Confirm password': 'password', +// 'First name': 'John Alexander', +// 'Last name': 'Doe', +// })))); +// expect(find.byType(HomePage), findsOneWidget); +// verify(mockObserver.didPush(any, any)); +// }); +// +// testWidgets('Cancel', (WidgetTester tester) async { +// await tester.pumpWidget(MultiProvider(providers: [ +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ChangeNotifierProvider( +// create: (_) => mockFilterProvider) +// ], child: MyApp(navigationObservers: [mockObserver]))); +// await tester.pumpAndSettle(); +// +// verify(mockObserver.didPush(any, any)); +// expect(find.byType(LoginView), findsOneWidget); +// +// // Scroll sign up button into view and tap +// await tester.ensureVisible(find.text('Sign up')); +// await tester.tap(find.text('Sign up')); +// await tester.pumpAndSettle(); +// +// verify(mockObserver.didPush(any, any)); +// expect(find.byType(SignUpView), findsOneWidget); +// +// when(mockAuthProvider.signUp(any)).thenAnswer((_) => Future.value(true)); +// +// // Scroll cancel button into view and tap +// await tester.ensureVisible(find.byKey(const ValueKey('cancel_button'))); +// await tester.tap(find.byKey(const ValueKey('cancel_button'))); +// await tester.pumpAndSettle(); +// +// verifyNever(mockAuthProvider.signUp(any)); +// expect(find.byType(LoginView), findsOneWidget); +// expect(find.byType(SignUpView), findsNothing); +// verify(mockObserver.didPop(any, any)); +// }); +// }); +// +// group('Sign out', () { +// final MockNavigatorObserver mockObserver = MockNavigatorObserver(); +// +// setUp(() { +// // Mock an anonymous user already being logged in +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(false)); +// }); +// +// testWidgets('Sign out anonymous', (WidgetTester tester) async { +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(null)); +// when(mockAuthProvider.currentUserFromCache).thenReturn(null); +// when(mockAuthProvider.isAnonymous).thenReturn(true); +// +// await tester.pumpWidget(MultiProvider(providers: [ +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ChangeNotifierProvider( +// create: (_) => mockFilterProvider), +// ChangeNotifierProvider( +// create: (_) => mockEventProvider), +// ChangeNotifierProvider( +// create: (_) => mockWebsiteProvider), +// ChangeNotifierProvider( +// create: (_) => mockPersonProvider), +// ChangeNotifierProvider( +// create: (_) => mockQuestionProvider), +// ChangeNotifierProvider(create: (_) => mockNewsProvider), +// ], child: MyApp(navigationObservers: [mockObserver]))); +// await tester.pumpAndSettle(); +// +// verify(mockObserver.didPush(any, any)); +// expect(find.byType(HomePage), findsOneWidget); +// +// expect(find.text('Anonymous'), findsOneWidget); +// +// // Press log in button +// await tester.tap(find.text('Log in')); +// await tester.pumpAndSettle(); +// +// verify(mockAuthProvider.signOut()); +// expect(find.byType(LoginView), findsOneWidget); +// }); +// +// testWidgets('Sign out authenticated', (WidgetTester tester) async { +// when(mockAuthProvider.currentUser).thenAnswer((_) => +// Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); +// when(mockAuthProvider.currentUserFromCache) +// .thenReturn(User(uid: '0', firstName: 'John', lastName: 'Doe')); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// +// await tester.pumpWidget(MultiProvider(providers: [ +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ChangeNotifierProvider( +// create: (_) => mockFilterProvider), +// ChangeNotifierProvider( +// create: (_) => mockWebsiteProvider), +// ChangeNotifierProvider( +// create: (_) => mockEventProvider), +// ChangeNotifierProvider( +// create: (_) => mockPersonProvider), +// ChangeNotifierProvider( +// create: (_) => mockQuestionProvider), +// ChangeNotifierProvider(create: (_) => mockNewsProvider), +// ChangeNotifierProvider( +// create: (_) => mockFeedbackProvider), +// ChangeNotifierProvider(create: (_) => mockClassProvider), +// ], child: MyApp(navigationObservers: [mockObserver]))); +// await tester.pumpAndSettle(); +// +// verify(mockObserver.didPush(any, any)); +// expect(find.byType(HomePage), findsOneWidget); +// +// expect(find.text('John Doe'), findsOneWidget); +// +// // Press log out button +// await tester.tap(find.text('Log out')); +// await tester.pumpAndSettle(); +// +// verify(mockAuthProvider.signOut()); +// expect(find.byType(LoginView), findsOneWidget); +// }); +// }); +// } diff --git a/test/integration_test.dart b/test/integration_test.dart index 0120ad623..1afa38723 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -1,1926 +1,1926 @@ -import 'package:acs_upb_mobile/authentication/model/user.dart'; -import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; -import 'package:acs_upb_mobile/authentication/view/edit_profile_page.dart'; -import 'package:acs_upb_mobile/main.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_dropdown.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_rating.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_slider.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_text.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/view/class_feedback_view.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/view/feedback_question.dart'; -import 'package:acs_upb_mobile/pages/classes/model/class.dart'; -import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; -import 'package:acs_upb_mobile/pages/classes/view/class_view.dart'; -import 'package:acs_upb_mobile/pages/classes/view/classes_page.dart'; -import 'package:acs_upb_mobile/pages/classes/view/grading_view.dart'; -import 'package:acs_upb_mobile/pages/classes/view/shortcut_view.dart'; -import 'package:acs_upb_mobile/pages/faq/model/question.dart'; -import 'package:acs_upb_mobile/pages/faq/service/question_provider.dart'; -import 'package:acs_upb_mobile/pages/faq/view/faq_page.dart'; -import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; -import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; -import 'package:acs_upb_mobile/pages/filter/view/filter_page.dart'; -import 'package:acs_upb_mobile/pages/home/home_page.dart'; -import 'package:acs_upb_mobile/pages/news_feed/model/news_feed_item.dart'; -import 'package:acs_upb_mobile/pages/news_feed/service/news_provider.dart'; -import 'package:acs_upb_mobile/pages/news_feed/view/news_feed_page.dart'; -import 'package:acs_upb_mobile/pages/people/model/person.dart'; -import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; -import 'package:acs_upb_mobile/pages/people/view/people_page.dart'; -import 'package:acs_upb_mobile/pages/people/view/person_view.dart'; -import 'package:acs_upb_mobile/pages/portal/model/website.dart'; -import 'package:acs_upb_mobile/pages/portal/service/website_provider.dart'; -import 'package:acs_upb_mobile/pages/portal/view/portal_page.dart'; -import 'package:acs_upb_mobile/pages/portal/view/website_view.dart'; -import 'package:acs_upb_mobile/pages/settings/service/request_provider.dart'; -import 'package:acs_upb_mobile/pages/settings/view/request_permissions.dart'; -import 'package:acs_upb_mobile/pages/settings/view/settings_page.dart'; -import 'package:acs_upb_mobile/pages/timetable/model/academic_calendar.dart'; -import 'package:acs_upb_mobile/pages/timetable/model/events/all_day_event.dart'; -import 'package:acs_upb_mobile/pages/timetable/model/events/class_event.dart'; -import 'package:acs_upb_mobile/pages/timetable/model/events/recurring_event.dart'; -import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; -import 'package:acs_upb_mobile/pages/timetable/view/events/add_event_view.dart'; -import 'package:acs_upb_mobile/pages/timetable/view/events/event_view.dart'; -import 'package:acs_upb_mobile/pages/timetable/view/timetable_page.dart'; -import 'package:acs_upb_mobile/resources/locale_provider.dart'; -import 'package:acs_upb_mobile/resources/remote_config.dart'; -import 'package:acs_upb_mobile/resources/utils.dart'; -import 'package:acs_upb_mobile/widgets/search_bar.dart'; -import 'package:firebase_core/firebase_core.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_feather_icons/flutter_feather_icons.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:network_image_mock/network_image_mock.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:preferences/preferences.dart'; -import 'package:provider/provider.dart'; -import 'package:rrule/rrule.dart'; -import 'package:time_machine/time_machine.dart' hide Offset; -import 'package:timetable/src/header/week_indicator.dart'; - -import 'firebase_mock.dart'; -import 'test_utils.dart'; - -// These tests open each page in the app on multiple screen sizes to make sure -// nothing overflows/breaks. - -class MockAuthProvider extends Mock implements AuthProvider {} - -class MockWebsiteProvider extends Mock implements WebsiteProvider {} - -class MockFilterProvider extends Mock implements FilterProvider {} - -class MockClassProvider extends Mock implements ClassProvider {} - -class MockPersonProvider extends Mock implements PersonProvider {} - -class MockQuestionProvider extends Mock implements QuestionProvider {} - -class MockUniEventProvider extends Mock implements UniEventProvider {} - -class MockNewsProvider extends Mock implements NewsProvider {} - -class MockRequestProvider extends Mock implements RequestProvider {} - -class MockNavigatorObserver extends Mock implements NavigatorObserver {} - -class MockFeedbackProvider extends Mock implements FeedbackProvider {} - -Future main() async { - AuthProvider mockAuthProvider; - WebsiteProvider mockWebsiteProvider; - FilterProvider mockFilterProvider; - ClassProvider mockClassProvider; - PersonProvider mockPersonProvider; - MockQuestionProvider mockQuestionProvider; - MockNewsProvider mockNewsProvider; - UniEventProvider mockEventProvider; - RequestProvider mockRequestProvider; - FeedbackProvider mockFeedbackProvider; - - setupFirebaseAuthMocks(); - await Firebase.initializeApp(); - - // Test layout for different screen sizes - // TODO(AdrianMargineanu): Use Flutter driver for integration tests, setting screen sizes here isn't reliable - final screenSizes = [ - // Phone - const Size(720, 1280), - // Tablet - const Size(600, 1024), - ]; - - // Add landscape mode sizes - screenSizes.addAll(List.from(screenSizes) - .map((size) => Size(size.height, size.width))); - - final TestWidgetsFlutterBinding binding = - TestWidgetsFlutterBinding.ensureInitialized(); - - Widget buildApp() => MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ChangeNotifierProvider( - create: (_) => mockWebsiteProvider), - ChangeNotifierProvider( - create: (_) => mockFilterProvider), - ChangeNotifierProvider( - create: (_) => mockClassProvider), - ChangeNotifierProvider( - create: (_) => mockPersonProvider), - ChangeNotifierProvider( - create: (_) => mockQuestionProvider), - ChangeNotifierProvider(create: (_) => mockNewsProvider), - ChangeNotifierProvider( - create: (_) => mockEventProvider), - Provider(create: (_) => mockRequestProvider), - ChangeNotifierProvider( - create: (_) => mockFeedbackProvider), - ], - child: const MyApp(), - ); - - setUp(() async { - WidgetsFlutterBinding.ensureInitialized(); - PrefService.enableCaching(); - PrefService.cache = {}; - PrefService.setString('language', 'en'); - - await Firebase.initializeApp(); - - LocaleProvider.cultures = testCultures; - LocaleProvider.rruleL10ns = {'en': await RruleL10nTest.create()}; - - Utils.packageInfo = PackageInfo( - version: '1.2.7', - buildNumber: '6', - appName: 'ACS UPB Mobile', - packageName: 'ro.upb.acs_upb_mobile', - ); - - // Pretend an anonymous user is already logged in - mockAuthProvider = MockAuthProvider(); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - // ignore: invalid_use_of_protected_member - when(mockAuthProvider.hasListeners).thenReturn(false); - when(mockAuthProvider.isAnonymous).thenReturn(true); - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(null)); - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(false)); - - mockWebsiteProvider = MockWebsiteProvider(); - // ignore: invalid_use_of_protected_member - when(mockWebsiteProvider.hasListeners).thenReturn(false); - when(mockWebsiteProvider.deleteWebsite(any)) - .thenAnswer((_) => Future.value(true)); - when(mockAuthProvider.getProfilePictureURL()) - .thenAnswer((_) => Future.value(null)); - when(mockWebsiteProvider.fetchWebsites(any)) - .thenAnswer((_) => Future.value([ - Website( - id: '1', - relevance: null, - category: WebsiteCategory.learning, - infoByLocale: {'en': 'info-en', 'ro': 'info-ro'}, - label: 'Moodle1', - link: 'http://acs.curs.pub.ro/', - isPrivate: false, - ), - Website( - id: '2', - relevance: null, - category: WebsiteCategory.learning, - infoByLocale: {}, - label: 'OCW1', - link: 'https://ocw.cs.pub.ro/', - isPrivate: false, - ), - Website( - id: '3', - relevance: null, - category: WebsiteCategory.learning, - infoByLocale: {'en': 'info-en', 'ro': 'info-ro'}, - label: 'Moodle2', - link: 'http://acs.curs.pub.ro/', - isPrivate: false, - ), - Website( - id: '4', - relevance: null, - category: WebsiteCategory.learning, - infoByLocale: {}, - label: 'OCW2', - link: 'https://ocw.cs.pub.ro/', - isPrivate: false, - ), - Website( - id: '5', - relevance: null, - category: WebsiteCategory.association, - infoByLocale: {}, - label: 'LSAC1', - link: 'https://lsacbucuresti.ro/', - isPrivate: false, - ), - Website( - id: '6', - relevance: null, - category: WebsiteCategory.administrative, - infoByLocale: {}, - label: 'LSAC2', - link: 'https://lsacbucuresti.ro/', - isPrivate: false, - ), - Website( - id: '7', - relevance: null, - category: WebsiteCategory.resource, - infoByLocale: {}, - label: 'LSAC3', - link: 'https://lsacbucuresti.ro/', - isPrivate: false, - ), - Website( - id: '8', - relevance: null, - category: WebsiteCategory.other, - infoByLocale: {}, - label: 'LSAC4', - link: 'https://lsacbucuresti.ro/', - isPrivate: false, - ), - ])); - when(mockWebsiteProvider.fetchFavouriteWebsites(any)).thenAnswer( - (_) async => (await mockWebsiteProvider.fetchWebsites(any)).take(3)); - - mockFilterProvider = MockFilterProvider(); - // ignore: invalid_use_of_protected_member - when(mockFilterProvider.hasListeners).thenReturn(false); - when(mockFilterProvider.filterEnabled).thenReturn(true); - final filter = Filter( - localizedLevelNames: [ - {'en': 'Degree', 'ro': 'Nivel de studiu'}, - {'en': 'Major', 'ro': 'Specializare'}, - {'en': 'Year', 'ro': 'An'}, - {'en': 'Series', 'ro': 'Serie'}, - {'en': 'Group', 'ro': 'Group'} - ], - root: FilterNode(name: 'All', value: true, children: [ - FilterNode(name: 'BSc', value: true, children: [ - FilterNode(name: 'CTI', value: true, children: [ - FilterNode(name: 'CTI-1', value: true, children: [ - FilterNode(name: '1-CA'), - FilterNode( - name: '1-CB', - value: true, - children: [ - FilterNode(name: '311CB'), - FilterNode(name: '312CB'), - FilterNode(name: '313CB'), - FilterNode( - name: '314CB', - value: true, - ), - ], - ), - FilterNode(name: '1-CC'), - FilterNode( - name: '1-CD', - children: [ - FilterNode(name: '311CD'), - FilterNode(name: '312CD'), - FilterNode(name: '313CD'), - FilterNode(name: '314CD'), - ], - ), - ]), - FilterNode( - name: 'CTI-2', - ), - FilterNode( - name: 'CTI-3', - ), - FilterNode( - name: 'CTI-4', - ), - ]), - FilterNode(name: 'IS') - ]), - FilterNode(name: 'MSc', children: [ - FilterNode( - name: 'IA', - ), - FilterNode(name: 'SPRC'), - ]) - ])); - when(mockFilterProvider.cachedFilter).thenReturn(filter); - when(mockFilterProvider.fetchFilter()) - .thenAnswer((_) => Future.value(filter)); - - mockClassProvider = MockClassProvider(); - // ignore: invalid_use_of_protected_member - when(mockClassProvider.hasListeners).thenReturn(false); - final userClassHeaders = [ - ClassHeader( - id: '3', - name: 'Programming', - acronym: 'PC', - category: 'A', - ), - ClassHeader( - id: '4', - name: 'Physics', - acronym: 'PH', - category: 'D', - ) - ]; - when(mockClassProvider.userClassHeadersCache).thenReturn(userClassHeaders); - when(mockClassProvider.fetchClassHeaders(uid: anyNamed('uid'))) - .thenAnswer((_) => Future.value([ - ClassHeader( - id: '1', - name: 'Maths 1', - acronym: 'M1', - category: 'A/B', - ), - ClassHeader( - id: '2', - name: 'Maths 2', - acronym: 'M2', - category: 'A/C', - ), - ] + - userClassHeaders)); - when(mockClassProvider.fetchUserClassIds(any)) - .thenAnswer((_) => Future.value(['3', '4'])); - when(mockClassProvider.fetchClassInfo(any)).thenAnswer((_) => Future.value( - Class( - header: ClassHeader( - id: '3', - name: 'Programming', - acronym: 'PC', - category: 'A', - ), - shortcuts: [ - Shortcut( - type: ShortcutType.main, - name: 'OCW', - link: 'https://ocw.cs.pub.ro/courses/programare'), - Shortcut( - type: ShortcutType.other, - name: 'Google', - link: 'https://google.com'), - ], - grading: { - 'Exam': 4, - 'Lab': 1.5, - 'Homework': 4, - 'Extra homework': 0.5, - }, - ), - )); - - RemoteConfigService.overrides = {'feedback_enabled': true}; - - mockPersonProvider = MockPersonProvider(); - // ignore: invalid_use_of_protected_member - when(mockPersonProvider.hasListeners).thenReturn(false); - when(mockPersonProvider.fetchPeople()).thenAnswer((_) => Future.value([ - Person( - name: 'John Doe', - email: 'john.doe@cs.pub.ro', - phone: '0712345678', - office: 'AB123', - position: 'Associate Professor, Dr., Department Council', - photo: 'https://cdn.worldvectorlogo.com/logos/flutter-logo.svg', - ), - Person( - name: 'Jane Doe', - email: 'jane.doe@cs.pub.ro', - phone: '-', - office: 'Narnia', - position: 'Professor, Dr.', - photo: 'https://cdn.worldvectorlogo.com/logos/flutter-logo.svg', - ), - Person( - name: 'Mary Poppins', - email: 'supercalifragilistic.expialidocious@cs.pub.ro', - phone: '0712-345-678', - office: 'Mary Poppins\' office', - position: 'Professor, Dr., Head of Department', - photo: 'https://cdn.worldvectorlogo.com/logos/flutter-logo.svg', - ), - ])); - - when(mockPersonProvider.mostRecentLecturer(any)) - .thenAnswer((_) => Future.value('Jane Doe')); - - mockFeedbackProvider = MockFeedbackProvider(); - // ignore: invalid_use_of_protected_member - when(mockFeedbackProvider.hasListeners).thenReturn(true); - when(mockFeedbackProvider.fetchQuestions()).thenAnswer((_) => Future.value({ - '0': FeedbackQuestionDropdown( - category: 'involvement', - question: - 'Approximate number of activities that you attended (lectures + applications):', - id: '0', - answerOptions: ['option 1', 'option 2', 'option 3', 'option 4'], - ), - '1': FeedbackQuestionRating( - category: 'applications', - question: 'Was the exposure method appropriate?', - id: '1', - ), - '2': FeedbackQuestionText( - category: 'personal', - question: 'What are the positive aspects of this class?', - id: '2', - ), - '3': FeedbackQuestionSlider( - category: 'homework', - question: - 'Estimate the average number of hours per week devoted to solving homework.', - id: '3', - ), - })); - when(mockFeedbackProvider.fetchCategories()) - .thenAnswer((_) => Future.value({ - 'applications': {'en': 'Applications', 'ro': 'Aplicații'}, - 'homework': {'en': 'Homework', 'ro': 'Temă'}, - 'involvement': {'en': 'Involvement', 'ro': 'Implicare'}, - 'personal': { - 'en': 'Personal comments', - 'ro': 'Comentarii personale' - }, - })); - - when(mockFeedbackProvider.userSubmittedFeedbackForClass(any, any)) - .thenAnswer((_) => Future.value(false)); - when(mockFeedbackProvider.submitFeedback(any, any, any, any, any)) - .thenAnswer((_) => Future.value(true)); - when(mockFeedbackProvider.getClassesWithCompletedFeedback(any)) - .thenAnswer((_) => Future.value({'M1': true, 'M2': true})); - when(mockFeedbackProvider.countClassesWithoutFeedback(any, any)) - .thenAnswer((_) => Future.value('2')); - - mockQuestionProvider = MockQuestionProvider(); - // ignore: invalid_use_of_protected_member - when(mockQuestionProvider.hasListeners).thenReturn(false); - when(mockQuestionProvider.fetchQuestions()) - .thenAnswer((_) => Future.value([ - Question( - question: 'Care este programul la secretariat?', - answer: - 'Secretariatul este deschis în timpul săptămânii între orele 9:00 si 11:00.', - tags: ['Licență']), - Question( - question: 'Cum mă conectez la eduroam?', - answer: - 'Conectarea în rețeaua *eduroam* se face pe baza aceluiași cont folosit și pe site-ul de cursuri.', - tags: ['Conectare', 'Informații']) - ])); - when(mockQuestionProvider.fetchQuestions(limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([ - Question( - question: 'Care este programul la secretariat?', - answer: - 'Secretariatul este deschis în timpul săptămânii între orele 9:00 si 11:00.', - tags: ['Licență']), - Question( - question: 'Cum mă conectez la eduroam?', - answer: - 'Conectarea în rețeaua *eduroam* se face pe baza aceluiași cont folosit și pe site-ul de cursuri.', - tags: ['Conectare', 'Informații']) - ])); - - mockNewsProvider = MockNewsProvider(); - // ignore: invalid_use_of_protected_member - when(mockNewsProvider.hasListeners).thenReturn(false); - when(mockNewsProvider.fetchNewsFeedItems()) - .thenAnswer((_) => Future.value([ - NewsFeedItem( - '03.10.2020', - 'Cazarea studentilor de anul II licenta', - 'https://acs.pub.ro/noutati/cazarea-studentilor-de-anul-ii-licenta/'), - NewsFeedItem( - '03.10.2020', - 'Festivitatea de deschidere a anului universitar 2020-2021', - 'https://acs.pub.ro/noutati/festivitatea-de-deschidere-a-anului-universitar-2020-2021/') - ])); - when(mockNewsProvider.fetchNewsFeedItems(limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([ - NewsFeedItem( - '03.10.2020', - 'Cazarea studentilor de anul II licenta', - 'https://acs.pub.ro/noutati/cazarea-studentilor-de-anul-ii-licenta/'), - NewsFeedItem( - '03.10.2020', - 'Festivitatea de deschidere a anului universitar 2020-2021', - 'https://acs.pub.ro/noutati/festivitatea-de-deschidere-a-anului-universitar-2020-2021/') - ])); - - mockEventProvider = MockUniEventProvider(); - // ignore: invalid_use_of_protected_member - when(mockEventProvider.hasListeners).thenReturn(false); - final now = LocalDate.today(); - final weekStart = now.subtractDays(now.dayOfWeek - DayOfWeek.monday); - final holidays = [ - // Holiday on Tuesday and Wednesday next week - AllDayUniEvent( - name: 'Holiday', - start: weekStart.addWeeks(1).addDays(1), - end: weekStart.addWeeks(1).addDays(2), - id: 'holiday0', - ), - AllDayUniEvent( - name: 'Inter-semester holiday', - start: weekStart.addWeeks(2).subtractDays(2), - end: weekStart.addWeeks(3).subtractDays(1), - id: 'holiday1', - ), - ]; - final calendar = AcademicCalendar( - id: '2020', - semesters: [ - AllDayUniEvent( - start: weekStart, - end: weekStart.addWeeks(2).subtractDays(3), - id: 'semester1', - ), - AllDayUniEvent( - start: weekStart.addWeeks(3), - end: weekStart.addWeeks(5).subtractDays(3), - id: 'semester2', - ), - ], - holidays: holidays, - ); - when(mockEventProvider.fetchCalendars()) - .thenAnswer((_) => Future.value({'2020': calendar})); - when(mockEventProvider.getUpcomingEvents(LocalDate.today())) - .thenAnswer((_) => Future.value([])); - when(mockEventProvider.getUpcomingEvents(LocalDate.today(), - limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([])); - when(mockEventProvider.getAllEventsOfClass(any)) - .thenAnswer((_) => Future.value([])); - final rruleEveryWeekFirstSem = RecurrenceRule( - frequency: Frequency.weekly, - interval: 1, - until: weekStart.addWeeks(2).subtractDays(3).atMidnight(), - ); - final rruleEveryTwoWeeksFirstSem = RecurrenceRule( - frequency: Frequency.weekly, - interval: 2, - until: weekStart.addWeeks(2).subtractDays(3).atMidnight(), - ); - final rruleEveryWeek = RecurrenceRule( - frequency: Frequency.weekly, - interval: 1, - ); - final rruleEveryTwoWeeks = RecurrenceRule( - frequency: Frequency.weekly, - interval: 2, - ); - const duration = Period(hours: 2); - final events = [ - RecurringUniEvent( - name: 'M1', - calendar: calendar, - rrule: rruleEveryWeekFirstSem, - start: weekStart.at(LocalTime(8, 0, 0)), - period: duration, - id: '0', - ), - RecurringUniEvent( - name: 'M2', - calendar: calendar, - rrule: rruleEveryTwoWeeksFirstSem, - start: weekStart.at(LocalTime(10, 0, 0)), - period: duration, - id: '1', - ), - RecurringUniEvent( - name: 'T1', - calendar: calendar, - rrule: rruleEveryWeekFirstSem, - start: weekStart.addDays(1).at(LocalTime(8, 0, 0)), - period: duration, - id: '2', - ), - RecurringUniEvent( - name: 'T2', - calendar: calendar, - rrule: rruleEveryTwoWeeksFirstSem, - start: weekStart.addDays(1).at(LocalTime(9, 0, 0)), - period: duration, - id: '3', - ), - RecurringUniEvent( - name: 'W1', - calendar: calendar, - rrule: rruleEveryWeekFirstSem, - start: weekStart.addDays(2).at(LocalTime(8, 0, 0)), - period: duration, - id: '4', - ), - RecurringUniEvent( - name: 'W2', - rrule: rruleEveryWeek, - start: weekStart.addDays(2).at(LocalTime(10, 0, 0)), - period: duration, - id: '5', - ), - RecurringUniEvent( - name: 'W3', - rrule: rruleEveryTwoWeeks, - start: weekStart.addDays(2).at(LocalTime(12, 0, 0)), - period: duration, - id: '6', - ), - ClassEvent( - classHeader: userClassHeaders[0], - type: UniEventType.lecture, - teacher: Person(name: 'Jane Doe'), - location: 'AB123', - degree: 'BSc', - relevance: ['314CB'], - calendar: calendar, - rrule: rruleEveryWeek, - start: weekStart.addDays(3).at(LocalTime(10, 0, 0)), - period: duration, - id: '7', - ), - RecurringUniEvent( - classHeader: userClassHeaders[1], - type: UniEventType.lecture, - location: 'AB123', - degree: 'BSc', - relevance: ['314CB'], - calendar: calendar, - rrule: rruleEveryTwoWeeks, - start: weekStart.addDays(3).at(LocalTime(12, 0, 0)), - period: duration, - id: '8', - ), - RecurringUniEvent( - name: 'F1', - calendar: calendar, - rrule: rruleEveryWeek, - start: weekStart.addDays(4).at(LocalTime(10, 0, 0)), - period: duration, - id: '9', - ), - RecurringUniEvent( - name: 'F2', - calendar: calendar, - rrule: rruleEveryTwoWeeks, - start: weekStart.addDays(4).at(LocalTime(12, 0, 0)), - period: duration, - id: '10', - ), - ]; - when(mockEventProvider.getAllDayEventsIntersecting(any)) - .thenAnswer((invocation) { - final DateInterval interval = invocation.positionalArguments[0]; - return Stream.value(holidays - .map((holiday) => - holiday.generateInstances(intersectingInterval: interval)) - .expand((e) => e)); - }); - when(mockEventProvider.getPartDayEventsIntersecting(any)) - .thenAnswer((invocation) { - final LocalDate date = invocation.positionalArguments[0]; - return Stream.value(events - .map((event) => event.generateInstances( - intersectingInterval: DateInterval(date, date))) - .expand((e) => e)); - }); - when(mockEventProvider.empty).thenReturn(false); - when(mockEventProvider.deleteEvent(any)) - .thenAnswer((_) => Future.value(true)); - when(mockEventProvider.updateEvent(any)) - .thenAnswer((_) => Future.value(true)); - when(mockEventProvider.addEvent(any)).thenAnswer((_) => Future.value(true)); - - mockRequestProvider = MockRequestProvider(); - when(mockRequestProvider.makeRequest(any)) - .thenAnswer((_) => Future.value(true)); - when(mockRequestProvider.userAlreadyRequested(any)) - .thenAnswer((_) => Future.value(false)); - }); - - group('Home', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - expect(find.byType(HomePage), findsOneWidget); - - // Open home - await tester.tap(find.byIcon(Icons.home)); - await tester.pumpAndSettle(); - - expect(find.byType(HomePage), findsOneWidget); - }); - } - }); - - group('Timetable', () { - setUp(() { - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( - uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); - when(mockAuthProvider.currentUserFromCache).thenReturn(User( - uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3)); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.uid).thenReturn('0'); - }); - - group('Timetable no events/no classes', () { - setUp(() { - when(mockEventProvider.getAllDayEventsIntersecting(any)) - .thenAnswer((_) => Stream.value([])); - when(mockEventProvider.getPartDayEventsIntersecting(any)) - .thenAnswer((_) => Stream.value([])); - when(mockEventProvider.empty).thenReturn(true); - - when(mockClassProvider.userClassHeadersCache).thenReturn(null); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(TimetablePage), findsOneWidget); - expect(find.text('No events to show'), findsOneWidget); - - await tester.tap(find.text('CHOOSE CLASSES')); - await tester.pumpAndSettle(); - - expect(find.byType(AddClassesPage), findsOneWidget); - }); - } - }); - - group('Timetable no events/no filter', () { - setUp(() { - when(mockEventProvider.getAllDayEventsIntersecting(any)) - .thenAnswer((_) => Stream.value([])); - when(mockEventProvider.getPartDayEventsIntersecting(any)) - .thenAnswer((_) => Stream.value([])); - when(mockEventProvider.empty).thenReturn(true); - - when(mockFilterProvider.cachedFilter).thenReturn(null); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(TimetablePage), findsOneWidget); - expect(find.text('No events to show'), findsOneWidget); - - await tester.tap(find.text('OPEN FILTER')); - await tester.pumpAndSettle(); - - expect(find.byType(FilterPage), findsOneWidget); - }); - } - }); - - group('Timetable no events/no permissions', () { - setUp(() { - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( - uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 0))); - when(mockAuthProvider.currentUserFromCache).thenReturn(User( - uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 0)); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); - - when(mockEventProvider.getAllDayEventsIntersecting(any)) - .thenAnswer((_) => Stream.value([])); - when(mockEventProvider.getPartDayEventsIntersecting(any)) - .thenAnswer((_) => Stream.value([])); - when(mockEventProvider.empty).thenReturn(true); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(TimetablePage), findsOneWidget); - expect(find.text('No events to show'), findsOneWidget); - - await tester.tap(find.text('REQUEST PERMISSIONS')); - await tester.pumpAndSettle(); - - expect(find.byType(RequestPermissionsPage), findsOneWidget); - }); - } - }); - - group('Timetable no events/add some', () { - setUp(() { - when(mockEventProvider.getAllDayEventsIntersecting(any)) - .thenAnswer((_) => Stream.value([])); - when(mockEventProvider.getPartDayEventsIntersecting(any)) - .thenAnswer((_) => Stream.value([])); - when(mockEventProvider.empty).thenReturn(true); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(TimetablePage), findsOneWidget); - expect(find.text('No events to show'), findsOneWidget); - expect( - find.byKey(const ValueKey('no_events_message')), findsOneWidget); - - await tester.tap(find.text('CANCEL')); - await tester.pumpAndSettle(); - - expect(find.text('No events to show'), findsNothing); - }); - } - }); - - group('Timetable events', () { - for (final size in screenSizes) { - if (size.width > size.height) { - // TODO(IoanaAlexandru): In landscape mode the test fails in a weird - // way - it seems as if two weeks are visible at the same time, but - // the behaviour cannot be reproduced on a device. Skipping this - // test in landscape mode for now. - continue; - } - - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(TimetablePage), findsOneWidget); - - // Scroll to previous week - await tester.drag(find.text('Tue'), Offset(size.width - 30, 0)); - await tester.pumpAndSettle(); - - // Expect previous week - final previousWeek = WeekYearRules.iso - .getWeekOfWeekYear(LocalDate.today().subtractWeeks(1)); - expect( - find.byWidgetPredicate((widget) => - widget is WeekIndicator && - widget.week.toString() == previousWeek.toString()), - findsOneWidget); - - expect(find.text('Holiday'), findsNothing); - expect(find.text('Inter-semester holiday'), findsNothing); - expect(find.text('M1'), findsNothing); - expect(find.text('M2'), findsNothing); - expect(find.text('T1'), findsNothing); - expect(find.text('T2'), findsNothing); - expect(find.text('W1'), findsNothing); - expect(find.text('W2'), findsNothing); - expect(find.text('W3'), findsNothing); - expect(find.text('PC'), findsNothing); - expect(find.text('PH'), findsNothing); - expect(find.text('F1'), findsNothing); - expect(find.text('F2'), findsNothing); - - // Scroll back to current week - await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); - await tester.pumpAndSettle(); - - // Expect current week - final currentWeek = - WeekYearRules.iso.getWeekOfWeekYear(LocalDate.today()); - expect( - find.byWidgetPredicate((widget) => - widget is WeekIndicator && - widget.week.toString() == currentWeek.toString()), - findsOneWidget); - - expect(find.text('Holiday'), findsNothing); - expect(find.text('Inter-semester holiday'), findsNothing); - expect(find.text('M1'), findsOneWidget); - expect(find.text('M2'), findsOneWidget); - expect(find.text('T1'), findsOneWidget); - expect(find.text('T2'), findsOneWidget); - expect(find.text('W1'), findsOneWidget); - expect(find.text('W2'), findsOneWidget); - expect(find.text('W3'), findsOneWidget); - expect(find.text('PC'), findsOneWidget); - expect(find.text('PH'), findsOneWidget); - expect(find.text('F1'), findsOneWidget); - expect(find.text('F2'), findsOneWidget); - - // Scroll to next week - await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); - await tester.pumpAndSettle(); - - // Expect next week - final nextWeek = WeekYearRules.iso - .getWeekOfWeekYear(LocalDate.today().addWeeks(1)); - expect( - find.byWidgetPredicate((widget) => - widget is WeekIndicator && - widget.week.toString() == nextWeek.toString()), - findsOneWidget); - - expect(find.text('Holiday'), findsOneWidget); - expect(find.text('Inter-semester holiday'), findsOneWidget); - expect(find.text('M1'), findsOneWidget); - expect(find.text('M2'), findsNothing); - expect(find.text('T1'), findsNothing); - expect(find.text('T2'), findsNothing); - expect(find.text('W1'), findsNothing); - expect(find.text('W2'), findsOneWidget); - expect(find.text('W3'), findsNothing); - expect(find.text('PC'), findsOneWidget); - expect(find.text('PH'), findsNothing); - expect(find.text('F1'), findsOneWidget); - expect(find.text('F2'), findsNothing); - - // Scroll to next week - await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); - await tester.pumpAndSettle(); - - // Expect next week - final nextNextWeek = WeekYearRules.iso - .getWeekOfWeekYear(LocalDate.today().addWeeks(2)); - expect( - find.byWidgetPredicate((widget) => - widget is WeekIndicator && - widget.week.toString() == nextNextWeek.toString()), - findsOneWidget); - - expect(find.text('Holiday'), findsNothing); - expect(find.text('Inter-semester holiday'), findsOneWidget); - expect(find.text('M1'), findsNothing); - expect(find.text('M2'), findsNothing); - expect(find.text('T1'), findsNothing); - expect(find.text('T2'), findsNothing); - expect(find.text('W1'), findsNothing); - expect(find.text('W2'), findsOneWidget); - expect(find.text('W3'), findsOneWidget); - expect(find.text('PC'), findsNothing); - expect(find.text('PH'), findsNothing); - expect(find.text('F1'), findsNothing); - expect(find.text('F2'), findsNothing); - - // Scroll to next week - await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); - await tester.pumpAndSettle(); - - // Expect next week - final nextNextNextWeek = WeekYearRules.iso - .getWeekOfWeekYear(LocalDate.today().addWeeks(3)); - expect( - find.byWidgetPredicate((widget) => - widget is WeekIndicator && - widget.week.toString() == nextNextNextWeek.toString()), - findsOneWidget); - - expect(find.text('Holiday'), findsNothing); - expect(find.text('Inter-semester holiday'), findsNothing); - expect(find.text('M1'), findsNothing); - expect(find.text('M2'), findsNothing); - expect(find.text('T1'), findsNothing); - expect(find.text('T2'), findsNothing); - expect(find.text('W1'), findsNothing); - expect(find.text('W2'), findsOneWidget); - expect(find.text('W3'), findsNothing); - expect(find.text('PC'), findsOneWidget); - expect(find.text('PH'), findsOneWidget); - expect(find.text('F1'), findsOneWidget); - expect(find.text('F2'), findsOneWidget); - - // Navigate to today - await tester.tap(find.byIcon(Icons.today_outlined)); - await tester.pumpAndSettle(); - - // Expect current week - expect( - find.byWidgetPredicate((widget) => - widget is WeekIndicator && - widget.week.toString() == currentWeek.toString()), - findsOneWidget); - }); - } - }); - - group('Event page - open all day event', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - // Expect current week - final currentWeek = - WeekYearRules.iso.getWeekOfWeekYear(LocalDate.today()); - expect( - find.byWidgetPredicate((widget) => - widget is WeekIndicator && - widget.week.toString() == currentWeek.toString()), - findsOneWidget); - - // Scroll to next week - await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); - await tester.pumpAndSettle(); - - // Expect next week - final nextWeek = WeekYearRules.iso - .getWeekOfWeekYear(LocalDate.today().addWeeks(1)); - expect( - find.byWidgetPredicate((widget) => - widget is WeekIndicator && - widget.week.toString() == nextWeek.toString()), - findsOneWidget); - - // Open holiday event - await tester.tap(find.text('Holiday')); - await tester.pumpAndSettle(); - - expect(find.byType(EventView), findsOneWidget); - }); - } - }); - - group('Event page - add event', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - // Open add event page - await tester - .tapAt(tester.getCenter(find.text('Sat')).translate(0, 100)); - await tester.pumpAndSettle(); - - expect(find.byType(AddEventView), findsOneWidget); - - // Select type - await tester.tap(find.text('Type')); - await tester.pumpAndSettle(); - await tester.tap(find.text('Seminar').last); - await tester.pumpAndSettle(); - - // Select class - await tester.tap(find.text('Class')); - await tester.pumpAndSettle(); - await tester.tap(find.text('Programming').last); - await tester.pumpAndSettle(); - - // Press back - await tester.tap(find.byIcon(Icons.arrow_back)); - await tester.pumpAndSettle(); - - await tester - .tapAt(tester.getCenter(find.text('Sat')).translate(0, 100)); - await tester.pumpAndSettle(); - - expect(find.byType(AddEventView), findsOneWidget); - - // Select type - await tester.tap(find.text('Type')); - await tester.pumpAndSettle(); - await tester.tap(find.text('Lecture').last); - await tester.pumpAndSettle(); - - // Select lecturer - partial name - await tester.tap(find.byIcon(FeatherIcons.user)); - await tester.pumpAndSettle(); - await tester.enterText( - find.byKey(const Key('AutocompleteLecturer')), 'John'); - await tester.pumpAndSettle(); - await tester.tap(find.text('John Doe')); - await tester.pumpAndSettle(); - - // Select lecturer - new name - await tester.tap(find.byIcon(FeatherIcons.user)); - await tester.pumpAndSettle(); - await tester.enterText( - find.byKey(const Key('AutocompleteLecturer')), 'Isabel Steward'); - await tester.tap(find.text('Isabel Steward')); - await tester.pumpAndSettle(); - - // Select lecturer - check autocomplete suggestions - await tester.tap(find.byIcon(FeatherIcons.user)); - await tester.pumpAndSettle(); - await tester.enterText( - find.byKey(const Key('AutocompleteLecturer')), 'Doe'); - await tester.pumpAndSettle(); - - expect(find.text('Jane Doe'), findsOneWidget); - expect(find.text('John Doe'), findsOneWidget); - - await tester.tap(find.text('Jane Doe')); - await tester.pumpAndSettle(); - }); - } - }); - - group('Event page - edit event', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - // Open PC event - await tester.tap(find.text('PC')); - await tester.pumpAndSettle(); - - expect(find.byType(EventView), findsOneWidget); - - // Open class page - await tester.tap(find.text('Programming')); - await tester.pumpAndSettle(); - - expect(find.byType(ClassView), findsOneWidget); - expect(find.byIcon(FeatherIcons.user), findsOneWidget); - expect(find.byKey(const Key('LecturerCard')), findsOneWidget); - - // Press back - await tester.tap(find.byIcon(Icons.arrow_back)); - await tester.pumpAndSettle(); - - expect(find.byType(EventView), findsOneWidget); - expect(find.byIcon(FeatherIcons.user), findsOneWidget); - expect(find.text('Jane Doe'), findsOneWidget); - - await tester.tap(find.byIcon(FeatherIcons.user)); - await tester.pumpAndSettle(); - - expect(find.byType(PersonView), findsOneWidget); - - // Press back - await tester.tap(find.byIcon(Icons.arrow_back)); - await tester.pumpAndSettle(); - - // Open edit event page - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(AddEventView), findsOneWidget); - expect(find.text('Lecturer'), findsOneWidget); - expect(find.text('Location'), findsOneWidget); - expect(find.text('Week'), findsOneWidget); - expect(find.text('Day'), findsOneWidget); - - // Select lecturer - await tester.tap(find.text('Lecturer')); - await tester.pumpAndSettle(); - await tester.enterText( - find.byKey(const Key('AutocompleteLecturer')), 'Doe'); - await tester.pumpAndSettle(); - - expect(find.text('Jane Doe'), findsOneWidget); - expect(find.text('John Doe'), findsOneWidget); - - await tester.enterText( - find.byKey(const Key('AutocompleteLecturer')), 'John Doe'); - await tester.pumpAndSettle(); - - FocusManager.instance.primaryFocus.unfocus(); - await tester.pumpAndSettle(); - await tester.tap(find.text('John Doe').last); - await tester.pumpAndSettle(); - - FocusManager.instance.primaryFocus.unfocus(); - await tester.pumpAndSettle(); - - expect(find.text('Jane Doe'), findsNothing); - expect(find.text('John Doe'), findsOneWidget); - - // Press save - await tester.tap(find.text('Save')); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - expect(find.byType(TimetablePage), findsOneWidget); - - // Open PC event - await tester.tap(find.text('PC')); - await tester.pumpAndSettle(); - - expect(find.byType(EventView), findsOneWidget); - expect(find.byIcon(FeatherIcons.user), findsOneWidget); - - // Press back - await tester.tap(find.byIcon(Icons.arrow_back)); - await tester.pumpAndSettle(); - - expect(find.byType(TimetablePage), findsOneWidget); - }); - } - }); - - group('Event page - delete event', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - // Open PH event - await tester.tap(find.text('PH')); - await tester.pumpAndSettle(); - - expect(find.byType(EventView), findsOneWidget); - - // Open edit event page - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(AddEventView), findsOneWidget); - - // Open delete dialog - await tester.tap(find.byIcon(Icons.more_vert_outlined)); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Delete event')); - await tester.pumpAndSettle(); - - // Confirm deletion - expect(find.text('Are you sure you want to delete this event?'), - findsOneWidget); - await tester.tap(find.text('DELETE EVENT')); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - verify(mockEventProvider.deleteEvent(any)); - expect(find.byType(TimetablePage), findsOneWidget); - }); - } - }); - }); - - group('Classes', () { - group('Class', () { - setUp(() { - when(mockAuthProvider.currentUser).thenAnswer((_) => - Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); - when(mockAuthProvider.currentUserFromCache) - .thenReturn(User(uid: '0', firstName: 'John', lastName: 'Doe')); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.uid).thenReturn('0'); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - // Open classes - await tester.tap(find.byIcon(FeatherIcons.bookOpen)); - await tester.pumpAndSettle(); - - // Open class view - expect(find.byType(ClassesPage), findsOneWidget); - }); - } - }); - - group('Add class', () { - setUp(() { - when(mockAuthProvider.currentUser).thenAnswer((_) => - Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); - when(mockAuthProvider.currentUserFromCache) - .thenReturn(User(uid: '0', firstName: 'John', lastName: 'Doe')); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.uid).thenReturn('0'); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - // Open classes - await tester.tap(find.byIcon(FeatherIcons.bookOpen)); - await tester.pumpAndSettle(); - - // Open add class view - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(AddClassesPage), findsOneWidget); - - // Save - await tester.tap(find.text('Save')); - await tester.pumpAndSettle(); - - expect(find.byType(ClassesPage), findsOneWidget); - }); - } - }); - - group('Class view', () { - setUp(() { - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( - uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); - when(mockAuthProvider.currentUserFromCache).thenReturn(User( - uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3)); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.uid).thenReturn('0'); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - // Open classes - await tester.tap(find.byIcon(FeatherIcons.bookOpen)); - await tester.pumpAndSettle(); - - // Open class view - await tester.tap(find.text('PC')); - await tester.pumpAndSettle(); - - expect(find.byType(ClassView), findsOneWidget); - - // Open add shortcut view - await tester.tap(find.byIcon(Icons.add_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(ShortcutView), findsOneWidget); - - await tester.tap(find.byIcon(Icons.arrow_back)); - await tester.pumpAndSettle(); - - expect(find.byType(ClassView), findsOneWidget); - - // Open grading view - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(GradingView), findsOneWidget); - - await tester.tap(find.text('Save')); - await tester.pumpAndSettle(); - - expect(find.byType(ClassView), findsOneWidget); - }); - } - }); - }); - - group('Feedback view', () { - setUp(() { - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( - uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); - when(mockAuthProvider.currentUserFromCache).thenReturn(User( - uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3)); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.uid).thenReturn('0'); - when(mockPersonProvider.fetchPerson(any)) - .thenAnswer((_) => Future.value(Person(name: 'John Doe'))); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - // Open classes - await tester.tap(find.byIcon(FeatherIcons.bookOpen)); - await tester.pumpAndSettle(); - - // Open class view - await tester.tap(find.text('PC')); - await tester.pumpAndSettle(); - - expect(find.byType(ClassView), findsOneWidget); - - // Open feedback page - await tester.tap(find.byIcon(Icons.rate_review_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(ClassFeedbackView), findsOneWidget); - - await tester.tap(find.byKey(const Key('AcknowledgementCheckbox'))); - await tester.pumpAndSettle(); - - await tester.enterText( - find.byKey(const Key('AutocompleteAssistant')), 'John'); - await tester.pumpAndSettle(); - await tester.tap(find.text('John Doe').last); - await tester.pumpAndSettle(); - - expect(find.byType(Card), findsNWidgets(4)); - expect(find.byType(FeedbackQuestionFormField), findsNWidgets(4)); - expect( - find.text( - 'Estimate the average number of hours per week devoted to solving homework.'), - findsOneWidget); - expect( - find.text( - 'Approximate number of activities that you attended (lectures + applications):'), - findsOneWidget); - expect( - find.text('Was the exposure method appropriate?'), findsOneWidget); - expect(find.text('What are the positive aspects of this class?'), - findsOneWidget); - - await tester.drag( - find.byKey(const Key('FeedbackSlider')), const Offset(2, 0)); - await tester.pumpAndSettle(); - - await tester.tap(find.byIcon(Icons.sentiment_very_satisfied)); - await tester.pumpAndSettle(); - - await tester.enterText( - find.byKey(const Key('FeedbackText')), 'Best class ever!'); - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(const Key('FeedbackDropdown'))); - await tester.pumpAndSettle(); - await tester.tap(find.text('option 3').last); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Send')); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - expect(find.text('You need to select your assistant for this class.'), - findsNothing); - expect(find.text('Answer cannot be empty.'), findsNothing); - - expect(find.byType(ClassView), findsOneWidget); - }); - } - }); - - group('Settings', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open settings - await tester.tap(find.byIcon(Icons.settings_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(SettingsPage), findsOneWidget); - }); - } - }); - - group('Portal', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open portal - await tester.tap(find.byIcon(FeatherIcons.globe)); - await tester.pumpAndSettle(); - - expect(find.byType(PortalPage), findsOneWidget); - }); - } - }); - - group('Filter', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open filter popup menu - await tester.tap(find.byIcon(FeatherIcons.globe)); - await tester.pumpAndSettle(); - await tester.tap(find.byIcon(FeatherIcons.filter)); - await tester.pumpAndSettle(); - - // Open filter on portal page - await tester.tap(find.text('Filter by relevance')); - await tester.pumpAndSettle(); - - expect(find.byType(FilterPage), findsOneWidget); - }); - } - }); - - group('Add website', () { - setUp(() { - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open portal page - await tester.tap(find.byIcon(FeatherIcons.globe)); - await tester.pumpAndSettle(); - - // Open add website page - final addWebsiteButton = - find.byKey(const ValueKey('add_website_associations')); - await tester.ensureVisible(addWebsiteButton); - await tester.pumpAndSettle(); - - await tester.tap(addWebsiteButton); - await tester.pumpAndSettle(); - - expect(find.byType(WebsiteView), findsOneWidget); - }); - } - }); - - group('Edit website', () { - setUp(() { - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( - uid: '1', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open portal page - await tester.tap(find.byIcon(FeatherIcons.globe)); - await tester.pumpAndSettle(); - - // Enable editing - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - // Open edit website page - await tester.ensureVisible(find.text('LSAC1')); - await tester.pumpAndSettle(); - - await tester.tap(find.text('LSAC1')); - await tester.pumpAndSettle(); - - expect(find.byType(WebsiteView), findsOneWidget); - }); - } - }); - - group('Delete website', () { - setUp(() { - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( - uid: '1', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open portal page - await tester.tap(find.byIcon(FeatherIcons.globe)); - await tester.pumpAndSettle(); - - // Enable editing - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - // Open edit website page - await tester.ensureVisible(find.text('LSAC1')); - await tester.pumpAndSettle(); - - await tester.tap(find.text('LSAC1')); - await tester.pumpAndSettle(); - - // Open delete dialog - await tester.tap(find.byIcon(Icons.more_vert_outlined)); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Delete website')); - await tester.pumpAndSettle(); - - // Cancel - expect(find.text('Are you sure you want to delete this website?'), - findsOneWidget); - await tester.tap(find.text('CANCEL')); - await tester.pumpAndSettle(); - - // Open delete dialog - await tester.tap(find.byIcon(Icons.more_vert_outlined)); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Delete website')); - await tester.pumpAndSettle(); - - // Confirm deletion - expect(find.text('Are you sure you want to delete this website?'), - findsOneWidget); - await tester.tap(find.text('DELETE WEBSITE')); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - verify(mockWebsiteProvider.deleteWebsite(any)); - expect(find.byType(PortalPage), findsOneWidget); - }); - } - }); - group('Edit Profile', () { - setUp(() { - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(false)); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( - uid: '1', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); - when(mockAuthProvider.currentUserFromCache).thenReturn(User( - uid: '1', firstName: 'John', lastName: 'Doe', permissionLevel: 3)); - when(mockAuthProvider.email).thenReturn('john.doe@stud.acs.upb.ro'); - when(mockAuthProvider.getProfilePictureURL()) - .thenAnswer((_) => Future.value(null)); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open Edit Profile page - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(EditProfilePage), findsOneWidget); - }); - - testWidgets('${size.width}x${size.height}, delete account', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open Edit Profile page - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - //Open delete account popup - await tester.tap(find.byIcon(Icons.more_vert_outlined)); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Delete account')); - await tester.pumpAndSettle(); - - expect(find.byKey(const ValueKey('delete_account_button')), - findsOneWidget); - }); - - testWidgets('${size.width}x${size.height}, change password', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open Edit Profile page - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - //Open change password popup - await tester.tap(find.byIcon(Icons.more_vert_outlined)); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Change password')); - await tester.pumpAndSettle(); - - expect(find.byKey(const ValueKey('change_password_button')), - findsOneWidget); - }); - - testWidgets('${size.width}x${size.height}, change email', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open Edit Profile page - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - // Edit the email - await tester.enterText( - find.text('john.doe'), 'johndoe@stud.acs.upb.ro'); - - //Open change email popup - await tester.tap(find.text('Save')); - await tester.pumpAndSettle(); - - expect( - find.byKey(const ValueKey('change_email_button')), findsOneWidget); - }); - } - }); - - group('People page', () { - setUp(() { - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(true); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await mockNetworkImagesFor(() async { - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open people page - await tester.tap(find.byIcon(Icons.people_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(PeoplePage), findsOneWidget); - - // Open bottom sheet with person info - final names = ['John Doe', 'Jane Doe', 'Mary Poppins']; - for (final name in names) { - await tester.tap(find.text(name)); - await tester.pumpAndSettle(); - } - - expect(find.byType(PersonView), findsOneWidget); - }); - }); - } - }); - - group('Show faq page', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - final showMoreFaq = - find.byKey(const ValueKey('show_more_faq'), skipOffstage: false); - - // Ensure FAQ card is visible - await tester.ensureVisible(showMoreFaq); - await tester.pumpAndSettle(); - - // Open faq page - await tester.tap(showMoreFaq); - await tester.pumpAndSettle(); - - expect(find.byType(FaqPage), findsOneWidget); - - await tester.tap(find.byIcon(Icons.search_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(SearchBar), findsOneWidget); - - final cancelSearchBar = find.byKey(const ValueKey('cancel_search_bar')); - - await tester.tap(cancelSearchBar); - await tester.pumpAndSettle(); - - expect(find.byType(SearchBar), findsNothing); - }); - } - }); - - group('Show news feed page', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open news feed page - final showMoreNewsFeed = - find.byKey(const ValueKey('show_more_news_feed')); - - await tester.tap(showMoreNewsFeed); - await tester.pumpAndSettle(); - - expect(find.byType(NewsFeedPage), findsOneWidget); - }); - } - }); -} +// import 'package:acs_upb_mobile/authentication/model/user.dart'; +// import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; +// import 'package:acs_upb_mobile/authentication/view/edit_profile_page.dart'; +// import 'package:acs_upb_mobile/main.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_dropdown.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_rating.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_slider.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_text.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/view/class_feedback_view.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/view/feedback_question.dart'; +// import 'package:acs_upb_mobile/pages/classes/model/class.dart'; +// import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; +// import 'package:acs_upb_mobile/pages/classes/view/class_view.dart'; +// import 'package:acs_upb_mobile/pages/classes/view/classes_page.dart'; +// import 'package:acs_upb_mobile/pages/classes/view/grading_view.dart'; +// import 'package:acs_upb_mobile/pages/classes/view/shortcut_view.dart'; +// import 'package:acs_upb_mobile/pages/faq/model/question.dart'; +// import 'package:acs_upb_mobile/pages/faq/service/question_provider.dart'; +// import 'package:acs_upb_mobile/pages/faq/view/faq_page.dart'; +// import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; +// import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; +// import 'package:acs_upb_mobile/pages/filter/view/filter_page.dart'; +// import 'package:acs_upb_mobile/pages/home/home_page.dart'; +// import 'package:acs_upb_mobile/pages/news_feed/model/news_feed_item.dart'; +// import 'package:acs_upb_mobile/pages/news_feed/service/news_provider.dart'; +// import 'package:acs_upb_mobile/pages/news_feed/view/news_feed_page.dart'; +// import 'package:acs_upb_mobile/pages/people/model/person.dart'; +// import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; +// import 'package:acs_upb_mobile/pages/people/view/people_page.dart'; +// import 'package:acs_upb_mobile/pages/people/view/person_view.dart'; +// import 'package:acs_upb_mobile/pages/portal/model/website.dart'; +// import 'package:acs_upb_mobile/pages/portal/service/website_provider.dart'; +// import 'package:acs_upb_mobile/pages/portal/view/portal_page.dart'; +// import 'package:acs_upb_mobile/pages/portal/view/website_view.dart'; +// import 'package:acs_upb_mobile/pages/settings/service/request_provider.dart'; +// import 'package:acs_upb_mobile/pages/settings/view/request_permissions.dart'; +// import 'package:acs_upb_mobile/pages/settings/view/settings_page.dart'; +// import 'package:acs_upb_mobile/pages/timetable/model/academic_calendar.dart'; +// import 'package:acs_upb_mobile/pages/timetable/model/events/all_day_event.dart'; +// import 'package:acs_upb_mobile/pages/timetable/model/events/class_event.dart'; +// import 'package:acs_upb_mobile/pages/timetable/model/events/recurring_event.dart'; +// import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; +// import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; +// import 'package:acs_upb_mobile/pages/timetable/view/events/add_event_view.dart'; +// import 'package:acs_upb_mobile/pages/timetable/view/events/event_view.dart'; +// import 'package:acs_upb_mobile/pages/timetable/view/timetable_page.dart'; +// import 'package:acs_upb_mobile/resources/locale_provider.dart'; +// import 'package:acs_upb_mobile/resources/remote_config.dart'; +// import 'package:acs_upb_mobile/resources/utils.dart'; +// import 'package:acs_upb_mobile/widgets/search_bar.dart'; +// import 'package:firebase_core/firebase_core.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:mockito/mockito.dart'; +// import 'package:network_image_mock/network_image_mock.dart'; +// import 'package:package_info_plus/package_info_plus.dart'; +// import 'package:preferences/preferences.dart'; +// import 'package:provider/provider.dart'; +// import 'package:rrule/rrule.dart'; +// import 'package:time_machine/time_machine.dart' hide Offset; +// import 'package:timetable/src/header/week_indicator.dart'; +// +// import 'firebase_mock.dart'; +// import 'test_utils.dart'; +// +// // These tests open each page in the app on multiple screen sizes to make sure +// // nothing overflows/breaks. +// +// class MockAuthProvider extends Mock implements AuthProvider {} +// +// class MockWebsiteProvider extends Mock implements WebsiteProvider {} +// +// class MockFilterProvider extends Mock implements FilterProvider {} +// +// class MockClassProvider extends Mock implements ClassProvider {} +// +// class MockPersonProvider extends Mock implements PersonProvider {} +// +// class MockQuestionProvider extends Mock implements QuestionProvider {} +// +// class MockUniEventProvider extends Mock implements UniEventProvider {} +// +// class MockNewsProvider extends Mock implements NewsProvider {} +// +// class MockRequestProvider extends Mock implements RequestProvider {} +// +// class MockNavigatorObserver extends Mock implements NavigatorObserver {} +// +// class MockFeedbackProvider extends Mock implements FeedbackProvider {} +// +// Future main() async { +// AuthProvider mockAuthProvider; +// WebsiteProvider mockWebsiteProvider; +// FilterProvider mockFilterProvider; +// ClassProvider mockClassProvider; +// PersonProvider mockPersonProvider; +// MockQuestionProvider mockQuestionProvider; +// MockNewsProvider mockNewsProvider; +// UniEventProvider mockEventProvider; +// RequestProvider mockRequestProvider; +// FeedbackProvider mockFeedbackProvider; +// +// setupFirebaseAuthMocks(); +// await Firebase.initializeApp(); +// +// // Test layout for different screen sizes +// // TODO(AdrianMargineanu): Use Flutter driver for integration tests, setting screen sizes here isn't reliable +// final screenSizes = [ +// // Phone +// const Size(720, 1280), +// // Tablet +// const Size(600, 1024), +// ]; +// +// // Add landscape mode sizes +// screenSizes.addAll(List.from(screenSizes) +// .map((size) => Size(size.height, size.width))); +// +// final TestWidgetsFlutterBinding binding = +// TestWidgetsFlutterBinding.ensureInitialized(); +// +// Widget buildApp() => MultiProvider( +// providers: [ +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ChangeNotifierProvider( +// create: (_) => mockWebsiteProvider), +// ChangeNotifierProvider( +// create: (_) => mockFilterProvider), +// ChangeNotifierProvider( +// create: (_) => mockClassProvider), +// ChangeNotifierProvider( +// create: (_) => mockPersonProvider), +// ChangeNotifierProvider( +// create: (_) => mockQuestionProvider), +// ChangeNotifierProvider(create: (_) => mockNewsProvider), +// ChangeNotifierProvider( +// create: (_) => mockEventProvider), +// Provider(create: (_) => mockRequestProvider), +// ChangeNotifierProvider( +// create: (_) => mockFeedbackProvider), +// ], +// child: const MyApp(), +// ); +// +// setUp(() async { +// WidgetsFlutterBinding.ensureInitialized(); +// PrefService.enableCaching(); +// PrefService.cache = {}; +// PrefService.setString('language', 'en'); +// +// await Firebase.initializeApp(); +// +// LocaleProvider.cultures = testCultures; +// LocaleProvider.rruleL10ns = {'en': await RruleL10nTest.create()}; +// +// Utils.packageInfo = PackageInfo( +// version: '1.2.7', +// buildNumber: '6', +// appName: 'ACS UPB Mobile', +// packageName: 'ro.upb.acs_upb_mobile', +// ); +// +// // Pretend an anonymous user is already logged in +// mockAuthProvider = MockAuthProvider(); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// // ignore: invalid_use_of_protected_member +// when(mockAuthProvider.hasListeners).thenReturn(false); +// when(mockAuthProvider.isAnonymous).thenReturn(true); +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(null)); +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(false)); +// +// mockWebsiteProvider = MockWebsiteProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockWebsiteProvider.hasListeners).thenReturn(false); +// when(mockWebsiteProvider.deleteWebsite(any)) +// .thenAnswer((_) => Future.value(true)); +// when(mockAuthProvider.getProfilePictureURL()) +// .thenAnswer((_) => Future.value(null)); +// when(mockWebsiteProvider.fetchWebsites(any)) +// .thenAnswer((_) => Future.value([ +// Website( +// id: '1', +// relevance: null, +// category: WebsiteCategory.learning, +// infoByLocale: {'en': 'info-en', 'ro': 'info-ro'}, +// label: 'Moodle1', +// link: 'http://acs.curs.pub.ro/', +// isPrivate: false, +// ), +// Website( +// id: '2', +// relevance: null, +// category: WebsiteCategory.learning, +// infoByLocale: {}, +// label: 'OCW1', +// link: 'https://ocw.cs.pub.ro/', +// isPrivate: false, +// ), +// Website( +// id: '3', +// relevance: null, +// category: WebsiteCategory.learning, +// infoByLocale: {'en': 'info-en', 'ro': 'info-ro'}, +// label: 'Moodle2', +// link: 'http://acs.curs.pub.ro/', +// isPrivate: false, +// ), +// Website( +// id: '4', +// relevance: null, +// category: WebsiteCategory.learning, +// infoByLocale: {}, +// label: 'OCW2', +// link: 'https://ocw.cs.pub.ro/', +// isPrivate: false, +// ), +// Website( +// id: '5', +// relevance: null, +// category: WebsiteCategory.association, +// infoByLocale: {}, +// label: 'LSAC1', +// link: 'https://lsacbucuresti.ro/', +// isPrivate: false, +// ), +// Website( +// id: '6', +// relevance: null, +// category: WebsiteCategory.administrative, +// infoByLocale: {}, +// label: 'LSAC2', +// link: 'https://lsacbucuresti.ro/', +// isPrivate: false, +// ), +// Website( +// id: '7', +// relevance: null, +// category: WebsiteCategory.resource, +// infoByLocale: {}, +// label: 'LSAC3', +// link: 'https://lsacbucuresti.ro/', +// isPrivate: false, +// ), +// Website( +// id: '8', +// relevance: null, +// category: WebsiteCategory.other, +// infoByLocale: {}, +// label: 'LSAC4', +// link: 'https://lsacbucuresti.ro/', +// isPrivate: false, +// ), +// ])); +// when(mockWebsiteProvider.fetchFavouriteWebsites(any)).thenAnswer( +// (_) async => (await mockWebsiteProvider.fetchWebsites(any)).take(3)); +// +// mockFilterProvider = MockFilterProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockFilterProvider.hasListeners).thenReturn(false); +// when(mockFilterProvider.filterEnabled).thenReturn(true); +// final filter = Filter( +// localizedLevelNames: [ +// {'en': 'Degree', 'ro': 'Nivel de studiu'}, +// {'en': 'Major', 'ro': 'Specializare'}, +// {'en': 'Year', 'ro': 'An'}, +// {'en': 'Series', 'ro': 'Serie'}, +// {'en': 'Group', 'ro': 'Group'} +// ], +// root: FilterNode(name: 'All', value: true, children: [ +// FilterNode(name: 'BSc', value: true, children: [ +// FilterNode(name: 'CTI', value: true, children: [ +// FilterNode(name: 'CTI-1', value: true, children: [ +// FilterNode(name: '1-CA'), +// FilterNode( +// name: '1-CB', +// value: true, +// children: [ +// FilterNode(name: '311CB'), +// FilterNode(name: '312CB'), +// FilterNode(name: '313CB'), +// FilterNode( +// name: '314CB', +// value: true, +// ), +// ], +// ), +// FilterNode(name: '1-CC'), +// FilterNode( +// name: '1-CD', +// children: [ +// FilterNode(name: '311CD'), +// FilterNode(name: '312CD'), +// FilterNode(name: '313CD'), +// FilterNode(name: '314CD'), +// ], +// ), +// ]), +// FilterNode( +// name: 'CTI-2', +// ), +// FilterNode( +// name: 'CTI-3', +// ), +// FilterNode( +// name: 'CTI-4', +// ), +// ]), +// FilterNode(name: 'IS') +// ]), +// FilterNode(name: 'MSc', children: [ +// FilterNode( +// name: 'IA', +// ), +// FilterNode(name: 'SPRC'), +// ]) +// ])); +// when(mockFilterProvider.cachedFilter).thenReturn(filter); +// when(mockFilterProvider.fetchFilter()) +// .thenAnswer((_) => Future.value(filter)); +// +// mockClassProvider = MockClassProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockClassProvider.hasListeners).thenReturn(false); +// final userClassHeaders = [ +// ClassHeader( +// id: '3', +// name: 'Programming', +// acronym: 'PC', +// category: 'A', +// ), +// ClassHeader( +// id: '4', +// name: 'Physics', +// acronym: 'PH', +// category: 'D', +// ) +// ]; +// when(mockClassProvider.userClassHeadersCache).thenReturn(userClassHeaders); +// when(mockClassProvider.fetchClassHeaders(uid: anyNamed('uid'))) +// .thenAnswer((_) => Future.value([ +// ClassHeader( +// id: '1', +// name: 'Maths 1', +// acronym: 'M1', +// category: 'A/B', +// ), +// ClassHeader( +// id: '2', +// name: 'Maths 2', +// acronym: 'M2', +// category: 'A/C', +// ), +// ] + +// userClassHeaders)); +// when(mockClassProvider.fetchUserClassIds(any)) +// .thenAnswer((_) => Future.value(['3', '4'])); +// when(mockClassProvider.fetchClassInfo(any)).thenAnswer((_) => Future.value( +// Class( +// header: ClassHeader( +// id: '3', +// name: 'Programming', +// acronym: 'PC', +// category: 'A', +// ), +// shortcuts: [ +// Shortcut( +// type: ShortcutType.main, +// name: 'OCW', +// link: 'https://ocw.cs.pub.ro/courses/programare'), +// Shortcut( +// type: ShortcutType.other, +// name: 'Google', +// link: 'https://google.com'), +// ], +// grading: { +// 'Exam': 4, +// 'Lab': 1.5, +// 'Homework': 4, +// 'Extra homework': 0.5, +// }, +// ), +// )); +// +// RemoteConfigService.overrides = {'feedback_enabled': true}; +// +// mockPersonProvider = MockPersonProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockPersonProvider.hasListeners).thenReturn(false); +// when(mockPersonProvider.fetchPeople()).thenAnswer((_) => Future.value([ +// Person( +// name: 'John Doe', +// email: 'john.doe@cs.pub.ro', +// phone: '0712345678', +// office: 'AB123', +// position: 'Associate Professor, Dr., Department Council', +// photo: 'https://cdn.worldvectorlogo.com/logos/flutter-logo.svg', +// ), +// Person( +// name: 'Jane Doe', +// email: 'jane.doe@cs.pub.ro', +// phone: '-', +// office: 'Narnia', +// position: 'Professor, Dr.', +// photo: 'https://cdn.worldvectorlogo.com/logos/flutter-logo.svg', +// ), +// Person( +// name: 'Mary Poppins', +// email: 'supercalifragilistic.expialidocious@cs.pub.ro', +// phone: '0712-345-678', +// office: 'Mary Poppins\' office', +// position: 'Professor, Dr., Head of Department', +// photo: 'https://cdn.worldvectorlogo.com/logos/flutter-logo.svg', +// ), +// ])); +// +// when(mockPersonProvider.mostRecentLecturer(any)) +// .thenAnswer((_) => Future.value('Jane Doe')); +// +// mockFeedbackProvider = MockFeedbackProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockFeedbackProvider.hasListeners).thenReturn(true); +// when(mockFeedbackProvider.fetchQuestions()).thenAnswer((_) => Future.value({ +// '0': FeedbackQuestionDropdown( +// category: 'involvement', +// question: +// 'Approximate number of activities that you attended (lectures + applications):', +// id: '0', +// answerOptions: ['option 1', 'option 2', 'option 3', 'option 4'], +// ), +// '1': FeedbackQuestionRating( +// category: 'applications', +// question: 'Was the exposure method appropriate?', +// id: '1', +// ), +// '2': FeedbackQuestionText( +// category: 'personal', +// question: 'What are the positive aspects of this class?', +// id: '2', +// ), +// '3': FeedbackQuestionSlider( +// category: 'homework', +// question: +// 'Estimate the average number of hours per week devoted to solving homework.', +// id: '3', +// ), +// })); +// when(mockFeedbackProvider.fetchCategories()) +// .thenAnswer((_) => Future.value({ +// 'applications': {'en': 'Applications', 'ro': 'Aplicații'}, +// 'homework': {'en': 'Homework', 'ro': 'Temă'}, +// 'involvement': {'en': 'Involvement', 'ro': 'Implicare'}, +// 'personal': { +// 'en': 'Personal comments', +// 'ro': 'Comentarii personale' +// }, +// })); +// +// when(mockFeedbackProvider.userSubmittedFeedbackForClass(any, any)) +// .thenAnswer((_) => Future.value(false)); +// when(mockFeedbackProvider.submitFeedback(any, any, any, any, any)) +// .thenAnswer((_) => Future.value(true)); +// when(mockFeedbackProvider.getClassesWithCompletedFeedback(any)) +// .thenAnswer((_) => Future.value({'M1': true, 'M2': true})); +// when(mockFeedbackProvider.countClassesWithoutFeedback(any, any)) +// .thenAnswer((_) => Future.value('2')); +// +// mockQuestionProvider = MockQuestionProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockQuestionProvider.hasListeners).thenReturn(false); +// when(mockQuestionProvider.fetchQuestions()) +// .thenAnswer((_) => Future.value([ +// Question( +// question: 'Care este programul la secretariat?', +// answer: +// 'Secretariatul este deschis în timpul săptămânii între orele 9:00 si 11:00.', +// tags: ['Licență']), +// Question( +// question: 'Cum mă conectez la eduroam?', +// answer: +// 'Conectarea în rețeaua *eduroam* se face pe baza aceluiași cont folosit și pe site-ul de cursuri.', +// tags: ['Conectare', 'Informații']) +// ])); +// when(mockQuestionProvider.fetchQuestions(limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([ +// Question( +// question: 'Care este programul la secretariat?', +// answer: +// 'Secretariatul este deschis în timpul săptămânii între orele 9:00 si 11:00.', +// tags: ['Licență']), +// Question( +// question: 'Cum mă conectez la eduroam?', +// answer: +// 'Conectarea în rețeaua *eduroam* se face pe baza aceluiași cont folosit și pe site-ul de cursuri.', +// tags: ['Conectare', 'Informații']) +// ])); +// +// mockNewsProvider = MockNewsProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockNewsProvider.hasListeners).thenReturn(false); +// when(mockNewsProvider.fetchNewsFeedItems()) +// .thenAnswer((_) => Future.value([ +// NewsFeedItem( +// '03.10.2020', +// 'Cazarea studentilor de anul II licenta', +// 'https://acs.pub.ro/noutati/cazarea-studentilor-de-anul-ii-licenta/'), +// NewsFeedItem( +// '03.10.2020', +// 'Festivitatea de deschidere a anului universitar 2020-2021', +// 'https://acs.pub.ro/noutati/festivitatea-de-deschidere-a-anului-universitar-2020-2021/') +// ])); +// when(mockNewsProvider.fetchNewsFeedItems(limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([ +// NewsFeedItem( +// '03.10.2020', +// 'Cazarea studentilor de anul II licenta', +// 'https://acs.pub.ro/noutati/cazarea-studentilor-de-anul-ii-licenta/'), +// NewsFeedItem( +// '03.10.2020', +// 'Festivitatea de deschidere a anului universitar 2020-2021', +// 'https://acs.pub.ro/noutati/festivitatea-de-deschidere-a-anului-universitar-2020-2021/') +// ])); +// +// mockEventProvider = MockUniEventProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockEventProvider.hasListeners).thenReturn(false); +// final now = LocalDate.today(); +// final weekStart = now.subtractDays(now.dayOfWeek - DayOfWeek.monday); +// final holidays = [ +// // Holiday on Tuesday and Wednesday next week +// AllDayUniEvent( +// name: 'Holiday', +// start: weekStart.addWeeks(1).addDays(1), +// end: weekStart.addWeeks(1).addDays(2), +// id: 'holiday0', +// ), +// AllDayUniEvent( +// name: 'Inter-semester holiday', +// start: weekStart.addWeeks(2).subtractDays(2), +// end: weekStart.addWeeks(3).subtractDays(1), +// id: 'holiday1', +// ), +// ]; +// final calendar = AcademicCalendar( +// id: '2020', +// semesters: [ +// AllDayUniEvent( +// start: weekStart, +// end: weekStart.addWeeks(2).subtractDays(3), +// id: 'semester1', +// ), +// AllDayUniEvent( +// start: weekStart.addWeeks(3), +// end: weekStart.addWeeks(5).subtractDays(3), +// id: 'semester2', +// ), +// ], +// holidays: holidays, +// ); +// when(mockEventProvider.fetchCalendars()) +// .thenAnswer((_) => Future.value({'2020': calendar})); +// when(mockEventProvider.getUpcomingEvents(LocalDate.today())) +// .thenAnswer((_) => Future.value([])); +// when(mockEventProvider.getUpcomingEvents(LocalDate.today(), +// limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([])); +// when(mockEventProvider.getAllEventsOfClass(any)) +// .thenAnswer((_) => Future.value([])); +// final rruleEveryWeekFirstSem = RecurrenceRule( +// frequency: Frequency.weekly, +// interval: 1, +// until: weekStart.addWeeks(2).subtractDays(3).atMidnight(), +// ); +// final rruleEveryTwoWeeksFirstSem = RecurrenceRule( +// frequency: Frequency.weekly, +// interval: 2, +// until: weekStart.addWeeks(2).subtractDays(3).atMidnight(), +// ); +// final rruleEveryWeek = RecurrenceRule( +// frequency: Frequency.weekly, +// interval: 1, +// ); +// final rruleEveryTwoWeeks = RecurrenceRule( +// frequency: Frequency.weekly, +// interval: 2, +// ); +// const duration = Period(hours: 2); +// final events = [ +// RecurringUniEvent( +// name: 'M1', +// calendar: calendar, +// rrule: rruleEveryWeekFirstSem, +// start: weekStart.at(LocalTime(8, 0, 0)), +// period: duration, +// id: '0', +// ), +// RecurringUniEvent( +// name: 'M2', +// calendar: calendar, +// rrule: rruleEveryTwoWeeksFirstSem, +// start: weekStart.at(LocalTime(10, 0, 0)), +// period: duration, +// id: '1', +// ), +// RecurringUniEvent( +// name: 'T1', +// calendar: calendar, +// rrule: rruleEveryWeekFirstSem, +// start: weekStart.addDays(1).at(LocalTime(8, 0, 0)), +// period: duration, +// id: '2', +// ), +// RecurringUniEvent( +// name: 'T2', +// calendar: calendar, +// rrule: rruleEveryTwoWeeksFirstSem, +// start: weekStart.addDays(1).at(LocalTime(9, 0, 0)), +// period: duration, +// id: '3', +// ), +// RecurringUniEvent( +// name: 'W1', +// calendar: calendar, +// rrule: rruleEveryWeekFirstSem, +// start: weekStart.addDays(2).at(LocalTime(8, 0, 0)), +// period: duration, +// id: '4', +// ), +// RecurringUniEvent( +// name: 'W2', +// rrule: rruleEveryWeek, +// start: weekStart.addDays(2).at(LocalTime(10, 0, 0)), +// period: duration, +// id: '5', +// ), +// RecurringUniEvent( +// name: 'W3', +// rrule: rruleEveryTwoWeeks, +// start: weekStart.addDays(2).at(LocalTime(12, 0, 0)), +// period: duration, +// id: '6', +// ), +// ClassEvent( +// classHeader: userClassHeaders[0], +// type: UniEventType.lecture, +// teacher: Person(name: 'Jane Doe'), +// location: 'AB123', +// degree: 'BSc', +// relevance: ['314CB'], +// calendar: calendar, +// rrule: rruleEveryWeek, +// start: weekStart.addDays(3).at(LocalTime(10, 0, 0)), +// period: duration, +// id: '7', +// ), +// RecurringUniEvent( +// classHeader: userClassHeaders[1], +// type: UniEventType.lecture, +// location: 'AB123', +// degree: 'BSc', +// relevance: ['314CB'], +// calendar: calendar, +// rrule: rruleEveryTwoWeeks, +// start: weekStart.addDays(3).at(LocalTime(12, 0, 0)), +// period: duration, +// id: '8', +// ), +// RecurringUniEvent( +// name: 'F1', +// calendar: calendar, +// rrule: rruleEveryWeek, +// start: weekStart.addDays(4).at(LocalTime(10, 0, 0)), +// period: duration, +// id: '9', +// ), +// RecurringUniEvent( +// name: 'F2', +// calendar: calendar, +// rrule: rruleEveryTwoWeeks, +// start: weekStart.addDays(4).at(LocalTime(12, 0, 0)), +// period: duration, +// id: '10', +// ), +// ]; +// when(mockEventProvider.getAllDayEventsIntersecting(any)) +// .thenAnswer((invocation) { +// final DateInterval interval = invocation.positionalArguments[0]; +// return Stream.value(holidays +// .map((holiday) => +// holiday.generateInstances(intersectingInterval: interval)) +// .expand((e) => e)); +// }); +// when(mockEventProvider.getPartDayEventsIntersecting(any)) +// .thenAnswer((invocation) { +// final LocalDate date = invocation.positionalArguments[0]; +// return Stream.value(events +// .map((event) => event.generateInstances( +// intersectingInterval: DateInterval(date, date))) +// .expand((e) => e)); +// }); +// when(mockEventProvider.empty).thenReturn(false); +// when(mockEventProvider.deleteEvent(any)) +// .thenAnswer((_) => Future.value(true)); +// when(mockEventProvider.updateEvent(any)) +// .thenAnswer((_) => Future.value(true)); +// when(mockEventProvider.addEvent(any)).thenAnswer((_) => Future.value(true)); +// +// mockRequestProvider = MockRequestProvider(); +// when(mockRequestProvider.makeRequest(any)) +// .thenAnswer((_) => Future.value(true)); +// when(mockRequestProvider.userAlreadyRequested(any)) +// .thenAnswer((_) => Future.value(false)); +// }); +// +// group('Home', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// expect(find.byType(HomePage), findsOneWidget); +// +// // Open home +// await tester.tap(find.byIcon(Icons.home)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(HomePage), findsOneWidget); +// }); +// } +// }); +// +// group('Timetable', () { +// setUp(() { +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( +// uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); +// when(mockAuthProvider.currentUserFromCache).thenReturn(User( +// uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3)); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.uid).thenReturn('0'); +// }); +// +// group('Timetable no events/no classes', () { +// setUp(() { +// when(mockEventProvider.getAllDayEventsIntersecting(any)) +// .thenAnswer((_) => Stream.value([])); +// when(mockEventProvider.getPartDayEventsIntersecting(any)) +// .thenAnswer((_) => Stream.value([])); +// when(mockEventProvider.empty).thenReturn(true); +// +// when(mockClassProvider.userClassHeadersCache).thenReturn(null); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(TimetablePage), findsOneWidget); +// expect(find.text('No events to show'), findsOneWidget); +// +// await tester.tap(find.text('CHOOSE CLASSES')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AddClassesPage), findsOneWidget); +// }); +// } +// }); +// +// group('Timetable no events/no filter', () { +// setUp(() { +// when(mockEventProvider.getAllDayEventsIntersecting(any)) +// .thenAnswer((_) => Stream.value([])); +// when(mockEventProvider.getPartDayEventsIntersecting(any)) +// .thenAnswer((_) => Stream.value([])); +// when(mockEventProvider.empty).thenReturn(true); +// +// when(mockFilterProvider.cachedFilter).thenReturn(null); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(TimetablePage), findsOneWidget); +// expect(find.text('No events to show'), findsOneWidget); +// +// await tester.tap(find.text('OPEN FILTER')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(FilterPage), findsOneWidget); +// }); +// } +// }); +// +// group('Timetable no events/no permissions', () { +// setUp(() { +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( +// uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 0))); +// when(mockAuthProvider.currentUserFromCache).thenReturn(User( +// uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 0)); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); +// +// when(mockEventProvider.getAllDayEventsIntersecting(any)) +// .thenAnswer((_) => Stream.value([])); +// when(mockEventProvider.getPartDayEventsIntersecting(any)) +// .thenAnswer((_) => Stream.value([])); +// when(mockEventProvider.empty).thenReturn(true); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(TimetablePage), findsOneWidget); +// expect(find.text('No events to show'), findsOneWidget); +// +// await tester.tap(find.text('REQUEST PERMISSIONS')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(RequestPermissionsPage), findsOneWidget); +// }); +// } +// }); +// +// group('Timetable no events/add some', () { +// setUp(() { +// when(mockEventProvider.getAllDayEventsIntersecting(any)) +// .thenAnswer((_) => Stream.value([])); +// when(mockEventProvider.getPartDayEventsIntersecting(any)) +// .thenAnswer((_) => Stream.value([])); +// when(mockEventProvider.empty).thenReturn(true); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(TimetablePage), findsOneWidget); +// expect(find.text('No events to show'), findsOneWidget); +// expect( +// find.byKey(const ValueKey('no_events_message')), findsOneWidget); +// +// await tester.tap(find.text('CANCEL')); +// await tester.pumpAndSettle(); +// +// expect(find.text('No events to show'), findsNothing); +// }); +// } +// }); +// +// group('Timetable events', () { +// for (final size in screenSizes) { +// if (size.width > size.height) { +// // TODO(IoanaAlexandru): In landscape mode the test fails in a weird +// // way - it seems as if two weeks are visible at the same time, but +// // the behaviour cannot be reproduced on a device. Skipping this +// // test in landscape mode for now. +// continue; +// } +// +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(TimetablePage), findsOneWidget); +// +// // Scroll to previous week +// await tester.drag(find.text('Tue'), Offset(size.width - 30, 0)); +// await tester.pumpAndSettle(); +// +// // Expect previous week +// final previousWeek = WeekYearRules.iso +// .getWeekOfWeekYear(LocalDate.today().subtractWeeks(1)); +// expect( +// find.byWidgetPredicate((widget) => +// widget is WeekIndicator && +// widget.week.toString() == previousWeek.toString()), +// findsOneWidget); +// +// expect(find.text('Holiday'), findsNothing); +// expect(find.text('Inter-semester holiday'), findsNothing); +// expect(find.text('M1'), findsNothing); +// expect(find.text('M2'), findsNothing); +// expect(find.text('T1'), findsNothing); +// expect(find.text('T2'), findsNothing); +// expect(find.text('W1'), findsNothing); +// expect(find.text('W2'), findsNothing); +// expect(find.text('W3'), findsNothing); +// expect(find.text('PC'), findsNothing); +// expect(find.text('PH'), findsNothing); +// expect(find.text('F1'), findsNothing); +// expect(find.text('F2'), findsNothing); +// +// // Scroll back to current week +// await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); +// await tester.pumpAndSettle(); +// +// // Expect current week +// final currentWeek = +// WeekYearRules.iso.getWeekOfWeekYear(LocalDate.today()); +// expect( +// find.byWidgetPredicate((widget) => +// widget is WeekIndicator && +// widget.week.toString() == currentWeek.toString()), +// findsOneWidget); +// +// expect(find.text('Holiday'), findsNothing); +// expect(find.text('Inter-semester holiday'), findsNothing); +// expect(find.text('M1'), findsOneWidget); +// expect(find.text('M2'), findsOneWidget); +// expect(find.text('T1'), findsOneWidget); +// expect(find.text('T2'), findsOneWidget); +// expect(find.text('W1'), findsOneWidget); +// expect(find.text('W2'), findsOneWidget); +// expect(find.text('W3'), findsOneWidget); +// expect(find.text('PC'), findsOneWidget); +// expect(find.text('PH'), findsOneWidget); +// expect(find.text('F1'), findsOneWidget); +// expect(find.text('F2'), findsOneWidget); +// +// // Scroll to next week +// await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); +// await tester.pumpAndSettle(); +// +// // Expect next week +// final nextWeek = WeekYearRules.iso +// .getWeekOfWeekYear(LocalDate.today().addWeeks(1)); +// expect( +// find.byWidgetPredicate((widget) => +// widget is WeekIndicator && +// widget.week.toString() == nextWeek.toString()), +// findsOneWidget); +// +// expect(find.text('Holiday'), findsOneWidget); +// expect(find.text('Inter-semester holiday'), findsOneWidget); +// expect(find.text('M1'), findsOneWidget); +// expect(find.text('M2'), findsNothing); +// expect(find.text('T1'), findsNothing); +// expect(find.text('T2'), findsNothing); +// expect(find.text('W1'), findsNothing); +// expect(find.text('W2'), findsOneWidget); +// expect(find.text('W3'), findsNothing); +// expect(find.text('PC'), findsOneWidget); +// expect(find.text('PH'), findsNothing); +// expect(find.text('F1'), findsOneWidget); +// expect(find.text('F2'), findsNothing); +// +// // Scroll to next week +// await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); +// await tester.pumpAndSettle(); +// +// // Expect next week +// final nextNextWeek = WeekYearRules.iso +// .getWeekOfWeekYear(LocalDate.today().addWeeks(2)); +// expect( +// find.byWidgetPredicate((widget) => +// widget is WeekIndicator && +// widget.week.toString() == nextNextWeek.toString()), +// findsOneWidget); +// +// expect(find.text('Holiday'), findsNothing); +// expect(find.text('Inter-semester holiday'), findsOneWidget); +// expect(find.text('M1'), findsNothing); +// expect(find.text('M2'), findsNothing); +// expect(find.text('T1'), findsNothing); +// expect(find.text('T2'), findsNothing); +// expect(find.text('W1'), findsNothing); +// expect(find.text('W2'), findsOneWidget); +// expect(find.text('W3'), findsOneWidget); +// expect(find.text('PC'), findsNothing); +// expect(find.text('PH'), findsNothing); +// expect(find.text('F1'), findsNothing); +// expect(find.text('F2'), findsNothing); +// +// // Scroll to next week +// await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); +// await tester.pumpAndSettle(); +// +// // Expect next week +// final nextNextNextWeek = WeekYearRules.iso +// .getWeekOfWeekYear(LocalDate.today().addWeeks(3)); +// expect( +// find.byWidgetPredicate((widget) => +// widget is WeekIndicator && +// widget.week.toString() == nextNextNextWeek.toString()), +// findsOneWidget); +// +// expect(find.text('Holiday'), findsNothing); +// expect(find.text('Inter-semester holiday'), findsNothing); +// expect(find.text('M1'), findsNothing); +// expect(find.text('M2'), findsNothing); +// expect(find.text('T1'), findsNothing); +// expect(find.text('T2'), findsNothing); +// expect(find.text('W1'), findsNothing); +// expect(find.text('W2'), findsOneWidget); +// expect(find.text('W3'), findsNothing); +// expect(find.text('PC'), findsOneWidget); +// expect(find.text('PH'), findsOneWidget); +// expect(find.text('F1'), findsOneWidget); +// expect(find.text('F2'), findsOneWidget); +// +// // Navigate to today +// await tester.tap(find.byIcon(Icons.today_outlined)); +// await tester.pumpAndSettle(); +// +// // Expect current week +// expect( +// find.byWidgetPredicate((widget) => +// widget is WeekIndicator && +// widget.week.toString() == currentWeek.toString()), +// findsOneWidget); +// }); +// } +// }); +// +// group('Event page - open all day event', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// // Expect current week +// final currentWeek = +// WeekYearRules.iso.getWeekOfWeekYear(LocalDate.today()); +// expect( +// find.byWidgetPredicate((widget) => +// widget is WeekIndicator && +// widget.week.toString() == currentWeek.toString()), +// findsOneWidget); +// +// // Scroll to next week +// await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); +// await tester.pumpAndSettle(); +// +// // Expect next week +// final nextWeek = WeekYearRules.iso +// .getWeekOfWeekYear(LocalDate.today().addWeeks(1)); +// expect( +// find.byWidgetPredicate((widget) => +// widget is WeekIndicator && +// widget.week.toString() == nextWeek.toString()), +// findsOneWidget); +// +// // Open holiday event +// await tester.tap(find.text('Holiday')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(EventView), findsOneWidget); +// }); +// } +// }); +// +// group('Event page - add event', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// // Open add event page +// await tester +// .tapAt(tester.getCenter(find.text('Sat')).translate(0, 100)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AddEventView), findsOneWidget); +// +// // Select type +// await tester.tap(find.text('Type')); +// await tester.pumpAndSettle(); +// await tester.tap(find.text('Seminar').last); +// await tester.pumpAndSettle(); +// +// // Select class +// await tester.tap(find.text('Class')); +// await tester.pumpAndSettle(); +// await tester.tap(find.text('Programming').last); +// await tester.pumpAndSettle(); +// +// // Press back +// await tester.tap(find.byIcon(Icons.arrow_back)); +// await tester.pumpAndSettle(); +// +// await tester +// .tapAt(tester.getCenter(find.text('Sat')).translate(0, 100)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AddEventView), findsOneWidget); +// +// // Select type +// await tester.tap(find.text('Type')); +// await tester.pumpAndSettle(); +// await tester.tap(find.text('Lecture').last); +// await tester.pumpAndSettle(); +// +// // Select lecturer - partial name +// await tester.tap(find.byIcon(FeatherIcons.user)); +// await tester.pumpAndSettle(); +// await tester.enterText( +// find.byKey(const Key('AutocompleteLecturer')), 'John'); +// await tester.pumpAndSettle(); +// await tester.tap(find.text('John Doe')); +// await tester.pumpAndSettle(); +// +// // Select lecturer - new name +// await tester.tap(find.byIcon(FeatherIcons.user)); +// await tester.pumpAndSettle(); +// await tester.enterText( +// find.byKey(const Key('AutocompleteLecturer')), 'Isabel Steward'); +// await tester.tap(find.text('Isabel Steward')); +// await tester.pumpAndSettle(); +// +// // Select lecturer - check autocomplete suggestions +// await tester.tap(find.byIcon(FeatherIcons.user)); +// await tester.pumpAndSettle(); +// await tester.enterText( +// find.byKey(const Key('AutocompleteLecturer')), 'Doe'); +// await tester.pumpAndSettle(); +// +// expect(find.text('Jane Doe'), findsOneWidget); +// expect(find.text('John Doe'), findsOneWidget); +// +// await tester.tap(find.text('Jane Doe')); +// await tester.pumpAndSettle(); +// }); +// } +// }); +// +// group('Event page - edit event', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// // Open PC event +// await tester.tap(find.text('PC')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(EventView), findsOneWidget); +// +// // Open class page +// await tester.tap(find.text('Programming')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(ClassView), findsOneWidget); +// expect(find.byIcon(FeatherIcons.user), findsOneWidget); +// expect(find.byKey(const Key('LecturerCard')), findsOneWidget); +// +// // Press back +// await tester.tap(find.byIcon(Icons.arrow_back)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(EventView), findsOneWidget); +// expect(find.byIcon(FeatherIcons.user), findsOneWidget); +// expect(find.text('Jane Doe'), findsOneWidget); +// +// await tester.tap(find.byIcon(FeatherIcons.user)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(PersonView), findsOneWidget); +// +// // Press back +// await tester.tap(find.byIcon(Icons.arrow_back)); +// await tester.pumpAndSettle(); +// +// // Open edit event page +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AddEventView), findsOneWidget); +// expect(find.text('Lecturer'), findsOneWidget); +// expect(find.text('Location'), findsOneWidget); +// expect(find.text('Week'), findsOneWidget); +// expect(find.text('Day'), findsOneWidget); +// +// // Select lecturer +// await tester.tap(find.text('Lecturer')); +// await tester.pumpAndSettle(); +// await tester.enterText( +// find.byKey(const Key('AutocompleteLecturer')), 'Doe'); +// await tester.pumpAndSettle(); +// +// expect(find.text('Jane Doe'), findsOneWidget); +// expect(find.text('John Doe'), findsOneWidget); +// +// await tester.enterText( +// find.byKey(const Key('AutocompleteLecturer')), 'John Doe'); +// await tester.pumpAndSettle(); +// +// FocusManager.instance.primaryFocus.unfocus(); +// await tester.pumpAndSettle(); +// await tester.tap(find.text('John Doe').last); +// await tester.pumpAndSettle(); +// +// FocusManager.instance.primaryFocus.unfocus(); +// await tester.pumpAndSettle(); +// +// expect(find.text('Jane Doe'), findsNothing); +// expect(find.text('John Doe'), findsOneWidget); +// +// // Press save +// await tester.tap(find.text('Save')); +// await tester.pumpAndSettle(const Duration(seconds: 5)); +// +// expect(find.byType(TimetablePage), findsOneWidget); +// +// // Open PC event +// await tester.tap(find.text('PC')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(EventView), findsOneWidget); +// expect(find.byIcon(FeatherIcons.user), findsOneWidget); +// +// // Press back +// await tester.tap(find.byIcon(Icons.arrow_back)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(TimetablePage), findsOneWidget); +// }); +// } +// }); +// +// group('Event page - delete event', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// // Open PH event +// await tester.tap(find.text('PH')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(EventView), findsOneWidget); +// +// // Open edit event page +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AddEventView), findsOneWidget); +// +// // Open delete dialog +// await tester.tap(find.byIcon(Icons.more_vert_outlined)); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Delete event')); +// await tester.pumpAndSettle(); +// +// // Confirm deletion +// expect(find.text('Are you sure you want to delete this event?'), +// findsOneWidget); +// await tester.tap(find.text('DELETE EVENT')); +// await tester.pumpAndSettle(const Duration(seconds: 5)); +// +// verify(mockEventProvider.deleteEvent(any)); +// expect(find.byType(TimetablePage), findsOneWidget); +// }); +// } +// }); +// }); +// +// group('Classes', () { +// group('Class', () { +// setUp(() { +// when(mockAuthProvider.currentUser).thenAnswer((_) => +// Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); +// when(mockAuthProvider.currentUserFromCache) +// .thenReturn(User(uid: '0', firstName: 'John', lastName: 'Doe')); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.uid).thenReturn('0'); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// // Open classes +// await tester.tap(find.byIcon(FeatherIcons.bookOpen)); +// await tester.pumpAndSettle(); +// +// // Open class view +// expect(find.byType(ClassesPage), findsOneWidget); +// }); +// } +// }); +// +// group('Add class', () { +// setUp(() { +// when(mockAuthProvider.currentUser).thenAnswer((_) => +// Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); +// when(mockAuthProvider.currentUserFromCache) +// .thenReturn(User(uid: '0', firstName: 'John', lastName: 'Doe')); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.uid).thenReturn('0'); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// // Open classes +// await tester.tap(find.byIcon(FeatherIcons.bookOpen)); +// await tester.pumpAndSettle(); +// +// // Open add class view +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AddClassesPage), findsOneWidget); +// +// // Save +// await tester.tap(find.text('Save')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(ClassesPage), findsOneWidget); +// }); +// } +// }); +// +// group('Class view', () { +// setUp(() { +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( +// uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); +// when(mockAuthProvider.currentUserFromCache).thenReturn(User( +// uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3)); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.uid).thenReturn('0'); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// // Open classes +// await tester.tap(find.byIcon(FeatherIcons.bookOpen)); +// await tester.pumpAndSettle(); +// +// // Open class view +// await tester.tap(find.text('PC')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(ClassView), findsOneWidget); +// +// // Open add shortcut view +// await tester.tap(find.byIcon(Icons.add_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(ShortcutView), findsOneWidget); +// +// await tester.tap(find.byIcon(Icons.arrow_back)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(ClassView), findsOneWidget); +// +// // Open grading view +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(GradingView), findsOneWidget); +// +// await tester.tap(find.text('Save')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(ClassView), findsOneWidget); +// }); +// } +// }); +// }); +// +// group('Feedback view', () { +// setUp(() { +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( +// uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); +// when(mockAuthProvider.currentUserFromCache).thenReturn(User( +// uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3)); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.uid).thenReturn('0'); +// when(mockPersonProvider.fetchPerson(any)) +// .thenAnswer((_) => Future.value(Person(name: 'John Doe'))); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// // Open classes +// await tester.tap(find.byIcon(FeatherIcons.bookOpen)); +// await tester.pumpAndSettle(); +// +// // Open class view +// await tester.tap(find.text('PC')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(ClassView), findsOneWidget); +// +// // Open feedback page +// await tester.tap(find.byIcon(Icons.rate_review_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(ClassFeedbackView), findsOneWidget); +// +// await tester.tap(find.byKey(const Key('AcknowledgementCheckbox'))); +// await tester.pumpAndSettle(); +// +// await tester.enterText( +// find.byKey(const Key('AutocompleteAssistant')), 'John'); +// await tester.pumpAndSettle(); +// await tester.tap(find.text('John Doe').last); +// await tester.pumpAndSettle(); +// +// expect(find.byType(Card), findsNWidgets(4)); +// expect(find.byType(FeedbackQuestionFormField), findsNWidgets(4)); +// expect( +// find.text( +// 'Estimate the average number of hours per week devoted to solving homework.'), +// findsOneWidget); +// expect( +// find.text( +// 'Approximate number of activities that you attended (lectures + applications):'), +// findsOneWidget); +// expect( +// find.text('Was the exposure method appropriate?'), findsOneWidget); +// expect(find.text('What are the positive aspects of this class?'), +// findsOneWidget); +// +// await tester.drag( +// find.byKey(const Key('FeedbackSlider')), const Offset(2, 0)); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.byIcon(Icons.sentiment_very_satisfied)); +// await tester.pumpAndSettle(); +// +// await tester.enterText( +// find.byKey(const Key('FeedbackText')), 'Best class ever!'); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.byKey(const Key('FeedbackDropdown'))); +// await tester.pumpAndSettle(); +// await tester.tap(find.text('option 3').last); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Send')); +// await tester.pumpAndSettle(const Duration(seconds: 5)); +// +// expect(find.text('You need to select your assistant for this class.'), +// findsNothing); +// expect(find.text('Answer cannot be empty.'), findsNothing); +// +// expect(find.byType(ClassView), findsOneWidget); +// }); +// } +// }); +// +// group('Settings', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open settings +// await tester.tap(find.byIcon(Icons.settings_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(SettingsPage), findsOneWidget); +// }); +// } +// }); +// +// group('Portal', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open portal +// await tester.tap(find.byIcon(FeatherIcons.globe)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(PortalPage), findsOneWidget); +// }); +// } +// }); +// +// group('Filter', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open filter popup menu +// await tester.tap(find.byIcon(FeatherIcons.globe)); +// await tester.pumpAndSettle(); +// await tester.tap(find.byIcon(FeatherIcons.filter)); +// await tester.pumpAndSettle(); +// +// // Open filter on portal page +// await tester.tap(find.text('Filter by relevance')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(FilterPage), findsOneWidget); +// }); +// } +// }); +// +// group('Add website', () { +// setUp(() { +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open portal page +// await tester.tap(find.byIcon(FeatherIcons.globe)); +// await tester.pumpAndSettle(); +// +// // Open add website page +// final addWebsiteButton = +// find.byKey(const ValueKey('add_website_associations')); +// await tester.ensureVisible(addWebsiteButton); +// await tester.pumpAndSettle(); +// +// await tester.tap(addWebsiteButton); +// await tester.pumpAndSettle(); +// +// expect(find.byType(WebsiteView), findsOneWidget); +// }); +// } +// }); +// +// group('Edit website', () { +// setUp(() { +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( +// uid: '1', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open portal page +// await tester.tap(find.byIcon(FeatherIcons.globe)); +// await tester.pumpAndSettle(); +// +// // Enable editing +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// // Open edit website page +// await tester.ensureVisible(find.text('LSAC1')); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('LSAC1')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(WebsiteView), findsOneWidget); +// }); +// } +// }); +// +// group('Delete website', () { +// setUp(() { +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( +// uid: '1', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open portal page +// await tester.tap(find.byIcon(FeatherIcons.globe)); +// await tester.pumpAndSettle(); +// +// // Enable editing +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// // Open edit website page +// await tester.ensureVisible(find.text('LSAC1')); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('LSAC1')); +// await tester.pumpAndSettle(); +// +// // Open delete dialog +// await tester.tap(find.byIcon(Icons.more_vert_outlined)); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Delete website')); +// await tester.pumpAndSettle(); +// +// // Cancel +// expect(find.text('Are you sure you want to delete this website?'), +// findsOneWidget); +// await tester.tap(find.text('CANCEL')); +// await tester.pumpAndSettle(); +// +// // Open delete dialog +// await tester.tap(find.byIcon(Icons.more_vert_outlined)); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Delete website')); +// await tester.pumpAndSettle(); +// +// // Confirm deletion +// expect(find.text('Are you sure you want to delete this website?'), +// findsOneWidget); +// await tester.tap(find.text('DELETE WEBSITE')); +// await tester.pumpAndSettle(const Duration(seconds: 5)); +// +// verify(mockWebsiteProvider.deleteWebsite(any)); +// expect(find.byType(PortalPage), findsOneWidget); +// }); +// } +// }); +// group('Edit Profile', () { +// setUp(() { +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(false)); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( +// uid: '1', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); +// when(mockAuthProvider.currentUserFromCache).thenReturn(User( +// uid: '1', firstName: 'John', lastName: 'Doe', permissionLevel: 3)); +// when(mockAuthProvider.email).thenReturn('john.doe@stud.acs.upb.ro'); +// when(mockAuthProvider.getProfilePictureURL()) +// .thenAnswer((_) => Future.value(null)); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open Edit Profile page +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(EditProfilePage), findsOneWidget); +// }); +// +// testWidgets('${size.width}x${size.height}, delete account', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open Edit Profile page +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// //Open delete account popup +// await tester.tap(find.byIcon(Icons.more_vert_outlined)); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Delete account')); +// await tester.pumpAndSettle(); +// +// expect(find.byKey(const ValueKey('delete_account_button')), +// findsOneWidget); +// }); +// +// testWidgets('${size.width}x${size.height}, change password', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open Edit Profile page +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// //Open change password popup +// await tester.tap(find.byIcon(Icons.more_vert_outlined)); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Change password')); +// await tester.pumpAndSettle(); +// +// expect(find.byKey(const ValueKey('change_password_button')), +// findsOneWidget); +// }); +// +// testWidgets('${size.width}x${size.height}, change email', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open Edit Profile page +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// // Edit the email +// await tester.enterText( +// find.text('john.doe'), 'johndoe@stud.acs.upb.ro'); +// +// //Open change email popup +// await tester.tap(find.text('Save')); +// await tester.pumpAndSettle(); +// +// expect( +// find.byKey(const ValueKey('change_email_button')), findsOneWidget); +// }); +// } +// }); +// +// group('People page', () { +// setUp(() { +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(true); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await mockNetworkImagesFor(() async { +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open people page +// await tester.tap(find.byIcon(Icons.people_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(PeoplePage), findsOneWidget); +// +// // Open bottom sheet with person info +// final names = ['John Doe', 'Jane Doe', 'Mary Poppins']; +// for (final name in names) { +// await tester.tap(find.text(name)); +// await tester.pumpAndSettle(); +// } +// +// expect(find.byType(PersonView), findsOneWidget); +// }); +// }); +// } +// }); +// +// group('Show faq page', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// final showMoreFaq = +// find.byKey(const ValueKey('show_more_faq'), skipOffstage: false); +// +// // Ensure FAQ card is visible +// await tester.ensureVisible(showMoreFaq); +// await tester.pumpAndSettle(); +// +// // Open faq page +// await tester.tap(showMoreFaq); +// await tester.pumpAndSettle(); +// +// expect(find.byType(FaqPage), findsOneWidget); +// +// await tester.tap(find.byIcon(Icons.search_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(SearchBar), findsOneWidget); +// +// final cancelSearchBar = find.byKey(const ValueKey('cancel_search_bar')); +// +// await tester.tap(cancelSearchBar); +// await tester.pumpAndSettle(); +// +// expect(find.byType(SearchBar), findsNothing); +// }); +// } +// }); +// +// group('Show news feed page', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open news feed page +// final showMoreNewsFeed = +// find.byKey(const ValueKey('show_more_news_feed')); +// +// await tester.tap(showMoreNewsFeed); +// await tester.pumpAndSettle(); +// +// expect(find.byType(NewsFeedPage), findsOneWidget); +// }); +// } +// }); +// } diff --git a/test/portal_test.dart b/test/portal_test.dart index 5d052c8d5..57c5d32d7 100644 --- a/test/portal_test.dart +++ b/test/portal_test.dart @@ -1,177 +1,177 @@ -import 'package:acs_upb_mobile/authentication/model/user.dart'; -import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; -import 'package:acs_upb_mobile/generated/l10n.dart'; -import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; -import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; -import 'package:acs_upb_mobile/pages/portal/model/website.dart'; -import 'package:acs_upb_mobile/pages/portal/service/website_provider.dart'; -import 'package:acs_upb_mobile/pages/portal/view/portal_page.dart'; -import 'package:acs_upb_mobile/resources/locale_provider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:preferences/preferences.dart'; -import 'package:provider/provider.dart'; -import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; - -import 'test_utils.dart'; - -class MockWebsiteProvider extends Mock implements WebsiteProvider {} - -class MockFilterProvider extends Mock implements FilterProvider {} - -class MockAuthProvider extends Mock implements AuthProvider {} - -class MockUrlLauncher extends Mock - with MockPlatformInterfaceMixin - implements UrlLauncherPlatform {} - -void main() { - final WebsiteProvider mockWebsiteProvider = MockWebsiteProvider(); - // ignore: invalid_use_of_protected_member - when(mockWebsiteProvider.hasListeners).thenReturn(false); - when(mockWebsiteProvider.fetchWebsites(any)).thenAnswer((_) => Future.value([ - Website( - id: '1', - relevance: null, - category: WebsiteCategory.learning, - infoByLocale: {'en': 'info-en', 'ro': 'info-ro'}, - label: 'Moodle', - link: 'http://acs.curs.pub.ro/', - isPrivate: false, - ), - Website( - id: '2', - relevance: null, - category: WebsiteCategory.learning, - infoByLocale: {}, - label: 'OCW', - link: 'https://ocw.cs.pub.ro/', - isPrivate: false, - ), - Website( - id: '3', - relevance: null, - category: WebsiteCategory.association, - infoByLocale: {}, - label: 'LSAC', - link: 'https://lsacbucuresti.ro/', - isPrivate: false, - ), - ])); - when(mockWebsiteProvider.fetchFavouriteWebsites(any)).thenAnswer( - (_) async => (await mockWebsiteProvider.fetchWebsites(any)).take(3)); - when(mockWebsiteProvider.incrementNumberOfVisits(any, uid: anyNamed('uid'))) - .thenAnswer((_) => Future.value(true)); - - final FilterProvider mockFilterProvider = MockFilterProvider(); - // ignore: invalid_use_of_protected_member - when(mockFilterProvider.hasListeners).thenReturn(false); - when(mockFilterProvider.fetchFilter()) - .thenAnswer((_) => Future.value(Filter(root: FilterNode(name: 'All')))); - when(mockFilterProvider.filterEnabled).thenReturn(true); - - final MockUrlLauncher mockUrlLauncher = MockUrlLauncher(); - UrlLauncherPlatform.instance = mockUrlLauncher; - when(mockUrlLauncher.canLaunch(any)).thenAnswer((_) => Future.value(true)); - - final MockAuthProvider mockAuthProvider = MockAuthProvider(); - // ignore: invalid_use_of_protected_member - when(mockAuthProvider.hasListeners).thenReturn(false); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.currentUser).thenAnswer( - (_) => Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); - when(mockAuthProvider.currentUserFromCache) - .thenReturn(User(uid: '0', firstName: 'John', lastName: 'Doe')); - - Widget buildPortalPage() => MultiProvider( - providers: [ - ChangeNotifierProvider( - create: (_) => mockWebsiteProvider), - ChangeNotifierProvider( - create: (_) => mockFilterProvider), - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ], - child: const MaterialApp( - localizationsDelegates: [S.delegate], - home: PortalPage(), - ), - ); - - group('Portal', () { - setUpAll(() async { - WidgetsFlutterBinding.ensureInitialized(); - PrefService.enableCaching(); - PrefService.cache = {}; - PrefService.setString('language', 'en'); - - LocaleProvider.cultures = testCultures; - LocaleProvider.rruleL10ns = {'en': await RruleL10nTest.create()}; - }); - - testWidgets('Names', (WidgetTester tester) async { - await tester.pumpWidget(buildPortalPage()); - await tester.pumpAndSettle(); - - expect(find.text('Moodle'), findsOneWidget); - expect(find.text('OCW'), findsOneWidget); - expect(find.text('LSAC'), findsOneWidget); - }); - - group('Localization', () { - testWidgets('en', (WidgetTester tester) async { - await tester.pumpWidget(buildPortalPage()); - await tester.pumpAndSettle(); - - expect(find.byTooltip('info-en'), findsOneWidget); - }); - - testWidgets('ro', (WidgetTester tester) async { - PrefService.setString('language', 'ro'); - await S.load(const Locale('ro', 'RO')); - - await tester.pumpWidget(buildPortalPage()); - await tester.pumpAndSettle(); - - expect(find.byTooltip('info-ro'), findsOneWidget); - }); - }); - - testWidgets('Links', (WidgetTester tester) async { - await tester.pumpWidget(buildPortalPage()); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Moodle')); - verify(mockUrlLauncher.launch('http://acs.curs.pub.ro/', - useSafariVC: anyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableDomStorage: anyNamed('enableDomStorage'), - enableJavaScript: anyNamed('enableJavaScript'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'))) - .called(1); - - await tester.tap(find.text('OCW')); - verify(mockUrlLauncher.launch('https://ocw.cs.pub.ro/', - useSafariVC: anyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableDomStorage: anyNamed('enableDomStorage'), - enableJavaScript: anyNamed('enableJavaScript'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'))) - .called(1); - - await tester.tap(find.text('LSAC')); - verify(mockUrlLauncher.launch('https://lsacbucuresti.ro/', - useSafariVC: anyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableDomStorage: anyNamed('enableDomStorage'), - enableJavaScript: anyNamed('enableJavaScript'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'))) - .called(1); - }); - }); -} +// import 'package:acs_upb_mobile/authentication/model/user.dart'; +// import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; +// import 'package:acs_upb_mobile/generated/l10n.dart'; +// import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; +// import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; +// import 'package:acs_upb_mobile/pages/portal/model/website.dart'; +// import 'package:acs_upb_mobile/pages/portal/service/website_provider.dart'; +// import 'package:acs_upb_mobile/pages/portal/view/portal_page.dart'; +// import 'package:acs_upb_mobile/resources/locale_provider.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:mockito/mockito.dart'; +// import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +// import 'package:preferences/preferences.dart'; +// import 'package:provider/provider.dart'; +// import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; +// +// import 'test_utils.dart'; +// +// class MockWebsiteProvider extends Mock implements WebsiteProvider {} +// +// class MockFilterProvider extends Mock implements FilterProvider {} +// +// class MockAuthProvider extends Mock implements AuthProvider {} +// +// class MockUrlLauncher extends Mock +// with MockPlatformInterfaceMixin +// implements UrlLauncherPlatform {} +// +// void main() { +// final WebsiteProvider mockWebsiteProvider = MockWebsiteProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockWebsiteProvider.hasListeners).thenReturn(false); +// when(mockWebsiteProvider.fetchWebsites(any)).thenAnswer((_) => Future.value([ +// Website( +// id: '1', +// relevance: null, +// category: WebsiteCategory.learning, +// infoByLocale: {'en': 'info-en', 'ro': 'info-ro'}, +// label: 'Moodle', +// link: 'http://acs.curs.pub.ro/', +// isPrivate: false, +// ), +// Website( +// id: '2', +// relevance: null, +// category: WebsiteCategory.learning, +// infoByLocale: {}, +// label: 'OCW', +// link: 'https://ocw.cs.pub.ro/', +// isPrivate: false, +// ), +// Website( +// id: '3', +// relevance: null, +// category: WebsiteCategory.association, +// infoByLocale: {}, +// label: 'LSAC', +// link: 'https://lsacbucuresti.ro/', +// isPrivate: false, +// ), +// ])); +// when(mockWebsiteProvider.fetchFavouriteWebsites(any)).thenAnswer( +// (_) async => (await mockWebsiteProvider.fetchWebsites(any)).take(3)); +// when(mockWebsiteProvider.incrementNumberOfVisits(any, uid: anyNamed('uid'))) +// .thenAnswer((_) => Future.value(true)); +// +// final FilterProvider mockFilterProvider = MockFilterProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockFilterProvider.hasListeners).thenReturn(false); +// when(mockFilterProvider.fetchFilter()) +// .thenAnswer((_) => Future.value(Filter(root: FilterNode(name: 'All')))); +// when(mockFilterProvider.filterEnabled).thenReturn(true); +// +// final MockUrlLauncher mockUrlLauncher = MockUrlLauncher(); +// UrlLauncherPlatform.instance = mockUrlLauncher; +// when(mockUrlLauncher.canLaunch(any)).thenAnswer((_) => Future.value(true)); +// +// final MockAuthProvider mockAuthProvider = MockAuthProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockAuthProvider.hasListeners).thenReturn(false); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.currentUser).thenAnswer( +// (_) => Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); +// when(mockAuthProvider.currentUserFromCache) +// .thenReturn(User(uid: '0', firstName: 'John', lastName: 'Doe')); +// +// Widget buildPortalPage() => MultiProvider( +// providers: [ +// ChangeNotifierProvider( +// create: (_) => mockWebsiteProvider), +// ChangeNotifierProvider( +// create: (_) => mockFilterProvider), +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ], +// child: const MaterialApp( +// localizationsDelegates: [S.delegate], +// home: PortalPage(), +// ), +// ); +// +// group('Portal', () { +// setUpAll(() async { +// WidgetsFlutterBinding.ensureInitialized(); +// PrefService.enableCaching(); +// PrefService.cache = {}; +// PrefService.setString('language', 'en'); +// +// LocaleProvider.cultures = testCultures; +// LocaleProvider.rruleL10ns = {'en': await RruleL10nTest.create()}; +// }); +// +// testWidgets('Names', (WidgetTester tester) async { +// await tester.pumpWidget(buildPortalPage()); +// await tester.pumpAndSettle(); +// +// expect(find.text('Moodle'), findsOneWidget); +// expect(find.text('OCW'), findsOneWidget); +// expect(find.text('LSAC'), findsOneWidget); +// }); +// +// group('Localization', () { +// testWidgets('en', (WidgetTester tester) async { +// await tester.pumpWidget(buildPortalPage()); +// await tester.pumpAndSettle(); +// +// expect(find.byTooltip('info-en'), findsOneWidget); +// }); +// +// testWidgets('ro', (WidgetTester tester) async { +// PrefService.setString('language', 'ro'); +// await S.load(const Locale('ro', 'RO')); +// +// await tester.pumpWidget(buildPortalPage()); +// await tester.pumpAndSettle(); +// +// expect(find.byTooltip('info-ro'), findsOneWidget); +// }); +// }); +// +// testWidgets('Links', (WidgetTester tester) async { +// await tester.pumpWidget(buildPortalPage()); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Moodle')); +// verify(mockUrlLauncher.launch('http://acs.curs.pub.ro/', +// useSafariVC: anyNamed('useSafariVC'), +// useWebView: anyNamed('useWebView'), +// enableDomStorage: anyNamed('enableDomStorage'), +// enableJavaScript: anyNamed('enableJavaScript'), +// universalLinksOnly: anyNamed('universalLinksOnly'), +// headers: anyNamed('headers'))) +// .called(1); +// +// await tester.tap(find.text('OCW')); +// verify(mockUrlLauncher.launch('https://ocw.cs.pub.ro/', +// useSafariVC: anyNamed('useSafariVC'), +// useWebView: anyNamed('useWebView'), +// enableDomStorage: anyNamed('enableDomStorage'), +// enableJavaScript: anyNamed('enableJavaScript'), +// universalLinksOnly: anyNamed('universalLinksOnly'), +// headers: anyNamed('headers'))) +// .called(1); +// +// await tester.tap(find.text('LSAC')); +// verify(mockUrlLauncher.launch('https://lsacbucuresti.ro/', +// useSafariVC: anyNamed('useSafariVC'), +// useWebView: anyNamed('useWebView'), +// enableDomStorage: anyNamed('enableDomStorage'), +// enableJavaScript: anyNamed('enableJavaScript'), +// universalLinksOnly: anyNamed('universalLinksOnly'), +// headers: anyNamed('headers'))) +// .called(1); +// }); +// }); +// } diff --git a/test/settings_test.dart b/test/settings_test.dart index b01dd17d4..ccf92cab7 100644 --- a/test/settings_test.dart +++ b/test/settings_test.dart @@ -1,386 +1,386 @@ -import 'package:acs_upb_mobile/authentication/model/user.dart'; -import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; -import 'package:acs_upb_mobile/main.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; -import 'package:acs_upb_mobile/pages/classes/model/class.dart'; -import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; -import 'package:acs_upb_mobile/pages/faq/model/question.dart'; -import 'package:acs_upb_mobile/pages/faq/service/question_provider.dart'; -import 'package:acs_upb_mobile/pages/news_feed/model/news_feed_item.dart'; -import 'package:acs_upb_mobile/pages/news_feed/service/news_provider.dart'; -import 'package:acs_upb_mobile/pages/portal/service/website_provider.dart'; -import 'package:acs_upb_mobile/pages/settings/service/request_provider.dart'; -import 'package:acs_upb_mobile/pages/settings/view/request_permissions.dart'; -import 'package:acs_upb_mobile/pages/settings/view/settings_page.dart'; -import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; -import 'package:acs_upb_mobile/resources/locale_provider.dart'; -import 'package:acs_upb_mobile/resources/utils.dart'; -import 'package:acs_upb_mobile/widgets/dialog.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:preferences/preferences.dart'; -import 'package:provider/provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:time_machine/time_machine.dart'; - -import 'test_utils.dart'; - -class MockAuthProvider extends Mock implements AuthProvider {} - -class MockWebsiteProvider extends Mock implements WebsiteProvider {} - -class MockQuestionProvider extends Mock implements QuestionProvider {} - -class MockRequestProvider extends Mock implements RequestProvider {} - -class MockNewsProvider extends Mock implements NewsProvider {} - -class MockUniEventProvider extends Mock implements UniEventProvider {} - -class MockFeedbackProvider extends Mock implements FeedbackProvider {} - -class MockClassProvider extends Mock implements ClassProvider {} - -void main() { - AuthProvider mockAuthProvider; - WebsiteProvider mockWebsiteProvider; - MockQuestionProvider mockQuestionProvider; - RequestProvider mockRequestProvider; - MockNewsProvider mockNewsProvider; - UniEventProvider mockEventProvider; - FeedbackProvider mockFeedbackProvider; - ClassProvider mockClassProvider; - - Widget buildApp() => MultiProvider(providers: [ - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ChangeNotifierProvider( - create: (_) => mockEventProvider), - ChangeNotifierProvider( - create: (_) => mockWebsiteProvider), - ChangeNotifierProvider( - create: (_) => mockQuestionProvider), - Provider(create: (_) => mockRequestProvider), - ChangeNotifierProvider(create: (_) => mockNewsProvider), - ChangeNotifierProvider( - create: (_) => mockFeedbackProvider), - ChangeNotifierProvider(create: (_) => mockClassProvider), - ], child: const MyApp()); - - group('Settings', () { - setUpAll(() async { - WidgetsFlutterBinding.ensureInitialized(); - PrefService.enableCaching(); - PrefService.cache = {}; - // Assuming mock system language is English - SharedPreferences.setMockInitialValues({'language': 'auto'}); - - LocaleProvider.cultures = testCultures; - LocaleProvider.rruleL10ns = {'en': await RruleL10nTest.create()}; - - Utils.packageInfo = PackageInfo( - version: '1.2.7', buildNumber: '6', appName: 'ACS UPB Mobile'); - - // Pretend an anonymous user is already logged in - mockAuthProvider = MockAuthProvider(); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - // ignore: invalid_use_of_protected_member - when(mockAuthProvider.hasListeners).thenReturn(false); - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(null)); - when(mockAuthProvider.isAnonymous).thenReturn(true); - when(mockAuthProvider.getProfilePictureURL()) - .thenAnswer((_) => Future.value(null)); - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); - - mockWebsiteProvider = MockWebsiteProvider(); - // ignore: invalid_use_of_protected_member - when(mockWebsiteProvider.hasListeners).thenReturn(false); - when(mockWebsiteProvider.deleteWebsite(any)) - .thenAnswer((_) => Future.value(true)); - when(mockWebsiteProvider.fetchWebsites(any)) - .thenAnswer((_) => Future.value([])); - when(mockWebsiteProvider.fetchFavouriteWebsites(any)) - .thenAnswer((_) => Future.value(null)); - - mockQuestionProvider = MockQuestionProvider(); - // ignore: invalid_use_of_protected_member - when(mockQuestionProvider.hasListeners).thenReturn(false); - when(mockQuestionProvider.fetchQuestions()) - .thenAnswer((_) => Future.value([])); - when(mockQuestionProvider.fetchQuestions(limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([])); - - mockRequestProvider = MockRequestProvider(); - when(mockRequestProvider.makeRequest(any)) - .thenAnswer((_) => Future.value(true)); - when(mockRequestProvider.userAlreadyRequested(any)) - .thenAnswer((_) => Future.value(false)); - - mockNewsProvider = MockNewsProvider(); - // ignore: invalid_use_of_protected_member - when(mockNewsProvider.hasListeners).thenReturn(false); - when(mockNewsProvider.fetchNewsFeedItems()) - .thenAnswer((_) => Future.value([])); - when(mockNewsProvider.fetchNewsFeedItems(limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([])); - - mockEventProvider = MockUniEventProvider(); - // ignore: invalid_use_of_protected_member - when(mockEventProvider.hasListeners).thenReturn(false); - when(mockEventProvider.getUpcomingEvents(LocalDate.today())) - .thenAnswer((_) => Future.value([])); - when(mockEventProvider.getUpcomingEvents(LocalDate.today(), - limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([])); - - mockFeedbackProvider = MockFeedbackProvider(); - // ignore: invalid_use_of_protected_member - when(mockFeedbackProvider.hasListeners).thenReturn(true); - when(mockFeedbackProvider.userSubmittedFeedbackForClass(any, any)) - .thenAnswer((_) => Future.value(false)); - when(mockFeedbackProvider.getClassesWithCompletedFeedback(any)) - .thenAnswer((_) => Future.value({'M1': true, 'M2': true})); - - mockClassProvider = MockClassProvider(); - // ignore: invalid_use_of_protected_member - when(mockClassProvider.hasListeners).thenReturn(false); - final userClassHeaders = [ - ClassHeader( - id: '3', - name: 'Programming', - acronym: 'PC', - category: 'A', - ), - ClassHeader( - id: '4', - name: 'Physics', - acronym: 'PH', - category: 'D', - ) - ]; - when(mockClassProvider.userClassHeadersCache) - .thenReturn(userClassHeaders); - when(mockClassProvider.fetchClassHeaders(uid: anyNamed('uid'))) - .thenAnswer((_) => Future.value([ - ClassHeader( - id: '1', - name: 'Maths 1', - acronym: 'M1', - category: 'A/B', - ), - ClassHeader( - id: '2', - name: 'Maths 2', - acronym: 'M2', - category: 'A/C', - ), - ] + - userClassHeaders)); - when(mockClassProvider.fetchUserClassIds(any)) - .thenAnswer((_) => Future.value(['3', '4'])); - }); - - testWidgets('Dark Mode', (WidgetTester tester) async { - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - MaterialApp app = find.byType(MaterialApp).evaluate().first.widget; - expect(app.theme.brightness, equals(Brightness.light)); - - // Open settings - await tester.tap(find.byIcon(Icons.settings_outlined)); - await tester.pumpAndSettle(); - - // Toggle dark mode - await tester.tap(find.text('Dark Mode')); - await tester.pumpAndSettle(); - - app = find.byType(MaterialApp).evaluate().first.widget; - expect(app.theme.brightness, equals(Brightness.dark)); - - // Toggle dark mode - await tester.tap(find.text('Dark Mode')); - await tester.pumpAndSettle(); - - app = find.byType(MaterialApp).evaluate().first.widget; - expect(app.theme.brightness, equals(Brightness.light)); - }); - - testWidgets('Language', (WidgetTester tester) async { - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open settings - await tester.tap(find.byIcon(Icons.settings_outlined)); - await tester.pumpAndSettle(); - - expect(find.text('Auto'), findsOneWidget); - - // Romanian - await tester.tap(find.text('Language')); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Romanian')); - await tester.pumpAndSettle(); - - expect(find.text('Setări'), findsOneWidget); - expect(find.text('Română'), findsOneWidget); - - // English - await tester.tap(find.text('Limbă')); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Engleză')); - await tester.pumpAndSettle(); - - expect(find.text('Settings'), findsOneWidget); - expect(find.text('English'), findsOneWidget); - - // Back to Auto (English) - await tester.tap(find.text('Language')); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Auto')); - await tester.pumpAndSettle(); - - expect(find.text('Settings'), findsOneWidget); - expect(find.text('Auto'), findsOneWidget); - }); - group('Request permissions', () { - setUpAll(() async { - when(mockAuthProvider.currentUser).thenAnswer((_) => - Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); - when(mockAuthProvider.isAnonymous).thenReturn(false); - }); - - testWidgets('Normal scenario', (WidgetTester tester) async { - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open settings - await tester.tap(find.byIcon(Icons.settings_outlined)); - await tester.pumpAndSettle(); - - // Open Ask Permissions page - expect(find.text('No special permissions'), findsOneWidget); - await tester.tap(find.byKey(const ValueKey('ask_permissions'))); - await tester.pumpAndSettle(); - expect(find.byType(RequestPermissionsPage), findsOneWidget); - - // Send a request - await tester.enterText( - find.byType(TextFormField), 'I love League of Legends'); - await tester.tap(find.byType(Checkbox)); - await tester.tap(find.text('Save')); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Verify the request is sent and Settings Page pops back - verify(mockRequestProvider.makeRequest(any)); - expect(find.byType(SettingsPage), findsOneWidget); - }); - - testWidgets('User has already sent a request scenario', - (WidgetTester tester) async { - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); - when(mockRequestProvider.userAlreadyRequested(any)) - .thenAnswer((_) => Future.value(true)); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open settings - await tester.tap(find.byIcon(Icons.settings_outlined)); - await tester.pumpAndSettle(); - - // Open Ask Permissions page - expect(find.text('Permissions request already sent'), findsOneWidget); - await tester.tap(find.byKey(const ValueKey('ask_permissions'))); - await tester.pumpAndSettle(); - expect(find.byType(RequestPermissionsPage), findsOneWidget); - - // Send a request - await tester.enterText( - find.byType(TextFormField), 'I love League of Legends'); - await tester.tap(find.byType(Checkbox)); - await tester.tap(find.text('Save')); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Check that warning Dialog appears and press Send - expect(find.byType(AppDialog), findsOneWidget); - await tester.tap(find.text('SEND')); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Verify the request is sent and Settings Page pops back - verify(mockRequestProvider.makeRequest(any)); - expect(find.byType(SettingsPage), findsOneWidget); - }); - - testWidgets('User is anonymous scenario', (WidgetTester tester) async { - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); - when(mockAuthProvider.isAnonymous).thenReturn(true); - when(mockRequestProvider.userAlreadyRequested(any)) - .thenAnswer((_) => Future.value(false)); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open settings - await tester.tap(find.byIcon(Icons.settings_outlined)); - await tester.pumpAndSettle(); - - // Press Ask Permissions page - expect(find.text('No special permissions'), findsOneWidget); - await tester.tap(find.byKey(const ValueKey('ask_permissions'))); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Verify nothing happens - expect(find.byType(SettingsPage), findsOneWidget); - }); - - testWidgets('User is not verified scenario', (WidgetTester tester) async { - when(mockAuthProvider.isVerified) - .thenAnswer((_) => Future.value(false)); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockRequestProvider.userAlreadyRequested(any)) - .thenAnswer((_) => Future.value(false)); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open settings - await tester.tap(find.byIcon(Icons.settings_outlined)); - await tester.pumpAndSettle(); - - // Press Ask Permissions page - expect(find.text('No special permissions'), findsOneWidget); - await tester.tap(find.byKey(const ValueKey('ask_permissions'))); - - // Verify Ask Permissions page is not opened - await tester.pumpAndSettle(const Duration(seconds: 4)); - expect(find.byType(SettingsPage), findsOneWidget); - expect(find.byType(RequestPermissionsPage), findsNothing); - - // Verify account - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); - - // Go back and open settings again - await tester.tap(find.byIcon(Icons.arrow_back)); - await tester.pumpAndSettle(); - await tester.tap(find.byIcon(Icons.settings_outlined)); - await tester.pumpAndSettle(); - - // Press Ask Permissions page - expect(find.text('No special permissions'), findsOneWidget); - await tester.tap(find.byKey(const ValueKey('ask_permissions'))); - - // Verify Ask Permissions page is opened - await tester.pumpAndSettle(); - expect(find.byType(RequestPermissionsPage), findsOneWidget); - }); - }); - }); -} +// import 'package:acs_upb_mobile/authentication/model/user.dart'; +// import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; +// import 'package:acs_upb_mobile/main.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; +// import 'package:acs_upb_mobile/pages/classes/model/class.dart'; +// import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; +// import 'package:acs_upb_mobile/pages/faq/model/question.dart'; +// import 'package:acs_upb_mobile/pages/faq/service/question_provider.dart'; +// import 'package:acs_upb_mobile/pages/news_feed/model/news_feed_item.dart'; +// import 'package:acs_upb_mobile/pages/news_feed/service/news_provider.dart'; +// import 'package:acs_upb_mobile/pages/portal/service/website_provider.dart'; +// import 'package:acs_upb_mobile/pages/settings/service/request_provider.dart'; +// import 'package:acs_upb_mobile/pages/settings/view/request_permissions.dart'; +// import 'package:acs_upb_mobile/pages/settings/view/settings_page.dart'; +// import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; +// import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; +// import 'package:acs_upb_mobile/resources/locale_provider.dart'; +// import 'package:acs_upb_mobile/resources/utils.dart'; +// import 'package:acs_upb_mobile/widgets/dialog.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:mockito/mockito.dart'; +// import 'package:package_info_plus/package_info_plus.dart'; +// import 'package:preferences/preferences.dart'; +// import 'package:provider/provider.dart'; +// import 'package:shared_preferences/shared_preferences.dart'; +// import 'package:time_machine/time_machine.dart'; +// +// import 'test_utils.dart'; +// +// class MockAuthProvider extends Mock implements AuthProvider {} +// +// class MockWebsiteProvider extends Mock implements WebsiteProvider {} +// +// class MockQuestionProvider extends Mock implements QuestionProvider {} +// +// class MockRequestProvider extends Mock implements RequestProvider {} +// +// class MockNewsProvider extends Mock implements NewsProvider {} +// +// class MockUniEventProvider extends Mock implements UniEventProvider {} +// +// class MockFeedbackProvider extends Mock implements FeedbackProvider {} +// +// class MockClassProvider extends Mock implements ClassProvider {} +// +// void main() { +// AuthProvider mockAuthProvider; +// WebsiteProvider mockWebsiteProvider; +// MockQuestionProvider mockQuestionProvider; +// RequestProvider mockRequestProvider; +// MockNewsProvider mockNewsProvider; +// UniEventProvider mockEventProvider; +// FeedbackProvider mockFeedbackProvider; +// ClassProvider mockClassProvider; +// +// Widget buildApp() => MultiProvider(providers: [ +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ChangeNotifierProvider( +// create: (_) => mockEventProvider), +// ChangeNotifierProvider( +// create: (_) => mockWebsiteProvider), +// ChangeNotifierProvider( +// create: (_) => mockQuestionProvider), +// Provider(create: (_) => mockRequestProvider), +// ChangeNotifierProvider(create: (_) => mockNewsProvider), +// ChangeNotifierProvider( +// create: (_) => mockFeedbackProvider), +// ChangeNotifierProvider(create: (_) => mockClassProvider), +// ], child: const MyApp()); +// +// group('Settings', () { +// setUpAll(() async { +// WidgetsFlutterBinding.ensureInitialized(); +// PrefService.enableCaching(); +// PrefService.cache = {}; +// // Assuming mock system language is English +// SharedPreferences.setMockInitialValues({'language': 'auto'}); +// +// LocaleProvider.cultures = testCultures; +// LocaleProvider.rruleL10ns = {'en': await RruleL10nTest.create()}; +// +// Utils.packageInfo = PackageInfo( +// version: '1.2.7', buildNumber: '6', appName: 'ACS UPB Mobile'); +// +// // Pretend an anonymous user is already logged in +// mockAuthProvider = MockAuthProvider(); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// // ignore: invalid_use_of_protected_member +// when(mockAuthProvider.hasListeners).thenReturn(false); +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(null)); +// when(mockAuthProvider.isAnonymous).thenReturn(true); +// when(mockAuthProvider.getProfilePictureURL()) +// .thenAnswer((_) => Future.value(null)); +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); +// +// mockWebsiteProvider = MockWebsiteProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockWebsiteProvider.hasListeners).thenReturn(false); +// when(mockWebsiteProvider.deleteWebsite(any)) +// .thenAnswer((_) => Future.value(true)); +// when(mockWebsiteProvider.fetchWebsites(any)) +// .thenAnswer((_) => Future.value([])); +// when(mockWebsiteProvider.fetchFavouriteWebsites(any)) +// .thenAnswer((_) => Future.value(null)); +// +// mockQuestionProvider = MockQuestionProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockQuestionProvider.hasListeners).thenReturn(false); +// when(mockQuestionProvider.fetchQuestions()) +// .thenAnswer((_) => Future.value([])); +// when(mockQuestionProvider.fetchQuestions(limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([])); +// +// mockRequestProvider = MockRequestProvider(); +// when(mockRequestProvider.makeRequest(any)) +// .thenAnswer((_) => Future.value(true)); +// when(mockRequestProvider.userAlreadyRequested(any)) +// .thenAnswer((_) => Future.value(false)); +// +// mockNewsProvider = MockNewsProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockNewsProvider.hasListeners).thenReturn(false); +// when(mockNewsProvider.fetchNewsFeedItems()) +// .thenAnswer((_) => Future.value([])); +// when(mockNewsProvider.fetchNewsFeedItems(limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([])); +// +// mockEventProvider = MockUniEventProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockEventProvider.hasListeners).thenReturn(false); +// when(mockEventProvider.getUpcomingEvents(LocalDate.today())) +// .thenAnswer((_) => Future.value([])); +// when(mockEventProvider.getUpcomingEvents(LocalDate.today(), +// limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([])); +// +// mockFeedbackProvider = MockFeedbackProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockFeedbackProvider.hasListeners).thenReturn(true); +// when(mockFeedbackProvider.userSubmittedFeedbackForClass(any, any)) +// .thenAnswer((_) => Future.value(false)); +// when(mockFeedbackProvider.getClassesWithCompletedFeedback(any)) +// .thenAnswer((_) => Future.value({'M1': true, 'M2': true})); +// +// mockClassProvider = MockClassProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockClassProvider.hasListeners).thenReturn(false); +// final userClassHeaders = [ +// ClassHeader( +// id: '3', +// name: 'Programming', +// acronym: 'PC', +// category: 'A', +// ), +// ClassHeader( +// id: '4', +// name: 'Physics', +// acronym: 'PH', +// category: 'D', +// ) +// ]; +// when(mockClassProvider.userClassHeadersCache) +// .thenReturn(userClassHeaders); +// when(mockClassProvider.fetchClassHeaders(uid: anyNamed('uid'))) +// .thenAnswer((_) => Future.value([ +// ClassHeader( +// id: '1', +// name: 'Maths 1', +// acronym: 'M1', +// category: 'A/B', +// ), +// ClassHeader( +// id: '2', +// name: 'Maths 2', +// acronym: 'M2', +// category: 'A/C', +// ), +// ] + +// userClassHeaders)); +// when(mockClassProvider.fetchUserClassIds(any)) +// .thenAnswer((_) => Future.value(['3', '4'])); +// }); +// +// testWidgets('Dark Mode', (WidgetTester tester) async { +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// MaterialApp app = find.byType(MaterialApp).evaluate().first.widget; +// expect(app.theme.brightness, equals(Brightness.light)); +// +// // Open settings +// await tester.tap(find.byIcon(Icons.settings_outlined)); +// await tester.pumpAndSettle(); +// +// // Toggle dark mode +// await tester.tap(find.text('Dark Mode')); +// await tester.pumpAndSettle(); +// +// app = find.byType(MaterialApp).evaluate().first.widget; +// expect(app.theme.brightness, equals(Brightness.dark)); +// +// // Toggle dark mode +// await tester.tap(find.text('Dark Mode')); +// await tester.pumpAndSettle(); +// +// app = find.byType(MaterialApp).evaluate().first.widget; +// expect(app.theme.brightness, equals(Brightness.light)); +// }); +// +// testWidgets('Language', (WidgetTester tester) async { +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open settings +// await tester.tap(find.byIcon(Icons.settings_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.text('Auto'), findsOneWidget); +// +// // Romanian +// await tester.tap(find.text('Language')); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Romanian')); +// await tester.pumpAndSettle(); +// +// expect(find.text('Setări'), findsOneWidget); +// expect(find.text('Română'), findsOneWidget); +// +// // English +// await tester.tap(find.text('Limbă')); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Engleză')); +// await tester.pumpAndSettle(); +// +// expect(find.text('Settings'), findsOneWidget); +// expect(find.text('English'), findsOneWidget); +// +// // Back to Auto (English) +// await tester.tap(find.text('Language')); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Auto')); +// await tester.pumpAndSettle(); +// +// expect(find.text('Settings'), findsOneWidget); +// expect(find.text('Auto'), findsOneWidget); +// }); +// group('Request permissions', () { +// setUpAll(() async { +// when(mockAuthProvider.currentUser).thenAnswer((_) => +// Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// }); +// +// testWidgets('Normal scenario', (WidgetTester tester) async { +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open settings +// await tester.tap(find.byIcon(Icons.settings_outlined)); +// await tester.pumpAndSettle(); +// +// // Open Ask Permissions page +// expect(find.text('No special permissions'), findsOneWidget); +// await tester.tap(find.byKey(const ValueKey('ask_permissions'))); +// await tester.pumpAndSettle(); +// expect(find.byType(RequestPermissionsPage), findsOneWidget); +// +// // Send a request +// await tester.enterText( +// find.byType(TextFormField), 'I love League of Legends'); +// await tester.tap(find.byType(Checkbox)); +// await tester.tap(find.text('Save')); +// await tester.pumpAndSettle(const Duration(seconds: 2)); +// +// // Verify the request is sent and Settings Page pops back +// verify(mockRequestProvider.makeRequest(any)); +// expect(find.byType(SettingsPage), findsOneWidget); +// }); +// +// testWidgets('User has already sent a request scenario', +// (WidgetTester tester) async { +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); +// when(mockRequestProvider.userAlreadyRequested(any)) +// .thenAnswer((_) => Future.value(true)); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open settings +// await tester.tap(find.byIcon(Icons.settings_outlined)); +// await tester.pumpAndSettle(); +// +// // Open Ask Permissions page +// expect(find.text('Permissions request already sent'), findsOneWidget); +// await tester.tap(find.byKey(const ValueKey('ask_permissions'))); +// await tester.pumpAndSettle(); +// expect(find.byType(RequestPermissionsPage), findsOneWidget); +// +// // Send a request +// await tester.enterText( +// find.byType(TextFormField), 'I love League of Legends'); +// await tester.tap(find.byType(Checkbox)); +// await tester.tap(find.text('Save')); +// await tester.pumpAndSettle(const Duration(seconds: 2)); +// +// // Check that warning Dialog appears and press Send +// expect(find.byType(AppDialog), findsOneWidget); +// await tester.tap(find.text('SEND')); +// await tester.pumpAndSettle(const Duration(seconds: 2)); +// +// // Verify the request is sent and Settings Page pops back +// verify(mockRequestProvider.makeRequest(any)); +// expect(find.byType(SettingsPage), findsOneWidget); +// }); +// +// testWidgets('User is anonymous scenario', (WidgetTester tester) async { +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); +// when(mockAuthProvider.isAnonymous).thenReturn(true); +// when(mockRequestProvider.userAlreadyRequested(any)) +// .thenAnswer((_) => Future.value(false)); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open settings +// await tester.tap(find.byIcon(Icons.settings_outlined)); +// await tester.pumpAndSettle(); +// +// // Press Ask Permissions page +// expect(find.text('No special permissions'), findsOneWidget); +// await tester.tap(find.byKey(const ValueKey('ask_permissions'))); +// await tester.pumpAndSettle(const Duration(seconds: 2)); +// +// // Verify nothing happens +// expect(find.byType(SettingsPage), findsOneWidget); +// }); +// +// testWidgets('User is not verified scenario', (WidgetTester tester) async { +// when(mockAuthProvider.isVerified) +// .thenAnswer((_) => Future.value(false)); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockRequestProvider.userAlreadyRequested(any)) +// .thenAnswer((_) => Future.value(false)); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open settings +// await tester.tap(find.byIcon(Icons.settings_outlined)); +// await tester.pumpAndSettle(); +// +// // Press Ask Permissions page +// expect(find.text('No special permissions'), findsOneWidget); +// await tester.tap(find.byKey(const ValueKey('ask_permissions'))); +// +// // Verify Ask Permissions page is not opened +// await tester.pumpAndSettle(const Duration(seconds: 4)); +// expect(find.byType(SettingsPage), findsOneWidget); +// expect(find.byType(RequestPermissionsPage), findsNothing); +// +// // Verify account +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); +// +// // Go back and open settings again +// await tester.tap(find.byIcon(Icons.arrow_back)); +// await tester.pumpAndSettle(); +// await tester.tap(find.byIcon(Icons.settings_outlined)); +// await tester.pumpAndSettle(); +// +// // Press Ask Permissions page +// expect(find.text('No special permissions'), findsOneWidget); +// await tester.tap(find.byKey(const ValueKey('ask_permissions'))); +// +// // Verify Ask Permissions page is opened +// await tester.pumpAndSettle(); +// expect(find.byType(RequestPermissionsPage), findsOneWidget); +// }); +// }); +// }); +// } diff --git a/test/test_utils.dart b/test/test_utils.dart index ef22e5c10..83399293c 100644 --- a/test/test_utils.dart +++ b/test/test_utils.dart @@ -1,283 +1,283 @@ -import 'package:acs_upb_mobile/resources/locale_provider.dart'; -import 'package:meta/meta.dart'; -import 'package:rrule/src/codecs/text/l10n/l10n.dart'; -import 'package:rrule/src/frequency.dart'; -import 'package:time_machine/time_machine.dart'; - -var testCultures = { - 'en': Culture( - 'en-US', - (DateTimeFormatBuilder() - ..amDesignator = 'AM' - ..pmDesignator = 'PM' - ..timeSeparator = ':' - ..dateSeparator = '/' - ..abbreviatedDayNames = const [ - 'Sun', - 'Mon', - 'Tue', - 'Wed', - 'Thu', - 'Fri', - 'Sat' - ] - ..dayNames = const [ - 'Sunday', - 'Monday', - 'Tuesday', - 'Wednesday', - 'Thursday', - 'Friday', - 'Saturday' - ] - ..monthNames = const [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', - '' - ] - ..abbreviatedMonthNames = const [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - '' - ] - ..monthGenitiveNames = const [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', - '' - ] - ..abbreviatedMonthGenitiveNames = const [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - '' - ] - ..calendar = CalendarType.gregorian - ..eraNames = const ['AD'] - ..fullDateTimePattern = 'dddd, MMMM d, yyyy h:mm:ss tt' - ..shortDatePattern = 'M/d/yyyy' - ..longDatePattern = 'dddd, MMMM d, yyyy' - ..shortTimePattern = 'h:mm tt' - ..longTimePattern = 'h:mm:ss tt') - .Build()) -}; - -@immutable -class RruleL10nTest extends RruleL10n { - const RruleL10nTest._(Culture culture) : super(culture); - - static Future create() async => - RruleL10nTest._(LocaleProvider.cultures['en']); - - @override - String frequencyInterval(Frequency frequency, int interval) { - String plurals({String one, String singular}) { - switch (interval) { - case 1: - return one; - case 2: - return 'Every other $singular'; - default: - return 'Every $interval ${singular}s'; - } - } - - return { - Frequency.secondly: plurals(one: 'Secondly', singular: 'second'), - Frequency.minutely: plurals(one: 'Minutely', singular: 'minute'), - Frequency.hourly: plurals(one: 'Hourly', singular: 'hour'), - Frequency.daily: plurals(one: 'Daily', singular: 'day'), - Frequency.weekly: plurals(one: 'Weekly', singular: 'week'), - Frequency.monthly: plurals(one: 'Monthly', singular: 'month'), - Frequency.yearly: plurals(one: 'Annually', singular: 'year'), - }[frequency]; - } - - @override - String until(LocalDateTime until) => - ', until ${until.toString('F', culture)}'; - - @override - String count(int count) { - switch (count) { - case 1: - return ', once'; - case 2: - return ', twice'; - default: - return ', $count times'; - } - } - - @override - String onInstances(String instances) => 'on the $instances instance'; - - @override - String inMonths(String months, {InOnVariant variant = InOnVariant.simple}) => - '${_inVariant(variant)} $months'; - - @override - String inWeeks(String weeks, {InOnVariant variant = InOnVariant.simple}) => - '${_inVariant(variant)} the $weeks week of the year'; - - String _inVariant(InOnVariant variant) { - switch (variant) { - case InOnVariant.simple: - return 'in'; - case InOnVariant.also: - return 'that are also in'; - case InOnVariant.instanceOf: - return 'of'; - default: - assert(false); - return null; - } - } - - @override - String onDaysOfWeek( - String days, { - bool indicateFrequency = false, - DaysOfWeekFrequency frequency = DaysOfWeekFrequency.monthly, - InOnVariant variant = InOnVariant.simple, - }) { - assert(variant != InOnVariant.also); - - final frequencyString = - frequency == DaysOfWeekFrequency.monthly ? 'month' : 'year'; - final suffix = indicateFrequency ? ' of the $frequencyString' : ''; - return '${_onVariant(variant)} $days$suffix'; - } - - @override - String get weekdaysString => 'weekdays'; - - @override - String get everyXDaysOfWeekPrefix => 'every '; - - @override - String nthDaysOfWeek(Iterable occurrences, String daysOfWeek) { - if (occurrences.isEmpty) { - return daysOfWeek; - } else { - final ordinals = list( - occurrences.map(ordinal).toList(), ListCombination.conjunctiveShort); - return 'the $ordinals $daysOfWeek'; - } - } - - @override - String onDaysOfMonth( - String days, { - DaysOfVariant daysOfVariant = DaysOfVariant.dayAndFrequency, - InOnVariant variant = InOnVariant.simple, - }) { - final suffix = { - DaysOfVariant.simple: '', - DaysOfVariant.day: ' day', - DaysOfVariant.dayAndFrequency: ' day of the month', - }[daysOfVariant]; - return '${_onVariant(variant)} the $days$suffix'; - } - - @override - String onDaysOfYear( - String days, { - InOnVariant variant = InOnVariant.simple, - }) => - '${_onVariant(variant)} the $days day of the year'; - - String _onVariant(InOnVariant variant) { - switch (variant) { - case InOnVariant.simple: - return 'on'; - case InOnVariant.also: - return 'that are also'; - case InOnVariant.instanceOf: - return 'of'; - default: - assert(false); - return null; - } - } - - @override - String list(List items, ListCombination combination) { - assert(items != null); - assert(combination != null); - - return RruleL10n.defaultList( - items, - two: { - ListCombination.conjunctiveShort: ' & ', - ListCombination.conjunctiveLong: ' and ', - ListCombination.disjunctive: ' or ', - }[combination], - end: { - ListCombination.conjunctiveShort: ' & ', - ListCombination.conjunctiveLong: ', and ', - ListCombination.disjunctive: ', or ', - }[combination], - ); - } - - @override - String ordinal(int number) { - assert(number != 0); - if (number == -1) { - return 'last'; - } - - final n = number.abs(); - String string; - if (n % 10 == 1 && n % 100 != 11) { - string = '${n}st'; - } else if (n % 10 == 2 && n % 100 != 12) { - string = '${n}nd'; - } else if (n % 10 == 3 && n % 100 != 13) { - string = '${n}rd'; - } else { - string = '${n}th'; - } - - return number < 0 ? '$string-to-last' : string; - } -} +// import 'package:acs_upb_mobile/resources/locale_provider.dart'; +// import 'package:meta/meta.dart'; +// import 'package:rrule/src/codecs/text/l10n/l10n.dart'; +// import 'package:rrule/src/frequency.dart'; +// import 'package:time_machine/time_machine.dart'; +// +// var testCultures = { +// 'en': Culture( +// 'en-US', +// (DateTimeFormatBuilder() +// ..amDesignator = 'AM' +// ..pmDesignator = 'PM' +// ..timeSeparator = ':' +// ..dateSeparator = '/' +// ..abbreviatedDayNames = const [ +// 'Sun', +// 'Mon', +// 'Tue', +// 'Wed', +// 'Thu', +// 'Fri', +// 'Sat' +// ] +// ..dayNames = const [ +// 'Sunday', +// 'Monday', +// 'Tuesday', +// 'Wednesday', +// 'Thursday', +// 'Friday', +// 'Saturday' +// ] +// ..monthNames = const [ +// 'January', +// 'February', +// 'March', +// 'April', +// 'May', +// 'June', +// 'July', +// 'August', +// 'September', +// 'October', +// 'November', +// 'December', +// '' +// ] +// ..abbreviatedMonthNames = const [ +// 'Jan', +// 'Feb', +// 'Mar', +// 'Apr', +// 'May', +// 'Jun', +// 'Jul', +// 'Aug', +// 'Sep', +// 'Oct', +// 'Nov', +// 'Dec', +// '' +// ] +// ..monthGenitiveNames = const [ +// 'January', +// 'February', +// 'March', +// 'April', +// 'May', +// 'June', +// 'July', +// 'August', +// 'September', +// 'October', +// 'November', +// 'December', +// '' +// ] +// ..abbreviatedMonthGenitiveNames = const [ +// 'Jan', +// 'Feb', +// 'Mar', +// 'Apr', +// 'May', +// 'Jun', +// 'Jul', +// 'Aug', +// 'Sep', +// 'Oct', +// 'Nov', +// 'Dec', +// '' +// ] +// ..calendar = CalendarType.gregorian +// ..eraNames = const ['AD'] +// ..fullDateTimePattern = 'dddd, MMMM d, yyyy h:mm:ss tt' +// ..shortDatePattern = 'M/d/yyyy' +// ..longDatePattern = 'dddd, MMMM d, yyyy' +// ..shortTimePattern = 'h:mm tt' +// ..longTimePattern = 'h:mm:ss tt') +// .Build()) +// }; +// +// @immutable +// class RruleL10nTest extends RruleL10n { +// const RruleL10nTest._(Culture culture) : super(culture); +// +// static Future create() async => +// RruleL10nTest._(LocaleProvider.cultures['en']); +// +// @override +// String frequencyInterval(Frequency frequency, int interval) { +// String plurals({String one, String singular}) { +// switch (interval) { +// case 1: +// return one; +// case 2: +// return 'Every other $singular'; +// default: +// return 'Every $interval ${singular}s'; +// } +// } +// +// return { +// Frequency.secondly: plurals(one: 'Secondly', singular: 'second'), +// Frequency.minutely: plurals(one: 'Minutely', singular: 'minute'), +// Frequency.hourly: plurals(one: 'Hourly', singular: 'hour'), +// Frequency.daily: plurals(one: 'Daily', singular: 'day'), +// Frequency.weekly: plurals(one: 'Weekly', singular: 'week'), +// Frequency.monthly: plurals(one: 'Monthly', singular: 'month'), +// Frequency.yearly: plurals(one: 'Annually', singular: 'year'), +// }[frequency]; +// } +// +// @override +// String until(LocalDateTime until) => +// ', until ${until.toString('F', culture)}'; +// +// @override +// String count(int count) { +// switch (count) { +// case 1: +// return ', once'; +// case 2: +// return ', twice'; +// default: +// return ', $count times'; +// } +// } +// +// @override +// String onInstances(String instances) => 'on the $instances instance'; +// +// @override +// String inMonths(String months, {InOnVariant variant = InOnVariant.simple}) => +// '${_inVariant(variant)} $months'; +// +// @override +// String inWeeks(String weeks, {InOnVariant variant = InOnVariant.simple}) => +// '${_inVariant(variant)} the $weeks week of the year'; +// +// String _inVariant(InOnVariant variant) { +// switch (variant) { +// case InOnVariant.simple: +// return 'in'; +// case InOnVariant.also: +// return 'that are also in'; +// case InOnVariant.instanceOf: +// return 'of'; +// default: +// assert(false); +// return null; +// } +// } +// +// @override +// String onDaysOfWeek( +// String days, { +// bool indicateFrequency = false, +// DaysOfWeekFrequency frequency = DaysOfWeekFrequency.monthly, +// InOnVariant variant = InOnVariant.simple, +// }) { +// assert(variant != InOnVariant.also); +// +// final frequencyString = +// frequency == DaysOfWeekFrequency.monthly ? 'month' : 'year'; +// final suffix = indicateFrequency ? ' of the $frequencyString' : ''; +// return '${_onVariant(variant)} $days$suffix'; +// } +// +// @override +// String get weekdaysString => 'weekdays'; +// +// @override +// String get everyXDaysOfWeekPrefix => 'every '; +// +// @override +// String nthDaysOfWeek(Iterable occurrences, String daysOfWeek) { +// if (occurrences.isEmpty) { +// return daysOfWeek; +// } else { +// final ordinals = list( +// occurrences.map(ordinal).toList(), ListCombination.conjunctiveShort); +// return 'the $ordinals $daysOfWeek'; +// } +// } +// +// @override +// String onDaysOfMonth( +// String days, { +// DaysOfVariant daysOfVariant = DaysOfVariant.dayAndFrequency, +// InOnVariant variant = InOnVariant.simple, +// }) { +// final suffix = { +// DaysOfVariant.simple: '', +// DaysOfVariant.day: ' day', +// DaysOfVariant.dayAndFrequency: ' day of the month', +// }[daysOfVariant]; +// return '${_onVariant(variant)} the $days$suffix'; +// } +// +// @override +// String onDaysOfYear( +// String days, { +// InOnVariant variant = InOnVariant.simple, +// }) => +// '${_onVariant(variant)} the $days day of the year'; +// +// String _onVariant(InOnVariant variant) { +// switch (variant) { +// case InOnVariant.simple: +// return 'on'; +// case InOnVariant.also: +// return 'that are also'; +// case InOnVariant.instanceOf: +// return 'of'; +// default: +// assert(false); +// return null; +// } +// } +// +// @override +// String list(List items, ListCombination combination) { +// assert(items != null); +// assert(combination != null); +// +// return RruleL10n.defaultList( +// items, +// two: { +// ListCombination.conjunctiveShort: ' & ', +// ListCombination.conjunctiveLong: ' and ', +// ListCombination.disjunctive: ' or ', +// }[combination], +// end: { +// ListCombination.conjunctiveShort: ' & ', +// ListCombination.conjunctiveLong: ', and ', +// ListCombination.disjunctive: ', or ', +// }[combination], +// ); +// } +// +// @override +// String ordinal(int number) { +// assert(number != 0); +// if (number == -1) { +// return 'last'; +// } +// +// final n = number.abs(); +// String string; +// if (n % 10 == 1 && n % 100 != 11) { +// string = '${n}st'; +// } else if (n % 10 == 2 && n % 100 != 12) { +// string = '${n}nd'; +// } else if (n % 10 == 3 && n % 100 != 13) { +// string = '${n}rd'; +// } else { +// string = '${n}th'; +// } +// +// return number < 0 ? '$string-to-last' : string; +// } +// } From b92d7ed322e3317b4684118c8c3b0d185f4dce4c Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Tue, 31 Aug 2021 22:38:00 +0300 Subject: [PATCH 37/60] Uncommented widgets code --- lib/pages/classes/view/class_events_card.dart | 34 ++-- .../timetable/view/events/event_widget.dart | 172 +++++++++--------- lib/widgets/event_list_tile.dart | 83 ++++----- 3 files changed, 146 insertions(+), 143 deletions(-) diff --git a/lib/pages/classes/view/class_events_card.dart b/lib/pages/classes/view/class_events_card.dart index 046ba503f..77be3ca26 100644 --- a/lib/pages/classes/view/class_events_card.dart +++ b/lib/pages/classes/view/class_events_card.dart @@ -2,6 +2,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../../../generated/l10n.dart'; +import '../../../widgets/event_list_tile.dart'; +import '../../../widgets/info_card.dart'; +import '../../timetable/model/events/uni_event.dart'; import '../../timetable/service/uni_event_provider.dart'; class ClassEventsCard extends StatefulWidget { @@ -17,21 +21,19 @@ class _ClassEventsCardState extends State { Widget build(BuildContext context) { final UniEventProvider eventProvider = Provider.of(context); - - return Container(); -// return InfoCard>( -// title: S.of(context).sectionEvents, -// padding: EdgeInsets.zero, -// future: eventProvider.getAllEventsOfClass(widget.currentClassId), -// builder: (events) => Column( -// children: events -// .map( -// (event) => EventListTile( -// uniEvent: event, -// ), -// ) -// .toList(), -// ), -// ); + return InfoCard>( + title: S.of(context).sectionEvents, + padding: EdgeInsets.zero, + future: eventProvider.getAllEventsOfClass(widget.currentClassId), + builder: (events) => Column( + children: events + .map( + (event) => EventListTile( + uniEvent: event, + ), + ) + .toList(), + ), + ); } } diff --git a/lib/pages/timetable/view/events/event_widget.dart b/lib/pages/timetable/view/events/event_widget.dart index e28c885dc..9ec26a87d 100644 --- a/lib/pages/timetable/view/events/event_widget.dart +++ b/lib/pages/timetable/view/events/event_widget.dart @@ -10,93 +10,93 @@ import 'event_view.dart'; /// Widget to display all day events in the timetable, based on /// [BasicEventWidget] from the timetable API. class UniEventWidget extends StatelessWidget { - const UniEventWidget(this.event, {Key key}) - : assert(event != null), - super(key: key); + const UniEventWidget(this.event, {Key key}) + : assert(event != null, 'event is null'), + super(key: key); - final UniEventInstance event; + final UniEventInstance event; - @override - Widget build(BuildContext context) { - final color = event.color ?? - event?.mainEvent?.color ?? - Theme.of(context).primaryColor; - final footer = - (event.location?.isNotEmpty ?? false) ? event.location : event.info; + @override + Widget build(BuildContext context) { + final color = event.color ?? + event?.mainEvent?.color ?? + Theme.of(context).primaryColor; + final footer = + (event.location?.isNotEmpty ?? false) ? event.location : event.info; - return GestureDetector( - onTap: () => Navigator.of(context).push(MaterialPageRoute( - builder: (_) => EventView(eventInstance: event), - )), - child: Material( - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).scaffoldBackgroundColor, - width: 0.75, - ), - borderRadius: BorderRadius.circular(4), - ), - color: color, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(4, 2, 4, 0), - child: AutoSizeText( - event.title ?? event.mainEvent?.classHeader?.acronym ?? '', - maxLines: 2, - minFontSize: 4, - maxFontSize: 12, - style: Theme.of(context).textTheme.bodyText2.copyWith( - fontSize: 12, - fontWeight: FontWeight.w600, - color: color.highEmphasisOnColor, - ), - ), - ), - if (event.mainEvent.type != null) - Expanded( - child: Padding( - padding: const EdgeInsets.fromLTRB(4, 2, 4, 2), - child: AutoSizeText( - event.mainEvent.type.toLocalizedString(), - wrapWords: false, - minFontSize: 10, - maxFontSize: 10, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyText2.copyWith( - fontSize: 10, - color: color.highEmphasisOnColor, - ), - ), - ), - ), - Expanded( - child: footer?.isNotEmpty ?? false - ? Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: const EdgeInsets.fromLTRB(4, 2, 4, 2), - child: AutoSizeText( - footer, - maxLines: 1, - minFontSize: 10, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyText2.copyWith( - fontSize: 12, - color: color.mediumEmphasisOnColor, - ), - ), - ), - ) - : Container(), - ), - ], - ), - ), - ); - } + return GestureDetector( + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (_) => EventView(eventInstance: event), + )), + child: Material( + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).scaffoldBackgroundColor, + width: 0.75, + ), + borderRadius: BorderRadius.circular(4), + ), + color: color, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(4, 2, 4, 0), + child: AutoSizeText( + event.title ?? event.mainEvent?.classHeader?.acronym ?? '', + maxLines: 2, + minFontSize: 4, + maxFontSize: 12, + style: Theme.of(context).textTheme.bodyText2.copyWith( + fontSize: 12, + fontWeight: FontWeight.w600, + color: color.highEmphasisOnColor, + ), + ), + ), + if (event.mainEvent.type != null) + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 2, 4, 2), + child: AutoSizeText( + event.mainEvent.type.toLocalizedString(), + wrapWords: false, + minFontSize: 10, + maxFontSize: 10, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyText2.copyWith( + fontSize: 10, + color: color.highEmphasisOnColor, + ), + ), + ), + ), + Expanded( + child: footer?.isNotEmpty ?? false + ? Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 2, 4, 2), + child: AutoSizeText( + footer, + maxLines: 1, + minFontSize: 10, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyText2.copyWith( + fontSize: 12, + color: color.mediumEmphasisOnColor, + ), + ), + ), + ) + : Container(), + ), + ], + ), + ), + ); + } } diff --git a/lib/widgets/event_list_tile.dart b/lib/widgets/event_list_tile.dart index 889d57ecc..5cc457f52 100644 --- a/lib/widgets/event_list_tile.dart +++ b/lib/widgets/event_list_tile.dart @@ -1,41 +1,42 @@ -//import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -//import 'package:acs_upb_mobile/pages/timetable/view/events/event_view.dart'; -//import 'package:flutter/cupertino.dart'; -//import 'package:flutter/material.dart'; -// -//class EventListTile extends StatelessWidget { -// const EventListTile({ -// this.uniEvent, -// }); -// -// final UniEvent uniEvent; -// -// @override -// Widget build(BuildContext context) { -// return ListTile( -// key: ValueKey(uniEvent.id), -// leading: Padding( -// padding: const EdgeInsets.all(10), -// child: Container( -// width: 20, -// height: 20, -// decoration: BoxDecoration( -// borderRadius: const BorderRadius.all(Radius.circular(4)), -// color: uniEvent.color, -// ), -// ), -// ), -// title: Text(uniEvent.type.toLocalizedString()), -// subtitle: Text( -// uniEvent.info, -// style: Theme.of(context) -// .textTheme -// .bodyText2 -// .copyWith(color: Theme.of(context).hintColor), -// ), -// onTap: () => Navigator.of(context).push(MaterialPageRoute( -// builder: (_) => EventView(uniEvent: uniEvent), -// )), -// ); -// } -//} +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import '../pages/timetable/model/events/uni_event.dart'; +import '../pages/timetable/view/events/event_view.dart'; + +class EventListTile extends StatelessWidget { + const EventListTile({ + this.uniEvent, + }); + + final UniEvent uniEvent; + + @override + Widget build(BuildContext context) { + return ListTile( + key: ValueKey(uniEvent.id), + leading: Padding( + padding: const EdgeInsets.all(10), + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(4)), + color: uniEvent.color, + ), + ), + ), + title: Text(uniEvent.type.toLocalizedString()), + subtitle: Text( + uniEvent.info, + style: Theme.of(context) + .textTheme + .bodyText2 + .copyWith(color: Theme.of(context).hintColor), + ), + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (_) => EventView(uniEvent: uniEvent), + )), + ); + } +} From 48b200c40d09340dd744dee1b257f4ea0d8c72f3 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Tue, 31 Aug 2021 22:40:10 +0300 Subject: [PATCH 38/60] Fixed errors in displaying time when adding events under new API Events are now preloaded including previous and next page (a page is a week). Fixed minor warnings --- lib/main.dart | 2 +- .../model/events/recurring_event.dart | 5 +- .../timetable/service/uni_event_provider.dart | 1 - lib/pages/timetable/view/date_header.dart | 21 +- .../timetable/view/events/add_event_view.dart | 14 +- .../view/events/all_day_event_widget.dart | 446 +++++++++--------- lib/pages/timetable/view/timetable_page.dart | 12 +- 7 files changed, 251 insertions(+), 250 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 5ee78fff6..85d2176b5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -150,7 +150,7 @@ class _MyAppState extends State { GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, S.delegate, - TimetableLocalizationsDelegate(), + const TimetableLocalizationsDelegate(), ], supportedLocales: S.delegate.supportedLocales, initialRoute: Routes.root, diff --git a/lib/pages/timetable/model/events/recurring_event.dart b/lib/pages/timetable/model/events/recurring_event.dart index 5b7e721ac..301b4db97 100644 --- a/lib/pages/timetable/model/events/recurring_event.dart +++ b/lib/pages/timetable/model/events/recurring_event.dart @@ -93,7 +93,7 @@ class RecurringUniEvent extends UniEvent { final RecurrenceRule rrule = rruleBasedOnCalendar; // Calculate recurrences - int i = 0; + // int i = 0; for (final start in rrule.getInstances(start: start.toUtc())) { final DateTime end = start.add(period.toTime().toDuration); @@ -124,8 +124,7 @@ class RecurringUniEvent extends UniEvent { location: location, ); } - - i++; + // i++; } } } diff --git a/lib/pages/timetable/service/uni_event_provider.dart b/lib/pages/timetable/service/uni_event_provider.dart index 61af5d2dc..db7eeaf58 100644 --- a/lib/pages/timetable/service/uni_event_provider.dart +++ b/lib/pages/timetable/service/uni_event_provider.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:async/async.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:dart_date/dart_date.dart' as DartDate show Interval; import 'package:flutter/material.dart'; import 'package:googleapis/calendar/v3.dart' as g_cal; import 'package:rrule/rrule.dart'; diff --git a/lib/pages/timetable/view/date_header.dart b/lib/pages/timetable/view/date_header.dart index 9928600af..329c1600d 100644 --- a/lib/pages/timetable/view/date_header.dart +++ b/lib/pages/timetable/view/date_header.dart @@ -1,13 +1,12 @@ import 'package:auto_size_text/auto_size_text.dart'; -import 'package:black_hole_flutter/black_hole_flutter.dart'; +// import 'package:black_hole_flutter/black_hole_flutter.dart'; import 'package:flutter/material.dart'; -import 'package:time_machine/time_machine_text_patterns.dart'; // ignore: implementation_imports import 'package:timetable/src/components/date_indicator.dart'; // ignore: implementation_imports -import 'package:timetable/src/theme.dart'; +// import 'package:timetable/src/theme.dart'; import '../timetable_utils.dart'; @@ -40,14 +39,14 @@ class WeekdayIndicator extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = context.theme; - final timetableTheme = TimetableTheme.of(context); - - final states = statesFor(date); - - final style = - TimetableTheme.of(context).weekdayIndicatorStyleProvider(date); - // ? + // // ? + // final theme = context.theme; + // final timetableTheme = TimetableTheme.of(context); + // + // final states = statesFor(date); + // + // final style = + // TimetableTheme.of(context).weekdayIndicatorStyleProvider(date); // final pattern = timetableTheme?.weekDayIndicatorPattern?.resolve(states) ?? // LocalDatePattern.createWithCurrentCulture('ddd'); // final decoration = diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index 984eacf75..b86d2b201 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -169,7 +169,9 @@ class _AddEventViewState extends State { final startHour = widget.initialEvent?.start?.hour ?? 8; duration = widget.initialEvent?.period?.toTime()?.toDuration ?? const Duration(hours: 2); - startTime = DateTime(startHour, 0, 0); + startTime = widget.initialEvent?.start + ?.copyWith(hour: startHour, minute: 0, second: 0, millisecond: 0) ?? + 0; List<_DayOfWeek> initialWeekDays = [ _DayOfWeek.from( @@ -429,8 +431,10 @@ class _AddEventViewState extends State { weekSelected[WeekType.odd] != weekSelected[WeekType.even] ? 2 : 1, - until: - semester.endDate.add(const Duration(days: 1)).atMidnight()); + until: semester.endDate + .add(const Duration(days: 1)) + .atMidnight() + .toUtc()); final event = ClassEvent( teacher: selectedTeacher, @@ -515,7 +519,7 @@ class _AddEventViewState extends State { child: Column( children: [ Text( - duration.toString().replaceAll(RegExp(r'[PT]'), ''), + duration.toString().substring(0, 4), style: Theme.of(context) .textTheme .bodyText1 @@ -645,7 +649,7 @@ class SelectableFormField extends FormField> { Row( children: [ Expanded( - child: Container( + child: SizedBox( height: 40, child: ListView.builder( itemCount: labels.length, diff --git a/lib/pages/timetable/view/events/all_day_event_widget.dart b/lib/pages/timetable/view/events/all_day_event_widget.dart index c0fe41afa..0ee27cf22 100644 --- a/lib/pages/timetable/view/events/all_day_event_widget.dart +++ b/lib/pages/timetable/view/events/all_day_event_widget.dart @@ -12,238 +12,238 @@ import 'event_view.dart'; /// Widget to display all day events in the timetable, based on /// [BasicAllDayEventWidget] from the timetable API. class UniAllDayEventWidget extends StatelessWidget { - const UniAllDayEventWidget( - this.event, { - @required this.info, - Key key, - this.borderRadius = 4, - }) : assert(event != null), - assert(info != null), - assert(borderRadius != null), - super(key: key); - - /// The event to be displayed. - final UniEventInstance event; - final AllDayEventLayoutInfo info; - final double borderRadius; - - @override - Widget build(BuildContext context) { - final color = event.color ?? - event?.mainEvent?.color ?? - Theme.of(context).primaryColor; - - return Padding( - padding: const EdgeInsets.all(2), - child: CustomPaint( - painter: AllDayEventBackgroundPainter( - info: info, - color: color, - borderRadius: borderRadius, - ), - child: Material( - shape: AllDayEventBorder( - info: info, - side: BorderSide.none, - borderRadius: borderRadius, - ), - clipBehavior: Clip.antiAlias, - color: Colors.transparent, - child: InkWell( - onTap: () => - Navigator.of(context).push(MaterialPageRoute( - builder: (_) => EventView(eventInstance: event), - )), - child: _buildContent(context), - ), - ), - ), - ); - } - - Widget _buildContent(BuildContext context) { - final color = event.color ?? Theme.of(context).primaryColor; - - return Padding( - padding: const EdgeInsets.fromLTRB(4, 2, 0, 2), - child: Align( - alignment: AlignmentDirectional.centerStart, - child: DefaultTextStyle( - style: context.textTheme.bodyText2.copyWith( - fontSize: 14, - color: color.highEmphasisOnColor, - ), - child: Text( - event.title ?? event.mainEvent?.classHeader?.acronym, - maxLines: 1, - ), - ), - ), - ); - } + const UniAllDayEventWidget( + this.event, { + @required this.info, + Key key, + this.borderRadius = 4, + }) : assert(event != null, 'event is null'), + assert(info != null, 'info is null'), + assert(borderRadius != null, 'border radius is null'), + super(key: key); + + /// The event to be displayed. + final UniEventInstance event; + final AllDayEventLayoutInfo info; + final double borderRadius; + + @override + Widget build(BuildContext context) { + final color = event.color ?? + event?.mainEvent?.color ?? + Theme.of(context).primaryColor; + + return Padding( + padding: const EdgeInsets.all(2), + child: CustomPaint( + painter: AllDayEventBackgroundPainter( + info: info, + color: color, + borderRadius: borderRadius, + ), + child: Material( + shape: AllDayEventBorder( + info: info, + side: BorderSide.none, + borderRadius: borderRadius, + ), + clipBehavior: Clip.antiAlias, + color: Colors.transparent, + child: InkWell( + onTap: () => + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => EventView(eventInstance: event), + )), + child: _buildContent(context), + ), + ), + ), + ); + } + + Widget _buildContent(BuildContext context) { + final color = event.color ?? Theme.of(context).primaryColor; + + return Padding( + padding: const EdgeInsets.fromLTRB(4, 2, 0, 2), + child: Align( + alignment: AlignmentDirectional.centerStart, + child: DefaultTextStyle( + style: context.textTheme.bodyText2.copyWith( + fontSize: 14, + color: color.highEmphasisOnColor, + ), + child: Text( + event.title ?? event.mainEvent?.classHeader?.acronym, + maxLines: 1, + ), + ), + ), + ); + } } class AllDayEventBackgroundPainter extends CustomPainter { - const AllDayEventBackgroundPainter({ - @required this.info, - @required this.color, - this.borderRadius = 0, - }) : assert(info != null), - assert(color != null), - assert(borderRadius != null); - - final AllDayEventLayoutInfo info; - final Color color; - final double borderRadius; - - @override - void paint(Canvas canvas, Size size) { - canvas.drawPath( - _getPath(size, info, borderRadius), - Paint()..color = color, - ); - } - - @override - bool shouldRepaint(covariant AllDayEventBackgroundPainter oldDelegate) { - return info != oldDelegate.info || - color != oldDelegate.color || - borderRadius != oldDelegate.borderRadius; - } + const AllDayEventBackgroundPainter({ + @required this.info, + @required this.color, + this.borderRadius = 0, + }) : assert(info != null, 'info is null'), + assert(color != null, 'color is null'), + assert(borderRadius != null, 'border radius is null'); + + final AllDayEventLayoutInfo info; + final Color color; + final double borderRadius; + + @override + void paint(Canvas canvas, Size size) { + canvas.drawPath( + _getPath(size, info, borderRadius), + Paint()..color = color, + ); + } + + @override + bool shouldRepaint(covariant AllDayEventBackgroundPainter oldDelegate) { + return info != oldDelegate.info || + color != oldDelegate.color || + borderRadius != oldDelegate.borderRadius; + } } /// A modified [RoundedRectangleBorder] that morphs to triangular left and/or /// right borders if not all of the event is currently visible. class AllDayEventBorder extends ShapeBorder { - const AllDayEventBorder({ - @required this.info, - this.side = BorderSide.none, - this.borderRadius = 0, - }) : assert(info != null, 'info is null'), - assert(side != null, 'side is null'), - assert(borderRadius != null, 'borderRadius is null'); - - final AllDayEventLayoutInfo info; - final BorderSide side; - final double borderRadius; - - @override - EdgeInsetsGeometry get dimensions => EdgeInsets.all(side.width); - - @override - ShapeBorder scale(double t) { - return AllDayEventBorder( - info: info, - side: side.scale(t), - borderRadius: borderRadius * t, - ); - } - - @override - Path getInnerPath(Rect rect, {TextDirection textDirection}) { - return null; - } - - @override - Path getOuterPath(Rect rect, {TextDirection textDirection}) { - return _getPath(rect.size, info, borderRadius); - } - - @override - void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) { - // For some reason, when we paint the background in this shape directly, it - // lags while scrolling. Hence, we only use it to provide the outer path - // used for clipping. - } - - @override - bool operator ==(Object other) { - if (other.runtimeType != runtimeType) { - return false; - } - return other is AllDayEventBorder && - other.info == info && - other.side == side && - other.borderRadius == borderRadius; - } - - @override - int get hashCode => hashValues(info, side, borderRadius); - - @override - String toString() => - '${objectRuntimeType(this, 'RoundedRectangleBorder')}($side, $borderRadius)'; + const AllDayEventBorder({ + @required this.info, + this.side = BorderSide.none, + this.borderRadius = 0, + }) : assert(info != null, 'info is null'), + assert(side != null, 'side is null'), + assert(borderRadius != null, 'borderRadius is null'); + + final AllDayEventLayoutInfo info; + final BorderSide side; + final double borderRadius; + + @override + EdgeInsetsGeometry get dimensions => EdgeInsets.all(side.width); + + @override + ShapeBorder scale(double t) { + return AllDayEventBorder( + info: info, + side: side.scale(t), + borderRadius: borderRadius * t, + ); + } + + @override + Path getInnerPath(Rect rect, {TextDirection textDirection}) { + return null; + } + + @override + Path getOuterPath(Rect rect, {TextDirection textDirection}) { + return _getPath(rect.size, info, borderRadius); + } + + @override + void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) { + // For some reason, when we paint the background in this shape directly, it + // lags while scrolling. Hence, we only use it to provide the outer path + // used for clipping. + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is AllDayEventBorder && + other.info == info && + other.side == side && + other.borderRadius == borderRadius; + } + + @override + int get hashCode => hashValues(info, side, borderRadius); + + @override + String toString() => + '${objectRuntimeType(this, 'RoundedRectangleBorder')}($side, $borderRadius)'; } Path _getPath(Size size, AllDayEventLayoutInfo info, double radius) { - final height = size.height; - // final radius = borderRadius.coerceAtMost(width / 2); - - final maxTipWidth = height / 4; - final leftTipWidth = info.hiddenStartDays.coerceAtMost(1) * maxTipWidth; - final rightTipWidth = info.hiddenEndDays.coerceAtMost(1) * maxTipWidth; - - final width = size.width; - // final leftTipBase = math.min(leftTipWidth + radius, width - radius); - // final rightTipBase = math.max(width - rightTipWidth - radius, radius); - final leftTipBase = info.hiddenStartDays > 0 - ? math.min(leftTipWidth + radius, width - radius) - : leftTipWidth + radius; - final rightTipBase = info.hiddenEndDays > 0 - ? math.max(width - rightTipWidth - radius, radius) - : width - rightTipWidth - radius; - - final tipSize = Size.square(radius * 2); - - // no tip: 0 ≈ 0° - // full tip: PI / 4 ≈ 45° - final leftTipAngle = math.pi / 2 - math.atan2(height / 2, leftTipWidth); - final rightTipAngle = math.pi / 2 - math.atan2(height / 2, rightTipWidth); - - return Path() - ..moveTo(leftTipBase, 0) - // Right top - ..arcTo( - Offset(rightTipBase - radius, 0) & tipSize, - math.pi * 3 / 2, - math.pi / 2 - rightTipAngle, - false, - ) - // Right tip - ..arcTo( - Offset(rightTipBase + rightTipWidth - radius, height / 2 - radius) & - tipSize, - -rightTipAngle, - 2 * rightTipAngle, - false, - ) - // Right bottom - ..arcTo( - Offset(rightTipBase - radius, height - radius * 2) & tipSize, - rightTipAngle, - math.pi / 2 - rightTipAngle, - false, - ) - // Left bottom - ..arcTo( - Offset(leftTipBase - radius, height - radius * 2) & tipSize, - math.pi / 2, - math.pi / 2 - leftTipAngle, - false, - ) - // Left tip - ..arcTo( - Offset(leftTipBase - leftTipWidth - radius, height / 2 - radius) & - tipSize, - math.pi - leftTipAngle, - 2 * leftTipAngle, - false, - ) - // Left top - ..arcTo( - Offset(leftTipBase - radius, 0) & tipSize, - math.pi + leftTipAngle, - math.pi / 2 - leftTipAngle, - false, - ); + final height = size.height; + // final radius = borderRadius.coerceAtMost(width / 2); + + final maxTipWidth = height / 4; + final leftTipWidth = info.hiddenStartDays.coerceAtMost(1) * maxTipWidth; + final rightTipWidth = info.hiddenEndDays.coerceAtMost(1) * maxTipWidth; + + final width = size.width; + // final leftTipBase = math.min(leftTipWidth + radius, width - radius); + // final rightTipBase = math.max(width - rightTipWidth - radius, radius); + final leftTipBase = info.hiddenStartDays > 0 + ? math.min(leftTipWidth + radius, width - radius) + : leftTipWidth + radius; + final rightTipBase = info.hiddenEndDays > 0 + ? math.max(width - rightTipWidth - radius, radius) + : width - rightTipWidth - radius; + + final tipSize = Size.square(radius * 2); + + // no tip: 0 ≈ 0° + // full tip: PI / 4 ≈ 45° + final leftTipAngle = math.pi / 2 - math.atan2(height / 2, leftTipWidth); + final rightTipAngle = math.pi / 2 - math.atan2(height / 2, rightTipWidth); + + return Path() + ..moveTo(leftTipBase, 0) + // Right top + ..arcTo( + Offset(rightTipBase - radius, 0) & tipSize, + math.pi * 3 / 2, + math.pi / 2 - rightTipAngle, + false, + ) + // Right tip + ..arcTo( + Offset(rightTipBase + rightTipWidth - radius, height / 2 - radius) & + tipSize, + -rightTipAngle, + 2 * rightTipAngle, + false, + ) + // Right bottom + ..arcTo( + Offset(rightTipBase - radius, height - radius * 2) & tipSize, + rightTipAngle, + math.pi / 2 - rightTipAngle, + false, + ) + // Left bottom + ..arcTo( + Offset(leftTipBase - radius, height - radius * 2) & tipSize, + math.pi / 2, + math.pi / 2 - leftTipAngle, + false, + ) + // Left tip + ..arcTo( + Offset(leftTipBase - leftTipWidth - radius, height / 2 - radius) & + tipSize, + math.pi - leftTipAngle, + 2 * leftTipAngle, + false, + ) + // Left top + ..arcTo( + Offset(leftTipBase - radius, 0) & tipSize, + math.pi + leftTipAngle, + math.pi / 2 - leftTipAngle, + false, + ); } diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index 7465a2793..9424cf066 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -21,7 +21,6 @@ import '../../filter/view/filter_page.dart'; import '../../settings/service/request_provider.dart'; import '../model/events/uni_event.dart'; import '../service/uni_event_provider.dart'; -import 'date_header.dart'; import 'events/add_event_view.dart'; import 'events/all_day_event_widget.dart'; import 'events/event_widget.dart'; @@ -116,14 +115,15 @@ class _TimetablePageState extends State Provider.of(context, listen: false) .getEventsIntersecting( DateTimeRange( - start: DateTimeTimetable.dateFromPage(value.page.floor()), + // Events are preloaded for previous, current and next page + start: DateTimeTimetable.dateFromPage(value.page.floor()) - + 7.days, end: DateTimeTimetable.dateFromPage( - value.page.ceil() + value.visibleDayCount, - ), + value.page.ceil() + value.visibleDayCount, + ) + + 7.days, ), ); - // Or probably preload events outside this range, maybe - // for the previous and next page. return StreamBuilder>( stream: events, From bfd877945b84d5ea2b3b37153b27b1f7c7a14894 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Wed, 1 Sep 2021 22:28:43 +0300 Subject: [PATCH 39/60] Updated upcoming events card, updated date format, & minor fixes --- lib/pages/home/upcoming_events_card.dart | 115 +++++++++--------- .../timetable/model/events/uni_event.dart | 4 +- lib/pages/timetable/view/timetable_page.dart | 12 +- lib/widgets/event_list_tile.dart | 64 +++++----- 4 files changed, 99 insertions(+), 96 deletions(-) diff --git a/lib/pages/home/upcoming_events_card.dart b/lib/pages/home/upcoming_events_card.dart index c39c8e5e7..cfcdddd17 100644 --- a/lib/pages/home/upcoming_events_card.dart +++ b/lib/pages/home/upcoming_events_card.dart @@ -1,57 +1,58 @@ -//import 'package:acs_upb_mobile/generated/l10n.dart'; -//import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -//import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; -//import 'package:acs_upb_mobile/pages/timetable/view/events/event_view.dart'; -//import 'package:acs_upb_mobile/widgets/info_card.dart'; -//import 'package:flutter/cupertino.dart'; -//import 'package:flutter/material.dart'; -//import 'package:provider/provider.dart'; -// -//class UpcomingEventsCard extends StatelessWidget { -// const UpcomingEventsCard({Key key, this.onShowMore}) : super(key: key); -// final void Function() onShowMore; -// -// @override -// Widget build(BuildContext context) { -// final UniEventProvider eventProvider = -// Provider.of(context); -// -// return InfoCard>( -// title: S.current.sectionEventsComingUp, -// onShowMore: onShowMore, -// future: eventProvider.getUpcomingEvents(DateTime), -// builder: (events) => Column( -// children: events -// .map( -// (event) => ListTile( -// key: ValueKey(event.id), -// contentPadding: EdgeInsets.zero, -// leading: Padding( -// padding: const EdgeInsets.all(10), -// child: Container( -// width: 20, -// height: 20, -// decoration: BoxDecoration( -// borderRadius: const BorderRadius.all(Radius.circular(4)), -// color: event.mainEvent.color, -// ), -// ), -// ), -// trailing: event.start.isBefore(DateTime.now()) -// ? Chip(label: Text(S.current.labelNow)) -// : null, -// title: Text( -// '${'${event.mainEvent.classHeader.acronym} - '}${event.mainEvent.type.toLocalizedString()}', -// ), -// subtitle: Text(event.relativeDateString), -// onTap: () => -// Navigator.of(context).push(MaterialPageRoute( -// builder: (_) => EventView(eventInstance: event), -// )), -// ), -// ) -// .toList(), -// ), -// ); -// } -//} +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../generated/l10n.dart'; +import '../../widgets/info_card.dart'; +import '../timetable/model/events/uni_event.dart'; +import '../timetable/service/uni_event_provider.dart'; +import '../timetable/view/events/event_view.dart'; + +class UpcomingEventsCard extends StatelessWidget { + const UpcomingEventsCard({Key key, this.onShowMore}) : super(key: key); + final void Function() onShowMore; + + @override + Widget build(BuildContext context) { + final UniEventProvider eventProvider = + Provider.of(context); + + return InfoCard>( + title: S.current.sectionEventsComingUp, + onShowMore: onShowMore, + future: eventProvider.getUpcomingEvents(DateTime.now()), + builder: (events) => Column( + children: events + .map( + (event) => ListTile( + key: ValueKey(event.mainEvent.id), + contentPadding: EdgeInsets.zero, + leading: Padding( + padding: const EdgeInsets.all(10), + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(4)), + color: event.mainEvent.color, + ), + ), + ), + trailing: event.start.isBefore(DateTime.now()) + ? Chip(label: Text(S.current.labelNow)) + : null, + title: Text( + '${'${event.mainEvent.classHeader.acronym} - '}${event.mainEvent.type.toLocalizedString()}', + ), + subtitle: Text(event.relativeDateString), + onTap: () => + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => EventView(eventInstance: event), + )), + ), + ) + .toList(), + ), + ); + } +} diff --git a/lib/pages/timetable/model/events/uni_event.dart b/lib/pages/timetable/model/events/uni_event.dart index b7306a1d1..7faac085c 100644 --- a/lib/pages/timetable/model/events/uni_event.dart +++ b/lib/pages/timetable/model/events/uni_event.dart @@ -202,13 +202,13 @@ class UniEventInstance extends Event { ? S.current.labelToday : useRelativeDayFormat && start.subtractDays(1).isToday ? S.current.labelTomorrow - : start.toStringWithFormat('dddd, dd MMMM'); + : start.toStringWithFormat('EEEE, dd MMMM'); if (!start.isMidnight()) { string += ' • ${start.toStringWithFormat('HH:mm')}'; } if (start.atStartOfDay != end.atStartOfDay) { - string += ' - ${end.toStringWithFormat('dddd, dd MMMM')}'; + string += ' - ${end.toStringWithFormat('EEEE, dd MMMM')}'; } if (!end.isMidnight()) { if (start.atStartOfDay != end.atStartOfDay) { diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index 9424cf066..6e9b6aa1b 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -210,11 +210,13 @@ class _TimetablePageState extends State // Show dialog if there are no events final eventProvider = Provider.of(context, listen: false); - if (eventProvider.empty) { - await showDialog( - context: context, - builder: buildDialog, - ); + if (eventProvider != null) { + if (eventProvider.empty) { + await showDialog( + context: context, + builder: buildDialog, + ); + } } }, ); diff --git a/lib/widgets/event_list_tile.dart b/lib/widgets/event_list_tile.dart index 5cc457f52..308cf96c1 100644 --- a/lib/widgets/event_list_tile.dart +++ b/lib/widgets/event_list_tile.dart @@ -5,38 +5,38 @@ import '../pages/timetable/model/events/uni_event.dart'; import '../pages/timetable/view/events/event_view.dart'; class EventListTile extends StatelessWidget { - const EventListTile({ - this.uniEvent, - }); + const EventListTile({ + this.uniEvent, + }); - final UniEvent uniEvent; + final UniEvent uniEvent; - @override - Widget build(BuildContext context) { - return ListTile( - key: ValueKey(uniEvent.id), - leading: Padding( - padding: const EdgeInsets.all(10), - child: Container( - width: 20, - height: 20, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(4)), - color: uniEvent.color, - ), - ), - ), - title: Text(uniEvent.type.toLocalizedString()), - subtitle: Text( - uniEvent.info, - style: Theme.of(context) - .textTheme - .bodyText2 - .copyWith(color: Theme.of(context).hintColor), - ), - onTap: () => Navigator.of(context).push(MaterialPageRoute( - builder: (_) => EventView(uniEvent: uniEvent), - )), - ); - } + @override + Widget build(BuildContext context) { + return ListTile( + key: ValueKey(uniEvent.id), + leading: Padding( + padding: const EdgeInsets.all(10), + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(4)), + color: uniEvent.color, + ), + ), + ), + title: Text(uniEvent.type.toLocalizedString()), + subtitle: Text( + uniEvent.info, + style: Theme.of(context) + .textTheme + .bodyText2 + .copyWith(color: Theme.of(context).hintColor), + ), + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (_) => EventView(uniEvent: uniEvent), + )), + ); + } } From 916de8985119000651f71a36fc7c2050cc4c9e8b Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Mon, 6 Sep 2021 00:43:00 +0300 Subject: [PATCH 40/60] Conversions to UTC time zone required by Timetable API To be improved, since I believe I also convert already-UTC DateTimes. --- lib/pages/classes/view/classes_page.dart | 33 ++++++------ .../model/events/recurring_event.dart | 4 +- .../service/google_calendar_services.dart | 2 +- .../timetable/service/uni_event_provider.dart | 8 ++- lib/pages/timetable/timetable_utils.dart | 50 ++++++++++++++++++- .../timetable/view/events/add_event_view.dart | 7 +-- lib/pages/timetable/view/timetable_page.dart | 22 ++++---- 7 files changed, 91 insertions(+), 35 deletions(-) diff --git a/lib/pages/classes/view/classes_page.dart b/lib/pages/classes/view/classes_page.dart index c7c3e5ce1..b499e1d34 100644 --- a/lib/pages/classes/view/classes_page.dart +++ b/lib/pages/classes/view/classes_page.dart @@ -163,9 +163,7 @@ class AddClassesPage extends StatefulWidget { } class _AddClassesPageState extends State { - _AddClassesPageState({Set classIds}) { - classIds = Set.from(widget.initialClassIds) ?? {}; - } + _AddClassesPageState(); Set classIds; Set headers; @@ -181,6 +179,7 @@ class _AddClassesPageState extends State { @override void initState() { super.initState(); + classIds = Set.from(widget.initialClassIds) ?? {}; updateClasses(); } @@ -364,15 +363,15 @@ class _Section { } class ClassListItem extends StatefulWidget { - ClassListItem( - {Key key, - this.classHeader, - this.initiallySelected = false, - void Function(bool) onSelected, - this.selectable = false, - void Function() onTap, - this.hint}) - : onSelected = onSelected ?? ((_) {}), + ClassListItem({ + Key key, + this.classHeader, + this.initiallySelected = false, + void Function(bool) onSelected, + this.selectable = false, + void Function() onTap, + this.hint, + }) : onSelected = onSelected ?? ((_) {}), onTap = onTap ?? (() {}), super(key: key); @@ -388,12 +387,16 @@ class ClassListItem extends StatefulWidget { } class _ClassListItemState extends State { - _ClassListItemState() { - selected = widget.initiallySelected; - } + _ClassListItemState(); bool selected; + @override + void initState() { + super.initState(); + selected = widget.initiallySelected; + } + @override Widget build(BuildContext context) { return ListTile( diff --git a/lib/pages/timetable/model/events/recurring_event.dart b/lib/pages/timetable/model/events/recurring_event.dart index 301b4db97..2bda8b069 100644 --- a/lib/pages/timetable/model/events/recurring_event.dart +++ b/lib/pages/timetable/model/events/recurring_event.dart @@ -119,8 +119,8 @@ class RecurringUniEvent extends UniEvent { title: name, mainEvent: this, color: color, - start: start, - end: end, + start: start.toUtc(), + end: end.toUtc(), location: location, ); } diff --git a/lib/pages/timetable/service/google_calendar_services.dart b/lib/pages/timetable/service/google_calendar_services.dart index 6581c6e41..742e34cea 100644 --- a/lib/pages/timetable/service/google_calendar_services.dart +++ b/lib/pages/timetable/service/google_calendar_services.dart @@ -11,7 +11,7 @@ import '../model/events/uni_event.dart'; import 'uni_event_provider.dart'; class GoogleCalendarServices { - GoogleCalendarServices(); + const GoogleCalendarServices(); // allows us to see, edit, share, and permanently delete all the calendars you can access using GCal static const List _scopes = [CalendarApi.calendarScope]; diff --git a/lib/pages/timetable/service/uni_event_provider.dart b/lib/pages/timetable/service/uni_event_provider.dart index db7eeaf58..73e0490fc 100644 --- a/lib/pages/timetable/service/uni_event_provider.dart +++ b/lib/pages/timetable/service/uni_event_provider.dart @@ -324,10 +324,14 @@ class UniEventProvider with ChangeNotifier { final Stream> partDay = getPartDayEventsIntersecting(interval); streams..add(allDay)..add(partDay); - final stream = StreamZip(streams); + final StreamZip> stream = StreamZip(streams); // Flatten zipped streams - return stream.map((events) => events.expand((i) => i).toList()); + return stream.map((events) => events + .expand((i) => i) + .map((UniEventInstance event) => + event.copyWith(start: event.start.toUtc(), end: event.end.toUtc())) + .toList()); } Stream> getAllDayEventsIntersecting( diff --git a/lib/pages/timetable/timetable_utils.dart b/lib/pages/timetable/timetable_utils.dart index c0eeeaca1..51995a814 100644 --- a/lib/pages/timetable/timetable_utils.dart +++ b/lib/pages/timetable/timetable_utils.dart @@ -7,12 +7,16 @@ import 'package:time_machine/time_machine.dart'; // ignore: implementation_imports import 'package:timetable/src/utils.dart'; +import 'model/events/recurring_event.dart'; +import 'model/events/uni_event.dart'; + export 'package:timetable/src/utils.dart'; extension DateTimeExtension on DateTime { bool isMidnight() => hour == 0 && minute == 0 && second == 0; - DateTime atMidnight() => DateTime(year, month, day, 0, 0, 0, 0, 0); + DateTime atMidnight() => + copyWith(hour: 0, minute: 0, second: 0, millisecond: 0); DateTime addDays(int noDays) => add(Duration(days: noDays)); @@ -37,6 +41,11 @@ extension DateTimeExtension on DateTime { minute: minute, ); } + + /// Forcing conversion to avoid hour changes from original toUtc() method from [DateTime] + DateTime toUtcForced() { + return copyWith(hour: hour, isUtc: true); + } } extension DateTimeRangeExtension on DateTimeRange { @@ -50,3 +59,42 @@ extension DurationExtension on Duration { return Period(minutes: inMinutes).normalize(); } } + +extension RecurringUniEventExtension on RecurringUniEvent { + RecurringUniEvent copyWith({ + DateTime start, + }) { + return RecurringUniEvent( + start: start ?? this.start, + period: period, + id: id, + name: name, + location: location, + color: color, + type: type, + classHeader: classHeader, + calendar: calendar, + relevance: relevance, + degree: degree, + addedBy: addedBy, + editable: editable, + rrule: rrule, + ); + } +} + +extension UniEventInstanceExtension on UniEventInstance { + UniEventInstance copyWith({ + DateTime start, + DateTime end, + }) { + return UniEventInstance( + start: start ?? this.start, + end: end ?? this.end, + title: title, + mainEvent: mainEvent, + color: color, + location: location, + info: info); + } +} diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index b86d2b201..3096675e1 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -170,7 +170,8 @@ class _AddEventViewState extends State { duration = widget.initialEvent?.period?.toTime()?.toDuration ?? const Duration(hours: 2); startTime = widget.initialEvent?.start - ?.copyWith(hour: startHour, minute: 0, second: 0, millisecond: 0) ?? + ?.copyWith(hour: startHour, minute: 0, second: 0, millisecond: 0) + ?.toUtc() ?? 0; List<_DayOfWeek> initialWeekDays = [ @@ -414,7 +415,7 @@ class _AddEventViewState extends State { onPressed: () async { if (!formKey.currentState.validate()) return; - DateTime start = semester.startDate.at(startTime); + DateTime start = semester.startDate.at(startTime).toUtcForced(); if (weekSelected[WeekType.even] && !weekSelected[WeekType.odd]) { // Event is every even week, add a week to start date start = start.addDays(7); @@ -439,7 +440,7 @@ class _AddEventViewState extends State { final event = ClassEvent( teacher: selectedTeacher, rrule: rrule, - start: start, + start: start.toUtc(), period: duration.toPeriod(), id: widget.initialEvent?.id, relevance: relevanceController.customRelevance, diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index 6e9b6aa1b..3e5e459b9 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -163,7 +163,7 @@ class _TimetablePageState extends State }, child: AddEventView( initialEvent: UniEvent( - start: dateTime, + start: dateTime.toUtc(), period: const Period(hours: 2), id: null), ), @@ -208,16 +208,16 @@ class _TimetablePageState extends State await Future.delayed(const Duration(milliseconds: 100)); // Show dialog if there are no events - final eventProvider = - Provider.of(context, listen: false); - if (eventProvider != null) { - if (eventProvider.empty) { - await showDialog( - context: context, - builder: buildDialog, - ); - } - } + // final eventProvider = + // Provider.of(context, listen: false); + // if (eventProvider != null) { + // if (eventProvider.empty) { + // await showDialog( + // context: context, + // builder: buildDialog, + // ); + // } + // } }, ); } From ae4791d8d7ddb895f84bd78e961c771fe82238d7 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Mon, 6 Sep 2021 17:18:23 +0300 Subject: [PATCH 41/60] Functional event editing --- lib/pages/timetable/timetable_utils.dart | 23 +++++++---- .../timetable/view/events/add_event_view.dart | 40 +++++++++++-------- 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/lib/pages/timetable/timetable_utils.dart b/lib/pages/timetable/timetable_utils.dart index 51995a814..c96fc771c 100644 --- a/lib/pages/timetable/timetable_utils.dart +++ b/lib/pages/timetable/timetable_utils.dart @@ -26,13 +26,22 @@ extension DateTimeExtension on DateTime { return DateFormat(format).format(this); } - DateTime at(DateTime time) { - return copyWith( - hour: time.hour, - minute: time.minute, - second: time.second, - millisecond: time.millisecond, - ); + DateTime at({DateTime dateTime, TimeOfDay timeOfDay}) { + if (dateTime != null) { + return copyWith( + hour: dateTime.hour, + minute: dateTime.minute, + second: dateTime.second, + millisecond: dateTime.millisecond); + } else if (timeOfDay != null) { + return copyWith( + hour: timeOfDay.hour, + minute: timeOfDay.minute, + second: 0, + millisecond: 0); + } else { + return null; + } } TimeOfDay toTimeOfDay() { diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index 3096675e1..0547563c4 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -56,7 +56,7 @@ class _AddEventViewState extends State { ClassHeader selectedClass; Person selectedTeacher; String selectedCalendar; - DateTime startTime; + DateTime startDateTime; Duration duration; Map weekSelected = { WeekType.odd: null, @@ -169,7 +169,7 @@ class _AddEventViewState extends State { final startHour = widget.initialEvent?.start?.hour ?? 8; duration = widget.initialEvent?.period?.toTime()?.toDuration ?? const Duration(hours: 2); - startTime = widget.initialEvent?.start + startDateTime = widget.initialEvent?.start ?.copyWith(hour: startHour, minute: 0, second: 0, millisecond: 0) ?.toUtc() ?? 0; @@ -415,7 +415,8 @@ class _AddEventViewState extends State { onPressed: () async { if (!formKey.currentState.validate()) return; - DateTime start = semester.startDate.at(startTime).toUtcForced(); + DateTime start = + semester.startDate.at(dateTime: startDateTime).toUtcForced(); if (weekSelected[WeekType.even] && !weekSelected[WeekType.odd]) { // Event is every even week, add a week to start date start = start.addDays(7); @@ -484,8 +485,10 @@ class _AddEventViewState extends State { ); Widget timeIntervalPicker() { - final endTime = startTime.add(duration); + final endDateTime = startDateTime.add(duration); final textColor = Theme.of(context).textTheme.headline4.color; + TimeOfDay startTimeOfDay = startDateTime.toTimeOfDay(); + TimeOfDay endTimeOfDay = endDateTime.toTimeOfDay(); return Padding( padding: const EdgeInsets.only(top: 10), child: Row( @@ -502,15 +505,16 @@ class _AddEventViewState extends State { padding: MaterialStateProperty.all(EdgeInsets.zero), ), onPressed: () async { - final TimeOfDay start = await showTimePicker( + startTimeOfDay = await showTimePicker( context: context, - initialTime: - TimeOfDay(hour: startTime.hour, minute: startTime.minute), + initialTime: TimeOfDay( + hour: startDateTime.hour, minute: startDateTime.minute), ); - setState(() => startTime = start.toDateTime()); + setState(() => + startDateTime = startDateTime.at(timeOfDay: startTimeOfDay)); }, child: Text( - startTime.toStringWithFormat('HH:mm'), + startDateTime.toStringWithFormat('HH:mm'), style: Theme.of(context).textTheme.headline4, ), ), @@ -520,7 +524,7 @@ class _AddEventViewState extends State { child: Column( children: [ Text( - duration.toString().substring(0, 4), + '${duration.inHours}H', style: Theme.of(context) .textTheme .bodyText1 @@ -543,14 +547,15 @@ class _AddEventViewState extends State { padding: MaterialStateProperty.all(EdgeInsets.zero), ), onPressed: () async { - final TimeOfDay end = await showTimePicker( + endTimeOfDay = await showTimePicker( context: context, - initialTime: startTime.add(duration).toTimeOfDay(), + initialTime: startDateTime.add(duration).toTimeOfDay(), ); - setState(() => duration = end.toDateTime().difference(startTime)); + setState( + () => duration = endTimeOfDay.difference(startTimeOfDay)); }, child: Text( - endTime.toStringWithFormat('HH:mm'), + endDateTime.toStringWithFormat('HH:mm'), style: Theme.of(context).textTheme.headline4, ), ), @@ -760,8 +765,11 @@ extension LocalTimeConversion on LocalTime { } extension TimeOfDayConversion on TimeOfDay { - DateTime toDateTime() => DateTime(0, 1, 1, hour, minute, 0); -// LocalTime toLocalTime() => LocalTime(hour, minute, 0); + Duration difference(TimeOfDay startTimeOfDay) { + return Duration( + hours: hour - startTimeOfDay.hour, + minutes: minute - startTimeOfDay.minute); + } } extension DateTimeComparisons on DateTime { From 7099654c455a05eceed44b9bbf9cbf74ed8cd245 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Mon, 6 Sep 2021 18:08:35 +0300 Subject: [PATCH 42/60] Canceling time picker no longer creates null exception, renamed TimeOfDay subtraction method --- lib/pages/timetable/view/events/add_event_view.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index 0547563c4..6269a7950 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -510,8 +510,9 @@ class _AddEventViewState extends State { initialTime: TimeOfDay( hour: startDateTime.hour, minute: startDateTime.minute), ); - setState(() => - startDateTime = startDateTime.at(timeOfDay: startTimeOfDay)); + setState(() => startDateTime = startTimeOfDay != null + ? startDateTime.at(timeOfDay: startTimeOfDay) + : startDateTime); }, child: Text( startDateTime.toStringWithFormat('HH:mm'), @@ -552,7 +553,7 @@ class _AddEventViewState extends State { initialTime: startDateTime.add(duration).toTimeOfDay(), ); setState( - () => duration = endTimeOfDay.difference(startTimeOfDay)); + () => duration = endTimeOfDay.subtract(startTimeOfDay)); }, child: Text( endDateTime.toStringWithFormat('HH:mm'), @@ -765,7 +766,7 @@ extension LocalTimeConversion on LocalTime { } extension TimeOfDayConversion on TimeOfDay { - Duration difference(TimeOfDay startTimeOfDay) { + Duration subtract(TimeOfDay startTimeOfDay) { return Duration( hours: hour - startTimeOfDay.hour, minutes: minute - startTimeOfDay.minute); From 85312612a27c566f2be91ef3b16846eabb40c901 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Mon, 6 Sep 2021 18:22:50 +0300 Subject: [PATCH 43/60] Animating to current time no longer zooms to show full day --- lib/pages/timetable/view/timetable_page.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index 3e5e459b9..083445dd1 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -78,9 +78,7 @@ class _TimetablePageState extends State icon: Icons.today_outlined, onPressed: () { _dateController.animateToToday(vsync: this); - _timeController.animateToShowFullDay(vsync: this); }, - // ? .animateToToday(), tooltip: S.current.actionJumpToToday, ), actions: [ From 59fbe56ecb02c365fe1da81782c2a9d7c6aa1064 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Tue, 7 Sep 2021 01:16:23 +0300 Subject: [PATCH 44/60] Treated negative duration case when editing events Fixed overflow on text from all-day events Refactoring --- .../service/google_calendar_services.dart | 7 ++-- .../timetable/service/uni_event_provider.dart | 7 ++++ lib/pages/timetable/timetable_utils.dart | 14 ++++++- .../timetable/view/events/add_event_view.dart | 10 ++++- .../timetable/view/events/event_view.dart | 12 +++--- lib/pages/timetable/view/timetable_page.dart | 41 ++++++++----------- 6 files changed, 57 insertions(+), 34 deletions(-) diff --git a/lib/pages/timetable/service/google_calendar_services.dart b/lib/pages/timetable/service/google_calendar_services.dart index 742e34cea..f0927a231 100644 --- a/lib/pages/timetable/service/google_calendar_services.dart +++ b/lib/pages/timetable/service/google_calendar_services.dart @@ -1,6 +1,7 @@ import 'package:googleapis/calendar/v3.dart' as g_cal; import 'package:googleapis/calendar/v3.dart'; import 'package:googleapis_auth/auth_io.dart'; +import 'package:rrule/rrule.dart'; import '../../../generated/l10n.dart'; import '../../../resources/utils.dart'; @@ -13,7 +14,7 @@ import 'uni_event_provider.dart'; class GoogleCalendarServices { const GoogleCalendarServices(); - // allows us to see, edit, share, and permanently delete all the calendars you can access using GCal + // Allows us to see, edit, share, and permanently delete all the calendars you can access using GCal static const List _scopes = [CalendarApi.calendarScope]; static List get scopes => _scopes; @@ -66,8 +67,8 @@ extension UniEventProviderGoogleCalendar on UniEventProvider { if (uniEvent is RecurringUniEvent) { final String rruleBasedOnCalendarString = uniEvent.rruleBasedOnCalendar - .toString() - .replaceAll(RegExp(r'T000000'), 'T000000Z'); + .toString( + options: const RecurrenceRuleToStringOptions(isTimeUtc: true)); googleCalendarEvent.recurrence = [rruleBasedOnCalendarString]; } diff --git a/lib/pages/timetable/service/uni_event_provider.dart b/lib/pages/timetable/service/uni_event_provider.dart index 73e0490fc..a32f901f5 100644 --- a/lib/pages/timetable/service/uni_event_provider.dart +++ b/lib/pages/timetable/service/uni_event_provider.dart @@ -4,6 +4,7 @@ import 'package:async/async.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:googleapis/calendar/v3.dart' as g_cal; +import 'package:recase/recase.dart'; import 'package:rrule/rrule.dart'; import 'package:time_machine/time_machine.dart'; import 'package:timetable/timetable.dart'; @@ -459,4 +460,10 @@ class UniEventProvider with ChangeNotifier { } } } + + String updateTimetablePageTitle(DateController _dateController) { + return _authProvider.isAuthenticated && !_authProvider.isAnonymous + ? _dateController?.currentMonth?.titleCase + : S.current.navigationTimetable; + } } diff --git a/lib/pages/timetable/timetable_utils.dart b/lib/pages/timetable/timetable_utils.dart index c96fc771c..3c6954001 100644 --- a/lib/pages/timetable/timetable_utils.dart +++ b/lib/pages/timetable/timetable_utils.dart @@ -6,10 +6,10 @@ import 'package:time_machine/time_machine.dart'; // ignore: implementation_imports import 'package:timetable/src/utils.dart'; +import 'package:timetable/timetable.dart'; import 'model/events/recurring_event.dart'; import 'model/events/uni_event.dart'; - export 'package:timetable/src/utils.dart'; extension DateTimeExtension on DateTime { @@ -107,3 +107,15 @@ extension UniEventInstanceExtension on UniEventInstance { info: info); } } + +extension MonthController on DateController { + String get currentMonth => DateTime( + value?.date?.year, + value?.date?.month, + 1, + 0, + 0, + 0, + ).toStringWithFormat('MMMM'); +// LocalDateTime(2020, this.value.monthOfYear, 1, 1, 1, 1).toString('MMMM'); +} diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index 6269a7950..3bb34ec61 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -553,7 +553,13 @@ class _AddEventViewState extends State { initialTime: startDateTime.add(duration).toTimeOfDay(), ); setState( - () => duration = endTimeOfDay.subtract(startTimeOfDay)); + () { + if (endTimeOfDay.subtract(startTimeOfDay).isNegative) { + endTimeOfDay = startTimeOfDay; + } + duration = endTimeOfDay.subtract(startTimeOfDay); + }, + ); }, child: Text( endDateTime.toStringWithFormat('HH:mm'), @@ -765,7 +771,7 @@ extension LocalTimeConversion on LocalTime { TimeOfDay toTimeOfDay() => TimeOfDay(hour: hourOfDay, minute: minuteOfHour); } -extension TimeOfDayConversion on TimeOfDay { +extension TimeOfDayExtension on TimeOfDay { Duration subtract(TimeOfDay startTimeOfDay) { return Duration( hours: hour - startTimeOfDay.hour, diff --git a/lib/pages/timetable/view/events/event_view.dart b/lib/pages/timetable/view/events/event_view.dart index c14f84783..87914edb1 100644 --- a/lib/pages/timetable/view/events/event_view.dart +++ b/lib/pages/timetable/view/events/event_view.dart @@ -151,11 +151,13 @@ class _EventViewState extends State { child: Icon(FeatherIcons.users), ), const SizedBox(width: 16), - Text( - mainEvent.relevance == null - ? S.current.relevanceAnyone - : '${FilterNode.localizeName(mainEvent.degree, context)}: ${mainEvent.relevance.join(', ')}', - style: Theme.of(context).textTheme.subtitle1), + Expanded( + child: Text( + mainEvent.relevance == null + ? S.current.relevanceAnyone + : '${FilterNode.localizeName(mainEvent.degree, context)}: ${mainEvent.relevance.join(', ')}', + style: Theme.of(context).textTheme.subtitle1), + ), ], ), ), diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index 083445dd1..344578165 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -5,7 +5,6 @@ import 'package:recase/recase.dart'; import 'package:supercharged/supercharged.dart'; import 'package:time_machine/time_machine.dart'; import 'package:timetable/timetable.dart'; -import 'package:intl/intl.dart'; import '../../../authentication/service/auth_provider.dart'; import '../../../generated/l10n.dart'; @@ -19,6 +18,7 @@ import '../../classes/view/classes_page.dart'; import '../../filter/service/filter_provider.dart'; import '../../filter/view/filter_page.dart'; import '../../settings/service/request_provider.dart'; +import '../../timetable/timetable_utils.dart'; import '../model/events/uni_event.dart'; import '../service/uni_event_provider.dart'; import 'events/add_event_view.dart'; @@ -47,6 +47,12 @@ class _TimetablePageState extends State @override Widget build(BuildContext context) { final authProvider = Provider.of(context); + // String timetablePageTitle = + // authProvider.isAuthenticated && !authProvider.isAnonymous + // ? _dateController?.currentMonth?.titleCase + // : S.current.navigationTimetable; + + String timetablePageTitle = S.current.navigationTimetable; _dateController ??= DateController( initialDate: DateTimeTimetable.today(), @@ -68,10 +74,9 @@ class _TimetablePageState extends State return AppScaffold( title: Builder( // ? AnimatedBuilder - builder: (context) => Text( - authProvider.isAuthenticated && !authProvider.isAnonymous - ? S.current.navigationTimetable - : _dateController.currentMonth.titleCase), + builder: (context) { + return Text(timetablePageTitle); + }, ), needsToBeAuthenticated: true, leading: AppScaffoldAction( @@ -109,7 +114,7 @@ class _TimetablePageState extends State ValueListenableBuilder( valueListenable: _dateController, builder: (context, value, child) { - final Stream> events = + final Stream> eventsInRange = Provider.of(context, listen: false) .getEventsIntersecting( DateTimeRange( @@ -123,12 +128,17 @@ class _TimetablePageState extends State ), ); + // timetablePageTitle = + // authProvider.isAuthenticated && !authProvider.isAnonymous + // ? _dateController?.currentMonth?.titleCase + // : S.current.navigationTimetable; + return StreamBuilder>( - stream: events, + stream: eventsInRange, builder: (context, AsyncSnapshot> snapshot) { if (snapshot.data == null || snapshot.hasError) { - // Handle loading and error states + // TODO(bogpie): Handle loading and error states return Container(); } @@ -143,7 +153,6 @@ class _TimetablePageState extends State allDayEventBuilder: (context, event, info) => UniAllDayEventWidget(event, info: info), callbacks: TimetableCallbacks( - // TODO(bogpie): Typing on an all day event (e.g.: holiday). onDateTimeBackgroundTap: (dateTime) { final user = Provider.of(context, listen: false) @@ -378,17 +387,3 @@ class _TimetablePageState extends State } } } - -extension MonthController on DateController { - String get currentMonth => DateFormat('MMMM').format( - DateTime( - value.date.year, - value.date.month, - 1, - 0, - 0, - 0, - ), - ); -// LocalDateTime(2020, this.value.monthOfYear, 1, 1, 1, 1).toString('MMMM'); -} From aee90507cdd3a980c46c7ee34b30486b0a619b97 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Tue, 7 Sep 2021 01:34:52 +0300 Subject: [PATCH 45/60] Timetable page title (w/ current month for page) fix --- lib/pages/timetable/view/timetable_page.dart | 22 ++++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index 344578165..47a5165a6 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -47,12 +47,6 @@ class _TimetablePageState extends State @override Widget build(BuildContext context) { final authProvider = Provider.of(context); - // String timetablePageTitle = - // authProvider.isAuthenticated && !authProvider.isAnonymous - // ? _dateController?.currentMonth?.titleCase - // : S.current.navigationTimetable; - - String timetablePageTitle = S.current.navigationTimetable; _dateController ??= DateController( initialDate: DateTimeTimetable.today(), @@ -72,10 +66,12 @@ class _TimetablePageState extends State } return AppScaffold( - title: Builder( - // ? AnimatedBuilder - builder: (context) { - return Text(timetablePageTitle); + title: AnimatedBuilder( + animation: _dateController, + builder: (context, child) { + return Text(authProvider.isAuthenticated && !authProvider.isAnonymous + ? _dateController.currentMonth.titleCase + : S.current.navigationTimetable); }, ), needsToBeAuthenticated: true, @@ -128,11 +124,6 @@ class _TimetablePageState extends State ), ); - // timetablePageTitle = - // authProvider.isAuthenticated && !authProvider.isAnonymous - // ? _dateController?.currentMonth?.titleCase - // : S.current.navigationTimetable; - return StreamBuilder>( stream: eventsInRange, builder: (context, @@ -146,7 +137,6 @@ class _TimetablePageState extends State eventProvider: eventProviderFromFixedList(snapshot.data ?? []), child: child, - // !, dateController: _dateController, timeController: _timeController, eventBuilder: (context, event) => UniEventWidget(event), From 63b829c604b759394723283b8d8a3dad46e6b800 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Tue, 7 Sep 2021 22:37:48 +0300 Subject: [PATCH 46/60] Fixed all-day event off-by one day display error. This is done by updating isUtc property for events without making hour conversions; those would shift the hour from midnight to ~9PM previous. day. --- lib/pages/home/home_page.dart | 11 +-- .../timetable/model/events/all_day_event.dart | 4 +- .../timetable/model/events/uni_event.dart | 4 +- lib/pages/timetable/view/date_header.dart | 84 ------------------- 4 files changed, 10 insertions(+), 93 deletions(-) delete mode 100644 lib/pages/timetable/view/date_header.dart diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index f510cb71d..91178f4aa 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -1,3 +1,4 @@ +import 'package:acs_upb_mobile/pages/home/upcoming_events_card.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -34,12 +35,12 @@ class HomePage extends StatelessWidget { body: ListView( children: [ if (authProvider.isAuthenticated) ProfileCard(), - if (authProvider.isAuthenticated && - !authProvider.isAnonymous && - RemoteConfigService.feedbackEnabled) + if (authProvider.isAuthenticated && !authProvider.isAnonymous + && RemoteConfigService.feedbackEnabled + ) FeedbackNudge(), - // if (authProvider.isAuthenticated && !authProvider.isAnonymous) - // UpcomingEventsCard(onShowMore: () => tabController?.animateTo(1)), + if (authProvider.isAuthenticated && !authProvider.isAnonymous) + UpcomingEventsCard(onShowMore: () => tabController?.animateTo(1)), if (authProvider.isAuthenticated && !authProvider.isAnonymous) FavouriteWebsitesCard( onShowMore: () => tabController?.animateTo(2)), diff --git a/lib/pages/timetable/model/events/all_day_event.dart b/lib/pages/timetable/model/events/all_day_event.dart index 94d894e21..f06e4de08 100644 --- a/lib/pages/timetable/model/events/all_day_event.dart +++ b/lib/pages/timetable/model/events/all_day_event.dart @@ -48,8 +48,8 @@ class AllDayUniEvent extends UniEvent { yield UniEventInstance( title: name, mainEvent: this, - start: startDate.atMidnight().toUtc(), - end: endDate.addDays(1).atMidnight().toUtc(), + start: startDate.atMidnight().toUtcForced(), + end: endDate.addDays(1).atMidnight().toUtcForced(), color: color, ); } diff --git a/lib/pages/timetable/model/events/uni_event.dart b/lib/pages/timetable/model/events/uni_event.dart index 7faac085c..4dba5e26f 100644 --- a/lib/pages/timetable/model/events/uni_event.dart +++ b/lib/pages/timetable/model/events/uni_event.dart @@ -153,8 +153,8 @@ class UniEvent { title: name, mainEvent: this, color: color, - start: start.toUtc(), - end: start.add(period.toTime().toDuration).toUtc(), + start: start.toUtcForced(), + end: start.add(period.toTime().toDuration).toUtcForced(), location: location, ); } diff --git a/lib/pages/timetable/view/date_header.dart b/lib/pages/timetable/view/date_header.dart deleted file mode 100644 index 329c1600d..000000000 --- a/lib/pages/timetable/view/date_header.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:auto_size_text/auto_size_text.dart'; -// import 'package:black_hole_flutter/black_hole_flutter.dart'; -import 'package:flutter/material.dart'; - -// ignore: implementation_imports -import 'package:timetable/src/components/date_indicator.dart'; - -// ignore: implementation_imports -// import 'package:timetable/src/theme.dart'; - -import '../timetable_utils.dart'; - -// TODO(IoanaAlexandru): This is a temporary fix because the default -// [DateHeader] from the timetable package has an overflow when the culture -// is set to Romanian. We copied it here with minor changes and it can be -// removed once the timetable package has it fixed. -class DateHeader extends StatelessWidget { - const DateHeader(this.date, {Key key}) : super(key: key); - final DateTime date; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - WeekdayIndicator(date), - const SizedBox(height: 4), - DateIndicator(date), - ], - ); - } -} - -class WeekdayIndicator extends StatelessWidget { - const WeekdayIndicator(this.date, {Key key}) : super(key: key); - - final DateTime date; - - @override - Widget build(BuildContext context) { - // // ? - // final theme = context.theme; - // final timetableTheme = TimetableTheme.of(context); - // - // final states = statesFor(date); - // - // final style = - // TimetableTheme.of(context).weekdayIndicatorStyleProvider(date); - // final pattern = timetableTheme?.weekDayIndicatorPattern?.resolve(states) ?? - // LocalDatePattern.createWithCurrentCulture('ddd'); - // final decoration = - // timetableTheme?.weekDayIndicatorDecoration?.resolve(states) ?? - // const BoxDecoration(); - // final textStyle = - // timetableTheme?.weekDayIndicatorTextStyle?.resolve(states) ?? - // TextStyle( - // color: date.isToday - // ? timetableTheme?.primaryColor ?? theme.primaryColor - // : theme.highEmphasisOnBackground, - // ); - - return const AutoSizeText('Placeholder'); - - // return DecoratedBox( - // decoration: decoration, - // child: Padding( - // padding: const EdgeInsets.all(4), - // child: AutoSizeText( - // pattern.format(date), - // style: textStyle, - // maxLines: 1, - // ), - // ), - // ); - } - - static Set statesFor(DateTime date) { - return { - if (date < DateTime.now()) MaterialState.disabled, - if (date.isToday) MaterialState.selected, - }; - } -} From 9e55ed023f8e55961d1824bf826f1eab2d9d4de2 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Tue, 7 Sep 2021 23:13:02 +0300 Subject: [PATCH 47/60] Decided to use a single utc-conversion function for consistency. However, it turned out that, while converting to Firebase Timestamp, I had to disable isUtc property to avoid an off-by-two-hours error (see toNonUtcForced() ) --- lib/pages/home/home_page.dart | 2 +- lib/pages/timetable/model/events/recurring_event.dart | 6 +++--- lib/pages/timetable/service/uni_event_provider.dart | 6 +++--- lib/pages/timetable/timetable_utils.dart | 6 +++++- lib/pages/timetable/view/events/add_event_view.dart | 6 +++--- lib/pages/timetable/view/timetable_page.dart | 2 +- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 91178f4aa..311ee55cb 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -36,7 +36,7 @@ class HomePage extends StatelessWidget { children: [ if (authProvider.isAuthenticated) ProfileCard(), if (authProvider.isAuthenticated && !authProvider.isAnonymous - && RemoteConfigService.feedbackEnabled + // && RemoteConfigService.feedbackEnabled ) FeedbackNudge(), if (authProvider.isAuthenticated && !authProvider.isAnonymous) diff --git a/lib/pages/timetable/model/events/recurring_event.dart b/lib/pages/timetable/model/events/recurring_event.dart index 2bda8b069..1133027b9 100644 --- a/lib/pages/timetable/model/events/recurring_event.dart +++ b/lib/pages/timetable/model/events/recurring_event.dart @@ -95,7 +95,7 @@ class RecurringUniEvent extends UniEvent { // Calculate recurrences // int i = 0; - for (final start in rrule.getInstances(start: start.toUtc())) { + for (final start in rrule.getInstances(start: start.toUtcForced())) { final DateTime end = start.add(period.toTime().toDuration); if (intersectingInterval != null) { if (end < intersectingInterval.start) continue; @@ -119,8 +119,8 @@ class RecurringUniEvent extends UniEvent { title: name, mainEvent: this, color: color, - start: start.toUtc(), - end: end.toUtc(), + start: start.toUtcForced(), + end: end.toUtcForced(), location: location, ); } diff --git a/lib/pages/timetable/service/uni_event_provider.dart b/lib/pages/timetable/service/uni_event_provider.dart index a32f901f5..62c9aca48 100644 --- a/lib/pages/timetable/service/uni_event_provider.dart +++ b/lib/pages/timetable/service/uni_event_provider.dart @@ -166,7 +166,7 @@ extension UniEventExtension on UniEvent { final json = { 'type': type, 'name': name, - 'start': start.toTimestamp(), + 'start': start.toNonUtcForced().toTimestamp(), 'duration': period.toJSON(), 'location': location, 'class': classHeader.id, @@ -330,8 +330,8 @@ class UniEventProvider with ChangeNotifier { // Flatten zipped streams return stream.map((events) => events .expand((i) => i) - .map((UniEventInstance event) => - event.copyWith(start: event.start.toUtc(), end: event.end.toUtc())) + .map((UniEventInstance event) => event.copyWith( + start: event.start.toUtcForced(), end: event.end.toUtcForced())) .toList()); } diff --git a/lib/pages/timetable/timetable_utils.dart b/lib/pages/timetable/timetable_utils.dart index 3c6954001..3bc4371d9 100644 --- a/lib/pages/timetable/timetable_utils.dart +++ b/lib/pages/timetable/timetable_utils.dart @@ -51,10 +51,14 @@ extension DateTimeExtension on DateTime { ); } - /// Forcing conversion to avoid hour changes from original toUtc() method from [DateTime] + /// Forcing conversion to avoid hour changes from original toUtcForced() method from [DateTime] DateTime toUtcForced() { return copyWith(hour: hour, isUtc: true); } + + DateTime toNonUtcForced() { + return copyWith(hour: hour, isUtc: false); + } } extension DateTimeRangeExtension on DateTimeRange { diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index 3bb34ec61..49fd73be3 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -171,7 +171,7 @@ class _AddEventViewState extends State { const Duration(hours: 2); startDateTime = widget.initialEvent?.start ?.copyWith(hour: startHour, minute: 0, second: 0, millisecond: 0) - ?.toUtc() ?? + ?.toUtcForced() ?? 0; List<_DayOfWeek> initialWeekDays = [ @@ -436,12 +436,12 @@ class _AddEventViewState extends State { until: semester.endDate .add(const Duration(days: 1)) .atMidnight() - .toUtc()); + .toUtcForced()); final event = ClassEvent( teacher: selectedTeacher, rrule: rrule, - start: start.toUtc(), + start: start, period: duration.toPeriod(), id: widget.initialEvent?.id, relevance: relevanceController.customRelevance, diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index 47a5165a6..3815ec2d4 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -160,7 +160,7 @@ class _TimetablePageState extends State }, child: AddEventView( initialEvent: UniEvent( - start: dateTime.toUtc(), + start: dateTime.toUtcForced(), period: const Period(hours: 2), id: null), ), From 70788fccbdf8866fe6dc68e4de1d6c65dcf82db3 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Wed, 8 Sep 2021 23:29:02 +0300 Subject: [PATCH 48/60] Improved GCal events export w/ all-day events (holidays, mid-terms, ..) --- .../service/google_calendar_services.dart | 2 +- .../timetable/service/uni_event_provider.dart | 60 +++++++++++++------ lib/pages/timetable/timetable_utils.dart | 2 +- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/lib/pages/timetable/service/google_calendar_services.dart b/lib/pages/timetable/service/google_calendar_services.dart index f0927a231..929cecbbb 100644 --- a/lib/pages/timetable/service/google_calendar_services.dart +++ b/lib/pages/timetable/service/google_calendar_services.dart @@ -61,7 +61,7 @@ extension UniEventProviderGoogleCalendar on UniEventProvider { googleCalendarEvent ..start = start ..end = end - ..summary = classHeader.acronym + ..summary = classHeader != null ? classHeader.acronym : uniEvent.name ..colorId = (uniEvent.type.googleCalendarColor.index).toString() ..location = uniEvent.location; diff --git a/lib/pages/timetable/service/uni_event_provider.dart b/lib/pages/timetable/service/uni_event_provider.dart index 62c9aca48..2ec825808 100644 --- a/lib/pages/timetable/service/uni_event_provider.dart +++ b/lib/pages/timetable/service/uni_event_provider.dart @@ -213,7 +213,6 @@ extension AcademicCalendarExtension on AcademicCalendar { } } -// extends DefaultEventProvider class UniEventProvider with ChangeNotifier { UniEventProvider({AuthProvider authProvider, PersonProvider personProvider}) : _authProvider = authProvider ?? AuthProvider(), @@ -309,10 +308,18 @@ class UniEventProvider with ChangeNotifier { Future exportToGoogleCalendar() async { final Stream> eventsStream = _events; - final List events = await eventsStream.first; + final List uniEvents = await eventsStream.first; + + final List allDayUniEvents = _calendars.values + .map(getAllDayUniEventsForCalendar) + .expand((e) => e) + .toList(); + + uniEvents.addAll(allDayUniEvents); + final List googleCalendarEvents = []; - for (final UniEvent eventInstance in events) { - final g_cal.Event googleCalendarEvent = convertEvent(eventInstance); + for (final UniEvent uniEvent in uniEvents) { + final g_cal.Event googleCalendarEvent = convertEvent(uniEvent); googleCalendarEvents.add(googleCalendarEvent); } await insertGoogleEvents(googleCalendarEvents); @@ -335,23 +342,38 @@ class UniEventProvider with ChangeNotifier { .toList()); } + Iterable getAllDayUniEventsForCalendar(AcademicCalendar cal) { + final List events = cal.holidays + cal.exams; + return events.where((event) => + event.relevance == null || + (_filter != null && + event.degree == _filter.baseNode && + event.relevance.any(_filter.relevantNodes.contains))); + } + Stream> getAllDayEventsIntersecting( DateTimeRange interval) { - return _events.map((events) => events - .map((event) => event.generateInstances(intersectingInterval: interval)) - .expand((i) => i) - .where((event) => event.isAllDay) - .followedBy(_calendars.values.map((cal) { - final List events = cal.holidays + cal.exams; - return events - .where((event) => - event.relevance == null || - (_filter != null && - event.degree == _filter.baseNode && - event.relevance.any(_filter.relevantNodes.contains))) - .map((e) => e.generateInstances(intersectingInterval: interval)) - .expand((e) => e); - }).expand((e) => e))); + return _events.map( + (events) => events + .map((event) => + event.generateInstances(intersectingInterval: interval)) + .expand((i) => i) + .where((event) => event.isAllDay) + .followedBy( + _calendars.values.map( + (AcademicCalendar cal) { + final Iterable allDayUniEvents = + getAllDayUniEventsForCalendar(cal); + final Iterable allDayUniEventInstances = + allDayUniEvents + .map((e) => + e.generateInstances(intersectingInterval: interval)) + .expand((e) => e); + return allDayUniEventInstances; + }, + ).expand((e) => e), + ), + ); } Stream> getPartDayEventsIntersecting( diff --git a/lib/pages/timetable/timetable_utils.dart b/lib/pages/timetable/timetable_utils.dart index 3bc4371d9..cd777c9ef 100644 --- a/lib/pages/timetable/timetable_utils.dart +++ b/lib/pages/timetable/timetable_utils.dart @@ -51,7 +51,7 @@ extension DateTimeExtension on DateTime { ); } - /// Forcing conversion to avoid hour changes from original toUtcForced() method from [DateTime] + /// Forcing conversion to avoid hour changes from original toUtc() function from [DateTime] DateTime toUtcForced() { return copyWith(hour: hour, isUtc: true); } From f1e1503b8f813220e70e53600d06b8f7b5f7e0dc Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Thu, 9 Sep 2021 00:00:05 +0300 Subject: [PATCH 49/60] Replaced DateTimeRange with DartDate Interval in order to be consistent with the Timetable package --- .../timetable/model/academic_calendar.dart | 17 ++++++++-------- .../timetable/model/events/all_day_event.dart | 7 ++++--- .../model/events/recurring_event.dart | 13 ++++++------ .../timetable/model/events/uni_event.dart | 7 ++++--- .../timetable/service/uni_event_provider.dart | 17 ++++++++-------- lib/pages/timetable/timetable_utils.dart | 6 ------ .../timetable/view/events/add_event_view.dart | 20 +++++++++---------- lib/pages/timetable/view/timetable_page.dart | 12 +++++------ 8 files changed, 47 insertions(+), 52 deletions(-) diff --git a/lib/pages/timetable/model/academic_calendar.dart b/lib/pages/timetable/model/academic_calendar.dart index d1095f98b..d9abd61e8 100644 --- a/lib/pages/timetable/model/academic_calendar.dart +++ b/lib/pages/timetable/model/academic_calendar.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:time_machine/time_machine.dart'; +import 'package:flutter/material.dart' hide Interval; +import 'package:time_machine/time_machine.dart' hide Interval; +import 'package:dart_date/dart_date.dart' show Interval; import '../../../resources/utils.dart'; import '../timetable_utils.dart'; @@ -18,7 +19,7 @@ class AcademicCalendar { List holidays; List exams; - Map> _getWeeksByYearInInterval(DateTimeRange interval) { + Map> _getWeeksByYearInInterval(Interval interval) { final Map> weeksByYear = {}; final rule = WeekYearRules.iso; @@ -47,7 +48,7 @@ class AcademicCalendar { for (final semester in semesters) { for (final entry in _getWeeksByYearInInterval( - DateTimeRange(start: semester.startDate, end: semester.endDate)) + Interval(semester.startDate, semester.endDate)) .entries) { weeksByYear[entry.key] ??= {}; weeksByYear[entry.key].addAll(entry.value); @@ -55,8 +56,8 @@ class AcademicCalendar { } for (final holiday in holidays) { - final DateTimeRange holidayInterval = - DateTimeRange(start: holiday.startDate, end: holiday.endDate); + final Interval holidayInterval = + Interval(holiday.startDate, holiday.endDate); final Map> holidayWeeksByYear = _getWeeksByYearInInterval(holidayInterval); @@ -74,8 +75,8 @@ class AcademicCalendar { // If the holiday includes Monday to Friday in a week, exclude week // number from [nonHolidayWeeks]. - if (holidayInterval.contains(monday) && - holidayInterval.contains(friday)) { + if (holidayInterval.includes(monday) && + holidayInterval.includes(friday)) { weeksByYear[year].remove(week); } } diff --git a/lib/pages/timetable/model/events/all_day_event.dart b/lib/pages/timetable/model/events/all_day_event.dart index f06e4de08..39ea639b1 100644 --- a/lib/pages/timetable/model/events/all_day_event.dart +++ b/lib/pages/timetable/model/events/all_day_event.dart @@ -1,5 +1,6 @@ -import 'package:flutter/material.dart'; -import 'package:time_machine/time_machine.dart'; +import 'package:flutter/material.dart' hide Interval; +import 'package:time_machine/time_machine.dart' hide Interval; +import 'package:dart_date/dart_date.dart' show Interval; import '../../../classes/model/class.dart'; import '../../timetable_utils.dart'; @@ -44,7 +45,7 @@ class AllDayUniEvent extends UniEvent { @override Iterable generateInstances( - {DateTimeRange intersectingInterval}) sync* { + {Interval intersectingInterval}) sync* { yield UniEventInstance( title: name, mainEvent: this, diff --git a/lib/pages/timetable/model/events/recurring_event.dart b/lib/pages/timetable/model/events/recurring_event.dart index 1133027b9..f7a9ac649 100644 --- a/lib/pages/timetable/model/events/recurring_event.dart +++ b/lib/pages/timetable/model/events/recurring_event.dart @@ -1,6 +1,7 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Interval; import 'package:rrule/rrule.dart'; -import 'package:time_machine/time_machine.dart'; +import 'package:time_machine/time_machine.dart' hide Interval; +import 'package:dart_date/dart_date.dart' show Interval; import '../../../../resources/locale_provider.dart'; import '../../../../resources/utils.dart'; @@ -89,7 +90,7 @@ class RecurringUniEvent extends UniEvent { @override Iterable generateInstances( - {DateTimeRange intersectingInterval}) sync* { + {Interval intersectingInterval}) sync* { final RecurrenceRule rrule = rruleBasedOnCalendar; // Calculate recurrences @@ -104,10 +105,8 @@ class RecurringUniEvent extends UniEvent { bool skip = false; for (final holiday in calendar?.holidays ?? []) { - final holidayInterval = - DateTimeRange(start: holiday.startDate, end: holiday.endDate); - // DateInterval(holiday.startDate, holiday.endDate); - if (holidayInterval.contains(start)) { + final holidayInterval = Interval(holiday.startDate, holiday.endDate); + if (holidayInterval.includes(start)) { // Skip holidays skip = true; } diff --git a/lib/pages/timetable/model/events/uni_event.dart b/lib/pages/timetable/model/events/uni_event.dart index 4dba5e26f..05d97c7bb 100644 --- a/lib/pages/timetable/model/events/uni_event.dart +++ b/lib/pages/timetable/model/events/uni_event.dart @@ -1,7 +1,8 @@ import 'dart:core'; -import 'package:flutter/material.dart'; -import 'package:time_machine/time_machine.dart'; +import 'package:flutter/material.dart' hide Interval; +import 'package:time_machine/time_machine.dart' hide Interval; +import 'package:dart_date/dart_date.dart' show Interval; import 'package:timetable/timetable.dart'; import '../../../../generated/l10n.dart'; import '../../../classes/model/class.dart'; @@ -139,7 +140,7 @@ class UniEvent { // } Iterable generateInstances( - {DateTimeRange intersectingInterval}) sync* { + {Interval intersectingInterval}) sync* { final DateTime end = start.add(period.toTime().toDuration); if (intersectingInterval != null) { if (end < intersectingInterval.start || diff --git a/lib/pages/timetable/service/uni_event_provider.dart b/lib/pages/timetable/service/uni_event_provider.dart index 2ec825808..c6d0395cb 100644 --- a/lib/pages/timetable/service/uni_event_provider.dart +++ b/lib/pages/timetable/service/uni_event_provider.dart @@ -2,12 +2,13 @@ import 'dart:async'; import 'package:async/async.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Interval; import 'package:googleapis/calendar/v3.dart' as g_cal; import 'package:recase/recase.dart'; import 'package:rrule/rrule.dart'; -import 'package:time_machine/time_machine.dart'; +import 'package:time_machine/time_machine.dart' hide Interval; import 'package:timetable/timetable.dart'; +import 'package:dart_date/dart_date.dart' show Interval; import '../../../authentication/service/auth_provider.dart'; import '../../../generated/l10n.dart'; @@ -325,7 +326,7 @@ class UniEventProvider with ChangeNotifier { await insertGoogleEvents(googleCalendarEvents); } - Stream> getEventsIntersecting(DateTimeRange interval) { + Stream> getEventsIntersecting(Interval interval) { final streams = >>[]; final Stream> allDay = getAllDayEventsIntersecting(interval); @@ -352,7 +353,7 @@ class UniEventProvider with ChangeNotifier { } Stream> getAllDayEventsIntersecting( - DateTimeRange interval) { + Interval interval) { return _events.map( (events) => events .map((event) => @@ -377,11 +378,10 @@ class UniEventProvider with ChangeNotifier { } Stream> getPartDayEventsIntersecting( - DateTimeRange interval) { + Interval interval) { return _events.map((events) => events .map((event) => event.generateInstances( - intersectingInterval: - DateTimeRange(start: interval.start, end: interval.end))) + intersectingInterval: Interval(interval.start, interval.end))) .expand((i) => i) .where((event) => event.isPartDay)); } @@ -392,8 +392,7 @@ class UniEventProvider with ChangeNotifier { .map((events) => events .where((event) => !(event is AllDayUniEvent)) .map((event) => event.generateInstances( - intersectingInterval: - DateTimeRange(start: date, end: date.addDays(6)))) + intersectingInterval: Interval(date, date.addDays(6)))) .expand((i) => i) .sortedByStartLength() .where((element) => element.end.isAfter(DateTime.now())) diff --git a/lib/pages/timetable/timetable_utils.dart b/lib/pages/timetable/timetable_utils.dart index cd777c9ef..1920c5ace 100644 --- a/lib/pages/timetable/timetable_utils.dart +++ b/lib/pages/timetable/timetable_utils.dart @@ -61,12 +61,6 @@ extension DateTimeExtension on DateTime { } } -extension DateTimeRangeExtension on DateTimeRange { - bool contains(DateTime dateTime) { - return dateTime >= start && dateTime <= end; - } -} - extension DurationExtension on Duration { Period toPeriod() { return Period(minutes: inMinutes).normalize(); diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index 49fd73be3..bbb2f4192 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -1,11 +1,12 @@ +import 'package:dart_date/dart_date.dart' show Interval; import 'package:dotted_line/dotted_line.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Interval; import 'package:flutter/rendering.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; import 'package:rrule/rrule.dart'; import 'package:time_machine/time_machine.dart' as time_machine show DayOfWeek; -import 'package:time_machine/time_machine.dart' hide DayOfWeek; +import 'package:time_machine/time_machine.dart' hide Interval; import 'package:time_machine/time_machine_text_patterns.dart'; import '../../../../authentication/model/user.dart'; @@ -106,12 +107,11 @@ class _AddEventViewState extends State { selectedCalendar = widget.initialEvent.calendar.id; final AllDayUniEvent secondSemester = widget.initialEvent.calendar.semesters.last; - selectedSemester = DateTimeRange( - start: secondSemester.startDate, - end: secondSemester.endDate) - .contains(widget.initialEvent.start) - ? 2 - : 1; + selectedSemester = + Interval(secondSemester.startDate, secondSemester.endDate) + .includes(widget.initialEvent.start) + ? 2 + : 1; } else { bool foundSemester = false; for (final calendar in calendars.entries) { @@ -781,8 +781,8 @@ extension TimeOfDayExtension on TimeOfDay { extension DateTimeComparisons on DateTime { bool isDuring(AllDayUniEvent semester) { - return DateTimeRange(start: semester.startDate, end: semester.endDate) - .contains(this); + return Interval(semester.startDate, semester.endDate) + .includes(this); } bool isBeforeOrDuring(AllDayUniEvent semester) { diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index 3815ec2d4..a797cd659 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -1,10 +1,11 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Interval; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; import 'package:recase/recase.dart'; import 'package:supercharged/supercharged.dart'; -import 'package:time_machine/time_machine.dart'; +import 'package:time_machine/time_machine.dart' hide Interval; import 'package:timetable/timetable.dart'; +import 'package:dart_date/dart_date.dart' show Interval; import '../../../authentication/service/auth_provider.dart'; import '../../../generated/l10n.dart'; @@ -113,11 +114,10 @@ class _TimetablePageState extends State final Stream> eventsInRange = Provider.of(context, listen: false) .getEventsIntersecting( - DateTimeRange( + Interval( // Events are preloaded for previous, current and next page - start: DateTimeTimetable.dateFromPage(value.page.floor()) - - 7.days, - end: DateTimeTimetable.dateFromPage( + DateTimeTimetable.dateFromPage(value.page.floor()) - 7.days, + DateTimeTimetable.dateFromPage( value.page.ceil() + value.visibleDayCount, ) + 7.days, From 129364c37fd5424bddc656bf72cf54b8216ceeba Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Sun, 12 Sep 2021 15:55:29 +0300 Subject: [PATCH 50/60] Renamed functions for clarifications --- lib/pages/timetable/model/events/all_day_event.dart | 4 ++-- lib/pages/timetable/model/events/recurring_event.dart | 6 +++--- lib/pages/timetable/model/events/uni_event.dart | 4 ++-- lib/pages/timetable/service/uni_event_provider.dart | 4 ++-- lib/pages/timetable/timetable_utils.dart | 6 +++--- lib/pages/timetable/view/events/add_event_view.dart | 6 +++--- lib/pages/timetable/view/timetable_page.dart | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/pages/timetable/model/events/all_day_event.dart b/lib/pages/timetable/model/events/all_day_event.dart index 39ea639b1..85a045ab4 100644 --- a/lib/pages/timetable/model/events/all_day_event.dart +++ b/lib/pages/timetable/model/events/all_day_event.dart @@ -49,8 +49,8 @@ class AllDayUniEvent extends UniEvent { yield UniEventInstance( title: name, mainEvent: this, - start: startDate.atMidnight().toUtcForced(), - end: endDate.addDays(1).atMidnight().toUtcForced(), + start: startDate.atMidnight().copyWithUtc(), + end: endDate.addDays(1).atMidnight().copyWithUtc(), color: color, ); } diff --git a/lib/pages/timetable/model/events/recurring_event.dart b/lib/pages/timetable/model/events/recurring_event.dart index f7a9ac649..b0d00c0f7 100644 --- a/lib/pages/timetable/model/events/recurring_event.dart +++ b/lib/pages/timetable/model/events/recurring_event.dart @@ -96,7 +96,7 @@ class RecurringUniEvent extends UniEvent { // Calculate recurrences // int i = 0; - for (final start in rrule.getInstances(start: start.toUtcForced())) { + for (final start in rrule.getInstances(start: start.copyWithUtc())) { final DateTime end = start.add(period.toTime().toDuration); if (intersectingInterval != null) { if (end < intersectingInterval.start) continue; @@ -118,8 +118,8 @@ class RecurringUniEvent extends UniEvent { title: name, mainEvent: this, color: color, - start: start.toUtcForced(), - end: end.toUtcForced(), + start: start.copyWithUtc(), + end: end.copyWithUtc(), location: location, ); } diff --git a/lib/pages/timetable/model/events/uni_event.dart b/lib/pages/timetable/model/events/uni_event.dart index 05d97c7bb..b3bc6cba6 100644 --- a/lib/pages/timetable/model/events/uni_event.dart +++ b/lib/pages/timetable/model/events/uni_event.dart @@ -154,8 +154,8 @@ class UniEvent { title: name, mainEvent: this, color: color, - start: start.toUtcForced(), - end: start.add(period.toTime().toDuration).toUtcForced(), + start: start.copyWithUtc(), + end: start.add(period.toTime().toDuration).copyWithUtc(), location: location, ); } diff --git a/lib/pages/timetable/service/uni_event_provider.dart b/lib/pages/timetable/service/uni_event_provider.dart index c6d0395cb..7fe70cac8 100644 --- a/lib/pages/timetable/service/uni_event_provider.dart +++ b/lib/pages/timetable/service/uni_event_provider.dart @@ -167,7 +167,7 @@ extension UniEventExtension on UniEvent { final json = { 'type': type, 'name': name, - 'start': start.toNonUtcForced().toTimestamp(), + 'start': start.copyWithoutUtc().toTimestamp(), 'duration': period.toJSON(), 'location': location, 'class': classHeader.id, @@ -339,7 +339,7 @@ class UniEventProvider with ChangeNotifier { return stream.map((events) => events .expand((i) => i) .map((UniEventInstance event) => event.copyWith( - start: event.start.toUtcForced(), end: event.end.toUtcForced())) + start: event.start.copyWithUtc(), end: event.end.copyWithUtc())) .toList()); } diff --git a/lib/pages/timetable/timetable_utils.dart b/lib/pages/timetable/timetable_utils.dart index 1920c5ace..035d48ee9 100644 --- a/lib/pages/timetable/timetable_utils.dart +++ b/lib/pages/timetable/timetable_utils.dart @@ -51,12 +51,12 @@ extension DateTimeExtension on DateTime { ); } - /// Forcing conversion to avoid hour changes from original toUtc() function from [DateTime] - DateTime toUtcForced() { + /// Returns the same DateTime with isUtc set as true to avoid hour changes from original toUtc() function of [DateTime] + DateTime copyWithUtc() { return copyWith(hour: hour, isUtc: true); } - DateTime toNonUtcForced() { + DateTime copyWithoutUtc() { return copyWith(hour: hour, isUtc: false); } } diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index bbb2f4192..5580dcf66 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -171,7 +171,7 @@ class _AddEventViewState extends State { const Duration(hours: 2); startDateTime = widget.initialEvent?.start ?.copyWith(hour: startHour, minute: 0, second: 0, millisecond: 0) - ?.toUtcForced() ?? + ?.copyWithUtc() ?? 0; List<_DayOfWeek> initialWeekDays = [ @@ -416,7 +416,7 @@ class _AddEventViewState extends State { if (!formKey.currentState.validate()) return; DateTime start = - semester.startDate.at(dateTime: startDateTime).toUtcForced(); + semester.startDate.at(dateTime: startDateTime).copyWithUtc(); if (weekSelected[WeekType.even] && !weekSelected[WeekType.odd]) { // Event is every even week, add a week to start date start = start.addDays(7); @@ -436,7 +436,7 @@ class _AddEventViewState extends State { until: semester.endDate .add(const Duration(days: 1)) .atMidnight() - .toUtcForced()); + .copyWithUtc()); final event = ClassEvent( teacher: selectedTeacher, diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index a797cd659..62cf65832 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -160,7 +160,7 @@ class _TimetablePageState extends State }, child: AddEventView( initialEvent: UniEvent( - start: dateTime.toUtcForced(), + start: dateTime.copyWithUtc(), period: const Period(hours: 2), id: null), ), From a56a29c97558b608c2317c2ddebc871a7287e87f Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Sun, 12 Sep 2021 16:09:18 +0300 Subject: [PATCH 51/60] Removed try-catch inside RemoteConfigService initialization for future debugging --- lib/pages/home/home_page.dart | 2 +- lib/resources/remote_config.dart | 12 ++- pubspec.lock | 155 ++++++++++++++++++------------- pubspec.yaml | 4 +- 4 files changed, 98 insertions(+), 75 deletions(-) diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 311ee55cb..98a8f5933 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -36,7 +36,7 @@ class HomePage extends StatelessWidget { children: [ if (authProvider.isAuthenticated) ProfileCard(), if (authProvider.isAuthenticated && !authProvider.isAnonymous - // && RemoteConfigService.feedbackEnabled + && RemoteConfigService.feedbackEnabled ) FeedbackNudge(), if (authProvider.isAuthenticated && !authProvider.isAnonymous) diff --git a/lib/resources/remote_config.dart b/lib/resources/remote_config.dart index 332d5c31c..570e63386 100644 --- a/lib/resources/remote_config.dart +++ b/lib/resources/remote_config.dart @@ -14,13 +14,15 @@ class RemoteConfigService { : _remoteConfig?.getBool(_feedbackEnabled) ?? defaults[_feedbackEnabled]; static Future initialize() async { - try { + // try { _remoteConfig = RemoteConfig.instance; await _remoteConfig.setDefaults(defaults); await _remoteConfig.fetchAndActivate(); - } catch (e) { - print( - 'Unable to fetch remote config. Cached or default values will be used.'); - } + // } catch (e) { + // print( + // 'Unable to fetch remote config. Cached or default values will be used.'); + // } + + // ? debug } } diff --git a/pubspec.lock b/pubspec.lock index 915543118..f0b0c0500 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -35,7 +35,7 @@ packages: name: args url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.2.0" async: dependency: "direct main" description: @@ -56,7 +56,7 @@ packages: name: basic_utils url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "3.6.0" black_hole_flutter: dependency: transitive description: @@ -77,28 +77,42 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" built_collection: dependency: transitive description: name: built_collection url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "5.1.1" built_value: dependency: transitive description: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.0.6" + version: "8.1.2" cached_network_image: dependency: "direct main" description: name: cached_network_image url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.1.0" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" characters: dependency: transitive description: @@ -119,7 +133,7 @@ packages: name: cli_util url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.3.3" clock: dependency: transitive description: @@ -133,28 +147,28 @@ packages: name: cloud_firestore url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.5.2" cloud_firestore_platform_interface: dependency: transitive description: name: cloud_firestore_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "5.1.1" + version: "5.4.1" cloud_firestore_web: dependency: transitive description: name: cloud_firestore_web url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.4.2" code_builder: dependency: transitive description: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.1.0" collection: dependency: transitive description: @@ -168,7 +182,14 @@ packages: name: convert url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" + cross_file: + dependency: transitive + description: + name: cross_file + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1+5" crypto: dependency: transitive description: @@ -203,7 +224,7 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.1.0" dartx: dependency: "direct main" description: @@ -259,7 +280,7 @@ packages: name: file url: "https://pub.dartlang.org" source: hosted - version: "6.1.1" + version: "6.1.2" firebase: dependency: transitive description: @@ -273,7 +294,7 @@ packages: name: firebase_analytics url: "https://pub.dartlang.org" source: hosted - version: "8.1.1" + version: "8.3.2" firebase_analytics_platform_interface: dependency: transitive description: @@ -294,28 +315,28 @@ packages: name: firebase_auth url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.4.1" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "4.2.4" + version: "4.3.1" firebase_auth_web: dependency: transitive description: name: firebase_auth_web url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" firebase_core: dependency: "direct main" description: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.6.0" firebase_core_platform_interface: dependency: transitive description: @@ -336,35 +357,35 @@ packages: name: firebase_remote_config url: "https://pub.dartlang.org" source: hosted - version: "0.10.0+1" + version: "0.11.0" firebase_remote_config_platform_interface: dependency: transitive description: name: firebase_remote_config_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "0.3.0+1" + version: "0.3.0+5" firebase_storage: dependency: "direct main" description: name: firebase_storage url: "https://pub.dartlang.org" source: hosted - version: "8.1.1" + version: "8.1.3" firebase_storage_platform_interface: dependency: transitive description: name: firebase_storage_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" firebase_storage_web: dependency: transitive description: name: firebase_storage_web url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.2" fixnum: dependency: transitive description: @@ -390,7 +411,7 @@ packages: name: flutter_cache_manager url: "https://pub.dartlang.org" source: hosted - version: "3.1.1" + version: "3.1.2" flutter_colorpicker: dependency: "direct main" description: @@ -411,14 +432,14 @@ packages: name: flutter_launcher_icons url: "https://pub.dartlang.org" source: hosted - version: "0.9.0" + version: "0.9.2" flutter_layout_grid: dependency: transitive description: name: flutter_layout_grid url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.3" flutter_localizations: dependency: "direct main" description: flutter @@ -430,21 +451,21 @@ packages: name: flutter_markdown url: "https://pub.dartlang.org" source: hosted - version: "0.6.2" + version: "0.6.6" flutter_native_splash: dependency: "direct dev" description: name: flutter_native_splash url: "https://pub.dartlang.org" source: hosted - version: "1.1.8+4" + version: "1.2.3" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" flutter_test: dependency: "direct dev" description: flutter @@ -510,35 +531,35 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.0.3" image_picker: dependency: "direct main" description: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.8.0+1" + version: "0.8.4" image_picker_for_web: dependency: transitive description: name: image_picker_for_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.3" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.4.1" image_picker_web: dependency: "direct main" description: name: image_picker_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3+1" intl: dependency: transitive description: @@ -594,7 +615,7 @@ packages: name: mockito url: "https://pub.dartlang.org" source: hosted - version: "5.0.9" + version: "5.0.15" nested: dependency: transitive description: @@ -622,7 +643,7 @@ packages: name: oktoast url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.1.1" package_config: dependency: transitive description: @@ -636,14 +657,14 @@ packages: name: package_info_plus url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.6" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" package_info_plus_macos: dependency: transitive description: @@ -657,21 +678,21 @@ packages: name: package_info_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.4" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.3" path: dependency: transitive description: @@ -685,21 +706,21 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" path_provider_platform_interface: dependency: transitive description: @@ -713,7 +734,7 @@ packages: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.3" pedantic: dependency: transitive description: @@ -741,21 +762,21 @@ packages: name: platform url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.2" plugin_platform_interface: dependency: "direct dev" description: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" pointycastle: dependency: transitive description: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "3.1.1" + version: "3.3.4" positioned_tap_detector_2: dependency: "direct main" description: @@ -769,14 +790,14 @@ packages: name: pref url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.4.0" process: dependency: transitive description: name: process url: "https://pub.dartlang.org" source: hosted - version: "4.2.1" + version: "4.2.3" provider: dependency: "direct dev" description: @@ -825,21 +846,21 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.7" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" shared_preferences_platform_interface: dependency: transitive description: @@ -853,14 +874,14 @@ packages: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" sky_engine: dependency: transitive description: flutter @@ -872,7 +893,7 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.3" source_span: dependency: transitive description: @@ -886,14 +907,14 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.0.0+3" + version: "2.0.0+4" sqflite_common: dependency: transitive description: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.0.0+2" + version: "2.0.1+1" stack_trace: dependency: transitive description: @@ -979,7 +1000,7 @@ packages: name: timeago url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.1.0" timetable: dependency: "direct main" description: @@ -1014,42 +1035,42 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.6" + version: "6.0.10" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.4" url_launcher_windows: dependency: transitive description: name: url_launcher_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" uuid: dependency: transitive description: @@ -1091,7 +1112,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.2.9" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 05cb1356b..ff03827ac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,8 +56,8 @@ dependencies: # Firebase products firebase_analytics: ^8.1.1 firebase_auth: ^1.3.0 - firebase_core: ^1.2.1 - firebase_remote_config: ^0.10.0 + firebase_core: ^1.6.0 + firebase_remote_config: ^0.11.0 firebase_storage: ^8.1.1 # Flutter SDK From bc97ffb87f2aadf839c7487fd991895b94ec5352 Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Sun, 12 Sep 2021 16:20:16 +0300 Subject: [PATCH 52/60] Moved exporting of all-day events in google calendar to another branch --- lib/pages/timetable/service/uni_event_provider.dart | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/pages/timetable/service/uni_event_provider.dart b/lib/pages/timetable/service/uni_event_provider.dart index 7fe70cac8..4f898bd8e 100644 --- a/lib/pages/timetable/service/uni_event_provider.dart +++ b/lib/pages/timetable/service/uni_event_provider.dart @@ -311,13 +311,6 @@ class UniEventProvider with ChangeNotifier { final Stream> eventsStream = _events; final List uniEvents = await eventsStream.first; - final List allDayUniEvents = _calendars.values - .map(getAllDayUniEventsForCalendar) - .expand((e) => e) - .toList(); - - uniEvents.addAll(allDayUniEvents); - final List googleCalendarEvents = []; for (final UniEvent uniEvent in uniEvents) { final g_cal.Event googleCalendarEvent = convertEvent(uniEvent); From a92442e2dd7bd0c9e584f923b9744d5eba99423f Mon Sep 17 00:00:00 2001 From: Bogdan Piele Date: Tue, 14 Sep 2021 15:14:49 +0300 Subject: [PATCH 53/60] Disabled feedback for web - RemoteConfig is not supported on web --- lib/pages/classes/view/class_view.dart | 3 +- lib/pages/classes/view/classes_page.dart | 213 +++++++++++++---------- lib/pages/home/home_page.dart | 6 +- lib/resources/remote_config.dart | 13 +- lib/resources/utils.dart | 7 + 5 files changed, 136 insertions(+), 106 deletions(-) diff --git a/lib/pages/classes/view/class_view.dart b/lib/pages/classes/view/class_view.dart index 5cbe5f60e..9bf1a3198 100644 --- a/lib/pages/classes/view/class_view.dart +++ b/lib/pages/classes/view/class_view.dart @@ -7,7 +7,6 @@ import 'package:provider/provider.dart'; import '../../../authentication/service/auth_provider.dart'; import '../../../generated/l10n.dart'; -import '../../../resources/remote_config.dart'; import '../../../resources/utils.dart'; import '../../../widgets/button.dart'; import '../../../widgets/class_icon.dart'; @@ -60,7 +59,7 @@ class _ClassViewState extends State { return AppScaffold( title: Text(S.current.navigationClassInfo), actions: [ - if (RemoteConfigService.feedbackEnabled) + if (Utils.feedbackEnabled) AppScaffoldAction( icon: Icons.rate_review_outlined, tooltip: S.current.navigationClassFeedback, diff --git a/lib/pages/classes/view/classes_page.dart b/lib/pages/classes/view/classes_page.dart index b499e1d34..b31b0c599 100644 --- a/lib/pages/classes/view/classes_page.dart +++ b/lib/pages/classes/view/classes_page.dart @@ -4,7 +4,7 @@ import 'package:provider/provider.dart'; import '../../../authentication/service/auth_provider.dart'; import '../../../generated/l10n.dart'; -import '../../../resources/remote_config.dart'; +import '../../../resources/utils.dart'; import '../../../widgets/class_icon.dart'; import '../../../widgets/error_page.dart'; import '../../../widgets/icon_text.dart'; @@ -34,9 +34,9 @@ class _ClassesPageState extends State { } final ClassProvider classProvider = - Provider.of(context, listen: false); + Provider.of(context, listen: false); final AuthProvider authProvider = - Provider.of(context, listen: false); + Provider.of(context, listen: false); headers = (await classProvider.fetchClassHeaders(uid: authProvider.uid)).toSet(); @@ -63,45 +63,50 @@ class _ClassesPageState extends State { // TODO(IoanaAlexandru): Simply show all classes if user is not authenticated needsToBeAuthenticated: true, actions: [ - if (RemoteConfigService.feedbackEnabled) + if (Utils.feedbackEnabled) AppScaffoldAction( icon: Icons.rate_review_outlined, tooltip: S.current.navigationClassesFeedbackChecklist, - onPressed: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => ClassFeedbackChecklist(classes: headers), - ), - ), + onPressed: () => + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ClassFeedbackChecklist(classes: headers), + ), + ), ), AppScaffoldAction( icon: Icons.edit_outlined, tooltip: S.current.actionChooseClasses, - onPressed: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => ChangeNotifierProvider.value( - value: classProvider, - child: FutureBuilder( - future: classProvider.fetchUserClassIds(authProvider.uid), - builder: (context, snap) { - if (snap.hasData) { - return AddClassesPage( - initialClassIds: snap.data, - onSave: (classIds) async { - await classProvider.setUserClassIds( - classIds, authProvider.uid); - unawaited(updateClasses()); - if (!mounted) { - return; + onPressed: () => + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => + ChangeNotifierProvider.value( + value: classProvider, + child: FutureBuilder( + future: classProvider.fetchUserClassIds( + authProvider.uid), + builder: (context, snap) { + if (snap.hasData) { + return AddClassesPage( + initialClassIds: snap.data, + onSave: (classIds) async { + await classProvider.setUserClassIds( + classIds, authProvider.uid); + unawaited(updateClasses()); + if (!mounted) { + return; + } + Navigator.pop(context); + }); + } else { + return const Center( + child: CircularProgressIndicator()); } - Navigator.pop(context); - }); - } else { - return const Center(child: CircularProgressIndicator()); - } - }, - )), - ), - ), + }, + )), + ), + ), ), ], body: Stack( @@ -109,41 +114,49 @@ class _ClassesPageState extends State { if (updating != null) headers != null && headers.isNotEmpty ? ClassList( - classes: headers, - sectioned: false, - onTap: (classHeader) => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ChangeNotifierProvider.value( - value: classProvider, - child: ClassView( - classHeader: classHeader, + classes: headers, + sectioned: false, + onTap: (classHeader) => + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + ChangeNotifierProvider.value( + value: classProvider, + child: ClassView( + classHeader: classHeader, + ), ), - ), - ), ), - ) + ), + ) : ErrorPage( - errorMessage: S.current.messageNoClassesYet, - imgPath: 'assets/illustrations/undraw_empty.png', - info: [ - TextSpan( - text: '${S.current.messageGetStartedByPressing} '), - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Icon( - Icons.edit_outlined, - size: - Theme.of(context).textTheme.subtitle1.fontSize + - 2, - ), - ), - TextSpan(text: ' ${S.current.messageButtonAbove}.'), - ]), + errorMessage: S.current.messageNoClassesYet, + imgPath: 'assets/illustrations/undraw_empty.png', + info: [ + TextSpan( + text: '${S.current.messageGetStartedByPressing} '), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon( + Icons.edit_outlined, + size: + Theme + .of(context) + .textTheme + .subtitle1 + .fontSize + + 2, + ), + ), + TextSpan(text: ' ${S.current.messageButtonAbove}.'), + ]), if (updating == null) const Center(child: CircularProgressIndicator()), if (updating == true) Container( - color: Theme.of(context).disabledColor, + color: Theme + .of(context) + .disabledColor, child: const Center(child: CircularProgressIndicator())), ], ), @@ -210,13 +223,12 @@ class _AddClassesPageState extends State { } class ClassList extends StatefulWidget { - ClassList( - {this.classes, - void Function(bool, String) onSelected, - Set initiallySelected, - this.selectable = false, - this.sectioned = true, - void Function(ClassHeader) onTap}) + ClassList({this.classes, + void Function(bool, String) onSelected, + Set initiallySelected, + this.selectable = false, + this.sectioned = true, + void Function(ClassHeader) onTap}) : onSelected = onSelected ?? ((selected, classId) {}), onTap = onTap ?? ((_) {}), initiallySelected = initiallySelected ?? {}; @@ -244,8 +256,8 @@ class _ClassListState extends State { String sectionName(BuildContext context, String year, String semester) => '${S.current.labelYear} $year, ${S.current.labelSemester} $semester'; - Map classesBySection( - Set classes, BuildContext context) { + Map classesBySection(Set classes, + BuildContext context) { final map = {}; for (final c in classes) { @@ -279,8 +291,8 @@ class _ClassListState extends State { children.addAll(values.map(buildClassItem)); expanded = values.fold( false, - (dynamic selected, ClassHeader header) => - selected || widget.initiallySelected.contains(header.id)); + (dynamic selected, ClassHeader header) => + selected || widget.initiallySelected.contains(header.id)); } else { final s = buildSections(context, sections[section], level: level + 1); expanded = expanded || s.containsSelected; @@ -302,7 +314,8 @@ class _ClassListState extends State { return _Section(widgets: children, containsSelected: expanded); } - Widget buildClassItem(ClassHeader header) => Column( + Widget buildClassItem(ClassHeader header) => + Column( children: [ ClassListItem( selectable: widget.selectable, @@ -334,7 +347,10 @@ class _ClassListState extends State { child: IconText( icon: Icons.info_outlined, text: '${S.current.infoSelect} ${S.current.infoClasses}.', - style: Theme.of(context).textTheme.bodyText1, + style: Theme + .of(context) + .textTheme + .bodyText1, ), ), Padding( @@ -342,8 +358,8 @@ class _ClassListState extends State { child: Column( children: widget.sectioned ? (buildSections( - context, classesBySection(widget.classes, context))) - .widgets + context, classesBySection(widget.classes, context))) + .widgets : widget.classes.map(buildClassItem).toList()), ), ], @@ -371,7 +387,8 @@ class ClassListItem extends StatefulWidget { this.selectable = false, void Function() onTap, this.hint, - }) : onSelected = onSelected ?? ((_) {}), + }) + : onSelected = onSelected ?? ((_) {}), onTap = onTap ?? (() {}), super(key: key); @@ -408,24 +425,32 @@ class _ClassListItemState extends State { widget.classHeader.name, style: widget.selectable ? (selected - ? Theme.of(context) - .textTheme - .subtitle1 - .copyWith(fontWeight: FontWeight.bold) - : Theme.of(context) - .textTheme - .subtitle1 - .copyWith(color: Theme.of(context).disabledColor)) - : Theme.of(context).textTheme.subtitle1, + ? Theme + .of(context) + .textTheme + .subtitle1 + .copyWith(fontWeight: FontWeight.bold) + : Theme + .of(context) + .textTheme + .subtitle1 + .copyWith(color: Theme + .of(context) + .disabledColor)) + : Theme + .of(context) + .textTheme + .subtitle1, ), subtitle: widget.hint != null ? Text(widget.hint) : null, - onTap: () => setState(() { - if (widget.selectable) { - selected = !selected; - widget.onSelected(selected); - } - widget.onTap(); - }), + onTap: () => + setState(() { + if (widget.selectable) { + selected = !selected; + widget.onSelected(selected); + } + widget.onTap(); + }), ); } } diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 98a8f5933..7bb865a39 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -1,4 +1,3 @@ -import 'package:acs_upb_mobile/pages/home/upcoming_events_card.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -6,13 +5,14 @@ import 'package:provider/provider.dart'; import '../../authentication/service/auth_provider.dart'; import '../../generated/l10n.dart'; import '../../navigation/routes.dart'; -import '../../resources/remote_config.dart'; +import '../../resources/utils.dart'; import '../../widgets/scaffold.dart'; import 'faq_card.dart'; import 'favourite_websites_card.dart'; import 'feedback_nudge.dart'; import 'news_feed_card.dart'; import 'profile_card.dart'; +import 'upcoming_events_card.dart'; class HomePage extends StatelessWidget { const HomePage({this.tabController, Key key}) : super(key: key); @@ -36,7 +36,7 @@ class HomePage extends StatelessWidget { children: [ if (authProvider.isAuthenticated) ProfileCard(), if (authProvider.isAuthenticated && !authProvider.isAnonymous - && RemoteConfigService.feedbackEnabled + && Utils.feedbackEnabled ) FeedbackNudge(), if (authProvider.isAuthenticated && !authProvider.isAnonymous) diff --git a/lib/resources/remote_config.dart b/lib/resources/remote_config.dart index 570e63386..35b54a8b4 100644 --- a/lib/resources/remote_config.dart +++ b/lib/resources/remote_config.dart @@ -14,15 +14,14 @@ class RemoteConfigService { : _remoteConfig?.getBool(_feedbackEnabled) ?? defaults[_feedbackEnabled]; static Future initialize() async { - // try { + try { _remoteConfig = RemoteConfig.instance; await _remoteConfig.setDefaults(defaults); await _remoteConfig.fetchAndActivate(); - // } catch (e) { - // print( - // 'Unable to fetch remote config. Cached or default values will be used.'); - // } - - // ? debug + } catch (e) { + print( + 'Unable to fetch remote config. Cached or default values will be used.'); + } + // Does not work on web } } diff --git a/lib/resources/utils.dart b/lib/resources/utils.dart index 0f496c3f8..7dba6d444 100644 --- a/lib/resources/utils.dart +++ b/lib/resources/utils.dart @@ -9,6 +9,8 @@ import '../authentication/service/auth_provider.dart'; import '../generated/l10n.dart'; import '../navigation/routes.dart'; import '../widgets/toast.dart'; +import 'platform.dart'; +import 'remote_config.dart'; export 'package:acs_upb_mobile/resources/platform.dart' if (dart.library.io) 'dart:io'; @@ -69,4 +71,9 @@ class Utils { appName: '\$appName', packageName: '\$packageName', ); + + static bool get feedbackEnabled { + if (!Platform.isAndroid && !Platform.isIOS) return false; + return RemoteConfigService.feedbackEnabled; + } } From 6e8e6e8fcde5e1ea7eeaaad5ac9613b8e19b26fa Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Sun, 19 Sep 2021 17:03:50 +0200 Subject: [PATCH 54/60] Migrate to new flutter theme. --- .../view/edit_profile_page.dart | 4 +- lib/authentication/view/login_view.dart | 9 ++-- lib/authentication/view/sign_up_view.dart | 5 +- lib/main.dart | 4 +- lib/navigation/bottom_navigation_bar.dart | 2 +- .../view/feedback_question.dart | 2 +- lib/pages/filter/view/relevance_picker.dart | 4 +- lib/pages/home/feedback_nudge.dart | 2 +- lib/pages/home/profile_card.dart | 3 +- lib/pages/people/view/person_view.dart | 4 +- .../settings/view/request_permissions.dart | 2 +- lib/pages/settings/view/settings_page.dart | 3 +- lib/resources/theme.dart | 47 ++++++++----------- lib/widgets/error_page.dart | 3 +- lib/widgets/icon_text.dart | 4 +- lib/widgets/info_card.dart | 7 +-- lib/widgets/scaffold.dart | 1 + lib/widgets/selectable.dart | 6 +-- 18 files changed, 55 insertions(+), 57 deletions(-) diff --git a/lib/authentication/view/edit_profile_page.dart b/lib/authentication/view/edit_profile_page.dart index ec8319b07..4b72f695d 100644 --- a/lib/authentication/view/edit_profile_page.dart +++ b/lib/authentication/view/edit_profile_page.dart @@ -128,7 +128,7 @@ class _EditProfilePageState extends State { AppButton( key: const ValueKey('change_password_button'), text: S.current.actionChangePassword.toUpperCase(), - color: Theme.of(context).accentColor, + color: Theme.of(context).primaryColor, width: 130, onTap: () async { if (changePasswordKey.currentState.validate()) { @@ -204,7 +204,7 @@ class _EditProfilePageState extends State { AppButton( key: const ValueKey('change_email_button'), text: S.current.actionChangeEmail, - color: Theme.of(context).accentColor, + color: Theme.of(context).primaryColor, width: 130, onTap: () async { final authProvider = diff --git a/lib/authentication/view/login_view.dart b/lib/authentication/view/login_view.dart index 1c4f5a9e1..410ecc916 100644 --- a/lib/authentication/view/login_view.dart +++ b/lib/authentication/view/login_view.dart @@ -1,3 +1,4 @@ +import 'package:acs_upb_mobile/resources/theme.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -117,7 +118,7 @@ class _LoginViewState extends State { child: Text( S.current.actionResetPassword, style: Theme.of(context) - .accentTextTheme + .coloredTextTheme .subtitle1 .copyWith(fontWeight: FontWeight.w500), ), @@ -177,7 +178,7 @@ class _LoginViewState extends State { minHeight: 1, ), child: Image.asset('assets/images/city_doodle.png', - color: Theme.of(context).accentColor.withOpacity(0.4)), + color: Theme.of(context).primaryColor.withOpacity(0.4)), ), ), ), @@ -224,7 +225,7 @@ class _LoginViewState extends State { Expanded( child: AppButton( key: const ValueKey('log_in_button'), - color: Theme.of(context).accentColor, + color: Theme.of(context).primaryColor, text: S.current.actionLogIn, onTap: () => loginForm.submit(), ), @@ -248,7 +249,7 @@ class _LoginViewState extends State { }, child: Text(S.current.actionSignUp, style: Theme.of(context) - .accentTextTheme + .coloredTextTheme .subtitle1 .copyWith(fontWeight: FontWeight.w500)), ), diff --git a/lib/authentication/view/sign_up_view.dart b/lib/authentication/view/sign_up_view.dart index cb463bd39..41fe6783c 100644 --- a/lib/authentication/view/sign_up_view.dart +++ b/lib/authentication/view/sign_up_view.dart @@ -1,3 +1,4 @@ +import 'package:acs_upb_mobile/resources/theme.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -138,7 +139,7 @@ class _SignUpViewState extends State { TextSpan( text: S.current.labelPrivacyPolicy, style: Theme.of(context) - .accentTextTheme + .coloredTextTheme .subtitle1 .apply(fontWeightDelta: 2), recognizer: TapGestureRecognizer() @@ -252,7 +253,7 @@ class _SignUpViewState extends State { Expanded( child: AppButton( key: const ValueKey('sign_up_button'), - color: Theme.of(context).accentColor, + color: Theme.of(context).primaryColor, text: S.current.actionSignUp, onTap: () => signUpForm.submit(), ), diff --git a/lib/main.dart b/lib/main.dart index c8754157a..7e1da4721 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -131,7 +131,7 @@ class _MyAppState extends State { Widget build(BuildContext context) { return OKToast( textStyle: lightThemeData.textTheme.button, - backgroundColor: accentColor.withOpacity(.8), + backgroundColor: primaryColor.withOpacity(.8), position: ToastPosition.bottom, child: GestureDetector( onTap: () { @@ -207,7 +207,7 @@ class AppLoadingScreen extends StatelessWidget { return LoadingScreen( navigateAfterFuture: _setUpAndChooseStartScreen(context), image: Image.asset('assets/icons/acs_logo.png'), - loaderColor: Theme.of(context).accentColor, + loaderColor: Theme.of(context).primaryColor, ); } } diff --git a/lib/navigation/bottom_navigation_bar.dart b/lib/navigation/bottom_navigation_bar.dart index c8b9d748f..7c6a56e65 100644 --- a/lib/navigation/bottom_navigation_bar.dart +++ b/lib/navigation/bottom_navigation_bar.dart @@ -95,7 +95,7 @@ class _AppBottomNavigationBarState extends State iconMargin: EdgeInsets.zero, ), ], - labelColor: Theme.of(context).accentColor, + labelColor: Theme.of(context).primaryColor, labelPadding: const EdgeInsets.only(top: 4), unselectedLabelColor: Theme.of(context).unselectedWidgetColor, diff --git a/lib/pages/class_feedback/view/feedback_question.dart b/lib/pages/class_feedback/view/feedback_question.dart index 5d6ea548f..718de08a4 100644 --- a/lib/pages/class_feedback/view/feedback_question.dart +++ b/lib/pages/class_feedback/view/feedback_question.dart @@ -92,7 +92,7 @@ class _FeedbackQuestionFormFieldState extends State { max: 10, divisions: 9, label: widget.question.answer, - activeColor: Theme.of(context).accentColor, + activeColor: Theme.of(context).primaryColor, ), ), ), diff --git a/lib/pages/filter/view/relevance_picker.dart b/lib/pages/filter/view/relevance_picker.dart index 7142ca81f..c0c5d8ed9 100644 --- a/lib/pages/filter/view/relevance_picker.dart +++ b/lib/pages/filter/view/relevance_picker.dart @@ -59,7 +59,7 @@ class RelevanceFormField extends ChipFormField> { RelevanceController controller, bool canBeForEveryone) { final User user = Provider.of(context).currentUserFromCache; final buttonColor = user?.canAddPublicInfo ?? false - ? Theme.of(context).accentColor + ? Theme.of(context).primaryColor : Theme.of(context).hintColor; return IntrinsicWidth( @@ -107,7 +107,7 @@ class RelevanceFormField extends ChipFormField> { Text( S.current.labelCustom, style: Theme.of(context) - .accentTextTheme + .coloredTextTheme .subtitle2 .copyWith(color: buttonColor), ), diff --git a/lib/pages/home/feedback_nudge.dart b/lib/pages/home/feedback_nudge.dart index d18994f3b..e5aeb11fe 100644 --- a/lib/pages/home/feedback_nudge.dart +++ b/lib/pages/home/feedback_nudge.dart @@ -52,7 +52,7 @@ class _FeedbackNudgeState extends State { child: ActionChip( padding: const EdgeInsets.all(12), tooltip: S.current.navigationClassesFeedbackChecklist, - backgroundColor: Theme.of(context).accentColor, + backgroundColor: Theme.of(context).primaryColor, onPressed: () { Navigator.of(context).push( MaterialPageRoute( diff --git a/lib/pages/home/profile_card.dart b/lib/pages/home/profile_card.dart index be8a56f38..86919a275 100644 --- a/lib/pages/home/profile_card.dart +++ b/lib/pages/home/profile_card.dart @@ -1,3 +1,4 @@ +import 'package:acs_upb_mobile/resources/theme.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -88,7 +89,7 @@ class _ProfileCardState extends State { ? S.current.actionLogIn : S.current.actionLogOut, style: Theme.of(context) - .accentTextTheme + .coloredTextTheme .subtitle2 .copyWith(fontWeight: FontWeight.w500)), ), diff --git a/lib/pages/people/view/person_view.dart b/lib/pages/people/view/person_view.dart index eee95b0b2..4db297775 100644 --- a/lib/pages/people/view/person_view.dart +++ b/lib/pages/people/view/person_view.dart @@ -21,10 +21,10 @@ class PersonView extends StatelessWidget { Container( width: MediaQuery.of(context).size.width, decoration: BoxDecoration( - color: Theme.of(context).accentColor, + color: Theme.of(context).primaryColor, shape: BoxShape.rectangle, border: - Border.all(color: Theme.of(context).accentColor, width: 10), + Border.all(color: Theme.of(context).primaryColor, width: 10), ), child: Center( child: Text(person.name, diff --git a/lib/pages/settings/view/request_permissions.dart b/lib/pages/settings/view/request_permissions.dart index 8cd603eb9..9dde671fd 100644 --- a/lib/pages/settings/view/request_permissions.dart +++ b/lib/pages/settings/view/request_permissions.dart @@ -43,7 +43,7 @@ class _RequestPermissionsPageState extends State { AppButton( key: const ValueKey('agree_overwrite_request'), text: S.current.buttonSend, - color: Theme.of(context).accentColor, + color: Theme.of(context).primaryColor, width: 130, onTap: () async { Navigator.of(context).pop(); diff --git a/lib/pages/settings/view/settings_page.dart b/lib/pages/settings/view/settings_page.dart index 3723e14d5..76faf291d 100644 --- a/lib/pages/settings/view/settings_page.dart +++ b/lib/pages/settings/view/settings_page.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:acs_upb_mobile/resources/theme.dart'; import 'package:easy_dynamic_theme/easy_dynamic_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; @@ -185,7 +186,7 @@ class _SettingsPageState extends State { child: Text( title, style: Theme.of(context) - .accentTextTheme + .coloredTextTheme .subtitle2 .apply(fontWeightDelta: 2), ), diff --git a/lib/resources/theme.dart b/lib/resources/theme.dart index 2deb91710..5b88aad0c 100644 --- a/lib/resources/theme.dart +++ b/lib/resources/theme.dart @@ -1,40 +1,29 @@ import 'package:flutter/material.dart'; -Color primaryColor = const Color(0xFF4DB5E4); -Color accentColor = const Color(0xFF43ACCD); +Color primaryColor = const Color(0xFF43ACCD); -Color chipSelectedColor(Brightness brightness) => - brightness == Brightness.light ? accentColor.withOpacity(0.3) : accentColor; +Color chipSelectedColor(Brightness brightness) => brightness == Brightness.light + ? primaryColor.withOpacity(0.3) + : primaryColor; ChipThemeData chipThemeData(Brightness brightness) => ChipThemeData.fromDefaults( brightness: brightness, - secondaryColor: accentColor, - labelStyle: ThemeData() - .accentTextTheme - .apply( - fontFamily: 'Montserrat', - bodyColor: accentColor, - displayColor: accentColor) - .bodyText2, + secondaryColor: primaryColor, + labelStyle: ThemeData().coloredTextTheme.bodyText2, ).copyWith( selectedColor: chipSelectedColor(brightness), secondarySelectedColor: chipSelectedColor(brightness), checkmarkColor: - brightness == Brightness.light ? accentColor : Colors.white, + brightness == Brightness.light ? primaryColor : Colors.white, ); var lightThemeData = ThemeData( brightness: Brightness.light, - accentColor: accentColor, // The following two lines are meant to remove the splash effect splashColor: Colors.transparent, highlightColor: Colors.transparent, - accentTextTheme: ThemeData().accentTextTheme.apply( - fontFamily: 'Montserrat', - bodyColor: accentColor, - displayColor: accentColor), - toggleableActiveColor: accentColor, + toggleableActiveColor: primaryColor, fontFamily: 'Montserrat', primaryColor: primaryColor, chipTheme: chipThemeData(Brightness.light), @@ -42,15 +31,10 @@ var lightThemeData = ThemeData( var darkThemeData = ThemeData( brightness: Brightness.dark, - accentColor: accentColor, // The following two lines are meant to remove the splash effect splashColor: Colors.transparent, highlightColor: Colors.transparent, - accentTextTheme: ThemeData().accentTextTheme.apply( - fontFamily: 'Montserrat', - bodyColor: accentColor, - displayColor: accentColor), - toggleableActiveColor: accentColor, + toggleableActiveColor: primaryColor, fontFamily: 'Montserrat', primaryColor: primaryColor, chipTheme: chipThemeData(Brightness.dark), @@ -60,20 +44,27 @@ extension ThemeExtension on ThemeData { TextStyle chipTextStyle({@required bool selected}) => TextStyle( color: selected ? brightness == Brightness.light - ? accentColor + ? primaryColor : Colors.white : textTheme.bodyText2.color, fontWeight: selected ? FontWeight.bold : FontWeight.normal, ); + // Coloured text, usually highlighting that it can be pressed, similar to + // HTML links. + TextTheme get coloredTextTheme => textTheme.apply( + fontFamily: 'Montserrat', + bodyColor: primaryColor, + displayColor: primaryColor, + ); + Color get formIconColor { switch (brightness) { case Brightness.dark: return Colors.white70; case Brightness.light: return Colors.black45; - default: - return iconTheme.color; } + return iconTheme.color; } } diff --git a/lib/widgets/error_page.dart b/lib/widgets/error_page.dart index 6d75753bc..53345ec3d 100644 --- a/lib/widgets/error_page.dart +++ b/lib/widgets/error_page.dart @@ -1,3 +1,4 @@ +import 'package:acs_upb_mobile/resources/theme.dart'; import 'package:flutter/material.dart'; class ErrorPage extends StatelessWidget { @@ -66,7 +67,7 @@ class ErrorPage extends StatelessWidget { onTap: actionOnTap, child: Text(actionText, style: Theme.of(context) - .accentTextTheme + .coloredTextTheme .subtitle2 .copyWith(fontWeight: FontWeight.w500)), ), diff --git a/lib/widgets/icon_text.dart b/lib/widgets/icon_text.dart index d2377d906..2fb31a5bd 100644 --- a/lib/widgets/icon_text.dart +++ b/lib/widgets/icon_text.dart @@ -16,7 +16,7 @@ class IconText extends StatelessWidget { final String text; /// Optional "action" text. If this is specified, it will show after the - /// [text], have the theme's `accentColor`, and will be the trigger area for + /// [text], have the theme's `primaryColor`, and will be the trigger area for /// [onTap]. final String actionText; @@ -32,7 +32,7 @@ class IconText extends StatelessWidget { Widget build(BuildContext context) { final textStyle = style ?? Theme.of(context).textTheme.bodyText1; final actionStyle = textStyle - .copyWith(color: Theme.of(context).accentColor) + .copyWith(color: Theme.of(context).primaryColor) .apply(fontWeightDelta: 2); return InkWell( diff --git a/lib/widgets/info_card.dart b/lib/widgets/info_card.dart index 550022f40..099598c0c 100644 --- a/lib/widgets/info_card.dart +++ b/lib/widgets/info_card.dart @@ -1,3 +1,4 @@ +import 'package:acs_upb_mobile/resources/theme.dart'; import 'package:flutter/material.dart'; import '../generated/l10n.dart'; @@ -51,13 +52,13 @@ class InfoCard extends StatelessWidget { Text( S.current.actionShowMore, style: Theme.of(context) - .accentTextTheme + .coloredTextTheme .subtitle2 - .copyWith(color: Theme.of(context).accentColor), + .copyWith(color: Theme.of(context).primaryColor), ), Icon( Icons.arrow_forward_ios_outlined, - color: Theme.of(context).accentColor, + color: Theme.of(context).primaryColor, size: Theme.of(context).textTheme.subtitle2.fontSize, ) diff --git a/lib/widgets/scaffold.dart b/lib/widgets/scaffold.dart index 531391de4..acbeb323a 100644 --- a/lib/widgets/scaffold.dart +++ b/lib/widgets/scaffold.dart @@ -143,6 +143,7 @@ class AppScaffold extends StatelessWidget { child: AppBar( title: title, centerTitle: true, + backgroundColor: Theme.of(context).primaryColor, toolbarOpacity: 0.8, leading: _widgetFromAction(leading, enableContent: enableContent, context: context), diff --git a/lib/widgets/selectable.dart b/lib/widgets/selectable.dart index f8462d5d7..084d9dbcb 100644 --- a/lib/widgets/selectable.dart +++ b/lib/widgets/selectable.dart @@ -59,13 +59,13 @@ class _SelectableState extends State { color: _isSelected ? (widget.disabled ? Theme.of(context).disabledColor - : Theme.of(context).accentColor) + : Theme.of(context).primaryColor) : Colors.transparent, borderRadius: const BorderRadius.all(Radius.circular(24)), border: Border.all( color: widget.disabled ? Theme.of(context).disabledColor - : Theme.of(context).accentColor), + : Theme.of(context).primaryColor), ), child: Material( color: Colors.transparent, @@ -97,7 +97,7 @@ class _SelectableState extends State { ? Colors.white : widget.disabled ? Theme.of(context).disabledColor - : Theme.of(context).accentColor, + : Theme.of(context).primaryColor, ), ), ), From 7eac5728d303c30ece4128087fbcabe4ebc5f4af Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Sun, 19 Sep 2021 17:08:10 +0200 Subject: [PATCH 55/60] Fix imports. --- lib/authentication/view/login_view.dart | 2 +- lib/authentication/view/sign_up_view.dart | 2 +- lib/pages/filter/view/filter_page.dart | 2 +- lib/pages/filter/view/relevance_picker.dart | 19 ++++++++++--------- lib/pages/home/profile_card.dart | 2 +- lib/pages/settings/view/settings_page.dart | 2 +- .../timetable/model/academic_calendar.dart | 2 +- .../timetable/model/events/all_day_event.dart | 2 +- .../model/events/recurring_event.dart | 2 +- .../timetable/model/events/uni_event.dart | 3 ++- .../timetable/service/uni_event_provider.dart | 2 +- lib/pages/timetable/view/timetable_page.dart | 2 +- lib/widgets/chip_form_field.dart | 7 ++++--- lib/widgets/error_page.dart | 3 ++- lib/widgets/info_card.dart | 2 +- 15 files changed, 29 insertions(+), 25 deletions(-) diff --git a/lib/authentication/view/login_view.dart b/lib/authentication/view/login_view.dart index 410ecc916..e6ead7bc9 100644 --- a/lib/authentication/view/login_view.dart +++ b/lib/authentication/view/login_view.dart @@ -1,4 +1,3 @@ -import 'package:acs_upb_mobile/resources/theme.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -6,6 +5,7 @@ import 'package:provider/provider.dart'; import '../../generated/l10n.dart'; import '../../navigation/routes.dart'; import '../../resources/banner.dart'; +import '../../resources/theme.dart'; import '../../widgets/button.dart'; import '../../widgets/dialog.dart'; import '../../widgets/form_card.dart'; diff --git a/lib/authentication/view/sign_up_view.dart b/lib/authentication/view/sign_up_view.dart index 41fe6783c..6c156216d 100644 --- a/lib/authentication/view/sign_up_view.dart +++ b/lib/authentication/view/sign_up_view.dart @@ -1,4 +1,3 @@ -import 'package:acs_upb_mobile/resources/theme.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -8,6 +7,7 @@ import '../../generated/l10n.dart'; import '../../navigation/routes.dart'; import '../../pages/filter/view/filter_dropdown.dart'; import '../../resources/banner.dart'; +import '../../resources/theme.dart'; import '../../resources/utils.dart'; import '../../resources/validator.dart'; import '../../widgets/button.dart'; diff --git a/lib/pages/filter/view/filter_page.dart b/lib/pages/filter/view/filter_page.dart index efc8c9dfe..4ba922d50 100644 --- a/lib/pages/filter/view/filter_page.dart +++ b/lib/pages/filter/view/filter_page.dart @@ -1,9 +1,9 @@ -import 'package:acs_upb_mobile/resources/theme.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../../generated/l10n.dart'; import '../../../resources/locale_provider.dart'; +import '../../../resources/theme.dart'; import '../../../widgets/icon_text.dart'; import '../../../widgets/scaffold.dart'; import '../../../widgets/toast.dart'; diff --git a/lib/pages/filter/view/relevance_picker.dart b/lib/pages/filter/view/relevance_picker.dart index c0c5d8ed9..95c8de8cd 100644 --- a/lib/pages/filter/view/relevance_picker.dart +++ b/lib/pages/filter/view/relevance_picker.dart @@ -1,16 +1,17 @@ -import 'package:acs_upb_mobile/authentication/model/user.dart'; -import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; -import 'package:acs_upb_mobile/generated/l10n.dart'; -import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; -import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; -import 'package:acs_upb_mobile/pages/filter/view/filter_page.dart'; -import 'package:acs_upb_mobile/resources/theme.dart'; -import 'package:acs_upb_mobile/widgets/chip_form_field.dart'; -import 'package:acs_upb_mobile/widgets/toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; +import '../../../authentication/model/user.dart'; +import '../../../authentication/service/auth_provider.dart'; +import '../../../generated/l10n.dart'; +import '../../../resources/theme.dart'; +import '../../../widgets/chip_form_field.dart'; +import '../../../widgets/toast.dart'; +import '../model/filter.dart'; +import '../service/filter_provider.dart'; +import 'filter_page.dart'; + class RelevanceFormField extends ChipFormField> { RelevanceFormField({ @required this.controller, diff --git a/lib/pages/home/profile_card.dart b/lib/pages/home/profile_card.dart index 86919a275..e0f596c15 100644 --- a/lib/pages/home/profile_card.dart +++ b/lib/pages/home/profile_card.dart @@ -1,4 +1,3 @@ -import 'package:acs_upb_mobile/resources/theme.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -6,6 +5,7 @@ import '../../authentication/model/user.dart'; import '../../authentication/service/auth_provider.dart'; import '../../authentication/view/edit_profile_page.dart'; import '../../generated/l10n.dart'; +import '../../resources/theme.dart'; import '../../resources/utils.dart'; class ProfileCard extends StatefulWidget { diff --git a/lib/pages/settings/view/settings_page.dart b/lib/pages/settings/view/settings_page.dart index 76faf291d..1bc4b8192 100644 --- a/lib/pages/settings/view/settings_page.dart +++ b/lib/pages/settings/view/settings_page.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:acs_upb_mobile/resources/theme.dart'; import 'package:easy_dynamic_theme/easy_dynamic_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; @@ -11,6 +10,7 @@ import '../../../authentication/service/auth_provider.dart'; import '../../../generated/l10n.dart'; import '../../../navigation/routes.dart'; import '../../../resources/locale_provider.dart'; +import '../../../resources/theme.dart'; import '../../../resources/utils.dart'; import '../../../widgets/icon_text.dart'; import '../../../widgets/scaffold.dart'; diff --git a/lib/pages/timetable/model/academic_calendar.dart b/lib/pages/timetable/model/academic_calendar.dart index d9abd61e8..d985c0347 100644 --- a/lib/pages/timetable/model/academic_calendar.dart +++ b/lib/pages/timetable/model/academic_calendar.dart @@ -1,7 +1,7 @@ +import 'package:dart_date/dart_date.dart' show Interval; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' hide Interval; import 'package:time_machine/time_machine.dart' hide Interval; -import 'package:dart_date/dart_date.dart' show Interval; import '../../../resources/utils.dart'; import '../timetable_utils.dart'; diff --git a/lib/pages/timetable/model/events/all_day_event.dart b/lib/pages/timetable/model/events/all_day_event.dart index 85a045ab4..084746bed 100644 --- a/lib/pages/timetable/model/events/all_day_event.dart +++ b/lib/pages/timetable/model/events/all_day_event.dart @@ -1,6 +1,6 @@ +import 'package:dart_date/dart_date.dart' show Interval; import 'package:flutter/material.dart' hide Interval; import 'package:time_machine/time_machine.dart' hide Interval; -import 'package:dart_date/dart_date.dart' show Interval; import '../../../classes/model/class.dart'; import '../../timetable_utils.dart'; diff --git a/lib/pages/timetable/model/events/recurring_event.dart b/lib/pages/timetable/model/events/recurring_event.dart index b0d00c0f7..4768e447c 100644 --- a/lib/pages/timetable/model/events/recurring_event.dart +++ b/lib/pages/timetable/model/events/recurring_event.dart @@ -1,7 +1,7 @@ +import 'package:dart_date/dart_date.dart' show Interval; import 'package:flutter/material.dart' hide Interval; import 'package:rrule/rrule.dart'; import 'package:time_machine/time_machine.dart' hide Interval; -import 'package:dart_date/dart_date.dart' show Interval; import '../../../../resources/locale_provider.dart'; import '../../../../resources/utils.dart'; diff --git a/lib/pages/timetable/model/events/uni_event.dart b/lib/pages/timetable/model/events/uni_event.dart index b3bc6cba6..e7dec06e5 100644 --- a/lib/pages/timetable/model/events/uni_event.dart +++ b/lib/pages/timetable/model/events/uni_event.dart @@ -1,9 +1,10 @@ import 'dart:core'; +import 'package:dart_date/dart_date.dart' show Interval; import 'package:flutter/material.dart' hide Interval; import 'package:time_machine/time_machine.dart' hide Interval; -import 'package:dart_date/dart_date.dart' show Interval; import 'package:timetable/timetable.dart'; + import '../../../../generated/l10n.dart'; import '../../../classes/model/class.dart'; import '../../timetable_utils.dart'; diff --git a/lib/pages/timetable/service/uni_event_provider.dart b/lib/pages/timetable/service/uni_event_provider.dart index 4f898bd8e..e30d250a2 100644 --- a/lib/pages/timetable/service/uni_event_provider.dart +++ b/lib/pages/timetable/service/uni_event_provider.dart @@ -2,13 +2,13 @@ import 'dart:async'; import 'package:async/async.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:dart_date/dart_date.dart' show Interval; import 'package:flutter/material.dart' hide Interval; import 'package:googleapis/calendar/v3.dart' as g_cal; import 'package:recase/recase.dart'; import 'package:rrule/rrule.dart'; import 'package:time_machine/time_machine.dart' hide Interval; import 'package:timetable/timetable.dart'; -import 'package:dart_date/dart_date.dart' show Interval; import '../../../authentication/service/auth_provider.dart'; import '../../../generated/l10n.dart'; diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index 62cf65832..b84bd5219 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -1,3 +1,4 @@ +import 'package:dart_date/dart_date.dart' show Interval; import 'package:flutter/material.dart' hide Interval; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; @@ -5,7 +6,6 @@ import 'package:recase/recase.dart'; import 'package:supercharged/supercharged.dart'; import 'package:time_machine/time_machine.dart' hide Interval; import 'package:timetable/timetable.dart'; -import 'package:dart_date/dart_date.dart' show Interval; import '../../../authentication/service/auth_provider.dart'; import '../../../generated/l10n.dart'; diff --git a/lib/widgets/chip_form_field.dart b/lib/widgets/chip_form_field.dart index a45a4f455..feefd10fd 100644 --- a/lib/widgets/chip_form_field.dart +++ b/lib/widgets/chip_form_field.dart @@ -1,8 +1,9 @@ -import 'package:acs_upb_mobile/generated/l10n.dart'; -import 'package:acs_upb_mobile/resources/locale_provider.dart'; -import 'package:acs_upb_mobile/resources/theme.dart'; import 'package:flutter/material.dart'; +import '../generated/l10n.dart'; +import '../resources/locale_provider.dart'; +import '../resources/theme.dart'; + class FilterChipFormField extends ChipFormField> { FilterChipFormField({ @required Map initialValues, diff --git a/lib/widgets/error_page.dart b/lib/widgets/error_page.dart index 53345ec3d..a33b59493 100644 --- a/lib/widgets/error_page.dart +++ b/lib/widgets/error_page.dart @@ -1,6 +1,7 @@ -import 'package:acs_upb_mobile/resources/theme.dart'; import 'package:flutter/material.dart'; +import '../resources/theme.dart'; + class ErrorPage extends StatelessWidget { const ErrorPage({ this.imgPath = 'assets/illustrations/undraw_warning.png', diff --git a/lib/widgets/info_card.dart b/lib/widgets/info_card.dart index 099598c0c..df81722db 100644 --- a/lib/widgets/info_card.dart +++ b/lib/widgets/info_card.dart @@ -1,7 +1,7 @@ -import 'package:acs_upb_mobile/resources/theme.dart'; import 'package:flutter/material.dart'; import '../generated/l10n.dart'; +import '../resources/theme.dart'; class InfoCard extends StatelessWidget { const InfoCard( From 0a49bec47363b0ac9b502ec2286ea0db7cf05865 Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Sun, 19 Sep 2021 17:12:41 +0200 Subject: [PATCH 56/60] Fix showDialog type cannot be inferred. --- lib/authentication/view/edit_profile_page.dart | 14 +++++++------- lib/authentication/view/login_view.dart | 3 ++- .../class_feedback/view/feedback_question.dart | 2 +- lib/pages/classes/view/class_view.dart | 2 +- lib/pages/portal/view/website_view.dart | 13 ++++++++----- lib/pages/settings/view/request_permissions.dart | 6 ++++-- .../timetable/view/events/add_event_view.dart | 12 ++++++++---- 7 files changed, 31 insertions(+), 21 deletions(-) diff --git a/lib/authentication/view/edit_profile_page.dart b/lib/authentication/view/edit_profile_page.dart index 4b72f695d..3e6520e1d 100644 --- a/lib/authentication/view/edit_profile_page.dart +++ b/lib/authentication/view/edit_profile_page.dart @@ -292,10 +292,10 @@ class _EditProfilePageState extends State { bool result = true; if (isVerified == false && emailController.text + emailDomain != authProvider.email) { - await showDialog( - context: context, - builder: _changeEmailConfirmationDialog) - .then((value) => result = value ?? false); + await showDialog( + context: context, + builder: _changeEmailConfirmationDialog, + ).then((value) => result = value ?? false); } if (uploadedImage != null) { imageAsPNG = await convertToPNG(uploadedImage); @@ -318,9 +318,9 @@ class _EditProfilePageState extends State { AppScaffoldAction( icon: Icons.more_vert_outlined, items: { - S.current.actionChangePassword: () => - showDialog(context: context, builder: _changePasswordDialog), - S.current.actionDeleteAccount: () => showDialog( + S.current.actionChangePassword: () => showDialog( + context: context, builder: _changePasswordDialog), + S.current.actionDeleteAccount: () => showDialog( context: context, builder: _deletionConfirmationDialog) }, ) diff --git a/lib/authentication/view/login_view.dart b/lib/authentication/view/login_view.dart index e6ead7bc9..c612dac42 100644 --- a/lib/authentication/view/login_view.dart +++ b/lib/authentication/view/login_view.dart @@ -123,7 +123,8 @@ class _LoginViewState extends State { .copyWith(fontWeight: FontWeight.w500), ), onTap: () { - showDialog(context: context, builder: _resetPasswordDialog); + showDialog( + context: context, builder: _resetPasswordDialog); final currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus) { currentFocus.unfocus(); diff --git a/lib/pages/class_feedback/view/feedback_question.dart b/lib/pages/class_feedback/view/feedback_question.dart index 718de08a4..103d5e2a5 100644 --- a/lib/pages/class_feedback/view/feedback_question.dart +++ b/lib/pages/class_feedback/view/feedback_question.dart @@ -45,7 +45,7 @@ class _FeedbackQuestionFormFieldState extends State { // Offset to bypass slider padding offset: const Offset(10, 0), child: GestureDetector( - onTap: () => showDialog( + onTap: () => showDialog( context: context, builder: (context) => Dialog( child: Padding( diff --git a/lib/pages/classes/view/class_view.dart b/lib/pages/classes/view/class_view.dart index 9bf1a3198..8721fa755 100644 --- a/lib/pages/classes/view/class_view.dart +++ b/lib/pages/classes/view/class_view.dart @@ -237,7 +237,7 @@ class _ClassViewState extends State { if (!mounted) { return; } - await showDialog( + await showDialog( context: context, builder: (context) => _deletionConfirmationDialog( context: context, diff --git a/lib/pages/portal/view/website_view.dart b/lib/pages/portal/view/website_view.dart index 13afca452..6601bc595 100644 --- a/lib/pages/portal/view/website_view.dart +++ b/lib/pages/portal/view/website_view.dart @@ -228,12 +228,15 @@ class _WebsiteViewState extends State { AppScaffoldAction( icon: Icons.more_vert_outlined, items: { - S.current.actionDeleteWebsite: () => showDialog( - context: context, - builder: _deletionConfirmationDialog) + S.current.actionDeleteWebsite: () => showDialog( + context: context, + builder: _deletionConfirmationDialog, + ) }, - onPressed: () => showDialog( - context: context, builder: _deletionConfirmationDialog), + onPressed: () => showDialog( + context: context, + builder: _deletionConfirmationDialog, + ), ) ] : []), diff --git a/lib/pages/settings/view/request_permissions.dart b/lib/pages/settings/view/request_permissions.dart index 9dde671fd..e9f761478 100644 --- a/lib/pages/settings/view/request_permissions.dart +++ b/lib/pages/settings/view/request_permissions.dart @@ -90,8 +90,10 @@ class _RequestPermissionsPageState extends State { if (!mounted) { return; } - await showDialog( - context: context, builder: _requestAlreadyExistsDialog); + await showDialog( + context: context, + builder: _requestAlreadyExistsDialog, + ); } queryResult = await requestProvider.makeRequest( diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index 3339924a1..4390b49fb 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -456,11 +456,15 @@ class _AddEventViewState extends State { AppScaffoldAction _deleteButton() => AppScaffoldAction( icon: Icons.more_vert_outlined, items: { - S.current.actionDeleteEvent: () => - showDialog(context: context, builder: _deletionConfirmationDialog) + S.current.actionDeleteEvent: () => showDialog( + context: context, + builder: _deletionConfirmationDialog, + ) }, - onPressed: () => - showDialog(context: context, builder: _deletionConfirmationDialog), + onPressed: () => showDialog( + context: context, + builder: _deletionConfirmationDialog, + ), ); Widget timeIntervalPicker() { From d1c7f1ac5994c77e208214fddeb47dade2a495f9 Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Sun, 19 Sep 2021 17:22:46 +0200 Subject: [PATCH 57/60] Fix errors about context being used in async gaps. --- .../view/edit_profile_page.dart | 4 +-- lib/authentication/view/login_view.dart | 12 +++------ lib/authentication/view/sign_up_view.dart | 4 +-- .../view/class_feedback_view.dart | 4 +-- lib/pages/classes/view/class_view.dart | 8 ++---- lib/pages/classes/view/classes_page.dart | 4 +-- lib/pages/home/profile_card.dart | 4 +-- lib/pages/portal/view/website_view.dart | 4 +-- .../settings/view/request_permissions.dart | 8 ++---- .../timetable/view/events/add_event_view.dart | 3 +++ lib/pages/timetable/view/timetable_page.dart | 26 ++++++++++++------- 11 files changed, 32 insertions(+), 49 deletions(-) diff --git a/lib/authentication/view/edit_profile_page.dart b/lib/authentication/view/edit_profile_page.dart index 3e6520e1d..4d3029533 100644 --- a/lib/authentication/view/edit_profile_page.dart +++ b/lib/authentication/view/edit_profile_page.dart @@ -307,9 +307,7 @@ class _EditProfilePageState extends State { if (result) { if (await authProvider.updateProfile(info)) { AppToast.show(S.current.messageEditProfileSuccess); - if (!mounted) { - return; - } + if (!mounted) return; Navigator.pop(context); } } diff --git a/lib/authentication/view/login_view.dart b/lib/authentication/view/login_view.dart index c612dac42..960203ecc 100644 --- a/lib/authentication/view/login_view.dart +++ b/lib/authentication/view/login_view.dart @@ -80,9 +80,7 @@ class _LoginViewState extends State { .sendPasswordResetEmail( emailController.text + S.current.stringEmailDomain); if (success) { - if (!mounted) { - return; - } + if (!mounted) return; Navigator.pop(context); } return; @@ -103,9 +101,7 @@ class _LoginViewState extends State { fields[S.current.labelPassword], ); if (result) { - if (!mounted) { - return; - } + if (!mounted) return; await Navigator.pushReplacementNamed(context, Routes.home); } }, @@ -213,9 +209,7 @@ class _LoginViewState extends State { final result = await authProvider.signInAnonymously(); if (result) { - if (!mounted) { - return; - } + if (!mounted) return; await Navigator.pushReplacementNamed( context, Routes.home); } diff --git a/lib/authentication/view/sign_up_view.dart b/lib/authentication/view/sign_up_view.dart index 6c156216d..789666414 100644 --- a/lib/authentication/view/sign_up_view.dart +++ b/lib/authentication/view/sign_up_view.dart @@ -182,9 +182,7 @@ class _SignUpViewState extends State { final result = await authProvider.signUp(fields); if (result) { - if (!mounted) { - return; - } + if (!mounted) return; // Remove all routes below and push home page await Navigator.pushNamedAndRemoveUntil( context, Routes.home, (route) => false); diff --git a/lib/pages/class_feedback/view/class_feedback_view.dart b/lib/pages/class_feedback/view/class_feedback_view.dart index b467a224e..9e1ada588 100644 --- a/lib/pages/class_feedback/view/class_feedback_view.dart +++ b/lib/pages/class_feedback/view/class_feedback_view.dart @@ -254,9 +254,7 @@ class _ClassFeedbackViewState extends State { selectedTeacher, classController.text); if (feedbackSentSuccessfully) { - if (!mounted) { - return; - } + if (!mounted) return; Navigator.of(context).pop(); AppToast.show(S.current.messageFeedbackHasBeenSent); } diff --git a/lib/pages/classes/view/class_view.dart b/lib/pages/classes/view/class_view.dart index 8721fa755..896fb323e 100644 --- a/lib/pages/classes/view/class_view.dart +++ b/lib/pages/classes/view/class_view.dart @@ -234,9 +234,7 @@ class _ClassViewState extends State { ) ]); if (option == S.current.actionDeleteShortcut) { - if (!mounted) { - return; - } + if (!mounted) return; await showDialog( context: context, builder: (context) => _deletionConfirmationDialog( @@ -300,9 +298,7 @@ class _ClassViewState extends State { final lecturer = await personProvider.fetchPerson(lecturerName); if (lecturer != null && lecturerName != null) { - if (!mounted) { - return; - } + if (!mounted) return; await showModalBottomSheet( isScrollControlled: true, context: context, diff --git a/lib/pages/classes/view/classes_page.dart b/lib/pages/classes/view/classes_page.dart index b31b0c599..241a3c708 100644 --- a/lib/pages/classes/view/classes_page.dart +++ b/lib/pages/classes/view/classes_page.dart @@ -94,9 +94,7 @@ class _ClassesPageState extends State { await classProvider.setUserClassIds( classIds, authProvider.uid); unawaited(updateClasses()); - if (!mounted) { - return; - } + if (!mounted) return; Navigator.pop(context); }); } else { diff --git a/lib/pages/home/profile_card.dart b/lib/pages/home/profile_card.dart index e0f596c15..058e862bb 100644 --- a/lib/pages/home/profile_card.dart +++ b/lib/pages/home/profile_card.dart @@ -111,9 +111,7 @@ class _ProfileCardState extends State { builder: (context) => const EditProfilePage(), ), ); - if (!mounted) { - return; - } + if (!mounted) return; final authProvider = Provider.of( context, listen: false); diff --git a/lib/pages/portal/view/website_view.dart b/lib/pages/portal/view/website_view.dart index 6601bc595..354e94907 100644 --- a/lib/pages/portal/view/website_view.dart +++ b/lib/pages/portal/view/website_view.dart @@ -180,9 +180,7 @@ class _WebsiteViewState extends State { Provider.of(context, listen: false); final res = await websiteProvider.deleteWebsite(widget.website); if (res) { - if (!mounted) { - return; - } + if (!mounted) return; Navigator.pop(context); // Pop editing page AppToast.show(S.current.messageWebsiteDeleted); } diff --git a/lib/pages/settings/view/request_permissions.dart b/lib/pages/settings/view/request_permissions.dart index e9f761478..546e415a7 100644 --- a/lib/pages/settings/view/request_permissions.dart +++ b/lib/pages/settings/view/request_permissions.dart @@ -87,9 +87,7 @@ class _RequestPermissionsPageState extends State { await requestProvider.userAlreadyRequested(user.uid); if (queryResult) { - if (!mounted) { - return; - } + if (!mounted) return; await showDialog( context: context, builder: _requestAlreadyExistsDialog, @@ -104,9 +102,7 @@ class _RequestPermissionsPageState extends State { ); if (queryResult) { AppToast.show(S.current.messageRequestHasBeenSent); - if (!mounted) { - return; - } + if (!mounted) return; Navigator.of(context).pop(); } }) diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index 4390b49fb..6a09fcd8a 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -380,6 +380,7 @@ class _AddEventViewState extends State { await Provider.of(context, listen: false) .deleteEvent(widget.initialEvent); if (res) { + if (!mounted) return; Navigator.of(context) .popUntil(ModalRoute.withName(Routes.home)); AppToast.show(S.current.messageEventDeleted); @@ -438,6 +439,7 @@ class _AddEventViewState extends State { await Provider.of(context, listen: false) .addEvent(event); if (res) { + if (!mounted) return; Navigator.of(context).pop(); AppToast.show(S.current.messageEventAdded); } @@ -446,6 +448,7 @@ class _AddEventViewState extends State { await Provider.of(context, listen: false) .updateEvent(event); if (res) { + if (!mounted) return; Navigator.of(context).popUntil(ModalRoute.withName(Routes.home)); AppToast.show(S.current.messageEventEdited); } diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index b84bd5219..f32787042 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -187,19 +187,23 @@ class _TimetablePageState extends State Future scheduleDialog(BuildContext context) async { WidgetsBinding.instance.addPostFrameCallback( (_) async { - if (!mounted) { - return; - } + if (!mounted) return; + + final authProvider = Provider.of(context, listen: false); + final classProvider = + Provider.of(context, listen: false); + final filterProvider = + Provider.of(context, listen: false); + final requestProvider = + Provider.of(context, listen: false); // Fetch user classes, request necessary info from providers so it's // cached when we check in the dialog - final user = Provider.of(context, listen: false) - .currentUserFromCache; - await Provider.of(context, listen: false) - .fetchClassHeaders(uid: user.uid); - await Provider.of(context, listen: false).fetchFilter(); - await Provider.of(context, listen: false) - .userAlreadyRequested(user.uid); + final user = authProvider.currentUserFromCache; + await classProvider.fetchClassHeaders(uid: user.uid); + + await filterProvider.fetchFilter(); + await requestProvider.userAlreadyRequested(user.uid); // Slight delay between last frame and dialog await Future.delayed(const Duration(milliseconds: 100)); @@ -267,6 +271,7 @@ class _TimetablePageState extends State onSave: (classIds) async { await classProvider.setUserClassIds( classIds, authProvider.uid); + if (!mounted) return; Navigator.pop(context); }); } else { @@ -327,6 +332,7 @@ class _TimetablePageState extends State // Check if user is verified final bool isVerified = await authProvider.isVerified; // Pop the dialog + if (!mounted) return; Navigator.of(context).pop(); // Push the Permissions page if (authProvider.isAnonymous) { From 93ce82adbe977302a058cd763cf3c9b6b38955af Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Sun, 19 Sep 2021 17:23:29 +0200 Subject: [PATCH 58/60] Use SizedBox instead of Container. --- lib/widgets/chip_form_field.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/chip_form_field.dart b/lib/widgets/chip_form_field.dart index feefd10fd..559793930 100644 --- a/lib/widgets/chip_form_field.dart +++ b/lib/widgets/chip_form_field.dart @@ -96,7 +96,7 @@ class ChipFormField extends FormField { children: [ const SizedBox(width: 12), Expanded( - child: Container( + child: SizedBox( height: 40, child: contentBuilder(state), ), From 9b5df95009d34be8079077c1830161be636920d1 Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Sun, 19 Sep 2021 18:40:23 +0200 Subject: [PATCH 59/60] Highlight search on People page. --- lib/pages/people/view/people_page.dart | 25 ++++++++++++++----------- pubspec.lock | 7 +++++++ pubspec.yaml | 6 +++--- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/lib/pages/people/view/people_page.dart b/lib/pages/people/view/people_page.dart index 5ad3b6e9e..0fb1985ef 100644 --- a/lib/pages/people/view/people_page.dart +++ b/lib/pages/people/view/people_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; import 'package:recase/recase.dart'; +import 'package:substring_highlight/substring_highlight.dart'; import '../../../generated/l10n.dart'; import '../../../widgets/scaffold.dart'; @@ -95,11 +96,6 @@ class PeopleList extends StatelessWidget { @override Widget build(BuildContext context) { - final List filteredWords = filter - .toLowerCase() - .split(' ') - .where((element) => element != '') - .toList(); people.sort((p1, p2) { final cmpLast = p1.lastName.compareTo(p2.lastName); if (cmpLast != 0) { @@ -119,12 +115,19 @@ class PeopleList extends StatelessWidget { people[index].photo, ), ), - title: filteredWords.isNotEmpty - ? Text( - people[index].name, - style: Theme.of(context).textTheme.subtitle1, - ) - : Text(people[index].name), + // NOTE: This package only supports a single search term, so even + // though we match each word in the search term in any order (e.g. + // "John Doe" matches "Doe John" and vice versa), they won't be + // correctly highlighted if the match is not exact. + // + // https://github.com/remoteportal/substring_highlight/issues/17 + title: SubstringHighlight( + text: people[index].name, + term: filter, + textStyle: Theme.of(context).textTheme.subtitle1, + textStyleHighlight: Theme.of(context).textTheme.subtitle1.copyWith( + backgroundColor: Theme.of(context).primaryColor.withAlpha(100)), + ), subtitle: Text(people[index].email), onTap: () => showModalBottomSheet( isScrollControlled: true, diff --git a/pubspec.lock b/pubspec.lock index 5ffcba958..45b91add4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -936,6 +936,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + substring_highlight: + dependency: "direct main" + description: + name: substring_highlight + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.33" supercharged: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 660f77dc8..a28e48b3b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,9 +46,6 @@ dependencies: # Dotted line painter dotted_line: ^3.0.0 - # Highlight words - # dynamic_text_highlighting: ^2.2.0 - # Package for dynamically changing the app theme # dynamic_theme: ^1.0.1 easy_dynamic_theme: ^2.2.0 @@ -125,6 +122,9 @@ dependencies: # Async utilities rxdart: ^0.26.0 + # Highlight words + substring_highlight: ^1.0.33 + # Support lock/mutex synchronized: ^3.0.0 From b682574e921df701b16eb7d1b2c585a32f801f0d Mon Sep 17 00:00:00 2001 From: Ioana Alexandru Date: Sun, 19 Sep 2021 18:48:13 +0200 Subject: [PATCH 60/60] Highlight search on FAQ page. --- lib/pages/faq/view/faq_page.dart | 49 +++++++++++++++----------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/lib/pages/faq/view/faq_page.dart b/lib/pages/faq/view/faq_page.dart index 7749e715c..9b6598684 100644 --- a/lib/pages/faq/view/faq_page.dart +++ b/lib/pages/faq/view/faq_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:markdown/markdown.dart' as md; import 'package:provider/provider.dart'; +import 'package:substring_highlight/substring_highlight.dart'; import '../../../generated/l10n.dart'; import '../../../resources/theme.dart'; @@ -129,52 +130,48 @@ class _FaqPageState extends State { } } -class QuestionsList extends StatefulWidget { +class QuestionsList extends StatelessWidget { const QuestionsList({this.questions, this.filter}); final List questions; final String filter; - @override - _QuestionsListState createState() => _QuestionsListState(); -} - -class _QuestionsListState extends State { @override Widget build(BuildContext context) { - final List filteredWords = - widget.filter.split(' ').where((element) => element != '').toList(); return Padding( padding: const EdgeInsets.only(top: 12), child: ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemCount: widget.questions.length, + itemCount: questions.length, itemBuilder: (context, index) { return ExpansionTile( - key: ValueKey(widget.questions[index].question), - title: filteredWords.isNotEmpty - ? Text( - widget.questions[index].question, - style: Theme.of(context).textTheme.subtitle1, - ) - : Text( - widget.questions[index].question, - style: Theme.of(context).textTheme.subtitle1, - ), + key: ValueKey(questions[index].question), + // NOTE: This package only highlights the exact search term, so some + // questions may be matched without displaying any highlight. + // + // https://github.com/remoteportal/substring_highlight/issues/17 + title: SubstringHighlight( + text: questions[index].question, + term: filter, + textStyle: Theme.of(context).textTheme.subtitle1, + textStyleHighlight: + Theme.of(context).textTheme.subtitle1.copyWith( + backgroundColor: + Theme.of(context).primaryColor.withAlpha(100), + ), + ), children: [ Padding( padding: const EdgeInsets.fromLTRB(15, 0, 15, 15), child: MarkdownBody( fitContent: false, onTapLink: (text, link, title) => Utils.launchURL(link), - /* - This is a workaround because the strings in Firebase represent - newlines as '\n' and Firebase replaces them with '\\n'. We need - to replace them back for them to display properly. - (See GitHub issue firebase/firebase-js-sdk#2366) - */ - data: widget.questions[index].answer.replaceAll('\\n', '\n'), + // This is a workaround because the strings in Firebase represent + // newlines as '\n' and Firebase replaces them with '\\n'. We need + // to replace them back for them to display properly. + // (See GitHub issue firebase/firebase-js-sdk#2366) + data: questions[index].answer.replaceAll('\\n', '\n'), extensionSet: md.ExtensionSet( md.ExtensionSet.gitHubFlavored.blockSyntaxes, [ md.EmojiSyntax(),