diff --git a/.fvmrc b/.fvmrc index 4cfa3d5f..1efca67f 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.29.0" + "flutter": "3.35.0" } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f14a2da4..2b9c33a8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "conventionalCommits.autoCommit": false, "conventionalCommits.gitmoji": false, "conventionalCommits.promptBody": false, - "dart.flutterSdkPath": ".fvm/versions/3.29.0", + "dart.flutterSdkPath": ".fvm/versions/3.35.0", "dart.lineLength": 150, "conventionalCommits.scopes": [ "auth", diff --git a/build.yaml b/build.yaml index 7cb5117d..89de2556 100644 --- a/build.yaml +++ b/build.yaml @@ -2,5 +2,11 @@ targets: $default: builders: json_serializable: - options: - explicit_to_json: true + generate_for: + - lib/src/*/domain/models/** + source_gen:combining_builder: + generate_for: + - lib/src/*/domain/models/** + freezed: + generate_for: + - lib/src/*/domain/models/** diff --git a/l10n.yaml b/l10n.yaml index 90f59075..eda0a68c 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -2,5 +2,4 @@ arb-dir: l10n template-arb-file: en.arb nullable-getter: false untranslated-messages-file: l10n/untranslated_messages.json -synthetic-package: false output-dir: lib/gen/l10n diff --git a/l10n/de.arb b/l10n/de.arb index 716c2b06..25359800 100644 --- a/l10n/de.arb +++ b/l10n/de.arb @@ -48,6 +48,10 @@ "@global_edit": { "description": "Global edit button label." }, + "global_duplicate": "Duplizieren", + "@global_duplicate": { + "description": "Global duplicate button label." + }, "global_delete": "Löschen", "@global_delete": { "description": "Global delete button label." @@ -595,7 +599,7 @@ } } }, - "slots_details_sizeCount": "Größe {count}", + "slots_details_sizeCount": "Schüler: {count}", "@slots_details_sizeCount": { "description": "Displays the size of the slot, using a count placeholder.", "placeholders": { @@ -651,7 +655,7 @@ "@slots_edit_room": { "description": "Label for the room input in slot editing." }, - "slots_edit_size": "Größe", + "slots_edit_size": "Schüler", "@slots_edit_size": { "description": "Label for the size input in slot editing." }, @@ -667,6 +671,10 @@ "@slots_edit_courseMappings": { "description": "Label for the course mappings section in slot editing." }, + "slots_edit_addCourseMapping": "Neue Zuordnung", + "@slots_edit_addCourseMapping": { + "description": "Label for the add mapping button." + }, "slots_edit_selectCourse": "Kurs auswählen", "@slots_edit_selectCourse": { "description": "Prompt to select a course for slot mapping." @@ -729,5 +737,75 @@ "slots_unbook_error": "Ein unerwarteter Fehler ist bei der Stornierung deiner Reservierung aufgetreten. Bitte versuche es später erneut.", "@slots_unbook_error": { "description": "Error message displayed when unbooking a slot fails." + }, + "kanban_title": "Kanban Board", + "@kanban_title": { + "description": "Title for the kanban board view." + }, + "kanban_card_dueOn": "Fällig {dueDate}", + "@kanban_card_dueOn": { + "description": "Display for when a task is due in the kanban board.", + "placeholders": { + "dueDate": { + "type": "String" + } + } + }, + "kanban_card_plannedOn": "Geplant {plannedDate}", + "@kanban_card_plannedOn": { + "description": "Display for when a task is planned in the kanban board", + "placeholders": { + "plannedDate": { + "type": "String" + } + } + }, + "kanban_screen_hideBacklog": "Backlog ausblenden", + "@kanban_screen_hideBacklog": { + "description": "Label for the hide backlog button." + }, + "kanban_screen_showBacklog": "Backlog einblenden", + "@kanban_screen_showBacklog": { + "description": "Label for the show backlog button" + }, + "kanban_screen_backlog": "Backlog", + "@kanban_screen_backlog": { + "description": "Label for the backlog column." + }, + "kanban_screen_toDo": "To Do", + "@kanban_screen_toDo": { + "description": "Label for the to do column." + }, + "kanban_screen_inProgress": "In Arbeit", + "@kanban_screen_inProgress": { + "description": "Label for the in progress column." + }, + "kanban_screen_done": "Fertig", + "@kanban_screen_done": { + "description": "Label for the done column." + }, + "kanban_settings_kanban": "Kanban", + "@kanban_settings_kanban": { + "description": "Label for the kanban settings" + }, + "kanban_settings_disabled": "Deaktiviert", + "@kanban_settings_disabled": { + "description": "Label for the disabled option." + }, + "kanban_settings_moveSubmittedTasks": "Abgegebene Aufgaben bewegen", + "@kanban_settings_moveSubmittedTasks": { + "description": "Label for the move submitted tasks setting." + }, + "kanban_settings_moveOverdueTasks": "Überfällige Aufgaben bewegen", + "@kanban_settings_moveOverdueTasks": { + "description": "Label for the move overdue tasks setting." + }, + "kanban_settings_moveCompletedTasks": "Erledigte Aufgaben bewegen", + "@kanban_settings_moveCompletedTasks": { + "description": "Label for the move completed tasks setting." + }, + "kanban_settings_columnColors": "Spaltenfarben", + "@kanban_settings_columnColors": { + "description": "Label for the column colors setting." } } \ No newline at end of file diff --git a/l10n/en.arb b/l10n/en.arb index d37c9360..a2473f95 100644 --- a/l10n/en.arb +++ b/l10n/en.arb @@ -48,6 +48,10 @@ "@global_edit": { "description": "Global edit button label." }, + "global_duplicate": "Duplicate", + "@global_duplicate": { + "description": "Global duplicate button label." + }, "global_delete": "Delete", "@global_delete": { "description": "Global delete button label." @@ -597,7 +601,7 @@ } } }, - "slots_details_sizeCount": "Size {count}", + "slots_details_sizeCount": "Students: {count}", "@slots_details_sizeCount": { "description": "Displays the size of the slot, using a count placeholder.", "placeholders": { @@ -653,7 +657,7 @@ "@slots_edit_room": { "description": "Label for the room input in slot editing." }, - "slots_edit_size": "Size", + "slots_edit_size": "Students", "@slots_edit_size": { "description": "Label for the size input in slot editing." }, @@ -669,6 +673,10 @@ "@slots_edit_courseMappings": { "description": "Label for the course mappings section in slot editing." }, + "slots_edit_addCourseMapping": "Add mapping", + "@slots_edit_addCourseMapping": { + "description": "Label for the add mapping button." + }, "slots_edit_selectCourse": "Select course", "@slots_edit_selectCourse": { "description": "Prompt to select a course for slot mapping." @@ -737,5 +745,75 @@ "global_disclaimer": "Please note that this app is currently in public **beta**. This means that there may be bugs and missing features. If you encounter any issues, please report them to us. Also, note that your faculty is still **in the process of migrating** to this new system. This means that some data may be **incomplete or incorrect**. Please **do not rely** on this app for any critical information just yet :)\n\nThank you for your understanding and support! ❤️", "@global_disclaimer": { "description": "Disclaimer message for the beta version of the app." + }, + "kanban_title": "Kanban Board", + "@kanban_title": { + "description": "Title for the kanban board view." + }, + "kanban_card_dueOn": "Due {dueDate}", + "@kanban_card_dueOn": { + "description": "Display for when a task is due in the kanban board.", + "placeholders": { + "dueDate": { + "type": "String" + } + } + }, + "kanban_card_plannedOn": "Planned {plannedDate}", + "@kanban_card_plannedOn": { + "description": "Display for when a task is planned in the kanban board", + "placeholders": { + "plannedDate": { + "type": "String" + } + } + }, + "kanban_screen_hideBacklog": "Hide Backlog", + "@kanban_screen_hideBacklog": { + "description": "Label for the hide backlog button." + }, + "kanban_screen_showBacklog": "Show Backlog", + "@kanban_screen_showBacklog": { + "description": "Label for the show backlog button" + }, + "kanban_screen_backlog": "Backlog", + "@kanban_screen_backlog": { + "description": "Label for the backlog column." + }, + "kanban_screen_toDo": "To Do", + "@kanban_screen_toDo": { + "description": "Label for the to do column." + }, + "kanban_screen_inProgress": "In Progress", + "@kanban_screen_inProgress": { + "description": "Label for the in progress column." + }, + "kanban_screen_done": "Done", + "@kanban_screen_done": { + "description": "Label for the done column." + }, + "kanban_settings_kanban": "Kanban", + "@kanban_settings_kanban": { + "description": "Label for the kanban settings" + }, + "kanban_settings_disabled": "Disabled", + "@kanban_settings_disabled": { + "description": "Label for the disabled option." + }, + "kanban_settings_moveSubmittedTasks": "Move Submitted Tasks", + "@kanban_settings_moveSubmittedTasks": { + "description": "Label for the move submitted tasks setting." + }, + "kanban_settings_moveOverdueTasks": "Move Overdue Tasks", + "@kanban_settings_moveOverdueTasks": { + "description": "Label for the move overdue tasks setting." + }, + "kanban_settings_moveCompletedTasks": "Move Completed Tasks", + "@kanban_settings_moveCompletedTasks": { + "description": "Label for the move completed tasks setting." + }, + "kanban_settings_columnColors": "Column Colors", + "@kanban_settings_columnColors": { + "description": "Label for the column colors setting." } } \ No newline at end of file diff --git a/lib/config/echidna.dart b/lib/config/echidna.dart index 903f6fa1..f3823c54 100644 --- a/lib/config/echidna.dart +++ b/lib/config/echidna.dart @@ -2,13 +2,17 @@ library lb_planner.configs.echidna; /// The client key for the echidna server. +@Deprecated('Licensing has been removed for the time being') const kEchidnaClientKey = String.fromEnvironment('ECHIDNA_CLIENT_KEY'); /// The client id for the echidna server. +@Deprecated('Licensing has been removed for the time being') const kEchidnaClientID = int.fromEnvironment('ECHIDNA_CLIENT_ID'); /// The url to the echidna server. +@Deprecated('Licensing has been removed for the time being') const kEchidnaHost = String.fromEnvironment('ECHIDNA_HOST'); /// The feature id for the calendar plan feature in echidna. +@Deprecated('Licensing has been removed for the time being') const kCalendarPlanFeatureID = int.fromEnvironment('CALENDAR_PLAN_FEATURE_ID'); diff --git a/lib/eduplanner.dart b/lib/eduplanner.dart index 2f381177..802444a0 100644 --- a/lib/eduplanner.dart +++ b/lib/eduplanner.dart @@ -3,6 +3,7 @@ export 'src/auth/auth.dart'; export 'src/calendar/calendar.dart'; export 'src/course_overview/course_overview.dart'; export 'src/dashboard/dashboard.dart'; +export 'src/kanban/kanban.dart'; export 'src/moodle/moodle.dart'; export 'src/notifications/notifications.dart'; export 'src/settings/settings.dart'; diff --git a/lib/gen/l10n/app_localizations.dart b/lib/gen/l10n/app_localizations.dart index 1ce009e8..64a2be90 100644 --- a/lib/gen/l10n/app_localizations.dart +++ b/lib/gen/l10n/app_localizations.dart @@ -62,7 +62,8 @@ import 'app_localizations_en.dart'; /// be consistent with the languages listed in the AppLocalizations.supportedLocales /// property. abstract class AppLocalizations { - AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; @@ -70,7 +71,8 @@ abstract class AppLocalizations { return Localizations.of(context, AppLocalizations)!; } - static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); /// A list of this localizations delegate along with the default localizations /// delegates. @@ -82,7 +84,8 @@ abstract class AppLocalizations { /// Additional delegates can be added by appending to this list in /// MaterialApp. This list does not have to be used at all if a custom list /// of delegates is preferred or required. - static const List> localizationsDelegates = >[ + static const List> localizationsDelegates = + >[ delegate, GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, @@ -167,6 +170,12 @@ abstract class AppLocalizations { /// **'Edit'** String get global_edit; + /// Global duplicate button label. + /// + /// In en, this message translates to: + /// **'Duplicate'** + String get global_duplicate; + /// Global delete button label. /// /// In en, this message translates to: @@ -860,7 +869,7 @@ abstract class AppLocalizations { /// Displays the size of the slot, using a count placeholder. /// /// In en, this message translates to: - /// **'Size {count}'** + /// **'Students: {count}'** String slots_details_sizeCount(int count); /// Button label to create a new slot. @@ -873,7 +882,8 @@ abstract class AppLocalizations { /// /// In en, this message translates to: /// **'Delete slot {room} {startUnit} - {endUnit}?'** - String slots_slotmaster_deleteSlot_title(String room, String startUnit, String endUnit); + String slots_slotmaster_deleteSlot_title( + String room, String startUnit, String endUnit); /// Confirmation message for deleting a slot. /// @@ -920,7 +930,7 @@ abstract class AppLocalizations { /// Label for the size input in slot editing. /// /// In en, this message translates to: - /// **'Size'** + /// **'Students'** String get slots_edit_size; /// Label for the supervisors section in slot editing. @@ -941,6 +951,12 @@ abstract class AppLocalizations { /// **'Course Mappings'** String get slots_edit_courseMappings; + /// Label for the add mapping button. + /// + /// In en, this message translates to: + /// **'Add mapping'** + String get slots_edit_addCourseMapping; + /// Prompt to select a course for slot mapping. /// /// In en, this message translates to: @@ -1036,9 +1052,100 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Please note that this app is currently in public **beta**. This means that there may be bugs and missing features. If you encounter any issues, please report them to us. Also, note that your faculty is still **in the process of migrating** to this new system. This means that some data may be **incomplete or incorrect**. Please **do not rely** on this app for any critical information just yet :)\n\nThank you for your understanding and support! ❤️'** String get global_disclaimer; + + /// Title for the kanban board view. + /// + /// In en, this message translates to: + /// **'Kanban Board'** + String get kanban_title; + + /// Display for when a task is due in the kanban board. + /// + /// In en, this message translates to: + /// **'Due {dueDate}'** + String kanban_card_dueOn(String dueDate); + + /// Display for when a task is planned in the kanban board + /// + /// In en, this message translates to: + /// **'Planned {plannedDate}'** + String kanban_card_plannedOn(String plannedDate); + + /// Label for the hide backlog button. + /// + /// In en, this message translates to: + /// **'Hide Backlog'** + String get kanban_screen_hideBacklog; + + /// Label for the show backlog button + /// + /// In en, this message translates to: + /// **'Show Backlog'** + String get kanban_screen_showBacklog; + + /// Label for the backlog column. + /// + /// In en, this message translates to: + /// **'Backlog'** + String get kanban_screen_backlog; + + /// Label for the to do column. + /// + /// In en, this message translates to: + /// **'To Do'** + String get kanban_screen_toDo; + + /// Label for the in progress column. + /// + /// In en, this message translates to: + /// **'In Progress'** + String get kanban_screen_inProgress; + + /// Label for the done column. + /// + /// In en, this message translates to: + /// **'Done'** + String get kanban_screen_done; + + /// Label for the kanban settings + /// + /// In en, this message translates to: + /// **'Kanban'** + String get kanban_settings_kanban; + + /// Label for the disabled option. + /// + /// In en, this message translates to: + /// **'Disabled'** + String get kanban_settings_disabled; + + /// Label for the move submitted tasks setting. + /// + /// In en, this message translates to: + /// **'Move Submitted Tasks'** + String get kanban_settings_moveSubmittedTasks; + + /// Label for the move overdue tasks setting. + /// + /// In en, this message translates to: + /// **'Move Overdue Tasks'** + String get kanban_settings_moveOverdueTasks; + + /// Label for the move completed tasks setting. + /// + /// In en, this message translates to: + /// **'Move Completed Tasks'** + String get kanban_settings_moveCompletedTasks; + + /// Label for the column colors setting. + /// + /// In en, this message translates to: + /// **'Column Colors'** + String get kanban_settings_columnColors; } -class _AppLocalizationsDelegate extends LocalizationsDelegate { +class _AppLocalizationsDelegate + extends LocalizationsDelegate { const _AppLocalizationsDelegate(); @override @@ -1047,25 +1154,25 @@ class _AppLocalizationsDelegate extends LocalizationsDelegate } @override - bool isSupported(Locale locale) => ['de', 'en'].contains(locale.languageCode); + bool isSupported(Locale locale) => + ['de', 'en'].contains(locale.languageCode); @override bool shouldReload(_AppLocalizationsDelegate old) => false; } AppLocalizations lookupAppLocalizations(Locale locale) { - - // Lookup logic when only language code is specified. switch (locale.languageCode) { - case 'de': return AppLocalizationsDe(); - case 'en': return AppLocalizationsEn(); + case 'de': + return AppLocalizationsDe(); + case 'en': + return AppLocalizationsEn(); } throw FlutterError( - 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' - 'an issue with the localizations generation tool. Please file an issue ' - 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.' - ); + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.'); } diff --git a/lib/gen/l10n/app_localizations_de.dart b/lib/gen/l10n/app_localizations_de.dart index c8e9a203..2aba1bc1 100644 --- a/lib/gen/l10n/app_localizations_de.dart +++ b/lib/gen/l10n/app_localizations_de.dart @@ -44,6 +44,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get global_edit => 'Bearbeiten'; + @override + String get global_duplicate => 'Duplizieren'; + @override String get global_delete => 'Löschen'; @@ -59,7 +62,8 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get app_update_aur => 'Führe `yay -S lb-planner` aus, um auf die neueste Version zu aktualisieren.'; + String get app_update_aur => + 'Führe `yay -S lb-planner` aus, um auf die neueste Version zu aktualisieren.'; @override String app_update_download(String url) { @@ -67,10 +71,12 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get app_update_web => 'Bitte aktualisiere die Seite mit `Strg + Shift + F5`, um auf die neueste Version zu aktualisieren.'; + String get app_update_web => + 'Bitte aktualisiere die Seite mit `Strg + Shift + F5`, um auf die neueste Version zu aktualisieren.'; @override - String get app_noMobile_message => 'Diese Seite ist nicht auf Mobilgeräten verfügbar.'; + String get app_noMobile_message => + 'Diese Seite ist nicht auf Mobilgeräten verfügbar.'; @override String get app_noMobile_goBack => 'Zurück'; @@ -101,7 +107,8 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get auth_dataCollectionConsent => 'Ich akzeptiere die Erhebung und Verarbeitung meiner Daten wie beschrieben in der '; + String get auth_dataCollectionConsent => + 'Ich akzeptiere die Erhebung und Verarbeitung meiner Daten wie beschrieben in der '; @override String get auth_dataCollectionConsentSuffix => '.'; @@ -119,7 +126,8 @@ class AppLocalizationsDe extends AppLocalizations { String get calendar_tasksOverview_description_title => 'Was ist das?'; @override - String get calendar_tasksOverview_description => 'Die Aufgabenübersicht zeigt die Verteilung der Aufgaben über die Monate basierend auf den von den Lehrkräften gesetzten Fristen.'; + String get calendar_tasksOverview_description => + 'Die Aufgabenübersicht zeigt die Verteilung der Aufgaben über die Monate basierend auf den von den Lehrkräften gesetzten Fristen.'; @override String get calendar_title => 'Kalender'; @@ -134,7 +142,8 @@ class AppLocalizationsDe extends AppLocalizations { String get calendar_leave_title => 'Plan verlassen?'; @override - String get calendar_leave_message => 'Bist du sicher, dass du diesen Plan verlassen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.\nKeine Sorge, eine Kopie des geteilten Plans wird in deinem Konto gespeichert und du kannst jederzeit wieder eingeladen werden.'; + String get calendar_leave_message => + 'Bist du sicher, dass du diesen Plan verlassen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.\nKeine Sorge, eine Kopie des geteilten Plans wird in deinem Konto gespeichert und du kannst jederzeit wieder eingeladen werden.'; @override String get calendar_invited => 'Eingeladen'; @@ -176,7 +185,8 @@ class AppLocalizationsDe extends AppLocalizations { String get calendar_clearPlan_title => 'Plan leeren?'; @override - String get calendar_clearPlan_message => 'Bist du sicher, dass du deinen Plan leeren möchtest? Dadurch werden alle geplanten Aufgaben entfernt und diese Aktion kann nicht rückgängig gemacht werden.'; + String get calendar_clearPlan_message => + 'Bist du sicher, dass du deinen Plan leeren möchtest? Dadurch werden alle geplanten Aufgaben entfernt und diese Aktion kann nicht rückgängig gemacht werden.'; @override String get calendar_tasks => 'Aufgaben'; @@ -206,7 +216,8 @@ class AppLocalizationsDe extends AppLocalizations { String get dashboard_todaysTasks => 'Heutige Aufgaben'; @override - String get dashboard_todaysTasks_noTasks => 'Du hast für heute nichts geplant'; + String get dashboard_todaysTasks_noTasks => + 'Du hast für heute nichts geplant'; @override String get dashboard_statusOverview => 'Statusübersicht'; @@ -215,7 +226,8 @@ class AppLocalizationsDe extends AppLocalizations { String get dashboard_burnDownChart => 'Burndown-Diagramm'; @override - String get dashboard_burnDownChart_plannedTrajectory => 'Geplante Entwicklung'; + String get dashboard_burnDownChart_plannedTrajectory => + 'Geplante Entwicklung'; @override String dashboard_burnDownChart_idealTrajectory(num count) { @@ -229,16 +241,19 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get dashboard_burnDownChart_explanation_title => 'Was zum Teufel ist ein Burndown-Diagramm?'; + String get dashboard_burnDownChart_explanation_title => + 'Was zum Teufel ist ein Burndown-Diagramm?'; @override - String get dashboard_burnDownChart_explanation_message => 'Das **Burndown-Diagramm** hilft dir dabei, deinen Fortschritt bei der Erledigung von Aufgaben zu visualisieren.\n\n1. Die **ideale Entwicklung** (gerade Linie) zeigt, wie viele Aufgaben du an jedem Tag übrig haben solltest, wenn du deine Aufgaben ideal geplant hast, um eine gleichmäßige Arbeitsbelastung beizubehalten.\n2. Die **geplante Entwicklung** (gekrümmte Linie) zeigt, wie viele Aufgaben basierend auf deinem geplanten Abschluss übrig sein sollten.\n\t- Grün, wenn am Ende des Semesters keine Aufgaben mehr übrig sind.\n\t- Rot, wenn du nicht alle Module rechtzeitig abschließen wirst.\n\nDieses Diagramm verfolgt nicht, wann Aufgaben tatsächlich abgeschlossen werden – es vergleicht lediglich deinen Plan mit dem idealen Tempo, damit du auf Kurs bleibst!\n'; + String get dashboard_burnDownChart_explanation_message => + 'Das **Burndown-Diagramm** hilft dir dabei, deinen Fortschritt bei der Erledigung von Aufgaben zu visualisieren.\n\n1. Die **ideale Entwicklung** (gerade Linie) zeigt, wie viele Aufgaben du an jedem Tag übrig haben solltest, wenn du deine Aufgaben ideal geplant hast, um eine gleichmäßige Arbeitsbelastung beizubehalten.\n2. Die **geplante Entwicklung** (gekrümmte Linie) zeigt, wie viele Aufgaben basierend auf deinem geplanten Abschluss übrig sein sollten.\n\t- Grün, wenn am Ende des Semesters keine Aufgaben mehr übrig sind.\n\t- Rot, wenn du nicht alle Module rechtzeitig abschließen wirst.\n\nDieses Diagramm verfolgt nicht, wann Aufgaben tatsächlich abgeschlossen werden – es vergleicht lediglich deinen Plan mit dem idealen Tempo, damit du auf Kurs bleibst!\n'; @override String get dashboard_exams => 'Bevorstehende Prüfungen'; @override - String get dashboard_exams_noExams => 'In naher Zukunft sind keine Prüfungen geplant'; + String get dashboard_exams_noExams => + 'In naher Zukunft sind keine Prüfungen geplant'; @override String get dashboard_title => 'Dashboard'; @@ -247,13 +262,15 @@ class AppLocalizationsDe extends AppLocalizations { String get dashboard_overdueTasks => 'Überfällige Aufgaben'; @override - String get dashboard_noTasksOverdue => 'Alles bestens, keine Aufgaben sind überfällig!'; + String get dashboard_noTasksOverdue => + 'Alles bestens, keine Aufgaben sind überfällig!'; @override String get dashboard_slotsReservedToday => 'Heute reservierte Slots'; @override - String get dashboard_noSlotsReservedToday => 'Für heute hast du keine Slots reserviert.'; + String get dashboard_noSlotsReservedToday => + 'Für heute hast du keine Slots reserviert.'; @override String get enum_taskStatus_done => 'Erledigt'; @@ -321,7 +338,8 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get notification_planRemoved => 'Du wurdest aus deinem geteilten Plan entfernt. Keine Sorge, eine Kopie des Plans wurde in deinem Konto gespeichert.'; + String get notification_planRemoved => + 'Du wurdest aus deinem geteilten Plan entfernt. Keine Sorge, eine Kopie des Plans wurde in deinem Konto gespeichert.'; @override String notification_planLeft(String username) { @@ -351,10 +369,12 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get settings_feedback_error_title => 'Feedback konnte nicht gesendet werden'; + String get settings_feedback_error_title => + 'Feedback konnte nicht gesendet werden'; @override - String get settings_feedback_error_message => 'Beim Senden deines Feedbacks ist ein Fehler aufgetreten und der Fehler wurde den Entwicklern gemeldet. Bitte versuche es später noch einmal.'; + String get settings_feedback_error_message => + 'Beim Senden deines Feedbacks ist ein Fehler aufgetreten und der Fehler wurde den Entwicklern gemeldet. Bitte versuche es später noch einmal.'; @override String get settings_feedback_title => 'Feedback'; @@ -363,7 +383,8 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_feedback_description => 'Bitte beschreibe dein Problem.'; @override - String get settings_feedback_consent => 'Ich stimme der Weitergabe meiner E-Mail-Adresse und meines Namens an die Entwickler gemäß unserer '; + String get settings_feedback_consent => + 'Ich stimme der Weitergabe meiner E-Mail-Adresse und meines Namens an die Entwickler gemäß unserer '; @override String get settings_feedback_consentSuffix => ' zu.'; @@ -404,7 +425,8 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_general_enableEK => 'EK-Module aktivieren'; @override - String get settings_general_displayTaskCount => 'Anzahl der Aufgaben anzeigen'; + String get settings_general_displayTaskCount => + 'Anzahl der Aufgaben anzeigen'; @override String get settings_general_manageSubscription => 'Abonament verwalten'; @@ -438,19 +460,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String slots_details_sizeCount(int count) { - return 'Größe $count'; + return 'Schüler: $count'; } @override String get slots_slotmaster_newSlot => 'Neuer Slot'; @override - String slots_slotmaster_deleteSlot_title(String room, String startUnit, String endUnit) { + String slots_slotmaster_deleteSlot_title( + String room, String startUnit, String endUnit) { return 'Slot $room $startUnit - $endUnit löschen?'; } @override - String get slots_slotmaster_deleteSlot_message => 'Bist du sicher, dass du diesen Slot löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.'; + String get slots_slotmaster_deleteSlot_message => + 'Bist du sicher, dass du diesen Slot löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.'; @override String get slots_edit_editSlot => 'Slot bearbeiten'; @@ -471,7 +495,7 @@ class AppLocalizationsDe extends AppLocalizations { String get slots_edit_room => 'Raum'; @override - String get slots_edit_size => 'Größe'; + String get slots_edit_size => 'Schüler'; @override String get slots_edit_supervisors => 'Aufsichtspersonen'; @@ -482,6 +506,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get slots_edit_courseMappings => 'Kurszuordnungen'; + @override + String get slots_edit_addCourseMapping => 'Neue Zuordnung'; + @override String get slots_edit_selectCourse => 'Kurs auswählen'; @@ -492,7 +519,8 @@ class AppLocalizationsDe extends AppLocalizations { String get slots_reserve_error => 'Slot konnte nicht reserviert werden'; @override - String get slots_unbook_error => 'Ein unerwarteter Fehler ist bei der Stornierung deiner Reservierung aufgetreten. Bitte versuche es später erneut.'; + String get slots_unbook_error => + 'Ein unerwarteter Fehler ist bei der Stornierung deiner Reservierung aufgetreten. Bitte versuche es später erneut.'; @override String get slots_weekday_monday => 'Montag'; @@ -530,5 +558,56 @@ class AppLocalizationsDe extends AppLocalizations { String get global_disclaimer_title => 'Öffentliche Beta'; @override - String get global_disclaimer => 'Bitte beachte, dass diese App sich derzeit in der öffentlichen **Beta** befindet.\nDas bedeutet, dass es zu Fehlern und fehlenden Funktionen kommen kann.\nWenn du auf Probleme stößt, melde sie bitte an uns.\nAußerdem beachte, dass deine Fakultät noch **im Prozess der Migration** zu diesem neuen System ist.\nDas bedeutet, dass einige Daten **unvollständig oder fehlerhaft** sein können.\nBitte **verlasse dich noch nicht** auf diese App für kritische Informationen :)\n\nVielen Dank für dein Verständnis und deine Unterstützung! ❤️'; + String get global_disclaimer => + 'Bitte beachte, dass diese App sich derzeit in der öffentlichen **Beta** befindet.\nDas bedeutet, dass es zu Fehlern und fehlenden Funktionen kommen kann.\nWenn du auf Probleme stößt, melde sie bitte an uns.\nAußerdem beachte, dass deine Fakultät noch **im Prozess der Migration** zu diesem neuen System ist.\nDas bedeutet, dass einige Daten **unvollständig oder fehlerhaft** sein können.\nBitte **verlasse dich noch nicht** auf diese App für kritische Informationen :)\n\nVielen Dank für dein Verständnis und deine Unterstützung! ❤️'; + + @override + String get kanban_title => 'Kanban Board'; + + @override + String kanban_card_dueOn(String dueDate) { + return 'Fällig $dueDate'; + } + + @override + String kanban_card_plannedOn(String plannedDate) { + return 'Geplant $plannedDate'; + } + + @override + String get kanban_screen_hideBacklog => 'Backlog ausblenden'; + + @override + String get kanban_screen_showBacklog => 'Backlog einblenden'; + + @override + String get kanban_screen_backlog => 'Backlog'; + + @override + String get kanban_screen_toDo => 'To Do'; + + @override + String get kanban_screen_inProgress => 'In Arbeit'; + + @override + String get kanban_screen_done => 'Fertig'; + + @override + String get kanban_settings_kanban => 'Kanban'; + + @override + String get kanban_settings_disabled => 'Deaktiviert'; + + @override + String get kanban_settings_moveSubmittedTasks => + 'Abgegebene Aufgaben bewegen'; + + @override + String get kanban_settings_moveOverdueTasks => 'Überfällige Aufgaben bewegen'; + + @override + String get kanban_settings_moveCompletedTasks => 'Erledigte Aufgaben bewegen'; + + @override + String get kanban_settings_columnColors => 'Spaltenfarben'; } diff --git a/lib/gen/l10n/app_localizations_en.dart b/lib/gen/l10n/app_localizations_en.dart index ece717f4..ecf338ff 100644 --- a/lib/gen/l10n/app_localizations_en.dart +++ b/lib/gen/l10n/app_localizations_en.dart @@ -44,6 +44,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get global_edit => 'Edit'; + @override + String get global_duplicate => 'Duplicate'; + @override String get global_delete => 'Delete'; @@ -59,7 +62,8 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get app_update_aur => 'Run `yay -S lb-planner` to update to the latest version.'; + String get app_update_aur => + 'Run `yay -S lb-planner` to update to the latest version.'; @override String app_update_download(String url) { @@ -67,10 +71,12 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get app_update_web => 'Please refresh the page with `Ctrl + Shift + F5` to update to the latest version.'; + String get app_update_web => + 'Please refresh the page with `Ctrl + Shift + F5` to update to the latest version.'; @override - String get app_noMobile_message => 'This feature is not available on mobile devices.'; + String get app_noMobile_message => + 'This feature is not available on mobile devices.'; @override String get app_noMobile_goBack => 'Go back'; @@ -101,7 +107,8 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get auth_dataCollectionConsent => 'I accept the collection and processing of my data as described in the '; + String get auth_dataCollectionConsent => + 'I accept the collection and processing of my data as described in the '; @override String get auth_dataCollectionConsentSuffix => '.'; @@ -119,7 +126,8 @@ class AppLocalizationsEn extends AppLocalizations { String get calendar_tasksOverview_description_title => 'What is this?'; @override - String get calendar_tasksOverview_description => 'The tasks overview shows the distribution of tasks over the months based on the deadlines set by the teachers.'; + String get calendar_tasksOverview_description => + 'The tasks overview shows the distribution of tasks over the months based on the deadlines set by the teachers.'; @override String get calendar_title => 'Calendar'; @@ -134,7 +142,8 @@ class AppLocalizationsEn extends AppLocalizations { String get calendar_leave_title => 'Leave plan?'; @override - String get calendar_leave_message => 'Are you sure you want to leave this plan? This action cannot be undone.\nBut no worries a copy of the shared plan will be saved to your account and you can be invited back at any time.'; + String get calendar_leave_message => + 'Are you sure you want to leave this plan? This action cannot be undone.\nBut no worries a copy of the shared plan will be saved to your account and you can be invited back at any time.'; @override String get calendar_invited => 'Invited'; @@ -176,7 +185,8 @@ class AppLocalizationsEn extends AppLocalizations { String get calendar_clearPlan_title => 'Clear plan?'; @override - String get calendar_clearPlan_message => 'Are you sure you want to clear your plan? This will remove all planned tasks and cannot be undone.'; + String get calendar_clearPlan_message => + 'Are you sure you want to clear your plan? This will remove all planned tasks and cannot be undone.'; @override String get calendar_tasks => 'Tasks'; @@ -206,7 +216,8 @@ class AppLocalizationsEn extends AppLocalizations { String get dashboard_todaysTasks => 'Today\'s tasks'; @override - String get dashboard_todaysTasks_noTasks => 'You\'ve nothing planned for today'; + String get dashboard_todaysTasks_noTasks => + 'You\'ve nothing planned for today'; @override String get dashboard_statusOverview => 'Status overview'; @@ -229,10 +240,12 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get dashboard_burnDownChart_explanation_title => 'WTF is a burndown chart?'; + String get dashboard_burnDownChart_explanation_title => + 'WTF is a burndown chart?'; @override - String get dashboard_burnDownChart_explanation_message => 'The **burndown chart** helps you visualize your progress toward completing tasks.\n\n1. The **ideal trajectory** (straight line) shows how many tasks you should have left each day if you\'ve planned your tasks in an ideal way to keep a steady workload.\n2. The **planned trajectory** (curved line) shows how many tasks you\'re expected to have left based on when you\'ve planned to complete them.\n\t- Green when no tasks are remaining by the end of the semester.\n\t- Becomes red if you will not complete all modules in time.\n\nThis chart doesn’t track when tasks are actually completed—it\'s all about comparing your plan to the ideal pace so you can stay on track!\n'; + String get dashboard_burnDownChart_explanation_message => + 'The **burndown chart** helps you visualize your progress toward completing tasks.\n\n1. The **ideal trajectory** (straight line) shows how many tasks you should have left each day if you\'ve planned your tasks in an ideal way to keep a steady workload.\n2. The **planned trajectory** (curved line) shows how many tasks you\'re expected to have left based on when you\'ve planned to complete them.\n\t- Green when no tasks are remaining by the end of the semester.\n\t- Becomes red if you will not complete all modules in time.\n\nThis chart doesn’t track when tasks are actually completed—it\'s all about comparing your plan to the ideal pace so you can stay on track!\n'; @override String get dashboard_exams => 'Upcoming exams'; @@ -247,13 +260,15 @@ class AppLocalizationsEn extends AppLocalizations { String get dashboard_overdueTasks => 'Overdue tasks'; @override - String get dashboard_noTasksOverdue => 'You\'re all good, no tasks are overdue!'; + String get dashboard_noTasksOverdue => + 'You\'re all good, no tasks are overdue!'; @override String get dashboard_slotsReservedToday => 'Slots reserved for today'; @override - String get dashboard_noSlotsReservedToday => 'You don\'t have any slots reserved for today.'; + String get dashboard_noSlotsReservedToday => + 'You don\'t have any slots reserved for today.'; @override String get enum_taskStatus_done => 'Done'; @@ -321,7 +336,8 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get notification_planRemoved => 'You have been removed from your shared plan. But don\'t worry, we\'ve got you covered - a copy of the plan has been saved to your account.'; + String get notification_planRemoved => + 'You have been removed from your shared plan. But don\'t worry, we\'ve got you covered - a copy of the plan has been saved to your account.'; @override String notification_planLeft(String username) { @@ -354,7 +370,8 @@ class AppLocalizationsEn extends AppLocalizations { String get settings_feedback_error_title => 'Unable to send feedback'; @override - String get settings_feedback_error_message => 'An error occurred while sending your feedback and the error has been reported to the developers. Please try again later.'; + String get settings_feedback_error_message => + 'An error occurred while sending your feedback and the error has been reported to the developers. Please try again later.'; @override String get settings_feedback_title => 'Feedback'; @@ -363,7 +380,8 @@ class AppLocalizationsEn extends AppLocalizations { String get settings_feedback_description => 'Please describe your problem.'; @override - String get settings_feedback_consent => 'I agree to sharing my email address and name with the developers in accordance with our '; + String get settings_feedback_consent => + 'I agree to sharing my email address and name with the developers in accordance with our '; @override String get settings_feedback_consentSuffix => '.'; @@ -438,19 +456,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String slots_details_sizeCount(int count) { - return 'Size $count'; + return 'Students: $count'; } @override String get slots_slotmaster_newSlot => 'New slot'; @override - String slots_slotmaster_deleteSlot_title(String room, String startUnit, String endUnit) { + String slots_slotmaster_deleteSlot_title( + String room, String startUnit, String endUnit) { return 'Delete slot $room $startUnit - $endUnit?'; } @override - String get slots_slotmaster_deleteSlot_message => 'Are you sure you want to delete this slot? This action cannot be undone.'; + String get slots_slotmaster_deleteSlot_message => + 'Are you sure you want to delete this slot? This action cannot be undone.'; @override String get slots_edit_editSlot => 'Edit slot'; @@ -471,7 +491,7 @@ class AppLocalizationsEn extends AppLocalizations { String get slots_edit_room => 'Room'; @override - String get slots_edit_size => 'Size'; + String get slots_edit_size => 'Students'; @override String get slots_edit_supervisors => 'Supervisors'; @@ -482,6 +502,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get slots_edit_courseMappings => 'Course Mappings'; + @override + String get slots_edit_addCourseMapping => 'Add mapping'; + @override String get slots_edit_selectCourse => 'Select course'; @@ -492,7 +515,8 @@ class AppLocalizationsEn extends AppLocalizations { String get slots_reserve_error => 'Failed to reserve slot'; @override - String get slots_unbook_error => 'An unexpected error occurred while canceling your reservation. Please try again later.'; + String get slots_unbook_error => + 'An unexpected error occurred while canceling your reservation. Please try again later.'; @override String get slots_weekday_monday => 'Monday'; @@ -521,7 +545,8 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get notFound => 'Sorry, we couldn\'t find the page you were looking for.'; + String get notFound => + 'Sorry, we couldn\'t find the page you were looking for.'; @override String get notFound_returnHome => 'Return to home'; @@ -530,5 +555,55 @@ class AppLocalizationsEn extends AppLocalizations { String get global_disclaimer_title => 'Public Beta'; @override - String get global_disclaimer => 'Please note that this app is currently in public **beta**. This means that there may be bugs and missing features. If you encounter any issues, please report them to us. Also, note that your faculty is still **in the process of migrating** to this new system. This means that some data may be **incomplete or incorrect**. Please **do not rely** on this app for any critical information just yet :)\n\nThank you for your understanding and support! ❤️'; + String get global_disclaimer => + 'Please note that this app is currently in public **beta**. This means that there may be bugs and missing features. If you encounter any issues, please report them to us. Also, note that your faculty is still **in the process of migrating** to this new system. This means that some data may be **incomplete or incorrect**. Please **do not rely** on this app for any critical information just yet :)\n\nThank you for your understanding and support! ❤️'; + + @override + String get kanban_title => 'Kanban Board'; + + @override + String kanban_card_dueOn(String dueDate) { + return 'Due $dueDate'; + } + + @override + String kanban_card_plannedOn(String plannedDate) { + return 'Planned $plannedDate'; + } + + @override + String get kanban_screen_hideBacklog => 'Hide Backlog'; + + @override + String get kanban_screen_showBacklog => 'Show Backlog'; + + @override + String get kanban_screen_backlog => 'Backlog'; + + @override + String get kanban_screen_toDo => 'To Do'; + + @override + String get kanban_screen_inProgress => 'In Progress'; + + @override + String get kanban_screen_done => 'Done'; + + @override + String get kanban_settings_kanban => 'Kanban'; + + @override + String get kanban_settings_disabled => 'Disabled'; + + @override + String get kanban_settings_moveSubmittedTasks => 'Move Submitted Tasks'; + + @override + String get kanban_settings_moveOverdueTasks => 'Move Overdue Tasks'; + + @override + String get kanban_settings_moveCompletedTasks => 'Move Completed Tasks'; + + @override + String get kanban_settings_columnColors => 'Column Colors'; } diff --git a/lib/main.dart b/lib/main.dart index dd9abdd3..068d3fff 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,8 +5,6 @@ import 'dart:io'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:context_menus/context_menus.dart'; import 'package:device_preview/device_preview.dart'; -import 'package:echidna_flutter/echidna_flutter.dart'; -import 'package:eduplanner/config/echidna.dart'; import 'package:eduplanner/config/posthog.dart'; import 'package:eduplanner/config/sentry.dart'; import 'package:eduplanner/config/version.dart'; @@ -105,7 +103,7 @@ void main() async { setPathUrlStrategy(); - initializeEchidnaApi(baseUrl: kEchidnaHost, clientKey: kEchidnaClientKey, clientId: kEchidnaClientID); + // initializeEchidnaApi(baseUrl: kEchidnaHost, clientKey: kEchidnaClientKey, clientId: kEchidnaClientID); for (final locale in AppLocalizations.supportedLocales) { await initializeDateFormatting(locale.languageCode); diff --git a/lib/src/app/app.dart b/lib/src/app/app.dart index b53e7ba4..987af261 100644 --- a/lib/src/app/app.dart +++ b/lib/src/app/app.dart @@ -1,8 +1,6 @@ library lb_planner.modules.app; import 'package:animations/animations.dart'; -import 'package:echidna_flutter/echidna_flutter.dart'; -import 'package:eduplanner/config/echidna.dart'; import 'package:eduplanner/config/version.dart'; import 'package:eduplanner/eduplanner.dart'; import 'package:flutter/material.dart'; @@ -33,6 +31,7 @@ class AppModule extends Module { UpdaterModule(), MoodleModule(), CalendarModule(), + KanbanModule(), ]; @override @@ -71,13 +70,24 @@ class AppModule extends Module { AuthGuard(redirectTo: '/auth/'), ], ), + ModuleRoute( + '/kanban/', + module: KanbanModule(), + transition: TransitionType.custom, + customTransition: defaultTransition, + guards: [ + // FeatureGuard([kCalendarPlanFeatureID], redirectTo: '/settings/'), + CapabilityGuard({UserCapability.student}, redirectTo: '/slots/'), + AuthGuard(redirectTo: '/auth/'), + ], + ), ModuleRoute( '/calendar/', module: CalendarModule(), transition: TransitionType.custom, customTransition: defaultTransition, guards: [ - FeatureGuard([kCalendarPlanFeatureID], redirectTo: '/settings/'), + // FeatureGuard([kCalendarPlanFeatureID], redirectTo: '/settings/'), CapabilityGuard({UserCapability.student}, redirectTo: '/slots/'), AuthGuard(redirectTo: '/auth/'), ], diff --git a/lib/src/app/presentation/widgets/sidebar.dart b/lib/src/app/presentation/widgets/sidebar.dart index b45749a4..46f8c44a 100644 --- a/lib/src/app/presentation/widgets/sidebar.dart +++ b/lib/src/app/presentation/widgets/sidebar.dart @@ -16,7 +16,7 @@ class Sidebar extends StatelessWidget with AdaptiveWidget { return Container( width: 60, decoration: BoxDecoration( - color: context.surface, + color: context.theme.colorScheme.surface, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), @@ -36,6 +36,15 @@ class Sidebar extends StatelessWidget with AdaptiveWidget { route: '/dashboard/', icon: Icons.dashboard, ), + if (capabilities.hasStudent) + SidebarTarget( + route: '/kanban/', + icon: Icons.bar_chart_rounded, + iconTransformer: (context, icon) => Transform.flip( + flipY: true, + child: icon, + ), + ), if (capabilities.hasStudent) const SidebarTarget( route: '/calendar/plan/', @@ -80,7 +89,7 @@ class Sidebar extends StatelessWidget with AdaptiveWidget { return Container( height: 60, decoration: BoxDecoration( - color: context.surface, + color: context.theme.colorScheme.surface, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), diff --git a/lib/src/app/presentation/widgets/sidebar_target.dart b/lib/src/app/presentation/widgets/sidebar_target.dart index 581709f3..d6aac1f9 100644 --- a/lib/src/app/presentation/widgets/sidebar_target.dart +++ b/lib/src/app/presentation/widgets/sidebar_target.dart @@ -21,8 +21,22 @@ class SidebarTarget extends StatefulWidget { /// Set this value if the target should be considered active when the route is not the same as the [route]. final String? activeRoute; + /// Transforms the icon widget before displaying it. + /// + /// If omitted just returns the icon. + final Widget Function(BuildContext, Widget) iconTransformer; + + static Widget _defaultIconTransformer(BuildContext context, Widget icon) => icon; + /// A target in the [Sidebar] that navigates to a specific [route]. - const SidebarTarget({super.key, required this.route, required this.icon, this.onTap, this.activeRoute}); + const SidebarTarget({ + super.key, + required this.route, + required this.icon, + this.onTap, + this.activeRoute, + this.iconTransformer = _defaultIconTransformer, + }); @override State createState() => _SidebarTargetState(); @@ -86,10 +100,13 @@ class _SidebarTargetState extends State with AdaptiveState { ), ), ), - child: Icon( - widget.icon, - color: isActive ? context.theme.colorScheme.onPrimary : context.theme.colorScheme.onSurface, - size: 20, + child: widget.iconTransformer( + context, + Icon( + widget.icon, + color: isActive ? context.theme.colorScheme.onPrimary : context.theme.colorScheme.onSurface, + size: 20, + ), ), ); }, @@ -100,10 +117,13 @@ class _SidebarTargetState extends State with AdaptiveState { Widget buildMobile(BuildContext context) { return GestureDetector( onTap: _onTap, - child: Icon( - widget.icon, - color: isActive ? context.theme.colorScheme.primary : context.theme.colorScheme.onSurface, - size: 25, + child: widget.iconTransformer( + context, + Icon( + widget.icon, + color: isActive ? context.theme.colorScheme.primary : context.theme.colorScheme.onSurface, + size: 25, + ), ), ); } diff --git a/lib/src/app/presentation/widgets/title_bar.dart b/lib/src/app/presentation/widgets/title_bar.dart index d1320c31..bdb03d3a 100644 --- a/lib/src/app/presentation/widgets/title_bar.dart +++ b/lib/src/app/presentation/widgets/title_bar.dart @@ -1,11 +1,9 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:data_widget/data_widget.dart'; -import 'package:echidna_flutter/echidna_flutter.dart'; import 'package:eduplanner/config/version.dart'; import 'package:eduplanner/src/app/app.dart'; import 'package:eduplanner/src/moodle/moodle.dart'; import 'package:eduplanner/src/notifications/notifications.dart'; -import 'package:eduplanner/src/theming/theming.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_vector_icons/flutter_vector_icons.dart'; @@ -198,9 +196,9 @@ class TitleBarState extends State with WindowListener, RouteAware, Ada final (title, featureId) = Modular.tryGet()?.call(context) ?? (null, null); final notifications = context.watch(); - final license = context.watch(); + // final license = context.watch(); - final showLicenseBadge = featureId != null && license.state.data != null; + // final showLicenseBadge = featureId != null && license.state.data != null; return Column( children: [ @@ -233,27 +231,27 @@ class TitleBarState extends State with WindowListener, RouteAware, Ada ).fontSize(24), ), ), - if (showLicenseBadge) Spacing.smallHorizontal(), - if (showLicenseBadge) - Container( - padding: PaddingAll(Spacing.xsSpacing).Horizontal(Spacing.smallSpacing), - decoration: ShapeDecoration( - shape: squircle( - radius: 5000, - side: BorderSide( - color: context.theme.colorScheme.primary, - ), - ), - color: context.theme.colorScheme.primary.withValues(alpha: 0.1), - ), - child: Text( - license.state.requireData.active ? context.t.app_titleBar_pro : context.t.app_titleBar_trial, - style: context.textTheme.bodySmall?.copyWith( - color: context.theme.colorScheme.primary, - ), - ), - ), - if (showLicenseBadge) Spacing.smallHorizontal(), + // if (showLicenseBadge) Spacing.smallHorizontal(), + // if (showLicenseBadge) + // Container( + // padding: PaddingAll(Spacing.xsSpacing).Horizontal(Spacing.smallSpacing), + // decoration: ShapeDecoration( + // shape: squircle( + // radius: 5000, + // side: BorderSide( + // color: context.theme.colorScheme.primary, + // ), + // ), + // color: context.theme.colorScheme.primary.withValues(alpha: 0.1), + // ), + // child: Text( + // license.state.requireData.active ? context.t.app_titleBar_pro : context.t.app_titleBar_trial, + // style: context.textTheme.bodySmall?.copyWith( + // color: context.theme.colorScheme.primary, + // ), + // ), + // ), + // if (showLicenseBadge) Spacing.smallHorizontal(), if (_trailing != null) _trailing!, ], ), @@ -351,9 +349,9 @@ class TitleBarState extends State with WindowListener, RouteAware, Ada final (title, featureId) = Modular.tryGet()?.call(context) ?? (null, null); final notifications = context.watch(); - final license = context.watch(); + // final license = context.watch(); - final showLicenseBadge = featureId != null && license.state.data != null; + // final showLicenseBadge = featureId != null && license.state.data != null; return Column( children: [ @@ -386,28 +384,28 @@ class TitleBarState extends State with WindowListener, RouteAware, Ada ).fontSize(24), ), ), - if (showLicenseBadge) Spacing.smallHorizontal(), - if (showLicenseBadge) - Container( - padding: PaddingAll(Spacing.xsSpacing).Horizontal(Spacing.smallSpacing), - decoration: ShapeDecoration( - shape: squircle( - radius: 5000, - side: BorderSide( - color: context.theme.colorScheme.primary, - ), - ), - color: context.theme.colorScheme.primary.withValues(alpha: 0.1), - ), - child: Text( - license.state.requireData.active ? context.t.app_titleBar_pro : context.t.app_titleBar_trial, - style: context.textTheme.bodySmall?.copyWith( - color: context.theme.colorScheme.primary, - fontSize: 10, - ), - ), - ), - if (showLicenseBadge) Spacing.smallHorizontal(), + // if (showLicenseBadge) Spacing.smallHorizontal(), + // if (showLicenseBadge) + // Container( + // padding: PaddingAll(Spacing.xsSpacing).Horizontal(Spacing.smallSpacing), + // decoration: ShapeDecoration( + // shape: squircle( + // radius: 5000, + // side: BorderSide( + // color: context.theme.colorScheme.primary, + // ), + // ), + // color: context.theme.colorScheme.primary.withValues(alpha: 0.1), + // ), + // child: Text( + // license.state.requireData.active ? context.t.app_titleBar_pro : context.t.app_titleBar_trial, + // style: context.textTheme.bodySmall?.copyWith( + // color: context.theme.colorScheme.primary, + // fontSize: 10, + // ), + // ), + // ), + // if (showLicenseBadge) Spacing.smallHorizontal(), if (_trailing != null) _trailing!, ], ), diff --git a/lib/src/app/utils/animate_utils.dart b/lib/src/app/utils/animate_utils.dart index 18de74ce..e27b1b3e 100644 --- a/lib/src/app/utils/animate_utils.dart +++ b/lib/src/app/utils/animate_utils.dart @@ -22,8 +22,9 @@ extension AnimateUtils on List { double begin = 2, double end = 0, int limit = 16, + String? keyPrefix, }) { - assert(limit >= 0, 'Limit must be positive'); + assert(limit >= 0, 'Limit must be non-negative'); stagger ??= AnimationStagger(); @@ -54,7 +55,9 @@ extension AnimateUtils on List { widgets.add( this[i] - .animate() + .animate( + key: keyPrefix != null ? ValueKey('$keyPrefix-$i') : null, + ) .slideY( begin: begin, end: end, @@ -83,8 +86,9 @@ extension AnimateX on Widget { AnimationStagger? stagger, { Duration duration = const Duration(milliseconds: 500), Duration delay = Duration.zero, + Key? key, }) { - return animate().scale(duration: duration, delay: stagger?.add() ?? delay, curve: Curves.easeOutCubic); + return animate(key: key).scale(duration: duration, delay: stagger?.add() ?? delay, curve: Curves.easeOutCubic); } /// Wraps this widget in [Static] to disable animations. diff --git a/lib/src/auth/auth.dart b/lib/src/auth/auth.dart index 885e0c26..fcee339e 100644 --- a/lib/src/auth/auth.dart +++ b/lib/src/auth/auth.dart @@ -1,4 +1,3 @@ -import 'package:echidna_flutter/echidna_flutter.dart'; import 'package:eduplanner/eduplanner.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:mcquenji_core/mcquenji_core.dart'; @@ -16,7 +15,7 @@ class AuthModule extends Module { List get imports => [ CoreModule(), LocalStorageModule(), - EchidnaModule(), + // EchidnaModule(), ]; @override @@ -27,8 +26,8 @@ class AuthModule extends Module { ..addRepository(AuthRepository.new) ..addRepository(UserRepository.new) ..addSerde(fromJson: Token.fromJson, toJson: (t) => t.toJson()) - ..addSerde(fromJson: User.fromJson, toJson: (u) => u.toJson()) - ..initializeLicenseRepo(EchidnaUserRepository.new); + ..addSerde(fromJson: User.fromJson, toJson: (u) => u.toJson()); + // ..initializeLicenseRepo(EchidnaUserRepository.new); @override void routes(RouteManager r) { diff --git a/lib/src/auth/domain/models/token.freezed.dart b/lib/src/auth/domain/models/token.freezed.dart index 99a355b3..36b57b1f 100644 --- a/lib/src/auth/domain/models/token.freezed.dart +++ b/lib/src/auth/domain/models/token.freezed.dart @@ -26,12 +26,8 @@ mixin _$Token { /// The [Webservice] the token is valid for Webservice get webservice => throw _privateConstructorUsedError; - /// Serializes this Token to a JSON map. Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of Token - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) $TokenCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -53,8 +49,6 @@ class _$TokenCopyWithImpl<$Res, $Val extends Token> // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of Token - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -92,8 +86,6 @@ class __$$TokenImplCopyWithImpl<$Res> _$TokenImpl _value, $Res Function(_$TokenImpl) _then) : super(_value, _then); - /// Create a copy of Token - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -145,13 +137,11 @@ class _$TokenImpl extends _Token { other.webservice == webservice)); } - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override int get hashCode => Object.hash(runtimeType, token, webservice); - /// Create a copy of Token - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$TokenImplCopyWith<_$TokenImpl> get copyWith => @@ -173,18 +163,16 @@ abstract class _Token extends Token { factory _Token.fromJson(Map json) = _$TokenImpl.fromJson; - /// The access token @override + + /// The access token String get token; + @override /// The [Webservice] the token is valid for - @override Webservice get webservice; - - /// Create a copy of Token - /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) _$$TokenImplCopyWith<_$TokenImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/auth/domain/models/user.dart b/lib/src/auth/domain/models/user.dart index 12c153b1..3811f066 100644 --- a/lib/src/auth/domain/models/user.dart +++ b/lib/src/auth/domain/models/user.dart @@ -48,6 +48,18 @@ class User with _$User { /// Whether to display the task count in the calendar view @Default(false) @JsonKey(name: 'displaytaskcount') @BoolConverterNullable() bool displayTaskCount, + /// Whether to show column colors in the kanban view. + @Default(true) @JsonKey(name: 'showcolumncolors') @BoolConverterNullable() bool showColumnColors, + + /// The column to auto-move completed tasks to + @Default(null) @JsonKey(name: 'automovecompletedtasks') @KanbanColumnConverter() KanbanColumn? autoMoveCompletedTasksTo, + + /// The column to auto-move submitted tasks to + @Default(null) @JsonKey(name: 'automovesubmittedtasks') @KanbanColumnConverter() KanbanColumn? autoMoveSubmittedTasksTo, + + /// The column to auto-move overdue tasks to + @Default(null) @JsonKey(name: 'automoveoverduetasks') @KanbanColumnConverter() KanbanColumn? autoMoveOverdueTasksTo, + /// The vintage of the user Vintage? vintage, }) = _User; diff --git a/lib/src/auth/domain/models/user.freezed.dart b/lib/src/auth/domain/models/user.freezed.dart index 2cb3d8bf..0b8a497a 100644 --- a/lib/src/auth/domain/models/user.freezed.dart +++ b/lib/src/auth/domain/models/user.freezed.dart @@ -66,15 +66,34 @@ mixin _$User { @BoolConverterNullable() bool get displayTaskCount => throw _privateConstructorUsedError; + /// Whether to show column colors in the kanban view. + @JsonKey(name: 'showcolumncolors') + @BoolConverterNullable() + bool get showColumnColors => throw _privateConstructorUsedError; + + /// The column to auto-move completed tasks to + @JsonKey(name: 'automovecompletedtasks') + @KanbanColumnConverter() + KanbanColumn? get autoMoveCompletedTasksTo => + throw _privateConstructorUsedError; + + /// The column to auto-move submitted tasks to + @JsonKey(name: 'automovesubmittedtasks') + @KanbanColumnConverter() + KanbanColumn? get autoMoveSubmittedTasksTo => + throw _privateConstructorUsedError; + + /// The column to auto-move overdue tasks to + @JsonKey(name: 'automoveoverduetasks') + @KanbanColumnConverter() + KanbanColumn? get autoMoveOverdueTasksTo => + throw _privateConstructorUsedError; + /// The vintage of the user Vintage? get vintage => throw _privateConstructorUsedError; - /// Serializes this User to a JSON map. Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of User - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) $UserCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -100,6 +119,18 @@ abstract class $UserCopyWith<$Res> { @JsonKey(name: 'displaytaskcount') @BoolConverterNullable() bool displayTaskCount, + @JsonKey(name: 'showcolumncolors') + @BoolConverterNullable() + bool showColumnColors, + @JsonKey(name: 'automovecompletedtasks') + @KanbanColumnConverter() + KanbanColumn? autoMoveCompletedTasksTo, + @JsonKey(name: 'automovesubmittedtasks') + @KanbanColumnConverter() + KanbanColumn? autoMoveSubmittedTasksTo, + @JsonKey(name: 'automoveoverduetasks') + @KanbanColumnConverter() + KanbanColumn? autoMoveOverdueTasksTo, Vintage? vintage}); } @@ -113,8 +144,6 @@ class _$UserCopyWithImpl<$Res, $Val extends User> // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of User - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -130,6 +159,10 @@ class _$UserCopyWithImpl<$Res, $Val extends User> Object? planId = null, Object? colorBlindnessString = null, Object? displayTaskCount = null, + Object? showColumnColors = null, + Object? autoMoveCompletedTasksTo = freezed, + Object? autoMoveSubmittedTasksTo = freezed, + Object? autoMoveOverdueTasksTo = freezed, Object? vintage = freezed, }) { return _then(_value.copyWith( @@ -181,6 +214,22 @@ class _$UserCopyWithImpl<$Res, $Val extends User> ? _value.displayTaskCount : displayTaskCount // ignore: cast_nullable_to_non_nullable as bool, + showColumnColors: null == showColumnColors + ? _value.showColumnColors + : showColumnColors // ignore: cast_nullable_to_non_nullable + as bool, + autoMoveCompletedTasksTo: freezed == autoMoveCompletedTasksTo + ? _value.autoMoveCompletedTasksTo + : autoMoveCompletedTasksTo // ignore: cast_nullable_to_non_nullable + as KanbanColumn?, + autoMoveSubmittedTasksTo: freezed == autoMoveSubmittedTasksTo + ? _value.autoMoveSubmittedTasksTo + : autoMoveSubmittedTasksTo // ignore: cast_nullable_to_non_nullable + as KanbanColumn?, + autoMoveOverdueTasksTo: freezed == autoMoveOverdueTasksTo + ? _value.autoMoveOverdueTasksTo + : autoMoveOverdueTasksTo // ignore: cast_nullable_to_non_nullable + as KanbanColumn?, vintage: freezed == vintage ? _value.vintage : vintage // ignore: cast_nullable_to_non_nullable @@ -213,6 +262,18 @@ abstract class _$$UserImplCopyWith<$Res> implements $UserCopyWith<$Res> { @JsonKey(name: 'displaytaskcount') @BoolConverterNullable() bool displayTaskCount, + @JsonKey(name: 'showcolumncolors') + @BoolConverterNullable() + bool showColumnColors, + @JsonKey(name: 'automovecompletedtasks') + @KanbanColumnConverter() + KanbanColumn? autoMoveCompletedTasksTo, + @JsonKey(name: 'automovesubmittedtasks') + @KanbanColumnConverter() + KanbanColumn? autoMoveSubmittedTasksTo, + @JsonKey(name: 'automoveoverduetasks') + @KanbanColumnConverter() + KanbanColumn? autoMoveOverdueTasksTo, Vintage? vintage}); } @@ -223,8 +284,6 @@ class __$$UserImplCopyWithImpl<$Res> __$$UserImplCopyWithImpl(_$UserImpl _value, $Res Function(_$UserImpl) _then) : super(_value, _then); - /// Create a copy of User - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -240,6 +299,10 @@ class __$$UserImplCopyWithImpl<$Res> Object? planId = null, Object? colorBlindnessString = null, Object? displayTaskCount = null, + Object? showColumnColors = null, + Object? autoMoveCompletedTasksTo = freezed, + Object? autoMoveSubmittedTasksTo = freezed, + Object? autoMoveOverdueTasksTo = freezed, Object? vintage = freezed, }) { return _then(_$UserImpl( @@ -291,6 +354,22 @@ class __$$UserImplCopyWithImpl<$Res> ? _value.displayTaskCount : displayTaskCount // ignore: cast_nullable_to_non_nullable as bool, + showColumnColors: null == showColumnColors + ? _value.showColumnColors + : showColumnColors // ignore: cast_nullable_to_non_nullable + as bool, + autoMoveCompletedTasksTo: freezed == autoMoveCompletedTasksTo + ? _value.autoMoveCompletedTasksTo + : autoMoveCompletedTasksTo // ignore: cast_nullable_to_non_nullable + as KanbanColumn?, + autoMoveSubmittedTasksTo: freezed == autoMoveSubmittedTasksTo + ? _value.autoMoveSubmittedTasksTo + : autoMoveSubmittedTasksTo // ignore: cast_nullable_to_non_nullable + as KanbanColumn?, + autoMoveOverdueTasksTo: freezed == autoMoveOverdueTasksTo + ? _value.autoMoveOverdueTasksTo + : autoMoveOverdueTasksTo // ignore: cast_nullable_to_non_nullable + as KanbanColumn?, vintage: freezed == vintage ? _value.vintage : vintage // ignore: cast_nullable_to_non_nullable @@ -319,6 +398,18 @@ class _$UserImpl extends _User { @JsonKey(name: 'displaytaskcount') @BoolConverterNullable() this.displayTaskCount = false, + @JsonKey(name: 'showcolumncolors') + @BoolConverterNullable() + this.showColumnColors = true, + @JsonKey(name: 'automovecompletedtasks') + @KanbanColumnConverter() + this.autoMoveCompletedTasksTo = null, + @JsonKey(name: 'automovesubmittedtasks') + @KanbanColumnConverter() + this.autoMoveSubmittedTasksTo = null, + @JsonKey(name: 'automoveoverduetasks') + @KanbanColumnConverter() + this.autoMoveOverdueTasksTo = null, this.vintage}) : super._(); @@ -384,13 +475,37 @@ class _$UserImpl extends _User { @BoolConverterNullable() final bool displayTaskCount; + /// Whether to show column colors in the kanban view. + @override + @JsonKey(name: 'showcolumncolors') + @BoolConverterNullable() + final bool showColumnColors; + + /// The column to auto-move completed tasks to + @override + @JsonKey(name: 'automovecompletedtasks') + @KanbanColumnConverter() + final KanbanColumn? autoMoveCompletedTasksTo; + + /// The column to auto-move submitted tasks to + @override + @JsonKey(name: 'automovesubmittedtasks') + @KanbanColumnConverter() + final KanbanColumn? autoMoveSubmittedTasksTo; + + /// The column to auto-move overdue tasks to + @override + @JsonKey(name: 'automoveoverduetasks') + @KanbanColumnConverter() + final KanbanColumn? autoMoveOverdueTasksTo; + /// The vintage of the user @override final Vintage? vintage; @override String toString() { - return 'User(id: $id, username: $username, firstname: $firstname, lastname: $lastname, optionalTasksEnabled: $optionalTasksEnabled, email: $email, capabilitiesBitMask: $capabilitiesBitMask, themeName: $themeName, profileImageUrl: $profileImageUrl, planId: $planId, colorBlindnessString: $colorBlindnessString, displayTaskCount: $displayTaskCount, vintage: $vintage)'; + return 'User(id: $id, username: $username, firstname: $firstname, lastname: $lastname, optionalTasksEnabled: $optionalTasksEnabled, email: $email, capabilitiesBitMask: $capabilitiesBitMask, themeName: $themeName, profileImageUrl: $profileImageUrl, planId: $planId, colorBlindnessString: $colorBlindnessString, displayTaskCount: $displayTaskCount, showColumnColors: $showColumnColors, autoMoveCompletedTasksTo: $autoMoveCompletedTasksTo, autoMoveSubmittedTasksTo: $autoMoveSubmittedTasksTo, autoMoveOverdueTasksTo: $autoMoveOverdueTasksTo, vintage: $vintage)'; } @override @@ -419,10 +534,20 @@ class _$UserImpl extends _User { other.colorBlindnessString == colorBlindnessString) && (identical(other.displayTaskCount, displayTaskCount) || other.displayTaskCount == displayTaskCount) && + (identical(other.showColumnColors, showColumnColors) || + other.showColumnColors == showColumnColors) && + (identical( + other.autoMoveCompletedTasksTo, autoMoveCompletedTasksTo) || + other.autoMoveCompletedTasksTo == autoMoveCompletedTasksTo) && + (identical( + other.autoMoveSubmittedTasksTo, autoMoveSubmittedTasksTo) || + other.autoMoveSubmittedTasksTo == autoMoveSubmittedTasksTo) && + (identical(other.autoMoveOverdueTasksTo, autoMoveOverdueTasksTo) || + other.autoMoveOverdueTasksTo == autoMoveOverdueTasksTo) && (identical(other.vintage, vintage) || other.vintage == vintage)); } - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override int get hashCode => Object.hash( runtimeType, @@ -438,11 +563,13 @@ class _$UserImpl extends _User { planId, colorBlindnessString, displayTaskCount, + showColumnColors, + autoMoveCompletedTasksTo, + autoMoveSubmittedTasksTo, + autoMoveOverdueTasksTo, vintage); - /// Create a copy of User - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$UserImplCopyWith<_$UserImpl> get copyWith => @@ -474,77 +601,111 @@ abstract class _User extends User { @JsonKey(name: 'displaytaskcount') @BoolConverterNullable() final bool displayTaskCount, + @JsonKey(name: 'showcolumncolors') + @BoolConverterNullable() + final bool showColumnColors, + @JsonKey(name: 'automovecompletedtasks') + @KanbanColumnConverter() + final KanbanColumn? autoMoveCompletedTasksTo, + @JsonKey(name: 'automovesubmittedtasks') + @KanbanColumnConverter() + final KanbanColumn? autoMoveSubmittedTasksTo, + @JsonKey(name: 'automoveoverduetasks') + @KanbanColumnConverter() + final KanbanColumn? autoMoveOverdueTasksTo, final Vintage? vintage}) = _$UserImpl; _User._() : super._(); factory _User.fromJson(Map json) = _$UserImpl.fromJson; - /// The id of the user @override + + /// The id of the user @JsonKey(name: 'userid') int get id; + @override /// The username of the user - @override String get username; + @override /// The firstname of the user - @override String get firstname; + @override /// The lastname of the user - @override String get lastname; + @override /// `true` if [MoodleTask]s of type [MoodleTaskType.optional] are enabled. - @override @JsonKey(name: 'ekenabled') @BoolConverterNullable() bool get optionalTasksEnabled; + @override /// The email address of the user - @override String get email; + @override /// A bitmask of the capabilities the user has - @override @JsonKey(name: 'capabilities') int get capabilitiesBitMask; + @override /// The name of the theme the user has selected - @override @JsonKey(name: 'theme') String get themeName; + @override /// The url of the profile image - @override @JsonKey(name: 'profileimageurl') String get profileImageUrl; + @override /// The id of the plan the user is assigned to - @override @JsonKey(name: 'planid') int get planId; + @override /// The color blindness of the user as a string - @override @JsonKey(name: 'colorblindness') String get colorBlindnessString; + @override /// Whether to display the task count in the calendar view - @override @JsonKey(name: 'displaytaskcount') @BoolConverterNullable() bool get displayTaskCount; + @override - /// The vintage of the user + /// Whether to show column colors in the kanban view. + @JsonKey(name: 'showcolumncolors') + @BoolConverterNullable() + bool get showColumnColors; @override - Vintage? get vintage; - /// Create a copy of User - /// with the given fields replaced by the non-null parameter values. + /// The column to auto-move completed tasks to + @JsonKey(name: 'automovecompletedtasks') + @KanbanColumnConverter() + KanbanColumn? get autoMoveCompletedTasksTo; + @override + + /// The column to auto-move submitted tasks to + @JsonKey(name: 'automovesubmittedtasks') + @KanbanColumnConverter() + KanbanColumn? get autoMoveSubmittedTasksTo; + @override + + /// The column to auto-move overdue tasks to + @JsonKey(name: 'automoveoverduetasks') + @KanbanColumnConverter() + KanbanColumn? get autoMoveOverdueTasksTo; + @override + + /// The vintage of the user + Vintage? get vintage; @override - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) _$$UserImplCopyWith<_$UserImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/auth/domain/models/user.g.dart b/lib/src/auth/domain/models/user.g.dart index 229c2c21..e9952dc6 100644 --- a/lib/src/auth/domain/models/user.g.dart +++ b/lib/src/auth/domain/models/user.g.dart @@ -19,6 +19,19 @@ _$UserImpl _$$UserImplFromJson(Map json) => _$UserImpl( planId: (json['planid'] as num?)?.toInt() ?? -1, colorBlindnessString: json['colorblindness'] as String? ?? '', displayTaskCount: json['displaytaskcount'] as bool? ?? false, + showColumnColors: json['showcolumncolors'] as bool? ?? true, + autoMoveCompletedTasksTo: _$JsonConverterFromJson( + json['automovecompletedtasks'], + const KanbanColumnConverter().fromJson) ?? + null, + autoMoveSubmittedTasksTo: _$JsonConverterFromJson( + json['automovesubmittedtasks'], + const KanbanColumnConverter().fromJson) ?? + null, + autoMoveOverdueTasksTo: _$JsonConverterFromJson( + json['automoveoverduetasks'], + const KanbanColumnConverter().fromJson) ?? + null, vintage: $enumDecodeNullable(_$VintageEnumMap, json['vintage']), ); @@ -36,9 +49,22 @@ Map _$$UserImplToJson(_$UserImpl instance) => 'planid': instance.planId, 'colorblindness': instance.colorBlindnessString, 'displaytaskcount': instance.displayTaskCount, + 'showcolumncolors': instance.showColumnColors, + 'automovecompletedtasks': const KanbanColumnConverter() + .toJson(instance.autoMoveCompletedTasksTo), + 'automovesubmittedtasks': const KanbanColumnConverter() + .toJson(instance.autoMoveSubmittedTasksTo), + 'automoveoverduetasks': + const KanbanColumnConverter().toJson(instance.autoMoveOverdueTasksTo), 'vintage': _$VintageEnumMap[instance.vintage], }; +Value? _$JsonConverterFromJson( + Object? json, + Value? Function(Json json) fromJson, +) => + json == null ? null : fromJson(json as Json); + const _$VintageEnumMap = { Vintage.$1: 1, Vintage.$2: 2, diff --git a/lib/src/auth/infra/datasources/moodle_user_datasource.dart b/lib/src/auth/infra/datasources/moodle_user_datasource.dart index 6e5f7c31..fe70b8d4 100644 --- a/lib/src/auth/infra/datasources/moodle_user_datasource.dart +++ b/lib/src/auth/infra/datasources/moodle_user_datasource.dart @@ -89,11 +89,15 @@ class MoodleUserDatasource extends UserDatasource { token: token, body: Map.fromEntries( user.toJson().entries.where( - (e) => [ + (e) => const [ 'theme', 'colorblindness', 'displaytaskcount', 'ekenabled', + 'showcolumncolors', + 'automoveoverduetasks', + 'automovesubmittedtasks', + 'automovecompletedtasks', ].contains(e.key), ), ), diff --git a/lib/src/auth/presentation/repositories/echidna_user_repository.dart b/lib/src/auth/presentation/repositories/echidna_user_repository.dart index e82c9005..d95c75b4 100644 --- a/lib/src/auth/presentation/repositories/echidna_user_repository.dart +++ b/lib/src/auth/presentation/repositories/echidna_user_repository.dart @@ -1,29 +1,31 @@ -import 'dart:async'; +// COMMENTED OUT FOR FUTURE USE ONCE LICENSING IS REESTABLISHED -import 'package:crypto/crypto.dart'; -import 'package:echidna_flutter/echidna_flutter.dart'; -import 'package:eduplanner/src/auth/auth.dart'; -import 'package:mcquenji_core/mcquenji_core.dart'; +// import 'dart:async'; -/// User ID repository for the echidna package. -class EchidnaUserRepository extends UserIdRepository { - final UserRepository _user; +// import 'package:crypto/crypto.dart'; +// import 'package:echidna_flutter/echidna_flutter.dart'; +// import 'package:eduplanner/src/auth/auth.dart'; +// import 'package:mcquenji_core/mcquenji_core.dart'; - /// User ID repository for the echidna package. - EchidnaUserRepository(this._user) { - watchAsync(_user); - } +// /// User ID repository for the echidna package. +// class EchidnaUserRepository extends UserIdRepository { +// final UserRepository _user; - @override - FutureOr build(Trigger trigger) async { - if (trigger is! UserRepository) return; +// /// User ID repository for the echidna package. +// EchidnaUserRepository(this._user) { +// watchAsync(_user); +// } - final user = waitForData(_user); +// @override +// FutureOr build(Trigger trigger) async { +// if (trigger is! UserRepository) return; - data( - UserID( - userId: sha256.convert(user.id.toString().codeUnits).toString(), - ), - ); - } -} +// final user = waitForData(_user); + +// data( +// UserID( +// userId: sha256.convert(user.id.toString().codeUnits).toString(), +// ), +// ); +// } +// } diff --git a/lib/src/auth/presentation/repositories/user_repository.dart b/lib/src/auth/presentation/repositories/user_repository.dart index 04ffc3ec..ed8f40e5 100644 --- a/lib/src/auth/presentation/repositories/user_repository.dart +++ b/lib/src/auth/presentation/repositories/user_repository.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'package:crypto/crypto.dart'; -import 'package:eduplanner/src/app/app.dart'; -import 'package:eduplanner/src/auth/auth.dart'; +import 'package:eduplanner/eduplanner.dart'; import 'package:flutter/foundation.dart'; import 'package:mcquenji_core/mcquenji_core.dart'; import 'package:posthog_dart/posthog_dart.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; /// UI state controller for the current user. class UserRepository extends Repository> with Tracable { @@ -69,6 +69,10 @@ class UserRepository extends Repository> with Tracable { 'theme': user.themeName, 'optional_tasks_enabled': user.optionalTasksEnabled, 'display_task_count': user.displayTaskCount, + 'show_column_colors': user.showColumnColors, + 'auto_move_completed_tasks': user.autoMoveCompletedTasksTo?.name, + 'auto_move_submitted_tasks': user.autoMoveSubmittedTasksTo?.name, + 'auto_move_overdue_tasks': user.autoMoveOverdueTasksTo?.name, }, ); @@ -84,7 +88,8 @@ class UserRepository extends Repository> with Tracable { } } - Future _updateUser(User patch) async { + /// Optimistically updates the user with [patch]. + Future _updateUser(User patch, ISentrySpan span) async { log('Updating user to $patch'); if (!state.hasData) { @@ -92,7 +97,9 @@ class UserRepository extends Repository> with Tracable { return; } - final transaction = startTransaction('updateUser'); + data(patch); + + final transaction = span.startChild('updateUser'); await guard( () => _userDatasource.updateUser( _auth.state.requireData[Webservice.lb_planner_api], @@ -121,6 +128,7 @@ class UserRepository extends Repository> with Tracable { return _updateUser( state.requireData.copyWith(themeName: theme), + transaction, ); } catch (e) { transaction.internalError(e); @@ -182,14 +190,7 @@ class UserRepository extends Repository> with Tracable { optionalTasksEnabled: enabled, ); - data( - patch, - ); - - await _userDatasource.updateUser( - _auth.state.requireData[Webservice.lb_planner_api], - patch, - ); + await _updateUser(patch, transaction); await captureEvent('optional_tasks_enabled', properties: {'enabled': enabled}); @@ -203,17 +204,16 @@ class UserRepository extends Repository> with Tracable { } /// Sets [User.displayTaskCount] to [value] for the current user. - // Using positional parameters here for ease of use in the UI. - // ignore: avoid_positional_boolean_parameters + // ignore: avoid_positional_boolean_parameters Using positional parameters here for ease of use in the UI. Future setDisplayTaskCount(bool? value) async { if (!state.hasData) { - log('Cannot set optional tasks enabled: No user loaded.'); + log('Cannot set display task count: No user loaded.'); return; } if (value == null) { - log('Cannot set optional tasks enabled: No value provided.'); + log('Cannot set display task count: No value provided.'); return; } @@ -225,18 +225,14 @@ class UserRepository extends Repository> with Tracable { displayTaskCount: value, ); - data( + await _updateUser( patch, + transaction, ); - await _userDatasource.updateUser( - _auth.state.requireData[Webservice.lb_planner_api], - patch, - ); - - await captureEvent('optional_tasks_enabled', properties: {'enabled': value}); + await captureEvent('display_task_count', properties: {'enabled': value}); } catch (e, st) { - log('Failed to set optional tasks enabled.', e, st); + log('Failed to set display task count.', e, st); transaction.internalError(e); } finally { await transaction.commit(); @@ -245,4 +241,116 @@ class UserRepository extends Repository> with Tracable { /// Agrees to the collection of analytics data. void agreeToAnalytics({bool agree = true}) => agree ? PostHog().enable() : PostHog().disable(); + + /// Sets [User.showColumnColors] to [value] for the current user. + /// + /// If [value] is null, it defaults to true. + // ignore: avoid_positional_boolean_parameters Using positional parameters here for ease of use in the UI. + Future setShowColumnColors(bool? value) async { + if (!state.hasData) { + log('Cannot set show column colors: No user loaded.'); + return; + } + final patch = state.requireData.copyWith( + showColumnColors: value ?? true, + ); + + log('Setting show column colors to ${value ?? true}'); + + final transaction = startTransaction('setShowColumnColors'); + + try { + await _updateUser( + patch, + transaction, + ); + + await captureEvent('kanban_column_colors', properties: {'enabled': value ?? true}); + log('Set show column colors to ${value ?? true}'); + } catch (e, s) { + log('Failed to set show column colors.', e, s); + transaction.internalError(e); + } finally { + await transaction.commit(); + } + } + + /// Sets [User.autoMoveCompletedTasksTo] to [column] for the current user. + Future setAutoMoveCompletedTasksTo(KanbanColumn? column) async { + final patch = state.requireData.copyWith( + autoMoveCompletedTasksTo: column, + ); + + final transaction = startTransaction('setAutoMoveCompletedTasksTo'); + + log('Setting auto move completed tasks to ${column?.name}'); + + try { + await _updateUser( + patch, + transaction, + ); + + await captureEvent('auto_move_completed_tasks', properties: {'column': '${column?.name}'}); + + log('Set auto move completed tasks to ${column?.name}'); + } catch (e, s) { + log('Failed to set auto move completed tasks.', e, s); + transaction.internalError(e); + } finally { + await transaction.commit(); + } + } + + /// Sets [User.autoMoveSubmittedTasksTo] to [column] for the current user. + Future setAutoMoveSubmittedTasksTo(KanbanColumn? column) async { + final patch = state.requireData.copyWith( + autoMoveSubmittedTasksTo: column, + ); + + log('Setting auto move submitted tasks to ${column?.name}'); + + final transaction = startTransaction('setAutoMoveSubmittedTasksTo'); + + try { + await _updateUser( + patch, + transaction, + ); + + await captureEvent('auto_move_submitted_tasks', properties: {'column': '${column?.name}'}); + } catch (e, s) { + log('Failed to set auto move submitted tasks.', e, s); + transaction.internalError(e); + } finally { + await transaction.commit(); + } + } + + /// Sets [User.autoMoveOverdueTasksTo] to [column] for the current user. + Future setAutoMoveOverdueTasksTo(KanbanColumn? column) async { + final patch = state.requireData.copyWith( + autoMoveOverdueTasksTo: column, + ); + + final transaction = startTransaction('setAutoMoveOverdueTasksTo'); + + log('Setting auto move overdue tasks to ${column?.name}'); + + try { + await _updateUser( + patch, + transaction, + ); + + await captureEvent('auto_move_overdue_tasks', properties: {'column': '${column?.name}'}); + + log('Set auto move overdue tasks to ${column?.name}'); + } catch (e, s) { + log('Failed to set auto move overdue tasks.', e, s); + transaction.internalError(e); + } finally { + await transaction.commit(); + } + } } diff --git a/lib/src/auth/utils/kanban_column_converter_utils.dart b/lib/src/auth/utils/kanban_column_converter_utils.dart new file mode 100644 index 00000000..8b6e7799 --- /dev/null +++ b/lib/src/auth/utils/kanban_column_converter_utils.dart @@ -0,0 +1,22 @@ +import 'package:eduplanner/eduplanner.dart'; +import 'package:mcquenji_core/mcquenji_core.dart'; + +/// Converts a nullable [KanbanColumn] to a string and vice versa. +/// +/// If the value is null it returns an empty string. +class KanbanColumnConverter extends IGenericSerializer { + /// Converts a nullable [KanbanColumn] to a string and vice versa. + /// + /// If the value is null it returns an empty string. + const KanbanColumnConverter(); + + @override + String serialize(KanbanColumn? data) { + return data?.name ?? ''; + } + + @override + KanbanColumn? deserialize(String data) { + return KanbanColumn.values.asNameMap()[data]; + } +} diff --git a/lib/src/auth/utils/utils.dart b/lib/src/auth/utils/utils.dart index 821e9cb4..37eacce1 100644 --- a/lib/src/auth/utils/utils.dart +++ b/lib/src/auth/utils/utils.dart @@ -1 +1,2 @@ +export 'kanban_column_converter_utils.dart'; export 'token_utils.dart'; diff --git a/lib/src/calendar/domain/models/calendar_plan.freezed.dart b/lib/src/calendar/domain/models/calendar_plan.freezed.dart index ea216da5..5ebae097 100644 --- a/lib/src/calendar/domain/models/calendar_plan.freezed.dart +++ b/lib/src/calendar/domain/models/calendar_plan.freezed.dart @@ -33,12 +33,8 @@ mixin _$CalendarPlan { /// A list of all [User]s participating in this plan and their respective access type. List get members => throw _privateConstructorUsedError; - /// Serializes this CalendarPlan to a JSON map. Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of CalendarPlan - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) $CalendarPlanCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -66,8 +62,6 @@ class _$CalendarPlanCopyWithImpl<$Res, $Val extends CalendarPlan> // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of CalendarPlan - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -120,8 +114,6 @@ class __$$CalendarPlanImplCopyWithImpl<$Res> _$CalendarPlanImpl _value, $Res Function(_$CalendarPlanImpl) _then) : super(_value, _then); - /// Create a copy of CalendarPlan - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -214,7 +206,7 @@ class _$CalendarPlanImpl extends _CalendarPlan { const DeepCollectionEquality().equals(other._members, _members)); } - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override int get hashCode => Object.hash( runtimeType, @@ -223,9 +215,7 @@ class _$CalendarPlanImpl extends _CalendarPlan { const DeepCollectionEquality().hash(_deadlines), const DeepCollectionEquality().hash(_members)); - /// Create a copy of CalendarPlan - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$CalendarPlanImplCopyWith<_$CalendarPlanImpl> get copyWith => @@ -250,27 +240,25 @@ abstract class _CalendarPlan extends CalendarPlan { factory _CalendarPlan.fromJson(Map json) = _$CalendarPlanImpl.fromJson; - /// The name of this plan. @override + + /// The name of this plan. String get name; + @override /// The ID of this plan. - @override @JsonKey(name: 'planid') int get id; + @override /// A list of deadlines planned by it's [members]. - @override List get deadlines; + @override /// A list of all [User]s participating in this plan and their respective access type. - @override List get members; - - /// Create a copy of CalendarPlan - /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) _$$CalendarPlanImplCopyWith<_$CalendarPlanImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/calendar/domain/models/calendar_plan.g.dart b/lib/src/calendar/domain/models/calendar_plan.g.dart index 85013303..5cd86fca 100644 --- a/lib/src/calendar/domain/models/calendar_plan.g.dart +++ b/lib/src/calendar/domain/models/calendar_plan.g.dart @@ -22,6 +22,6 @@ Map _$$CalendarPlanImplToJson(_$CalendarPlanImpl instance) => { 'name': instance.name, 'planid': instance.id, - 'deadlines': instance.deadlines.map((e) => e.toJson()).toList(), - 'members': instance.members.map((e) => e.toJson()).toList(), + 'deadlines': instance.deadlines, + 'members': instance.members, }; diff --git a/lib/src/calendar/domain/models/plan_deadline.freezed.dart b/lib/src/calendar/domain/models/plan_deadline.freezed.dart index bf15d2e4..63f6ab8b 100644 --- a/lib/src/calendar/domain/models/plan_deadline.freezed.dart +++ b/lib/src/calendar/domain/models/plan_deadline.freezed.dart @@ -34,12 +34,8 @@ mixin _$PlanDeadline { @UnixTimestampConverter() DateTime get end => throw _privateConstructorUsedError; - /// Serializes this PlanDeadline to a JSON map. Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of PlanDeadline - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) $PlanDeadlineCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -66,8 +62,6 @@ class _$PlanDeadlineCopyWithImpl<$Res, $Val extends PlanDeadline> // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of PlanDeadline - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -114,8 +108,6 @@ class __$$PlanDeadlineImplCopyWithImpl<$Res> _$PlanDeadlineImpl _value, $Res Function(_$PlanDeadlineImpl) _then) : super(_value, _then); - /// Create a copy of PlanDeadline - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -188,13 +180,11 @@ class _$PlanDeadlineImpl extends _PlanDeadline { (identical(other.end, end) || other.end == end)); } - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override int get hashCode => Object.hash(runtimeType, id, start, end); - /// Create a copy of PlanDeadline - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$PlanDeadlineImplCopyWith<_$PlanDeadlineImpl> get copyWith => @@ -222,27 +212,25 @@ abstract class _PlanDeadline extends PlanDeadline { factory _PlanDeadline.fromJson(Map json) = _$PlanDeadlineImpl.fromJson; - /// The ID of this deadline. @override + + /// The ID of this deadline. @JsonKey(name: 'moduleid') int get id; + @override /// The start date of this deadline. - @override @JsonKey(name: 'deadlinestart') @UnixTimestampConverter() DateTime get start; + @override /// The end date of this deadline. - @override @JsonKey(name: 'deadlineend') @UnixTimestampConverter() DateTime get end; - - /// Create a copy of PlanDeadline - /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) _$$PlanDeadlineImplCopyWith<_$PlanDeadlineImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/calendar/domain/models/plan_invite.freezed.dart b/lib/src/calendar/domain/models/plan_invite.freezed.dart index ed8908d7..e17550b0 100644 --- a/lib/src/calendar/domain/models/plan_invite.freezed.dart +++ b/lib/src/calendar/domain/models/plan_invite.freezed.dart @@ -42,12 +42,8 @@ mixin _$PlanInvite { @UnixTimestampConverter() DateTime get timestamp => throw _privateConstructorUsedError; - /// Serializes this PlanInvite to a JSON map. Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of PlanInvite - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) $PlanInviteCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -77,8 +73,6 @@ class _$PlanInviteCopyWithImpl<$Res, $Val extends PlanInvite> // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of PlanInvite - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -143,8 +137,6 @@ class __$$PlanInviteImplCopyWithImpl<$Res> _$PlanInviteImpl _value, $Res Function(_$PlanInviteImpl) _then) : super(_value, _then); - /// Create a copy of PlanInvite - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -248,14 +240,12 @@ class _$PlanInviteImpl extends _PlanInvite { other.timestamp == timestamp)); } - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override int get hashCode => Object.hash( runtimeType, id, inviterId, planId, invitedUserId, status, timestamp); - /// Create a copy of PlanInvite - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$PlanInviteImplCopyWith<_$PlanInviteImpl> get copyWith => @@ -283,38 +273,36 @@ abstract class _PlanInvite extends PlanInvite { factory _PlanInvite.fromJson(Map json) = _$PlanInviteImpl.fromJson; - /// The ID of this invitation. @override + + /// The ID of this invitation. int get id; + @override /// The ID of the [User] who created this invite. - @override @JsonKey(name: 'inviterid') int get inviterId; + @override /// The ID of the [CalendarPlan] this invite is for. - @override @JsonKey(name: 'planid') int get planId; + @override /// The ID of the [User] who is invited. - @override @JsonKey(name: 'inviteeid') int get invitedUserId; + @override /// The status of this invite. - @override PlanInviteStatus get status; + @override /// The date and time this invite was created. - @override @UnixTimestampConverter() DateTime get timestamp; - - /// Create a copy of PlanInvite - /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) _$$PlanInviteImplCopyWith<_$PlanInviteImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/calendar/domain/models/plan_member.freezed.dart b/lib/src/calendar/domain/models/plan_member.freezed.dart index 27ca59fb..dde63d96 100644 --- a/lib/src/calendar/domain/models/plan_member.freezed.dart +++ b/lib/src/calendar/domain/models/plan_member.freezed.dart @@ -28,12 +28,8 @@ mixin _$PlanMember { @JsonKey(name: 'accesstype') PlanMemberAccessType get accessType => throw _privateConstructorUsedError; - /// Serializes this PlanMember to a JSON map. Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of PlanMember - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) $PlanMemberCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -59,8 +55,6 @@ class _$PlanMemberCopyWithImpl<$Res, $Val extends PlanMember> // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of PlanMember - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -101,8 +95,6 @@ class __$$PlanMemberImplCopyWithImpl<$Res> _$PlanMemberImpl _value, $Res Function(_$PlanMemberImpl) _then) : super(_value, _then); - /// Create a copy of PlanMember - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -158,13 +150,11 @@ class _$PlanMemberImpl extends _PlanMember { other.accessType == accessType)); } - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override int get hashCode => Object.hash(runtimeType, id, accessType); - /// Create a copy of PlanMember - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$PlanMemberImplCopyWith<_$PlanMemberImpl> get copyWith => @@ -188,20 +178,18 @@ abstract class _PlanMember extends PlanMember { factory _PlanMember.fromJson(Map json) = _$PlanMemberImpl.fromJson; - /// The ID of the [User]. @override + + /// The ID of the [User]. @JsonKey(name: 'userid') int get id; + @override /// The access type of the member. - @override @JsonKey(name: 'accesstype') PlanMemberAccessType get accessType; - - /// Create a copy of PlanMember - /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) _$$PlanMemberImplCopyWith<_$PlanMemberImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/calendar/presentation/repositories/calendar_plan_repository.dart b/lib/src/calendar/presentation/repositories/calendar_plan_repository.dart index 28de5cf8..f102de57 100644 --- a/lib/src/calendar/presentation/repositories/calendar_plan_repository.dart +++ b/lib/src/calendar/presentation/repositories/calendar_plan_repository.dart @@ -343,7 +343,7 @@ class CalendarPlanRepository extends Repository> with T final _start = DateTime(deadline.start.year, deadline.start.month, deadline.start.day); final _end = DateTime(deadline.end.year, deadline.end.month, deadline.end.day); - if (taskIds != null && taskIds.contains(deadline.id)) return false; + if (taskIds != null && !taskIds.contains(deadline.id)) return false; if (start != null && _start != start) return false; if (end != null && _end != end) return false; diff --git a/lib/src/calendar/presentation/widgets/plan_popup.dart b/lib/src/calendar/presentation/widgets/plan_popup.dart index 5f671981..1fd2e0c3 100644 --- a/lib/src/calendar/presentation/widgets/plan_popup.dart +++ b/lib/src/calendar/presentation/widgets/plan_popup.dart @@ -30,50 +30,53 @@ class _PlanPopupState extends State with SingleTickerProviderStateMix @override Widget build(BuildContext context) { - return Container( - padding: PaddingAll(Spacing.smallSpacing), - decoration: ShapeDecoration( - color: context.theme.cardColor, - shape: squircle(), - shadows: kElevationToShadow[16], - ), - child: Column( - children: [ - Row( - children: [ - TabBar( - controller: controller, - tabs: [ - Tab( - text: context.t.calendar_tasks, - ), - Tab( - text: context.t.calendar_members, - ), - ], - onTap: (index) { - setState(() { - controller.index = index; - }); - }, - ).expanded(), - Spacing.smallHorizontal(), - IconButton( - icon: const Icon(Icons.close), - color: context.theme.colorScheme.error, - onPressed: widget.close, - ), - ], - ), - Padding( - padding: PaddingTop(Spacing.mediumSpacing), - child: controller.index == 0 - ? PlanPopupTasks( - dragWidth: widget.dragWidth, - ) - : const PlanPopupMembers(), - ).expanded(), - ], + return TabBarTheme( + data: const TabBarThemeData(), + child: Container( + padding: PaddingAll(Spacing.smallSpacing), + decoration: ShapeDecoration( + color: context.theme.cardColor, + shape: squircle(), + shadows: kElevationToShadow[16], + ), + child: Column( + children: [ + Row( + children: [ + TabBar( + controller: controller, + tabs: [ + Tab( + text: context.t.calendar_tasks, + ), + Tab( + text: context.t.calendar_members, + ), + ], + onTap: (index) { + setState(() { + controller.index = index; + }); + }, + ).expanded(), + Spacing.smallHorizontal(), + IconButton( + icon: const Icon(Icons.close), + color: context.theme.colorScheme.error, + onPressed: widget.close, + ), + ], + ), + Padding( + padding: PaddingTop(Spacing.mediumSpacing), + child: controller.index == 0 + ? PlanPopupTasks( + dragWidth: widget.dragWidth, + ) + : const PlanPopupMembers(), + ).expanded(), + ], + ), ), ); } diff --git a/lib/src/kanban/domain/datasources/datasources.dart b/lib/src/kanban/domain/datasources/datasources.dart new file mode 100644 index 00000000..fa56486c --- /dev/null +++ b/lib/src/kanban/domain/datasources/datasources.dart @@ -0,0 +1 @@ +export 'kanban_datasource.dart'; diff --git a/lib/src/kanban/domain/datasources/kanban_datasource.dart b/lib/src/kanban/domain/datasources/kanban_datasource.dart new file mode 100644 index 00000000..b568fed7 --- /dev/null +++ b/lib/src/kanban/domain/datasources/kanban_datasource.dart @@ -0,0 +1,19 @@ +import 'package:eduplanner/eduplanner.dart'; +import 'package:mcquenji_core/mcquenji_core.dart'; + +/// Datasource for the Kanban board feature. +abstract class KanbanDatasource extends Datasource with Tracable { + @override + String get name => 'Kanban'; + + /// Fetches the Kanban board for the user associated with the given [token]. + /// + /// Optionally, a list of [backlogCandidates] can be provided to suggest tasks that might belong in the backlog. + /// These will be filtered to exclude tasks already present in other columns. + /// + /// **Note:** [KanbanBoard.backlog] will be empty if [backlogCandidates] is not provided as the backend does not store backlog tasks. + Future getBoard(String token, {List backlogCandidates = const []}); + + /// Moves the given [taskId] to the specified [to] column for the user associated with the given [token]. + Future move(String token, {required int taskId, required KanbanColumn to}); +} diff --git a/lib/src/kanban/domain/domain.dart b/lib/src/kanban/domain/domain.dart new file mode 100644 index 00000000..bce50988 --- /dev/null +++ b/lib/src/kanban/domain/domain.dart @@ -0,0 +1,3 @@ +export 'datasources/datasources.dart'; +export 'models/models.dart'; +export 'services/services.dart'; diff --git a/lib/src/kanban/domain/models/kanban_board.dart b/lib/src/kanban/domain/models/kanban_board.dart new file mode 100644 index 00000000..adb83b42 --- /dev/null +++ b/lib/src/kanban/domain/models/kanban_board.dart @@ -0,0 +1,61 @@ +// ignore_for_file: invalid_annotation_target + +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'kanban_board.freezed.dart'; +part 'kanban_board.g.dart'; + +/// Kanban board model. +@freezed +class KanbanBoard with _$KanbanBoard { + /// Kanban board model. + const factory KanbanBoard({ + @Default([]) List backlog, + required List todo, + @JsonKey(name: 'inprogress') required List inProgress, + required List done, + }) = _KanbanBoard; + + const KanbanBoard._(); + + /// Kanbanboard from json. + factory KanbanBoard.fromJson(Map json) => _$KanbanBoardFromJson(json); + + /// Creates a scaffold Kanban board with sample data. + /// + /// All task ids are negative to avoid conflicts with real task ids. + factory KanbanBoard.scaffold() => const KanbanBoard( + backlog: [-8798739812, -829, -3983, -87893], + todo: [-8798739812, -829, -3983, -87893], + inProgress: [-8798739812, -829, -3983, -87893], + done: [-8798739812, -829, -3983, -87893], + ); + + /// All task ids in the board. + List get all => [...backlog, ...todo, ...inProgress, ...done]; +} + +/// The columns of the Kanban board. +/// +/// !Names are used in the backend, so do not change them. +enum KanbanColumn { + /// The backlog column. + /// + /// This is where unassigned tasks live. + backlog, + + /// The to-do column. + /// + /// This is where tasks that are planned to be done live. + todo, + + /// The in-progress column. + /// + /// This is where tasks that are currently being worked on live. + inprogress, + + /// The done column. + /// + /// This is where completed tasks live. + done, +} diff --git a/lib/src/kanban/domain/models/kanban_board.freezed.dart b/lib/src/kanban/domain/models/kanban_board.freezed.dart new file mode 100644 index 00000000..154d742c --- /dev/null +++ b/lib/src/kanban/domain/models/kanban_board.freezed.dart @@ -0,0 +1,254 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'kanban_board.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +KanbanBoard _$KanbanBoardFromJson(Map json) { + return _KanbanBoard.fromJson(json); +} + +/// @nodoc +mixin _$KanbanBoard { + List get backlog => throw _privateConstructorUsedError; + List get todo => throw _privateConstructorUsedError; + @JsonKey(name: 'inprogress') + List get inProgress => throw _privateConstructorUsedError; + List get done => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $KanbanBoardCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $KanbanBoardCopyWith<$Res> { + factory $KanbanBoardCopyWith( + KanbanBoard value, $Res Function(KanbanBoard) then) = + _$KanbanBoardCopyWithImpl<$Res, KanbanBoard>; + @useResult + $Res call( + {List backlog, + List todo, + @JsonKey(name: 'inprogress') List inProgress, + List done}); +} + +/// @nodoc +class _$KanbanBoardCopyWithImpl<$Res, $Val extends KanbanBoard> + implements $KanbanBoardCopyWith<$Res> { + _$KanbanBoardCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? backlog = null, + Object? todo = null, + Object? inProgress = null, + Object? done = null, + }) { + return _then(_value.copyWith( + backlog: null == backlog + ? _value.backlog + : backlog // ignore: cast_nullable_to_non_nullable + as List, + todo: null == todo + ? _value.todo + : todo // ignore: cast_nullable_to_non_nullable + as List, + inProgress: null == inProgress + ? _value.inProgress + : inProgress // ignore: cast_nullable_to_non_nullable + as List, + done: null == done + ? _value.done + : done // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$KanbanBoardImplCopyWith<$Res> + implements $KanbanBoardCopyWith<$Res> { + factory _$$KanbanBoardImplCopyWith( + _$KanbanBoardImpl value, $Res Function(_$KanbanBoardImpl) then) = + __$$KanbanBoardImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {List backlog, + List todo, + @JsonKey(name: 'inprogress') List inProgress, + List done}); +} + +/// @nodoc +class __$$KanbanBoardImplCopyWithImpl<$Res> + extends _$KanbanBoardCopyWithImpl<$Res, _$KanbanBoardImpl> + implements _$$KanbanBoardImplCopyWith<$Res> { + __$$KanbanBoardImplCopyWithImpl( + _$KanbanBoardImpl _value, $Res Function(_$KanbanBoardImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? backlog = null, + Object? todo = null, + Object? inProgress = null, + Object? done = null, + }) { + return _then(_$KanbanBoardImpl( + backlog: null == backlog + ? _value._backlog + : backlog // ignore: cast_nullable_to_non_nullable + as List, + todo: null == todo + ? _value._todo + : todo // ignore: cast_nullable_to_non_nullable + as List, + inProgress: null == inProgress + ? _value._inProgress + : inProgress // ignore: cast_nullable_to_non_nullable + as List, + done: null == done + ? _value._done + : done // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$KanbanBoardImpl extends _KanbanBoard { + const _$KanbanBoardImpl( + {final List backlog = const [], + required final List todo, + @JsonKey(name: 'inprogress') required final List inProgress, + required final List done}) + : _backlog = backlog, + _todo = todo, + _inProgress = inProgress, + _done = done, + super._(); + + factory _$KanbanBoardImpl.fromJson(Map json) => + _$$KanbanBoardImplFromJson(json); + + final List _backlog; + @override + @JsonKey() + List get backlog { + if (_backlog is EqualUnmodifiableListView) return _backlog; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_backlog); + } + + final List _todo; + @override + List get todo { + if (_todo is EqualUnmodifiableListView) return _todo; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_todo); + } + + final List _inProgress; + @override + @JsonKey(name: 'inprogress') + List get inProgress { + if (_inProgress is EqualUnmodifiableListView) return _inProgress; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_inProgress); + } + + final List _done; + @override + List get done { + if (_done is EqualUnmodifiableListView) return _done; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_done); + } + + @override + String toString() { + return 'KanbanBoard(backlog: $backlog, todo: $todo, inProgress: $inProgress, done: $done)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$KanbanBoardImpl && + const DeepCollectionEquality().equals(other._backlog, _backlog) && + const DeepCollectionEquality().equals(other._todo, _todo) && + const DeepCollectionEquality() + .equals(other._inProgress, _inProgress) && + const DeepCollectionEquality().equals(other._done, _done)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_backlog), + const DeepCollectionEquality().hash(_todo), + const DeepCollectionEquality().hash(_inProgress), + const DeepCollectionEquality().hash(_done)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$KanbanBoardImplCopyWith<_$KanbanBoardImpl> get copyWith => + __$$KanbanBoardImplCopyWithImpl<_$KanbanBoardImpl>(this, _$identity); + + @override + Map toJson() { + return _$$KanbanBoardImplToJson( + this, + ); + } +} + +abstract class _KanbanBoard extends KanbanBoard { + const factory _KanbanBoard( + {final List backlog, + required final List todo, + @JsonKey(name: 'inprogress') required final List inProgress, + required final List done}) = _$KanbanBoardImpl; + const _KanbanBoard._() : super._(); + + factory _KanbanBoard.fromJson(Map json) = + _$KanbanBoardImpl.fromJson; + + @override + List get backlog; + @override + List get todo; + @override + @JsonKey(name: 'inprogress') + List get inProgress; + @override + List get done; + @override + @JsonKey(ignore: true) + _$$KanbanBoardImplCopyWith<_$KanbanBoardImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/src/kanban/domain/models/kanban_board.g.dart b/lib/src/kanban/domain/models/kanban_board.g.dart new file mode 100644 index 00000000..29498e48 --- /dev/null +++ b/lib/src/kanban/domain/models/kanban_board.g.dart @@ -0,0 +1,32 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'kanban_board.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$KanbanBoardImpl _$$KanbanBoardImplFromJson(Map json) => + _$KanbanBoardImpl( + backlog: (json['backlog'] as List?) + ?.map((e) => (e as num).toInt()) + .toList() ?? + const [], + todo: (json['todo'] as List) + .map((e) => (e as num).toInt()) + .toList(), + inProgress: (json['inprogress'] as List) + .map((e) => (e as num).toInt()) + .toList(), + done: (json['done'] as List) + .map((e) => (e as num).toInt()) + .toList(), + ); + +Map _$$KanbanBoardImplToJson(_$KanbanBoardImpl instance) => + { + 'backlog': instance.backlog, + 'todo': instance.todo, + 'inprogress': instance.inProgress, + 'done': instance.done, + }; diff --git a/lib/src/kanban/domain/models/models.dart b/lib/src/kanban/domain/models/models.dart new file mode 100644 index 00000000..1a7be5b2 --- /dev/null +++ b/lib/src/kanban/domain/models/models.dart @@ -0,0 +1 @@ +export 'kanban_board.dart'; diff --git a/lib/src/kanban/domain/services/services.dart b/lib/src/kanban/domain/services/services.dart new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/lib/src/kanban/domain/services/services.dart @@ -0,0 +1 @@ + diff --git a/lib/src/kanban/infra/datasources/datasources.dart b/lib/src/kanban/infra/datasources/datasources.dart new file mode 100644 index 00000000..50b02536 --- /dev/null +++ b/lib/src/kanban/infra/datasources/datasources.dart @@ -0,0 +1,2 @@ +export 'local_kanban_datasource.dart'; +export 'moodle_kanban_datasource.dart'; diff --git a/lib/src/kanban/infra/datasources/local_kanban_datasource.dart b/lib/src/kanban/infra/datasources/local_kanban_datasource.dart new file mode 100644 index 00000000..4ce871bf --- /dev/null +++ b/lib/src/kanban/infra/datasources/local_kanban_datasource.dart @@ -0,0 +1,49 @@ +import 'package:eduplanner/eduplanner.dart'; + +/// Implementation of [KanbanDatasource] for local development using a static [KanbanBoard]. +/// +/// **DO NOT USE IN PROD** This is just for development of the UI until the backend is ready. +class LocalKanbanDatasource extends KanbanDatasource { + static KanbanBoard? _board; + + /// Implementation of [KanbanDatasource] for local development using a static [KanbanBoard]. + /// + /// **DO NOT USE IN PROD** This is just for development of the UI until the backend is ready. + LocalKanbanDatasource(); + + @override + Future getBoard(String token, {List backlogCandidates = const []}) async { + return _board ??= KanbanBoard( + backlog: backlogCandidates, + todo: [], + inProgress: [], + done: [], + ); + } + + @override + Future move(String token, {required int taskId, required KanbanColumn to}) async { + if (_board == null) return; + + _board = _board!.copyWith( + backlog: _board!.backlog.where((id) => id != taskId).toList(), + todo: _board!.todo.where((id) => id != taskId).toList(), + inProgress: _board!.inProgress.where((id) => id != taskId).toList(), + done: _board!.done.where((id) => id != taskId).toList(), + ); + switch (to) { + case KanbanColumn.backlog: + _board = _board!.copyWith(backlog: [..._board!.backlog, taskId]); + break; + case KanbanColumn.todo: + _board = _board!.copyWith(todo: [..._board!.todo, taskId]); + break; + case KanbanColumn.inprogress: + _board = _board!.copyWith(inProgress: [..._board!.inProgress, taskId]); + break; + case KanbanColumn.done: + _board = _board!.copyWith(done: [..._board!.done, taskId]); + break; + } + } +} diff --git a/lib/src/kanban/infra/datasources/moodle_kanban_datasource.dart b/lib/src/kanban/infra/datasources/moodle_kanban_datasource.dart new file mode 100644 index 00000000..38e93dc7 --- /dev/null +++ b/lib/src/kanban/infra/datasources/moodle_kanban_datasource.dart @@ -0,0 +1,70 @@ +import 'package:eduplanner/eduplanner.dart'; + +/// Implementation of [KanbanDatasource] using the moodle api implemented in the [backend](https://github.com/necodeIT/lb_planner_plugin/pull/73) +class MoodleKanbanDatasource extends KanbanDatasource { + final ApiService _api; + + /// Implementation of [KanbanDatasource] using the moodle api implemented in the [backend](https://github.com/necodeIT/lb_planner_plugin/pull/73) + MoodleKanbanDatasource(this._api) { + _api.parent = this; + } + + @override + void dispose() { + super.dispose(); + _api.dispose(); + } + + @override + Future getBoard(String token, {List backlogCandidates = const []}) async { + final transaction = startTransaction('getBoard'); + + log('fetching kanban board'); + + try { + final data = await _api.callFunction(function: 'local_lbplanner_kanban_get_board', token: token); + + data.assertJson(); + + final board = KanbanBoard.fromJson(data.asJson); + + log('kanban board fetched'); + + return board.copyWith( + backlog: backlogCandidates.where((id) => !board.all.contains(id)).toList(), + ); + } catch (e, s) { + transaction.internalError(e); + log('failed to fetch kanban board', e, s); + rethrow; + } finally { + await transaction.commit(); + } + } + + @override + Future move(String token, {required int taskId, required KanbanColumn to}) async { + final transaction = startTransaction('move'); + + log('moving task $taskId to $to'); + + try { + await _api.callFunction( + function: 'local_lbplanner_kanban_move_module', + token: token, + body: { + 'cmid': taskId, + 'column': to.name, + }, + ); + + log('task $taskId moved to $to'); + } catch (e, s) { + transaction.internalError(e); + log('failed to move task $taskId to $to', e, s); + rethrow; + } finally { + await transaction.commit(); + } + } +} diff --git a/lib/src/kanban/infra/infra.dart b/lib/src/kanban/infra/infra.dart new file mode 100644 index 00000000..3d2ed1c4 --- /dev/null +++ b/lib/src/kanban/infra/infra.dart @@ -0,0 +1,2 @@ +export 'datasources/datasources.dart'; +export 'services/services.dart'; diff --git a/lib/src/kanban/infra/services/services.dart b/lib/src/kanban/infra/services/services.dart new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/lib/src/kanban/infra/services/services.dart @@ -0,0 +1 @@ + diff --git a/lib/src/kanban/kanban.dart b/lib/src/kanban/kanban.dart new file mode 100644 index 00000000..d737185f --- /dev/null +++ b/lib/src/kanban/kanban.dart @@ -0,0 +1,36 @@ +import 'package:eduplanner/eduplanner.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:mcquenji_core/mcquenji_core.dart'; + +import 'infra/infra.dart'; + +export 'domain/domain.dart'; +export 'presentation/presentation.dart'; +export 'utils/utils.dart'; + +/// Module for the Kanban board feature. +class KanbanModule extends Module { + @override + List get imports => [ + CoreModule(), + MoodleModule(), + AuthModule(), + ]; + + @override + void binds(Injector i) { + i + ..add(MoodleKanbanDatasource.new) + ..add(() => (BuildContext context) => (context.t.kanban_title, null)) + ..addRepository(KanbanRepository.new); + } + + @override + void exportedBinds(Injector i) {} + + @override + void routes(RouteManager r) { + r.child('/', child: (_) => const KanbanScreen()); + } +} diff --git a/lib/src/kanban/presentation/guards/guards.dart b/lib/src/kanban/presentation/guards/guards.dart new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/lib/src/kanban/presentation/guards/guards.dart @@ -0,0 +1 @@ + diff --git a/lib/src/kanban/presentation/presentation.dart b/lib/src/kanban/presentation/presentation.dart new file mode 100644 index 00000000..912ebd2f --- /dev/null +++ b/lib/src/kanban/presentation/presentation.dart @@ -0,0 +1,4 @@ +export 'guards/guards.dart'; +export 'repositories/repositories.dart'; +export 'screens/screens.dart'; +export 'widgets/widgets.dart'; diff --git a/lib/src/kanban/presentation/repositories/kanban_repository.dart b/lib/src/kanban/presentation/repositories/kanban_repository.dart new file mode 100644 index 00000000..5717a50f --- /dev/null +++ b/lib/src/kanban/presentation/repositories/kanban_repository.dart @@ -0,0 +1,200 @@ +import 'dart:async'; + +import 'package:eduplanner/eduplanner.dart'; +import 'package:mcquenji_core/mcquenji_core.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +/// Repository for managing the Kanban board. +class KanbanRepository extends Repository> with Tracable { + final KanbanDatasource _datasource; + final AuthRepository _auth; + final MoodleTasksRepository _tasks; + final UserRepository _user; + + /// Repository for managing the Kanban board. + KanbanRepository(this._datasource, this._auth, this._tasks, this._user) : super(AsyncValue.loading()) { + watchAsync(_auth); + watchAsync(_tasks); + + _datasource.parent = this; + } + + @override + FutureOr build(Trigger trigger) async { + final token = waitForData(_auth).pick(Webservice.lb_planner_api); + final tasks = waitForData(_tasks).map((e) => e.cmid).toList(); + + log('Loading kanban board with ${tasks.length} backlog candidates'); + + final transaction = startTransaction('loadKanbanBoard'); + + try { + final board = await _datasource.getBoard(token, backlogCandidates: tasks); + + data(board); + log('Kanban board loaded'); + } catch (e, s) { + log('Error loading kanban board', e, s); + transaction.internalError(e); + } finally { + await transaction.commit(); + } + + if (trigger is! _AutoMoveTrigger) { + await autoMove(); + } + } + + /// Moves the given [taskId] to the specified [to] column. + Future move({required int taskId, required KanbanColumn to, ISentrySpan? span, bool skipAnalytics = false, Trigger? trigger}) async { + if (!state.hasData) { + log('Cannot move task: No board data available'); + return; + } + + final transaction = span?.startChild('moveKanbanTask') ?? startTransaction('moveKanbanTask'); + + final token = _auth.state.requireData.pick(Webservice.lb_planner_api); + + try { + final old = state.requireData; + + var _board = old.copyWith( + backlog: old.backlog.where((id) => id != taskId).toList(), + todo: old.todo.where((id) => id != taskId).toList(), + inProgress: old.inProgress.where((id) => id != taskId).toList(), + done: old.done.where((id) => id != taskId).toList(), + ); + switch (to) { + case KanbanColumn.backlog: + _board = _board.copyWith(backlog: [..._board.backlog, taskId]); + break; + case KanbanColumn.todo: + _board = _board.copyWith(todo: [..._board.todo, taskId]); + break; + case KanbanColumn.inprogress: + _board = _board.copyWith(inProgress: [..._board.inProgress, taskId]); + break; + case KanbanColumn.done: + _board = _board.copyWith(done: [..._board.done, taskId]); + break; + } + + data(_board); + + log('Moving task $taskId to $to'); + + await _datasource.move( + token, + taskId: taskId, + to: to, + ); + + log('Task $taskId moved to $to'); + + if (!skipAnalytics) { + await captureEvent( + 'kanban_task_moved', + properties: { + 'taskId': taskId, + 'to': to.name, + }, + ); + } + + await refresh(trigger ?? this); + } catch (e, st) { + transaction.internalError(e); + log('Error moving task', e, st); + } finally { + await transaction.commit(); + } + } + + /// Automatically moves tasks based on user settings. + Future autoMove() async { + if (_user.state.data == null) { + log('Cannot auto-move tasks: No user data available'); + return; + } + + if (!state.hasData) { + log('Cannot auto-move tasks: No board data available'); + return; + } + + log('Auto-moving tasks based on user settings'); + + final settings = _user.state.data!; + final board = state.requireData; + + final mapping = { + MoodleTaskStatus.uploaded: settings.autoMoveSubmittedTasksTo, + MoodleTaskStatus.late: settings.autoMoveOverdueTasksTo, + MoodleTaskStatus.done: settings.autoMoveCompletedTasksTo, + }; + + final span = startTransaction('autoMoveTasks'); + + await Future.wait([ + _autoMove(from: KanbanColumn.backlog, tasks: board.backlog, to: mapping, span: span), + _autoMove(from: KanbanColumn.todo, tasks: board.todo, to: mapping, span: span), + _autoMove(from: KanbanColumn.inprogress, tasks: board.inProgress, to: mapping, span: span), + _autoMove(from: KanbanColumn.done, tasks: board.done, to: mapping, span: span), + ]); + } + + Future _autoMove({ + required KanbanColumn from, + required List tasks, + required Map to, + required ISentrySpan span, + }) async { + if (tasks.isEmpty) return; + + final transaction = span.startChild('autoMoveFrom${from.name}'); + + log('Auto-moving tasks from $from: ${tasks.length} candidates'); + + try { + await Future.wait( + tasks.map( + (id) async { + final status = _tasks.getByCmid(id)?.status; + + if (status == null) return; + + final target = to[status]; + + if (target == null) return; + + log('Auto-moving task $id from $from to $target due to status $status'); + + return move( + taskId: id, + to: target, + skipAnalytics: true, + span: transaction.startChild('autoMoveTask'), + trigger: _AutoMoveTrigger(), + ); + }, + ), + ); + + log('Auto-moving tasks from $from completed'); + } catch (e, s) { + transaction.internalError(e); + log('Error auto-moving tasks from $from', e, s); + } finally { + await transaction.finish(); + } + } + + @override + void dispose() { + super.dispose(); + _datasource.dispose(); + } +} + +class _AutoMoveTrigger extends Trigger {} diff --git a/lib/src/kanban/presentation/repositories/repositories.dart b/lib/src/kanban/presentation/repositories/repositories.dart new file mode 100644 index 00000000..5a19e5de --- /dev/null +++ b/lib/src/kanban/presentation/repositories/repositories.dart @@ -0,0 +1 @@ +export 'kanban_repository.dart'; diff --git a/lib/src/kanban/presentation/screens/kanban_screen.dart b/lib/src/kanban/presentation/screens/kanban_screen.dart new file mode 100644 index 00000000..998c4724 --- /dev/null +++ b/lib/src/kanban/presentation/screens/kanban_screen.dart @@ -0,0 +1,89 @@ +import 'package:awesome_extensions/awesome_extensions_flutter.dart'; +import 'package:eduplanner/eduplanner.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_modular/flutter_modular.dart'; + +/// Shows all kanban columns and their tasks. +class KanbanScreen extends StatefulWidget { + /// Shows all kanban columns and their tasks. + const KanbanScreen({super.key}); + + @override + State createState() => _KanbanScreenState(); +} + +class _KanbanScreenState extends State with AdaptiveState, NoMobile { + final animationDuration = 300.ms; + + bool showBacklog = false; + + void toggleBacklog() { + setState(() { + showBacklog = !showBacklog; + }); + } + + @override + Widget buildDesktop(BuildContext context) { + final repo = context.watch(); + final user = context.watch(); + + final board = repo.state.data ?? KanbanBoard.scaffold(); + + Color? applyColor(Color color) { + if (!(user.state.data?.showColumnColors ?? true)) return null; + + return color; + } + + return Padding( + padding: PaddingAll(), + child: Row( + spacing: Spacing.smallSpacing, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Tooltip( + message: showBacklog ? context.t.kanban_screen_hideBacklog : context.t.kanban_screen_showBacklog, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: toggleBacklog, + child: showBacklog + ? const Icon(Icons.folder) + : const Icon( + Icons.folder_open, + ), + ), + ), + ), + if (showBacklog) + KanbanColumnWidget( + name: context.t.kanban_screen_backlog, + tasks: board.backlog, + color: applyColor(context.theme.taskStatusTheme.pendingColor), + column: KanbanColumn.backlog, + ), + KanbanColumnWidget( + name: context.t.kanban_screen_toDo, + tasks: board.todo, + color: applyColor(context.theme.colorScheme.primary), + column: KanbanColumn.todo, + ).expanded(), + KanbanColumnWidget( + name: context.t.kanban_screen_inProgress, + tasks: board.inProgress, + column: KanbanColumn.inprogress, + color: applyColor(context.theme.taskStatusTheme.uploadedColor), + ).expanded(), + KanbanColumnWidget( + name: context.t.kanban_screen_done, + tasks: board.done, + column: KanbanColumn.done, + color: applyColor(context.theme.taskStatusTheme.doneColor), + ).expanded(), + ], + ).expanded(), + ); + } +} diff --git a/lib/src/kanban/presentation/screens/screens.dart b/lib/src/kanban/presentation/screens/screens.dart new file mode 100644 index 00000000..906f08de --- /dev/null +++ b/lib/src/kanban/presentation/screens/screens.dart @@ -0,0 +1 @@ +export 'kanban_screen.dart'; diff --git a/lib/src/kanban/presentation/widgets/kanban_card.dart b/lib/src/kanban/presentation/widgets/kanban_card.dart new file mode 100644 index 00000000..4d49e16a --- /dev/null +++ b/lib/src/kanban/presentation/widgets/kanban_card.dart @@ -0,0 +1,137 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:context_menus/context_menus.dart'; +import 'package:eduplanner/eduplanner.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:flutter_vector_icons/flutter_vector_icons.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:timeago/timeago.dart'; +import 'package:url_launcher/url_launcher.dart'; + +/// A card within a kanban column. +class KanbanCard extends StatelessWidget { + /// A card within a kanban column. + const KanbanCard({super.key, required this.task}); + + /// The task represented by this card. + final MoodleTask task; + + /// Used to keep the same size when [Draggable] is activated. + static double lastKnownWidth = 300; + + @override + Widget build(BuildContext context) { + final courses = context.watch(); + final calendar = context.watch(); + + final course = courses.getById(task.courseId); + + final planned = calendar.filterDeadlines(taskIds: {task.id}).firstOrNull; + + return LayoutBuilder( + builder: (context, size) { + lastKnownWidth = size.maxWidth; + return ContextMenuRegion( + contextMenu: GenericContextMenu( + buttonConfigs: [ + ContextMenuButtonConfig( + context.t.moodle_moodleTaskWidget_openInMoodle, + onPressed: () => launchUrl(Uri.parse(task.url)), + icon: const Icon(Icons.link), + iconHover: Icon(Icons.link, color: context.theme.colorScheme.primary), + ), + ], + ), + child: SizedBox( + height: 150, + child: Card( + elevation: 0, + child: Padding( + padding: PaddingAll(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: Spacing.smallSpacing, + children: [ + Skeletonizer( + enabled: course == null, + child: CourseTag(course: course ?? MoodleCourse.skeleton()), + ), + Text( + task.name, + ).bold(), + ], + ).stretch(), + Spacing.xsVertical(), + Column( + spacing: Spacing.xsSpacing, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + spacing: Spacing.smallSpacing, + children: [ + Icon( + FontAwesome5Solid.circle, + size: 12, + color: context.theme.taskStatusTheme.colorOf(task.status), + ), + Text(task.status.translate(context)), + ], + ).stretch(), + if (task.deadline != null) + Row( + spacing: Spacing.xsSpacing, + children: [ + const Icon(Icons.calendar_month, size: 16), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: context.t.kanban_card_dueOn(format(task.deadline!, allowFromNow: true)), + ), + const TextSpan(text: ' ('), + TextSpan( + text: CourseOverviewScreen.formatter.format(task.deadline!), + style: context.theme.textTheme.bodyMedium?.bold, + ), + const TextSpan(text: ')'), + ], + ), + ), + ], + ), + if (planned != null) + Row( + spacing: Spacing.xsSpacing, + children: [ + const Icon(Icons.timelapse_rounded, size: 16), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: context.t.kanban_card_plannedOn(format(planned.end, allowFromNow: true)), + ), + const TextSpan(text: ' ('), + TextSpan( + text: CourseOverviewScreen.formatter.format(planned.end), + style: context.theme.textTheme.bodyMedium?.bold, + ), + const TextSpan(text: ')'), + ], + ), + ), + ], + ), + ], + ).expanded(), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/src/kanban/presentation/widgets/kanban_column_widget.dart b/lib/src/kanban/presentation/widgets/kanban_column_widget.dart new file mode 100644 index 00000000..510a7f96 --- /dev/null +++ b/lib/src/kanban/presentation/widgets/kanban_column_widget.dart @@ -0,0 +1,117 @@ +import 'package:awesome_extensions/awesome_extensions_flutter.dart'; +import 'package:eduplanner/eduplanner.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:skeletonizer/skeletonizer.dart'; + +/// A column within the Kanban board. +class KanbanColumnWidget extends StatefulWidget { + /// A column within the Kanban board. + const KanbanColumnWidget({super.key, required this.tasks, this.color, required this.name, required this.column}); + + /// The tasks assigned to this column. + final List tasks; + + /// The color of this column. + final Color? color; + + /// The name of this column. + final String name; + + /// The type of column. + final KanbanColumn column; + + @override + State createState() => _KanbanColumnWidgetState(); +} + +class _KanbanColumnWidgetState extends State { + final searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + + searchController.addListener(() { + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + final tasksRepo = context.watch(); + final board = context.read(); + + return DragTarget( + onWillAcceptWithDetails: (details) => !widget.tasks.contains(details.data.cmid), + onAcceptWithDetails: (d) => board.move(taskId: d.data.cmid, to: widget.column), + builder: (context, candiates, _) { + final tasks = tasksRepo.filter(cmids: widget.tasks.toSet(), query: searchController.text); + + final tasksWithDropCandidates = [...candiates.nonNulls, ...tasks]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${widget.name} (${tasksWithDropCandidates.length})', + style: context.theme.textTheme.titleLarge, + ), + Spacing.largeVertical(), + TextField( + controller: searchController, + style: context.textTheme.bodyMedium, + decoration: InputDecoration( + hintText: context.t.global_search, + prefixIcon: const Icon(Icons.search), + filled: true, + fillColor: context.theme.colorScheme.surface, + focusColor: context.theme.colorScheme.surface, + hoverColor: context.theme.colorScheme.surface, + isDense: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + ), + ).stretch(), + Spacing.smallVertical(), + Container( + padding: PaddingAll(Spacing.smallSpacing), + decoration: ShapeDecoration( + shape: squircle(), + color: widget.color?.withValues(alpha: 0.1), + ), + child: ListView.separated( + itemBuilder: (context, index) { + final task = tasksWithDropCandidates[index]; + return Draggable( + data: task, + feedback: Builder( + builder: (context) { + return SizedBox( + width: KanbanCard.lastKnownWidth, + child: KanbanCard( + task: task, + ), + ); + }, + ), + childWhenDragging: Skeletonizer( + child: KanbanCard(task: task), + ), + child: KanbanCard( + task: task, + ), + ).stretch(); + }, + separatorBuilder: (context, index) => Spacing.smallVertical(), + itemCount: tasksWithDropCandidates.length, + ), + ).expanded(), + ], + ); + }, + ).expanded(); + } +} diff --git a/lib/src/kanban/presentation/widgets/widgets.dart b/lib/src/kanban/presentation/widgets/widgets.dart new file mode 100644 index 00000000..5de5e4e2 --- /dev/null +++ b/lib/src/kanban/presentation/widgets/widgets.dart @@ -0,0 +1,2 @@ +export 'kanban_card.dart'; +export 'kanban_column_widget.dart'; diff --git a/lib/src/kanban/utils/utils.dart b/lib/src/kanban/utils/utils.dart new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/lib/src/kanban/utils/utils.dart @@ -0,0 +1 @@ + diff --git a/lib/src/moodle/domain/models/moodle_course.freezed.dart b/lib/src/moodle/domain/models/moodle_course.freezed.dart index cf2bfd5a..20ea899d 100644 --- a/lib/src/moodle/domain/models/moodle_course.freezed.dart +++ b/lib/src/moodle/domain/models/moodle_course.freezed.dart @@ -39,12 +39,8 @@ mixin _$MoodleCourse { @BoolConverter() bool get enabled => throw _privateConstructorUsedError; - /// Serializes this MoodleCourse to a JSON map. Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of MoodleCourse - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) $MoodleCourseCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -73,8 +69,6 @@ class _$MoodleCourseCopyWithImpl<$Res, $Val extends MoodleCourse> // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of MoodleCourse - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -133,8 +127,6 @@ class __$$MoodleCourseImplCopyWithImpl<$Res> _$MoodleCourseImpl _value, $Res Function(_$MoodleCourseImpl) _then) : super(_value, _then); - /// Create a copy of MoodleCourse - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -225,14 +217,12 @@ class _$MoodleCourseImpl extends _MoodleCourse { (identical(other.enabled, enabled) || other.enabled == enabled)); } - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override int get hashCode => Object.hash(runtimeType, id, color, name, shortname, enabled); - /// Create a copy of MoodleCourse - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$MoodleCourseImplCopyWith<_$MoodleCourseImpl> get copyWith => @@ -258,34 +248,32 @@ abstract class _MoodleCourse extends MoodleCourse { factory _MoodleCourse.fromJson(Map json) = _$MoodleCourseImpl.fromJson; - /// The ID of this course. @override + + /// The ID of this course. @JsonKey(name: 'courseid') int get id; + @override /// The color of this course in hexadecimal format. - @override @HexColorConverter() Color get color; + @override /// The name of this course. - @override String get name; + @override /// The shortname chosen by the user for this course. /// Limited to 5 characters. - @override String get shortname; + @override /// Whether the user want's the app to track this course. - @override @BoolConverter() bool get enabled; - - /// Create a copy of MoodleCourse - /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) _$$MoodleCourseImplCopyWith<_$MoodleCourseImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/moodle/domain/models/moodle_task.freezed.dart b/lib/src/moodle/domain/models/moodle_task.freezed.dart index 1d5da2d8..0be40bd7 100644 --- a/lib/src/moodle/domain/models/moodle_task.freezed.dart +++ b/lib/src/moodle/domain/models/moodle_task.freezed.dart @@ -46,12 +46,8 @@ mixin _$MoodleTask { @UnixTimestampConverterNullable() DateTime? get deadline => throw _privateConstructorUsedError; - /// Serializes this MoodleTask to a JSON map. Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of MoodleTask - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) $MoodleTaskCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -84,8 +80,6 @@ class _$MoodleTaskCopyWithImpl<$Res, $Val extends MoodleTask> // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of MoodleTask - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -158,8 +152,6 @@ class __$$MoodleTaskImplCopyWithImpl<$Res> _$MoodleTaskImpl _value, $Res Function(_$MoodleTaskImpl) _then) : super(_value, _then); - /// Create a copy of MoodleTask - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -276,14 +268,12 @@ class _$MoodleTaskImpl extends _MoodleTask { other.deadline == deadline)); } - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override int get hashCode => Object.hash( runtimeType, id, cmid, name, courseId, status, type, deadline); - /// Create a copy of MoodleTask - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$MoodleTaskImplCopyWith<_$MoodleTaskImpl> get copyWith => @@ -313,43 +303,41 @@ abstract class _MoodleTask extends MoodleTask { factory _MoodleTask.fromJson(Map json) = _$MoodleTaskImpl.fromJson; - /// The ID of this task. @override + + /// The ID of this task. @JsonKey(name: 'assignid') int get id; + @override /// The id of the task within it's parent [MoodleCourse]. - @override @JsonKey(name: 'cmid') int get cmid; + @override /// The name of this task. - @override String get name; + @override /// The ID of the [MoodleCourse] this task is part of. - @override @JsonKey(name: 'courseid') int get courseId; + @override /// The status of this task. - @override MoodleTaskStatus get status; + @override /// The type of this task. - @override MoodleTaskType get type; + @override /// The timestamp of when this task is due in seconds since the Unix epoch. - @override @JsonKey(name: 'duedate') @UnixTimestampConverterNullable() DateTime? get deadline; - - /// Create a copy of MoodleTask - /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) _$$MoodleTaskImplCopyWith<_$MoodleTaskImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/moodle/presentation/repositories/moodle_courses_repository.dart b/lib/src/moodle/presentation/repositories/moodle_courses_repository.dart index 98aaf0d8..3e3f37f8 100644 --- a/lib/src/moodle/presentation/repositories/moodle_courses_repository.dart +++ b/lib/src/moodle/presentation/repositories/moodle_courses_repository.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:eduplanner/config/endpoints.dart'; import 'package:eduplanner/src/app/app.dart'; import 'package:eduplanner/src/moodle/moodle.dart'; @@ -130,6 +131,19 @@ class MoodleCoursesRepository extends Repository>> }).toList(); } + /// Gets a course by its [id]. + MoodleCourse? getById(int id) { + if (!state.hasData) { + log('Cannot get course: No data available.'); + + return null; + } + + final courses = state.requireData; + + return courses.firstWhereOrNull((element) => element.id == id); + } + @override void dispose() { super.dispose(); diff --git a/lib/src/moodle/presentation/repositories/moodle_tasks_repository.dart b/lib/src/moodle/presentation/repositories/moodle_tasks_repository.dart index 90ad7a2b..c86fd35f 100644 --- a/lib/src/moodle/presentation/repositories/moodle_tasks_repository.dart +++ b/lib/src/moodle/presentation/repositories/moodle_tasks_repository.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:eduplanner/config/endpoints.dart'; import 'package:eduplanner/src/app/app.dart'; import 'package:eduplanner/src/moodle/moodle.dart'; @@ -62,6 +63,7 @@ class MoodleTasksRepository extends Repository>> wit Set? courseIds, int? taskId, Set? taskIds, + Set? cmids, Duration? deadlineDiff, Duration? minDeadlineDiff, Duration? maxDeadlineDiff, @@ -77,6 +79,7 @@ class MoodleTasksRepository extends Repository>> wit courseIds: courseIds, taskId: taskId, taskIds: taskIds, + cmids: cmids, deadlineDiff: deadlineDiff, minDeadlineDiff: minDeadlineDiff, maxDeadlineDiff: maxDeadlineDiff, @@ -86,6 +89,20 @@ class MoodleTasksRepository extends Repository>> wit ); } + /// Gets a task by its course module ID ([cmid]). + MoodleTask? getByCmid(int cmid) { + if (!state.hasData) return null; + + return state.requireData.firstWhereOrNull((task) => task.cmid == cmid); + } + + /// Gets a task by its [id]. + MoodleTask? getById(int id) { + if (!state.hasData) return null; + + return state.requireData.firstWhereOrNull((task) => task.id == id); + } + @override void dispose() { super.dispose(); @@ -115,6 +132,7 @@ extension TasksFilterX on Iterable { int? courseId, Set? courseIds, int? taskId, + Set? cmids, Set? taskIds, Duration? deadlineDiff, Duration? minDeadlineDiff, @@ -142,6 +160,7 @@ extension TasksFilterX on Iterable { if (query != null && !task.name.toLowerCase().contains(query.toLowerCase())) return false; if (courseIds != null && !courseIds.contains(task.courseId)) return false; if (taskIds != null && !taskIds.contains(task.id)) return false; + if (cmids != null && !cmids.contains(task.cmid)) return false; if (test != null && !test.call(task)) return false; diff --git a/lib/src/notifications/domain/models/notification.freezed.dart b/lib/src/notifications/domain/models/notification.freezed.dart index bb38b297..03f236f6 100644 --- a/lib/src/notifications/domain/models/notification.freezed.dart +++ b/lib/src/notifications/domain/models/notification.freezed.dart @@ -50,12 +50,8 @@ mixin _$Notification { @JsonKey(name: 'userid') int get userId => throw _privateConstructorUsedError; - /// Serializes this Notification to a JSON map. Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of Notification - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) $NotificationCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -88,8 +84,6 @@ class _$NotificationCopyWithImpl<$Res, $Val extends Notification> // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of Notification - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -162,8 +156,6 @@ class __$$NotificationImplCopyWithImpl<$Res> _$NotificationImpl _value, $Res Function(_$NotificationImpl) _then) : super(_value, _then); - /// Create a copy of Notification - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -281,14 +273,12 @@ class _$NotificationImpl extends _Notification { (identical(other.userId, userId) || other.userId == userId)); } - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override int get hashCode => Object.hash( runtimeType, id, timestamp, readAt, type, context, read, userId); - /// Create a copy of Notification - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$NotificationImplCopyWith<_$NotificationImpl> get copyWith => @@ -318,47 +308,45 @@ abstract class _Notification extends Notification { factory _Notification.fromJson(Map json) = _$NotificationImpl.fromJson; - /// The notification's unique identifier. @override + + /// The notification's unique identifier. @JsonKey(name: 'notificationid') int get id; + @override /// The timestamp when the notification was sent. - @override @UnixTimestampConverter() DateTime get timestamp; + @override /// The timestamp when the notification was read. - @override @UnixTimestampConverter() @JsonKey(name: 'timestamp_read') DateTime? get readAt; + @override /// The type of the notification. /// /// The message is displayed differently based on the type. - @override NotificationType get type; + @override /// Additional context for the notification. /// Interpretation depends on the [type]. - @override @JsonKey(name: 'info') int? get context; + @override /// `true` if the notification has been read. - @override @BoolConverter() @JsonKey(name: 'status') bool get read; @override @JsonKey(name: 'userid') int get userId; - - /// Create a copy of Notification - /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) _$$NotificationImplCopyWith<_$NotificationImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/settings/presentation/screens/settings_screen.dart b/lib/src/settings/presentation/screens/settings_screen.dart index 8b91b66f..8d01df05 100644 --- a/lib/src/settings/presentation/screens/settings_screen.dart +++ b/lib/src/settings/presentation/screens/settings_screen.dart @@ -29,6 +29,10 @@ class SettingsScreen extends StatelessWidget with AdaptiveWidget { Expanded( child: const GeneralSettings().stretch(), ).show(stagger), + if (capabilities.hasStudent) + Expanded( + child: const KanbanSettings().stretch(), + ).show(stagger), Expanded( child: const ThemesSettings().stretch(), ).show(stagger), diff --git a/lib/src/settings/presentation/widgets/feedback_widget.dart b/lib/src/settings/presentation/widgets/feedback_widget.dart index bdc530e3..f210046f 100644 --- a/lib/src/settings/presentation/widgets/feedback_widget.dart +++ b/lib/src/settings/presentation/widgets/feedback_widget.dart @@ -147,7 +147,6 @@ class _FeedbackWidgetState extends State with AdaptiveState { ), child: DropdownMenu( width: 135, - alignmentOffset: const Offset(60, 65), trailingIcon: const Icon( FontAwesome5Solid.chevron_down, size: 13, diff --git a/lib/src/settings/presentation/widgets/general_settings.dart b/lib/src/settings/presentation/widgets/general_settings.dart index 0fb1fdb8..d3aa2f58 100644 --- a/lib/src/settings/presentation/widgets/general_settings.dart +++ b/lib/src/settings/presentation/widgets/general_settings.dart @@ -1,9 +1,8 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:eduplanner/config/posthog.dart'; import 'package:eduplanner/config/version.dart'; -import 'package:eduplanner/src/app/app.dart'; -import 'package:eduplanner/src/auth/auth.dart'; -import 'package:eduplanner/src/theming/theming.dart'; +import 'package:eduplanner/eduplanner.dart'; +import 'package:eduplanner/src/settings/presentation/widgets/generic_settings.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_vector_icons/flutter_vector_icons.dart'; @@ -18,7 +17,7 @@ class GeneralSettings extends StatefulWidget { State createState() => _GeneralSettingsState(); } -class _GeneralSettingsState extends State with AdaptiveState { +class _GeneralSettingsState extends State { bool checkingUpdates = false; bool clearingCache = false; bool deletingProfile = false; @@ -69,143 +68,51 @@ class _GeneralSettingsState extends State with AdaptiveState { Modular.to.navigate('/auth/'); } - List buildItems(BuildContext context) { + @override + Widget build(BuildContext context) { final user = context.watch(); final isStudent = user.state.data?.capabilities.hasStudent ?? false; - return [ - iconItem( - title: context.t.settings_general_version(kInstalledRelease.toString()), - icon: Icons.update, - onPressed: checkUpdates, - ), - - // item(context.t.settings_general_deleteProfile, Icons.delete, deleteProfile, context.theme.colorScheme.error), - if (isStudent) - checkboxItem( - title: context.t.settings_general_enableEK, - value: user.state.data?.optionalTasksEnabled ?? false, - onChanged: user.enableOptionalTasks, + return GenericSettings( + title: context.t.settings_general, + items: [ + IconSettingsItem( + name: context.t.settings_general_version(kInstalledRelease.toString()), + icon: Icons.info_outline_rounded, + hoverColor: context.theme.colorScheme.onSurface, + // icon: Icons.update, + // onPressed: checkUpdates, ), - if (isStudent) - checkboxItem( - title: context.t.settings_general_displayTaskCount, - value: user.state.data?.displayTaskCount ?? false, - onChanged: user.setDisplayTaskCount, + IconSettingsItem( + name: context.t.auth_privacyPolicy, + icon: FontAwesome5Solid.balance_scale, + onPressed: () => launchUrl(kPrivacyPolicyUrl), + iconSize: 14, ), - iconItem( - title: context.t.auth_privacyPolicy, - icon: FontAwesome5Solid.balance_scale, - onPressed: () => launchUrl(kPrivacyPolicyUrl), - iconSize: 14, - ), - // iconItem( - // title: context.t.settings_general_manageSubscription, - // icon: FontAwesome5Solid.credit_card, - // onPressed: () => Modular.to.pushNamed('/subscription'), // TODO(mcquenji): Implement subscription screen - // iconSize: 14, - // ), - - if (context.isMobile) iconItem(title: context.t.settings_logout, icon: Icons.logout, onPressed: logout), - ]; - } - @override - Widget buildDesktop(BuildContext context) { - return Card( - child: Padding( - padding: PaddingAll(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.t.settings_general, - style: context.textTheme.titleMedium?.bold, - ).alignAtTopLeft(), - Expanded( - child: ListView( - children: buildItems(context).vSpaced(Spacing.smallSpacing), - ), - ), - ], - ), - ), - ); - } - - @override - Widget buildMobile(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.t.settings_general, - style: context.textTheme.titleMedium?.bold, - ).alignAtTopLeft(), - Spacing.smallVertical(), - Column( - children: buildItems(context).vSpaced(Spacing.smallSpacing), - ), - ], - ); - } - - Widget iconItem({required String title, required IconData icon, VoidCallback? onPressed, Color? hoverColor, double? iconSize = 20}) { - return HoverBuilder( - onTap: onPressed, - builder: (context, hovering) => item( - title: title, - suffix: ConditionalWrapper( - condition: !context.isMobile, - child: Icon( - icon, - color: hovering ? hoverColor ?? context.theme.colorScheme.primary : context.theme.colorScheme.onSurface, - size: iconSize, + // Delete profile could be re-added as a destructive action item when needed + if (isStudent) + BooleanSettingsItem( + name: context.t.settings_general_enableEK, + value: user.state.data?.optionalTasksEnabled ?? false, + onChanged: user.enableOptionalTasks, ), - wrapper: (context, child) => Container( - height: 35, - width: 35, - decoration: ShapeDecoration( - shape: squircle(), - color: context.theme.scaffoldBackgroundColor, - ), - child: child, + if (isStudent) + BooleanSettingsItem( + name: context.t.settings_general_displayTaskCount, + value: user.state.data?.displayTaskCount ?? false, + onChanged: user.setDisplayTaskCount, ), - ), - onPressed: onPressed, - ), - ); - } - // ignore: avoid_positional_boolean_parameters - Widget checkboxItem({required String title, required bool value, required Function(bool?) onChanged}) { - return item( - title: title, - suffix: Checkbox( - value: value, - onChanged: onChanged, - ), - onPressed: () => onChanged(!value), - ); - } + // Manage subscription could be added as another IconSettingsItem - // ignore: avoid_positional_boolean_parameters - Widget item({required String title, required Widget suffix, VoidCallback? onPressed}) { - final children = [ - Text(title).expanded(), - Spacing.smallHorizontal(), - suffix, - ]; - - return SizedBox( - height: 35, - child: GestureDetector( - onTap: onPressed, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: context.isMobile ? children.reversed.toList() : children, - ), - ), + if (context.isMobile) + IconSettingsItem( + name: context.t.settings_logout, + icon: Icons.logout, + onPressed: logout, + ), + ], ); } } diff --git a/lib/src/settings/presentation/widgets/generic_settings.dart b/lib/src/settings/presentation/widgets/generic_settings.dart new file mode 100644 index 00000000..12f178d6 --- /dev/null +++ b/lib/src/settings/presentation/widgets/generic_settings.dart @@ -0,0 +1,351 @@ +/// Settings widgets and utilities for building the settings UI. +/// +/// This library provides a small set of composable primitives used to render +/// the Settings screen: +/// - `GenericSettingsItem`: base contract for a single settings row. +/// - `SettingsRowLayout`: common row layout with a trailing/leading suffix. +/// - `IconSettingsItem`: actionable row that shows an icon as its suffix. +/// - `BooleanSettingsItem`: toggle row rendered as a switch or checkbox. +/// - `EnumSettingsItem`: dropdown row for choosing among predefined values. +/// - `GenericSettings`: titled group that lays out a list of items. +/// +/// All widgets are designed to adapt to form factors via `AdaptiveWidget` and +/// share a consistent look-and-feel. +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:eduplanner/eduplanner.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_vector_icons/flutter_vector_icons.dart'; + +/// Base type for items rendered by [GenericSettings]. +abstract class GenericSettingsItem { + /// Base type for items rendered by [GenericSettings]. + const GenericSettingsItem({ + required this.name, + this.onPressed, + this.description, + }); + + /// Human-readable label displayed for this settings row. + final String name; + + /// Callback invoked when the row is tapped/clicked. + /// + /// Implementations may choose to wire this to an inner interactive control + /// (e.g. a toggle), or leave it unused. When null, the row is non-interactive + /// except for its inner widgets. + final VoidCallback? onPressed; + + /// Optional secondary text describing the setting. + /// + /// Not directly used by the base layout but available to concrete + /// implementations that choose to display additional context. + final String? description; + + /// Canonical max height used by suffix controls to align rows consistently. + static const maxItemHeight = 35.0; + + /// Builds the visual representation of this item. + /// + /// The default implementation renders a simple [Text] with [name]. + /// Subclasses typically override this to provide richer content but should + /// still respect the overall row height and spacing conventions. + Widget build(BuildContext context) { + return Text(name); + } +} + +/// Base layout for a single-line settings row with a suffix widget. +/// +/// The main content (usually the [name] and optional description) is laid out +/// opposite a suffix built by [buildSuffix]. On mobile, the order is reversed +/// so the suffix appears on the leading side to improve reachability. +abstract class SettingsRowLayout extends GenericSettingsItem { + /// Base layout for a single-line settings row with a suffix widget. + /// + /// The main content (usually the [name] and optional description) is laid out + /// opposite a suffix built by [buildSuffix]. On mobile, the order is reversed + /// so the suffix appears on the leading side to improve reachability. + const SettingsRowLayout({ + required super.name, + super.onPressed, + super.description, + }); + + @override + Widget build(BuildContext context) { + final children = [ + super.build(context).expanded(), + Spacing.smallHorizontal(), + buildSuffix(context), + ]; + + return SizedBox( + child: GestureDetector( + onTap: onPressed, + behavior: HitTestBehavior.opaque, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: context.isMobile ? children.reversed.toList() : children, + ), + ), + ); + } + + /// Builds the trailing/leading widget shown opposite to the main content. + /// + /// On desktop the suffix appears at the end of the row; on mobile, the row + /// order is reversed to improve ergonomics. + Widget buildSuffix(BuildContext context); +} + +/// An actionable row with an icon on the trailing/leading side. +class IconSettingsItem extends SettingsRowLayout { + /// An actionable row with an icon on the trailing/leading side. + const IconSettingsItem({ + required super.name, + required this.icon, + super.onPressed, + this.hoverColor, + this.iconSize = 20, + super.description, + }); + + /// Icon to display as the row suffix. + final IconData icon; + + /// Optional color applied to the icon when the row is hovered. + final Color? hoverColor; + + /// Size of the icon in logical pixels. + final double iconSize; + + @override + Widget build(BuildContext context) { + return HoverBuilder( + cursor: onPressed != null ? SystemMouseCursors.click : SystemMouseCursors.basic, + onTap: onPressed, + builder: (context, hovering) => IconTheme( + data: IconThemeData(color: hovering ? hoverColor ?? context.theme.colorScheme.primary : context.theme.colorScheme.onSurface), + child: super.build(context), + ), + ); + } + + @override + Widget buildSuffix(BuildContext context) { + return ConditionalWrapper( + condition: !context.isMobile, + child: Icon( + icon, + size: iconSize, + ), + wrapper: (context, child) => Container( + width: GenericSettingsItem.maxItemHeight, + height: GenericSettingsItem.maxItemHeight, + decoration: ShapeDecoration( + shape: squircle(), + color: context.theme.scaffoldBackgroundColor, + ), + child: child, + ), + ); + } +} + +/// A boolean setting rendered as a switch or checkbox. +/// +/// - By default this shows a [Checkbox]. +/// - Set [checkbox] to false to use a [Switch] instead. +/// - Tapping the row toggles the value by calling [onChanged] with `!value`. +class BooleanSettingsItem extends SettingsRowLayout { + /// A boolean setting rendered as a switch or checkbox. + /// + /// - By default this shows a [Checkbox]. + /// - Set [checkbox] to false to use a [Switch] instead. + /// - Tapping the row toggles the value by calling [onChanged] with `!value`. + BooleanSettingsItem({ + required super.name, + required this.value, + required this.onChanged, + this.checkbox = true, + super.description, + }) : super(onPressed: () => onChanged(!value)); + + /// Current value of the toggle. + final bool value; + + /// Callback fired when the toggle changes. + final ValueChanged onChanged; + + /// If true, renders a [Checkbox]; otherwise renders a [Switch]. + final bool checkbox; + + @override + Widget buildSuffix(BuildContext context) { + return checkbox + ? Checkbox(value: value, onChanged: onChanged) + : Switch( + value: value, + onChanged: onChanged, + ); + } +} + +/// A dropdown row for choosing one value from a predefined list. +/// +/// Generic over [T] and uses [itemBuilder] (and optional [iconBuilder]) to map +/// each value to its presentation. +class EnumSettingsItem extends SettingsRowLayout { + /// A dropdown row for choosing one value from a predefined list. + /// + /// Generic over [T] and uses [itemBuilder] (and optional [iconBuilder]) to map + /// each value to its presentation. + EnumSettingsItem({ + required super.name, + required this.value, + required this.values, + required this.onChanged, + required this.itemBuilder, + this.iconBuilder, + this.iconSize = 16, + this.label, + this.helperText, + super.description, + this.dropDownWidth, + }) : super(onPressed: null); + + /// Currently selected value. + final T value; + + /// All available values that can be selected. + final List values; + + /// Called when a new value is selected from the dropdown. + final ValueChanged onChanged; + + /// Returns the display label for a value. + /// + /// This allows the widget to remain generic over `T` while presenting a + /// meaningful string in the UI. + final String Function(BuildContext, T) itemBuilder; + + /// Optionally returns an icon to show next to the value. + final IconData? Function(BuildContext, T)? iconBuilder; + + /// Icon size used for both the selected value and entries. + final double iconSize; + + /// Optional label displayed above the dropdown field. + final String? label; + + /// Optional helper text displayed below the dropdown field. + final String? helperText; + + /// Fixed width for the dropdown; defaults to intrinsic width when null. + final double? dropDownWidth; + + @override + Widget buildSuffix(BuildContext context) { + final icon = iconBuilder?.call(context, value); + + return Theme( + data: context.theme.copyWith( + colorScheme: context.theme.colorScheme.copyWith(onSurface: context.theme.colorScheme.primary), + ), + child: SizedBox( + height: GenericSettingsItem.maxItemHeight, + child: DropdownMenu( + inputDecorationTheme: context.theme.dropdownMenuTheme.inputDecorationTheme?.copyWith( + contentPadding: PaddingAll(Spacing.xsSpacing).Horizontal(Spacing.smallSpacing), + ), + width: dropDownWidth, + trailingIcon: const Icon( + FontAwesome5Solid.chevron_down, + size: 13, + ), + enableSearch: false, + requestFocusOnTap: false, + label: label != null ? Text(label!) : null, + helperText: helperText, + onSelected: onChanged, + initialSelection: value, + leadingIcon: icon != null ? Icon(icon, size: iconSize) : null, + dropdownMenuEntries: values.map((e) { + final icon = iconBuilder?.call(context, e); + + return DropdownMenuEntry( + value: e, + label: itemBuilder(context, e), + leadingIcon: icon != null ? Icon(icon, size: iconSize) : null, + ); + }).toList(), + ), + ), + ); + } +} + +/// Renders a list of settings items with consistent layout and behavior. +class GenericSettings extends StatelessWidget with AdaptiveWidget { + /// Renders a list of settings items with consistent layout and behavior. + const GenericSettings({ + super.key, + required this.title, + required this.items, + }); + + /// Title shown above the group of settings items. + final String title; + + /// Items to render in order. + final List items; + + /// Builds the list of item widgets separated by vertical spacing. + List _buildItemWidgets(BuildContext context) { + return items.map((e) => e.build(context)).toList().vSpaced(Spacing.smallSpacing); + } + + @override + + /// Builds the desktop variant: a titled [Card] with a scrolling list. + Widget buildDesktop(BuildContext context) { + return Card( + child: Padding( + padding: PaddingAll(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: context.textTheme.titleMedium?.bold, + ).alignAtTopLeft(), + Expanded( + child: ListView( + children: _buildItemWidgets(context).show(), + ), + ), + ], + ), + ), + ); + } + + @override + + /// Builds the mobile variant: a titled [Column] without a card shell. + Widget buildMobile(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: context.textTheme.titleMedium?.bold, + ).alignAtTopLeft(), + Spacing.smallVertical(), + Column( + children: _buildItemWidgets(context).show(), + ), + ], + ); + } +} diff --git a/lib/src/settings/presentation/widgets/kanban_settings.dart b/lib/src/settings/presentation/widgets/kanban_settings.dart new file mode 100644 index 00000000..02ae9302 --- /dev/null +++ b/lib/src/settings/presentation/widgets/kanban_settings.dart @@ -0,0 +1,68 @@ +import 'package:eduplanner/eduplanner.dart'; +import 'package:eduplanner/src/settings/presentation/widgets/generic_settings.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; + +/// Settings ui for the kanban board +class KanbanSettings extends StatelessWidget { + /// Settings ui for the kanban board + const KanbanSettings({super.key}); + + @override + Widget build(BuildContext context) { + final user = context.watch(); + final settings = user.state.data; + + String translateColumn(KanbanColumn? column) { + switch (column) { + case KanbanColumn.backlog: + return context.t.kanban_screen_backlog; + case KanbanColumn.todo: + return context.t.kanban_screen_toDo; + case KanbanColumn.inprogress: + return context.t.kanban_screen_inProgress; + case KanbanColumn.done: + return context.t.kanban_screen_done; + case null: + return context.t.kanban_settings_disabled; + } + } + + GenericSettingsItem autoMoveItem({required String name, required KanbanColumn? value, required void Function(KanbanColumn?) onChanged}) { + return EnumSettingsItem( + name: name, + value: value, + values: [null, ...KanbanColumn.values], + itemBuilder: (context, value) => translateColumn(value), + onChanged: onChanged, + dropDownWidth: 125, + ); + } + + return GenericSettings( + title: context.t.kanban_settings_kanban, + items: [ + autoMoveItem( + name: context.t.kanban_settings_moveSubmittedTasks, + value: settings?.autoMoveSubmittedTasksTo, + onChanged: user.setAutoMoveSubmittedTasksTo, + ), + autoMoveItem( + name: context.t.kanban_settings_moveOverdueTasks, + value: settings?.autoMoveOverdueTasksTo, + onChanged: user.setAutoMoveOverdueTasksTo, + ), + autoMoveItem( + name: context.t.kanban_settings_moveCompletedTasks, + value: settings?.autoMoveCompletedTasksTo, + onChanged: user.setAutoMoveCompletedTasksTo, + ), + BooleanSettingsItem( + name: context.t.kanban_settings_columnColors, + value: settings?.showColumnColors ?? true, + onChanged: user.setShowColumnColors, + ), + ], + ); + } +} diff --git a/lib/src/settings/presentation/widgets/widgets.dart b/lib/src/settings/presentation/widgets/widgets.dart index b77796f2..f87f9bf0 100644 --- a/lib/src/settings/presentation/widgets/widgets.dart +++ b/lib/src/settings/presentation/widgets/widgets.dart @@ -1,5 +1,6 @@ export 'course_selector_dialog.dart'; export 'feedback_widget.dart'; export 'general_settings.dart'; +export 'kanban_settings.dart'; export 'theme_preview.dart'; export 'themes_settings.dart'; diff --git a/lib/src/slots/domain/models/course_to_slot.freezed.dart b/lib/src/slots/domain/models/course_to_slot.freezed.dart index 0e321cf3..624a0725 100644 --- a/lib/src/slots/domain/models/course_to_slot.freezed.dart +++ b/lib/src/slots/domain/models/course_to_slot.freezed.dart @@ -34,12 +34,8 @@ mixin _$CourseToSlot { /// The vintage a user must be in to attend this slot. Vintage get vintage => throw _privateConstructorUsedError; - /// Serializes this CourseToSlot to a JSON map. Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of CourseToSlot - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) $CourseToSlotCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -67,8 +63,6 @@ class _$CourseToSlotCopyWithImpl<$Res, $Val extends CourseToSlot> // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of CourseToSlot - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -121,8 +115,6 @@ class __$$CourseToSlotImplCopyWithImpl<$Res> _$CourseToSlotImpl _value, $Res Function(_$CourseToSlotImpl) _then) : super(_value, _then); - /// Create a copy of CourseToSlot - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -200,13 +192,11 @@ class _$CourseToSlotImpl extends _CourseToSlot { (identical(other.vintage, vintage) || other.vintage == vintage)); } - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override int get hashCode => Object.hash(runtimeType, id, courseId, slotId, vintage); - /// Create a copy of CourseToSlot - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$CourseToSlotImplCopyWith<_$CourseToSlotImpl> get copyWith => @@ -231,28 +221,26 @@ abstract class _CourseToSlot extends CourseToSlot { factory _CourseToSlot.fromJson(Map json) = _$CourseToSlotImpl.fromJson; - /// Unique identifier of this mapping. @override + + /// Unique identifier of this mapping. int get id; + @override /// The id of the course. - @override @JsonKey(name: 'courseid') int get courseId; + @override /// The id of the slot. - @override @JsonKey(name: 'slotid') int get slotId; + @override /// The vintage a user must be in to attend this slot. - @override Vintage get vintage; - - /// Create a copy of CourseToSlot - /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) _$$CourseToSlotImplCopyWith<_$CourseToSlotImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/slots/domain/models/reservation.freezed.dart b/lib/src/slots/domain/models/reservation.freezed.dart index 9015d9f2..1a8bf712 100644 --- a/lib/src/slots/domain/models/reservation.freezed.dart +++ b/lib/src/slots/domain/models/reservation.freezed.dart @@ -39,12 +39,8 @@ mixin _$Reservation { @JsonKey(name: 'reserverid') int get reserverId => throw _privateConstructorUsedError; - /// Serializes this Reservation to a JSON map. Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of Reservation - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) $ReservationCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -73,8 +69,6 @@ class _$ReservationCopyWithImpl<$Res, $Val extends Reservation> // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of Reservation - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -133,8 +127,6 @@ class __$$ReservationImplCopyWithImpl<$Res> _$ReservationImpl _value, $Res Function(_$ReservationImpl) _then) : super(_value, _then); - /// Create a copy of Reservation - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -225,14 +217,12 @@ class _$ReservationImpl extends _Reservation { other.reserverId == reserverId)); } - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override int get hashCode => Object.hash(runtimeType, id, slotId, date, userId, reserverId); - /// Create a copy of Reservation - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$ReservationImplCopyWith<_$ReservationImpl> get copyWith => @@ -259,34 +249,32 @@ abstract class _Reservation extends Reservation { factory _Reservation.fromJson(Map json) = _$ReservationImpl.fromJson; - /// Unique identifier of this reservation. @override + + /// Unique identifier of this reservation. int get id; + @override /// The id of the slot this reservation is for. - @override @JsonKey(name: 'slotid') int get slotId; + @override /// The date of this reservation. - @override @ReservationDateTimeConverter() DateTime get date; + @override /// The id of this reservation is for. - @override @JsonKey(name: 'userid') int get userId; + @override /// The id of the user that made this reservation. - @override @JsonKey(name: 'reserverid') int get reserverId; - - /// Create a copy of Reservation - /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) _$$ReservationImplCopyWith<_$ReservationImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/slots/domain/models/slot.freezed.dart b/lib/src/slots/domain/models/slot.freezed.dart index 4c9115e0..83e55c71 100644 --- a/lib/src/slots/domain/models/slot.freezed.dart +++ b/lib/src/slots/domain/models/slot.freezed.dart @@ -54,12 +54,8 @@ mixin _$Slot { @JsonKey(name: 'filters') List get mappings => throw _privateConstructorUsedError; - /// Serializes this Slot to a JSON map. Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of Slot - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) $SlotCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -91,8 +87,6 @@ class _$SlotCopyWithImpl<$Res, $Val extends Slot> // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of Slot - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -179,8 +173,6 @@ class __$$SlotImplCopyWithImpl<$Res> __$$SlotImplCopyWithImpl(_$SlotImpl _value, $Res Function(_$SlotImpl) _then) : super(_value, _then); - /// Create a copy of Slot - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -347,7 +339,7 @@ class _$SlotImpl extends _Slot { const DeepCollectionEquality().equals(other._mappings, _mappings)); } - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override int get hashCode => Object.hash( runtimeType, @@ -362,9 +354,7 @@ class _$SlotImpl extends _Slot { const DeepCollectionEquality().hash(_supervisors), const DeepCollectionEquality().hash(_mappings)); - /// Create a copy of Slot - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$SlotImplCopyWith<_$SlotImpl> get copyWith => @@ -395,54 +385,52 @@ abstract class _Slot extends Slot { factory _Slot.fromJson(Map json) = _$SlotImpl.fromJson; - /// Unique identifier of this slot. @override + + /// Unique identifier of this slot. int get id; + @override /// The start time of this slot. - @override @JsonKey(name: 'startunit') SlotTimeUnit get startUnit; + @override /// The duration of this slot interpreted as [SlotTimeUnit]s. - @override int get duration; + @override /// The weekday this slot takes place on. - @override Weekday get weekday; + @override /// The room this slot takes place in. - @override String get room; + @override /// The number of students that can attend this slot. - @override int get size; + @override /// The number of students that have already reserved this slot. - @override @JsonKey(name: 'fullness') int get reservations; + @override /// `true` if the current user has reserved this slot. - @override @JsonKey(name: 'forcuruser') bool get reserved; + @override /// The user ids of those supervising this slot. - @override List get supervisors; + @override /// The course mappings of this slot. - @override @JsonKey(name: 'filters') List get mappings; - - /// Create a copy of Slot - /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) _$$SlotImplCopyWith<_$SlotImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/slots/domain/models/slot.g.dart b/lib/src/slots/domain/models/slot.g.dart index baf033c9..1f42843d 100644 --- a/lib/src/slots/domain/models/slot.g.dart +++ b/lib/src/slots/domain/models/slot.g.dart @@ -36,7 +36,7 @@ Map _$$SlotImplToJson(_$SlotImpl instance) => 'fullness': instance.reservations, 'forcuruser': instance.reserved, 'supervisors': instance.supervisors, - 'filters': instance.mappings.map((e) => e.toJson()).toList(), + 'filters': instance.mappings, }; const _$SlotTimeUnitEnumMap = { diff --git a/lib/src/slots/presentation/repositories/slot_master_slots_repository.dart b/lib/src/slots/presentation/repositories/slot_master_slots_repository.dart index 29d98b56..602497bc 100644 --- a/lib/src/slots/presentation/repositories/slot_master_slots_repository.dart +++ b/lib/src/slots/presentation/repositories/slot_master_slots_repository.dart @@ -346,6 +346,20 @@ class SlotMasterSlotsRepository extends Repository>> with ); } + /// Groups all slots by their weekday and startunit. + Map>> groupByStartUnit() { + if (!state.hasData) { + log('Cannot group slots: No data'); + return {}; + } + + final byDay = state.requireData.groupListsBy((s) => s.weekday); + + return { + for (final e in byDay.entries) e.key: e.value.groupListsBy((s) => s.startUnit), + }; + } + @override void dispose() { _datasource.dispose(); diff --git a/lib/src/slots/presentation/screens/slot_master_screen.dart b/lib/src/slots/presentation/screens/slot_master_screen.dart index 2d9ede31..0b49aead 100644 --- a/lib/src/slots/presentation/screens/slot_master_screen.dart +++ b/lib/src/slots/presentation/screens/slot_master_screen.dart @@ -1,7 +1,9 @@ -import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:awesome_extensions/awesome_extensions.dart' hide NumExtension; +import 'package:collection/collection.dart'; import 'package:data_widget/data_widget.dart'; import 'package:eduplanner/eduplanner.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_modular/flutter_modular.dart'; /// A screen for managing slots. @@ -13,18 +15,28 @@ class SlotMasterScreen extends StatefulWidget { State createState() => _SlotMasterScreenState(); } -class _SlotMasterScreenState extends State with AdaptiveState, NoMobile { +class _SlotMasterScreenState extends State with AdaptiveState, TickerProviderStateMixin, NoMobile { + late final TabController _tabController; final searchController = TextEditingController(); @override void initState() { super.initState(); + _tabController = TabController(length: Weekday.values.length, vsync: this, animationDuration: 500.ms); + searchController.addListener(() { setState(() {}); }); } + @override + void dispose() { + _tabController.dispose(); + searchController.dispose(); + super.dispose(); + } + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -32,10 +44,13 @@ class _SlotMasterScreenState extends State with AdaptiveState, Data.of(context).setSearchController(searchController); } - void createSlot(Weekday weekday) { + void createSlot(Weekday weekday, SlotTimeUnit startUnit) { showAnimatedDialog( context: context, - pageBuilder: (_, __, ___) => EditSlotDialog(weekday: weekday), + pageBuilder: (_, __, ___) => EditSlotDialog( + weekday: weekday, + startUnit: startUnit, + ), ); } @@ -46,51 +61,75 @@ class _SlotMasterScreenState extends State with AdaptiveState, Widget buildDesktop(BuildContext context) { final slots = context.watch(); - final groups = slots.group(); + final groups = slots.groupByStartUnit(); return Padding( padding: PaddingAll(), - child: SingleChildScrollView( - child: Column( - spacing: Spacing.largeSpacing, - children: [ + child: Scaffold( + appBar: TabBar( + controller: _tabController, + tabs: [ for (final weekday in Weekday.values) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - weekday.translate(context), - style: context.theme.textTheme.titleMedium, - ), - Spacing.xsHorizontal(), - TextButton( - onPressed: () => createSlot(weekday), - child: const Text('New slot'), - ), - ], - ), - Spacing.smallVertical(), - if (groups[weekday]?.isNotEmpty ?? false) - Wrap( - spacing: Spacing.mediumSpacing, - runSpacing: Spacing.mediumSpacing, - children: [ - for (final slot in (groups[weekday] ?? []).query(searchController.text)) - SizedBox( - key: ValueKey(slot), - width: tileWidth, - height: tileHeight, - child: SlotMasterWidget(slot: slot), - ), - ].show(), - ).stretch(), - ], + Tab( + text: weekday.translate(context), ), ], ), + body: Padding( + padding: PaddingTop(Spacing.mediumSpacing).Horizontal(Spacing.smallSpacing), + child: TabBarView( + controller: _tabController, + children: [ + for (final weekday in Weekday.values) slotTimeTable(groups[weekday] ?? >{}, weekday), + ], + ), + ), ), ); } + + Widget slotTimeTable(Map> activeGroup, Weekday weekday) { + return SingleChildScrollView( + child: Column( + spacing: Spacing.largeSpacing, + children: [ + for (final timeUnit in SlotTimeUnit.values) + Column( + spacing: Spacing.smallSpacing, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + timeUnit.humanReadable(), + style: context.theme.textTheme.titleMedium, + ), + Spacing.xsHorizontal(), + TextButton( + onPressed: () => createSlot(weekday, timeUnit), + child: Text(context.t.slots_slotmaster_newSlot), + ), + ], + ), + if (activeGroup[timeUnit]?.isNotEmpty ?? false) + Wrap( + spacing: Spacing.mediumSpacing, + runSpacing: Spacing.mediumSpacing, + children: [ + // TODO(mastermarcohd): implement more intelligent sorting to account for building and floor. + for (final slot in (activeGroup[timeUnit] ?? []).query(searchController.text).sortedBy((s) => s.room)) + SizedBox( + key: ValueKey(slot), + width: tileWidth, + height: tileHeight, + child: SlotMasterWidget(slot: slot), + ), + ].show(), + ).stretch(), + ], + ), + ], + ), + ).expanded(); + } } diff --git a/lib/src/slots/presentation/widgets/edit_slot_dialog.dart b/lib/src/slots/presentation/widgets/edit_slot_dialog.dart index ff9cc0cc..9b0b12d8 100644 --- a/lib/src/slots/presentation/widgets/edit_slot_dialog.dart +++ b/lib/src/slots/presentation/widgets/edit_slot_dialog.dart @@ -1,5 +1,4 @@ import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:collection/collection.dart'; import 'package:eduplanner/eduplanner.dart'; import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; @@ -12,14 +11,21 @@ import 'package:mcquenji_core/mcquenji_core.dart'; class EditSlotDialog extends StatefulWidget { /// A dialog for editing or creating a slot. /// Edits a slot if [slot] is provided and creates a new slot if [weekday] is provided. - const EditSlotDialog({super.key, this.slot, this.weekday}) : assert(slot != null || weekday != null, 'Either slot or weekday must be provided'); + const EditSlotDialog({super.key, this.slot, this.weekday, this.startUnit, this.duplicate = false}) + : assert(slot != null || weekday != null, 'Either slot or weekday must be provided'); /// The weekday the slot will be created for. final Weekday? weekday; + /// The timeunit in which the slot starts. + final SlotTimeUnit? startUnit; + /// The slot to edit. final Slot? slot; + /// Whether the slot is being duplicated. + final bool duplicate; + @override State createState() => _EditSlotDialogState(); } @@ -27,8 +33,6 @@ class EditSlotDialog extends StatefulWidget { class _EditSlotDialogState extends State { final supervisorController = TextEditingController(); final roomController = TextEditingController(); - final courseController = TextEditingController(); - final vintageController = TextEditingController(); final roomFocusNode = FocusNode(); Weekday? weekday; @@ -42,6 +46,7 @@ class _EditSlotDialogState extends State { List supervisors = []; List mappings = []; + List courseMappings = []; bool submitting = false; @@ -49,18 +54,20 @@ class _EditSlotDialogState extends State { void initState() { super.initState(); - editing = widget.slot != null; + editing = widget.slot != null && widget.duplicate == false; weekday = widget.slot?.weekday ?? widget.weekday; roomController.text = widget.slot?.room ?? ''; - start = widget.slot?.startUnit; + start = widget.slot?.startUnit ?? widget.startUnit; end = widget.slot?.endUnit; supervisors = List.of(widget.slot?.supervisors ?? []); size = widget.slot?.size ?? 1; mappings = List.of(widget.slot?.mappings ?? []); + courseMappings = mappings.map((m) => MappingElement(mappingId: m.id, courseId: m.courseId, vintage: m.vintage)).toList(); + if (courseMappings.isEmpty) courseMappings.add(MappingElement()); if (widget.slot != null) { roomController.text = widget.slot!.room; @@ -78,7 +85,8 @@ class _EditSlotDialogState extends State { size != null && roomController.text.isNotEmpty && supervisors.isNotEmpty && - mappings.isNotEmpty; + courseMappings.isNotEmpty && + courseMappings.every((element) => element.isSubmitable() == true); void setSize(int value) { setState(() { @@ -95,6 +103,11 @@ class _EditSlotDialogState extends State { final repo = context.read(); + mappings = []; + for (final element in courseMappings) { + addMapping(element); + } + final slot = widget.slot?.copyWith( room: roomController.text, startUnit: start!, @@ -119,6 +132,8 @@ class _EditSlotDialogState extends State { if (editing) { await repo.updateSlot(slot); + } else if (widget.duplicate) { + await repo.createSlot(slot.copyWith(id: -1)); } else { await repo.createSlot(slot); } @@ -179,36 +194,17 @@ class _EditSlotDialogState extends State { }); } - void addMapping(int courseId, Vintage vintage) { - setState(() { - final mapping = CourseToSlot.noId(courseId: courseId, slotId: widget.slot?.id ?? -1, vintage: vintage); - - mappings.add(mapping); - - courseController.clear(); - vintageController.clear(); - }); - } - - void removeMapping(int courseId, Vintage vintage) { - setState(() { - mappings.removeWhere((mapping) => mapping.courseId == courseId && mapping.vintage == vintage); - }); - } - - Vintage? vintage; - MoodleCourse? course; + void addMapping(MappingElement element) { + final CourseToSlot mapping; + final slotId = widget.slot?.id ?? -1; - void setCourse(MoodleCourse? course) { - setState(() { - this.course = course; - }); - } + if (element.mappingId == null) { + mapping = CourseToSlot.noId(courseId: element.courseId!, slotId: slotId, vintage: element.vintage!); + } else { + mapping = CourseToSlot(id: element.mappingId!, courseId: element.courseId!, slotId: slotId, vintage: element.vintage!); + } - void setVintage(Vintage? vintage) { - setState(() { - this.vintage = vintage; - }); + mappings.add(mapping); } @override @@ -221,7 +217,6 @@ class _EditSlotDialogState extends State { final rooms = slots.state.data?.map((slot) => slot.room).toSet() ?? const Iterable.empty(); final courses = context.watch(); - final courseMappings = mappings.map((m) => (courses.filter(id: m.courseId).firstOrNull, m.vintage)).toList(); return GenericDialog( shrinkWrap: false, @@ -296,7 +291,7 @@ class _EditSlotDialogState extends State { helperText: context.t.slots_edit_weekday, enabled: !submitting, - leadingIcon: const Icon(FontAwesome.calendar_check_o), + leadingIcon: const Icon(FontAwesome5Solid.calendar_check), trailingIcon: const Icon( FontAwesome5Solid.chevron_down, size: 13, @@ -307,7 +302,7 @@ class _EditSlotDialogState extends State { return DropdownMenuEntry( label: weekday.translate(context), value: weekday, - leadingIcon: const Icon(FontAwesome.calendar_check_o), + leadingIcon: const Icon(FontAwesome5Solid.calendar_check), ); }).toList(), ); @@ -334,19 +329,37 @@ class _EditSlotDialogState extends State { return Align( alignment: Alignment.topLeft, child: SizedBox( - width: size.maxWidth, + height: (options.length * 50.0).clamp(0, 200), child: Card( elevation: 8, color: context.theme.scaffoldBackgroundColor, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - for (final option in options) - MenuItemButton( - child: Text(option), - onPressed: () => onSelected(option), - ), - ], + child: ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: options.length, + itemBuilder: (BuildContext context, int index) { + final option = options.elementAt(index); + return HoverBuilder( + onTap: () { + onSelected(option); + }, + builder: (context, hover) { + return Container( + height: 42, + padding: PaddingAll(Spacing.mediumSpacing).Vertical(Spacing.xsSpacing), + decoration: ShapeDecoration( + shape: squircle(), + color: hover ? context.theme.colorScheme.primary : context.theme.scaffoldBackgroundColor, + ), + child: Center( + child: Text( + option, + style: TextStyle(color: hover ? context.theme.colorScheme.onPrimary : null, fontSize: 18), + ), + ), + ); + }, + ); + }, ), ), ), @@ -440,7 +453,7 @@ class _EditSlotDialogState extends State { splashColor: Colors.transparent, highlightColor: Colors.transparent, hoverColor: Colors.transparent, - icon: const Icon(Icons.close), + icon: const Icon(Icons.remove_circle_outline_rounded), onPressed: () => removeSupervisor(supervisor.id), ), ], @@ -456,109 +469,98 @@ class _EditSlotDialogState extends State { style: context.theme.textTheme.titleMedium, ), Spacing.smallVertical(), - Row( - spacing: Spacing.mediumSpacing, + ListView( children: [ - LayoutBuilder( - builder: (context, size) { - return DropdownMenu( - initialSelection: course, - onSelected: setCourse, - enabled: !submitting, - trailingIcon: const Icon( - FontAwesome5Solid.chevron_down, - size: 13, - ), - controller: courseController, - leadingIcon: const Icon(Icons.book), - width: size.maxWidth, - filterCallback: (entries, filter) => entries.where((entry) => entry.label.containsIgnoreCase(filter)).toList(), - enableSearch: false, - enableFilter: true, - hintText: context.t.slots_edit_selectCourse, - menuHeight: 200, - dropdownMenuEntries: [ - for (final course in courses.filter()) - DropdownMenuEntry( - value: course, - label: course.name, - leadingIcon: CourseTag(course: course), - ), - ], - ); - }, - ).expanded(), - LayoutBuilder( - builder: (context, size) { - return DropdownMenu( - width: size.maxWidth, - enabled: !submitting, - trailingIcon: const Icon( - FontAwesome5Solid.chevron_down, - size: 13, - ), - initialSelection: vintage, - hintText: context.t.slots_edit_selectClass, - leadingIcon: const Icon(Icons.school), - menuHeight: 200, - onSelected: setVintage, - controller: vintageController, - filterCallback: (entries, filter) => entries.where((entry) => entry.label.containsIgnoreCase(filter)).toList(), - enableFilter: true, - dropdownMenuEntries: [ - for (final vintage in Vintage.values.where((v) => v.suffix.isNotEmpty)) - DropdownMenuEntry( - value: vintage, - label: vintage.humanReadable, + for (final element in courseMappings) + Row( + spacing: Spacing.mediumSpacing, + children: [ + LayoutBuilder( + builder: (context, size) { + return DropdownMenu( + initialSelection: element.courseId, + onSelected: (courseId) { + setState(() { + element.courseId = courseId; + }); + }, + enabled: !submitting, + trailingIcon: const Icon( + FontAwesome5Solid.chevron_down, + size: 13, + ), + controller: element.courseController, + leadingIcon: const Icon(Icons.book), + width: size.maxWidth, + filterCallback: (entries, filter) => entries.where((entry) => entry.label.containsIgnoreCase(filter)).toList(), + enableSearch: false, + enableFilter: true, + hintText: context.t.slots_edit_selectCourse, + menuHeight: 200, + dropdownMenuEntries: [ + for (final course in courses.filter()) + DropdownMenuEntry( + value: course.id, + label: course.name, + leadingIcon: CourseTag(course: course), + ), + ], + ); + }, + ).expanded(), + LayoutBuilder( + builder: (context, size) { + return DropdownMenu( + width: size.maxWidth, + enabled: !submitting, + trailingIcon: const Icon( + FontAwesome5Solid.chevron_down, + size: 13, + ), + initialSelection: element.vintage, + hintText: context.t.slots_edit_selectClass, leadingIcon: const Icon(Icons.school), - ), - ], - ); - }, - ).expanded(), - IconButton( - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - hoverColor: Colors.transparent, - onPressed: course == null || vintage == null || submitting - ? null - : () { - addMapping(course!.id, vintage!); - setCourse(null); - setVintage(null); + menuHeight: 200, + onSelected: (vintage) { + setState(() { + element.vintage = vintage; + }); + }, + controller: element.vintageController, + filterCallback: (entries, filter) => entries.where((entry) => entry.label.containsIgnoreCase(filter)).toList(), + enableFilter: true, + dropdownMenuEntries: [ + for (final vintage in Vintage.values.where((v) => v.suffix.isNotEmpty)) + DropdownMenuEntry( + value: vintage, + label: vintage.humanReadable, + leadingIcon: const Icon(Icons.school), + ), + ], + ); }, - icon: const Icon(Icons.add), - ), - ], - ), - Spacing.mediumVertical(), - ListView( - children: [ - for (final (course, vintage) in courseMappings) - Container( - padding: PaddingAll(Spacing.smallSpacing), - decoration: ShapeDecoration( - shape: squircle(), - color: context.theme.scaffoldBackgroundColor, - ), - child: Row( - children: [ - CourseTag(course: course!), - Spacing.smallHorizontal(), - Text(course.name), - Spacing.smallHorizontal(), - Text(vintage.humanReadable), - const Spacer(), - IconButton( - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - hoverColor: Colors.transparent, - onPressed: () => removeMapping(course.id, vintage), - icon: const Icon(Icons.close), - ), - ], - ), + ).expanded(), + IconButton( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + hoverColor: Colors.transparent, + onPressed: () { + setState(() { + courseMappings.removeWhere((e) => e.id == element.id); + }); + }, + icon: const Icon(Icons.delete), + ), + ], ), + FilledButton( + onPressed: () { + setState(() { + courseMappings.add(MappingElement()); + }); + }, + child: Text(context.t.slots_edit_addCourseMapping), + ).stretch(), ].vSpaced(Spacing.smallSpacing), ).expanded(), ], @@ -578,3 +580,35 @@ class _EditSlotDialogState extends State { ); } } + +/// Elements to keep track of mappings while editing. +class MappingElement { + /// The current number of elements in the List. + static int currentlistId = 0; + + /// Elements to keep track of mappings while editing. + MappingElement({this.mappingId, this.courseId, this.vintage}); + + /// The id of the element + final int id = currentlistId++; + + /// The id of the mapping in the database. + int? mappingId; + + /// The id of the corresponding course. + int? courseId; + + /// TextEditingController to keep track of user input. + TextEditingController courseController = TextEditingController(); + + /// The vintage of the mapping. + Vintage? vintage; + + /// TextEditingController to keep track of user input. + TextEditingController vintageController = TextEditingController(); + + /// Whether or not the mapping is valid for submitting. + bool isSubmitable() { + return courseId != null && vintage != null; + } +} diff --git a/lib/src/slots/presentation/widgets/number_spinner.dart b/lib/src/slots/presentation/widgets/number_spinner.dart index b54199ab..d14e5b52 100644 --- a/lib/src/slots/presentation/widgets/number_spinner.dart +++ b/lib/src/slots/presentation/widgets/number_spinner.dart @@ -51,6 +51,7 @@ class NumberSpinner extends StatefulWidget { class _NumberSpinnerState extends State> { T _value = 0 as T; final TextEditingController controller = TextEditingController(); + TextSelection? selection; set value(T value) { _value = value; @@ -74,6 +75,7 @@ class _NumberSpinnerState extends State> { value = widget.max!; } + selection = controller.selection; controller.text = value.toString(); }); } @@ -86,6 +88,7 @@ class _NumberSpinnerState extends State> { value = widget.min!; } + selection = controller.selection; controller.text = value.toString(); }); } @@ -93,6 +96,14 @@ class _NumberSpinnerState extends State> { @override Widget build(BuildContext context) { controller.text = value.toString(); + // TODO(mastermarcohd): when the length of the input text increases in length through the increment button it takes on the wrong selection + if (selection != null) { + if (selection!.start <= controller.text.length && selection!.end <= controller.text.length) { + controller.selection = selection!; + } else { + controller.selection = TextSelection.collapsed(offset: controller.text.length); + } + } final enabled = widget.enabled ?? true; @@ -135,6 +146,8 @@ class _NumberSpinnerState extends State> { return; } + selection = controller.selection; + setState(() { value = number as T; }); diff --git a/lib/src/slots/presentation/widgets/slot_master_widget.dart b/lib/src/slots/presentation/widgets/slot_master_widget.dart index 11609d7c..1daabb93 100644 --- a/lib/src/slots/presentation/widgets/slot_master_widget.dart +++ b/lib/src/slots/presentation/widgets/slot_master_widget.dart @@ -52,6 +52,13 @@ class _SlotMasterWidgetState extends State { } } + void duplicateSlot() { + showAnimatedDialog( + context: context, + pageBuilder: (_, __, ___) => EditSlotDialog(slot: widget.slot, duplicate: true), + ); + } + void editSlot() { showAnimatedDialog( context: context, @@ -164,11 +171,16 @@ class _SlotMasterWidgetState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( - onPressed: deleteSlot, + onPressed: isDeleting ? null : deleteSlot, + style: TextButton.styleFrom(foregroundColor: context.theme.colorScheme.error), child: Text(context.t.global_delete), ), TextButton( - onPressed: editSlot, + onPressed: isDeleting ? null : duplicateSlot, + child: Text(context.t.global_duplicate), + ), + TextButton( + onPressed: isDeleting ? null : editSlot, child: Text(context.t.global_edit), ), ], diff --git a/lib/src/statistics/domain/models/chart_value.freezed.dart b/lib/src/statistics/domain/models/chart_value.freezed.dart index edfddfa1..77f9c769 100644 --- a/lib/src/statistics/domain/models/chart_value.freezed.dart +++ b/lib/src/statistics/domain/models/chart_value.freezed.dart @@ -28,9 +28,7 @@ mixin _$ChartValue { /// The color of the value. Color get color => throw _privateConstructorUsedError; - /// Create a copy of ChartValue - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) $ChartValueCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -54,8 +52,6 @@ class _$ChartValueCopyWithImpl<$Res, $Val extends ChartValue> // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of ChartValue - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -104,8 +100,6 @@ class __$$ChartValueImplCopyWithImpl<$Res> _$ChartValueImpl _value, $Res Function(_$ChartValueImpl) _then) : super(_value, _then); - /// Create a copy of ChartValue - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -181,9 +175,7 @@ class _$ChartValueImpl extends _ChartValue { @override int get hashCode => Object.hash(runtimeType, name, value, percentage, color); - /// Create a copy of ChartValue - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$ChartValueImplCopyWith<_$ChartValueImpl> get copyWith => @@ -198,26 +190,24 @@ abstract class _ChartValue extends ChartValue { required final Color color}) = _$ChartValueImpl; const _ChartValue._() : super._(); - /// The name of the value. @override + + /// The name of the value. String get name; + @override /// The value itself. - @override double get value; + @override /// The percentage of the value compared to the total. - @override double get percentage; + @override /// The color of the value. - @override Color get color; - - /// Create a copy of ChartValue - /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) _$$ChartValueImplCopyWith<_$ChartValueImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/statistics/domain/models/status_aggregate.freezed.dart b/lib/src/statistics/domain/models/status_aggregate.freezed.dart index 29d7db17..6c39e307 100644 --- a/lib/src/statistics/domain/models/status_aggregate.freezed.dart +++ b/lib/src/statistics/domain/models/status_aggregate.freezed.dart @@ -32,12 +32,8 @@ mixin _$StatusAggregate { /// The number of tasks with [MoodleTaskStatus.late]. int get late => throw _privateConstructorUsedError; - /// Serializes this StatusAggregate to a JSON map. Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of StatusAggregate - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) $StatusAggregateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -61,8 +57,6 @@ class _$StatusAggregateCopyWithImpl<$Res, $Val extends StatusAggregate> // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of StatusAggregate - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -111,8 +105,6 @@ class __$$StatusAggregateImplCopyWithImpl<$Res> _$StatusAggregateImpl _value, $Res Function(_$StatusAggregateImpl) _then) : super(_value, _then); - /// Create a copy of StatusAggregate - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -188,13 +180,11 @@ class _$StatusAggregateImpl extends _StatusAggregate { (identical(other.late, late) || other.late == late)); } - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override int get hashCode => Object.hash(runtimeType, done, pending, uploaded, late); - /// Create a copy of StatusAggregate - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$StatusAggregateImplCopyWith<_$StatusAggregateImpl> get copyWith => @@ -220,26 +210,24 @@ abstract class _StatusAggregate extends StatusAggregate { factory _StatusAggregate.fromJson(Map json) = _$StatusAggregateImpl.fromJson; - /// The number of tasks with [MoodleTaskStatus.done]. @override + + /// The number of tasks with [MoodleTaskStatus.done]. int get done; + @override /// The number of tasks with [MoodleTaskStatus.pending]. - @override int get pending; + @override /// The number of tasks with [MoodleTaskStatus.uploaded]. - @override int get uploaded; + @override /// The number of tasks with [MoodleTaskStatus.late]. - @override int get late; - - /// Create a copy of StatusAggregate - /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) _$$StatusAggregateImplCopyWith<_$StatusAggregateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/statistics/domain/models/task_aggregate.freezed.dart b/lib/src/statistics/domain/models/task_aggregate.freezed.dart index 06439e6c..4f7e6513 100644 --- a/lib/src/statistics/domain/models/task_aggregate.freezed.dart +++ b/lib/src/statistics/domain/models/task_aggregate.freezed.dart @@ -26,12 +26,8 @@ mixin _$TaskAggregate { /// Aggregation by type. TypeAggregate get type => throw _privateConstructorUsedError; - /// Serializes this TaskAggregate to a JSON map. Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of TaskAggregate - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) $TaskAggregateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -58,8 +54,6 @@ class _$TaskAggregateCopyWithImpl<$Res, $Val extends TaskAggregate> // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of TaskAggregate - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -78,8 +72,6 @@ class _$TaskAggregateCopyWithImpl<$Res, $Val extends TaskAggregate> ) as $Val); } - /// Create a copy of TaskAggregate - /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $StatusAggregateCopyWith<$Res> get status { @@ -88,8 +80,6 @@ class _$TaskAggregateCopyWithImpl<$Res, $Val extends TaskAggregate> }); } - /// Create a copy of TaskAggregate - /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $TypeAggregateCopyWith<$Res> get type { @@ -123,8 +113,6 @@ class __$$TaskAggregateImplCopyWithImpl<$Res> _$TaskAggregateImpl _value, $Res Function(_$TaskAggregateImpl) _then) : super(_value, _then); - /// Create a copy of TaskAggregate - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -175,13 +163,11 @@ class _$TaskAggregateImpl extends _TaskAggregate { (identical(other.type, type) || other.type == type)); } - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override int get hashCode => Object.hash(runtimeType, status, type); - /// Create a copy of TaskAggregate - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$TaskAggregateImplCopyWith<_$TaskAggregateImpl> get copyWith => @@ -204,18 +190,16 @@ abstract class _TaskAggregate extends TaskAggregate { factory _TaskAggregate.fromJson(Map json) = _$TaskAggregateImpl.fromJson; - /// Aggregation by status. @override + + /// Aggregation by status. StatusAggregate get status; + @override /// Aggregation by type. - @override TypeAggregate get type; - - /// Create a copy of TaskAggregate - /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) _$$TaskAggregateImplCopyWith<_$TaskAggregateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/statistics/domain/models/task_aggregate.g.dart b/lib/src/statistics/domain/models/task_aggregate.g.dart index cb7ddad8..6e4509d6 100644 --- a/lib/src/statistics/domain/models/task_aggregate.g.dart +++ b/lib/src/statistics/domain/models/task_aggregate.g.dart @@ -14,6 +14,6 @@ _$TaskAggregateImpl _$$TaskAggregateImplFromJson(Map json) => Map _$$TaskAggregateImplToJson(_$TaskAggregateImpl instance) => { - 'status': instance.status.toJson(), - 'type': instance.type.toJson(), + 'status': instance.status, + 'type': instance.type, }; diff --git a/lib/src/statistics/domain/models/type_aggregate.freezed.dart b/lib/src/statistics/domain/models/type_aggregate.freezed.dart index 40c11ef4..e2e267b5 100644 --- a/lib/src/statistics/domain/models/type_aggregate.freezed.dart +++ b/lib/src/statistics/domain/models/type_aggregate.freezed.dart @@ -35,12 +35,8 @@ mixin _$TypeAggregate { /// The number of tasks with [MoodleTaskType.none]. int get none => throw _privateConstructorUsedError; - /// Serializes this TypeAggregate to a JSON map. Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of TypeAggregate - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) $TypeAggregateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -64,8 +60,6 @@ class _$TypeAggregateCopyWithImpl<$Res, $Val extends TypeAggregate> // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of TypeAggregate - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -119,8 +113,6 @@ class __$$TypeAggregateImplCopyWithImpl<$Res> _$TypeAggregateImpl _value, $Res Function(_$TypeAggregateImpl) _then) : super(_value, _then); - /// Create a copy of TypeAggregate - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -209,14 +201,12 @@ class _$TypeAggregateImpl extends _TypeAggregate { (identical(other.none, none) || other.none == none)); } - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override int get hashCode => Object.hash(runtimeType, required, optional, compensation, exam, none); - /// Create a copy of TypeAggregate - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$TypeAggregateImplCopyWith<_$TypeAggregateImpl> get copyWith => @@ -242,30 +232,28 @@ abstract class _TypeAggregate extends TypeAggregate { factory _TypeAggregate.fromJson(Map json) = _$TypeAggregateImpl.fromJson; - /// The number of tasks with [MoodleTaskType.required]. @override + + /// The number of tasks with [MoodleTaskType.required]. int get required; + @override /// The number of tasks with [MoodleTaskType.optional]. - @override int get optional; + @override /// The number of tasks with [MoodleTaskType.compensation]. - @override int get compensation; + @override /// The number of tasks with [MoodleTaskType.exam]. - @override int get exam; + @override /// The number of tasks with [MoodleTaskType.none]. - @override int get none; - - /// Create a copy of TypeAggregate - /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) _$$TypeAggregateImplCopyWith<_$TypeAggregateImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/theming/domain/models/theme_base.freezed.dart b/lib/src/theming/domain/models/theme_base.freezed.dart index 82be0f87..33055cb2 100644 --- a/lib/src/theming/domain/models/theme_base.freezed.dart +++ b/lib/src/theming/domain/models/theme_base.freezed.dart @@ -58,9 +58,7 @@ mixin _$ThemeBase { /// Whether the theme uses Material 3. bool get usesMaterial3 => throw _privateConstructorUsedError; - /// Create a copy of ThemeBase - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) $ThemeBaseCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -97,8 +95,6 @@ class _$ThemeBaseCopyWithImpl<$Res, $Val extends ThemeBase> // ignore: unused_field final $Res Function($Val) _then; - /// Create a copy of ThemeBase - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -211,8 +207,6 @@ class __$$ThemeBaseImplCopyWithImpl<$Res> _$ThemeBaseImpl _value, $Res Function(_$ThemeBaseImpl) _then) : super(_value, _then); - /// Create a copy of ThemeBase - /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -446,9 +440,7 @@ class _$ThemeBaseImpl extends _ThemeBase with DiagnosticableTreeMixin { brightness, usesMaterial3); - /// Create a copy of ThemeBase - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') _$$ThemeBaseImplCopyWith<_$ThemeBaseImpl> get copyWith => @@ -473,66 +465,64 @@ abstract class _ThemeBase extends ThemeBase { final bool usesMaterial3}) = _$ThemeBaseImpl; _ThemeBase._() : super._(); - /// The color to use for the surface of components. @override + + /// The color to use for the surface of components. Color get primaryColor; + @override /// The color to use for the background of the app. - @override Color get secondaryColor; + @override /// The color to use for separators and dividers. - @override Color get tertiaryColor; + @override /// The color to use for buttons and other interactive elements. - @override Color get accentColor; + @override /// The color to use for text on top of the primary color. - @override Color get onAccentColor; + @override /// The color to use to indicate errors. - @override Color get errorColor; + @override /// The color to use for modules that are completed. - @override Color get moduleDoneColor; + @override /// The color to use for modules that are pending. - @override Color get modulePendingColor; + @override /// The color to use for modules that have been uploaded. - @override Color get moduleUploadedColor; + @override /// The color to use for text. - @override Color get textColor; + @override /// The name of the theme. - @override String get name; + @override /// The icon of the theme. - @override IconData get icon; + @override /// The brightness of the theme. - @override Brightness get brightness; + @override /// Whether the theme uses Material 3. - @override bool get usesMaterial3; - - /// Create a copy of ThemeBase - /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(ignore: true) _$$ThemeBaseImplCopyWith<_$ThemeBaseImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/theming/domain/services/theme_generator_service.dart b/lib/src/theming/domain/services/theme_generator_service.dart index 3e814d87..305e86ca 100644 --- a/lib/src/theming/domain/services/theme_generator_service.dart +++ b/lib/src/theming/domain/services/theme_generator_service.dart @@ -1,6 +1,6 @@ import 'package:eduplanner/src/theming/theming.dart'; import 'package:figma_squircle/figma_squircle.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Theme; import 'package:mcquenji_core/mcquenji_core.dart'; /// A service that generates a theme of type [Theme] based on a [ThemeBase]. diff --git a/lib/src/theming/infra/services/material_theme_generator_service.dart b/lib/src/theming/infra/services/material_theme_generator_service.dart index b1cc8efd..063c59f2 100644 --- a/lib/src/theming/infra/services/material_theme_generator_service.dart +++ b/lib/src/theming/infra/services/material_theme_generator_service.dart @@ -43,7 +43,7 @@ class MaterialThemeGeneratorService extends ThemeGeneratorService { borderSide: BorderSide.none, ), ), - cardTheme: CardTheme( + cardTheme: CardThemeData( color: themeBase.primaryColor, elevation: 6, margin: EdgeInsets.zero, @@ -69,6 +69,16 @@ class MaterialThemeGeneratorService extends ThemeGeneratorService { checkColor: WidgetStateProperty.all(themeBase.onAccentColor), side: BorderSide(color: themeBase.accentColor, width: 2), ), + tabBarTheme: TabBarThemeData( + labelColor: themeBase.onAccentColor, + unselectedLabelColor: themeBase.textColor, + indicator: BoxDecoration( + color: themeBase.accentColor, + borderRadius: squircle(radius: 6).borderRadius, + ), + splashBorderRadius: squircle(radius: 6).borderRadius, + labelStyle: textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600), + ), elevatedButtonTheme: ElevatedButtonThemeData( style: ButtonStyle( backgroundColor: WidgetStateProperty.resolveWith((states) { diff --git a/pubspec.lock b/pubspec.lock index 07ced377..fb69a662 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,23 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "76.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "6.4.1" analyzer_plugin: dependency: transitive description: @@ -66,26 +61,26 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" auto_injector: dependency: transitive description: name: auto_injector - sha256: ad7a95d7c381363d48b54e00cb680f024fd97009067244454e9b4850337608e8 + sha256: "1fc2624898e92485122eb2b1698dd42511d7ff6574f84a3a8606fc4549a1e8f8" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" awesome_extensions: dependency: "direct main" description: name: awesome_extensions - sha256: "9b1693e986e4045141add298fa2d7f9aa6cdd3c125b951e2cde739a5058ed879" + sha256: d61c85a583c753e106fcbff392c705c8cd72f6fcacc86ddd1bdcc0a6f498efb3 url: "https://pub.dev" source: hosted - version: "2.0.21" + version: "2.0.25" bloc: dependency: "direct main" description: @@ -114,10 +109,10 @@ packages: dependency: transitive description: name: build - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.1" build_config: dependency: transitive description: @@ -138,26 +133,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.15" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "7.3.2" built_collection: dependency: transitive description: @@ -170,18 +165,18 @@ packages: dependency: transitive description: name: built_value - sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 + sha256: ba95c961bafcd8686d1cf63be864eb59447e795e124d98d6a27d91fcd13602fb url: "https://pub.dev" source: hosted - version: "8.9.5" + version: "8.11.1" carousel_slider: dependency: "direct main" description: name: carousel_slider - sha256: "7b006ec356205054af5beaef62e2221160ea36b90fb70a35e4deacd49d0349ae" + sha256: bcc61735345c9ab5cb81073896579e735f81e35fd588907a393143ea986be8ff url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.1.1" characters: dependency: transitive description: @@ -202,10 +197,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" circular_buffer: dependency: transitive description: @@ -214,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.12.0" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" clock: dependency: transitive description: @@ -266,10 +269,10 @@ packages: dependency: transitive description: name: coverage - sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.15.0" crypto: dependency: "direct main" description: @@ -290,10 +293,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "2.3.6" data_widget: dependency: "direct main" description: @@ -306,10 +309,10 @@ packages: dependency: transitive description: name: device_frame - sha256: d031a06f5d6f4750009672db98a5aa1536aa4a231713852469ce394779a23d75 + sha256: a58796a9a2efc0fd8a7903cee0eed2e2d111f4a7d81fa2319ab89430b020f624 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" device_preview: dependency: "direct main" description: @@ -338,10 +341,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 url: "https://pub.dev" source: hosted - version: "5.8.0+1" + version: "5.9.0" dio_web_adapter: dependency: transitive description: @@ -350,32 +353,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" - dotenv: - dependency: transitive - description: - name: dotenv - sha256: "379e64b6fc82d3df29461d349a1796ecd2c436c480d4653f3af6872eccbc90e1" - url: "https://pub.dev" - source: hosted - version: "4.2.0" - echidna_dto: - dependency: transitive - description: - path: "." - ref: HEAD - resolved-ref: "259f5889208f5a73c9bb3dfe45091dc3a80f91be" - url: "https://github.com/necodeIT/echidna_dto.git" - source: git - version: "0.0.1" - echidna_flutter: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "7421eedd6009324e60eb529c988fcc7c16e85398" - url: "https://github.com/necodeIT/echidna_flutter.git" - source: git - version: "0.0.2" either_dart: dependency: "direct main" description: @@ -396,10 +373,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -488,19 +465,19 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5 + sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27" url: "https://pub.dev" source: hosted - version: "0.7.6+2" + version: "0.7.7+1" flutter_modular: dependency: "direct main" description: path: flutter_modular ref: HEAD - resolved-ref: "7c9f208f89e4b8fa31486ae75ceac493b93bdf08" + resolved-ref: f53f7c9c99d8ae1674f5cb3216dcbdb9406093c5 url: "https://github.com/necodeIT/modular.git" source: git - version: "6.3.3" + version: "6.3.4" flutter_shaders: dependency: transitive description: @@ -529,18 +506,18 @@ packages: dependency: "direct main" description: name: flutter_sticky_header - sha256: "7f76d24d119424ca0c95c146b8627a457e8de8169b0d584f766c2c545db8f8be" + sha256: fb4fda6164ef3e5fc7ab73aba34aad253c17b7c6ecf738fa26f1a905b7d2d1e2 url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.8.0" flutter_svg: dependency: "direct main" description: name: flutter_svg - sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845 url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.2.0" flutter_test: dependency: "direct dev" description: flutter @@ -580,18 +557,18 @@ packages: dependency: "direct main" description: name: font_awesome_flutter - sha256: d3a89184101baec7f4600d58840a764d2ef760fe1c5a20ef9e6b0e9b24a07a3a + sha256: b738e35f8bb4957896c34957baf922f99c5d415b38ddc8b070d14b7fa95715d4 url: "https://pub.dev" source: hosted - version: "10.8.0" + version: "10.9.1" freezed: dependency: "direct dev" description: name: freezed - sha256: "62b248b2dfb06ded10c84b713215b25aea020a5b08c32e801a974361557ebc3f" + sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 url: "https://pub.dev" source: hosted - version: "3.0.0-0.0.dev" + version: "2.5.2" freezed_annotation: dependency: "direct main" description: @@ -628,18 +605,18 @@ packages: dependency: transitive description: name: google_identity_services_web - sha256: "55580f436822d64c8ff9a77e37d61f5fb1e6c7ec9d632a43ee324e2a05c3c6c9" + sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.3.3+1" googleapis_auth: dependency: transitive description: name: googleapis_auth - sha256: befd71383a955535060acde8792e7efc11d2fccd03dd1d3ec434e85b68775938 + sha256: b81fe352cc4a330b3710d2b7ad258d9bcef6f909bb759b306bf42973a7d046db url: "https://pub.dev" source: hosted - version: "1.6.0" + version: "2.0.0" graphs: dependency: transitive description: @@ -652,26 +629,26 @@ packages: dependency: transitive description: name: grpc - sha256: "5b99b7a420937d4361ece68b798c9af8e04b5bc128a7859f2a4be87427694813" + sha256: "2dde469ddd8bbd7a33a0765da417abe1ad2142813efce3a86c512041294e2b26" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.1.0" html: dependency: transitive description: name: html - sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" url: "https://pub.dev" source: hosted - version: "0.15.5" + version: "0.15.6" http: dependency: transitive description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" http2: dependency: transitive description: @@ -708,10 +685,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: @@ -740,34 +717,34 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: "81f04dee10969f89f604e1249382d46b97a1ccad53872875369622b5bfc9e58a" + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b url: "https://pub.dev" source: hosted - version: "6.9.4" + version: "6.8.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "11.0.1" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -784,14 +761,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" markdown: dependency: transitive description: @@ -872,7 +841,7 @@ packages: description: path: modular_core ref: HEAD - resolved-ref: "7c9f208f89e4b8fa31486ae75ceac493b93bdf08" + resolved-ref: f53f7c9c99d8ae1674f5cb3216dcbdb9406093c5 url: "https://github.com/necodeIT/modular.git" source: git version: "3.3.0" @@ -912,18 +881,18 @@ packages: dependency: transitive description: name: package_info_plus - sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" url: "https://pub.dev" source: hosted - version: "8.3.0" + version: "8.3.1" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" path: dependency: "direct main" description: @@ -952,18 +921,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db" url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.2.18" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" path_provider_linux: dependency: transitive description: @@ -992,10 +961,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "7.0.1" platform: dependency: transitive description: @@ -1042,10 +1011,10 @@ packages: dependency: transitive description: name: process - sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 url: "https://pub.dev" source: hosted - version: "5.0.3" + version: "5.0.5" properties: dependency: transitive description: @@ -1066,10 +1035,10 @@ packages: dependency: transitive description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5+1" pub_semver: dependency: transitive description: @@ -1090,10 +1059,10 @@ packages: dependency: transitive description: name: result_dart - sha256: "3c69c864a08df0f413a86be211d07405e9a53cc1ac111e3cc8365845a0fb5288" + sha256: "0666b21fbdf697b3bdd9986348a380aa204b3ebe7c146d8e4cdaa7ce735e6054" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "2.1.1" rxdart: dependency: transitive description: @@ -1146,10 +1115,10 @@ packages: dependency: transitive description: name: sentry - sha256: "3a64dd001bff768ce5ab6fc3608deef4dde22acd4b5d947763557b20db9e2a32" + sha256: "599701ca0693a74da361bc780b0752e1abc98226cf5095f6b069648116c896bb" url: "https://pub.dev" source: hosted - version: "8.14.0" + version: "8.14.2" sentry_dart_plugin: dependency: "direct dev" description: @@ -1162,34 +1131,34 @@ packages: dependency: "direct main" description: name: sentry_dio - sha256: f810a71b36e0e0a3405baf7f3eeaa8481ca55bb65a5822f0befc5bdda0049264 + sha256: "9ad805892ff8db0dc15c4992ae11c118565cd087a65ba4a36211b93b27430ee4" url: "https://pub.dev" source: hosted - version: "8.14.0" + version: "8.14.2" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: "3d361f2d5f805783e2e4ed1bd475ef126b36cf525b359dc3627a765a3fb7424d" + sha256: "5ba2cf40646a77d113b37a07bd69f61bb3ec8a73cbabe5537b05a7c89d2656f8" url: "https://pub.dev" source: hosted - version: "8.14.0" + version: "8.14.2" shared_preferences: dependency: transitive description: name: shared_preferences - sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad" + sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74 url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.12" shared_preferences_foundation: dependency: transitive description: @@ -1258,18 +1227,18 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "2.0.1" skeletonizer: dependency: "direct main" description: name: skeletonizer - sha256: "0dcacc51c144af4edaf37672072156f49e47036becbc394d7c51850c5c1e884b" + sha256: eebc03dc86b298e2d7f61e0ebce5713e9dbbc3e786f825909b4591756f196eb6 url: "https://pub.dev" source: hosted - version: "1.4.3" + version: "2.1.0+1" sky_engine: dependency: transitive description: flutter @@ -1287,10 +1256,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.5.0" source_helper: dependency: transitive description: @@ -1391,34 +1360,34 @@ packages: dependency: "direct dev" description: name: test - sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" + sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" url: "https://pub.dev" source: hosted - version: "1.25.15" + version: "1.26.2" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" test_core: dependency: transitive description: name: test_core - sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" + sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" url: "https://pub.dev" source: hosted - version: "0.6.8" + version: "0.6.11" timeago: dependency: "direct main" description: name: timeago - sha256: "054cedf68706bb142839ba0ae6b135f6b68039f0b8301cbe8784ae653d5ff8de" + sha256: b05159406a97e1cbb2b9ee4faa9fb096fe0e2dfcd8b08fcd2a00553450d3422e url: "https://pub.dev" source: hosted - version: "3.7.0" + version: "3.7.1" timing: dependency: transitive description: @@ -1463,26 +1432,26 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4" + sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7" url: "https://pub.dev" source: hosted - version: "6.3.15" + version: "6.3.18" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.4" url_launcher_linux: dependency: transitive description: @@ -1495,10 +1464,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.2.3" url_launcher_platform_interface: dependency: transitive description: @@ -1511,10 +1480,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" url_launcher_windows: dependency: transitive description: @@ -1535,18 +1504,18 @@ packages: dependency: transitive description: name: value_layout_builder - sha256: c02511ea91ca5c643b514a33a38fa52536f74aa939ec367d02938b5ede6807fa + sha256: ab4b7d98bac8cefeb9713154d43ee0477490183f5aa23bb4ffa5103d9bbf6275 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.5.0" vector_graphics: dependency: "direct main" description: name: vector_graphics - sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 url: "https://pub.dev" source: hosted - version: "1.1.18" + version: "1.1.19" vector_graphics_codec: dependency: transitive description: @@ -1559,34 +1528,34 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc url: "https://pub.dev" source: hosted - version: "1.1.16" + version: "1.1.19" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" watcher: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.3" web: dependency: transitive description: @@ -1599,18 +1568,18 @@ packages: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" webkit_inspection_protocol: dependency: transitive description: @@ -1623,10 +1592,10 @@ packages: dependency: transitive description: name: win32 - sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" url: "https://pub.dev" source: hosted - version: "5.12.0" + version: "5.14.0" window_manager: dependency: "direct main" description: @@ -1647,10 +1616,10 @@ packages: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.6.1" yaml: dependency: transitive description: @@ -1660,5 +1629,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.0 <4.0.0" - flutter: ">=3.27.0" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index fea60b13..3a190fcf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: animations: ^2.0.11 - awesome_extensions: ^2.0.17 + awesome_extensions: ^2.0.25 bloc: ^8.1.4 carousel_slider: ^5.0.0 collection: any @@ -21,9 +21,9 @@ dependencies: device_preview: ^1.2.0 diffutil_dart: ^4.0.1 dio: ^5.7.0 - echidna_flutter: - git: - url: https://github.com/necodeIT/echidna_flutter.git + # echidna_flutter: + # git: + # url: https://github.com/necodeIT/echidna_flutter.git either_dart: ^1.0.0 figma_squircle: ^0.5.3 fl_chart: ^0.70.2 @@ -38,7 +38,7 @@ dependencies: flutter_modular: ^6.3.4 flutter_single_instance: ^1.2.0 flutter_staggered_animations: ^1.1.1 - flutter_sticky_header: ^0.7.0 + flutter_sticky_header: ^0.8.0 flutter_svg: ^2.0.10+1 flutter_utils: git: @@ -65,7 +65,7 @@ dependencies: url: https://github.com/necodeIT/posthog_dart.git sentry_dio: ^8.13.0 sentry_flutter: ^8.9.0 - skeletonizer: ^1.4.2 + skeletonizer: ^2.1.0+1 sliver_tools: ^0.2.12 sprung: ^3.0.1 timeago: ^3.7.0 @@ -82,8 +82,8 @@ dev_dependencies: flutter_test: sdk: flutter flutterando_analysis: ^0.0.2 - freezed: ^3.0.0-0.0.dev - json_serializable: ^6.9.4 + freezed: any + json_serializable: any sentry_dart_plugin: ^2.1.0 test: ^1.25.2 diff --git a/test/kanban_module_test.dart b/test/kanban_module_test.dart new file mode 100644 index 00000000..7965db9e --- /dev/null +++ b/test/kanban_module_test.dart @@ -0,0 +1,18 @@ +import 'package:eduplanner/src/kanban/kanban.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:logging/logging.dart'; + +Future main() async { + Logger.root.level = Level.ALL; + + setUp(() { + Modular.init(KanbanModule()); + }); + + tearDown(() { + Modular.destroy(); + }); + + // Your unit tests here. +}