diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..3566b291 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,20 @@ +# This file configures the analyzer to use the lint rule set from `package:lint` + +# For apps, use the default set +include: package:lint/analysis_options.yaml + +# You might want to exclude auto-generated files from dart analysis +analyzer: + exclude: + - "**.g.dart" + +# You can customize the lint rules set to your own liking. A list of all rules +# can be found at https://dart-lang.github.io/linter/lints/options/options.html +linter: + rules: + constant_identifier_names: false + avoid_print: false + avoid_positional_boolean_parameters: false + use_build_context_synchronously: false + parameter_assignments: false + avoid_setters_without_getters: false diff --git a/lib/assets.dart b/lib/assets.dart new file mode 100644 index 00000000..bfec560e --- /dev/null +++ b/lib/assets.dart @@ -0,0 +1,36 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// Total assets: 9. +// Generated by https://pub.dev/packages/assets_gen. +// ************************************************************************** +class Assets { + static const String package = 'dhbwstudentapp'; + + static const String assets_app_icon_png = 'assets/app_icon.png'; + static const String dhbwstudentapp$assets_app_icon_png = 'packages/dhbwstudentapp/assets/app_icon.png'; + + static const String assets_empty_state_png = 'assets/empty_state.png'; + static const String dhbwstudentapp$assets_empty_state_png = 'packages/dhbwstudentapp/assets/empty_state.png'; + + static const String assets_onboarding_bottom_background_png = 'assets/onboarding_bottom_background.png'; + static const String dhbwstudentapp$assets_onboarding_bottom_background_png = 'packages/dhbwstudentapp/assets/onboarding_bottom_background.png'; + + static const String assets_onboarding_bottom_background_dark_png = 'assets/onboarding_bottom_background_dark.png'; + static const String dhbwstudentapp$assets_onboarding_bottom_background_dark_png = 'packages/dhbwstudentapp/assets/onboarding_bottom_background_dark.png'; + + static const String assets_onboarding_bottom_foreground_png = 'assets/onboarding_bottom_foreground.png'; + static const String dhbwstudentapp$assets_onboarding_bottom_foreground_png = 'packages/dhbwstudentapp/assets/onboarding_bottom_foreground.png'; + + static const String assets_onboarding_bottom_foreground_dark_png = 'assets/onboarding_bottom_foreground_dark.png'; + static const String dhbwstudentapp$assets_onboarding_bottom_foreground_dark_png = 'packages/dhbwstudentapp/assets/onboarding_bottom_foreground_dark.png'; + + static const String assets_schedule_empty_state_png = 'assets/schedule_empty_state.png'; + static const String dhbwstudentapp$assets_schedule_empty_state_png = 'packages/dhbwstudentapp/assets/schedule_empty_state.png'; + + static const String assets_schedule_empty_state_dark_png = 'assets/schedule_empty_state_dark.png'; + static const String dhbwstudentapp$assets_schedule_empty_state_dark_png = 'packages/dhbwstudentapp/assets/schedule_empty_state_dark.png'; + + static const String assets_splash_png = 'assets/splash.png'; + static const String dhbwstudentapp$assets_splash_png = 'packages/dhbwstudentapp/assets/splash.png'; +} diff --git a/lib/common/appstart/app_initializer.dart b/lib/common/appstart/app_initializer.dart index 98cebdd0..e614e19b 100644 --- a/lib/common/appstart/app_initializer.dart +++ b/lib/common/appstart/app_initializer.dart @@ -5,13 +5,13 @@ import 'package:dhbwstudentapp/common/appstart/localization_initialize.dart'; import 'package:dhbwstudentapp/common/appstart/notification_schedule_changed_initialize.dart'; import 'package:dhbwstudentapp/common/appstart/notifications_initialize.dart'; import 'package:dhbwstudentapp/common/appstart/service_injector.dart'; +import 'package:dhbwstudentapp/common/data/preferences/preferences_provider.dart'; import 'package:dhbwstudentapp/common/iap/in_app_purchase_manager.dart'; import 'package:dhbwstudentapp/native/widget/widget_update_callback.dart'; import 'package:dhbwstudentapp/schedule/background/calendar_synchronizer.dart'; +import 'package:dhbwstudentapp/schedule/business/schedule_provider.dart'; import 'package:dhbwstudentapp/schedule/business/schedule_source_provider.dart'; import 'package:kiwi/kiwi.dart'; -import 'package:dhbwstudentapp/schedule/business/schedule_provider.dart'; -import 'package:dhbwstudentapp/common/data/preferences/preferences_provider.dart'; import 'package:timezone/data/latest.dart' as tz; bool isInitialized = false; @@ -52,20 +52,21 @@ Future initializeApp(bool isBackground) async { WidgetUpdateCallback(KiwiContainer().resolve()) .registerCallback(KiwiContainer().resolve()); - NotificationsInitialize().setupNotifications(); - BackgroundInitialize().setupBackgroundScheduling(); - NotificationScheduleChangedInitialize().setupNotification(); + const NotificationsInitialize().setupNotifications(); + const BackgroundInitialize().setupBackgroundScheduling(); + const NotificationScheduleChangedInitialize().setupNotification(); if (isBackground) { - var setup = KiwiContainer().resolve(); + final setup = KiwiContainer().resolve(); setup.setupScheduleSource(); } // Callback-Function for synchronizing the device calendar with the schedule, when schedule is updated - CalendarSynchronizer calendarSynchronizer = new CalendarSynchronizer( - KiwiContainer().resolve(), - KiwiContainer().resolve(), - KiwiContainer().resolve()); + final CalendarSynchronizer calendarSynchronizer = CalendarSynchronizer( + KiwiContainer().resolve(), + KiwiContainer().resolve(), + KiwiContainer().resolve(), + ); calendarSynchronizer.registerSynchronizationCallback(); calendarSynchronizer.scheduleSyncInAFewSeconds(); diff --git a/lib/common/appstart/background_initialize.dart b/lib/common/appstart/background_initialize.dart index 0b3dbb31..6b5f0c26 100644 --- a/lib/common/appstart/background_initialize.dart +++ b/lib/common/appstart/background_initialize.dart @@ -12,6 +12,8 @@ import 'package:kiwi/kiwi.dart'; /// Note: More or less reliable background scheduling only works on android /// class BackgroundInitialize { + const BackgroundInitialize(); + Future setupBackgroundScheduling() async { WorkSchedulerService scheduler; if (Platform.isAndroid) { @@ -22,7 +24,7 @@ class BackgroundInitialize { KiwiContainer().registerInstance(scheduler); - var tasks = [ + final tasks = [ BackgroundScheduleUpdate( KiwiContainer().resolve(), KiwiContainer().resolve(), @@ -37,7 +39,7 @@ class BackgroundInitialize { ), ]; - for (var task in tasks) { + for (final task in tasks) { scheduler.registerTask(task); KiwiContainer().registerInstance( diff --git a/lib/common/appstart/localization_initialize.dart b/lib/common/appstart/localization_initialize.dart index c773fd90..97c0efbb 100644 --- a/lib/common/appstart/localization_initialize.dart +++ b/lib/common/appstart/localization_initialize.dart @@ -9,24 +9,28 @@ import 'package:kiwi/kiwi.dart'; /// correct language /// class LocalizationInitialize { - PreferencesProvider _preferencesProvider; - String _languageCode; + final PreferencesProvider? _preferencesProvider; + final String? _languageCode; /// /// Initialize the localization using the provided language code /// - LocalizationInitialize.fromLanguageCode(this._languageCode); + const LocalizationInitialize.fromLanguageCode(this._languageCode) + : _preferencesProvider = null; /// /// Initialize the localization using the locale from the preferences provider /// - LocalizationInitialize.fromPreferences(this._preferencesProvider); + const LocalizationInitialize.fromPreferences(this._preferencesProvider) + : _languageCode = null; Future setupLocalizations() async { - var localization = L(Locale( - _languageCode ?? - (await _preferencesProvider?.getLastUsedLanguageCode() ?? "en"), - )); + final localization = L( + Locale( + _languageCode ?? + (await _preferencesProvider?.getLastUsedLanguageCode() ?? "en"), + ), + ); KiwiContainer().registerInstance(localization); } } diff --git a/lib/common/appstart/notification_schedule_changed_initialize.dart b/lib/common/appstart/notification_schedule_changed_initialize.dart index 4d1728f5..f29b69d6 100644 --- a/lib/common/appstart/notification_schedule_changed_initialize.dart +++ b/lib/common/appstart/notification_schedule_changed_initialize.dart @@ -8,19 +8,21 @@ import 'package:kiwi/kiwi.dart'; /// Initializes the notification for when the schedule changed /// class NotificationScheduleChangedInitialize { + const NotificationScheduleChangedInitialize(); + void setupNotification() { - var provider = KiwiContainer().resolve(); + final provider = KiwiContainer().resolve(); provider.addScheduleEntryChangedCallback(_scheduleChangedCallback); } Future _scheduleChangedCallback(ScheduleDiff scheduleDiff) async { - PreferencesProvider preferences = KiwiContainer().resolve(); - var doNotify = await preferences.getNotifyAboutScheduleChanges(); + final PreferencesProvider preferences = KiwiContainer().resolve(); + final doNotify = await preferences.getNotifyAboutScheduleChanges(); if (!doNotify) return; - var notification = ScheduleChangedNotification( + final notification = ScheduleChangedNotification( KiwiContainer().resolve(), KiwiContainer().resolve(), ); diff --git a/lib/common/appstart/notifications_initialize.dart b/lib/common/appstart/notifications_initialize.dart index 2fce4c9f..0bf419c7 100644 --- a/lib/common/appstart/notifications_initialize.dart +++ b/lib/common/appstart/notifications_initialize.dart @@ -9,15 +9,18 @@ import 'package:kiwi/kiwi.dart'; /// won't show the notification /// class NotificationsInitialize { + const NotificationsInitialize(); + Future setupNotifications() async { if (Platform.isAndroid) { - var notificationApi = NotificationApi(); + final notificationApi = NotificationApi(); KiwiContainer().registerInstance(notificationApi); await notificationApi.initialize(); } else { - KiwiContainer().registerInstance(VoidNotificationApi()); + KiwiContainer() + .registerInstance(const VoidNotificationApi()); } } } diff --git a/lib/common/appstart/service_injector.dart b/lib/common/appstart/service_injector.dart index 3d3f6bc9..e5c49f77 100644 --- a/lib/common/appstart/service_injector.dart +++ b/lib/common/appstart/service_injector.dart @@ -28,46 +28,62 @@ bool _isInjected = false; void injectServices(bool isBackground) { if (_isInjected) return; - KiwiContainer c = KiwiContainer(); - c.registerInstance(PreferencesProvider( - PreferencesAccess(), - SecureStorageAccess(), - )); - c.registerInstance(DatabaseAccess()); - c.registerInstance(ScheduleEntryRepository( - c.resolve(), - )); - c.registerInstance(ScheduleFilterRepository( - c.resolve(), - )); - c.registerInstance(ScheduleQueryInformationRepository( - c.resolve(), - )); - c.registerInstance(ScheduleSourceProvider( - c.resolve(), - isBackground, - c.resolve(), - c.resolve(), - )); - c.registerInstance(ScheduleProvider( - c.resolve(), - c.resolve(), - c.resolve(), - c.resolve(), - c.resolve(), - )); + final KiwiContainer c = KiwiContainer(); + c.registerInstance( + const PreferencesProvider( + PreferencesAccess(), + SecureStorageAccess(), + ), + ); + c.registerInstance(const DatabaseAccess()); + c.registerInstance( + ScheduleEntryRepository( + c.resolve(), + ), + ); + c.registerInstance( + ScheduleFilterRepository( + c.resolve(), + ), + ); + c.registerInstance( + ScheduleQueryInformationRepository( + c.resolve(), + ), + ); + c.registerInstance( + ScheduleSourceProvider( + c.resolve(), + isBackground, + c.resolve(), + c.resolve(), + ), + ); + c.registerInstance( + ScheduleProvider( + c.resolve(), + c.resolve(), + c.resolve(), + c.resolve(), + c.resolve(), + ), + ); c.registerInstance( FakeAccountDualisScraperDecorator(DualisScraper()), ); - c.registerInstance(CacheDualisServiceDecorator( - DualisServiceImpl( - c.resolve(), + c.registerInstance( + CacheDualisServiceDecorator( + DualisServiceImpl( + c.resolve(), + ), ), - )); - c.registerInstance(DateEntryProvider( - DateManagementService(), - DateEntryRepository(c.resolve()), - )); + ); + c.registerInstance( + DateEntryProvider( + const DateManagementService(), + DateEntryRepository(c.resolve()), + ), + ); c.registerInstance(WidgetHelper()); c.registerInstance(ListDateEntries30d(List.empty(growable: true))); diff --git a/lib/common/background/background_work_scheduler.dart b/lib/common/background/background_work_scheduler.dart index 76f6e14f..f84c0312 100644 --- a/lib/common/background/background_work_scheduler.dart +++ b/lib/common/background/background_work_scheduler.dart @@ -23,7 +23,10 @@ class BackgroundWorkScheduler extends WorkSchedulerService { /// @override Future scheduleOneShotTaskIn( - Duration delay, String id, String name) async { + Duration delay, + String id, + String name, + ) async { print( "Scheduling one shot task: $id. With a delay of ${delay.inMinutes} minutes.", ); @@ -103,13 +106,16 @@ class BackgroundWorkScheduler extends WorkSchedulerService { /// /// Entry point for when a background task is executed /// - static Future backgroundTaskMain(taskId, inputData) async { + static Future backgroundTaskMain( + String taskId, + Map? inputData, + ) async { try { print("Background task started: $taskId with data: $inputData"); await initializeApp(true); - WorkSchedulerService scheduler = KiwiContainer().resolve(); + final WorkSchedulerService scheduler = KiwiContainer().resolve(); await scheduler.executeTask(taskId); } catch (e, trace) { @@ -129,7 +135,6 @@ class BackgroundWorkScheduler extends WorkSchedulerService { await workmanager.initialize( callbackDispatcher, - isInDebugMode: false, ); } diff --git a/lib/common/background/task_callback.dart b/lib/common/background/task_callback.dart index e2a36a1e..dc3e63cf 100644 --- a/lib/common/background/task_callback.dart +++ b/lib/common/background/task_callback.dart @@ -2,6 +2,8 @@ /// Override this class in order to receive a background callback /// abstract class TaskCallback { + const TaskCallback(); + Future run(); Future schedule(); diff --git a/lib/common/background/work_scheduler_service.dart b/lib/common/background/work_scheduler_service.dart index d11baa58..e692db22 100644 --- a/lib/common/background/work_scheduler_service.dart +++ b/lib/common/background/work_scheduler_service.dart @@ -1,6 +1,8 @@ import 'package:dhbwstudentapp/common/background/task_callback.dart'; abstract class WorkSchedulerService { + const WorkSchedulerService(); + Future scheduleOneShotTaskIn(Duration delay, String id, String name); Future scheduleOneShotTaskAt( diff --git a/lib/common/data/database_access.dart b/lib/common/data/database_access.dart index 8dfd9819..87dbb25c 100644 --- a/lib/common/data/database_access.dart +++ b/lib/common/data/database_access.dart @@ -3,25 +3,26 @@ import 'package:dhbwstudentapp/common/data/sql_scripts.dart'; import 'package:sqflite/sqflite.dart'; class DatabaseAccess { + const DatabaseAccess(); + static const String _databaseName = "Database.db"; - static Database _databaseInstance; + static Database? _databaseInstance; static const String idColumnName = "id"; Future get _database async { - if (_databaseInstance != null) return _databaseInstance; - - _databaseInstance = await _initDatabase(); - return _databaseInstance; + return _databaseInstance ??= await _initDatabase(); } Future _initDatabase() async { final String path = await getDatabasePath(_databaseName); - return await openDatabase(path, - version: SqlScripts.databaseMigrationScripts.length, - onCreate: _onCreate, - onUpgrade: _onUpgrade); + return openDatabase( + path, + version: SqlScripts.databaseMigrationScripts.length, + onCreate: _onCreate, + onUpgrade: _onUpgrade, + ); } Future _onCreate(Database db, int version) async { @@ -36,7 +37,7 @@ class DatabaseAccess { for (var i = oldVersion; i <= newVersion; i++) { print(" -> Execute migration to $i"); - for (var s in SqlScripts.databaseMigrationScripts[i - 1]) { + for (final s in SqlScripts.databaseMigrationScripts[i - 1]) { print(" -> $s"); await db.execute(s); } @@ -44,38 +45,43 @@ class DatabaseAccess { } Future insert(String table, Map row) async { - Database db = await _database; - return await db.insert(table, row); + final Database db = await _database; + return db.insert(table, row); } Future>> queryAllRows(String table) async { - Database db = await _database; - return await db.query(table); + final Database db = await _database; + return db.query(table); } Future>> rawQuery( - String sql, List parameters) async { - Database db = await _database; - return await db.rawQuery(sql, parameters); + String sql, + List parameters, + ) async { + final Database db = await _database; + return db.rawQuery(sql, parameters); } - Future>> queryRows(String table, - {bool distinct, - List columns, - String where, - List whereArgs, - String groupBy, - String having, - String orderBy, - int limit, - int offset}) async { - Database db = await _database; + Future>> queryRows( + String table, { + bool? distinct, + List? columns, + String? where, + List? whereArgs, + String? groupBy, + String? having, + String? orderBy, + int? limit, + int? offset, + }) async { + final Database db = await _database; + // TODO: [Leptopoda] is there a reason this is done? or at maybe use whereArgs.removeWhere() for (int i = 0; i < (whereArgs?.length ?? 0); i++) { - whereArgs[i] = whereArgs[i] ?? ""; + whereArgs![i] = whereArgs[i] ?? ""; } - return await db.query( + return db.query( table, distinct: distinct, columns: columns, @@ -89,36 +95,36 @@ class DatabaseAccess { ); } - Future queryRowCount(String table) async { - Database db = await _database; + Future queryRowCount(String table) async { + final Database db = await _database; return Sqflite.firstIntValue( - await db.rawQuery('SELECT COUNT(*) FROM $table')); + await db.rawQuery('SELECT COUNT(*) FROM $table'), + ); } - Future queryAggregator(String query, List arguments) async { - Database db = await _database; + Future queryAggregator(String query, List arguments) async { + final Database db = await _database; return Sqflite.firstIntValue(await db.rawQuery(query, arguments)); } Future update(String table, Map row) async { - Database db = await _database; - int id = row[idColumnName]; - return await db - .update(table, row, where: '$idColumnName = ?', whereArgs: [id]); + final Database db = await _database; + final id = row[idColumnName]; + return db.update(table, row, where: '$idColumnName = ?', whereArgs: [id]); } - Future delete(String table, int id) async { - Database db = await _database; - return await db.delete(table, where: '$idColumnName = ?', whereArgs: [id]); + Future delete(String table, int? id) async { + final Database db = await _database; + return db.delete(table, where: '$idColumnName = ?', whereArgs: [id]); } Future deleteWhere( String table, { - String where, - List whereArgs, + String? where, + List? whereArgs, }) async { - Database db = await _database; - return await db.delete( + final Database db = await _database; + return db.delete( table, where: where, whereArgs: whereArgs, diff --git a/lib/common/data/database_entity.dart b/lib/common/data/database_entity.dart deleted file mode 100644 index 121892b8..00000000 --- a/lib/common/data/database_entity.dart +++ /dev/null @@ -1,4 +0,0 @@ -abstract class DatabaseEntity { - Map toMap(); - void fromMap(Map map); -} diff --git a/lib/common/data/database_path_provider.dart b/lib/common/data/database_path_provider.dart index 6d78badc..d89ca77a 100644 --- a/lib/common/data/database_path_provider.dart +++ b/lib/common/data/database_path_provider.dart @@ -1,11 +1,11 @@ import 'dart:io'; -import 'package:path_provider/path_provider.dart'; +import 'package:app_group_directory/app_group_directory.dart'; import 'package:path/path.dart'; -import 'package:ios_app_group/ios_app_group.dart'; +import 'package:path_provider/path_provider.dart'; Future getDatabasePath(String databaseName) async { - Directory documentsDirectory = await getApplicationDocumentsDirectory(); + final Directory documentsDirectory = await getApplicationDocumentsDirectory(); var path = join(documentsDirectory.path, databaseName); @@ -24,14 +24,16 @@ Future _getiOSDatabasePathAndMigrate( // it must be saved in a group shared between the app module and the widget // module. "Migration" means that the database at the old path gets // copied to the new path + assert(Platform.isIOS); - var groupDirectory = await IosAppGroup.getAppGroupDirectory( + final Directory? groupDirectory = + await AppGroupDirectory.getAppGroupDirectory( 'group.de.bennik2000.dhbwstudentapp', ); - var newPath = join(groupDirectory.path, databaseName); + final newPath = join(groupDirectory!.path, databaseName); - var migrateSuccess = await _migrateOldDatabase(oldPath, newPath); + final migrateSuccess = await _migrateOldDatabase(oldPath, newPath); if (!migrateSuccess) { print("Failed to migrate database"); @@ -42,8 +44,8 @@ Future _getiOSDatabasePathAndMigrate( Future _migrateOldDatabase(String oldPath, String newPath) async { try { - var newFile = File(newPath); - var oldFile = File(oldPath); + final newFile = File(newPath); + final oldFile = File(oldPath); if (await oldFile.exists() && !(await newFile.exists())) { print("Migrating database..."); diff --git a/lib/common/data/epoch_date_time_converter.dart b/lib/common/data/epoch_date_time_converter.dart new file mode 100644 index 00000000..555731ba --- /dev/null +++ b/lib/common/data/epoch_date_time_converter.dart @@ -0,0 +1,11 @@ +import 'package:json_annotation/json_annotation.dart'; + +class EpochDateTimeConverter implements JsonConverter { + const EpochDateTimeConverter(); + + @override + DateTime fromJson(int json) => DateTime.fromMillisecondsSinceEpoch(json); + + @override + int toJson(DateTime object) => object.millisecondsSinceEpoch; +} diff --git a/lib/common/data/preferences/app_theme_enum.dart b/lib/common/data/preferences/app_theme_enum.dart deleted file mode 100644 index 01aab80e..00000000 --- a/lib/common/data/preferences/app_theme_enum.dart +++ /dev/null @@ -1,9 +0,0 @@ - -/// -/// Enum which holds the possible themes the app can be displayed in -/// -enum AppTheme { - Dark, - Light, - System -} \ No newline at end of file diff --git a/lib/common/data/preferences/preferences_access.dart b/lib/common/data/preferences/preferences_access.dart index 20cc4c9c..08465854 100644 --- a/lib/common/data/preferences/preferences_access.dart +++ b/lib/common/data/preferences/preferences_access.dart @@ -1,6 +1,8 @@ import 'package:shared_preferences/shared_preferences.dart'; class PreferencesAccess { + const PreferencesAccess(); + Future set(String key, T value) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); @@ -17,37 +19,31 @@ class PreferencesAccess { case int: await prefs.setInt(key, value as int); return; + default: + throw InvalidValueTypeException(T); } - - throw InvalidValueTypeException(T); } - Future get(String key) async { + Future get(String key) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - T value; - switch (T) { case bool: - value = prefs.getBool(key) as T; - break; + return prefs.getBool(key) as T?; case String: - value = prefs.getString(key) as T; - break; + return prefs.getString(key) as T?; case double: - value = prefs.getDouble(key) as T; - break; + return prefs.getDouble(key) as T?; case int: - value = prefs.getInt(key) as T; - break; + return prefs.getInt(key) as T?; + default: + return null; } - - return value; } } class InvalidValueTypeException implements Exception { final Type type; - InvalidValueTypeException(this.type); + const InvalidValueTypeException(this.type); } diff --git a/lib/common/data/preferences/preferences_provider.dart b/lib/common/data/preferences/preferences_provider.dart index 5c6beaf5..2570bd5c 100644 --- a/lib/common/data/preferences/preferences_provider.dart +++ b/lib/common/data/preferences/preferences_provider.dart @@ -1,10 +1,10 @@ import 'package:device_calendar/device_calendar.dart'; import 'package:dhbwstudentapp/common/application_constants.dart'; -import 'package:dhbwstudentapp/common/data/preferences/app_theme_enum.dart'; import 'package:dhbwstudentapp/common/data/preferences/preferences_access.dart'; import 'package:dhbwstudentapp/common/data/preferences/secure_storage_access.dart'; import 'package:dhbwstudentapp/date_management/data/calendar_access.dart'; import 'package:dhbwstudentapp/dualis/model/credentials.dart'; +import 'package:flutter/material.dart'; class PreferencesProvider { static const String AppThemeKey = "AppTheme"; @@ -33,72 +33,79 @@ class PreferencesProvider { final PreferencesAccess _preferencesAccess; final SecureStorageAccess _secureStorageAccess; - PreferencesProvider(this._preferencesAccess, this._secureStorageAccess); + const PreferencesProvider(this._preferencesAccess, this._secureStorageAccess); - Future appTheme() async { - var theme = await _preferencesAccess.get(AppThemeKey); + Future appTheme() async { + final theme = await _preferencesAccess.get(AppThemeKey); + final themeName = theme?.toLowerCase(); - return AppTheme.values.firstWhere( - (element) => element.name == theme, + return ThemeMode.values.firstWhere( + (element) => element.name == themeName, orElse: () { - return AppTheme.System; + return ThemeMode.system; }, ); } - Future setAppTheme(AppTheme value) async { + Future setAppTheme(ThemeMode value) async { await _preferencesAccess.set(AppThemeKey, value.name); } Future setIsCalendarSyncEnabled(bool value) async { - await _preferencesAccess.set('isCalendarSyncEnabled', value); + await _preferencesAccess.set('isCalendarSyncEnabled', value); } Future isCalendarSyncEnabled() async { - return await _preferencesAccess.get('isCalendarSyncEnabled') ?? false; + return await _preferencesAccess.get('isCalendarSyncEnabled') ?? false; } - Future setSelectedCalendar(Calendar selectedCalendar) async { - String selectedCalendarId = selectedCalendar?.id; - await _preferencesAccess.set( - 'SelectedCalendarId', selectedCalendarId ?? ''); + Future setSelectedCalendar(Calendar? selectedCalendar) async { + final selectedCalendarId = selectedCalendar?.id ?? ""; + await _preferencesAccess.set( + 'SelectedCalendarId', + selectedCalendarId, + ); } - Future getSelectedCalendar() async { - Calendar selectedCalendar; - String selectedCalendarId = - await _preferencesAccess.get('SelectedCalendarId') ?? null; - if (selectedCalendarId == null) return null; - List availableCalendars = + Future getSelectedCalendar() async { + Calendar? selectedCalendar; + final String? selectedCalendarId = + await _preferencesAccess.get('SelectedCalendarId'); + final List? availableCalendars = await CalendarAccess().queryWriteableCalendars(); - availableCalendars.forEach((cal) => { - if (cal.id == selectedCalendarId) {selectedCalendar = cal} - }); + if (selectedCalendarId == null || availableCalendars == null) return null; + for (final cal in availableCalendars) { + { + if (cal.id == selectedCalendarId) { + selectedCalendar = cal; + } + } + } return selectedCalendar; } Future getRaplaUrl() async { - return await _preferencesAccess.get(RaplaUrlKey) ?? ""; + return await _preferencesAccess.get(RaplaUrlKey) ?? ""; } Future setRaplaUrl(String url) async { - await _preferencesAccess.set(RaplaUrlKey, url); + await _preferencesAccess.set(RaplaUrlKey, url); } Future isFirstStart() async { - return await _preferencesAccess.get(IsFirstStartKey) ?? true; + return await _preferencesAccess.get(IsFirstStartKey) ?? true; } Future setIsFirstStart(bool isFirstStart) async { - await _preferencesAccess.set(IsFirstStartKey, isFirstStart); + await _preferencesAccess.set(IsFirstStartKey, isFirstStart); } - Future getLastUsedLanguageCode() async { - return await _preferencesAccess.get(LastUsedLanguageCode); + Future getLastUsedLanguageCode() async { + return _preferencesAccess.get(LastUsedLanguageCode); } Future setLastUsedLanguageCode(String languageCode) async { - await _preferencesAccess.set(LastUsedLanguageCode, languageCode); + await _preferencesAccess.set(LastUsedLanguageCode, languageCode); } Future getNotifyAboutNextDay() async { @@ -106,7 +113,7 @@ class PreferencesProvider { } Future setNotifyAboutNextDay(bool value) async { - await _preferencesAccess.set(NotifyAboutNextDay, value); + await _preferencesAccess.set(NotifyAboutNextDay, value); } Future getNotifyAboutScheduleChanges() async { @@ -115,7 +122,7 @@ class PreferencesProvider { } Future setNotifyAboutScheduleChanges(bool value) async { - await _preferencesAccess.set(NotifyAboutScheduleChanges, value); + await _preferencesAccess.set(NotifyAboutScheduleChanges, value); } Future getDontShowRateNowDialog() async { @@ -123,17 +130,22 @@ class PreferencesProvider { } Future setDontShowRateNowDialog(bool value) async { - await _preferencesAccess.set(DontShowRateNowDialog, value); + await _preferencesAccess.set(DontShowRateNowDialog, value); } Future storeDualisCredentials(Credentials credentials) async { - await _secureStorageAccess.set(DualisUsername, credentials.username ?? ""); - await _secureStorageAccess.set(DualisPassword, credentials.password ?? ""); + await _secureStorageAccess.set(DualisUsername, credentials.username); + await _secureStorageAccess.set(DualisPassword, credentials.password); } - Future loadDualisCredentials() async { - var username = await _secureStorageAccess.get(DualisUsername); - var password = await _secureStorageAccess.get(DualisPassword); + Future loadDualisCredentials() async { + final username = await _secureStorageAccess.get(DualisUsername); + final password = await _secureStorageAccess.get(DualisPassword); + + if (username == null || + password == null || + username.isEmpty || + password.isEmpty) return null; return Credentials(username, password); } @@ -147,30 +159,38 @@ class PreferencesProvider { } Future setStoreDualisCredentials(bool value) async { - await _preferencesAccess.set(DualisStoreCredentials, value ?? false); + await _preferencesAccess.set(DualisStoreCredentials, value); } - Future getLastViewedSemester() async { - return await _preferencesAccess.get(LastViewedSemester); + Future getLastViewedSemester() async { + return _preferencesAccess.get(LastViewedSemester); } - Future setLastViewedSemester(String lastViewedSemester) async { - await _preferencesAccess.set(LastViewedSemester, lastViewedSemester); + Future setLastViewedSemester(String? lastViewedSemester) async { + if (lastViewedSemester == null) return; + await _preferencesAccess.set( + LastViewedSemester, + lastViewedSemester, + ); } - Future getLastViewedDateEntryDatabase() async { - return await _preferencesAccess.get(LastViewedDateEntryDatabase); + Future getLastViewedDateEntryDatabase() async { + return _preferencesAccess.get(LastViewedDateEntryDatabase); } - Future setLastViewedDateEntryDatabase(String value) async { - await _preferencesAccess.set(LastViewedDateEntryDatabase, value); + Future setLastViewedDateEntryDatabase(String? value) async { + await _preferencesAccess.set( + LastViewedDateEntryDatabase, + value ?? "", + ); } - Future getLastViewedDateEntryYear() async { - return await _preferencesAccess.get(LastViewedDateEntryYear); + Future getLastViewedDateEntryYear() async { + return _preferencesAccess.get(LastViewedDateEntryYear); } - Future setLastViewedDateEntryYear(String value) async { + Future setLastViewedDateEntryYear(String? value) async { + if (value == null) return; await _preferencesAccess.set(LastViewedDateEntryYear, value); } @@ -182,85 +202,87 @@ class PreferencesProvider { await _preferencesAccess.set(ScheduleSourceType, value); } - Future getIcalUrl() { - return _preferencesAccess.get(ScheduleIcalUrl); + Future getIcalUrl() { + return _preferencesAccess.get(ScheduleIcalUrl); } Future setIcalUrl(String url) { - return _preferencesAccess.set(ScheduleIcalUrl, url); + return _preferencesAccess.set(ScheduleIcalUrl, url); } - Future getMannheimScheduleId() { - return _preferencesAccess.get(MannheimScheduleId); + Future getMannheimScheduleId() { + return _preferencesAccess.get(MannheimScheduleId); } Future setMannheimScheduleId(String url) { - return _preferencesAccess.set(MannheimScheduleId, url); + return _preferencesAccess.set(MannheimScheduleId, url); } Future getPrettifySchedule() async { - return await _preferencesAccess.get(PrettifySchedule) ?? true; + return await _preferencesAccess.get(PrettifySchedule) ?? true; } Future setPrettifySchedule(bool value) { - return _preferencesAccess.set(PrettifySchedule, value); + return _preferencesAccess.set(PrettifySchedule, value); } Future getSynchronizeScheduleWithCalendar() async { - return await _preferencesAccess.get(SynchronizeScheduleWithCalendar) ?? + return await _preferencesAccess + .get(SynchronizeScheduleWithCalendar) ?? true; } Future setSynchronizeScheduleWithCalendar(bool value) { - return _preferencesAccess.set(SynchronizeScheduleWithCalendar, value); + return _preferencesAccess.set(SynchronizeScheduleWithCalendar, value); } Future getDidShowWidgetHelpDialog() async { - return await _preferencesAccess.get(DidShowWidgetHelpDialog) ?? false; + return await _preferencesAccess.get(DidShowWidgetHelpDialog) ?? false; } Future setDidShowWidgetHelpDialog(bool value) { - return _preferencesAccess.set(DidShowWidgetHelpDialog, value); + return _preferencesAccess.set(DidShowWidgetHelpDialog, value); } Future set(String key, T value) async { + if (value == null) return; return _preferencesAccess.set(key, value); } - Future get(String key) async { - return _preferencesAccess.get(key); + Future get(String key) async { + return _preferencesAccess.get(key); } Future getAppLaunchCounter() async { - return await _preferencesAccess.get("AppLaunchCount") ?? 0; + return await _preferencesAccess.get("AppLaunchCount") ?? 0; } Future setAppLaunchCounter(int value) async { - return await _preferencesAccess.set("AppLaunchCount", value); + return _preferencesAccess.set("AppLaunchCount", value); } Future getNextRateInStoreLaunchCount() async { - return await _preferencesAccess.get("NextRateInStoreLaunchCount") ?? + return await _preferencesAccess.get("NextRateInStoreLaunchCount") ?? RateInStoreLaunchAfter; } Future setNextRateInStoreLaunchCount(int value) async { - return await _preferencesAccess.set("NextRateInStoreLaunchCount", value); + return _preferencesAccess.set("NextRateInStoreLaunchCount", value); } Future getDidShowDonateDialog() async { - return await _preferencesAccess.get("DidShowDonateDialog") ?? false; + return await _preferencesAccess.get("DidShowDonateDialog") ?? false; } Future setDidShowDonateDialog(bool value) { - return _preferencesAccess.set("DidShowDonateDialog", value); + return _preferencesAccess.set("DidShowDonateDialog", value); } Future getHasPurchasedSomething() async { - return await _preferencesAccess.get("HasPurchasedSomething") ?? false; + return await _preferencesAccess.get("HasPurchasedSomething") ?? false; } Future setHasPurchasedSomething(bool value) { - return _preferencesAccess.set("HasPurchasedSomething", value); + return _preferencesAccess.set("HasPurchasedSomething", value); } } diff --git a/lib/common/data/preferences/secure_storage_access.dart b/lib/common/data/preferences/secure_storage_access.dart index ec8e7442..e8fe4da4 100644 --- a/lib/common/data/preferences/secure_storage_access.dart +++ b/lib/common/data/preferences/secure_storage_access.dart @@ -1,13 +1,15 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class SecureStorageAccess { - final _secureStorage = const FlutterSecureStorage(); + static const _secureStorage = FlutterSecureStorage(); + + const SecureStorageAccess(); Future set(String key, String value) async { await _secureStorage.write(key: key, value: value); } - Future get(String key) async { - return await _secureStorage.read(key: key); + Future get(String key) async { + return _secureStorage.read(key: key); } } diff --git a/lib/common/data/sql_scripts.dart b/lib/common/data/sql_scripts.dart index 108a991e..22c79fa9 100644 --- a/lib/common/data/sql_scripts.dart +++ b/lib/common/data/sql_scripts.dart @@ -1,6 +1,9 @@ +// ignore_for_file: leading_newlines_in_multiline_strings + import 'dart:core'; class SqlScripts { + const SqlScripts(); static final databaseMigrationScripts = [ // Version 1 - init database [ diff --git a/lib/common/i18n/localization_strings_de.dart b/lib/common/i18n/localization_strings_de.dart index 252116e8..753959be 100644 --- a/lib/common/i18n/localization_strings_de.dart +++ b/lib/common/i18n/localization_strings_de.dart @@ -5,7 +5,8 @@ final de = { "settingsViewSourceCode": "Source code auf GitHub ansehen", "settingsCalendarSync": "Kalendersynchronisation", "calendarSyncPageTitle": "Kalender synchronisieren", - "calendarSyncPageSubtitle": "Wähle den Kalender aus, in den der Vorlesungsplan übertragen werden soll:", + "calendarSyncPageSubtitle": + "Wähle den Kalender aus, in den der Vorlesungsplan übertragen werden soll:", "calendarSyncPageEndSync": "Synchronisation beenden", "calendarSyncPageBeginSync": "Kalender synchronisieren", "applicationName": "DHBW Studenten App", @@ -114,7 +115,8 @@ final de = { "dialogOk": "Ok", "dialogCancel": "Cancel", "dialogSetRaplaUrlTitle": "Rapla Url festlegen", - "dialogCalendarAccessNotGranted": "Wenn du den Vorlesungsplan mit deinem nativen Kalender synchronisieren möchtest, musst du in den Einstellungen den Kalenderzugriff erlauben.", + "dialogCalendarAccessNotGranted": + "Wenn du den Vorlesungsplan mit deinem nativen Kalender synchronisieren möchtest, musst du in den Einstellungen den Kalenderzugriff erlauben.", "dialogTitleCalendarAccessNotGranted": "Keine Zugriffsrechte", "scheduleEmptyStateSetUrl": "Konfigurieren", "scheduleEmptyStateBannerMessage": diff --git a/lib/common/i18n/localizations.dart b/lib/common/i18n/localizations.dart index c43f5842..fb82fdbc 100644 --- a/lib/common/i18n/localizations.dart +++ b/lib/common/i18n/localizations.dart @@ -6,10 +6,10 @@ import 'package:flutter/widgets.dart'; class L { final Locale locale; - String _language; + late String _language; L(this.locale) { - _language = locale?.languageCode?.substring(0, 2); + _language = locale.languageCode.substring(0, 2); if (!_localizedValues.containsKey(_language)) { _language = "en"; @@ -84,7 +84,8 @@ class L { String get calendarSyncPageEndSync => _getValue("calendarSyncPageEndSync"); - String get calendarSyncPageBeginSync => _getValue("calendarSyncPageBeginSync"); + String get calendarSyncPageBeginSync => + _getValue("calendarSyncPageBeginSync"); String get notificationScheduleChangedNewClass => _getValue("notificationScheduleChangedNewClass"); @@ -283,9 +284,11 @@ class L { String get dialogSetRaplaUrlTitle => _getValue("dialogSetRaplaUrlTitle"); - String get dialogCalendarAccessNotGranted => _getValue("dialogCalendarAccessNotGranted"); + String get dialogCalendarAccessNotGranted => + _getValue("dialogCalendarAccessNotGranted"); - String get dialogTitleCalendarAccessNotGranted => _getValue("dialogTitleCalendarAccessNotGranted"); + String get dialogTitleCalendarAccessNotGranted => + _getValue("dialogTitleCalendarAccessNotGranted"); String get scheduleEmptyStateSetUrl => _getValue("scheduleEmptyStateSetUrl"); @@ -403,7 +406,7 @@ class L { String get filterTitle => _getValue("filterTitle"); static L of(BuildContext context) { - return Localizations.of(context, L); + return Localizations.of(context, L)!; } static final Map> _localizedValues = { @@ -412,7 +415,7 @@ class L { }; String _getValue(String key) { - return _localizedValues[_language][key] ?? ""; + return _localizedValues[_language]![key] ?? ""; } String getValue(String key) => _getValue(key); diff --git a/lib/common/iap/in_app_purchase_helper.dart b/lib/common/iap/in_app_purchase_helper.dart index efe60867..5f0fe27b 100644 --- a/lib/common/iap/in_app_purchase_helper.dart +++ b/lib/common/iap/in_app_purchase_helper.dart @@ -20,7 +20,7 @@ enum PurchaseResultEnum { } typedef PurchaseCompletedCallback = Function( - String productId, + String? productId, PurchaseResultEnum result, ); @@ -33,10 +33,10 @@ class InAppPurchaseHelper { final PreferencesProvider _preferencesProvider; - StreamSubscription _purchaseUpdatedSubscription; - StreamSubscription _purchaseErrorSubscription; + StreamSubscription? _purchaseUpdatedSubscription; + StreamSubscription? _purchaseErrorSubscription; - PurchaseCompletedCallback _purchaseCallback; + PurchaseCompletedCallback? _purchaseCallback; InAppPurchaseHelper(this._preferencesProvider); @@ -76,9 +76,7 @@ class InAppPurchaseHelper { return PurchaseResultEnum.Success; } on PlatformException catch (_) { - if (_purchaseCallback != null) { - _purchaseCallback(id, PurchaseResultEnum.Error); - } + _purchaseCallback?.call(id, PurchaseResultEnum.Error); return PurchaseResultEnum.Error; } @@ -95,37 +93,37 @@ class InAppPurchaseHelper { return PurchaseStateEnum.NotPurchased; } - var allPurchases = []; - try { - allPurchases = + final allPurchases = await FlutterInappPurchase.instance.getAvailablePurchases(); - } on Exception catch (_) { - return PurchaseStateEnum.Unknown; - } - var productIdPurchases = - allPurchases.where((element) => element.productId == id); + final productIdPurchases = + allPurchases?.where((element) => element.productId == id); + + if (productIdPurchases != null && productIdPurchases.isNotEmpty) { + return productIdPurchases.any((element) => _isPurchased(element)) + ? PurchaseStateEnum.Purchased + : PurchaseStateEnum.NotPurchased; + } - if (productIdPurchases.isEmpty) { return PurchaseStateEnum.NotPurchased; + } on Exception catch (_) { + return PurchaseStateEnum.Unknown; } - - return productIdPurchases.any((element) => _isPurchased(element)) - ? PurchaseStateEnum.Purchased - : PurchaseStateEnum.NotPurchased; } /// /// Sets the callback function that gets executed when a purchase succeeded /// or failed /// - void setPurchaseCompleteCallback(PurchaseCompletedCallback callback) { + set purchaseCompleteCallback(PurchaseCompletedCallback callback) { _purchaseCallback = callback; } - Future _completePurchase(PurchasedItem item) async { - var purchaseResult = _purchaseResultFromItem(item); + Future _completePurchase(PurchasedItem? item) async { + if (item == null) return; + + final purchaseResult = _purchaseResultFromItem(item); _purchaseCallback?.call(item.productId, purchaseResult); @@ -152,48 +150,40 @@ class InAppPurchaseHelper { } PurchaseResultEnum _purchaseResultFromItem(PurchasedItem item) { - var purchaseResult = PurchaseResultEnum.Error; - if (Platform.isAndroid) { switch (item.purchaseStateAndroid) { case PurchaseState.pending: - purchaseResult = PurchaseResultEnum.Pending; - break; + return PurchaseResultEnum.Pending; + case PurchaseState.purchased: - purchaseResult = PurchaseResultEnum.Success; - break; + return PurchaseResultEnum.Success; case PurchaseState.unspecified: - purchaseResult = PurchaseResultEnum.Error; - break; + default: + return PurchaseResultEnum.Error; } } else if (Platform.isIOS) { switch (item.transactionStateIOS) { case TransactionState.purchasing: - purchaseResult = PurchaseResultEnum.Pending; - break; + return PurchaseResultEnum.Pending; case TransactionState.purchased: - purchaseResult = PurchaseResultEnum.Success; - break; - case TransactionState.failed: - purchaseResult = PurchaseResultEnum.Error; - break; + return PurchaseResultEnum.Success; case TransactionState.restored: - purchaseResult = PurchaseResultEnum.Success; - break; + return PurchaseResultEnum.Success; case TransactionState.deferred: - purchaseResult = PurchaseResultEnum.Pending; - break; + return PurchaseResultEnum.Pending; + case TransactionState.failed: + default: + return PurchaseResultEnum.Error; } } - - return purchaseResult; + return PurchaseResultEnum.Error; } Future _hasFinishedTransaction(PurchasedItem item) async { if (item.isAcknowledgedAndroid ?? false) return true; return await _preferencesProvider - .get("purchase_${item.productId}_finished") ?? + .get("purchase_${item.productId}_finished") ?? false; } @@ -202,11 +192,12 @@ class InAppPurchaseHelper { if (!await _preferencesProvider.getHasPurchasedSomething()) { print( - "Abort complete pending purchases, the user did not buy something in the past"); + "Abort complete pending purchases, the user did not buy something in the past", + ); return; } - List purchasedItems = []; + List? purchasedItems; if (Platform.isAndroid) { purchasedItems = @@ -216,20 +207,23 @@ class InAppPurchaseHelper { await FlutterInappPurchase.instance.getPendingTransactionsIOS(); } - print("Found ${purchasedItems.length} pending purchases"); + if (purchasedItems != null) { + print("Found ${purchasedItems.length} pending purchases"); - purchasedItems.forEach(_completePurchase); + purchasedItems.forEach(_completePurchase); + } } - void _onPurchaseError(PurchaseResult event) { + void _onPurchaseError(PurchaseResult? event) { print("Failed to purchase:"); - print(event.message); - print(event.debugMessage); + print(event?.message); + print(event?.debugMessage); _purchaseCallback?.call(null, PurchaseResultEnum.Error); } - bool _isConsumable(String id) { + // TODO: [Leptopdoa] remove this¿? + bool _isConsumable(String? id) { return false; } diff --git a/lib/common/iap/in_app_purchase_manager.dart b/lib/common/iap/in_app_purchase_manager.dart index c56b8f96..cfefe765 100644 --- a/lib/common/iap/in_app_purchase_manager.dart +++ b/lib/common/iap/in_app_purchase_manager.dart @@ -13,33 +13,33 @@ class InAppPurchaseManager { final Map> purchaseCallbacks = {}; InAppPurchaseManager( - PreferencesProvider _preferencesProvider, + PreferencesProvider preferencesProvider, this._widgetHelper, - ) : _inAppPurchaseHelper = InAppPurchaseHelper(_preferencesProvider) { + ) : _inAppPurchaseHelper = InAppPurchaseHelper(preferencesProvider) { _initialize(); } - void _initialize() async { + Future _initialize() async { addPurchaseCallback( InAppPurchaseHelper.WidgetProductId, - (String productId, PurchaseResultEnum result) => + (String? productId, PurchaseResultEnum result) => _setWidgetEnabled(result == PurchaseResultEnum.Success), ); - _inAppPurchaseHelper - .setPurchaseCompleteCallback(_purchaseCompletedCallback); + _inAppPurchaseHelper.purchaseCompleteCallback = _purchaseCompletedCallback; try { await _inAppPurchaseHelper.initialize(); await _restorePurchases(); - } - catch (ex) { + } catch (ex) { + // TODO: [Leptopoda] disable purchases or show error message in settings when initialization was not sucessfull (i.e. no play services) print("Failed to initialize in app purchase!"); } } Future _restorePurchases() async { - var didPurchaseWidget = await didBuyWidget() == PurchaseStateEnum.Purchased; + final didPurchaseWidget = + await didBuyWidget() == PurchaseStateEnum.Purchased; await _setWidgetEnabled(didPurchaseWidget); } @@ -61,27 +61,31 @@ class InAppPurchaseManager { await buyWidget(); } - void _purchaseCompletedCallback(String productId, PurchaseResultEnum result) { + // TODO: [Leptopoda] better nullseafety + void _purchaseCompletedCallback( + String? productId, + PurchaseResultEnum result, + ) { if (purchaseCallbacks.containsKey(productId)) { - var callback = purchaseCallbacks[productId] ?? []; + final callback = purchaseCallbacks[productId!]; - callback.forEach((element) { + callback?.forEach((element) { element(productId, result); }); } if (purchaseCallbacks.containsKey("*")) { - var callback = purchaseCallbacks["*"] ?? []; + final callback = purchaseCallbacks["*"]; - callback.forEach((element) { + callback?.forEach((element) { element(productId, result); }); } - for (var pair in purchaseCallbacks.entries) { - pair.value.forEach((element) { + for (final pair in purchaseCallbacks.entries) { + for (final element in pair.value) { element(null, result); - }); + } } } @@ -91,30 +95,22 @@ class InAppPurchaseManager { /// for all product ids, pass null or "*" as productId /// void addPurchaseCallback( - String productId, + String? productId, PurchaseCompletedCallback callback, ) { - if (productId == null) { - productId = "*"; - } + productId ??= "*"; - if (!purchaseCallbacks.containsKey(productId)) { - purchaseCallbacks[productId] = []; - } - - purchaseCallbacks[productId].add(callback); + purchaseCallbacks[productId]?.add(callback); } /// /// Removes a callback which was registered using [addPurchaseCallback] /// void removePurchaseCallback( - String productId, + String? productId, PurchaseCompletedCallback callback, ) { - if (productId == null) { - productId = "*"; - } + productId ??= "*"; purchaseCallbacks[productId]?.remove(callback); } diff --git a/lib/common/logging/crash_reporting.dart b/lib/common/logging/crash_reporting.dart index ad0dde6b..901f6b27 100644 --- a/lib/common/logging/crash_reporting.dart +++ b/lib/common/logging/crash_reporting.dart @@ -1,12 +1,13 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; -Future reportException(ex, StackTrace trace) async { +Future reportException(dynamic ex, StackTrace? trace) async { if (kReleaseMode) { print("Reporting exception to crashlytics: $ex with stack trace $trace"); await FirebaseCrashlytics.instance.recordError(ex, trace); } else { print( - "Did not report exception (not in release mode) to crashlytics: $ex with stack trace $trace"); + "Did not report exception (not in release mode) to crashlytics: $ex with stack trace $trace", + ); } } diff --git a/lib/common/ui/app_launch_dialogs.dart b/lib/common/ui/app_launch_dialogs.dart index 3f3cbe04..3251b313 100644 --- a/lib/common/ui/app_launch_dialogs.dart +++ b/lib/common/ui/app_launch_dialogs.dart @@ -11,7 +11,7 @@ import 'package:flutter/material.dart'; class AppLaunchDialog { final PreferencesProvider _preferencesProvider; - AppLaunchDialog(this._preferencesProvider); + const AppLaunchDialog(this._preferencesProvider); Future showAppLaunchDialogs(BuildContext context) async { final appLaunchCounter = await _preferencesProvider.getAppLaunchCounter(); diff --git a/lib/common/ui/app_theme.dart b/lib/common/ui/app_theme.dart new file mode 100644 index 00000000..33d87c59 --- /dev/null +++ b/lib/common/ui/app_theme.dart @@ -0,0 +1,154 @@ +import 'package:dhbwstudentapp/common/ui/schedule_entry_theme.dart'; +import 'package:dhbwstudentapp/common/ui/schedule_theme.dart'; +import 'package:dhbwstudentapp/common/ui/text_theme.dart'; +import 'package:flutter/material.dart'; + +class AppTheme { + const AppTheme._(); + + /// Light theme + static final lightThemeData = ThemeData( + brightness: Brightness.light, + toggleableActiveColor: AppTheme.main[600], + colorScheme: ColorScheme.fromSwatch(primarySwatch: main).copyWith( + secondary: AppTheme.main[500], + brightness: Brightness.light, + ), + textButtonTheme: textButtonTheme, + snackBarTheme: const SnackBarThemeData( + backgroundColor: Color(0xfffafafa), + ), + extensions: const >[ + scheduleEntryThemeLight, + scheduleThemeLight, + textTheme, + ], + ); + + /// Dark theme + static final darkThemeData = ThemeData( + brightness: Brightness.dark, + toggleableActiveColor: AppTheme.main[700], + colorScheme: ColorScheme.fromSwatch(primarySwatch: main).copyWith( + secondary: AppTheme.main[500], + brightness: Brightness.dark, + ), + textButtonTheme: textButtonTheme, + snackBarTheme: const SnackBarThemeData( + backgroundColor: Color(0xff363635), + contentTextStyle: TextStyle( + color: Color(0xffe4e4e4), + ), + ), + extensions: const >[ + scheduleEntryThemeDark, + scheduleThemeDark, + textTheme, + ], + ); + + static final textButtonTheme = TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: AppTheme.main, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + ), + ); + + static const scheduleEntryThemeLight = ScheduleEntryTheme( + unknown: Color(0xffcbcbcb), + lesson: Color(0xffe63f3b), + online: Color(0xffAFC7EA), + publicHoliday: Color(0xffcbcbcb), + exam: Color(0xfffdb531), + ); + + static const scheduleEntryThemeDark = ScheduleEntryTheme( + unknown: Color(0xff515151), + lesson: Color(0xffa52632), + online: Color(0xff2659A6), + publicHoliday: Color(0xff515151), + exam: Color(0xffb17f22), + ); + + static const scheduleThemeLight = ScheduleTheme( + scheduleGridGridLines: Color(0xffe0e0e0), + scheduleInPastOverlay: Color(0x1F000000), + currentTimeIndicator: Color(0xffffa500), + ); + + static const scheduleThemeDark = ScheduleTheme( + scheduleGridGridLines: Color(0xff515151), + scheduleInPastOverlay: Color(0x3F000000), + currentTimeIndicator: Color(0xffb37300), + ); + + static const textTheme = CustomTextTheme( + dailyScheduleEntryType: TextStyle( + inherit: false, + fontWeight: FontWeight.w300, + letterSpacing: 0.15, + ), + dailyScheduleEntryTimeStart: TextStyle( + inherit: false, + fontWeight: FontWeight.w600, + letterSpacing: 0.15, + ), + scheduleEntryWidgetTitle: TextStyle( + inherit: false, + fontWeight: FontWeight.normal, + ), + scheduleEntryBottomPageType: TextStyle( + inherit: false, + fontWeight: FontWeight.w300, + ), + scheduleWidgetColumnTitleDay: TextStyle( + inherit: false, + fontWeight: FontWeight.w300, + ), + ); + + static const onboardingDecorationForeground = Color(0xFFA62828); + static const onboardingDecorationBackground = Color(0xFFC91A1A); + static const success = Color(0xFFC91A1A); + static const dailyScheduleTimeVerticalConnector = Colors.grey; + static const separator = Colors.grey; + static const noConnectionBackground = Colors.black87; + static const noConnectionForeground = Colors.white; + + static const MaterialColor main = MaterialColor(0xffff061c, { + 050: Color(0xFFff838e), + 100: Color(0xFFff6a77), + 200: Color(0xFFff5160), + 300: Color(0xFFff3849), + 400: Color(0xFFff1f33), + 500: Color(0xffff061c), + 600: Color(0xFFe60519), + 700: Color(0xFFcc0516), + 800: Color(0xFFb30414), + 900: Color(0xFF990411), + }); + + static const MaterialColor secondary = MaterialColor(0xFFCECED0, { + 050: Color(0xFFF9F9F9), + 100: Color(0xFFF0F0F1), + 200: Color(0xFFE7E7E8), + 300: Color(0xFFDDDDDE), + 400: Color(0xFFD5D5D7), + 500: Color(0xFFCECED0), + 600: Color(0xFFC9C9CB), + 700: Color(0xFFC2C2C4), + 800: Color(0xFFBCBCBE), + 900: Color(0xFFB0B0B3), + }); + + static const MaterialColor secondaryAccent = + MaterialColor(0xFFFFFFFF, { + 100: Color(0xFFFFFFFF), + 200: Color(0xFFFFFFFF), + 400: Color(0xFFFFFFFF), + 700: Color(0xFFEAEAFF), + }); +} diff --git a/lib/common/ui/colors.dart b/lib/common/ui/colors.dart deleted file mode 100644 index 181b7c65..00000000 --- a/lib/common/ui/colors.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:dhbwstudentapp/common/data/preferences/app_theme_enum.dart'; -import 'package:dhbwstudentapp/common/util/platform_util.dart'; -import 'package:flutter/material.dart'; - -Color colorScheduleEntryPublicHoliday(BuildContext context) => - Theme.of(context).brightness == Brightness.light - ? const Color(0xffcbcbcb) - : const Color(0xff515151); - -Color colorScheduleEntryClass(BuildContext context) => - Theme.of(context).brightness == Brightness.light - ? const Color(0xffe63f3b) - : const Color(0xffa52632); - -Color colorScheduleEntryExam(BuildContext context) => - Theme.of(context).brightness == Brightness.light - ? const Color(0xfffdb531) - : const Color(0xffb17f22); - -Color colorScheduleEntryOnline(BuildContext context) => - Theme.of(context).brightness == Brightness.light - ? const Color(0xffAFC7EA) - : const Color(0xff2659A6); - -Color colorScheduleEntryUnknown(BuildContext context) => - Theme.of(context).brightness == Brightness.light - ? const Color(0xffcbcbcb) - : const Color(0xff515151); - -Color colorScheduleGridGridLines(BuildContext context) => - Theme.of(context).brightness == Brightness.light - ? const Color(0xffe0e0e0) - : const Color(0xff515151); - -Color colorScheduleInPastOverlay(BuildContext context) => - Theme.of(context).brightness == Brightness.light - ? const Color(0x1F000000) - : const Color(0x3F000000); - -Color colorCurrentTimeIndicator(BuildContext context) => - Theme.of(context).brightness == Brightness.light - ? const Color(0xffffa500) - : const Color(0xffb37300); - -Color colorOnboardingDecorationForeground(BuildContext context) => - Theme.of(context).brightness == Brightness.light - ? const Color(0xFFA62828) - : const Color(0xFFA62828); - -Color colorOnboardingDecorationBackground(BuildContext context) => - Theme.of(context).brightness == Brightness.light - ? const Color(0xFFC91A1A) - : const Color(0xFFC91A1A); - -Color colorSuccess(BuildContext context) => - Theme.of(context).brightness == Brightness.light - ? const Color(0xFFC91A1A) - : const Color(0xFFC91A1A); - -Color colorDailyScheduleTimeVerticalConnector() => Colors.grey; - -Color colorSeparator() => Colors.grey; - -Color colorNoConnectionBackground() => Colors.black87; - -Color colorNoConnectionForeground() => Colors.white; - -class ColorPalettes { - ColorPalettes._(); - - static ThemeData buildTheme(AppTheme theme) { - if (theme == AppTheme.System) { - theme = PlatformUtil.platformBrightness() == Brightness.light - ? AppTheme.Light - : AppTheme.Dark; - } - - var isDark = theme == AppTheme.Dark; - - var brightness = isDark ? Brightness.dark : Brightness.light; - - var themeData = ThemeData( - brightness: brightness, - toggleableActiveColor: - isDark ? ColorPalettes.main[700] : ColorPalettes.main[600], - colorScheme: - ColorScheme.fromSwatch(primarySwatch: ColorPalettes.main).copyWith( - secondary: ColorPalettes.main[500], - brightness: brightness, - ), - ); - - return themeData.copyWith( - snackBarTheme: themeData.snackBarTheme.copyWith( - backgroundColor: isDark ? Color(0xff363635) : Color(0xfffafafa), - contentTextStyle: themeData.textTheme.bodyText1.copyWith( - color: - isDark ? Color(0xffe4e4e4) : themeData.textTheme.bodyText1.color, - ), - ), - textButtonTheme: TextButtonThemeData( - style: TextButton.styleFrom( - primary: ColorPalettes.main, - padding: EdgeInsets.symmetric(horizontal: 16.0), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(4.0)), - ), - ), - ), - ); - } - - static const MaterialColor main = MaterialColor(0xffff061c, { - 050: Color(0xFFff838e), - 100: Color(0xFFff6a77), - 200: Color(0xFFff5160), - 300: Color(0xFFff3849), - 400: Color(0xFFff1f33), - 500: Color(0xffff061c), - 600: Color(0xFFe60519), - 700: Color(0xFFcc0516), - 800: Color(0xFFb30414), - 900: Color(0xFF990411), - }); - - static const MaterialColor secondary = MaterialColor(0xFFCECED0, { - 050: Color(0xFFF9F9F9), - 100: Color(0xFFF0F0F1), - 200: Color(0xFFE7E7E8), - 300: Color(0xFFDDDDDE), - 400: Color(0xFFD5D5D7), - 500: Color(0xFFCECED0), - 600: Color(0xFFC9C9CB), - 700: Color(0xFFC2C2C4), - 800: Color(0xFFBCBCBE), - 900: Color(0xFFB0B0B3), - }); - - static const MaterialColor secondaryAccent = - MaterialColor(0xFFFFFFFF, { - 100: Color(0xFFFFFFFF), - 200: Color(0xFFFFFFFF), - 400: Color(0xFFFFFFFF), - 700: Color(0xFFEAEAFF), - }); -} diff --git a/lib/common/ui/custom_icons_icons.dart b/lib/common/ui/custom_icons_icons.dart index 759832fb..72be9796 100644 --- a/lib/common/ui/custom_icons_icons.dart +++ b/lib/common/ui/custom_icons_icons.dart @@ -11,7 +11,7 @@ /// fonts: /// - asset: fonts/CustomIcons.ttf /// -/// +/// /// * Font Awesome 4, Copyright (C) 2016 by Dave Gandy /// Author: Dave Gandy /// License: SIL () @@ -20,10 +20,9 @@ import 'package:flutter/widgets.dart'; class CustomIcons { - CustomIcons._(); + const CustomIcons._(); static const _kFontFam = 'CustomIcons'; - static const _kFontPkg = null; - static const IconData logout = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData logout = IconData(0xe800, fontFamily: _kFontFam); } diff --git a/lib/common/ui/donate_to_developer_dialog.dart b/lib/common/ui/donate_to_developer_dialog.dart index f38ee705..78b5e5f0 100644 --- a/lib/common/ui/donate_to_developer_dialog.dart +++ b/lib/common/ui/donate_to_developer_dialog.dart @@ -11,12 +11,15 @@ import 'package:kiwi/kiwi.dart'; /// developer /// class DonateToDeveloperDialog { - PreferencesProvider _preferencesProvider; - int _appLaunchCounter; + final PreferencesProvider _preferencesProvider; + final int _appLaunchCounter; - DonateToDeveloperDialog(this._preferencesProvider, this._appLaunchCounter); + const DonateToDeveloperDialog( + this._preferencesProvider, + this._appLaunchCounter, + ); - void showIfNeeded(BuildContext context) async { + Future showIfNeeded(BuildContext context) async { if (await _preferencesProvider.getDidShowDonateDialog()) return; if (_appLaunchCounter >= DonateLaunchAfter) { @@ -28,32 +31,33 @@ class DonateToDeveloperDialog { } Future _showDialog(BuildContext context) async { - return await showDialog( + return showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( - buttonPadding: const EdgeInsets.all(0), - actionsPadding: const EdgeInsets.all(0), - contentPadding: const EdgeInsets.all(0), + buttonPadding: EdgeInsets.zero, + actionsPadding: EdgeInsets.zero, + contentPadding: EdgeInsets.zero, title: Text(L.of(context).donateDialogTitle), content: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( - padding: const EdgeInsets.all(24), - child: Column( - children: [ - Text(L.of(context).donateDialogMessage), - Padding( - padding: const EdgeInsets.fromLTRB(0, 32, 0, 0), - child: Icon( - Icons.free_breakfast_outlined, - size: 60, - ), + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Text(L.of(context).donateDialogMessage), + const Padding( + padding: EdgeInsets.fromLTRB(0, 32, 0, 0), + child: Icon( + Icons.free_breakfast_outlined, + size: 60, ), - ], - )), + ), + ], + ), + ), Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 0), child: _buildButtonBar(context), diff --git a/lib/common/ui/notification_api.dart b/lib/common/ui/notification_api.dart index 9810bf1e..3286df1b 100644 --- a/lib/common/ui/notification_api.dart +++ b/lib/common/ui/notification_api.dart @@ -10,20 +10,23 @@ class NotificationApi { final FlutterLocalNotificationsPlugin _localNotificationsPlugin = FlutterLocalNotificationsPlugin(); + NotificationApi(); + /// /// Initialize the notifications. You can't show any notifications before you /// call this method /// Future initialize() async { - var initializationSettingsAndroid = const AndroidInitializationSettings( + const initializationSettingsAndroid = AndroidInitializationSettings( 'outline_event_note_24', ); - var initializationSettingsIOS = IOSInitializationSettings( + final initializationSettingsIOS = IOSInitializationSettings( + // TODO: [Leptopoda] the below always returns null so why register it? onDidReceiveLocalNotification: onDidReceiveLocalNotification, ); - var initializationSettings = InitializationSettings( + final initializationSettings = InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsIOS, ); @@ -37,26 +40,23 @@ class NotificationApi { /// /// Show a notification with the given title and message /// - Future showNotification(String title, String message, [int id]) async { - var androidPlatformChannelSpecifics = AndroidNotificationDetails( + Future showNotification(String title, String? message, [int? id]) async { + const androidPlatformChannelSpecifics = AndroidNotificationDetails( 'Notifications', 'Notifications', channelDescription: 'This is the main notification channel', icon: 'outline_event_note_24', - channelAction: AndroidNotificationChannelAction.createIfNotExists, - autoCancel: true, channelShowBadge: false, color: Colors.red, enableLights: true, - enableVibration: true, importance: Importance.high, priority: Priority.high, ticker: 'ticker', ); - var iOSPlatformChannelSpecifics = const IOSNotificationDetails(); + const iOSPlatformChannelSpecifics = IOSNotificationDetails(); - var platformChannelSpecifics = NotificationDetails( + const platformChannelSpecifics = NotificationDetails( android: androidPlatformChannelSpecifics, iOS: iOSPlatformChannelSpecifics, ); @@ -72,18 +72,25 @@ class NotificationApi { } Future onDidReceiveLocalNotification( - int id, String title, String body, String payload) => + int id, + String? title, + String? body, + String? payload, + ) => Future.value(); - Future selectNotification(String payload) => Future.value(); + Future selectNotification(String? payload) => Future.value(); } /// /// This class implements the methods of the NotificationApi with empty stubs /// class VoidNotificationApi implements NotificationApi { + const VoidNotificationApi(); + @override - FlutterLocalNotificationsPlugin get _localNotificationsPlugin => null; + FlutterLocalNotificationsPlugin get _localNotificationsPlugin => + throw UnimplementedError(); @override Future initialize() { @@ -92,17 +99,21 @@ class VoidNotificationApi implements NotificationApi { @override Future onDidReceiveLocalNotification( - int id, String title, String body, String payload) { + int id, + String? title, + String? body, + String? payload, + ) { return Future.value(); } @override - Future selectNotification(String payload) { + Future selectNotification(String? payload) { return Future.value(); } @override - Future showNotification(String title, String message, [int id]) { + Future showNotification(String title, String? message, [int? id]) { return Future.value(); } } diff --git a/lib/common/ui/rate_in_store_dialog.dart b/lib/common/ui/rate_in_store_dialog.dart index 76f66cb5..e21a3f6b 100644 --- a/lib/common/ui/rate_in_store_dialog.dart +++ b/lib/common/ui/rate_in_store_dialog.dart @@ -13,7 +13,7 @@ class RateInStoreDialog { final PreferencesProvider _preferencesProvider; final int _appLaunchCounter; - RateInStoreDialog(this._preferencesProvider, this._appLaunchCounter); + const RateInStoreDialog(this._preferencesProvider, this._appLaunchCounter); Future showIfNeeded(BuildContext context) async { if (!PlatformUtil.isAndroid()) return; @@ -28,14 +28,14 @@ class RateInStoreDialog { Future _showRateDialog(BuildContext context) async { await analytics.logEvent(name: "rateRequestShown"); - return await showDialog( + return showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( - buttonPadding: const EdgeInsets.all(0), - actionsPadding: const EdgeInsets.all(0), - contentPadding: const EdgeInsets.all(0), + buttonPadding: EdgeInsets.zero, + actionsPadding: EdgeInsets.zero, + contentPadding: EdgeInsets.zero, title: Text(L.of(context).rateDialogTitle), content: Column( mainAxisSize: MainAxisSize.min, @@ -89,7 +89,8 @@ class RateInStoreDialog { await analytics.logEvent(name: "rateLater"); await _preferencesProvider.setNextRateInStoreLaunchCount( - RateInStoreLaunchAfter + _appLaunchCounter); + RateInStoreLaunchAfter + _appLaunchCounter, + ); } Future _rateNow() async { diff --git a/lib/common/ui/schedule_entry_theme.dart b/lib/common/ui/schedule_entry_theme.dart new file mode 100644 index 00000000..2cea0ff8 --- /dev/null +++ b/lib/common/ui/schedule_entry_theme.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +@immutable +class ScheduleEntryTheme extends ThemeExtension { + const ScheduleEntryTheme({ + required this.unknown, + required this.lesson, + required this.online, + required this.publicHoliday, + required this.exam, + }); + + final Color unknown; + final Color lesson; + final Color online; + final Color publicHoliday; + final Color exam; + + @override + ScheduleEntryTheme copyWith({ + Color? unknown, + Color? lesson, + Color? online, + Color? publicHoliday, + Color? exam, + }) { + return ScheduleEntryTheme( + unknown: unknown ?? this.unknown, + lesson: lesson ?? this.lesson, + online: online ?? this.online, + publicHoliday: publicHoliday ?? this.publicHoliday, + exam: exam ?? this.exam, + ); + } + + @override + ScheduleEntryTheme lerp(ThemeExtension? other, double t) { + if (other is! ScheduleEntryTheme) { + return this; + } + return copyWith( + unknown: Color.lerp(unknown, other.unknown, t), + lesson: Color.lerp(lesson, other.lesson, t), + online: Color.lerp(online, other.online, t), + publicHoliday: Color.lerp(publicHoliday, other.publicHoliday, t), + exam: Color.lerp(exam, other.exam, t), + ); + } +} diff --git a/lib/common/ui/schedule_entry_type_mappings.dart b/lib/common/ui/schedule_entry_type_mappings.dart index 9c162016..fc41e3df 100644 --- a/lib/common/ui/schedule_entry_type_mappings.dart +++ b/lib/common/ui/schedule_entry_type_mappings.dart @@ -1,38 +1,22 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; -import 'package:dhbwstudentapp/common/ui/colors.dart'; import 'package:dhbwstudentapp/schedule/model/schedule_entry.dart'; import 'package:flutter/material.dart'; typedef ColorDelegate = Color Function(BuildContext context); typedef TextDelegate = String Function(BuildContext context); -final Map scheduleEntryTypeColorMapping = { - ScheduleEntryType.PublicHoliday: colorScheduleEntryPublicHoliday, - ScheduleEntryType.Class: colorScheduleEntryClass, - ScheduleEntryType.Exam: colorScheduleEntryExam, - ScheduleEntryType.Online: colorScheduleEntryOnline, - ScheduleEntryType.Unknown: colorScheduleEntryUnknown, -}; - final Map scheduleEntryTypeTextMapping = { ScheduleEntryType.PublicHoliday: (c) => L.of(c).scheduleEntryTypePublicHoliday, - ScheduleEntryType.Class: (c) => L.of(c).scheduleEntryTypeClass, + ScheduleEntryType.Lesson: (c) => L.of(c).scheduleEntryTypeClass, ScheduleEntryType.Exam: (c) => L.of(c).scheduleEntryTypeExam, ScheduleEntryType.Online: (c) => L.of(c).scheduleEntryTypeOnline, ScheduleEntryType.Unknown: (c) => L.of(c).scheduleEntryTypeUnknown, }; -Color scheduleEntryTypeToColor( - BuildContext context, - ScheduleEntryType type, -) { - return scheduleEntryTypeColorMapping[type](context); -} - String scheduleEntryTypeToReadableString( BuildContext context, ScheduleEntryType type, ) { - return scheduleEntryTypeTextMapping[type](context); + return scheduleEntryTypeTextMapping[type]!(context); } diff --git a/lib/common/ui/schedule_theme.dart b/lib/common/ui/schedule_theme.dart new file mode 100644 index 00000000..763bfe5b --- /dev/null +++ b/lib/common/ui/schedule_theme.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +@immutable +class ScheduleTheme extends ThemeExtension { + const ScheduleTheme({ + required this.scheduleGridGridLines, + required this.scheduleInPastOverlay, + required this.currentTimeIndicator, + }); + + final Color scheduleGridGridLines; + final Color scheduleInPastOverlay; + final Color currentTimeIndicator; + + @override + ScheduleTheme copyWith({ + Color? scheduleGridGridLines, + Color? scheduleInPastOverlay, + Color? currentTimeIndicator, + }) { + return ScheduleTheme( + scheduleGridGridLines: + scheduleGridGridLines ?? this.scheduleGridGridLines, + scheduleInPastOverlay: + scheduleInPastOverlay ?? this.scheduleInPastOverlay, + currentTimeIndicator: currentTimeIndicator ?? this.currentTimeIndicator, + ); + } + + @override + ScheduleTheme lerp(ThemeExtension? other, double t) { + if (other is! ScheduleTheme) { + return this; + } + return copyWith( + scheduleGridGridLines: + Color.lerp(scheduleGridGridLines, other.scheduleGridGridLines, t), + scheduleInPastOverlay: + Color.lerp(scheduleInPastOverlay, other.scheduleInPastOverlay, t), + currentTimeIndicator: + Color.lerp(currentTimeIndicator, other.currentTimeIndicator, t), + ); + } +} diff --git a/lib/common/ui/text_styles.dart b/lib/common/ui/text_styles.dart deleted file mode 100644 index f4ec8f22..00000000 --- a/lib/common/ui/text_styles.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; - -TextStyle textStyleDailyScheduleEntryWidgetProfessor(BuildContext context) => - Theme.of(context).textTheme.subtitle2; - -TextStyle textStyleDailyScheduleEntryWidgetTitle(BuildContext context) => - Theme.of(context) - .textTheme - .headline4 - .copyWith(color: Theme.of(context).textTheme.headline6.color); - -TextStyle textStyleDailyScheduleEntryWidgetType(BuildContext context) => - Theme.of(context).textTheme.bodyText2.copyWith( - fontWeight: FontWeight.w300, - letterSpacing: 0.15, - ); - -TextStyle textStyleDailyScheduleEntryWidgetTimeStart(BuildContext context) => - Theme.of(context).textTheme.headline5.copyWith( - fontWeight: FontWeight.w600, - letterSpacing: 0.15, - ); - -TextStyle textStyleDailyScheduleEntryWidgetTimeEnd(BuildContext context) => - Theme.of(context).textTheme.subtitle2; - -TextStyle textStyleDailyScheduleCurrentDate(BuildContext context) => - Theme.of(context).textTheme.headline4.copyWith( - color: Theme.of(context).textTheme.headline5.color, - ); -TextStyle textStyleDailyScheduleNoEntries(BuildContext context) => - Theme.of(context).textTheme.headline5; - -TextStyle textStyleScheduleEntryWidgetTitle(BuildContext context) => - Theme.of(context).textTheme.bodyText1.copyWith( - fontWeight: FontWeight.normal, - ); - -TextStyle textStyleScheduleEntryBottomPageTitle(BuildContext context) => - Theme.of(context).textTheme.subtitle2; - -TextStyle textStyleScheduleEntryBottomPageTimeFromTo(BuildContext context) => - Theme.of(context).textTheme.caption; - -TextStyle textStyleScheduleEntryBottomPageTime(BuildContext context) => - Theme.of(context).textTheme.headline5; - -TextStyle textStyleScheduleEntryBottomPageType(BuildContext context) => - Theme.of(context).textTheme.bodyText1.copyWith( - fontWeight: FontWeight.w300, - ); - -TextStyle textStyleScheduleWidgetColumnTitleDay(BuildContext context) => - Theme.of(context).textTheme.subtitle2.copyWith( - fontWeight: FontWeight.w300, - ); diff --git a/lib/common/ui/text_theme.dart b/lib/common/ui/text_theme.dart new file mode 100644 index 00000000..ecbceb34 --- /dev/null +++ b/lib/common/ui/text_theme.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; + +@immutable +class CustomTextTheme extends ThemeExtension { + const CustomTextTheme({ + required this.dailyScheduleEntryType, + required this.dailyScheduleEntryTimeStart, + required this.scheduleEntryWidgetTitle, + required this.scheduleEntryBottomPageType, + required this.scheduleWidgetColumnTitleDay, + }); + + final TextStyle dailyScheduleEntryType; + final TextStyle dailyScheduleEntryTimeStart; + final TextStyle scheduleEntryWidgetTitle; + final TextStyle scheduleEntryBottomPageType; + final TextStyle scheduleWidgetColumnTitleDay; + + @override + CustomTextTheme copyWith({ + TextStyle? dailyScheduleEntryType, + TextStyle? dailyScheduleEntryTimeStart, + TextStyle? scheduleEntryWidgetTitle, + TextStyle? scheduleEntryBottomPageType, + TextStyle? scheduleWidgetColumnTitleDay, + }) { + return CustomTextTheme( + dailyScheduleEntryType: + dailyScheduleEntryType ?? this.dailyScheduleEntryType, + dailyScheduleEntryTimeStart: + dailyScheduleEntryTimeStart ?? this.dailyScheduleEntryTimeStart, + scheduleEntryWidgetTitle: + scheduleEntryWidgetTitle ?? this.scheduleEntryWidgetTitle, + scheduleEntryBottomPageType: + scheduleEntryBottomPageType ?? this.scheduleEntryBottomPageType, + scheduleWidgetColumnTitleDay: + scheduleWidgetColumnTitleDay ?? this.scheduleWidgetColumnTitleDay, + ); + } + + @override + CustomTextTheme lerp(ThemeExtension? other, double t) { + if (other is! CustomTextTheme) { + return this; + } + return copyWith( + dailyScheduleEntryType: TextStyle.lerp( + dailyScheduleEntryType, + other.dailyScheduleEntryType, + t, + ), + dailyScheduleEntryTimeStart: TextStyle.lerp( + dailyScheduleEntryTimeStart, + other.dailyScheduleEntryTimeStart, + t, + ), + scheduleEntryWidgetTitle: TextStyle.lerp( + scheduleEntryWidgetTitle, + other.scheduleEntryWidgetTitle, + t, + ), + scheduleEntryBottomPageType: TextStyle.lerp( + scheduleEntryBottomPageType, + other.scheduleEntryBottomPageType, + t, + ), + scheduleWidgetColumnTitleDay: TextStyle.lerp( + scheduleWidgetColumnTitleDay, + other.scheduleWidgetColumnTitleDay, + t, + ), + ); + } +} diff --git a/lib/common/ui/viewmodels/root_view_model.dart b/lib/common/ui/viewmodels/root_view_model.dart index dd2a360a..05555242 100644 --- a/lib/common/ui/viewmodels/root_view_model.dart +++ b/lib/common/ui/viewmodels/root_view_model.dart @@ -1,29 +1,28 @@ -import 'package:dhbwstudentapp/common/data/preferences/app_theme_enum.dart'; import 'package:dhbwstudentapp/common/data/preferences/preferences_provider.dart'; import 'package:dhbwstudentapp/common/ui/viewmodels/base_view_model.dart'; +import 'package:flutter/material.dart'; class RootViewModel extends BaseViewModel { final PreferencesProvider _preferencesProvider; - AppTheme _appTheme; - AppTheme get appTheme => _appTheme; + late ThemeMode _appTheme; + ThemeMode get appTheme => _appTheme; - bool _isOnboarding; + late bool _isOnboarding; bool get isOnboarding => _isOnboarding; RootViewModel(this._preferencesProvider); Future loadFromPreferences() async { - var darkMode = await _preferencesProvider.appTheme(); - - _appTheme = darkMode; + _appTheme = await _preferencesProvider.appTheme(); _isOnboarding = await _preferencesProvider.isFirstStart(); notifyListeners("appTheme"); notifyListeners("isOnboarding"); } - Future setAppTheme(AppTheme value) async { + Future setAppTheme(ThemeMode? value) async { + if (value == null) return; await _preferencesProvider.setAppTheme(value); _appTheme = value; notifyListeners("appTheme"); diff --git a/lib/common/ui/widget_help_dialog.dart b/lib/common/ui/widget_help_dialog.dart index 085c9ee7..c41279d5 100644 --- a/lib/common/ui/widget_help_dialog.dart +++ b/lib/common/ui/widget_help_dialog.dart @@ -11,7 +11,7 @@ class WidgetHelpDialog { final PreferencesProvider _preferencesProvider; final int _appLaunchCounter; - WidgetHelpDialog(this._preferencesProvider, this._appLaunchCounter); + const WidgetHelpDialog(this._preferencesProvider, this._appLaunchCounter); Future showIfNeeded(BuildContext context) async { if (!PlatformUtil.isAndroid()) return; @@ -24,21 +24,22 @@ class WidgetHelpDialog { } Future _showDialog(BuildContext context) async { - return await showDialog( + return showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( - buttonPadding: const EdgeInsets.all(0), - actionsPadding: const EdgeInsets.all(0), - contentPadding: const EdgeInsets.all(0), + buttonPadding: EdgeInsets.zero, + actionsPadding: EdgeInsets.zero, + contentPadding: EdgeInsets.zero, title: Text(L.of(context).widgetHelpDialogTitle), content: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( - padding: const EdgeInsets.all(24), - child: Text(L.of(context).widgetHelpDialogMessage)), + padding: const EdgeInsets.all(24), + child: Text(L.of(context).widgetHelpDialogMessage), + ), Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 0), child: _buildButtonBar(context), diff --git a/lib/common/ui/widgets/dots_indicator.dart b/lib/common/ui/widgets/dots_indicator.dart index 816e3c79..4e50d4e0 100644 --- a/lib/common/ui/widgets/dots_indicator.dart +++ b/lib/common/ui/widgets/dots_indicator.dart @@ -4,12 +4,15 @@ class DotsIndicator extends StatelessWidget { final int numberSteps; final int currentStep; - const DotsIndicator({Key key, this.numberSteps, this.currentStep}) - : super(key: key); + const DotsIndicator({ + super.key, + required this.numberSteps, + required this.currentStep, + }); @override Widget build(BuildContext context) { - var dots = []; + final dots = []; for (int i = 0; i < numberSteps; i++) { dots.add( diff --git a/lib/common/ui/widgets/error_display.dart b/lib/common/ui/widgets/error_display.dart index 6cefef8f..a6d5c631 100644 --- a/lib/common/ui/widgets/error_display.dart +++ b/lib/common/ui/widgets/error_display.dart @@ -1,11 +1,11 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; -import 'package:dhbwstudentapp/common/ui/colors.dart'; +import 'package:dhbwstudentapp/common/ui/app_theme.dart'; import 'package:flutter/material.dart'; class ErrorDisplay extends StatelessWidget { final bool show; - const ErrorDisplay({Key key, this.show}) : super(key: key); + const ErrorDisplay({super.key, required this.show}); @override Widget build(BuildContext context) { @@ -13,20 +13,21 @@ class ErrorDisplay extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ AnimatedSwitcher( + duration: const Duration(milliseconds: 300), child: show ? Padding( - padding: const EdgeInsets.all(0), + padding: EdgeInsets.zero, child: Container( width: double.infinity, - color: colorNoConnectionBackground(), + color: AppTheme.noConnectionBackground, child: Padding( padding: const EdgeInsets.fromLTRB(24, 4, 24, 4), child: Text( L.of(context).noConnectionMessage, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.subtitle2.copyWith( - color: colorNoConnectionForeground(), - ), + style: const TextStyle( + color: AppTheme.noConnectionForeground, + ), ), ), ), @@ -34,7 +35,6 @@ class ErrorDisplay extends StatelessWidget { : Container( width: double.infinity, ), - duration: const Duration(milliseconds: 300), ), ], ); diff --git a/lib/common/ui/widgets/help_dialog.dart b/lib/common/ui/widgets/help_dialog.dart index e3098298..08b87df3 100644 --- a/lib/common/ui/widgets/help_dialog.dart +++ b/lib/common/ui/widgets/help_dialog.dart @@ -6,6 +6,8 @@ import 'package:flutter/material.dart'; /// Deriving classes only need to implement the title() and content() methods /// abstract class HelpDialog { + const HelpDialog(); + Future show(BuildContext context) async { await showDialog( context: context, diff --git a/lib/common/ui/widgets/title_list_tile.dart b/lib/common/ui/widgets/title_list_tile.dart index c3576a63..58b6c7d9 100644 --- a/lib/common/ui/widgets/title_list_tile.dart +++ b/lib/common/ui/widgets/title_list_tile.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; class TitleListTile extends StatelessWidget { final String title; - const TitleListTile({Key key, this.title}) : super(key: key); + const TitleListTile({super.key, required this.title}); @override Widget build(BuildContext context) { diff --git a/lib/common/util/cancelable_mutex.dart b/lib/common/util/cancelable_mutex.dart index 5335981a..fdcb1aa2 100644 --- a/lib/common/util/cancelable_mutex.dart +++ b/lib/common/util/cancelable_mutex.dart @@ -1,17 +1,19 @@ +import 'package:dhbwstudentapp/common/util/cancellation_token.dart'; import 'package:mutex/mutex.dart'; -import 'cancellation_token.dart'; class CancelableMutex { final Mutex _mutex = Mutex(); - CancellationToken _token; - CancellationToken get token => _token; + CancellationToken? _token; + CancellationToken? get token => _token; + + CancelableMutex(); void cancel() { _token?.cancel(); } Future acquireAndCancelOther() async { - if (!(token?.isCancelled() ?? false)) { + if (!(token?.isCancelled ?? false)) { token?.cancel(); } diff --git a/lib/common/util/cancellation_token.dart b/lib/common/util/cancellation_token.dart index 59f097cc..27fc3c2e 100644 --- a/lib/common/util/cancellation_token.dart +++ b/lib/common/util/cancellation_token.dart @@ -2,11 +2,11 @@ typedef CancellationCallback = void Function(); class CancellationToken { bool _isCancelled = false; - CancellationCallback _callback; + CancellationCallback? _callback; - bool isCancelled() { - return _isCancelled; - } + CancellationToken([this._callback]); + + bool get isCancelled => _isCancelled; void throwIfCancelled() { if (_isCancelled) { @@ -17,10 +17,10 @@ class CancellationToken { void cancel() { _isCancelled = true; - if (_callback != null) _callback(); + _callback?.call(); } - void setCancellationCallback(CancellationCallback callback) { + set cancellationCallback(CancellationCallback? callback) { _callback = callback; } } diff --git a/lib/common/util/date_utils.dart b/lib/common/util/date_utils.dart index 57b54605..7a8e2679 100644 --- a/lib/common/util/date_utils.dart +++ b/lib/common/util/date_utils.dart @@ -1,20 +1,20 @@ -DateTime toStartOfDay(DateTime dateTime) { +// TODO: [Leptopoda] write extension methods directly on [DateTime] + +DateTime? toStartOfDay(DateTime? dateTime) { return dateTime == null ? null - : DateTime(dateTime.year, dateTime.month, dateTime.day, 0, 0, 0); + : DateTime(dateTime.year, dateTime.month, dateTime.day); } -DateTime toStartOfMonth(DateTime dateTime) { - return dateTime == null - ? null - : DateTime(dateTime.year, dateTime.month, 1, 0, 0, 0); +DateTime? toStartOfMonth(DateTime? dateTime) { + return dateTime == null ? null : DateTime(dateTime.year, dateTime.month); } -DateTime tomorrow(DateTime dateTime) { +DateTime? tomorrow(DateTime? dateTime) { return addDays(dateTime, 1); } -DateTime addDays(DateTime dateTime, int days) { +DateTime? addDays(DateTime? dateTime, int days) { return dateTime == null ? null : DateTime( @@ -28,12 +28,10 @@ DateTime addDays(DateTime dateTime, int days) { } DateTime toMonday(DateTime dateTime) { - return dateTime == null - ? null - : dateTime.subtract(Duration(days: dateTime.weekday - 1)); + return dateTime.subtract(Duration(days: dateTime.weekday - 1)); } -DateTime toPreviousWeek(DateTime dateTime) { +DateTime? toPreviousWeek(DateTime? dateTime) { return dateTime == null ? null : DateTime( @@ -46,7 +44,7 @@ DateTime toPreviousWeek(DateTime dateTime) { ); } -DateTime toNextWeek(DateTime dateTime) { +DateTime? toNextWeek(DateTime? dateTime) { return dateTime == null ? null : DateTime( @@ -60,20 +58,17 @@ DateTime toNextWeek(DateTime dateTime) { } DateTime toNextMonth(DateTime dateTime) { - return dateTime == null - ? null - : DateTime( - dateTime.year, - dateTime.month + 1, - dateTime.day, - dateTime.hour, - dateTime.minute, - dateTime.second, - ); + return DateTime( + dateTime.year, + dateTime.month + 1, + dateTime.day, + dateTime.hour, + dateTime.minute, + dateTime.second, + ); } DateTime toTimeOfDay(DateTime dateTime, int hour, int minute) { - if (dateTime == null) return null; return DateTime( dateTime.year, dateTime.month, @@ -84,8 +79,6 @@ DateTime toTimeOfDay(DateTime dateTime, int hour, int minute) { } DateTime toTimeOfDayInFuture(DateTime dateTime, int hour, int minute) { - if (dateTime == null) return null; - var newDateTime = DateTime( dateTime.year, dateTime.month, @@ -94,7 +87,7 @@ DateTime toTimeOfDayInFuture(DateTime dateTime, int hour, int minute) { minute, ); - if (dateTime.isAfter(newDateTime)) newDateTime = tomorrow(newDateTime); + if (dateTime.isAfter(newDateTime)) newDateTime = tomorrow(newDateTime)!; return newDateTime; } @@ -105,10 +98,10 @@ bool isAtSameDay(DateTime date1, DateTime date2) { date1.day == date2.day; } -DateTime toDayOfWeek(DateTime dateTime, int weekday) { +DateTime? toDayOfWeek(DateTime? dateTime, int weekday) { if (dateTime == null) return null; - var startOfWeek = addDays(dateTime, -dateTime.weekday); + final startOfWeek = addDays(dateTime, -dateTime.weekday); return addDays(startOfWeek, weekday); } diff --git a/lib/common/util/platform_util.dart b/lib/common/util/platform_util.dart index a602f6c6..21030e96 100644 --- a/lib/common/util/platform_util.dart +++ b/lib/common/util/platform_util.dart @@ -4,6 +4,8 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; class PlatformUtil { + const PlatformUtil(); + static bool isPhone() { final data = MediaQueryData.fromWindow(WidgetsBinding.instance.window); return data.size.shortestSide < 600; @@ -19,13 +21,14 @@ class PlatformUtil { static Brightness platformBrightness() { final data = MediaQueryData.fromWindow(WidgetsBinding.instance.window); - return data.platformBrightness ?? Brightness.light; + return data.platformBrightness; } static Future initializePortraitLandscapeMode() async { if (isPhone()) { await SystemChrome.setPreferredOrientations( - [DeviceOrientation.portraitUp]); + [DeviceOrientation.portraitUp], + ); } else { await SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, diff --git a/lib/common/util/string_utils.dart b/lib/common/util/string_utils.dart index f308d63b..e55a777b 100644 --- a/lib/common/util/string_utils.dart +++ b/lib/common/util/string_utils.dart @@ -1,21 +1,8 @@ -String concatStringList(List list, String separator) { - var result = ""; - - for (var element in list ?? []) { - result += element + separator; - } - - if (result != "") { - result = result.substring(0, result.length - separator.length); - } - - return result; -} - -String interpolate(String string, List params) { +// TODO: [Leptopoda] deprecate this as propper localization does already have interpolation +String interpolate(String string, List params) { String result = string; for (int i = 0; i < params.length; i++) { - result = result.replaceAll('%$i', params[i]); + result = result.replaceAll('%$i', params[i]!); } return result; diff --git a/lib/date_management/business/date_entry_provider.dart b/lib/date_management/business/date_entry_provider.dart index 714d453d..a493625c 100644 --- a/lib/date_management/business/date_entry_provider.dart +++ b/lib/date_management/business/date_entry_provider.dart @@ -8,12 +8,12 @@ class DateEntryProvider { final DateManagementService _dateEntryService; final DateEntryRepository _dateEntryRepository; - DateEntryProvider(this._dateEntryService, this._dateEntryRepository); + const DateEntryProvider(this._dateEntryService, this._dateEntryRepository); Future> getCachedDateEntries( DateSearchParameters parameters, ) async { - var cachedEntries = []; + List cachedEntries = []; if (parameters.includeFuture && parameters.includePast) { cachedEntries = await _dateEntryRepository.queryAllDateEntries( @@ -21,9 +21,9 @@ class DateEntryProvider { parameters.year, ); } else { - var now = DateTime.now(); + final now = DateTime.now(); if (parameters.includeFuture) { - var datesAfter = await _dateEntryRepository.queryDateEntriesAfter( + final datesAfter = await _dateEntryRepository.queryDateEntriesAfter( parameters.databaseName, parameters.year, now, @@ -32,7 +32,7 @@ class DateEntryProvider { } if (parameters.includePast) { - var datesBefore = await _dateEntryRepository.queryDateEntriesBefore( + final datesBefore = await _dateEntryRepository.queryDateEntriesBefore( parameters.databaseName, parameters.year, now, @@ -41,8 +41,9 @@ class DateEntryProvider { } } - cachedEntries.sort((DateEntry a1, DateEntry a2) => - a1?.start?.compareTo(a2?.start)); + cachedEntries.sort( + (DateEntry a1, DateEntry a2) => a1.start.compareTo(a2.start), + ); print("Read cached ${cachedEntries.length} date entries"); @@ -51,9 +52,9 @@ class DateEntryProvider { Future> getDateEntries( DateSearchParameters parameters, - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ) async { - var updatedEntries = await _dateEntryService.queryAllDates( + final updatedEntries = await _dateEntryService.queryAllDates( parameters, cancellationToken, ); @@ -64,7 +65,7 @@ class DateEntryProvider { ); await _dateEntryRepository.saveDateEntries(updatedEntries); - var filteredDates = _filterDates(updatedEntries, parameters); + final filteredDates = _filterDates(updatedEntries, parameters); print("Read ${filteredDates.length} date entries"); @@ -72,12 +73,14 @@ class DateEntryProvider { } List _filterDates( - List updatedEntries, DateSearchParameters parameters) { - var filteredDateEntries = []; + List updatedEntries, + DateSearchParameters parameters, + ) { + final filteredDateEntries = []; - var now = DateTime.now(); + final now = DateTime.now(); - for (var dateEntry in updatedEntries) { + for (final dateEntry in updatedEntries) { if (dateEntry.databaseName != parameters.databaseName) { continue; } diff --git a/lib/date_management/data/calendar_access.dart b/lib/date_management/data/calendar_access.dart index d57bdec7..5be9a092 100644 --- a/lib/date_management/data/calendar_access.dart +++ b/lib/date_management/data/calendar_access.dart @@ -5,8 +5,6 @@ import 'package:dhbwstudentapp/date_management/model/date_entry.dart'; import 'package:flutter/services.dart'; import 'package:timezone/timezone.dart' as tz; - - enum CalendarPermission { PermissionGranted, PermissionDenied, @@ -19,14 +17,15 @@ enum CalendarPermission { class CalendarAccess { final DeviceCalendarPlugin _deviceCalendarPlugin = DeviceCalendarPlugin(); + CalendarAccess(); Future requestCalendarPermission() async { try { var permissionsGranted = await _deviceCalendarPlugin.hasPermissions(); - if (permissionsGranted.isSuccess && !permissionsGranted.data) { + if (permissionsGranted.isSuccess && !permissionsGranted.data!) { permissionsGranted = await _deviceCalendarPlugin.requestPermissions(); - if (!permissionsGranted.isSuccess || !permissionsGranted.data) { + if (!permissionsGranted.isSuccess || !permissionsGranted.data!) { return CalendarPermission.PermissionDenied; } } @@ -39,39 +38,48 @@ class CalendarAccess { return CalendarPermission.PermissionDenied; } - Future> queryWriteableCalendars() async { + Future?> queryWriteableCalendars() async { final calendarsResult = await _deviceCalendarPlugin.retrieveCalendars(); - var writeableCalendars = []; - for (var calendar in calendarsResult?.data ?? []) { - if (!calendar.isReadOnly) { - writeableCalendars.add(calendar); + final calendars = calendarsResult.data; + + if (calendars != null) { + final writeableCalendars = []; + for (final calendar in calendars) { + if (!calendar.isReadOnly!) { + writeableCalendars.add(calendar); + } } + return writeableCalendars; } - - return writeableCalendars; + return null; } Future addOrUpdateDates( List dateEntries, - Calendar calendar, + Calendar? calendar, ) async { - if ((dateEntries ?? []).isEmpty) return; + if (dateEntries.isEmpty) return; - var existingEvents = - await _getExistingEventsFromCalendar(dateEntries, calendar); + final existingEvents = + await _getExistingEventsFromCalendar(dateEntries, calendar!); - for (var entry in dateEntries) { + for (final entry in dateEntries) { await _addOrUpdateEntry(existingEvents, entry, calendar); } } Future _addOrUpdateEntry( - List existingEvents, DateEntry entry, Calendar calendar) async { + List existingEvents, + DateEntry entry, + Calendar calendar, + ) async { // Find the id in the existing events in order that the "update" part of // createOrUpdateEvent(...) works - var id = _getIdOfExistingEvent(existingEvents, entry); + final id = _getIdOfExistingEvent(existingEvents, entry); - var isAllDay, start, end; + bool isAllDay; + DateTime start; + DateTime end; if (entry.start.isAtSameMomentAs(entry.end)) { isAllDay = isAtMidnight(entry.start); start = entry.start; @@ -82,24 +90,30 @@ class CalendarAccess { end = entry.end; } - - return await _deviceCalendarPlugin.createOrUpdateEvent(Event( - calendar.id, - location: entry.room, - title: entry.description, - description: "${entry.comment}", - eventId: id, - allDay: isAllDay, - start: tz.TZDateTime.from(start, tz.getLocation('Europe/Berlin')), - end: tz.TZDateTime.from(end, tz.getLocation('Europe/Berlin')), - )); + return _deviceCalendarPlugin.createOrUpdateEvent( + Event( + calendar.id, + location: entry.room, + title: entry.description, + description: entry.comment, + eventId: id, + allDay: isAllDay, + start: tz.TZDateTime.from(start, tz.getLocation('Europe/Berlin')), + end: tz.TZDateTime.from(end, tz.getLocation('Europe/Berlin')), + ), + ); } - String _getIdOfExistingEvent(List existingEvents, DateEntry entry) { - var existingEvent = existingEvents - .where((element) => (element.title == entry.description && element.start.toUtc().isAtSameMomentAs(entry.start.toUtc()))) + String? _getIdOfExistingEvent(List existingEvents, DateEntry entry) { + final existingEvent = existingEvents + .where( + (element) => + element.title == entry.description && + (element.start?.toUtc().isAtSameMomentAs(entry.start.toUtc()) ?? + false), + ) .toList(); - String id; + String? id; if (existingEvent.isNotEmpty) { id = existingEvent[0].eventId; @@ -108,30 +122,31 @@ class CalendarAccess { } Future> _getExistingEventsFromCalendar( - List dateEntries, Calendar calendar) async { - var firstEntry = _findFirstEntry(dateEntries); - var lastEntry = _findLastEntry(dateEntries); - - var existingEventsResult = await _deviceCalendarPlugin.retrieveEvents( - calendar.id, - RetrieveEventsParams( - startDate: firstEntry.start, - endDate: lastEntry.end, - )); + List dateEntries, + Calendar calendar, + ) async { + final firstEntry = _findFirstEntry(dateEntries); + final lastEntry = _findLastEntry(dateEntries); - var existingEvents = []; + final existingEventsResult = await _deviceCalendarPlugin.retrieveEvents( + calendar.id, + RetrieveEventsParams( + startDate: firstEntry.start, + endDate: lastEntry.end, + ), + ); if (existingEventsResult.isSuccess) { - existingEvents = existingEventsResult.data.toList(); + return existingEventsResult.data!.toList(); } - return existingEvents; + return []; } DateEntry _findFirstEntry(List entries) { var firstEntry = entries[0]; - for (var entry in entries) { - if (entry.end.isBefore(firstEntry?.end)) { + for (final entry in entries) { + if (entry.end.isBefore(firstEntry.end)) { firstEntry = entry; } } @@ -142,7 +157,7 @@ class CalendarAccess { DateEntry _findLastEntry(List entries) { var lastEntry = entries[0]; - for (var entry in entries) { + for (final entry in entries) { if (entry.end.isAfter(lastEntry.end)) { lastEntry = entry; } diff --git a/lib/date_management/data/date_entry_entity.dart b/lib/date_management/data/date_entry_entity.dart deleted file mode 100644 index 20302338..00000000 --- a/lib/date_management/data/date_entry_entity.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:dhbwstudentapp/common/data/database_entity.dart'; -import 'package:dhbwstudentapp/date_management/model/date_entry.dart'; - -class DateEntryEntity extends DatabaseEntity { - DateEntry _dateEntry; - - DateEntryEntity.fromModel(DateEntry dateEntry) { - _dateEntry = dateEntry; - } - - DateEntryEntity.fromMap(Map map) { - fromMap(map); - } - - @override - void fromMap(Map map) { - DateTime date; - if (map["date"] != null) { - date = DateTime.fromMillisecondsSinceEpoch(map["date"]); - } - - _dateEntry = DateEntry( - comment: map["comment"], - description: map["description"], - year: map["year"], - databaseName: map["databaseName"], - start: date, - end: date, - room: map["room"] - ); - } - - @override - Map toMap() { - return { - "date": _dateEntry.start?.millisecondsSinceEpoch ?? 0, - "comment": _dateEntry.comment ?? "", - "description": _dateEntry.description ?? "", - "year": _dateEntry.year ?? "", - "databaseName": _dateEntry.databaseName ?? "" - }; - } - - DateEntry asDateEntry() => _dateEntry; - - static String tableName() => "DateEntries"; -} diff --git a/lib/date_management/data/date_entry_repository.dart b/lib/date_management/data/date_entry_repository.dart index 7a4f675b..86aef15e 100644 --- a/lib/date_management/data/date_entry_repository.dart +++ b/lib/date_management/data/date_entry_repository.dart @@ -1,18 +1,17 @@ import 'package:dhbwstudentapp/common/data/database_access.dart'; -import 'package:dhbwstudentapp/date_management/data/date_entry_entity.dart'; import 'package:dhbwstudentapp/date_management/model/date_entry.dart'; class DateEntryRepository { final DatabaseAccess _database; - DateEntryRepository(this._database); + const DateEntryRepository(this._database); Future> queryAllDateEntries( - String databaseName, - String year, + String? databaseName, + String? year, ) async { - var rows = await _database.queryRows( - DateEntryEntity.tableName(), + final rows = await _database.queryRows( + DateEntry.tableName, where: "databaseName=? AND year=?", whereArgs: [databaseName, year], ); @@ -20,14 +19,14 @@ class DateEntryRepository { return _rowsToDateEntries(rows); } - Future> queryDateEntriesBetween( + Future> queryDateEntriesBetween( String databaseName, String year, DateTime start, DateTime end, ) async { - var rows = await _database.queryRows( - DateEntryEntity.tableName(), + final rows = await _database.queryRows( + DateEntry.tableName, where: "date>=? AND date<=? AND databaseName=? AND year=?", whereArgs: [ start.millisecondsSinceEpoch, @@ -41,12 +40,12 @@ class DateEntryRepository { } Future> queryDateEntriesAfter( - String databaseName, - String year, + String? databaseName, + String? year, DateTime date, ) async { - var rows = await _database.queryRows( - DateEntryEntity.tableName(), + final rows = await _database.queryRows( + DateEntry.tableName, where: "date>=? AND databaseName=? AND year=?", whereArgs: [ date.millisecondsSinceEpoch, @@ -59,12 +58,12 @@ class DateEntryRepository { } Future> queryDateEntriesBefore( - String databaseName, - String year, + String? databaseName, + String? year, DateTime date, ) async { - var rows = await _database.queryRows( - DateEntryEntity.tableName(), + final rows = await _database.queryRows( + DateEntry.tableName, where: "date<=? AND databaseName=? AND year=?", whereArgs: [ date.millisecondsSinceEpoch, @@ -77,34 +76,36 @@ class DateEntryRepository { } Future saveDateEntry(DateEntry entry) async { - var row = DateEntryEntity.fromModel(entry).toMap(); - await _database.insert(DateEntryEntity.tableName(), row); + final row = entry.toJson(); + await _database.insert(DateEntry.tableName, row); } Future saveDateEntries(List entries) async { - for (var entry in entries) { + for (final entry in entries) { await saveDateEntry(entry); } } Future deleteAllDateEntries( - String databaseName, - String year, + String? databaseName, + String? year, ) async { - await _database.deleteWhere(DateEntryEntity.tableName(), - where: "databaseName=? AND year=?", - whereArgs: [ - databaseName, - year, - ]); + await _database.deleteWhere( + DateEntry.tableName, + where: "databaseName=? AND year=?", + whereArgs: [ + databaseName, + year, + ], + ); } List _rowsToDateEntries(List> rows) { - var dateEntries = []; + final dateEntries = []; - for (var row in rows) { + for (final row in rows) { dateEntries.add( - DateEntryEntity.fromMap(row).asDateEntry(), + DateEntry.fromJson(row), ); } diff --git a/lib/date_management/model/date_database.dart b/lib/date_management/model/date_database.dart index e9da69ed..a10f437a 100644 --- a/lib/date_management/model/date_database.dart +++ b/lib/date_management/model/date_database.dart @@ -2,5 +2,5 @@ class DateDatabase { final String id; final String displayName; - DateDatabase(this.displayName, this.id); + const DateDatabase(this.displayName, this.id); } diff --git a/lib/date_management/model/date_entry.dart b/lib/date_management/model/date_entry.dart index 02ba7271..52252419 100644 --- a/lib/date_management/model/date_entry.dart +++ b/lib/date_management/model/date_entry.dart @@ -1,19 +1,40 @@ +import 'package:dhbwstudentapp/common/data/epoch_date_time_converter.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'date_entry.g.dart'; + +@JsonSerializable() class DateEntry { final String description; final String year; final String comment; final String databaseName; + @JsonKey(name: "date") + @EpochDateTimeConverter() final DateTime start; + @EpochDateTimeConverter() final DateTime end; - final String room; + final String? room; DateEntry({ - this.description, - this.year, - this.comment, - this.databaseName, - this.start, - this.end, - this.room - }); + String? description, + String? year, + String? comment, + String? databaseName, + DateTime? start, + DateTime? end, + this.room, + }) : start = start ?? DateTime.fromMicrosecondsSinceEpoch(0), + end = end ?? start ?? DateTime.fromMicrosecondsSinceEpoch(0), + comment = comment ?? "", + description = description ?? "", + year = year ?? "", + databaseName = databaseName ?? ""; + + factory DateEntry.fromJson(Map json) => + _$DateEntryFromJson(json); + + Map toJson() => _$DateEntryToJson(this); + + static const tableName = "DateEntries"; } diff --git a/lib/date_management/model/date_entry.g.dart b/lib/date_management/model/date_entry.g.dart new file mode 100644 index 00000000..b7c6130d --- /dev/null +++ b/lib/date_management/model/date_entry.g.dart @@ -0,0 +1,35 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'date_entry.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DateEntry _$DateEntryFromJson(Map json) => DateEntry( + description: json['description'] as String?, + year: json['year'] as String?, + comment: json['comment'] as String?, + databaseName: json['databaseName'] as String?, + start: _$JsonConverterFromJson( + json['date'], const EpochDateTimeConverter().fromJson), + end: _$JsonConverterFromJson( + json['end'], const EpochDateTimeConverter().fromJson), + room: json['room'] as String?, + ); + +Map _$DateEntryToJson(DateEntry instance) => { + 'description': instance.description, + 'year': instance.year, + 'comment': instance.comment, + 'databaseName': instance.databaseName, + 'date': const EpochDateTimeConverter().toJson(instance.start), + 'end': const EpochDateTimeConverter().toJson(instance.end), + 'room': instance.room, + }; + +Value? _$JsonConverterFromJson( + Object? json, + Value? Function(Json json) fromJson, +) => + json == null ? null : fromJson(json as Json); diff --git a/lib/date_management/model/date_search_parameters.dart b/lib/date_management/model/date_search_parameters.dart index a6a1dfc5..430f8298 100644 --- a/lib/date_management/model/date_search_parameters.dart +++ b/lib/date_management/model/date_search_parameters.dart @@ -1,10 +1,10 @@ class DateSearchParameters { - bool includePast; - bool includeFuture; - String year; - String databaseName; + final bool includePast; + final bool includeFuture; + final String? year; + final String? databaseName; - DateSearchParameters( + const DateSearchParameters( this.includePast, this.includeFuture, this.year, diff --git a/lib/date_management/service/date_management_service.dart b/lib/date_management/service/date_management_service.dart index a192677a..1f153a82 100644 --- a/lib/date_management/service/date_management_service.dart +++ b/lib/date_management/service/date_management_service.dart @@ -5,16 +5,18 @@ import 'package:dhbwstudentapp/date_management/service/parsing/all_dates_extract import 'package:dhbwstudentapp/dualis/service/session.dart'; class DateManagementService { + const DateManagementService(); + Future> queryAllDates( DateSearchParameters parameters, - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ) async { - var queryResult = await Session().get( + final queryResult = await Session().get( _buildRequestUrl(parameters), cancellationToken, ); - var allDates = AllDatesExtract().extractAllDates( + final allDates = const AllDatesExtract().extractAllDates( queryResult, parameters.databaseName, ); diff --git a/lib/date_management/service/parsing/all_dates_extract.dart b/lib/date_management/service/parsing/all_dates_extract.dart index 3db64a4f..a8e94c55 100644 --- a/lib/date_management/service/parsing/all_dates_extract.dart +++ b/lib/date_management/service/parsing/all_dates_extract.dart @@ -5,7 +5,10 @@ import 'package:intl/intl.dart'; // TODO: Parse exception to common module class AllDatesExtract { - List extractAllDates(String body, String databaseName) { + const AllDatesExtract(); + + List extractAllDates(String? body, String? databaseName) { + if (body == null) return []; try { return _extractAllDates(body, databaseName); } catch (e, trace) { @@ -14,22 +17,22 @@ class AllDatesExtract { } } - List _extractAllDates(String body, String databaseName) { - body = body.replaceAll(new RegExp("<(br|BR)[ /]*>"), "\n"); - var document = parse(body); + List _extractAllDates(String body, String? databaseName) { + body = body.replaceAll(RegExp("<(br|BR)[ /]*>"), "\n"); + final document = parse(body); // The dates are located in the first

element of the page - var dateContainingElement = document.getElementsByTagName("p")[0]; + final dateContainingElement = document.getElementsByTagName("p")[0]; - var dateEntries = []; + final dateEntries = []; - for (var a in dateContainingElement.nodes.sublist(0)) { - var text = a.text; + for (final a in dateContainingElement.nodes.sublist(0)) { + final text = a.text!; - var lines = text.split("\n"); + final lines = text.split("\n"); - for (var line in lines) { - var dateEntry = _parseDateEntryLine(line, databaseName); + for (final line in lines) { + final dateEntry = _parseDateEntryLine(line, databaseName); if (dateEntry != null) { dateEntries.add(dateEntry); @@ -38,41 +41,42 @@ class AllDatesExtract { } dateEntries - .sort((DateEntry e1, DateEntry e2) => e1.start?.compareTo(e2.start)); + .sort((DateEntry e1, DateEntry e2) => e1.start.compareTo(e2.start)); return dateEntries; } - DateEntry _parseDateEntryLine(String line, String databaseName) { - var parts = line.split(';'); + DateEntry? _parseDateEntryLine(String line, String? databaseName) { + final parts = line.split(';'); if (parts.length != 5) { return null; } - var date = _parseDateTime( + final date = _parseDateTime( parts[2].trim(), parts[3].trim(), ); return DateEntry( - comment: parts[4].trim(), - description: parts[0].trim(), - year: parts[1].trim(), - databaseName: databaseName, - start: date, - end: date); + comment: parts[4].trim(), + description: parts[0].trim(), + year: parts[1].trim(), + databaseName: databaseName, + start: date, + end: date, + ); } - DateTime _parseDateTime(String date, String time) { + DateTime? _parseDateTime(String date, String time) { if (time == "24:00") { time = "00:00"; } - var dateAndTimeString = date + " " + time; + final dateAndTimeString = "$date $time"; try { - var date = DateFormat("dd.MM.yyyy hh:mm").parse(dateAndTimeString); + final date = DateFormat("dd.MM.yyyy hh:mm").parse(dateAndTimeString); return date; } on FormatException catch (_) { return null; diff --git a/lib/date_management/ui/calendar_export_page.dart b/lib/date_management/ui/calendar_export_page.dart index 6ee2188d..e0231574 100644 --- a/lib/date_management/ui/calendar_export_page.dart +++ b/lib/date_management/ui/calendar_export_page.dart @@ -1,7 +1,7 @@ import 'package:device_calendar/device_calendar.dart'; import 'package:dhbwstudentapp/common/data/preferences/preferences_provider.dart'; import 'package:dhbwstudentapp/common/i18n/localizations.dart'; -import 'package:dhbwstudentapp/common/ui/colors.dart'; +import 'package:dhbwstudentapp/common/ui/app_theme.dart'; import 'package:dhbwstudentapp/date_management/data/calendar_access.dart'; import 'package:dhbwstudentapp/date_management/model/date_entry.dart'; import 'package:dhbwstudentapp/date_management/ui/viewmodels/calendar_export_view_model.dart'; @@ -14,36 +14,32 @@ class CalendarExportPage extends StatefulWidget { final bool isCalendarSyncWidget; final bool isCalendarSyncEnabled; - const CalendarExportPage( - {Key key, - this.entriesToExport, - this.isCalendarSyncWidget = false, - this.isCalendarSyncEnabled = false}) - : super(key: key); + const CalendarExportPage({ + super.key, + required this.entriesToExport, + this.isCalendarSyncWidget = false, + this.isCalendarSyncEnabled = false, + }); @override - _CalendarExportPageState createState() => _CalendarExportPageState( - entriesToExport, isCalendarSyncWidget, isCalendarSyncEnabled); + _CalendarExportPageState createState() => _CalendarExportPageState(); } class _CalendarExportPageState extends State { - final List entriesToExport; - final bool isCalendarSyncWidget; - final bool isCalendarSyncEnabled; - CalendarExportViewModel viewModel; + late CalendarExportViewModel viewModel; - _CalendarExportPageState(this.entriesToExport, this.isCalendarSyncWidget, - this.isCalendarSyncEnabled); + _CalendarExportPageState(); @override void initState() { super.initState(); - viewModel = CalendarExportViewModel(entriesToExport, CalendarAccess(), - KiwiContainer().resolve()); - viewModel.setOnPermissionDeniedCallback(() { - Navigator.of(context).pop(); - }); + viewModel = CalendarExportViewModel( + widget.entriesToExport, + CalendarAccess(), + KiwiContainer().resolve(), + ); + viewModel.onPermissionDeniedCallback = Navigator.of(context).pop; viewModel.loadSelectedCalendar(); } @@ -57,9 +53,11 @@ class _CalendarExportPageState extends State { actionsIconTheme: Theme.of(context).iconTheme, elevation: 0, iconTheme: Theme.of(context).iconTheme, - title: Text(this.isCalendarSyncWidget - ? L.of(context).calendarSyncPageTitle - : L.of(context).dateManagementExportToCalendar), + title: Text( + widget.isCalendarSyncWidget + ? L.of(context).calendarSyncPageTitle + : L.of(context).dateManagementExportToCalendar, + ), toolbarTextStyle: Theme.of(context).textTheme.bodyText2, titleTextStyle: Theme.of(context).textTheme.headline6, ), @@ -69,9 +67,11 @@ class _CalendarExportPageState extends State { padding: const EdgeInsets.all(24), child: Align( alignment: Alignment.centerLeft, - child: Text(this.isCalendarSyncWidget - ? L.of(context).calendarSyncPageSubtitle - : L.of(context).dateManagementExportToCalendarDescription), + child: Text( + widget.isCalendarSyncWidget + ? L.of(context).calendarSyncPageSubtitle + : L.of(context).dateManagementExportToCalendarDescription, + ), ), ), _buildCalendarList(), @@ -89,12 +89,13 @@ class _CalendarExportPageState extends State { Widget _buildCalendarList() { return Expanded( child: PropertyChangeConsumer( - builder: (BuildContext context, CalendarExportViewModel viewModel, _) => - ListView.builder( - itemCount: viewModel.calendars.length, + builder: + (BuildContext context, CalendarExportViewModel? viewModel, _) => + ListView.builder( + itemCount: viewModel!.calendars.length, itemBuilder: (BuildContext context, int index) { - var isSelected = viewModel.selectedCalendar?.id == - viewModel.calendars[index]?.id; + final isSelected = + viewModel.selectedCalendar?.id == viewModel.calendars[index].id; return _buildCalendarListEntry( viewModel.calendars[index], @@ -109,33 +110,31 @@ class _CalendarExportPageState extends State { Widget _buildStopCalendarSyncBtn() { // Dont display the "Synchronisation beenden" button, //if synchronization is not enabled or if it is not the right page - if (!this.isCalendarSyncWidget) return SizedBox(); + if (!widget.isCalendarSyncWidget) return const SizedBox(); return PropertyChangeProvider( value: viewModel, child: Column( children: [ - Container( - decoration: this.isCalendarSyncEnabled ? null : null, - child: ListTile( - enabled: this.isCalendarSyncEnabled ? true : false, - title: Text( - L.of(context).calendarSyncPageEndSync.toUpperCase(), - textAlign: TextAlign.center, - style: TextStyle( - color: this.isCalendarSyncEnabled - ? ColorPalettes.main - : Theme.of(context).disabledColor, - fontSize: 14), + ListTile( + enabled: widget.isCalendarSyncEnabled, + title: Text( + L.of(context).calendarSyncPageEndSync.toUpperCase(), + textAlign: TextAlign.center, + style: TextStyle( + color: widget.isCalendarSyncEnabled + ? AppTheme.main + : Theme.of(context).disabledColor, + fontSize: 14, ), - onTap: () async { - KiwiContainer() - .resolve() - .setIsCalendarSyncEnabled(false); - viewModel.resetSelectedCalendar(); - Navigator.of(context).pop(); - }, ), + onTap: () async { + KiwiContainer() + .resolve() + .setIsCalendarSyncEnabled(false); + viewModel.resetSelectedCalendar(); + Navigator.of(context).pop(); + }, ), const Divider( height: 1, @@ -156,21 +155,20 @@ class _CalendarExportPageState extends State { child: Padding( padding: const EdgeInsets.fromLTRB(32, 16, 32, 16), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( width: 24, height: 24, decoration: BoxDecoration( shape: BoxShape.circle, - color: isSelected ? Color(calendar.color) : Colors.transparent, + color: isSelected ? Color(calendar.color!) : Colors.transparent, border: Border.all( - color: Color(calendar.color), + color: Color(calendar.color!), width: 4, ), ), child: isSelected - ? Center( + ? const Center( child: Icon( Icons.check, size: 16, @@ -181,7 +179,7 @@ class _CalendarExportPageState extends State { ), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 0, 0), - child: Text(calendar.name), + child: Text(calendar.name!), ), ], ), @@ -191,20 +189,22 @@ class _CalendarExportPageState extends State { Widget _buildExportButton() { return PropertyChangeConsumer( - builder: (BuildContext context, CalendarExportViewModel viewModel, _) => - viewModel.isExporting + builder: (BuildContext context, CalendarExportViewModel? viewModel, _) => + viewModel!.isExporting ? const Padding( padding: EdgeInsets.fromLTRB(8, 8, 8, 15), child: SizedBox( - height: 32, - width: 32, - child: CircularProgressIndicator()), + height: 32, + width: 32, + child: CircularProgressIndicator(), + ), ) - : Container( + : DecoratedBox( decoration: !viewModel.canExport - ? new BoxDecoration( - color: Theme.of(context).colorScheme.background) - : new BoxDecoration( + ? BoxDecoration( + color: Theme.of(context).colorScheme.background, + ) + : BoxDecoration( color: Theme.of(context).colorScheme.primary, ), child: Padding( @@ -214,7 +214,7 @@ class _CalendarExportPageState extends State { borderRadius: BorderRadius.all(Radius.circular(4.0)), ), title: Text( - this.isCalendarSyncWidget + widget.isCalendarSyncWidget ? L .of(context) .calendarSyncPageBeginSync @@ -224,18 +224,19 @@ class _CalendarExportPageState extends State { .dateManagementExportToCalendarConfirm .toUpperCase(), textAlign: TextAlign.center, - style: TextStyle( + style: const TextStyle( fontSize: 14, color: Colors.white, ), ), onTap: viewModel.canExport ? () async { - if (this.isCalendarSyncWidget) { - var preferencesProvider = KiwiContainer() + if (widget.isCalendarSyncWidget) { + final preferencesProvider = KiwiContainer() .resolve(); preferencesProvider.setSelectedCalendar( - viewModel.selectedCalendar); + viewModel.selectedCalendar, + ); preferencesProvider .setIsCalendarSyncEnabled(true); } diff --git a/lib/date_management/ui/date_management_navigation_entry.dart b/lib/date_management/ui/date_management_navigation_entry.dart index 57856113..dc46e32b 100644 --- a/lib/date_management/ui/date_management_navigation_entry.dart +++ b/lib/date_management/ui/date_management_navigation_entry.dart @@ -1,5 +1,4 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; -import 'package:dhbwstudentapp/common/ui/viewmodels/base_view_model.dart'; import 'package:dhbwstudentapp/date_management/ui/calendar_export_page.dart'; import 'package:dhbwstudentapp/date_management/ui/date_management_page.dart'; import 'package:dhbwstudentapp/date_management/ui/viewmodels/date_management_view_model.dart'; @@ -10,13 +9,12 @@ import 'package:flutter/material.dart'; import 'package:kiwi/kiwi.dart'; import 'package:property_change_notifier/property_change_notifier.dart'; -class DateManagementNavigationEntry extends NavigationEntry { - DateManagementViewModel _viewModel; +class DateManagementNavigationEntry + extends NavigationEntry { + DateManagementNavigationEntry(); @override - Widget icon(BuildContext context) { - return Icon(Icons.date_range); - } + Icon icon = const Icon(Icons.date_range); @override String title(BuildContext context) { @@ -24,40 +22,37 @@ class DateManagementNavigationEntry extends NavigationEntry { } @override - BaseViewModel initViewModel() { - if (_viewModel == null) { - _viewModel = DateManagementViewModel( - KiwiContainer().resolve(), - KiwiContainer().resolve(), - ); - } - - return _viewModel; + DateManagementViewModel initViewModel() { + return DateManagementViewModel( + KiwiContainer().resolve(), + KiwiContainer().resolve(), + ); } @override List appBarActions(BuildContext context) { - initViewModel(); return [ IconButton( - icon: Icon(Icons.help_outline), + icon: const Icon(Icons.help_outline), onPressed: () async { - await DateManagementHelpDialog().show(context); + await const DateManagementHelpDialog().show(context); }, tooltip: L.of(context).helpButtonTooltip, ), PropertyChangeProvider( - value: _viewModel, + value: model, child: PropertyChangeConsumer( - builder: - (BuildContext context, DateManagementViewModel viewModel, _) => - PopupMenuButton( + builder: (BuildContext context, DateManagementViewModel? _, __) => + PopupMenuButton( onSelected: (i) async { - await NavigatorKey.rootKey.currentState.push(MaterialPageRoute( + await NavigatorKey.rootKey.currentState?.push( + MaterialPageRoute( builder: (BuildContext context) => CalendarExportPage( - entriesToExport: viewModel.allDates, - ), - settings: RouteSettings(name: "settings"))); + entriesToExport: model.allDates!, + ), + settings: const RouteSettings(name: "settings"), + ), + ); }, itemBuilder: (BuildContext context) { return [ @@ -75,7 +70,7 @@ class DateManagementNavigationEntry extends NavigationEntry { @override Widget build(BuildContext context) { - return Scaffold(body: DateManagementPage()); + return const Scaffold(body: DateManagementPage()); } @override diff --git a/lib/date_management/ui/date_management_page.dart b/lib/date_management/ui/date_management_page.dart index 5ace61e4..e321030e 100644 --- a/lib/date_management/ui/date_management_page.dart +++ b/lib/date_management/ui/date_management_page.dart @@ -1,5 +1,4 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; -import 'package:dhbwstudentapp/common/ui/viewmodels/base_view_model.dart'; import 'package:dhbwstudentapp/common/ui/widgets/error_display.dart'; import 'package:dhbwstudentapp/common/util/date_utils.dart'; import 'package:dhbwstudentapp/date_management/model/date_entry.dart'; @@ -12,9 +11,12 @@ import 'package:property_change_notifier/property_change_notifier.dart'; import 'package:provider/provider.dart'; class DateManagementPage extends StatelessWidget { + const DateManagementPage({super.key}); + @override Widget build(BuildContext context) { - DateManagementViewModel viewModel = Provider.of(context); + final DateManagementViewModel viewModel = + Provider.of(context); return PropertyChangeProvider( value: viewModel, @@ -26,10 +28,11 @@ class DateManagementPage extends StatelessWidget { children: [ const Divider(), AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: viewModel.isLoading - ? const LinearProgressIndicator() - : Container()), + duration: const Duration(milliseconds: 200), + child: viewModel.isLoading + ? const LinearProgressIndicator() + : Container(), + ), ], ), _buildBody(viewModel, context), @@ -43,28 +46,27 @@ class DateManagementPage extends StatelessWidget { child: Stack( children: [ SingleChildScrollView( - scrollDirection: Axis.vertical, child: PropertyChangeConsumer( builder: ( BuildContext context, - DateManagementViewModel model, + DateManagementViewModel? model, _, ) => AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: Column( - key: ValueKey( - viewModel?.dateSearchParameters?.toString() ?? ""), - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildAllDatesDataTable(model, context), - ], - )), + duration: const Duration(milliseconds: 200), + child: Column( + key: ValueKey(viewModel.dateSearchParameters.toString()), + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildAllDatesDataTable(model!, context), + ], + ), + ), ), ), Align( - child: buildErrorDisplay(context), alignment: Alignment.bottomCenter, + child: buildErrorDisplay(context), ), ], ), @@ -93,43 +95,50 @@ class DateManagementPage extends StatelessWidget { DateManagementViewModel model, BuildContext context, ) { - var dataRows = []; - for (DateEntry dateEntry in model?.allDates ?? []) { + final dataRows = []; + for (final DateEntry dateEntry in model.allDates ?? []) { dataRows.add( DataRow( cells: [ DataCell( - Text(dateEntry.description, - style: dateEntry.end.isBefore(DateTime.now()) - ? TextStyle(decoration: TextDecoration.lineThrough) - : null), onTap: () { - showDateEntryDetailBottomSheet(context, dateEntry); - }), + Text( + dateEntry.description, + style: dateEntry.end.isBefore(DateTime.now()) + ? const TextStyle(decoration: TextDecoration.lineThrough) + : null, + ), + onTap: () { + showDateEntryDetailBottomSheet(context, dateEntry); + }, + ), DataCell( - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - DateFormat.yMd(L.of(context).locale.languageCode) - .format(dateEntry.start), - style: Theme.of(context).textTheme.bodyText1, + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + DateFormat.yMd(L.of(context).locale.languageCode) + .format(dateEntry.start), + style: Theme.of(context).textTheme.bodyText1, + ), + // When the date entry has a time of 00:00 don't show it. + // It means the date entry is for the whole day + if (isAtMidnight(dateEntry.start)) + Container() + else + Padding( + padding: const EdgeInsets.fromLTRB(0, 4, 0, 0), + child: Text( + DateFormat.Hm(L.of(context).locale.languageCode) + .format(dateEntry.start), + ), ), - // When the date entry has a time of 00:00 don't show it. - // It means the date entry is for the whole day - isAtMidnight(dateEntry.start) - ? Container() - : Padding( - padding: const EdgeInsets.fromLTRB(0, 4, 0, 0), - child: Text( - DateFormat.Hm(L.of(context).locale.languageCode) - .format(dateEntry.start), - ), - ), - ], - ), onTap: () { - showDateEntryDetailBottomSheet(context, dateEntry); - }), + ], + ), + onTap: () { + showDateEntryDetailBottomSheet(context, dateEntry); + }, + ), ], ), ); @@ -142,9 +151,7 @@ class DateManagementPage extends StatelessWidget { showModalBottomSheet( useRootNavigator: true, context: context, - builder: (context) => DateDetailBottomSheet( - dateEntry: entry, - ), + builder: (context) => DateDetailBottomSheet(dateEntry: entry), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(12.0)), ), @@ -156,10 +163,13 @@ class DateManagementPage extends StatelessWidget { properties: const [ "updateFailed", ], - builder: (BuildContext context, DateManagementViewModel model, - Set properties) => + builder: ( + BuildContext context, + DateManagementViewModel? model, + Set? properties, + ) => ErrorDisplay( - show: model.updateFailed, + show: model!.updateFailed, ), ); } diff --git a/lib/date_management/ui/viewmodels/calendar_export_view_model.dart b/lib/date_management/ui/viewmodels/calendar_export_view_model.dart index 7bbf6d6a..4070663f 100644 --- a/lib/date_management/ui/viewmodels/calendar_export_view_model.dart +++ b/lib/date_management/ui/viewmodels/calendar_export_view_model.dart @@ -11,25 +11,29 @@ class CalendarExportViewModel extends BaseViewModel { final PreferencesProvider preferencesProvider; final List _entriesToExport; - OnPermissionDenied _onPermissionDenied; + OnPermissionDenied? _onPermissionDenied; - List _calendars; - List get calendars => _calendars ?? []; + List? _calendars; + List get calendars => _calendars ??= []; - Calendar _selectedCalendar; - Calendar get selectedCalendar => _selectedCalendar; + Calendar? _selectedCalendar; + Calendar? get selectedCalendar => _selectedCalendar; bool get canExport => _selectedCalendar != null; bool _isExporting = false; bool get isExporting => _isExporting; - CalendarExportViewModel(this._entriesToExport, this.calendarAccess, this.preferencesProvider) { + CalendarExportViewModel( + this._entriesToExport, + this.calendarAccess, + this.preferencesProvider, + ) { loadCalendars(); } Future loadCalendars() async { - var access = await calendarAccess.requestCalendarPermission(); + final access = await calendarAccess.requestCalendarPermission(); if (access == CalendarPermission.PermissionDenied) { _onPermissionDenied?.call(); @@ -41,14 +45,14 @@ class CalendarExportViewModel extends BaseViewModel { notifyListeners("_calendars"); } - void loadSelectedCalendar() async{ + Future loadSelectedCalendar() async { _selectedCalendar = await preferencesProvider.getSelectedCalendar(); notifyListeners("selectedCalendar"); } - void resetSelectedCalendar() async{ + Future resetSelectedCalendar() async { await preferencesProvider.setSelectedCalendar(null); - this.loadCalendars(); + loadCalendars(); } void toggleSelection(Calendar calendar) { @@ -77,7 +81,7 @@ class CalendarExportViewModel extends BaseViewModel { } } - void setOnPermissionDeniedCallback(OnPermissionDenied function) { + set onPermissionDeniedCallback(OnPermissionDenied function) { _onPermissionDenied = function; } } diff --git a/lib/date_management/ui/viewmodels/date_management_view_model.dart b/lib/date_management/ui/viewmodels/date_management_view_model.dart index 2c4b1765..7cc666e9 100644 --- a/lib/date_management/ui/viewmodels/date_management_view_model.dart +++ b/lib/date_management/ui/viewmodels/date_management_view_model.dart @@ -3,46 +3,51 @@ import 'dart:async'; import 'package:dhbwstudentapp/common/data/preferences/preferences_provider.dart'; import 'package:dhbwstudentapp/common/ui/viewmodels/base_view_model.dart'; import 'package:dhbwstudentapp/common/util/cancelable_mutex.dart'; -import 'package:dhbwstudentapp/common/util/cancellation_token.dart'; import 'package:dhbwstudentapp/date_management/business/date_entry_provider.dart'; import 'package:dhbwstudentapp/date_management/model/date_database.dart'; import 'package:dhbwstudentapp/date_management/model/date_entry.dart'; import 'package:dhbwstudentapp/date_management/model/date_search_parameters.dart'; -import 'package:dhbwstudentapp/schedule/service/schedule_source.dart'; class DateManagementViewModel extends BaseViewModel { final DateEntryProvider _dateEntryProvider; final PreferencesProvider _preferencesProvider; final List _allDateDatabases = [ - DateDatabase("BWL-Bank", "Termine_BWL_Bank"), - DateDatabase("Immobilienwirtschaft", "Termine_BWL_Immo"), - DateDatabase( - "Dienstleistungsmanagement Consulting & Sales", "Termine_DLM_Consult"), - DateDatabase("Dienstleistungsmanagement Logistik", "Termine_DLM_Logistik"), - DateDatabase("Campus Horb Informatik", "Termine_Horb_INF"), - DateDatabase("Campus Horb Maschinenbau", "Termine_Horb_MB"), - DateDatabase("International Business", "Termine_IB"), - DateDatabase("Informatik", "Termine_Informatik"), - DateDatabase("MUK (DLM - C&S, LogM, MUK)", "Termine_MUK"), - DateDatabase("SO_GuO (Abweichungen und Ergänzungen zum Vorlesungsplan)", - "Termine_SO_GuO"), - DateDatabase("Wirtschaftsingenieurwesen", "Termine_WIW"), + const DateDatabase("BWL-Bank", "Termine_BWL_Bank"), + const DateDatabase("Immobilienwirtschaft", "Termine_BWL_Immo"), + const DateDatabase( + "Dienstleistungsmanagement Consulting & Sales", + "Termine_DLM_Consult", + ), + const DateDatabase( + "Dienstleistungsmanagement Logistik", + "Termine_DLM_Logistik", + ), + const DateDatabase("Campus Horb Informatik", "Termine_Horb_INF"), + const DateDatabase("Campus Horb Maschinenbau", "Termine_Horb_MB"), + const DateDatabase("International Business", "Termine_IB"), + const DateDatabase("Informatik", "Termine_Informatik"), + const DateDatabase("MUK (DLM - C&S, LogM, MUK)", "Termine_MUK"), + const DateDatabase( + "SO_GuO (Abweichungen und Ergänzungen zum Vorlesungsplan)", + "Termine_SO_GuO", + ), + const DateDatabase("Wirtschaftsingenieurwesen", "Termine_WIW"), ]; List get allDateDatabases => _allDateDatabases; final CancelableMutex _updateMutex = CancelableMutex(); - Timer _errorResetTimer; + Timer? _errorResetTimer; - List _years; + final List _years = []; List get years => _years; - String _currentSelectedYear; - String get currentSelectedYear => _currentSelectedYear; + String? _currentSelectedYear; + String? get currentSelectedYear => _currentSelectedYear; - List _allDates; - List get allDates => _allDates; + List? _allDates; + List? get allDates => _allDates; bool _showPassedDates = false; bool get showPassedDates => _showPassedDates; @@ -53,8 +58,8 @@ class DateManagementViewModel extends BaseViewModel { bool _isLoading = false; bool get isLoading => _isLoading; - DateDatabase _currentDateDatabase; - DateDatabase get currentDateDatabase => _currentDateDatabase; + DateDatabase? _currentDateDatabase; + DateDatabase? get currentDateDatabase => _currentDateDatabase; int _dateEntriesKeyIndex = 0; int get dateEntriesKeyIndex => _dateEntriesKeyIndex; @@ -75,8 +80,6 @@ class DateManagementViewModel extends BaseViewModel { } void _buildYearsArray() { - _years = []; - for (var i = 2017; i < DateTime.now().year + 3; i++) { _years.add(i.toString()); } @@ -90,7 +93,8 @@ class DateManagementViewModel extends BaseViewModel { notifyListeners("isLoading"); await _doUpdateDates(); - } catch (_) {} finally { + } catch (_) { + } finally { _isLoading = false; _updateMutex.release(); notifyListeners("isLoading"); @@ -98,12 +102,12 @@ class DateManagementViewModel extends BaseViewModel { } Future _doUpdateDates() async { - var cachedDateEntries = await _readCachedDateEntries(); - _updateMutex.token.throwIfCancelled(); + final cachedDateEntries = await _readCachedDateEntries(); + _updateMutex.token?.throwIfCancelled(); _setAllDates(cachedDateEntries); - var loadedDateEntries = await _readUpdatedDateEntries(); - _updateMutex.token.throwIfCancelled(); + final loadedDateEntries = await _readUpdatedDateEntries(); + _updateMutex.token?.throwIfCancelled(); if (loadedDateEntries != null) { _setAllDates(loadedDateEntries); @@ -117,23 +121,22 @@ class DateManagementViewModel extends BaseViewModel { notifyListeners("updateFailed"); } - Future> _readUpdatedDateEntries() async { + Future?> _readUpdatedDateEntries() async { try { - var loadedDateEntries = await _dateEntryProvider.getDateEntries( + final loadedDateEntries = await _dateEntryProvider.getDateEntries( dateSearchParameters, _updateMutex.token, ); return loadedDateEntries; - } on OperationCancelledException {} on ServiceRequestFailed {} - - return null; + } catch (_) { + return null; + } } Future> _readCachedDateEntries() async { - var cachedDateEntries = await _dateEntryProvider.getCachedDateEntries( + return _dateEntryProvider.getCachedDateEntries( dateSearchParameters, ); - return cachedDateEntries; } void _setAllDates(List dateEntries) { @@ -143,59 +146,60 @@ class DateManagementViewModel extends BaseViewModel { notifyListeners("allDates"); } - void setShowPassedDates(bool value) { + set showPassedDates(bool? value) { + if (value == null) return; + _showPassedDates = value; notifyListeners("showPassedDates"); } - void setShowFutureDates(bool value) { + set showFutureDates(bool? value) { + if (value == null) return; + _showFutureDates = value; notifyListeners("showFutureDates"); } - void setCurrentDateDatabase(DateDatabase database) { + set currentDateDatabase(DateDatabase? database) { _currentDateDatabase = database; notifyListeners("currentDateDatabase"); _preferencesProvider.setLastViewedDateEntryDatabase(database?.id); } - void setCurrentSelectedYear(String year) { + set currentSelectedYear(String? year) { + if (year == null) return; + _currentSelectedYear = year; notifyListeners("currentSelectedYear"); _preferencesProvider.setLastViewedDateEntryYear(year); } - void _loadDefaultSelection() async { - var database = await _preferencesProvider.getLastViewedDateEntryDatabase(); + Future _loadDefaultSelection() async { + final database = + await _preferencesProvider.getLastViewedDateEntryDatabase(); bool didSetDatabase = false; - for (var db in allDateDatabases) { + for (final db in allDateDatabases) { if (db.id == database) { - setCurrentDateDatabase(db); + currentDateDatabase = db; didSetDatabase = true; } } if (!didSetDatabase) { - setCurrentDateDatabase(allDateDatabases[0]); + currentDateDatabase = allDateDatabases[0]; } - var year = await _preferencesProvider.getLastViewedDateEntryYear(); - if (year != null) { - setCurrentSelectedYear(year); - } else { - setCurrentSelectedYear(years[0]); - } + final year = await _preferencesProvider.getLastViewedDateEntryYear(); + currentSelectedYear = year ?? years[0]; await updateDates(); } - void _cancelErrorInFuture() async { - if (_errorResetTimer != null) { - _errorResetTimer.cancel(); - } + Future _cancelErrorInFuture() async { + _errorResetTimer?.cancel(); _errorResetTimer = Timer( const Duration(seconds: 5), diff --git a/lib/date_management/ui/widgets/date_detail_bottom_sheet.dart b/lib/date_management/ui/widgets/date_detail_bottom_sheet.dart index 679eba0e..656c1bd6 100644 --- a/lib/date_management/ui/widgets/date_detail_bottom_sheet.dart +++ b/lib/date_management/ui/widgets/date_detail_bottom_sheet.dart @@ -1,5 +1,5 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; -import 'package:dhbwstudentapp/common/ui/colors.dart'; +import 'package:dhbwstudentapp/common/ui/app_theme.dart'; import 'package:dhbwstudentapp/common/util/date_utils.dart'; import 'package:dhbwstudentapp/date_management/model/date_entry.dart'; import 'package:flutter/material.dart'; @@ -8,21 +8,20 @@ import 'package:intl/intl.dart'; class DateDetailBottomSheet extends StatelessWidget { final DateEntry dateEntry; - const DateDetailBottomSheet({Key key, this.dateEntry}) : super(key: key); + const DateDetailBottomSheet({super.key, required this.dateEntry}); @override Widget build(BuildContext context) { - var date = DateFormat.yMd(L.of(context).locale.languageCode) + final date = DateFormat.yMd(L.of(context).locale.languageCode) .format(dateEntry.start); - var time = DateFormat.Hm(L.of(context).locale.languageCode) + final time = DateFormat.Hm(L.of(context).locale.languageCode) .format(dateEntry.start); - return Container( + return SizedBox( height: 400, child: Padding( padding: const EdgeInsets.fromLTRB(24, 12, 24, 24), child: Column( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( @@ -31,10 +30,10 @@ class DateDetailBottomSheet extends StatelessWidget { child: Container( height: 8, width: 30, - decoration: BoxDecoration( - color: colorSeparator(), - borderRadius: const BorderRadius.all(Radius.circular(4))), - child: null, + decoration: const BoxDecoration( + color: AppTheme.separator, + borderRadius: BorderRadius.all(Radius.circular(4)), + ), ), ), ), @@ -60,12 +59,13 @@ class DateDetailBottomSheet extends StatelessWidget { softWrap: true, style: Theme.of(context).textTheme.subtitle2, ), - isAtMidnight(dateEntry.start) - ? Container() - : Text( - time, - softWrap: true, - ), + if (isAtMidnight(dateEntry.start)) + Container() + else + Text( + time, + softWrap: true, + ), ], ), ), @@ -74,9 +74,7 @@ class DateDetailBottomSheet extends StatelessWidget { ), Padding( padding: const EdgeInsets.fromLTRB(0, 8, 0, 0), - child: Text( - dateEntry.comment, - ), + child: Text(dateEntry.comment), ), ], ), diff --git a/lib/date_management/ui/widgets/date_filter_options.dart b/lib/date_management/ui/widgets/date_filter_options.dart index de063007..25886289 100644 --- a/lib/date_management/ui/widgets/date_filter_options.dart +++ b/lib/date_management/ui/widgets/date_filter_options.dart @@ -6,18 +6,16 @@ import 'package:flutter/material.dart'; class DateFilterOptions extends StatefulWidget { final DateManagementViewModel viewModel; - const DateFilterOptions({Key key, this.viewModel}) : super(key: key); + const DateFilterOptions({super.key, required this.viewModel}); @override - _DateFilterOptionsState createState() => _DateFilterOptionsState(viewModel); + _DateFilterOptionsState createState() => _DateFilterOptionsState(); } class _DateFilterOptionsState extends State { - final DateManagementViewModel viewModel; - bool _isExpanded = false; - _DateFilterOptionsState(this.viewModel); + _DateFilterOptionsState(); @override Widget build(BuildContext context) { @@ -46,14 +44,11 @@ class _DateFilterOptionsState extends State { Widget _buildCollapsed() { return GestureDetector( - onTap: () { - setState(() { - _isExpanded = true; - }); - }, + onTap: () => setState(() { + _isExpanded = true; + }), child: Row( mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.max, children: [ Expanded( child: _buildCollapsedChips(), @@ -62,9 +57,8 @@ class _DateFilterOptionsState extends State { padding: const EdgeInsets.fromLTRB(0, 0, 16, 0), child: ButtonTheme( minWidth: 36, - height: 36, child: IconButton( - icon: Icon(Icons.tune), + icon: const Icon(Icons.tune), onPressed: () { setState(() { _isExpanded = true; @@ -79,38 +73,48 @@ class _DateFilterOptionsState extends State { } Widget _buildCollapsedChips() { - var chips = []; - - if (viewModel.showPassedDates && viewModel.showFutureDates) { - chips.add(Chip( - label: Text(L.of(context).dateManagementChipFutureAndPast), - visualDensity: VisualDensity.compact, - )); - } else if (viewModel.showFutureDates) { - chips.add(Chip( - label: Text(L.of(context).dateManagementChipOnlyFuture), - visualDensity: VisualDensity.compact, - )); - } else if (viewModel.showPassedDates) { - chips.add(Chip( - label: Text(L.of(context).dateManagementChipOnlyPassed), - visualDensity: VisualDensity.compact, - )); + final chips = []; + + if (widget.viewModel.showPassedDates && widget.viewModel.showFutureDates) { + chips.add( + Chip( + label: Text(L.of(context).dateManagementChipFutureAndPast), + visualDensity: VisualDensity.compact, + ), + ); + } else if (widget.viewModel.showFutureDates) { + chips.add( + Chip( + label: Text(L.of(context).dateManagementChipOnlyFuture), + visualDensity: VisualDensity.compact, + ), + ); + } else if (widget.viewModel.showPassedDates) { + chips.add( + Chip( + label: Text(L.of(context).dateManagementChipOnlyPassed), + visualDensity: VisualDensity.compact, + ), + ); } - if (viewModel.currentSelectedYear != null) { - chips.add(Chip( - label: Text(viewModel.currentSelectedYear), - visualDensity: VisualDensity.compact, - )); + if (widget.viewModel.currentSelectedYear != null) { + chips.add( + Chip( + label: Text(widget.viewModel.currentSelectedYear!), + visualDensity: VisualDensity.compact, + ), + ); } - var database = viewModel.currentDateDatabase?.displayName ?? ""; + final database = widget.viewModel.currentDateDatabase?.displayName ?? ""; if (database != "") { - chips.add(Chip( - label: Text(database), - visualDensity: VisualDensity.compact, - )); + chips.add( + Chip( + label: Text(database), + visualDensity: VisualDensity.compact, + ), + ); } return Padding( @@ -124,18 +128,15 @@ class _DateFilterOptionsState extends State { Widget _buildExpanded() { return Row( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: Column( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( - flex: 1, child: Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), child: Text( @@ -144,13 +145,11 @@ class _DateFilterOptionsState extends State { ), ), Expanded( - flex: 1, - child: DropdownButton( + child: DropdownButton( isExpanded: true, - value: viewModel.currentDateDatabase, - onChanged: (value) { - viewModel.setCurrentDateDatabase(value); - }, + value: widget.viewModel.currentDateDatabase, + onChanged: (v) => + widget.viewModel.currentDateDatabase = v, items: _buildDatabaseMenuItems(), ), ), @@ -159,7 +158,6 @@ class _DateFilterOptionsState extends State { Row( children: [ Expanded( - flex: 1, child: Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), child: Text( @@ -168,13 +166,11 @@ class _DateFilterOptionsState extends State { ), ), Expanded( - flex: 1, - child: DropdownButton( + child: DropdownButton( isExpanded: true, - value: viewModel.currentSelectedYear, - onChanged: (value) { - viewModel.setCurrentSelectedYear(value); - }, + value: widget.viewModel.currentSelectedYear, + onChanged: (v) => + widget.viewModel.currentSelectedYear = v, items: _buildYearsMenuItems(), ), ), @@ -182,19 +178,15 @@ class _DateFilterOptionsState extends State { ), CheckboxListTile( title: Text(L.of(context).dateManagementCheckBoxFutureDates), - value: viewModel.showFutureDates, + value: widget.viewModel.showFutureDates, dense: true, - onChanged: (bool value) { - viewModel.setShowFutureDates(value); - }, + onChanged: (v) => widget.viewModel.showFutureDates = v, ), CheckboxListTile( title: Text(L.of(context).dateManagementCheckBoxPassedDates), - value: viewModel.showPassedDates, + value: widget.viewModel.showPassedDates, dense: true, - onChanged: (bool value) { - viewModel.setShowPassedDates(value); - }, + onChanged: (v) => widget.viewModel.showPassedDates = v, ), ], ), @@ -203,7 +195,6 @@ class _DateFilterOptionsState extends State { padding: const EdgeInsets.fromLTRB(0, 0, 16, 8), child: ButtonTheme( minWidth: 36, - height: 36, child: IconButton( icon: const Icon(Icons.check), onPressed: () { @@ -211,7 +202,7 @@ class _DateFilterOptionsState extends State { _isExpanded = false; }); - viewModel.updateDates(); + widget.viewModel.updateDates(); }, ), ), @@ -221,25 +212,29 @@ class _DateFilterOptionsState extends State { } List> _buildYearsMenuItems() { - var yearMenuItems = >[]; + final yearMenuItems = >[]; - for (var year in viewModel.years) { - yearMenuItems.add(DropdownMenuItem( - child: Text(year), - value: year, - )); + for (final year in widget.viewModel.years) { + yearMenuItems.add( + DropdownMenuItem( + value: year, + child: Text(year), + ), + ); } return yearMenuItems; } List> _buildDatabaseMenuItems() { - var databaseMenuItems = >[]; + final databaseMenuItems = >[]; - for (var database in viewModel.allDateDatabases) { - databaseMenuItems.add(DropdownMenuItem( - child: Text(database.displayName), - value: database, - )); + for (final database in widget.viewModel.allDateDatabases) { + databaseMenuItems.add( + DropdownMenuItem( + value: database, + child: Text(database.displayName), + ), + ); } return databaseMenuItems; } diff --git a/lib/date_management/ui/widgets/date_management_help_dialog.dart b/lib/date_management/ui/widgets/date_management_help_dialog.dart index 55c97285..2b403083 100644 --- a/lib/date_management/ui/widgets/date_management_help_dialog.dart +++ b/lib/date_management/ui/widgets/date_management_help_dialog.dart @@ -3,6 +3,8 @@ import 'package:dhbwstudentapp/common/ui/widgets/help_dialog.dart'; import 'package:flutter/material.dart'; class DateManagementHelpDialog extends HelpDialog { + const DateManagementHelpDialog(); + @override String content(BuildContext context) { return L.of(context).dateManagementHelpDialogContent; diff --git a/lib/dualis/model/credentials.dart b/lib/dualis/model/credentials.dart index 81eb7831..d409e54f 100644 --- a/lib/dualis/model/credentials.dart +++ b/lib/dualis/model/credentials.dart @@ -1,10 +1,42 @@ -class Credentials { - final String password; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +class Credentials extends Equatable { final String username; + final String password; + + const Credentials(this.username, this.password); + + @override + List get props => [username, password]; +} - Credentials(this.username, this.password); +class CredentialsEditingController { + final _usernameController = TextEditingController(); + final _passwordController = TextEditingController(); - bool allFieldsFilled() { - return password != null && username != null; + CredentialsEditingController([Credentials? credentials]) { + if (credentials != null) { + _usernameController.text = credentials.username; + _passwordController.text = credentials.password; + } } + + set credentials(Credentials credentials) { + _usernameController.text = credentials.username; + _passwordController.text = credentials.password; + } + + void dispose() { + _usernameController.dispose(); + _passwordController.dispose(); + } + + TextEditingController get username => _usernameController; + TextEditingController get password => _passwordController; + + Credentials get credentials => Credentials( + _usernameController.text, + _passwordController.text, + ); } diff --git a/lib/dualis/model/exam.dart b/lib/dualis/model/exam.dart index e8947ddb..33d21730 100644 --- a/lib/dualis/model/exam.dart +++ b/lib/dualis/model/exam.dart @@ -7,12 +7,12 @@ enum ExamState { } class Exam { - final String name; + final String? name; final ExamGrade grade; - final String semester; + final String? semester; final ExamState state; - Exam( + const Exam( this.name, this.grade, this.state, diff --git a/lib/dualis/model/exam_grade.dart b/lib/dualis/model/exam_grade.dart index a494edac..90c9518f 100644 --- a/lib/dualis/model/exam_grade.dart +++ b/lib/dualis/model/exam_grade.dart @@ -5,31 +5,32 @@ enum ExamGradeState { Failed, } +// TODO: [leptopoda] implement into the enum class ExamGrade { - ExamGradeState state; - String gradeValue; + final ExamGradeState state; + final String? gradeValue; - ExamGrade.failed() + const ExamGrade.failed() : state = ExamGradeState.Failed, gradeValue = ""; - ExamGrade.notGraded() + const ExamGrade.notGraded() : state = ExamGradeState.NotGraded, gradeValue = ""; - ExamGrade.passed() + const ExamGrade.passed() : state = ExamGradeState.Passed, gradeValue = ""; ExamGrade.graded(this.gradeValue) : state = ExamGradeState.Graded; - static ExamGrade fromString(String grade) { + factory ExamGrade.fromString(String? grade) { if (grade == "noch nicht gesetzt" || grade == "") { - return ExamGrade.notGraded(); + return const ExamGrade.notGraded(); } if (grade == "b") { - return ExamGrade.passed(); + return const ExamGrade.passed(); } // TODO: Determine the value when a exam is in the "failed" state diff --git a/lib/dualis/model/module.dart b/lib/dualis/model/module.dart index d6e4dc56..d26a6a96 100644 --- a/lib/dualis/model/module.dart +++ b/lib/dualis/model/module.dart @@ -2,13 +2,13 @@ import 'package:dhbwstudentapp/dualis/model/exam.dart'; class Module { final List exams; - final String id; - final String name; - final String credits; - final String grade; - final ExamState state; + final String? id; + final String? name; + final String? credits; + final String? grade; + final ExamState? state; - Module( + const Module( this.exams, this.id, this.name, diff --git a/lib/dualis/model/semester.dart b/lib/dualis/model/semester.dart index 84eecc90..d8e73449 100644 --- a/lib/dualis/model/semester.dart +++ b/lib/dualis/model/semester.dart @@ -1,10 +1,10 @@ import 'package:dhbwstudentapp/dualis/model/module.dart'; class Semester { - final String name; + final String? name; final List modules; - Semester( + const Semester( this.name, this.modules, ); diff --git a/lib/dualis/model/study_grades.dart b/lib/dualis/model/study_grades.dart index d57266b2..83caee79 100644 --- a/lib/dualis/model/study_grades.dart +++ b/lib/dualis/model/study_grades.dart @@ -1,10 +1,10 @@ class StudyGrades { - final double gpaTotal; - final double gpaMainModules; - final double creditsTotal; - final double creditsGained; + final double? gpaTotal; + final double? gpaMainModules; + final double? creditsTotal; + final double? creditsGained; - StudyGrades( + const StudyGrades( this.gpaTotal, this.gpaMainModules, this.creditsTotal, diff --git a/lib/dualis/service/cache_dualis_service_decorator.dart b/lib/dualis/service/cache_dualis_service_decorator.dart index 95a2ec9c..a0e4a178 100644 --- a/lib/dualis/service/cache_dualis_service_decorator.dart +++ b/lib/dualis/service/cache_dualis_service_decorator.dart @@ -1,4 +1,5 @@ import 'package:dhbwstudentapp/common/util/cancellation_token.dart'; +import 'package:dhbwstudentapp/dualis/model/credentials.dart'; import 'package:dhbwstudentapp/dualis/model/module.dart'; import 'package:dhbwstudentapp/dualis/model/semester.dart'; import 'package:dhbwstudentapp/dualis/model/study_grades.dart'; @@ -10,31 +11,30 @@ import 'package:dhbwstudentapp/dualis/service/dualis_service.dart'; class CacheDualisServiceDecorator extends DualisService { final DualisService _service; - List _allModulesCached; - List _allSemesterNamesCached; - Map _semestersCached = {}; - StudyGrades _studyGradesCached; + List? _allModulesCached; + List? _allSemesterNamesCached; + Map _semestersCached = {}; + StudyGrades? _studyGradesCached; CacheDualisServiceDecorator(this._service); @override Future login( - String username, - String password, [ - CancellationToken cancellationToken, + Credentials credentials, [ + CancellationToken? cancellationToken, ]) { - return _service.login(username, password, cancellationToken); + return _service.login(credentials, cancellationToken); } @override Future> queryAllModules([ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]) async { if (_allModulesCached != null) { return Future.value(_allModulesCached); } - var allModules = await _service.queryAllModules(cancellationToken); + final allModules = await _service.queryAllModules(cancellationToken); _allModulesCached = allModules; @@ -43,13 +43,13 @@ class CacheDualisServiceDecorator extends DualisService { @override Future querySemester( - String name, [ - CancellationToken cancellationToken, + String? name, [ + CancellationToken? cancellationToken, ]) async { if (_semestersCached.containsKey(name)) { return Future.value(_semestersCached[name]); } - var semester = await _service.querySemester(name, cancellationToken); + final semester = await _service.querySemester(name, cancellationToken); _semestersCached[name] = semester; @@ -58,13 +58,14 @@ class CacheDualisServiceDecorator extends DualisService { @override Future> querySemesterNames([ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]) async { if (_allSemesterNamesCached != null) { return Future.value(_allSemesterNamesCached); } - var allSemesterNames = await _service.querySemesterNames(cancellationToken); + final allSemesterNames = + await _service.querySemesterNames(cancellationToken); _allSemesterNamesCached = allSemesterNames; @@ -73,13 +74,13 @@ class CacheDualisServiceDecorator extends DualisService { @override Future queryStudyGrades([ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]) async { if (_studyGradesCached != null) { return Future.value(_studyGradesCached); } - var studyGrades = await _service.queryStudyGrades(cancellationToken); + final studyGrades = await _service.queryStudyGrades(cancellationToken); _studyGradesCached = studyGrades; @@ -95,7 +96,7 @@ class CacheDualisServiceDecorator extends DualisService { @override Future logout([ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]) async { await _service.logout(cancellationToken); clearCache(); diff --git a/lib/dualis/service/dualis_authentication.dart b/lib/dualis/service/dualis_authentication.dart index 193b0c1b..4291f2d9 100644 --- a/lib/dualis/service/dualis_authentication.dart +++ b/lib/dualis/service/dualis_authentication.dart @@ -1,4 +1,5 @@ import 'package:dhbwstudentapp/common/util/cancellation_token.dart'; +import 'package:dhbwstudentapp/dualis/model/credentials.dart'; import 'package:dhbwstudentapp/dualis/service/dualis_service.dart'; import 'package:dhbwstudentapp/dualis/service/dualis_website_model.dart'; import 'package:dhbwstudentapp/dualis/service/parsing/access_denied_extract.dart'; @@ -14,38 +15,32 @@ import 'package:http/http.dart'; /// with the username and password and then use the [authenticatedGet] method. /// class DualisAuthentication { + DualisAuthentication(); + final RegExp _tokenRegex = RegExp("ARGUMENTS=-N([0-9]{15})"); - String _username; - String _password; + Credentials? _credentials; + + // TODO: [Leptopoda] make singletons :) - DualisUrls _dualisUrls; - DualisUrls get dualisUrls => _dualisUrls; + DualisUrls? _dualisUrls; + DualisUrls get dualisUrls => _dualisUrls ??= DualisUrls(); - String _authToken; - Session _session; + String? _authToken; + Session? _session; + Session get session => _session ??= Session(); - LoginResult _loginState = LoginResult.LoggedOut; - LoginResult get loginState => _loginState; + LoginResult? _loginState; + LoginResult get loginState => _loginState ??= LoginResult.LoggedOut; Future login( - String username, - String password, - CancellationToken cancellationToken, + Credentials credentials, + CancellationToken? cancellationToken, ) async { - username = username ?? this._username; - password = password ?? this._password; - - _dualisUrls = dualisUrls ?? DualisUrls(); + _credentials = credentials; - this._username = username; - this._password = password; - - _session = Session(); - - var loginResponse = await _makeLoginRequest( - username, - password, + final loginResponse = await _makeLoginRequest( + credentials, cancellationToken, ); @@ -58,7 +53,7 @@ class DualisAuthentication { // TODO: Test for login failed page - var redirectUrl = LoginRedirectUrlExtract().getUrlFromHeader( + final redirectUrl = const LoginRedirectUrlExtract().getUrlFromHeader( loginResponse.headers['refresh'], dualisEndpoint, ); @@ -68,12 +63,12 @@ class DualisAuthentication { return loginState; } - var redirectPage = await _session.get( + final redirectPage = await session.get( redirectUrl, cancellationToken, ); - dualisUrls.mainPageUrl = LoginRedirectUrlExtract().readRedirectUrl( + dualisUrls.mainPageUrl = const LoginRedirectUrlExtract().readRedirectUrl( redirectPage, dualisEndpoint, ); @@ -83,14 +78,14 @@ class DualisAuthentication { return loginState; } - _updateAccessToken(dualisUrls.mainPageUrl); + _updateAccessToken(dualisUrls.mainPageUrl!); - var mainPage = await _session.get( - dualisUrls.mainPageUrl, + final mainPage = await session.get( + dualisUrls.mainPageUrl!, cancellationToken, ); - UrlsFromMainPageExtract().parseMainPage( + const UrlsFromMainPageExtract().parseMainPage( mainPage, dualisUrls, dualisEndpoint, @@ -100,16 +95,15 @@ class DualisAuthentication { return loginState; } - Future _makeLoginRequest( - String user, - String password, [ - CancellationToken cancellationToken, + Future _makeLoginRequest( + Credentials credentials, [ + CancellationToken? cancellationToken, ]) async { - var loginUrl = dualisEndpoint + "/scripts/mgrqispi.dll"; + const loginUrl = "$dualisEndpoint/scripts/mgrqispi.dll"; - var data = { - "usrname": user, - "pass": password, + final data = { + "usrname": credentials.username, + "pass": credentials.password, "APPNAME": "CampusNet", "PRGNAME": "LOGINCHECK", "ARGUMENTS": "clino,usrname,pass,menuno,menu_type,browser,platform", @@ -121,7 +115,7 @@ class DualisAuthentication { }; try { - var loginResponse = await _session.rawPost( + final loginResponse = await session.rawPost( loginUrl, data, cancellationToken, @@ -138,26 +132,30 @@ class DualisAuthentication { /// This method handles the authentication cookie and token. If the session /// timed out, it will renew the session by logging in again /// - Future authenticatedGet( + Future authenticatedGet( String url, - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ) async { - var result = await _session.get( + assert(_credentials != null); + + final result = await session.get( _fillUrlWithAuthToken(url), cancellationToken, ); + if (result == null) return null; + cancellationToken?.throwIfCancelled(); - if (!TimeoutExtract().isTimeoutErrorPage(result) && - !AccessDeniedExtract().isAccessDeniedPage(result)) { + if (!const TimeoutExtract().isTimeoutErrorPage(result) && + !const AccessDeniedExtract().isAccessDeniedPage(result)) { return result; } - var loginResult = await login(_username, _password, cancellationToken); + final loginResult = await login(_credentials!, cancellationToken); if (loginResult == LoginResult.LoggedIn) { - return await _session.get( + return session.get( _fillUrlWithAuthToken(url), cancellationToken, ); @@ -167,9 +165,9 @@ class DualisAuthentication { } Future logout([ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]) async { - var logoutRequest = _session.get(dualisUrls.logoutUrl, cancellationToken); + final logoutRequest = session.get(dualisUrls.logoutUrl, cancellationToken); _session = null; _dualisUrls = null; @@ -184,11 +182,11 @@ class DualisAuthentication { /// wrapped in a [fillUrlWithAuthToken()] call /// void _updateAccessToken(String urlWithNewToken) { - var tokenMatch = _tokenRegex.firstMatch(urlWithNewToken); - - if (tokenMatch == null) return; + final tokenMatch = _tokenRegex.firstMatch(urlWithNewToken); - _authToken = tokenMatch.group(1); + if (tokenMatch != null) { + _authToken = tokenMatch.group(1); + } } /// @@ -198,22 +196,26 @@ class DualisAuthentication { /// updated api token /// String _fillUrlWithAuthToken(String url) { - var match = _tokenRegex.firstMatch(url); + final match = _tokenRegex.firstMatch(url); if (match != null) { return url.replaceRange( - match.start, match.end, "ARGUMENTS=-N$_authToken"); + match.start, + match.end, + "ARGUMENTS=-N$_authToken", + ); } return url; } - void setLoginCredentials(String username, String password) { - _username = username; - _password = password; + set loginCredentials(Credentials credentials) { + _credentials = credentials; } Future loginWithPreviousCredentials( - CancellationToken cancellationToken) async { - return await login(_username, _password, cancellationToken); + CancellationToken cancellationToken, + ) async { + assert(_credentials != null); + return login(_credentials!, cancellationToken); } } diff --git a/lib/dualis/service/dualis_scraper.dart b/lib/dualis/service/dualis_scraper.dart index 7c9c65fd..44bc4629 100644 --- a/lib/dualis/service/dualis_scraper.dart +++ b/lib/dualis/service/dualis_scraper.dart @@ -1,12 +1,13 @@ import 'package:dhbwstudentapp/common/util/cancellation_token.dart'; +import 'package:dhbwstudentapp/dualis/model/credentials.dart'; import 'package:dhbwstudentapp/dualis/model/study_grades.dart'; import 'package:dhbwstudentapp/dualis/service/dualis_authentication.dart'; import 'package:dhbwstudentapp/dualis/service/dualis_service.dart'; -import 'package:dhbwstudentapp/dualis/service/parsing/monthly_schedule_extract.dart'; import 'package:dhbwstudentapp/dualis/service/dualis_website_model.dart'; import 'package:dhbwstudentapp/dualis/service/parsing/all_modules_extract.dart'; import 'package:dhbwstudentapp/dualis/service/parsing/exams_from_module_details_extract.dart'; import 'package:dhbwstudentapp/dualis/service/parsing/modules_from_course_result_page_extract.dart'; +import 'package:dhbwstudentapp/dualis/service/parsing/monthly_schedule_extract.dart'; import 'package:dhbwstudentapp/dualis/service/parsing/semesters_from_course_result_page_extract.dart'; import 'package:dhbwstudentapp/dualis/service/parsing/study_grades_from_student_results_page_extract.dart'; import 'package:dhbwstudentapp/schedule/model/schedule.dart'; @@ -15,49 +16,51 @@ import 'package:dhbwstudentapp/schedule/model/schedule.dart'; /// Provides one single class to access the dualis api. /// class DualisScraper { - DualisAuthentication _dualisAuthentication = DualisAuthentication(); + final DualisAuthentication _dualisAuthentication = DualisAuthentication(); DualisUrls get _dualisUrls => _dualisAuthentication.dualisUrls; + DualisScraper(); + Future> loadAllModules([ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]) async { - var allModulesPageResponse = await _dualisAuthentication.authenticatedGet( - _dualisUrls.studentResultsUrl, + final allModulesPageResponse = await _dualisAuthentication.authenticatedGet( + _dualisUrls.studentResultsUrl!, cancellationToken, ); - return AllModulesExtract().extractAllModules(allModulesPageResponse); + return const AllModulesExtract().extractAllModules(allModulesPageResponse); } Future> loadModuleExams( String moduleDetailsUrl, [ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]) async { - var detailsResponse = await _dualisAuthentication.authenticatedGet( + final detailsResponse = await _dualisAuthentication.authenticatedGet( moduleDetailsUrl, cancellationToken, ); - return ExamsFromModuleDetailsExtract().extractExamsFromModuleDetails( + return const ExamsFromModuleDetailsExtract().extractExamsFromModuleDetails( detailsResponse, ); } Future> loadSemesters([ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]) async { - var courseResultsResponse = await _dualisAuthentication.authenticatedGet( - _dualisUrls.courseResultUrl, + final courseResultsResponse = await _dualisAuthentication.authenticatedGet( + _dualisUrls.courseResultUrl!, cancellationToken, ); - var semesters = SemestersFromCourseResultPageExtract() + final semesters = const SemestersFromCourseResultPageExtract() .extractSemestersFromCourseResults( courseResultsResponse, dualisEndpoint, ); - for (var semester in semesters) { + for (final semester in semesters) { _dualisUrls.semesterCourseResultUrls[semester.semesterName] = semester.semesterCourseResultsUrl; } @@ -65,12 +68,12 @@ class DualisScraper { return semesters; } - Future> loadSemesterModules( - String semesterName, [ - CancellationToken cancellationToken, + Future> loadSemesterModules( + String? semesterName, [ + CancellationToken? cancellationToken, ]) async { - var coursePage = await _dualisAuthentication.authenticatedGet( - _dualisUrls.semesterCourseResultUrls[semesterName], + final coursePage = await _dualisAuthentication.authenticatedGet( + _dualisUrls.semesterCourseResultUrls[semesterName!]!, cancellationToken, ); @@ -79,14 +82,14 @@ class DualisScraper { } Future loadStudyGrades( - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ) async { - var studentsResultsPage = await _dualisAuthentication.authenticatedGet( - _dualisUrls.studentResultsUrl, + final studentsResultsPage = await _dualisAuthentication.authenticatedGet( + _dualisUrls.studentResultsUrl!, cancellationToken, ); - return StudyGradesFromStudentResultsPageExtract() + return const StudyGradesFromStudentResultsPageExtract() .extractStudyGradesFromStudentsResultsPage(studentsResultsPage); } @@ -94,13 +97,16 @@ class DualisScraper { DateTime dateInMonth, CancellationToken cancellationToken, ) async { - var requestUrl = + final requestUrl = "${_dualisUrls.monthlyScheduleUrl}01.${dateInMonth.month}.${dateInMonth.year}"; - var result = await _dualisAuthentication.authenticatedGet( - requestUrl, cancellationToken); + final result = await _dualisAuthentication.authenticatedGet( + requestUrl, + cancellationToken, + ); - var schedule = MonthlyScheduleExtract().extractScheduleFromMonthly(result); + final schedule = + const MonthlyScheduleExtract().extractScheduleFromMonthly(result); schedule.urls.add(dualisEndpoint); @@ -108,11 +114,10 @@ class DualisScraper { } Future login( - String username, - String password, - CancellationToken cancellationToken, + Credentials credentials, + CancellationToken? cancellationToken, ) { - return _dualisAuthentication.login(username, password, cancellationToken); + return _dualisAuthentication.login(credentials, cancellationToken); } Future loginWithPreviousCredentials( @@ -122,7 +127,7 @@ class DualisScraper { .loginWithPreviousCredentials(cancellationToken); } - Future logout(CancellationToken cancellationToken) { + Future logout(CancellationToken? cancellationToken) { return _dualisAuthentication.logout(cancellationToken); } @@ -130,7 +135,7 @@ class DualisScraper { return _dualisAuthentication.loginState == LoginResult.LoggedIn; } - void setLoginCredentials(String username, String password) { - _dualisAuthentication.setLoginCredentials(username, password); + set loginCredentials(Credentials credentials) { + _dualisAuthentication.loginCredentials = credentials; } } diff --git a/lib/dualis/service/dualis_service.dart b/lib/dualis/service/dualis_service.dart index ddbd93fd..fdc6a11d 100644 --- a/lib/dualis/service/dualis_service.dart +++ b/lib/dualis/service/dualis_service.dart @@ -1,4 +1,5 @@ import 'package:dhbwstudentapp/common/util/cancellation_token.dart'; +import 'package:dhbwstudentapp/dualis/model/credentials.dart'; import 'package:dhbwstudentapp/dualis/model/exam.dart'; import 'package:dhbwstudentapp/dualis/model/module.dart'; import 'package:dhbwstudentapp/dualis/model/semester.dart'; @@ -6,31 +7,32 @@ import 'package:dhbwstudentapp/dualis/model/study_grades.dart'; import 'package:dhbwstudentapp/dualis/service/dualis_scraper.dart'; abstract class DualisService { + const DualisService(); + Future login( - String username, - String password, [ - CancellationToken cancellationToken, + Credentials credentials, [ + CancellationToken? cancellationToken, ]); Future queryStudyGrades([ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]); Future> querySemesterNames([ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]); Future> queryAllModules([ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]); Future querySemester( - String name, [ - CancellationToken cancellationToken, + String? name, [ + CancellationToken? cancellationToken, ]); Future logout([ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]); } @@ -44,37 +46,35 @@ enum LoginResult { class DualisServiceImpl extends DualisService { final DualisScraper _dualisScraper; - DualisServiceImpl(this._dualisScraper); + const DualisServiceImpl(this._dualisScraper); @override Future login( - String username, - String password, [ - CancellationToken cancellationToken, + Credentials credentials, [ + CancellationToken? cancellationToken, ]) async { - return await _dualisScraper.login( - username, - password, + return _dualisScraper.login( + credentials, cancellationToken, ); } @override Future queryStudyGrades([ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]) async { - return await _dualisScraper.loadStudyGrades(cancellationToken); + return _dualisScraper.loadStudyGrades(cancellationToken); } @override Future> querySemesterNames([ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]) async { - var semesters = await _dualisScraper.loadSemesters(cancellationToken); + final semesters = await _dualisScraper.loadSemesters(cancellationToken); - var names = []; + final names = []; - for (var semester in semesters) { + for (final semester in semesters) { names.add(semester.semesterName); } @@ -83,43 +83,46 @@ class DualisServiceImpl extends DualisService { @override Future> queryAllModules([ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]) async { - var dualisModules = await _dualisScraper.loadAllModules(cancellationToken); - - var modules = []; - for (var module in dualisModules) { - modules.add(Module( - [], - module.id, - module.name, - module.credits, - module.finalGrade, - module.state, - )); + final dualisModules = + await _dualisScraper.loadAllModules(cancellationToken); + + final modules = []; + for (final module in dualisModules) { + modules.add( + Module( + [], + module.id, + module.name, + module.credits, + module.finalGrade, + module.state, + ), + ); } return modules; } @override Future querySemester( - String name, [ - CancellationToken cancellationToken, + String? name, [ + CancellationToken? cancellationToken, ]) async { - var semesterModules = await _dualisScraper.loadSemesterModules( + final semesterModules = await _dualisScraper.loadSemesterModules( name, cancellationToken, ); - var modules = []; + final modules = []; - for (var dualisModule in semesterModules) { - var moduleExams = await _dualisScraper.loadModuleExams( - dualisModule.detailsUrl, + for (final dualisModule in semesterModules) { + final moduleExams = await _dualisScraper.loadModuleExams( + dualisModule!.detailsUrl!, cancellationToken, ); - var module = Module( + final module = Module( moduleExams .map( (exam) => Exam( @@ -145,7 +148,7 @@ class DualisServiceImpl extends DualisService { @override Future logout([ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]) async { await _dualisScraper.logout(cancellationToken); } diff --git a/lib/dualis/service/dualis_website_model.dart b/lib/dualis/service/dualis_website_model.dart index 8e7bdf60..a3d076c1 100644 --- a/lib/dualis/service/dualis_website_model.dart +++ b/lib/dualis/service/dualis_website_model.dart @@ -8,21 +8,23 @@ const String dualisEndpoint = "https://dualis.dhbw.de"; /// access token contained within the urls. /// class DualisUrls { - String courseResultUrl; - String studentResultsUrl; - String logoutUrl; - String mainPageUrl; - String monthlyScheduleUrl; + String? courseResultUrl; + String? studentResultsUrl; + late String logoutUrl; + String? mainPageUrl; + String? monthlyScheduleUrl; - Map semesterCourseResultUrls = {}; + DualisUrls(); + + Map semesterCourseResultUrls = {}; } class DualisSemester { final String semesterName; - final String semesterCourseResultsUrl; + final String? semesterCourseResultsUrl; final List modules; - DualisSemester( + const DualisSemester( this.semesterName, this.semesterCourseResultsUrl, this.modules, @@ -30,14 +32,14 @@ class DualisSemester { } class DualisModule { - final String id; - final String name; - final String finalGrade; - final String credits; - final String detailsUrl; - final ExamState state; + final String? id; + final String? name; + final String? finalGrade; + final String? credits; + final String? detailsUrl; + final ExamState? state; - DualisModule( + const DualisModule( this.id, this.name, this.finalGrade, @@ -48,13 +50,13 @@ class DualisModule { } class DualisExam { - final String tryNr; - final String moduleName; - final String name; + final String? tryNr; + final String? moduleName; + final String? name; final ExamGrade grade; - final String semester; + final String? semester; - DualisExam( + const DualisExam( this.name, this.moduleName, this.grade, diff --git a/lib/dualis/service/fake_account_dualis_scraper_decorator.dart b/lib/dualis/service/fake_account_dualis_scraper_decorator.dart index 54a0e3c0..8b05ed09 100644 --- a/lib/dualis/service/fake_account_dualis_scraper_decorator.dart +++ b/lib/dualis/service/fake_account_dualis_scraper_decorator.dart @@ -1,4 +1,5 @@ import 'package:dhbwstudentapp/common/util/cancellation_token.dart'; +import 'package:dhbwstudentapp/dualis/model/credentials.dart'; import 'package:dhbwstudentapp/dualis/model/study_grades.dart'; import 'package:dhbwstudentapp/dualis/service/dualis_scraper.dart'; import 'package:dhbwstudentapp/dualis/service/dualis_service.dart'; @@ -14,13 +15,12 @@ import 'package:dhbwstudentapp/schedule/model/schedule.dart'; /// area of the app. /// class FakeAccountDualisScraperDecorator implements DualisScraper { - final fakeUsername = "fakeAccount@domain.de"; - final fakePassword = "Passw0rd"; + static const _credentials = Credentials("fakeAccount@domain.de", "Passw0rd"); final DualisScraper _fakeDualisScraper = FakeDataDualisScraper(); final DualisScraper _originalDualisScraper; - DualisScraper _currentDualisScraper; + late DualisScraper _currentDualisScraper; FakeAccountDualisScraperDecorator( this._originalDualisScraper, @@ -32,15 +32,16 @@ class FakeAccountDualisScraperDecorator implements DualisScraper { } @override - Future> loadAllModules( - [CancellationToken cancellationToken]) { + Future> loadAllModules([ + CancellationToken? cancellationToken, + ]) { return _currentDualisScraper.loadAllModules(); } @override Future> loadModuleExams( String moduleDetailsUrl, [ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]) { return _currentDualisScraper.loadModuleExams( moduleDetailsUrl, @@ -60,9 +61,9 @@ class FakeAccountDualisScraperDecorator implements DualisScraper { } @override - Future> loadSemesterModules( - String semesterName, [ - CancellationToken cancellationToken, + Future> loadSemesterModules( + String? semesterName, [ + CancellationToken? cancellationToken, ]) { return _currentDualisScraper.loadSemesterModules( semesterName, @@ -71,60 +72,57 @@ class FakeAccountDualisScraperDecorator implements DualisScraper { } @override - Future> loadSemesters( - [CancellationToken cancellationToken]) { + Future> loadSemesters([ + CancellationToken? cancellationToken, + ]) { return _currentDualisScraper.loadSemesters(cancellationToken); } @override - Future loadStudyGrades(CancellationToken cancellationToken) { + Future loadStudyGrades(CancellationToken? cancellationToken) { return _currentDualisScraper.loadStudyGrades(cancellationToken); } @override Future login( - String username, - String password, - CancellationToken cancellationToken, + Credentials credentials, + CancellationToken? cancellationToken, ) { - if (username == fakeUsername && password == fakePassword) { + if (credentials == _credentials) { _currentDualisScraper = _fakeDualisScraper; } else { _currentDualisScraper = _originalDualisScraper; } return _currentDualisScraper.login( - username, - password, + credentials, cancellationToken, ); } @override Future loginWithPreviousCredentials( - CancellationToken cancellationToken) { + CancellationToken cancellationToken, + ) { return _currentDualisScraper.loginWithPreviousCredentials( cancellationToken, ); } @override - Future logout(CancellationToken cancellationToken) { + Future logout(CancellationToken? cancellationToken) { return _currentDualisScraper.logout( cancellationToken, ); } @override - void setLoginCredentials(String username, String password) { - if (username == fakeUsername && password == fakePassword) { + set loginCredentials(Credentials credentials) { + if (credentials == _credentials) { _currentDualisScraper = _fakeDualisScraper; } else { _currentDualisScraper = _originalDualisScraper; } - return _currentDualisScraper.setLoginCredentials( - username, - password, - ); + _currentDualisScraper.loginCredentials = credentials; } } diff --git a/lib/dualis/service/fake_data_dualis_scraper.dart b/lib/dualis/service/fake_data_dualis_scraper.dart index fe7a90b0..93291d68 100644 --- a/lib/dualis/service/fake_data_dualis_scraper.dart +++ b/lib/dualis/service/fake_data_dualis_scraper.dart @@ -1,4 +1,5 @@ import 'package:dhbwstudentapp/common/util/cancellation_token.dart'; +import 'package:dhbwstudentapp/dualis/model/credentials.dart'; import 'package:dhbwstudentapp/dualis/model/exam.dart'; import 'package:dhbwstudentapp/dualis/model/exam_grade.dart'; import 'package:dhbwstudentapp/dualis/model/study_grades.dart'; @@ -13,18 +14,21 @@ import 'package:dhbwstudentapp/schedule/model/schedule.dart'; class FakeDataDualisScraper implements DualisScraper { bool _isLoggedIn = false; + FakeDataDualisScraper(); + @override bool isLoggedIn() { return _isLoggedIn; } @override - Future> loadAllModules( - [CancellationToken cancellationToken]) async { - await Future.delayed(Duration(milliseconds: 200)); + Future> loadAllModules([ + CancellationToken? cancellationToken, + ]) async { + await Future.delayed(const Duration(milliseconds: 200)); return Future.value([ - DualisModule( + const DualisModule( "Module1", "Informatik", "1.0", @@ -38,9 +42,9 @@ class FakeDataDualisScraper implements DualisScraper { @override Future> loadModuleExams( String moduleDetailsUrl, [ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]) async { - await Future.delayed(Duration(milliseconds: 200)); + await Future.delayed(const Duration(milliseconds: 200)); return Future.value([ DualisExam( "Klausur", @@ -57,18 +61,18 @@ class FakeDataDualisScraper implements DualisScraper { DateTime dateInMonth, CancellationToken cancellationToken, ) async { - await Future.delayed(Duration(milliseconds: 200)); - return Future.value(Schedule.fromList([])); + await Future.delayed(const Duration(milliseconds: 200)); + return Future.value(const Schedule()); } @override Future> loadSemesterModules( - String semesterName, [ - CancellationToken cancellationToken, + String? semesterName, [ + CancellationToken? cancellationToken, ]) async { - await Future.delayed(Duration(milliseconds: 200)); + await Future.delayed(const Duration(milliseconds: 200)); return Future.value([ - DualisModule( + const DualisModule( "Module1", "Informatik", "1.0", @@ -80,18 +84,20 @@ class FakeDataDualisScraper implements DualisScraper { } @override - Future> loadSemesters( - [CancellationToken cancellationToken]) async { - await Future.delayed(Duration(milliseconds: 200)); - return Future.value([DualisSemester("SoSe2020", "", [])]); + Future> loadSemesters([ + CancellationToken? cancellationToken, + ]) async { + await Future.delayed(const Duration(milliseconds: 200)); + return Future.value([const DualisSemester("SoSe2020", "", [])]); } @override Future loadStudyGrades( - CancellationToken cancellationToken) async { - await Future.delayed(Duration(milliseconds: 200)); + CancellationToken? cancellationToken, + ) async { + await Future.delayed(const Duration(milliseconds: 200)); return Future.value( - StudyGrades( + const StudyGrades( 1.5, 1.5, 210, @@ -102,31 +108,31 @@ class FakeDataDualisScraper implements DualisScraper { @override Future login( - String username, - String password, - CancellationToken cancellationToken, + Credentials credentials, + CancellationToken? cancellationToken, ) async { - await Future.delayed(Duration(milliseconds: 200)); + await Future.delayed(const Duration(milliseconds: 200)); _isLoggedIn = true; return Future.value(LoginResult.LoggedIn); } @override Future loginWithPreviousCredentials( - CancellationToken cancellationToken) async { - await Future.delayed(Duration(milliseconds: 200)); + CancellationToken cancellationToken, + ) async { + await Future.delayed(const Duration(milliseconds: 200)); _isLoggedIn = true; return Future.value(LoginResult.LoggedIn); } @override - Future logout(CancellationToken cancellationToken) async { - await Future.delayed(Duration(milliseconds: 200)); + Future logout(CancellationToken? cancellationToken) async { + await Future.delayed(const Duration(milliseconds: 200)); _isLoggedIn = false; } @override - void setLoginCredentials(String username, String password) { + set loginCredentials(Credentials? credentials) { // TODO: implement setLoginCredentials } } diff --git a/lib/dualis/service/parsing/access_denied_extract.dart b/lib/dualis/service/parsing/access_denied_extract.dart index d61f47a8..1c2cd464 100644 --- a/lib/dualis/service/parsing/access_denied_extract.dart +++ b/lib/dualis/service/parsing/access_denied_extract.dart @@ -1,4 +1,6 @@ class AccessDeniedExtract { + const AccessDeniedExtract(); + bool isAccessDeniedPage(String body) { return body.contains("Zugang verweigert"); } diff --git a/lib/dualis/service/parsing/all_modules_extract.dart b/lib/dualis/service/parsing/all_modules_extract.dart index 02e76db6..bb5baa4d 100644 --- a/lib/dualis/service/parsing/all_modules_extract.dart +++ b/lib/dualis/service/parsing/all_modules_extract.dart @@ -5,7 +5,9 @@ import 'package:html/dom.dart'; import 'package:html/parser.dart'; class AllModulesExtract { - List extractAllModules(String body) { + const AllModulesExtract(); + + List extractAllModules(String? body) { try { return _extractAllModules(body); } catch (e, trace) { @@ -14,20 +16,20 @@ class AllModulesExtract { } } - List _extractAllModules(String body) { - var document = parse(body); + List _extractAllModules(String? body) { + final document = parse(body); - var modulesTable = getElementByTagName(document, "tbody"); - var rows = modulesTable.getElementsByTagName("tr"); + final modulesTable = getElementByTagName(document, "tbody"); + final rows = modulesTable.getElementsByTagName("tr"); - var modules = []; + final modules = []; - for (var row in rows) { + for (final row in rows) { // Rows with the subhead class do not contain any modules if (row.classes.contains("subhead")) continue; // When there are not 6 cells the row is not a module - var cells = row.getElementsByClassName("tbdata"); + final cells = row.getElementsByClassName("tbdata"); if (cells.length != 6) continue; modules.add(_extractModuleFromCells(cells)); @@ -38,18 +40,18 @@ class AllModulesExtract { DualisModule _extractModuleFromCells(List cells) { var name = cells[1].innerHtml.trim(); - var nameHyperlink = cells[1].getElementsByTagName("a"); + final nameHyperlink = cells[1].getElementsByTagName("a"); if (nameHyperlink.isNotEmpty) { name = nameHyperlink[0].innerHtml; } - var id = cells[0].innerHtml; - var credits = cells[3].innerHtml; - var grade = cells[4].innerHtml; - var state = cells[5].children[0].attributes["alt"]; + final id = cells[0].innerHtml; + final credits = cells[3].innerHtml; + final grade = cells[4].innerHtml; + final state = cells[5].children[0].attributes["alt"]; - ExamState stateEnum; + ExamState? stateEnum; if (state == "Bestanden") { stateEnum = ExamState.Passed; @@ -58,7 +60,7 @@ class AllModulesExtract { stateEnum = ExamState.Pending; } - var module = DualisModule( + final module = DualisModule( trimAndEscapeString(id), trimAndEscapeString(name), trimAndEscapeString(grade), diff --git a/lib/dualis/service/parsing/exams_from_module_details_extract.dart b/lib/dualis/service/parsing/exams_from_module_details_extract.dart index 769c92be..689d73ab 100644 --- a/lib/dualis/service/parsing/exams_from_module_details_extract.dart +++ b/lib/dualis/service/parsing/exams_from_module_details_extract.dart @@ -4,7 +4,9 @@ import 'package:dhbwstudentapp/dualis/service/parsing/parsing_utils.dart'; import 'package:html/parser.dart'; class ExamsFromModuleDetailsExtract { - List extractExamsFromModuleDetails(String body) { + const ExamsFromModuleDetailsExtract(); + + List extractExamsFromModuleDetails(String? body) { try { return _extractExamsFromModuleDetails(body); } on ParseException catch (e, trace) { @@ -13,27 +15,27 @@ class ExamsFromModuleDetailsExtract { } } - List _extractExamsFromModuleDetails(String body) { - var document = parse(body); + List _extractExamsFromModuleDetails(String? body) { + final document = parse(body); - var tableExams = getElementByTagName(document, "tbody"); - var tableExamsRows = tableExams.getElementsByTagName("tr"); + final tableExams = getElementByTagName(document, "tbody"); + final tableExamsRows = tableExams.getElementsByTagName("tr"); - String currentTry; - String currentModule; + String? currentTry; + String? currentModule; - var exams = []; + final exams = []; - for (var row in tableExamsRows) { + for (final row in tableExamsRows) { // Save the try for all following exams - var level01s = row.getElementsByClassName("level01"); + final level01s = row.getElementsByClassName("level01"); if (level01s.isNotEmpty) { currentTry = level01s[0].innerHtml; continue; } // Save the module for all following exams - var level02s = row.getElementsByClassName("level02"); + final level02s = row.getElementsByClassName("level02"); if (level02s.isNotEmpty) { currentModule = level02s[0].innerHtml; continue; @@ -41,20 +43,22 @@ class ExamsFromModuleDetailsExtract { // All exam rows contain cells with the tbdata class. // If there are none continue with the next row - var tbdata = row.getElementsByClassName("tbdata"); + final tbdata = row.getElementsByClassName("tbdata"); if (tbdata.length < 4) continue; - var semester = tbdata[0].innerHtml; - var name = tbdata[1].innerHtml; - var grade = trimAndEscapeString(tbdata[3].innerHtml); - - exams.add(DualisExam( - trimAndEscapeString(name), - trimAndEscapeString(currentModule), - ExamGrade.fromString(grade), - trimAndEscapeString(currentTry), - trimAndEscapeString(semester), - )); + final semester = tbdata[0].innerHtml; + final name = tbdata[1].innerHtml; + final grade = trimAndEscapeString(tbdata[3].innerHtml); + + exams.add( + DualisExam( + trimAndEscapeString(name), + trimAndEscapeString(currentModule), + ExamGrade.fromString(grade), + trimAndEscapeString(currentTry), + trimAndEscapeString(semester), + ), + ); } return exams; diff --git a/lib/dualis/service/parsing/login_redirect_url_extract.dart b/lib/dualis/service/parsing/login_redirect_url_extract.dart index 847edff5..823f2e8b 100644 --- a/lib/dualis/service/parsing/login_redirect_url_extract.dart +++ b/lib/dualis/service/parsing/login_redirect_url_extract.dart @@ -1,17 +1,19 @@ import 'package:html/parser.dart'; class LoginRedirectUrlExtract { - String readRedirectUrl(String body, String redirectUrl) { - var document = parse(body); + const LoginRedirectUrlExtract(); - var metaTags = document.getElementsByTagName("meta"); + String? readRedirectUrl(String? body, String redirectUrl) { + final document = parse(body); - String redirectContent; + final metaTags = document.getElementsByTagName("meta"); - for (var metaTag in metaTags) { + String? redirectContent; + + for (final metaTag in metaTags) { if (!metaTag.attributes.containsKey("http-equiv")) continue; - var httpEquiv = metaTag.attributes["http-equiv"]; + final httpEquiv = metaTag.attributes["http-equiv"]; if (httpEquiv != "refresh") continue; if (!metaTag.attributes.containsKey("content")) continue; @@ -23,10 +25,10 @@ class LoginRedirectUrlExtract { return getUrlFromHeader(redirectContent, redirectUrl); } - String getUrlFromHeader(String refreshHeader, String endpointUrl) { + String? getUrlFromHeader(String? refreshHeader, String endpointUrl) { if (refreshHeader == null || !refreshHeader.contains("URL=")) return null; - var refreshHeaderUrlIndex = refreshHeader.indexOf("URL=") + "URL=".length; + final refreshHeaderUrlIndex = refreshHeader.indexOf("URL=") + "URL=".length; return endpointUrl + refreshHeader.substring(refreshHeaderUrlIndex); } } diff --git a/lib/dualis/service/parsing/modules_from_course_result_page_extract.dart b/lib/dualis/service/parsing/modules_from_course_result_page_extract.dart index f903a77f..106e70b3 100644 --- a/lib/dualis/service/parsing/modules_from_course_result_page_extract.dart +++ b/lib/dualis/service/parsing/modules_from_course_result_page_extract.dart @@ -7,8 +7,10 @@ import 'package:html/parser.dart'; class ModulesFromCourseResultPageExtract { final RegExp _extractUrlRegex = RegExp('dl_popUp\\("(.+?)"'); - List extractModulesFromCourseResultPage( - String body, + ModulesFromCourseResultPageExtract(); + + List extractModulesFromCourseResultPage( + String? body, String endpointUrl, ) { try { @@ -19,43 +21,45 @@ class ModulesFromCourseResultPageExtract { } } - List _extractModulesFromCourseResultPage( - String body, String endpointUrl) { - var document = parse(body); + List _extractModulesFromCourseResultPage( + String? body, + String endpointUrl, + ) { + final document = parse(body); - var tableBodies = getElementByTagName(document, "tbody"); - var rows = tableBodies.getElementsByTagName("tr"); + final tableBodies = getElementByTagName(document, "tbody"); + final rows = tableBodies.getElementsByTagName("tr"); - var modulesOfSemester = []; + final modulesOfSemester = []; - for (var row in rows) { + for (final row in rows) { // Only rows with tds as child are modules if (row.children[0].localName != "td") continue; - DualisModule module = _extractModuleFromRow(row, endpointUrl); + final DualisModule? module = _extractModuleFromRow(row, endpointUrl); modulesOfSemester.add(module); } return modulesOfSemester; } - DualisModule _extractModuleFromRow( + DualisModule? _extractModuleFromRow( Element row, String endpointUrl, ) { if (row.children.length < 6) return null; - var id = row.children[0].innerHtml; - var name = row.children[1].innerHtml; + final id = row.children[0].innerHtml; + final name = row.children[1].innerHtml; var grade = row.children[2].innerHtml; - var credits = row.children[3].innerHtml; - var status = row.children[4].innerHtml; - var detailsButton = row.children[5]; - var url = _extractDetailsUrlFromButton(detailsButton, endpointUrl); + final credits = row.children[3].innerHtml; + String? status = row.children[4].innerHtml; + final detailsButton = row.children[5]; + final url = _extractDetailsUrlFromButton(detailsButton, endpointUrl); status = trimAndEscapeString(status); - ExamState statusEnum; + ExamState? statusEnum; if (status == "bestanden") { statusEnum = ExamState.Passed; @@ -65,7 +69,7 @@ class ModulesFromCourseResultPageExtract { grade = ""; } - var module = DualisModule( + final module = DualisModule( trimAndEscapeString(id), trimAndEscapeString(name), trimAndEscapeString(grade), @@ -76,11 +80,11 @@ class ModulesFromCourseResultPageExtract { return module; } - String _extractDetailsUrlFromButton( + String? _extractDetailsUrlFromButton( Element detailsButton, String endpointUrl, ) { - var firstMatch = _extractUrlRegex.firstMatch(detailsButton.innerHtml); + final firstMatch = _extractUrlRegex.firstMatch(detailsButton.innerHtml); var url = firstMatch?.group(1); if (url != null) { diff --git a/lib/dualis/service/parsing/monthly_schedule_extract.dart b/lib/dualis/service/parsing/monthly_schedule_extract.dart index d0f8e22a..1d18577f 100644 --- a/lib/dualis/service/parsing/monthly_schedule_extract.dart +++ b/lib/dualis/service/parsing/monthly_schedule_extract.dart @@ -6,7 +6,9 @@ import 'package:html/parser.dart'; import 'package:intl/intl.dart'; class MonthlyScheduleExtract { - Schedule extractScheduleFromMonthly(String body) { + const MonthlyScheduleExtract(); + + Schedule extractScheduleFromMonthly(String? body) { try { return _extractScheduleFromMonthly(body); } catch (e, trace) { @@ -15,50 +17,50 @@ class MonthlyScheduleExtract { } } - Schedule _extractScheduleFromMonthly(String body) { - var document = parse(body); + Schedule _extractScheduleFromMonthly(String? body) { + final document = parse(body); - var appointments = document.getElementsByClassName("apmntLink"); + final appointments = document.getElementsByClassName("apmntLink"); - var allEntries = []; + final allEntries = []; - for (var appointment in appointments) { - var entry = _extractEntry(appointment); + for (final appointment in appointments) { + final entry = _extractEntry(appointment); allEntries.add(entry); } allEntries.sort( - (ScheduleEntry e1, ScheduleEntry e2) => e1?.start?.compareTo(e2?.start), + (ScheduleEntry e1, ScheduleEntry e2) => e1.start.compareTo(e2.start), ); - return Schedule.fromList(allEntries); + return Schedule(entries: allEntries); } ScheduleEntry _extractEntry(Element appointment) { - var date = appointment.parent.parent - .querySelector(".tbsubhead a") - .attributes["title"]; + final date = appointment.parent?.parent + ?.querySelector(".tbsubhead a") + ?.attributes["title"]; - var information = appointment.attributes["title"]; - var informationParts = information.split(" / "); + final information = appointment.attributes["title"]!; + final informationParts = information.split(" / "); - var startAndEnd = informationParts[0].split(" - "); - var start = "$date ${startAndEnd[0]}"; - var end = "$date ${startAndEnd[1]}"; - var room = informationParts[1]; - var title = informationParts[2]; + final startAndEnd = informationParts[0].split(" - "); + final start = "$date ${startAndEnd[0]}"; + final end = "$date ${startAndEnd[1]}"; + final room = informationParts[1]; + final title = informationParts[2]; - var dateFormat = DateFormat("dd.MM.yyyy HH:mm"); - var startDate = dateFormat.parse(start); - var endDate = dateFormat.parse(end); + final dateFormat = DateFormat("dd.MM.yyyy HH:mm"); + final startDate = dateFormat.parse(start); + final endDate = dateFormat.parse(end); - var entry = ScheduleEntry( + final entry = ScheduleEntry( title: title, professor: "", details: "", room: room, - type: ScheduleEntryType.Class, + type: ScheduleEntryType.Lesson, start: startDate, end: endDate, ); diff --git a/lib/dualis/service/parsing/parsing_utils.dart b/lib/dualis/service/parsing/parsing_utils.dart index 4091c44a..434d8e65 100644 --- a/lib/dualis/service/parsing/parsing_utils.dart +++ b/lib/dualis/service/parsing/parsing_utils.dart @@ -2,10 +2,10 @@ import 'package:html/dom.dart'; import 'package:universal_html/html.dart' as html; -String trimAndEscapeString(String htmlString) { +String? trimAndEscapeString(String? htmlString) { if (htmlString == null) return null; - var text = html.Element.span()..appendHtml(htmlString); + final text = html.Element.span()..appendHtml(htmlString); return text.innerText.trim(); } @@ -14,7 +14,7 @@ Element getElementByTagName( String localName, [ int index = 0, ]) { - var list = document.getElementsByTagName(localName); + final list = document.getElementsByTagName(localName); if (index >= list.length) throw ElementNotFoundParseException(localName); @@ -26,7 +26,7 @@ Element getElementByClassName( String className, [ int index = 0, ]) { - var list = document.getElementsByClassName(className); + final list = document.getElementsByClassName(className); if (index >= list.length) throw ElementNotFoundParseException(className); @@ -37,7 +37,7 @@ Element getElementById( Document document, String id, ) { - var element = document.getElementById(id); + final element = document.getElementById(id); if (element == null) throw ElementNotFoundParseException(id); @@ -45,10 +45,10 @@ Element getElementById( } class ParseException implements Exception { - Object innerException; - StackTrace trace; + final Object? innerException; + final StackTrace? trace; - ParseException.withInner(this.innerException, this.trace); + const ParseException.withInner(this.innerException, this.trace); @override String toString() { @@ -58,10 +58,10 @@ class ParseException implements Exception { class ElementNotFoundParseException implements ParseException { @override - Object innerException; + Object? innerException; @override - StackTrace trace; + StackTrace? trace; final String elementDescription; diff --git a/lib/dualis/service/parsing/semesters_from_course_result_page_extract.dart b/lib/dualis/service/parsing/semesters_from_course_result_page_extract.dart index c6ae1853..36be55a6 100644 --- a/lib/dualis/service/parsing/semesters_from_course_result_page_extract.dart +++ b/lib/dualis/service/parsing/semesters_from_course_result_page_extract.dart @@ -4,8 +4,10 @@ import 'package:html/dom.dart'; import 'package:html/parser.dart'; class SemestersFromCourseResultPageExtract { + const SemestersFromCourseResultPageExtract(); + List extractSemestersFromCourseResults( - String body, + String? body, String endpointUrl, ) { try { @@ -17,56 +19,61 @@ class SemestersFromCourseResultPageExtract { } List _extractSemestersFromCourseResults( - String body, String endpointUrl) { - var page = parse(body); + String? body, + String endpointUrl, + ) { + final page = parse(body); - var semesterSelector = page.getElementById("semester"); + final semesterSelector = page.getElementById("semester"); - if (semesterSelector == null) + if (semesterSelector == null) { throw ElementNotFoundParseException("semester selector container"); + } - var url = _extractSemesterDetailUrlPart(semesterSelector, endpointUrl); + final url = _extractSemesterDetailUrlPart(semesterSelector, endpointUrl); - var semesters = []; + final semesters = []; - for (var option in semesterSelector.getElementsByTagName("option")) { - var id = option.attributes["value"]; - var name = option.innerHtml; + for (final option in semesterSelector.getElementsByTagName("option")) { + final id = option.attributes["value"]; + final name = option.innerHtml; - String detailsUrl; + String? detailsUrl; if (url != null) { - detailsUrl = url + id; + detailsUrl = url + id!; } - semesters.add(DualisSemester( - name, - detailsUrl, - [], - )); + semesters.add( + DualisSemester( + name, + detailsUrl, + [], + ), + ); } return semesters; } - String _extractSemesterDetailUrlPart( + String? _extractSemesterDetailUrlPart( Element semesterSelector, String endpointUrl, ) { - var dropDownSemesterSelector = semesterSelector.attributes["onchange"]; + final dropDownSemesterSelector = semesterSelector.attributes["onchange"]!; - var regExp = RegExp("'([A-z0-9]*)'"); + final regExp = RegExp("'([A-z0-9]*)'"); - var matches = regExp.allMatches(dropDownSemesterSelector).toList(); + final matches = regExp.allMatches(dropDownSemesterSelector).toList(); if (matches.length != 4) return null; - var applicationName = matches[0].group(1); - var programName = matches[1].group(1); - var sessionNo = matches[2].group(1); - var menuId = matches[3].group(1); + final applicationName = matches[0].group(1); + final programName = matches[1].group(1); + final sessionNo = matches[2].group(1); + final menuId = matches[3].group(1); - var url = endpointUrl + "/scripts/mgrqispi.dll"; + var url = "$endpointUrl/scripts/mgrqispi.dll"; url += "?APPNAME=$applicationName"; url += "&PRGNAME=$programName"; url += "&ARGUMENTS=-N$sessionNo,-N$menuId,-N"; diff --git a/lib/dualis/service/parsing/study_grades_from_student_results_page_extract.dart b/lib/dualis/service/parsing/study_grades_from_student_results_page_extract.dart index 8b6ff079..632092cd 100644 --- a/lib/dualis/service/parsing/study_grades_from_student_results_page_extract.dart +++ b/lib/dualis/service/parsing/study_grades_from_student_results_page_extract.dart @@ -4,7 +4,9 @@ import 'package:html/dom.dart'; import 'package:html/parser.dart'; class StudyGradesFromStudentResultsPageExtract { - StudyGrades extractStudyGradesFromStudentsResultsPage(String body) { + const StudyGradesFromStudentResultsPageExtract(); + + StudyGrades extractStudyGradesFromStudentsResultsPage(String? body) { try { return _extractStudyGradesFromStudentsResultsPage(body); } catch (e, trace) { @@ -13,14 +15,14 @@ class StudyGradesFromStudentResultsPageExtract { } } - StudyGrades _extractStudyGradesFromStudentsResultsPage(String body) { - var document = parse(body); + StudyGrades _extractStudyGradesFromStudentsResultsPage(String? body) { + final document = parse(body); - var creditsTable = getElementByTagName(document, "tbody", 0); - var gpaTable = getElementByTagName(document, "tbody", 1); + final creditsTable = getElementByTagName(document, "tbody"); + final gpaTable = getElementByTagName(document, "tbody", 1); - var credits = _extractCredits(creditsTable); - var gpa = _extractGpa(gpaTable); + final credits = _extractCredits(creditsTable); + final gpa = _extractGpa(gpaTable); return StudyGrades( gpa.totalGpa, @@ -31,61 +33,74 @@ class StudyGradesFromStudentResultsPageExtract { } _Credits _extractCredits(Element table) { - var rows = table.getElementsByTagName("tr"); + final rows = table.getElementsByTagName("tr"); - if (rows.length < 2) + if (rows.length < 2) { throw ElementNotFoundParseException("credits container"); + } - var neededCreditsRow = rows[rows.length - 1]; + final neededCreditsRow = rows[rows.length - 1]; var neededCredits = neededCreditsRow.children[0].innerHtml; // Only take the number after the colon - neededCredits = trimAndEscapeString(neededCredits.split(":")[1]); + neededCredits = trimAndEscapeString(neededCredits.split(":")[1])!; - var gainedCreditsRow = rows[rows.length - 2]; + final gainedCreditsRow = rows[rows.length - 2]; var gainedCredits = trimAndEscapeString( gainedCreditsRow.children[2].innerHtml, - ); + )!; neededCredits = neededCredits.replaceAll(",", "."); gainedCredits = gainedCredits.replaceAll(",", "."); - var credits = _Credits(); - credits.gainedCredits = double.tryParse(gainedCredits); - credits.totalCredits = double.tryParse(neededCredits); + final credits = _Credits( + double.tryParse(neededCredits), + double.tryParse(gainedCredits), + ); return credits; } _Gpa _extractGpa(Element table) { - var rows = table.getElementsByTagName("tr"); + final rows = table.getElementsByTagName("tr"); if (rows.length < 2) throw ElementNotFoundParseException("gpa container"); - var totalGpaRowCells = rows[0].getElementsByTagName("th"); - var totalGpa = trimAndEscapeString( + final totalGpaRowCells = rows[0].getElementsByTagName("th"); + final totalGpa = trimAndEscapeString( totalGpaRowCells[1].innerHtml, - ); + )!; - var mainCoursesGpaRowCells = rows[1].getElementsByTagName("th"); - var mainModulesGpa = trimAndEscapeString( + final mainCoursesGpaRowCells = rows[1].getElementsByTagName("th"); + final mainModulesGpa = trimAndEscapeString( mainCoursesGpaRowCells[1].innerHtml, - ); + )!; - _Gpa gpa = _Gpa(); - gpa.totalGpa = double.tryParse(totalGpa.replaceAll(",", ".")); - gpa.mainModulesGpa = double.tryParse(mainModulesGpa.replaceAll(",", ".")); + final _Gpa gpa = _Gpa( + double.tryParse(totalGpa.replaceAll(",", ".")), + double.tryParse(mainModulesGpa.replaceAll(",", ".")), + ); return gpa; } } class _Credits { - double totalCredits; - double gainedCredits; + final double? totalCredits; + final double? gainedCredits; + + const _Credits( + this.totalCredits, + this.gainedCredits, + ); } class _Gpa { - double totalGpa; - double mainModulesGpa; + final double? totalGpa; + final double? mainModulesGpa; + + const _Gpa( + this.totalGpa, + this.mainModulesGpa, + ); } diff --git a/lib/dualis/service/parsing/timeout_extract.dart b/lib/dualis/service/parsing/timeout_extract.dart index d63ad874..a565aebe 100644 --- a/lib/dualis/service/parsing/timeout_extract.dart +++ b/lib/dualis/service/parsing/timeout_extract.dart @@ -1,4 +1,6 @@ class TimeoutExtract { + const TimeoutExtract(); + bool isTimeoutErrorPage(String body) { return body.contains("Timeout!") && body.contains("Bitte melden Sie sich erneut"); diff --git a/lib/dualis/service/parsing/urls_from_main_page_extract.dart b/lib/dualis/service/parsing/urls_from_main_page_extract.dart index 1cea8a03..94185cf1 100644 --- a/lib/dualis/service/parsing/urls_from_main_page_extract.dart +++ b/lib/dualis/service/parsing/urls_from_main_page_extract.dart @@ -3,8 +3,10 @@ import 'package:dhbwstudentapp/dualis/service/parsing/parsing_utils.dart'; import 'package:html/parser.dart'; class UrlsFromMainPageExtract { + const UrlsFromMainPageExtract(); + void parseMainPage( - String body, + String? body, DualisUrls dualsUrls, String endpointUrl, ) { @@ -17,26 +19,27 @@ class UrlsFromMainPageExtract { } void _parseMainPage( - String body, + String? body, DualisUrls dualisUrls, String endpointUrl, ) { - var document = parse(body); + final document = parse(body); - var courseResultsElement = getElementByClassName(document, "link000307"); - var studentResultsElement = getElementByClassName(document, "link000310"); - var monthlyScheduleElement = getElementByClassName(document, "link000031"); - var logoutElement = getElementById(document, "logoutButton"); + final courseResultsElement = getElementByClassName(document, "link000307"); + final studentResultsElement = getElementByClassName(document, "link000310"); + final monthlyScheduleElement = + getElementByClassName(document, "link000031"); + final logoutElement = getElementById(document, "logoutButton"); dualisUrls.courseResultUrl = - endpointUrl + courseResultsElement.attributes['href']; + endpointUrl + courseResultsElement.attributes['href']!; dualisUrls.studentResultsUrl = - endpointUrl + studentResultsElement.attributes['href']; + endpointUrl + studentResultsElement.attributes['href']!; dualisUrls.monthlyScheduleUrl = - endpointUrl + monthlyScheduleElement.attributes["href"]; + endpointUrl + monthlyScheduleElement.attributes["href"]!; - dualisUrls.logoutUrl = endpointUrl + logoutElement.attributes['href']; + dualisUrls.logoutUrl = endpointUrl + logoutElement.attributes['href']!; } } diff --git a/lib/dualis/service/session.dart b/lib/dualis/service/session.dart index a1563f8d..e533a493 100644 --- a/lib/dualis/service/session.dart +++ b/lib/dualis/service/session.dart @@ -5,21 +5,26 @@ import 'package:dhbwstudentapp/schedule/service/schedule_source.dart'; import 'package:http/http.dart'; import 'package:http_client_helper/http_client_helper.dart' as http; +// TODO: [Leptopoda] requestCancellationToken and Cancellation token cleanup +// TODO: [Leptopoda] Pass Uri objects and not strings + /// /// Handles cookies and provides a session. Execute your api calls with the /// provided get and set methods. /// class Session { - Map cookies = {}; + final Map _cookies = {}; + + Session(); /// /// Execute a GET request and return the result body as string /// - Future get( + Future get( String url, [ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]) async { - var response = await rawGet(url, cancellationToken); + final response = await rawGet(url, cancellationToken); if (response == null) { return null; @@ -32,39 +37,38 @@ class Session { } } - Future rawGet( + Future rawGet( String url, [ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]) async { - if (cancellationToken == null) cancellationToken = CancellationToken(); - - var requestCancellationToken = http.CancellationToken(); + final requestCancellationToken = http.CancellationToken(); + cancellationToken ??= CancellationToken(); try { - cancellationToken.setCancellationCallback(() { - requestCancellationToken.cancel(); - }); + cancellationToken.cancellationCallback = requestCancellationToken.cancel; - var requestUri = Uri.parse(url); + final requestUri = Uri.parse(url); - var response = await http.HttpClientHelper.get( + final response = await http.HttpClientHelper.get( requestUri, cancelToken: requestCancellationToken, - headers: cookies, + headers: _cookies, ); - if (response == null && !requestCancellationToken.isCanceled) - throw ServiceRequestFailed("Http request failed!"); + if (response == null && !requestCancellationToken.isCanceled) { + throw const ServiceRequestFailed("Http request failed!"); + } - _updateCookie(response); + _updateCookie(response!); return response; + // ignore: avoid_catching_errors } on http.OperationCanceledError catch (_) { throw OperationCancelledException(); } catch (ex) { if (!requestCancellationToken.isCanceled) rethrow; } finally { - cancellationToken.setCancellationCallback(null); + cancellationToken.cancellationCallback = null; } return null; @@ -73,12 +77,12 @@ class Session { /// /// Execute a POST request and return the result body as string /// - Future post( + Future post( String url, - dynamic data, [ - CancellationToken cancellationToken, + Map data, [ + CancellationToken? cancellationToken, ]) async { - var response = await rawPost(url, data, cancellationToken); + final response = await rawPost(url, data, cancellationToken); if (response == null) { return null; @@ -91,53 +95,53 @@ class Session { } } - Future rawPost( + Future rawPost( String url, - dynamic data, [ - CancellationToken cancellationToken, + Map data, [ + CancellationToken? cancellationToken, ]) async { - if (cancellationToken == null) cancellationToken = CancellationToken(); - var requestCancellationToken = http.CancellationToken(); + cancellationToken ??= CancellationToken(); + final requestCancellationToken = http.CancellationToken(); try { - cancellationToken.setCancellationCallback(() { - requestCancellationToken.cancel(); - }); + cancellationToken.cancellationCallback = requestCancellationToken.cancel; - var response = await http.HttpClientHelper.post( + final response = await http.HttpClientHelper.post( Uri.parse(url), body: data, - headers: cookies, + headers: _cookies, cancelToken: requestCancellationToken, ); - if (response == null && !requestCancellationToken.isCanceled) - throw ServiceRequestFailed("Http request failed!"); + if (response == null && !requestCancellationToken.isCanceled) { + throw const ServiceRequestFailed("Http request failed!"); + } - _updateCookie(response); + _updateCookie(response!); return response; + // ignore: avoid_catching_errors } on http.OperationCanceledError catch (_) { throw OperationCancelledException(); } catch (ex) { if (!requestCancellationToken.isCanceled) rethrow; } finally { - cancellationToken.setCancellationCallback(null); + cancellationToken.cancellationCallback = null; } return null; } void _updateCookie(Response response) { - String rawCookie = response.headers['set-cookie']; + final String? rawCookie = response.headers['set-cookie']; if (rawCookie != null) { - int index = rawCookie.indexOf(';'); + final int index = rawCookie.indexOf(';'); var cookie = (index == -1) ? rawCookie : rawCookie.substring(0, index); cookie = cookie.replaceAll(" ", ""); - cookies['cookie'] = cookie; + _cookies['cookie'] = cookie; } } } diff --git a/lib/dualis/ui/dualis_navigation_entry.dart b/lib/dualis/ui/dualis_navigation_entry.dart index 95e2097f..56482fc5 100644 --- a/lib/dualis/ui/dualis_navigation_entry.dart +++ b/lib/dualis/ui/dualis_navigation_entry.dart @@ -1,6 +1,5 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; import 'package:dhbwstudentapp/common/ui/custom_icons_icons.dart'; -import 'package:dhbwstudentapp/common/ui/viewmodels/base_view_model.dart'; import 'package:dhbwstudentapp/dualis/ui/dualis_page.dart'; import 'package:dhbwstudentapp/dualis/ui/viewmodels/study_grades_view_model.dart'; import 'package:dhbwstudentapp/dualis/ui/widgets/dualis_help_dialog.dart'; @@ -9,13 +8,11 @@ import 'package:flutter/material.dart'; import 'package:kiwi/kiwi.dart'; import 'package:property_change_notifier/property_change_notifier.dart'; -class DualisNavigationEntry extends NavigationEntry { - StudyGradesViewModel _viewModel; +class DualisNavigationEntry extends NavigationEntry { + DualisNavigationEntry(); @override - Widget icon(BuildContext context) { - return Icon(Icons.data_usage); - } + Icon icon = const Icon(Icons.data_usage); @override String title(BuildContext context) { @@ -23,45 +20,41 @@ class DualisNavigationEntry extends NavigationEntry { } @override - BaseViewModel initViewModel() { - if (_viewModel == null) { - _viewModel = StudyGradesViewModel( - KiwiContainer().resolve(), - KiwiContainer().resolve(), - ); - } - - return _viewModel; + StudyGradesViewModel initViewModel() { + return StudyGradesViewModel( + KiwiContainer().resolve(), + KiwiContainer().resolve(), + ); } @override Widget build(BuildContext context) { - return DualisPage(); + return const DualisPage(); } @override List appBarActions(BuildContext context) { - initViewModel(); return [ PropertyChangeProvider( - value: _viewModel, + value: model, child: PropertyChangeConsumer( - builder: (BuildContext _, StudyGradesViewModel __, Set ___) => - _viewModel.loginState != LoginState.LoggedIn - ? IconButton( - icon: Icon(Icons.help_outline), - onPressed: () async { - await DualisHelpDialog().show(context); - }, - tooltip: L.of(context).helpButtonTooltip, - ) - : IconButton( - icon: const Icon(CustomIcons.logout), - onPressed: () async { - await _viewModel.logout(); - }, - tooltip: L.of(context).logoutButtonTooltip, - ), + builder: + (BuildContext _, StudyGradesViewModel? __, Set? ___) => + model.loginState != LoginState.LoggedIn + ? IconButton( + icon: const Icon(Icons.help_outline), + onPressed: () async { + await const DualisHelpDialog().show(context); + }, + tooltip: L.of(context).helpButtonTooltip, + ) + : IconButton( + icon: const Icon(CustomIcons.logout), + onPressed: () async { + await model.logout(); + }, + tooltip: L.of(context).logoutButtonTooltip, + ), ), ), ]; diff --git a/lib/dualis/ui/dualis_page.dart b/lib/dualis/ui/dualis_page.dart index 5f866d55..1d37e2e7 100644 --- a/lib/dualis/ui/dualis_page.dart +++ b/lib/dualis/ui/dualis_page.dart @@ -1,5 +1,4 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; -import 'package:dhbwstudentapp/common/ui/viewmodels/base_view_model.dart'; import 'package:dhbwstudentapp/dualis/ui/exam_results_page/exam_results_page.dart'; import 'package:dhbwstudentapp/dualis/ui/login/dualis_login_page.dart'; import 'package:dhbwstudentapp/dualis/ui/study_overview/study_overview_page.dart'; @@ -10,14 +9,17 @@ import 'package:property_change_notifier/property_change_notifier.dart'; import 'package:provider/provider.dart'; class DualisPage extends StatelessWidget { + const DualisPage({super.key}); + @override Widget build(BuildContext context) { - StudyGradesViewModel viewModel = Provider.of(context); + final StudyGradesViewModel viewModel = + Provider.of(context); Widget widget; if (viewModel.loginState != LoginState.LoggedIn) { - widget = DualisLoginPage(); + widget = const DualisLoginPage(); } else { widget = PropertyChangeProvider( value: viewModel, @@ -26,13 +28,13 @@ class DualisPage extends StatelessWidget { pages: [ PageDefinition( text: L.of(context).pageDualisOverview, - icon: Icon(Icons.dashboard), - builder: (BuildContext context) => StudyOverviewPage(), + icon: const Icon(Icons.dashboard), + builder: (BuildContext context) => const StudyOverviewPage(), ), PageDefinition( text: L.of(context).pageDualisExams, - icon: Icon(Icons.book), - builder: (BuildContext context) => ExamResultsPage(), + icon: const Icon(Icons.book), + builder: (BuildContext context) => const ExamResultsPage(), ), ], ), diff --git a/lib/dualis/ui/exam_results_page/exam_results_page.dart b/lib/dualis/ui/exam_results_page/exam_results_page.dart index 315dc141..f5a73130 100644 --- a/lib/dualis/ui/exam_results_page/exam_results_page.dart +++ b/lib/dualis/ui/exam_results_page/exam_results_page.dart @@ -1,19 +1,20 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; import 'package:dhbwstudentapp/common/util/platform_util.dart'; import 'package:dhbwstudentapp/dualis/model/exam_grade.dart'; +import 'package:dhbwstudentapp/dualis/model/module.dart'; import 'package:dhbwstudentapp/dualis/ui/viewmodels/study_grades_view_model.dart'; import 'package:flutter/material.dart'; import 'package:property_change_notifier/property_change_notifier.dart'; class ExamResultsPage extends StatelessWidget { + const ExamResultsPage({super.key}); + @override Widget build(BuildContext context) { - return Container( + return SizedBox( height: double.infinity, child: SingleChildScrollView( - scrollDirection: Axis.vertical, child: Column( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( @@ -26,7 +27,6 @@ class ExamResultsPage extends StatelessWidget { Padding( padding: const EdgeInsets.fromLTRB(24, 8, 24, 0), child: Row( - mainAxisAlignment: MainAxisAlignment.start, children: [ Text(L.of(context).dualisExamResultsSemesterSelect), Padding( @@ -38,17 +38,17 @@ class ExamResultsPage extends StatelessWidget { ], builder: ( BuildContext context, - StudyGradesViewModel model, - Set properties, + StudyGradesViewModel? model, + Set? properties, ) => DropdownButton( - onChanged: (value) => model.loadSemester(value), - value: model.currentSemesterName, - items: (model.allSemesterNames ?? []) - .map( + onChanged: model?.loadSemester, + value: model?.currentSemesterName, + items: model?.allSemesterNames + ?.map( (n) => DropdownMenuItem( - child: Text(n), value: n, + child: Text(n), ), ) .toList(), @@ -62,11 +62,11 @@ class ExamResultsPage extends StatelessWidget { properties: const ["currentSemester"], builder: ( BuildContext context, - StudyGradesViewModel model, - Set properties, + StudyGradesViewModel? model, + Set? properties, ) => - model.currentSemester != null - ? buildModulesColumn(context, model) + model?.currentSemester != null + ? buildModulesColumn(context, model!) : const Padding( padding: EdgeInsets.fromLTRB(0, 16, 0, 0), child: Center(child: CircularProgressIndicator()), @@ -79,72 +79,89 @@ class ExamResultsPage extends StatelessWidget { } Widget buildModulesColumn( - BuildContext context, StudyGradesViewModel viewModel) { + BuildContext context, + StudyGradesViewModel viewModel, + ) { return AnimatedSwitcher( - layoutBuilder: (Widget currentChild, List previousChildren) { + layoutBuilder: (Widget? currentChild, List previousChildren) { List children = previousChildren; - if (currentChild != null) + if (currentChild != null) { children = children.toList()..add(currentChild); + } return Stack( - children: children, alignment: Alignment.topCenter, + children: children, ); }, + duration: const Duration(milliseconds: 200), child: Column( key: ValueKey("semester_${viewModel.currentSemester?.name}"), children: buildModulesDataTables(context, viewModel), ), - duration: const Duration(milliseconds: 200), ); } List buildModulesDataTables( - BuildContext context, StudyGradesViewModel viewModel) { - var dataTables = []; + BuildContext context, + StudyGradesViewModel viewModel, + ) { + final dataTables = []; - var isFirstModule = true; - for (var module in viewModel.currentSemester.modules) { - dataTables.add(DataTable( - horizontalMargin: 24, - columnSpacing: 0, - dataRowHeight: 45, - headingRowHeight: 65, - rows: buildModuleDataRows(context, module), - columns: buildModuleColumns(context, module, - displayGradeHeader: isFirstModule), - )); - isFirstModule = false; + final modules = viewModel.currentSemester?.modules; + if (modules != null) { + var isFirstModule = true; + for (final module in modules) { + dataTables.add( + DataTable( + horizontalMargin: 24, + columnSpacing: 0, + dataRowHeight: 45, + headingRowHeight: 65, + rows: buildModuleDataRows(context, module), + columns: buildModuleColumns( + context, + module, + displayGradeHeader: isFirstModule, + ), + ), + ); + isFirstModule = false; + } } return dataTables; } - List buildModuleDataRows(BuildContext context, var module) { - var dataRows = []; + List buildModuleDataRows(BuildContext context, Module module) { + final dataRows = []; - for (var exam in module.exams) { + for (final exam in module.exams) { dataRows.add( DataRow( cells: [ - DataCell(Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - exam.name ?? "", - style: Theme.of(context).textTheme.caption, - ), - Text( - exam.semester ?? "", - style: Theme.of(context).textTheme.caption, - textScaleFactor: exam.semester == "" ? 0 : 1, - ), - ], - )), + DataCell( + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + exam.name ?? "", + style: Theme.of(context).textTheme.caption, + ), + Text( + exam.semester ?? "", + style: Theme.of(context).textTheme.caption, + textScaleFactor: exam.semester == "" ? 0 : 1, + ), + ], + ), + ), DataCell.empty, - DataCell(Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 16, 0), - child: _examGradeToWidget(context, exam.grade), - )), + DataCell( + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 16, 0), + child: _examGradeToWidget(context, exam.grade), + ), + ), ], ), ); @@ -155,20 +172,21 @@ class ExamResultsPage extends StatelessWidget { Widget _examGradeToWidget(BuildContext context, ExamGrade grade) { switch (grade.state) { case ExamGradeState.NotGraded: - return Text(""); + return const Text(""); case ExamGradeState.Graded: - return Text(grade.gradeValue); + return Text(grade.gradeValue!); case ExamGradeState.Passed: return Text(L.of(context).examPassed); case ExamGradeState.Failed: return Text(L.of(context).examNotPassed); } - - return Text(""); } - List buildModuleColumns(BuildContext context, var module, - {var displayGradeHeader = false}) { + List buildModuleColumns( + BuildContext context, + Module module, { + bool displayGradeHeader = false, + }) { var displayWidth = MediaQuery.of(context).size.width; if (!PlatformUtil.isPortrait(context) && PlatformUtil.isTablet()) { @@ -188,14 +206,13 @@ class ExamResultsPage extends StatelessWidget { child: Padding( padding: const EdgeInsets.fromLTRB(0, 0, 10, 16), child: Text( - (module.name ?? ""), + module.name ?? "", style: Theme.of(context).textTheme.subtitle2, softWrap: true, ), ), ), ), - numeric: false, ), DataColumn( label: ConstrainedBox( @@ -207,7 +224,8 @@ class ExamResultsPage extends StatelessWidget { child: Padding( padding: const EdgeInsets.fromLTRB(0, 0, 10, 16), child: Text( - "${L.of(context).dualisExamResultsCreditsColumnHeader}: ${module.credits}"), + "${L.of(context).dualisExamResultsCreditsColumnHeader}: ${module.credits}", + ), ), ), ), diff --git a/lib/dualis/ui/login/dualis_login_page.dart b/lib/dualis/ui/login/dualis_login_page.dart index 204dd8a2..142cade3 100644 --- a/lib/dualis/ui/login/dualis_login_page.dart +++ b/lib/dualis/ui/login/dualis_login_page.dart @@ -1,18 +1,17 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; -import 'package:dhbwstudentapp/common/ui/viewmodels/base_view_model.dart'; import 'package:dhbwstudentapp/dualis/ui/viewmodels/study_grades_view_model.dart'; import 'package:dhbwstudentapp/dualis/ui/widgets/login_form_widget.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class DualisLoginPage extends StatelessWidget { + const DualisLoginPage({super.key}); + @override Widget build(BuildContext context) { - StudyGradesViewModel viewModel = Provider.of(context); - return buildLoginPage(context, viewModel); - } + final StudyGradesViewModel model = + Provider.of(context); - Widget buildLoginPage(BuildContext context, StudyGradesViewModel model) { return SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, @@ -20,7 +19,7 @@ class DualisLoginPage extends StatelessWidget { Padding( padding: const EdgeInsets.all(32), child: Container( - constraints: BoxConstraints(maxWidth: 500), + constraints: const BoxConstraints(maxWidth: 500), child: LoginForm( loginFailedText: L.of(context).dualisLoginFailed, title: Text( diff --git a/lib/dualis/ui/study_overview/study_overview_page.dart b/lib/dualis/ui/study_overview/study_overview_page.dart index 1cd0f267..e68c14c0 100644 --- a/lib/dualis/ui/study_overview/study_overview_page.dart +++ b/lib/dualis/ui/study_overview/study_overview_page.dart @@ -5,14 +5,14 @@ import 'package:flutter/material.dart'; import 'package:property_change_notifier/property_change_notifier.dart'; class StudyOverviewPage extends StatelessWidget { + const StudyOverviewPage({super.key}); + @override Widget build(BuildContext context) { - return Container( + return SizedBox( height: double.infinity, child: SingleChildScrollView( - scrollDirection: Axis.vertical, child: Column( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ buildGpaCredits(context), @@ -39,12 +39,11 @@ class StudyOverviewPage extends StatelessWidget { properties: const ["studyGrades"], builder: ( BuildContext context, - StudyGradesViewModel model, - Set properties, + StudyGradesViewModel? model, + Set? properties, ) => - model.studyGrades != null + model?.studyGrades != null ? Column( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( @@ -54,7 +53,7 @@ class StudyOverviewPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.baseline, children: [ Text( - model.studyGrades.gpaTotal.toString(), + model!.studyGrades!.gpaTotal.toString(), style: Theme.of(context).textTheme.headline3, ), Padding( @@ -72,7 +71,7 @@ class StudyOverviewPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.baseline, children: [ Text( - model.studyGrades.gpaMainModules.toString(), + model.studyGrades!.gpaMainModules.toString(), style: Theme.of(context).textTheme.headline3, ), Padding( @@ -90,7 +89,7 @@ class StudyOverviewPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.baseline, children: [ Text( - "${model.studyGrades.creditsGained} / ${model.studyGrades.creditsTotal}", + "${model.studyGrades!.creditsGained} / ${model.studyGrades!.creditsTotal}", style: Theme.of(context).textTheme.headline3, ), Padding( @@ -129,11 +128,11 @@ class StudyOverviewPage extends StatelessWidget { properties: const ["allModules"], builder: ( BuildContext context, - StudyGradesViewModel model, - Set properties, + StudyGradesViewModel? model, + Set? properties, ) => - model.allModules != null - ? buildModulesDataTable(context, model) + model?.allModules != null + ? buildModulesDataTable(context, model!) : buildProgressIndicator(), ), ], @@ -152,15 +151,15 @@ class StudyOverviewPage extends StatelessWidget { BuildContext context, StudyGradesViewModel model, ) { - var dataRows = []; + final dataRows = []; - for (var module in model.allModules) { + for (final module in model.allModules!) { dataRows.add( DataRow( cells: [ - DataCell(Text(module.name)), - DataCell(Text(module.credits)), - DataCell(Text(module.grade)), + DataCell(Text(module.name!)), + DataCell(Text(module.credits!)), + DataCell(Text(module.grade!)), DataCell(GradeStateIcon(state: module.state)), ], ), @@ -179,7 +178,6 @@ class StudyOverviewPage extends StatelessWidget { return [ DataColumn( label: Text(L.of(context).dualisOverviewModuleColumnHeader), - numeric: false, ), DataColumn( label: Text(L.of(context).dualisOverviewCreditsColumnHeader), diff --git a/lib/dualis/ui/viewmodels/study_grades_view_model.dart b/lib/dualis/ui/viewmodels/study_grades_view_model.dart index 2b7305b1..e61aa5c0 100644 --- a/lib/dualis/ui/viewmodels/study_grades_view_model.dart +++ b/lib/dualis/ui/viewmodels/study_grades_view_model.dart @@ -25,26 +25,26 @@ class StudyGradesViewModel extends BaseViewModel { LoginState _loginState = LoginState.LoggedOut; LoginState get loginState => _loginState; - StudyGrades _studyGrades; - StudyGrades get studyGrades => _studyGrades; + StudyGrades? _studyGrades; + StudyGrades? get studyGrades => _studyGrades; final CancelableMutex _studyGradesCancellationToken = CancelableMutex(); - List _allModules; - List get allModules => _allModules; + List? _allModules; + List? get allModules => _allModules; final CancelableMutex _allModulesCancellationToken = CancelableMutex(); - List _semesterNames; - List get allSemesterNames => _semesterNames; + List? _semesterNames; + List? get allSemesterNames => _semesterNames; final CancelableMutex _semesterNamesCancellationToken = CancelableMutex(); - Semester _currentSemester; - Semester get currentSemester => _currentSemester; + Semester? _currentSemester; + Semester? get currentSemester => _currentSemester; final CancelableMutex _currentSemesterCancellationToken = CancelableMutex(); - String _currentSemesterName; - String get currentSemesterName => _currentSemesterName; + String? _currentSemesterName; + String? get currentSemesterName => _currentSemesterName; - String _currentLoadingSemesterName; + String? _currentLoadingSemesterName; StudyGradesViewModel(this._preferencesProvider, this._dualisService); @@ -54,10 +54,7 @@ class StudyGradesViewModel extends BaseViewModel { bool success; try { - var result = await _dualisService.login( - credentials.username, - credentials.password, - ); + final result = await _dualisService.login(credentials); success = result == LoginResult.LoggedIn; } on OperationCancelledException catch (_) { @@ -95,8 +92,8 @@ class StudyGradesViewModel extends BaseViewModel { await _preferencesProvider.clearDualisCredentials(); } - Future loadCredentials() async { - return await _preferencesProvider.loadDualisCredentials(); + Future loadCredentials() async { + return _preferencesProvider.loadDualisCredentials(); } Future saveCredentials(Credentials credentials) async { @@ -109,7 +106,7 @@ class StudyGradesViewModel extends BaseViewModel { } Future loadStudyGrades() async { - if (_studyGrades != null) return Future.value(); + if (_studyGrades != null) return; print("Loading study grades..."); @@ -130,7 +127,7 @@ class StudyGradesViewModel extends BaseViewModel { } Future loadAllModules() async { - if (_allModules != null) return Future.value(); + if (_allModules != null) return; print("Loading all modules..."); @@ -151,9 +148,10 @@ class StudyGradesViewModel extends BaseViewModel { notifyListeners("allModules"); } - Future loadSemester(String semesterName) async { - if (_currentSemester != null && _currentSemesterName == semesterName) + Future loadSemester(String? semesterName) async { + if (_currentSemester != null && _currentSemesterName == semesterName) { return Future.value(); + } if (_currentLoadingSemesterName == semesterName) return Future.value(); @@ -186,7 +184,7 @@ class StudyGradesViewModel extends BaseViewModel { } Future loadSemesterNames() async { - if (_semesterNames != null) return Future.value(); + if (_semesterNames != null) return; print("Loading semester names..."); @@ -211,14 +209,15 @@ class StudyGradesViewModel extends BaseViewModel { Future _loadInitialSemester() async { if (_semesterNames == null) return; - if (_semesterNames.isEmpty) return; + if (_semesterNames!.isEmpty) return; - var lastViewedSemester = await _preferencesProvider.getLastViewedSemester(); + final lastViewedSemester = + await _preferencesProvider.getLastViewedSemester(); - if (_semesterNames.contains(lastViewedSemester)) { + if (_semesterNames!.contains(lastViewedSemester)) { loadSemester(lastViewedSemester); } else { - loadSemester(_semesterNames.first); + loadSemester(_semesterNames!.first); } } diff --git a/lib/dualis/ui/widgets/dualis_help_dialog.dart b/lib/dualis/ui/widgets/dualis_help_dialog.dart index 30b39c0e..489d8743 100644 --- a/lib/dualis/ui/widgets/dualis_help_dialog.dart +++ b/lib/dualis/ui/widgets/dualis_help_dialog.dart @@ -3,6 +3,8 @@ import 'package:dhbwstudentapp/common/ui/widgets/help_dialog.dart'; import 'package:flutter/material.dart'; class DualisHelpDialog extends HelpDialog { + const DualisHelpDialog(); + @override String content(BuildContext context) { return L.of(context).dualisHelpDialogContent; diff --git a/lib/dualis/ui/widgets/grade_state_icon.dart b/lib/dualis/ui/widgets/grade_state_icon.dart index 8c3e1236..31729e6d 100644 --- a/lib/dualis/ui/widgets/grade_state_icon.dart +++ b/lib/dualis/ui/widgets/grade_state_icon.dart @@ -2,12 +2,9 @@ import 'package:dhbwstudentapp/dualis/model/exam.dart'; import 'package:flutter/material.dart'; class GradeStateIcon extends StatelessWidget { - final ExamState state; + final ExamState? state; - const GradeStateIcon({ - Key key, - this.state, - }) : super(key: key); + const GradeStateIcon({super.key, required this.state}); @override Widget build(BuildContext context) { @@ -17,9 +14,8 @@ class GradeStateIcon extends StatelessWidget { case ExamState.Failed: return const Icon(Icons.close, color: Colors.red); case ExamState.Pending: + default: return Container(); } - - return Container(); } } diff --git a/lib/dualis/ui/widgets/login_form_widget.dart b/lib/dualis/ui/widgets/login_form_widget.dart index 2e71236e..1ebaca9d 100644 --- a/lib/dualis/ui/widgets/login_form_widget.dart +++ b/lib/dualis/ui/widgets/login_form_widget.dart @@ -3,7 +3,7 @@ import 'package:dhbwstudentapp/dualis/model/credentials.dart'; import 'package:flutter/material.dart'; typedef OnLogin = Future Function(Credentials credentials); -typedef OnLoadCredentials = Future Function(); +typedef OnLoadCredentials = Future Function(); typedef OnSaveCredentials = Future Function(Credentials credentials); typedef OnClearCredentials = Future Function(); typedef GetDoSaveCredentials = Future Function(); @@ -19,77 +19,54 @@ class LoginForm extends StatefulWidget { final String loginFailedText; const LoginForm({ - Key key, - this.onLogin, - this.title, - this.loginFailedText, - this.onLoadCredentials, - this.onSaveCredentials, - this.onClearCredentials, - this.getDoSaveCredentials, - }) : super(key: key); + super.key, + required this.onLogin, + required this.title, + required this.loginFailedText, + required this.onLoadCredentials, + required this.onSaveCredentials, + required this.onClearCredentials, + required this.getDoSaveCredentials, + }); @override - _LoginFormState createState() => _LoginFormState( - onLogin, - title, - loginFailedText, - onLoadCredentials, - onSaveCredentials, - onClearCredentials, - getDoSaveCredentials, - ); + _LoginFormState createState() => _LoginFormState(); } class _LoginFormState extends State { - final OnLogin _onLogin; - final OnLoadCredentials _onLoadCredentials; - final OnSaveCredentials _onSaveCredentials; - final OnClearCredentials _onClearCredentials; - final GetDoSaveCredentials _getDoSaveCredentials; - final Widget _title; - - final String _loginFailedText; - bool _storeCredentials = false; bool _loginFailed = false; bool _isLoading = false; - final TextEditingController _usernameEditingController = - TextEditingController(); - final TextEditingController _passwordEditingController = - TextEditingController(); - - _LoginFormState( - this._onLogin, - this._title, - this._loginFailedText, - this._onLoadCredentials, - this._onSaveCredentials, - this._onClearCredentials, - this._getDoSaveCredentials, - ); + final CredentialsEditingController _controller = + CredentialsEditingController(); + + _LoginFormState(); @override void initState() { super.initState(); - if (_onLoadCredentials != null && _getDoSaveCredentials != null) { - _getDoSaveCredentials().then((value) { - setState(() { - _storeCredentials = value; - }); - - _onLoadCredentials().then((value) { - _usernameEditingController.text = value.username; - _passwordEditingController.text = value.password; + widget.getDoSaveCredentials().then((value) { + setState(() { + _storeCredentials = value; + }); + widget.onLoadCredentials().then((credentials) { + if (credentials != null) { + _controller.credentials = credentials; if (mounted) { setState(() {}); } - }); + } }); - } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); } @override @@ -98,28 +75,26 @@ class _LoginFormState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - _title != null - ? Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 24), - child: _title, - ) - : Container(), + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 24), + child: widget.title, + ), TextField( - controller: _usernameEditingController, + controller: _controller.username, decoration: InputDecoration( enabled: !_isLoading, hintText: L.of(context).loginUsername, - icon: Icon(Icons.alternate_email), + icon: const Icon(Icons.alternate_email), ), ), TextField( - controller: _passwordEditingController, + controller: _controller.password, obscureText: true, decoration: InputDecoration( enabled: !_isLoading, hintText: L.of(context).loginPassword, - icon: Icon(Icons.lock_outline), - errorText: _loginFailed ? _loginFailedText : null, + icon: const Icon(Icons.lock_outline), + errorText: _loginFailed ? widget.loginFailedText : null, ), ), Padding( @@ -130,7 +105,8 @@ class _LoginFormState extends State { title: Text( L.of(context).dualisStoreCredentials, ), - onChanged: (bool value) { + onChanged: (bool? value) { + if (value == null) return; setState(() { _storeCredentials = value; }); @@ -138,7 +114,7 @@ class _LoginFormState extends State { value: _storeCredentials, ), ), - Container( + SizedBox( height: 80, child: Align( alignment: Alignment.topRight, @@ -150,7 +126,7 @@ class _LoginFormState extends State { onPressed: () async { await loginButtonPressed(); }, - icon: Icon(Icons.chevron_right), + icon: const Icon(Icons.chevron_right), label: Text(L.of(context).loginButton.toUpperCase()), ), ), @@ -165,19 +141,16 @@ class _LoginFormState extends State { _isLoading = true; }); - if (!_storeCredentials && _onClearCredentials != null) { - await _onClearCredentials(); + if (!_storeCredentials) { + await widget.onClearCredentials(); } - var credentials = Credentials( - _usernameEditingController.text, - _passwordEditingController.text, - ); + final credentials = _controller.credentials; - bool loginSuccess = await _onLogin(credentials); + final bool loginSuccess = await widget.onLogin(credentials); - if (loginSuccess && _storeCredentials && _onSaveCredentials != null) { - await _onSaveCredentials(credentials); + if (loginSuccess && _storeCredentials) { + await widget.onSaveCredentials(credentials); } setState(() { diff --git a/lib/information/ui/useful_information_navigation_entry.dart b/lib/information/ui/useful_information_navigation_entry.dart index 14271bf5..d308c6bc 100644 --- a/lib/information/ui/useful_information_navigation_entry.dart +++ b/lib/information/ui/useful_information_navigation_entry.dart @@ -1,18 +1,19 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; +import 'package:dhbwstudentapp/common/ui/viewmodels/base_view_model.dart'; import 'package:dhbwstudentapp/information/ui/usefulinformation/useful_information_page.dart'; import 'package:dhbwstudentapp/ui/navigation/navigation_entry.dart'; import 'package:flutter/material.dart'; -class UsefulInformationNavigationEntry extends NavigationEntry { +class UsefulInformationNavigationEntry extends NavigationEntry { + UsefulInformationNavigationEntry(); + @override Widget build(BuildContext context) { - return UsefulInformationPage(); + return const UsefulInformationPage(); } @override - Widget icon(BuildContext context) { - return Icon(Icons.info_outline); - } + Icon icon = const Icon(Icons.info_outline); @override String title(BuildContext context) { @@ -21,4 +22,10 @@ class UsefulInformationNavigationEntry extends NavigationEntry { @override String get route => "usefulInformation"; + + @override + BaseViewModel initViewModel() => BaseViewModel(); + + @override + List appBarActions(BuildContext context) => []; } diff --git a/lib/information/ui/usefulinformation/useful_information_page.dart b/lib/information/ui/usefulinformation/useful_information_page.dart index 903db7af..2fb49074 100644 --- a/lib/information/ui/usefulinformation/useful_information_page.dart +++ b/lib/information/ui/usefulinformation/useful_information_page.dart @@ -3,68 +3,75 @@ import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; class UsefulInformationPage extends StatelessWidget { + const UsefulInformationPage({super.key}); + @override Widget build(BuildContext context) { return Scaffold( body: ListView( children: [ ListTile( - leading: Icon(Icons.language), + leading: const Icon(Icons.language), title: Text(L.of(context).informationPageDHBWHomepage), onTap: () { openLink("https://www.dhbw-stuttgart.de/home/"); }, ), ListTile( - leading: Icon(Icons.data_usage), + leading: const Icon(Icons.data_usage), title: Text(L.of(context).informationPageDualis), onTap: () { openLink("https://dualis.dhbw.de/"); }, ), ListTile( - leading: Icon(Icons.email), + leading: const Icon(Icons.email), title: Text(L.of(context).informationPageRoundcube), onTap: () { - openLink("https://lehre-webmail.dhbw-stuttgart.de/roundcubemail/"); + openLink( + "https://lehre-webmail.dhbw-stuttgart.de/roundcubemail/", + ); }, ), ListTile( - leading: Icon(Icons.room_service), + leading: const Icon(Icons.room_service), title: Text(L.of(context).informationPageMoodle), onTap: () { openLink("http://elearning.dhbw-stuttgart.de/"); }, ), ListTile( - leading: Icon(Icons.location_on), + leading: const Icon(Icons.location_on), title: Text(L.of(context).informationPageLocation), onTap: () { openLink( - "https://www.dhbw-stuttgart.de/themen/hochschule/standorte/"); + "https://www.dhbw-stuttgart.de/themen/hochschule/standorte/", + ); }, ), ListTile( - leading: Icon(Icons.wifi), + leading: const Icon(Icons.wifi), title: Text(L.of(context).informationPageEduroam), onTap: () { openLink( - "https://www.dhbw-stuttgart.de/themen/einrichtungen/itservice-center/informationen-fuer-studierende/wlan-vpn-zugang/"); + "https://www.dhbw-stuttgart.de/themen/einrichtungen/itservice-center/informationen-fuer-studierende/wlan-vpn-zugang/", + ); }, ), ListTile( - leading: Icon(Icons.person_outline), + leading: const Icon(Icons.person_outline), title: Text(L.of(context).informationPageStuV), onTap: () { openLink("https://stuv-stuttgart.de/"); }, ), ListTile( - leading: Icon(Icons.pool), + leading: const Icon(Icons.pool), title: Text(L.of(context).informationPageDHBWSports), onTap: () { openLink( - "https://www.dhbw-stuttgart.de/themen/einrichtungen/hochschulsport/"); + "https://www.dhbw-stuttgart.de/themen/einrichtungen/hochschulsport/", + ); }, ), ], diff --git a/lib/main.dart b/lib/main.dart index 592e5ad7..bad7cf00 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,15 +1,14 @@ import 'dart:io'; -import 'package:dhbwstudentapp/ui/root_page.dart'; -import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:dhbwstudentapp/common/appstart/app_initializer.dart'; import 'package:dhbwstudentapp/common/data/preferences/preferences_provider.dart'; import 'package:dhbwstudentapp/common/ui/viewmodels/root_view_model.dart'; -import 'package:kiwi/kiwi.dart'; +import 'package:dhbwstudentapp/common/util/platform_util.dart'; +import 'package:dhbwstudentapp/ui/root_page.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/material.dart'; - -import 'common/util/platform_util.dart'; +import 'package:kiwi/kiwi.dart'; /// /// Main entry point for the app @@ -26,9 +25,11 @@ void main() async { await PlatformUtil.initializePortraitLandscapeMode(); - runApp(RootPage( - rootViewModel: await loadRootViewModel(), - )); + runApp( + RootPage( + rootViewModel: await loadRootViewModel(), + ), + ); } /// @@ -38,12 +39,12 @@ void main() async { /// used language /// Future saveLastStartLanguage() async { - PreferencesProvider preferencesProvider = KiwiContainer().resolve(); + final PreferencesProvider preferencesProvider = KiwiContainer().resolve(); await preferencesProvider.setLastUsedLanguageCode(Platform.localeName); } Future loadRootViewModel() async { - var rootViewModel = RootViewModel( + final rootViewModel = RootViewModel( KiwiContainer().resolve(), ); diff --git a/lib/native/widget/android_widget_helper.dart b/lib/native/widget/android_widget_helper.dart index 464d69de..f58d734d 100644 --- a/lib/native/widget/android_widget_helper.dart +++ b/lib/native/widget/android_widget_helper.dart @@ -5,8 +5,8 @@ import 'package:flutter/services.dart'; /// WidgetHelper which calls native code to control the widget on android /// class AndroidWidgetHelper implements WidgetHelper { - static const platform = - const MethodChannel('de.bennik2000.dhbwstudentapp/widget'); + static const platform = MethodChannel('de.bennik2000.dhbwstudentapp/widget'); + const AndroidWidgetHelper(); @override Future disableWidget() async { @@ -26,11 +26,11 @@ class AndroidWidgetHelper implements WidgetHelper { Future requestWidgetRefresh() async { try { await platform.invokeMethod('requestWidgetRefresh'); - } on PlatformException {} + } on PlatformException catch (_) {} } @override - Future areWidgetsSupported() async { + Future areWidgetsSupported() async { try { return await platform.invokeMethod('areWidgetsSupported'); } on Exception catch (_) { diff --git a/lib/native/widget/ios_widget_helper.dart b/lib/native/widget/ios_widget_helper.dart index 7e4a2b2e..c495d12c 100644 --- a/lib/native/widget/ios_widget_helper.dart +++ b/lib/native/widget/ios_widget_helper.dart @@ -6,8 +6,9 @@ import 'package:flutter_widgetkit/flutter_widgetkit.dart'; /// WidgetHelper which calls native code to control the widget on iOS /// class IOSWidgetHelper implements WidgetHelper { - static const platform = - const MethodChannel('de.bennik2000.dhbwstudentapp/widget'); + static const platform = MethodChannel('de.bennik2000.dhbwstudentapp/widget'); + + const IOSWidgetHelper(); @override Future disableWidget() async { @@ -32,7 +33,7 @@ class IOSWidgetHelper implements WidgetHelper { } @override - Future areWidgetsSupported() async { + Future areWidgetsSupported() async { try { return await platform.invokeMethod('areWidgetsSupported'); } on PlatformException catch (_) { diff --git a/lib/native/widget/widget_helper.dart b/lib/native/widget/widget_helper.dart index 8b8e98ad..ab972564 100644 --- a/lib/native/widget/widget_helper.dart +++ b/lib/native/widget/widget_helper.dart @@ -8,17 +8,15 @@ import 'package:dhbwstudentapp/native/widget/ios_widget_helper.dart'; /// methods to enable/disable or update the widget /// class WidgetHelper { - static WidgetHelper _instance; + static late WidgetHelper _instance; WidgetHelper() { - if (_instance != null) return; - if (Platform.isAndroid) { - _instance = AndroidWidgetHelper(); + _instance = const AndroidWidgetHelper(); } else if (Platform.isIOS) { - _instance = IOSWidgetHelper(); + _instance = const IOSWidgetHelper(); } else { - _instance = VoidWidgetHelper(); + _instance = const VoidWidgetHelper(); } } @@ -33,7 +31,7 @@ class WidgetHelper { /// /// Checks if widgets are supported by the device /// - Future areWidgetsSupported() { + Future areWidgetsSupported() { return _instance.areWidgetsSupported(); } @@ -58,6 +56,8 @@ class WidgetHelper { /// Implementation of the WidgetHelper which does nothing /// class VoidWidgetHelper implements WidgetHelper { + const VoidWidgetHelper(); + @override Future disableWidget() { return Future.value(); diff --git a/lib/native/widget/widget_update_callback.dart b/lib/native/widget/widget_update_callback.dart index 17324ae7..7fb14f42 100644 --- a/lib/native/widget/widget_update_callback.dart +++ b/lib/native/widget/widget_update_callback.dart @@ -9,7 +9,7 @@ import 'package:dhbwstudentapp/schedule/model/schedule.dart'; class WidgetUpdateCallback { final WidgetHelper _widgetHelper; - WidgetUpdateCallback(this._widgetHelper); + const WidgetUpdateCallback(this._widgetHelper); void registerCallback(ScheduleProvider provider) { provider.addScheduleUpdatedCallback(_callback); diff --git a/lib/schedule/background/background_schedule_update.dart b/lib/schedule/background/background_schedule_update.dart index 7c63d1a6..e58ce7e9 100644 --- a/lib/schedule/background/background_schedule_update.dart +++ b/lib/schedule/background/background_schedule_update.dart @@ -10,7 +10,7 @@ class BackgroundScheduleUpdate extends TaskCallback { final ScheduleSourceProvider scheduleSource; final WorkSchedulerService scheduler; - BackgroundScheduleUpdate( + const BackgroundScheduleUpdate( this.scheduleProvider, this.scheduleSource, this.scheduler, @@ -22,10 +22,10 @@ class BackgroundScheduleUpdate extends TaskCallback { return; } - var today = toDayOfWeek(toStartOfDay(DateTime.now()), DateTime.monday); - var end = addDays(today, 7 * 3); + final today = toDayOfWeek(toStartOfDay(DateTime.now()), DateTime.monday)!; + final end = addDays(today, 7 * 3)!; - var cancellationToken = CancellationToken(); + final cancellationToken = CancellationToken(); await scheduleProvider.getUpdatedSchedule( today, diff --git a/lib/schedule/background/calendar_synchronizer.dart b/lib/schedule/background/calendar_synchronizer.dart index 158f89d2..1bc0477a 100644 --- a/lib/schedule/background/calendar_synchronizer.dart +++ b/lib/schedule/background/calendar_synchronizer.dart @@ -12,30 +12,33 @@ class CalendarSynchronizer { final ScheduleSourceProvider scheduleSourceProvider; final PreferencesProvider preferencesProvider; - CalendarSynchronizer(this.scheduleProvider, this.scheduleSourceProvider, - this.preferencesProvider); + const CalendarSynchronizer( + this.scheduleProvider, + this.scheduleSourceProvider, + this.preferencesProvider, + ); void registerSynchronizationCallback() { scheduleProvider.addScheduleUpdatedCallback((schedule, start, end) async { - List listDateEntries = List.empty(growable: true); - schedule.entries.forEach( - (element) { - DateEntry date = DateEntry( - room: element.room, - comment: element.details, - databaseName: 'DHBW', - description: element.title, - year: element.start.year.toString(), - start: element.start, - end: element.end); - listDateEntries.add(date); - }, - ); + final List listDateEntries = + List.empty(growable: true); + for (final element in schedule.entries) { + final DateEntry date = DateEntry( + room: element.room, + comment: element.details, + databaseName: 'DHBW', + description: element.title, + year: element.start.year.toString(), + start: element.start, + end: element.end, + ); + listDateEntries.add(date); + } KiwiContainer().resolve().listDateEntries = listDateEntries; if (await preferencesProvider.isCalendarSyncEnabled()) { - Calendar selectedCalendar = + final Calendar? selectedCalendar = await preferencesProvider.getSelectedCalendar(); if (selectedCalendar == null) return; CalendarAccess().addOrUpdateDates(listDateEntries, selectedCalendar); @@ -44,12 +47,11 @@ class CalendarSynchronizer { } void scheduleSyncInAFewSeconds() { - Future.delayed(Duration(seconds: 10), () { + Future.delayed(const Duration(seconds: 10), () { if (!scheduleSourceProvider.didSetupCorrectly()) return; scheduleProvider.getUpdatedSchedule( - DateTime.now(), - DateTime.now().add(Duration(days: 30)), + DateTime.now().add(const Duration(days: 30)), CancellationToken(), ); }); diff --git a/lib/schedule/business/schedule_diff_calculator.dart b/lib/schedule/business/schedule_diff_calculator.dart index 077bdc17..c8e1750d 100644 --- a/lib/schedule/business/schedule_diff_calculator.dart +++ b/lib/schedule/business/schedule_diff_calculator.dart @@ -2,31 +2,35 @@ import 'package:dhbwstudentapp/schedule/model/schedule.dart'; import 'package:dhbwstudentapp/schedule/model/schedule_entry.dart'; class ScheduleDiffCalculator { + const ScheduleDiffCalculator(); + ScheduleDiff calculateDiff( Schedule oldSchedule, Schedule newSchedule, ) { - var oldEntries = List.from(oldSchedule.entries); - var newEntries = List.from(newSchedule.entries); + final oldEntries = List.from(oldSchedule.entries); + final newEntries = List.from(newSchedule.entries); - var removedEntries = []; - var addedEntries = []; + final removedEntries = []; + final addedEntries = []; - for (var entry in oldEntries) { - if (!newEntries.any((ScheduleEntry element) => - _areScheduleEntriesEqual(element, entry))) { + for (final entry in oldEntries) { + if (!newEntries.any( + (ScheduleEntry element) => _areScheduleEntriesEqual(element, entry), + )) { removedEntries.add(entry); } } - for (var entry in newEntries) { - if (!oldEntries.any((ScheduleEntry element) => - _areScheduleEntriesEqual(element, entry))) { + for (final entry in newEntries) { + if (!oldEntries.any( + (ScheduleEntry element) => _areScheduleEntriesEqual(element, entry), + )) { addedEntries.add(entry); } } - var scheduleDiff = + final scheduleDiff = _tryConnectNewAndOldEntries(addedEntries, removedEntries); return scheduleDiff; @@ -39,32 +43,32 @@ class ScheduleDiffCalculator { return entry1.start.isAtSameMomentAs(entry2.start) && entry1.end.isAtSameMomentAs(entry2.end) && entry1.type == entry2.type && - (entry1.room ?? "") == (entry2.room ?? "") && - (entry1.details ?? "") == (entry2.details ?? "") && - (entry1.title ?? "") == (entry2.title ?? "") && - (entry1.professor ?? "") == (entry2.professor ?? ""); + entry1.room == entry2.room && + entry1.details == entry2.details && + entry1.title == entry2.title && + entry1.professor == entry2.professor; } ScheduleDiff _tryConnectNewAndOldEntries( List addedEntries, List removedEntries, ) { - var allDistinctTitles = []; + var allDistinctTitles = []; allDistinctTitles.addAll(addedEntries.map((ScheduleEntry e) => e.title)); allDistinctTitles.addAll(removedEntries.map((ScheduleEntry e) => e.title)); allDistinctTitles = allDistinctTitles.toSet().toList(); - var updatedEntries = []; - var newEntries = []; - var oldEntries = []; + final updatedEntries = []; + final newEntries = []; + final oldEntries = []; - for (var intersectingTitle in allDistinctTitles) { - var oldElementsWithName = removedEntries + for (final intersectingTitle in allDistinctTitles) { + final oldElementsWithName = removedEntries .where((ScheduleEntry e) => e.title == intersectingTitle) .toList(); - var newElementsWithName = addedEntries + final newElementsWithName = addedEntries .where((ScheduleEntry e) => e.title == intersectingTitle) .toList(); @@ -86,10 +90,13 @@ class ScheduleDiffCalculator { } if (oldElementsWithName.length == 1 && newElementsWithName.length == 1) { - updatedEntries.add(UpdatedEntry( - newElementsWithName[0], - newElementsWithName[0].getDifferentProperties(oldElementsWithName[0]), - )); + updatedEntries.add( + UpdatedEntry( + newElementsWithName[0], + newElementsWithName[0] + .getDifferentProperties(oldElementsWithName[0]), + ), + ); continue; } @@ -113,14 +120,15 @@ class ScheduleDiffCalculator { } void _matchMNChangedElements( - List oldElementsWithName, - List newElementsWithName, - List updatedEntries, - List oldEntries, - List newEntries) { + List oldElementsWithName, + List newElementsWithName, + List updatedEntries, + List oldEntries, + List newEntries, + ) { if (oldElementsWithName.length == newElementsWithName.length) { - for (var oldElement in oldElementsWithName) { - ScheduleEntry nearestElement = + for (final oldElement in oldElementsWithName) { + final ScheduleEntry nearestElement = _findNearestElementByStart(newElementsWithName, oldElement); newElementsWithName.remove(nearestElement); @@ -133,24 +141,26 @@ class ScheduleDiffCalculator { ); } } else { - for (var oldElement in oldElementsWithName) { + for (final oldElement in oldElementsWithName) { oldEntries.add(oldElement); } - for (var newElement in newElementsWithName) { + for (final newElement in newElementsWithName) { newEntries.add(newElement); } } } ScheduleEntry _findNearestElementByStart( - List elements, ScheduleEntry reference) { + List elements, + ScheduleEntry reference, + ) { ScheduleEntry nearestElement = elements[0]; Duration minimalDifference = reference.start.difference(nearestElement.start).abs(); - for (var newElement in elements) { - var difference = reference.start.difference(newElement.start).abs(); + for (final newElement in elements) { + final difference = reference.start.difference(newElement.start).abs(); if (difference < minimalDifference) { nearestElement = newElement; @@ -166,7 +176,11 @@ class ScheduleDiff { final List removedEntries; final List updatedEntries; - ScheduleDiff({this.addedEntries, this.removedEntries, this.updatedEntries}); + const ScheduleDiff({ + required this.addedEntries, + required this.removedEntries, + required this.updatedEntries, + }); bool didSomethingChange() { return addedEntries.isNotEmpty || @@ -179,5 +193,5 @@ class UpdatedEntry { final ScheduleEntry entry; final List properties; - UpdatedEntry(this.entry, this.properties); + const UpdatedEntry(this.entry, this.properties); } diff --git a/lib/schedule/business/schedule_filter.dart b/lib/schedule/business/schedule_filter.dart index 4520e82e..3cd31f32 100644 --- a/lib/schedule/business/schedule_filter.dart +++ b/lib/schedule/business/schedule_filter.dart @@ -5,14 +5,15 @@ import 'package:dhbwstudentapp/schedule/model/schedule_entry.dart'; class ScheduleFilter { final ScheduleFilterRepository _scheduleFilterRepository; - ScheduleFilter(this._scheduleFilterRepository); + const ScheduleFilter(this._scheduleFilterRepository); Future filter(Schedule original) async { - var allHiddenNames = await _scheduleFilterRepository.queryAllHiddenNames(); + final allHiddenNames = + await _scheduleFilterRepository.queryAllHiddenNames(); - var allEntries = []; + final allEntries = []; - for (var entry in original.entries) { + for (final entry in original.entries) { if (allHiddenNames.contains(entry.title)) { continue; } diff --git a/lib/schedule/business/schedule_provider.dart b/lib/schedule/business/schedule_provider.dart index 69d7b426..32345a35 100644 --- a/lib/schedule/business/schedule_provider.dart +++ b/lib/schedule/business/schedule_provider.dart @@ -28,52 +28,53 @@ class ScheduleProvider { final PreferencesProvider _preferencesProvider; final ScheduleSourceProvider _scheduleSource; final ScheduleEntryRepository _scheduleEntryRepository; - final ScheduleFilterRepository _scheduleFilterRepository; final ScheduleQueryInformationRepository _scheduleQueryInformationRepository; final List _scheduleUpdatedCallbacks = []; - ScheduleFilter _scheduleFilter; + final ScheduleFilter _scheduleFilter; final List _scheduleEntryChangedCallbacks = []; ScheduleProvider( - this._scheduleSource, - this._scheduleEntryRepository, - this._scheduleQueryInformationRepository, - this._preferencesProvider, - this._scheduleFilterRepository) { - _scheduleFilter = ScheduleFilter(_scheduleFilterRepository); - } + this._scheduleSource, + this._scheduleEntryRepository, + this._scheduleQueryInformationRepository, + this._preferencesProvider, + ScheduleFilterRepository scheduleFilterRepository, + ) : _scheduleFilter = ScheduleFilter(scheduleFilterRepository); Future getCachedSchedule(DateTime start, DateTime end) async { var cachedSchedule = await _scheduleEntryRepository.queryScheduleBetweenDates(start, end); print( - "Read chached schedule with ${cachedSchedule.entries.length.toString()} entries"); + "Read chached schedule with ${cachedSchedule.entries.length.toString()} entries", + ); cachedSchedule = await _scheduleFilter.filter(cachedSchedule); print( - "Filtered cached schedule has ${cachedSchedule.entries.length} entries"); + "Filtered cached schedule has ${cachedSchedule.entries.length} entries", + ); return cachedSchedule; } - Future getUpdatedSchedule( + Future getUpdatedSchedule( DateTime start, DateTime end, CancellationToken cancellationToken, ) async { print( - "Fetching schedule for ${DateFormat.yMd().format(start)} - ${DateFormat.yMd().format(end)}"); + "Fetching schedule for ${DateFormat.yMd().format(start)} - ${DateFormat.yMd().format(end)}", + ); try { - var updatedSchedule = await _scheduleSource.currentScheduleSource + final updatedSchedule = await _scheduleSource.currentScheduleSource .querySchedule(start, end, cancellationToken); - var schedule = updatedSchedule.schedule; + var schedule = updatedSchedule?.schedule; if (schedule == null) { print("No schedule returned!"); @@ -96,18 +97,16 @@ class ScheduleProvider { print("Filtered schedule has ${schedule.entries.length} entries"); } - for (var c in _scheduleUpdatedCallbacks) { - await c(schedule, start, end); + for (final c in _scheduleUpdatedCallbacks) { + await c(schedule!, start, end); } - updatedSchedule = ScheduleQueryResult(schedule, updatedSchedule.errors); - - return updatedSchedule; + return ScheduleQueryResult(schedule!, updatedSchedule!.errors); } on ScheduleQueryFailedException catch (e, trace) { print("Failed to fetch schedule!"); print(e.innerException.toString()); print(trace); - rethrow; + return null; } } @@ -116,16 +115,17 @@ class ScheduleProvider { DateTime end, Schedule updatedSchedule, ) async { - var oldSchedule = + final oldSchedule = await _scheduleEntryRepository.queryScheduleBetweenDates(start, end); - var diff = - ScheduleDiffCalculator().calculateDiff(oldSchedule, updatedSchedule); + final diff = const ScheduleDiffCalculator() + .calculateDiff(oldSchedule, updatedSchedule); - var cleanedDiff = await _cleanDiffFromNewlyQueriedEntries(start, end, diff); + final cleanedDiff = + await _cleanDiffFromNewlyQueriedEntries(start, end, diff); if (cleanedDiff.didSomethingChange()) { - for (var c in _scheduleEntryChangedCallbacks) { + for (final c in _scheduleEntryChangedCallbacks) { await c(cleanedDiff); } } @@ -136,8 +136,9 @@ class ScheduleProvider { } void removeScheduleUpdatedCallback(ScheduleUpdatedCallback callback) { - if (_scheduleUpdatedCallbacks.contains(callback)) + if (_scheduleUpdatedCallbacks.contains(callback)) { _scheduleUpdatedCallbacks.remove(callback); + } } void addScheduleEntryChangedCallback(ScheduleEntryChangedCallback callback) { @@ -145,9 +146,11 @@ class ScheduleProvider { } void removeScheduleEntryChangedCallback( - ScheduleEntryChangedCallback callback) { - if (_scheduleUpdatedCallbacks.contains(callback)) + ScheduleEntryChangedCallback callback, + ) { + if (_scheduleUpdatedCallbacks.contains(callback)) { _scheduleEntryChangedCallbacks.remove(callback); + } } Future _cleanDiffFromNewlyQueriedEntries( @@ -155,15 +158,17 @@ class ScheduleProvider { DateTime end, ScheduleDiff diff, ) async { - var queryInformation = await _scheduleQueryInformationRepository + final queryInformation = await _scheduleQueryInformationRepository .getQueryInformationBetweenDates(start, end); - var cleanedAddedEntries = []; + final cleanedAddedEntries = []; - for (var addedEntry in diff.addedEntries) { - if (queryInformation.any((i) => - addedEntry.end.isAfter(i.start) && - addedEntry.start.isBefore(i.end))) { + for (final addedEntry in diff.addedEntries) { + if (queryInformation.any( + (i) => + addedEntry.end.isAfter(i!.start) && + addedEntry.start.isBefore(i.end), + )) { cleanedAddedEntries.add(addedEntry); } } diff --git a/lib/schedule/business/schedule_source_provider.dart b/lib/schedule/business/schedule_source_provider.dart index 06e244b9..fefc72ed 100644 --- a/lib/schedule/business/schedule_source_provider.dart +++ b/lib/schedule/business/schedule_source_provider.dart @@ -29,11 +29,12 @@ class ScheduleSourceProvider { final ScheduleEntryRepository _scheduleEntryRepository; final ScheduleQueryInformationRepository _scheduleQueryInformationRepository; - ScheduleSource _currentScheduleSource = InvalidScheduleSource(); + ScheduleSource _currentScheduleSource = const InvalidScheduleSource(); ScheduleSource get currentScheduleSource => _currentScheduleSource; - List _onDidChangeScheduleSourceCallbacks = []; + final List _onDidChangeScheduleSourceCallbacks = + []; ScheduleSourceProvider( this._preferencesProvider, @@ -43,63 +44,57 @@ class ScheduleSourceProvider { ); Future setupScheduleSource() async { - var scheduleSourceType = await _getScheduleSourceType(); + final scheduleSourceType = await _getScheduleSourceType(); - ScheduleSource scheduleSource = InvalidScheduleSource(); + ScheduleSource scheduleSource = const InvalidScheduleSource(); final initializer = { - ScheduleSourceType.Dualis: () async => await _dualisScheduleSource(), - ScheduleSourceType.Rapla: () async => await _raplaScheduleSource(), - ScheduleSourceType.Ical: () async => await _icalScheduleSource(), - ScheduleSourceType.Mannheim: () async => await _icalScheduleSource(), + ScheduleSourceType.Dualis: () async => _dualisScheduleSource(), + ScheduleSourceType.Rapla: () async => _raplaScheduleSource(), + ScheduleSourceType.Ical: () async => _icalScheduleSource(), + ScheduleSourceType.Mannheim: () async => _icalScheduleSource(), }; if (initializer.containsKey(scheduleSourceType)) { - scheduleSource = await initializer[scheduleSourceType](); + scheduleSource = await initializer[scheduleSourceType]!(); } _currentScheduleSource = scheduleSource; - var success = didSetupCorrectly(); + final success = didSetupCorrectly(); - _onDidChangeScheduleSourceCallbacks.forEach((element) { + for (final element in _onDidChangeScheduleSourceCallbacks) { element(scheduleSource, success); - }); + } return success; } Future _getScheduleSourceType() async { - var type = await _preferencesProvider.getScheduleSourceType(); - - var scheduleSourceType = type != null - ? ScheduleSourceType.values[type] - : ScheduleSourceType.None; + final type = await _preferencesProvider.getScheduleSourceType(); - return scheduleSourceType; + return ScheduleSourceType.values[type]; } Future _dualisScheduleSource() async { - var dualis = DualisScheduleSource(KiwiContainer().resolve()); - - var credentials = await _preferencesProvider.loadDualisCredentials(); + final credentials = await _preferencesProvider.loadDualisCredentials(); - if (credentials.allFieldsFilled()) { - dualis.setLoginCredentials(credentials); + if (credentials != null) { + final dualis = + DualisScheduleSource(KiwiContainer().resolve(), credentials); return ErrorReportScheduleSourceDecorator(dualis); } else { - return InvalidScheduleSource(); + return const InvalidScheduleSource(); } } Future _raplaScheduleSource() async { - var raplaUrl = await _preferencesProvider.getRaplaUrl(); + final raplaUrl = await _preferencesProvider.getRaplaUrl(); - var rapla = RaplaScheduleSource(); - var urlValid = RaplaScheduleSource.isValidUrl(raplaUrl); + final urlValid = RaplaScheduleSource.isValidUrl(raplaUrl); if (urlValid) { - rapla.setEndpointUrl(raplaUrl); + final rapla = RaplaScheduleSource(raplaUrl: raplaUrl); ScheduleSource source = ErrorReportScheduleSourceDecorator(rapla); @@ -109,28 +104,31 @@ class ScheduleSourceProvider { return source; } - return InvalidScheduleSource(); + return const InvalidScheduleSource(); } Future _icalScheduleSource() async { - var url = await _preferencesProvider.getIcalUrl(); + final url = await _preferencesProvider.getIcalUrl(); - var ical = IcalScheduleSource(); - ical.setIcalUrl(url); + if (url != null) { + final ical = IcalScheduleSource(url); - if (ical.canQuery()) { - ScheduleSource source = ErrorReportScheduleSourceDecorator(ical); + if (ical.canQuery()) { + ScheduleSource source = ErrorReportScheduleSourceDecorator(ical); - if (!_appRunningInBackground) { - source = IsolateScheduleSourceDecorator(source); + if (!_appRunningInBackground) { + source = IsolateScheduleSourceDecorator(source); + } + return source; } - return source; } - return InvalidScheduleSource(); + return const InvalidScheduleSource(); } - Future setupForRapla(String url) async { + Future setupForRapla(String? url) async { + if (url == null) return; + await _preferencesProvider.setRaplaUrl(url); await _preferencesProvider .setScheduleSourceType(ScheduleSourceType.Rapla.index); @@ -157,7 +155,9 @@ class ScheduleSourceProvider { ); } - Future setupForIcal(String url) async { + Future setupForIcal(String? url) async { + if (url == null) return; + await _preferencesProvider.setIcalUrl(url); await _preferencesProvider .setScheduleSourceType(ScheduleSourceType.Ical.index); @@ -171,8 +171,8 @@ class ScheduleSourceProvider { ); } - Future setupForMannheim(Course selectedCourse) async { - if(selectedCourse == null) return; + Future setupForMannheim(Course? selectedCourse) async { + if (selectedCourse == null) return; await _preferencesProvider.setMannheimScheduleId(selectedCourse.scheduleId); await _preferencesProvider.setIcalUrl(selectedCourse.icalUrl); await _preferencesProvider @@ -188,8 +188,7 @@ class ScheduleSourceProvider { } bool didSetupCorrectly() { - return _currentScheduleSource != null && - !(_currentScheduleSource is InvalidScheduleSource); + return _currentScheduleSource is! InvalidScheduleSource; } void addDidChangeScheduleSourceCallback(OnDidChangeScheduleSource callback) { @@ -197,16 +196,16 @@ class ScheduleSourceProvider { } Future _clearEntryCache() async { - var scheduleEntries = _scheduleEntryRepository.deleteAllScheduleEntries(); - var queryInformation = + final scheduleEntries = _scheduleEntryRepository.deleteAllScheduleEntries(); + final queryInformation = _scheduleQueryInformationRepository.deleteAllQueryInformation(); await Future.wait([scheduleEntries, queryInformation]); } void fireScheduleSourceChanged() { - _onDidChangeScheduleSourceCallbacks.forEach((element) { + for (final element in _onDidChangeScheduleSourceCallbacks) { element(currentScheduleSource, true); - }); + } } } diff --git a/lib/schedule/data/schedule_entry_entity.dart b/lib/schedule/data/schedule_entry_entity.dart deleted file mode 100644 index 2f532d8f..00000000 --- a/lib/schedule/data/schedule_entry_entity.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:dhbwstudentapp/common/data/database_entity.dart'; -import 'package:dhbwstudentapp/schedule/model/schedule_entry.dart'; - -class ScheduleEntryEntity extends DatabaseEntity { - ScheduleEntry _scheduleEntry; - - ScheduleEntryEntity.fromModel(ScheduleEntry scheduleEntry) { - _scheduleEntry = scheduleEntry; - } - - ScheduleEntryEntity.fromMap(Map map) { - fromMap(map); - } - - @override - void fromMap(Map map) { - DateTime startDate; - if (map["start"] != null) { - startDate = DateTime.fromMillisecondsSinceEpoch(map["start"]); - } - - DateTime endDate; - if (map["end"] != null) { - endDate = DateTime.fromMillisecondsSinceEpoch(map["end"]); - } - - _scheduleEntry = ScheduleEntry( - id: map["id"], - start: startDate, - end: endDate, - details: map["details"], - professor: map["professor"], - title: map["title"], - room: map["room"], - type: ScheduleEntryType.values[map["type"]]); - } - - @override - Map toMap() { - return { - "id": _scheduleEntry.id, - "start": _scheduleEntry.start?.millisecondsSinceEpoch ?? 0, - "end": _scheduleEntry.end?.millisecondsSinceEpoch ?? 0, - "details": _scheduleEntry.details ?? "", - "professor": _scheduleEntry.professor ?? "", - "room": _scheduleEntry.room ?? "", - "title": _scheduleEntry.title ?? "", - "type": _scheduleEntry.type?.index - }; - } - - ScheduleEntry asScheduleEntry() => _scheduleEntry; - - static String tableName() => "ScheduleEntries"; -} diff --git a/lib/schedule/data/schedule_entry_repository.dart b/lib/schedule/data/schedule_entry_repository.dart index 0f72149f..321b0886 100644 --- a/lib/schedule/data/schedule_entry_repository.dart +++ b/lib/schedule/data/schedule_entry_repository.dart @@ -1,21 +1,22 @@ import 'package:dhbwstudentapp/common/data/database_access.dart'; -import 'package:dhbwstudentapp/schedule/data/schedule_entry_entity.dart'; import 'package:dhbwstudentapp/schedule/model/schedule.dart'; import 'package:dhbwstudentapp/schedule/model/schedule_entry.dart'; class ScheduleEntryRepository { final DatabaseAccess _database; - ScheduleEntryRepository(this._database); + const ScheduleEntryRepository(this._database); Future queryScheduleForDay(DateTime date) async { return queryScheduleBetweenDates(date, date.add(const Duration(days: 1))); } Future queryScheduleBetweenDates( - DateTime start, DateTime end) async { - var rows = await _database.queryRows( - ScheduleEntryEntity.tableName(), + DateTime start, + DateTime end, + ) async { + final rows = await _database.queryRows( + ScheduleEntry.tableName, where: "end>? AND start[]; + for (final row in rows) { + entries.add(ScheduleEntry.fromJson(row)); } - return schedule; + return Schedule(entries: entries); } - Future queryExistingScheduleEntry(ScheduleEntry entry) async { - var rows = await _database.queryRows( - ScheduleEntryEntity.tableName(), + Future queryExistingScheduleEntry(ScheduleEntry entry) async { + final rows = await _database.queryRows( + ScheduleEntry.tableName, where: "start=? AND end=? AND title=? AND details=? AND professor=?", whereArgs: [ entry.start.millisecondsSinceEpoch, @@ -49,68 +47,69 @@ class ScheduleEntryRepository { if (rows.isEmpty) return null; - return ScheduleEntryEntity.fromMap(rows[0]).asScheduleEntry(); + return ScheduleEntry.fromJson(rows[0]); } - Future queryNextScheduleEntry(DateTime dateTime) async { - var nextScheduleEntry = await _database.queryRows( - ScheduleEntryEntity.tableName(), + Future queryNextScheduleEntry(DateTime dateTime) async { + final nextScheduleEntry = await _database.queryRows( + ScheduleEntry.tableName, where: "start>?", whereArgs: [dateTime.millisecondsSinceEpoch], limit: 1, orderBy: "start ASC", ); - var entriesList = nextScheduleEntry.toList(); + final entriesList = nextScheduleEntry.toList(); if (entriesList.length == 1) { - return ScheduleEntryEntity.fromMap(entriesList[0]).asScheduleEntry(); + return ScheduleEntry.fromJson(entriesList[0]); } return null; } - Future> queryAllNamesOfScheduleEntries() async { - var allNames = await _database.rawQuery( + Future> queryAllNamesOfScheduleEntries() async { + final allNames = await _database.rawQuery( "SELECT DISTINCT title FROM ScheduleEntries", [], ); - return allNames.map((e) => e["title"] as String).toList(); + return allNames.map((e) => e["title"] as String?).toList(); } Future saveScheduleEntry(ScheduleEntry entry) async { - var row = ScheduleEntryEntity.fromModel(entry).toMap(); - - var existingEntry = await queryExistingScheduleEntry(entry); + final existingEntry = await queryExistingScheduleEntry(entry); if (existingEntry != null) { - entry.id = existingEntry.id; + entry = entry.copyWith.id(existingEntry.id); return; } + final row = entry.toJson(); if (entry.id == null) { - var id = await _database.insert(ScheduleEntryEntity.tableName(), row); - entry.id = id; + final id = await _database.insert(ScheduleEntry.tableName, row); + entry = entry.copyWith.id(id); } else { - await _database.update(ScheduleEntryEntity.tableName(), row); + await _database.update(ScheduleEntry.tableName, row); } } Future saveSchedule(Schedule schedule) async { - for (var entry in schedule.entries ?? []) { + for (final entry in schedule.entries) { saveScheduleEntry(entry); } } Future deleteScheduleEntry(ScheduleEntry entry) async { - await _database.delete(ScheduleEntryEntity.tableName(), entry.id); + await _database.delete(ScheduleEntry.tableName, entry.id); } Future deleteScheduleEntriesBetween( - DateTime start, DateTime end) async { + DateTime start, + DateTime end, + ) async { await _database.deleteWhere( - ScheduleEntryEntity.tableName(), + ScheduleEntry.tableName, where: "start>=? AND end<=?", whereArgs: [ start.millisecondsSinceEpoch, @@ -121,7 +120,7 @@ class ScheduleEntryRepository { Future deleteAllScheduleEntries() async { await _database.deleteWhere( - ScheduleEntryEntity.tableName(), + ScheduleEntry.tableName, where: "1=1", whereArgs: [], ); diff --git a/lib/schedule/data/schedule_filter_repository.dart b/lib/schedule/data/schedule_filter_repository.dart index 39e41480..2aa5c4cb 100644 --- a/lib/schedule/data/schedule_filter_repository.dart +++ b/lib/schedule/data/schedule_filter_repository.dart @@ -3,19 +3,19 @@ import 'package:dhbwstudentapp/common/data/database_access.dart'; class ScheduleFilterRepository { final DatabaseAccess _database; - ScheduleFilterRepository(this._database); + const ScheduleFilterRepository(this._database); - Future> queryAllHiddenNames() async { - var rows = await _database.queryRows("ScheduleEntryFilters"); + Future> queryAllHiddenNames() async { + final rows = await _database.queryRows("ScheduleEntryFilters"); - var names = rows.map((e) => e['title'] as String).toList(); + final names = rows.map((e) => e['title'] as String?).toList(); return names; } - Future saveAllHiddenNames(List hiddenNames) async { + Future saveAllHiddenNames(List hiddenNames) async { await _database.deleteWhere("ScheduleEntryFilters"); - for (var name in hiddenNames) { + for (final name in hiddenNames) { await _database.insert("ScheduleEntryFilters", {'title': name}); } } diff --git a/lib/schedule/data/schedule_query_information_entity.dart b/lib/schedule/data/schedule_query_information_entity.dart deleted file mode 100644 index abd0d5e2..00000000 --- a/lib/schedule/data/schedule_query_information_entity.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:dhbwstudentapp/common/data/database_entity.dart'; -import 'package:dhbwstudentapp/schedule/model/schedule_query_information.dart'; - -class ScheduleQueryInformationEntity extends DatabaseEntity { - ScheduleQueryInformation _scheduleQueryInformation; - - ScheduleQueryInformationEntity.fromModel( - ScheduleQueryInformation scheduleQueryInformation, - ) { - _scheduleQueryInformation = scheduleQueryInformation; - } - - ScheduleQueryInformationEntity.fromMap(Map map) { - fromMap(map); - } - - @override - void fromMap(Map map) { - DateTime startDate; - if (map["start"] != null) { - startDate = DateTime.fromMillisecondsSinceEpoch(map["start"]); - } - - DateTime endDate; - if (map["end"] != null) { - endDate = DateTime.fromMillisecondsSinceEpoch(map["end"]); - } - - DateTime queryTimeDate; - if (map["end"] != null) { - queryTimeDate = DateTime.fromMillisecondsSinceEpoch(map["queryTime"]); - } - - _scheduleQueryInformation = - ScheduleQueryInformation(startDate, endDate, queryTimeDate); - } - - @override - Map toMap() { - return { - "start": _scheduleQueryInformation.start?.millisecondsSinceEpoch ?? 0, - "end": _scheduleQueryInformation.end?.millisecondsSinceEpoch ?? 0, - "queryTime": _scheduleQueryInformation.queryTime?.millisecondsSinceEpoch, - }; - } - - ScheduleQueryInformation asScheduleQueryInformation() => - _scheduleQueryInformation; - - static String tableName() => "ScheduleQueryInformation"; -} diff --git a/lib/schedule/data/schedule_query_information_repository.dart b/lib/schedule/data/schedule_query_information_repository.dart index 669d300e..64d5b716 100644 --- a/lib/schedule/data/schedule_query_information_repository.dart +++ b/lib/schedule/data/schedule_query_information_repository.dart @@ -1,15 +1,16 @@ import 'package:dhbwstudentapp/common/data/database_access.dart'; -import 'package:dhbwstudentapp/schedule/data/schedule_query_information_entity.dart'; import 'package:dhbwstudentapp/schedule/model/schedule_query_information.dart'; class ScheduleQueryInformationRepository { final DatabaseAccess _database; - ScheduleQueryInformationRepository(this._database); + const ScheduleQueryInformationRepository(this._database); - Future getOldestQueryTimeBetweenDates( - DateTime start, DateTime end) async { - var oldestQueryTimeDate = await _database.queryAggregator( + Future getOldestQueryTimeBetweenDates( + DateTime start, + DateTime end, + ) async { + final oldestQueryTimeDate = await _database.queryAggregator( "SELECT MIN(queryTime) FROM ScheduleQueryInformation WHERE start<=? AND end>=?", [ start.millisecondsSinceEpoch, @@ -17,13 +18,17 @@ class ScheduleQueryInformationRepository { ], ); + if (oldestQueryTimeDate == null) return null; + return DateTime.fromMillisecondsSinceEpoch(oldestQueryTimeDate); } - Future> getQueryInformationBetweenDates( - DateTime start, DateTime end) async { - var rows = await _database.queryRows( - ScheduleQueryInformationEntity.tableName(), + Future> getQueryInformationBetweenDates( + DateTime start, + DateTime end, + ) async { + final rows = await _database.queryRows( + ScheduleQueryInformation.tableName, where: "start<=? AND end>=?", whereArgs: [ start.millisecondsSinceEpoch, @@ -31,12 +36,11 @@ class ScheduleQueryInformationRepository { ], ); - var scheduleQueryInformation = []; + final scheduleQueryInformation = []; - for (var row in rows) { + for (final row in rows) { scheduleQueryInformation.add( - ScheduleQueryInformationEntity.fromMap(row) - .asScheduleQueryInformation(), + ScheduleQueryInformation.fromJson(row), ); } @@ -44,9 +48,10 @@ class ScheduleQueryInformationRepository { } Future saveScheduleQueryInformation( - ScheduleQueryInformation queryInformation) async { + ScheduleQueryInformation queryInformation, + ) async { await _database.deleteWhere( - ScheduleQueryInformationEntity.tableName(), + ScheduleQueryInformation.tableName, where: "start=? AND end=?", whereArgs: [ queryInformation.start.millisecondsSinceEpoch, @@ -55,14 +60,14 @@ class ScheduleQueryInformationRepository { ); await _database.insert( - ScheduleQueryInformationEntity.tableName(), - ScheduleQueryInformationEntity.fromModel(queryInformation).toMap(), + ScheduleQueryInformation.tableName, + queryInformation.toJson(), ); } Future deleteAllQueryInformation() async { await _database.deleteWhere( - ScheduleQueryInformationEntity.tableName(), + ScheduleQueryInformation.tableName, where: "1=1", whereArgs: [], ); diff --git a/lib/schedule/model/schedule.dart b/lib/schedule/model/schedule.dart index 7a948471..cb425db5 100644 --- a/lib/schedule/model/schedule.dart +++ b/lib/schedule/model/schedule.dart @@ -1,23 +1,24 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:dhbwstudentapp/schedule/model/schedule_entry.dart'; +part 'schedule.g.dart'; + +@CopyWith() class Schedule { final List entries; - final List urls = []; - - Schedule() : entries = []; + final List urls; - Schedule.fromList(this.entries); - - void addEntry(ScheduleEntry entry) { - entries.add(entry); - } + const Schedule({ + this.entries = const [], + this.urls = const [], + }); void merge(Schedule schedule) { // TODO: Return new schedule instead of adding it to the list urls.addAll(schedule.urls); - for (var newEntry in schedule.entries) { - if (entries.any((element) => element.equalsWithIdIgnored(newEntry))) { + for (final newEntry in schedule.entries) { + if (entries.any((element) => element == newEntry)) { continue; } @@ -25,46 +26,46 @@ class Schedule { } } - Schedule trim(DateTime startDate, DateTime endDate) { - var newList = []; + // TODO: [Leptopoda] improve nullability + Schedule trim(DateTime? startDate, DateTime? endDate) { + final newList = []; - for (var entry in entries) { - if (startDate.isBefore(entry.end) && endDate.isAfter(entry.start)) { + for (final entry in entries) { + if (startDate!.isBefore(entry.end) && endDate!.isAfter(entry.start)) { newList.add(entry); } } - var schedule = Schedule.fromList(newList); - schedule.urls.addAll(urls); + final schedule = Schedule(entries: newList, urls: urls); return schedule; } - DateTime getStartDate() { + DateTime? getStartDate() { if (entries.isEmpty) return null; - var date = entries?.reduce((ScheduleEntry a, ScheduleEntry b) { - return a.start.isBefore(b.start) ? a : b; - })?.start; + final date = entries.reduce((ScheduleEntry? a, ScheduleEntry? b) { + return a!.start.isBefore(b!.start) ? a : b; + }).start; return date; } - DateTime getEndDate() { + DateTime? getEndDate() { if (entries.isEmpty) return null; - var date = entries?.reduce((ScheduleEntry a, ScheduleEntry b) { - return a.end.isAfter(b.end) ? a : b; - })?.end; + final date = entries.reduce((ScheduleEntry? a, ScheduleEntry? b) { + return a!.end.isAfter(b!.end) ? a : b; + }).end; return date; } - DateTime getStartTime() { - DateTime earliestTime; + DateTime? getStartTime() { + DateTime? earliestTime; - for (var entry in entries) { - var entryTime = DateTime( + for (final entry in entries) { + final entryTime = DateTime( 0, 1, 1, @@ -83,11 +84,11 @@ class Schedule { return earliestTime; } - DateTime getEndTime() { - DateTime latestTime; + DateTime? getEndTime() { + DateTime? latestTime; - for (var entry in entries) { - var entryTime = DateTime( + for (final entry in entries) { + final entryTime = DateTime( 0, 1, 1, @@ -105,12 +106,4 @@ class Schedule { return latestTime; } - - Schedule copyWith({List entries}) { - var schedule = Schedule.fromList(entries); - - schedule.urls.addAll(urls); - - return schedule; - } } diff --git a/lib/schedule/model/schedule.g.dart b/lib/schedule/model/schedule.g.dart new file mode 100644 index 00000000..2694f159 --- /dev/null +++ b/lib/schedule/model/schedule.g.dart @@ -0,0 +1,67 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'schedule.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$ScheduleCWProxy { + Schedule entries(List entries); + + Schedule urls(List urls); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Schedule(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// Schedule(...).copyWith(id: 12, name: "My name") + /// ```` + Schedule call({ + List? entries, + List? urls, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfSchedule.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfSchedule.copyWith.fieldName(...)` +class _$ScheduleCWProxyImpl implements _$ScheduleCWProxy { + final Schedule _value; + + const _$ScheduleCWProxyImpl(this._value); + + @override + Schedule entries(List entries) => this(entries: entries); + + @override + Schedule urls(List urls) => this(urls: urls); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Schedule(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// Schedule(...).copyWith(id: 12, name: "My name") + /// ```` + Schedule call({ + Object? entries = const $CopyWithPlaceholder(), + Object? urls = const $CopyWithPlaceholder(), + }) { + return Schedule( + entries: entries == const $CopyWithPlaceholder() || entries == null + ? _value.entries + // ignore: cast_nullable_to_non_nullable + : entries as List, + urls: urls == const $CopyWithPlaceholder() || urls == null + ? _value.urls + // ignore: cast_nullable_to_non_nullable + : urls as List, + ); + } +} + +extension $ScheduleCopyWith on Schedule { + /// Returns a callable class that can be used as follows: `instanceOfSchedule.copyWith(...)` or like so:`instanceOfSchedule.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$ScheduleCWProxy get copyWith => _$ScheduleCWProxyImpl(this); +} diff --git a/lib/schedule/model/schedule_entry.dart b/lib/schedule/model/schedule_entry.dart index 619df572..5442e36f 100644 --- a/lib/schedule/model/schedule_entry.dart +++ b/lib/schedule/model/schedule_entry.dart @@ -1,46 +1,76 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:dhbwstudentapp/common/data/epoch_date_time_converter.dart'; +import 'package:dhbwstudentapp/common/ui/schedule_entry_theme.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'schedule_entry.g.dart'; + enum ScheduleEntryType { Unknown, - Class, + Lesson, Online, PublicHoliday, - Exam, + Exam; + + Color color(BuildContext context) { + final scheduleEntryTheme = + Theme.of(context).extension()!; + + switch (this) { + case ScheduleEntryType.PublicHoliday: + return scheduleEntryTheme.publicHoliday; + case ScheduleEntryType.Lesson: + return scheduleEntryTheme.lesson; + case ScheduleEntryType.Exam: + return scheduleEntryTheme.exam; + case ScheduleEntryType.Online: + return scheduleEntryTheme.online; + case ScheduleEntryType.Unknown: + return scheduleEntryTheme.unknown; + } + } } -class ScheduleEntry { - int id; +@CopyWith() +@JsonSerializable() +class ScheduleEntry extends Equatable { + final int? id; + @EpochDateTimeConverter() final DateTime start; + @EpochDateTimeConverter() final DateTime end; final String title; final String details; final String professor; final String room; + @JsonKey( + toJson: _typeToJson, + fromJson: _typeFromJson, + ) final ScheduleEntryType type; ScheduleEntry({ this.id, - this.start, - this.end, - this.title, - this.details, - this.professor, - this.room, - this.type, - }); - - bool equalsWithIdIgnored(ScheduleEntry other) { - return this.start == other.start && - this.end == other.end && - this.title == other.title && - this.details == other.details && - this.professor == other.professor && - this.room == other.room && - this.type == other.type; - } + DateTime? start, + DateTime? end, + String? title, + String? details, + String? professor, + String? room, + required this.type, + }) : start = start ?? DateTime.fromMicrosecondsSinceEpoch(0), + end = end ?? DateTime.fromMicrosecondsSinceEpoch(0), + details = details ?? "", + professor = professor ?? "", + room = room ?? "", + title = title ?? ""; List getDifferentProperties(ScheduleEntry entry) { - var changedProperties = []; + final changedProperties = []; - if ((title ?? "") != (entry.title ?? "")) { + if (title != entry.title) { changedProperties.add("title"); } if (start != entry.start) { @@ -49,13 +79,13 @@ class ScheduleEntry { if (end != entry.end) { changedProperties.add("end"); } - if ((details ?? "") != (entry.details ?? "")) { + if (details != entry.details) { changedProperties.add("details"); } - if ((professor ?? "") != (entry.professor ?? "")) { + if (professor != entry.professor) { changedProperties.add("professor"); } - if ((room ?? "") != (entry.room ?? "")) { + if (room != entry.room) { changedProperties.add("room"); } if (type != entry.type) { @@ -65,23 +95,18 @@ class ScheduleEntry { return changedProperties; } - ScheduleEntry copyWith( - {DateTime start, - DateTime end, - String title, - String details, - String professor, - String room, - ScheduleEntryType type}) { - return ScheduleEntry( - id: id, - start: start ?? this.start, - end: end ?? this.end, - title: title ?? this.title, - details: details ?? this.details, - professor: professor ?? this.professor, - room: room ?? this.room, - type: type ?? this.type, - ); - } + factory ScheduleEntry.fromJson(Map json) => + _$ScheduleEntryFromJson(json); + + Map toJson() => _$ScheduleEntryToJson(this); + + static const tableName = "ScheduleEntries"; + + static int _typeToJson(ScheduleEntryType value) => value.index; + static ScheduleEntryType _typeFromJson(int value) => + ScheduleEntryType.values[value]; + + @override + List get props => + [start, end, title, details, professor, room, type]; } diff --git a/lib/schedule/model/schedule_entry.g.dart b/lib/schedule/model/schedule_entry.g.dart new file mode 100644 index 00000000..03d213e9 --- /dev/null +++ b/lib/schedule/model/schedule_entry.g.dart @@ -0,0 +1,169 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'schedule_entry.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$ScheduleEntryCWProxy { + ScheduleEntry details(String? details); + + ScheduleEntry end(DateTime? end); + + ScheduleEntry id(int? id); + + ScheduleEntry professor(String? professor); + + ScheduleEntry room(String? room); + + ScheduleEntry start(DateTime? start); + + ScheduleEntry title(String? title); + + ScheduleEntry type(ScheduleEntryType type); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `ScheduleEntry(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// ScheduleEntry(...).copyWith(id: 12, name: "My name") + /// ```` + ScheduleEntry call({ + String? details, + DateTime? end, + int? id, + String? professor, + String? room, + DateTime? start, + String? title, + ScheduleEntryType? type, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfScheduleEntry.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfScheduleEntry.copyWith.fieldName(...)` +class _$ScheduleEntryCWProxyImpl implements _$ScheduleEntryCWProxy { + final ScheduleEntry _value; + + const _$ScheduleEntryCWProxyImpl(this._value); + + @override + ScheduleEntry details(String? details) => this(details: details); + + @override + ScheduleEntry end(DateTime? end) => this(end: end); + + @override + ScheduleEntry id(int? id) => this(id: id); + + @override + ScheduleEntry professor(String? professor) => this(professor: professor); + + @override + ScheduleEntry room(String? room) => this(room: room); + + @override + ScheduleEntry start(DateTime? start) => this(start: start); + + @override + ScheduleEntry title(String? title) => this(title: title); + + @override + ScheduleEntry type(ScheduleEntryType type) => this(type: type); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `ScheduleEntry(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// ScheduleEntry(...).copyWith(id: 12, name: "My name") + /// ```` + ScheduleEntry call({ + Object? details = const $CopyWithPlaceholder(), + Object? end = const $CopyWithPlaceholder(), + Object? id = const $CopyWithPlaceholder(), + Object? professor = const $CopyWithPlaceholder(), + Object? room = const $CopyWithPlaceholder(), + Object? start = const $CopyWithPlaceholder(), + Object? title = const $CopyWithPlaceholder(), + Object? type = const $CopyWithPlaceholder(), + }) { + return ScheduleEntry( + details: details == const $CopyWithPlaceholder() + ? _value.details + // ignore: cast_nullable_to_non_nullable + : details as String?, + end: end == const $CopyWithPlaceholder() + ? _value.end + // ignore: cast_nullable_to_non_nullable + : end as DateTime?, + id: id == const $CopyWithPlaceholder() + ? _value.id + // ignore: cast_nullable_to_non_nullable + : id as int?, + professor: professor == const $CopyWithPlaceholder() + ? _value.professor + // ignore: cast_nullable_to_non_nullable + : professor as String?, + room: room == const $CopyWithPlaceholder() + ? _value.room + // ignore: cast_nullable_to_non_nullable + : room as String?, + start: start == const $CopyWithPlaceholder() + ? _value.start + // ignore: cast_nullable_to_non_nullable + : start as DateTime?, + title: title == const $CopyWithPlaceholder() + ? _value.title + // ignore: cast_nullable_to_non_nullable + : title as String?, + type: type == const $CopyWithPlaceholder() || type == null + ? _value.type + // ignore: cast_nullable_to_non_nullable + : type as ScheduleEntryType, + ); + } +} + +extension $ScheduleEntryCopyWith on ScheduleEntry { + /// Returns a callable class that can be used as follows: `instanceOfScheduleEntry.copyWith(...)` or like so:`instanceOfScheduleEntry.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$ScheduleEntryCWProxy get copyWith => _$ScheduleEntryCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ScheduleEntry _$ScheduleEntryFromJson(Map json) => + ScheduleEntry( + id: json['id'] as int?, + start: _$JsonConverterFromJson( + json['start'], const EpochDateTimeConverter().fromJson), + end: _$JsonConverterFromJson( + json['end'], const EpochDateTimeConverter().fromJson), + title: json['title'] as String?, + details: json['details'] as String?, + professor: json['professor'] as String?, + room: json['room'] as String?, + type: ScheduleEntry._typeFromJson(json['type'] as int), + ); + +Map _$ScheduleEntryToJson(ScheduleEntry instance) => + { + 'id': instance.id, + 'start': const EpochDateTimeConverter().toJson(instance.start), + 'end': const EpochDateTimeConverter().toJson(instance.end), + 'title': instance.title, + 'details': instance.details, + 'professor': instance.professor, + 'room': instance.room, + 'type': ScheduleEntry._typeToJson(instance.type), + }; + +Value? _$JsonConverterFromJson( + Object? json, + Value? Function(Json json) fromJson, +) => + json == null ? null : fromJson(json as Json); diff --git a/lib/schedule/model/schedule_query_information.dart b/lib/schedule/model/schedule_query_information.dart index e527f01a..ec5be49a 100644 --- a/lib/schedule/model/schedule_query_information.dart +++ b/lib/schedule/model/schedule_query_information.dart @@ -1,7 +1,28 @@ +import 'package:dhbwstudentapp/common/data/epoch_date_time_converter.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'schedule_query_information.g.dart'; + +@JsonSerializable() class ScheduleQueryInformation { + @EpochDateTimeConverter() final DateTime start; + @EpochDateTimeConverter() final DateTime end; - final DateTime queryTime; + @EpochDateTimeConverter() + final DateTime? queryTime; + + ScheduleQueryInformation( + DateTime? start, + DateTime? end, + this.queryTime, + ) : start = start ?? DateTime.fromMillisecondsSinceEpoch(0), + end = end ?? DateTime.fromMillisecondsSinceEpoch(0); + + factory ScheduleQueryInformation.fromJson(Map json) => + _$ScheduleQueryInformationFromJson(json); + + Map toJson() => _$ScheduleQueryInformationToJson(this); - ScheduleQueryInformation(this.start, this.end, this.queryTime); + static const tableName = "ScheduleQueryInformation"; } diff --git a/lib/schedule/model/schedule_query_information.g.dart b/lib/schedule/model/schedule_query_information.g.dart new file mode 100644 index 00000000..a0492ffc --- /dev/null +++ b/lib/schedule/model/schedule_query_information.g.dart @@ -0,0 +1,39 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'schedule_query_information.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ScheduleQueryInformation _$ScheduleQueryInformationFromJson( + Map json) => + ScheduleQueryInformation( + _$JsonConverterFromJson( + json['start'], const EpochDateTimeConverter().fromJson), + _$JsonConverterFromJson( + json['end'], const EpochDateTimeConverter().fromJson), + _$JsonConverterFromJson( + json['queryTime'], const EpochDateTimeConverter().fromJson), + ); + +Map _$ScheduleQueryInformationToJson( + ScheduleQueryInformation instance) => + { + 'start': const EpochDateTimeConverter().toJson(instance.start), + 'end': const EpochDateTimeConverter().toJson(instance.end), + 'queryTime': _$JsonConverterToJson( + instance.queryTime, const EpochDateTimeConverter().toJson), + }; + +Value? _$JsonConverterFromJson( + Object? json, + Value? Function(Json json) fromJson, +) => + json == null ? null : fromJson(json as Json); + +Json? _$JsonConverterToJson( + Value? value, + Json? Function(Value value) toJson, +) => + value == null ? null : toJson(value); diff --git a/lib/schedule/model/schedule_query_result.dart b/lib/schedule/model/schedule_query_result.dart index 26382398..4b34b5f8 100644 --- a/lib/schedule/model/schedule_query_result.dart +++ b/lib/schedule/model/schedule_query_result.dart @@ -4,16 +4,16 @@ class ScheduleQueryResult { final Schedule schedule; final List errors; - bool get hasError => errors?.isNotEmpty ?? false; + bool get hasError => errors.isNotEmpty; - ScheduleQueryResult(this.schedule, this.errors); + const ScheduleQueryResult(this.schedule, this.errors); } class ParseError { final String object; - final String trace; + final String? trace; - ParseError(Object object, StackTrace trace) - : object = object?.toString(), + ParseError(Object object, [StackTrace? trace]) + : object = object.toString(), trace = trace?.toString(); } diff --git a/lib/schedule/service/dualis/dualis_schedule_source.dart b/lib/schedule/service/dualis/dualis_schedule_source.dart index f515f501..c479e87f 100644 --- a/lib/schedule/service/dualis/dualis_schedule_source.dart +++ b/lib/schedule/service/dualis/dualis_schedule_source.dart @@ -10,27 +10,35 @@ import 'package:dhbwstudentapp/schedule/service/schedule_source.dart'; class DualisScheduleSource extends ScheduleSource { final DualisScraper _dualisScraper; - DualisScheduleSource(this._dualisScraper); + DualisScheduleSource(this._dualisScraper, Credentials credentials) { + _dualisScraper.loginCredentials = credentials; + } @override - Future querySchedule(DateTime from, DateTime to, - [CancellationToken cancellationToken]) async { - if (cancellationToken == null) cancellationToken = CancellationToken(); + Future querySchedule( + DateTime? from, + DateTime? to, [ + CancellationToken? cancellationToken, + ]) async { + cancellationToken ??= CancellationToken(); - DateTime current = toStartOfMonth(from); + DateTime current = toStartOfMonth(from)!; - var schedule = Schedule(); - var allErrors = []; + var schedule = const Schedule(); + final allErrors = []; - if (!_dualisScraper.isLoggedIn()) + if (!_dualisScraper.isLoggedIn()) { await _dualisScraper.loginWithPreviousCredentials(cancellationToken); + } - while (to.isAfter(current) && !cancellationToken.isCancelled()) { + while (to!.isAfter(current) && !cancellationToken.isCancelled) { try { - var monthSchedule = await _dualisScraper.loadMonthlySchedule( - current, cancellationToken); + final monthSchedule = await _dualisScraper.loadMonthlySchedule( + current, + cancellationToken, + ); - if (monthSchedule != null) schedule.merge(monthSchedule); + schedule.merge(monthSchedule); } on OperationCancelledException { rethrow; } on ParseException catch (ex, trace) { @@ -43,20 +51,13 @@ class DualisScheduleSource extends ScheduleSource { current = toNextMonth(current); } - if (cancellationToken.isCancelled()) throw OperationCancelledException(); + cancellationToken.throwIfCancelled(); schedule = schedule.trim(from, to); return ScheduleQueryResult(schedule, allErrors); } - Future setLoginCredentials(Credentials credentials) async { - _dualisScraper.setLoginCredentials( - credentials.username, - credentials.password, - ); - } - @override bool canQuery() { return _dualisScraper.isLoggedIn(); diff --git a/lib/schedule/service/error_report_schedule_source_decorator.dart b/lib/schedule/service/error_report_schedule_source_decorator.dart index 15c5e88b..3f5e785f 100644 --- a/lib/schedule/service/error_report_schedule_source_decorator.dart +++ b/lib/schedule/service/error_report_schedule_source_decorator.dart @@ -6,13 +6,16 @@ import 'package:dhbwstudentapp/schedule/service/schedule_source.dart'; class ErrorReportScheduleSourceDecorator extends ScheduleSource { final ScheduleSource _scheduleSource; - ErrorReportScheduleSourceDecorator(this._scheduleSource); + const ErrorReportScheduleSourceDecorator(this._scheduleSource); @override - Future querySchedule(DateTime from, DateTime to, - [CancellationToken cancellationToken]) async { + Future querySchedule( + DateTime? from, + DateTime? to, [ + CancellationToken? cancellationToken, + ]) async { try { - var schedule = await _scheduleSource.querySchedule( + final schedule = await _scheduleSource.querySchedule( from, to, cancellationToken, diff --git a/lib/schedule/service/ical/ical_parser.dart b/lib/schedule/service/ical/ical_parser.dart index 60435489..f60dd84b 100644 --- a/lib/schedule/service/ical/ical_parser.dart +++ b/lib/schedule/service/ical/ical_parser.dart @@ -6,6 +6,8 @@ import 'package:dhbwstudentapp/schedule/model/schedule_query_result.dart'; /// Parses an ICAL file extracts all schedule entries /// class IcalParser { + IcalParser(); + /// Matches a calendar entry. The first group contains the text between /// the BEGIN:VEVENT and END:VEVENT final String calendarEntryRegex = "BEGIN:VEVENT(.*?)END:VEVENT"; @@ -22,37 +24,37 @@ class IcalParser { "([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})(Z?)"; ScheduleQueryResult parseIcal(String icalData) { - var regex = RegExp( + final regex = RegExp( calendarEntryRegex, multiLine: true, unicode: true, dotAll: true, ); - var matches = regex.allMatches(icalData); + final matches = regex.allMatches(icalData); - List entries = []; + final List entries = []; - for (var match in matches) { - var entry = _parseEntry(match.group(1)); + for (final match in matches) { + final entry = _parseEntry(match.group(1)!); entries.add(entry); } return ScheduleQueryResult( - Schedule.fromList(entries), + Schedule(entries: entries), [], ); } ScheduleEntry _parseEntry(String entryData) { - var allProperties = RegExp( + final allProperties = RegExp( propertyRegex, unicode: true, ).allMatches(entryData); - Map properties = {}; + final Map properties = {}; - for (var property in allProperties) { + for (final property in allProperties) { properties[property.group(1)] = property.group(3); } @@ -61,29 +63,29 @@ class IcalParser { end: _parseDate(properties["DTEND"]), room: properties["LOCATION"], title: properties["SUMMARY"], - type: ScheduleEntryType.Class, + type: ScheduleEntryType.Lesson, details: properties["DESCRIPTION"] ?? "", professor: "", ); } - DateTime _parseDate(String date) { - var match = RegExp( + DateTime? _parseDate(String? date) { + if (date == null) return null; + + final match = RegExp( dateTimeRegex, unicode: true, - ).firstMatch(date ?? ""); + ).firstMatch(date); - if (match == null) { - return null; - } + if (match == null) return null; return DateTime( - int.tryParse(match.group(1)), - int.tryParse(match.group(2)), - int.tryParse(match.group(3)), - int.tryParse(match.group(4)), - int.tryParse(match.group(5)), - int.tryParse(match.group(6)), + int.tryParse(match.group(1)!)!, + int.tryParse(match.group(2)!)!, + int.tryParse(match.group(3)!)!, + int.tryParse(match.group(4)!)!, + int.tryParse(match.group(5)!)!, + int.tryParse(match.group(6)!)!, ); } } diff --git a/lib/schedule/service/ical/ical_schedule_source.dart b/lib/schedule/service/ical/ical_schedule_source.dart index 495ada39..0f7cd4bb 100644 --- a/lib/schedule/service/ical/ical_schedule_source.dart +++ b/lib/schedule/service/ical/ical_schedule_source.dart @@ -10,11 +10,9 @@ import 'package:http_client_helper/http_client_helper.dart' as http; class IcalScheduleSource extends ScheduleSource { final IcalParser _icalParser = IcalParser(); - String _url; + final String _url; - void setIcalUrl(String url) { - _url = url; - } + IcalScheduleSource(this._url); @override bool canQuery() { @@ -22,17 +20,17 @@ class IcalScheduleSource extends ScheduleSource { } @override - Future querySchedule( - DateTime from, - DateTime to, [ - CancellationToken cancellationToken, + Future querySchedule( + DateTime? from, + DateTime? to, [ + CancellationToken? cancellationToken, ]) async { - var response = await _makeRequest(_url, cancellationToken); + final response = await _makeRequest(_url, cancellationToken!); if (response == null) return null; try { - var body = utf8.decode(response.bodyBytes); - var schedule = _icalParser.parseIcal(body); + final body = utf8.decode(response.bodyBytes); + final schedule = _icalParser.parseIcal(body); return ScheduleQueryResult( schedule.schedule.trim(from, to), @@ -45,39 +43,42 @@ class IcalScheduleSource extends ScheduleSource { } } - Future _makeRequest( - String url, CancellationToken cancellationToken) async { + Future _makeRequest( + String url, + CancellationToken cancellationToken, + ) async { url = url.replaceAll("webcal://", "https://"); - var requestCancellationToken = http.CancellationToken(); + final requestCancellationToken = http.CancellationToken(); try { - cancellationToken.setCancellationCallback(() { - requestCancellationToken.cancel(); - }); + cancellationToken.cancellationCallback = requestCancellationToken.cancel; - var response = await http.HttpClientHelper.get( + final response = await http.HttpClientHelper.get( Uri.parse(url), cancelToken: requestCancellationToken, ); - if (response == null && !requestCancellationToken.isCanceled) - throw ServiceRequestFailed("Http request failed!"); + if (response == null && !requestCancellationToken.isCanceled) { + throw const ServiceRequestFailed("Http request failed!"); + } return response; + // ignore: avoid_catching_errors } on http.OperationCanceledError catch (_) { throw OperationCancelledException(); } catch (ex) { if (!requestCancellationToken.isCanceled) rethrow; } finally { - cancellationToken.setCancellationCallback(null); + cancellationToken.cancellationCallback = null; } return null; } - static bool isValidUrl(String url) { + static bool isValidUrl(String? url) { try { + if (url == null) return false; Uri.parse(url); } catch (e) { return false; diff --git a/lib/schedule/service/invalid_schedule_source.dart b/lib/schedule/service/invalid_schedule_source.dart index 0dcd0030..8a15ce5b 100644 --- a/lib/schedule/service/invalid_schedule_source.dart +++ b/lib/schedule/service/invalid_schedule_source.dart @@ -3,11 +3,13 @@ import 'package:dhbwstudentapp/schedule/model/schedule_query_result.dart'; import 'package:dhbwstudentapp/schedule/service/schedule_source.dart'; class InvalidScheduleSource extends ScheduleSource { + const InvalidScheduleSource(); + @override Future querySchedule( - DateTime from, - DateTime to, [ - CancellationToken cancellationToken, + DateTime? from, + DateTime? to, [ + CancellationToken? cancellationToken, ]) { throw StateError("Schedule source not properly configured"); } diff --git a/lib/schedule/service/isolate_schedule_source_decorator.dart b/lib/schedule/service/isolate_schedule_source_decorator.dart index 02ea3a96..8b62bf85 100644 --- a/lib/schedule/service/isolate_schedule_source_decorator.dart +++ b/lib/schedule/service/isolate_schedule_source_decorator.dart @@ -11,40 +11,43 @@ import 'package:dhbwstudentapp/schedule/service/schedule_source.dart'; class IsolateScheduleSourceDecorator extends ScheduleSource { final ScheduleSource _scheduleSource; - Stream _isolateToMain; - Isolate _isolate; - SendPort _sendPort; + Stream? _isolateToMain; + Isolate? _isolate; + SendPort? _sendPort; IsolateScheduleSourceDecorator(this._scheduleSource); @override - Future querySchedule(DateTime from, DateTime to, - [CancellationToken cancellationToken]) async { + Future querySchedule( + DateTime? from, + DateTime? to, [ + CancellationToken? cancellationToken, + ]) async { await _initializeIsolate(); // Use the cancellation token to send a cancel message. // The isolate then uses a new instance to cancel the request - cancellationToken.setCancellationCallback(() { - _sendPort.send({"type": "cancel"}); - }); + cancellationToken!.cancellationCallback = + () => _sendPort!.send({"type": "cancel"}); - _sendPort.send({ + _sendPort!.send({ "type": "execute", "source": _scheduleSource, "from": from, "to": to, }); - final completer = Completer(); + final completer = Completer(); - ScheduleQueryFailedException potentialException; + ScheduleQueryFailedException? potentialException; - final subscription = _isolateToMain.listen((result) { - cancellationToken.setCancellationCallback(null); + final subscription = _isolateToMain!.listen((result) { + cancellationToken.cancellationCallback = null; - if (result != null && !(result is ScheduleQueryResult)) { + // TODO: [Leptopoda] validate changes + if (result == null || result is! ScheduleQueryResult) { potentialException = ScheduleQueryFailedException(result); - completer.complete(null); + completer.complete(); } else { completer.complete(result); } @@ -55,7 +58,7 @@ class IsolateScheduleSourceDecorator extends ScheduleSource { subscription.cancel(); if (potentialException != null) { - throw potentialException; + throw potentialException!; } return result; @@ -64,13 +67,15 @@ class IsolateScheduleSourceDecorator extends ScheduleSource { Future _initializeIsolate() async { if (_isolate != null && _isolateToMain != null && _sendPort != null) return; - var isolateToMain = ReceivePort(); + final isolateToMain = ReceivePort(); // Use a broadcast stream. The normal ReceivePort closes after one subscription _isolateToMain = isolateToMain.asBroadcastStream(); _isolate = await Isolate.spawn( - scheduleSourceIsolateEntryPoint, isolateToMain.sendPort); - _sendPort = await _isolateToMain.first; + _scheduleSourceIsolateEntryPoint, + isolateToMain.sendPort, + ); + _sendPort = await _isolateToMain!.first as SendPort?; } @override @@ -79,34 +84,35 @@ class IsolateScheduleSourceDecorator extends ScheduleSource { } } -void scheduleSourceIsolateEntryPoint(SendPort sendPort) async { +Future _scheduleSourceIsolateEntryPoint(SendPort sendPort) async { // Using the given send port, send back a send port for two way communication - var port = ReceivePort(); + final port = ReceivePort(); sendPort.send(port.sendPort); - CancellationToken token; + CancellationToken? token; - await for (var message in port) { + await for (final message in port) { + message as Map; if (message["type"] == "execute") { token = CancellationToken(); - executeQueryScheduleMessage(message, sendPort, token); + _executeQueryScheduleMessage(message, sendPort, token); } else if (message["type"] == "cancel") { token?.cancel(); } } } -Future executeQueryScheduleMessage( +Future _executeQueryScheduleMessage( Map map, SendPort sendPort, - CancellationToken token, + CancellationToken? token, ) async { try { - ScheduleSource source = map["source"]; - DateTime from = map["from"]; - DateTime to = map["to"]; + final ScheduleSource source = map["source"] as ScheduleSource; + final DateTime? from = map["from"] as DateTime?; + final DateTime? to = map["to"] as DateTime?; - var result = await source.querySchedule(from, to, token); + final result = await source.querySchedule(from, to, token); sendPort.send(result); } on OperationCancelledException catch (_) { diff --git a/lib/schedule/service/mannheim/mannheim_course_response_parser.dart b/lib/schedule/service/mannheim/mannheim_course_response_parser.dart index 0cb543f0..eb57d14c 100644 --- a/lib/schedule/service/mannheim/mannheim_course_response_parser.dart +++ b/lib/schedule/service/mannheim/mannheim_course_response_parser.dart @@ -3,28 +3,32 @@ import 'package:dhbwstudentapp/schedule/service/mannheim/mannheim_course_scraper import 'package:html/parser.dart'; class MannheimCourseResponseParser { + const MannheimCourseResponseParser(); + List parseCoursePage(String body) { - var document = parse(body); + final document = parse(body); - var selectElement = getElementById(document, "class_select"); - var options = selectElement.getElementsByTagName("option"); + final selectElement = getElementById(document, "class_select"); + final options = selectElement.getElementsByTagName("option"); - var courses = []; + final courses = []; - for (var e in options) { - var label = e.attributes["label"] ?? ""; - var value = e.attributes["value"] ?? ""; - var title = e.parent.attributes["label"] ?? ""; + for (final e in options) { + final label = e.attributes["label"] ?? ""; + final value = e.attributes["value"] ?? ""; + final title = e.parent!.attributes["label"] ?? ""; if (label == "" || value == "") continue; if (label.trim() == "Kurs auswählen") continue; - courses.add(Course( - label, - "http://vorlesungsplan.dhbw-mannheim.de/ical.php?uid=$value", - title, - value, - )); + courses.add( + Course( + label, + "http://vorlesungsplan.dhbw-mannheim.de/ical.php?uid=$value", + title, + value, + ), + ); } courses.sort((c1, c2) => c1.name.compareTo(c2.name)); diff --git a/lib/schedule/service/mannheim/mannheim_course_scraper.dart b/lib/schedule/service/mannheim/mannheim_course_scraper.dart index 7ca27aa0..7b24da9d 100644 --- a/lib/schedule/service/mannheim/mannheim_course_scraper.dart +++ b/lib/schedule/service/mannheim/mannheim_course_scraper.dart @@ -10,47 +10,53 @@ class Course { final String icalUrl; final String scheduleId; - Course(this.name, this.icalUrl, this.title, this.scheduleId); + const Course(this.name, this.icalUrl, this.title, this.scheduleId); } class MannheimCourseScraper { + const MannheimCourseScraper(); + Future> loadCourses([ - CancellationToken cancellationToken, + CancellationToken? cancellationToken, ]) async { - if (cancellationToken == null) cancellationToken = CancellationToken(); + cancellationToken ??= CancellationToken(); - var coursesPage = await _makeRequest( + final coursesPage = await _makeRequest( Uri.parse("https://vorlesungsplan.dhbw-mannheim.de/ical.php"), cancellationToken, ); - return MannheimCourseResponseParser().parseCoursePage(coursesPage.body); + return const MannheimCourseResponseParser() + .parseCoursePage(coursesPage.body); } Future _makeRequest( - Uri uri, CancellationToken cancellationToken) async { - var requestCancellationToken = http.CancellationToken(); + Uri uri, + CancellationToken cancellationToken, + ) async { + final requestCancellationToken = http.CancellationToken(); try { - cancellationToken.setCancellationCallback(() { - requestCancellationToken.cancel(); - }); + cancellationToken.cancellationCallback = requestCancellationToken.cancel; - var response = await http.HttpClientHelper.get(uri, - cancelToken: requestCancellationToken); + final response = await http.HttpClientHelper.get( + uri, + cancelToken: requestCancellationToken, + ); - if (response == null && !requestCancellationToken.isCanceled) - throw ServiceRequestFailed("Http request failed!"); + if (response == null && !requestCancellationToken.isCanceled) { + throw const ServiceRequestFailed("Http request failed!"); + } - return response; + return response!; + // ignore: avoid_catching_errors } on http.OperationCanceledError catch (_) { throw OperationCancelledException(); } catch (ex) { if (!requestCancellationToken.isCanceled) rethrow; + throw const ServiceRequestFailed("Http request failed!"); } finally { - cancellationToken.setCancellationCallback(null); + cancellationToken.cancellationCallback = null; } - - return null; } } diff --git a/lib/schedule/service/rapla/rapla_monthly_response_parser.dart b/lib/schedule/service/rapla/rapla_monthly_response_parser.dart index 5886f610..d78f6fa8 100644 --- a/lib/schedule/service/rapla/rapla_monthly_response_parser.dart +++ b/lib/schedule/service/rapla/rapla_monthly_response_parser.dart @@ -9,35 +9,37 @@ import 'package:html/dom.dart'; /// table /// class RaplaMonthlyResponseParser { + const RaplaMonthlyResponseParser(); + static ScheduleQueryResult parseMonthlyTable( Element monthTable, ) { - var title = monthTable.parent.getElementsByClassName("title"); + final title = monthTable.parent!.getElementsByClassName("title"); - var monthAndYear = title[0].text; - var yearString = monthAndYear.split(" ")[1]; - var year = int.tryParse(yearString); - var month = _monthStringToDateTime(monthAndYear); + final monthAndYear = title[0].text; + final yearString = monthAndYear.split(" ")[1]; + final year = int.tryParse(yearString); + final month = _monthStringToDateTime(monthAndYear); - var dayCells = monthTable.getElementsByClassName("month_cell"); + final dayCells = monthTable.getElementsByClassName("month_cell"); - var allEntries = []; - var parseErrors = []; + final allEntries = []; + final parseErrors = []; - for (var dayCell in dayCells) { - var dayNumber = dayCell.getElementsByTagName("div"); - var dayEntries = dayCell.getElementsByClassName("month_block"); + for (final dayCell in dayCells) { + final dayNumber = dayCell.getElementsByTagName("div"); + final dayEntries = dayCell.getElementsByClassName("month_block"); if (dayNumber.isEmpty || dayEntries.isEmpty) continue; - var dayNumberString = dayNumber[0].text; - var day = int.tryParse(dayNumberString); + final dayNumberString = dayNumber[0].text; + final day = int.tryParse(dayNumberString); - for (var dayEntry in dayEntries) { + for (final dayEntry in dayEntries) { try { - var entry = RaplaParsingUtils.extractScheduleEntryOrThrow( + final entry = RaplaParsingUtils.extractScheduleEntryOrThrow( dayEntry, - DateTime(year, month, day), + DateTime(year!, month!, day!), ); allEntries.add(entry); @@ -47,12 +49,12 @@ class RaplaMonthlyResponseParser { } } - return ScheduleQueryResult(Schedule.fromList(allEntries), parseErrors); + return ScheduleQueryResult(Schedule(entries: allEntries), parseErrors); } - static int _monthStringToDateTime(String monthAndYear) { - var monthString = monthAndYear.split(" ")[0]; - var monthNames = { + static int? _monthStringToDateTime(String monthAndYear) { + final monthString = monthAndYear.split(" ")[0]; + final monthNames = { "Januar": DateTime.january, "Februar": DateTime.february, "März": DateTime.march, @@ -67,7 +69,7 @@ class RaplaMonthlyResponseParser { "Dezember": DateTime.december, }; - var month = monthNames[monthString]; + final month = monthNames[monthString]; return month; } } diff --git a/lib/schedule/service/rapla/rapla_parsing_utils.dart b/lib/schedule/service/rapla/rapla_parsing_utils.dart index 90cc3aec..e164bb48 100644 --- a/lib/schedule/service/rapla/rapla_parsing_utils.dart +++ b/lib/schedule/service/rapla/rapla_parsing_utils.dart @@ -1,10 +1,11 @@ -import 'package:dhbwstudentapp/common/util/string_utils.dart'; import 'package:dhbwstudentapp/dualis/service/parsing/parsing_utils.dart'; import 'package:dhbwstudentapp/schedule/model/schedule_entry.dart'; import 'package:html/dom.dart'; import 'package:intl/intl.dart'; class RaplaParsingUtils { + const RaplaParsingUtils(); + static const String WEEK_BLOCK_CLASS = "week_block"; static const String TOOLTIP_CLASS = "tooltip"; static const String INFOTABLE_CLASS = "infotable"; @@ -21,32 +22,36 @@ class RaplaParsingUtils { static const Map entryTypeMapping = { "Feiertag": ScheduleEntryType.PublicHoliday, "Online-Format (ohne Raumbelegung)": ScheduleEntryType.Online, - "Vorlesung / Lehrbetrieb": ScheduleEntryType.Class, - "Lehrveranstaltung": ScheduleEntryType.Class, + "Vorlesung / Lehrbetrieb": ScheduleEntryType.Lesson, + "Lehrveranstaltung": ScheduleEntryType.Lesson, "Klausur / Prüfung": ScheduleEntryType.Exam, "Prüfung": ScheduleEntryType.Exam }; static ScheduleEntry extractScheduleEntryOrThrow( - Element value, DateTime date) { + Element value, + DateTime date, + ) { // The tooltip tag contains the most relevant information - var tooltip = value.getElementsByClassName(TOOLTIP_CLASS); + final tooltip = value.getElementsByClassName(TOOLTIP_CLASS); // The only reliable way to extract the time - var timeAndClassName = value.getElementsByTagName("a"); + final timeAndClassName = value.getElementsByTagName("a"); - if (timeAndClassName.isEmpty) + if (timeAndClassName.isEmpty) { throw ElementNotFoundParseException("time and date container"); + } - var descriptionInCell = timeAndClassName[0].text; + final descriptionInCell = timeAndClassName[0].text; - var start = _parseTime(descriptionInCell.substring(0, 5), date); - var end = _parseTime(descriptionInCell.substring(7, 12), date); + final start = _parseTime(descriptionInCell.substring(0, 5), date); + final end = _parseTime(descriptionInCell.substring(7, 12), date); - if (start == null || end == null) + if (start == null || end == null) { throw ElementNotFoundParseException("start and end date container"); + } - ScheduleEntry scheduleEntry; + ScheduleEntry? scheduleEntry; // The important information is stored in a html element called tooltip. // Depending on the Rapla configuration the tooltip is available or not. @@ -56,7 +61,11 @@ class RaplaParsingUtils { // tooltip. Then provide a link with a manual to activate it in Rapla if (tooltip.isEmpty) { scheduleEntry = extractScheduleDetailsFromCell( - timeAndClassName, scheduleEntry, start, end); + timeAndClassName, + scheduleEntry, + start, + end, + ); } else { scheduleEntry = extractScheduleFromTooltip(tooltip, value, scheduleEntry, start, end); @@ -66,14 +75,15 @@ class RaplaParsingUtils { } static ScheduleEntry improveScheduleEntry(ScheduleEntry scheduleEntry) { - if (scheduleEntry.title == null) { + if (scheduleEntry.title.isEmpty) { throw ElementNotFoundParseException("title"); } - var professor = scheduleEntry.professor; - if (professor?.endsWith(",") ?? false) { + final professor = scheduleEntry.professor; + if (professor.endsWith(",")) { scheduleEntry = scheduleEntry.copyWith( - professor: professor.substring(0, professor.length - 1)); + professor: professor.substring(0, professor.length - 1), + ); } return scheduleEntry.copyWith( @@ -85,28 +95,29 @@ class RaplaParsingUtils { } static ScheduleEntry extractScheduleFromTooltip( - List tooltip, - Element value, - ScheduleEntry scheduleEntry, - DateTime start, - DateTime end) { - var infotable = tooltip[0].getElementsByClassName(INFOTABLE_CLASS); + List tooltip, + Element value, + ScheduleEntry? scheduleEntry, + DateTime start, + DateTime end, + ) { + final infotable = tooltip[0].getElementsByClassName(INFOTABLE_CLASS); if (infotable.isEmpty) { throw ElementNotFoundParseException("infotable container"); } - Map properties = _parsePropertiesTable(infotable[0]); - var type = _extractEntryType(tooltip); - var title = properties[CLASS_NAME_LABEL] ?? + final Map properties = _parsePropertiesTable(infotable[0]); + final type = _extractEntryType(tooltip); + final title = properties[CLASS_NAME_LABEL] ?? properties[CLASS_TITLE_LABEL] ?? properties[CLASS_NAME_LABEL_ALTERNATIVE]; - var professor = properties[PROFESSOR_NAME_LABEL]; - var details = properties[DETAILS_LABEL]; - var resource = properties[RESOURCES_LABEL] ?? _extractResources(value); + final professor = properties[PROFESSOR_NAME_LABEL]; + final details = properties[DETAILS_LABEL]; + final resource = properties[RESOURCES_LABEL] ?? _extractResources(value); - scheduleEntry = ScheduleEntry( + return ScheduleEntry( start: start, end: end, title: title, @@ -115,28 +126,28 @@ class RaplaParsingUtils { type: type, room: resource, ); - return scheduleEntry; } static ScheduleEntry extractScheduleDetailsFromCell( - List timeAndClassName, - ScheduleEntry scheduleEntry, - DateTime start, - DateTime end) { - var descriptionHtml = timeAndClassName[0].innerHtml.substring(12); - var descriptionParts = descriptionHtml.split("
"); + List timeAndClassName, + ScheduleEntry? scheduleEntry, + DateTime start, + DateTime end, + ) { + final descriptionHtml = timeAndClassName[0].innerHtml.substring(12); + final descriptionParts = descriptionHtml.split("
"); var title = ""; var details = ""; if (descriptionParts.length == 1) { title = descriptionParts[0]; - } else if (descriptionParts.length > 0) { + } else if (descriptionParts.isNotEmpty) { title = descriptionParts[1]; details = descriptionParts.join("\n"); } - scheduleEntry = ScheduleEntry( + return ScheduleEntry( start: start, end: end, title: title, @@ -145,29 +156,27 @@ class RaplaParsingUtils { type: ScheduleEntryType.Unknown, room: "", ); - return scheduleEntry; } static ScheduleEntryType _extractEntryType(List tooltip) { if (tooltip.isEmpty) return ScheduleEntryType.Unknown; - var strongTag = tooltip[0].getElementsByTagName("strong"); + final strongTag = tooltip[0].getElementsByTagName("strong"); if (strongTag.isEmpty) return ScheduleEntryType.Unknown; - var typeString = strongTag[0].innerHtml; + final typeString = strongTag[0].innerHtml; - var type = ScheduleEntryType.Unknown; if (entryTypeMapping.containsKey(typeString)) { - type = entryTypeMapping[typeString]; + return entryTypeMapping[typeString]!; + } else { + return ScheduleEntryType.Unknown; } - - return type; } static Map _parsePropertiesTable(Element infotable) { - var map = {}; - var labels = infotable.getElementsByClassName(LABEL_CLASS); - var values = infotable.getElementsByClassName(VALUE_CLASS); + final map = {}; + final labels = infotable.getElementsByClassName(LABEL_CLASS); + final values = infotable.getElementsByClassName(VALUE_CLASS); for (var i = 0; i < labels.length; i++) { map[labels[i].innerHtml] = values[i].innerHtml; @@ -175,9 +184,9 @@ class RaplaParsingUtils { return map; } - static DateTime _parseTime(String timeString, DateTime date) { + static DateTime? _parseTime(String timeString, DateTime date) { try { - var time = DateFormat("HH:mm").parse(timeString.substring(0, 5)); + final time = DateFormat("HH:mm").parse(timeString.substring(0, 5)); return DateTime(date.year, date.month, date.day, time.hour, time.minute); } catch (e) { return null; @@ -185,28 +194,31 @@ class RaplaParsingUtils { } static String _extractResources(Element value) { - var resources = value.getElementsByClassName(RESOURCE_CLASS); + final resources = value.getElementsByClassName(RESOURCE_CLASS); - var resourcesList = []; - for (var resource in resources) { + final resourcesList = []; + for (final resource in resources) { resourcesList.add(resource.innerHtml); } - return concatStringList(resourcesList, ", "); + final buffer = StringBuffer(); + buffer.writeAll(resourcesList, ", "); + + return buffer.toString(); } static String readYearOrThrow(Document document) { // The only reliable way to read the year of this schedule is to parse the // selected year in the date selector - var comboBoxes = document.getElementsByTagName("select"); + final comboBoxes = document.getElementsByTagName("select"); - String year; - for (var box in comboBoxes) { + String? year; + for (final box in comboBoxes) { if (box.attributes.containsKey("name") && box.attributes["name"] == "year") { - var entries = box.getElementsByTagName("option"); + final entries = box.getElementsByTagName("option"); - for (var entry in entries) { + for (final entry in entries) { if (entry.attributes.containsKey("selected") && entry.attributes["selected"] == "") { year = entry.text; diff --git a/lib/schedule/service/rapla/rapla_response_parser.dart b/lib/schedule/service/rapla/rapla_response_parser.dart index f0a7e353..bf5cab4b 100644 --- a/lib/schedule/service/rapla/rapla_response_parser.dart +++ b/lib/schedule/service/rapla/rapla_response_parser.dart @@ -8,11 +8,13 @@ import 'package:html/parser.dart' show parse; /// Parsing implementation which parses the response of the rapla schedule source. /// class RaplaResponseParser { + RaplaResponseParser(); + ScheduleQueryResult parseSchedule(String responseBody) { - var document = parse(responseBody); + final document = parse(responseBody); - var weekTable = document.getElementsByClassName("week_table"); - var monthTable = document.getElementsByClassName("month_table"); + final weekTable = document.getElementsByClassName("week_table"); + final monthTable = document.getElementsByClassName("month_table"); if (weekTable.isNotEmpty) { return RaplaWeekResponseParser.parseWeeklyTable( @@ -26,8 +28,8 @@ class RaplaResponseParser { ); } - return ScheduleQueryResult(Schedule(), [ - ParseError("Did not find a week_table and month_table class", null), + return ScheduleQueryResult(const Schedule(), [ + ParseError("Did not find a week_table and month_table class"), ]); } } diff --git a/lib/schedule/service/rapla/rapla_schedule_source.dart b/lib/schedule/service/rapla/rapla_schedule_source.dart index fac25cf4..f1abd688 100644 --- a/lib/schedule/service/rapla/rapla_schedule_source.dart +++ b/lib/schedule/service/rapla/rapla_schedule_source.dart @@ -13,34 +13,36 @@ class RaplaScheduleSource extends ScheduleSource { String raplaUrl; - RaplaScheduleSource({this.raplaUrl}); - - void setEndpointUrl(String url) { - raplaUrl = url; - } + RaplaScheduleSource({required this.raplaUrl}); @override - Future querySchedule(DateTime from, DateTime to, - [CancellationToken cancellationToken]) async { - DateTime current = toDayOfWeek(from, DateTime.monday); + Future querySchedule( + DateTime? from, + DateTime? to, [ + CancellationToken? cancellationToken, + ]) async { + DateTime current = toDayOfWeek(from, DateTime.monday)!; - if (cancellationToken == null) cancellationToken = CancellationToken(); + cancellationToken ??= CancellationToken(); - var schedule = Schedule(); - var allErrors = []; + var schedule = const Schedule(); + final allErrors = []; var didChangeMonth = false; - while ((to.isAfter(current) && !cancellationToken.isCancelled()) || + while ((to!.isAfter(current) && !cancellationToken.isCancelled) || didChangeMonth) { try { - var weekSchedule = await _fetchRaplaSource(current, cancellationToken); + final weekSchedule = + await _fetchRaplaSource(current, cancellationToken); - if (weekSchedule.schedule != null) { - schedule.merge(weekSchedule.schedule); + if (weekSchedule?.schedule != null) { + schedule.merge(weekSchedule!.schedule); } - allErrors.addAll(weekSchedule.errors ?? []); + if (weekSchedule != null) { + allErrors.addAll(weekSchedule.errors); + } } on OperationCancelledException { rethrow; } on ParseException catch (ex, trace) { @@ -49,9 +51,9 @@ class RaplaScheduleSource extends ScheduleSource { throw ScheduleQueryFailedException(e, trace); } - var currentMonth = current.month; - current = toNextWeek(current); - var nextMonth = current.month; + final currentMonth = current.month; + current = toNextWeek(current)!; + final nextMonth = current.month; // Some rapla instances only return the dates in the current month. // If the month changes in the middle of a week only half the week is @@ -60,28 +62,26 @@ class RaplaScheduleSource extends ScheduleSource { didChangeMonth = currentMonth != nextMonth; } - if (cancellationToken.isCancelled()) throw OperationCancelledException(); + cancellationToken.throwIfCancelled(); schedule = schedule.trim(from, to); return ScheduleQueryResult(schedule, allErrors); } - Future _fetchRaplaSource( + Future _fetchRaplaSource( DateTime date, CancellationToken cancellationToken, ) async { - var requestUri = _buildRequestUri(date); + final requestUri = _buildRequestUri(date); - var response = await _makeRequest(requestUri, cancellationToken); + final response = await _makeRequest(requestUri, cancellationToken); if (response == null) return null; try { - var schedule = responseParser.parseSchedule(response.body); + final schedule = responseParser.parseSchedule(response.body); - if (schedule?.schedule?.urls != null) { - schedule.schedule.urls.add(requestUri.toString()); - } + schedule.schedule.urls.add(requestUri.toString()); return schedule; } on ParseException catch (_) { @@ -104,17 +104,18 @@ class RaplaScheduleSource extends ScheduleSource { raplaUrl = "http://$raplaUrl"; } - var uri = Uri.parse(raplaUrl); + final uri = Uri.parse(raplaUrl); - bool hasKeyParameter = uri.queryParameters.containsKey("key"); - bool hasUserParameter = uri.queryParameters.containsKey("user"); - bool hasFileParameter = uri.queryParameters.containsKey("file"); - bool hasPageParameter = uri.queryParameters.containsKey("page"); + final bool hasKeyParameter = uri.queryParameters.containsKey("key"); + final bool hasUserParameter = uri.queryParameters.containsKey("user"); + final bool hasFileParameter = uri.queryParameters.containsKey("file"); + final bool hasPageParameter = uri.queryParameters.containsKey("page"); - bool hasAllocatableId = uri.queryParameters.containsKey("allocatable_id"); - bool hasSalt = uri.queryParameters.containsKey("salt"); + final bool hasAllocatableId = + uri.queryParameters.containsKey("allocatable_id"); + final bool hasSalt = uri.queryParameters.containsKey("salt"); - Map parameters = {}; + final Map parameters = {}; if (hasKeyParameter) { parameters["key"] = uri.queryParameters["key"]; @@ -140,28 +141,32 @@ class RaplaScheduleSource extends ScheduleSource { } } - Future _makeRequest( - Uri uri, CancellationToken cancellationToken) async { - var requestCancellationToken = http.CancellationToken(); + Future _makeRequest( + Uri uri, + CancellationToken cancellationToken, + ) async { + final requestCancellationToken = http.CancellationToken(); try { - cancellationToken.setCancellationCallback(() { - requestCancellationToken.cancel(); - }); + cancellationToken.cancellationCallback = requestCancellationToken.cancel; - var response = await http.HttpClientHelper.get(uri, - cancelToken: requestCancellationToken); + final response = await http.HttpClientHelper.get( + uri, + cancelToken: requestCancellationToken, + ); - if (response == null && !requestCancellationToken.isCanceled) - throw ServiceRequestFailed("Http request failed!"); + if (response == null && !requestCancellationToken.isCanceled) { + throw const ServiceRequestFailed("Http request failed!"); + } return response; + // ignore: avoid_catching_errors } on http.OperationCanceledError catch (_) { throw OperationCancelledException(); } catch (ex) { if (!requestCancellationToken.isCanceled) rethrow; } finally { - cancellationToken.setCancellationCallback(null); + cancellationToken.cancellationCallback = null; } return null; @@ -181,26 +186,25 @@ class RaplaScheduleSource extends ScheduleSource { return false; } - if (uri != null) { - bool hasKeyParameter = uri.queryParameters.containsKey("key"); - bool hasUserParameter = uri.queryParameters.containsKey("user"); - bool hasFileParameter = uri.queryParameters.containsKey("file"); - bool hasPageParameter = uri.queryParameters.containsKey("page"); + final bool hasKeyParameter = uri.queryParameters.containsKey("key"); + final bool hasUserParameter = uri.queryParameters.containsKey("user"); + final bool hasFileParameter = uri.queryParameters.containsKey("file"); + final bool hasPageParameter = uri.queryParameters.containsKey("page"); - bool hasAllocatableId = uri.queryParameters.containsKey("allocatable_id"); - bool hasSalt = uri.queryParameters.containsKey("salt"); + final bool hasAllocatableId = + uri.queryParameters.containsKey("allocatable_id"); + final bool hasSalt = uri.queryParameters.containsKey("salt"); - if (hasUserParameter && hasFileParameter && hasPageParameter) { - return true; - } + if (hasUserParameter && hasFileParameter && hasPageParameter) { + return true; + } - if (hasSalt && hasAllocatableId && hasKeyParameter) { - return true; - } + if (hasSalt && hasAllocatableId && hasKeyParameter) { + return true; + } - if (hasKeyParameter) { - return true; - } + if (hasKeyParameter) { + return true; } return false; @@ -213,24 +217,25 @@ enum FailureReason { ParseError, } +// TODO: [Leptopoda] make enum class ScheduleOrFailure { final FailureReason reason; - final Schedule schedule; - final Object exception; - final StackTrace trace; + final Schedule? schedule; + final Object? exception; + final StackTrace? trace; bool get success => reason == FailureReason.Success; - ScheduleOrFailure.success(this.schedule) + const ScheduleOrFailure.success(this.schedule) : reason = FailureReason.Success, exception = null, trace = null; - ScheduleOrFailure.failParseError(this.exception, this.trace) + const ScheduleOrFailure.failParseError(this.exception, this.trace) : reason = FailureReason.ParseError, schedule = null; - ScheduleOrFailure.failRequestError(this.exception, this.trace) + const ScheduleOrFailure.failRequestError(this.exception, this.trace) : reason = FailureReason.RequestError, schedule = null; } diff --git a/lib/schedule/service/rapla/rapla_several_months_response_parser.dart b/lib/schedule/service/rapla/rapla_several_months_response_parser.dart index dad3b018..6d427a8d 100644 --- a/lib/schedule/service/rapla/rapla_several_months_response_parser.dart +++ b/lib/schedule/service/rapla/rapla_several_months_response_parser.dart @@ -5,23 +5,22 @@ import 'package:dhbwstudentapp/schedule/service/rapla/rapla_monthly_response_par import 'package:html/dom.dart'; class RaplaSeveralMonthsResponseParser { + const RaplaSeveralMonthsResponseParser(); + static ScheduleQueryResult parseSeveralMonthlyTables( Document document, List monthTables, ) { + final parseErrors = []; + final allEntries = []; - var parseErrors = []; - var allEntries = []; - - for(var monthTable in monthTables) { - var result = RaplaMonthlyResponseParser.parseMonthlyTable( - monthTable - ); + for (final monthTable in monthTables) { + final result = RaplaMonthlyResponseParser.parseMonthlyTable(monthTable); parseErrors.addAll(result.errors); allEntries.addAll(result.schedule.entries); } - return ScheduleQueryResult(Schedule.fromList(allEntries), parseErrors); + return ScheduleQueryResult(Schedule(entries: allEntries), parseErrors); } } diff --git a/lib/schedule/service/rapla/rapla_week_response_parser.dart b/lib/schedule/service/rapla/rapla_week_response_parser.dart index 6d27a811..54de3087 100644 --- a/lib/schedule/service/rapla/rapla_week_response_parser.dart +++ b/lib/schedule/service/rapla/rapla_week_response_parser.dart @@ -11,19 +11,21 @@ import 'package:intl/intl.dart'; /// table /// class RaplaWeekResponseParser { + const RaplaWeekResponseParser(); + static ScheduleQueryResult parseWeeklyTable( Document document, Element weekTable, ) { - var dates = _readDatesFromHeadersOrThrow(document); - var allRows = weekTable.getElementsByTagName("tr"); + final dates = _readDatesFromHeadersOrThrow(document); + final allRows = weekTable.getElementsByTagName("tr"); - var allEntries = []; - var parseErrors = []; + final allEntries = []; + final parseErrors = []; - for (var row in allRows) { + for (final row in allRows) { var currentDayInWeekIndex = 0; - for (var cell in row.children) { + for (final cell in row.children) { if (cell.localName != "td") continue; // Skip all spacer cells. They are only used for the alignment in the html page @@ -49,7 +51,7 @@ class RaplaWeekResponseParser { // The important information is inside a week_block cell if (cell.classes.contains("week_block")) { try { - var entry = RaplaParsingUtils.extractScheduleEntryOrThrow( + final entry = RaplaParsingUtils.extractScheduleEntryOrThrow( cell, dates[currentDayInWeekIndex], ); @@ -63,30 +65,30 @@ class RaplaWeekResponseParser { } allEntries.sort( - (ScheduleEntry e1, ScheduleEntry e2) => e1?.start?.compareTo(e2?.start), + (ScheduleEntry e1, ScheduleEntry e2) => e1.start.compareTo(e2.start), ); return ScheduleQueryResult( - Schedule.fromList(allEntries), + Schedule(entries: allEntries), parseErrors, ); } static List _readDatesFromHeadersOrThrow(Document document) { - var year = _readYearOrThrow(document); + final year = _readYearOrThrow(document); // The only reliable way to read the dates is the table header. // Some schedule entries contain the dates in the description but not // in every case. - var weekHeaders = document.getElementsByClassName("week_header"); - var dates = []; + final weekHeaders = document.getElementsByClassName("week_header"); + final dates = []; - for (var header in weekHeaders) { + for (final header in weekHeaders) { var dateString = header.text + year; dateString = dateString.replaceAll(RegExp(r"\s+\b|\b\s"), ""); try { - var date = DateFormat("dd.MM.yyyy").parse(dateString.substring(2)); + final date = DateFormat("dd.MM.yyyy").parse(dateString.substring(2)); dates.add(date); } catch (exception, trace) { throw ParseException.withInner(exception, trace); @@ -98,15 +100,15 @@ class RaplaWeekResponseParser { static String _readYearOrThrow(Document document) { // The only reliable way to read the year of this schedule is to parse the // selected year in the date selector - var comboBoxes = document.getElementsByTagName("select"); + final comboBoxes = document.getElementsByTagName("select"); - String year; - for (var box in comboBoxes) { + String? year; + for (final box in comboBoxes) { if (box.attributes.containsKey("name") && box.attributes["name"] == "year") { - var entries = box.getElementsByTagName("option"); + final entries = box.getElementsByTagName("option"); - for (var entry in entries) { + for (final entry in entries) { if (entry.attributes.containsKey("selected") && (entry.attributes["selected"] == "" || entry.attributes["selected"] == "selected")) { diff --git a/lib/schedule/service/schedule_prettifier.dart b/lib/schedule/service/schedule_prettifier.dart index a5f9d00e..75752cb7 100644 --- a/lib/schedule/service/schedule_prettifier.dart +++ b/lib/schedule/service/schedule_prettifier.dart @@ -2,13 +2,17 @@ import 'package:dhbwstudentapp/schedule/model/schedule.dart'; import 'package:dhbwstudentapp/schedule/model/schedule_entry.dart'; class SchedulePrettifier { - final RegExp onlinePrefixRegExp = RegExp(r'\(?online\)?([ -]*)', caseSensitive: false); - final RegExp onlineSuffixRegExp = RegExp(r'([ -]*)\(?online\)?', caseSensitive: false); + SchedulePrettifier(); + + final RegExp onlinePrefixRegExp = + RegExp(r'\(?online\)?([ -]*)', caseSensitive: false); + final RegExp onlineSuffixRegExp = + RegExp(r'([ -]*)\(?online\)?', caseSensitive: false); Schedule prettifySchedule(Schedule schedule) { - var allEntries = []; + final allEntries = []; - for (var entry in schedule.entries) { + for (final entry in schedule.entries) { allEntries.add(prettifyScheduleEntry(entry)); } @@ -35,26 +39,24 @@ class SchedulePrettifier { return entry; } - var type = ScheduleEntryType.Online; + const type = ScheduleEntryType.Online; return entry.copyWith(title: newTitle, type: type); } ScheduleEntry _removeCourseFromTitle(ScheduleEntry entry) { - var title = entry.title ?? ""; - var details = entry.details ?? ""; + var title = entry.title; + var details = entry.details; - var titleRegex = - RegExp("[A-Z]{3,}-?[A-Z]+[0-9]*[A-Z]*[0-9]*[\/]?[A-Z]*[0-9]*[ ]*-?"); - var match = titleRegex.firstMatch(entry.title); + final titleRegex = + RegExp("[A-Z]{3,}-?[A-Z]+[0-9]*[A-Z]*[0-9]*[/]?[A-Z]*[0-9]*[ ]*-?"); + final match = titleRegex.firstMatch(entry.title); if (match != null && match.start == 0) { - details = title.substring(0, match.end) + " - $details"; + details = "${title.substring(0, match.end)} - $details"; title = title.substring(match.end).trim(); } else { - var first = title - .split(" ") - .first; + final first = title.split(" ").first; // Prettify titles: T3MB9025 Fluidmechanik -> Fluidmechanik @@ -62,14 +64,12 @@ class SchedulePrettifier { // or less than 2 charcters long if (!(first == first.toUpperCase() && first.length >= 3)) return entry; - var numberCount = first - .split(new RegExp("[0-9]")) - .length; + final numberCount = first.split(RegExp("[0-9]")).length; // If there are less thant two numbers in the title, do not prettify it if (numberCount < 2) return entry; - details = title.substring(0, first.length) + " - $details"; + details = "${title.substring(0, first.length)} - $details"; title = title.substring(first.length).trim(); } diff --git a/lib/schedule/service/schedule_source.dart b/lib/schedule/service/schedule_source.dart index 87f08c7c..d09e33b9 100644 --- a/lib/schedule/service/schedule_source.dart +++ b/lib/schedule/service/schedule_source.dart @@ -2,6 +2,8 @@ import 'package:dhbwstudentapp/common/util/cancellation_token.dart'; import 'package:dhbwstudentapp/schedule/model/schedule_query_result.dart'; abstract class ScheduleSource { + const ScheduleSource(); + /// /// Queries the schedule from the implemented service. The resulting schedule /// contains all entries between the `from` and `to` date. @@ -9,28 +11,31 @@ abstract class ScheduleSource { /// Returns a future which gives the updated schedule or throws an exception /// if an error happened or the operation was cancelled /// - Future querySchedule(DateTime from, DateTime to, - [CancellationToken cancellationToken]); + Future querySchedule( + DateTime? from, + DateTime? to, [ + CancellationToken? cancellationToken, + ]); bool canQuery(); } class ScheduleQueryFailedException implements Exception { final dynamic innerException; - final StackTrace trace; + final StackTrace? trace; - ScheduleQueryFailedException(this.innerException, [this.trace]); + const ScheduleQueryFailedException(this.innerException, [this.trace]); @override String toString() { - return (innerException?.toString() ?? "") + "\n" + trace?.toString(); + return "${innerException.toString()}\n${trace?.toString() ?? ""}"; } } class ServiceRequestFailed implements Exception { final String message; - ServiceRequestFailed(this.message); + const ServiceRequestFailed(this.message); @override String toString() { diff --git a/lib/schedule/ui/dailyschedule/daily_schedule_page.dart b/lib/schedule/ui/dailyschedule/daily_schedule_page.dart index 8f4342e2..7f64a5e3 100644 --- a/lib/schedule/ui/dailyschedule/daily_schedule_page.dart +++ b/lib/schedule/ui/dailyschedule/daily_schedule_page.dart @@ -1,26 +1,23 @@ +import 'package:dhbwstudentapp/assets.dart'; import 'package:dhbwstudentapp/common/i18n/localizations.dart'; -import 'package:dhbwstudentapp/common/ui/viewmodels/base_view_model.dart'; -import 'package:dhbwstudentapp/common/ui/text_styles.dart'; import 'package:dhbwstudentapp/schedule/ui/dailyschedule/widgets/current_time_indicator_widget.dart'; import 'package:dhbwstudentapp/schedule/ui/dailyschedule/widgets/daily_schedule_entry_widget.dart'; import 'package:dhbwstudentapp/schedule/ui/viewmodels/daily_schedule_view_model.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; import 'package:property_change_notifier/property_change_notifier.dart'; import 'package:provider/provider.dart'; -class DailySchedulePage extends StatefulWidget { - @override - _DailySchedulePageState createState() => _DailySchedulePageState(); -} - -class _DailySchedulePageState extends State { - DailyScheduleViewModel viewModel; +class DailySchedulePage extends StatelessWidget { + const DailySchedulePage({super.key}); @override Widget build(BuildContext context) { - viewModel = Provider.of(context); + final viewModel = Provider.of(context); + + final textTheme = Theme.of(context).textTheme; + final dailyScheduleEntryTitle = + textTheme.headline4?.copyWith(color: textTheme.headline5?.color); return PropertyChangeProvider( value: viewModel, @@ -29,58 +26,60 @@ class _DailySchedulePageState extends State { padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(0, 0, 0, 16), child: PropertyChangeConsumer( - builder: (BuildContext context, DailyScheduleViewModel model, - Set properties) { - var dateFormat = DateFormat.yMMMMEEEEd( - L.of(context).locale.languageCode); + builder: ( + BuildContext context, + DailyScheduleViewModel? model, + Set? properties, + ) { + final dateFormat = DateFormat.yMMMMEEEEd( + L.of(context).locale.languageCode, + ); return Text( - dateFormat.format(model.currentDate), - style: textStyleDailyScheduleCurrentDate(context), + dateFormat.format(model!.currentDate!), + style: dailyScheduleEntryTitle, ); }, ), ), - (viewModel.daySchedule?.entries?.length ?? 0) == 0 - ? Padding( - padding: const EdgeInsets.fromLTRB(0, 32, 0, 0), - child: Column( + if (viewModel.schedule.entries.isEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(0, 32, 0, 0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded(flex: 1, child: Container()), - Expanded( - flex: 8, - child: Text( - L.of(context).dailyScheduleNoEntriesToday, - softWrap: true, - textAlign: TextAlign.center, - style: - textStyleDailyScheduleNoEntries(context), - ), - ), - Expanded(flex: 1, child: Container()), - ], - ), - Padding( - padding: const EdgeInsets.fromLTRB(32, 64, 32, 32), - child: Opacity( - child: Image.asset("assets/empty_state.png"), - opacity: 0.9, + Expanded(child: Container()), + Expanded( + flex: 8, + child: Text( + L.of(context).dailyScheduleNoEntriesToday, + softWrap: true, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headline5, ), - ) + ), + Expanded(child: Container()), ], ), - ) - : Column( - children: buildEntryWidgets(), - ) + Padding( + padding: const EdgeInsets.fromLTRB(32, 64, 32, 32), + child: Opacity( + opacity: 0.9, + child: Image.asset(Assets.assets_empty_state_png), + ), + ) + ], + ), + ) + else + Column( + children: buildEntryWidgets(viewModel), + ) ], ), ), @@ -88,14 +87,14 @@ class _DailySchedulePageState extends State { ); } - List buildEntryWidgets() { - var entryWidgets = []; - var now = DateTime.now(); + List buildEntryWidgets(DailyScheduleViewModel viewModel) { + final entryWidgets = []; + final now = DateTime.now(); var nowIndicatorInserted = false; - for (var entry in viewModel.daySchedule?.entries) { + for (final entry in viewModel.schedule.entries) { if (!nowIndicatorInserted && (entry.end.isAfter(now))) { - entryWidgets.add(CurrentTimeIndicatorWidget()); + entryWidgets.add(const CurrentTimeIndicatorWidget()); nowIndicatorInserted = true; } @@ -108,7 +107,9 @@ class _DailySchedulePageState extends State { ), ); } - if (!nowIndicatorInserted) entryWidgets.add(CurrentTimeIndicatorWidget()); + if (!nowIndicatorInserted) { + entryWidgets.add(const CurrentTimeIndicatorWidget()); + } return entryWidgets; } } diff --git a/lib/schedule/ui/dailyschedule/widgets/current_time_indicator_widget.dart b/lib/schedule/ui/dailyschedule/widgets/current_time_indicator_widget.dart index 902507bd..74141dd7 100644 --- a/lib/schedule/ui/dailyschedule/widgets/current_time_indicator_widget.dart +++ b/lib/schedule/ui/dailyschedule/widgets/current_time_indicator_widget.dart @@ -1,23 +1,27 @@ -import 'package:dhbwstudentapp/common/ui/colors.dart'; +import 'package:dhbwstudentapp/common/ui/schedule_theme.dart'; import 'package:flutter/material.dart'; class CurrentTimeIndicatorWidget extends StatelessWidget { + const CurrentTimeIndicatorWidget({super.key}); + @override Widget build(BuildContext context) { + final scheduleTheme = Theme.of(context).extension()!; + return Row( children: [ Container( height: 10, width: 10, decoration: BoxDecoration( - color: colorCurrentTimeIndicator(context), + color: scheduleTheme.currentTimeIndicator, shape: BoxShape.circle, ), ), Expanded( child: Container( height: 1, - color: colorCurrentTimeIndicator(context), + color: scheduleTheme.currentTimeIndicator, ), ) ], diff --git a/lib/schedule/ui/dailyschedule/widgets/daily_schedule_entry_widget.dart b/lib/schedule/ui/dailyschedule/widgets/daily_schedule_entry_widget.dart index 9c0fe1e1..15068a23 100644 --- a/lib/schedule/ui/dailyschedule/widgets/daily_schedule_entry_widget.dart +++ b/lib/schedule/ui/dailyschedule/widgets/daily_schedule_entry_widget.dart @@ -1,7 +1,7 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; -import 'package:dhbwstudentapp/common/ui/colors.dart'; +import 'package:dhbwstudentapp/common/ui/app_theme.dart'; import 'package:dhbwstudentapp/common/ui/schedule_entry_type_mappings.dart'; -import 'package:dhbwstudentapp/common/ui/text_styles.dart'; +import 'package:dhbwstudentapp/common/ui/text_theme.dart'; import 'package:dhbwstudentapp/schedule/model/schedule_entry.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -9,22 +9,22 @@ import 'package:intl/intl.dart'; class DailyScheduleEntryWidget extends StatelessWidget { final ScheduleEntry scheduleEntry; - const DailyScheduleEntryWidget({Key key, this.scheduleEntry}) - : super(key: key); + const DailyScheduleEntryWidget({super.key, required this.scheduleEntry}); @override Widget build(BuildContext context) { - if (scheduleEntry == null) return Container(); + final timeFormatter = DateFormat.Hm(L.of(context).locale.languageCode); - var timeFormatter = DateFormat.Hm(L.of(context).locale.languageCode); + final startTime = timeFormatter.format(scheduleEntry.start); + final endTime = timeFormatter.format(scheduleEntry.end); - var startTime = timeFormatter.format(scheduleEntry.start); - var endTime = timeFormatter.format(scheduleEntry.end); + final textTheme = Theme.of(context).textTheme; + final customTextThme = Theme.of(context).extension()!; + final dailyScheduleEntryTitle = + textTheme.headline4?.copyWith(color: textTheme.headline6?.color); return IntrinsicHeight( child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( @@ -32,24 +32,22 @@ class DailyScheduleEntryWidget extends StatelessWidget { child: Padding( padding: const EdgeInsets.fromLTRB(0, 8, 8, 8), child: Column( - crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( startTime, - style: textStyleDailyScheduleEntryWidgetTimeStart(context), + style: textTheme.headline5 + ?.merge(customTextThme.dailyScheduleEntryTimeStart), ), Expanded( - child: Padding( + child: Container( padding: const EdgeInsets.all(4), - child: Container( - width: 1, - color: colorDailyScheduleTimeVerticalConnector(), - ), + width: 1, + color: AppTheme.dailyScheduleTimeVerticalConnector, ), ), Text( endTime, - style: textStyleDailyScheduleEntryWidgetTimeEnd(context), + style: Theme.of(context).textTheme.subtitle2, ), ], ), @@ -58,20 +56,19 @@ class DailyScheduleEntryWidget extends StatelessWidget { Expanded( flex: 7, child: Card( - margin: const EdgeInsets.all(0), + margin: EdgeInsets.zero, elevation: 8, - color: scheduleEntryTypeToColor(context, scheduleEntry.type), + color: scheduleEntry.type.color(context), child: Padding( padding: const EdgeInsets.all(8), child: Column( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), child: Text( scheduleEntry.title, - style: textStyleDailyScheduleEntryWidgetTitle(context), + style: dailyScheduleEntryTitle, ), ), Row( @@ -81,14 +78,16 @@ class DailyScheduleEntryWidget extends StatelessWidget { children: [ Text( scheduleEntry.professor, - style: textStyleDailyScheduleEntryWidgetProfessor( - context), + style: Theme.of(context).textTheme.subtitle2, ), Text( scheduleEntryTypeToReadableString( - context, scheduleEntry.type), - style: textStyleDailyScheduleEntryWidgetType( - context), + context, + scheduleEntry.type, + ), + style: textTheme.bodyText2?.merge( + customTextThme.dailyScheduleEntryType, + ), ), ], ), @@ -96,7 +95,7 @@ class DailyScheduleEntryWidget extends StatelessWidget { child: Padding( padding: const EdgeInsets.fromLTRB(32, 0, 0, 0), child: Text( - scheduleEntry.room ?? "", + scheduleEntry.room, softWrap: true, textAlign: TextAlign.end, ), diff --git a/lib/schedule/ui/notification/next_day_information_notification.dart b/lib/schedule/ui/notification/next_day_information_notification.dart index d70ad2ce..54cceeb5 100644 --- a/lib/schedule/ui/notification/next_day_information_notification.dart +++ b/lib/schedule/ui/notification/next_day_information_notification.dart @@ -16,7 +16,7 @@ class NextDayInformationNotification extends TaskCallback { final WorkSchedulerService _scheduler; final L _localization; - NextDayInformationNotification( + const NextDayInformationNotification( this._notificationApi, this._scheduleEntryRepository, this._scheduler, @@ -32,21 +32,21 @@ class NextDayInformationNotification extends TaskCallback { return; } - var now = DateTime.now(); + final now = DateTime.now(); - var nextScheduleEntry = + final nextScheduleEntry = await _scheduleEntryRepository.queryNextScheduleEntry(now); if (nextScheduleEntry == null) return; - var format = DateFormat.Hm(); - var daysToNextEntry = toStartOfDay(nextScheduleEntry.start) - .difference(toStartOfDay(now)) + final format = DateFormat.Hm(); + final daysToNextEntry = toStartOfDay(nextScheduleEntry.start)! + .difference(toStartOfDay(now)!) .inDays; if (daysToNextEntry > 1) return; - var message = _getNotificationMessage( + final message = _getNotificationMessage( daysToNextEntry, nextScheduleEntry, format, @@ -58,44 +58,50 @@ class NextDayInformationNotification extends TaskCallback { ); } - String _getNotificationMessage( - int daysToNextEntry, ScheduleEntry nextScheduleEntry, DateFormat format) { - String message; - if (daysToNextEntry == 0) { - message = interpolate( - _localization.notificationNextClassNextClassAtMessage, - [ - nextScheduleEntry.title, - format.format(nextScheduleEntry.start), - ], - ); - } else if (daysToNextEntry == 1) { - message = interpolate( - _localization.notificationNextClassTomorrow, - [ - nextScheduleEntry.title, - format.format(nextScheduleEntry.start), - ], - ); + String? _getNotificationMessage( + int daysToNextEntry, + ScheduleEntry nextScheduleEntry, + DateFormat format, + ) { + switch (daysToNextEntry) { + case 0: + return interpolate( + _localization.notificationNextClassNextClassAtMessage, + [ + nextScheduleEntry.title, + format.format(nextScheduleEntry.start), + ], + ); + + case 1: + return interpolate( + _localization.notificationNextClassTomorrow, + [ + nextScheduleEntry.title, + format.format(nextScheduleEntry.start), + ], + ); + + default: + return null; } - return message; } @override Future schedule() async { - var nextSchedule = toTimeOfDayInFuture(DateTime.now(), 20, 00); + final nextSchedule = toTimeOfDayInFuture(DateTime.now(), 20, 00); await _scheduler.scheduleOneShotTaskAt( nextSchedule, - "NextDayInformationNotification" + DateFormat.yMd().format(nextSchedule), + "NextDayInformationNotification${DateFormat.yMd().format(nextSchedule)}", "NextDayInformationNotification", ); } @override Future cancel() async { - var nextSchedule = toTimeOfDayInFuture(DateTime.now(), 20, 00); + final nextSchedule = toTimeOfDayInFuture(DateTime.now(), 20, 00); await _scheduler.cancelTask( - "NextDayInformationNotification" + DateFormat.yMd().format(nextSchedule), + "NextDayInformationNotification${DateFormat.yMd().format(nextSchedule)}", ); } diff --git a/lib/schedule/ui/notification/schedule_changed_notification.dart b/lib/schedule/ui/notification/schedule_changed_notification.dart index 7c6443eb..f7f2b0d6 100644 --- a/lib/schedule/ui/notification/schedule_changed_notification.dart +++ b/lib/schedule/ui/notification/schedule_changed_notification.dart @@ -8,7 +8,7 @@ class ScheduleChangedNotification { final NotificationApi notificationApi; final L _localization; - ScheduleChangedNotification(this.notificationApi, this._localization); + const ScheduleChangedNotification(this.notificationApi, this._localization); void showNotification(ScheduleDiff scheduleDiff) { showEntriesAddedNotifications(scheduleDiff); @@ -21,8 +21,8 @@ class ScheduleChangedNotification { return; } - for (var entry in scheduleDiff.updatedEntries) { - var message = interpolate( + for (final entry in scheduleDiff.updatedEntries) { + final message = interpolate( _localization.notificationScheduleChangedClass, [ entry.entry.title, @@ -42,8 +42,8 @@ class ScheduleChangedNotification { return; } - for (var entry in scheduleDiff.removedEntries) { - var message = interpolate( + for (final entry in scheduleDiff.removedEntries) { + final message = interpolate( _localization.notificationScheduleChangedRemovedClass, [ entry.title, @@ -64,8 +64,8 @@ class ScheduleChangedNotification { return; } - for (var entry in scheduleDiff.addedEntries) { - var message = interpolate( + for (final entry in scheduleDiff.addedEntries) { + final message = interpolate( _localization.notificationScheduleChangedNewClass, [ entry.title, diff --git a/lib/schedule/ui/schedule_navigation_entry.dart b/lib/schedule/ui/schedule_navigation_entry.dart index 096dc47f..0f77d817 100644 --- a/lib/schedule/ui/schedule_navigation_entry.dart +++ b/lib/schedule/ui/schedule_navigation_entry.dart @@ -1,5 +1,4 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; -import 'package:dhbwstudentapp/common/ui/viewmodels/base_view_model.dart'; import 'package:dhbwstudentapp/schedule/ui/schedule_page.dart'; import 'package:dhbwstudentapp/schedule/ui/viewmodels/schedule_view_model.dart'; import 'package:dhbwstudentapp/schedule/ui/weeklyschedule/filter/schedule_filter_page.dart'; @@ -9,23 +8,17 @@ import 'package:flutter/material.dart'; import 'package:kiwi/kiwi.dart'; import 'package:property_change_notifier/property_change_notifier.dart'; -class ScheduleNavigationEntry extends NavigationEntry { - ScheduleViewModel _viewModel; +class ScheduleNavigationEntry extends NavigationEntry { + ScheduleNavigationEntry(); @override - Widget icon(BuildContext context) { - return Icon(Icons.calendar_today); - } + Icon icon = const Icon(Icons.calendar_today); @override - BaseViewModel initViewModel() { - if (_viewModel != null) return _viewModel; - - _viewModel = ScheduleViewModel( + ScheduleViewModel initViewModel() { + return ScheduleViewModel( KiwiContainer().resolve(), ); - - return _viewModel; } @override @@ -35,40 +28,42 @@ class ScheduleNavigationEntry extends NavigationEntry { @override Widget build(BuildContext context) { - return SchedulePage(); + return const SchedulePage(); } @override List appBarActions(BuildContext context) { - initViewModel(); return [ PropertyChangeProvider( - value: _viewModel, + value: model, child: PropertyChangeConsumer( - properties: const ["didSetupProperly"], - builder: (BuildContext _, ScheduleViewModel __, Set ___) => - _viewModel.didSetupProperly - ? Container() - : IconButton( - icon: Icon(Icons.help_outline), - onPressed: () async { - await ScheduleHelpDialog().show(context); - }, - tooltip: L.of(context).helpButtonTooltip, - )), + properties: const ["didSetupProperly"], + builder: (BuildContext _, ScheduleViewModel? __, Set? ___) => + model.didSetupProperly + ? Container() + : IconButton( + icon: const Icon(Icons.help_outline), + onPressed: () async { + await const ScheduleHelpDialog().show(context); + }, + tooltip: L.of(context).helpButtonTooltip, + ), + ), ), PropertyChangeProvider( - value: _viewModel, + value: model, child: PropertyChangeConsumer( properties: const ["didSetupProperly"], - builder: (BuildContext _, ScheduleViewModel __, Set ___) => - _viewModel.didSetupProperly + builder: (BuildContext _, ScheduleViewModel? __, Set? ___) => + model.didSetupProperly ? IconButton( - icon: Icon(Icons.filter_alt), + icon: const Icon(Icons.filter_alt), onPressed: () async { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => ScheduleFilterPage(), - )); + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ScheduleFilterPage(), + ), + ); }, ) : Container(), diff --git a/lib/schedule/ui/schedule_page.dart b/lib/schedule/ui/schedule_page.dart index 3da65712..f41cf23a 100644 --- a/lib/schedule/ui/schedule_page.dart +++ b/lib/schedule/ui/schedule_page.dart @@ -1,5 +1,4 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; -import 'package:dhbwstudentapp/common/ui/viewmodels/base_view_model.dart'; import 'package:dhbwstudentapp/schedule/ui/dailyschedule/daily_schedule_page.dart'; import 'package:dhbwstudentapp/schedule/ui/viewmodels/daily_schedule_view_model.dart'; import 'package:dhbwstudentapp/schedule/ui/viewmodels/schedule_view_model.dart'; @@ -12,6 +11,7 @@ import 'package:kiwi/kiwi.dart'; import 'package:provider/provider.dart'; class SchedulePage extends StatefulWidget { + const SchedulePage({super.key}); @override _SchedulePageState createState() => _SchedulePageState(); } @@ -27,25 +27,27 @@ class _SchedulePageState extends State { KiwiContainer().resolve(), ); + _SchedulePageState(); + @override Widget build(BuildContext context) { - ScheduleViewModel viewModel = Provider.of(context); + final ScheduleViewModel viewModel = Provider.of(context); if (!viewModel.didSetupProperly) { - return ScheduleEmptyState(); + return const ScheduleEmptyState(); } else { return PagerWidget( - pages: [ - PageDefinition( - icon: Icon(Icons.view_week), + pages: [ + PageDefinition( + icon: const Icon(Icons.view_week), text: L.of(context).pageWeekOverviewTitle, - builder: (_) => WeeklySchedulePage(), + builder: (_) => const WeeklySchedulePage(), viewModel: weeklyScheduleViewModel, ), - PageDefinition( - icon: Icon(Icons.view_day), + PageDefinition( + icon: const Icon(Icons.view_day), text: L.of(context).pageDayOverviewTitle, - builder: (_) => DailySchedulePage(), + builder: (_) => const DailySchedulePage(), viewModel: dailyScheduleViewModel, ), ], diff --git a/lib/schedule/ui/viewmodels/daily_schedule_view_model.dart b/lib/schedule/ui/viewmodels/daily_schedule_view_model.dart index 793b76ab..25113a59 100644 --- a/lib/schedule/ui/viewmodels/daily_schedule_view_model.dart +++ b/lib/schedule/ui/viewmodels/daily_schedule_view_model.dart @@ -8,9 +8,9 @@ class DailyScheduleViewModel extends BaseViewModel { final ScheduleProvider scheduleProvider; - DateTime currentDate; + DateTime? currentDate; - Schedule daySchedule; + Schedule? _daySchedule; DailyScheduleViewModel(this.scheduleProvider) { scheduleProvider.addScheduleUpdatedCallback(_scheduleUpdatedCallback); @@ -18,13 +18,15 @@ class DailyScheduleViewModel extends BaseViewModel { loadScheduleForToday(); } - Future setSchedule(Schedule schedule) async { - daySchedule = schedule; + set schedule(Schedule schedule) { + _daySchedule = schedule; notifyListeners("daySchedule"); } + Schedule get schedule => _daySchedule ??= const Schedule(); + Future loadScheduleForToday() async { - var now = DateTime.now(); + final now = DateTime.now(); currentDate = toStartOfDay(now); await updateSchedule(); @@ -35,30 +37,24 @@ class DailyScheduleViewModel extends BaseViewModel { } Future _updateScheduleFromCache() async { - setSchedule( - await scheduleProvider.getCachedSchedule( - currentDate, - tomorrow(currentDate), - ), + schedule = await scheduleProvider.getCachedSchedule( + currentDate!, + tomorrow(currentDate)!, ); } Future _scheduleUpdatedCallback( Schedule schedule, - DateTime start, - DateTime end, + DateTime? start, + DateTime? end, ) async { - if (schedule == null) return; - start = toStartOfDay(start); end = toStartOfDay(tomorrow(end)); - if (!(start.isAfter(currentDate) || end.isBefore(currentDate))) { - setSchedule( - schedule.trim( - toStartOfDay(currentDate), - toStartOfDay(tomorrow(currentDate)), - ), + if (!(start!.isAfter(currentDate!) || end!.isBefore(currentDate!))) { + schedule = schedule.trim( + toStartOfDay(currentDate), + toStartOfDay(tomorrow(currentDate)), ); } } diff --git a/lib/schedule/ui/viewmodels/weekly_schedule_view_model.dart b/lib/schedule/ui/viewmodels/weekly_schedule_view_model.dart index af98bb16..03e79420 100644 --- a/lib/schedule/ui/viewmodels/weekly_schedule_view_model.dart +++ b/lib/schedule/ui/viewmodels/weekly_schedule_view_model.dart @@ -18,11 +18,11 @@ class WeeklyScheduleViewModel extends BaseViewModel { final ScheduleProvider scheduleProvider; final ScheduleSourceProvider scheduleSourceProvider; - DateTime currentDateStart; - DateTime currentDateEnd; + DateTime? currentDateStart; + DateTime? currentDateEnd; - DateTime clippedDateStart; - DateTime clippedDateEnd; + DateTime? clippedDateStart; + DateTime? clippedDateEnd; bool didUpdateScheduleIntoFuture = true; @@ -33,24 +33,24 @@ class WeeklyScheduleViewModel extends BaseViewModel { bool get hasQueryErrors => _hasQueryErrors; - VoidCallback _queryFailedCallback; + VoidCallback? _queryFailedCallback; bool updateFailed = false; bool isUpdating = false; - Schedule weekSchedule; + Schedule? weekSchedule; - String scheduleUrl; + String? scheduleUrl; DateTime get now => DateTime.now(); - Timer _errorResetTimer; - Timer _updateNowTimer; + Timer? _errorResetTimer; + Timer? _updateNowTimer; final CancelableMutex _updateMutex = CancelableMutex(); - DateTime lastRequestedStart; - DateTime lastRequestedEnd; + DateTime? lastRequestedStart; + DateTime? lastRequestedEnd; WeeklyScheduleViewModel( this.scheduleProvider, @@ -62,7 +62,7 @@ class WeeklyScheduleViewModel extends BaseViewModel { Future _initViewModel() async { _setSchedule( null, - toDayOfWeek(DateTime.now(), DateTime.monday), + toDayOfWeek(DateTime.now(), DateTime.monday)!, toDayOfWeek(DateTime.now(), DateTime.friday), ); @@ -80,15 +80,15 @@ class WeeklyScheduleViewModel extends BaseViewModel { if (setupSuccess) await updateSchedule(currentDateStart, currentDateEnd); } - void _setSchedule(Schedule schedule, DateTime start, DateTime end) { + void _setSchedule(Schedule? schedule, DateTime start, DateTime? end) { weekSchedule = schedule; didUpdateScheduleIntoFuture = currentDateStart?.isBefore(start) ?? true; currentDateStart = start; currentDateEnd = end; if (weekSchedule != null) { - var scheduleStart = weekSchedule.getStartDate(); - var scheduleEnd = weekSchedule.getEndDate(); + final scheduleStart = weekSchedule!.getStartDate(); + final scheduleEnd = weekSchedule!.getEndDate(); if (scheduleStart == null && scheduleEnd == null) { clippedDateStart = toDayOfWeek(start, DateTime.monday); @@ -98,8 +98,9 @@ class WeeklyScheduleViewModel extends BaseViewModel { clippedDateEnd = toDayOfWeek(scheduleEnd, DateTime.friday); } - if (scheduleEnd?.isAfter(clippedDateEnd) ?? false) + if (scheduleEnd?.isAfter(clippedDateEnd!) ?? false) { clippedDateEnd = scheduleEnd; + } displayStartHour = weekSchedule?.getStartTime()?.hour ?? 23; displayStartHour = min(7, displayStartHour); @@ -136,7 +137,7 @@ class WeeklyScheduleViewModel extends BaseViewModel { await updateSchedule(currentDateStart, currentDateEnd); } - Future updateSchedule(DateTime start, DateTime end) async { + Future updateSchedule(DateTime? start, DateTime? end) async { lastRequestedEnd = end; lastRequestedStart = start; @@ -151,8 +152,9 @@ class WeeklyScheduleViewModel extends BaseViewModel { isUpdating = true; notifyListeners("isUpdating"); - await _doUpdateSchedule(start, end); - } catch (_) {} finally { + await _doUpdateSchedule(start!, end!); + } catch (_) { + } finally { isUpdating = false; _updateMutex.release(); notifyListeners("isUpdating"); @@ -162,15 +164,15 @@ class WeeklyScheduleViewModel extends BaseViewModel { Future _doUpdateSchedule(DateTime start, DateTime end) async { print("Refreshing schedule..."); - var cancellationToken = _updateMutex.token; + final cancellationToken = _updateMutex.token!; scheduleUrl = null; - var cachedSchedule = await scheduleProvider.getCachedSchedule(start, end); + final cachedSchedule = await scheduleProvider.getCachedSchedule(start, end); cancellationToken.throwIfCancelled(); _setSchedule(cachedSchedule, start, end); - var updatedSchedule = await _readScheduleFromService( + final updatedSchedule = await _readScheduleFromService( start, end, cancellationToken, @@ -178,7 +180,7 @@ class WeeklyScheduleViewModel extends BaseViewModel { cancellationToken.throwIfCancelled(); if (updatedSchedule?.schedule != null) { - var schedule = updatedSchedule.schedule; + final schedule = updatedSchedule!.schedule; _setSchedule(schedule, start, end); @@ -202,25 +204,21 @@ class WeeklyScheduleViewModel extends BaseViewModel { print("Refreshing done"); } - Future _readScheduleFromService( + Future _readScheduleFromService( DateTime start, DateTime end, CancellationToken token, ) async { - try { - return await scheduleProvider.getUpdatedSchedule( - start, - end, - token, - ); - } on OperationCancelledException {} on ScheduleQueryFailedException {} - - return null; + return scheduleProvider.getUpdatedSchedule( + start, + end, + token, + ); } void _cancelErrorInFuture() { if (_errorResetTimer != null) { - _errorResetTimer.cancel(); + _errorResetTimer!.cancel(); } _errorResetTimer = Timer( @@ -233,7 +231,7 @@ class WeeklyScheduleViewModel extends BaseViewModel { } void ensureUpdateNowTimerRunning() { - if (_updateNowTimer == null || !_updateNowTimer.isActive) { + if (_updateNowTimer == null || !_updateNowTimer!.isActive) { _updateNowTimer = Timer.periodic(const Duration(minutes: 1), (_) { notifyListeners("now"); }); @@ -251,7 +249,7 @@ class WeeklyScheduleViewModel extends BaseViewModel { _errorResetTimer = null; } - void setQueryFailedCallback(VoidCallback callback) { + set queryFailedCallback(VoidCallback callback) { _queryFailedCallback = callback; } } diff --git a/lib/schedule/ui/weeklyschedule/filter/filter_view_model.dart b/lib/schedule/ui/weeklyschedule/filter/filter_view_model.dart index d2555c59..98b6ba17 100644 --- a/lib/schedule/ui/weeklyschedule/filter/filter_view_model.dart +++ b/lib/schedule/ui/weeklyschedule/filter/filter_view_model.dart @@ -10,30 +10,33 @@ class FilterViewModel extends BaseViewModel { List filterStates = []; - FilterViewModel(this._scheduleEntryRepository, this._scheduleFilterRepository, - this._scheduleSource) { + FilterViewModel( + this._scheduleEntryRepository, + this._scheduleFilterRepository, + this._scheduleSource, + ) { loadFilterStates(); } - void loadFilterStates() async { - var allNames = + Future loadFilterStates() async { + final allNames = await _scheduleEntryRepository.queryAllNamesOfScheduleEntries(); - var filteredNames = await _scheduleFilterRepository.queryAllHiddenNames(); + final filteredNames = await _scheduleFilterRepository.queryAllHiddenNames(); - allNames.sort((s1, s2) => s1.compareTo(s2)); + allNames.sort((s1, s2) => s1!.compareTo(s2!)); filterStates = allNames.map((e) { - var isFiltered = filteredNames.contains(e); + final isFiltered = filteredNames.contains(e); return ScheduleEntryFilterState(!isFiltered, e); }).toList(); notifyListeners(); } - void applyFilter() async { - var allFilteredNames = filterStates - .where((element) => !element.isDisplayed) + Future applyFilter() async { + final allFilteredNames = filterStates + .where((element) => !element.isDisplayed!) .map((e) => e.entryName) .toList(); @@ -44,8 +47,8 @@ class FilterViewModel extends BaseViewModel { } class ScheduleEntryFilterState { - bool isDisplayed; - String entryName; + bool? isDisplayed; + final String? entryName; ScheduleEntryFilterState(this.isDisplayed, this.entryName); } diff --git a/lib/schedule/ui/weeklyschedule/filter/schedule_filter_page.dart b/lib/schedule/ui/weeklyschedule/filter/schedule_filter_page.dart index 735392eb..79c30c74 100644 --- a/lib/schedule/ui/weeklyschedule/filter/schedule_filter_page.dart +++ b/lib/schedule/ui/weeklyschedule/filter/schedule_filter_page.dart @@ -5,6 +5,8 @@ import 'package:kiwi/kiwi.dart'; import 'package:property_change_notifier/property_change_notifier.dart'; class ScheduleFilterPage extends StatelessWidget { + ScheduleFilterPage({super.key}); + final FilterViewModel _viewModel = FilterViewModel( KiwiContainer().resolve(), KiwiContainer().resolve(), @@ -46,14 +48,18 @@ class ScheduleFilterPage extends StatelessWidget { child: PropertyChangeProvider( value: _viewModel, child: PropertyChangeConsumer( - builder: (BuildContext _, FilterViewModel viewModel, - Set ___) => - ListView.builder( - shrinkWrap: true, - itemCount: viewModel.filterStates.length, - itemBuilder: (context, index) => - FilterStateRow(viewModel.filterStates[index]), - )), + builder: ( + BuildContext _, + FilterViewModel? viewModel, + Set? ___, + ) => + ListView.builder( + shrinkWrap: true, + itemCount: viewModel!.filterStates.length, + itemBuilder: (context, index) => + FilterStateRow(viewModel.filterStates[index]), + ), + ), ), ) ], @@ -74,7 +80,9 @@ class FilterStateRow extends StatefulWidget { } class _FilterStateRowState extends State { - bool isChecked = false; + bool? isChecked = false; + + _FilterStateRowState(); @override void initState() { @@ -94,7 +102,7 @@ class _FilterStateRowState extends State { }); }, controlAffinity: ListTileControlAffinity.leading, - title: Text(widget.filterState.entryName), + title: Text(widget.filterState.entryName!), ); } } diff --git a/lib/schedule/ui/weeklyschedule/schedule_entry_detail_bottom_sheet.dart b/lib/schedule/ui/weeklyschedule/schedule_entry_detail_bottom_sheet.dart index a5ad1ea1..3691ea78 100644 --- a/lib/schedule/ui/weeklyschedule/schedule_entry_detail_bottom_sheet.dart +++ b/lib/schedule/ui/weeklyschedule/schedule_entry_detail_bottom_sheet.dart @@ -1,33 +1,38 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; -import 'package:dhbwstudentapp/common/ui/colors.dart'; +import 'package:dhbwstudentapp/common/ui/app_theme.dart'; import 'package:dhbwstudentapp/common/ui/schedule_entry_type_mappings.dart'; -import 'package:dhbwstudentapp/common/ui/text_styles.dart'; +import 'package:dhbwstudentapp/common/ui/text_theme.dart'; import 'package:dhbwstudentapp/schedule/model/schedule_entry.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; class ScheduleEntryDetailBottomSheet extends StatelessWidget { final ScheduleEntry scheduleEntry; - const ScheduleEntryDetailBottomSheet({Key key, this.scheduleEntry}) - : super(key: key); + const ScheduleEntryDetailBottomSheet({ + super.key, + required this.scheduleEntry, + }); @override Widget build(BuildContext context) { - var formatter = DateFormat.Hm(L.of(context).locale.languageCode); - var timeStart = formatter.format(scheduleEntry.start); - var timeEnd = formatter.format(scheduleEntry.end); + final formatter = DateFormat.Hm(L.of(context).locale.languageCode); + final timeStart = formatter.format(scheduleEntry.start); + final timeEnd = formatter.format(scheduleEntry.end); - var typeString = + final typeString = scheduleEntryTypeToReadableString(context, scheduleEntry.type); - return Container( + final textTheme = Theme.of(context).textTheme; + final customTextThme = Theme.of(context).extension(); + final scheduleEntryBottomPageType = + textTheme.bodyText1?.merge(customTextThme?.scheduleEntryBottomPageType); + + return SizedBox( height: 400, child: Padding( padding: const EdgeInsets.fromLTRB(24, 12, 24, 24), child: Column( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( @@ -36,17 +41,16 @@ class ScheduleEntryDetailBottomSheet extends StatelessWidget { child: Container( height: 8, width: 30, - decoration: BoxDecoration( - color: colorSeparator(), - borderRadius: const BorderRadius.all(Radius.circular(4))), - child: null, + decoration: const BoxDecoration( + color: AppTheme.separator, + borderRadius: BorderRadius.all(Radius.circular(4)), + ), ), ), ), Padding( padding: const EdgeInsets.fromLTRB(0, 0, 0, 16.0), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ Column( crossAxisAlignment: CrossAxisAlignment.end, @@ -57,13 +61,11 @@ class ScheduleEntryDetailBottomSheet extends StatelessWidget { children: [ Text( L.of(context).scheduleEntryDetailFrom, - style: textStyleScheduleEntryBottomPageTimeFromTo( - context), + style: Theme.of(context).textTheme.caption, ), Text( timeStart, - style: - textStyleScheduleEntryBottomPageTime(context), + style: Theme.of(context).textTheme.caption, ), ], ), @@ -73,14 +75,11 @@ class ScheduleEntryDetailBottomSheet extends StatelessWidget { children: [ Text( L.of(context).scheduleEntryDetailTo, - style: textStyleScheduleEntryBottomPageTimeFromTo( - context, - ), + style: Theme.of(context).textTheme.caption, ), Text( timeEnd, - style: - textStyleScheduleEntryBottomPageTime(context), + style: Theme.of(context).textTheme.headline5, ), ], ), @@ -90,9 +89,9 @@ class ScheduleEntryDetailBottomSheet extends StatelessWidget { child: Padding( padding: const EdgeInsets.fromLTRB(16, 0, 0, 0), child: Text( - scheduleEntry.title ?? "", + scheduleEntry.title, softWrap: true, - style: textStyleScheduleEntryBottomPageTitle(context), + style: Theme.of(context).textTheme.subtitle2, ), ), ), @@ -106,34 +105,37 @@ class ScheduleEntryDetailBottomSheet extends StatelessWidget { children: [ Expanded( child: Text( - scheduleEntry.professor ?? "", + scheduleEntry.professor, ), ), Text( typeString, - style: textStyleScheduleEntryBottomPageType(context), + style: scheduleEntryBottomPageType, ), ], ), ), - scheduleEntry.room?.isEmpty ?? true - ? Container() - : Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 0, 0), - child: Text(scheduleEntry.room.replaceAll(",", "\n")), - ), - scheduleEntry.details?.isEmpty ?? true - ? Container() - : Padding( - padding: const EdgeInsets.fromLTRB(0, 16, 0, 16), - child: Container( - color: colorSeparator(), - height: 1, - ), - ), - scheduleEntry.details?.isEmpty ?? true - ? Container() - : Text(scheduleEntry.details), + if (scheduleEntry.room.isEmpty) + Container() + else + Padding( + padding: const EdgeInsets.fromLTRB(0, 8, 0, 0), + child: Text(scheduleEntry.room.replaceAll(",", "\n")), + ), + if (scheduleEntry.details.isEmpty) + Container() + else + Padding( + padding: const EdgeInsets.fromLTRB(0, 16, 0, 16), + child: Container( + color: AppTheme.separator, + height: 1, + ), + ), + if (scheduleEntry.details.isEmpty) + Container() + else + Text(scheduleEntry.details), ], ), ), diff --git a/lib/schedule/ui/weeklyschedule/weekly_schedule_page.dart b/lib/schedule/ui/weeklyschedule/weekly_schedule_page.dart index baa28690..91d9307f 100644 --- a/lib/schedule/ui/weeklyschedule/weekly_schedule_page.dart +++ b/lib/schedule/ui/weeklyschedule/weekly_schedule_page.dart @@ -1,8 +1,6 @@ import 'package:animations/animations.dart'; import 'package:dhbwstudentapp/common/i18n/localizations.dart'; -import 'package:dhbwstudentapp/common/ui/viewmodels/base_view_model.dart'; import 'package:dhbwstudentapp/common/ui/widgets/error_display.dart'; -import 'package:dhbwstudentapp/schedule/model/schedule.dart'; import 'package:dhbwstudentapp/schedule/model/schedule_entry.dart'; import 'package:dhbwstudentapp/schedule/ui/viewmodels/weekly_schedule_view_model.dart'; import 'package:dhbwstudentapp/schedule/ui/weeklyschedule/schedule_entry_detail_bottom_sheet.dart'; @@ -13,13 +11,14 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; class WeeklySchedulePage extends StatefulWidget { + const WeeklySchedulePage({super.key}); + @override _WeeklySchedulePageState createState() => _WeeklySchedulePageState(); } class _WeeklySchedulePageState extends State { - WeeklyScheduleViewModel viewModel; - Schedule schedule; + late WeeklyScheduleViewModel viewModel; _WeeklySchedulePageState(); @@ -29,7 +28,7 @@ class _WeeklySchedulePageState extends State { } void _showQueryFailedSnackBar() { - Scaffold.of(context).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Column( crossAxisAlignment: CrossAxisAlignment.end, @@ -38,28 +37,30 @@ class _WeeklySchedulePageState extends State { Text(L.of(context).scheduleQueryFailedMessage), TextButton( child: Text( - L.of(context).scheduleQueryFailedOpenInBrowser.toUpperCase()), + L.of(context).scheduleQueryFailedOpenInBrowser.toUpperCase(), + ), onPressed: () { - launchUrl(Uri.parse(viewModel.scheduleUrl)); + // TDOD: [Leptopoda] this can throw a null error + launchUrl(Uri.parse(viewModel.scheduleUrl!)); }, ) ], ), - duration: Duration(seconds: 15), + duration: const Duration(seconds: 15), ), ); } - void _previousWeek() async { - await viewModel?.previousWeek(); + Future _previousWeek() async { + await viewModel.previousWeek(); } - void _nextWeek() async { - await viewModel?.nextWeek(); + Future _nextWeek() async { + await viewModel.nextWeek(); } - void _goToToday() async { - await viewModel?.goToToday(); + Future _goToToday() async { + await viewModel.goToToday(); } void _onScheduleEntryTap(BuildContext context, ScheduleEntry entry) { @@ -77,10 +78,10 @@ class _WeeklySchedulePageState extends State { @override Widget build(BuildContext context) { - viewModel = Provider.of(context); + viewModel = Provider.of(context); viewModel.ensureUpdateNowTimerRunning(); - viewModel.setQueryFailedCallback(_showQueryFailedSnackBar); + viewModel.queryFailedCallback = _showQueryFailedSnackBar; return PropertyChangeProvider( value: viewModel, @@ -108,24 +109,28 @@ class _WeeklySchedulePageState extends State { padding: const EdgeInsets.all(8.0), child: PropertyChangeConsumer( properties: const ["weekSchedule", "now"], - builder: (BuildContext context, - WeeklyScheduleViewModel model, Set properties) { + builder: ( + BuildContext context, + WeeklyScheduleViewModel? model, + Set? _, + ) { return PageTransitionSwitcher( - reverse: !model.didUpdateScheduleIntoFuture, - duration: const Duration(milliseconds: 300), - transitionBuilder: (Widget child, - Animation animation, - Animation secondaryAnimation) => + reverse: !model!.didUpdateScheduleIntoFuture, + transitionBuilder: ( + Widget child, + Animation animation, + Animation secondaryAnimation, + ) => SharedAxisTransition( - child: child, animation: animation, secondaryAnimation: secondaryAnimation, transitionType: SharedAxisTransitionType.horizontal, + child: child, ), child: ScheduleWidget( key: ValueKey( - model.currentDateStart.toIso8601String(), + model.currentDateStart!.toIso8601String(), ), schedule: model.weekSchedule, displayStart: model.clippedDateStart ?? @@ -145,9 +150,12 @@ class _WeeklySchedulePageState extends State { ), PropertyChangeConsumer( properties: const ["isUpdating"], - builder: (BuildContext context, - WeeklyScheduleViewModel model, Set properties) { - return model.isUpdating + builder: ( + BuildContext context, + WeeklyScheduleViewModel? model, + Set? _, + ) { + return model!.isUpdating ? const LinearProgressIndicator() : Container(); }, @@ -169,15 +177,15 @@ class _WeeklySchedulePageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( - icon: Icon(Icons.chevron_left), + icon: const Icon(Icons.chevron_left), onPressed: _previousWeek, ), IconButton( - icon: Icon(Icons.today), + icon: const Icon(Icons.today), onPressed: _goToToday, ), IconButton( - icon: Icon(Icons.chevron_right), + icon: const Icon(Icons.chevron_right), onPressed: _nextWeek, ), ], @@ -189,10 +197,9 @@ class _WeeklySchedulePageState extends State { properties: const [ "updateFailed", ], - builder: (BuildContext context, WeeklyScheduleViewModel model, - Set properties) => + builder: (BuildContext context, WeeklyScheduleViewModel? model, Set? _) => ErrorDisplay( - show: model.updateFailed, + show: model!.updateFailed, ), ); } diff --git a/lib/schedule/ui/weeklyschedule/widgets/schedule_entry_alignment.dart b/lib/schedule/ui/weeklyschedule/widgets/schedule_entry_alignment.dart index f1748038..efa8bff6 100644 --- a/lib/schedule/ui/weeklyschedule/widgets/schedule_entry_alignment.dart +++ b/lib/schedule/ui/weeklyschedule/widgets/schedule_entry_alignment.dart @@ -3,8 +3,8 @@ import 'package:dhbwstudentapp/schedule/model/schedule_entry.dart'; class ScheduleEntryAlignmentInformation { final ScheduleEntry entry; - double leftColumn; - double rightColumn; + late double leftColumn; + late double rightColumn; ScheduleEntryAlignmentInformation(this.entry); } @@ -14,10 +14,12 @@ class ScheduleEntryAlignmentInformation { /// https://stackoverflow.com/questions/11311410/visualization-of-calendar-events-algorithm-to-layout-events-with-maximum-width /// class ScheduleEntryAlignmentAlgorithm { + const ScheduleEntryAlignmentAlgorithm(); + List layoutEntries( List entries, ) { - var events = entries + final events = entries .map( (e) => ScheduleEntryAlignmentInformation(e), ) @@ -32,11 +34,11 @@ class ScheduleEntryAlignmentAlgorithm { return result; }); - List> columns = [[]]; + final List> columns = [[]]; - DateTime lastEventEnd; + DateTime? lastEventEnd; - for (var event in events) { + for (final event in events) { if (!event.entry.start .isBefore(lastEventEnd ?? DateTime.fromMillisecondsSinceEpoch(0))) { _packEvents(columns); @@ -47,7 +49,7 @@ class ScheduleEntryAlignmentAlgorithm { bool placed = false; // Try to put the event in one existing column. - for (var column in columns) { + for (final column in columns) { if (!_entriesCollide(column.last.entry, event.entry)) { column.add(event); placed = true; @@ -77,9 +79,9 @@ class ScheduleEntryAlignmentAlgorithm { void _packEvents(List> columns) { int columnIndex = 0; - for (var column in columns) { - for (var event in column) { - var span = _expandEvent(event, columnIndex, columns); + for (final column in columns) { + for (final event in column) { + final span = _expandEvent(event, columnIndex, columns); event.leftColumn = columnIndex / columns.length; event.rightColumn = (columnIndex + span) / columns.length; } @@ -93,8 +95,8 @@ class ScheduleEntryAlignmentAlgorithm { List> columns, ) { var span = 1; - for (var column in columns.skip(columnIndex + 1)) { - for (var ev in column) { + for (final column in columns.skip(columnIndex + 1)) { + for (final ev in column) { if (_entriesCollide(ev.entry, event.entry)) { return span; } diff --git a/lib/schedule/ui/weeklyschedule/widgets/schedule_entry_widget.dart b/lib/schedule/ui/weeklyschedule/widgets/schedule_entry_widget.dart index 8511ad5c..45ec38e5 100644 --- a/lib/schedule/ui/weeklyschedule/widgets/schedule_entry_widget.dart +++ b/lib/schedule/ui/weeklyschedule/widgets/schedule_entry_widget.dart @@ -1,5 +1,4 @@ -import 'package:dhbwstudentapp/common/ui/schedule_entry_type_mappings.dart'; -import 'package:dhbwstudentapp/common/ui/text_styles.dart'; +import 'package:dhbwstudentapp/common/ui/text_theme.dart'; import 'package:dhbwstudentapp/schedule/model/schedule_entry.dart'; import 'package:flutter/material.dart'; @@ -10,14 +9,22 @@ class ScheduleEntryWidget extends StatelessWidget { final ScheduleEntryTapCallback onScheduleEntryTap; const ScheduleEntryWidget({ - Key key, - this.scheduleEntry, - this.onScheduleEntryTap, - }) : super(key: key); + super.key, + required this.scheduleEntry, + required this.onScheduleEntryTap, + }); @override Widget build(BuildContext context) { - Color color = scheduleEntryTypeToColor(context, scheduleEntry.type); + final Color color = scheduleEntry.type.color(context); + + Theme.of(context).textTheme.bodyText1!.copyWith(); + + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final customTextThme = theme.extension()!; + final textStyle = + textTheme.headline1?.merge(customTextThme.scheduleEntryWidgetTitle); return Card( color: color, @@ -25,7 +32,7 @@ class ScheduleEntryWidget extends StatelessWidget { margin: const EdgeInsets.fromLTRB(1, 0, 1, 0), child: InkWell( onTap: () { - if (onScheduleEntryTap != null) onScheduleEntryTap(scheduleEntry); + onScheduleEntryTap.call(scheduleEntry); }, child: Padding( padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), @@ -34,7 +41,7 @@ class ScheduleEntryWidget extends StatelessWidget { softWrap: true, overflow: TextOverflow.clip, textAlign: TextAlign.left, - style: textStyleScheduleEntryWidgetTitle(context), + style: textStyle, ), ), ), diff --git a/lib/schedule/ui/weeklyschedule/widgets/schedule_grid.dart b/lib/schedule/ui/weeklyschedule/widgets/schedule_grid.dart index c306466c..9acf73bc 100644 --- a/lib/schedule/ui/weeklyschedule/widgets/schedule_grid.dart +++ b/lib/schedule/ui/weeklyschedule/widgets/schedule_grid.dart @@ -9,17 +9,23 @@ class ScheduleGrid extends CustomPaint { final int columns; final Color gridLinesColor; - ScheduleGrid(this.fromHour, this.toHour, this.timeLabelsWidth, - this.dateLabelsHeight, this.columns, this.gridLinesColor) - : super( - painter: ScheduleGridCustomPaint( - fromHour, - toHour, - timeLabelsWidth, - dateLabelsHeight, - columns, - gridLinesColor, - )); + ScheduleGrid( + this.fromHour, + this.toHour, + this.timeLabelsWidth, + this.dateLabelsHeight, + this.columns, + this.gridLinesColor, + ) : super( + painter: ScheduleGridCustomPaint( + fromHour, + toHour, + timeLabelsWidth, + dateLabelsHeight, + columns, + gridLinesColor, + ), + ); } class ScheduleGridCustomPaint extends CustomPainter { @@ -30,7 +36,7 @@ class ScheduleGridCustomPaint extends CustomPainter { final int columns; final Color gridLineColor; - ScheduleGridCustomPaint( + const ScheduleGridCustomPaint( this.fromHour, this.toHour, this.timeLabelsWidth, @@ -45,7 +51,7 @@ class ScheduleGridCustomPaint extends CustomPainter { ..color = gridLineColor ..strokeWidth = 1; - var lines = toHour - fromHour; + final lines = toHour - fromHour; drawHorizontalLines(lines, size, canvas, secondaryPaint); drawVerticalLines(size, canvas, secondaryPaint); @@ -58,7 +64,8 @@ class ScheduleGridCustomPaint extends CustomPainter { Paint secondaryPaint, ) { for (var i = 0; i < lines; i++) { - var y = ((size.height - dateLabelsHeight) / lines) * i + dateLabelsHeight; + final y = + ((size.height - dateLabelsHeight) / lines) * i + dateLabelsHeight; canvas.drawLine(Offset(0, y), Offset(size.width, y), secondaryPaint); } @@ -66,7 +73,8 @@ class ScheduleGridCustomPaint extends CustomPainter { void drawVerticalLines(Size size, Canvas canvas, Paint secondaryPaint) { for (var i = 0; i < columns; i++) { - var x = ((size.width - timeLabelsWidth) / columns) * i + timeLabelsWidth; + final x = + ((size.width - timeLabelsWidth) / columns) * i + timeLabelsWidth; canvas.drawLine(Offset(x, 0), Offset(x, size.height), secondaryPaint); } diff --git a/lib/schedule/ui/weeklyschedule/widgets/schedule_past_overlay.dart b/lib/schedule/ui/weeklyschedule/widgets/schedule_past_overlay.dart index 94234607..344a1a60 100644 --- a/lib/schedule/ui/weeklyschedule/widgets/schedule_past_overlay.dart +++ b/lib/schedule/ui/weeklyschedule/widgets/schedule_past_overlay.dart @@ -5,11 +5,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; class SchedulePastOverlay extends CustomPaint { - final DateTime fromDate; - final DateTime toDate; - final DateTime now; - final int fromHour; - final int toHour; + final DateTime? fromDate; + final DateTime? toDate; + final DateTime? now; + final int? fromHour; + final int? toHour; final int columns; final Color overlayColor; @@ -35,15 +35,15 @@ class SchedulePastOverlay extends CustomPaint { } class SchedulePastOverlayCustomPaint extends CustomPainter { - final DateTime fromDate; - final DateTime toDate; - final DateTime now; - final int fromHour; - final int toHour; + final DateTime? fromDate; + final DateTime? toDate; + final DateTime? now; + final int? fromHour; + final int? toHour; final int columns; final Color overlayColor; - SchedulePastOverlayCustomPaint( + const SchedulePastOverlayCustomPaint( this.fromDate, this.toDate, this.now, @@ -59,28 +59,29 @@ class SchedulePastOverlayCustomPaint extends CustomPainter { ..color = overlayColor ..strokeWidth = 1; - if (now.isBefore(toDate) && now.isAfter(fromDate)) { + if (now!.isBefore(toDate!) && now!.isAfter(fromDate!)) { drawPartialPastOverlay(canvas, size, overlayPaint); - } else if (now.isAfter(toDate)) { + } else if (now!.isAfter(toDate!)) { drawCompletePastOverlay(canvas, size, overlayPaint); } } void drawCompletePastOverlay(Canvas canvas, Size size, Paint overlayPaint) { canvas.drawRect( - Rect.fromLTRB( - 0, - 0, - size.width, - size.height, - ), - overlayPaint); + Rect.fromLTRB( + 0, + 0, + size.width, + size.height, + ), + overlayPaint, + ); } void drawPartialPastOverlay(Canvas canvas, Size size, Paint overlayPaint) { - var difference = now.difference(fromDate); - var differenceInDays = (difference.inHours / 24).floor(); - var dayWidth = size.width / columns; + final difference = now!.difference(fromDate!); + final differenceInDays = (difference.inHours / 24).floor(); + final dayWidth = size.width / columns; canvas.drawRect( Rect.fromLTWH( @@ -92,11 +93,11 @@ class SchedulePastOverlayCustomPaint extends CustomPainter { overlayPaint, ); - var leftoverMinutes = - difference.inMinutes - (differenceInDays * 24 * 60) - (60 * fromHour); + final leftoverMinutes = + difference.inMinutes - (differenceInDays * 24 * 60) - (60 * fromHour!); - var displayedMinutes = (toHour - fromHour) * 60; - var leftoverHeight = leftoverMinutes * (size.height / displayedMinutes); + final displayedMinutes = (toHour! - fromHour!) * 60; + final leftoverHeight = leftoverMinutes * (size.height / displayedMinutes); if (leftoverHeight > 0) { canvas.drawRect( diff --git a/lib/schedule/ui/weeklyschedule/widgets/schedule_widget.dart b/lib/schedule/ui/weeklyschedule/widgets/schedule_widget.dart index 59fcaeef..1c402533 100644 --- a/lib/schedule/ui/weeklyschedule/widgets/schedule_widget.dart +++ b/lib/schedule/ui/weeklyschedule/widgets/schedule_widget.dart @@ -1,6 +1,6 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; -import 'package:dhbwstudentapp/common/ui/colors.dart'; -import 'package:dhbwstudentapp/common/ui/text_styles.dart'; +import 'package:dhbwstudentapp/common/ui/schedule_theme.dart'; +import 'package:dhbwstudentapp/common/ui/text_theme.dart'; import 'package:dhbwstudentapp/common/util/date_utils.dart'; import 'package:dhbwstudentapp/schedule/model/schedule.dart'; import 'package:dhbwstudentapp/schedule/model/schedule_entry.dart'; @@ -9,51 +9,51 @@ import 'package:dhbwstudentapp/schedule/ui/weeklyschedule/widgets/schedule_entry import 'package:dhbwstudentapp/schedule/ui/weeklyschedule/widgets/schedule_grid.dart'; import 'package:dhbwstudentapp/schedule/ui/weeklyschedule/widgets/schedule_past_overlay.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; class ScheduleWidget extends StatelessWidget { - final Schedule schedule; - final DateTime displayStart; - final DateTime displayEnd; + final Schedule? schedule; + final DateTime? displayStart; + final DateTime? displayEnd; final DateTime now; final int displayStartHour; final int displayEndHour; final ScheduleEntryTapCallback onScheduleEntryTap; const ScheduleWidget({ - Key key, - this.schedule, - this.displayStart, - this.displayEnd, - this.onScheduleEntryTap, - this.now, - this.displayStartHour, - this.displayEndHour, - }) : super(key: key); + super.key, + required this.schedule, + required this.displayStart, + required this.displayEnd, + required this.onScheduleEntryTap, + required this.now, + required this.displayStartHour, + required this.displayEndHour, + }); @override Widget build(BuildContext context) { - return Container( - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return buildWithSize( - context, constraints.biggest.width, constraints.biggest.height); - }), + return LayoutBuilder( + builder: buildWithSize, ); } - Widget buildWithSize(BuildContext context, double width, double height) { - var dayLabelsHeight = 40.0; - var timeLabelsWidth = 50.0; + Widget buildWithSize(BuildContext context, BoxConstraints constraints) { + final scheduleTheme = Theme.of(context).extension()!; - var hourHeight = + final height = constraints.biggest.height; + final width = constraints.biggest.width; + + const dayLabelsHeight = 40.0; + const timeLabelsWidth = 50.0; + + final hourHeight = (height - dayLabelsHeight) / (displayEndHour - displayStartHour); - var minuteHeight = hourHeight / 60; + final minuteHeight = hourHeight / 60; - var days = calculateDisplayedDays(); + final days = calculateDisplayedDays(); - var labelWidgets = buildLabelWidgets( + final labelWidgets = buildLabelWidgets( context, hourHeight, (width - timeLabelsWidth) / days, @@ -83,10 +83,11 @@ class ScheduleWidget extends StatelessWidget { timeLabelsWidth, dayLabelsHeight, days, - colorScheduleGridGridLines(context), + scheduleTheme.scheduleGridGridLines, ), Padding( - padding: EdgeInsets.fromLTRB(timeLabelsWidth, dayLabelsHeight, 0, 0), + padding: + const EdgeInsets.fromLTRB(timeLabelsWidth, dayLabelsHeight, 0, 0), child: Stack( children: entryWidgets, ), @@ -95,11 +96,12 @@ class ScheduleWidget extends StatelessWidget { children: labelWidgets, ), Padding( - padding: EdgeInsets.fromLTRB(timeLabelsWidth, dayLabelsHeight, 0, 0), + padding: + const EdgeInsets.fromLTRB(timeLabelsWidth, dayLabelsHeight, 0, 0), child: SchedulePastOverlay( displayStartHour, displayEndHour, - colorScheduleInPastOverlay(context), + scheduleTheme.scheduleInPastOverlay, displayStart, displayEnd, now, @@ -111,8 +113,8 @@ class ScheduleWidget extends StatelessWidget { } int calculateDisplayedDays() { - var startEndDifference = - toStartOfDay(displayEnd)?.difference(toStartOfDay(displayStart)); + final startEndDifference = + toStartOfDay(displayEnd)?.difference(toStartOfDay(displayStart)!); var days = (startEndDifference?.inDays ?? 0) + 1; @@ -133,10 +135,10 @@ class ScheduleWidget extends StatelessWidget { double hourHeight, double minuteHeight, ) { - var labelWidgets = []; + final labelWidgets = []; for (var i = displayStartHour; i < displayEndHour; i++) { - var hourLabelText = i.toString() + ":00"; + final hourLabelText = "$i:00"; labelWidgets.add( Positioned( @@ -152,14 +154,20 @@ class ScheduleWidget extends StatelessWidget { var i = 0; - var dayFormatter = DateFormat("E", L.of(context).locale.languageCode); - var dateFormatter = DateFormat("d. MMM", L.of(context).locale.languageCode); + final dayFormatter = DateFormat("E", L.of(context).locale.languageCode); + final dateFormatter = + DateFormat("d. MMM", L.of(context).locale.languageCode); - var loopEnd = toStartOfDay(tomorrow(displayEnd)); + final loopEnd = toStartOfDay(tomorrow(displayEnd))!; - for (var columnDate = toStartOfDay(displayStart); + final textTheme = Theme.of(context).textTheme; + final customTextThme = Theme.of(context).extension(); + final scheduleWidgetColumnTitleDay = textTheme.subtitle2 + ?.merge(customTextThme?.scheduleWidgetColumnTitleDay); + + for (var columnDate = toStartOfDay(displayStart)!; columnDate.isBefore(loopEnd); - columnDate = tomorrow(columnDate)) { + columnDate = tomorrow(columnDate)!) { labelWidgets.add( Positioned( top: 0, @@ -174,7 +182,7 @@ class ScheduleWidget extends StatelessWidget { children: [ Text( dayFormatter.format(columnDate), - style: textStyleScheduleWidgetColumnTitleDay(context), + style: scheduleWidgetColumnTitleDay, ), Text(dateFormatter.format(columnDate)), ], @@ -190,30 +198,36 @@ class ScheduleWidget extends StatelessWidget { } List buildEntryWidgets( - double hourHeight, double minuteHeight, double width, int columns) { + double hourHeight, + double minuteHeight, + double width, + int columns, + ) { if (schedule == null) return []; - if (schedule.entries.isEmpty) return []; + if (schedule!.entries.isEmpty) return []; - var entryWidgets = []; + final entryWidgets = []; - var columnWidth = width / columns; + final columnWidth = width / columns; - DateTime columnStartDate = toStartOfDay(displayStart); - DateTime columnEndDate = tomorrow(columnStartDate); + DateTime? columnStartDate = toStartOfDay(displayStart); + DateTime? columnEndDate = tomorrow(columnStartDate); for (int i = 0; i < columns; i++) { - var xPosition = columnWidth * i; - var maxWidth = columnWidth; - - var columnSchedule = schedule.trim(columnStartDate, columnEndDate); - - entryWidgets.addAll(buildEntryWidgetsForColumn( - maxWidth, - hourHeight, - minuteHeight, - xPosition, - columnSchedule.entries, - )); + final xPosition = columnWidth * i; + final maxWidth = columnWidth; + + final columnSchedule = schedule!.trim(columnStartDate, columnEndDate); + + entryWidgets.addAll( + buildEntryWidgetsForColumn( + maxWidth, + hourHeight, + minuteHeight, + xPosition, + columnSchedule.entries, + ), + ); columnStartDate = columnEndDate; columnEndDate = tomorrow(columnEndDate); @@ -229,24 +243,24 @@ class ScheduleWidget extends StatelessWidget { double xPosition, List entries, ) { - var entryWidgets = []; + final entryWidgets = []; - var laidOutEntries = - ScheduleEntryAlignmentAlgorithm().layoutEntries(entries); + final laidOutEntries = + const ScheduleEntryAlignmentAlgorithm().layoutEntries(entries); - for (var value in laidOutEntries) { - var entry = value.entry; + for (final value in laidOutEntries) { + final entry = value.entry; - var yStart = hourHeight * (entry.start.hour - displayStartHour) + + final yStart = hourHeight * (entry.start.hour - displayStartHour) + minuteHeight * entry.start.minute; - var yEnd = hourHeight * (entry.end.hour - displayStartHour) + + final yEnd = hourHeight * (entry.end.hour - displayStartHour) + minuteHeight * entry.end.minute; - var entryLeft = maxWidth * value.leftColumn; - var entryWidth = maxWidth * (value.rightColumn - value.leftColumn); + final entryLeft = maxWidth * value.leftColumn; + final entryWidth = maxWidth * (value.rightColumn - value.leftColumn); - var widget = Positioned( + final widget = Positioned( top: yStart, left: entryLeft + xPosition, height: yEnd - yStart, diff --git a/lib/schedule/ui/widgets/enter_dualis_credentials_dialog.dart b/lib/schedule/ui/widgets/enter_dualis_credentials_dialog.dart index cca38e58..011233c8 100644 --- a/lib/schedule/ui/widgets/enter_dualis_credentials_dialog.dart +++ b/lib/schedule/ui/widgets/enter_dualis_credentials_dialog.dart @@ -9,13 +9,9 @@ class EnterDualisCredentialsDialog { final PreferencesProvider _preferencesProvider; final ScheduleSourceProvider _scheduleSourceProvider; - final TextEditingController _usernameEditingController = - TextEditingController(); - final TextEditingController _passwordEditingController = - TextEditingController(); + final _controller = CredentialsEditingController(); - String username; - String password; + Credentials? _credentials; EnterDualisCredentialsDialog( this._preferencesProvider, @@ -23,22 +19,18 @@ class EnterDualisCredentialsDialog { ); Future show(BuildContext context) async { - var credentials = await _preferencesProvider.loadDualisCredentials(); - username = credentials.username; - password = credentials.password; + _credentials = await _preferencesProvider.loadDualisCredentials(); await showDialog( context: context, - builder: (context) => _buildDialog(context), + builder: _buildDialog, ); } AlertDialog _buildDialog(BuildContext context) { - if (_usernameEditingController.text != username) { - _usernameEditingController.text = username; - } - if (_passwordEditingController.text != password) { - _passwordEditingController.text = password; + final credentials = _credentials; + if (credentials != null) { + _controller.credentials = credentials; } return AlertDialog( @@ -55,8 +47,7 @@ class EnterDualisCredentialsDialog { ), ), LoginCredentialsWidget( - usernameEditingController: _usernameEditingController, - passwordEditingController: _passwordEditingController, + controller: _controller, onSubmitted: () async {}, ), ], @@ -70,11 +61,9 @@ class EnterDualisCredentialsDialog { TextButton( child: Text(L.of(context).dialogOk.toUpperCase()), onPressed: () async { + // TODO: [Leptopoda] validate credentials like [DualisLoginViewModel] await _preferencesProvider.storeDualisCredentials( - Credentials( - _usernameEditingController.text, - _passwordEditingController.text, - ), + _controller.credentials, ); await _scheduleSourceProvider.setupForDualis(); diff --git a/lib/schedule/ui/widgets/enter_ical_url.dart b/lib/schedule/ui/widgets/enter_ical_url.dart index 25cc1d17..8cef7432 100644 --- a/lib/schedule/ui/widgets/enter_ical_url.dart +++ b/lib/schedule/ui/widgets/enter_ical_url.dart @@ -12,17 +12,17 @@ class EnterIcalDialog extends EnterUrlDialog { EnterIcalDialog(this._preferencesProvider, this._scheduleSource); @override - Future loadUrl() { + Future loadUrl() { return _preferencesProvider.getIcalUrl(); } @override - Future saveUrl(String url) async { + Future saveUrl(String? url) async { await _scheduleSource.setupForIcal(url); } @override - bool isValidUrl(String url) { + bool isValidUrl(String? url) { return IcalScheduleSource.isValidUrl(url); } diff --git a/lib/schedule/ui/widgets/enter_rapla_url_dialog.dart b/lib/schedule/ui/widgets/enter_rapla_url_dialog.dart index 0bc5167b..f74ad99a 100644 --- a/lib/schedule/ui/widgets/enter_rapla_url_dialog.dart +++ b/lib/schedule/ui/widgets/enter_rapla_url_dialog.dart @@ -15,18 +15,18 @@ class EnterRaplaUrlDialog extends EnterUrlDialog { EnterRaplaUrlDialog(this._preferencesProvider, this._scheduleSource); @override - Future saveUrl(String url) async { + Future saveUrl(String? url) async { await _scheduleSource.setupForRapla(url); } @override Future loadUrl() async { - return await _preferencesProvider.getRaplaUrl(); + return _preferencesProvider.getRaplaUrl(); } @override - bool isValidUrl(String url) { - return RaplaScheduleSource.isValidUrl(url); + bool isValidUrl(String? url) { + return RaplaScheduleSource.isValidUrl(url!); } @override diff --git a/lib/schedule/ui/widgets/enter_url_dialog.dart b/lib/schedule/ui/widgets/enter_url_dialog.dart index 58a65dc4..80e20976 100644 --- a/lib/schedule/ui/widgets/enter_url_dialog.dart +++ b/lib/schedule/ui/widgets/enter_url_dialog.dart @@ -18,7 +18,7 @@ abstract class EnterUrlDialog { Future show(BuildContext context) async { await showDialog( context: context, - builder: (context) => _buildDialog(context), + builder: _buildDialog, ); } @@ -49,16 +49,17 @@ abstract class EnterUrlDialog { builder: ( BuildContext context, ValueNotifier url, - Widget child, + Widget? child, ) { - if (_urlTextController.text != url.value) + if (_urlTextController.text != url.value) { _urlTextController.text = url.value; + } return Consumer( builder: ( BuildContext context, ValueNotifier hasUrlError, - Widget child, + Widget? child, ) => TextField( controller: _urlTextController, @@ -81,7 +82,7 @@ abstract class EnterUrlDialog { await _pasteUrl(); }, tooltip: L.of(context).onboardingRaplaUrlPaste, - icon: Icon(Icons.content_paste), + icon: const Icon(Icons.content_paste), ), ], ), @@ -102,10 +103,12 @@ abstract class EnterUrlDialog { ListenableProvider.value( value: _hasUrlError, child: Consumer( - builder: (BuildContext context, ValueNotifier hasUrlError, - Widget child) => + builder: ( + BuildContext context, + ValueNotifier hasUrlError, + Widget? child, + ) => TextButton( - child: Text(L.of(context).dialogOk.toUpperCase()), onPressed: hasUrlError.value ? null : () async { @@ -113,6 +116,7 @@ abstract class EnterUrlDialog { await saveUrl(_url.value); }, + child: Text(L.of(context).dialogOk.toUpperCase()), ), ), ), @@ -120,15 +124,15 @@ abstract class EnterUrlDialog { } Future _pasteUrl() async { - ClipboardData data = await Clipboard.getData('text/plain'); + final ClipboardData? data = await Clipboard.getData('text/plain'); if (data?.text != null) { - _setUrl(data.text); + _setUrl(data!.text!); } } Future _loadUrl() async { - _url.value = await loadUrl(); + _url.value = await loadUrl() ?? ""; } void _setUrl(String url) { @@ -140,9 +144,9 @@ abstract class EnterUrlDialog { _hasUrlError.value = !isValidUrl(_url.value); } - bool isValidUrl(String url); - Future saveUrl(String url); - Future loadUrl(); + bool isValidUrl(String? url); + Future saveUrl(String? url); + Future loadUrl(); String title(BuildContext context); String message(BuildContext context); diff --git a/lib/schedule/ui/widgets/schedule_empty_state.dart b/lib/schedule/ui/widgets/schedule_empty_state.dart index c52a1e59..fab8b29e 100644 --- a/lib/schedule/ui/widgets/schedule_empty_state.dart +++ b/lib/schedule/ui/widgets/schedule_empty_state.dart @@ -1,3 +1,4 @@ +import 'package:dhbwstudentapp/assets.dart'; import 'package:dhbwstudentapp/common/i18n/localizations.dart'; import 'package:dhbwstudentapp/schedule/ui/widgets/select_source_dialog.dart'; import 'package:dhbwstudentapp/ui/banner_widget.dart'; @@ -5,9 +6,11 @@ import 'package:flutter/material.dart'; import 'package:kiwi/kiwi.dart'; class ScheduleEmptyState extends StatelessWidget { - final Map image = { - Brightness.dark: "assets/schedule_empty_state_dark.png", - Brightness.light: "assets/schedule_empty_state.png", + const ScheduleEmptyState({super.key}); + + static const Map image = { + Brightness.dark: Assets.assets_schedule_empty_state_dark_png, + Brightness.light: Assets.assets_schedule_empty_state_png, }; @override @@ -32,7 +35,7 @@ class ScheduleEmptyState extends StatelessWidget { child: ClipRRect( child: Padding( padding: const EdgeInsets.all(32), - child: Image.asset(image[Theme.of(context).brightness]), + child: Image.asset(image[Theme.of(context).brightness]!), ), ), ) diff --git a/lib/schedule/ui/widgets/schedule_help_dialog.dart b/lib/schedule/ui/widgets/schedule_help_dialog.dart index dd5c4335..ebde3d6e 100644 --- a/lib/schedule/ui/widgets/schedule_help_dialog.dart +++ b/lib/schedule/ui/widgets/schedule_help_dialog.dart @@ -3,6 +3,8 @@ import 'package:dhbwstudentapp/common/ui/widgets/help_dialog.dart'; import 'package:flutter/material.dart'; class ScheduleHelpDialog extends HelpDialog { + const ScheduleHelpDialog(); + @override String content(BuildContext context) { return L.of(context).scheduleHelpDialogContent; diff --git a/lib/schedule/ui/widgets/select_mannheim_course_dialog.dart b/lib/schedule/ui/widgets/select_mannheim_course_dialog.dart index 70943bb4..c01ac6ea 100644 --- a/lib/schedule/ui/widgets/select_mannheim_course_dialog.dart +++ b/lib/schedule/ui/widgets/select_mannheim_course_dialog.dart @@ -7,34 +7,29 @@ import 'package:flutter/material.dart'; import 'package:property_change_notifier/property_change_notifier.dart'; class SelectMannheimCourseDialog { - final ScheduleSourceProvider _scheduleSourceProvider; - - MannheimViewModel _mannheimViewModel; + final MannheimViewModel _mannheimViewModel; SelectMannheimCourseDialog( - this._scheduleSourceProvider, - ); + ScheduleSourceProvider scheduleSourceProvider, + ) : _mannheimViewModel = MannheimViewModel(scheduleSourceProvider); Future show(BuildContext context) async { - _mannheimViewModel = MannheimViewModel(_scheduleSourceProvider); - await showDialog( context: context, - builder: (context) => _buildDialog(context), + builder: _buildDialog, ); } AlertDialog _buildDialog(BuildContext context) { return AlertDialog( title: Text(L.of(context).onboardingMannheimTitle), - contentPadding: EdgeInsets.all(0), + contentPadding: EdgeInsets.zero, content: PropertyChangeProvider( value: _mannheimViewModel, - child: Container( + child: SizedBox( height: MediaQuery.of(context).size.height, width: MediaQuery.of(context).size.width, child: Column( - mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( @@ -44,9 +39,9 @@ class SelectMannheimCourseDialog { style: Theme.of(context).textTheme.bodyText2, ), ), - Expanded( + const Expanded( child: Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + padding: EdgeInsets.fromLTRB(8, 0, 8, 0), child: SelectMannheimCourseWidget(), ), ), diff --git a/lib/schedule/ui/widgets/select_source_dialog.dart b/lib/schedule/ui/widgets/select_source_dialog.dart index 83d03fd7..75713d79 100644 --- a/lib/schedule/ui/widgets/select_source_dialog.dart +++ b/lib/schedule/ui/widgets/select_source_dialog.dart @@ -13,17 +13,17 @@ class SelectSourceDialog { final PreferencesProvider _preferencesProvider; final ScheduleSourceProvider _scheduleSourceProvider; - ScheduleSourceType _currentScheduleSource; + ScheduleSourceType? _currentScheduleSource; SelectSourceDialog(this._preferencesProvider, this._scheduleSourceProvider); Future show(BuildContext context) async { - var type = await _preferencesProvider.getScheduleSourceType(); + final type = await _preferencesProvider.getScheduleSourceType(); _currentScheduleSource = ScheduleSourceType.values[type]; await showDialog( context: context, - builder: (context) => _buildDialog(context), + builder: _buildDialog, ); } @@ -38,31 +38,31 @@ class SelectSourceDialog { style: Theme.of(context).textTheme.bodyText2, ), ), - RadioListTile( + RadioListTile( groupValue: _currentScheduleSource, value: ScheduleSourceType.Rapla, onChanged: (v) => sourceSelected(v, context), title: Text(L.of(context).scheduleSourceTypeRapla), ), - RadioListTile( + RadioListTile( groupValue: _currentScheduleSource, value: ScheduleSourceType.Dualis, onChanged: (v) => sourceSelected(v, context), title: Text(L.of(context).scheduleSourceTypeDualis), ), - RadioListTile( + RadioListTile( groupValue: _currentScheduleSource, value: ScheduleSourceType.Mannheim, onChanged: (v) => sourceSelected(v, context), title: Text(L.of(context).scheduleSourceTypeMannheim), ), - RadioListTile( + RadioListTile( groupValue: _currentScheduleSource, value: ScheduleSourceType.Ical, onChanged: (v) => sourceSelected(v, context), title: Text(L.of(context).scheduleSourceTypeIcal), ), - RadioListTile( + RadioListTile( groupValue: _currentScheduleSource, value: ScheduleSourceType.None, onChanged: (v) => sourceSelected(v, context), @@ -73,9 +73,11 @@ class SelectSourceDialog { } Future sourceSelected( - ScheduleSourceType type, + ScheduleSourceType? type, BuildContext context, ) async { + if (type == null) return; + // TODO: [Leptopoda] only switch the type when the setup is completed. _preferencesProvider.setScheduleSourceType(type.index); Navigator.of(context).pop(); diff --git a/lib/ui/banner_widget.dart b/lib/ui/banner_widget.dart index 7700d6e2..ad34acc2 100644 --- a/lib/ui/banner_widget.dart +++ b/lib/ui/banner_widget.dart @@ -5,12 +5,16 @@ class BannerWidget extends StatelessWidget { final String buttonText; final VoidCallback onButtonTap; - const BannerWidget({Key key, this.message, this.buttonText, this.onButtonTap}) - : super(key: key); + const BannerWidget({ + super.key, + required this.message, + required this.buttonText, + required this.onButtonTap, + }); @override Widget build(BuildContext context) { - return Container( + return DecoratedBox( decoration: BoxDecoration( color: Theme.of(context).scaffoldBackgroundColor, boxShadow: [ @@ -27,23 +31,19 @@ class BannerWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - message, + Text(message), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(0, 12, 0, 0), + child: TextButton( + onPressed: onButtonTap, + child: Text(buttonText), + ), + ), + ], ), - buttonText != null - ? Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(0, 12, 0, 0), - child: TextButton( - child: Text(buttonText), - onPressed: onButtonTap, - ), - ), - ], - ) - : Container(), ], ), ), diff --git a/lib/ui/login_credentials_widget.dart b/lib/ui/login_credentials_widget.dart index b8ee1134..9e2601ef 100644 --- a/lib/ui/login_credentials_widget.dart +++ b/lib/ui/login_credentials_widget.dart @@ -1,67 +1,43 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; +import 'package:dhbwstudentapp/dualis/model/credentials.dart'; import 'package:flutter/material.dart'; -class LoginCredentialsWidget extends StatefulWidget { - final TextEditingController usernameEditingController; - final TextEditingController passwordEditingController; - final Function onSubmitted; +class LoginCredentialsWidget extends StatelessWidget { + final CredentialsEditingController controller; + final VoidCallback onSubmitted; const LoginCredentialsWidget({ - Key key, - this.usernameEditingController, - this.passwordEditingController, - this.onSubmitted, - }) : super(key: key); - - @override - _LoginCredentialsWidgetState createState() => _LoginCredentialsWidgetState( - usernameEditingController, - passwordEditingController, - onSubmitted, - ); -} - -class _LoginCredentialsWidgetState extends State { - final TextEditingController _usernameEditingController; - final TextEditingController _passwordEditingController; - final Function _onSubmitted; - - final _focus = FocusNode(); - - _LoginCredentialsWidgetState( - this._usernameEditingController, - this._passwordEditingController, - this._onSubmitted, - ); + super.key, + required this.controller, + required this.onSubmitted, + }); @override Widget build(BuildContext context) { + final focus = FocusNode(); + return Column( children: [ TextField( - controller: _usernameEditingController, + controller: controller.username, decoration: InputDecoration( hintText: L.of(context).loginUsername, - icon: Icon(Icons.alternate_email), + icon: const Icon(Icons.alternate_email), ), onSubmitted: (v) { - FocusScope.of(context).requestFocus(_focus); + FocusScope.of(context).requestFocus(focus); }, textInputAction: TextInputAction.next, ), TextField( - controller: _passwordEditingController, + controller: controller.password, obscureText: true, decoration: InputDecoration( hintText: L.of(context).loginPassword, - icon: Icon(Icons.lock_outline), + icon: const Icon(Icons.lock_outline), ), - focusNode: _focus, - onSubmitted: (v) { - if (_onSubmitted != null) { - _onSubmitted(); - } - }, + focusNode: focus, + onSubmitted: (_) => onSubmitted(), ), ], ); diff --git a/lib/ui/main_page.dart b/lib/ui/main_page.dart index 9584c87c..bd70cf86 100644 --- a/lib/ui/main_page.dart +++ b/lib/ui/main_page.dart @@ -15,6 +15,8 @@ import 'package:provider/provider.dart'; /// To navigate to a new route inside this widget use the [NavigatorKey.mainKey] /// class MainPage extends StatefulWidget { + const MainPage({super.key}); + @override _MainPageState createState() => _MainPageState(); } @@ -27,16 +29,13 @@ class _MainPageState extends State with NavigatorObserver { NavigationEntry get currentEntry => navigationEntries[_currentEntryIndex.value]; - @override - void initState() { - super.initState(); - } + _MainPageState(); @override Widget build(BuildContext context) { _showAppLaunchDialogsIfNeeded(context); - var navigator = Navigator( + final navigator = Navigator( key: NavigatorKey.mainKey, onGenerateRoute: generateDrawerRoute, initialRoute: "schedule", @@ -46,7 +45,7 @@ class _MainPageState extends State with NavigatorObserver { return ChangeNotifierProvider.value( value: _currentEntryIndex, child: Consumer>( - builder: (BuildContext context, value, Widget child) { + builder: (BuildContext context, value, Widget? child) { Widget content; if (PlatformUtil.isPhone() || PlatformUtil.isPortrait(context)) { @@ -64,11 +63,11 @@ class _MainPageState extends State with NavigatorObserver { Widget buildPhoneLayout(BuildContext context, Navigator navigator) { return WillPopScope( onWillPop: () async { - var canPop = NavigatorKey.mainKey.currentState.canPop(); + final canPop = NavigatorKey.mainKey.currentState!.canPop(); if (!canPop) return true; - NavigatorKey.mainKey.currentState.pop(); + NavigatorKey.mainKey.currentState!.pop(); return false; }, @@ -122,8 +121,8 @@ class _MainPageState extends State with NavigatorObserver { width: 1, ), Expanded( - child: navigator, flex: 3, + child: navigator, ), ], ), @@ -131,13 +130,15 @@ class _MainPageState extends State with NavigatorObserver { } List _buildDrawerEntries() { - var drawerEntries = []; + final drawerEntries = []; - for (var entry in navigationEntries) { - drawerEntries.add(DrawerNavigationEntry( - entry.icon(context), - entry.title(context), - )); + for (final entry in navigationEntries) { + drawerEntries.add( + DrawerNavigationEntry( + entry.icon, + entry.title(context), + ), + ); } return drawerEntries; @@ -146,7 +147,7 @@ class _MainPageState extends State with NavigatorObserver { void _onNavigationTapped(int index) { _currentEntryIndex.value = index; - NavigatorKey.mainKey.currentState + NavigatorKey.mainKey.currentState! .pushNamedAndRemoveUntil(currentEntry.route, (route) { return route.settings.name == navigationEntries[0].route; }); @@ -160,7 +161,7 @@ class _MainPageState extends State with NavigatorObserver { } } - void updateNavigationDrawer(String routeName) { + void updateNavigationDrawer(String? routeName) { for (int i = 0; i < navigationEntries.length; i++) { if (navigationEntries[i].route == routeName) { _currentEntryIndex.value = i; @@ -170,17 +171,21 @@ class _MainPageState extends State with NavigatorObserver { } @override - void didPop(Route route, Route previousRoute) { - updateNavigationDrawer(previousRoute.settings.name); + void didPop(Route route, Route? previousRoute) { + if (previousRoute != null) { + updateNavigationDrawer(previousRoute.settings.name); + } } @override - void didPush(Route route, Route previousRoute) { + void didPush(Route route, Route? previousRoute) { updateNavigationDrawer(route.settings.name); } @override - void didReplace({Route newRoute, Route oldRoute}) { - updateNavigationDrawer(newRoute.settings.name); + void didReplace({Route? newRoute, Route? oldRoute}) { + if (newRoute != null) { + updateNavigationDrawer(newRoute.settings.name); + } } } diff --git a/lib/ui/navigation/navigation_entry.dart b/lib/ui/navigation/navigation_entry.dart index dedbf575..128c7772 100644 --- a/lib/ui/navigation/navigation_entry.dart +++ b/lib/ui/navigation/navigation_entry.dart @@ -2,38 +2,32 @@ import 'package:dhbwstudentapp/common/ui/viewmodels/base_view_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -abstract class NavigationEntry { - BaseViewModel _viewModel; +abstract class NavigationEntry { + NavigationEntry(); + + T? _viewModel; String get route; String title(BuildContext context); - Widget icon(BuildContext context); + Icon get icon; Widget buildRoute(BuildContext context) { - var model = viewModel(); - if (model != null) { - return ChangeNotifierProvider.value( - value: model, - child: build(context), - ); - } else { - return build(context); - } + return ChangeNotifierProvider.value( + value: model, + child: build(context), + ); } Widget build(BuildContext context); - BaseViewModel viewModel() { - if (_viewModel == null) { - _viewModel = initViewModel(); - } - - return _viewModel; + T get model { + return _viewModel ??= initViewModel(); } - BaseViewModel initViewModel() => null; + T initViewModel(); - List appBarActions(BuildContext context) => []; + // TODO: [Leptopoda] clean up but not null related so something for later + List appBarActions(BuildContext context); } diff --git a/lib/ui/navigation/navigator_key.dart b/lib/ui/navigation/navigator_key.dart index 1dee2eef..86556a16 100644 --- a/lib/ui/navigation/navigator_key.dart +++ b/lib/ui/navigation/navigator_key.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; class NavigatorKey { + const NavigatorKey(); static final mainKey = GlobalKey(); static final rootKey = GlobalKey(); } diff --git a/lib/ui/navigation/router.dart b/lib/ui/navigation/router.dart index 72c5fe58..160effe3 100644 --- a/lib/ui/navigation/router.dart +++ b/lib/ui/navigation/router.dart @@ -18,9 +18,9 @@ final List navigationEntries = [ Route generateDrawerRoute(RouteSettings settings) { print("=== === === === === === Navigating to: ${settings.name}"); - WidgetBuilder widget; + WidgetBuilder? widget; - for (var route in navigationEntries) { + for (final route in navigationEntries) { if (route.route == settings.name) { widget = route.buildRoute; break; @@ -28,17 +28,17 @@ Route generateDrawerRoute(RouteSettings settings) { } if (widget == null) { - print("Failed to navigate to: " + settings.name); + print("Failed to navigate to: ${settings.name!}"); widget = (BuildContext context) => Container(); } return PageRouteBuilder( settings: settings, transitionDuration: const Duration(milliseconds: 200), - pageBuilder: (context, animation, secondaryAnimation) => widget(context), + pageBuilder: (context, animation, secondaryAnimation) => widget!(context), transitionsBuilder: (context, animation, secondaryAnimation, child) { const offsetBegin = Offset(0.0, 0.005); - final offsetEnd = Offset.zero; + const offsetEnd = Offset.zero; final offsetTween = Tween(begin: offsetBegin, end: offsetEnd) .chain(CurveTween(curve: Curves.fastOutSlowIn)); @@ -51,9 +51,9 @@ Route generateDrawerRoute(RouteSettings settings) { position: animation.drive(offsetTween), child: FadeTransition( opacity: animation.drive(opacityTween), - child: Container( - child: child, + child: ColoredBox( color: Theme.of(context).scaffoldBackgroundColor, + child: child, ), ), ); @@ -70,13 +70,13 @@ Route generateRoute(RouteSettings settings) { target = const OnboardingPage(); break; case "main": - target = MainPage(); + target = const MainPage(); break; case "settings": - target = SettingsPage(); + target = const SettingsPage(); break; default: - print("Failed to navigate to: " + settings.name); + print("Failed to navigate to: ${settings.name!}"); target = Container(); } diff --git a/lib/ui/navigation_drawer.dart b/lib/ui/navigation_drawer.dart index 6266b583..023b81a4 100644 --- a/lib/ui/navigation_drawer.dart +++ b/lib/ui/navigation_drawer.dart @@ -16,36 +16,39 @@ class NavigationDrawer extends StatelessWidget { final bool isInDrawer; const NavigationDrawer({ - Key key, - this.selectedIndex, - this.onTap, - this.entries, + super.key, + required this.selectedIndex, + required this.onTap, + required this.entries, this.isInDrawer = true, - }) : super(key: key); + }); @override Widget build(BuildContext context) { - var widgets = []; + final widgets = []; if (isInDrawer) { widgets.add(_createHeader(context)); } int i = 0; - for (var entry in entries) { - widgets.add(_createDrawerItem(context, + for (final entry in entries) { + widgets.add( + _createDrawerItem( + context, icon: entry.icon, text: entry.title, index: i, - isSelected: i == selectedIndex)); + isSelected: i == selectedIndex, + ), + ); i++; } widgets.add(_createSettingsItem(context)); - var widget = Column( - mainAxisAlignment: MainAxisAlignment.start, + final widget = Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: widgets, ); @@ -81,10 +84,10 @@ class NavigationDrawer extends StatelessWidget { Widget _createDrawerItem( BuildContext context, { - Widget icon, - String text, - bool isSelected, - int index, + required Widget icon, + required String text, + required bool isSelected, + required int index, }) { return Padding( padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), @@ -95,12 +98,11 @@ class NavigationDrawer extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: InkWell( - child: Container( + child: SizedBox( height: 48, child: Padding( padding: const EdgeInsets.fromLTRB(8, 0, 0, 0), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ icon, Padding( @@ -132,26 +134,23 @@ class NavigationDrawer extends StatelessWidget { child: Container( height: 56, margin: const EdgeInsets.fromLTRB(0, 0, 0, 20), - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 15, 0, 10), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon( - Icons.settings, - color: Theme.of(context).disabledColor, - ), - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 0, 0), - child: Text( - L.of(context).settingsPageTitle, - style: TextStyle( - color: Theme.of(context).disabledColor, - ), + padding: const EdgeInsets.fromLTRB(20, 15, 0, 10), + child: Row( + children: [ + Icon( + Icons.settings, + color: Theme.of(context).disabledColor, + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 0, 0), + child: Text( + L.of(context).settingsPageTitle, + style: TextStyle( + color: Theme.of(context).disabledColor, ), ), - ], - ), + ), + ], ), ), onTap: () { @@ -169,8 +168,8 @@ class NavigationDrawer extends StatelessWidget { } class DrawerNavigationEntry { - final Widget icon; + final Icon icon; final String title; - DrawerNavigationEntry(this.icon, this.title); + const DrawerNavigationEntry(this.icon, this.title); } diff --git a/lib/ui/onboarding/onboardin_step.dart b/lib/ui/onboarding/onboardin_step.dart index 37ee31aa..d36f61f4 100644 --- a/lib/ui/onboarding/onboardin_step.dart +++ b/lib/ui/onboarding/onboardin_step.dart @@ -1,8 +1,8 @@ import 'package:dhbwstudentapp/ui/onboarding/viewmodels/dualis_login_view_model.dart'; import 'package:dhbwstudentapp/ui/onboarding/viewmodels/ical_url_view_model.dart'; import 'package:dhbwstudentapp/ui/onboarding/viewmodels/mannheim_view_model.dart'; -import 'package:dhbwstudentapp/ui/onboarding/viewmodels/rapla_url_view_model.dart'; import 'package:dhbwstudentapp/ui/onboarding/viewmodels/onboarding_view_model_base.dart'; +import 'package:dhbwstudentapp/ui/onboarding/viewmodels/rapla_url_view_model.dart'; import 'package:dhbwstudentapp/ui/onboarding/viewmodels/select_source_view_model.dart'; import 'package:dhbwstudentapp/ui/onboarding/widgets/dualis_login_page.dart'; import 'package:dhbwstudentapp/ui/onboarding/widgets/ical_url_page.dart'; @@ -13,25 +13,29 @@ import 'package:flutter/cupertino.dart'; import 'package:kiwi/kiwi.dart'; abstract class OnboardingStep { + const OnboardingStep(); + Widget buildContent(BuildContext context); OnboardingStepViewModel viewModel(); - String nextStep(); + String? nextStep(); } class SelectSourceOnboardingStep extends OnboardingStep { + SelectSourceOnboardingStep(); + final SelectSourceViewModel _viewModel = SelectSourceViewModel( KiwiContainer().resolve(), ); @override Widget buildContent(BuildContext context) { - return SelectSourcePage(); + return const SelectSourcePage(); } @override - String nextStep() { + String? nextStep() { return _viewModel.nextStep(); } @@ -42,6 +46,8 @@ class SelectSourceOnboardingStep extends OnboardingStep { } class DualisCredentialsOnboardingStep extends OnboardingStep { + DualisCredentialsOnboardingStep(); + final DualisLoginViewModel _viewModel = DualisLoginViewModel( KiwiContainer().resolve(), KiwiContainer().resolve(), @@ -49,11 +55,11 @@ class DualisCredentialsOnboardingStep extends OnboardingStep { @override Widget buildContent(BuildContext context) { - return DualisLoginCredentialsPage(); + return const DualisLoginCredentialsPage(); } @override - String nextStep() { + String? nextStep() { return null; } @@ -64,14 +70,16 @@ class DualisCredentialsOnboardingStep extends OnboardingStep { } class RaplaOnboardingStep extends OnboardingStep { - RaplaUrlViewModel _viewModel = RaplaUrlViewModel( + RaplaOnboardingStep(); + + final RaplaUrlViewModel _viewModel = RaplaUrlViewModel( KiwiContainer().resolve(), KiwiContainer().resolve(), ); @override Widget buildContent(BuildContext context) { - return RaplaUrlPage(); + return const RaplaUrlPage(); } @override @@ -86,14 +94,16 @@ class RaplaOnboardingStep extends OnboardingStep { } class IcalOnboardingStep extends OnboardingStep { - IcalUrlViewModel _viewModel = IcalUrlViewModel( + IcalOnboardingStep(); + + final IcalUrlViewModel _viewModel = IcalUrlViewModel( KiwiContainer().resolve(), KiwiContainer().resolve(), ); @override Widget buildContent(BuildContext context) { - return IcalUrlPage(); + return const IcalUrlPage(); } @override @@ -108,11 +118,12 @@ class IcalOnboardingStep extends OnboardingStep { } class MannheimOnboardingStep extends OnboardingStep { - MannheimViewModel _viewModel; + MannheimViewModel? _viewModel; + MannheimOnboardingStep(); @override Widget buildContent(BuildContext context) { - return MannheimPage(); + return const MannheimPage(); } @override @@ -122,12 +133,8 @@ class MannheimOnboardingStep extends OnboardingStep { @override OnboardingStepViewModel viewModel() { - if (_viewModel == null) { - _viewModel = MannheimViewModel( - KiwiContainer().resolve(), - ); - } - - return _viewModel; + return _viewModel ??= MannheimViewModel( + KiwiContainer().resolve(), + ); } } diff --git a/lib/ui/onboarding/onboarding_page.dart b/lib/ui/onboarding/onboarding_page.dart index 58a9f086..5e832802 100644 --- a/lib/ui/onboarding/onboarding_page.dart +++ b/lib/ui/onboarding/onboarding_page.dart @@ -1,5 +1,4 @@ import 'package:animations/animations.dart'; -import 'package:dhbwstudentapp/common/ui/viewmodels/base_view_model.dart'; import 'package:dhbwstudentapp/common/ui/viewmodels/root_view_model.dart'; import 'package:dhbwstudentapp/ui/onboarding/viewmodels/onboarding_view_model.dart'; import 'package:dhbwstudentapp/ui/onboarding/viewmodels/onboarding_view_model_base.dart'; @@ -10,7 +9,7 @@ import 'package:kiwi/kiwi.dart'; import 'package:property_change_notifier/property_change_notifier.dart'; class OnboardingPage extends StatefulWidget { - const OnboardingPage({Key key}) : super(key: key); + const OnboardingPage({super.key}); @override _OnboardingPageState createState() => _OnboardingPageState(); @@ -18,8 +17,8 @@ class OnboardingPage extends StatefulWidget { class _OnboardingPageState extends State with TickerProviderStateMixin { - AnimationController _controller; - OnboardingViewModel viewModel; + late AnimationController _controller; + late OnboardingViewModel viewModel; _OnboardingPageState(); @@ -35,9 +34,10 @@ class _OnboardingPageState extends State viewModel.addListener( () async { await _controller.animateTo( - viewModel.stepIndex / viewModel.onboardingSteps, - curve: Curves.ease, - duration: const Duration(milliseconds: 300)); + viewModel.stepIndex! / viewModel.onboardingSteps, + curve: Curves.ease, + duration: const Duration(milliseconds: 300), + ); }, ); @@ -71,16 +71,14 @@ class _OnboardingPageState extends State behavior: HitTestBehavior.translucent, child: Center( child: Container( - constraints: BoxConstraints(maxWidth: 500), + constraints: const BoxConstraints(maxWidth: 500), child: Padding( padding: const EdgeInsets.fromLTRB(0, 120, 0, 90), child: PropertyChangeConsumer( - builder: (BuildContext context, BaseViewModel model, _) { + builder: (BuildContext context, OnboardingViewModel? model, _) { return Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, children: [ - Expanded(child: _buildActiveOnboardingPage(model)), + Expanded(child: _buildActiveOnboardingPage(model!)), OnboardingButtonBar( onPrevious: () { _navigateBack(context); @@ -101,36 +99,33 @@ class _OnboardingPageState extends State } Widget _buildActiveOnboardingPage(OnboardingViewModel model) { - var currentStep = model.pages[model.currentStep]; - var contentWidget = currentStep.buildContent(context); + final currentStep = model.pages[model.currentStep]!; + final contentWidget = currentStep.buildContent(context); Widget body = Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), child: contentWidget, ); - if (currentStep != null) { - body = PropertyChangeProvider( - key: ValueKey(model.currentStep), - value: currentStep.viewModel(), - child: body, - ); - } + body = PropertyChangeProvider( + key: ValueKey(model.currentStep), + value: currentStep.viewModel(), + child: body, + ); return IntrinsicHeight( child: PageTransitionSwitcher( reverse: !model.didStepForward, - duration: const Duration(milliseconds: 300), transitionBuilder: ( Widget child, Animation animation, Animation secondaryAnimation, ) => SharedAxisTransition( - child: child, animation: animation, secondaryAnimation: secondaryAnimation, transitionType: SharedAxisTransitionType.horizontal, + child: child, ), child: body, ), @@ -146,7 +141,8 @@ class _OnboardingPageState extends State } void _onboardingFinished() { - var rootViewModel = PropertyChangeProvider.of(context).value; + final rootViewModel = + PropertyChangeProvider.of(context)!.value; rootViewModel.setIsOnboarding(false); Navigator.of(context).pushReplacementNamed("main"); diff --git a/lib/ui/onboarding/viewmodels/dualis_login_view_model.dart b/lib/ui/onboarding/viewmodels/dualis_login_view_model.dart index d040074b..ab609ca8 100644 --- a/lib/ui/onboarding/viewmodels/dualis_login_view_model.dart +++ b/lib/ui/onboarding/viewmodels/dualis_login_view_model.dart @@ -7,8 +7,7 @@ class DualisLoginViewModel extends OnboardingStepViewModel { final PreferencesProvider preferencesProvider; final DualisService dualisService; - String username; - String password; + Credentials? credentials; bool _isLoading = false; bool get isLoading => _isLoading; @@ -16,27 +15,28 @@ class DualisLoginViewModel extends OnboardingStepViewModel { bool _loginSuccess = false; bool get loginSuccess => _loginSuccess; - bool _passwordOrUsernameWrong = false; - bool get passwordOrUsernameWrong => _passwordOrUsernameWrong; - DualisLoginViewModel(this.preferencesProvider, this.dualisService); - Future testCredentials(String username, String password) async { - this.username = username; - this.password = password; + @override + set isValid(bool value) { + if (credentials == null) { + super.isValid = false; + } else { + super.isValid = value; + } + } + Future testCredentials(Credentials credentials) async { try { _isLoading = true; notifyListeners("isLoading"); _loginSuccess = - await dualisService.login(username, password) == LoginResult.LoggedIn; - _passwordOrUsernameWrong = !_loginSuccess; - setIsValid(_loginSuccess); + await dualisService.login(credentials) == LoginResult.LoggedIn; + isValid = _loginSuccess; } catch (ex) { - setIsValid(false); - _passwordOrUsernameWrong = true; + isValid = false; } finally { _isLoading = false; } @@ -46,13 +46,12 @@ class DualisLoginViewModel extends OnboardingStepViewModel { @override Future save() async { + if (!isValid) return; + await preferencesProvider.setStoreDualisCredentials(true); // To improve the onboarding experience we do not await this call because // it may take a few seconds - preferencesProvider.storeDualisCredentials(Credentials( - username, - password, - )); + preferencesProvider.storeDualisCredentials(credentials!); } } diff --git a/lib/ui/onboarding/viewmodels/ical_url_view_model.dart b/lib/ui/onboarding/viewmodels/ical_url_view_model.dart index 78ed03e6..dda3d155 100644 --- a/lib/ui/onboarding/viewmodels/ical_url_view_model.dart +++ b/lib/ui/onboarding/viewmodels/ical_url_view_model.dart @@ -8,14 +8,14 @@ class IcalUrlViewModel extends OnboardingStepViewModel { final PreferencesProvider preferencesProvider; final ScheduleSourceProvider scheduleSourceProvider; - String _url; - String get url => _url; + String? _url; + String? get url => _url; bool urlHasError = false; IcalUrlViewModel(this.preferencesProvider, this.scheduleSourceProvider); - void setUrl(String url) { + set url(String? url) { _url = url; notifyListeners("url"); @@ -26,16 +26,16 @@ class IcalUrlViewModel extends OnboardingStepViewModel { void _validateUrl() { urlHasError = !IcalScheduleSource.isValidUrl(_url); - setIsValid(!urlHasError); + isValid = !urlHasError; notifyListeners("urlHasError"); } Future pasteUrl() async { - ClipboardData data = await Clipboard.getData('text/plain'); + final ClipboardData? data = await Clipboard.getData('text/plain'); if (data?.text != null) { - setUrl(data.text); + url = data!.text; } } diff --git a/lib/ui/onboarding/viewmodels/mannheim_view_model.dart b/lib/ui/onboarding/viewmodels/mannheim_view_model.dart index 757125fa..4037c0ee 100644 --- a/lib/ui/onboarding/viewmodels/mannheim_view_model.dart +++ b/lib/ui/onboarding/viewmodels/mannheim_view_model.dart @@ -14,14 +14,14 @@ class MannheimViewModel extends OnboardingStepViewModel { LoadCoursesState _loadingState = LoadCoursesState.Loading; LoadCoursesState get loadingState => _loadingState; - Course _selectedCourse; - Course get selectedCourse => _selectedCourse; + Course? _selectedCourse; + Course? get selectedCourse => _selectedCourse; - List _courses; - List get courses => _courses; + List? _courses; + List? get courses => _courses; MannheimViewModel(this._scheduleSourceProvider) { - setIsValid(false); + isValid = false; loadCourses(); } @@ -30,8 +30,8 @@ class MannheimViewModel extends OnboardingStepViewModel { notifyListeners("loadingState"); try { - await Future.delayed(Duration(seconds: 1)); - _courses = await MannheimCourseScraper().loadCourses(); + await Future.delayed(const Duration(seconds: 1)); + _courses = await const MannheimCourseScraper().loadCourses(); _loadingState = LoadCoursesState.Loaded; } catch (ex) { _courses = null; @@ -49,7 +49,7 @@ class MannheimViewModel extends OnboardingStepViewModel { _selectedCourse = course; } - setIsValid(_selectedCourse != null); + isValid = _selectedCourse != null; } @override diff --git a/lib/ui/onboarding/viewmodels/onboarding_view_model.dart b/lib/ui/onboarding/viewmodels/onboarding_view_model.dart index 9128e541..2460615d 100644 --- a/lib/ui/onboarding/viewmodels/onboarding_view_model.dart +++ b/lib/ui/onboarding/viewmodels/onboarding_view_model.dart @@ -28,14 +28,14 @@ class OnboardingViewModel extends BaseViewModel { final Map stepsBackstack = {}; int _stepIndex = 0; - int get stepIndex => _stepIndex; + int? get stepIndex => _stepIndex; String get currentStep => steps[_stepIndex]; - bool get currentPageValid => pages[currentStep].viewModel().isValid; + bool get currentPageValid => pages[currentStep]!.viewModel().isValid; bool get isLastStep => _stepIndex >= steps.length - 1; - get onboardingSteps => steps.length; + int get onboardingSteps => steps.length; bool _didStepForward = true; bool get didStepForward => _didStepForward; @@ -44,10 +44,13 @@ class OnboardingViewModel extends BaseViewModel { this.preferencesProvider, this._onboardingFinished, ) { - for (var page in pages.values) { - page.viewModel().addListener(() { - notifyListeners("currentPageValid"); - }, ["isValid"]); + for (final page in pages.values) { + page.viewModel().addListener( + () { + notifyListeners("currentPageValid"); + }, + ["isValid"], + ); } analytics.logTutorialBegin(); @@ -55,13 +58,13 @@ class OnboardingViewModel extends BaseViewModel { } void previousPage() { - if (stepsBackstack.length == 0) { + if (stepsBackstack.isEmpty) { return; } - var lastPage = stepsBackstack.keys.last; + final lastPage = stepsBackstack.keys.last; - _stepIndex = stepsBackstack[lastPage]; + _stepIndex = stepsBackstack[lastPage]!; stepsBackstack.remove(lastPage); @@ -77,13 +80,11 @@ class OnboardingViewModel extends BaseViewModel { return; } - var nextDesiredStep = pages[currentStep].nextStep(); + var nextDesiredStep = pages[currentStep]!.nextStep(); stepsBackstack[currentStep] = _stepIndex; - if (nextDesiredStep == null) { - nextDesiredStep = steps[_stepIndex + 1]; - } + nextDesiredStep ??= steps[_stepIndex + 1]; while (nextDesiredStep != currentStep) { _stepIndex++; @@ -95,11 +96,11 @@ class OnboardingViewModel extends BaseViewModel { } Future finishOnboarding() async { - for (var step in stepsBackstack.keys) { - await pages[step].viewModel().save(); + for (final step in stepsBackstack.keys) { + await pages[step]!.viewModel().save(); } - _onboardingFinished?.call(); + _onboardingFinished.call(); await analytics.logTutorialComplete(); await analytics.setUserProperty(name: "onboarding_finished", value: "true"); diff --git a/lib/ui/onboarding/viewmodels/onboarding_view_model_base.dart b/lib/ui/onboarding/viewmodels/onboarding_view_model_base.dart index 26ebd7da..9504a2b7 100644 --- a/lib/ui/onboarding/viewmodels/onboarding_view_model_base.dart +++ b/lib/ui/onboarding/viewmodels/onboarding_view_model_base.dart @@ -1,10 +1,12 @@ import 'package:dhbwstudentapp/common/ui/viewmodels/base_view_model.dart'; abstract class OnboardingStepViewModel extends BaseViewModel { + OnboardingStepViewModel(); + bool _isValid = false; bool get isValid => _isValid; - void setIsValid(bool isValid) { + set isValid(bool isValid) { _isValid = isValid; notifyListeners("isValid"); } diff --git a/lib/ui/onboarding/viewmodels/rapla_url_view_model.dart b/lib/ui/onboarding/viewmodels/rapla_url_view_model.dart index 76fbf4c1..cd4072af 100644 --- a/lib/ui/onboarding/viewmodels/rapla_url_view_model.dart +++ b/lib/ui/onboarding/viewmodels/rapla_url_view_model.dart @@ -8,8 +8,8 @@ class RaplaUrlViewModel extends OnboardingStepViewModel { final PreferencesProvider preferencesProvider; final ScheduleSourceProvider scheduleSourceProvider; - String _raplaUrl; - String get raplaUrl => _raplaUrl; + String? _raplaUrl; + String? get raplaUrl => _raplaUrl; bool urlHasError = false; @@ -18,7 +18,7 @@ class RaplaUrlViewModel extends OnboardingStepViewModel { this.scheduleSourceProvider, ); - void setRaplaUrl(String url) { + void setRaplaUrl(String? url) { _raplaUrl = url; notifyListeners("raplaUrl"); @@ -27,18 +27,18 @@ class RaplaUrlViewModel extends OnboardingStepViewModel { } void _validateUrl() { - urlHasError = !RaplaScheduleSource.isValidUrl(_raplaUrl); + urlHasError = !RaplaScheduleSource.isValidUrl(_raplaUrl!); - setIsValid(!urlHasError); + isValid = !urlHasError; notifyListeners("urlHasError"); } Future pasteUrl() async { - ClipboardData data = await Clipboard.getData('text/plain'); + final ClipboardData? data = await Clipboard.getData('text/plain'); if (data?.text != null) { - setRaplaUrl(data.text); + setRaplaUrl(data!.text); } } diff --git a/lib/ui/onboarding/viewmodels/select_source_view_model.dart b/lib/ui/onboarding/viewmodels/select_source_view_model.dart index ec95f6fa..ad94586d 100644 --- a/lib/ui/onboarding/viewmodels/select_source_view_model.dart +++ b/lib/ui/onboarding/viewmodels/select_source_view_model.dart @@ -9,12 +9,14 @@ class SelectSourceViewModel extends OnboardingStepViewModel { ScheduleSourceType get scheduleSourceType => _scheduleSourceType; SelectSourceViewModel(this._preferencesProvider) { - setIsValid(true); + isValid = true; } - void setScheduleSourceType(ScheduleSourceType type) { + void setScheduleSourceType(ScheduleSourceType? type) { + if (type == null) return; + _scheduleSourceType = type; - setIsValid(_scheduleSourceType != null); + isValid = true; notifyListeners("scheduleSourceType"); } @@ -24,7 +26,7 @@ class SelectSourceViewModel extends OnboardingStepViewModel { await _preferencesProvider.setScheduleSourceType(scheduleSourceType.index); } - String nextStep() { + String? nextStep() { switch (_scheduleSourceType) { case ScheduleSourceType.Rapla: return "rapla"; @@ -36,8 +38,8 @@ class SelectSourceViewModel extends OnboardingStepViewModel { return "mannheim"; case ScheduleSourceType.Ical: return "ical"; + default: + return null; } - - return null; } } diff --git a/lib/ui/onboarding/widgets/dualis_login_page.dart b/lib/ui/onboarding/widgets/dualis_login_page.dart index 52630741..b0da52db 100644 --- a/lib/ui/onboarding/widgets/dualis_login_page.dart +++ b/lib/ui/onboarding/widgets/dualis_login_page.dart @@ -1,4 +1,5 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; +import 'package:dhbwstudentapp/dualis/model/credentials.dart'; import 'package:dhbwstudentapp/ui/login_credentials_widget.dart'; import 'package:dhbwstudentapp/ui/onboarding/viewmodels/dualis_login_view_model.dart'; import 'package:dhbwstudentapp/ui/onboarding/viewmodels/onboarding_view_model_base.dart'; @@ -10,46 +11,56 @@ import 'package:property_change_notifier/property_change_notifier.dart'; /// credentials. /// class DualisLoginCredentialsPage extends StatefulWidget { + const DualisLoginCredentialsPage({super.key}); + @override - _DualisLoginCredentialsPageState createState() => + State createState() => _DualisLoginCredentialsPageState(); } class _DualisLoginCredentialsPageState extends State { - final TextEditingController _usernameEditingController = - TextEditingController(); - final TextEditingController _passwordEditingController = - TextEditingController(); + final CredentialsEditingController controller = + CredentialsEditingController(); + + _DualisLoginCredentialsPageState(); + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { return PropertyChangeConsumer( - builder: - (BuildContext context, OnboardingStepViewModel base, Set _) { - var viewModel = base as DualisLoginViewModel; + builder: ( + BuildContext context, + OnboardingStepViewModel? base, + Set? _, + ) { + final viewModel = base as DualisLoginViewModel?; - if (_usernameEditingController.text != viewModel.username) { - _usernameEditingController.text = viewModel.username; - } - if (_passwordEditingController.text != viewModel.password) { - _passwordEditingController.text = viewModel.password; + final credentials = viewModel?.credentials; + + if (credentials != null) { + controller.credentials = credentials; } - var widgets = []; + final widgets = []; widgets.addAll(_buildHeader()); - widgets.add(LoginCredentialsWidget( - usernameEditingController: _usernameEditingController, - passwordEditingController: _passwordEditingController, - onSubmitted: () async { - await _testCredentials(viewModel); - }, - )); + widgets.add( + LoginCredentialsWidget( + controller: controller, + onSubmitted: () async { + await _testCredentials(viewModel); + }, + ), + ); widgets.add(_buildTestCredentialsButton(viewModel)); return Column( - mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: widgets, ); @@ -57,7 +68,7 @@ class _DualisLoginCredentialsPageState ); } - Widget _buildTestCredentialsButton(DualisLoginViewModel viewModel) { + Widget _buildTestCredentialsButton(DualisLoginViewModel? viewModel) { return Expanded( child: SizedBox( height: 64, @@ -66,10 +77,9 @@ class _DualisLoginCredentialsPageState child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( - child: viewModel.passwordOrUsernameWrong + child: viewModel?.isValid == false ? Text( L.of(context).onboardingDualisWrongCredentials, textAlign: TextAlign.start, @@ -77,28 +87,31 @@ class _DualisLoginCredentialsPageState ) : Container(), ), - viewModel.isLoading - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 1, - ), - ) - : viewModel.loginSuccess - ? const Icon( - Icons.check, - color: Colors.green, - ) - : TextButton( - onPressed: () async { - await _testCredentials(viewModel); - }, - child: Text(L + if (viewModel?.isLoading ?? false) + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 1, + ), + ) + else + viewModel?.loginSuccess ?? false + ? const Icon( + Icons.check, + color: Colors.green, + ) + : TextButton( + onPressed: () async { + await _testCredentials(viewModel); + }, + child: Text( + L .of(context) .onboardingDualisTestButton - .toUpperCase()), + .toUpperCase(), ), + ), ], ), ), @@ -106,11 +119,10 @@ class _DualisLoginCredentialsPageState ); } - Future _testCredentials(DualisLoginViewModel viewModel) async { - await viewModel.testCredentials( - _usernameEditingController.text, - _passwordEditingController.text, - ); + Future _testCredentials(DualisLoginViewModel? viewModel) async { + if (viewModel == null) return; + + await viewModel.testCredentials(controller.credentials); } List _buildHeader() { diff --git a/lib/ui/onboarding/widgets/ical_url_page.dart b/lib/ui/onboarding/widgets/ical_url_page.dart index dfd690bc..29d737d9 100644 --- a/lib/ui/onboarding/widgets/ical_url_page.dart +++ b/lib/ui/onboarding/widgets/ical_url_page.dart @@ -4,18 +4,13 @@ import 'package:dhbwstudentapp/ui/onboarding/viewmodels/onboarding_view_model_ba import 'package:flutter/material.dart'; import 'package:property_change_notifier/property_change_notifier.dart'; -class IcalUrlPage extends StatefulWidget { - @override - _IcalUrlPageState createState() => _IcalUrlPageState(); -} - -class _IcalUrlPageState extends State { - final TextEditingController _urlTextController = TextEditingController(); +class IcalUrlPage extends StatelessWidget { + const IcalUrlPage({super.key}); @override Widget build(BuildContext context) { + final TextEditingController urlTextController = TextEditingController(); return Column( - mainAxisAlignment: MainAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(32, 0, 32, 0), @@ -40,12 +35,17 @@ class _IcalUrlPageState extends State { child: Padding( padding: const EdgeInsets.fromLTRB(0, 32, 0, 0), child: PropertyChangeConsumer( - builder: (BuildContext context, OnboardingStepViewModel model, - Set _) { - var viewModel = model as IcalUrlViewModel; + builder: ( + BuildContext context, + OnboardingStepViewModel? model, + Set? _, + ) { + final viewModel = model as IcalUrlViewModel?; - if (_urlTextController.text != viewModel.url) - _urlTextController.text = viewModel.url; + if (viewModel?.url != null && + urlTextController.text != viewModel!.url) { + urlTextController.text = viewModel.url!; + } return Padding( padding: const EdgeInsets.fromLTRB(0, 0, 0, 16), @@ -54,23 +54,21 @@ class _IcalUrlPageState extends State { children: [ Expanded( child: TextField( - controller: _urlTextController, + controller: urlTextController, decoration: InputDecoration( - errorText: (viewModel.urlHasError ?? false) + errorText: (viewModel?.urlHasError == true) ? L.of(context).onboardingRaplaUrlInvalid : null, hintText: L.of(context).onboardingIcalUrlHint, ), - onChanged: (value) { - viewModel.setUrl(value); - }, + onChanged: (v) => viewModel?.url = v, ), ), TextButton.icon( onPressed: () async { - await viewModel.pasteUrl(); + await viewModel?.pasteUrl(); }, - icon: Icon(Icons.content_paste), + icon: const Icon(Icons.content_paste), label: Text( L.of(context).onboardingRaplaUrlPaste.toUpperCase(), ), diff --git a/lib/ui/onboarding/widgets/mannheim_page.dart b/lib/ui/onboarding/widgets/mannheim_page.dart index 83495596..4a6ff63a 100644 --- a/lib/ui/onboarding/widgets/mannheim_page.dart +++ b/lib/ui/onboarding/widgets/mannheim_page.dart @@ -4,17 +4,12 @@ import 'package:dhbwstudentapp/ui/onboarding/viewmodels/onboarding_view_model_ba import 'package:flutter/material.dart'; import 'package:property_change_notifier/property_change_notifier.dart'; -class MannheimPage extends StatefulWidget { - @override - _MannheimPageState createState() => _MannheimPageState(); -} +class MannheimPage extends StatelessWidget { + const MannheimPage({super.key}); -class _MannheimPageState extends State { @override Widget build(BuildContext context) { return Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(32, 0, 32, 0), @@ -35,9 +30,9 @@ class _MannheimPageState extends State { style: Theme.of(context).textTheme.bodyText2, textAlign: TextAlign.center, ), - Expanded( + const Expanded( child: Padding( - padding: const EdgeInsets.fromLTRB(0, 32, 0, 0), + padding: EdgeInsets.fromLTRB(0, 32, 0, 0), child: SelectMannheimCourseWidget(), ), ), @@ -47,36 +42,40 @@ class _MannheimPageState extends State { } class SelectMannheimCourseWidget extends StatelessWidget { + const SelectMannheimCourseWidget({super.key}); + @override Widget build(BuildContext context) { return PropertyChangeConsumer( - builder: - (BuildContext context, OnboardingStepViewModel model, Set _) { - var viewModel = model as MannheimViewModel; + builder: ( + BuildContext context, + OnboardingStepViewModel? model, + Set? _, + ) { + final viewModel = model as MannheimViewModel?; - switch (viewModel.loadingState) { + switch (viewModel?.loadingState) { case LoadCoursesState.Loading: return _buildLoadingIndicator(); case LoadCoursesState.Loaded: - return _buildCourseList(context, viewModel); + return _buildCourseList(context, viewModel!); case LoadCoursesState.Failed: + default: return _buildLoadingError(context, viewModel); } - - return Container(); }, ); } Widget _buildLoadingIndicator() { - return Center(child: CircularProgressIndicator()); + return const Center(child: CircularProgressIndicator()); } Widget _buildCourseList(BuildContext context, MannheimViewModel viewModel) { return Material( color: Colors.transparent, child: ListView.builder( - padding: EdgeInsets.all(0), + padding: EdgeInsets.zero, itemCount: viewModel.courses?.length ?? 0, itemBuilder: (BuildContext context, int index) => _buildCourseListTile(viewModel, index, context), @@ -89,7 +88,8 @@ class SelectMannheimCourseWidget extends StatelessWidget { int index, BuildContext context, ) { - var isSelected = viewModel.selectedCourse == viewModel.courses[index]; + // TODO: [Leptopoda] why is nullsafety garanttueed here but checked above ¿? + final isSelected = viewModel.selectedCourse == viewModel.courses![index]; return ListTile( trailing: isSelected @@ -99,19 +99,22 @@ class SelectMannheimCourseWidget extends StatelessWidget { ) : null, title: Text( - viewModel.courses[index].name, + viewModel.courses![index].name, style: isSelected ? TextStyle( color: Theme.of(context).colorScheme.secondary, ) : null, ), - subtitle: Text(viewModel.courses[index].title), - onTap: () => viewModel.setSelectedCourse(viewModel.courses[index]), + subtitle: Text(viewModel.courses![index].title), + onTap: () => viewModel.setSelectedCourse(viewModel.courses![index]), ); } - Widget _buildLoadingError(BuildContext context, MannheimViewModel viewModel) { + Widget _buildLoadingError( + BuildContext context, + MannheimViewModel? viewModel, + ) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -120,8 +123,8 @@ class SelectMannheimCourseWidget extends StatelessWidget { Padding( padding: const EdgeInsets.all(16), child: MaterialButton( - onPressed: viewModel.loadCourses, - child: Icon(Icons.refresh), + onPressed: viewModel?.loadCourses, + child: const Icon(Icons.refresh), ), ), ], diff --git a/lib/ui/onboarding/widgets/onboarding_button_bar.dart b/lib/ui/onboarding/widgets/onboarding_button_bar.dart index 41f4deb6..5b141d39 100644 --- a/lib/ui/onboarding/widgets/onboarding_button_bar.dart +++ b/lib/ui/onboarding/widgets/onboarding_button_bar.dart @@ -4,22 +4,21 @@ import 'package:flutter/material.dart'; class OnboardingButtonBar extends StatelessWidget { final OnboardingViewModel viewModel; - final Function onNext; - final Function onPrevious; + final VoidCallback onNext; + final VoidCallback onPrevious; const OnboardingButtonBar({ - Key key, - @required this.viewModel, - @required this.onNext, - @required this.onPrevious, - }) : super(key: key); + super.key, + required this.viewModel, + required this.onNext, + required this.onPrevious, + }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildPreviousButton(context), @@ -30,7 +29,7 @@ class OnboardingButtonBar extends StatelessWidget { } Widget _buildPreviousButton(BuildContext context) { - bool isFirstPage = viewModel.stepIndex == 0; + final bool isFirstPage = viewModel.stepIndex == 0; return AnimatedSwitcher( duration: const Duration(milliseconds: 100), @@ -38,7 +37,7 @@ class OnboardingButtonBar extends StatelessWidget { ? Container() : TextButton.icon( onPressed: onPrevious, - icon: Icon(Icons.navigate_before), + icon: const Icon(Icons.navigate_before), label: Text(L.of(context).onboardingBackButton.toUpperCase()), ), ); @@ -61,8 +60,8 @@ class OnboardingButtonBar extends StatelessWidget { key: ValueKey(buttonText), onPressed: onNext, icon: viewModel.isLastStep - ? Icon(Icons.arrow_forward) - : Icon(Icons.navigate_next), + ? const Icon(Icons.arrow_forward) + : const Icon(Icons.navigate_next), label: Text(buttonText.toUpperCase()), ), ); diff --git a/lib/ui/onboarding/widgets/onboarding_page_background.dart b/lib/ui/onboarding/widgets/onboarding_page_background.dart index 715b0747..5d7e6832 100644 --- a/lib/ui/onboarding/widgets/onboarding_page_background.dart +++ b/lib/ui/onboarding/widgets/onboarding_page_background.dart @@ -1,5 +1,6 @@ +import 'package:dhbwstudentapp/assets.dart'; import 'package:dhbwstudentapp/common/math/math.dart'; -import 'package:dhbwstudentapp/common/ui/colors.dart'; +import 'package:dhbwstudentapp/common/ui/app_theme.dart'; import 'package:dhbwstudentapp/common/util/platform_util.dart'; import 'package:flutter/material.dart'; @@ -10,17 +11,17 @@ class OnboardingPageBackground extends StatelessWidget { final Animation bottomForeground; final Animation bottomBackground; - final Map foreground = { - Brightness.light: "assets/onboarding_bottom_foreground.png", - Brightness.dark: "assets/onboarding_bottom_foreground_dark.png", + static const Map foreground = { + Brightness.light: Assets.assets_onboarding_bottom_foreground_png, + Brightness.dark: Assets.assets_onboarding_bottom_foreground_dark_png, }; - final Map background = { - Brightness.light: "assets/onboarding_bottom_background.png", - Brightness.dark: "assets/onboarding_bottom_background_dark.png", + static const Map background = { + Brightness.light: Assets.assets_onboarding_bottom_background_png, + Brightness.dark: Assets.assets_onboarding_bottom_background_dark_png, }; - OnboardingPageBackground({Key key, this.controller}) + OnboardingPageBackground({super.key, required this.controller}) : angleTopBackground = Tween( begin: PlatformUtil.isPhone() ? -32 : -10, end: PlatformUtil.isPhone() ? -10 : -5, @@ -30,7 +31,6 @@ class OnboardingPageBackground extends StatelessWidget { curve: const Interval( 0.0, 1, - curve: Curves.linear, ), ), ), @@ -43,7 +43,6 @@ class OnboardingPageBackground extends StatelessWidget { curve: const Interval( 0.0, 1, - curve: Curves.linear, ), ), ), @@ -56,7 +55,6 @@ class OnboardingPageBackground extends StatelessWidget { curve: const Interval( 0.0, 1, - curve: Curves.linear, ), ), ), @@ -69,73 +67,73 @@ class OnboardingPageBackground extends StatelessWidget { curve: const Interval( 0.0, 1, - curve: Curves.linear, ), ), - ), - super(key: key); + ); - Widget _buildAnimation(BuildContext context, Widget child) { + Widget _buildAnimation(BuildContext context, Widget? child) { return Stack( children: [ Transform.rotate( + angle: toRadian(angleTopForeground.value), child: Transform.translate( + offset: const Offset(20, -450), child: Container( width: 15000, height: 500, - color: colorOnboardingDecorationForeground(context), + color: AppTheme.onboardingDecorationForeground, ), - offset: const Offset(20, -450), ), - angle: toRadian(angleTopForeground.value), ), Transform.rotate( + angle: toRadian(angleTopBackground.value), child: Transform.translate( + offset: const Offset(20, -480), child: Container( width: 1500, height: 500, - color: colorOnboardingDecorationBackground(context), + color: AppTheme.onboardingDecorationBackground, ), - offset: const Offset(20, -480), ), - angle: toRadian(angleTopBackground.value), ), - PlatformUtil.isPhone() - ? Align( - alignment: Alignment.bottomCenter, - child: SizedBox( - height: 90, - width: double.infinity, - child: Transform.translate( - offset: Offset(bottomBackground.value, 20), - child: Transform.scale( - scale: 1.5, - child: Image.asset( - background[Theme.of(context).brightness], - ), - ), + if (PlatformUtil.isPhone()) + Align( + alignment: Alignment.bottomCenter, + child: SizedBox( + height: 90, + width: double.infinity, + child: Transform.translate( + offset: Offset(bottomBackground.value, 20), + child: Transform.scale( + scale: 1.5, + child: Image.asset( + background[Theme.of(context).brightness]!, ), ), - ) - : Container(), - PlatformUtil.isPhone() - ? Align( - alignment: Alignment.bottomCenter, - child: SizedBox( - height: 90, - width: double.infinity, - child: Transform.translate( - offset: Offset(bottomForeground.value, 20), - child: Transform.scale( - scale: 1.5, - child: Image.asset( - foreground[Theme.of(context).brightness], - ), - ), + ), + ), + ) + else + Container(), + if (PlatformUtil.isPhone()) + Align( + alignment: Alignment.bottomCenter, + child: SizedBox( + height: 90, + width: double.infinity, + child: Transform.translate( + offset: Offset(bottomForeground.value, 20), + child: Transform.scale( + scale: 1.5, + child: Image.asset( + foreground[Theme.of(context).brightness]!, ), ), - ) - : Container(), + ), + ), + ) + else + Container(), ], ); } diff --git a/lib/ui/onboarding/widgets/rapla_url_page.dart b/lib/ui/onboarding/widgets/rapla_url_page.dart index 2e5b5f3e..78e48f05 100644 --- a/lib/ui/onboarding/widgets/rapla_url_page.dart +++ b/lib/ui/onboarding/widgets/rapla_url_page.dart @@ -1,21 +1,16 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; -import 'package:dhbwstudentapp/ui/onboarding/viewmodels/rapla_url_view_model.dart'; import 'package:dhbwstudentapp/ui/onboarding/viewmodels/onboarding_view_model_base.dart'; +import 'package:dhbwstudentapp/ui/onboarding/viewmodels/rapla_url_view_model.dart'; import 'package:flutter/material.dart'; import 'package:property_change_notifier/property_change_notifier.dart'; -class RaplaUrlPage extends StatefulWidget { - @override - _RaplaUrlPageState createState() => _RaplaUrlPageState(); -} - -class _RaplaUrlPageState extends State { - final TextEditingController _urlTextController = TextEditingController(); +class RaplaUrlPage extends StatelessWidget { + const RaplaUrlPage({super.key}); @override Widget build(BuildContext context) { + final TextEditingController urlTextController = TextEditingController(); return Column( - mainAxisAlignment: MainAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(32, 0, 32, 0), @@ -40,12 +35,17 @@ class _RaplaUrlPageState extends State { child: Padding( padding: const EdgeInsets.fromLTRB(0, 32, 0, 0), child: PropertyChangeConsumer( - builder: (BuildContext context, OnboardingStepViewModel model, - Set _) { - var viewModel = model as RaplaUrlViewModel; + builder: ( + BuildContext context, + OnboardingStepViewModel? model, + Set? _, + ) { + final viewModel = model as RaplaUrlViewModel?; - if (_urlTextController.text != viewModel.raplaUrl) - _urlTextController.text = viewModel.raplaUrl; + if (viewModel?.raplaUrl != null && + urlTextController.text != viewModel!.raplaUrl) { + urlTextController.text = viewModel.raplaUrl!; + } return Padding( padding: const EdgeInsets.fromLTRB(0, 0, 0, 16), @@ -54,23 +54,21 @@ class _RaplaUrlPageState extends State { children: [ Expanded( child: TextField( - controller: _urlTextController, + controller: urlTextController, decoration: InputDecoration( - errorText: (viewModel.urlHasError ?? false) + errorText: viewModel?.urlHasError == true ? L.of(context).onboardingRaplaUrlInvalid : null, hintText: L.of(context).onboardingRaplaUrlHint, ), - onChanged: (value) { - viewModel.setRaplaUrl(value); - }, + onChanged: viewModel?.setRaplaUrl, ), ), TextButton.icon( onPressed: () async { - await viewModel.pasteUrl(); + await viewModel?.pasteUrl(); }, - icon: Icon(Icons.content_paste), + icon: const Icon(Icons.content_paste), label: Text( L.of(context).onboardingRaplaUrlPaste.toUpperCase(), ), diff --git a/lib/ui/onboarding/widgets/select_source_page.dart b/lib/ui/onboarding/widgets/select_source_page.dart index e10eb9e5..baa1984b 100644 --- a/lib/ui/onboarding/widgets/select_source_page.dart +++ b/lib/ui/onboarding/widgets/select_source_page.dart @@ -6,19 +6,18 @@ import 'package:flutter/material.dart'; import 'package:property_change_notifier/property_change_notifier.dart'; class SelectSourcePage extends StatelessWidget { - const SelectSourcePage({Key key}) : super(key: key); + const SelectSourcePage({super.key}); @override Widget build(BuildContext context) { return PropertyChangeConsumer( builder: ( BuildContext context, - OnboardingStepViewModel model, - Set _, + OnboardingStepViewModel? model, + Set? _, ) { - var viewModel = model as SelectSourceViewModel; + final viewModel = model as SelectSourceViewModel?; return Column( - mainAxisAlignment: MainAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(32, 0, 32, 0), @@ -92,15 +91,16 @@ class SelectSourcePage extends StatelessWidget { } RadioListTile buildScheduleTypeRadio( - SelectSourceViewModel viewModel, - BuildContext context, - ScheduleSourceType type, - String title) { + SelectSourceViewModel? viewModel, + BuildContext context, + ScheduleSourceType type, + String title, + ) { return RadioListTile( value: type, //model.useDualis, - onChanged: viewModel.setScheduleSourceType, + onChanged: viewModel?.setScheduleSourceType, title: Text(title), - groupValue: viewModel.scheduleSourceType, + groupValue: viewModel?.scheduleSourceType, ); } } diff --git a/lib/ui/pager_widget.dart b/lib/ui/pager_widget.dart index 96cbc68f..11c75fdc 100644 --- a/lib/ui/pager_widget.dart +++ b/lib/ui/pager_widget.dart @@ -15,23 +15,20 @@ import 'package:provider/provider.dart'; /// class PagerWidget extends StatefulWidget { final List pages; - final String pagesId; + final String? pagesId; - const PagerWidget({Key key, @required this.pages, this.pagesId}) - : super(key: key); + const PagerWidget({super.key, required this.pages, this.pagesId}); @override - _PagerWidgetState createState() => _PagerWidgetState(pages, pagesId); + _PagerWidgetState createState() => _PagerWidgetState(); } class _PagerWidgetState extends State { final PreferencesProvider preferencesProvider = KiwiContainer().resolve(); - final String pagesId; - final List pages; int _currentPage = 0; - _PagerWidgetState(this.pages, this.pagesId); + _PagerWidgetState(); @override void initState() { @@ -49,37 +46,23 @@ class _PagerWidgetState extends State { key: ValueKey(_currentPage), children: [ Expanded( - child: _wrapWithChangeNotifierProvider( - pages[_currentPage].builder(context), - pages[_currentPage].viewModel, - ), + child: widget.pages[_currentPage].widget(context), ), ], ), ), bottomNavigationBar: BottomNavigationBar( currentIndex: _currentPage, - onTap: (int index) async { - await setActivePage(index); - }, + onTap: setActivePage, items: buildBottomNavigationBarItems(), ), ); } - Widget _wrapWithChangeNotifierProvider(Widget child, BaseViewModel value) { - if (value == null) return child; - - return ChangeNotifierProvider.value( - child: child, - value: value, - ); - } - List buildBottomNavigationBarItems() { - var bottomNavigationBarItems = []; + final bottomNavigationBarItems = []; - for (var page in pages) { + for (final page in widget.pages) { bottomNavigationBarItems.add( BottomNavigationBarItem( icon: page.icon, @@ -91,7 +74,7 @@ class _PagerWidgetState extends State { } Future setActivePage(int page) async { - if (page < 0 || page >= pages.length) { + if (page < 0 || page >= widget.pages.length) { return; } @@ -99,20 +82,20 @@ class _PagerWidgetState extends State { _currentPage = page; }); - if (pagesId == null) return; - await preferencesProvider.set("${pagesId}_active_page", page); + if (widget.pagesId == null) return; + await preferencesProvider.set("${widget.pagesId}_active_page", page); } Future loadActivePage() async { - if (pagesId == null) return; + if (widget.pagesId == null) return; - var selectedPage = await preferencesProvider.get( - "${pagesId}_active_page", + final selectedPage = await preferencesProvider.get( + "${widget.pagesId}_active_page", ); if (selectedPage == null) return; - if (selectedPage > 0 && selectedPage < pages.length) { + if (selectedPage > 0 && selectedPage < widget.pages.length) { setState(() { _currentPage = selectedPage; }); @@ -120,16 +103,26 @@ class _PagerWidgetState extends State { } } -class PageDefinition { +class PageDefinition { final Widget icon; final String text; final WidgetBuilder builder; - final BaseViewModel viewModel; + final T? viewModel; - PageDefinition({ - @required this.icon, - @required this.text, - @required this.builder, + const PageDefinition({ + required this.icon, + required this.text, + required this.builder, this.viewModel, }); + + /// Wraps the Widget with a [ChangeNotifierProvider] if [viewModel] is specified. + Widget widget(BuildContext context) { + if (viewModel == null) return builder(context); + + return ChangeNotifierProvider.value( + value: viewModel, + child: builder(context), + ); + } } diff --git a/lib/ui/root_page.dart b/lib/ui/root_page.dart index 82397d57..6585369e 100644 --- a/lib/ui/root_page.dart +++ b/lib/ui/root_page.dart @@ -1,6 +1,6 @@ import 'package:dhbwstudentapp/common/i18n/localizations.dart'; import 'package:dhbwstudentapp/common/logging/analytics.dart'; -import 'package:dhbwstudentapp/common/ui/colors.dart'; +import 'package:dhbwstudentapp/common/ui/app_theme.dart'; import 'package:dhbwstudentapp/common/ui/viewmodels/root_view_model.dart'; import 'package:dhbwstudentapp/ui/navigation/navigator_key.dart'; import 'package:dhbwstudentapp/ui/navigation/router.dart'; @@ -13,38 +13,28 @@ import 'package:property_change_notifier/property_change_notifier.dart'; /// This is the top level widget of the app. It handles navigation of the /// root navigator and rebuilds its child widgets on theme changes /// -class RootPage extends StatefulWidget { +class RootPage extends StatelessWidget { final RootViewModel rootViewModel; - const RootPage({Key key, this.rootViewModel}) : super(key: key); - - @override - _RootPageState createState() => _RootPageState(rootViewModel); -} - -class _RootPageState extends State { - final RootViewModel rootViewModel; - - _RootPageState(this.rootViewModel); - - @override - void initState() { - super.initState(); - } + const RootPage({super.key, required this.rootViewModel}); @override Widget build(BuildContext context) { return PropertyChangeProvider( + value: rootViewModel, child: PropertyChangeConsumer( properties: const ["appTheme", "isOnboarding"], - builder: (BuildContext context, RootViewModel model, Set properties) => - MaterialApp( - theme: ColorPalettes.buildTheme(model.appTheme), + builder: + (BuildContext context, RootViewModel? model, Set? properties) => + MaterialApp( + theme: AppTheme.lightThemeData, + darkTheme: AppTheme.darkThemeData, + themeMode: model?.appTheme, initialRoute: rootViewModel.isOnboarding ? "onboarding" : "main", navigatorKey: NavigatorKey.rootKey, navigatorObservers: [rootNavigationObserver], - localizationsDelegates: [ - const LocalizationDelegate(), + localizationsDelegates: const [ + LocalizationDelegate(), GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, @@ -57,7 +47,6 @@ class _RootPageState extends State { onGenerateRoute: generateRoute, ), ), - value: rootViewModel, ); } } diff --git a/lib/ui/settings/donate_list_tile.dart b/lib/ui/settings/donate_list_tile.dart index 9562b533..f1c59da9 100644 --- a/lib/ui/settings/donate_list_tile.dart +++ b/lib/ui/settings/donate_list_tile.dart @@ -7,6 +7,8 @@ import 'package:kiwi/kiwi.dart'; import 'package:property_change_notifier/property_change_notifier.dart'; class DonateListTile extends StatefulWidget { + const DonateListTile({super.key}); + @override _DonateListTileState createState() => _DonateListTileState(); } @@ -14,7 +16,7 @@ class DonateListTile extends StatefulWidget { class _DonateListTileState extends State { final InAppPurchaseManager inAppPurchaseManager; - SettingsViewModel model; + late SettingsViewModel model; bool isPurchasing = false; @@ -34,7 +36,7 @@ class _DonateListTileState extends State { inAppPurchaseManager.removePurchaseCallback(null, purchaseCallback); } - void purchaseCallback(String productId, PurchaseResultEnum result) { + void purchaseCallback(String? productId, PurchaseResultEnum result) { if (!mounted) return; setState(() { @@ -49,18 +51,19 @@ class _DonateListTileState extends State { properties: const [ "didPurchaseWidget", ], - ).value; + )! + .value; if (model.widgetPurchaseState == null) { - return Padding( - padding: const EdgeInsets.all(8.0), + return const Padding( + padding: EdgeInsets.all(8.0), child: Center( child: SizedBox( + width: 16, + height: 16, child: CircularProgressIndicator( strokeWidth: 2, ), - width: 16, - height: 16, ), ), ); @@ -68,21 +71,21 @@ class _DonateListTileState extends State { return ListTile( title: Text(L.of(context).donateButtonTitle), subtitle: isPurchasing - ? Align( + ? const Align( alignment: Alignment.centerLeft, child: Padding( - padding: const EdgeInsets.all(8.0), + padding: EdgeInsets.all(8.0), child: SizedBox( + width: 16, + height: 16, child: CircularProgressIndicator( strokeWidth: 2, ), - width: 16, - height: 16, ), ), ) : Text(L.of(context).donateButtonSubtitle), - trailing: Icon(Icons.free_breakfast), + trailing: const Icon(Icons.free_breakfast), onTap: _purchaseClicked, ); } @@ -90,9 +93,9 @@ class _DonateListTileState extends State { return Container(); } - void _purchaseClicked() async { - if (isPurchasing || model.widgetPurchaseState == PurchaseStateEnum.Purchased) - return; + Future _purchaseClicked() async { + if (isPurchasing || + model.widgetPurchaseState == PurchaseStateEnum.Purchased) return; setState(() { isPurchasing = true; diff --git a/lib/ui/settings/purchase_widget_list_tile.dart b/lib/ui/settings/purchase_widget_list_tile.dart index 8f8d9438..d519c81b 100644 --- a/lib/ui/settings/purchase_widget_list_tile.dart +++ b/lib/ui/settings/purchase_widget_list_tile.dart @@ -7,13 +7,15 @@ import 'package:kiwi/kiwi.dart'; import 'package:property_change_notifier/property_change_notifier.dart'; class PurchaseWidgetListTile extends StatefulWidget { + const PurchaseWidgetListTile({super.key}); + @override _PurchaseWidgetListTileState createState() => _PurchaseWidgetListTileState(); } class _PurchaseWidgetListTileState extends State { final InAppPurchaseManager inAppPurchaseManager; - SettingsViewModel model; + late SettingsViewModel model; bool isPurchasing = false; @@ -34,7 +36,7 @@ class _PurchaseWidgetListTileState extends State { inAppPurchaseManager.removePurchaseCallback(null, purchaseCallback); } - void purchaseCallback(String productId, PurchaseResultEnum result) { + void purchaseCallback(String? productId, PurchaseResultEnum result) { if (!mounted) return; setState(() { @@ -50,9 +52,10 @@ class _PurchaseWidgetListTileState extends State { "didPurchaseWidget", "areWidgetsSupported", ], - ).value; + )! + .value; - if (!model.areWidgetsSupported || + if (!model.areWidgetsSupported! || model.widgetPurchaseState == PurchaseStateEnum.Unknown || model.widgetPurchaseState == null) { return Container(); @@ -60,24 +63,24 @@ class _PurchaseWidgetListTileState extends State { return ListTile( title: Text(L.of(context).settingsWidgetPurchase), - trailing: Icon(Icons.add_to_home_screen_outlined), + trailing: const Icon(Icons.add_to_home_screen_outlined), subtitle: _buildWidgetSubtitle(), onTap: _purchaseClicked, ); } - Widget _buildWidgetSubtitle() { + Widget? _buildWidgetSubtitle() { if (isPurchasing) { - return Align( + return const Align( alignment: Alignment.centerLeft, child: Padding( - padding: const EdgeInsets.all(8.0), + padding: EdgeInsets.all(8.0), child: SizedBox( + width: 16, + height: 16, child: CircularProgressIndicator( strokeWidth: 2, ), - width: 16, - height: 16, ), ), ); @@ -86,8 +89,8 @@ class _PurchaseWidgetListTileState extends State { if (model.widgetPurchaseState == PurchaseStateEnum.Purchased) { return Row( children: [ - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 8, 0), + const Padding( + padding: EdgeInsets.fromLTRB(0, 0, 8, 0), child: Icon( Icons.check, color: Colors.green, @@ -96,7 +99,7 @@ class _PurchaseWidgetListTileState extends State { ), Text( L.of(context).settingsWidgetDidPurchase, - style: TextStyle(color: Colors.green), + style: const TextStyle(color: Colors.green), ), ], ); @@ -104,7 +107,7 @@ class _PurchaseWidgetListTileState extends State { return null; } - void _purchaseClicked() async { + Future _purchaseClicked() async { if (isPurchasing || model.widgetPurchaseState == PurchaseStateEnum.Purchased) return; diff --git a/lib/ui/settings/select_theme_dialog.dart b/lib/ui/settings/select_theme_dialog.dart index c6a337c1..3b92fdc5 100644 --- a/lib/ui/settings/select_theme_dialog.dart +++ b/lib/ui/settings/select_theme_dialog.dart @@ -1,4 +1,3 @@ -import 'package:dhbwstudentapp/common/data/preferences/app_theme_enum.dart'; import 'package:dhbwstudentapp/common/i18n/localizations.dart'; import 'package:dhbwstudentapp/common/ui/viewmodels/root_view_model.dart'; import 'package:flutter/material.dart'; @@ -10,7 +9,7 @@ import 'package:property_change_notifier/property_change_notifier.dart'; class SelectThemeDialog { final RootViewModel _rootViewModel; - SelectThemeDialog(this._rootViewModel); + const SelectThemeDialog(this._rootViewModel); Future show(BuildContext context) async { await showDialog( @@ -28,27 +27,28 @@ class SelectThemeDialog { properties: const [ "appTheme", ], - builder: (BuildContext context, RootViewModel model, Set properties) { + builder: + (BuildContext context, RootViewModel? model, Set? properties) { return Column( mainAxisSize: MainAxisSize.min, children: [ RadioListTile( title: Text(L.of(context).selectThemeLight), - value: AppTheme.Light, + value: ThemeMode.light, groupValue: _rootViewModel.appTheme, - onChanged: (v) => _rootViewModel.setAppTheme(v), + onChanged: _rootViewModel.setAppTheme, ), RadioListTile( title: Text(L.of(context).selectThemeDark), - value: AppTheme.Dark, + value: ThemeMode.dark, groupValue: _rootViewModel.appTheme, - onChanged: (v) => _rootViewModel.setAppTheme(v), + onChanged: _rootViewModel.setAppTheme, ), RadioListTile( title: Text(L.of(context).selectThemeSystem), - value: AppTheme.System, + value: ThemeMode.system, groupValue: _rootViewModel.appTheme, - onChanged: (v) => _rootViewModel.setAppTheme(v), + onChanged: _rootViewModel.setAppTheme, ), ], ); diff --git a/lib/ui/settings/settings_page.dart b/lib/ui/settings/settings_page.dart index 7c7c173a..0bd21eb9 100644 --- a/lib/ui/settings/settings_page.dart +++ b/lib/ui/settings/settings_page.dart @@ -1,7 +1,7 @@ +import 'package:dhbwstudentapp/assets.dart'; import 'package:dhbwstudentapp/common/application_constants.dart'; import 'package:dhbwstudentapp/common/background/task_callback.dart'; import 'package:dhbwstudentapp/common/background/work_scheduler_service.dart'; -import 'package:dhbwstudentapp/common/data/preferences/app_theme_enum.dart'; import 'package:dhbwstudentapp/common/data/preferences/preferences_provider.dart'; import 'package:dhbwstudentapp/common/i18n/localizations.dart'; import 'package:dhbwstudentapp/common/ui/viewmodels/root_view_model.dart'; @@ -13,9 +13,9 @@ import 'package:dhbwstudentapp/schedule/background/calendar_synchronizer.dart'; import 'package:dhbwstudentapp/schedule/ui/notification/next_day_information_notification.dart'; import 'package:dhbwstudentapp/schedule/ui/widgets/select_source_dialog.dart'; import 'package:dhbwstudentapp/ui/navigation/navigator_key.dart'; -import 'package:dhbwstudentapp/ui/settings/select_theme_dialog.dart'; import 'package:dhbwstudentapp/ui/settings/donate_list_tile.dart'; import 'package:dhbwstudentapp/ui/settings/purchase_widget_list_tile.dart'; +import 'package:dhbwstudentapp/ui/settings/select_theme_dialog.dart'; import 'package:dhbwstudentapp/ui/settings/viewmodels/settings_view_model.dart'; import 'package:flutter/material.dart'; import 'package:kiwi/kiwi.dart'; @@ -30,27 +30,32 @@ import 'package:url_launcher/url_launcher.dart'; /// of the app /// class SettingsPage extends StatefulWidget { + const SettingsPage({super.key}); + @override _SettingsPageState createState() => _SettingsPageState(); } class _SettingsPageState extends State { final SettingsViewModel settingsViewModel = SettingsViewModel( - KiwiContainer().resolve(), - KiwiContainer().resolve(NextDayInformationNotification.name) - as NextDayInformationNotification, - KiwiContainer().resolve(), - KiwiContainer().resolve()); + KiwiContainer().resolve(), + KiwiContainer().resolve(NextDayInformationNotification.name) + as NextDayInformationNotification, + KiwiContainer().resolve(), + KiwiContainer().resolve(), + ); + + _SettingsPageState(); @override Widget build(BuildContext context) { - var widgets = []; - - widgets.addAll(buildScheduleSourceSettings(context)); - widgets.addAll(buildDesignSettings(context)); - widgets.addAll(buildNotificationSettings(context)); - widgets.addAll(buildAboutSettings(context)); - widgets.add(buildDisclaimer(context)); + final widgets = [ + ...buildScheduleSourceSettings(), + ...buildDesignSettings(), + ...buildNotificationSettings(), + ...buildAboutSettings(), + buildDisclaimer(), + ]; return Scaffold( appBar: AppBar( @@ -71,7 +76,7 @@ class _SettingsPageState extends State { ); } - Widget buildDisclaimer(BuildContext context) { + Widget buildDisclaimer() { return Padding( padding: const EdgeInsets.all(32), child: Text( @@ -81,18 +86,18 @@ class _SettingsPageState extends State { ); } - List buildAboutSettings(BuildContext context) { + List buildAboutSettings() { return [ TitleListTile(title: L.of(context).settingsAboutTitle), - PurchaseWidgetListTile(), - DonateListTile(), + const PurchaseWidgetListTile(), + const DonateListTile(), ListTile( title: Text(L.of(context).settingsAbout), onTap: () { showAboutDialog( context: context, applicationIcon: Image.asset( - "assets/app_icon.png", + Assets.assets_app_icon_png, width: 75, ), applicationLegalese: L.of(context).applicationLegalese, @@ -111,7 +116,7 @@ class _SettingsPageState extends State { ]; } - List buildScheduleSourceSettings(BuildContext context) { + List buildScheduleSourceSettings() { return [ TitleListTile(title: L.of(context).settingsScheduleSourceTitle), ListTile( @@ -128,67 +133,39 @@ class _SettingsPageState extends State { "prettifySchedule", ], builder: - (BuildContext context, SettingsViewModel model, Set properties) { + (BuildContext context, SettingsViewModel? model, Set? properties) { return SwitchListTile( title: Text(L.of(context).settingsPrettifySchedule), - onChanged: model.setPrettifySchedule, + onChanged: model!.setPrettifySchedule, value: model.prettifySchedule, ); }, ), ListTile( title: Text(L.of(context).settingsCalendarSync), - onTap: () async { - if (await CalendarAccess().requestCalendarPermission() == - CalendarPermission.PermissionDenied) { - await showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: Text( - L.of(context).dialogTitleCalendarAccessNotGranted), - content: - Text(L.of(context).dialogCalendarAccessNotGranted), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(L.of(context).dialogOk), - ) - ], - )); - return; - } - var isCalendarSyncEnabled = await KiwiContainer() - .resolve() - .isCalendarSyncEnabled(); - List entriesToExport = - KiwiContainer().resolve().listDateEntries; - await NavigatorKey.rootKey.currentState.push(MaterialPageRoute( - builder: (BuildContext context) => CalendarExportPage( - entriesToExport: entriesToExport, - isCalendarSyncWidget: true, - isCalendarSyncEnabled: isCalendarSyncEnabled, - ), - settings: RouteSettings(name: "settings"))); - }, + onTap: requestCalendarPermission, ), const Divider(), ]; } - List buildNotificationSettings(BuildContext context) { - WorkSchedulerService service = KiwiContainer().resolve(); - if (service?.isSchedulingAvailable() ?? false) { + List buildNotificationSettings() { + final WorkSchedulerService service = KiwiContainer().resolve(); + if (service.isSchedulingAvailable()) { return [ TitleListTile(title: L.of(context).settingsNotificationsTitle), PropertyChangeConsumer( properties: const [ "notifyAboutNextDay", ], - builder: - (BuildContext context, SettingsViewModel model, Set properties) { + builder: ( + BuildContext context, + SettingsViewModel? model, + Set? properties, + ) { return SwitchListTile( title: Text(L.of(context).settingsNotificationsNextDay), - onChanged: model.setNotifyAboutNextDay, + onChanged: model!.setNotifyAboutNextDay, value: model.notifyAboutNextDay, ); }, @@ -197,11 +174,14 @@ class _SettingsPageState extends State { properties: const [ "notifyAboutScheduleChanges", ], - builder: - (BuildContext context, SettingsViewModel model, Set properties) { + builder: ( + BuildContext context, + SettingsViewModel? model, + Set? properties, + ) { return SwitchListTile( title: Text(L.of(context).settingsNotificationsScheduleChange), - onChanged: model.setNotifyAboutScheduleChanges, + onChanged: model!.setNotifyAboutScheduleChanges, value: model.notifyAboutScheduleChanges, ); }, @@ -213,24 +193,26 @@ class _SettingsPageState extends State { } } - List buildDesignSettings(BuildContext context) { + List buildDesignSettings() { return [ TitleListTile(title: L.of(context).settingsDesign), PropertyChangeConsumer( properties: const [ "appTheme", ], - builder: (BuildContext context, RootViewModel model, Set properties) { + builder: (BuildContext context, RootViewModel? model, Set? properties) { return ListTile( title: Text(L.of(context).settingsDarkMode), onTap: () async { - await SelectThemeDialog(model).show(context); + await SelectThemeDialog(model!).show(context); }, - subtitle: Text({ - AppTheme.Dark: L.of(context).selectThemeDark, - AppTheme.Light: L.of(context).selectThemeLight, - AppTheme.System: L.of(context).selectThemeSystem, - }[model.appTheme]), + subtitle: Text( + { + ThemeMode.dark: L.of(context).selectThemeDark, + ThemeMode.light: L.of(context).selectThemeLight, + ThemeMode.system: L.of(context).selectThemeSystem, + }[model!.appTheme]!, + ), ); }, ), @@ -238,6 +220,43 @@ class _SettingsPageState extends State { ]; } + Future requestCalendarPermission() async { + final permission = await CalendarAccess().requestCalendarPermission(); + if (permission == CalendarPermission.PermissionDenied) { + await showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text( + L.of(context).dialogTitleCalendarAccessNotGranted, + ), + content: Text(L.of(context).dialogCalendarAccessNotGranted), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(L.of(context).dialogOk), + ) + ], + ), + ); + return; + } + final isCalendarSyncEnabled = await KiwiContainer() + .resolve() + .isCalendarSyncEnabled(); + final List entriesToExport = + KiwiContainer().resolve().listDateEntries; + await NavigatorKey.rootKey.currentState!.push( + MaterialPageRoute( + builder: (BuildContext context) => CalendarExportPage( + entriesToExport: entriesToExport, + isCalendarSyncWidget: true, + isCalendarSyncEnabled: isCalendarSyncEnabled, + ), + settings: const RouteSettings(name: "settings"), + ), + ); + } + @override void dispose() { super.dispose(); diff --git a/lib/ui/settings/viewmodels/settings_view_model.dart b/lib/ui/settings/viewmodels/settings_view_model.dart index 9db30d99..26daf18e 100644 --- a/lib/ui/settings/viewmodels/settings_view_model.dart +++ b/lib/ui/settings/viewmodels/settings_view_model.dart @@ -2,13 +2,12 @@ import 'package:dhbwstudentapp/common/data/preferences/preferences_provider.dart import 'package:dhbwstudentapp/common/iap/in_app_purchase_helper.dart'; import 'package:dhbwstudentapp/common/iap/in_app_purchase_manager.dart'; import 'package:dhbwstudentapp/common/ui/viewmodels/base_view_model.dart'; +import 'package:dhbwstudentapp/common/util/cancellation_token.dart'; import 'package:dhbwstudentapp/native/widget/widget_helper.dart'; +import 'package:dhbwstudentapp/schedule/business/schedule_provider.dart'; import 'package:dhbwstudentapp/schedule/ui/notification/next_day_information_notification.dart'; import 'package:kiwi/kiwi.dart'; -import '../../../common/util/cancellation_token.dart'; -import '../../../schedule/business/schedule_provider.dart'; - /// /// The view model for the settings page. /// @@ -34,19 +33,19 @@ class SettingsViewModel extends BaseViewModel { bool get isCalendarSyncEnabled => _isCalendarSyncEnabled; - PurchaseStateEnum _widgetPurchaseState; + PurchaseStateEnum? _widgetPurchaseState; - PurchaseStateEnum get widgetPurchaseState => _widgetPurchaseState; + PurchaseStateEnum? get widgetPurchaseState => _widgetPurchaseState; - bool _areWidgetsSupported = false; + bool? _areWidgetsSupported = false; - bool get areWidgetsSupported => _areWidgetsSupported; + bool? get areWidgetsSupported => _areWidgetsSupported; SettingsViewModel( this._preferencesProvider, this._nextDayInformationNotification, this._widgetHelper, - this._inAppPurchaseManager + this._inAppPurchaseManager, ) { _loadPreferences(); @@ -63,15 +62,15 @@ class SettingsViewModel extends BaseViewModel { await _preferencesProvider.setIsCalendarSyncEnabled(value); - var scheduleProvider = KiwiContainer().resolve(); + final scheduleProvider = KiwiContainer().resolve(); scheduleProvider.getUpdatedSchedule( DateTime.now(), - DateTime.now().add(Duration(days: 30)), + DateTime.now().add(const Duration(days: 30)), CancellationToken(), ); } - void _widgetPurchaseCallback(String id, PurchaseResultEnum result) { + void _widgetPurchaseCallback(String? id, PurchaseResultEnum result) { if (result == PurchaseResultEnum.Success) { _widgetPurchaseState = PurchaseStateEnum.Purchased; } @@ -101,10 +100,11 @@ class SettingsViewModel extends BaseViewModel { await _preferencesProvider.setNotifyAboutNextDay(value); - if (value) + if (value) { await _nextDayInformationNotification.schedule(); - else + } else { await _nextDayInformationNotification.cancel(); + } } Future _loadPreferences() async { @@ -136,6 +136,7 @@ class SettingsViewModel extends BaseViewModel { await _inAppPurchaseManager.donate(); } + @override void dispose() { super.dispose(); diff --git a/pubspec.lock b/pubspec.lock index f05222ab..40dc89d2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "40.0.0" + version: "47.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.7.0" animations: dependency: "direct main" description: @@ -22,6 +22,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.2" + ansicolor: + dependency: transitive + description: + name: ansicolor + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + app_group_directory: + dependency: "direct main" + description: + name: app_group_directory + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" archive: dependency: transitive description: @@ -36,13 +50,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.3.0" + assets_gen: + dependency: "direct dev" + description: + name: assets_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" boolean_selector: dependency: transitive description: @@ -50,13 +71,69 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.10" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.4" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.4.1" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -64,13 +141,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.0" collection: dependency: transitive description: @@ -85,6 +176,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + copy_with_extension: + dependency: "direct main" + description: + name: copy_with_extension + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.4" + copy_with_extension_gen: + dependency: "direct dev" + description: + name: copy_with_extension_gen + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.4" coverage: dependency: transitive description: @@ -106,6 +211,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.17.1" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" dbus: dependency: transitive description: @@ -120,13 +232,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.0" + equatable: + dependency: "direct main" + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: transitive description: @@ -197,6 +316,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.2.4" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" flutter: dependency: "direct main" description: flutter @@ -322,6 +448,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.2" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" html: dependency: "direct main" description: @@ -378,13 +511,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" - ios_app_group: - dependency: "direct main" - description: - name: ios_app_group - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" js: dependency: transitive description: @@ -392,6 +518,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.4" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.7.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + url: "https://pub.dartlang.org" + source: hosted + version: "6.5.1" kiwi: dependency: "direct main" description: @@ -407,12 +547,12 @@ packages: source: hosted version: "3.0.1" lint: - dependency: transitive + dependency: "direct dev" description: name: lint url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.10.0" logging: dependency: transitive description: @@ -426,21 +566,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -477,12 +617,12 @@ packages: source: hosted version: "2.0.2" path: - dependency: transitive + dependency: "direct main" description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_provider: dependency: "direct main" description: @@ -588,6 +728,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" shared_preferences: dependency: "direct main" description: @@ -677,6 +824,20 @@ packages: description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.2" + source_helper: + dependency: transitive + description: + name: source_helper + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.3" source_map_stack_trace: dependency: transitive description: @@ -697,7 +858,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.9.0" sprintf: dependency: transitive description: @@ -733,13 +894,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" synchronized: dependency: transitive description: @@ -753,28 +921,28 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test: dependency: "direct dev" description: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.21.1" + version: "1.21.4" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.12" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.13" + version: "0.4.16" timezone: dependency: "direct main" description: @@ -782,6 +950,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.8.0" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" typed_data: dependency: transitive description: @@ -907,7 +1082,7 @@ packages: name: workmanager url: "https://pub.dartlang.org" source: hosted - version: "0.4.1" + version: "0.5.0" xdg_directories: dependency: transitive description: @@ -930,5 +1105,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.17.0-0 <3.0.0" + dart: ">=2.17.0 <3.0.0" flutter: ">=2.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 18ed687b..393a9ee4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,56 +14,61 @@ description: An app for the DHBW Stuttgart version: 1.0.1+1 environment: - sdk: ">=2.10.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" dependencies: + animations: ^2.0.2 + app_group_directory: ^2.0.0 + copy_with_extension: ^4.0.0 + device_calendar: ^4.2.0 + equatable: ^2.0.0 + firebase_analytics: ^9.1.6 + firebase_core: ^1.15.0 + firebase_crashlytics: ^2.6.3 # BREAKING CHANGES: iOS Min Plaform has to be updated flutter: sdk: flutter + flutter_inapp_purchase: ^5.1.2 + flutter_local_notifications: ^9.4.1 # BREAKING CHANGES HERE! flutter_localizations: sdk: flutter - - # cupertino_icons: ^1.0.0 - intl: ^0.17.0 + flutter_native_splash: ^2.1.6 + flutter_secure_storage: ^5.0.2 + flutter_widgetkit: ^1.0.3 html: ^0.15.0 http: ^0.13.4 - sqflite: ^2.0.2+1 - path_provider: ^2.0.9 - provider: ^6.0.2 - property_change_notifier: ^0.3.0 http_client_helper: ^2.0.2 - shared_preferences: ^2.0.13 - url_launcher: ^6.1.0 + intl: ^0.17.0 + json_annotation: ^4.7.0 kiwi: ^4.0.2 - flutter_local_notifications: ^9.4.1 # BREAKING CHANGES HERE! - workmanager: ^0.4.1 - universal_html: ^2.0.8 - firebase_core: ^1.15.0 - firebase_analytics: ^9.1.6 - firebase_crashlytics: ^2.6.3 # BREAKING CHANGES: iOS Min Plaform has to be updated launch_review: ^3.0.1 - flutter_secure_storage: ^5.0.2 - animations: ^2.0.2 - flutter_native_splash: ^2.1.6 mutex: ^3.0.0 - device_calendar: ^4.2.0 - flutter_inapp_purchase: ^5.1.2 - flutter_widgetkit: ^1.0.3 - ios_app_group: ^1.0.0 + path: ^1.8.0 + path_provider: ^2.0.9 + property_change_notifier: ^0.3.0 + provider: ^6.0.2 + shared_preferences: ^2.0.13 + sqflite: ^2.0.2+1 timezone: ^0.8.0 + universal_html: ^2.0.8 + url_launcher: ^6.1.0 + workmanager: ^0.5.0 dev_dependencies: + assets_gen: ^1.2.0 + build_runner: ^2.3.0 + copy_with_extension_gen: ^4.0.0 flutter_test: sdk: flutter + json_serializable: ^6.5.0 + lint: ^1.10.0 test: any - # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: - # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. @@ -81,6 +86,6 @@ flutter: - assets/schedule_empty_state_dark.png fonts: - - family: CustomIcons + - family: CustomIcons fonts: - asset: fonts/CustomIcons.ttf diff --git a/test/common/util/string_utils_test.dart b/test/common/util/string_utils_test.dart index c076c7da..cf32d9d2 100644 --- a/test/common/util/string_utils_test.dart +++ b/test/common/util/string_utils_test.dart @@ -3,8 +3,8 @@ import 'package:test/test.dart'; void main() { test('String interpolation', () async { - var format = "%0 %1!"; - var result = interpolate(format, ["Hello", "world"]); + const format = "%0 %1!"; + final result = interpolate(format, ["Hello", "world"]); expect(result, "Hello world!"); }); diff --git a/test/date_management/service/parsing/all_dates_extract_test.dart b/test/date_management/service/parsing/all_dates_extract_test.dart index 0d885eb4..14f86bcd 100644 --- a/test/date_management/service/parsing/all_dates_extract_test.dart +++ b/test/date_management/service/parsing/all_dates_extract_test.dart @@ -4,26 +4,26 @@ import 'package:dhbwstudentapp/date_management/service/parsing/all_dates_extract import 'package:test/test.dart'; Future main() async { - var allDatesPage = await File(Directory.current.absolute.path + - '/test/date_management/service/parsing/html_resources/all_dates.html') - .readAsString(); + final allDatesPage = await File( + '${Directory.current.absolute.path}/test/date_management/service/parsing/html_resources/all_dates.html', + ).readAsString(); test('AllDatesExtract extract all dates', () async { - var extract = AllDatesExtract(); + const extract = AllDatesExtract(); - var dateEntries = extract.extractAllDates(allDatesPage, ""); + final dateEntries = extract.extractAllDates(allDatesPage, ""); expect(dateEntries.length, 4); expect(dateEntries[0].description, "Termin 1"); expect(dateEntries[0].year, "2020"); expect(dateEntries[0].comment, "bis 22.12.21"); - expect(dateEntries[0].start, DateTime(2020, 11, 10, 08, 00)); + expect(dateEntries[0].start, DateTime(2020, 11, 10, 08)); expect(dateEntries[1].description, "Abgabe Studienarbeit"); expect(dateEntries[1].year, "2021"); expect(dateEntries[1].comment, "Abgabesystem"); - expect(dateEntries[1].start, DateTime(2021, 03, 04, 00, 00)); + expect(dateEntries[1].start, DateTime(2021, 03, 04)); expect(dateEntries[2].description, "Abgabe Bachelorarbeit"); expect(dateEntries[2].year, "2019"); @@ -33,6 +33,6 @@ Future main() async { expect(dateEntries[3].description, "Abgabe Bericht Praxis 1"); expect(dateEntries[3].year, "2021"); expect(dateEntries[3].comment, "Elektronisches Abgabesystem"); - expect(dateEntries[3].start, DateTime(2022, 09, 05, 00, 00)); + expect(dateEntries[3].start, DateTime(2022, 09, 05)); }); } diff --git a/test/dualis/service/parsing/all_modules_extract_test.dart b/test/dualis/service/parsing/all_modules_extract_test.dart index faf73b78..eddf4453 100644 --- a/test/dualis/service/parsing/all_modules_extract_test.dart +++ b/test/dualis/service/parsing/all_modules_extract_test.dart @@ -6,14 +6,14 @@ import 'package:dhbwstudentapp/dualis/service/parsing/parsing_utils.dart'; import 'package:test/test.dart'; Future main() async { - var studentResultsPage = await File(Directory.current.absolute.path + - '/test/dualis/service/parsing/html_resources/student_results.html') - .readAsString(); + final studentResultsPage = await File( + '${Directory.current.absolute.path}/test/dualis/service/parsing/html_resources/student_results.html', + ).readAsString(); test('AllModulesExtract extract all modules', () async { - var extract = AllModulesExtract(); + const extract = AllModulesExtract(); - var modules = extract.extractAllModules(studentResultsPage); + final modules = extract.extractAllModules(studentResultsPage); expect(modules.length, 6); @@ -27,7 +27,7 @@ Future main() async { }); test('AllModulesExtract invalid html throws exception', () async { - var extract = AllModulesExtract(); + const extract = AllModulesExtract(); try { extract.extractAllModules("Lorem ipsum"); diff --git a/test/dualis/service/parsing/dualis_timeout_extract_test.dart b/test/dualis/service/parsing/dualis_timeout_extract_test.dart index 79e477d5..8f17eae4 100644 --- a/test/dualis/service/parsing/dualis_timeout_extract_test.dart +++ b/test/dualis/service/parsing/dualis_timeout_extract_test.dart @@ -4,19 +4,22 @@ import 'package:dhbwstudentapp/dualis/service/parsing/timeout_extract.dart'; import 'package:test/test.dart'; Future main() async { - var monthlySchedulePage = await File(Directory.current.absolute.path + - '/test/dualis/service/parsing/html_resources/monthly_schedule.html') - .readAsString(); + final monthlySchedulePage = await File( + '${Directory.current.absolute.path}/test/dualis/service/parsing/html_resources/monthly_schedule.html', + ).readAsString(); - var timeoutPage = await File(Directory.current.absolute.path + - '/test/dualis/service/parsing/html_resources/dualis_timeout.html') - .readAsString(); + final timeoutPage = await File( + '${Directory.current.absolute.path}/test/dualis/service/parsing/html_resources/dualis_timeout.html', + ).readAsString(); test('DualisTimeoutExtract detect timeout page', () async { - expect(TimeoutExtract().isTimeoutErrorPage(timeoutPage), isTrue); + expect(const TimeoutExtract().isTimeoutErrorPage(timeoutPage), isTrue); }); test('DualisTimeoutExtract detect non timeout page', () async { - expect(TimeoutExtract().isTimeoutErrorPage(monthlySchedulePage), isFalse); + expect( + const TimeoutExtract().isTimeoutErrorPage(monthlySchedulePage), + isFalse, + ); }); } diff --git a/test/dualis/service/parsing/exams_from_module_details_extract_test.dart b/test/dualis/service/parsing/exams_from_module_details_extract_test.dart index 55e997c9..84f3f7de 100644 --- a/test/dualis/service/parsing/exams_from_module_details_extract_test.dart +++ b/test/dualis/service/parsing/exams_from_module_details_extract_test.dart @@ -5,14 +5,14 @@ import 'package:dhbwstudentapp/dualis/service/parsing/parsing_utils.dart'; import 'package:test/test.dart'; Future main() async { - var moduleDetailsPage = await File(Directory.current.absolute.path + - '/test/dualis/service/parsing/html_resources/module_details.html') - .readAsString(); + final moduleDetailsPage = await File( + '${Directory.current.absolute.path}/test/dualis/service/parsing/html_resources/module_details.html', + ).readAsString(); test('ExamsFromModuleDetailsExtract', () async { - var extract = ExamsFromModuleDetailsExtract(); + const extract = ExamsFromModuleDetailsExtract(); - var exams = extract.extractExamsFromModuleDetails(moduleDetailsPage); + final exams = extract.extractExamsFromModuleDetails(moduleDetailsPage); expect(exams.length, 2); @@ -26,7 +26,7 @@ Future main() async { }); test('ExamsFromModuleDetailsExtract invalid html throws exception', () async { - var extract = ExamsFromModuleDetailsExtract(); + const extract = ExamsFromModuleDetailsExtract(); try { extract.extractExamsFromModuleDetails("Lorem ipsum"); diff --git a/test/dualis/service/parsing/modules_from_course_result_page_extract_test.dart b/test/dualis/service/parsing/modules_from_course_result_page_extract_test.dart index 28bcc7d9..79f18a43 100644 --- a/test/dualis/service/parsing/modules_from_course_result_page_extract_test.dart +++ b/test/dualis/service/parsing/modules_from_course_result_page_extract_test.dart @@ -5,30 +5,34 @@ import 'package:dhbwstudentapp/dualis/service/parsing/parsing_utils.dart'; import 'package:test/test.dart'; Future main() async { - var courseResultsPage = await File(Directory.current.absolute.path + - '/test/dualis/service/parsing/html_resources/course_results.html') - .readAsString(); + final courseResultsPage = await File( + '${Directory.current.absolute.path}/test/dualis/service/parsing/html_resources/course_results.html', + ).readAsString(); test('ModulesFromCourseResultPageExtract', () async { - var extract = ModulesFromCourseResultPageExtract(); + final extract = ModulesFromCourseResultPageExtract(); - var modules = extract.extractModulesFromCourseResultPage( - courseResultsPage, "www.endpoint.com"); + final modules = extract.extractModulesFromCourseResultPage( + courseResultsPage, + "www.endpoint.com", + ); expect(modules.length, 3); - expect(modules[1].id, "T3INF1001"); - expect(modules[1].name, "Mathematik I"); - expect(modules[1].state, null); - expect(modules[1].credits, "8,0"); - expect(modules[1].finalGrade, "4,0"); - expect(modules[1].detailsUrl, - "www.endpoint.com/scripts/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=RESULTDETAILS&ARGUMENTS=-N123456789876543,-N000307,-N121212121212121"); + expect(modules[1]!.id, "T3INF1001"); + expect(modules[1]!.name, "Mathematik I"); + expect(modules[1]!.state, null); + expect(modules[1]!.credits, "8,0"); + expect(modules[1]!.finalGrade, "4,0"); + expect( + modules[1]!.detailsUrl, + "www.endpoint.com/scripts/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=RESULTDETAILS&ARGUMENTS=-N123456789876543,-N000307,-N121212121212121", + ); }); test('ModulesFromCourseResultPageExtract invalid html throws exception', () async { - var extract = ModulesFromCourseResultPageExtract(); + final extract = ModulesFromCourseResultPageExtract(); try { extract.extractModulesFromCourseResultPage("Lorem ipsum", ""); diff --git a/test/dualis/service/parsing/monthly_schedule_extract_test.dart b/test/dualis/service/parsing/monthly_schedule_extract_test.dart index 60c6d073..fa32a675 100644 --- a/test/dualis/service/parsing/monthly_schedule_extract_test.dart +++ b/test/dualis/service/parsing/monthly_schedule_extract_test.dart @@ -4,18 +4,18 @@ import 'package:dhbwstudentapp/dualis/service/parsing/monthly_schedule_extract.d import 'package:test/test.dart'; Future main() async { - var monthlySchedulePage = await File(Directory.current.absolute.path + - '/test/dualis/service/parsing/html_resources/monthly_schedule.html') - .readAsString(); + final monthlySchedulePage = await File( + '${Directory.current.absolute.path}/test/dualis/service/parsing/html_resources/monthly_schedule.html', + ).readAsString(); test('MonthlyScheduleExtract extract all appointments', () async { - var extract = MonthlyScheduleExtract(); + const extract = MonthlyScheduleExtract(); - var modules = extract.extractScheduleFromMonthly(monthlySchedulePage); + final modules = extract.extractScheduleFromMonthly(monthlySchedulePage); expect(modules.entries.length, 7); - var entryTitles = [ + final entryTitles = [ "HOR-TMB20xxKE1 - 3. Sem. Antriebstechnik", "HOR-TMB20xxKE1/KE2 - 3. Sem. Konstruktion III", "HOR-TMB20xxKE1/KE2 3 . Sem. Thermodynamik Grundlagen 1", @@ -25,11 +25,11 @@ Future main() async { "HOR-TMB20xxKE1 3. Sem. Managementsysteme -Projektmanagement", ]; - var startAndEndTime = [ - [DateTime(2020, 09, 08, 08, 00), DateTime(2020, 09, 08, 12, 15)], + final startAndEndTime = [ + [DateTime(2020, 09, 08, 08), DateTime(2020, 09, 08, 12, 15)], [DateTime(2020, 09, 10, 08, 30), DateTime(2020, 09, 10, 12, 45)], [DateTime(2020, 09, 10, 13, 30), DateTime(2020, 09, 10, 16, 45)], - [DateTime(2020, 09, 14, 09, 00), DateTime(2020, 09, 14, 11, 30)], + [DateTime(2020, 09, 14, 09), DateTime(2020, 09, 14, 11, 30)], [DateTime(2020, 09, 18, 08, 15), DateTime(2020, 09, 18, 11, 15)], [DateTime(2020, 09, 21, 08, 30), DateTime(2020, 09, 21, 11, 45)], [DateTime(2020, 09, 21, 13, 30), DateTime(2020, 09, 21, 16, 45)], diff --git a/test/dualis/service/parsing/semesters_from_course_result_page_extract_test.dart b/test/dualis/service/parsing/semesters_from_course_result_page_extract_test.dart index 261c3692..fd336485 100644 --- a/test/dualis/service/parsing/semesters_from_course_result_page_extract_test.dart +++ b/test/dualis/service/parsing/semesters_from_course_result_page_extract_test.dart @@ -5,14 +5,14 @@ import 'package:dhbwstudentapp/dualis/service/parsing/semesters_from_course_resu import 'package:test/test.dart'; Future main() async { - var courseResultsPage = await File(Directory.current.absolute.path + - '/test/dualis/service/parsing/html_resources/course_results.html') - .readAsString(); + final courseResultsPage = await File( + '${Directory.current.absolute.path}/test/dualis/service/parsing/html_resources/course_results.html', + ).readAsString(); test('SemestersFromCourseResultPageExtract', () async { - var extract = SemestersFromCourseResultPageExtract(); + const extract = SemestersFromCourseResultPageExtract(); - var semesters = extract.extractSemestersFromCourseResults( + final semesters = extract.extractSemestersFromCourseResults( courseResultsPage, "www.endpoint.com", ); @@ -20,17 +20,21 @@ Future main() async { expect(semesters.length, 2); expect(semesters[0].semesterName, "SoSe yyyy"); - expect(semesters[0].semesterCourseResultsUrl, - "www.endpoint.com/scripts/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N123456789876543,-N000307,-N000000012345000"); + expect( + semesters[0].semesterCourseResultsUrl, + "www.endpoint.com/scripts/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N123456789876543,-N000307,-N000000012345000", + ); expect(semesters[1].semesterName, "WiSe xx/yy"); - expect(semesters[1].semesterCourseResultsUrl, - "www.endpoint.com/scripts/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N123456789876543,-N000307,-N000000015048000"); + expect( + semesters[1].semesterCourseResultsUrl, + "www.endpoint.com/scripts/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N123456789876543,-N000307,-N000000015048000", + ); }); test('SemestersFromCourseResultPageExtract invalid html throws exception', () async { - var extract = SemestersFromCourseResultPageExtract(); + const extract = SemestersFromCourseResultPageExtract(); try { extract.extractSemestersFromCourseResults("Lorem ipsum", ""); diff --git a/test/dualis/service/parsing/study_grades_from_student_results_page_extract_test.dart b/test/dualis/service/parsing/study_grades_from_student_results_page_extract_test.dart index 660e0747..eb01dc61 100644 --- a/test/dualis/service/parsing/study_grades_from_student_results_page_extract_test.dart +++ b/test/dualis/service/parsing/study_grades_from_student_results_page_extract_test.dart @@ -5,14 +5,14 @@ import 'package:dhbwstudentapp/dualis/service/parsing/study_grades_from_student_ import 'package:test/test.dart'; Future main() async { - var studentResultsPage = await File(Directory.current.absolute.path + - '/test/dualis/service/parsing/html_resources/student_results.html') - .readAsString(); + final studentResultsPage = await File( + '${Directory.current.absolute.path}/test/dualis/service/parsing/html_resources/student_results.html', + ).readAsString(); test('StudyGradesFromStudentResultsPageExtract', () async { - var extract = StudyGradesFromStudentResultsPageExtract(); + const extract = StudyGradesFromStudentResultsPageExtract(); - var studyGrades = + final studyGrades = extract.extractStudyGradesFromStudentsResultsPage(studentResultsPage); expect(studyGrades.creditsGained, 13); @@ -24,7 +24,7 @@ Future main() async { test('StudyGradesFromStudentResultsPageExtract invalid html throws exception', () async { - var extract = StudyGradesFromStudentResultsPageExtract(); + const extract = StudyGradesFromStudentResultsPageExtract(); try { extract.extractStudyGradesFromStudentsResultsPage("Lorem ipsum"); diff --git a/test/dualis/service/parsing/urls_from_main_page_extract_test.dart b/test/dualis/service/parsing/urls_from_main_page_extract_test.dart index eaeaff6b..14d37e37 100644 --- a/test/dualis/service/parsing/urls_from_main_page_extract_test.dart +++ b/test/dualis/service/parsing/urls_from_main_page_extract_test.dart @@ -6,28 +6,34 @@ import 'package:dhbwstudentapp/dualis/service/parsing/urls_from_main_page_extrac import 'package:test/test.dart'; Future main() async { - var mainPage = await File(Directory.current.absolute.path + - '/test/dualis/service/parsing/html_resources/main_page.html') - .readAsString(); + final mainPage = await File( + '${Directory.current.absolute.path}/test/dualis/service/parsing/html_resources/main_page.html', + ).readAsString(); test('UrlsFromMainPageExtract', () async { - var extract = UrlsFromMainPageExtract(); + const extract = UrlsFromMainPageExtract(); - var mainPageUrls = DualisUrls(); + final mainPageUrls = DualisUrls(); extract.parseMainPage(mainPage, mainPageUrls, "www.endpoint.com"); - expect(mainPageUrls.studentResultsUrl, - "www.endpoint.com/scripts/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=STUDENT_RESULT&ARGUMENTS=-N123456789876543,-N000310,-N0,-N000000000000000,-N000000000000000,-N000000000000000,-N0,-N000000000000000"); - expect(mainPageUrls.courseResultUrl, - "www.endpoint.com/scripts/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N123456789876543,-N000307,"); - expect(mainPageUrls.monthlyScheduleUrl, - "www.endpoint.com/scripts/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=MONTH&ARGUMENTS=-N123456789876543,-N000031,-A"); + expect( + mainPageUrls.studentResultsUrl, + "www.endpoint.com/scripts/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=STUDENT_RESULT&ARGUMENTS=-N123456789876543,-N000310,-N0,-N000000000000000,-N000000000000000,-N000000000000000,-N0,-N000000000000000", + ); + expect( + mainPageUrls.courseResultUrl, + "www.endpoint.com/scripts/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=COURSERESULTS&ARGUMENTS=-N123456789876543,-N000307,", + ); + expect( + mainPageUrls.monthlyScheduleUrl, + "www.endpoint.com/scripts/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=MONTH&ARGUMENTS=-N123456789876543,-N000031,-A", + ); expect(mainPageUrls.semesterCourseResultUrls, {}); }); test('UrlsFromMainPageExtract invalid html throws exception', () async { - var extract = UrlsFromMainPageExtract(); + const extract = UrlsFromMainPageExtract(); try { extract.parseMainPage("Lorem ipsum", DualisUrls(), ""); diff --git a/test/rapla_service_test.dart b/test/rapla_service_test.dart index 76bfee08..2fa06c74 100644 --- a/test/rapla_service_test.dart +++ b/test/rapla_service_test.dart @@ -3,9 +3,12 @@ import 'package:test/test.dart'; void main() { test('debugging', () async { - var source = RaplaScheduleSource(); - source.setEndpointUrl( - "https://rapla.dhbw-stuttgart.de/rapla?key=txB1FOi5xd1wUJBWuX8lJhGDUgtMSFmnKLgAG_NVMhCn4AzVqTBQM-yMcTKkIDCa"); + // ignore: unused_local_variable + final source = RaplaScheduleSource( + raplaUrl: + "https://rapla.dhbw-stuttgart.de/rapla?key=txB1FOi5xd1wUJBWuX8lJhGDUgtMSFmnKLgAG_NVMhCn4AzVqTBQM-yMcTKkIDCa", + ); + //var schedule = await source.querySchedule( // DateTime(2020, 01, 20), DateTime(2020, 01, 25)); //print(schedule); diff --git a/test/schedule/business/schedule_diff_calculator_test.dart b/test/schedule/business/schedule_diff_calculator_test.dart index 89b834d7..1bc4c182 100644 --- a/test/schedule/business/schedule_diff_calculator_test.dart +++ b/test/schedule/business/schedule_diff_calculator_test.dart @@ -5,12 +5,12 @@ import 'package:test/test.dart'; void main() { test('Diff detect identical schedules', () async { - var calculator = ScheduleDiffCalculator(); + const calculator = ScheduleDiffCalculator(); - var oldSchedule = generateSchedule(); - var newSchedule = generateSchedule(); + final oldSchedule = generateSchedule(); + final newSchedule = generateSchedule(); - var diff = calculator.calculateDiff(oldSchedule, newSchedule); + final diff = calculator.calculateDiff(oldSchedule, newSchedule); expect(diff.addedEntries.length, 0); expect(diff.removedEntries.length, 0); @@ -18,7 +18,7 @@ void main() { }); test('Diff detect removed entry', () async { - final calculator = ScheduleDiffCalculator(); + const calculator = ScheduleDiffCalculator(); final oldSchedule = generateSchedule(); final newSchedule = generateSchedule(); @@ -35,24 +35,24 @@ void main() { }); test('Diff detect new entry', () async { - var calculator = ScheduleDiffCalculator(); + const calculator = ScheduleDiffCalculator(); - var oldSchedule = generateSchedule(); - var newSchedule = generateSchedule(); + final oldSchedule = generateSchedule(); + final newSchedule = generateSchedule(); - var newEntry = ScheduleEntry( + final newEntry = ScheduleEntry( room: "Room3", - type: ScheduleEntryType.Class, + type: ScheduleEntryType.Lesson, title: "Project management", professor: "Sam", details: "ipsum", - start: DateTime(2020, 06, 09, 17, 00), - end: DateTime(2020, 06, 09, 18, 00), + start: DateTime(2020, 06, 09, 17), + end: DateTime(2020, 06, 09, 18), ); - newSchedule.addEntry(newEntry); + newSchedule.entries.add(newEntry); - var diff = calculator.calculateDiff(oldSchedule, newSchedule); + final diff = calculator.calculateDiff(oldSchedule, newSchedule); expect(diff.addedEntries.length, 1); expect(diff.addedEntries[0], newEntry); @@ -61,12 +61,12 @@ void main() { }); test('Diff detect changed entry (time)', () async { - var calculator = ScheduleDiffCalculator(); + const calculator = ScheduleDiffCalculator(); - var oldSchedule = generateSchedule(); - var newSchedule = generateSchedule(); + final oldSchedule = generateSchedule(); + final newSchedule = generateSchedule(); - var updatedEntry = ScheduleEntry( + final updatedEntry = ScheduleEntry( room: newSchedule.entries[0].room, type: newSchedule.entries[0].type, title: newSchedule.entries[0].title, @@ -77,7 +77,7 @@ void main() { ); newSchedule.entries[0] = updatedEntry; - var diff = calculator.calculateDiff(oldSchedule, newSchedule); + final diff = calculator.calculateDiff(oldSchedule, newSchedule); expect(diff.addedEntries.length, 0); expect(diff.removedEntries.length, 0); @@ -88,12 +88,12 @@ void main() { }); test('Diff detect changed entry (start and room)', () async { - var calculator = ScheduleDiffCalculator(); + const calculator = ScheduleDiffCalculator(); - var oldSchedule = generateSchedule(); - var newSchedule = generateSchedule(); + final oldSchedule = generateSchedule(); + final newSchedule = generateSchedule(); - var updatedEntry = ScheduleEntry( + final updatedEntry = ScheduleEntry( room: "Changed room", type: newSchedule.entries[0].type, title: newSchedule.entries[0].title, @@ -104,7 +104,7 @@ void main() { ); newSchedule.entries[0] = updatedEntry; - var diff = calculator.calculateDiff(oldSchedule, newSchedule); + final diff = calculator.calculateDiff(oldSchedule, newSchedule); expect(diff.addedEntries.length, 0); expect(diff.removedEntries.length, 0); @@ -116,12 +116,12 @@ void main() { }); test('Diff detect two changed entries of same name (start)', () async { - var calculator = ScheduleDiffCalculator(); + const calculator = ScheduleDiffCalculator(); - var oldSchedule = generateSchedule(); - var newSchedule = generateSchedule(); + final oldSchedule = generateSchedule(); + final newSchedule = generateSchedule(); - var updatedEntry1 = ScheduleEntry( + final updatedEntry1 = ScheduleEntry( room: newSchedule.entries[2].room, type: newSchedule.entries[2].type, title: newSchedule.entries[2].title, @@ -132,7 +132,7 @@ void main() { ); newSchedule.entries[2] = updatedEntry1; - var updatedEntry2 = ScheduleEntry( + final updatedEntry2 = ScheduleEntry( room: newSchedule.entries[3].room, type: newSchedule.entries[3].type, title: newSchedule.entries[3].title, @@ -143,7 +143,7 @@ void main() { ); newSchedule.entries[3] = updatedEntry2; - var diff = calculator.calculateDiff(oldSchedule, newSchedule); + final diff = calculator.calculateDiff(oldSchedule, newSchedule); expect(diff.addedEntries.length, 0); expect(diff.removedEntries.length, 0); @@ -158,44 +158,44 @@ void main() { } Schedule generateSchedule() { - var scheduleEntries = [ + final scheduleEntries = [ ScheduleEntry( room: "Room1", - type: ScheduleEntryType.Class, + type: ScheduleEntryType.Lesson, title: "Chemistry", professor: "Mr. White", details: "We will make breaks", - start: DateTime(2020, 06, 09, 10, 00), - end: DateTime(2020, 06, 09, 12, 00), + start: DateTime(2020, 06, 09, 10), + end: DateTime(2020, 06, 09, 12), ), ScheduleEntry( room: "Room2", - type: ScheduleEntryType.Class, + type: ScheduleEntryType.Lesson, title: "Computer Science", professor: "Mr. Turing", details: "Lorem", - start: DateTime(2020, 06, 09, 13, 00), - end: DateTime(2020, 06, 09, 14, 00), + start: DateTime(2020, 06, 09, 13), + end: DateTime(2020, 06, 09, 14), ), ScheduleEntry( room: "Room3", - type: ScheduleEntryType.Class, + type: ScheduleEntryType.Lesson, title: "Physics", professor: "Mr. Hawking", details: "ipsum", - start: DateTime(2020, 06, 09, 15, 00), - end: DateTime(2020, 06, 09, 16, 00), + start: DateTime(2020, 06, 09, 15), + end: DateTime(2020, 06, 09, 16), ), ScheduleEntry( room: "Room3", - type: ScheduleEntryType.Class, + type: ScheduleEntryType.Lesson, title: "Physics", professor: "Mr. Hawking", details: "ipsum", - start: DateTime(2020, 06, 09, 17, 00), - end: DateTime(2020, 06, 09, 18, 00), + start: DateTime(2020, 06, 09, 17), + end: DateTime(2020, 06, 09, 18), ), ]; - return Schedule.fromList(scheduleEntries); + return Schedule(entries: scheduleEntries); } diff --git a/test/schedule/service/ical/ical_parser_test.dart b/test/schedule/service/ical/ical_parser_test.dart index 0af71462..bcc4a0cd 100644 --- a/test/schedule/service/ical/ical_parser_test.dart +++ b/test/schedule/service/ical/ical_parser_test.dart @@ -5,33 +5,33 @@ import 'package:dhbwstudentapp/schedule/service/ical/ical_parser.dart'; import 'package:test/test.dart'; Future main() async { - var icalFile = await File(Directory.current.absolute.path + - '/test/schedule/service/ical/file_resources/ical_test.ics') - .readAsString(); + final icalFile = await File( + '${Directory.current.absolute.path}/test/schedule/service/ical/file_resources/ical_test.ics', + ).readAsString(); test('ical correctly read all entries', () async { - var parser = IcalParser(); + final parser = IcalParser(); - var schedule = parser.parseIcal(icalFile).schedule; + final schedule = parser.parseIcal(icalFile).schedule; expect(schedule.entries.length, 3); expect(schedule.entries[0].title, "Angewandte Mathematik"); - expect(schedule.entries[0].start, DateTime(2019, 04, 01, 10, 00, 00)); - expect(schedule.entries[0].end, DateTime(2019, 04, 01, 14, 30, 00)); - expect(schedule.entries[0].type, ScheduleEntryType.Class); + expect(schedule.entries[0].start, DateTime(2019, 04, 01, 10)); + expect(schedule.entries[0].end, DateTime(2019, 04, 01, 14, 30)); + expect(schedule.entries[0].type, ScheduleEntryType.Lesson); expect(schedule.entries[0].room, "Raum 035B"); expect(schedule.entries[1].title, "Elektronik"); - expect(schedule.entries[1].start, DateTime(2019, 04, 02, 08, 00, 00)); - expect(schedule.entries[1].end, DateTime(2019, 04, 02, 14, 00, 00)); - expect(schedule.entries[1].type, ScheduleEntryType.Class); + expect(schedule.entries[1].start, DateTime(2019, 04, 02, 08)); + expect(schedule.entries[1].end, DateTime(2019, 04, 02, 14)); + expect(schedule.entries[1].type, ScheduleEntryType.Lesson); expect(schedule.entries[1].room, "Raum 035B"); expect(schedule.entries[2].title, "Informatik"); - expect(schedule.entries[2].start, DateTime(2019, 04, 03, 09, 00, 00)); - expect(schedule.entries[2].end, DateTime(2019, 04, 03, 12, 15, 00)); - expect(schedule.entries[2].type, ScheduleEntryType.Class); + expect(schedule.entries[2].start, DateTime(2019, 04, 03, 09)); + expect(schedule.entries[2].end, DateTime(2019, 04, 03, 12, 15)); + expect(schedule.entries[2].type, ScheduleEntryType.Lesson); expect(schedule.entries[2].room, "Raum 035B"); }); } diff --git a/test/schedule/service/mannheim/mannheim_course_response_parser_test.dart b/test/schedule/service/mannheim/mannheim_course_response_parser_test.dart index 9ad88b9a..d1db7b28 100644 --- a/test/schedule/service/mannheim/mannheim_course_response_parser_test.dart +++ b/test/schedule/service/mannheim/mannheim_course_response_parser_test.dart @@ -4,14 +4,14 @@ import 'package:dhbwstudentapp/schedule/service/mannheim/mannheim_course_respons import 'package:test/test.dart'; Future main() async { - var coursePage = await File(Directory.current.absolute.path + - '/test/schedule/service/mannheim/html_resources/mannheim_ical.html') - .readAsString(); + final coursePage = await File( + '${Directory.current.absolute.path}/test/schedule/service/mannheim/html_resources/mannheim_ical.html', + ).readAsString(); test('Mannheim course parser parses correctly', () async { - var parser = MannheimCourseResponseParser(); + const parser = MannheimCourseResponseParser(); - var courses = parser.parseCoursePage(coursePage); + final courses = parser.parseCoursePage(coursePage); expect(courses.length, 8); diff --git a/test/schedule/service/rapla/rapla_response_parser_test.dart b/test/schedule/service/rapla/rapla_response_parser_test.dart index ced0b81d..a6a80616 100644 --- a/test/schedule/service/rapla/rapla_response_parser_test.dart +++ b/test/schedule/service/rapla/rapla_response_parser_test.dart @@ -5,130 +5,133 @@ import 'package:dhbwstudentapp/schedule/service/rapla/rapla_response_parser.dart import 'package:test/test.dart'; Future main() async { - var monthlyRaplaPage = await File(Directory.current.absolute.path + - '/test/schedule/service/rapla/html_resources/rapla_monthly_response.html') - .readAsString(); + final monthlyRaplaPage = await File( + '${Directory.current.absolute.path}/test/schedule/service/rapla/html_resources/rapla_monthly_response.html', + ).readAsString(); - var raplaPage = await File(Directory.current.absolute.path + - '/test/schedule/service/rapla/html_resources/rapla_response.html') - .readAsString(); + final raplaPage = await File( + '${Directory.current.absolute.path}/test/schedule/service/rapla/html_resources/rapla_response.html', + ).readAsString(); - var raplaPage1 = await File(Directory.current.absolute.path + - '/test/schedule/service/rapla/html_resources/rapla_response_1.html') - .readAsString(); + final raplaPage1 = await File( + '${Directory.current.absolute.path}/test/schedule/service/rapla/html_resources/rapla_response_1.html', + ).readAsString(); - var severalMonthsPage = await File(Directory.current.absolute.path + - '/test/schedule/service/rapla/html_resources/rapla_several_months_response.html') - .readAsString(); + final severalMonthsPage = await File( + '${Directory.current.absolute.path}/test/schedule/service/rapla/html_resources/rapla_several_months_response.html', + ).readAsString(); - var severalMonthsPage1 = await File(Directory.current.absolute.path + - '/test/schedule/service/rapla/html_resources/rapla_several_months_response_1.html') - .readAsString(); + final severalMonthsPage1 = await File( + '${Directory.current.absolute.path}/test/schedule/service/rapla/html_resources/rapla_several_months_response_1.html', + ).readAsString(); - var severalMonthsPage2 = await File(Directory.current.absolute.path + - '/test/schedule/service/rapla/html_resources/rapla_several_months_response_2.html') - .readAsString(); + final severalMonthsPage2 = await File( + '${Directory.current.absolute.path}/test/schedule/service/rapla/html_resources/rapla_several_months_response_2.html', + ).readAsString(); - var invalidRaplaPage = await File(Directory.current.absolute.path + - '/test/schedule/service/rapla/html_resources/invalid_rapla_response.html') - .readAsString(); + final invalidRaplaPage = await File( + '${Directory.current.absolute.path}/test/schedule/service/rapla/html_resources/invalid_rapla_response.html', + ).readAsString(); - var raplaWeekResponse = await File(Directory.current.absolute.path + - '/test/schedule/service/rapla/html_resources/rapla_week_response.html') - .readAsString(); + final raplaWeekResponse = await File( + '${Directory.current.absolute.path}/test/schedule/service/rapla/html_resources/rapla_week_response.html', + ).readAsString(); - var raplaWeekResponse1 = await File(Directory.current.absolute.path + - '/test/schedule/service/rapla/html_resources/rapla_week_response_1.html') - .readAsString(); + final raplaWeekResponse1 = await File( + '${Directory.current.absolute.path}/test/schedule/service/rapla/html_resources/rapla_week_response_1.html', + ).readAsString(); test('Rapla correctly read all classes of weekly view', () async { - var parser = RaplaResponseParser(); + final parser = RaplaResponseParser(); - var schedule = parser.parseSchedule(raplaPage).schedule; + final schedule = parser.parseSchedule(raplaPage).schedule; expect(schedule.entries.length, 8); expect(schedule.entries[0].title, "Netztechnik I"); expect(schedule.entries[0].start, DateTime(2020, 09, 07, 09, 15)); expect(schedule.entries[0].end, DateTime(2020, 09, 07, 11, 45)); - expect(schedule.entries[0].type, ScheduleEntryType.Class); + expect(schedule.entries[0].type, ScheduleEntryType.Lesson); expect(schedule.entries[0].professor, "Müller, Georg"); expect(schedule.entries[1].title, "Semestereinführung"); - expect(schedule.entries[1].start, DateTime(2020, 09, 07, 12, 00)); + expect(schedule.entries[1].start, DateTime(2020, 09, 07, 12)); expect(schedule.entries[1].end, DateTime(2020, 09, 07, 12, 30)); - expect(schedule.entries[1].type, ScheduleEntryType.Class); + expect(schedule.entries[1].type, ScheduleEntryType.Lesson); expect(schedule.entries[2].title, "Messdatenerfassung"); - expect(schedule.entries[2].start, DateTime(2020, 09, 07, 13, 00)); + expect(schedule.entries[2].start, DateTime(2020, 09, 07, 13)); expect(schedule.entries[2].end, DateTime(2020, 09, 07, 14, 30)); - expect(schedule.entries[2].type, ScheduleEntryType.Class); + expect(schedule.entries[2].type, ScheduleEntryType.Lesson); expect(schedule.entries[3].title, "Formale Sprachen & Automaten"); expect(schedule.entries[3].start, DateTime(2020, 09, 08, 08, 15)); expect(schedule.entries[3].end, DateTime(2020, 09, 08, 11, 45)); - expect(schedule.entries[3].type, ScheduleEntryType.Class); + expect(schedule.entries[3].type, ScheduleEntryType.Lesson); expect(schedule.entries[4].title, "Signale & Systeme I"); - expect(schedule.entries[4].start, DateTime(2020, 09, 08, 13, 00)); - expect(schedule.entries[4].end, DateTime(2020, 09, 08, 15, 00)); - expect(schedule.entries[4].type, ScheduleEntryType.Class); + expect(schedule.entries[4].start, DateTime(2020, 09, 08, 13)); + expect(schedule.entries[4].end, DateTime(2020, 09, 08, 15)); + expect(schedule.entries[4].type, ScheduleEntryType.Lesson); expect(schedule.entries[5].title, "Angewandte Mathematik"); - expect(schedule.entries[5].start, DateTime(2020, 09, 09, 09, 00)); + expect(schedule.entries[5].start, DateTime(2020, 09, 09, 09)); expect(schedule.entries[5].end, DateTime(2020, 09, 09, 11, 45)); - expect(schedule.entries[5].type, ScheduleEntryType.Class); + expect(schedule.entries[5].type, ScheduleEntryType.Lesson); expect(schedule.entries[6].title, "SWE"); expect(schedule.entries[6].start, DateTime(2020, 09, 10, 09, 15)); - expect(schedule.entries[6].end, DateTime(2020, 09, 10, 12, 00)); - expect(schedule.entries[6].type, ScheduleEntryType.Class); + expect(schedule.entries[6].end, DateTime(2020, 09, 10, 12)); + expect(schedule.entries[6].type, ScheduleEntryType.Lesson); expect(schedule.entries[7].title, "Messdatenerfassung"); - expect(schedule.entries[7].start, DateTime(2020, 09, 10, 13, 00)); + expect(schedule.entries[7].start, DateTime(2020, 09, 10, 13)); expect(schedule.entries[7].end, DateTime(2020, 09, 10, 14, 30)); - expect(schedule.entries[7].type, ScheduleEntryType.Class); + expect(schedule.entries[7].type, ScheduleEntryType.Lesson); }); test('Rapla correctly read all classes of monthly view', () async { - var parser = RaplaResponseParser(); + final parser = RaplaResponseParser(); - var schedule = parser.parseSchedule(monthlyRaplaPage).schedule; + final schedule = parser.parseSchedule(monthlyRaplaPage).schedule; expect(schedule.entries.length, 9); expect(schedule.entries[0].title, "Mikrocontroller ONLINE"); - expect(schedule.entries[0].start, DateTime(2020, 10, 01, 13, 00)); - expect(schedule.entries[0].end, DateTime(2020, 10, 01, 18, 00)); - expect(schedule.entries[0].type, ScheduleEntryType.Class); + expect(schedule.entries[0].start, DateTime(2020, 10, 01, 13)); + expect(schedule.entries[0].end, DateTime(2020, 10, 01, 18)); + expect(schedule.entries[0].type, ScheduleEntryType.Lesson); expect(schedule.entries[0].professor, "Schmitt, Tobias"); }); test('Rapla correctly read all classes of several months view', () async { - var parser = RaplaResponseParser(); + final parser = RaplaResponseParser(); - var schedule = parser.parseSchedule(severalMonthsPage).schedule; + final schedule = parser.parseSchedule(severalMonthsPage).schedule; expect(schedule.entries[0].title, "Modulprüfung T3_2000"); - expect(schedule.entries[0].start, DateTime(2021, 09, 22, 08, 00)); - expect(schedule.entries[0].end, DateTime(2021, 09, 22, 15, 00)); - expect(schedule.entries[0].type, ScheduleEntryType.Class); + expect(schedule.entries[0].start, DateTime(2021, 09, 22, 08)); + expect(schedule.entries[0].end, DateTime(2021, 09, 22, 15)); + expect(schedule.entries[0].type, ScheduleEntryType.Lesson); expect(schedule.entries[0].professor, "A"); - expect(schedule.entries[0].room, "MOS-TINF19A,A 1.380 Vorlesungsraum (Mi 22.09.21 08:00, Do 23.09.21 08:00),A 1.390 Vorlesungsraum (Mi 22.09.21 08:00, Do 23.09.21 08:00)"); + expect( + schedule.entries[0].room, + "MOS-TINF19A,A 1.380 Vorlesungsraum (Mi 22.09.21 08:00, Do 23.09.21 08:00),A 1.390 Vorlesungsraum (Mi 22.09.21 08:00, Do 23.09.21 08:00)", + ); expect(schedule.entries[2].title, "Tag der Deutschen Einheit"); - expect(schedule.entries[2].start, DateTime(2021, 10, 03, 08, 00)); - expect(schedule.entries[2].end, DateTime(2021, 10, 03, 18, 00)); + expect(schedule.entries[2].start, DateTime(2021, 10, 03, 08)); + expect(schedule.entries[2].end, DateTime(2021, 10, 03, 18)); expect(schedule.entries[2].type, ScheduleEntryType.Unknown); expect(schedule.entries[8].title, "Ausgewählte Themen der Informatik"); expect(schedule.entries[8].start, DateTime(2021, 10, 06, 13, 45)); - expect(schedule.entries[8].end, DateTime(2021, 10, 06, 17, 00)); - expect(schedule.entries[8].type, ScheduleEntryType.Class); + expect(schedule.entries[8].end, DateTime(2021, 10, 06, 17)); + expect(schedule.entries[8].type, ScheduleEntryType.Lesson); expect(schedule.entries[84].title, "Silvester"); - expect(schedule.entries[84].start, DateTime(2021, 12, 31, 08, 00)); - expect(schedule.entries[84].end, DateTime(2021, 12, 31, 18, 00)); + expect(schedule.entries[84].start, DateTime(2021, 12, 31, 08)); + expect(schedule.entries[84].end, DateTime(2021, 12, 31, 18)); expect(schedule.entries[84].type, ScheduleEntryType.Unknown); expect(schedule.entries.length, 85); @@ -136,112 +139,136 @@ Future main() async { test('Rapla correctly read all classes of problematic several months view', () async { - var parser = RaplaResponseParser(); + final parser = RaplaResponseParser(); - var schedule = parser.parseSchedule(severalMonthsPage1).schedule; + final schedule = parser.parseSchedule(severalMonthsPage1).schedule; expect(schedule.entries[0].title, "Verkehrswegebau und Straßenwesen"); expect(schedule.entries[0].start, DateTime(2021, 12, 01, 08, 15)); expect(schedule.entries[0].end, DateTime(2021, 12, 01, 12, 15)); - expect(schedule.entries[0].type, ScheduleEntryType.Class); + expect(schedule.entries[0].type, ScheduleEntryType.Lesson); expect(schedule.entries[0].professor, "Müller"); expect(schedule.entries[3].title, "Marketing und Unternehmensstrategie"); - expect(schedule.entries[3].start, DateTime(2021, 12, 03, 13, 00)); + expect(schedule.entries[3].start, DateTime(2021, 12, 03, 13)); expect(schedule.entries[3].end, DateTime(2021, 12, 03, 16, 15)); - expect(schedule.entries[3].type, ScheduleEntryType.Class); + expect(schedule.entries[3].type, ScheduleEntryType.Lesson); expect(schedule.entries[3].professor, "Mayer"); expect(schedule.entries[11].title, "Stahlbetonbau"); - expect(schedule.entries[11].start, DateTime(2021, 12, 17, 09, 00)); + expect(schedule.entries[11].start, DateTime(2021, 12, 17, 09)); expect(schedule.entries[11].end, DateTime(2021, 12, 17, 10, 30)); expect(schedule.entries[11].type, ScheduleEntryType.Exam); expect(schedule.entries[17].title, "Silvester"); - expect(schedule.entries[17].start, DateTime(2021, 12, 31, 08, 00)); - expect(schedule.entries[17].end, DateTime(2021, 12, 31, 18, 00)); + expect(schedule.entries[17].start, DateTime(2021, 12, 31, 08)); + expect(schedule.entries[17].end, DateTime(2021, 12, 31, 18)); expect(schedule.entries[17].type, ScheduleEntryType.Unknown); expect(schedule.entries.length, 20); }); - test('Rapla correctly read all classes of several months view 3', - () async { - var parser = RaplaResponseParser(); + test('Rapla correctly read all classes of several months view 3', () async { + final parser = RaplaResponseParser(); - var schedule = parser.parseSchedule(severalMonthsPage2).schedule; + final schedule = parser.parseSchedule(severalMonthsPage2).schedule; expect(schedule.entries[0].title, "Marketing und Unternehmensstrategie"); - expect(schedule.entries[0].start, DateTime(2021, 12, 01, 10, 00)); + expect(schedule.entries[0].start, DateTime(2021, 12, 01, 10)); expect(schedule.entries[0].end, DateTime(2021, 12, 01, 13, 30)); expect(schedule.entries[0].type, ScheduleEntryType.Unknown); expect(schedule.entries.length, 36); }); - test('Rapla correctly read the day of a class in week view', - () async { - var parser = RaplaResponseParser(); + test('Rapla correctly read the day of a class in week view', () async { + final parser = RaplaResponseParser(); - var schedule = parser.parseSchedule(raplaPage1).schedule; + final schedule = parser.parseSchedule(raplaPage1).schedule; expect(schedule.entries[0].title, "Grundlagen der Handelsbetriebslehre"); - expect(schedule.entries[0].start, DateTime(2021, 11, 02, 09, 00)); + expect(schedule.entries[0].start, DateTime(2021, 11, 02, 09)); expect(schedule.entries[0].end, DateTime(2021, 11, 02, 12, 15)); - expect(schedule.entries[0].type, ScheduleEntryType.Class); + expect(schedule.entries[0].type, ScheduleEntryType.Lesson); expect(schedule.entries[0].professor, "Fr, Ta"); - expect(schedule.entries[0].room, "WDCM21B,G086 W Hörsaal (Di 26.10.21 13:00, Do 04.11.21 12:45, Di 16.11.21 10:00),A167 W Hörsaal (Di 30.11.21 10:00),F218_1 PA Hörsaal (Di 07.12.21 10:00, Mi 08.12.21 13:00),XOnline-Veranstaltung A Virtueller Raum (Di 02.11.21 09:00, Do 11.11.21 09:00)"); - - expect(schedule.entries[1].title, "Einführung in die Volkswirtschaftslehre und Mikroökonomik"); + expect( + schedule.entries[0].room, + "WDCM21B,G086 W Hörsaal (Di 26.10.21 13:00, Do 04.11.21 12:45, Di 16.11.21 10:00),A167 W Hörsaal (Di 30.11.21 10:00),F218_1 PA Hörsaal (Di 07.12.21 10:00, Mi 08.12.21 13:00),XOnline-Veranstaltung A Virtueller Raum (Di 02.11.21 09:00, Do 11.11.21 09:00)", + ); + + expect( + schedule.entries[1].title, + "Einführung in die Volkswirtschaftslehre und Mikroökonomik", + ); expect(schedule.entries[1].start, DateTime(2021, 11, 02, 13, 45)); - expect(schedule.entries[1].end, DateTime(2021, 11, 02, 17, 00)); - expect(schedule.entries[1].type, ScheduleEntryType.Class); + expect(schedule.entries[1].end, DateTime(2021, 11, 02, 17)); + expect(schedule.entries[1].type, ScheduleEntryType.Lesson); expect(schedule.entries[1].professor, "Le, An"); - expect(schedule.entries[1].room, "WDCM21B,D221 W Hörsaal (Mo 11.10.21 09:00),G086 W Hörsaal (Mo 25.10.21 09:00, Mo 15.11.21 09:00),F218_1 PA Hörsaal (Mo 22.11.21 09:00, Mo 06.12.21 09:00),A167 W Hörsaal (Mo 29.11.21 09:00),XOnline-Veranstaltung A Virtueller Raum (Di 02.11.21 13:45)"); + expect( + schedule.entries[1].room, + "WDCM21B,D221 W Hörsaal (Mo 11.10.21 09:00),G086 W Hörsaal (Mo 25.10.21 09:00, Mo 15.11.21 09:00),F218_1 PA Hörsaal (Mo 22.11.21 09:00, Mo 06.12.21 09:00),A167 W Hörsaal (Mo 29.11.21 09:00),XOnline-Veranstaltung A Virtueller Raum (Di 02.11.21 13:45)", + ); expect(schedule.entries[2].title, "Grundlagen des Bürgerlichen Rechts"); - expect(schedule.entries[2].start, DateTime(2021, 11, 03, 09, 00)); + expect(schedule.entries[2].start, DateTime(2021, 11, 03, 09)); expect(schedule.entries[2].end, DateTime(2021, 11, 03, 11, 30)); - expect(schedule.entries[2].type, ScheduleEntryType.Class); + expect(schedule.entries[2].type, ScheduleEntryType.Lesson); expect(schedule.entries[2].professor, "Ei, An"); - expect(schedule.entries[2].room, "WDCM21B,XOnline-Veranstaltung A Virtueller Raum (Mi 13.10.21 09:00, Mi 27.10.21 09:00, Mi 10.11.21 09:00, Mi 24.11.21 09:00, Fr 03.12.21 10:00),D221 W Hörsaal (Mi 20.10.21 13:00, Mi 17.11.21 09:00),G086 W Hörsaal (Di 26.10.21 09:00, Mi 03.11.21 09:00),B354 W Hörsaal (Fr 03.12.21 10:00),F218_1 PA Hörsaal (Mi 08.12.21 09:00)"); + expect( + schedule.entries[2].room, + "WDCM21B,XOnline-Veranstaltung A Virtueller Raum (Mi 13.10.21 09:00, Mi 27.10.21 09:00, Mi 10.11.21 09:00, Mi 24.11.21 09:00, Fr 03.12.21 10:00),D221 W Hörsaal (Mi 20.10.21 13:00, Mi 17.11.21 09:00),G086 W Hörsaal (Di 26.10.21 09:00, Mi 03.11.21 09:00),B354 W Hörsaal (Fr 03.12.21 10:00),F218_1 PA Hörsaal (Mi 08.12.21 09:00)", + ); expect(schedule.entries[3].title, "Technik der Finanzbuchführung I"); - expect(schedule.entries[3].start, DateTime(2021, 11, 03, 13, 00)); + expect(schedule.entries[3].start, DateTime(2021, 11, 03, 13)); expect(schedule.entries[3].end, DateTime(2021, 11, 03, 16, 15)); - expect(schedule.entries[3].type, ScheduleEntryType.Class); + expect(schedule.entries[3].type, ScheduleEntryType.Lesson); expect(schedule.entries[3].professor, "Se, Ka"); - expect(schedule.entries[3].room, "WDCM21B,D221 W Hörsaal (Mi 17.11.21 13:00, Mi 06.10.21 13:00, Mi 13.10.21 13:00),G086 W Hörsaal (Mi 27.10.21 13:00, Mi 03.11.21 13:00, Mi 10.11.21 13:00),A167 W Hörsaal (Mi 24.11.21 13:00, Mi 01.12.21 14:00)"); - - expect(schedule.entries[4].title, "Grundlagen des wissenschaftlichen Arbeitens"); - expect(schedule.entries[4].start, DateTime(2021, 11, 04, 09, 00)); + expect( + schedule.entries[3].room, + "WDCM21B,D221 W Hörsaal (Mi 17.11.21 13:00, Mi 06.10.21 13:00, Mi 13.10.21 13:00),G086 W Hörsaal (Mi 27.10.21 13:00, Mi 03.11.21 13:00, Mi 10.11.21 13:00),A167 W Hörsaal (Mi 24.11.21 13:00, Mi 01.12.21 14:00)", + ); + + expect( + schedule.entries[4].title, + "Grundlagen des wissenschaftlichen Arbeitens", + ); + expect(schedule.entries[4].start, DateTime(2021, 11, 04, 09)); expect(schedule.entries[4].end, DateTime(2021, 11, 04, 12, 15)); - expect(schedule.entries[4].type, ScheduleEntryType.Class); + expect(schedule.entries[4].type, ScheduleEntryType.Lesson); expect(schedule.entries[4].professor, "He, Be"); - expect(schedule.entries[4].room, "WDCM21B,D221 W Hörsaal (Di 05.10.21 09:00, Di 12.10.21 09:00),A167 W Hörsaal (Di 23.11.21 09:00),G086 W Hörsaal (Do 04.11.21 09:00)"); + expect( + schedule.entries[4].room, + "WDCM21B,D221 W Hörsaal (Di 05.10.21 09:00, Di 12.10.21 09:00),A167 W Hörsaal (Di 23.11.21 09:00),G086 W Hörsaal (Do 04.11.21 09:00)", + ); expect(schedule.entries[5].title, "Grundlagen der Handelsbetriebslehre"); expect(schedule.entries[5].start, DateTime(2021, 11, 04, 12, 45)); - expect(schedule.entries[5].end, DateTime(2021, 11, 04, 16, 00)); - expect(schedule.entries[5].type, ScheduleEntryType.Class); + expect(schedule.entries[5].end, DateTime(2021, 11, 04, 16)); + expect(schedule.entries[5].type, ScheduleEntryType.Lesson); expect(schedule.entries[5].professor, "Fr, Ta"); - expect(schedule.entries[5].room, "WDCM21B,G086 W Hörsaal (Di 26.10.21 13:00, Do 04.11.21 12:45, Di 16.11.21 10:00),A167 W Hörsaal (Di 30.11.21 10:00),F218_1 PA Hörsaal (Di 07.12.21 10:00, Mi 08.12.21 13:00),XOnline-Veranstaltung A Virtueller Raum (Di 02.11.21 09:00, Do 11.11.21 09:00)"); + expect( + schedule.entries[5].room, + "WDCM21B,G086 W Hörsaal (Di 26.10.21 13:00, Do 04.11.21 12:45, Di 16.11.21 10:00),A167 W Hörsaal (Di 30.11.21 10:00),F218_1 PA Hörsaal (Di 07.12.21 10:00, Mi 08.12.21 13:00),XOnline-Veranstaltung A Virtueller Raum (Di 02.11.21 09:00, Do 11.11.21 09:00)", + ); expect(schedule.entries[6].title, "Einführung in die Programmierung"); - expect(schedule.entries[6].start, DateTime(2021, 11, 05, 13, 00)); + expect(schedule.entries[6].start, DateTime(2021, 11, 05, 13)); expect(schedule.entries[6].end, DateTime(2021, 11, 05, 16, 15)); - expect(schedule.entries[6].type, ScheduleEntryType.Class); + expect(schedule.entries[6].type, ScheduleEntryType.Lesson); expect(schedule.entries[6].professor, "He, Ma"); - expect(schedule.entries[6].room, "WDCM21B,C348 PC Raum,D221 W Hörsaal (Fr 08.10.21 13:00, Fr 15.10.21 13:00, Fr 22.10.21 13:00),G086 W Hörsaal (Fr 29.10.21 13:00, Fr 05.11.21 13:00, Fr 12.11.21 13:00, Do 18.11.21 13:00),A167 W Hörsaal (Fr 26.11.21 13:00, Do 02.12.21 13:00)"); + expect( + schedule.entries[6].room, + "WDCM21B,C348 PC Raum,D221 W Hörsaal (Fr 08.10.21 13:00, Fr 15.10.21 13:00, Fr 22.10.21 13:00),G086 W Hörsaal (Fr 29.10.21 13:00, Fr 05.11.21 13:00, Fr 12.11.21 13:00, Do 18.11.21 13:00),A167 W Hörsaal (Fr 26.11.21 13:00, Do 02.12.21 13:00)", + ); expect(schedule.entries.length, 7); }); - test('Rapla correctly read the week response', - () async { - var parser = RaplaResponseParser(); + test('Rapla correctly read the week response', () async { + final parser = RaplaResponseParser(); - var schedule = parser.parseSchedule(raplaWeekResponse).schedule; + final schedule = parser.parseSchedule(raplaWeekResponse).schedule; expect(schedule.entries[0].title, "Geschäftsprozesse, T3ELG1010"); expect(schedule.entries[0].start, DateTime(2022, 02, 21, 08, 30)); @@ -253,28 +280,30 @@ Future main() async { expect(schedule.entries.length, 6); }); - test('Rapla correctly read the week response 1', - () async { - var parser = RaplaResponseParser(); + test('Rapla correctly read the week response 1', () async { + final parser = RaplaResponseParser(); - var schedule = parser.parseSchedule(raplaWeekResponse1).schedule; + final schedule = parser.parseSchedule(raplaWeekResponse1).schedule; expect(schedule.entries[0].title, "Klausur Elektronik und Messtechnik"); - expect(schedule.entries[0].start, DateTime(2021, 12, 13, 08, 00)); - expect(schedule.entries[0].end, DateTime(2021, 12, 13, 10, 00)); + expect(schedule.entries[0].start, DateTime(2021, 12, 13, 08)); + expect(schedule.entries[0].end, DateTime(2021, 12, 13, 10)); expect(schedule.entries[0].type, ScheduleEntryType.Exam); expect(schedule.entries[0].professor, "Man, R."); - expect(schedule.entries[0].room, "TEA20,H031, Hörsaal,N003, Hörsaal,N004, Hörsaal"); + expect( + schedule.entries[0].room, + "TEA20,H031, Hörsaal,N003, Hörsaal,N004, Hörsaal", + ); expect(schedule.entries.length, 7); }); test('Rapla robust parse', () async { - var parser = RaplaResponseParser(); + final parser = RaplaResponseParser(); - var result = parser.parseSchedule(invalidRaplaPage); - var schedule = result.schedule; - var errors = result.errors; + final result = parser.parseSchedule(invalidRaplaPage); + final schedule = result.schedule; + final errors = result.errors; expect(errors.length, 3); expect(schedule.entries.length, 5);