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> snap) { -// final data = snap.data(); -// return AcademicCalendar( -// id: snap.id, -// semesters: _eventsFromMapList(data['semesters'], 'semester'), -// holidays: _eventsFromMapList(data['holidays'], 'holiday'), -// exams: _eventsFromMapList(data['exams'], 'examSession'), -// ); -// } -//} - -class UniEventProvider // extends EventProvider - with - ChangeNotifier { + if (this is ClassEvent) { + json['teacher'] = (this as ClassEvent).teacher?.name; + } + + return json; + } +} + +extension AcademicCalendarExtension on AcademicCalendar { + static List _eventsFromMapList( + List list, String type) => + List.from((list ?? []).asMap().map((index, e) { + e['type'] = type; + return MapEntry( + index, UniEventExtension.fromJSON(type + index.toString(), e)); + }).values); + + static AcademicCalendar fromSnap( + DocumentSnapshot> snap) { + final data = snap.data(); + return AcademicCalendar( + id: snap.id, + semesters: _eventsFromMapList(data['semesters'], 'semester'), + holidays: _eventsFromMapList(data['holidays'], 'holiday'), + exams: _eventsFromMapList(data['exams'], 'examSession'), + ); + } +} + +class UniEventProvider with ChangeNotifier { UniEventProvider({AuthProvider authProvider, PersonProvider personProvider}) : _authProvider = authProvider ?? AuthProvider(), _personProvider = personProvider ?? PersonProvider() { -// fetchCalendars(); + fetchCalendars(); } -// final Map _calendars = {}; + final Map _calendars = {}; ClassProvider _classProvider; FilterProvider _filterProvider; final AuthProvider _authProvider; @@ -209,220 +230,254 @@ class UniEventProvider // extends EventProvider Filter _filter; bool empty; -// Future> fetchCalendars() async { -// final QuerySnapshot> query = -// await FirebaseFirestore.instance.collection('calendars').get(); -// for (final doc in query.docs) { -// _calendars[doc.id] = AcademicCalendarExtension.fromSnap(doc); -// } -// -// notifyListeners(); -// return _calendars; -// } -// -// Future checkIfEmpty(List>> streams) async { -// for (final stream in streams) { -// if ((await stream.first)?.isNotEmpty ?? false) { -// empty = false; -// return; -// } -// } -// empty = true; -// } -// -// Stream> get _events { -// if (!_authProvider.isAuthenticated || -// _filter == null || -// _calendars == null) { -// return Stream.value([]); -// } -// -// final streams = >>[]; -// -// if (_filter.relevantNodes.length > 1) { -// for (final classId in _classIds ?? []) { -// final Stream> stream = FirebaseFirestore.instance -// .collection('events') -// .where('class', isEqualTo: classId) -// .where('degree', isEqualTo: _filter.baseNode) -// .where('relevance', -// arrayContainsAny: _filter.relevantNodes..remove('All')) -// .snapshots() -// .asyncMap((snapshot) async { -// final events = []; -// -// try { -// for (final doc in snapshot.docs) { -// ClassHeader classHeader; -// Person teacher; -// final data = doc.data(); -// if (data['class'] != null) { -// classHeader = -// await _classProvider.fetchClassHeader(data['class']); -// } -// if (data['teacher'] != null) { -// teacher = await _personProvider.fetchPerson(data['teacher']); -// } -// -// events.add(UniEventExtension.fromJSON(doc.id, data, -// classHeader: classHeader, -// teacher: teacher, -// calendars: _calendars)); -// } -// return events.where((element) => element != null).toList(); -// } catch (e) { -// print(e); -// return events; -// } -// }); -// streams.add(stream); -// } -// } -// -// checkIfEmpty(streams); -// -// final stream = StreamZip(streams); -// -// // Flatten zipped streams -// return stream.map((events) => events.expand((i) => i).toList()); -// } -// -// Future exportToGoogleCalendar() async { -// final Stream> eventsStream = _events; -// final List streamElement = await eventsStream.first; -// final List googleCalendarEvents = []; -// for (final UniEvent eventInstance in streamElement) { -// final g_cal.Event googleCalendarEvent = convertEvent(eventInstance); -// googleCalendarEvents.add(googleCalendarEvent); -// } -// await insertGoogleEvents(googleCalendarEvents); -// } -// -// @override -// Stream> getAllDayEventsIntersecting( -// DateInterval interval) { -// return _events.map((events) => events -// .map((event) => event.generateInstances(intersectingInterval: interval)) -// .expand((i) => i) -// .allDayEvents -// .followedBy(_calendars.values.map((cal) { -// final List events = cal.holidays + cal.exams; -// return events -// .where((event) => -// event.relevance == null || -// (_filter != null && -// event.degree == _filter.baseNode && -// event.relevance.any(_filter.relevantNodes.contains))) -// .map((e) => e.generateInstances(intersectingInterval: interval)) -// .expand((e) => e); -// }).expand((e) => e))); -// } -// -// @override -// Stream> getPartDayEventsIntersecting( -// LocalDate date) { -// return _events.map((events) => events -// .map((event) => event.generateInstances( -// intersectingInterval: DateInterval(date, date))) -// .expand((i) => i) -// .partDayEvents); -// } -// -// Future> getUpcomingEvents(LocalDate date, -// {int limit = 3}) async { -// return _events -// .map((events) => events -// .where((event) => !(event is AllDayUniEvent)) -// .map((event) => event.generateInstances( -// intersectingInterval: DateInterval(date, date.addDays(6)))) -// .expand((i) => i) -// .sortedByStartLength() -// .where((element) => -// element.end.toDateTimeLocal().isAfter(DateTime.now())) -// .take(limit)) -// .first; -// } -// -// Future> getAllEventsOfClass(String classId) async { -// return _events -// .map((events) => -// events.where((event) => event.classHeader.id == classId)) -// .first; -// } -// + Future> fetchCalendars() async { + final QuerySnapshot> query = + await FirebaseFirestore.instance.collection('calendars').get(); + for (final doc in query.docs) { + _calendars[doc.id] = AcademicCalendarExtension.fromSnap(doc); + } + + notifyListeners(); + return _calendars; + } + + Future checkIfEmpty(List>> streams) async { + for (final stream in streams) { + if ((await stream.first)?.isNotEmpty ?? false) { + empty = false; + return; + } + } + empty = true; + } + + Stream> get _events { + if (!_authProvider.isAuthenticated || + _filter == null || + _calendars == null) { + return Stream.value([]); + } + + final streams = >>[]; + + if (_filter.relevantNodes.length > 1) { + for (final classId in _classIds ?? []) { + final Stream> stream = FirebaseFirestore.instance + .collection('events') + .where('class', isEqualTo: classId) + .where('degree', isEqualTo: _filter.baseNode) + .where('relevance', + arrayContainsAny: _filter.relevantNodes..remove('All')) + .snapshots() + .asyncMap((snapshot) async { + final events = []; + try { + for (final doc in snapshot.docs) { + ClassHeader classHeader; + Person teacher; + final data = doc.data(); + if (data['class'] != null) { + classHeader = + await _classProvider.fetchClassHeader(data['class']); + } + if (data['teacher'] != null) { + teacher = await _personProvider.fetchPerson(data['teacher']); + } + + events.add(UniEventExtension.fromJSON(doc.id, data, + classHeader: classHeader, + teacher: teacher, + calendars: _calendars)); + } + return events.where((element) => element != null).toList(); + } catch (e) { + print(e); + return events; + } + }); + streams.add(stream); + } + } + + checkIfEmpty(streams); + + final stream = StreamZip(streams); + + // Flatten zipped streams + return stream.map((events) => events.expand((i) => i).toList()); + } + + Future exportToGoogleCalendar() async { + final Stream> eventsStream = _events; + final List uniEvents = await eventsStream.first; + + final List googleCalendarEvents = []; + for (final UniEvent uniEvent in uniEvents) { + final g_cal.Event googleCalendarEvent = convertEvent(uniEvent); + googleCalendarEvents.add(googleCalendarEvent); + } + await insertGoogleEvents(googleCalendarEvents); + } + + Stream> getEventsIntersecting(Interval interval) { + final streams = >>[]; + final Stream> allDay = + getAllDayEventsIntersecting(interval); + final Stream> partDay = + getPartDayEventsIntersecting(interval); + streams..add(allDay)..add(partDay); + final StreamZip> stream = StreamZip(streams); + + // Flatten zipped streams + return stream.map((events) => events + .expand((i) => i) + .map((UniEventInstance event) => event.copyWith( + start: event.start.copyWithUtc(), end: event.end.copyWithUtc())) + .toList()); + } + + Iterable getAllDayUniEventsForCalendar(AcademicCalendar cal) { + final List events = cal.holidays + cal.exams; + return events.where((event) => + event.relevance == null || + (_filter != null && + event.degree == _filter.baseNode && + event.relevance.any(_filter.relevantNodes.contains))); + } + + Stream> getAllDayEventsIntersecting( + Interval interval) { + return _events.map( + (events) => events + .map((event) => + event.generateInstances(intersectingInterval: interval)) + .expand((i) => i) + .where((event) => event.isAllDay) + .followedBy( + _calendars.values.map( + (AcademicCalendar cal) { + final Iterable allDayUniEvents = + getAllDayUniEventsForCalendar(cal); + final Iterable allDayUniEventInstances = + allDayUniEvents + .map((e) => + e.generateInstances(intersectingInterval: interval)) + .expand((e) => e); + return allDayUniEventInstances; + }, + ).expand((e) => e), + ), + ); + } + + Stream> getPartDayEventsIntersecting( + Interval interval) { + return _events.map((events) => events + .map((event) => event.generateInstances( + intersectingInterval: Interval(interval.start, interval.end))) + .expand((i) => i) + .where((event) => event.isPartDay)); + } + + Future> getUpcomingEvents(DateTime date, + {int limit = 3}) async { + return _events + .map((events) => events + .where((event) => !(event is AllDayUniEvent)) + .map((event) => event.generateInstances( + intersectingInterval: Interval(date, date.addDays(6)))) + .expand((i) => i) + .sortedByStartLength() + .where((element) => element.end.isAfter(DateTime.now())) + .take(limit)) + .first; + } + + Future> getAllEventsOfClass(String classId) async { + return _events + .map((events) => + events.where((event) => event.classHeader.id == classId)) + .first; + } + void updateClasses(ClassProvider classProvider) { -// _classProvider = classProvider; -// _classProvider.fetchUserClassIds(_authProvider.uid).then((classIds) { -// _classIds = classIds; -// notifyListeners(); -// }); + _classProvider = classProvider; + _classProvider.fetchUserClassIds(_authProvider.uid).then((classIds) { + _classIds = classIds; + notifyListeners(); + }); } -// void updateFilter(FilterProvider filterProvider) { -// _filterProvider = filterProvider; -// _filterProvider.fetchFilter().then((filter) { -// _filter = filter; -// notifyListeners(); -// }); + _filterProvider = filterProvider; + _filterProvider.fetchFilter().then((filter) { + _filter = filter; + notifyListeners(); + }); + } + + Future addEvent(UniEvent event) async { + try { + await FirebaseFirestore.instance.collection('events').add(event.toData()); + notifyListeners(); + return true; + } catch (e) { + _errorHandler(e); + return false; + } + } + + Future updateEvent(UniEvent event) async { + try { + final ref = FirebaseFirestore.instance.collection('events').doc(event.id); + + if ((await ref.get()).data == null) { + print('Event not found.'); + return false; + } + + await ref.update(event.toData()); + notifyListeners(); + return true; + } catch (e) { + _errorHandler(e); + return false; + } + } + + Future deleteEvent(UniEvent event) async { + try { + DocumentReference ref; + ref = FirebaseFirestore.instance.collection('events').doc(event.id); + await ref.delete(); + notifyListeners(); + return true; + } catch (e) { + _errorHandler(e); + return false; + } + } + + @override + // ignore: must_call_super + void dispose() { + // TODO(IoanaAlexandru): Find a better way to prevent Timetable from calling dispose on this provider + } + + void _errorHandler(dynamic e, {bool showToast = true}) { + print(e.message); + if (showToast) { + if (e.message.contains('PERMISSION_DENIED')) { + AppToast.show(S.current.errorPermissionDenied); + } else { + AppToast.show(S.current.errorSomethingWentWrong); + } + } + } + + String updateTimetablePageTitle(DateController _dateController) { + return _authProvider.isAuthenticated && !_authProvider.isAnonymous + ? _dateController?.currentMonth?.titleCase + : S.current.navigationTimetable; } -// -// Future addEvent(UniEvent event) async { -// try { -// await FirebaseFirestore.instance.collection('events').add(event.toData()); -// notifyListeners(); -// return true; -// } catch (e) { -// _errorHandler(e); -// return false; -// } -// } -// -// Future updateEvent(UniEvent event) async { -// try { -// final ref = FirebaseFirestore.instance.collection('events').doc(event.id); -// -// if ((await ref.get()).data == null) { -// print('Event not found.'); -// return false; -// } -// -// await ref.update(event.toData()); -// notifyListeners(); -// return true; -// } catch (e) { -// _errorHandler(e); -// return false; -// } -// } -// -// Future deleteEvent(UniEvent event) async { -// try { -// DocumentReference ref; -// ref = FirebaseFirestore.instance.collection('events').doc(event.id); -// await ref.delete(); -// notifyListeners(); -// return true; -// } catch (e) { -// _errorHandler(e); -// return false; -// } -// } -// -// @override -// // ignore: must_call_super -// void dispose() { -// // TODO(IoanaAlexandru): Find a better way to prevent Timetable from calling dispose on this provider -// } -// -// void _errorHandler(dynamic e, {bool showToast = true}) { -// print(e.message); -// if (showToast) { -// if (e.message.contains('PERMISSION_DENIED')) { -// AppToast.show(S.current.errorPermissionDenied); -// } else { -// AppToast.show(S.current.errorSomethingWentWrong); -// } -// } -// } } diff --git a/lib/pages/timetable/timetable_utils.dart b/lib/pages/timetable/timetable_utils.dart new file mode 100644 index 000000000..035d48ee9 --- /dev/null +++ b/lib/pages/timetable/timetable_utils.dart @@ -0,0 +1,119 @@ +library timetable_utils; + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:time_machine/time_machine.dart'; + +// ignore: implementation_imports +import 'package:timetable/src/utils.dart'; +import 'package:timetable/timetable.dart'; + +import 'model/events/recurring_event.dart'; +import 'model/events/uni_event.dart'; +export 'package:timetable/src/utils.dart'; + +extension DateTimeExtension on DateTime { + bool isMidnight() => hour == 0 && minute == 0 && second == 0; + + DateTime atMidnight() => + copyWith(hour: 0, minute: 0, second: 0, millisecond: 0); + + DateTime addDays(int noDays) => add(Duration(days: noDays)); + + DateTime subtractDays(int noDays) => subtract(Duration(days: noDays)); + + String toStringWithFormat(String format) { + return DateFormat(format).format(this); + } + + DateTime at({DateTime dateTime, TimeOfDay timeOfDay}) { + if (dateTime != null) { + return copyWith( + hour: dateTime.hour, + minute: dateTime.minute, + second: dateTime.second, + millisecond: dateTime.millisecond); + } else if (timeOfDay != null) { + return copyWith( + hour: timeOfDay.hour, + minute: timeOfDay.minute, + second: 0, + millisecond: 0); + } else { + return null; + } + } + + TimeOfDay toTimeOfDay() { + return TimeOfDay( + hour: hour, + minute: minute, + ); + } + + /// Returns the same DateTime with isUtc set as true to avoid hour changes from original toUtc() function of [DateTime] + DateTime copyWithUtc() { + return copyWith(hour: hour, isUtc: true); + } + + DateTime copyWithoutUtc() { + return copyWith(hour: hour, isUtc: false); + } +} + +extension DurationExtension on Duration { + Period toPeriod() { + return Period(minutes: inMinutes).normalize(); + } +} + +extension RecurringUniEventExtension on RecurringUniEvent { + RecurringUniEvent copyWith({ + DateTime start, + }) { + return RecurringUniEvent( + start: start ?? this.start, + period: period, + id: id, + name: name, + location: location, + color: color, + type: type, + classHeader: classHeader, + calendar: calendar, + relevance: relevance, + degree: degree, + addedBy: addedBy, + editable: editable, + rrule: rrule, + ); + } +} + +extension UniEventInstanceExtension on UniEventInstance { + UniEventInstance copyWith({ + DateTime start, + DateTime end, + }) { + return UniEventInstance( + start: start ?? this.start, + end: end ?? this.end, + title: title, + mainEvent: mainEvent, + color: color, + location: location, + info: info); + } +} + +extension MonthController on DateController { + String get currentMonth => DateTime( + value?.date?.year, + value?.date?.month, + 1, + 0, + 0, + 0, + ).toStringWithFormat('MMMM'); +// LocalDateTime(2020, this.value.monthOfYear, 1, 1, 1, 1).toString('MMMM'); +} diff --git a/lib/pages/timetable/view/date_header.dart b/lib/pages/timetable/view/date_header.dart deleted file mode 100644 index f28fff8db..000000000 --- a/lib/pages/timetable/view/date_header.dart +++ /dev/null @@ -1,72 +0,0 @@ -//import 'package:auto_size_text/auto_size_text.dart'; -//import 'package:black_hole_flutter/black_hole_flutter.dart'; -//import 'package:flutter/material.dart'; -//import 'package:time_machine/time_machine.dart'; -//import 'package:time_machine/time_machine_text_patterns.dart'; -//// ignore: implementation_imports -//import 'package:timetable/src/header/date_indicator.dart'; -//// ignore: implementation_imports -//import 'package:timetable/src/theme.dart'; -//// ignore: implementation_imports -//import 'package:timetable/src/utils/utils.dart'; -// -//// TODO(IoanaAlexandru): This is a temporary fix because the default -//// [DateHeader] from the timetable package has an overflow when the culture -//// is set to Romanian. We copied it here with minor changes and it can be -//// removed once the timetable package has it fixed. -//class DateHeader extends StatelessWidget { -// const DateHeader(this.date, {Key key}) : super(key: key); -// -// final LocalDate date; -// -// @override -// Widget build(BuildContext context) { -// return Column( -// mainAxisSize: MainAxisSize.min, -// crossAxisAlignment: CrossAxisAlignment.center, -// children: [ -// WeekdayIndicator(date), -// const SizedBox(height: 4), -// DateIndicator(date), -// ], -// ); -// } -//} -// -//class WeekdayIndicator extends StatelessWidget { -// const WeekdayIndicator(this.date, {Key key}) : super(key: key); -// -// final LocalDate date; -// -// @override -// Widget build(BuildContext context) { -// final theme = context.theme; -// final timetableTheme = context.timetableTheme; -// -// final states = DateIndicator.statesFor(date); -// final pattern = timetableTheme?.weekDayIndicatorPattern?.resolve(states) ?? -// LocalDatePattern.createWithCurrentCulture('ddd'); -// final decoration = -// timetableTheme?.weekDayIndicatorDecoration?.resolve(states) ?? -// const BoxDecoration(); -// final textStyle = -// timetableTheme?.weekDayIndicatorTextStyle?.resolve(states) ?? -// TextStyle( -// color: date.isToday -// ? timetableTheme?.primaryColor ?? theme.primaryColor -// : theme.highEmphasisOnBackground, -// ); -// -// return DecoratedBox( -// decoration: decoration, -// child: Padding( -// padding: const EdgeInsets.all(4), -// child: AutoSizeText( -// pattern.format(date), -// style: textStyle, -// maxLines: 1, -// ), -// ), -// ); -// } -//} diff --git a/lib/pages/timetable/view/events/add_event_view.dart b/lib/pages/timetable/view/events/add_event_view.dart index ab3675547..6a09fcd8a 100644 --- a/lib/pages/timetable/view/events/add_event_view.dart +++ b/lib/pages/timetable/view/events/add_event_view.dart @@ -1,765 +1,636 @@ -// import 'package:acs_upb_mobile/authentication/model/user.dart'; -// import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; -// import 'package:acs_upb_mobile/generated/l10n.dart'; -// import 'package:acs_upb_mobile/navigation/routes.dart'; -// import 'package:acs_upb_mobile/pages/classes/model/class.dart'; -// import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; -// import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; -// import 'package:acs_upb_mobile/pages/filter/view/relevance_picker.dart'; -// import 'package:acs_upb_mobile/pages/people/model/person.dart'; -// import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; -// import 'package:acs_upb_mobile/pages/people/view/people_page.dart'; -// import 'package:acs_upb_mobile/pages/timetable/model/academic_calendar.dart'; -// import 'package:acs_upb_mobile/pages/timetable/model/events/all_day_event.dart'; -// import 'package:acs_upb_mobile/pages/timetable/model/events/class_event.dart'; -// import 'package:acs_upb_mobile/pages/timetable/model/events/recurring_event.dart'; -// import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -// import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; -// import 'package:acs_upb_mobile/resources/custom_icons.dart'; -// import 'package:acs_upb_mobile/resources/locale_provider.dart'; -// import 'package:acs_upb_mobile/widgets/button.dart'; -// import 'package:acs_upb_mobile/widgets/dialog.dart'; -// import 'package:acs_upb_mobile/widgets/scaffold.dart'; -// import 'package:acs_upb_mobile/widgets/selectable.dart'; -// import 'package:acs_upb_mobile/widgets/toast.dart'; -// import 'package:dotted_line/dotted_line.dart'; -// import 'package:flutter/material.dart'; -// import 'package:flutter/rendering.dart'; -// import 'package:flutter_feather_icons/flutter_feather_icons.dart'; -// import 'package:provider/provider.dart'; -// import 'package:rrule/rrule.dart'; -// import 'package:time_machine/time_machine.dart' as time_machine show DayOfWeek; -// import 'package:time_machine/time_machine.dart' hide DayOfWeek; -// import 'package:time_machine/time_machine_text_patterns.dart'; -// -// class AddEventView extends StatefulWidget { -// /// If the `id` of [initialEvent] is not null, this acts like an "Edit event" -// /// page starting from the info in [initialEvent]. Otherwise, it acts like an -// /// "Add event" page with optional default values based on [initialEvent]. -// const AddEventView({Key key, this.initialEvent}) : super(key: key); -// -// final UniEvent initialEvent; -// -// @override -// _AddEventViewState createState() => _AddEventViewState(); -// } -// -// class _AddEventViewState extends State { -// final formKey = GlobalKey(); -// -// TextEditingController locationController; -// RelevanceController relevanceController = RelevanceController(); -// -// UniEventType selectedEventType; -// ClassHeader selectedClass; -// Person selectedTeacher; -// String selectedCalendar; -// LocalTime startTime; -// Period duration; -// Map weekSelected = { -// WeekType.odd: null, -// WeekType.even: null, -// }; -// Map<_DayOfWeek, bool> weekDaySelected = { -// _DayOfWeek.monday: false, -// _DayOfWeek.tuesday: false, -// _DayOfWeek.wednesday: false, -// _DayOfWeek.thursday: false, -// _DayOfWeek.friday: false, -// _DayOfWeek.saturday: false, -// _DayOfWeek.sunday: false, -// }; -// -// int selectedSemester = 1; -// -// AllDayUniEvent get semester => -// calendars[selectedCalendar]?.semesters?.elementAt(selectedSemester - 1); -// -// List classHeaders = []; -// List classTeachers = []; -// User user; -// Map calendars = {}; -// -// @override -// void initState() { -// super.initState(); -// -// user = -// Provider.of(context, listen: false).currentUserFromCache; -// Provider.of(context, listen: false) -// .fetchClassHeaders(uid: user.uid) -// .then((headers) => setState(() => classHeaders = headers)); -// Provider.of(context, listen: false) -// .fetchPeople() -// .then((teachers) => setState(() => classTeachers = teachers)); -// Provider.of(context, listen: false) -// .fetchCalendars() -// .then((calendars) { -// setState(() { -// this.calendars = calendars; -// selectedCalendar = calendars.keys.first; -// }); -// -// if (widget.initialEvent?.id != null) { -// selectedCalendar = widget.initialEvent.calendar.id; -// final AllDayUniEvent secondSemester = -// widget.initialEvent.calendar.semesters.last; -// selectedSemester = -// DateInterval(secondSemester.startDate, secondSemester.endDate) -// .contains(widget.initialEvent.start.calendarDate) -// ? 2 -// : 1; -// } else { -// bool foundSemester = false; -// for (final calendar in calendars.entries) { -// for (final semester in calendar.value.semesters) { -// final LocalDate date = -// widget.initialEvent.start.calendarDate ?? LocalDate.today(); -// if (date.isBeforeOrDuring(semester)) { -// // semester.id is represented as "semesterN", where "semester0" is the first semester -// selectedSemester = -// 1 + int.tryParse(semester.id[semester.id.length - 1]); -// selectedCalendar = calendar.key; -// foundSemester = true; -// break; -// } -// } -// if (foundSemester) break; -// } -// if (!foundSemester) { -// selectedCalendar = calendars.entries.last.value.id; -// selectedSemester = 2; -// } -// } -// -// if (widget.initialEvent != null && -// widget.initialEvent is RecurringUniEvent) { -// final RecurringUniEvent event = widget.initialEvent; -// if (event.rrule.interval != 1) { -// final rule = WeekYearRules.iso; -// if (rule.getWeekOfWeekYear(semester.start.calendarDate) == -// rule.getWeekOfWeekYear(event.start.calendarDate)) { -// // Week is odd -// weekSelected[WeekType.even] = false; -// weekSelected[WeekType.odd] = true; -// } else { -// // Week is even -// weekSelected[WeekType.even] = true; -// weekSelected[WeekType.odd] = false; -// } -// } -// } -// -// setState(() { -// weekSelected[WeekType.even] ??= true; -// weekSelected[WeekType.odd] ??= true; -// }); -// }); -// -// selectedEventType = widget.initialEvent?.type; -// selectedClass = widget.initialEvent?.classHeader; -// selectedTeacher = widget.initialEvent is ClassEvent -// ? (widget.initialEvent as ClassEvent).teacher -// : null; -// locationController = -// TextEditingController(text: widget.initialEvent?.location ?? ''); -// -// final startHour = widget.initialEvent?.start?.hourOfDay ?? 8; -// duration = widget.initialEvent?.duration ?? const Period(hours: 2); -// startTime = LocalTime(startHour, 0, 0); -// -// var initialWeekDays = [ -// _DayOfWeek.from(widget.initialEvent?.start?.dayOfWeek) ?? -// _DayOfWeek.monday -// ]; -// if (widget.initialEvent != null && -// widget.initialEvent is RecurringUniEvent && -// (widget.initialEvent as RecurringUniEvent) -// .rrule -// .byWeekDays -// .isNotEmpty) { -// initialWeekDays = (widget.initialEvent as RecurringUniEvent) -// .rrule -// .byWeekDays -// .map((entry) => _DayOfWeek.from(entry.day)) -// .toList(); -// } -// for (final initialWeekDay in initialWeekDays) { -// weekDaySelected[initialWeekDay] = true; -// } -// } -// -// @override -// Widget build(BuildContext context) { -// return AppScaffold( -// title: Text(widget.initialEvent?.id == null -// ? S.current.actionAddEvent -// : S.current.actionEditEvent), -// actions: widget.initialEvent?.id == null -// ? [_saveButton()] -// : [ -// _saveButton(), -// _deleteButton(), -// ], -// body: ListView( -// children: [ -// Padding( -// padding: const EdgeInsets.only(left: 16, right: 16), -// child: Form( -// key: formKey, -// child: Column( -// mainAxisSize: MainAxisSize.min, -// children: [ -// Row( -// mainAxisSize: MainAxisSize.min, -// children: [ -// Expanded( -// child: DropdownButtonFormField( -// decoration: InputDecoration( -// labelText: S.current.labelUniversityYear, -// prefixIcon: -// const Icon(Icons.calendar_today_outlined), -// ), -// value: selectedCalendar, -// items: calendars.keys.map((key) { -// final year = int.tryParse(key); -// return DropdownMenuItem( -// value: key, -// child: Text( -// year != null ? '$year-${year + 1}' : key), -// ); -// }).toList(), -// onChanged: (selection) => -// selectedCalendar = selection, -// ), -// ), -// const SizedBox(width: 16), -// Expanded( -// child: DropdownButtonFormField( -// decoration: InputDecoration( -// labelText: S.current.labelSemester, -// prefixIcon: const Icon(FeatherIcons.columns), -// ), -// value: selectedSemester, -// items: [1, 2] -// .map((semester) => DropdownMenuItem( -// value: semester, -// child: Text(semester.toString()), -// )) -// .toList(), -// onChanged: (selection) => -// selectedSemester = selection, -// ), -// ), -// ], -// ), -// RelevanceFormField( -// controller: relevanceController, -// validator: (_) { -// if (relevanceController.customRelevance?.isEmpty ?? -// true) { -// return S.current.warningYouNeedToSelectAtLeastOne; -// } -// return null; -// }, -// ), -// DropdownButtonFormField( -// decoration: InputDecoration( -// labelText: S.current.labelType, -// prefixIcon: const Icon(Icons.category_outlined), -// ), -// value: selectedEventType, -// items: UniEventTypeExtension.classTypes -// .map( -// (type) => DropdownMenuItem( -// value: type, -// child: Text(type.toLocalizedString()), -// ), -// ) -// .toList(), -// onChanged: (selection) { -// formKey.currentState.validate(); -// setState(() => selectedEventType = selection); -// }, -// validator: (selection) { -// if (selection == null) { -// return S.current.errorEventTypeCannotBeEmpty; -// } -// return null; -// }, -// ), -// if (selectedEventType != null) -// Column( -// children: [ -// if (classHeaders.isNotEmpty) -// DropdownButtonFormField( -// isExpanded: true, -// decoration: InputDecoration( -// labelText: S.current.labelClass, -// prefixIcon: const Icon(FeatherIcons.bookOpen), -// ), -// value: selectedClass, -// items: classHeaders -// .map( -// (header) => DropdownMenuItem( -// value: header, child: Text(header.name)), -// ) -// .toList(), -// onChanged: (selection) { -// formKey.currentState.validate(); -// setState(() => selectedClass = selection); -// }, -// validator: (selection) { -// if (selection == null) { -// return S.current.errorClassCannotBeEmpty; -// } -// return null; -// }, -// ), -// if ([UniEventType.lecture].contains(selectedEventType)) -// AutocompletePerson( -// key: const Key('AutocompleteLecturer'), -// labelText: S.current.labelLecturer, -// formKey: formKey, -// onSaved: (value) => selectedTeacher = value, -// classTeachers: classTeachers, -// personDisplayed: selectedTeacher, -// ), -// TextFormField( -// controller: locationController, -// decoration: InputDecoration( -// labelText: S.current.labelLocation, -// prefixIcon: const Icon(FeatherIcons.mapPin), -// ), -// onChanged: (_) => setState(() {}), -// ), -// timeIntervalPicker(), -// if (weekSelected[WeekType.odd] != null && -// weekSelected[WeekType.even] != null) -// SelectableFormField( -// key: const ValueKey('week_picker'), -// icon: FeatherIcons.calendar, -// label: S.current.labelWeek, -// initialValues: weekSelected, -// validator: (selection) { -// if (selection.values -// .where((e) => e != false) -// .isEmpty) { -// return S -// .of(context) -// .warningYouNeedToSelectAtLeastOne; -// } -// return null; -// }, -// ), -// SelectableFormField( -// key: const ValueKey('day_picker'), -// icon: Icons.today_outlined, -// label: S.current.labelDay, -// initialValues: weekDaySelected, -// validator: (selection) { -// if (selection.values -// .where((e) => e != false) -// .isEmpty) { -// return S -// .of(context) -// .warningYouNeedToSelectAtLeastOne; -// } -// return null; -// }, -// ), -// ], -// ), -// const SizedBox(width: 16), -// ], -// ), -// ), -// ), -// ], -// ), -// ); -// } -// -// AppDialog _deletionConfirmationDialog(BuildContext context) => AppDialog( -// icon: const Icon(Icons.delete_outlined), -// title: S.current.actionDeleteEvent, -// info: S.current.messageThisCouldAffectOtherStudents, -// message: S.current.messageDeleteEvent, -// actions: [ -// AppButton( -// text: S.current.actionDeleteEvent, -// width: 130, -// onTap: () async { -// final res = -// await Provider.of(context, listen: false) -// .deleteEvent(widget.initialEvent); -// if (res) { -// Navigator.of(context) -// .popUntil(ModalRoute.withName(Routes.home)); -// AppToast.show(S.current.messageEventDeleted); -// } -// }, -// ) -// ], -// ); -// -// AppScaffoldAction _saveButton() => AppScaffoldAction( -// text: S.current.buttonSave, -// onPressed: () async { -// if (!formKey.currentState.validate()) return; -// -// LocalDateTime start = semester.startDate.at(startTime); -// if (weekSelected[WeekType.even] && !weekSelected[WeekType.odd]) { -// // Event is every even week, add a week to start date -// start = start.add(const Period(weeks: 1)); -// } -// -// final rrule = RecurrenceRule( -// frequency: Frequency.weekly, -// byWeekDays: (Map<_DayOfWeek, bool>.from(weekDaySelected) -// ..removeWhere((key, value) => !value)) -// .keys -// .map((weekDay) => ByWeekDayEntry(weekDay)) -// .toSet(), -// interval: -// weekSelected[WeekType.odd] != weekSelected[WeekType.even] -// ? 2 -// : 1, -// until: semester.endDate.add(const Period(days: 1)).atMidnight()); -// -// final event = ClassEvent( -// teacher: selectedTeacher, -// rrule: rrule, -// start: start, -// duration: duration, -// id: widget.initialEvent?.id, -// relevance: relevanceController.customRelevance, -// degree: relevanceController.degree, -// location: locationController.text, -// type: selectedEventType, -// classHeader: selectedClass, -// calendar: calendars[selectedCalendar], -// addedBy: Provider.of(context, listen: false) -// .currentUserFromCache -// .uid); -// -// if (widget.initialEvent?.id == null) { -// final res = -// await Provider.of(context, listen: false) -// .addEvent(event); -// if (res) { -// Navigator.of(context).pop(); -// AppToast.show(S.current.messageEventAdded); -// } -// } else { -// final res = -// await Provider.of(context, listen: false) -// .updateEvent(event); -// if (res) { -// Navigator.of(context).popUntil(ModalRoute.withName(Routes.home)); -// AppToast.show(S.current.messageEventEdited); -// } -// } -// }, -// ); -// -// AppScaffoldAction _deleteButton() => AppScaffoldAction( -// icon: Icons.more_vert_outlined, -// items: { -// S.current.actionDeleteEvent: () => -// showDialog(context: context, builder: _deletionConfirmationDialog) -// }, -// onPressed: () => -// showDialog(context: context, builder: _deletionConfirmationDialog), -// ); -// -// Widget timeIntervalPicker() { -// final endTime = startTime.add(duration); -// final textColor = Theme.of(context).textTheme.headline4.color; -// return Padding( -// padding: const EdgeInsets.only(top: 10), -// child: Row( -// children: [ -// Padding( -// padding: const EdgeInsets.all(12), -// child: Icon( -// FeatherIcons.clock, -// color: CustomIcons.formIconColor(Theme.of(context)), -// ), -// ), -// TextButton( -// style: ButtonStyle( -// padding: MaterialStateProperty.all(EdgeInsets.zero), -// ), -// onPressed: () async { -// final TimeOfDay start = await showTimePicker( -// context: context, -// initialTime: startTime.toTimeOfDay(), -// ); -// setState(() => startTime = start.toLocalTime()); -// }, -// child: Text( -// startTime.toString('HH:mm'), -// style: Theme.of(context).textTheme.headline4, -// ), -// ), -// Expanded( -// child: Padding( -// padding: const EdgeInsets.symmetric(horizontal: 12), -// child: Column( -// children: [ -// Text( -// duration.toString().replaceAll(RegExp(r'[PT]'), ''), -// style: Theme.of(context) -// .textTheme -// .bodyText1 -// .copyWith(color: textColor), -// ), -// DottedLine( -// lineThickness: 4, -// dashRadius: 2, -// dashColor: textColor, -// ), -// // Text-sized box so that the line is centered -// SizedBox( -// height: Theme.of(context).textTheme.bodyText1.fontSize), -// ], -// ), -// ), -// ), -// TextButton( -// style: ButtonStyle( -// padding: MaterialStateProperty.all(EdgeInsets.zero), -// ), -// onPressed: () async { -// final TimeOfDay end = await showTimePicker( -// context: context, -// initialTime: startTime.add(duration).toTimeOfDay(), -// ); -// setState(() => duration = -// Period.differenceBetweenTimes(startTime, end.toLocalTime())); -// }, -// child: Text( -// endTime.toString('HH:mm'), -// style: Theme.of(context).textTheme.headline4, -// ), -// ), -// const SizedBox(width: 12), -// ], -// ), -// ); -// } -// } -// -// class RelevanceFormField extends FormField> { -// RelevanceFormField({ -// @required this.controller, -// String Function(List) validator, -// Key key, -// }) : super( -// key: key, -// autovalidateMode: AutovalidateMode.onUserInteraction, -// validator: validator, -// builder: (FormFieldState> state) { -// controller.onChanged = () { -// state.didChange(controller.customRelevance); -// }; -// final context = state.context; -// return Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// RelevancePicker( -// canBePrivate: false, -// canBeForEveryone: false, -// filterProvider: Provider.of(context), -// controller: controller, -// ), -// if (state.hasError) -// Padding( -// padding: const EdgeInsets.only(top: 10), -// child: Text( -// state.errorText, -// style: Theme.of(context) -// .textTheme -// .caption -// .copyWith(color: Theme.of(context).errorColor), -// ), -// ), -// ], -// ); -// }, -// ); -// -// final RelevanceController controller; -// } -// -// class SelectableFormField extends FormField> { -// SelectableFormField({ -// @required Map initialValues, -// @required IconData icon, -// @required String label, -// String Function(Map) validator, -// Key key, -// }) : super( -// autovalidateMode: AutovalidateMode.onUserInteraction, -// initialValue: initialValues, -// key: key, -// validator: validator, -// builder: (state) { -// final context = state.context; -// final labels = state.value.keys.toList(); -// return Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// IntrinsicHeight( -// child: Padding( -// padding: const EdgeInsets.only(top: 12, left: 12), -// child: Row( -// mainAxisSize: MainAxisSize.min, -// children: [ -// Icon(icon, -// color: -// CustomIcons.formIconColor(Theme.of(context))), -// const SizedBox(width: 12), -// Expanded( -// child: Column( -// mainAxisSize: MainAxisSize.min, -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Expanded( -// child: Text( -// label, -// style: Theme.of(context) -// .textTheme -// .caption -// .apply( -// color: Theme.of(context).hintColor), -// ), -// ), -// const SizedBox(height: 10), -// Row( -// children: [ -// Expanded( -// child: Container( -// height: 40, -// child: ListView.builder( -// itemCount: labels.length, -// scrollDirection: Axis.horizontal, -// itemBuilder: (context, index) { -// return Row( -// children: [ -// Selectable( -// label: labels[index] -// .toLocalizedString(), -// initiallySelected: -// state.value[labels[index]], -// onSelected: (selected) { -// state.value[labels[index]] = -// selected; -// state.didChange(state.value); -// }, -// ), -// const SizedBox(width: 10), -// ], -// ); -// }, -// ), -// ), -// ), -// ], -// ), -// ], -// ), -// ), -// ], -// ), -// ), -// ), -// if (state.hasError) -// Padding( -// padding: const EdgeInsets.only(top: 10), -// child: Text( -// state.errorText, -// style: Theme.of(context) -// .textTheme -// .caption -// .copyWith(color: Theme.of(context).errorColor), -// ), -// ), -// ], -// ); -// }, -// ); -// } -// -// class _DayOfWeek extends time_machine.DayOfWeek with Localizable { -// const _DayOfWeek(int value) : super(value); -// -// _DayOfWeek.from(time_machine.DayOfWeek dayOfWeek) : super(dayOfWeek.value); -// -// @override -// String toLocalizedString() { -// final helperDate = LocalDate.today().next(this); -// return LocalDatePattern.createWithCurrentCulture('ddd') -// .format(helperDate) -// .substring(0, 3); -// } -// -// static const _DayOfWeek monday = _DayOfWeek(1); -// static const _DayOfWeek tuesday = _DayOfWeek(2); -// static const _DayOfWeek wednesday = _DayOfWeek(3); -// static const _DayOfWeek thursday = _DayOfWeek(4); -// static const _DayOfWeek friday = _DayOfWeek(5); -// static const _DayOfWeek saturday = _DayOfWeek(6); -// static const _DayOfWeek sunday = _DayOfWeek(7); -// } -// -// class WeekType with Localizable { -// const WeekType(this._value); -// -// final int _value; -// -// int get value => _value; -// -// static const WeekType odd = WeekType(0); -// static const WeekType even = WeekType(1); -// -// @override -// int get hashCode => _value.hashCode; -// -// @override -// bool operator ==(dynamic other) => -// other is WeekType && other._value == _value || -// other is int && other == _value; -// -// @override -// String toLocalizedString() { -// switch (_value) { -// case 0: -// return S.current.labelOdd; -// case 1: -// return S.current.labelEven; -// default: -// return ''; -// } -// } -// } -// -// extension LocalTimeConversion on LocalTime { -// TimeOfDay toTimeOfDay() => TimeOfDay(hour: hourOfDay, minute: minuteOfHour); -// } -// -// extension TimeOfDayConversion on TimeOfDay { -// LocalTime toLocalTime() => LocalTime(hour, minute, 0); -// } -// -// extension LocalDateComparisons on LocalDate { -// bool isDuring(AllDayUniEvent semester) { -// return DateInterval(semester.startDate, semester.endDate).contains(this); -// } -// -// bool isBeforeOrDuring(AllDayUniEvent semester) { -// if (compareTo(semester.startDate) < 0) return true; -// return isDuring(semester); -// } -// } +import 'package:dart_date/dart_date.dart' show Interval; +import 'package:dotted_line/dotted_line.dart'; +import 'package:flutter/material.dart' hide Interval; +import 'package:flutter/rendering.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:provider/provider.dart'; +import 'package:rrule/rrule.dart'; +import 'package:time_machine/time_machine.dart' as time_machine show DayOfWeek; +import 'package:time_machine/time_machine.dart' hide Interval; +import 'package:time_machine/time_machine_text_patterns.dart'; + +import '../../../../authentication/model/user.dart'; +import '../../../../authentication/service/auth_provider.dart'; +import '../../../../generated/l10n.dart'; +import '../../../../navigation/routes.dart'; +import '../../../../resources/locale_provider.dart'; +import '../../../../resources/theme.dart'; +import '../../../../widgets/button.dart'; +import '../../../../widgets/chip_form_field.dart'; +import '../../../../widgets/dialog.dart'; +import '../../../../widgets/scaffold.dart'; +import '../../../../widgets/toast.dart'; +import '../../../classes/model/class.dart'; +import '../../../classes/service/class_provider.dart'; +import '../../../filter/view/relevance_picker.dart'; +import '../../../people/model/person.dart'; +import '../../../people/service/person_provider.dart'; +import '../../../people/view/people_page.dart'; +import '../../model/academic_calendar.dart'; +import '../../model/events/all_day_event.dart'; +import '../../model/events/class_event.dart'; +import '../../model/events/recurring_event.dart'; +import '../../model/events/uni_event.dart'; +import '../../service/uni_event_provider.dart'; +import '../../timetable_utils.dart'; + +class AddEventView extends StatefulWidget { + /// If the `id` of [initialEvent] is not null, this acts like an "Edit event" + /// page starting from the info in [initialEvent]. Otherwise, it acts like an + /// "Add event" page with optional default values based on [initialEvent]. + const AddEventView({Key key, this.initialEvent}) : super(key: key); + + final UniEvent initialEvent; + + @override + _AddEventViewState createState() => _AddEventViewState(); +} + +class _AddEventViewState extends State { + final formKey = GlobalKey(); + + TextEditingController locationController; + RelevanceController relevanceController = RelevanceController(); + + UniEventType selectedEventType; + ClassHeader selectedClass; + Person selectedTeacher; + String selectedCalendar; + DateTime startDateTime; + Duration duration; + Map weekSelected = { + WeekType.odd: null, + WeekType.even: null, + }; + Map<_DayOfWeek, bool> weekDaySelected = { + _DayOfWeek.monday: false, + _DayOfWeek.tuesday: false, + _DayOfWeek.wednesday: false, + _DayOfWeek.thursday: false, + _DayOfWeek.friday: false, + _DayOfWeek.saturday: false, + _DayOfWeek.sunday: false, + }; + + int selectedSemester = 1; + + AllDayUniEvent get semester => + calendars[selectedCalendar]?.semesters?.elementAt(selectedSemester - 1); + + List classHeaders = []; + List classTeachers = []; + User user; + Map calendars = {}; + + @override + void initState() { + super.initState(); + + user = + Provider.of(context, listen: false).currentUserFromCache; + Provider.of(context, listen: false) + .fetchClassHeaders(uid: user.uid) + .then((headers) => setState(() => classHeaders = headers)); + Provider.of(context, listen: false) + .fetchPeople() + .then((teachers) => setState(() => classTeachers = teachers)); + Provider.of(context, listen: false) + .fetchCalendars() + .then((calendars) { + setState(() { + this.calendars = calendars; + selectedCalendar = calendars.keys.first; + }); + + if (widget.initialEvent?.id != null) { + selectedCalendar = widget.initialEvent.calendar.id; + final AllDayUniEvent secondSemester = + widget.initialEvent.calendar.semesters.last; + selectedSemester = + Interval(secondSemester.startDate, secondSemester.endDate) + .includes(widget.initialEvent.start) + ? 2 + : 1; + } else { + bool foundSemester = false; + for (final calendar in calendars.entries) { + for (final semester in calendar.value.semesters) { + final DateTime date = widget.initialEvent.start ?? DateTime.now(); + if (date.isBeforeOrDuring(semester)) { + // semester.id is represented as "semesterN", where "semester0" is the first semester + selectedSemester = + 1 + int.tryParse(semester.id[semester.id.length - 1]); + selectedCalendar = calendar.key; + foundSemester = true; + break; + } + } + if (foundSemester) break; + } + if (!foundSemester) { + selectedCalendar = calendars.entries.last.value.id; + selectedSemester = 2; + } + } + + if (widget.initialEvent != null && + widget.initialEvent is RecurringUniEvent) { + final RecurringUniEvent event = widget.initialEvent; + if (event.rrule.interval != 1) { + final rule = WeekYearRules.iso; + if (rule.getWeekOfWeekYear(LocalDate.dateTime(semester.start)) == + rule.getWeekOfWeekYear(LocalDate.dateTime(event.start))) { + // Week is odd + weekSelected[WeekType.even] = false; + weekSelected[WeekType.odd] = true; + } else { + // Week is even + weekSelected[WeekType.even] = true; + weekSelected[WeekType.odd] = false; + } + } + } + + setState(() { + weekSelected[WeekType.even] ??= true; + weekSelected[WeekType.odd] ??= true; + }); + }); + + selectedEventType = widget.initialEvent?.type; + selectedClass = widget.initialEvent?.classHeader; + selectedTeacher = widget.initialEvent is ClassEvent + ? (widget.initialEvent as ClassEvent).teacher + : null; + locationController = + TextEditingController(text: widget.initialEvent?.location ?? ''); + + final startHour = widget.initialEvent?.start?.hour ?? 8; + duration = widget.initialEvent?.period?.toTime()?.toDuration ?? + const Duration(hours: 2); + startDateTime = widget.initialEvent?.start + ?.copyWith(hour: startHour, minute: 0, second: 0, millisecond: 0) + ?.copyWithUtc() ?? + 0; + + List<_DayOfWeek> initialWeekDays = [ + _DayOfWeek.from( + LocalDate.dateTime(widget.initialEvent?.start)?.dayOfWeek) ?? + _DayOfWeek.monday + ]; + if (widget.initialEvent != null && + widget.initialEvent is RecurringUniEvent && + (widget.initialEvent as RecurringUniEvent) + .rrule + .byWeekDays + .isNotEmpty) { + initialWeekDays = (widget.initialEvent as RecurringUniEvent) + .rrule + .byWeekDays + .map((entry) => _DayOfWeek.from(time_machine.DayOfWeek(entry.day))) + .toList(); + } + for (final initialWeekDay in initialWeekDays) { + weekDaySelected[initialWeekDay] = true; + } + } + + @override + Widget build(BuildContext context) { + return AppScaffold( + title: Text(widget.initialEvent?.id == null + ? S.current.actionAddEvent + : S.current.actionEditEvent), + actions: widget.initialEvent?.id == null + ? [_saveButton()] + : [ + _saveButton(), + _deleteButton(), + ], + body: ListView( + children: [ + Padding( + padding: const EdgeInsets.only(left: 16, right: 16), + child: Form( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: DropdownButtonFormField( + decoration: InputDecoration( + labelText: S.current.labelUniversityYear, + prefixIcon: + const Icon(Icons.calendar_today_outlined), + ), + value: selectedCalendar, + items: calendars.keys.map((key) { + final year = int.tryParse(key); + return DropdownMenuItem( + value: key, + child: Text( + year != null ? '$year-${year + 1}' : key), + ); + }).toList(), + onChanged: (selection) => + selectedCalendar = selection, + ), + ), + const SizedBox(width: 16), + Expanded( + child: DropdownButtonFormField( + decoration: InputDecoration( + labelText: S.current.labelSemester, + prefixIcon: const Icon(FeatherIcons.columns), + ), + value: selectedSemester, + items: [1, 2] + .map((semester) => DropdownMenuItem( + value: semester, + child: Text(semester.toString()), + )) + .toList(), + onChanged: (selection) => + selectedSemester = selection, + ), + ), + ], + ), + RelevanceFormField( + canBePrivate: false, + canBeForEveryone: false, + controller: relevanceController, + ), + DropdownButtonFormField( + decoration: InputDecoration( + labelText: S.current.labelType, + prefixIcon: const Icon(Icons.category_outlined), + ), + value: selectedEventType, + items: UniEventTypeExtension.classTypes + .map( + (type) => DropdownMenuItem( + value: type, + child: Text(type.toLocalizedString()), + ), + ) + .toList(), + onChanged: (selection) { + formKey.currentState.validate(); + setState(() => selectedEventType = selection); + }, + validator: (selection) { + if (selection == null) { + return S.current.errorEventTypeCannotBeEmpty; + } + return null; + }, + ), + if (selectedEventType != null) + Column( + children: [ + if (classHeaders.isNotEmpty) + DropdownButtonFormField( + isExpanded: true, + decoration: InputDecoration( + labelText: S.current.labelClass, + prefixIcon: const Icon(FeatherIcons.bookOpen), + ), + value: selectedClass, + items: classHeaders + .map( + (header) => DropdownMenuItem( + value: header, child: Text(header.name)), + ) + .toList(), + onChanged: (selection) { + formKey.currentState.validate(); + setState(() => selectedClass = selection); + }, + validator: (selection) { + if (selection == null) { + return S.current.errorClassCannotBeEmpty; + } + return null; + }, + ), + if ([UniEventType.lecture].contains(selectedEventType)) + AutocompletePerson( + key: const Key('AutocompleteLecturer'), + labelText: S.current.labelLecturer, + formKey: formKey, + onSaved: (value) => selectedTeacher = value, + classTeachers: classTeachers, + personDisplayed: selectedTeacher, + ), + TextFormField( + controller: locationController, + decoration: InputDecoration( + labelText: S.current.labelLocation, + prefixIcon: const Icon(FeatherIcons.mapPin), + ), + onChanged: (_) => setState(() {}), + ), + timeIntervalPicker(), + Divider( + thickness: 0.7, + color: Theme.of(context).hintColor, + ), + if (weekSelected[WeekType.odd] != null && + weekSelected[WeekType.even] != null) + FilterChipFormField( + key: const ValueKey('week_picker'), + icon: FeatherIcons.calendar, + label: S.current.labelWeek, + initialValues: weekSelected, + ), + FilterChipFormField( + key: const ValueKey('day_picker'), + icon: Icons.today_outlined, + label: S.current.labelDay, + initialValues: weekDaySelected, + ), + const SizedBox(height: 16), + ], + ), + const SizedBox(width: 16), + ], + ), + ), + ), + ], + ), + ); + } + + AppDialog _deletionConfirmationDialog(BuildContext context) => AppDialog( + icon: const Icon(Icons.delete_outlined), + title: S.current.actionDeleteEvent, + info: S.current.messageThisCouldAffectOtherStudents, + message: S.current.messageDeleteEvent, + actions: [ + AppButton( + text: S.current.actionDeleteEvent, + width: 130, + onTap: () async { + final res = + await Provider.of(context, listen: false) + .deleteEvent(widget.initialEvent); + if (res) { + if (!mounted) return; + Navigator.of(context) + .popUntil(ModalRoute.withName(Routes.home)); + AppToast.show(S.current.messageEventDeleted); + } + }, + ) + ], + ); + + AppScaffoldAction _saveButton() => AppScaffoldAction( + text: S.current.buttonSave, + onPressed: () async { + if (!formKey.currentState.validate()) return; + + DateTime start = + semester.startDate.at(dateTime: startDateTime).copyWithUtc(); + if (weekSelected[WeekType.even] && !weekSelected[WeekType.odd]) { + // Event is every even week, add a week to start date + start = start.addDays(7); + } + + final rrule = RecurrenceRule( + frequency: Frequency.weekly, + byWeekDays: (Map<_DayOfWeek, bool>.from(weekDaySelected) + ..removeWhere((key, value) => !value)) + .keys + .map((weekDay) => ByWeekDayEntry(weekDay.value)) + .toSet(), + interval: + weekSelected[WeekType.odd] != weekSelected[WeekType.even] + ? 2 + : 1, + until: semester.endDate + .add(const Duration(days: 1)) + .atMidnight() + .copyWithUtc()); + + final event = ClassEvent( + teacher: selectedTeacher, + rrule: rrule, + start: start, + period: duration.toPeriod(), + id: widget.initialEvent?.id, + relevance: relevanceController.customRelevance, + degree: relevanceController.degree, + location: locationController.text, + type: selectedEventType, + classHeader: selectedClass, + calendar: calendars[selectedCalendar], + addedBy: Provider.of(context, listen: false) + .currentUserFromCache + .uid); + + if (widget.initialEvent?.id == null) { + final res = + await Provider.of(context, listen: false) + .addEvent(event); + if (res) { + if (!mounted) return; + Navigator.of(context).pop(); + AppToast.show(S.current.messageEventAdded); + } + } else { + final res = + await Provider.of(context, listen: false) + .updateEvent(event); + if (res) { + if (!mounted) return; + Navigator.of(context).popUntil(ModalRoute.withName(Routes.home)); + AppToast.show(S.current.messageEventEdited); + } + } + }, + ); + + AppScaffoldAction _deleteButton() => AppScaffoldAction( + icon: Icons.more_vert_outlined, + items: { + S.current.actionDeleteEvent: () => showDialog( + context: context, + builder: _deletionConfirmationDialog, + ) + }, + onPressed: () => showDialog( + context: context, + builder: _deletionConfirmationDialog, + ), + ); + + Widget timeIntervalPicker() { + final endDateTime = startDateTime.add(duration); + final textColor = Theme.of(context).textTheme.headline4.color; + TimeOfDay startTimeOfDay = startDateTime.toTimeOfDay(); + TimeOfDay endTimeOfDay = endDateTime.toTimeOfDay(); + return Padding( + padding: const EdgeInsets.only(top: 10), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Icon( + FeatherIcons.clock, + color: Theme.of(context).formIconColor, + ), + ), + TextButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () async { + startTimeOfDay = await showTimePicker( + context: context, + initialTime: TimeOfDay( + hour: startDateTime.hour, minute: startDateTime.minute), + ); + setState(() => startDateTime = startTimeOfDay != null + ? startDateTime.at(timeOfDay: startTimeOfDay) + : startDateTime); + }, + child: Text( + startDateTime.toStringWithFormat('HH:mm'), + style: Theme.of(context).textTheme.headline4, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( + children: [ + Text( + '${duration.inHours}H', + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith(color: textColor), + ), + DottedLine( + lineThickness: 4, + dashRadius: 2, + dashColor: textColor, + ), + // Text-sized box so that the line is centered + SizedBox( + height: Theme.of(context).textTheme.bodyText1.fontSize), + ], + ), + ), + ), + TextButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () async { + endTimeOfDay = await showTimePicker( + context: context, + initialTime: startDateTime.add(duration).toTimeOfDay(), + ); + setState( + () { + if (endTimeOfDay.subtract(startTimeOfDay).isNegative) { + endTimeOfDay = startTimeOfDay; + } + duration = endTimeOfDay.subtract(startTimeOfDay); + }, + ); + }, + child: Text( + endDateTime.toStringWithFormat('HH:mm'), + style: Theme.of(context).textTheme.headline4, + ), + ), + const SizedBox(width: 12), + ], + ), + ); + } +} + +class _DayOfWeek extends time_machine.DayOfWeek with Localizable { + const _DayOfWeek(int value) : super(value); + + _DayOfWeek.from(time_machine.DayOfWeek dayOfWeek) : super(dayOfWeek.value); + + @override + String toLocalizedString() { + final helperDate = LocalDate.today().next(this); + return LocalDatePattern.createWithCurrentCulture('ddd') + .format(helperDate) + .substring(0, 3); + } + + static const _DayOfWeek monday = _DayOfWeek(1); + static const _DayOfWeek tuesday = _DayOfWeek(2); + static const _DayOfWeek wednesday = _DayOfWeek(3); + static const _DayOfWeek thursday = _DayOfWeek(4); + static const _DayOfWeek friday = _DayOfWeek(5); + static const _DayOfWeek saturday = _DayOfWeek(6); + static const _DayOfWeek sunday = _DayOfWeek(7); +} + +class WeekType with Localizable { + const WeekType(this._value); + + final int _value; + + int get value => _value; + + static const WeekType odd = WeekType(0); + static const WeekType even = WeekType(1); + + @override + int get hashCode => _value.hashCode; + + @override + bool operator ==(dynamic other) => + other is WeekType && other._value == _value || + other is int && other == _value; + + @override + String toLocalizedString() { + switch (_value) { + case 0: + return S.current.labelOdd; + case 1: + return S.current.labelEven; + default: + return ''; + } + } +} + +extension LocalTimeConversion on LocalTime { + TimeOfDay toTimeOfDay() => TimeOfDay(hour: hourOfDay, minute: minuteOfHour); +} + +extension TimeOfDayExtension on TimeOfDay { + Duration subtract(TimeOfDay startTimeOfDay) { + return Duration( + hours: hour - startTimeOfDay.hour, + minutes: minute - startTimeOfDay.minute); + } +} + +extension DateTimeComparisons on DateTime { + bool isDuring(AllDayUniEvent semester) { + return Interval(semester.startDate, semester.endDate).includes(this); + } + + bool isBeforeOrDuring(AllDayUniEvent semester) { + if (compareTo(semester.startDate) < 0) return true; + return isDuring(semester); + } +} diff --git a/lib/pages/timetable/view/events/all_day_event_widget.dart b/lib/pages/timetable/view/events/all_day_event_widget.dart index d4fd99ecc..0ee27cf22 100644 --- a/lib/pages/timetable/view/events/all_day_event_widget.dart +++ b/lib/pages/timetable/view/events/all_day_event_widget.dart @@ -1,248 +1,249 @@ -//import 'dart:math' as math; -// -//import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -//import 'package:acs_upb_mobile/pages/timetable/view/events/event_view.dart'; -//import 'package:black_hole_flutter/black_hole_flutter.dart'; -//import 'package:dartx/dartx.dart'; -//import 'package:flutter/foundation.dart'; -//import 'package:flutter/material.dart'; -//import 'package:timetable/timetable.dart'; -// -///// Widget to display all day events in the timetable, based on -///// [BasicAllDayEventWidget] from the timetable API. -//class UniAllDayEventWidget extends StatelessWidget { -// const UniAllDayEventWidget( -// this.event, { -// @required this.info, -// Key key, -// this.borderRadius = 4, -// }) : assert(event != null), -// assert(info != null), -// assert(borderRadius != null), -// super(key: key); -// -// /// The event to be displayed. -// final UniEventInstance event; -// final AllDayEventLayoutInfo info; -// final double borderRadius; -// -// @override -// Widget build(BuildContext context) { -// final color = event.color ?? -// event?.mainEvent?.color ?? -// Theme.of(context).primaryColor; -// -// return Padding( -// padding: const EdgeInsets.all(2), -// child: CustomPaint( -// painter: AllDayEventBackgroundPainter( -// info: info, -// color: color, -// borderRadius: borderRadius, -// ), -// child: Material( -// shape: AllDayEventBorder( -// info: info, -// side: BorderSide.none, -// borderRadius: borderRadius, -// ), -// clipBehavior: Clip.antiAlias, -// color: Colors.transparent, -// child: InkWell( -// onTap: () => -// Navigator.of(context).push(MaterialPageRoute( -// builder: (_) => EventView(eventInstance: event), -// )), -// child: _buildContent(context), -// ), -// ), -// ), -// ); -// } -// -// Widget _buildContent(BuildContext context) { -// final color = event.color ?? Theme.of(context).primaryColor; -// -// return Padding( -// padding: const EdgeInsets.fromLTRB(4, 2, 0, 2), -// child: Align( -// alignment: AlignmentDirectional.centerStart, -// child: DefaultTextStyle( -// style: context.textTheme.bodyText2.copyWith( -// fontSize: 14, -// color: color.highEmphasisOnColor, -// ), -// child: Text( -// event.title ?? event.mainEvent?.classHeader?.acronym, -// maxLines: 1, -// ), -// ), -// ), -// ); -// } -//} -// -//class AllDayEventBackgroundPainter extends CustomPainter { -// const AllDayEventBackgroundPainter({ -// @required this.info, -// @required this.color, -// this.borderRadius = 0, -// }) : assert(info != null), -// assert(color != null), -// assert(borderRadius != null); -// -// final AllDayEventLayoutInfo info; -// final Color color; -// final double borderRadius; -// -// @override -// void paint(Canvas canvas, Size size) { -// canvas.drawPath( -// _getPath(size, info, borderRadius), -// Paint()..color = color, -// ); -// } -// -// @override -// bool shouldRepaint(covariant AllDayEventBackgroundPainter oldDelegate) { -// return info != oldDelegate.info || -// color != oldDelegate.color || -// borderRadius != oldDelegate.borderRadius; -// } -//} -// -///// A modified [RoundedRectangleBorder] that morphs to triangular left and/or -///// right borders if not all of the event is currently visible. -//class AllDayEventBorder extends ShapeBorder { -// const AllDayEventBorder({ -// @required this.info, -// this.side = BorderSide.none, -// this.borderRadius = 0, -// }) : assert(info != null), -// assert(side != null), -// assert(borderRadius != null); -// -// final AllDayEventLayoutInfo info; -// final BorderSide side; -// final double borderRadius; -// -// @override -// EdgeInsetsGeometry get dimensions => EdgeInsets.all(side.width); -// -// @override -// ShapeBorder scale(double t) { -// return AllDayEventBorder( -// info: info, -// side: side.scale(t), -// borderRadius: borderRadius * t, -// ); -// } -// -// @override -// Path getInnerPath(Rect rect, {TextDirection textDirection}) { -// return null; -// } -// -// @override -// Path getOuterPath(Rect rect, {TextDirection textDirection}) { -// return _getPath(rect.size, info, borderRadius); -// } -// -// @override -// void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) { -// // For some reason, when we paint the background in this shape directly, it -// // lags while scrolling. Hence, we only use it to provide the outer path -// // used for clipping. -// } -// -// @override -// bool operator ==(Object other) { -// if (other.runtimeType != runtimeType) { -// return false; -// } -// return other is AllDayEventBorder && -// other.info == info && -// other.side == side && -// other.borderRadius == borderRadius; -// } -// -// @override -// int get hashCode => hashValues(info, side, borderRadius); -// -// @override -// String toString() => -// '${objectRuntimeType(this, 'RoundedRectangleBorder')}($side, $borderRadius)'; -//} -// -//Path _getPath(Size size, AllDayEventLayoutInfo info, double radius) { -// final height = size.height; -// // final radius = borderRadius.coerceAtMost(width / 2); -// -// final maxTipWidth = height / 4; -// final leftTipWidth = info.hiddenStartDays.coerceAtMost(1) * maxTipWidth; -// final rightTipWidth = info.hiddenEndDays.coerceAtMost(1) * maxTipWidth; -// -// final width = size.width; -// // final leftTipBase = math.min(leftTipWidth + radius, width - radius); -// // final rightTipBase = math.max(width - rightTipWidth - radius, radius); -// final leftTipBase = info.hiddenStartDays > 0 -// ? math.min(leftTipWidth + radius, width - radius) -// : leftTipWidth + radius; -// final rightTipBase = info.hiddenEndDays > 0 -// ? math.max(width - rightTipWidth - radius, radius) -// : width - rightTipWidth - radius; -// -// final tipSize = Size.square(radius * 2); -// -// // no tip: 0 ≈ 0° -// // full tip: PI / 4 ≈ 45° -// final leftTipAngle = math.pi / 2 - math.atan2(height / 2, leftTipWidth); -// final rightTipAngle = math.pi / 2 - math.atan2(height / 2, rightTipWidth); -// -// return Path() -// ..moveTo(leftTipBase, 0) -// // Right top -// ..arcTo( -// Offset(rightTipBase - radius, 0) & tipSize, -// math.pi * 3 / 2, -// math.pi / 2 - rightTipAngle, -// false, -// ) -// // Right tip -// ..arcTo( -// Offset(rightTipBase + rightTipWidth - radius, height / 2 - radius) & -// tipSize, -// -rightTipAngle, -// 2 * rightTipAngle, -// false, -// ) -// // Right bottom -// ..arcTo( -// Offset(rightTipBase - radius, height - radius * 2) & tipSize, -// rightTipAngle, -// math.pi / 2 - rightTipAngle, -// false, -// ) -// // Left bottom -// ..arcTo( -// Offset(leftTipBase - radius, height - radius * 2) & tipSize, -// math.pi / 2, -// math.pi / 2 - leftTipAngle, -// false, -// ) -// // Left tip -// ..arcTo( -// Offset(leftTipBase - leftTipWidth - radius, height / 2 - radius) & -// tipSize, -// math.pi - leftTipAngle, -// 2 * leftTipAngle, -// false, -// ) -// // Left top -// ..arcTo( -// Offset(leftTipBase - radius, 0) & tipSize, -// math.pi + leftTipAngle, -// math.pi / 2 - leftTipAngle, -// false, -// ); -//} +import 'dart:math' as math; + +import 'package:black_hole_flutter/black_hole_flutter.dart'; +import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:timetable/timetable.dart'; + +import '../../model/events/uni_event.dart'; +import 'event_view.dart'; + +/// Widget to display all day events in the timetable, based on +/// [BasicAllDayEventWidget] from the timetable API. +class UniAllDayEventWidget extends StatelessWidget { + const UniAllDayEventWidget( + this.event, { + @required this.info, + Key key, + this.borderRadius = 4, + }) : assert(event != null, 'event is null'), + assert(info != null, 'info is null'), + assert(borderRadius != null, 'border radius is null'), + super(key: key); + + /// The event to be displayed. + final UniEventInstance event; + final AllDayEventLayoutInfo info; + final double borderRadius; + + @override + Widget build(BuildContext context) { + final color = event.color ?? + event?.mainEvent?.color ?? + Theme.of(context).primaryColor; + + return Padding( + padding: const EdgeInsets.all(2), + child: CustomPaint( + painter: AllDayEventBackgroundPainter( + info: info, + color: color, + borderRadius: borderRadius, + ), + child: Material( + shape: AllDayEventBorder( + info: info, + side: BorderSide.none, + borderRadius: borderRadius, + ), + clipBehavior: Clip.antiAlias, + color: Colors.transparent, + child: InkWell( + onTap: () => + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => EventView(eventInstance: event), + )), + child: _buildContent(context), + ), + ), + ), + ); + } + + Widget _buildContent(BuildContext context) { + final color = event.color ?? Theme.of(context).primaryColor; + + return Padding( + padding: const EdgeInsets.fromLTRB(4, 2, 0, 2), + child: Align( + alignment: AlignmentDirectional.centerStart, + child: DefaultTextStyle( + style: context.textTheme.bodyText2.copyWith( + fontSize: 14, + color: color.highEmphasisOnColor, + ), + child: Text( + event.title ?? event.mainEvent?.classHeader?.acronym, + maxLines: 1, + ), + ), + ), + ); + } +} + +class AllDayEventBackgroundPainter extends CustomPainter { + const AllDayEventBackgroundPainter({ + @required this.info, + @required this.color, + this.borderRadius = 0, + }) : assert(info != null, 'info is null'), + assert(color != null, 'color is null'), + assert(borderRadius != null, 'border radius is null'); + + final AllDayEventLayoutInfo info; + final Color color; + final double borderRadius; + + @override + void paint(Canvas canvas, Size size) { + canvas.drawPath( + _getPath(size, info, borderRadius), + Paint()..color = color, + ); + } + + @override + bool shouldRepaint(covariant AllDayEventBackgroundPainter oldDelegate) { + return info != oldDelegate.info || + color != oldDelegate.color || + borderRadius != oldDelegate.borderRadius; + } +} + +/// A modified [RoundedRectangleBorder] that morphs to triangular left and/or +/// right borders if not all of the event is currently visible. +class AllDayEventBorder extends ShapeBorder { + const AllDayEventBorder({ + @required this.info, + this.side = BorderSide.none, + this.borderRadius = 0, + }) : assert(info != null, 'info is null'), + assert(side != null, 'side is null'), + assert(borderRadius != null, 'borderRadius is null'); + + final AllDayEventLayoutInfo info; + final BorderSide side; + final double borderRadius; + + @override + EdgeInsetsGeometry get dimensions => EdgeInsets.all(side.width); + + @override + ShapeBorder scale(double t) { + return AllDayEventBorder( + info: info, + side: side.scale(t), + borderRadius: borderRadius * t, + ); + } + + @override + Path getInnerPath(Rect rect, {TextDirection textDirection}) { + return null; + } + + @override + Path getOuterPath(Rect rect, {TextDirection textDirection}) { + return _getPath(rect.size, info, borderRadius); + } + + @override + void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) { + // For some reason, when we paint the background in this shape directly, it + // lags while scrolling. Hence, we only use it to provide the outer path + // used for clipping. + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is AllDayEventBorder && + other.info == info && + other.side == side && + other.borderRadius == borderRadius; + } + + @override + int get hashCode => hashValues(info, side, borderRadius); + + @override + String toString() => + '${objectRuntimeType(this, 'RoundedRectangleBorder')}($side, $borderRadius)'; +} + +Path _getPath(Size size, AllDayEventLayoutInfo info, double radius) { + final height = size.height; + // final radius = borderRadius.coerceAtMost(width / 2); + + final maxTipWidth = height / 4; + final leftTipWidth = info.hiddenStartDays.coerceAtMost(1) * maxTipWidth; + final rightTipWidth = info.hiddenEndDays.coerceAtMost(1) * maxTipWidth; + + final width = size.width; + // final leftTipBase = math.min(leftTipWidth + radius, width - radius); + // final rightTipBase = math.max(width - rightTipWidth - radius, radius); + final leftTipBase = info.hiddenStartDays > 0 + ? math.min(leftTipWidth + radius, width - radius) + : leftTipWidth + radius; + final rightTipBase = info.hiddenEndDays > 0 + ? math.max(width - rightTipWidth - radius, radius) + : width - rightTipWidth - radius; + + final tipSize = Size.square(radius * 2); + + // no tip: 0 ≈ 0° + // full tip: PI / 4 ≈ 45° + final leftTipAngle = math.pi / 2 - math.atan2(height / 2, leftTipWidth); + final rightTipAngle = math.pi / 2 - math.atan2(height / 2, rightTipWidth); + + return Path() + ..moveTo(leftTipBase, 0) + // Right top + ..arcTo( + Offset(rightTipBase - radius, 0) & tipSize, + math.pi * 3 / 2, + math.pi / 2 - rightTipAngle, + false, + ) + // Right tip + ..arcTo( + Offset(rightTipBase + rightTipWidth - radius, height / 2 - radius) & + tipSize, + -rightTipAngle, + 2 * rightTipAngle, + false, + ) + // Right bottom + ..arcTo( + Offset(rightTipBase - radius, height - radius * 2) & tipSize, + rightTipAngle, + math.pi / 2 - rightTipAngle, + false, + ) + // Left bottom + ..arcTo( + Offset(leftTipBase - radius, height - radius * 2) & tipSize, + math.pi / 2, + math.pi / 2 - leftTipAngle, + false, + ) + // Left tip + ..arcTo( + Offset(leftTipBase - leftTipWidth - radius, height / 2 - radius) & + tipSize, + math.pi - leftTipAngle, + 2 * leftTipAngle, + false, + ) + // Left top + ..arcTo( + Offset(leftTipBase - radius, 0) & tipSize, + math.pi + leftTipAngle, + math.pi / 2 - leftTipAngle, + false, + ); +} diff --git a/lib/pages/timetable/view/events/event_view.dart b/lib/pages/timetable/view/events/event_view.dart index 96b727f37..87914edb1 100644 --- a/lib/pages/timetable/view/events/event_view.dart +++ b/lib/pages/timetable/view/events/event_view.dart @@ -1,191 +1,194 @@ -// import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; -// import 'package:acs_upb_mobile/generated/l10n.dart'; -// import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; -// import 'package:acs_upb_mobile/pages/classes/view/class_view.dart'; -// import 'package:acs_upb_mobile/pages/classes/view/classes_page.dart'; -// import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; -// import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; -// import 'package:acs_upb_mobile/pages/people/view/person_view.dart'; -// import 'package:acs_upb_mobile/pages/timetable/model/events/class_event.dart'; -// import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -// import 'package:acs_upb_mobile/pages/timetable/view/events/add_event_view.dart'; -// import 'package:acs_upb_mobile/widgets/scaffold.dart'; -// import 'package:acs_upb_mobile/widgets/toast.dart'; -// import 'package:flutter/material.dart'; -// import 'package:flutter/rendering.dart'; -// import 'package:flutter_feather_icons/flutter_feather_icons.dart'; -// import 'package:provider/provider.dart'; -// -// class EventView extends StatefulWidget { -// const EventView({Key key, this.eventInstance, this.uniEvent}) -// : assert( -// (eventInstance != null && uniEvent == null) || -// (eventInstance == null && uniEvent != null), -// 'Only one of the parameters must be provided'), -// super(key: key); -// final UniEventInstance eventInstance; -// final UniEvent uniEvent; -// -// @override -// _EventViewState createState() => _EventViewState(); -// } -// -// class _EventViewState extends State { -// Padding _colorIcon() => Padding( -// padding: const EdgeInsets.all(10), -// child: Container( -// width: 20, -// height: 20, -// decoration: BoxDecoration( -// borderRadius: const BorderRadius.all(Radius.circular(4)), -// color: widget.uniEvent?.color ?? widget.eventInstance.color), -// ), -// ); -// -// @override -// Widget build(BuildContext context) { -// final user = Provider.of(context).currentUserFromCache; -// final UniEvent mainEvent = -// widget.eventInstance?.mainEvent ?? widget.uniEvent; -// return AppScaffold( -// title: Text(S.current.navigationEventDetails), -// actions: [ -// AppScaffoldAction( -// icon: Icons.edit_outlined, -// disabled: !mainEvent.editable || !user.canAddPublicInfo, -// onPressed: () { -// if (!mainEvent.editable) { -// AppToast.show(S.current.warningEventNotEditable); -// } else if (!user.canAddPublicInfo) { -// AppToast.show(S.current.errorPermissionDenied); -// } else { -// Navigator.of(context).push(MaterialPageRoute( -// builder: (_) => ChangeNotifierProvider( -// create: (_) => FilterProvider( -// defaultDegree: mainEvent.degree, -// defaultRelevance: mainEvent.relevance, -// ), -// child: AddEventView( -// initialEvent: mainEvent, -// ), -// ), -// )); -// } -// }, -// ) -// ], -// body: SafeArea( -// child: ListView(children: [ -// Padding( -// padding: const EdgeInsets.all(16), -// child: Row( -// children: [ -// _colorIcon(), -// const SizedBox(width: 16), -// Expanded( -// child: Column( -// mainAxisSize: MainAxisSize.min, -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Text( -// widget.eventInstance?.title ?? -// mainEvent.type.toLocalizedString(), -// style: Theme.of(context).textTheme.headline6), -// const SizedBox(height: 4), -// if (widget.eventInstance != null) -// Text(widget.eventInstance.dateString), -// if (mainEvent.info != widget.eventInstance?.dateString) -// Padding( -// padding: const EdgeInsets.only(top: 2), -// child: Text( -// mainEvent.info, -// style: Theme.of(context) -// .textTheme -// .bodyText2 -// .copyWith(color: Theme.of(context).hintColor), -// ), -// ), -// ], -// ), -// ), -// ], -// ), -// ), -// if (mainEvent?.classHeader != null) -// ClassListItem( -// classHeader: mainEvent.classHeader, -// hint: S.current.messageTapForMoreInfo, -// onTap: () => Navigator.of(context).push( -// MaterialPageRoute( -// builder: (context) => ChangeNotifierProvider.value( -// value: Provider.of(context), -// child: ClassView( -// classHeader: mainEvent.classHeader, -// ), -// ), -// ), -// ), -// ), -// if (widget.eventInstance?.location?.isNotEmpty ?? false) -// Padding( -// padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), -// child: Row( -// children: [ -// const Padding( -// padding: EdgeInsets.all(10), -// child: Icon(FeatherIcons.mapPin), -// ), -// const SizedBox(width: 16), -// Text(widget.eventInstance?.location, -// style: Theme.of(context).textTheme.subtitle1), -// ], -// ), -// ), -// Padding( -// padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), -// child: Row( -// children: [ -// const Padding( -// padding: EdgeInsets.all(10), -// child: Icon(FeatherIcons.users), -// ), -// const SizedBox(width: 16), -// Text( -// mainEvent.relevance == null -// ? S.current.relevanceAnyone -// : '${FilterNode.localizeName(mainEvent.degree, context)}: ${mainEvent.relevance.join(', ')}', -// style: Theme.of(context).textTheme.subtitle1), -// ], -// ), -// ), -// if (mainEvent is ClassEvent) -// Padding( -// padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), -// child: GestureDetector( -// onTap: () { -// if (mainEvent.teacher != null) { -// showModalBottomSheet( -// isScrollControlled: true, -// context: context, -// builder: (BuildContext buildContext) => -// PersonView(person: mainEvent.teacher)); -// } -// }, -// child: Row( -// children: [ -// const Padding( -// padding: EdgeInsets.all(10), -// child: Icon(FeatherIcons.user), -// ), -// const SizedBox(width: 16), -// Text(mainEvent.teacher.name ?? S.current.labelUnknown, -// style: Theme.of(context).textTheme.subtitle1), -// ], -// ), -// ), -// ), -// ]), -// ), -// ); -// } -// } +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:provider/provider.dart'; + +import '../../../../authentication/service/auth_provider.dart'; +import '../../../../generated/l10n.dart'; +import '../../../../widgets/scaffold.dart'; +import '../../../../widgets/toast.dart'; +import '../../../classes/service/class_provider.dart'; +import '../../../classes/view/class_view.dart'; +import '../../../classes/view/classes_page.dart'; +import '../../../filter/model/filter.dart'; +import '../../../filter/service/filter_provider.dart'; +import '../../../people/view/person_view.dart'; +import '../../model/events/class_event.dart'; +import '../../model/events/uni_event.dart'; +import 'add_event_view.dart'; + +class EventView extends StatefulWidget { + const EventView({Key key, this.eventInstance, this.uniEvent}) + : assert( + (eventInstance != null && uniEvent == null) || + (eventInstance == null && uniEvent != null), + 'Only one of the parameters must be provided'), + super(key: key); + final UniEventInstance eventInstance; + final UniEvent uniEvent; + + @override + _EventViewState createState() => _EventViewState(); +} + +class _EventViewState extends State { + Padding _colorIcon() => Padding( + padding: const EdgeInsets.all(10), + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(4)), + color: widget.uniEvent?.color ?? widget.eventInstance.color), + ), + ); + + @override + Widget build(BuildContext context) { + final user = Provider.of(context).currentUserFromCache; + final UniEvent mainEvent = + widget.eventInstance?.mainEvent ?? widget.uniEvent; + return AppScaffold( + title: Text(S.current.navigationEventDetails), + actions: [ + AppScaffoldAction( + icon: Icons.edit_outlined, + disabled: !mainEvent.editable || !user.canAddPublicInfo, + onPressed: () { + if (!mainEvent.editable) { + AppToast.show(S.current.warningEventNotEditable); + } else if (!user.canAddPublicInfo) { + AppToast.show(S.current.errorPermissionDenied); + } else { + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => ChangeNotifierProvider( + create: (_) => FilterProvider( + defaultDegree: mainEvent.degree, + defaultRelevance: mainEvent.relevance, + ), + child: AddEventView( + initialEvent: mainEvent, + ), + ), + )); + } + }, + ) + ], + body: SafeArea( + child: ListView(children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + _colorIcon(), + const SizedBox(width: 16), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.eventInstance?.title ?? + mainEvent.type.toLocalizedString(), + style: Theme.of(context).textTheme.headline6), + const SizedBox(height: 4), + if (widget.eventInstance != null) + Text(widget.eventInstance.dateString), + if (mainEvent.info != widget.eventInstance?.dateString) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + mainEvent.info, + style: Theme.of(context) + .textTheme + .bodyText2 + .copyWith(color: Theme.of(context).hintColor), + ), + ), + ], + ), + ), + ], + ), + ), + if (mainEvent?.classHeader != null) + ClassListItem( + classHeader: mainEvent.classHeader, + hint: S.current.messageTapForMoreInfo, + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ChangeNotifierProvider.value( + value: Provider.of(context), + child: ClassView( + classHeader: mainEvent.classHeader, + ), + ), + ), + ), + ), + if (widget.eventInstance?.location?.isNotEmpty ?? false) + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Row( + children: [ + const Padding( + padding: EdgeInsets.all(10), + child: Icon(FeatherIcons.mapPin), + ), + const SizedBox(width: 16), + Text(widget.eventInstance?.location, + style: Theme.of(context).textTheme.subtitle1), + ], + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Row( + children: [ + const Padding( + padding: EdgeInsets.all(10), + child: Icon(FeatherIcons.users), + ), + const SizedBox(width: 16), + Expanded( + child: Text( + mainEvent.relevance == null + ? S.current.relevanceAnyone + : '${FilterNode.localizeName(mainEvent.degree, context)}: ${mainEvent.relevance.join(', ')}', + style: Theme.of(context).textTheme.subtitle1), + ), + ], + ), + ), + if (mainEvent is ClassEvent) + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: GestureDetector( + onTap: () { + if (mainEvent.teacher != null) { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (BuildContext buildContext) => + PersonView(person: mainEvent.teacher)); + } + }, + child: Row( + children: [ + const Padding( + padding: EdgeInsets.all(10), + child: Icon(FeatherIcons.user), + ), + const SizedBox(width: 16), + Text(mainEvent.teacher.name ?? S.current.labelUnknown, + style: Theme.of(context).textTheme.subtitle1), + ], + ), + ), + ), + ]), + ), + ); + } +} diff --git a/lib/pages/timetable/view/events/event_widget.dart b/lib/pages/timetable/view/events/event_widget.dart index 95e9a5fc2..9ec26a87d 100644 --- a/lib/pages/timetable/view/events/event_widget.dart +++ b/lib/pages/timetable/view/events/event_widget.dart @@ -1,101 +1,102 @@ -//import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -//import 'package:acs_upb_mobile/pages/timetable/view/events/event_view.dart'; -//import 'package:auto_size_text/auto_size_text.dart'; -//import 'package:black_hole_flutter/black_hole_flutter.dart'; -//import 'package:flutter/foundation.dart'; -//import 'package:flutter/material.dart'; -//import 'package:timetable/timetable.dart'; -// -///// Widget to display all day events in the timetable, based on -///// [BasicEventWidget] from the timetable API. -//class UniEventWidget extends StatelessWidget { -// const UniEventWidget(this.event, {Key key}) -// : assert(event != null), -// super(key: key); -// -// final UniEventInstance event; -// -// @override -// Widget build(BuildContext context) { -// final color = event.color ?? -// event?.mainEvent?.color ?? -// Theme.of(context).primaryColor; -// final footer = -// (event.location?.isNotEmpty ?? false) ? event.location : event.info; -// -// return GestureDetector( -// onTap: () => Navigator.of(context).push(MaterialPageRoute( -// builder: (_) => EventView(eventInstance: event), -// )), -// child: Material( -// shape: RoundedRectangleBorder( -// side: BorderSide( -// color: Theme.of(context).scaffoldBackgroundColor, -// width: 0.75, -// ), -// borderRadius: BorderRadius.circular(4), -// ), -// color: color, -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// mainAxisSize: MainAxisSize.min, -// mainAxisAlignment: MainAxisAlignment.end, -// children: [ -// Padding( -// padding: const EdgeInsets.fromLTRB(4, 2, 4, 0), -// child: AutoSizeText( -// event.title ?? event.mainEvent?.classHeader?.acronym ?? '', -// maxLines: 2, -// minFontSize: 4, -// maxFontSize: 12, -// style: Theme.of(context).textTheme.bodyText2.copyWith( -// fontSize: 12, -// fontWeight: FontWeight.w600, -// color: color.highEmphasisOnColor, -// ), -// ), -// ), -// if (event.mainEvent.type != null) -// Expanded( -// child: Padding( -// padding: const EdgeInsets.fromLTRB(4, 2, 4, 2), -// child: AutoSizeText( -// event.mainEvent.type.toLocalizedString(), -// wrapWords: false, -// minFontSize: 10, -// maxFontSize: 10, -// maxLines: 1, -// overflow: TextOverflow.ellipsis, -// style: Theme.of(context).textTheme.bodyText2.copyWith( -// fontSize: 10, -// color: color.highEmphasisOnColor, -// ), -// ), -// ), -// ), -// Expanded( -// child: footer?.isNotEmpty ?? false -// ? Align( -// alignment: Alignment.bottomRight, -// child: Padding( -// padding: const EdgeInsets.fromLTRB(4, 2, 4, 2), -// child: AutoSizeText( -// footer, -// maxLines: 1, -// minFontSize: 10, -// overflow: TextOverflow.ellipsis, -// style: Theme.of(context).textTheme.bodyText2.copyWith( -// fontSize: 12, -// color: color.mediumEmphasisOnColor, -// ), -// ), -// ), -// ) -// : Container(), -// ), -// ], -// ), -// ), -// ); -// } -//} +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:black_hole_flutter/black_hole_flutter.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:timetable/timetable.dart'; + +import '../../model/events/uni_event.dart'; +import 'event_view.dart'; + +/// Widget to display all day events in the timetable, based on +/// [BasicEventWidget] from the timetable API. +class UniEventWidget extends StatelessWidget { + const UniEventWidget(this.event, {Key key}) + : assert(event != null, 'event is null'), + super(key: key); + + final UniEventInstance event; + + @override + Widget build(BuildContext context) { + final color = event.color ?? + event?.mainEvent?.color ?? + Theme.of(context).primaryColor; + final footer = + (event.location?.isNotEmpty ?? false) ? event.location : event.info; + + return GestureDetector( + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (_) => EventView(eventInstance: event), + )), + child: Material( + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).scaffoldBackgroundColor, + width: 0.75, + ), + borderRadius: BorderRadius.circular(4), + ), + color: color, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(4, 2, 4, 0), + child: AutoSizeText( + event.title ?? event.mainEvent?.classHeader?.acronym ?? '', + maxLines: 2, + minFontSize: 4, + maxFontSize: 12, + style: Theme.of(context).textTheme.bodyText2.copyWith( + fontSize: 12, + fontWeight: FontWeight.w600, + color: color.highEmphasisOnColor, + ), + ), + ), + if (event.mainEvent.type != null) + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 2, 4, 2), + child: AutoSizeText( + event.mainEvent.type.toLocalizedString(), + wrapWords: false, + minFontSize: 10, + maxFontSize: 10, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyText2.copyWith( + fontSize: 10, + color: color.highEmphasisOnColor, + ), + ), + ), + ), + Expanded( + child: footer?.isNotEmpty ?? false + ? Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 2, 4, 2), + child: AutoSizeText( + footer, + maxLines: 1, + minFontSize: 10, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyText2.copyWith( + fontSize: 12, + color: color.mediumEmphasisOnColor, + ), + ), + ), + ) + : Container(), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/timetable/view/timetable_page.dart b/lib/pages/timetable/view/timetable_page.dart index 4132dd5f1..f32787042 100644 --- a/lib/pages/timetable/view/timetable_page.dart +++ b/lib/pages/timetable/view/timetable_page.dart @@ -1,332 +1,385 @@ -//import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; -//import 'package:acs_upb_mobile/generated/l10n.dart'; -//import 'package:acs_upb_mobile/navigation/routes.dart'; -//import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; -//import 'package:acs_upb_mobile/pages/classes/view/classes_page.dart'; -//import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; -//import 'package:acs_upb_mobile/pages/filter/view/filter_page.dart'; -//import 'package:acs_upb_mobile/pages/settings/service/request_provider.dart'; -//import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -//import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; -//import 'package:acs_upb_mobile/pages/timetable/view/date_header.dart'; -//import 'package:acs_upb_mobile/pages/timetable/view/events/add_event_view.dart'; -//import 'package:acs_upb_mobile/pages/timetable/view/events/all_day_event_widget.dart'; -//import 'package:acs_upb_mobile/pages/timetable/view/events/event_widget.dart'; -//import 'package:acs_upb_mobile/widgets/button.dart'; -//import 'package:acs_upb_mobile/widgets/dialog.dart'; -//import 'package:acs_upb_mobile/widgets/scaffold.dart'; -//import 'package:acs_upb_mobile/widgets/toast.dart'; -//import 'package:flutter/material.dart'; -//import 'package:flutter_feather_icons/flutter_feather_icons.dart'; -//import 'package:provider/provider.dart'; -//import 'package:recase/recase.dart'; -//import 'package:time_machine/time_machine.dart'; -//import 'package:timetable/timetable.dart'; -// -//class TimetablePage extends StatefulWidget { -// const TimetablePage({Key key}) : super(key: key); -// -// @override -// _TimetablePageState createState() => _TimetablePageState(); -//} -// -//class _TimetablePageState extends State { -// TimetableController _controller; -// -// @override -// void dispose() { -// _controller?.dispose(); -// super.dispose(); -// } -// -// @override -// Widget build(BuildContext context) { -// final authProvider = Provider.of(context); -// final eventProvider = Provider.of(context); -// if (_controller == null) { -// _controller = TimetableController( -// // TODO(IoanaAlexandru): Make initialTimeRange customizable in settings -// initialTimeRange: InitialTimeRange.range( -// startTime: LocalTime(7, 55, 0), endTime: LocalTime(20, 5, 0)), -// eventProvider: eventProvider); -// -// if (authProvider.isAuthenticated && !authProvider.isAnonymous) { -// scheduleDialog(context); -// } -// } -// -// return AppScaffold( -// title: AnimatedBuilder( -// animation: _controller.dateListenable, -// builder: (context, child) => Text( -// authProvider.isAuthenticated && !authProvider.isAnonymous -// ? S.current.navigationTimetable -// : _controller.currentMonth.titleCase), -// ), -// needsToBeAuthenticated: true, -// leading: AppScaffoldAction( -// icon: Icons.today_outlined, -// onPressed: () => _controller.animateToToday(), -// tooltip: S.current.actionJumpToToday, -// ), -// actions: [ -// AppScaffoldAction( -// icon: FeatherIcons.bookOpen, -// tooltip: S.current.navigationClasses, -// onPressed: () => Navigator.of(context).push( -// MaterialPageRoute( -// builder: (_) => ChangeNotifierProvider.value( -// value: Provider.of(context), -// child: const ClassesPage()), -// ), -// ), -// ), -// AppScaffoldAction( -// icon: FeatherIcons.filter, -// tooltip: S.current.navigationFilter, -// onPressed: () => Navigator.push( -// context, -// MaterialPageRoute(builder: (_) => const FilterPage()), -// ), -// ), -// ], -// body: Padding( -// padding: const EdgeInsets.all(10), -// child: Stack( -// children: [ -// Timetable( -// controller: _controller, -// dateHeaderBuilder: (_, date) => DateHeader(date), -// eventBuilder: (event) => UniEventWidget(event), -// allDayEventBuilder: (context, event, info) => -// UniAllDayEventWidget( -// event, -// info: info, -// ), -// onEventBackgroundTap: (dateTime, isAllDay) { -// if (!isAllDay) { -// final user = Provider.of(context, listen: false) -// .currentUserFromCache; -// if (user.canAddPublicInfo) { -// Navigator.of(context).push(MaterialPageRoute( -// builder: (_) => ChangeNotifierProxyProvider( -// create: (_) => FilterProvider(), -// update: (context, authProvider, filterProvider) { -// return filterProvider..updateAuth(authProvider); -// }, -// child: AddEventView( -// initialEvent: UniEvent( -// start: dateTime, -// duration: const Period(hours: 2), -// id: null), -// ), -// ), -// )); -// } else { -// AppToast.show(S.current.errorPermissionDenied); -// } -// } -// }, -// ), -// ], -// ), -// ), -// ); -// } -// -// Future scheduleDialog(BuildContext context) async { -// WidgetsBinding.instance.addPostFrameCallback((_) async { -// if (!mounted) { -// return; -// } -// -// // Fetch user classes, request necessary info from providers so it's -// // cached when we check in the dialog -// final user = Provider.of(context, listen: false) -// .currentUserFromCache; -// await Provider.of(context, listen: false) -// .fetchClassHeaders(uid: user.uid); -// await Provider.of(context, listen: false).fetchFilter(); -// await Provider.of(context, listen: false) -// .userAlreadyRequested(user.uid); -// -// // Slight delay between last frame and dialog -// await Future.delayed(const Duration(milliseconds: 100)); -// -// // Show dialog if there are no events -// final eventProvider = -// Provider.of(context, listen: false); -// if (eventProvider.empty) { -// await showDialog( -// context: context, -// builder: buildDialog, -// ); -// } -// }); -// } -// -// Widget buildDialog(BuildContext context) { -// final classProvider = Provider.of(context); -// final authProvider = Provider.of(context); -// final filterProvider = Provider.of(context); -// final user = authProvider.currentUserFromCache; -// -// if (classProvider.userClassHeadersCache?.isEmpty ?? true) { -// return AppDialog( -// title: S.current.warningNoEvents, -// content: [ -// RichText( -// text: TextSpan( -// style: Theme.of(context).textTheme.subtitle1, -// children: [ -// TextSpan(text: '${S.current.infoYouNeedToSelect} '), -// WidgetSpan( -// alignment: PlaceholderAlignment.top, -// child: Icon( -// FeatherIcons.bookOpen, -// size: Theme.of(context).textTheme.subtitle1.fontSize + 2, -// ), -// ), -// TextSpan(text: ' ${S.current.infoClasses}.'), -// ], -// ), -// ), -// ], -// actions: [ -// AppButton( -// text: S.current.actionChooseClasses, -// width: 130, -// onTap: () async { -// // Pop the dialog -// Navigator.of(context).pop(); -// // Push the Add classes page -// await Navigator.of(context) -// .push(MaterialPageRoute( -// builder: (_) => ChangeNotifierProvider.value( -// value: classProvider, -// child: FutureBuilder( -// future: classProvider.fetchUserClassIds(user.uid), -// builder: (context, snap) { -// if (snap.hasData) { -// return AddClassesPage( -// initialClassIds: snap.data, -// onSave: (classIds) async { -// await classProvider.setUserClassIds( -// classIds, authProvider.uid); -// Navigator.pop(context); -// }); -// } else { -// return const Center( -// child: CircularProgressIndicator()); -// } -// }, -// )), -// )); -// }, -// ) -// ], -// ); -// } else if ((filterProvider.cachedFilter?.relevantNodes?.length ?? 0) < 6) { -// return AppDialog( -// title: S.current.warningNoEvents, -// content: [ -// RichText( -// text: TextSpan( -// style: Theme.of(context).textTheme.subtitle1, -// children: [ -// TextSpan(text: '${S.current.infoMakeSureGroupIsSelected} '), -// WidgetSpan( -// alignment: PlaceholderAlignment.top, -// child: Icon( -// FeatherIcons.filter, -// size: Theme.of(context).textTheme.subtitle1.fontSize + 2, -// ), -// ), -// TextSpan(text: ' ${S.current.navigationFilter.toLowerCase()}.'), -// ], -// ), -// ), -// ], -// actions: [ -// AppButton( -// text: S.current.actionOpenFilter, -// width: 130, -// onTap: () async { -// // Pop the dialog -// Navigator.of(context).pop(); -// // Push the Filter page -// await Navigator.pushNamed(context, Routes.filter); -// }, -// ) -// ], -// ); -// } else if (user.permissionLevel < 3) { -// // TODO(IoanaAlexandru): Check if user already requested and show a different message -// return AppDialog( -// title: S.current.warningNoEvents, -// content: [Text(S.current.messageYouCanContribute)], -// actions: [ -// AppButton( -// text: S.current.actionRequestPermissions, -// width: 130, -// onTap: () async { -// // Check if user is verified -// final bool isVerified = await authProvider.isVerified; -// // Pop the dialog -// Navigator.of(context).pop(); -// // Push the Permissions page -// if (authProvider.isAnonymous) { -// AppToast.show(S.current.messageNotLoggedIn); -// } else if (!isVerified) { -// AppToast.show(S.current.messageEmailNotVerifiedToPerformAction); -// } else { -// await Navigator.of(context) -// .pushNamed(Routes.requestPermissions); -// } -// }, -// ) -// ], -// ); -// } else { -// return AppDialog( -// title: S.current.warningNoEvents, -// content: [ -// RichText( -// key: const ValueKey('no_events_message'), -// text: TextSpan( -// style: Theme.of(context).textTheme.subtitle1, -// children: [ -// TextSpan(text: S.current.messageThereAreNoEventsForSelected), -// WidgetSpan( -// alignment: PlaceholderAlignment.top, -// child: Icon( -// FeatherIcons.bookOpen, -// size: Theme.of(context).textTheme.subtitle1.fontSize + 2, -// ), -// ), -// TextSpan( -// text: -// '${S.current.navigationClasses.toLowerCase()} ${S.current.stringAnd} '), -// WidgetSpan( -// alignment: PlaceholderAlignment.top, -// child: Icon( -// FeatherIcons.filter, -// size: Theme.of(context).textTheme.subtitle1.fontSize + 2, -// ), -// ), -// TextSpan(text: ' ${S.current.navigationFilter.toLowerCase()}.'), -// ], -// ), -// ), -// ], -// ); -// } -// } -//} -// -//extension MonthController on TimetableController { -// String get currentMonth => -// LocalDateTime(2020, dateListenable.value.monthOfYear, 1, 1, 1, 1) -// .toString('MMMM'); -//} +import 'package:dart_date/dart_date.dart' show Interval; +import 'package:flutter/material.dart' hide Interval; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:provider/provider.dart'; +import 'package:recase/recase.dart'; +import 'package:supercharged/supercharged.dart'; +import 'package:time_machine/time_machine.dart' hide Interval; +import 'package:timetable/timetable.dart'; + +import '../../../authentication/service/auth_provider.dart'; +import '../../../generated/l10n.dart'; +import '../../../navigation/routes.dart'; +import '../../../widgets/button.dart'; +import '../../../widgets/dialog.dart'; +import '../../../widgets/scaffold.dart'; +import '../../../widgets/toast.dart'; +import '../../classes/service/class_provider.dart'; +import '../../classes/view/classes_page.dart'; +import '../../filter/service/filter_provider.dart'; +import '../../filter/view/filter_page.dart'; +import '../../settings/service/request_provider.dart'; +import '../../timetable/timetable_utils.dart'; +import '../model/events/uni_event.dart'; +import '../service/uni_event_provider.dart'; +import 'events/add_event_view.dart'; +import 'events/all_day_event_widget.dart'; +import 'events/event_widget.dart'; + +class TimetablePage extends StatefulWidget { + const TimetablePage({Key key}) : super(key: key); + + @override + _TimetablePageState createState() => _TimetablePageState(); +} + +class _TimetablePageState extends State + with TickerProviderStateMixin { + TimeController _timeController; + DateController _dateController; + + @override + void dispose() { + _timeController?.dispose(); + _dateController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final authProvider = Provider.of(context); + + _dateController ??= DateController( + initialDate: DateTimeTimetable.today(), + visibleRange: VisibleDateRange.week(startOfWeek: DateTime.monday), + ); + + if (_timeController == null) { + _timeController = TimeController( + initialRange: TimeRange(7.hours + 55.minutes, 20.hours + 5.minutes), + // TODO(IoanaAlexandru): Make initialTimeRange customizable in settings + maxRange: TimeRange(0.hours, 24.hours), + ); + + if (authProvider.isAuthenticated && !authProvider.isAnonymous) { + scheduleDialog(context); + } + } + + return AppScaffold( + title: AnimatedBuilder( + animation: _dateController, + builder: (context, child) { + return Text(authProvider.isAuthenticated && !authProvider.isAnonymous + ? _dateController.currentMonth.titleCase + : S.current.navigationTimetable); + }, + ), + needsToBeAuthenticated: true, + leading: AppScaffoldAction( + icon: Icons.today_outlined, + onPressed: () { + _dateController.animateToToday(vsync: this); + }, + tooltip: S.current.actionJumpToToday, + ), + actions: [ + AppScaffoldAction( + icon: FeatherIcons.bookOpen, + tooltip: S.current.navigationClasses, + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ChangeNotifierProvider.value( + value: Provider.of(context), + child: const ClassesPage()), + ), + ), + ), + AppScaffoldAction( + icon: FeatherIcons.filter, + tooltip: S.current.navigationFilter, + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const FilterPage()), + ), + ), + ], + body: Padding( + padding: const EdgeInsets.all(10), + child: Stack( + children: [ + ValueListenableBuilder( + valueListenable: _dateController, + builder: (context, value, child) { + final Stream> eventsInRange = + Provider.of(context, listen: false) + .getEventsIntersecting( + Interval( + // Events are preloaded for previous, current and next page + DateTimeTimetable.dateFromPage(value.page.floor()) - 7.days, + DateTimeTimetable.dateFromPage( + value.page.ceil() + value.visibleDayCount, + ) + + 7.days, + ), + ); + + return StreamBuilder>( + stream: eventsInRange, + builder: (context, + AsyncSnapshot> snapshot) { + if (snapshot.data == null || snapshot.hasError) { + // TODO(bogpie): Handle loading and error states + return Container(); + } + + return TimetableConfig( + eventProvider: + eventProviderFromFixedList(snapshot.data ?? []), + child: child, + dateController: _dateController, + timeController: _timeController, + eventBuilder: (context, event) => UniEventWidget(event), + allDayEventBuilder: (context, event, info) => + UniAllDayEventWidget(event, info: info), + callbacks: TimetableCallbacks( + onDateTimeBackgroundTap: (dateTime) { + final user = + Provider.of(context, listen: false) + .currentUserFromCache; + if (user.canAddPublicInfo) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ChangeNotifierProxyProvider< + AuthProvider, FilterProvider>( + create: (_) => FilterProvider(), + update: + (context, authProvider, filterProvider) { + return filterProvider + ..updateAuth(authProvider); + }, + child: AddEventView( + initialEvent: UniEvent( + start: dateTime.copyWithUtc(), + period: const Period(hours: 2), + id: null), + ), + ), + ), + ); + } else { + AppToast.show(S.current.errorPermissionDenied); + } + }, + ), + ); + }, + ); + }, + child: MultiDateTimetable(), + ) + ], + ), + ), + ); + } + + Future scheduleDialog(BuildContext context) async { + WidgetsBinding.instance.addPostFrameCallback( + (_) async { + if (!mounted) return; + + final authProvider = Provider.of(context, listen: false); + final classProvider = + Provider.of(context, listen: false); + final filterProvider = + Provider.of(context, listen: false); + final requestProvider = + Provider.of(context, listen: false); + + // Fetch user classes, request necessary info from providers so it's + // cached when we check in the dialog + final user = authProvider.currentUserFromCache; + await classProvider.fetchClassHeaders(uid: user.uid); + + await filterProvider.fetchFilter(); + await requestProvider.userAlreadyRequested(user.uid); + + // Slight delay between last frame and dialog + await Future.delayed(const Duration(milliseconds: 100)); + + // Show dialog if there are no events + // final eventProvider = + // Provider.of(context, listen: false); + // if (eventProvider != null) { + // if (eventProvider.empty) { + // await showDialog( + // context: context, + // builder: buildDialog, + // ); + // } + // } + }, + ); + } + + Widget buildDialog(BuildContext context) { + final classProvider = Provider.of(context); + final authProvider = Provider.of(context); + final filterProvider = Provider.of(context); + final user = authProvider.currentUserFromCache; + + if (classProvider.userClassHeadersCache?.isEmpty ?? true) { + return AppDialog( + title: S.current.warningNoEvents, + content: [ + RichText( + text: TextSpan( + style: Theme.of(context).textTheme.subtitle1, + children: [ + TextSpan(text: '${S.current.infoYouNeedToSelect} '), + WidgetSpan( + alignment: PlaceholderAlignment.top, + child: Icon( + FeatherIcons.bookOpen, + size: Theme.of(context).textTheme.subtitle1.fontSize + 2, + ), + ), + TextSpan(text: ' ${S.current.infoClasses}.'), + ], + ), + ), + ], + actions: [ + AppButton( + text: S.current.actionChooseClasses, + width: 130, + onTap: () async { + // Pop the dialog + Navigator.of(context).pop(); + // Push the Add classes page + await Navigator.of(context) + .push(MaterialPageRoute( + builder: (_) => ChangeNotifierProvider.value( + value: classProvider, + child: FutureBuilder( + future: classProvider.fetchUserClassIds(user.uid), + builder: (context, snap) { + if (snap.hasData) { + return AddClassesPage( + initialClassIds: snap.data, + onSave: (classIds) async { + await classProvider.setUserClassIds( + classIds, authProvider.uid); + if (!mounted) return; + Navigator.pop(context); + }); + } else { + return const Center( + child: CircularProgressIndicator()); + } + }, + )), + )); + }, + ) + ], + ); + } else if ((filterProvider.cachedFilter?.relevantNodes?.length ?? 0) < 6) { + return AppDialog( + title: S.current.warningNoEvents, + content: [ + RichText( + text: TextSpan( + style: Theme.of(context).textTheme.subtitle1, + children: [ + TextSpan(text: '${S.current.infoMakeSureGroupIsSelected} '), + WidgetSpan( + alignment: PlaceholderAlignment.top, + child: Icon( + FeatherIcons.filter, + size: Theme.of(context).textTheme.subtitle1.fontSize + 2, + ), + ), + TextSpan(text: ' ${S.current.navigationFilter.toLowerCase()}.'), + ], + ), + ), + ], + actions: [ + AppButton( + text: S.current.actionOpenFilter, + width: 130, + onTap: () async { + // Pop the dialog + Navigator.of(context).pop(); + // Push the Filter page + await Navigator.pushNamed(context, Routes.filter); + }, + ) + ], + ); + } else if (user.permissionLevel < 3) { + // TODO(IoanaAlexandru): Check if user already requested and show a different message + return AppDialog( + title: S.current.warningNoEvents, + content: [Text(S.current.messageYouCanContribute)], + actions: [ + AppButton( + text: S.current.actionRequestPermissions, + width: 130, + onTap: () async { + // Check if user is verified + final bool isVerified = await authProvider.isVerified; + // Pop the dialog + if (!mounted) return; + Navigator.of(context).pop(); + // Push the Permissions page + if (authProvider.isAnonymous) { + AppToast.show(S.current.messageNotLoggedIn); + } else if (!isVerified) { + AppToast.show(S.current.messageEmailNotVerifiedToPerformAction); + } else { + await Navigator.of(context) + .pushNamed(Routes.requestPermissions); + } + }, + ) + ], + ); + } else { + return AppDialog( + title: S.current.warningNoEvents, + content: [ + RichText( + key: const ValueKey('no_events_message'), + text: TextSpan( + style: Theme.of(context).textTheme.subtitle1, + children: [ + TextSpan(text: S.current.messageThereAreNoEventsForSelected), + WidgetSpan( + alignment: PlaceholderAlignment.top, + child: Icon( + FeatherIcons.bookOpen, + size: Theme.of(context).textTheme.subtitle1.fontSize + 2, + ), + ), + TextSpan( + text: + '${S.current.navigationClasses.toLowerCase()} ${S.current.stringAnd} '), + WidgetSpan( + alignment: PlaceholderAlignment.top, + child: Icon( + FeatherIcons.filter, + size: Theme.of(context).textTheme.subtitle1.fontSize + 2, + ), + ), + TextSpan(text: ' ${S.current.navigationFilter.toLowerCase()}.'), + ], + ), + ), + ], + ); + } + } +} diff --git a/lib/resources/custom_icons.dart b/lib/resources/custom_icons.dart index 13aa800e6..d43d17eed 100644 --- a/lib/resources/custom_icons.dart +++ b/lib/resources/custom_icons.dart @@ -32,14 +32,4 @@ class CustomIcons { // Transparent icon to be used as a placeholder static const Icon empty = Icon(Icons.cancel_outlined, color: Color(0x00000000)); - - static Color formIconColor(ThemeData themeData) { - switch (themeData.brightness) { - case Brightness.dark: - return Colors.white70; - case Brightness.light: - return Colors.black45; - } - return themeData.iconTheme.color; - } } diff --git a/lib/resources/remote_config.dart b/lib/resources/remote_config.dart index 332d5c31c..35b54a8b4 100644 --- a/lib/resources/remote_config.dart +++ b/lib/resources/remote_config.dart @@ -22,5 +22,6 @@ class RemoteConfigService { print( 'Unable to fetch remote config. Cached or default values will be used.'); } + // Does not work on web } } diff --git a/lib/resources/theme.dart b/lib/resources/theme.dart new file mode 100644 index 000000000..5b88aad0c --- /dev/null +++ b/lib/resources/theme.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +Color primaryColor = const Color(0xFF43ACCD); + +Color chipSelectedColor(Brightness brightness) => brightness == Brightness.light + ? primaryColor.withOpacity(0.3) + : primaryColor; + +ChipThemeData chipThemeData(Brightness brightness) => + ChipThemeData.fromDefaults( + brightness: brightness, + secondaryColor: primaryColor, + labelStyle: ThemeData().coloredTextTheme.bodyText2, + ).copyWith( + selectedColor: chipSelectedColor(brightness), + secondarySelectedColor: chipSelectedColor(brightness), + checkmarkColor: + brightness == Brightness.light ? primaryColor : Colors.white, + ); + +var lightThemeData = ThemeData( + brightness: Brightness.light, +// The following two lines are meant to remove the splash effect + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + toggleableActiveColor: primaryColor, + fontFamily: 'Montserrat', + primaryColor: primaryColor, + chipTheme: chipThemeData(Brightness.light), +); + +var darkThemeData = ThemeData( + brightness: Brightness.dark, +// The following two lines are meant to remove the splash effect + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + toggleableActiveColor: primaryColor, + fontFamily: 'Montserrat', + primaryColor: primaryColor, + chipTheme: chipThemeData(Brightness.dark), +); + +extension ThemeExtension on ThemeData { + TextStyle chipTextStyle({@required bool selected}) => TextStyle( + color: selected + ? brightness == Brightness.light + ? primaryColor + : Colors.white + : textTheme.bodyText2.color, + fontWeight: selected ? FontWeight.bold : FontWeight.normal, + ); + + // Coloured text, usually highlighting that it can be pressed, similar to + // HTML links. + TextTheme get coloredTextTheme => textTheme.apply( + fontFamily: 'Montserrat', + bodyColor: primaryColor, + displayColor: primaryColor, + ); + + Color get formIconColor { + switch (brightness) { + case Brightness.dark: + return Colors.white70; + case Brightness.light: + return Colors.black45; + } + return iconTheme.color; + } +} diff --git a/lib/resources/themes.dart b/lib/resources/themes.dart deleted file mode 100644 index 317945e9d..000000000 --- a/lib/resources/themes.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; - -Color primaryColor = const Color(0xFF4DB5E4); -Color accentColor = const Color(0xFF43ACCD); - -var lightThemeData = ThemeData( - brightness: Brightness.light, - accentColor: accentColor, -// The following two lines are meant to remove the splash effect - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - accentTextTheme: ThemeData().accentTextTheme.apply( - fontFamily: 'Montserrat', - bodyColor: accentColor, - displayColor: accentColor), - toggleableActiveColor: accentColor, - fontFamily: 'Montserrat', - primaryColor: primaryColor, -); - -var darkThemeData = ThemeData( - brightness: Brightness.dark, - accentColor: accentColor, -// The following two lines are meant to remove the splash effect - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - accentTextTheme: ThemeData().accentTextTheme.apply( - fontFamily: 'Montserrat', - bodyColor: accentColor, - displayColor: accentColor), - toggleableActiveColor: accentColor, - fontFamily: 'Montserrat', - primaryColor: primaryColor, -); diff --git a/lib/resources/utils.dart b/lib/resources/utils.dart index 0f496c3f8..7dba6d444 100644 --- a/lib/resources/utils.dart +++ b/lib/resources/utils.dart @@ -9,6 +9,8 @@ import '../authentication/service/auth_provider.dart'; import '../generated/l10n.dart'; import '../navigation/routes.dart'; import '../widgets/toast.dart'; +import 'platform.dart'; +import 'remote_config.dart'; export 'package:acs_upb_mobile/resources/platform.dart' if (dart.library.io) 'dart:io'; @@ -69,4 +71,9 @@ class Utils { appName: '\$appName', packageName: '\$packageName', ); + + static bool get feedbackEnabled { + if (!Platform.isAndroid && !Platform.isIOS) return false; + return RemoteConfigService.feedbackEnabled; + } } diff --git a/lib/widgets/chip_form_field.dart b/lib/widgets/chip_form_field.dart new file mode 100644 index 000000000..559793930 --- /dev/null +++ b/lib/widgets/chip_form_field.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; + +import '../generated/l10n.dart'; +import '../resources/locale_provider.dart'; +import '../resources/theme.dart'; + +class FilterChipFormField extends ChipFormField> { + FilterChipFormField({ + @required Map initialValues, + @required IconData icon, + @required String label, + Key key, + }) : super( + key: key, + icon: icon, + label: label, + initialValues: initialValues, + validator: (selection) { + if (selection.values.where((e) => e != false).isEmpty) { + return S.current.warningYouNeedToSelectAtLeastOne; + } + return null; + }, + contentBuilder: (state) { + final labels = state.value.keys.toList(); + return ListView.builder( + itemCount: labels.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + return Row( + children: [ + FilterChip( + label: Text( + labels[index].toLocalizedString(), + style: Theme.of(context).chipTextStyle( + selected: state.value[labels[index]]), + ), + selected: state.value[labels[index]], + onSelected: (selected) { + state.value[labels[index]] = selected; + state.didChange(state.value); + }, + ), + const SizedBox(width: 10), + ], + ); + }, + ); + }, + ); +} + +class ChipFormField extends FormField { + ChipFormField({ + @required IconData icon, + @required String label, + @required Widget Function(FormFieldState state) contentBuilder, + Widget Function(FormFieldState) trailingBuilder, + T initialValues, + String Function(T) validator, + Key key, + }) : super( + autovalidateMode: AutovalidateMode.onUserInteraction, + initialValue: initialValues, + key: key, + validator: validator, + builder: (state) { + final context = state.context; + return Padding( + padding: const EdgeInsets.only(top: 12), + child: IntrinsicHeight( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Row( + children: [ + const SizedBox(width: 12), + Icon(icon, color: Theme.of(context).formIconColor), + const SizedBox(width: 12), + Text( + label, + style: Theme.of(context) + .textTheme + .subtitle1 + .copyWith(fontWeight: FontWeight.w400), + ), + Expanded(child: Container()), + if (trailingBuilder != null) trailingBuilder(state), + ], + ), + ), + const SizedBox(height: 10), + Row( + children: [ + const SizedBox(width: 12), + Expanded( + child: SizedBox( + height: 40, + child: contentBuilder(state), + ), + ), + ], + ), + const SizedBox(height: 8), + Divider( + thickness: 0.7, + color: state.hasError + ? Theme.of(context).errorColor + : Theme.of(context).hintColor), + if (state.hasError) + Text( + state.errorText, + style: Theme.of(context).textTheme.caption.copyWith( + color: Theme.of(context).errorColor.withOpacity(1)), + ), + ], + ), + ), + ); + }, + ); +} diff --git a/lib/widgets/error_page.dart b/lib/widgets/error_page.dart index 6d75753bc..a33b59493 100644 --- a/lib/widgets/error_page.dart +++ b/lib/widgets/error_page.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import '../resources/theme.dart'; + class ErrorPage extends StatelessWidget { const ErrorPage({ this.imgPath = 'assets/illustrations/undraw_warning.png', @@ -66,7 +68,7 @@ class ErrorPage extends StatelessWidget { onTap: actionOnTap, child: Text(actionText, style: Theme.of(context) - .accentTextTheme + .coloredTextTheme .subtitle2 .copyWith(fontWeight: FontWeight.w500)), ), diff --git a/lib/widgets/event_list_tile.dart b/lib/widgets/event_list_tile.dart index 889d57ecc..308cf96c1 100644 --- a/lib/widgets/event_list_tile.dart +++ b/lib/widgets/event_list_tile.dart @@ -1,41 +1,42 @@ -//import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -//import 'package:acs_upb_mobile/pages/timetable/view/events/event_view.dart'; -//import 'package:flutter/cupertino.dart'; -//import 'package:flutter/material.dart'; -// -//class EventListTile extends StatelessWidget { -// const EventListTile({ -// this.uniEvent, -// }); -// -// final UniEvent uniEvent; -// -// @override -// Widget build(BuildContext context) { -// return ListTile( -// key: ValueKey(uniEvent.id), -// leading: Padding( -// padding: const EdgeInsets.all(10), -// child: Container( -// width: 20, -// height: 20, -// decoration: BoxDecoration( -// borderRadius: const BorderRadius.all(Radius.circular(4)), -// color: uniEvent.color, -// ), -// ), -// ), -// title: Text(uniEvent.type.toLocalizedString()), -// subtitle: Text( -// uniEvent.info, -// style: Theme.of(context) -// .textTheme -// .bodyText2 -// .copyWith(color: Theme.of(context).hintColor), -// ), -// onTap: () => Navigator.of(context).push(MaterialPageRoute( -// builder: (_) => EventView(uniEvent: uniEvent), -// )), -// ); -// } -//} +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import '../pages/timetable/model/events/uni_event.dart'; +import '../pages/timetable/view/events/event_view.dart'; + +class EventListTile extends StatelessWidget { + const EventListTile({ + this.uniEvent, + }); + + final UniEvent uniEvent; + + @override + Widget build(BuildContext context) { + return ListTile( + key: ValueKey(uniEvent.id), + leading: Padding( + padding: const EdgeInsets.all(10), + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(4)), + color: uniEvent.color, + ), + ), + ), + title: Text(uniEvent.type.toLocalizedString()), + subtitle: Text( + uniEvent.info, + style: Theme.of(context) + .textTheme + .bodyText2 + .copyWith(color: Theme.of(context).hintColor), + ), + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (_) => EventView(uniEvent: uniEvent), + )), + ); + } +} diff --git a/lib/widgets/icon_text.dart b/lib/widgets/icon_text.dart index d2377d906..2fb31a5bd 100644 --- a/lib/widgets/icon_text.dart +++ b/lib/widgets/icon_text.dart @@ -16,7 +16,7 @@ class IconText extends StatelessWidget { final String text; /// Optional "action" text. If this is specified, it will show after the - /// [text], have the theme's `accentColor`, and will be the trigger area for + /// [text], have the theme's `primaryColor`, and will be the trigger area for /// [onTap]. final String actionText; @@ -32,7 +32,7 @@ class IconText extends StatelessWidget { Widget build(BuildContext context) { final textStyle = style ?? Theme.of(context).textTheme.bodyText1; final actionStyle = textStyle - .copyWith(color: Theme.of(context).accentColor) + .copyWith(color: Theme.of(context).primaryColor) .apply(fontWeightDelta: 2); return InkWell( diff --git a/lib/widgets/info_card.dart b/lib/widgets/info_card.dart index 550022f40..df81722db 100644 --- a/lib/widgets/info_card.dart +++ b/lib/widgets/info_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../generated/l10n.dart'; +import '../resources/theme.dart'; class InfoCard extends StatelessWidget { const InfoCard( @@ -51,13 +52,13 @@ class InfoCard extends StatelessWidget { Text( S.current.actionShowMore, style: Theme.of(context) - .accentTextTheme + .coloredTextTheme .subtitle2 - .copyWith(color: Theme.of(context).accentColor), + .copyWith(color: Theme.of(context).primaryColor), ), Icon( Icons.arrow_forward_ios_outlined, - color: Theme.of(context).accentColor, + color: Theme.of(context).primaryColor, size: Theme.of(context).textTheme.subtitle2.fontSize, ) diff --git a/lib/widgets/scaffold.dart b/lib/widgets/scaffold.dart index 531391de4..acbeb323a 100644 --- a/lib/widgets/scaffold.dart +++ b/lib/widgets/scaffold.dart @@ -143,6 +143,7 @@ class AppScaffold extends StatelessWidget { child: AppBar( title: title, centerTitle: true, + backgroundColor: Theme.of(context).primaryColor, toolbarOpacity: 0.8, leading: _widgetFromAction(leading, enableContent: enableContent, context: context), diff --git a/lib/widgets/selectable.dart b/lib/widgets/selectable.dart index f8462d5d7..084d9dbcb 100644 --- a/lib/widgets/selectable.dart +++ b/lib/widgets/selectable.dart @@ -59,13 +59,13 @@ class _SelectableState extends State { color: _isSelected ? (widget.disabled ? Theme.of(context).disabledColor - : Theme.of(context).accentColor) + : Theme.of(context).primaryColor) : Colors.transparent, borderRadius: const BorderRadius.all(Radius.circular(24)), border: Border.all( color: widget.disabled ? Theme.of(context).disabledColor - : Theme.of(context).accentColor), + : Theme.of(context).primaryColor), ), child: Material( color: Colors.transparent, @@ -97,7 +97,7 @@ class _SelectableState extends State { ? Colors.white : widget.disabled ? Theme.of(context).disabledColor - : Theme.of(context).accentColor, + : Theme.of(context).primaryColor, ), ), ), diff --git a/pubspec.lock b/pubspec.lock index ca12464ed..45b91add4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -14,14 +14,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "22.0.0" + version: "26.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "1.7.1" + version: "2.3.0" archive: dependency: transitive description: @@ -35,14 +35,14 @@ packages: name: args url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.3.0" async: dependency: "direct main" description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.1" auto_size_text: dependency: "direct main" description: @@ -56,7 +56,7 @@ packages: name: basic_utils url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "3.6.0" black_hole_flutter: dependency: transitive description: @@ -77,28 +77,42 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" built_collection: dependency: transitive description: name: built_collection url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "5.1.1" built_value: dependency: transitive description: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.0.6" + version: "8.1.2" cached_network_image: dependency: "direct main" description: name: cached_network_image url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.1.0" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" characters: dependency: transitive description: @@ -112,14 +126,14 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" cli_util: dependency: transitive description: name: cli_util url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.3.3" clock: dependency: transitive description: @@ -133,28 +147,28 @@ packages: name: cloud_firestore url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.5.3" cloud_firestore_platform_interface: dependency: transitive description: name: cloud_firestore_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "5.1.1" + version: "5.4.1" cloud_firestore_web: dependency: transitive description: name: cloud_firestore_web url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.4.2" code_builder: dependency: transitive description: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.1.0" collection: dependency: transitive description: @@ -168,7 +182,14 @@ packages: name: convert url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" + cross_file: + dependency: transitive + description: + name: cross_file + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1+5" crypto: dependency: transitive description: @@ -203,7 +224,14 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.1.0" + dartx: + dependency: "direct main" + description: + name: dartx + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.1" datetime_picker_formfield: dependency: "direct main" description: @@ -252,21 +280,21 @@ packages: name: file url: "https://pub.dartlang.org" source: hosted - version: "6.1.1" + version: "6.1.2" firebase: dependency: transitive description: name: firebase url: "https://pub.dartlang.org" source: hosted - version: "9.0.1" + version: "9.0.2" firebase_analytics: dependency: "direct main" description: name: firebase_analytics url: "https://pub.dartlang.org" source: hosted - version: "8.1.1" + version: "8.3.2" firebase_analytics_platform_interface: dependency: transitive description: @@ -287,28 +315,28 @@ packages: name: firebase_auth url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.4.1" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "4.2.4" + version: "4.3.1" firebase_auth_web: dependency: transitive description: name: firebase_auth_web url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" firebase_core: dependency: "direct main" description: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.6.0" firebase_core_platform_interface: dependency: transitive description: @@ -329,35 +357,35 @@ packages: name: firebase_remote_config url: "https://pub.dartlang.org" source: hosted - version: "0.10.0+1" + version: "0.11.0" firebase_remote_config_platform_interface: dependency: transitive description: name: firebase_remote_config_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "0.3.0+1" + version: "0.3.0+5" firebase_storage: dependency: "direct main" description: name: firebase_storage url: "https://pub.dartlang.org" source: hosted - version: "8.1.1" + version: "8.1.3" firebase_storage_platform_interface: dependency: transitive description: name: firebase_storage_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" firebase_storage_web: dependency: transitive description: name: firebase_storage_web url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.2" fixnum: dependency: transitive description: @@ -383,7 +411,7 @@ packages: name: flutter_cache_manager url: "https://pub.dartlang.org" source: hosted - version: "3.1.1" + version: "3.1.2" flutter_colorpicker: dependency: "direct main" description: @@ -404,14 +432,14 @@ packages: name: flutter_launcher_icons url: "https://pub.dartlang.org" source: hosted - version: "0.9.0" + version: "0.9.2" flutter_layout_grid: dependency: transitive description: name: flutter_layout_grid url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.3" flutter_localizations: dependency: "direct main" description: flutter @@ -423,21 +451,21 @@ packages: name: flutter_markdown url: "https://pub.dartlang.org" source: hosted - version: "0.6.2" + version: "0.6.6" flutter_native_splash: dependency: "direct dev" description: name: flutter_native_splash url: "https://pub.dartlang.org" source: hosted - version: "1.1.8+4" + version: "1.2.3" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" flutter_test: dependency: "direct dev" description: flutter @@ -503,35 +531,35 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.0.4" image_picker: dependency: "direct main" description: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.8.0+1" + version: "0.8.4+2" image_picker_for_web: dependency: transitive description: name: image_picker_for_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.3" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.4.1" image_picker_web: dependency: "direct main" description: name: image_picker_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3+1" intl: dependency: transitive description: @@ -552,14 +580,14 @@ packages: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.1.0" logging: dependency: transitive description: name: logging url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" markdown: dependency: transitive description: @@ -580,14 +608,14 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" mockito: dependency: "direct dev" description: name: mockito url: "https://pub.dartlang.org" source: hosted - version: "5.0.9" + version: "5.0.16" nested: dependency: transitive description: @@ -615,28 +643,28 @@ packages: name: oktoast url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.1.3+1" package_config: dependency: transitive description: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" package_info_plus: dependency: "direct main" description: name: package_info_plus url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.6" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" package_info_plus_macos: dependency: transitive description: @@ -650,21 +678,21 @@ packages: name: package_info_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.4" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.3" path: dependency: transitive description: @@ -678,21 +706,21 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.5" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" path_provider_platform_interface: dependency: transitive description: @@ -706,7 +734,7 @@ packages: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.3" pedantic: dependency: transitive description: @@ -720,7 +748,7 @@ packages: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.3.0" pie_chart: dependency: "direct main" description: @@ -741,14 +769,14 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" pointycastle: dependency: transitive description: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "3.1.1" + version: "3.3.4" positioned_tap_detector_2: dependency: "direct main" description: @@ -762,14 +790,14 @@ packages: name: pref url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.4.0" process: dependency: transitive description: name: process url: "https://pub.dartlang.org" source: hosted - version: "4.2.1" + version: "4.2.3" provider: dependency: "direct dev" description: @@ -783,7 +811,7 @@ packages: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" quiver: dependency: "direct main" description: @@ -818,21 +846,21 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.8" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" shared_preferences_platform_interface: dependency: transitive description: @@ -846,14 +874,14 @@ packages: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" sky_engine: dependency: transitive description: flutter @@ -865,7 +893,7 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.1.0" source_span: dependency: transitive description: @@ -879,14 +907,14 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.0.0+3" + version: "2.0.0+4" sqflite_common: dependency: transitive description: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.0.0+2" + version: "2.0.1+1" stack_trace: dependency: transitive description: @@ -908,20 +936,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + substring_highlight: + dependency: "direct main" + description: + name: substring_highlight + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.33" supercharged: dependency: transitive description: name: supercharged url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.1" supercharged_dart: dependency: transitive description: name: supercharged_dart url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.1" synchronized: dependency: "direct main" description: @@ -942,7 +977,23 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.2" + time: + dependency: transitive + description: + name: time + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + time_machine: + dependency: "direct main" + description: + path: "." + ref: master + resolved-ref: "040de1a261df442538ed97f6de5895465d7ca4dd" + url: "https://github.com/Dana-Ferguson/time_machine" + source: git + version: "0.9.17" time_range_picker: dependency: "direct main" description: @@ -956,14 +1007,14 @@ packages: name: timeago url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.1.0" timetable: dependency: "direct main" description: name: timetable url: "https://pub.dartlang.org" source: hosted - version: "1.0.0-alpha.0" + version: "1.0.0-alpha.5" tuple: dependency: transitive description: @@ -991,42 +1042,42 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.6" + version: "6.0.11" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.4" url_launcher_windows: dependency: transitive description: name: url_launcher_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" uuid: dependency: transitive description: @@ -1068,7 +1119,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.2.9" xdg_directories: dependency: transitive description: @@ -1082,7 +1133,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.1.2" + version: "5.3.0" yaml: dependency: transitive description: @@ -1091,5 +1142,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.13.0 <3.0.0" - flutter: ">=2.0.0" + dart: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" diff --git a/pubspec.yaml b/pubspec.yaml index b3a8621b3..a28e48b3b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ description: A mobile application for students at ACS UPB. # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # # ACS UPB Mobile uses semantic versioning. You can read more in the CONTRIBUTING.md file. -version: 1.2.12+16 +version: 1.2.13+19 environment: sdk: ">=2.7.0 <3.0.0" @@ -38,24 +38,23 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.3 + dartx: any + # Date & time picker widget datetime_picker_formfield: ^2.0.0 # Dotted line painter dotted_line: ^3.0.0 - # Highlight words -# dynamic_text_highlighting: ^2.2.0 - # Package for dynamically changing the app theme -# dynamic_theme: ^1.0.1 + # dynamic_theme: ^1.0.1 easy_dynamic_theme: ^2.2.0 # Firebase products firebase_analytics: ^8.1.1 firebase_auth: ^1.3.0 - firebase_core: ^1.2.1 - firebase_remote_config: ^0.10.0 + firebase_core: ^1.6.0 + firebase_remote_config: ^0.11.0 firebase_storage: ^8.1.1 # Flutter SDK @@ -94,7 +93,7 @@ dependencies: image_picker_web: ^2.0.2 # Interval picker widget -# interval_time_picker: ^0.1.0 + # interval_time_picker: ^0.1.0 # Displays a custom toast oktoast: ^3.0.0 @@ -123,17 +122,23 @@ dependencies: # Async utilities rxdart: ^0.26.0 + # Highlight words + substring_highlight: ^1.0.33 + # Support lock/mutex synchronized: ^3.0.0 # DateTime utilities -# time_machine: ^0.9.16 + time_machine: + git: + url: https://github.com/Dana-Ferguson/time_machine + ref: master # Time interval picker time_range_picker: ^2.0.1 # Timetable widget - timetable: ^1.0.0-alpha.0 + timetable: ^1.0.0-alpha.5 # URL opener url_launcher: ^6.0.6 @@ -159,7 +164,7 @@ dev_dependencies: sdk: flutter # Helps with generating Dart code with the messages from `.arb` files -# intl_translation: ^0.17.1 + # intl_translation: ^0.17.1 # Mocking utility used for testing mockito: ^5.0.9 @@ -182,63 +187,63 @@ flutter: uses-material-design: true assets: - - assets/icons/ - - assets/illustrations/ - - assets/images/ -# - packages/time_machine/data/cultures/cultures.bin -# - packages/time_machine/data/tzdb/tzdb.bin + - assets/icons/ + - assets/illustrations/ + - assets/images/ + - packages/time_machine/data/cultures/cultures.bin + - packages/time_machine/data/tzdb/tzdb.bin fonts: - - family: CustomIcons - fonts: - - asset: assets/fonts/CustomIcons/CustomIcons.ttf - - family: Montserrat - fonts: - - asset: assets/fonts/Montserrat/Montserrat-Thin.otf - weight: 100 - - asset: assets/fonts/Montserrat/Montserrat-ThinItalic.otf - weight: 100 - style: italic - - asset: assets/fonts/Montserrat/Montserrat-ExtraLight.otf - weight: 200 - - asset: assets/fonts/Montserrat/Montserrat-ExtraLightItalic.otf - weight: 200 - style: italic - - asset: assets/fonts/Montserrat/Montserrat-Light.otf - weight: 300 - - asset: assets/fonts/Montserrat/Montserrat-LightItalic.otf - weight: 300 - style: italic - - asset: assets/fonts/Montserrat/Montserrat-Regular.otf - weight: 400 - - asset: assets/fonts/Montserrat/Montserrat-Italic.otf - weight: 400 - style: italic - - asset: assets/fonts/Montserrat/Montserrat-Medium.otf - weight: 500 - - asset: assets/fonts/Montserrat/Montserrat-MediumItalic.otf - weight: 500 - style: italic - - asset: assets/fonts/Montserrat/Montserrat-SemiBold.otf - weight: 600 - - asset: assets/fonts/Montserrat/Montserrat-SemiBoldItalic.otf - weight: 600 - style: italic - - asset: assets/fonts/Montserrat/Montserrat-Bold.otf - weight: 700 - - asset: assets/fonts/Montserrat/Montserrat-BoldItalic.otf - weight: 700 - style: italic - - asset: assets/fonts/Montserrat/Montserrat-ExtraBold.otf - weight: 800 - - asset: assets/fonts/Montserrat/Montserrat-ExtraBoldItalic.otf - weight: 800 - style: italic - - asset: assets/fonts/Montserrat/Montserrat-Black.otf - weight: 900 - - asset: assets/fonts/Montserrat/Montserrat-BlackItalic.otf - weight: 900 - style: italic + - family: CustomIcons + fonts: + - asset: assets/fonts/CustomIcons/CustomIcons.ttf + - family: Montserrat + fonts: + - asset: assets/fonts/Montserrat/Montserrat-Thin.otf + weight: 100 + - asset: assets/fonts/Montserrat/Montserrat-ThinItalic.otf + weight: 100 + style: italic + - asset: assets/fonts/Montserrat/Montserrat-ExtraLight.otf + weight: 200 + - asset: assets/fonts/Montserrat/Montserrat-ExtraLightItalic.otf + weight: 200 + style: italic + - asset: assets/fonts/Montserrat/Montserrat-Light.otf + weight: 300 + - asset: assets/fonts/Montserrat/Montserrat-LightItalic.otf + weight: 300 + style: italic + - asset: assets/fonts/Montserrat/Montserrat-Regular.otf + weight: 400 + - asset: assets/fonts/Montserrat/Montserrat-Italic.otf + weight: 400 + style: italic + - asset: assets/fonts/Montserrat/Montserrat-Medium.otf + weight: 500 + - asset: assets/fonts/Montserrat/Montserrat-MediumItalic.otf + weight: 500 + style: italic + - asset: assets/fonts/Montserrat/Montserrat-SemiBold.otf + weight: 600 + - asset: assets/fonts/Montserrat/Montserrat-SemiBoldItalic.otf + weight: 600 + style: italic + - asset: assets/fonts/Montserrat/Montserrat-Bold.otf + weight: 700 + - asset: assets/fonts/Montserrat/Montserrat-BoldItalic.otf + weight: 700 + style: italic + - asset: assets/fonts/Montserrat/Montserrat-ExtraBold.otf + weight: 800 + - asset: assets/fonts/Montserrat/Montserrat-ExtraBoldItalic.otf + weight: 800 + style: italic + - asset: assets/fonts/Montserrat/Montserrat-Black.otf + weight: 900 + - asset: assets/fonts/Montserrat/Montserrat-BlackItalic.otf + weight: 900 + style: italic flutter_intl: enabled: true diff --git a/test/authentication_test.dart b/test/authentication_test.dart index dcbc2baf3..5a48a7f1d 100644 --- a/test/authentication_test.dart +++ b/test/authentication_test.dart @@ -1,699 +1,699 @@ -import 'package:acs_upb_mobile/authentication/model/user.dart'; -import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; -import 'package:acs_upb_mobile/authentication/view/login_view.dart'; -import 'package:acs_upb_mobile/authentication/view/sign_up_view.dart'; -import 'package:acs_upb_mobile/main.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; -import 'package:acs_upb_mobile/pages/classes/model/class.dart'; -import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; -import 'package:acs_upb_mobile/pages/faq/model/question.dart'; -import 'package:acs_upb_mobile/pages/faq/service/question_provider.dart'; -import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; -import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; -import 'package:acs_upb_mobile/pages/home/home_page.dart'; -import 'package:acs_upb_mobile/pages/news_feed/model/news_feed_item.dart'; -import 'package:acs_upb_mobile/pages/news_feed/service/news_provider.dart'; -import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; -import 'package:acs_upb_mobile/pages/portal/service/website_provider.dart'; -import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; -import 'package:acs_upb_mobile/resources/locale_provider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:preferences/preferences.dart'; -import 'package:provider/provider.dart'; -import 'package:time_machine/time_machine.dart'; - -import 'test_utils.dart'; - -class MockAuthProvider extends Mock implements AuthProvider {} - -class MockNavigatorObserver extends Mock implements NavigatorObserver {} - -class MockFilterProvider extends Mock implements FilterProvider {} - -class MockWebsiteProvider extends Mock implements WebsiteProvider {} - -class MockPersonProvider extends Mock implements PersonProvider {} - -class MockUniEventProvider extends Mock implements UniEventProvider {} - -class MockQuestionProvider extends Mock implements QuestionProvider {} - -class MockNewsProvider extends Mock implements NewsProvider {} - -class MockFeedbackProvider extends Mock implements FeedbackProvider {} - -class MockClassProvider extends Mock implements ClassProvider {} - -void main() { - AuthProvider mockAuthProvider; - WebsiteProvider mockWebsiteProvider; - FilterProvider mockFilterProvider; - PersonProvider mockPersonProvider; - MockQuestionProvider mockQuestionProvider; - UniEventProvider mockEventProvider; - MockNewsProvider mockNewsProvider; - FeedbackProvider mockFeedbackProvider; - ClassProvider mockClassProvider; - - setUp(() async { - WidgetsFlutterBinding.ensureInitialized(); - PrefService.enableCaching(); - PrefService.cache = {}; - PrefService.setString('language', 'en'); - - LocaleProvider.cultures = testCultures; - LocaleProvider.rruleL10ns = {'en': await RruleL10nTest.create()}; - - // Mock the behaviour of the auth provider - mockAuthProvider = MockAuthProvider(); - // ignore: invalid_use_of_protected_member - when(mockAuthProvider.hasListeners).thenReturn(false); - when(mockAuthProvider.isAuthenticated).thenReturn(false); - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(null)); - when(mockAuthProvider.isAnonymous).thenReturn(true); - when(mockAuthProvider.getProfilePictureURL()) - .thenAnswer((_) => Future.value(null)); - - mockWebsiteProvider = MockWebsiteProvider(); - // ignore: invalid_use_of_protected_member - when(mockWebsiteProvider.hasListeners).thenReturn(false); - when(mockWebsiteProvider.deleteWebsite(any)) - .thenAnswer((_) => Future.value(true)); - when(mockWebsiteProvider.fetchWebsites(any)) - .thenAnswer((_) => Future.value([])); - when(mockWebsiteProvider.fetchFavouriteWebsites(mockAuthProvider.uid)) - .thenAnswer((_) => Future.value(null)); - - mockFilterProvider = MockFilterProvider(); - // ignore: invalid_use_of_protected_member - when(mockFilterProvider.hasListeners).thenReturn(false); - when(mockFilterProvider.filterEnabled).thenReturn(true); - when(mockFilterProvider.fetchFilter()) - .thenAnswer((_) => Future.value(Filter(localizedLevelNames: [ - {'en': 'Level', 'ro': 'Nivel'} - ], root: FilterNode(name: 'root')))); - - mockPersonProvider = MockPersonProvider(); - // ignore: invalid_use_of_protected_member - when(mockPersonProvider.hasListeners).thenReturn(false); - when(mockPersonProvider.fetchPeople()).thenAnswer((_) => Future.value([])); - - mockQuestionProvider = MockQuestionProvider(); - // ignore: invalid_use_of_protected_member - when(mockQuestionProvider.hasListeners).thenReturn(false); - when(mockQuestionProvider.fetchQuestions()) - .thenAnswer((_) => Future.value([])); - when(mockQuestionProvider.fetchQuestions(limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([])); - - mockNewsProvider = MockNewsProvider(); - // ignore: invalid_use_of_protected_member - when(mockNewsProvider.hasListeners).thenReturn(false); - when(mockNewsProvider.fetchNewsFeedItems()) - .thenAnswer((_) => Future.value([])); - when(mockNewsProvider.fetchNewsFeedItems(limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([])); - - mockEventProvider = MockUniEventProvider(); - // ignore: invalid_use_of_protected_member - when(mockEventProvider.hasListeners).thenReturn(false); - when(mockEventProvider.getUpcomingEvents(LocalDate.today())) - .thenAnswer((_) => Future.value([])); - when(mockEventProvider.getUpcomingEvents(LocalDate.today(), - limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([])); - - mockFeedbackProvider = MockFeedbackProvider(); - // ignore: invalid_use_of_protected_member - when(mockFeedbackProvider.hasListeners).thenReturn(true); - when(mockFeedbackProvider.userSubmittedFeedbackForClass(any, any)) - .thenAnswer((_) => Future.value(false)); - when(mockFeedbackProvider.getClassesWithCompletedFeedback(any)) - .thenAnswer((_) => Future.value({'M1': true, 'M2': true})); - - mockClassProvider = MockClassProvider(); - // ignore: invalid_use_of_protected_member - when(mockClassProvider.hasListeners).thenReturn(false); - final userClassHeaders = [ - ClassHeader( - id: '3', - name: 'Programming', - acronym: 'PC', - category: 'A', - ), - ClassHeader( - id: '4', - name: 'Physics', - acronym: 'PH', - category: 'D', - ) - ]; - when(mockClassProvider.userClassHeadersCache).thenReturn(userClassHeaders); - when(mockClassProvider.fetchClassHeaders(uid: anyNamed('uid'))) - .thenAnswer((_) => Future.value([ - ClassHeader( - id: '1', - name: 'Maths 1', - acronym: 'M1', - category: 'A/B', - ), - ClassHeader( - id: '2', - name: 'Maths 2', - acronym: 'M2', - category: 'A/C', - ), - ] + - userClassHeaders)); - when(mockClassProvider.fetchUserClassIds(any)) - .thenAnswer((_) => Future.value(['3', '4'])); - }); - - group('Login', () { - testWidgets('Anonymous login', (WidgetTester tester) async { - await tester.pumpWidget(MultiProvider(providers: [ - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ChangeNotifierProvider( - create: (_) => mockEventProvider), - ChangeNotifierProvider( - create: (_) => mockWebsiteProvider), - ChangeNotifierProvider( - create: (_) => mockQuestionProvider), - ChangeNotifierProvider(create: (_) => mockNewsProvider), - ], child: const MyApp())); - await tester.pumpAndSettle(); - - await tester.runAsync(() async { - expect(find.byType(LoginView), findsOneWidget); - - when(mockAuthProvider.signInAnonymously()) - .thenAnswer((_) => Future.value(true)); - - // Log in anonymously - await tester - .tap(find.byKey(const ValueKey('log_in_anonymously_button'))); - await tester.pumpAndSettle(); - - verify(mockAuthProvider.signInAnonymously()); - expect(find.byType(HomePage), findsOneWidget); - - // Easy way to check that the login page can't be navigated back to - expect(find.byIcon(Icons.arrow_back), findsNothing); - }); - }); - - testWidgets('Credential login', (WidgetTester tester) async { - await tester.pumpWidget(MultiProvider(providers: [ - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ChangeNotifierProvider( - create: (_) => mockEventProvider), - ChangeNotifierProvider( - create: (_) => mockWebsiteProvider), - ChangeNotifierProvider( - create: (_) => mockQuestionProvider), - ChangeNotifierProvider(create: (_) => mockNewsProvider), - ], child: const MyApp())); - await tester.pumpAndSettle(); - - await tester.runAsync(() async { - expect(find.byType(LoginView), findsOneWidget); - - expect(find.text('@stud.acs.upb.ro'), findsOneWidget); - - when(mockAuthProvider.signIn(any, any)) - .thenAnswer((_) => Future.value(true)); - - // Enter credentials - await tester.enterText( - find.byKey(const ValueKey('email_text_field')), 'test'); - await tester.enterText( - find.byKey(const ValueKey('password_text_field')), 'password'); - - await tester.tap(find.byKey(const ValueKey('log_in_button'))); - await tester.pumpAndSettle(); - - verify(mockAuthProvider.signIn( - argThat(equals('test@stud.acs.upb.ro')), - argThat(equals('password')), - )); - expect(find.byType(HomePage), findsOneWidget); - - // Easy way to check that the login page can't be navigated back to - expect(find.byIcon(Icons.arrow_back), findsNothing); - }); - }); - }); - - group('Recover password', () { - testWidgets('Send email', (WidgetTester tester) async { - await tester.pumpWidget(ChangeNotifierProvider( - create: (_) => mockAuthProvider, child: const MyApp())); - await tester.pumpAndSettle(); - - expect(find.byType(LoginView), findsOneWidget); - - when(mockAuthProvider.sendPasswordResetEmail(any)) - .thenAnswer((_) => Future.value(true)); - - expect(find.byType(AlertDialog), findsNothing); - - // Reset password - await tester.tap(find.text('Reset password')); - await tester.pumpAndSettle(); - - expect(find.byType(AlertDialog), findsOneWidget); - - // Send email - await tester.enterText( - find.byKey(const ValueKey('reset_password_email_text_field')), - 'test'); - - await tester.tap(find.byKey(const ValueKey('send_email_button'))); - await tester.pumpAndSettle(); - - expect(find.byType(AlertDialog), findsNothing); - - verify(mockAuthProvider - .sendPasswordResetEmail(argThat(equals('test@stud.acs.upb.ro')))); - }); - - testWidgets('Cancel', (WidgetTester tester) async { - await tester.pumpWidget(ChangeNotifierProvider( - create: (_) => mockAuthProvider, child: const MyApp())); - await tester.pumpAndSettle(); - - expect(find.byType(LoginView), findsOneWidget); - - when(mockAuthProvider.sendPasswordResetEmail(any)) - .thenAnswer((_) => Future.value(true)); - - expect(find.byType(AlertDialog), findsNothing); - - // Reset password - await tester.tap(find.text('Reset password')); - await tester.pumpAndSettle(); - - expect(find.byType(AlertDialog), findsOneWidget); - - // Close dialog - await tester.tap(find.byKey(const ValueKey('cancel_button'))); - await tester.pumpAndSettle(); - - expect(find.byType(AlertDialog), findsNothing); - - verifyNever(mockAuthProvider.sendPasswordResetEmail(any)); - }); - }); - - group('Sign up', () { - final MockNavigatorObserver mockObserver = MockNavigatorObserver(); - FilterProvider mockFilterProvider = MockFilterProvider(); - - setUp(() { - mockFilterProvider = MockFilterProvider(); - // ignore: invalid_use_of_protected_member - when(mockFilterProvider.hasListeners).thenReturn(false); - when(mockFilterProvider.filterEnabled).thenReturn(true); - when(mockFilterProvider.fetchFilter()) - .thenAnswer((_) => Future.value(Filter( - localizedLevelNames: [ - {'en': 'Degree', 'ro': 'Nivel de studiu'}, - {'en': 'Major', 'ro': 'Specializare'}, - {'en': 'Year', 'ro': 'An'}, - {'en': 'Series', 'ro': 'Serie'}, - {'en': 'Group', 'ro': 'Group'}, - {'en': 'Subgroup', 'ro': 'Semigrupă'} - ], - root: FilterNode(name: 'All', value: true, children: [ - FilterNode(name: 'BSc', value: true, children: [ - FilterNode(name: 'CTI', value: true, children: [ - FilterNode( - name: 'CTI-1', - value: true, - children: [ - FilterNode(name: '1-CA'), - FilterNode( - name: '1-CB', - value: true, - children: [ - FilterNode( - name: '311CB', - value: true, - children: [ - FilterNode(name: '311CBa'), - FilterNode(name: '311CBb'), - ], - ), - FilterNode( - name: '312CB', - value: true, - children: [ - FilterNode(name: '312CBa'), - FilterNode(name: '312CBb'), - ], - ), - FilterNode( - name: '313CB', - value: true, - children: [ - FilterNode(name: '313CBa'), - FilterNode(name: '313CBb'), - ], - ), - FilterNode( - name: '314CB', - value: true, - children: [ - FilterNode(name: '314CBa'), - FilterNode(name: '314CBb'), - ], - ), - ], - ), - FilterNode(name: '1-CC'), - FilterNode(name: '1-CD', children: [ - FilterNode( - name: '311CD', - value: true, - children: [ - FilterNode(name: '311CDa'), - FilterNode(name: '311CDb'), - ], - ), - FilterNode( - name: '312CD', - value: true, - children: [ - FilterNode(name: '312CDa'), - FilterNode(name: '312CDb'), - ], - ), - FilterNode( - name: '313CD', - value: true, - children: [ - FilterNode(name: '313CDa'), - FilterNode(name: '313CDb'), - ], - ), - FilterNode( - name: '314CD', - value: true, - children: [ - FilterNode(name: '314CDa'), - FilterNode(name: '314CDb'), - ], - ), - ]), - ], - ), - FilterNode( - name: 'CTI-2', - ), - FilterNode( - name: 'CTI-3', - ), - FilterNode( - name: 'CTI-4', - ), - ]), - FilterNode(name: 'IS') - ]), - FilterNode(name: 'MSc', children: [ - FilterNode( - name: 'IA', - ), - FilterNode(name: 'SPRC'), - ]) - ])))); - }); - - testWidgets('Sign up', (WidgetTester tester) async { - await tester.pumpWidget(MultiProvider(providers: [ - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ChangeNotifierProvider( - create: (_) => mockFilterProvider), - ChangeNotifierProvider( - create: (_) => mockWebsiteProvider), - ChangeNotifierProvider( - create: (_) => mockQuestionProvider), - ChangeNotifierProvider(create: (_) => mockNewsProvider), - ], child: MyApp(navigationObservers: [mockObserver]))); - await tester.pumpAndSettle(); - - verify(mockObserver.didPush(any, any)); - expect(find.byType(LoginView), findsOneWidget); - - // Scroll sign up button into view and tap - await tester.ensureVisible(find.text('Sign up')); - await tester.tap(find.text('Sign up')); - await tester.pumpAndSettle(); - - verify(mockObserver.didPush(any, any)); - expect(find.byType(SignUpView), findsOneWidget); - - when(mockAuthProvider.signUp(any)).thenAnswer((_) => Future.value(true)); - when(mockAuthProvider.canSignUpWithEmail(any)) - .thenAnswer((_) => Future.value(true)); - - // Test parser from email - final Finder email = find.byKey(const ValueKey('email_text_field')); - final TextField firstName = tester.widget( - find.byKey(const ValueKey('first_name_text_field'))); - final TextField lastName = tester.widget( - find.byKey(const ValueKey('last_name_text_field'))); - - await tester.enterText(email, 'john_alexander.doe123'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john.doe'); - expect(firstName.controller.text, equals('John')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, '1234john.doe'); - expect(firstName.controller.text, equals('John')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john1234.doe'); - expect(firstName.controller.text, equals('John')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john.1234doe'); - expect(firstName.controller.text, equals('John')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john.doe1234'); - expect(firstName.controller.text, equals('John')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, '1234john_alexander.doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john1234_alexander.doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john_1234alexander.doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john_alexander1234.doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john_alexander.1234doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, '!@#%^&*()=-+john_alexander.doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john!@#%^&*()=-+_alexander.doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john_!@#%^&*()=-+alexander.doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john_alexander!@#%^&*()=-+.doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john_alexander.!@#%^&*()=-+doe'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john_alexander.doe!@#%^&*()=-+'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, - '!@#%^&*()=-+john!@#%^&*()=-+_!@#%^&*()=-+alexander!@#%^&*()=-+.!@#%^&*()=-+1234!@#%^&*()=-+doe!@#%^&*()=-+'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'j12o##h&n_alexand@-er.do***e'); - expect(firstName.controller.text, equals('John Alexander')); - expect(lastName.controller.text, equals('Doe')); - - await tester.enterText(email, 'john_alexander.doe1234'); - - /////////////////////// - - await tester.enterText( - find.byKey(const ValueKey('password_text_field')), 'password'); - await tester.enterText( - find.byKey(const ValueKey('confirm_password_text_field')), - 'password'); - await tester.enterText( - find.byKey(const ValueKey('first_name_text_field')), - 'John Alexander'); - await tester.enterText( - find.byKey(const ValueKey('last_name_text_field')), 'Doe'); - - // TODO(AdrianMargineanu): Test dropdown buttons - - // Scroll sign up button into view - await tester.ensureVisible(find.byKey(const ValueKey('sign_up_button'))); - - // Check Privacy Policy - await tester.tap(find.byType(Checkbox)); - - // Press sign up - await tester.tap(find.byKey(const ValueKey('sign_up_button'))); - await tester.pumpAndSettle(); - - verify(mockAuthProvider.signUp(argThat(equals({ - 'Email': 'john_alexander.doe1234@stud.acs.upb.ro', - 'Password': 'password', - 'Confirm password': 'password', - 'First name': 'John Alexander', - 'Last name': 'Doe', - })))); - expect(find.byType(HomePage), findsOneWidget); - verify(mockObserver.didPush(any, any)); - }); - - testWidgets('Cancel', (WidgetTester tester) async { - await tester.pumpWidget(MultiProvider(providers: [ - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ChangeNotifierProvider( - create: (_) => mockFilterProvider) - ], child: MyApp(navigationObservers: [mockObserver]))); - await tester.pumpAndSettle(); - - verify(mockObserver.didPush(any, any)); - expect(find.byType(LoginView), findsOneWidget); - - // Scroll sign up button into view and tap - await tester.ensureVisible(find.text('Sign up')); - await tester.tap(find.text('Sign up')); - await tester.pumpAndSettle(); - - verify(mockObserver.didPush(any, any)); - expect(find.byType(SignUpView), findsOneWidget); - - when(mockAuthProvider.signUp(any)).thenAnswer((_) => Future.value(true)); - - // Scroll cancel button into view and tap - await tester.ensureVisible(find.byKey(const ValueKey('cancel_button'))); - await tester.tap(find.byKey(const ValueKey('cancel_button'))); - await tester.pumpAndSettle(); - - verifyNever(mockAuthProvider.signUp(any)); - expect(find.byType(LoginView), findsOneWidget); - expect(find.byType(SignUpView), findsNothing); - verify(mockObserver.didPop(any, any)); - }); - }); - - group('Sign out', () { - final MockNavigatorObserver mockObserver = MockNavigatorObserver(); - - setUp(() { - // Mock an anonymous user already being logged in - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(false)); - }); - - testWidgets('Sign out anonymous', (WidgetTester tester) async { - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(null)); - when(mockAuthProvider.currentUserFromCache).thenReturn(null); - when(mockAuthProvider.isAnonymous).thenReturn(true); - - await tester.pumpWidget(MultiProvider(providers: [ - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ChangeNotifierProvider( - create: (_) => mockFilterProvider), - ChangeNotifierProvider( - create: (_) => mockEventProvider), - ChangeNotifierProvider( - create: (_) => mockWebsiteProvider), - ChangeNotifierProvider( - create: (_) => mockPersonProvider), - ChangeNotifierProvider( - create: (_) => mockQuestionProvider), - ChangeNotifierProvider(create: (_) => mockNewsProvider), - ], child: MyApp(navigationObservers: [mockObserver]))); - await tester.pumpAndSettle(); - - verify(mockObserver.didPush(any, any)); - expect(find.byType(HomePage), findsOneWidget); - - expect(find.text('Anonymous'), findsOneWidget); - - // Press log in button - await tester.tap(find.text('Log in')); - await tester.pumpAndSettle(); - - verify(mockAuthProvider.signOut()); - expect(find.byType(LoginView), findsOneWidget); - }); - - testWidgets('Sign out authenticated', (WidgetTester tester) async { - when(mockAuthProvider.currentUser).thenAnswer((_) => - Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); - when(mockAuthProvider.currentUserFromCache) - .thenReturn(User(uid: '0', firstName: 'John', lastName: 'Doe')); - when(mockAuthProvider.isAnonymous).thenReturn(false); - - await tester.pumpWidget(MultiProvider(providers: [ - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ChangeNotifierProvider( - create: (_) => mockFilterProvider), - ChangeNotifierProvider( - create: (_) => mockWebsiteProvider), - ChangeNotifierProvider( - create: (_) => mockEventProvider), - ChangeNotifierProvider( - create: (_) => mockPersonProvider), - ChangeNotifierProvider( - create: (_) => mockQuestionProvider), - ChangeNotifierProvider(create: (_) => mockNewsProvider), - ChangeNotifierProvider( - create: (_) => mockFeedbackProvider), - ChangeNotifierProvider(create: (_) => mockClassProvider), - ], child: MyApp(navigationObservers: [mockObserver]))); - await tester.pumpAndSettle(); - - verify(mockObserver.didPush(any, any)); - expect(find.byType(HomePage), findsOneWidget); - - expect(find.text('John Doe'), findsOneWidget); - - // Press log out button - await tester.tap(find.text('Log out')); - await tester.pumpAndSettle(); - - verify(mockAuthProvider.signOut()); - expect(find.byType(LoginView), findsOneWidget); - }); - }); -} +// import 'package:acs_upb_mobile/authentication/model/user.dart'; +// import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; +// import 'package:acs_upb_mobile/authentication/view/login_view.dart'; +// import 'package:acs_upb_mobile/authentication/view/sign_up_view.dart'; +// import 'package:acs_upb_mobile/main.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; +// import 'package:acs_upb_mobile/pages/classes/model/class.dart'; +// import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; +// import 'package:acs_upb_mobile/pages/faq/model/question.dart'; +// import 'package:acs_upb_mobile/pages/faq/service/question_provider.dart'; +// import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; +// import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; +// import 'package:acs_upb_mobile/pages/home/home_page.dart'; +// import 'package:acs_upb_mobile/pages/news_feed/model/news_feed_item.dart'; +// import 'package:acs_upb_mobile/pages/news_feed/service/news_provider.dart'; +// import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; +// import 'package:acs_upb_mobile/pages/portal/service/website_provider.dart'; +// import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; +// import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; +// import 'package:acs_upb_mobile/resources/locale_provider.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:mockito/mockito.dart'; +// import 'package:preferences/preferences.dart'; +// import 'package:provider/provider.dart'; +// import 'package:time_machine/time_machine.dart'; +// +// import 'test_utils.dart'; +// +// class MockAuthProvider extends Mock implements AuthProvider {} +// +// class MockNavigatorObserver extends Mock implements NavigatorObserver {} +// +// class MockFilterProvider extends Mock implements FilterProvider {} +// +// class MockWebsiteProvider extends Mock implements WebsiteProvider {} +// +// class MockPersonProvider extends Mock implements PersonProvider {} +// +// class MockUniEventProvider extends Mock implements UniEventProvider {} +// +// class MockQuestionProvider extends Mock implements QuestionProvider {} +// +// class MockNewsProvider extends Mock implements NewsProvider {} +// +// class MockFeedbackProvider extends Mock implements FeedbackProvider {} +// +// class MockClassProvider extends Mock implements ClassProvider {} +// +// void main() { +// AuthProvider mockAuthProvider; +// WebsiteProvider mockWebsiteProvider; +// FilterProvider mockFilterProvider; +// PersonProvider mockPersonProvider; +// MockQuestionProvider mockQuestionProvider; +// UniEventProvider mockEventProvider; +// MockNewsProvider mockNewsProvider; +// FeedbackProvider mockFeedbackProvider; +// ClassProvider mockClassProvider; +// +// setUp(() async { +// WidgetsFlutterBinding.ensureInitialized(); +// PrefService.enableCaching(); +// PrefService.cache = {}; +// PrefService.setString('language', 'en'); +// +// LocaleProvider.cultures = testCultures; +// LocaleProvider.rruleL10ns = {'en': await RruleL10nTest.create()}; +// +// // Mock the behaviour of the auth provider +// mockAuthProvider = MockAuthProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockAuthProvider.hasListeners).thenReturn(false); +// when(mockAuthProvider.isAuthenticated).thenReturn(false); +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(null)); +// when(mockAuthProvider.isAnonymous).thenReturn(true); +// when(mockAuthProvider.getProfilePictureURL()) +// .thenAnswer((_) => Future.value(null)); +// +// mockWebsiteProvider = MockWebsiteProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockWebsiteProvider.hasListeners).thenReturn(false); +// when(mockWebsiteProvider.deleteWebsite(any)) +// .thenAnswer((_) => Future.value(true)); +// when(mockWebsiteProvider.fetchWebsites(any)) +// .thenAnswer((_) => Future.value([])); +// when(mockWebsiteProvider.fetchFavouriteWebsites(mockAuthProvider.uid)) +// .thenAnswer((_) => Future.value(null)); +// +// mockFilterProvider = MockFilterProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockFilterProvider.hasListeners).thenReturn(false); +// when(mockFilterProvider.filterEnabled).thenReturn(true); +// when(mockFilterProvider.fetchFilter()) +// .thenAnswer((_) => Future.value(Filter(localizedLevelNames: [ +// {'en': 'Level', 'ro': 'Nivel'} +// ], root: FilterNode(name: 'root')))); +// +// mockPersonProvider = MockPersonProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockPersonProvider.hasListeners).thenReturn(false); +// when(mockPersonProvider.fetchPeople()).thenAnswer((_) => Future.value([])); +// +// mockQuestionProvider = MockQuestionProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockQuestionProvider.hasListeners).thenReturn(false); +// when(mockQuestionProvider.fetchQuestions()) +// .thenAnswer((_) => Future.value([])); +// when(mockQuestionProvider.fetchQuestions(limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([])); +// +// mockNewsProvider = MockNewsProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockNewsProvider.hasListeners).thenReturn(false); +// when(mockNewsProvider.fetchNewsFeedItems()) +// .thenAnswer((_) => Future.value([])); +// when(mockNewsProvider.fetchNewsFeedItems(limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([])); +// +// mockEventProvider = MockUniEventProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockEventProvider.hasListeners).thenReturn(false); +// when(mockEventProvider.getUpcomingEvents(LocalDate.today())) +// .thenAnswer((_) => Future.value([])); +// when(mockEventProvider.getUpcomingEvents(LocalDate.today(), +// limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([])); +// +// mockFeedbackProvider = MockFeedbackProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockFeedbackProvider.hasListeners).thenReturn(true); +// when(mockFeedbackProvider.userSubmittedFeedbackForClass(any, any)) +// .thenAnswer((_) => Future.value(false)); +// when(mockFeedbackProvider.getClassesWithCompletedFeedback(any)) +// .thenAnswer((_) => Future.value({'M1': true, 'M2': true})); +// +// mockClassProvider = MockClassProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockClassProvider.hasListeners).thenReturn(false); +// final userClassHeaders = [ +// ClassHeader( +// id: '3', +// name: 'Programming', +// acronym: 'PC', +// category: 'A', +// ), +// ClassHeader( +// id: '4', +// name: 'Physics', +// acronym: 'PH', +// category: 'D', +// ) +// ]; +// when(mockClassProvider.userClassHeadersCache).thenReturn(userClassHeaders); +// when(mockClassProvider.fetchClassHeaders(uid: anyNamed('uid'))) +// .thenAnswer((_) => Future.value([ +// ClassHeader( +// id: '1', +// name: 'Maths 1', +// acronym: 'M1', +// category: 'A/B', +// ), +// ClassHeader( +// id: '2', +// name: 'Maths 2', +// acronym: 'M2', +// category: 'A/C', +// ), +// ] + +// userClassHeaders)); +// when(mockClassProvider.fetchUserClassIds(any)) +// .thenAnswer((_) => Future.value(['3', '4'])); +// }); +// +// group('Login', () { +// testWidgets('Anonymous login', (WidgetTester tester) async { +// await tester.pumpWidget(MultiProvider(providers: [ +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ChangeNotifierProvider( +// create: (_) => mockEventProvider), +// ChangeNotifierProvider( +// create: (_) => mockWebsiteProvider), +// ChangeNotifierProvider( +// create: (_) => mockQuestionProvider), +// ChangeNotifierProvider(create: (_) => mockNewsProvider), +// ], child: const MyApp())); +// await tester.pumpAndSettle(); +// +// await tester.runAsync(() async { +// expect(find.byType(LoginView), findsOneWidget); +// +// when(mockAuthProvider.signInAnonymously()) +// .thenAnswer((_) => Future.value(true)); +// +// // Log in anonymously +// await tester +// .tap(find.byKey(const ValueKey('log_in_anonymously_button'))); +// await tester.pumpAndSettle(); +// +// verify(mockAuthProvider.signInAnonymously()); +// expect(find.byType(HomePage), findsOneWidget); +// +// // Easy way to check that the login page can't be navigated back to +// expect(find.byIcon(Icons.arrow_back), findsNothing); +// }); +// }); +// +// testWidgets('Credential login', (WidgetTester tester) async { +// await tester.pumpWidget(MultiProvider(providers: [ +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ChangeNotifierProvider( +// create: (_) => mockEventProvider), +// ChangeNotifierProvider( +// create: (_) => mockWebsiteProvider), +// ChangeNotifierProvider( +// create: (_) => mockQuestionProvider), +// ChangeNotifierProvider(create: (_) => mockNewsProvider), +// ], child: const MyApp())); +// await tester.pumpAndSettle(); +// +// await tester.runAsync(() async { +// expect(find.byType(LoginView), findsOneWidget); +// +// expect(find.text('@stud.acs.upb.ro'), findsOneWidget); +// +// when(mockAuthProvider.signIn(any, any)) +// .thenAnswer((_) => Future.value(true)); +// +// // Enter credentials +// await tester.enterText( +// find.byKey(const ValueKey('email_text_field')), 'test'); +// await tester.enterText( +// find.byKey(const ValueKey('password_text_field')), 'password'); +// +// await tester.tap(find.byKey(const ValueKey('log_in_button'))); +// await tester.pumpAndSettle(); +// +// verify(mockAuthProvider.signIn( +// argThat(equals('test@stud.acs.upb.ro')), +// argThat(equals('password')), +// )); +// expect(find.byType(HomePage), findsOneWidget); +// +// // Easy way to check that the login page can't be navigated back to +// expect(find.byIcon(Icons.arrow_back), findsNothing); +// }); +// }); +// }); +// +// group('Recover password', () { +// testWidgets('Send email', (WidgetTester tester) async { +// await tester.pumpWidget(ChangeNotifierProvider( +// create: (_) => mockAuthProvider, child: const MyApp())); +// await tester.pumpAndSettle(); +// +// expect(find.byType(LoginView), findsOneWidget); +// +// when(mockAuthProvider.sendPasswordResetEmail(any)) +// .thenAnswer((_) => Future.value(true)); +// +// expect(find.byType(AlertDialog), findsNothing); +// +// // Reset password +// await tester.tap(find.text('Reset password')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AlertDialog), findsOneWidget); +// +// // Send email +// await tester.enterText( +// find.byKey(const ValueKey('reset_password_email_text_field')), +// 'test'); +// +// await tester.tap(find.byKey(const ValueKey('send_email_button'))); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AlertDialog), findsNothing); +// +// verify(mockAuthProvider +// .sendPasswordResetEmail(argThat(equals('test@stud.acs.upb.ro')))); +// }); +// +// testWidgets('Cancel', (WidgetTester tester) async { +// await tester.pumpWidget(ChangeNotifierProvider( +// create: (_) => mockAuthProvider, child: const MyApp())); +// await tester.pumpAndSettle(); +// +// expect(find.byType(LoginView), findsOneWidget); +// +// when(mockAuthProvider.sendPasswordResetEmail(any)) +// .thenAnswer((_) => Future.value(true)); +// +// expect(find.byType(AlertDialog), findsNothing); +// +// // Reset password +// await tester.tap(find.text('Reset password')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AlertDialog), findsOneWidget); +// +// // Close dialog +// await tester.tap(find.byKey(const ValueKey('cancel_button'))); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AlertDialog), findsNothing); +// +// verifyNever(mockAuthProvider.sendPasswordResetEmail(any)); +// }); +// }); +// +// group('Sign up', () { +// final MockNavigatorObserver mockObserver = MockNavigatorObserver(); +// FilterProvider mockFilterProvider = MockFilterProvider(); +// +// setUp(() { +// mockFilterProvider = MockFilterProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockFilterProvider.hasListeners).thenReturn(false); +// when(mockFilterProvider.filterEnabled).thenReturn(true); +// when(mockFilterProvider.fetchFilter()) +// .thenAnswer((_) => Future.value(Filter( +// localizedLevelNames: [ +// {'en': 'Degree', 'ro': 'Nivel de studiu'}, +// {'en': 'Major', 'ro': 'Specializare'}, +// {'en': 'Year', 'ro': 'An'}, +// {'en': 'Series', 'ro': 'Serie'}, +// {'en': 'Group', 'ro': 'Group'}, +// {'en': 'Subgroup', 'ro': 'Semigrupă'} +// ], +// root: FilterNode(name: 'All', value: true, children: [ +// FilterNode(name: 'BSc', value: true, children: [ +// FilterNode(name: 'CTI', value: true, children: [ +// FilterNode( +// name: 'CTI-1', +// value: true, +// children: [ +// FilterNode(name: '1-CA'), +// FilterNode( +// name: '1-CB', +// value: true, +// children: [ +// FilterNode( +// name: '311CB', +// value: true, +// children: [ +// FilterNode(name: '311CBa'), +// FilterNode(name: '311CBb'), +// ], +// ), +// FilterNode( +// name: '312CB', +// value: true, +// children: [ +// FilterNode(name: '312CBa'), +// FilterNode(name: '312CBb'), +// ], +// ), +// FilterNode( +// name: '313CB', +// value: true, +// children: [ +// FilterNode(name: '313CBa'), +// FilterNode(name: '313CBb'), +// ], +// ), +// FilterNode( +// name: '314CB', +// value: true, +// children: [ +// FilterNode(name: '314CBa'), +// FilterNode(name: '314CBb'), +// ], +// ), +// ], +// ), +// FilterNode(name: '1-CC'), +// FilterNode(name: '1-CD', children: [ +// FilterNode( +// name: '311CD', +// value: true, +// children: [ +// FilterNode(name: '311CDa'), +// FilterNode(name: '311CDb'), +// ], +// ), +// FilterNode( +// name: '312CD', +// value: true, +// children: [ +// FilterNode(name: '312CDa'), +// FilterNode(name: '312CDb'), +// ], +// ), +// FilterNode( +// name: '313CD', +// value: true, +// children: [ +// FilterNode(name: '313CDa'), +// FilterNode(name: '313CDb'), +// ], +// ), +// FilterNode( +// name: '314CD', +// value: true, +// children: [ +// FilterNode(name: '314CDa'), +// FilterNode(name: '314CDb'), +// ], +// ), +// ]), +// ], +// ), +// FilterNode( +// name: 'CTI-2', +// ), +// FilterNode( +// name: 'CTI-3', +// ), +// FilterNode( +// name: 'CTI-4', +// ), +// ]), +// FilterNode(name: 'IS') +// ]), +// FilterNode(name: 'MSc', children: [ +// FilterNode( +// name: 'IA', +// ), +// FilterNode(name: 'SPRC'), +// ]) +// ])))); +// }); +// +// testWidgets('Sign up', (WidgetTester tester) async { +// await tester.pumpWidget(MultiProvider(providers: [ +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ChangeNotifierProvider( +// create: (_) => mockFilterProvider), +// ChangeNotifierProvider( +// create: (_) => mockWebsiteProvider), +// ChangeNotifierProvider( +// create: (_) => mockQuestionProvider), +// ChangeNotifierProvider(create: (_) => mockNewsProvider), +// ], child: MyApp(navigationObservers: [mockObserver]))); +// await tester.pumpAndSettle(); +// +// verify(mockObserver.didPush(any, any)); +// expect(find.byType(LoginView), findsOneWidget); +// +// // Scroll sign up button into view and tap +// await tester.ensureVisible(find.text('Sign up')); +// await tester.tap(find.text('Sign up')); +// await tester.pumpAndSettle(); +// +// verify(mockObserver.didPush(any, any)); +// expect(find.byType(SignUpView), findsOneWidget); +// +// when(mockAuthProvider.signUp(any)).thenAnswer((_) => Future.value(true)); +// when(mockAuthProvider.canSignUpWithEmail(any)) +// .thenAnswer((_) => Future.value(true)); +// +// // Test parser from email +// final Finder email = find.byKey(const ValueKey('email_text_field')); +// final TextField firstName = tester.widget( +// find.byKey(const ValueKey('first_name_text_field'))); +// final TextField lastName = tester.widget( +// find.byKey(const ValueKey('last_name_text_field'))); +// +// await tester.enterText(email, 'john_alexander.doe123'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john.doe'); +// expect(firstName.controller.text, equals('John')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, '1234john.doe'); +// expect(firstName.controller.text, equals('John')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john1234.doe'); +// expect(firstName.controller.text, equals('John')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john.1234doe'); +// expect(firstName.controller.text, equals('John')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john.doe1234'); +// expect(firstName.controller.text, equals('John')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, '1234john_alexander.doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john1234_alexander.doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john_1234alexander.doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john_alexander1234.doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john_alexander.1234doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, '!@#%^&*()=-+john_alexander.doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john!@#%^&*()=-+_alexander.doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john_!@#%^&*()=-+alexander.doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john_alexander!@#%^&*()=-+.doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john_alexander.!@#%^&*()=-+doe'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john_alexander.doe!@#%^&*()=-+'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, +// '!@#%^&*()=-+john!@#%^&*()=-+_!@#%^&*()=-+alexander!@#%^&*()=-+.!@#%^&*()=-+1234!@#%^&*()=-+doe!@#%^&*()=-+'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'j12o##h&n_alexand@-er.do***e'); +// expect(firstName.controller.text, equals('John Alexander')); +// expect(lastName.controller.text, equals('Doe')); +// +// await tester.enterText(email, 'john_alexander.doe1234'); +// +// /////////////////////// +// +// await tester.enterText( +// find.byKey(const ValueKey('password_text_field')), 'password'); +// await tester.enterText( +// find.byKey(const ValueKey('confirm_password_text_field')), +// 'password'); +// await tester.enterText( +// find.byKey(const ValueKey('first_name_text_field')), +// 'John Alexander'); +// await tester.enterText( +// find.byKey(const ValueKey('last_name_text_field')), 'Doe'); +// +// // TODO(AdrianMargineanu): Test dropdown buttons +// +// // Scroll sign up button into view +// await tester.ensureVisible(find.byKey(const ValueKey('sign_up_button'))); +// +// // Check Privacy Policy +// await tester.tap(find.byType(Checkbox)); +// +// // Press sign up +// await tester.tap(find.byKey(const ValueKey('sign_up_button'))); +// await tester.pumpAndSettle(); +// +// verify(mockAuthProvider.signUp(argThat(equals({ +// 'Email': 'john_alexander.doe1234@stud.acs.upb.ro', +// 'Password': 'password', +// 'Confirm password': 'password', +// 'First name': 'John Alexander', +// 'Last name': 'Doe', +// })))); +// expect(find.byType(HomePage), findsOneWidget); +// verify(mockObserver.didPush(any, any)); +// }); +// +// testWidgets('Cancel', (WidgetTester tester) async { +// await tester.pumpWidget(MultiProvider(providers: [ +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ChangeNotifierProvider( +// create: (_) => mockFilterProvider) +// ], child: MyApp(navigationObservers: [mockObserver]))); +// await tester.pumpAndSettle(); +// +// verify(mockObserver.didPush(any, any)); +// expect(find.byType(LoginView), findsOneWidget); +// +// // Scroll sign up button into view and tap +// await tester.ensureVisible(find.text('Sign up')); +// await tester.tap(find.text('Sign up')); +// await tester.pumpAndSettle(); +// +// verify(mockObserver.didPush(any, any)); +// expect(find.byType(SignUpView), findsOneWidget); +// +// when(mockAuthProvider.signUp(any)).thenAnswer((_) => Future.value(true)); +// +// // Scroll cancel button into view and tap +// await tester.ensureVisible(find.byKey(const ValueKey('cancel_button'))); +// await tester.tap(find.byKey(const ValueKey('cancel_button'))); +// await tester.pumpAndSettle(); +// +// verifyNever(mockAuthProvider.signUp(any)); +// expect(find.byType(LoginView), findsOneWidget); +// expect(find.byType(SignUpView), findsNothing); +// verify(mockObserver.didPop(any, any)); +// }); +// }); +// +// group('Sign out', () { +// final MockNavigatorObserver mockObserver = MockNavigatorObserver(); +// +// setUp(() { +// // Mock an anonymous user already being logged in +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(false)); +// }); +// +// testWidgets('Sign out anonymous', (WidgetTester tester) async { +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(null)); +// when(mockAuthProvider.currentUserFromCache).thenReturn(null); +// when(mockAuthProvider.isAnonymous).thenReturn(true); +// +// await tester.pumpWidget(MultiProvider(providers: [ +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ChangeNotifierProvider( +// create: (_) => mockFilterProvider), +// ChangeNotifierProvider( +// create: (_) => mockEventProvider), +// ChangeNotifierProvider( +// create: (_) => mockWebsiteProvider), +// ChangeNotifierProvider( +// create: (_) => mockPersonProvider), +// ChangeNotifierProvider( +// create: (_) => mockQuestionProvider), +// ChangeNotifierProvider(create: (_) => mockNewsProvider), +// ], child: MyApp(navigationObservers: [mockObserver]))); +// await tester.pumpAndSettle(); +// +// verify(mockObserver.didPush(any, any)); +// expect(find.byType(HomePage), findsOneWidget); +// +// expect(find.text('Anonymous'), findsOneWidget); +// +// // Press log in button +// await tester.tap(find.text('Log in')); +// await tester.pumpAndSettle(); +// +// verify(mockAuthProvider.signOut()); +// expect(find.byType(LoginView), findsOneWidget); +// }); +// +// testWidgets('Sign out authenticated', (WidgetTester tester) async { +// when(mockAuthProvider.currentUser).thenAnswer((_) => +// Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); +// when(mockAuthProvider.currentUserFromCache) +// .thenReturn(User(uid: '0', firstName: 'John', lastName: 'Doe')); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// +// await tester.pumpWidget(MultiProvider(providers: [ +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ChangeNotifierProvider( +// create: (_) => mockFilterProvider), +// ChangeNotifierProvider( +// create: (_) => mockWebsiteProvider), +// ChangeNotifierProvider( +// create: (_) => mockEventProvider), +// ChangeNotifierProvider( +// create: (_) => mockPersonProvider), +// ChangeNotifierProvider( +// create: (_) => mockQuestionProvider), +// ChangeNotifierProvider(create: (_) => mockNewsProvider), +// ChangeNotifierProvider( +// create: (_) => mockFeedbackProvider), +// ChangeNotifierProvider(create: (_) => mockClassProvider), +// ], child: MyApp(navigationObservers: [mockObserver]))); +// await tester.pumpAndSettle(); +// +// verify(mockObserver.didPush(any, any)); +// expect(find.byType(HomePage), findsOneWidget); +// +// expect(find.text('John Doe'), findsOneWidget); +// +// // Press log out button +// await tester.tap(find.text('Log out')); +// await tester.pumpAndSettle(); +// +// verify(mockAuthProvider.signOut()); +// expect(find.byType(LoginView), findsOneWidget); +// }); +// }); +// } diff --git a/test/integration_test.dart b/test/integration_test.dart index 4fe811def..1afa38723 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -1,1926 +1,1926 @@ -import 'package:acs_upb_mobile/authentication/model/user.dart'; -import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; -import 'package:acs_upb_mobile/authentication/view/edit_profile_page.dart'; -import 'package:acs_upb_mobile/main.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_dropdown.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_rating.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_slider.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_text.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/view/class_feedback_view.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/view/feedback_question.dart'; -import 'package:acs_upb_mobile/pages/classes/model/class.dart'; -import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; -import 'package:acs_upb_mobile/pages/classes/view/class_view.dart'; -import 'package:acs_upb_mobile/pages/classes/view/classes_page.dart'; -import 'package:acs_upb_mobile/pages/classes/view/grading_view.dart'; -import 'package:acs_upb_mobile/pages/classes/view/shortcut_view.dart'; -import 'package:acs_upb_mobile/pages/faq/model/question.dart'; -import 'package:acs_upb_mobile/pages/faq/service/question_provider.dart'; -import 'package:acs_upb_mobile/pages/faq/view/faq_page.dart'; -import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; -import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; -import 'package:acs_upb_mobile/pages/filter/view/filter_page.dart'; -import 'package:acs_upb_mobile/pages/home/home_page.dart'; -import 'package:acs_upb_mobile/pages/news_feed/model/news_feed_item.dart'; -import 'package:acs_upb_mobile/pages/news_feed/service/news_provider.dart'; -import 'package:acs_upb_mobile/pages/news_feed/view/news_feed_page.dart'; -import 'package:acs_upb_mobile/pages/people/model/person.dart'; -import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; -import 'package:acs_upb_mobile/pages/people/view/people_page.dart'; -import 'package:acs_upb_mobile/pages/people/view/person_view.dart'; -import 'package:acs_upb_mobile/pages/portal/model/website.dart'; -import 'package:acs_upb_mobile/pages/portal/service/website_provider.dart'; -import 'package:acs_upb_mobile/pages/portal/view/portal_page.dart'; -import 'package:acs_upb_mobile/pages/portal/view/website_view.dart'; -import 'package:acs_upb_mobile/pages/settings/service/request_provider.dart'; -import 'package:acs_upb_mobile/pages/settings/view/request_permissions.dart'; -import 'package:acs_upb_mobile/pages/settings/view/settings_page.dart'; -import 'package:acs_upb_mobile/pages/timetable/model/academic_calendar.dart'; -import 'package:acs_upb_mobile/pages/timetable/model/events/all_day_event.dart'; -import 'package:acs_upb_mobile/pages/timetable/model/events/class_event.dart'; -import 'package:acs_upb_mobile/pages/timetable/model/events/recurring_event.dart'; -import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; -import 'package:acs_upb_mobile/pages/timetable/view/events/add_event_view.dart'; -import 'package:acs_upb_mobile/pages/timetable/view/events/event_view.dart'; -import 'package:acs_upb_mobile/pages/timetable/view/timetable_page.dart'; -import 'package:acs_upb_mobile/resources/locale_provider.dart'; -import 'package:acs_upb_mobile/resources/remote_config.dart'; -import 'package:acs_upb_mobile/resources/utils.dart'; -import 'package:acs_upb_mobile/widgets/search_bar.dart'; -import 'package:firebase_core/firebase_core.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_feather_icons/flutter_feather_icons.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:network_image_mock/network_image_mock.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:preferences/preferences.dart'; -import 'package:provider/provider.dart'; -import 'package:rrule/rrule.dart'; -import 'package:time_machine/time_machine.dart' hide Offset; -import 'package:timetable/src/header/week_indicator.dart'; - -import 'firebase_mock.dart'; -import 'test_utils.dart'; - -// These tests open each page in the app on multiple screen sizes to make sure -// nothing overflows/breaks. - -class MockAuthProvider extends Mock implements AuthProvider {} - -class MockWebsiteProvider extends Mock implements WebsiteProvider {} - -class MockFilterProvider extends Mock implements FilterProvider {} - -class MockClassProvider extends Mock implements ClassProvider {} - -class MockPersonProvider extends Mock implements PersonProvider {} - -class MockQuestionProvider extends Mock implements QuestionProvider {} - -class MockUniEventProvider extends Mock implements UniEventProvider {} - -class MockNewsProvider extends Mock implements NewsProvider {} - -class MockRequestProvider extends Mock implements RequestProvider {} - -class MockNavigatorObserver extends Mock implements NavigatorObserver {} - -class MockFeedbackProvider extends Mock implements FeedbackProvider {} - -Future main() async { - AuthProvider mockAuthProvider; - WebsiteProvider mockWebsiteProvider; - FilterProvider mockFilterProvider; - ClassProvider mockClassProvider; - PersonProvider mockPersonProvider; - MockQuestionProvider mockQuestionProvider; - MockNewsProvider mockNewsProvider; - UniEventProvider mockEventProvider; - RequestProvider mockRequestProvider; - FeedbackProvider mockFeedbackProvider; - - setupFirebaseAuthMocks(); - await Firebase.initializeApp(); - - // Test layout for different screen sizes - // TODO(AdrianMargineanu): Use Flutter driver for integration tests, setting screen sizes here isn't reliable - final screenSizes = [ - // Phone - const Size(720, 1280), - // Tablet - const Size(600, 1024), - ]; - - // Add landscape mode sizes - screenSizes.addAll(List.from(screenSizes) - .map((size) => Size(size.height, size.width))); - - final TestWidgetsFlutterBinding binding = - TestWidgetsFlutterBinding.ensureInitialized(); - - Widget buildApp() => MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ChangeNotifierProvider( - create: (_) => mockWebsiteProvider), - ChangeNotifierProvider( - create: (_) => mockFilterProvider), - ChangeNotifierProvider( - create: (_) => mockClassProvider), - ChangeNotifierProvider( - create: (_) => mockPersonProvider), - ChangeNotifierProvider( - create: (_) => mockQuestionProvider), - ChangeNotifierProvider(create: (_) => mockNewsProvider), - ChangeNotifierProvider( - create: (_) => mockEventProvider), - Provider(create: (_) => mockRequestProvider), - ChangeNotifierProvider( - create: (_) => mockFeedbackProvider), - ], - child: const MyApp(), - ); - - setUp(() async { - WidgetsFlutterBinding.ensureInitialized(); - PrefService.enableCaching(); - PrefService.cache = {}; - PrefService.setString('language', 'en'); - - await Firebase.initializeApp(); - - LocaleProvider.cultures = testCultures; - LocaleProvider.rruleL10ns = {'en': await RruleL10nTest.create()}; - - Utils.packageInfo = PackageInfo( - version: '1.2.7', - buildNumber: '6', - appName: 'ACS UPB Mobile', - packageName: 'ro.upb.acs_upb_mobile', - ); - - // Pretend an anonymous user is already logged in - mockAuthProvider = MockAuthProvider(); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - // ignore: invalid_use_of_protected_member - when(mockAuthProvider.hasListeners).thenReturn(false); - when(mockAuthProvider.isAnonymous).thenReturn(true); - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(null)); - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(false)); - - mockWebsiteProvider = MockWebsiteProvider(); - // ignore: invalid_use_of_protected_member - when(mockWebsiteProvider.hasListeners).thenReturn(false); - when(mockWebsiteProvider.deleteWebsite(any)) - .thenAnswer((_) => Future.value(true)); - when(mockAuthProvider.getProfilePictureURL()) - .thenAnswer((_) => Future.value(null)); - when(mockWebsiteProvider.fetchWebsites(any)) - .thenAnswer((_) => Future.value([ - Website( - id: '1', - relevance: null, - category: WebsiteCategory.learning, - infoByLocale: {'en': 'info-en', 'ro': 'info-ro'}, - label: 'Moodle1', - link: 'http://acs.curs.pub.ro/', - isPrivate: false, - ), - Website( - id: '2', - relevance: null, - category: WebsiteCategory.learning, - infoByLocale: {}, - label: 'OCW1', - link: 'https://ocw.cs.pub.ro/', - isPrivate: false, - ), - Website( - id: '3', - relevance: null, - category: WebsiteCategory.learning, - infoByLocale: {'en': 'info-en', 'ro': 'info-ro'}, - label: 'Moodle2', - link: 'http://acs.curs.pub.ro/', - isPrivate: false, - ), - Website( - id: '4', - relevance: null, - category: WebsiteCategory.learning, - infoByLocale: {}, - label: 'OCW2', - link: 'https://ocw.cs.pub.ro/', - isPrivate: false, - ), - Website( - id: '5', - relevance: null, - category: WebsiteCategory.association, - infoByLocale: {}, - label: 'LSAC1', - link: 'https://lsacbucuresti.ro/', - isPrivate: false, - ), - Website( - id: '6', - relevance: null, - category: WebsiteCategory.administrative, - infoByLocale: {}, - label: 'LSAC2', - link: 'https://lsacbucuresti.ro/', - isPrivate: false, - ), - Website( - id: '7', - relevance: null, - category: WebsiteCategory.resource, - infoByLocale: {}, - label: 'LSAC3', - link: 'https://lsacbucuresti.ro/', - isPrivate: false, - ), - Website( - id: '8', - relevance: null, - category: WebsiteCategory.other, - infoByLocale: {}, - label: 'LSAC4', - link: 'https://lsacbucuresti.ro/', - isPrivate: false, - ), - ])); - when(mockWebsiteProvider.fetchFavouriteWebsites(any)).thenAnswer( - (_) async => (await mockWebsiteProvider.fetchWebsites(any)).take(3)); - - mockFilterProvider = MockFilterProvider(); - // ignore: invalid_use_of_protected_member - when(mockFilterProvider.hasListeners).thenReturn(false); - when(mockFilterProvider.filterEnabled).thenReturn(true); - final filter = Filter( - localizedLevelNames: [ - {'en': 'Degree', 'ro': 'Nivel de studiu'}, - {'en': 'Major', 'ro': 'Specializare'}, - {'en': 'Year', 'ro': 'An'}, - {'en': 'Series', 'ro': 'Serie'}, - {'en': 'Group', 'ro': 'Group'} - ], - root: FilterNode(name: 'All', value: true, children: [ - FilterNode(name: 'BSc', value: true, children: [ - FilterNode(name: 'CTI', value: true, children: [ - FilterNode(name: 'CTI-1', value: true, children: [ - FilterNode(name: '1-CA'), - FilterNode( - name: '1-CB', - value: true, - children: [ - FilterNode(name: '311CB'), - FilterNode(name: '312CB'), - FilterNode(name: '313CB'), - FilterNode( - name: '314CB', - value: true, - ), - ], - ), - FilterNode(name: '1-CC'), - FilterNode( - name: '1-CD', - children: [ - FilterNode(name: '311CD'), - FilterNode(name: '312CD'), - FilterNode(name: '313CD'), - FilterNode(name: '314CD'), - ], - ), - ]), - FilterNode( - name: 'CTI-2', - ), - FilterNode( - name: 'CTI-3', - ), - FilterNode( - name: 'CTI-4', - ), - ]), - FilterNode(name: 'IS') - ]), - FilterNode(name: 'MSc', children: [ - FilterNode( - name: 'IA', - ), - FilterNode(name: 'SPRC'), - ]) - ])); - when(mockFilterProvider.cachedFilter).thenReturn(filter); - when(mockFilterProvider.fetchFilter()) - .thenAnswer((_) => Future.value(filter)); - - mockClassProvider = MockClassProvider(); - // ignore: invalid_use_of_protected_member - when(mockClassProvider.hasListeners).thenReturn(false); - final userClassHeaders = [ - ClassHeader( - id: '3', - name: 'Programming', - acronym: 'PC', - category: 'A', - ), - ClassHeader( - id: '4', - name: 'Physics', - acronym: 'PH', - category: 'D', - ) - ]; - when(mockClassProvider.userClassHeadersCache).thenReturn(userClassHeaders); - when(mockClassProvider.fetchClassHeaders(uid: anyNamed('uid'))) - .thenAnswer((_) => Future.value([ - ClassHeader( - id: '1', - name: 'Maths 1', - acronym: 'M1', - category: 'A/B', - ), - ClassHeader( - id: '2', - name: 'Maths 2', - acronym: 'M2', - category: 'A/C', - ), - ] + - userClassHeaders)); - when(mockClassProvider.fetchUserClassIds(any)) - .thenAnswer((_) => Future.value(['3', '4'])); - when(mockClassProvider.fetchClassInfo(any)).thenAnswer((_) => Future.value( - Class( - header: ClassHeader( - id: '3', - name: 'Programming', - acronym: 'PC', - category: 'A', - ), - shortcuts: [ - Shortcut( - type: ShortcutType.main, - name: 'OCW', - link: 'https://ocw.cs.pub.ro/courses/programare'), - Shortcut( - type: ShortcutType.other, - name: 'Google', - link: 'https://google.com'), - ], - grading: { - 'Exam': 4, - 'Lab': 1.5, - 'Homework': 4, - 'Extra homework': 0.5, - }, - ), - )); - - RemoteConfigService.overrides = {'feedback_enabled': true}; - - mockPersonProvider = MockPersonProvider(); - // ignore: invalid_use_of_protected_member - when(mockPersonProvider.hasListeners).thenReturn(false); - when(mockPersonProvider.fetchPeople()).thenAnswer((_) => Future.value([ - Person( - name: 'John Doe', - email: 'john.doe@cs.pub.ro', - phone: '0712345678', - office: 'AB123', - position: 'Associate Professor, Dr., Department Council', - photo: 'https://cdn.worldvectorlogo.com/logos/flutter-logo.svg', - ), - Person( - name: 'Jane Doe', - email: 'jane.doe@cs.pub.ro', - phone: '-', - office: 'Narnia', - position: 'Professor, Dr.', - photo: 'https://cdn.worldvectorlogo.com/logos/flutter-logo.svg', - ), - Person( - name: 'Mary Poppins', - email: 'supercalifragilistic.expialidocious@cs.pub.ro', - phone: '0712-345-678', - office: 'Mary Poppins\' office', - position: 'Professor, Dr., Head of Department', - photo: 'https://cdn.worldvectorlogo.com/logos/flutter-logo.svg', - ), - ])); - - when(mockPersonProvider.mostRecentLecturer(any)) - .thenAnswer((_) => Future.value('Jane Doe')); - - mockFeedbackProvider = MockFeedbackProvider(); - // ignore: invalid_use_of_protected_member - when(mockFeedbackProvider.hasListeners).thenReturn(true); - when(mockFeedbackProvider.fetchQuestions()).thenAnswer((_) => Future.value({ - '0': FeedbackQuestionDropdown( - category: 'involvement', - question: - 'Approximate number of activities that you attended (lectures + applications):', - id: '0', - answerOptions: ['option 1', 'option 2', 'option 3', 'option 4'], - ), - '1': FeedbackQuestionRating( - category: 'applications', - question: 'Was the exposure method appropriate?', - id: '1', - ), - '2': FeedbackQuestionText( - category: 'personal', - question: 'What are the positive aspects of this class?', - id: '2', - ), - '3': FeedbackQuestionSlider( - category: 'homework', - question: - 'Estimate the average number of hours per week devoted to solving homework.', - id: '3', - ), - })); - when(mockFeedbackProvider.fetchCategories()) - .thenAnswer((_) => Future.value({ - 'applications': {'en': 'Applications', 'ro': 'Aplicații'}, - 'homework': {'en': 'Homework', 'ro': 'Temă'}, - 'involvement': {'en': 'Involvement', 'ro': 'Implicare'}, - 'personal': { - 'en': 'Personal comments', - 'ro': 'Comentarii personale' - }, - })); - - when(mockFeedbackProvider.userSubmittedFeedbackForClass(any, any)) - .thenAnswer((_) => Future.value(false)); - when(mockFeedbackProvider.submitFeedback(any, any, any, any, any)) - .thenAnswer((_) => Future.value(true)); - when(mockFeedbackProvider.getClassesWithCompletedFeedback(any)) - .thenAnswer((_) => Future.value({'M1': true, 'M2': true})); - when(mockFeedbackProvider.countClassesWithoutFeedback(any, any)) - .thenAnswer((_) => Future.value('2')); - - mockQuestionProvider = MockQuestionProvider(); - // ignore: invalid_use_of_protected_member - when(mockQuestionProvider.hasListeners).thenReturn(false); - when(mockQuestionProvider.fetchQuestions()) - .thenAnswer((_) => Future.value([ - Question( - question: 'Care este programul la secretariat?', - answer: - 'Secretariatul este deschis în timpul săptămânii între orele 9:00 si 11:00.', - tags: ['Licență']), - Question( - question: 'Cum mă conectez la eduroam?', - answer: - 'Conectarea în rețeaua *eduroam* se face pe baza aceluiași cont folosit și pe site-ul de cursuri.', - tags: ['Conectare', 'Informații']) - ])); - when(mockQuestionProvider.fetchQuestions(limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([ - Question( - question: 'Care este programul la secretariat?', - answer: - 'Secretariatul este deschis în timpul săptămânii între orele 9:00 si 11:00.', - tags: ['Licență']), - Question( - question: 'Cum mă conectez la eduroam?', - answer: - 'Conectarea în rețeaua *eduroam* se face pe baza aceluiași cont folosit și pe site-ul de cursuri.', - tags: ['Conectare', 'Informații']) - ])); - - mockNewsProvider = MockNewsProvider(); - // ignore: invalid_use_of_protected_member - when(mockNewsProvider.hasListeners).thenReturn(false); - when(mockNewsProvider.fetchNewsFeedItems()) - .thenAnswer((_) => Future.value([ - NewsFeedItem( - '03.10.2020', - 'Cazarea studentilor de anul II licenta', - 'https://acs.pub.ro/noutati/cazarea-studentilor-de-anul-ii-licenta/'), - NewsFeedItem( - '03.10.2020', - 'Festivitatea de deschidere a anului universitar 2020-2021', - 'https://acs.pub.ro/noutati/festivitatea-de-deschidere-a-anului-universitar-2020-2021/') - ])); - when(mockNewsProvider.fetchNewsFeedItems(limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([ - NewsFeedItem( - '03.10.2020', - 'Cazarea studentilor de anul II licenta', - 'https://acs.pub.ro/noutati/cazarea-studentilor-de-anul-ii-licenta/'), - NewsFeedItem( - '03.10.2020', - 'Festivitatea de deschidere a anului universitar 2020-2021', - 'https://acs.pub.ro/noutati/festivitatea-de-deschidere-a-anului-universitar-2020-2021/') - ])); - - mockEventProvider = MockUniEventProvider(); - // ignore: invalid_use_of_protected_member - when(mockEventProvider.hasListeners).thenReturn(false); - final now = LocalDate.today(); - final weekStart = now.subtractDays(now.dayOfWeek - DayOfWeek.monday); - final holidays = [ - // Holiday on Tuesday and Wednesday next week - AllDayUniEvent( - name: 'Holiday', - start: weekStart.addWeeks(1).addDays(1), - end: weekStart.addWeeks(1).addDays(2), - id: 'holiday0', - ), - AllDayUniEvent( - name: 'Inter-semester holiday', - start: weekStart.addWeeks(2).subtractDays(2), - end: weekStart.addWeeks(3).subtractDays(1), - id: 'holiday1', - ), - ]; - final calendar = AcademicCalendar( - id: '2020', - semesters: [ - AllDayUniEvent( - start: weekStart, - end: weekStart.addWeeks(2).subtractDays(3), - id: 'semester1', - ), - AllDayUniEvent( - start: weekStart.addWeeks(3), - end: weekStart.addWeeks(5).subtractDays(3), - id: 'semester2', - ), - ], - holidays: holidays, - ); - when(mockEventProvider.fetchCalendars()) - .thenAnswer((_) => Future.value({'2020': calendar})); - when(mockEventProvider.getUpcomingEvents(LocalDate.today())) - .thenAnswer((_) => Future.value([])); - when(mockEventProvider.getUpcomingEvents(LocalDate.today(), - limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([])); - when(mockEventProvider.getAllEventsOfClass(any)) - .thenAnswer((_) => Future.value([])); - final rruleEveryWeekFirstSem = RecurrenceRule( - frequency: Frequency.weekly, - interval: 1, - until: weekStart.addWeeks(2).subtractDays(3).atMidnight(), - ); - final rruleEveryTwoWeeksFirstSem = RecurrenceRule( - frequency: Frequency.weekly, - interval: 2, - until: weekStart.addWeeks(2).subtractDays(3).atMidnight(), - ); - final rruleEveryWeek = RecurrenceRule( - frequency: Frequency.weekly, - interval: 1, - ); - final rruleEveryTwoWeeks = RecurrenceRule( - frequency: Frequency.weekly, - interval: 2, - ); - const duration = Period(hours: 2); - final events = [ - RecurringUniEvent( - name: 'M1', - calendar: calendar, - rrule: rruleEveryWeekFirstSem, - start: weekStart.at(LocalTime(8, 0, 0)), - duration: duration, - id: '0', - ), - RecurringUniEvent( - name: 'M2', - calendar: calendar, - rrule: rruleEveryTwoWeeksFirstSem, - start: weekStart.at(LocalTime(10, 0, 0)), - duration: duration, - id: '1', - ), - RecurringUniEvent( - name: 'T1', - calendar: calendar, - rrule: rruleEveryWeekFirstSem, - start: weekStart.addDays(1).at(LocalTime(8, 0, 0)), - duration: duration, - id: '2', - ), - RecurringUniEvent( - name: 'T2', - calendar: calendar, - rrule: rruleEveryTwoWeeksFirstSem, - start: weekStart.addDays(1).at(LocalTime(9, 0, 0)), - duration: duration, - id: '3', - ), - RecurringUniEvent( - name: 'W1', - calendar: calendar, - rrule: rruleEveryWeekFirstSem, - start: weekStart.addDays(2).at(LocalTime(8, 0, 0)), - duration: duration, - id: '4', - ), - RecurringUniEvent( - name: 'W2', - rrule: rruleEveryWeek, - start: weekStart.addDays(2).at(LocalTime(10, 0, 0)), - duration: duration, - id: '5', - ), - RecurringUniEvent( - name: 'W3', - rrule: rruleEveryTwoWeeks, - start: weekStart.addDays(2).at(LocalTime(12, 0, 0)), - duration: duration, - id: '6', - ), - ClassEvent( - classHeader: userClassHeaders[0], - type: UniEventType.lecture, - teacher: Person(name: 'Jane Doe'), - location: 'AB123', - degree: 'BSc', - relevance: ['314CB'], - calendar: calendar, - rrule: rruleEveryWeek, - start: weekStart.addDays(3).at(LocalTime(10, 0, 0)), - duration: duration, - id: '7', - ), - RecurringUniEvent( - classHeader: userClassHeaders[1], - type: UniEventType.lecture, - location: 'AB123', - degree: 'BSc', - relevance: ['314CB'], - calendar: calendar, - rrule: rruleEveryTwoWeeks, - start: weekStart.addDays(3).at(LocalTime(12, 0, 0)), - duration: duration, - id: '8', - ), - RecurringUniEvent( - name: 'F1', - calendar: calendar, - rrule: rruleEveryWeek, - start: weekStart.addDays(4).at(LocalTime(10, 0, 0)), - duration: duration, - id: '9', - ), - RecurringUniEvent( - name: 'F2', - calendar: calendar, - rrule: rruleEveryTwoWeeks, - start: weekStart.addDays(4).at(LocalTime(12, 0, 0)), - duration: duration, - id: '10', - ), - ]; - when(mockEventProvider.getAllDayEventsIntersecting(any)) - .thenAnswer((invocation) { - final DateInterval interval = invocation.positionalArguments[0]; - return Stream.value(holidays - .map((holiday) => - holiday.generateInstances(intersectingInterval: interval)) - .expand((e) => e)); - }); - when(mockEventProvider.getPartDayEventsIntersecting(any)) - .thenAnswer((invocation) { - final LocalDate date = invocation.positionalArguments[0]; - return Stream.value(events - .map((event) => event.generateInstances( - intersectingInterval: DateInterval(date, date))) - .expand((e) => e)); - }); - when(mockEventProvider.empty).thenReturn(false); - when(mockEventProvider.deleteEvent(any)) - .thenAnswer((_) => Future.value(true)); - when(mockEventProvider.updateEvent(any)) - .thenAnswer((_) => Future.value(true)); - when(mockEventProvider.addEvent(any)).thenAnswer((_) => Future.value(true)); - - mockRequestProvider = MockRequestProvider(); - when(mockRequestProvider.makeRequest(any)) - .thenAnswer((_) => Future.value(true)); - when(mockRequestProvider.userAlreadyRequested(any)) - .thenAnswer((_) => Future.value(false)); - }); - - group('Home', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - expect(find.byType(HomePage), findsOneWidget); - - // Open home - await tester.tap(find.byIcon(Icons.home)); - await tester.pumpAndSettle(); - - expect(find.byType(HomePage), findsOneWidget); - }); - } - }); - - group('Timetable', () { - setUp(() { - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( - uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); - when(mockAuthProvider.currentUserFromCache).thenReturn(User( - uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3)); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.uid).thenReturn('0'); - }); - - group('Timetable no events/no classes', () { - setUp(() { - when(mockEventProvider.getAllDayEventsIntersecting(any)) - .thenAnswer((_) => Stream.value([])); - when(mockEventProvider.getPartDayEventsIntersecting(any)) - .thenAnswer((_) => Stream.value([])); - when(mockEventProvider.empty).thenReturn(true); - - when(mockClassProvider.userClassHeadersCache).thenReturn(null); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(TimetablePage), findsOneWidget); - expect(find.text('No events to show'), findsOneWidget); - - await tester.tap(find.text('CHOOSE CLASSES')); - await tester.pumpAndSettle(); - - expect(find.byType(AddClassesPage), findsOneWidget); - }); - } - }); - - group('Timetable no events/no filter', () { - setUp(() { - when(mockEventProvider.getAllDayEventsIntersecting(any)) - .thenAnswer((_) => Stream.value([])); - when(mockEventProvider.getPartDayEventsIntersecting(any)) - .thenAnswer((_) => Stream.value([])); - when(mockEventProvider.empty).thenReturn(true); - - when(mockFilterProvider.cachedFilter).thenReturn(null); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(TimetablePage), findsOneWidget); - expect(find.text('No events to show'), findsOneWidget); - - await tester.tap(find.text('OPEN FILTER')); - await tester.pumpAndSettle(); - - expect(find.byType(FilterPage), findsOneWidget); - }); - } - }); - - group('Timetable no events/no permissions', () { - setUp(() { - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( - uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 0))); - when(mockAuthProvider.currentUserFromCache).thenReturn(User( - uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 0)); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); - - when(mockEventProvider.getAllDayEventsIntersecting(any)) - .thenAnswer((_) => Stream.value([])); - when(mockEventProvider.getPartDayEventsIntersecting(any)) - .thenAnswer((_) => Stream.value([])); - when(mockEventProvider.empty).thenReturn(true); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(TimetablePage), findsOneWidget); - expect(find.text('No events to show'), findsOneWidget); - - await tester.tap(find.text('REQUEST PERMISSIONS')); - await tester.pumpAndSettle(); - - expect(find.byType(RequestPermissionsPage), findsOneWidget); - }); - } - }); - - group('Timetable no events/add some', () { - setUp(() { - when(mockEventProvider.getAllDayEventsIntersecting(any)) - .thenAnswer((_) => Stream.value([])); - when(mockEventProvider.getPartDayEventsIntersecting(any)) - .thenAnswer((_) => Stream.value([])); - when(mockEventProvider.empty).thenReturn(true); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(TimetablePage), findsOneWidget); - expect(find.text('No events to show'), findsOneWidget); - expect( - find.byKey(const ValueKey('no_events_message')), findsOneWidget); - - await tester.tap(find.text('CANCEL')); - await tester.pumpAndSettle(); - - expect(find.text('No events to show'), findsNothing); - }); - } - }); - - group('Timetable events', () { - for (final size in screenSizes) { - if (size.width > size.height) { - // TODO(IoanaAlexandru): In landscape mode the test fails in a weird - // way - it seems as if two weeks are visible at the same time, but - // the behaviour cannot be reproduced on a device. Skipping this - // test in landscape mode for now. - continue; - } - - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(TimetablePage), findsOneWidget); - - // Scroll to previous week - await tester.drag(find.text('Tue'), Offset(size.width - 30, 0)); - await tester.pumpAndSettle(); - - // Expect previous week - final previousWeek = WeekYearRules.iso - .getWeekOfWeekYear(LocalDate.today().subtractWeeks(1)); - expect( - find.byWidgetPredicate((widget) => - widget is WeekIndicator && - widget.week.toString() == previousWeek.toString()), - findsOneWidget); - - expect(find.text('Holiday'), findsNothing); - expect(find.text('Inter-semester holiday'), findsNothing); - expect(find.text('M1'), findsNothing); - expect(find.text('M2'), findsNothing); - expect(find.text('T1'), findsNothing); - expect(find.text('T2'), findsNothing); - expect(find.text('W1'), findsNothing); - expect(find.text('W2'), findsNothing); - expect(find.text('W3'), findsNothing); - expect(find.text('PC'), findsNothing); - expect(find.text('PH'), findsNothing); - expect(find.text('F1'), findsNothing); - expect(find.text('F2'), findsNothing); - - // Scroll back to current week - await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); - await tester.pumpAndSettle(); - - // Expect current week - final currentWeek = - WeekYearRules.iso.getWeekOfWeekYear(LocalDate.today()); - expect( - find.byWidgetPredicate((widget) => - widget is WeekIndicator && - widget.week.toString() == currentWeek.toString()), - findsOneWidget); - - expect(find.text('Holiday'), findsNothing); - expect(find.text('Inter-semester holiday'), findsNothing); - expect(find.text('M1'), findsOneWidget); - expect(find.text('M2'), findsOneWidget); - expect(find.text('T1'), findsOneWidget); - expect(find.text('T2'), findsOneWidget); - expect(find.text('W1'), findsOneWidget); - expect(find.text('W2'), findsOneWidget); - expect(find.text('W3'), findsOneWidget); - expect(find.text('PC'), findsOneWidget); - expect(find.text('PH'), findsOneWidget); - expect(find.text('F1'), findsOneWidget); - expect(find.text('F2'), findsOneWidget); - - // Scroll to next week - await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); - await tester.pumpAndSettle(); - - // Expect next week - final nextWeek = WeekYearRules.iso - .getWeekOfWeekYear(LocalDate.today().addWeeks(1)); - expect( - find.byWidgetPredicate((widget) => - widget is WeekIndicator && - widget.week.toString() == nextWeek.toString()), - findsOneWidget); - - expect(find.text('Holiday'), findsOneWidget); - expect(find.text('Inter-semester holiday'), findsOneWidget); - expect(find.text('M1'), findsOneWidget); - expect(find.text('M2'), findsNothing); - expect(find.text('T1'), findsNothing); - expect(find.text('T2'), findsNothing); - expect(find.text('W1'), findsNothing); - expect(find.text('W2'), findsOneWidget); - expect(find.text('W3'), findsNothing); - expect(find.text('PC'), findsOneWidget); - expect(find.text('PH'), findsNothing); - expect(find.text('F1'), findsOneWidget); - expect(find.text('F2'), findsNothing); - - // Scroll to next week - await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); - await tester.pumpAndSettle(); - - // Expect next week - final nextNextWeek = WeekYearRules.iso - .getWeekOfWeekYear(LocalDate.today().addWeeks(2)); - expect( - find.byWidgetPredicate((widget) => - widget is WeekIndicator && - widget.week.toString() == nextNextWeek.toString()), - findsOneWidget); - - expect(find.text('Holiday'), findsNothing); - expect(find.text('Inter-semester holiday'), findsOneWidget); - expect(find.text('M1'), findsNothing); - expect(find.text('M2'), findsNothing); - expect(find.text('T1'), findsNothing); - expect(find.text('T2'), findsNothing); - expect(find.text('W1'), findsNothing); - expect(find.text('W2'), findsOneWidget); - expect(find.text('W3'), findsOneWidget); - expect(find.text('PC'), findsNothing); - expect(find.text('PH'), findsNothing); - expect(find.text('F1'), findsNothing); - expect(find.text('F2'), findsNothing); - - // Scroll to next week - await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); - await tester.pumpAndSettle(); - - // Expect next week - final nextNextNextWeek = WeekYearRules.iso - .getWeekOfWeekYear(LocalDate.today().addWeeks(3)); - expect( - find.byWidgetPredicate((widget) => - widget is WeekIndicator && - widget.week.toString() == nextNextNextWeek.toString()), - findsOneWidget); - - expect(find.text('Holiday'), findsNothing); - expect(find.text('Inter-semester holiday'), findsNothing); - expect(find.text('M1'), findsNothing); - expect(find.text('M2'), findsNothing); - expect(find.text('T1'), findsNothing); - expect(find.text('T2'), findsNothing); - expect(find.text('W1'), findsNothing); - expect(find.text('W2'), findsOneWidget); - expect(find.text('W3'), findsNothing); - expect(find.text('PC'), findsOneWidget); - expect(find.text('PH'), findsOneWidget); - expect(find.text('F1'), findsOneWidget); - expect(find.text('F2'), findsOneWidget); - - // Navigate to today - await tester.tap(find.byIcon(Icons.today_outlined)); - await tester.pumpAndSettle(); - - // Expect current week - expect( - find.byWidgetPredicate((widget) => - widget is WeekIndicator && - widget.week.toString() == currentWeek.toString()), - findsOneWidget); - }); - } - }); - - group('Event page - open all day event', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - // Expect current week - final currentWeek = - WeekYearRules.iso.getWeekOfWeekYear(LocalDate.today()); - expect( - find.byWidgetPredicate((widget) => - widget is WeekIndicator && - widget.week.toString() == currentWeek.toString()), - findsOneWidget); - - // Scroll to next week - await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); - await tester.pumpAndSettle(); - - // Expect next week - final nextWeek = WeekYearRules.iso - .getWeekOfWeekYear(LocalDate.today().addWeeks(1)); - expect( - find.byWidgetPredicate((widget) => - widget is WeekIndicator && - widget.week.toString() == nextWeek.toString()), - findsOneWidget); - - // Open holiday event - await tester.tap(find.text('Holiday')); - await tester.pumpAndSettle(); - - expect(find.byType(EventView), findsOneWidget); - }); - } - }); - - group('Event page - add event', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - // Open add event page - await tester - .tapAt(tester.getCenter(find.text('Sat')).translate(0, 100)); - await tester.pumpAndSettle(); - - expect(find.byType(AddEventView), findsOneWidget); - - // Select type - await tester.tap(find.text('Type')); - await tester.pumpAndSettle(); - await tester.tap(find.text('Seminar').last); - await tester.pumpAndSettle(); - - // Select class - await tester.tap(find.text('Class')); - await tester.pumpAndSettle(); - await tester.tap(find.text('Programming').last); - await tester.pumpAndSettle(); - - // Press back - await tester.tap(find.byIcon(Icons.arrow_back)); - await tester.pumpAndSettle(); - - await tester - .tapAt(tester.getCenter(find.text('Sat')).translate(0, 100)); - await tester.pumpAndSettle(); - - expect(find.byType(AddEventView), findsOneWidget); - - // Select type - await tester.tap(find.text('Type')); - await tester.pumpAndSettle(); - await tester.tap(find.text('Lecture').last); - await tester.pumpAndSettle(); - - // Select lecturer - partial name - await tester.tap(find.byIcon(FeatherIcons.user)); - await tester.pumpAndSettle(); - await tester.enterText( - find.byKey(const Key('AutocompleteLecturer')), 'John'); - await tester.pumpAndSettle(); - await tester.tap(find.text('John Doe')); - await tester.pumpAndSettle(); - - // Select lecturer - new name - await tester.tap(find.byIcon(FeatherIcons.user)); - await tester.pumpAndSettle(); - await tester.enterText( - find.byKey(const Key('AutocompleteLecturer')), 'Isabel Steward'); - await tester.tap(find.text('Isabel Steward')); - await tester.pumpAndSettle(); - - // Select lecturer - check autocomplete suggestions - await tester.tap(find.byIcon(FeatherIcons.user)); - await tester.pumpAndSettle(); - await tester.enterText( - find.byKey(const Key('AutocompleteLecturer')), 'Doe'); - await tester.pumpAndSettle(); - - expect(find.text('Jane Doe'), findsOneWidget); - expect(find.text('John Doe'), findsOneWidget); - - await tester.tap(find.text('Jane Doe')); - await tester.pumpAndSettle(); - }); - } - }); - - group('Event page - edit event', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - // Open PC event - await tester.tap(find.text('PC')); - await tester.pumpAndSettle(); - - expect(find.byType(EventView), findsOneWidget); - - // Open class page - await tester.tap(find.text('Programming')); - await tester.pumpAndSettle(); - - expect(find.byType(ClassView), findsOneWidget); - expect(find.byIcon(FeatherIcons.user), findsOneWidget); - expect(find.byKey(const Key('LecturerCard')), findsOneWidget); - - // Press back - await tester.tap(find.byIcon(Icons.arrow_back)); - await tester.pumpAndSettle(); - - expect(find.byType(EventView), findsOneWidget); - expect(find.byIcon(FeatherIcons.user), findsOneWidget); - expect(find.text('Jane Doe'), findsOneWidget); - - await tester.tap(find.byIcon(FeatherIcons.user)); - await tester.pumpAndSettle(); - - expect(find.byType(PersonView), findsOneWidget); - - // Press back - await tester.tap(find.byIcon(Icons.arrow_back)); - await tester.pumpAndSettle(); - - // Open edit event page - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(AddEventView), findsOneWidget); - expect(find.text('Lecturer'), findsOneWidget); - expect(find.text('Location'), findsOneWidget); - expect(find.text('Week'), findsOneWidget); - expect(find.text('Day'), findsOneWidget); - - // Select lecturer - await tester.tap(find.text('Lecturer')); - await tester.pumpAndSettle(); - await tester.enterText( - find.byKey(const Key('AutocompleteLecturer')), 'Doe'); - await tester.pumpAndSettle(); - - expect(find.text('Jane Doe'), findsOneWidget); - expect(find.text('John Doe'), findsOneWidget); - - await tester.enterText( - find.byKey(const Key('AutocompleteLecturer')), 'John Doe'); - await tester.pumpAndSettle(); - - FocusManager.instance.primaryFocus.unfocus(); - await tester.pumpAndSettle(); - await tester.tap(find.text('John Doe').last); - await tester.pumpAndSettle(); - - FocusManager.instance.primaryFocus.unfocus(); - await tester.pumpAndSettle(); - - expect(find.text('Jane Doe'), findsNothing); - expect(find.text('John Doe'), findsOneWidget); - - // Press save - await tester.tap(find.text('Save')); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - expect(find.byType(TimetablePage), findsOneWidget); - - // Open PC event - await tester.tap(find.text('PC')); - await tester.pumpAndSettle(); - - expect(find.byType(EventView), findsOneWidget); - expect(find.byIcon(FeatherIcons.user), findsOneWidget); - - // Press back - await tester.tap(find.byIcon(Icons.arrow_back)); - await tester.pumpAndSettle(); - - expect(find.byType(TimetablePage), findsOneWidget); - }); - } - }); - - group('Event page - delete event', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - // Open PH event - await tester.tap(find.text('PH')); - await tester.pumpAndSettle(); - - expect(find.byType(EventView), findsOneWidget); - - // Open edit event page - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(AddEventView), findsOneWidget); - - // Open delete dialog - await tester.tap(find.byIcon(Icons.more_vert_outlined)); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Delete event')); - await tester.pumpAndSettle(); - - // Confirm deletion - expect(find.text('Are you sure you want to delete this event?'), - findsOneWidget); - await tester.tap(find.text('DELETE EVENT')); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - verify(mockEventProvider.deleteEvent(any)); - expect(find.byType(TimetablePage), findsOneWidget); - }); - } - }); - }); - - group('Classes', () { - group('Class', () { - setUp(() { - when(mockAuthProvider.currentUser).thenAnswer((_) => - Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); - when(mockAuthProvider.currentUserFromCache) - .thenReturn(User(uid: '0', firstName: 'John', lastName: 'Doe')); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.uid).thenReturn('0'); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - // Open classes - await tester.tap(find.byIcon(FeatherIcons.bookOpen)); - await tester.pumpAndSettle(); - - // Open class view - expect(find.byType(ClassesPage), findsOneWidget); - }); - } - }); - - group('Add class', () { - setUp(() { - when(mockAuthProvider.currentUser).thenAnswer((_) => - Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); - when(mockAuthProvider.currentUserFromCache) - .thenReturn(User(uid: '0', firstName: 'John', lastName: 'Doe')); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.uid).thenReturn('0'); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - // Open classes - await tester.tap(find.byIcon(FeatherIcons.bookOpen)); - await tester.pumpAndSettle(); - - // Open add class view - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(AddClassesPage), findsOneWidget); - - // Save - await tester.tap(find.text('Save')); - await tester.pumpAndSettle(); - - expect(find.byType(ClassesPage), findsOneWidget); - }); - } - }); - - group('Class view', () { - setUp(() { - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( - uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); - when(mockAuthProvider.currentUserFromCache).thenReturn(User( - uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3)); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.uid).thenReturn('0'); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - // Open classes - await tester.tap(find.byIcon(FeatherIcons.bookOpen)); - await tester.pumpAndSettle(); - - // Open class view - await tester.tap(find.text('PC')); - await tester.pumpAndSettle(); - - expect(find.byType(ClassView), findsOneWidget); - - // Open add shortcut view - await tester.tap(find.byIcon(Icons.add_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(ShortcutView), findsOneWidget); - - await tester.tap(find.byIcon(Icons.arrow_back)); - await tester.pumpAndSettle(); - - expect(find.byType(ClassView), findsOneWidget); - - // Open grading view - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(GradingView), findsOneWidget); - - await tester.tap(find.text('Save')); - await tester.pumpAndSettle(); - - expect(find.byType(ClassView), findsOneWidget); - }); - } - }); - }); - - group('Feedback view', () { - setUp(() { - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( - uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); - when(mockAuthProvider.currentUserFromCache).thenReturn(User( - uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3)); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.uid).thenReturn('0'); - when(mockPersonProvider.fetchPerson(any)) - .thenAnswer((_) => Future.value(Person(name: 'John Doe'))); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open timetable - await tester.tap(find.byIcon(Icons.calendar_today_outlined)); - await tester.pumpAndSettle(); - - // Open classes - await tester.tap(find.byIcon(FeatherIcons.bookOpen)); - await tester.pumpAndSettle(); - - // Open class view - await tester.tap(find.text('PC')); - await tester.pumpAndSettle(); - - expect(find.byType(ClassView), findsOneWidget); - - // Open feedback page - await tester.tap(find.byIcon(Icons.rate_review_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(ClassFeedbackView), findsOneWidget); - - await tester.tap(find.byKey(const Key('AcknowledgementCheckbox'))); - await tester.pumpAndSettle(); - - await tester.enterText( - find.byKey(const Key('AutocompleteAssistant')), 'John'); - await tester.pumpAndSettle(); - await tester.tap(find.text('John Doe').last); - await tester.pumpAndSettle(); - - expect(find.byType(Card), findsNWidgets(4)); - expect(find.byType(FeedbackQuestionFormField), findsNWidgets(4)); - expect( - find.text( - 'Estimate the average number of hours per week devoted to solving homework.'), - findsOneWidget); - expect( - find.text( - 'Approximate number of activities that you attended (lectures + applications):'), - findsOneWidget); - expect( - find.text('Was the exposure method appropriate?'), findsOneWidget); - expect(find.text('What are the positive aspects of this class?'), - findsOneWidget); - - await tester.drag( - find.byKey(const Key('FeedbackSlider')), const Offset(2, 0)); - await tester.pumpAndSettle(); - - await tester.tap(find.byIcon(Icons.sentiment_very_satisfied)); - await tester.pumpAndSettle(); - - await tester.enterText( - find.byKey(const Key('FeedbackText')), 'Best class ever!'); - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(const Key('FeedbackDropdown'))); - await tester.pumpAndSettle(); - await tester.tap(find.text('option 3').last); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Send')); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - expect(find.text('You need to select your assistant for this class.'), - findsNothing); - expect(find.text('Answer cannot be empty.'), findsNothing); - - expect(find.byType(ClassView), findsOneWidget); - }); - } - }); - - group('Settings', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open settings - await tester.tap(find.byIcon(Icons.settings_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(SettingsPage), findsOneWidget); - }); - } - }); - - group('Portal', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open portal - await tester.tap(find.byIcon(FeatherIcons.globe)); - await tester.pumpAndSettle(); - - expect(find.byType(PortalPage), findsOneWidget); - }); - } - }); - - group('Filter', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open filter popup menu - await tester.tap(find.byIcon(FeatherIcons.globe)); - await tester.pumpAndSettle(); - await tester.tap(find.byIcon(FeatherIcons.filter)); - await tester.pumpAndSettle(); - - // Open filter on portal page - await tester.tap(find.text('Filter by relevance')); - await tester.pumpAndSettle(); - - expect(find.byType(FilterPage), findsOneWidget); - }); - } - }); - - group('Add website', () { - setUp(() { - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open portal page - await tester.tap(find.byIcon(FeatherIcons.globe)); - await tester.pumpAndSettle(); - - // Open add website page - final addWebsiteButton = - find.byKey(const ValueKey('add_website_associations')); - await tester.ensureVisible(addWebsiteButton); - await tester.pumpAndSettle(); - - await tester.tap(addWebsiteButton); - await tester.pumpAndSettle(); - - expect(find.byType(WebsiteView), findsOneWidget); - }); - } - }); - - group('Edit website', () { - setUp(() { - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( - uid: '1', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open portal page - await tester.tap(find.byIcon(FeatherIcons.globe)); - await tester.pumpAndSettle(); - - // Enable editing - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - // Open edit website page - await tester.ensureVisible(find.text('LSAC1')); - await tester.pumpAndSettle(); - - await tester.tap(find.text('LSAC1')); - await tester.pumpAndSettle(); - - expect(find.byType(WebsiteView), findsOneWidget); - }); - } - }); - - group('Delete website', () { - setUp(() { - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( - uid: '1', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open portal page - await tester.tap(find.byIcon(FeatherIcons.globe)); - await tester.pumpAndSettle(); - - // Enable editing - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - // Open edit website page - await tester.ensureVisible(find.text('LSAC1')); - await tester.pumpAndSettle(); - - await tester.tap(find.text('LSAC1')); - await tester.pumpAndSettle(); - - // Open delete dialog - await tester.tap(find.byIcon(Icons.more_vert_outlined)); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Delete website')); - await tester.pumpAndSettle(); - - // Cancel - expect(find.text('Are you sure you want to delete this website?'), - findsOneWidget); - await tester.tap(find.text('CANCEL')); - await tester.pumpAndSettle(); - - // Open delete dialog - await tester.tap(find.byIcon(Icons.more_vert_outlined)); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Delete website')); - await tester.pumpAndSettle(); - - // Confirm deletion - expect(find.text('Are you sure you want to delete this website?'), - findsOneWidget); - await tester.tap(find.text('DELETE WEBSITE')); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - verify(mockWebsiteProvider.deleteWebsite(any)); - expect(find.byType(PortalPage), findsOneWidget); - }); - } - }); - group('Edit Profile', () { - setUp(() { - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(false)); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( - uid: '1', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); - when(mockAuthProvider.currentUserFromCache).thenReturn(User( - uid: '1', firstName: 'John', lastName: 'Doe', permissionLevel: 3)); - when(mockAuthProvider.email).thenReturn('john.doe@stud.acs.upb.ro'); - when(mockAuthProvider.getProfilePictureURL()) - .thenAnswer((_) => Future.value(null)); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open Edit Profile page - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(EditProfilePage), findsOneWidget); - }); - - testWidgets('${size.width}x${size.height}, delete account', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open Edit Profile page - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - //Open delete account popup - await tester.tap(find.byIcon(Icons.more_vert_outlined)); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Delete account')); - await tester.pumpAndSettle(); - - expect(find.byKey(const ValueKey('delete_account_button')), - findsOneWidget); - }); - - testWidgets('${size.width}x${size.height}, change password', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open Edit Profile page - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - //Open change password popup - await tester.tap(find.byIcon(Icons.more_vert_outlined)); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Change password')); - await tester.pumpAndSettle(); - - expect(find.byKey(const ValueKey('change_password_button')), - findsOneWidget); - }); - - testWidgets('${size.width}x${size.height}, change email', - (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open Edit Profile page - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); - - // Edit the email - await tester.enterText( - find.text('john.doe'), 'johndoe@stud.acs.upb.ro'); - - //Open change email popup - await tester.tap(find.text('Save')); - await tester.pumpAndSettle(); - - expect( - find.byKey(const ValueKey('change_email_button')), findsOneWidget); - }); - } - }); - - group('People page', () { - setUp(() { - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.isAnonymous).thenReturn(true); - }); - - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await mockNetworkImagesFor(() async { - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open people page - await tester.tap(find.byIcon(Icons.people_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(PeoplePage), findsOneWidget); - - // Open bottom sheet with person info - final names = ['John Doe', 'Jane Doe', 'Mary Poppins']; - for (final name in names) { - await tester.tap(find.text(name)); - await tester.pumpAndSettle(); - } - - expect(find.byType(PersonView), findsOneWidget); - }); - }); - } - }); - - group('Show faq page', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - final showMoreFaq = - find.byKey(const ValueKey('show_more_faq'), skipOffstage: false); - - // Ensure FAQ card is visible - await tester.ensureVisible(showMoreFaq); - await tester.pumpAndSettle(); - - // Open faq page - await tester.tap(showMoreFaq); - await tester.pumpAndSettle(); - - expect(find.byType(FaqPage), findsOneWidget); - - await tester.tap(find.byIcon(Icons.search_outlined)); - await tester.pumpAndSettle(); - - expect(find.byType(SearchBar), findsOneWidget); - - final cancelSearchBar = find.byKey(const ValueKey('cancel_search_bar')); - - await tester.tap(cancelSearchBar); - await tester.pumpAndSettle(); - - expect(find.byType(SearchBar), findsNothing); - }); - } - }); - - group('Show news feed page', () { - for (final size in screenSizes) { - testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { - await binding.setSurfaceSize(size); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open news feed page - final showMoreNewsFeed = - find.byKey(const ValueKey('show_more_news_feed')); - - await tester.tap(showMoreNewsFeed); - await tester.pumpAndSettle(); - - expect(find.byType(NewsFeedPage), findsOneWidget); - }); - } - }); -} +// import 'package:acs_upb_mobile/authentication/model/user.dart'; +// import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; +// import 'package:acs_upb_mobile/authentication/view/edit_profile_page.dart'; +// import 'package:acs_upb_mobile/main.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_dropdown.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_rating.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_slider.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/model/questions/question_text.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/view/class_feedback_view.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/view/feedback_question.dart'; +// import 'package:acs_upb_mobile/pages/classes/model/class.dart'; +// import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; +// import 'package:acs_upb_mobile/pages/classes/view/class_view.dart'; +// import 'package:acs_upb_mobile/pages/classes/view/classes_page.dart'; +// import 'package:acs_upb_mobile/pages/classes/view/grading_view.dart'; +// import 'package:acs_upb_mobile/pages/classes/view/shortcut_view.dart'; +// import 'package:acs_upb_mobile/pages/faq/model/question.dart'; +// import 'package:acs_upb_mobile/pages/faq/service/question_provider.dart'; +// import 'package:acs_upb_mobile/pages/faq/view/faq_page.dart'; +// import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; +// import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; +// import 'package:acs_upb_mobile/pages/filter/view/filter_page.dart'; +// import 'package:acs_upb_mobile/pages/home/home_page.dart'; +// import 'package:acs_upb_mobile/pages/news_feed/model/news_feed_item.dart'; +// import 'package:acs_upb_mobile/pages/news_feed/service/news_provider.dart'; +// import 'package:acs_upb_mobile/pages/news_feed/view/news_feed_page.dart'; +// import 'package:acs_upb_mobile/pages/people/model/person.dart'; +// import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; +// import 'package:acs_upb_mobile/pages/people/view/people_page.dart'; +// import 'package:acs_upb_mobile/pages/people/view/person_view.dart'; +// import 'package:acs_upb_mobile/pages/portal/model/website.dart'; +// import 'package:acs_upb_mobile/pages/portal/service/website_provider.dart'; +// import 'package:acs_upb_mobile/pages/portal/view/portal_page.dart'; +// import 'package:acs_upb_mobile/pages/portal/view/website_view.dart'; +// import 'package:acs_upb_mobile/pages/settings/service/request_provider.dart'; +// import 'package:acs_upb_mobile/pages/settings/view/request_permissions.dart'; +// import 'package:acs_upb_mobile/pages/settings/view/settings_page.dart'; +// import 'package:acs_upb_mobile/pages/timetable/model/academic_calendar.dart'; +// import 'package:acs_upb_mobile/pages/timetable/model/events/all_day_event.dart'; +// import 'package:acs_upb_mobile/pages/timetable/model/events/class_event.dart'; +// import 'package:acs_upb_mobile/pages/timetable/model/events/recurring_event.dart'; +// import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; +// import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; +// import 'package:acs_upb_mobile/pages/timetable/view/events/add_event_view.dart'; +// import 'package:acs_upb_mobile/pages/timetable/view/events/event_view.dart'; +// import 'package:acs_upb_mobile/pages/timetable/view/timetable_page.dart'; +// import 'package:acs_upb_mobile/resources/locale_provider.dart'; +// import 'package:acs_upb_mobile/resources/remote_config.dart'; +// import 'package:acs_upb_mobile/resources/utils.dart'; +// import 'package:acs_upb_mobile/widgets/search_bar.dart'; +// import 'package:firebase_core/firebase_core.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:mockito/mockito.dart'; +// import 'package:network_image_mock/network_image_mock.dart'; +// import 'package:package_info_plus/package_info_plus.dart'; +// import 'package:preferences/preferences.dart'; +// import 'package:provider/provider.dart'; +// import 'package:rrule/rrule.dart'; +// import 'package:time_machine/time_machine.dart' hide Offset; +// import 'package:timetable/src/header/week_indicator.dart'; +// +// import 'firebase_mock.dart'; +// import 'test_utils.dart'; +// +// // These tests open each page in the app on multiple screen sizes to make sure +// // nothing overflows/breaks. +// +// class MockAuthProvider extends Mock implements AuthProvider {} +// +// class MockWebsiteProvider extends Mock implements WebsiteProvider {} +// +// class MockFilterProvider extends Mock implements FilterProvider {} +// +// class MockClassProvider extends Mock implements ClassProvider {} +// +// class MockPersonProvider extends Mock implements PersonProvider {} +// +// class MockQuestionProvider extends Mock implements QuestionProvider {} +// +// class MockUniEventProvider extends Mock implements UniEventProvider {} +// +// class MockNewsProvider extends Mock implements NewsProvider {} +// +// class MockRequestProvider extends Mock implements RequestProvider {} +// +// class MockNavigatorObserver extends Mock implements NavigatorObserver {} +// +// class MockFeedbackProvider extends Mock implements FeedbackProvider {} +// +// Future main() async { +// AuthProvider mockAuthProvider; +// WebsiteProvider mockWebsiteProvider; +// FilterProvider mockFilterProvider; +// ClassProvider mockClassProvider; +// PersonProvider mockPersonProvider; +// MockQuestionProvider mockQuestionProvider; +// MockNewsProvider mockNewsProvider; +// UniEventProvider mockEventProvider; +// RequestProvider mockRequestProvider; +// FeedbackProvider mockFeedbackProvider; +// +// setupFirebaseAuthMocks(); +// await Firebase.initializeApp(); +// +// // Test layout for different screen sizes +// // TODO(AdrianMargineanu): Use Flutter driver for integration tests, setting screen sizes here isn't reliable +// final screenSizes = [ +// // Phone +// const Size(720, 1280), +// // Tablet +// const Size(600, 1024), +// ]; +// +// // Add landscape mode sizes +// screenSizes.addAll(List.from(screenSizes) +// .map((size) => Size(size.height, size.width))); +// +// final TestWidgetsFlutterBinding binding = +// TestWidgetsFlutterBinding.ensureInitialized(); +// +// Widget buildApp() => MultiProvider( +// providers: [ +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ChangeNotifierProvider( +// create: (_) => mockWebsiteProvider), +// ChangeNotifierProvider( +// create: (_) => mockFilterProvider), +// ChangeNotifierProvider( +// create: (_) => mockClassProvider), +// ChangeNotifierProvider( +// create: (_) => mockPersonProvider), +// ChangeNotifierProvider( +// create: (_) => mockQuestionProvider), +// ChangeNotifierProvider(create: (_) => mockNewsProvider), +// ChangeNotifierProvider( +// create: (_) => mockEventProvider), +// Provider(create: (_) => mockRequestProvider), +// ChangeNotifierProvider( +// create: (_) => mockFeedbackProvider), +// ], +// child: const MyApp(), +// ); +// +// setUp(() async { +// WidgetsFlutterBinding.ensureInitialized(); +// PrefService.enableCaching(); +// PrefService.cache = {}; +// PrefService.setString('language', 'en'); +// +// await Firebase.initializeApp(); +// +// LocaleProvider.cultures = testCultures; +// LocaleProvider.rruleL10ns = {'en': await RruleL10nTest.create()}; +// +// Utils.packageInfo = PackageInfo( +// version: '1.2.7', +// buildNumber: '6', +// appName: 'ACS UPB Mobile', +// packageName: 'ro.upb.acs_upb_mobile', +// ); +// +// // Pretend an anonymous user is already logged in +// mockAuthProvider = MockAuthProvider(); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// // ignore: invalid_use_of_protected_member +// when(mockAuthProvider.hasListeners).thenReturn(false); +// when(mockAuthProvider.isAnonymous).thenReturn(true); +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(null)); +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(false)); +// +// mockWebsiteProvider = MockWebsiteProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockWebsiteProvider.hasListeners).thenReturn(false); +// when(mockWebsiteProvider.deleteWebsite(any)) +// .thenAnswer((_) => Future.value(true)); +// when(mockAuthProvider.getProfilePictureURL()) +// .thenAnswer((_) => Future.value(null)); +// when(mockWebsiteProvider.fetchWebsites(any)) +// .thenAnswer((_) => Future.value([ +// Website( +// id: '1', +// relevance: null, +// category: WebsiteCategory.learning, +// infoByLocale: {'en': 'info-en', 'ro': 'info-ro'}, +// label: 'Moodle1', +// link: 'http://acs.curs.pub.ro/', +// isPrivate: false, +// ), +// Website( +// id: '2', +// relevance: null, +// category: WebsiteCategory.learning, +// infoByLocale: {}, +// label: 'OCW1', +// link: 'https://ocw.cs.pub.ro/', +// isPrivate: false, +// ), +// Website( +// id: '3', +// relevance: null, +// category: WebsiteCategory.learning, +// infoByLocale: {'en': 'info-en', 'ro': 'info-ro'}, +// label: 'Moodle2', +// link: 'http://acs.curs.pub.ro/', +// isPrivate: false, +// ), +// Website( +// id: '4', +// relevance: null, +// category: WebsiteCategory.learning, +// infoByLocale: {}, +// label: 'OCW2', +// link: 'https://ocw.cs.pub.ro/', +// isPrivate: false, +// ), +// Website( +// id: '5', +// relevance: null, +// category: WebsiteCategory.association, +// infoByLocale: {}, +// label: 'LSAC1', +// link: 'https://lsacbucuresti.ro/', +// isPrivate: false, +// ), +// Website( +// id: '6', +// relevance: null, +// category: WebsiteCategory.administrative, +// infoByLocale: {}, +// label: 'LSAC2', +// link: 'https://lsacbucuresti.ro/', +// isPrivate: false, +// ), +// Website( +// id: '7', +// relevance: null, +// category: WebsiteCategory.resource, +// infoByLocale: {}, +// label: 'LSAC3', +// link: 'https://lsacbucuresti.ro/', +// isPrivate: false, +// ), +// Website( +// id: '8', +// relevance: null, +// category: WebsiteCategory.other, +// infoByLocale: {}, +// label: 'LSAC4', +// link: 'https://lsacbucuresti.ro/', +// isPrivate: false, +// ), +// ])); +// when(mockWebsiteProvider.fetchFavouriteWebsites(any)).thenAnswer( +// (_) async => (await mockWebsiteProvider.fetchWebsites(any)).take(3)); +// +// mockFilterProvider = MockFilterProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockFilterProvider.hasListeners).thenReturn(false); +// when(mockFilterProvider.filterEnabled).thenReturn(true); +// final filter = Filter( +// localizedLevelNames: [ +// {'en': 'Degree', 'ro': 'Nivel de studiu'}, +// {'en': 'Major', 'ro': 'Specializare'}, +// {'en': 'Year', 'ro': 'An'}, +// {'en': 'Series', 'ro': 'Serie'}, +// {'en': 'Group', 'ro': 'Group'} +// ], +// root: FilterNode(name: 'All', value: true, children: [ +// FilterNode(name: 'BSc', value: true, children: [ +// FilterNode(name: 'CTI', value: true, children: [ +// FilterNode(name: 'CTI-1', value: true, children: [ +// FilterNode(name: '1-CA'), +// FilterNode( +// name: '1-CB', +// value: true, +// children: [ +// FilterNode(name: '311CB'), +// FilterNode(name: '312CB'), +// FilterNode(name: '313CB'), +// FilterNode( +// name: '314CB', +// value: true, +// ), +// ], +// ), +// FilterNode(name: '1-CC'), +// FilterNode( +// name: '1-CD', +// children: [ +// FilterNode(name: '311CD'), +// FilterNode(name: '312CD'), +// FilterNode(name: '313CD'), +// FilterNode(name: '314CD'), +// ], +// ), +// ]), +// FilterNode( +// name: 'CTI-2', +// ), +// FilterNode( +// name: 'CTI-3', +// ), +// FilterNode( +// name: 'CTI-4', +// ), +// ]), +// FilterNode(name: 'IS') +// ]), +// FilterNode(name: 'MSc', children: [ +// FilterNode( +// name: 'IA', +// ), +// FilterNode(name: 'SPRC'), +// ]) +// ])); +// when(mockFilterProvider.cachedFilter).thenReturn(filter); +// when(mockFilterProvider.fetchFilter()) +// .thenAnswer((_) => Future.value(filter)); +// +// mockClassProvider = MockClassProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockClassProvider.hasListeners).thenReturn(false); +// final userClassHeaders = [ +// ClassHeader( +// id: '3', +// name: 'Programming', +// acronym: 'PC', +// category: 'A', +// ), +// ClassHeader( +// id: '4', +// name: 'Physics', +// acronym: 'PH', +// category: 'D', +// ) +// ]; +// when(mockClassProvider.userClassHeadersCache).thenReturn(userClassHeaders); +// when(mockClassProvider.fetchClassHeaders(uid: anyNamed('uid'))) +// .thenAnswer((_) => Future.value([ +// ClassHeader( +// id: '1', +// name: 'Maths 1', +// acronym: 'M1', +// category: 'A/B', +// ), +// ClassHeader( +// id: '2', +// name: 'Maths 2', +// acronym: 'M2', +// category: 'A/C', +// ), +// ] + +// userClassHeaders)); +// when(mockClassProvider.fetchUserClassIds(any)) +// .thenAnswer((_) => Future.value(['3', '4'])); +// when(mockClassProvider.fetchClassInfo(any)).thenAnswer((_) => Future.value( +// Class( +// header: ClassHeader( +// id: '3', +// name: 'Programming', +// acronym: 'PC', +// category: 'A', +// ), +// shortcuts: [ +// Shortcut( +// type: ShortcutType.main, +// name: 'OCW', +// link: 'https://ocw.cs.pub.ro/courses/programare'), +// Shortcut( +// type: ShortcutType.other, +// name: 'Google', +// link: 'https://google.com'), +// ], +// grading: { +// 'Exam': 4, +// 'Lab': 1.5, +// 'Homework': 4, +// 'Extra homework': 0.5, +// }, +// ), +// )); +// +// RemoteConfigService.overrides = {'feedback_enabled': true}; +// +// mockPersonProvider = MockPersonProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockPersonProvider.hasListeners).thenReturn(false); +// when(mockPersonProvider.fetchPeople()).thenAnswer((_) => Future.value([ +// Person( +// name: 'John Doe', +// email: 'john.doe@cs.pub.ro', +// phone: '0712345678', +// office: 'AB123', +// position: 'Associate Professor, Dr., Department Council', +// photo: 'https://cdn.worldvectorlogo.com/logos/flutter-logo.svg', +// ), +// Person( +// name: 'Jane Doe', +// email: 'jane.doe@cs.pub.ro', +// phone: '-', +// office: 'Narnia', +// position: 'Professor, Dr.', +// photo: 'https://cdn.worldvectorlogo.com/logos/flutter-logo.svg', +// ), +// Person( +// name: 'Mary Poppins', +// email: 'supercalifragilistic.expialidocious@cs.pub.ro', +// phone: '0712-345-678', +// office: 'Mary Poppins\' office', +// position: 'Professor, Dr., Head of Department', +// photo: 'https://cdn.worldvectorlogo.com/logos/flutter-logo.svg', +// ), +// ])); +// +// when(mockPersonProvider.mostRecentLecturer(any)) +// .thenAnswer((_) => Future.value('Jane Doe')); +// +// mockFeedbackProvider = MockFeedbackProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockFeedbackProvider.hasListeners).thenReturn(true); +// when(mockFeedbackProvider.fetchQuestions()).thenAnswer((_) => Future.value({ +// '0': FeedbackQuestionDropdown( +// category: 'involvement', +// question: +// 'Approximate number of activities that you attended (lectures + applications):', +// id: '0', +// answerOptions: ['option 1', 'option 2', 'option 3', 'option 4'], +// ), +// '1': FeedbackQuestionRating( +// category: 'applications', +// question: 'Was the exposure method appropriate?', +// id: '1', +// ), +// '2': FeedbackQuestionText( +// category: 'personal', +// question: 'What are the positive aspects of this class?', +// id: '2', +// ), +// '3': FeedbackQuestionSlider( +// category: 'homework', +// question: +// 'Estimate the average number of hours per week devoted to solving homework.', +// id: '3', +// ), +// })); +// when(mockFeedbackProvider.fetchCategories()) +// .thenAnswer((_) => Future.value({ +// 'applications': {'en': 'Applications', 'ro': 'Aplicații'}, +// 'homework': {'en': 'Homework', 'ro': 'Temă'}, +// 'involvement': {'en': 'Involvement', 'ro': 'Implicare'}, +// 'personal': { +// 'en': 'Personal comments', +// 'ro': 'Comentarii personale' +// }, +// })); +// +// when(mockFeedbackProvider.userSubmittedFeedbackForClass(any, any)) +// .thenAnswer((_) => Future.value(false)); +// when(mockFeedbackProvider.submitFeedback(any, any, any, any, any)) +// .thenAnswer((_) => Future.value(true)); +// when(mockFeedbackProvider.getClassesWithCompletedFeedback(any)) +// .thenAnswer((_) => Future.value({'M1': true, 'M2': true})); +// when(mockFeedbackProvider.countClassesWithoutFeedback(any, any)) +// .thenAnswer((_) => Future.value('2')); +// +// mockQuestionProvider = MockQuestionProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockQuestionProvider.hasListeners).thenReturn(false); +// when(mockQuestionProvider.fetchQuestions()) +// .thenAnswer((_) => Future.value([ +// Question( +// question: 'Care este programul la secretariat?', +// answer: +// 'Secretariatul este deschis în timpul săptămânii între orele 9:00 si 11:00.', +// tags: ['Licență']), +// Question( +// question: 'Cum mă conectez la eduroam?', +// answer: +// 'Conectarea în rețeaua *eduroam* se face pe baza aceluiași cont folosit și pe site-ul de cursuri.', +// tags: ['Conectare', 'Informații']) +// ])); +// when(mockQuestionProvider.fetchQuestions(limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([ +// Question( +// question: 'Care este programul la secretariat?', +// answer: +// 'Secretariatul este deschis în timpul săptămânii între orele 9:00 si 11:00.', +// tags: ['Licență']), +// Question( +// question: 'Cum mă conectez la eduroam?', +// answer: +// 'Conectarea în rețeaua *eduroam* se face pe baza aceluiași cont folosit și pe site-ul de cursuri.', +// tags: ['Conectare', 'Informații']) +// ])); +// +// mockNewsProvider = MockNewsProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockNewsProvider.hasListeners).thenReturn(false); +// when(mockNewsProvider.fetchNewsFeedItems()) +// .thenAnswer((_) => Future.value([ +// NewsFeedItem( +// '03.10.2020', +// 'Cazarea studentilor de anul II licenta', +// 'https://acs.pub.ro/noutati/cazarea-studentilor-de-anul-ii-licenta/'), +// NewsFeedItem( +// '03.10.2020', +// 'Festivitatea de deschidere a anului universitar 2020-2021', +// 'https://acs.pub.ro/noutati/festivitatea-de-deschidere-a-anului-universitar-2020-2021/') +// ])); +// when(mockNewsProvider.fetchNewsFeedItems(limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([ +// NewsFeedItem( +// '03.10.2020', +// 'Cazarea studentilor de anul II licenta', +// 'https://acs.pub.ro/noutati/cazarea-studentilor-de-anul-ii-licenta/'), +// NewsFeedItem( +// '03.10.2020', +// 'Festivitatea de deschidere a anului universitar 2020-2021', +// 'https://acs.pub.ro/noutati/festivitatea-de-deschidere-a-anului-universitar-2020-2021/') +// ])); +// +// mockEventProvider = MockUniEventProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockEventProvider.hasListeners).thenReturn(false); +// final now = LocalDate.today(); +// final weekStart = now.subtractDays(now.dayOfWeek - DayOfWeek.monday); +// final holidays = [ +// // Holiday on Tuesday and Wednesday next week +// AllDayUniEvent( +// name: 'Holiday', +// start: weekStart.addWeeks(1).addDays(1), +// end: weekStart.addWeeks(1).addDays(2), +// id: 'holiday0', +// ), +// AllDayUniEvent( +// name: 'Inter-semester holiday', +// start: weekStart.addWeeks(2).subtractDays(2), +// end: weekStart.addWeeks(3).subtractDays(1), +// id: 'holiday1', +// ), +// ]; +// final calendar = AcademicCalendar( +// id: '2020', +// semesters: [ +// AllDayUniEvent( +// start: weekStart, +// end: weekStart.addWeeks(2).subtractDays(3), +// id: 'semester1', +// ), +// AllDayUniEvent( +// start: weekStart.addWeeks(3), +// end: weekStart.addWeeks(5).subtractDays(3), +// id: 'semester2', +// ), +// ], +// holidays: holidays, +// ); +// when(mockEventProvider.fetchCalendars()) +// .thenAnswer((_) => Future.value({'2020': calendar})); +// when(mockEventProvider.getUpcomingEvents(LocalDate.today())) +// .thenAnswer((_) => Future.value([])); +// when(mockEventProvider.getUpcomingEvents(LocalDate.today(), +// limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([])); +// when(mockEventProvider.getAllEventsOfClass(any)) +// .thenAnswer((_) => Future.value([])); +// final rruleEveryWeekFirstSem = RecurrenceRule( +// frequency: Frequency.weekly, +// interval: 1, +// until: weekStart.addWeeks(2).subtractDays(3).atMidnight(), +// ); +// final rruleEveryTwoWeeksFirstSem = RecurrenceRule( +// frequency: Frequency.weekly, +// interval: 2, +// until: weekStart.addWeeks(2).subtractDays(3).atMidnight(), +// ); +// final rruleEveryWeek = RecurrenceRule( +// frequency: Frequency.weekly, +// interval: 1, +// ); +// final rruleEveryTwoWeeks = RecurrenceRule( +// frequency: Frequency.weekly, +// interval: 2, +// ); +// const duration = Period(hours: 2); +// final events = [ +// RecurringUniEvent( +// name: 'M1', +// calendar: calendar, +// rrule: rruleEveryWeekFirstSem, +// start: weekStart.at(LocalTime(8, 0, 0)), +// period: duration, +// id: '0', +// ), +// RecurringUniEvent( +// name: 'M2', +// calendar: calendar, +// rrule: rruleEveryTwoWeeksFirstSem, +// start: weekStart.at(LocalTime(10, 0, 0)), +// period: duration, +// id: '1', +// ), +// RecurringUniEvent( +// name: 'T1', +// calendar: calendar, +// rrule: rruleEveryWeekFirstSem, +// start: weekStart.addDays(1).at(LocalTime(8, 0, 0)), +// period: duration, +// id: '2', +// ), +// RecurringUniEvent( +// name: 'T2', +// calendar: calendar, +// rrule: rruleEveryTwoWeeksFirstSem, +// start: weekStart.addDays(1).at(LocalTime(9, 0, 0)), +// period: duration, +// id: '3', +// ), +// RecurringUniEvent( +// name: 'W1', +// calendar: calendar, +// rrule: rruleEveryWeekFirstSem, +// start: weekStart.addDays(2).at(LocalTime(8, 0, 0)), +// period: duration, +// id: '4', +// ), +// RecurringUniEvent( +// name: 'W2', +// rrule: rruleEveryWeek, +// start: weekStart.addDays(2).at(LocalTime(10, 0, 0)), +// period: duration, +// id: '5', +// ), +// RecurringUniEvent( +// name: 'W3', +// rrule: rruleEveryTwoWeeks, +// start: weekStart.addDays(2).at(LocalTime(12, 0, 0)), +// period: duration, +// id: '6', +// ), +// ClassEvent( +// classHeader: userClassHeaders[0], +// type: UniEventType.lecture, +// teacher: Person(name: 'Jane Doe'), +// location: 'AB123', +// degree: 'BSc', +// relevance: ['314CB'], +// calendar: calendar, +// rrule: rruleEveryWeek, +// start: weekStart.addDays(3).at(LocalTime(10, 0, 0)), +// period: duration, +// id: '7', +// ), +// RecurringUniEvent( +// classHeader: userClassHeaders[1], +// type: UniEventType.lecture, +// location: 'AB123', +// degree: 'BSc', +// relevance: ['314CB'], +// calendar: calendar, +// rrule: rruleEveryTwoWeeks, +// start: weekStart.addDays(3).at(LocalTime(12, 0, 0)), +// period: duration, +// id: '8', +// ), +// RecurringUniEvent( +// name: 'F1', +// calendar: calendar, +// rrule: rruleEveryWeek, +// start: weekStart.addDays(4).at(LocalTime(10, 0, 0)), +// period: duration, +// id: '9', +// ), +// RecurringUniEvent( +// name: 'F2', +// calendar: calendar, +// rrule: rruleEveryTwoWeeks, +// start: weekStart.addDays(4).at(LocalTime(12, 0, 0)), +// period: duration, +// id: '10', +// ), +// ]; +// when(mockEventProvider.getAllDayEventsIntersecting(any)) +// .thenAnswer((invocation) { +// final DateInterval interval = invocation.positionalArguments[0]; +// return Stream.value(holidays +// .map((holiday) => +// holiday.generateInstances(intersectingInterval: interval)) +// .expand((e) => e)); +// }); +// when(mockEventProvider.getPartDayEventsIntersecting(any)) +// .thenAnswer((invocation) { +// final LocalDate date = invocation.positionalArguments[0]; +// return Stream.value(events +// .map((event) => event.generateInstances( +// intersectingInterval: DateInterval(date, date))) +// .expand((e) => e)); +// }); +// when(mockEventProvider.empty).thenReturn(false); +// when(mockEventProvider.deleteEvent(any)) +// .thenAnswer((_) => Future.value(true)); +// when(mockEventProvider.updateEvent(any)) +// .thenAnswer((_) => Future.value(true)); +// when(mockEventProvider.addEvent(any)).thenAnswer((_) => Future.value(true)); +// +// mockRequestProvider = MockRequestProvider(); +// when(mockRequestProvider.makeRequest(any)) +// .thenAnswer((_) => Future.value(true)); +// when(mockRequestProvider.userAlreadyRequested(any)) +// .thenAnswer((_) => Future.value(false)); +// }); +// +// group('Home', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// expect(find.byType(HomePage), findsOneWidget); +// +// // Open home +// await tester.tap(find.byIcon(Icons.home)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(HomePage), findsOneWidget); +// }); +// } +// }); +// +// group('Timetable', () { +// setUp(() { +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( +// uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); +// when(mockAuthProvider.currentUserFromCache).thenReturn(User( +// uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3)); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.uid).thenReturn('0'); +// }); +// +// group('Timetable no events/no classes', () { +// setUp(() { +// when(mockEventProvider.getAllDayEventsIntersecting(any)) +// .thenAnswer((_) => Stream.value([])); +// when(mockEventProvider.getPartDayEventsIntersecting(any)) +// .thenAnswer((_) => Stream.value([])); +// when(mockEventProvider.empty).thenReturn(true); +// +// when(mockClassProvider.userClassHeadersCache).thenReturn(null); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(TimetablePage), findsOneWidget); +// expect(find.text('No events to show'), findsOneWidget); +// +// await tester.tap(find.text('CHOOSE CLASSES')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AddClassesPage), findsOneWidget); +// }); +// } +// }); +// +// group('Timetable no events/no filter', () { +// setUp(() { +// when(mockEventProvider.getAllDayEventsIntersecting(any)) +// .thenAnswer((_) => Stream.value([])); +// when(mockEventProvider.getPartDayEventsIntersecting(any)) +// .thenAnswer((_) => Stream.value([])); +// when(mockEventProvider.empty).thenReturn(true); +// +// when(mockFilterProvider.cachedFilter).thenReturn(null); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(TimetablePage), findsOneWidget); +// expect(find.text('No events to show'), findsOneWidget); +// +// await tester.tap(find.text('OPEN FILTER')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(FilterPage), findsOneWidget); +// }); +// } +// }); +// +// group('Timetable no events/no permissions', () { +// setUp(() { +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( +// uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 0))); +// when(mockAuthProvider.currentUserFromCache).thenReturn(User( +// uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 0)); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); +// +// when(mockEventProvider.getAllDayEventsIntersecting(any)) +// .thenAnswer((_) => Stream.value([])); +// when(mockEventProvider.getPartDayEventsIntersecting(any)) +// .thenAnswer((_) => Stream.value([])); +// when(mockEventProvider.empty).thenReturn(true); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(TimetablePage), findsOneWidget); +// expect(find.text('No events to show'), findsOneWidget); +// +// await tester.tap(find.text('REQUEST PERMISSIONS')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(RequestPermissionsPage), findsOneWidget); +// }); +// } +// }); +// +// group('Timetable no events/add some', () { +// setUp(() { +// when(mockEventProvider.getAllDayEventsIntersecting(any)) +// .thenAnswer((_) => Stream.value([])); +// when(mockEventProvider.getPartDayEventsIntersecting(any)) +// .thenAnswer((_) => Stream.value([])); +// when(mockEventProvider.empty).thenReturn(true); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(TimetablePage), findsOneWidget); +// expect(find.text('No events to show'), findsOneWidget); +// expect( +// find.byKey(const ValueKey('no_events_message')), findsOneWidget); +// +// await tester.tap(find.text('CANCEL')); +// await tester.pumpAndSettle(); +// +// expect(find.text('No events to show'), findsNothing); +// }); +// } +// }); +// +// group('Timetable events', () { +// for (final size in screenSizes) { +// if (size.width > size.height) { +// // TODO(IoanaAlexandru): In landscape mode the test fails in a weird +// // way - it seems as if two weeks are visible at the same time, but +// // the behaviour cannot be reproduced on a device. Skipping this +// // test in landscape mode for now. +// continue; +// } +// +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(TimetablePage), findsOneWidget); +// +// // Scroll to previous week +// await tester.drag(find.text('Tue'), Offset(size.width - 30, 0)); +// await tester.pumpAndSettle(); +// +// // Expect previous week +// final previousWeek = WeekYearRules.iso +// .getWeekOfWeekYear(LocalDate.today().subtractWeeks(1)); +// expect( +// find.byWidgetPredicate((widget) => +// widget is WeekIndicator && +// widget.week.toString() == previousWeek.toString()), +// findsOneWidget); +// +// expect(find.text('Holiday'), findsNothing); +// expect(find.text('Inter-semester holiday'), findsNothing); +// expect(find.text('M1'), findsNothing); +// expect(find.text('M2'), findsNothing); +// expect(find.text('T1'), findsNothing); +// expect(find.text('T2'), findsNothing); +// expect(find.text('W1'), findsNothing); +// expect(find.text('W2'), findsNothing); +// expect(find.text('W3'), findsNothing); +// expect(find.text('PC'), findsNothing); +// expect(find.text('PH'), findsNothing); +// expect(find.text('F1'), findsNothing); +// expect(find.text('F2'), findsNothing); +// +// // Scroll back to current week +// await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); +// await tester.pumpAndSettle(); +// +// // Expect current week +// final currentWeek = +// WeekYearRules.iso.getWeekOfWeekYear(LocalDate.today()); +// expect( +// find.byWidgetPredicate((widget) => +// widget is WeekIndicator && +// widget.week.toString() == currentWeek.toString()), +// findsOneWidget); +// +// expect(find.text('Holiday'), findsNothing); +// expect(find.text('Inter-semester holiday'), findsNothing); +// expect(find.text('M1'), findsOneWidget); +// expect(find.text('M2'), findsOneWidget); +// expect(find.text('T1'), findsOneWidget); +// expect(find.text('T2'), findsOneWidget); +// expect(find.text('W1'), findsOneWidget); +// expect(find.text('W2'), findsOneWidget); +// expect(find.text('W3'), findsOneWidget); +// expect(find.text('PC'), findsOneWidget); +// expect(find.text('PH'), findsOneWidget); +// expect(find.text('F1'), findsOneWidget); +// expect(find.text('F2'), findsOneWidget); +// +// // Scroll to next week +// await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); +// await tester.pumpAndSettle(); +// +// // Expect next week +// final nextWeek = WeekYearRules.iso +// .getWeekOfWeekYear(LocalDate.today().addWeeks(1)); +// expect( +// find.byWidgetPredicate((widget) => +// widget is WeekIndicator && +// widget.week.toString() == nextWeek.toString()), +// findsOneWidget); +// +// expect(find.text('Holiday'), findsOneWidget); +// expect(find.text('Inter-semester holiday'), findsOneWidget); +// expect(find.text('M1'), findsOneWidget); +// expect(find.text('M2'), findsNothing); +// expect(find.text('T1'), findsNothing); +// expect(find.text('T2'), findsNothing); +// expect(find.text('W1'), findsNothing); +// expect(find.text('W2'), findsOneWidget); +// expect(find.text('W3'), findsNothing); +// expect(find.text('PC'), findsOneWidget); +// expect(find.text('PH'), findsNothing); +// expect(find.text('F1'), findsOneWidget); +// expect(find.text('F2'), findsNothing); +// +// // Scroll to next week +// await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); +// await tester.pumpAndSettle(); +// +// // Expect next week +// final nextNextWeek = WeekYearRules.iso +// .getWeekOfWeekYear(LocalDate.today().addWeeks(2)); +// expect( +// find.byWidgetPredicate((widget) => +// widget is WeekIndicator && +// widget.week.toString() == nextNextWeek.toString()), +// findsOneWidget); +// +// expect(find.text('Holiday'), findsNothing); +// expect(find.text('Inter-semester holiday'), findsOneWidget); +// expect(find.text('M1'), findsNothing); +// expect(find.text('M2'), findsNothing); +// expect(find.text('T1'), findsNothing); +// expect(find.text('T2'), findsNothing); +// expect(find.text('W1'), findsNothing); +// expect(find.text('W2'), findsOneWidget); +// expect(find.text('W3'), findsOneWidget); +// expect(find.text('PC'), findsNothing); +// expect(find.text('PH'), findsNothing); +// expect(find.text('F1'), findsNothing); +// expect(find.text('F2'), findsNothing); +// +// // Scroll to next week +// await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); +// await tester.pumpAndSettle(); +// +// // Expect next week +// final nextNextNextWeek = WeekYearRules.iso +// .getWeekOfWeekYear(LocalDate.today().addWeeks(3)); +// expect( +// find.byWidgetPredicate((widget) => +// widget is WeekIndicator && +// widget.week.toString() == nextNextNextWeek.toString()), +// findsOneWidget); +// +// expect(find.text('Holiday'), findsNothing); +// expect(find.text('Inter-semester holiday'), findsNothing); +// expect(find.text('M1'), findsNothing); +// expect(find.text('M2'), findsNothing); +// expect(find.text('T1'), findsNothing); +// expect(find.text('T2'), findsNothing); +// expect(find.text('W1'), findsNothing); +// expect(find.text('W2'), findsOneWidget); +// expect(find.text('W3'), findsNothing); +// expect(find.text('PC'), findsOneWidget); +// expect(find.text('PH'), findsOneWidget); +// expect(find.text('F1'), findsOneWidget); +// expect(find.text('F2'), findsOneWidget); +// +// // Navigate to today +// await tester.tap(find.byIcon(Icons.today_outlined)); +// await tester.pumpAndSettle(); +// +// // Expect current week +// expect( +// find.byWidgetPredicate((widget) => +// widget is WeekIndicator && +// widget.week.toString() == currentWeek.toString()), +// findsOneWidget); +// }); +// } +// }); +// +// group('Event page - open all day event', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// // Expect current week +// final currentWeek = +// WeekYearRules.iso.getWeekOfWeekYear(LocalDate.today()); +// expect( +// find.byWidgetPredicate((widget) => +// widget is WeekIndicator && +// widget.week.toString() == currentWeek.toString()), +// findsOneWidget); +// +// // Scroll to next week +// await tester.drag(find.text('Sun'), Offset(-size.width + 10, 0)); +// await tester.pumpAndSettle(); +// +// // Expect next week +// final nextWeek = WeekYearRules.iso +// .getWeekOfWeekYear(LocalDate.today().addWeeks(1)); +// expect( +// find.byWidgetPredicate((widget) => +// widget is WeekIndicator && +// widget.week.toString() == nextWeek.toString()), +// findsOneWidget); +// +// // Open holiday event +// await tester.tap(find.text('Holiday')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(EventView), findsOneWidget); +// }); +// } +// }); +// +// group('Event page - add event', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// // Open add event page +// await tester +// .tapAt(tester.getCenter(find.text('Sat')).translate(0, 100)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AddEventView), findsOneWidget); +// +// // Select type +// await tester.tap(find.text('Type')); +// await tester.pumpAndSettle(); +// await tester.tap(find.text('Seminar').last); +// await tester.pumpAndSettle(); +// +// // Select class +// await tester.tap(find.text('Class')); +// await tester.pumpAndSettle(); +// await tester.tap(find.text('Programming').last); +// await tester.pumpAndSettle(); +// +// // Press back +// await tester.tap(find.byIcon(Icons.arrow_back)); +// await tester.pumpAndSettle(); +// +// await tester +// .tapAt(tester.getCenter(find.text('Sat')).translate(0, 100)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AddEventView), findsOneWidget); +// +// // Select type +// await tester.tap(find.text('Type')); +// await tester.pumpAndSettle(); +// await tester.tap(find.text('Lecture').last); +// await tester.pumpAndSettle(); +// +// // Select lecturer - partial name +// await tester.tap(find.byIcon(FeatherIcons.user)); +// await tester.pumpAndSettle(); +// await tester.enterText( +// find.byKey(const Key('AutocompleteLecturer')), 'John'); +// await tester.pumpAndSettle(); +// await tester.tap(find.text('John Doe')); +// await tester.pumpAndSettle(); +// +// // Select lecturer - new name +// await tester.tap(find.byIcon(FeatherIcons.user)); +// await tester.pumpAndSettle(); +// await tester.enterText( +// find.byKey(const Key('AutocompleteLecturer')), 'Isabel Steward'); +// await tester.tap(find.text('Isabel Steward')); +// await tester.pumpAndSettle(); +// +// // Select lecturer - check autocomplete suggestions +// await tester.tap(find.byIcon(FeatherIcons.user)); +// await tester.pumpAndSettle(); +// await tester.enterText( +// find.byKey(const Key('AutocompleteLecturer')), 'Doe'); +// await tester.pumpAndSettle(); +// +// expect(find.text('Jane Doe'), findsOneWidget); +// expect(find.text('John Doe'), findsOneWidget); +// +// await tester.tap(find.text('Jane Doe')); +// await tester.pumpAndSettle(); +// }); +// } +// }); +// +// group('Event page - edit event', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// // Open PC event +// await tester.tap(find.text('PC')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(EventView), findsOneWidget); +// +// // Open class page +// await tester.tap(find.text('Programming')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(ClassView), findsOneWidget); +// expect(find.byIcon(FeatherIcons.user), findsOneWidget); +// expect(find.byKey(const Key('LecturerCard')), findsOneWidget); +// +// // Press back +// await tester.tap(find.byIcon(Icons.arrow_back)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(EventView), findsOneWidget); +// expect(find.byIcon(FeatherIcons.user), findsOneWidget); +// expect(find.text('Jane Doe'), findsOneWidget); +// +// await tester.tap(find.byIcon(FeatherIcons.user)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(PersonView), findsOneWidget); +// +// // Press back +// await tester.tap(find.byIcon(Icons.arrow_back)); +// await tester.pumpAndSettle(); +// +// // Open edit event page +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AddEventView), findsOneWidget); +// expect(find.text('Lecturer'), findsOneWidget); +// expect(find.text('Location'), findsOneWidget); +// expect(find.text('Week'), findsOneWidget); +// expect(find.text('Day'), findsOneWidget); +// +// // Select lecturer +// await tester.tap(find.text('Lecturer')); +// await tester.pumpAndSettle(); +// await tester.enterText( +// find.byKey(const Key('AutocompleteLecturer')), 'Doe'); +// await tester.pumpAndSettle(); +// +// expect(find.text('Jane Doe'), findsOneWidget); +// expect(find.text('John Doe'), findsOneWidget); +// +// await tester.enterText( +// find.byKey(const Key('AutocompleteLecturer')), 'John Doe'); +// await tester.pumpAndSettle(); +// +// FocusManager.instance.primaryFocus.unfocus(); +// await tester.pumpAndSettle(); +// await tester.tap(find.text('John Doe').last); +// await tester.pumpAndSettle(); +// +// FocusManager.instance.primaryFocus.unfocus(); +// await tester.pumpAndSettle(); +// +// expect(find.text('Jane Doe'), findsNothing); +// expect(find.text('John Doe'), findsOneWidget); +// +// // Press save +// await tester.tap(find.text('Save')); +// await tester.pumpAndSettle(const Duration(seconds: 5)); +// +// expect(find.byType(TimetablePage), findsOneWidget); +// +// // Open PC event +// await tester.tap(find.text('PC')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(EventView), findsOneWidget); +// expect(find.byIcon(FeatherIcons.user), findsOneWidget); +// +// // Press back +// await tester.tap(find.byIcon(Icons.arrow_back)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(TimetablePage), findsOneWidget); +// }); +// } +// }); +// +// group('Event page - delete event', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// // Open PH event +// await tester.tap(find.text('PH')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(EventView), findsOneWidget); +// +// // Open edit event page +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AddEventView), findsOneWidget); +// +// // Open delete dialog +// await tester.tap(find.byIcon(Icons.more_vert_outlined)); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Delete event')); +// await tester.pumpAndSettle(); +// +// // Confirm deletion +// expect(find.text('Are you sure you want to delete this event?'), +// findsOneWidget); +// await tester.tap(find.text('DELETE EVENT')); +// await tester.pumpAndSettle(const Duration(seconds: 5)); +// +// verify(mockEventProvider.deleteEvent(any)); +// expect(find.byType(TimetablePage), findsOneWidget); +// }); +// } +// }); +// }); +// +// group('Classes', () { +// group('Class', () { +// setUp(() { +// when(mockAuthProvider.currentUser).thenAnswer((_) => +// Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); +// when(mockAuthProvider.currentUserFromCache) +// .thenReturn(User(uid: '0', firstName: 'John', lastName: 'Doe')); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.uid).thenReturn('0'); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// // Open classes +// await tester.tap(find.byIcon(FeatherIcons.bookOpen)); +// await tester.pumpAndSettle(); +// +// // Open class view +// expect(find.byType(ClassesPage), findsOneWidget); +// }); +// } +// }); +// +// group('Add class', () { +// setUp(() { +// when(mockAuthProvider.currentUser).thenAnswer((_) => +// Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); +// when(mockAuthProvider.currentUserFromCache) +// .thenReturn(User(uid: '0', firstName: 'John', lastName: 'Doe')); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.uid).thenReturn('0'); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// // Open classes +// await tester.tap(find.byIcon(FeatherIcons.bookOpen)); +// await tester.pumpAndSettle(); +// +// // Open add class view +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(AddClassesPage), findsOneWidget); +// +// // Save +// await tester.tap(find.text('Save')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(ClassesPage), findsOneWidget); +// }); +// } +// }); +// +// group('Class view', () { +// setUp(() { +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( +// uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); +// when(mockAuthProvider.currentUserFromCache).thenReturn(User( +// uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3)); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.uid).thenReturn('0'); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// // Open classes +// await tester.tap(find.byIcon(FeatherIcons.bookOpen)); +// await tester.pumpAndSettle(); +// +// // Open class view +// await tester.tap(find.text('PC')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(ClassView), findsOneWidget); +// +// // Open add shortcut view +// await tester.tap(find.byIcon(Icons.add_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(ShortcutView), findsOneWidget); +// +// await tester.tap(find.byIcon(Icons.arrow_back)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(ClassView), findsOneWidget); +// +// // Open grading view +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(GradingView), findsOneWidget); +// +// await tester.tap(find.text('Save')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(ClassView), findsOneWidget); +// }); +// } +// }); +// }); +// +// group('Feedback view', () { +// setUp(() { +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( +// uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); +// when(mockAuthProvider.currentUserFromCache).thenReturn(User( +// uid: '0', firstName: 'John', lastName: 'Doe', permissionLevel: 3)); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.uid).thenReturn('0'); +// when(mockPersonProvider.fetchPerson(any)) +// .thenAnswer((_) => Future.value(Person(name: 'John Doe'))); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open timetable +// await tester.tap(find.byIcon(Icons.calendar_today_outlined)); +// await tester.pumpAndSettle(); +// +// // Open classes +// await tester.tap(find.byIcon(FeatherIcons.bookOpen)); +// await tester.pumpAndSettle(); +// +// // Open class view +// await tester.tap(find.text('PC')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(ClassView), findsOneWidget); +// +// // Open feedback page +// await tester.tap(find.byIcon(Icons.rate_review_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(ClassFeedbackView), findsOneWidget); +// +// await tester.tap(find.byKey(const Key('AcknowledgementCheckbox'))); +// await tester.pumpAndSettle(); +// +// await tester.enterText( +// find.byKey(const Key('AutocompleteAssistant')), 'John'); +// await tester.pumpAndSettle(); +// await tester.tap(find.text('John Doe').last); +// await tester.pumpAndSettle(); +// +// expect(find.byType(Card), findsNWidgets(4)); +// expect(find.byType(FeedbackQuestionFormField), findsNWidgets(4)); +// expect( +// find.text( +// 'Estimate the average number of hours per week devoted to solving homework.'), +// findsOneWidget); +// expect( +// find.text( +// 'Approximate number of activities that you attended (lectures + applications):'), +// findsOneWidget); +// expect( +// find.text('Was the exposure method appropriate?'), findsOneWidget); +// expect(find.text('What are the positive aspects of this class?'), +// findsOneWidget); +// +// await tester.drag( +// find.byKey(const Key('FeedbackSlider')), const Offset(2, 0)); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.byIcon(Icons.sentiment_very_satisfied)); +// await tester.pumpAndSettle(); +// +// await tester.enterText( +// find.byKey(const Key('FeedbackText')), 'Best class ever!'); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.byKey(const Key('FeedbackDropdown'))); +// await tester.pumpAndSettle(); +// await tester.tap(find.text('option 3').last); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Send')); +// await tester.pumpAndSettle(const Duration(seconds: 5)); +// +// expect(find.text('You need to select your assistant for this class.'), +// findsNothing); +// expect(find.text('Answer cannot be empty.'), findsNothing); +// +// expect(find.byType(ClassView), findsOneWidget); +// }); +// } +// }); +// +// group('Settings', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open settings +// await tester.tap(find.byIcon(Icons.settings_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(SettingsPage), findsOneWidget); +// }); +// } +// }); +// +// group('Portal', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open portal +// await tester.tap(find.byIcon(FeatherIcons.globe)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(PortalPage), findsOneWidget); +// }); +// } +// }); +// +// group('Filter', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open filter popup menu +// await tester.tap(find.byIcon(FeatherIcons.globe)); +// await tester.pumpAndSettle(); +// await tester.tap(find.byIcon(FeatherIcons.filter)); +// await tester.pumpAndSettle(); +// +// // Open filter on portal page +// await tester.tap(find.text('Filter by relevance')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(FilterPage), findsOneWidget); +// }); +// } +// }); +// +// group('Add website', () { +// setUp(() { +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open portal page +// await tester.tap(find.byIcon(FeatherIcons.globe)); +// await tester.pumpAndSettle(); +// +// // Open add website page +// final addWebsiteButton = +// find.byKey(const ValueKey('add_website_associations')); +// await tester.ensureVisible(addWebsiteButton); +// await tester.pumpAndSettle(); +// +// await tester.tap(addWebsiteButton); +// await tester.pumpAndSettle(); +// +// expect(find.byType(WebsiteView), findsOneWidget); +// }); +// } +// }); +// +// group('Edit website', () { +// setUp(() { +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( +// uid: '1', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open portal page +// await tester.tap(find.byIcon(FeatherIcons.globe)); +// await tester.pumpAndSettle(); +// +// // Enable editing +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// // Open edit website page +// await tester.ensureVisible(find.text('LSAC1')); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('LSAC1')); +// await tester.pumpAndSettle(); +// +// expect(find.byType(WebsiteView), findsOneWidget); +// }); +// } +// }); +// +// group('Delete website', () { +// setUp(() { +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( +// uid: '1', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open portal page +// await tester.tap(find.byIcon(FeatherIcons.globe)); +// await tester.pumpAndSettle(); +// +// // Enable editing +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// // Open edit website page +// await tester.ensureVisible(find.text('LSAC1')); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('LSAC1')); +// await tester.pumpAndSettle(); +// +// // Open delete dialog +// await tester.tap(find.byIcon(Icons.more_vert_outlined)); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Delete website')); +// await tester.pumpAndSettle(); +// +// // Cancel +// expect(find.text('Are you sure you want to delete this website?'), +// findsOneWidget); +// await tester.tap(find.text('CANCEL')); +// await tester.pumpAndSettle(); +// +// // Open delete dialog +// await tester.tap(find.byIcon(Icons.more_vert_outlined)); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Delete website')); +// await tester.pumpAndSettle(); +// +// // Confirm deletion +// expect(find.text('Are you sure you want to delete this website?'), +// findsOneWidget); +// await tester.tap(find.text('DELETE WEBSITE')); +// await tester.pumpAndSettle(const Duration(seconds: 5)); +// +// verify(mockWebsiteProvider.deleteWebsite(any)); +// expect(find.byType(PortalPage), findsOneWidget); +// }); +// } +// }); +// group('Edit Profile', () { +// setUp(() { +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(false)); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(User( +// uid: '1', firstName: 'John', lastName: 'Doe', permissionLevel: 3))); +// when(mockAuthProvider.currentUserFromCache).thenReturn(User( +// uid: '1', firstName: 'John', lastName: 'Doe', permissionLevel: 3)); +// when(mockAuthProvider.email).thenReturn('john.doe@stud.acs.upb.ro'); +// when(mockAuthProvider.getProfilePictureURL()) +// .thenAnswer((_) => Future.value(null)); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open Edit Profile page +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(EditProfilePage), findsOneWidget); +// }); +// +// testWidgets('${size.width}x${size.height}, delete account', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open Edit Profile page +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// //Open delete account popup +// await tester.tap(find.byIcon(Icons.more_vert_outlined)); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Delete account')); +// await tester.pumpAndSettle(); +// +// expect(find.byKey(const ValueKey('delete_account_button')), +// findsOneWidget); +// }); +// +// testWidgets('${size.width}x${size.height}, change password', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open Edit Profile page +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// //Open change password popup +// await tester.tap(find.byIcon(Icons.more_vert_outlined)); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Change password')); +// await tester.pumpAndSettle(); +// +// expect(find.byKey(const ValueKey('change_password_button')), +// findsOneWidget); +// }); +// +// testWidgets('${size.width}x${size.height}, change email', +// (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open Edit Profile page +// await tester.tap(find.byIcon(Icons.edit_outlined)); +// await tester.pumpAndSettle(); +// +// // Edit the email +// await tester.enterText( +// find.text('john.doe'), 'johndoe@stud.acs.upb.ro'); +// +// //Open change email popup +// await tester.tap(find.text('Save')); +// await tester.pumpAndSettle(); +// +// expect( +// find.byKey(const ValueKey('change_email_button')), findsOneWidget); +// }); +// } +// }); +// +// group('People page', () { +// setUp(() { +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.isAnonymous).thenReturn(true); +// }); +// +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await mockNetworkImagesFor(() async { +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open people page +// await tester.tap(find.byIcon(Icons.people_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(PeoplePage), findsOneWidget); +// +// // Open bottom sheet with person info +// final names = ['John Doe', 'Jane Doe', 'Mary Poppins']; +// for (final name in names) { +// await tester.tap(find.text(name)); +// await tester.pumpAndSettle(); +// } +// +// expect(find.byType(PersonView), findsOneWidget); +// }); +// }); +// } +// }); +// +// group('Show faq page', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// final showMoreFaq = +// find.byKey(const ValueKey('show_more_faq'), skipOffstage: false); +// +// // Ensure FAQ card is visible +// await tester.ensureVisible(showMoreFaq); +// await tester.pumpAndSettle(); +// +// // Open faq page +// await tester.tap(showMoreFaq); +// await tester.pumpAndSettle(); +// +// expect(find.byType(FaqPage), findsOneWidget); +// +// await tester.tap(find.byIcon(Icons.search_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.byType(SearchBar), findsOneWidget); +// +// final cancelSearchBar = find.byKey(const ValueKey('cancel_search_bar')); +// +// await tester.tap(cancelSearchBar); +// await tester.pumpAndSettle(); +// +// expect(find.byType(SearchBar), findsNothing); +// }); +// } +// }); +// +// group('Show news feed page', () { +// for (final size in screenSizes) { +// testWidgets('${size.width}x${size.height}', (WidgetTester tester) async { +// await binding.setSurfaceSize(size); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open news feed page +// final showMoreNewsFeed = +// find.byKey(const ValueKey('show_more_news_feed')); +// +// await tester.tap(showMoreNewsFeed); +// await tester.pumpAndSettle(); +// +// expect(find.byType(NewsFeedPage), findsOneWidget); +// }); +// } +// }); +// } diff --git a/test/portal_test.dart b/test/portal_test.dart index 5d052c8d5..57c5d32d7 100644 --- a/test/portal_test.dart +++ b/test/portal_test.dart @@ -1,177 +1,177 @@ -import 'package:acs_upb_mobile/authentication/model/user.dart'; -import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; -import 'package:acs_upb_mobile/generated/l10n.dart'; -import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; -import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; -import 'package:acs_upb_mobile/pages/portal/model/website.dart'; -import 'package:acs_upb_mobile/pages/portal/service/website_provider.dart'; -import 'package:acs_upb_mobile/pages/portal/view/portal_page.dart'; -import 'package:acs_upb_mobile/resources/locale_provider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:preferences/preferences.dart'; -import 'package:provider/provider.dart'; -import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; - -import 'test_utils.dart'; - -class MockWebsiteProvider extends Mock implements WebsiteProvider {} - -class MockFilterProvider extends Mock implements FilterProvider {} - -class MockAuthProvider extends Mock implements AuthProvider {} - -class MockUrlLauncher extends Mock - with MockPlatformInterfaceMixin - implements UrlLauncherPlatform {} - -void main() { - final WebsiteProvider mockWebsiteProvider = MockWebsiteProvider(); - // ignore: invalid_use_of_protected_member - when(mockWebsiteProvider.hasListeners).thenReturn(false); - when(mockWebsiteProvider.fetchWebsites(any)).thenAnswer((_) => Future.value([ - Website( - id: '1', - relevance: null, - category: WebsiteCategory.learning, - infoByLocale: {'en': 'info-en', 'ro': 'info-ro'}, - label: 'Moodle', - link: 'http://acs.curs.pub.ro/', - isPrivate: false, - ), - Website( - id: '2', - relevance: null, - category: WebsiteCategory.learning, - infoByLocale: {}, - label: 'OCW', - link: 'https://ocw.cs.pub.ro/', - isPrivate: false, - ), - Website( - id: '3', - relevance: null, - category: WebsiteCategory.association, - infoByLocale: {}, - label: 'LSAC', - link: 'https://lsacbucuresti.ro/', - isPrivate: false, - ), - ])); - when(mockWebsiteProvider.fetchFavouriteWebsites(any)).thenAnswer( - (_) async => (await mockWebsiteProvider.fetchWebsites(any)).take(3)); - when(mockWebsiteProvider.incrementNumberOfVisits(any, uid: anyNamed('uid'))) - .thenAnswer((_) => Future.value(true)); - - final FilterProvider mockFilterProvider = MockFilterProvider(); - // ignore: invalid_use_of_protected_member - when(mockFilterProvider.hasListeners).thenReturn(false); - when(mockFilterProvider.fetchFilter()) - .thenAnswer((_) => Future.value(Filter(root: FilterNode(name: 'All')))); - when(mockFilterProvider.filterEnabled).thenReturn(true); - - final MockUrlLauncher mockUrlLauncher = MockUrlLauncher(); - UrlLauncherPlatform.instance = mockUrlLauncher; - when(mockUrlLauncher.canLaunch(any)).thenAnswer((_) => Future.value(true)); - - final MockAuthProvider mockAuthProvider = MockAuthProvider(); - // ignore: invalid_use_of_protected_member - when(mockAuthProvider.hasListeners).thenReturn(false); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - when(mockAuthProvider.currentUser).thenAnswer( - (_) => Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); - when(mockAuthProvider.currentUserFromCache) - .thenReturn(User(uid: '0', firstName: 'John', lastName: 'Doe')); - - Widget buildPortalPage() => MultiProvider( - providers: [ - ChangeNotifierProvider( - create: (_) => mockWebsiteProvider), - ChangeNotifierProvider( - create: (_) => mockFilterProvider), - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ], - child: const MaterialApp( - localizationsDelegates: [S.delegate], - home: PortalPage(), - ), - ); - - group('Portal', () { - setUpAll(() async { - WidgetsFlutterBinding.ensureInitialized(); - PrefService.enableCaching(); - PrefService.cache = {}; - PrefService.setString('language', 'en'); - - LocaleProvider.cultures = testCultures; - LocaleProvider.rruleL10ns = {'en': await RruleL10nTest.create()}; - }); - - testWidgets('Names', (WidgetTester tester) async { - await tester.pumpWidget(buildPortalPage()); - await tester.pumpAndSettle(); - - expect(find.text('Moodle'), findsOneWidget); - expect(find.text('OCW'), findsOneWidget); - expect(find.text('LSAC'), findsOneWidget); - }); - - group('Localization', () { - testWidgets('en', (WidgetTester tester) async { - await tester.pumpWidget(buildPortalPage()); - await tester.pumpAndSettle(); - - expect(find.byTooltip('info-en'), findsOneWidget); - }); - - testWidgets('ro', (WidgetTester tester) async { - PrefService.setString('language', 'ro'); - await S.load(const Locale('ro', 'RO')); - - await tester.pumpWidget(buildPortalPage()); - await tester.pumpAndSettle(); - - expect(find.byTooltip('info-ro'), findsOneWidget); - }); - }); - - testWidgets('Links', (WidgetTester tester) async { - await tester.pumpWidget(buildPortalPage()); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Moodle')); - verify(mockUrlLauncher.launch('http://acs.curs.pub.ro/', - useSafariVC: anyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableDomStorage: anyNamed('enableDomStorage'), - enableJavaScript: anyNamed('enableJavaScript'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'))) - .called(1); - - await tester.tap(find.text('OCW')); - verify(mockUrlLauncher.launch('https://ocw.cs.pub.ro/', - useSafariVC: anyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableDomStorage: anyNamed('enableDomStorage'), - enableJavaScript: anyNamed('enableJavaScript'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'))) - .called(1); - - await tester.tap(find.text('LSAC')); - verify(mockUrlLauncher.launch('https://lsacbucuresti.ro/', - useSafariVC: anyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableDomStorage: anyNamed('enableDomStorage'), - enableJavaScript: anyNamed('enableJavaScript'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'))) - .called(1); - }); - }); -} +// import 'package:acs_upb_mobile/authentication/model/user.dart'; +// import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; +// import 'package:acs_upb_mobile/generated/l10n.dart'; +// import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; +// import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; +// import 'package:acs_upb_mobile/pages/portal/model/website.dart'; +// import 'package:acs_upb_mobile/pages/portal/service/website_provider.dart'; +// import 'package:acs_upb_mobile/pages/portal/view/portal_page.dart'; +// import 'package:acs_upb_mobile/resources/locale_provider.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:mockito/mockito.dart'; +// import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +// import 'package:preferences/preferences.dart'; +// import 'package:provider/provider.dart'; +// import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; +// +// import 'test_utils.dart'; +// +// class MockWebsiteProvider extends Mock implements WebsiteProvider {} +// +// class MockFilterProvider extends Mock implements FilterProvider {} +// +// class MockAuthProvider extends Mock implements AuthProvider {} +// +// class MockUrlLauncher extends Mock +// with MockPlatformInterfaceMixin +// implements UrlLauncherPlatform {} +// +// void main() { +// final WebsiteProvider mockWebsiteProvider = MockWebsiteProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockWebsiteProvider.hasListeners).thenReturn(false); +// when(mockWebsiteProvider.fetchWebsites(any)).thenAnswer((_) => Future.value([ +// Website( +// id: '1', +// relevance: null, +// category: WebsiteCategory.learning, +// infoByLocale: {'en': 'info-en', 'ro': 'info-ro'}, +// label: 'Moodle', +// link: 'http://acs.curs.pub.ro/', +// isPrivate: false, +// ), +// Website( +// id: '2', +// relevance: null, +// category: WebsiteCategory.learning, +// infoByLocale: {}, +// label: 'OCW', +// link: 'https://ocw.cs.pub.ro/', +// isPrivate: false, +// ), +// Website( +// id: '3', +// relevance: null, +// category: WebsiteCategory.association, +// infoByLocale: {}, +// label: 'LSAC', +// link: 'https://lsacbucuresti.ro/', +// isPrivate: false, +// ), +// ])); +// when(mockWebsiteProvider.fetchFavouriteWebsites(any)).thenAnswer( +// (_) async => (await mockWebsiteProvider.fetchWebsites(any)).take(3)); +// when(mockWebsiteProvider.incrementNumberOfVisits(any, uid: anyNamed('uid'))) +// .thenAnswer((_) => Future.value(true)); +// +// final FilterProvider mockFilterProvider = MockFilterProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockFilterProvider.hasListeners).thenReturn(false); +// when(mockFilterProvider.fetchFilter()) +// .thenAnswer((_) => Future.value(Filter(root: FilterNode(name: 'All')))); +// when(mockFilterProvider.filterEnabled).thenReturn(true); +// +// final MockUrlLauncher mockUrlLauncher = MockUrlLauncher(); +// UrlLauncherPlatform.instance = mockUrlLauncher; +// when(mockUrlLauncher.canLaunch(any)).thenAnswer((_) => Future.value(true)); +// +// final MockAuthProvider mockAuthProvider = MockAuthProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockAuthProvider.hasListeners).thenReturn(false); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// when(mockAuthProvider.currentUser).thenAnswer( +// (_) => Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); +// when(mockAuthProvider.currentUserFromCache) +// .thenReturn(User(uid: '0', firstName: 'John', lastName: 'Doe')); +// +// Widget buildPortalPage() => MultiProvider( +// providers: [ +// ChangeNotifierProvider( +// create: (_) => mockWebsiteProvider), +// ChangeNotifierProvider( +// create: (_) => mockFilterProvider), +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ], +// child: const MaterialApp( +// localizationsDelegates: [S.delegate], +// home: PortalPage(), +// ), +// ); +// +// group('Portal', () { +// setUpAll(() async { +// WidgetsFlutterBinding.ensureInitialized(); +// PrefService.enableCaching(); +// PrefService.cache = {}; +// PrefService.setString('language', 'en'); +// +// LocaleProvider.cultures = testCultures; +// LocaleProvider.rruleL10ns = {'en': await RruleL10nTest.create()}; +// }); +// +// testWidgets('Names', (WidgetTester tester) async { +// await tester.pumpWidget(buildPortalPage()); +// await tester.pumpAndSettle(); +// +// expect(find.text('Moodle'), findsOneWidget); +// expect(find.text('OCW'), findsOneWidget); +// expect(find.text('LSAC'), findsOneWidget); +// }); +// +// group('Localization', () { +// testWidgets('en', (WidgetTester tester) async { +// await tester.pumpWidget(buildPortalPage()); +// await tester.pumpAndSettle(); +// +// expect(find.byTooltip('info-en'), findsOneWidget); +// }); +// +// testWidgets('ro', (WidgetTester tester) async { +// PrefService.setString('language', 'ro'); +// await S.load(const Locale('ro', 'RO')); +// +// await tester.pumpWidget(buildPortalPage()); +// await tester.pumpAndSettle(); +// +// expect(find.byTooltip('info-ro'), findsOneWidget); +// }); +// }); +// +// testWidgets('Links', (WidgetTester tester) async { +// await tester.pumpWidget(buildPortalPage()); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Moodle')); +// verify(mockUrlLauncher.launch('http://acs.curs.pub.ro/', +// useSafariVC: anyNamed('useSafariVC'), +// useWebView: anyNamed('useWebView'), +// enableDomStorage: anyNamed('enableDomStorage'), +// enableJavaScript: anyNamed('enableJavaScript'), +// universalLinksOnly: anyNamed('universalLinksOnly'), +// headers: anyNamed('headers'))) +// .called(1); +// +// await tester.tap(find.text('OCW')); +// verify(mockUrlLauncher.launch('https://ocw.cs.pub.ro/', +// useSafariVC: anyNamed('useSafariVC'), +// useWebView: anyNamed('useWebView'), +// enableDomStorage: anyNamed('enableDomStorage'), +// enableJavaScript: anyNamed('enableJavaScript'), +// universalLinksOnly: anyNamed('universalLinksOnly'), +// headers: anyNamed('headers'))) +// .called(1); +// +// await tester.tap(find.text('LSAC')); +// verify(mockUrlLauncher.launch('https://lsacbucuresti.ro/', +// useSafariVC: anyNamed('useSafariVC'), +// useWebView: anyNamed('useWebView'), +// enableDomStorage: anyNamed('enableDomStorage'), +// enableJavaScript: anyNamed('enableJavaScript'), +// universalLinksOnly: anyNamed('universalLinksOnly'), +// headers: anyNamed('headers'))) +// .called(1); +// }); +// }); +// } diff --git a/test/settings_test.dart b/test/settings_test.dart index b01dd17d4..ccf92cab7 100644 --- a/test/settings_test.dart +++ b/test/settings_test.dart @@ -1,386 +1,386 @@ -import 'package:acs_upb_mobile/authentication/model/user.dart'; -import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; -import 'package:acs_upb_mobile/main.dart'; -import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; -import 'package:acs_upb_mobile/pages/classes/model/class.dart'; -import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; -import 'package:acs_upb_mobile/pages/faq/model/question.dart'; -import 'package:acs_upb_mobile/pages/faq/service/question_provider.dart'; -import 'package:acs_upb_mobile/pages/news_feed/model/news_feed_item.dart'; -import 'package:acs_upb_mobile/pages/news_feed/service/news_provider.dart'; -import 'package:acs_upb_mobile/pages/portal/service/website_provider.dart'; -import 'package:acs_upb_mobile/pages/settings/service/request_provider.dart'; -import 'package:acs_upb_mobile/pages/settings/view/request_permissions.dart'; -import 'package:acs_upb_mobile/pages/settings/view/settings_page.dart'; -import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; -import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; -import 'package:acs_upb_mobile/resources/locale_provider.dart'; -import 'package:acs_upb_mobile/resources/utils.dart'; -import 'package:acs_upb_mobile/widgets/dialog.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:preferences/preferences.dart'; -import 'package:provider/provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:time_machine/time_machine.dart'; - -import 'test_utils.dart'; - -class MockAuthProvider extends Mock implements AuthProvider {} - -class MockWebsiteProvider extends Mock implements WebsiteProvider {} - -class MockQuestionProvider extends Mock implements QuestionProvider {} - -class MockRequestProvider extends Mock implements RequestProvider {} - -class MockNewsProvider extends Mock implements NewsProvider {} - -class MockUniEventProvider extends Mock implements UniEventProvider {} - -class MockFeedbackProvider extends Mock implements FeedbackProvider {} - -class MockClassProvider extends Mock implements ClassProvider {} - -void main() { - AuthProvider mockAuthProvider; - WebsiteProvider mockWebsiteProvider; - MockQuestionProvider mockQuestionProvider; - RequestProvider mockRequestProvider; - MockNewsProvider mockNewsProvider; - UniEventProvider mockEventProvider; - FeedbackProvider mockFeedbackProvider; - ClassProvider mockClassProvider; - - Widget buildApp() => MultiProvider(providers: [ - ChangeNotifierProvider(create: (_) => mockAuthProvider), - ChangeNotifierProvider( - create: (_) => mockEventProvider), - ChangeNotifierProvider( - create: (_) => mockWebsiteProvider), - ChangeNotifierProvider( - create: (_) => mockQuestionProvider), - Provider(create: (_) => mockRequestProvider), - ChangeNotifierProvider(create: (_) => mockNewsProvider), - ChangeNotifierProvider( - create: (_) => mockFeedbackProvider), - ChangeNotifierProvider(create: (_) => mockClassProvider), - ], child: const MyApp()); - - group('Settings', () { - setUpAll(() async { - WidgetsFlutterBinding.ensureInitialized(); - PrefService.enableCaching(); - PrefService.cache = {}; - // Assuming mock system language is English - SharedPreferences.setMockInitialValues({'language': 'auto'}); - - LocaleProvider.cultures = testCultures; - LocaleProvider.rruleL10ns = {'en': await RruleL10nTest.create()}; - - Utils.packageInfo = PackageInfo( - version: '1.2.7', buildNumber: '6', appName: 'ACS UPB Mobile'); - - // Pretend an anonymous user is already logged in - mockAuthProvider = MockAuthProvider(); - when(mockAuthProvider.isAuthenticated).thenReturn(true); - // ignore: invalid_use_of_protected_member - when(mockAuthProvider.hasListeners).thenReturn(false); - when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(null)); - when(mockAuthProvider.isAnonymous).thenReturn(true); - when(mockAuthProvider.getProfilePictureURL()) - .thenAnswer((_) => Future.value(null)); - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); - - mockWebsiteProvider = MockWebsiteProvider(); - // ignore: invalid_use_of_protected_member - when(mockWebsiteProvider.hasListeners).thenReturn(false); - when(mockWebsiteProvider.deleteWebsite(any)) - .thenAnswer((_) => Future.value(true)); - when(mockWebsiteProvider.fetchWebsites(any)) - .thenAnswer((_) => Future.value([])); - when(mockWebsiteProvider.fetchFavouriteWebsites(any)) - .thenAnswer((_) => Future.value(null)); - - mockQuestionProvider = MockQuestionProvider(); - // ignore: invalid_use_of_protected_member - when(mockQuestionProvider.hasListeners).thenReturn(false); - when(mockQuestionProvider.fetchQuestions()) - .thenAnswer((_) => Future.value([])); - when(mockQuestionProvider.fetchQuestions(limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([])); - - mockRequestProvider = MockRequestProvider(); - when(mockRequestProvider.makeRequest(any)) - .thenAnswer((_) => Future.value(true)); - when(mockRequestProvider.userAlreadyRequested(any)) - .thenAnswer((_) => Future.value(false)); - - mockNewsProvider = MockNewsProvider(); - // ignore: invalid_use_of_protected_member - when(mockNewsProvider.hasListeners).thenReturn(false); - when(mockNewsProvider.fetchNewsFeedItems()) - .thenAnswer((_) => Future.value([])); - when(mockNewsProvider.fetchNewsFeedItems(limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([])); - - mockEventProvider = MockUniEventProvider(); - // ignore: invalid_use_of_protected_member - when(mockEventProvider.hasListeners).thenReturn(false); - when(mockEventProvider.getUpcomingEvents(LocalDate.today())) - .thenAnswer((_) => Future.value([])); - when(mockEventProvider.getUpcomingEvents(LocalDate.today(), - limit: anyNamed('limit'))) - .thenAnswer((_) => Future.value([])); - - mockFeedbackProvider = MockFeedbackProvider(); - // ignore: invalid_use_of_protected_member - when(mockFeedbackProvider.hasListeners).thenReturn(true); - when(mockFeedbackProvider.userSubmittedFeedbackForClass(any, any)) - .thenAnswer((_) => Future.value(false)); - when(mockFeedbackProvider.getClassesWithCompletedFeedback(any)) - .thenAnswer((_) => Future.value({'M1': true, 'M2': true})); - - mockClassProvider = MockClassProvider(); - // ignore: invalid_use_of_protected_member - when(mockClassProvider.hasListeners).thenReturn(false); - final userClassHeaders = [ - ClassHeader( - id: '3', - name: 'Programming', - acronym: 'PC', - category: 'A', - ), - ClassHeader( - id: '4', - name: 'Physics', - acronym: 'PH', - category: 'D', - ) - ]; - when(mockClassProvider.userClassHeadersCache) - .thenReturn(userClassHeaders); - when(mockClassProvider.fetchClassHeaders(uid: anyNamed('uid'))) - .thenAnswer((_) => Future.value([ - ClassHeader( - id: '1', - name: 'Maths 1', - acronym: 'M1', - category: 'A/B', - ), - ClassHeader( - id: '2', - name: 'Maths 2', - acronym: 'M2', - category: 'A/C', - ), - ] + - userClassHeaders)); - when(mockClassProvider.fetchUserClassIds(any)) - .thenAnswer((_) => Future.value(['3', '4'])); - }); - - testWidgets('Dark Mode', (WidgetTester tester) async { - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - MaterialApp app = find.byType(MaterialApp).evaluate().first.widget; - expect(app.theme.brightness, equals(Brightness.light)); - - // Open settings - await tester.tap(find.byIcon(Icons.settings_outlined)); - await tester.pumpAndSettle(); - - // Toggle dark mode - await tester.tap(find.text('Dark Mode')); - await tester.pumpAndSettle(); - - app = find.byType(MaterialApp).evaluate().first.widget; - expect(app.theme.brightness, equals(Brightness.dark)); - - // Toggle dark mode - await tester.tap(find.text('Dark Mode')); - await tester.pumpAndSettle(); - - app = find.byType(MaterialApp).evaluate().first.widget; - expect(app.theme.brightness, equals(Brightness.light)); - }); - - testWidgets('Language', (WidgetTester tester) async { - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open settings - await tester.tap(find.byIcon(Icons.settings_outlined)); - await tester.pumpAndSettle(); - - expect(find.text('Auto'), findsOneWidget); - - // Romanian - await tester.tap(find.text('Language')); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Romanian')); - await tester.pumpAndSettle(); - - expect(find.text('Setări'), findsOneWidget); - expect(find.text('Română'), findsOneWidget); - - // English - await tester.tap(find.text('Limbă')); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Engleză')); - await tester.pumpAndSettle(); - - expect(find.text('Settings'), findsOneWidget); - expect(find.text('English'), findsOneWidget); - - // Back to Auto (English) - await tester.tap(find.text('Language')); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Auto')); - await tester.pumpAndSettle(); - - expect(find.text('Settings'), findsOneWidget); - expect(find.text('Auto'), findsOneWidget); - }); - group('Request permissions', () { - setUpAll(() async { - when(mockAuthProvider.currentUser).thenAnswer((_) => - Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); - when(mockAuthProvider.isAnonymous).thenReturn(false); - }); - - testWidgets('Normal scenario', (WidgetTester tester) async { - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open settings - await tester.tap(find.byIcon(Icons.settings_outlined)); - await tester.pumpAndSettle(); - - // Open Ask Permissions page - expect(find.text('No special permissions'), findsOneWidget); - await tester.tap(find.byKey(const ValueKey('ask_permissions'))); - await tester.pumpAndSettle(); - expect(find.byType(RequestPermissionsPage), findsOneWidget); - - // Send a request - await tester.enterText( - find.byType(TextFormField), 'I love League of Legends'); - await tester.tap(find.byType(Checkbox)); - await tester.tap(find.text('Save')); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Verify the request is sent and Settings Page pops back - verify(mockRequestProvider.makeRequest(any)); - expect(find.byType(SettingsPage), findsOneWidget); - }); - - testWidgets('User has already sent a request scenario', - (WidgetTester tester) async { - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); - when(mockRequestProvider.userAlreadyRequested(any)) - .thenAnswer((_) => Future.value(true)); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open settings - await tester.tap(find.byIcon(Icons.settings_outlined)); - await tester.pumpAndSettle(); - - // Open Ask Permissions page - expect(find.text('Permissions request already sent'), findsOneWidget); - await tester.tap(find.byKey(const ValueKey('ask_permissions'))); - await tester.pumpAndSettle(); - expect(find.byType(RequestPermissionsPage), findsOneWidget); - - // Send a request - await tester.enterText( - find.byType(TextFormField), 'I love League of Legends'); - await tester.tap(find.byType(Checkbox)); - await tester.tap(find.text('Save')); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Check that warning Dialog appears and press Send - expect(find.byType(AppDialog), findsOneWidget); - await tester.tap(find.text('SEND')); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Verify the request is sent and Settings Page pops back - verify(mockRequestProvider.makeRequest(any)); - expect(find.byType(SettingsPage), findsOneWidget); - }); - - testWidgets('User is anonymous scenario', (WidgetTester tester) async { - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); - when(mockAuthProvider.isAnonymous).thenReturn(true); - when(mockRequestProvider.userAlreadyRequested(any)) - .thenAnswer((_) => Future.value(false)); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open settings - await tester.tap(find.byIcon(Icons.settings_outlined)); - await tester.pumpAndSettle(); - - // Press Ask Permissions page - expect(find.text('No special permissions'), findsOneWidget); - await tester.tap(find.byKey(const ValueKey('ask_permissions'))); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - // Verify nothing happens - expect(find.byType(SettingsPage), findsOneWidget); - }); - - testWidgets('User is not verified scenario', (WidgetTester tester) async { - when(mockAuthProvider.isVerified) - .thenAnswer((_) => Future.value(false)); - when(mockAuthProvider.isAnonymous).thenReturn(false); - when(mockRequestProvider.userAlreadyRequested(any)) - .thenAnswer((_) => Future.value(false)); - - await tester.pumpWidget(buildApp()); - await tester.pumpAndSettle(); - - // Open settings - await tester.tap(find.byIcon(Icons.settings_outlined)); - await tester.pumpAndSettle(); - - // Press Ask Permissions page - expect(find.text('No special permissions'), findsOneWidget); - await tester.tap(find.byKey(const ValueKey('ask_permissions'))); - - // Verify Ask Permissions page is not opened - await tester.pumpAndSettle(const Duration(seconds: 4)); - expect(find.byType(SettingsPage), findsOneWidget); - expect(find.byType(RequestPermissionsPage), findsNothing); - - // Verify account - when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); - - // Go back and open settings again - await tester.tap(find.byIcon(Icons.arrow_back)); - await tester.pumpAndSettle(); - await tester.tap(find.byIcon(Icons.settings_outlined)); - await tester.pumpAndSettle(); - - // Press Ask Permissions page - expect(find.text('No special permissions'), findsOneWidget); - await tester.tap(find.byKey(const ValueKey('ask_permissions'))); - - // Verify Ask Permissions page is opened - await tester.pumpAndSettle(); - expect(find.byType(RequestPermissionsPage), findsOneWidget); - }); - }); - }); -} +// import 'package:acs_upb_mobile/authentication/model/user.dart'; +// import 'package:acs_upb_mobile/authentication/service/auth_provider.dart'; +// import 'package:acs_upb_mobile/main.dart'; +// import 'package:acs_upb_mobile/pages/class_feedback/service/feedback_provider.dart'; +// import 'package:acs_upb_mobile/pages/classes/model/class.dart'; +// import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; +// import 'package:acs_upb_mobile/pages/faq/model/question.dart'; +// import 'package:acs_upb_mobile/pages/faq/service/question_provider.dart'; +// import 'package:acs_upb_mobile/pages/news_feed/model/news_feed_item.dart'; +// import 'package:acs_upb_mobile/pages/news_feed/service/news_provider.dart'; +// import 'package:acs_upb_mobile/pages/portal/service/website_provider.dart'; +// import 'package:acs_upb_mobile/pages/settings/service/request_provider.dart'; +// import 'package:acs_upb_mobile/pages/settings/view/request_permissions.dart'; +// import 'package:acs_upb_mobile/pages/settings/view/settings_page.dart'; +// import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; +// import 'package:acs_upb_mobile/pages/timetable/service/uni_event_provider.dart'; +// import 'package:acs_upb_mobile/resources/locale_provider.dart'; +// import 'package:acs_upb_mobile/resources/utils.dart'; +// import 'package:acs_upb_mobile/widgets/dialog.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:mockito/mockito.dart'; +// import 'package:package_info_plus/package_info_plus.dart'; +// import 'package:preferences/preferences.dart'; +// import 'package:provider/provider.dart'; +// import 'package:shared_preferences/shared_preferences.dart'; +// import 'package:time_machine/time_machine.dart'; +// +// import 'test_utils.dart'; +// +// class MockAuthProvider extends Mock implements AuthProvider {} +// +// class MockWebsiteProvider extends Mock implements WebsiteProvider {} +// +// class MockQuestionProvider extends Mock implements QuestionProvider {} +// +// class MockRequestProvider extends Mock implements RequestProvider {} +// +// class MockNewsProvider extends Mock implements NewsProvider {} +// +// class MockUniEventProvider extends Mock implements UniEventProvider {} +// +// class MockFeedbackProvider extends Mock implements FeedbackProvider {} +// +// class MockClassProvider extends Mock implements ClassProvider {} +// +// void main() { +// AuthProvider mockAuthProvider; +// WebsiteProvider mockWebsiteProvider; +// MockQuestionProvider mockQuestionProvider; +// RequestProvider mockRequestProvider; +// MockNewsProvider mockNewsProvider; +// UniEventProvider mockEventProvider; +// FeedbackProvider mockFeedbackProvider; +// ClassProvider mockClassProvider; +// +// Widget buildApp() => MultiProvider(providers: [ +// ChangeNotifierProvider(create: (_) => mockAuthProvider), +// ChangeNotifierProvider( +// create: (_) => mockEventProvider), +// ChangeNotifierProvider( +// create: (_) => mockWebsiteProvider), +// ChangeNotifierProvider( +// create: (_) => mockQuestionProvider), +// Provider(create: (_) => mockRequestProvider), +// ChangeNotifierProvider(create: (_) => mockNewsProvider), +// ChangeNotifierProvider( +// create: (_) => mockFeedbackProvider), +// ChangeNotifierProvider(create: (_) => mockClassProvider), +// ], child: const MyApp()); +// +// group('Settings', () { +// setUpAll(() async { +// WidgetsFlutterBinding.ensureInitialized(); +// PrefService.enableCaching(); +// PrefService.cache = {}; +// // Assuming mock system language is English +// SharedPreferences.setMockInitialValues({'language': 'auto'}); +// +// LocaleProvider.cultures = testCultures; +// LocaleProvider.rruleL10ns = {'en': await RruleL10nTest.create()}; +// +// Utils.packageInfo = PackageInfo( +// version: '1.2.7', buildNumber: '6', appName: 'ACS UPB Mobile'); +// +// // Pretend an anonymous user is already logged in +// mockAuthProvider = MockAuthProvider(); +// when(mockAuthProvider.isAuthenticated).thenReturn(true); +// // ignore: invalid_use_of_protected_member +// when(mockAuthProvider.hasListeners).thenReturn(false); +// when(mockAuthProvider.currentUser).thenAnswer((_) => Future.value(null)); +// when(mockAuthProvider.isAnonymous).thenReturn(true); +// when(mockAuthProvider.getProfilePictureURL()) +// .thenAnswer((_) => Future.value(null)); +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); +// +// mockWebsiteProvider = MockWebsiteProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockWebsiteProvider.hasListeners).thenReturn(false); +// when(mockWebsiteProvider.deleteWebsite(any)) +// .thenAnswer((_) => Future.value(true)); +// when(mockWebsiteProvider.fetchWebsites(any)) +// .thenAnswer((_) => Future.value([])); +// when(mockWebsiteProvider.fetchFavouriteWebsites(any)) +// .thenAnswer((_) => Future.value(null)); +// +// mockQuestionProvider = MockQuestionProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockQuestionProvider.hasListeners).thenReturn(false); +// when(mockQuestionProvider.fetchQuestions()) +// .thenAnswer((_) => Future.value([])); +// when(mockQuestionProvider.fetchQuestions(limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([])); +// +// mockRequestProvider = MockRequestProvider(); +// when(mockRequestProvider.makeRequest(any)) +// .thenAnswer((_) => Future.value(true)); +// when(mockRequestProvider.userAlreadyRequested(any)) +// .thenAnswer((_) => Future.value(false)); +// +// mockNewsProvider = MockNewsProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockNewsProvider.hasListeners).thenReturn(false); +// when(mockNewsProvider.fetchNewsFeedItems()) +// .thenAnswer((_) => Future.value([])); +// when(mockNewsProvider.fetchNewsFeedItems(limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([])); +// +// mockEventProvider = MockUniEventProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockEventProvider.hasListeners).thenReturn(false); +// when(mockEventProvider.getUpcomingEvents(LocalDate.today())) +// .thenAnswer((_) => Future.value([])); +// when(mockEventProvider.getUpcomingEvents(LocalDate.today(), +// limit: anyNamed('limit'))) +// .thenAnswer((_) => Future.value([])); +// +// mockFeedbackProvider = MockFeedbackProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockFeedbackProvider.hasListeners).thenReturn(true); +// when(mockFeedbackProvider.userSubmittedFeedbackForClass(any, any)) +// .thenAnswer((_) => Future.value(false)); +// when(mockFeedbackProvider.getClassesWithCompletedFeedback(any)) +// .thenAnswer((_) => Future.value({'M1': true, 'M2': true})); +// +// mockClassProvider = MockClassProvider(); +// // ignore: invalid_use_of_protected_member +// when(mockClassProvider.hasListeners).thenReturn(false); +// final userClassHeaders = [ +// ClassHeader( +// id: '3', +// name: 'Programming', +// acronym: 'PC', +// category: 'A', +// ), +// ClassHeader( +// id: '4', +// name: 'Physics', +// acronym: 'PH', +// category: 'D', +// ) +// ]; +// when(mockClassProvider.userClassHeadersCache) +// .thenReturn(userClassHeaders); +// when(mockClassProvider.fetchClassHeaders(uid: anyNamed('uid'))) +// .thenAnswer((_) => Future.value([ +// ClassHeader( +// id: '1', +// name: 'Maths 1', +// acronym: 'M1', +// category: 'A/B', +// ), +// ClassHeader( +// id: '2', +// name: 'Maths 2', +// acronym: 'M2', +// category: 'A/C', +// ), +// ] + +// userClassHeaders)); +// when(mockClassProvider.fetchUserClassIds(any)) +// .thenAnswer((_) => Future.value(['3', '4'])); +// }); +// +// testWidgets('Dark Mode', (WidgetTester tester) async { +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// MaterialApp app = find.byType(MaterialApp).evaluate().first.widget; +// expect(app.theme.brightness, equals(Brightness.light)); +// +// // Open settings +// await tester.tap(find.byIcon(Icons.settings_outlined)); +// await tester.pumpAndSettle(); +// +// // Toggle dark mode +// await tester.tap(find.text('Dark Mode')); +// await tester.pumpAndSettle(); +// +// app = find.byType(MaterialApp).evaluate().first.widget; +// expect(app.theme.brightness, equals(Brightness.dark)); +// +// // Toggle dark mode +// await tester.tap(find.text('Dark Mode')); +// await tester.pumpAndSettle(); +// +// app = find.byType(MaterialApp).evaluate().first.widget; +// expect(app.theme.brightness, equals(Brightness.light)); +// }); +// +// testWidgets('Language', (WidgetTester tester) async { +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open settings +// await tester.tap(find.byIcon(Icons.settings_outlined)); +// await tester.pumpAndSettle(); +// +// expect(find.text('Auto'), findsOneWidget); +// +// // Romanian +// await tester.tap(find.text('Language')); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Romanian')); +// await tester.pumpAndSettle(); +// +// expect(find.text('Setări'), findsOneWidget); +// expect(find.text('Română'), findsOneWidget); +// +// // English +// await tester.tap(find.text('Limbă')); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Engleză')); +// await tester.pumpAndSettle(); +// +// expect(find.text('Settings'), findsOneWidget); +// expect(find.text('English'), findsOneWidget); +// +// // Back to Auto (English) +// await tester.tap(find.text('Language')); +// await tester.pumpAndSettle(); +// +// await tester.tap(find.text('Auto')); +// await tester.pumpAndSettle(); +// +// expect(find.text('Settings'), findsOneWidget); +// expect(find.text('Auto'), findsOneWidget); +// }); +// group('Request permissions', () { +// setUpAll(() async { +// when(mockAuthProvider.currentUser).thenAnswer((_) => +// Future.value(User(uid: '0', firstName: 'John', lastName: 'Doe'))); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// }); +// +// testWidgets('Normal scenario', (WidgetTester tester) async { +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open settings +// await tester.tap(find.byIcon(Icons.settings_outlined)); +// await tester.pumpAndSettle(); +// +// // Open Ask Permissions page +// expect(find.text('No special permissions'), findsOneWidget); +// await tester.tap(find.byKey(const ValueKey('ask_permissions'))); +// await tester.pumpAndSettle(); +// expect(find.byType(RequestPermissionsPage), findsOneWidget); +// +// // Send a request +// await tester.enterText( +// find.byType(TextFormField), 'I love League of Legends'); +// await tester.tap(find.byType(Checkbox)); +// await tester.tap(find.text('Save')); +// await tester.pumpAndSettle(const Duration(seconds: 2)); +// +// // Verify the request is sent and Settings Page pops back +// verify(mockRequestProvider.makeRequest(any)); +// expect(find.byType(SettingsPage), findsOneWidget); +// }); +// +// testWidgets('User has already sent a request scenario', +// (WidgetTester tester) async { +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); +// when(mockRequestProvider.userAlreadyRequested(any)) +// .thenAnswer((_) => Future.value(true)); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open settings +// await tester.tap(find.byIcon(Icons.settings_outlined)); +// await tester.pumpAndSettle(); +// +// // Open Ask Permissions page +// expect(find.text('Permissions request already sent'), findsOneWidget); +// await tester.tap(find.byKey(const ValueKey('ask_permissions'))); +// await tester.pumpAndSettle(); +// expect(find.byType(RequestPermissionsPage), findsOneWidget); +// +// // Send a request +// await tester.enterText( +// find.byType(TextFormField), 'I love League of Legends'); +// await tester.tap(find.byType(Checkbox)); +// await tester.tap(find.text('Save')); +// await tester.pumpAndSettle(const Duration(seconds: 2)); +// +// // Check that warning Dialog appears and press Send +// expect(find.byType(AppDialog), findsOneWidget); +// await tester.tap(find.text('SEND')); +// await tester.pumpAndSettle(const Duration(seconds: 2)); +// +// // Verify the request is sent and Settings Page pops back +// verify(mockRequestProvider.makeRequest(any)); +// expect(find.byType(SettingsPage), findsOneWidget); +// }); +// +// testWidgets('User is anonymous scenario', (WidgetTester tester) async { +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); +// when(mockAuthProvider.isAnonymous).thenReturn(true); +// when(mockRequestProvider.userAlreadyRequested(any)) +// .thenAnswer((_) => Future.value(false)); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open settings +// await tester.tap(find.byIcon(Icons.settings_outlined)); +// await tester.pumpAndSettle(); +// +// // Press Ask Permissions page +// expect(find.text('No special permissions'), findsOneWidget); +// await tester.tap(find.byKey(const ValueKey('ask_permissions'))); +// await tester.pumpAndSettle(const Duration(seconds: 2)); +// +// // Verify nothing happens +// expect(find.byType(SettingsPage), findsOneWidget); +// }); +// +// testWidgets('User is not verified scenario', (WidgetTester tester) async { +// when(mockAuthProvider.isVerified) +// .thenAnswer((_) => Future.value(false)); +// when(mockAuthProvider.isAnonymous).thenReturn(false); +// when(mockRequestProvider.userAlreadyRequested(any)) +// .thenAnswer((_) => Future.value(false)); +// +// await tester.pumpWidget(buildApp()); +// await tester.pumpAndSettle(); +// +// // Open settings +// await tester.tap(find.byIcon(Icons.settings_outlined)); +// await tester.pumpAndSettle(); +// +// // Press Ask Permissions page +// expect(find.text('No special permissions'), findsOneWidget); +// await tester.tap(find.byKey(const ValueKey('ask_permissions'))); +// +// // Verify Ask Permissions page is not opened +// await tester.pumpAndSettle(const Duration(seconds: 4)); +// expect(find.byType(SettingsPage), findsOneWidget); +// expect(find.byType(RequestPermissionsPage), findsNothing); +// +// // Verify account +// when(mockAuthProvider.isVerified).thenAnswer((_) => Future.value(true)); +// +// // Go back and open settings again +// await tester.tap(find.byIcon(Icons.arrow_back)); +// await tester.pumpAndSettle(); +// await tester.tap(find.byIcon(Icons.settings_outlined)); +// await tester.pumpAndSettle(); +// +// // Press Ask Permissions page +// expect(find.text('No special permissions'), findsOneWidget); +// await tester.tap(find.byKey(const ValueKey('ask_permissions'))); +// +// // Verify Ask Permissions page is opened +// await tester.pumpAndSettle(); +// expect(find.byType(RequestPermissionsPage), findsOneWidget); +// }); +// }); +// }); +// } diff --git a/test/test_utils.dart b/test/test_utils.dart index ef22e5c10..83399293c 100644 --- a/test/test_utils.dart +++ b/test/test_utils.dart @@ -1,283 +1,283 @@ -import 'package:acs_upb_mobile/resources/locale_provider.dart'; -import 'package:meta/meta.dart'; -import 'package:rrule/src/codecs/text/l10n/l10n.dart'; -import 'package:rrule/src/frequency.dart'; -import 'package:time_machine/time_machine.dart'; - -var testCultures = { - 'en': Culture( - 'en-US', - (DateTimeFormatBuilder() - ..amDesignator = 'AM' - ..pmDesignator = 'PM' - ..timeSeparator = ':' - ..dateSeparator = '/' - ..abbreviatedDayNames = const [ - 'Sun', - 'Mon', - 'Tue', - 'Wed', - 'Thu', - 'Fri', - 'Sat' - ] - ..dayNames = const [ - 'Sunday', - 'Monday', - 'Tuesday', - 'Wednesday', - 'Thursday', - 'Friday', - 'Saturday' - ] - ..monthNames = const [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', - '' - ] - ..abbreviatedMonthNames = const [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - '' - ] - ..monthGenitiveNames = const [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', - '' - ] - ..abbreviatedMonthGenitiveNames = const [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - '' - ] - ..calendar = CalendarType.gregorian - ..eraNames = const ['AD'] - ..fullDateTimePattern = 'dddd, MMMM d, yyyy h:mm:ss tt' - ..shortDatePattern = 'M/d/yyyy' - ..longDatePattern = 'dddd, MMMM d, yyyy' - ..shortTimePattern = 'h:mm tt' - ..longTimePattern = 'h:mm:ss tt') - .Build()) -}; - -@immutable -class RruleL10nTest extends RruleL10n { - const RruleL10nTest._(Culture culture) : super(culture); - - static Future create() async => - RruleL10nTest._(LocaleProvider.cultures['en']); - - @override - String frequencyInterval(Frequency frequency, int interval) { - String plurals({String one, String singular}) { - switch (interval) { - case 1: - return one; - case 2: - return 'Every other $singular'; - default: - return 'Every $interval ${singular}s'; - } - } - - return { - Frequency.secondly: plurals(one: 'Secondly', singular: 'second'), - Frequency.minutely: plurals(one: 'Minutely', singular: 'minute'), - Frequency.hourly: plurals(one: 'Hourly', singular: 'hour'), - Frequency.daily: plurals(one: 'Daily', singular: 'day'), - Frequency.weekly: plurals(one: 'Weekly', singular: 'week'), - Frequency.monthly: plurals(one: 'Monthly', singular: 'month'), - Frequency.yearly: plurals(one: 'Annually', singular: 'year'), - }[frequency]; - } - - @override - String until(LocalDateTime until) => - ', until ${until.toString('F', culture)}'; - - @override - String count(int count) { - switch (count) { - case 1: - return ', once'; - case 2: - return ', twice'; - default: - return ', $count times'; - } - } - - @override - String onInstances(String instances) => 'on the $instances instance'; - - @override - String inMonths(String months, {InOnVariant variant = InOnVariant.simple}) => - '${_inVariant(variant)} $months'; - - @override - String inWeeks(String weeks, {InOnVariant variant = InOnVariant.simple}) => - '${_inVariant(variant)} the $weeks week of the year'; - - String _inVariant(InOnVariant variant) { - switch (variant) { - case InOnVariant.simple: - return 'in'; - case InOnVariant.also: - return 'that are also in'; - case InOnVariant.instanceOf: - return 'of'; - default: - assert(false); - return null; - } - } - - @override - String onDaysOfWeek( - String days, { - bool indicateFrequency = false, - DaysOfWeekFrequency frequency = DaysOfWeekFrequency.monthly, - InOnVariant variant = InOnVariant.simple, - }) { - assert(variant != InOnVariant.also); - - final frequencyString = - frequency == DaysOfWeekFrequency.monthly ? 'month' : 'year'; - final suffix = indicateFrequency ? ' of the $frequencyString' : ''; - return '${_onVariant(variant)} $days$suffix'; - } - - @override - String get weekdaysString => 'weekdays'; - - @override - String get everyXDaysOfWeekPrefix => 'every '; - - @override - String nthDaysOfWeek(Iterable occurrences, String daysOfWeek) { - if (occurrences.isEmpty) { - return daysOfWeek; - } else { - final ordinals = list( - occurrences.map(ordinal).toList(), ListCombination.conjunctiveShort); - return 'the $ordinals $daysOfWeek'; - } - } - - @override - String onDaysOfMonth( - String days, { - DaysOfVariant daysOfVariant = DaysOfVariant.dayAndFrequency, - InOnVariant variant = InOnVariant.simple, - }) { - final suffix = { - DaysOfVariant.simple: '', - DaysOfVariant.day: ' day', - DaysOfVariant.dayAndFrequency: ' day of the month', - }[daysOfVariant]; - return '${_onVariant(variant)} the $days$suffix'; - } - - @override - String onDaysOfYear( - String days, { - InOnVariant variant = InOnVariant.simple, - }) => - '${_onVariant(variant)} the $days day of the year'; - - String _onVariant(InOnVariant variant) { - switch (variant) { - case InOnVariant.simple: - return 'on'; - case InOnVariant.also: - return 'that are also'; - case InOnVariant.instanceOf: - return 'of'; - default: - assert(false); - return null; - } - } - - @override - String list(List items, ListCombination combination) { - assert(items != null); - assert(combination != null); - - return RruleL10n.defaultList( - items, - two: { - ListCombination.conjunctiveShort: ' & ', - ListCombination.conjunctiveLong: ' and ', - ListCombination.disjunctive: ' or ', - }[combination], - end: { - ListCombination.conjunctiveShort: ' & ', - ListCombination.conjunctiveLong: ', and ', - ListCombination.disjunctive: ', or ', - }[combination], - ); - } - - @override - String ordinal(int number) { - assert(number != 0); - if (number == -1) { - return 'last'; - } - - final n = number.abs(); - String string; - if (n % 10 == 1 && n % 100 != 11) { - string = '${n}st'; - } else if (n % 10 == 2 && n % 100 != 12) { - string = '${n}nd'; - } else if (n % 10 == 3 && n % 100 != 13) { - string = '${n}rd'; - } else { - string = '${n}th'; - } - - return number < 0 ? '$string-to-last' : string; - } -} +// import 'package:acs_upb_mobile/resources/locale_provider.dart'; +// import 'package:meta/meta.dart'; +// import 'package:rrule/src/codecs/text/l10n/l10n.dart'; +// import 'package:rrule/src/frequency.dart'; +// import 'package:time_machine/time_machine.dart'; +// +// var testCultures = { +// 'en': Culture( +// 'en-US', +// (DateTimeFormatBuilder() +// ..amDesignator = 'AM' +// ..pmDesignator = 'PM' +// ..timeSeparator = ':' +// ..dateSeparator = '/' +// ..abbreviatedDayNames = const [ +// 'Sun', +// 'Mon', +// 'Tue', +// 'Wed', +// 'Thu', +// 'Fri', +// 'Sat' +// ] +// ..dayNames = const [ +// 'Sunday', +// 'Monday', +// 'Tuesday', +// 'Wednesday', +// 'Thursday', +// 'Friday', +// 'Saturday' +// ] +// ..monthNames = const [ +// 'January', +// 'February', +// 'March', +// 'April', +// 'May', +// 'June', +// 'July', +// 'August', +// 'September', +// 'October', +// 'November', +// 'December', +// '' +// ] +// ..abbreviatedMonthNames = const [ +// 'Jan', +// 'Feb', +// 'Mar', +// 'Apr', +// 'May', +// 'Jun', +// 'Jul', +// 'Aug', +// 'Sep', +// 'Oct', +// 'Nov', +// 'Dec', +// '' +// ] +// ..monthGenitiveNames = const [ +// 'January', +// 'February', +// 'March', +// 'April', +// 'May', +// 'June', +// 'July', +// 'August', +// 'September', +// 'October', +// 'November', +// 'December', +// '' +// ] +// ..abbreviatedMonthGenitiveNames = const [ +// 'Jan', +// 'Feb', +// 'Mar', +// 'Apr', +// 'May', +// 'Jun', +// 'Jul', +// 'Aug', +// 'Sep', +// 'Oct', +// 'Nov', +// 'Dec', +// '' +// ] +// ..calendar = CalendarType.gregorian +// ..eraNames = const ['AD'] +// ..fullDateTimePattern = 'dddd, MMMM d, yyyy h:mm:ss tt' +// ..shortDatePattern = 'M/d/yyyy' +// ..longDatePattern = 'dddd, MMMM d, yyyy' +// ..shortTimePattern = 'h:mm tt' +// ..longTimePattern = 'h:mm:ss tt') +// .Build()) +// }; +// +// @immutable +// class RruleL10nTest extends RruleL10n { +// const RruleL10nTest._(Culture culture) : super(culture); +// +// static Future create() async => +// RruleL10nTest._(LocaleProvider.cultures['en']); +// +// @override +// String frequencyInterval(Frequency frequency, int interval) { +// String plurals({String one, String singular}) { +// switch (interval) { +// case 1: +// return one; +// case 2: +// return 'Every other $singular'; +// default: +// return 'Every $interval ${singular}s'; +// } +// } +// +// return { +// Frequency.secondly: plurals(one: 'Secondly', singular: 'second'), +// Frequency.minutely: plurals(one: 'Minutely', singular: 'minute'), +// Frequency.hourly: plurals(one: 'Hourly', singular: 'hour'), +// Frequency.daily: plurals(one: 'Daily', singular: 'day'), +// Frequency.weekly: plurals(one: 'Weekly', singular: 'week'), +// Frequency.monthly: plurals(one: 'Monthly', singular: 'month'), +// Frequency.yearly: plurals(one: 'Annually', singular: 'year'), +// }[frequency]; +// } +// +// @override +// String until(LocalDateTime until) => +// ', until ${until.toString('F', culture)}'; +// +// @override +// String count(int count) { +// switch (count) { +// case 1: +// return ', once'; +// case 2: +// return ', twice'; +// default: +// return ', $count times'; +// } +// } +// +// @override +// String onInstances(String instances) => 'on the $instances instance'; +// +// @override +// String inMonths(String months, {InOnVariant variant = InOnVariant.simple}) => +// '${_inVariant(variant)} $months'; +// +// @override +// String inWeeks(String weeks, {InOnVariant variant = InOnVariant.simple}) => +// '${_inVariant(variant)} the $weeks week of the year'; +// +// String _inVariant(InOnVariant variant) { +// switch (variant) { +// case InOnVariant.simple: +// return 'in'; +// case InOnVariant.also: +// return 'that are also in'; +// case InOnVariant.instanceOf: +// return 'of'; +// default: +// assert(false); +// return null; +// } +// } +// +// @override +// String onDaysOfWeek( +// String days, { +// bool indicateFrequency = false, +// DaysOfWeekFrequency frequency = DaysOfWeekFrequency.monthly, +// InOnVariant variant = InOnVariant.simple, +// }) { +// assert(variant != InOnVariant.also); +// +// final frequencyString = +// frequency == DaysOfWeekFrequency.monthly ? 'month' : 'year'; +// final suffix = indicateFrequency ? ' of the $frequencyString' : ''; +// return '${_onVariant(variant)} $days$suffix'; +// } +// +// @override +// String get weekdaysString => 'weekdays'; +// +// @override +// String get everyXDaysOfWeekPrefix => 'every '; +// +// @override +// String nthDaysOfWeek(Iterable occurrences, String daysOfWeek) { +// if (occurrences.isEmpty) { +// return daysOfWeek; +// } else { +// final ordinals = list( +// occurrences.map(ordinal).toList(), ListCombination.conjunctiveShort); +// return 'the $ordinals $daysOfWeek'; +// } +// } +// +// @override +// String onDaysOfMonth( +// String days, { +// DaysOfVariant daysOfVariant = DaysOfVariant.dayAndFrequency, +// InOnVariant variant = InOnVariant.simple, +// }) { +// final suffix = { +// DaysOfVariant.simple: '', +// DaysOfVariant.day: ' day', +// DaysOfVariant.dayAndFrequency: ' day of the month', +// }[daysOfVariant]; +// return '${_onVariant(variant)} the $days$suffix'; +// } +// +// @override +// String onDaysOfYear( +// String days, { +// InOnVariant variant = InOnVariant.simple, +// }) => +// '${_onVariant(variant)} the $days day of the year'; +// +// String _onVariant(InOnVariant variant) { +// switch (variant) { +// case InOnVariant.simple: +// return 'on'; +// case InOnVariant.also: +// return 'that are also'; +// case InOnVariant.instanceOf: +// return 'of'; +// default: +// assert(false); +// return null; +// } +// } +// +// @override +// String list(List items, ListCombination combination) { +// assert(items != null); +// assert(combination != null); +// +// return RruleL10n.defaultList( +// items, +// two: { +// ListCombination.conjunctiveShort: ' & ', +// ListCombination.conjunctiveLong: ' and ', +// ListCombination.disjunctive: ' or ', +// }[combination], +// end: { +// ListCombination.conjunctiveShort: ' & ', +// ListCombination.conjunctiveLong: ', and ', +// ListCombination.disjunctive: ', or ', +// }[combination], +// ); +// } +// +// @override +// String ordinal(int number) { +// assert(number != 0); +// if (number == -1) { +// return 'last'; +// } +// +// final n = number.abs(); +// String string; +// if (n % 10 == 1 && n % 100 != 11) { +// string = '${n}st'; +// } else if (n % 10 == 2 && n % 100 != 12) { +// string = '${n}nd'; +// } else if (n % 10 == 3 && n % 100 != 13) { +// string = '${n}rd'; +// } else { +// string = '${n}th'; +// } +// +// return number < 0 ? '$string-to-last' : string; +// } +// }