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"
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index c3cf38af1..4291ba738 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -73,7 +73,14 @@ 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/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
- name: Create a Release APK
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.
diff --git a/README.md b/README.md
index f178e9f0c..67fd552a1 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,12 @@ 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)
+* [Ștefan-Dragoș Badea](https://github.com/GhiaraD)
+* [Ștefan-Andrei Popa](https://github.com/AndreiPopa21)
## Building from source with Android Studio
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/android/build.gradle b/android/build.gradle
index 13b27d764..a407870dc 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -6,7 +6,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.1.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/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 670851ebc..df5e824e4 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)
@@ -363,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):
@@ -383,10 +399,11 @@ 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`)
- path_provider (from `.symlinks/plugins/path_provider/ios`)
- shared_preferences (from `.symlinks/plugins/shared_preferences/ios`)
@@ -398,12 +415,14 @@ SPEC REPOS:
- abseil
- BoringSSL-GRPC
- Firebase
+ - FirebaseABTesting
- FirebaseAnalytics
- FirebaseAuth
- FirebaseCore
- FirebaseCoreDiagnostics
- FirebaseFirestore
- FirebaseInstallations
+ - FirebaseRemoteConfig
- FirebaseStorage
- FMDB
- GoogleAppMeasurement
@@ -425,14 +444,16 @@ 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:
- :path: ".symlinks/plugins/package_info/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider:
@@ -452,15 +473,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
@@ -471,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 082c6ea32..806a8a4b2 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -261,12 +261,12 @@
"${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",
"${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",
@@ -282,12 +282,12 @@
"${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",
"${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/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/authentication/view/edit_profile_page.dart b/lib/authentication/view/edit_profile_page.dart
index ec8319b07..4d3029533 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 =
@@ -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);
@@ -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);
}
}
@@ -318,9 +316,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 1c4f5a9e1..960203ecc 100644
--- a/lib/authentication/view/login_view.dart
+++ b/lib/authentication/view/login_view.dart
@@ -5,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';
@@ -79,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;
@@ -102,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);
}
},
@@ -117,12 +114,13 @@ class _LoginViewState extends State {
child: Text(
S.current.actionResetPassword,
style: Theme.of(context)
- .accentTextTheme
+ .coloredTextTheme
.subtitle1
.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();
@@ -177,7 +175,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)),
),
),
),
@@ -211,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);
}
@@ -224,7 +220,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 +244,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..789666414 100644
--- a/lib/authentication/view/sign_up_view.dart
+++ b/lib/authentication/view/sign_up_view.dart
@@ -7,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';
@@ -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()
@@ -181,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);
@@ -252,7 +251,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/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/main.dart b/lib/main.dart
index c6bbc978e..7e1da4721 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';
@@ -34,21 +35,21 @@ import 'pages/settings/view/settings_page.dart';
import 'pages/timetable/service/uni_event_provider.dart';
import 'resources/locale_provider.dart';
import 'resources/remote_config.dart';
-import 'resources/themes.dart';
+import 'resources/theme.dart';
import 'resources/utils.dart';
import 'widgets/loading_screen.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';
};
}
}
@@ -130,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: () {
@@ -148,7 +149,8 @@ class _MyAppState extends State {
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
- S.delegate
+ S.delegate,
+ const TimetableLocalizationsDelegate(),
],
supportedLocales: S.delegate.supportedLocales,
initialRoute: Routes.root,
@@ -205,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 0d93cf4ef..7c6a56e65 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,
+ labelColor: Theme.of(context).primaryColor,
+ labelPadding: const EdgeInsets.only(top: 4),
unselectedLabelColor:
- Theme.of(context).unselectedWidgetColor,
- indicatorColor: Theme.of(context).accentColor,
+ Theme.of(context).unselectedWidgetColor,
+ indicatorColor: Colors.transparent,
),
),
+ const SizedBox(height: 2)
],
),
),
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/class_feedback/view/feedback_question.dart b/lib/pages/class_feedback/view/feedback_question.dart
index 5d6ea548f..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(
@@ -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/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/classes/view/class_view.dart b/lib/pages/classes/view/class_view.dart
index 5cbe5f60e..896fb323e 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,
@@ -235,10 +234,8 @@ class _ClassViewState extends State {
)
]);
if (option == S.current.actionDeleteShortcut) {
- if (!mounted) {
- return;
- }
- await showDialog(
+ if (!mounted) return;
+ await showDialog(
context: context,
builder: (context) => _deletionConfirmationDialog(
context: context,
@@ -301,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 c7c3e5ce1..241a3c708 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,48 @@ 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 +112,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())),
],
),
@@ -163,9 +174,7 @@ class AddClassesPage extends StatefulWidget {
}
class _AddClassesPageState extends State {
- _AddClassesPageState({Set classIds}) {
- classIds = Set.from(widget.initialClassIds) ?? {};
- }
+ _AddClassesPageState();
Set classIds;
Set headers;
@@ -181,6 +190,7 @@ class _AddClassesPageState extends State {
@override
void initState() {
super.initState();
+ classIds = Set.from(widget.initialClassIds) ?? {};
updateClasses();
}
@@ -211,13 +221,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 ?? {};
@@ -245,8 +254,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) {
@@ -280,8 +289,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;
@@ -303,7 +312,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,
@@ -335,7 +345,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(
@@ -343,8 +356,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()),
),
],
@@ -364,14 +377,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})
+ 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 +402,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(
@@ -405,24 +423,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/faq/view/faq_page.dart b/lib/pages/faq/view/faq_page.dart
index bcd486557..9b6598684 100644
--- a/lib/pages/faq/view/faq_page.dart
+++ b/lib/pages/faq/view/faq_page.dart
@@ -3,12 +3,13 @@ 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';
import '../../../resources/utils.dart';
import '../../../widgets/scaffold.dart';
import '../../../widgets/search_bar.dart';
-import '../../../widgets/selectable.dart';
import '../model/question.dart';
import '../service/question_provider.dart';
@@ -21,7 +22,7 @@ class FaqPage extends StatefulWidget {
class _FaqPageState extends State {
List questions = [];
- List categories;
+ List tags;
String filter = '';
bool searchClosed = true;
List activeTags = [];
@@ -40,12 +41,17 @@ 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,
+ style: Theme.of(context).chipTextStyle(
+ selected: activeTags.contains(category),
+ ),
+ ),
+ selected: activeTags.contains(category),
onSelected: (selection) {
setState(() {
if (selection) {
@@ -83,7 +89,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(
@@ -124,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(),
diff --git a/lib/pages/filter/service/filter_provider.dart b/lib/pages/filter/service/filter_provider.dart
index 98ac14839..315aa4085 100644
--- a/lib/pages/filter/service/filter_provider.dart
+++ b/lib/pages/filter/service/filter_provider.dart
@@ -39,7 +39,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;
@@ -139,18 +141,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 a660073dc..4ba922d50 100644
--- a/lib/pages/filter/view/filter_page.dart
+++ b/lib/pages/filter/view/filter_page.dart
@@ -3,9 +3,9 @@ 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/selectable.dart';
import '../../../widgets/toast.dart';
import '../model/filter.dart';
import '../service/filter_provider.dart';
@@ -47,7 +47,6 @@ class FilterPage extends StatefulWidget {
class FilterPageState extends State {
Filter filter;
- Map nodeControllers = {};
int selectedNodes = 0;
final int maxSelectedNodes = 10;
@@ -93,37 +92,29 @@ 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),
+ style: Theme.of(context).chipTextStyle(selected: child.value),
+ ),
+ 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(
@@ -195,7 +186,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 11e6863d3..95c8de8cd 100644
--- a/lib/pages/filter/view/relevance_picker.dart
+++ b/lib/pages/filter/view/relevance_picker.dart
@@ -5,13 +5,125 @@ import 'package:provider/provider.dart';
import '../../../authentication/model/user.dart';
import '../../../authentication/service/auth_provider.dart';
import '../../../generated/l10n.dart';
-import '../../../resources/custom_icons.dart';
-import '../../../widgets/selectable.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,
+ 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).primaryColor
+ : 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)
+ .coloredTextTheme
+ .subtitle2
+ .copyWith(color: buttonColor),
+ ),
+ Icon(
+ Icons.arrow_forward_ios_outlined,
+ color: buttonColor,
+ size: Theme.of(context).textTheme.subtitle2.fontSize,
+ )
+ ],
+ ),
+ ),
+ );
+ }
+}
+
class RelevanceController {
RelevanceController({this.onChanged});
@@ -21,20 +133,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);
}
});
@@ -44,17 +154,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;
@@ -70,11 +177,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;
@@ -88,7 +194,8 @@ class _RelevancePickerState extends State {
}
Future _fetchFilter() async {
- _filter = await widget.filterProvider.fetchFilter();
+ _filter =
+ await Provider.of(context, listen: false).fetchFilter();
if (mounted) {
setState(() {});
}
@@ -99,115 +206,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));
}
@@ -215,20 +277,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();
+ }
+ }),
+ ),
+ ),
+ );
}
}
@@ -241,124 +328,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: SizedBox(
- 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/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/home_page.dart b/lib/pages/home/home_page.dart
index f510cb71d..7bb865a39 100644
--- a/lib/pages/home/home_page.dart
+++ b/lib/pages/home/home_page.dart
@@ -5,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);
@@ -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
+ && Utils.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/home/profile_card.dart b/lib/pages/home/profile_card.dart
index be8a56f38..058e862bb 100644
--- a/lib/pages/home/profile_card.dart
+++ b/lib/pages/home/profile_card.dart
@@ -5,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 {
@@ -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)),
),
@@ -110,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/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/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 06f73cb71..0fb1985ef 100644
--- a/lib/pages/people/view/people_page.dart
+++ b/lib/pages/people/view/people_page.dart
@@ -1,8 +1,10 @@
+import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.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:substring_highlight/substring_highlight.dart';
import '../../../generated/l10n.dart';
import '../../../widgets/scaffold.dart';
@@ -86,56 +88,57 @@ 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
- .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: CachedNetworkImageProvider(
+ people[index].photo,
+ ),
+ ),
+ // 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,
+ context: context,
+ builder: (BuildContext buildContext) =>
+ PersonView(person: people[index]),
),
- title: filteredWords.isNotEmpty
- ? Text(
- widget.people[index].name,
- style: Theme.of(context).textTheme.subtitle1,
- )
- : Text(
- widget.people[index].name,
- ),
- subtitle: Text(widget.people[index].email),
- onTap: () => showPersonInfo(widget.people[index]),
);
},
);
}
-
- void showPersonInfo(Person person) {
- showModalBottomSheet(
- isScrollControlled: true,
- context: context,
- builder: (BuildContext buildContext) => PersonView(person: person),
- );
- }
}
class AutocompletePerson extends StatefulWidget {
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/portal/view/portal_page.dart b/lib/pages/portal/view/portal_page.dart
index 85584a945..bf408caab 100644
--- a/lib/pages/portal/view/portal_page.dart
+++ b/lib/pages/portal/view/portal_page.dart
@@ -88,14 +88,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,
@@ -349,15 +350,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 c36e036ec..354e94907 100644
--- a/lib/pages/portal/view/website_view.dart
+++ b/lib/pages/portal/view/website_view.dart
@@ -9,16 +9,15 @@ import 'package:validators/validators.dart';
import '../../../authentication/model/user.dart';
import '../../../authentication/service/auth_provider.dart';
import '../../../generated/l10n.dart';
-import '../../../resources/custom_icons.dart';
import '../../../resources/locale_provider.dart';
import '../../../resources/storage/storage_provider.dart';
+import '../../../resources/theme.dart';
import '../../../resources/utils.dart';
import '../../../widgets/button.dart';
import '../../../widgets/circle_image.dart';
import '../../../widgets/dialog.dart';
import '../../../widgets/scaffold.dart';
import '../../../widgets/toast.dart';
-import '../../filter/service/filter_provider.dart';
import '../../filter/view/relevance_picker.dart';
import '../model/website.dart';
import '../service/website_provider.dart';
@@ -121,8 +120,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}:',
@@ -179,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);
}
@@ -227,12 +226,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,
+ ),
)
]
: []),
@@ -288,8 +290,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/settings/view/request_permissions.dart b/lib/pages/settings/view/request_permissions.dart
index 8cd603eb9..546e415a7 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();
@@ -87,11 +87,11 @@ class _RequestPermissionsPageState extends State {
await requestProvider.userAlreadyRequested(user.uid);
if (queryResult) {
- if (!mounted) {
- return;
- }
- await showDialog(
- context: context, builder: _requestAlreadyExistsDialog);
+ if (!mounted) return;
+ await showDialog(
+ context: context,
+ builder: _requestAlreadyExistsDialog,
+ );
}
queryResult = await requestProvider.makeRequest(
@@ -102,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/settings/view/settings_page.dart b/lib/pages/settings/view/settings_page.dart
index 07fd7c070..1bc4b8192 100644
--- a/lib/pages/settings/view/settings_page.dart
+++ b/lib/pages/settings/view/settings_page.dart
@@ -10,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';
@@ -131,7 +132,7 @@ class _SettingsPageState extends State {
final eventProvider = Provider.of(
context,
listen: false);
-// await eventProvider.exportToGoogleCalendar();
+ await eventProvider.exportToGoogleCalendar();
}
},
title: Text(S.current.settingsExportToGoogleCalendar),
@@ -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/pages/timetable/model/academic_calendar.dart b/lib/pages/timetable/model/academic_calendar.dart
index e2690ac22..d985c0347 100644
--- a/lib/pages/timetable/model/academic_calendar.dart
+++ b/lib/pages/timetable/model/academic_calendar.dart
@@ -1,80 +1,88 @@
-//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: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 '../../../resources/utils.dart';
+import '../timetable_utils.dart';
+import 'events/all_day_event.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(Interval interval) {
+ final Map> weeksByYear = {};
+ final rule = WeekYearRules.iso;
+
+ 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();
+ } 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(
+ Interval(semester.startDate, semester.endDate))
+ .entries) {
+ weeksByYear[entry.key] ??= {};
+ weeksByYear[entry.key].addAll(entry.value);
+ }
+ }
+
+ for (final holiday in holidays) {
+ final Interval holidayInterval =
+ Interval(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 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].
+ if (holidayInterval.includes(monday) &&
+ holidayInterval.includes(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..084746bed 100644
--- a/lib/pages/timetable/model/events/all_day_event.dart
+++ b/lib/pages/timetable/model/events/all_day_event.dart
@@ -1,54 +1,57 @@
-//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:dart_date/dart_date.dart' show Interval;
+import 'package:flutter/material.dart' hide Interval;
+import 'package:time_machine/time_machine.dart' hide Interval;
+
+import '../../../classes/model/class.dart';
+import '../../timetable_utils.dart';
+import '../academic_calendar.dart';
+import 'uni_event.dart';
+
+class AllDayUniEvent extends UniEvent {
+ AllDayUniEvent({
+ @required DateTime start,
+ @required DateTime 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(),
+ period: 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);
+
+ DateTime startDate;
+ DateTime endDate;
+
+ @override
+ Iterable generateInstances(
+ {Interval intersectingInterval}) sync* {
+ yield UniEventInstance(
+ title: name,
+ mainEvent: this,
+ start: startDate.atMidnight().copyWithUtc(),
+ end: endDate.addDays(1).atMidnight().copyWithUtc(),
+ color: color,
+ );
+ }
+}
diff --git a/lib/pages/timetable/model/events/class_event.dart b/lib/pages/timetable/model/events/class_event.dart
index f5d8f0acf..dc683ca6e 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 DateTime start,
+ @required Period period,
+ @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,
+ period: period,
+ 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..4768e447c 100644
--- a/lib/pages/timetable/model/events/recurring_event.dart
+++ b/lib/pages/timetable/model/events/recurring_event.dart
@@ -1,127 +1,129 @@
-//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: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 '../../../../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 DateTime start,
+ @required Period period,
+ @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,
+ period: period,
+ 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(LocalDate.dateTime(start))) %
+ rrule.interval)
+ .toSet();
+ }
+ return rrule.copyWith(
+ frequency: Frequency.yearly,
+ interval: 1,
+ byWeekDays: rrule.byWeekDays.isNotEmpty
+ ? rrule.byWeekDays
+ : {ByWeekDayEntry(start.weekday)},
+ byWeeks: weeks);
+ }
+ return rrule;
+ }
+
+ @override
+ Iterable generateInstances(
+ {Interval intersectingInterval}) sync* {
+ final RecurrenceRule rrule = rruleBasedOnCalendar;
+
+ // Calculate recurrences
+ // int i = 0;
+
+ 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;
+ if (start > intersectingInterval.end) break;
+ }
+
+ bool skip = false;
+ for (final holiday in calendar?.holidays ?? []) {
+ final holidayInterval = Interval(holiday.startDate, holiday.endDate);
+ if (holidayInterval.includes(start)) {
+ // Skip holidays
+ skip = true;
+ }
+ }
+
+ if (!skip) {
+ yield UniEventInstance(
+ // id: '$id-$i',
+ title: name,
+ mainEvent: this,
+ color: color,
+ start: start.copyWithUtc(),
+ end: end.copyWithUtc(),
+ 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..e7dec06e5 100644
--- a/lib/pages/timetable/model/events/uni_event.dart
+++ b/lib/pages/timetable/model/events/uni_event.dart
@@ -1,213 +1,225 @@
-//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: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:timetable/timetable.dart';
+
+import '../../../../generated/l10n.dart';
+import '../../../classes/model/class.dart';
+import '../../timetable_utils.dart';
+import '../academic_calendar.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;
+ case UniEventType.other:
+ return S.current.uniEventTypeOther;
+ }
+ 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;
+ case UniEventType.other:
+ return Colors.white;
+ }
+ return Colors.white;
+ }
+}
+
+class UniEvent {
+ const UniEvent({
+ @required this.start,
+ @required this.period,
+ @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 DateTime start;
+ final Period period;
+ 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;
+ }
+
+ // UniEventInstance toUniEventInstance() {
+ // return UniEventInstance(
+ // title: name,
+ // mainEvent: this,
+ // start: start,
+ // end: start.add(period.toTime().toDuration),
+ // location: location,
+ // );
+ // }
+
+ Iterable generateInstances(
+ {Interval intersectingInterval}) sync* {
+ final DateTime end = start.add(period.toTime().toDuration);
+ if (intersectingInterval != null) {
+ if (end < intersectingInterval.start ||
+ start > intersectingInterval.end) {
+ return;
+ }
+ }
+
+ yield UniEventInstance(
+ // id: id,
+ title: name,
+ mainEvent: this,
+ color: color,
+ start: start.copyWithUtc(),
+ end: start.add(period.toTime().toDuration).copyWithUtc(),
+ location: location,
+ );
+ }
+}
+
+class UniEventInstance extends Event {
+ UniEventInstance({
+ @required this.title,
+ @required this.mainEvent,
+ @required DateTime start,
+ @required DateTime end,
+ Color color,
+ this.location,
+ this.info,
+ }) : color = color ?? mainEvent?.color,
+ super(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 DateTime end =
+ this.end.isMidnight() ? this.end.subtractDays(1) : this.end;
+
+ String string = useRelativeDayFormat && start.isToday
+ ? S.current.labelToday
+ : useRelativeDayFormat && start.subtractDays(1).isToday
+ ? S.current.labelTomorrow
+ : start.toStringWithFormat('EEEE, dd MMMM');
+
+ if (!start.isMidnight()) {
+ string += ' • ${start.toStringWithFormat('HH:mm')}';
+ }
+ if (start.atStartOfDay != end.atStartOfDay) {
+ string += ' - ${end.toStringWithFormat('EEEE, dd MMMM')}';
+ }
+ if (!end.isMidnight()) {
+ if (start.atStartOfDay != end.atStartOfDay) {
+ string += ' • ';
+ } else {
+ string += '-';
+ }
+ 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 cc54f7123..929cecbbb 100644
--- a/lib/pages/timetable/service/google_calendar_services.dart
+++ b/lib/pages/timetable/service/google_calendar_services.dart
@@ -1,164 +1,167 @@
-//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 'package:rrule/rrule.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 {
+ 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];
+
+ 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;
+
+ 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.period.toTime().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 != null ? classHeader.acronym : uniEvent.name
+ ..colorId = (uniEvent.type.googleCalendarColor.index).toString()
+ ..location = uniEvent.location;
+
+ if (uniEvent is RecurringUniEvent) {
+ final String rruleBasedOnCalendarString = uniEvent.rruleBasedOnCalendar
+ .toString(
+ options: const RecurrenceRuleToStringOptions(isTimeUtc: true));
+ 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;
+ case UniEventType.other:
+ return GoogleCalendarColorNames.undefined;
+ }
+ 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..e30d250a2 100644
--- a/lib/pages/timetable/service/uni_event_provider.dart
+++ b/lib/pages/timetable/service/uni_event_provider.dart
@@ -1,206 +1,227 @@
-import 'package:flutter/material.dart';
+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 '../../../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) {
+ 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 DateTimeExtension on DateTime {
+ Timestamp toTimestamp() => Timestamp.fromDate(this);
+}
+
+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).toDate(),
+ end: (json['end'] as Timestamp).toDate(),
+ 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).toDate(),
+ period: 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).toDate(),
+ period: 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).toDate(),
+ period: 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.copyWithoutUtc().toTimestamp(),
+ 'duration': period.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();
+ }
-//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