From 352c4f7c0dd289b4f3589864c98d7df0826ac7e7 Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Tue, 15 Jul 2025 15:49:28 +0200 Subject: [PATCH 01/22] chore: update slotediting translations --- l10n/de.arb | 4 ++-- l10n/en.arb | 4 ++-- lib/gen/l10n/app_localizations.dart | 4 ++-- lib/gen/l10n/app_localizations_de.dart | 4 ++-- lib/gen/l10n/app_localizations_en.dart | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/l10n/de.arb b/l10n/de.arb index 716c2b06..7873c6ec 100644 --- a/l10n/de.arb +++ b/l10n/de.arb @@ -595,7 +595,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 +651,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." }, diff --git a/l10n/en.arb b/l10n/en.arb index d37c9360..0aab37f3 100644 --- a/l10n/en.arb +++ b/l10n/en.arb @@ -597,7 +597,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 +653,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." }, diff --git a/lib/gen/l10n/app_localizations.dart b/lib/gen/l10n/app_localizations.dart index 1ce009e8..5b8aa568 100644 --- a/lib/gen/l10n/app_localizations.dart +++ b/lib/gen/l10n/app_localizations.dart @@ -860,7 +860,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. @@ -920,7 +920,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. diff --git a/lib/gen/l10n/app_localizations_de.dart b/lib/gen/l10n/app_localizations_de.dart index c8e9a203..70c84ae3 100644 --- a/lib/gen/l10n/app_localizations_de.dart +++ b/lib/gen/l10n/app_localizations_de.dart @@ -438,7 +438,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String slots_details_sizeCount(int count) { - return 'Größe $count'; + return 'Schüler: $count'; } @override @@ -471,7 +471,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'; diff --git a/lib/gen/l10n/app_localizations_en.dart b/lib/gen/l10n/app_localizations_en.dart index ece717f4..1d22fe7b 100644 --- a/lib/gen/l10n/app_localizations_en.dart +++ b/lib/gen/l10n/app_localizations_en.dart @@ -438,7 +438,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String slots_details_sizeCount(int count) { - return 'Size $count'; + return 'Students: $count'; } @override @@ -471,7 +471,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'; From 922d4bd88bd9c9990b5992e03bdf94a843286719 Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Tue, 15 Jul 2025 15:51:10 +0200 Subject: [PATCH 02/22] fix: improve numberspinner input --- .../slots/presentation/widgets/number_spinner.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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; }); From dafe220aa2ea0dd6a665477da0672a392c6081cc Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Tue, 15 Jul 2025 16:56:43 +0200 Subject: [PATCH 03/22] feat: implement first draft of slotmasterscreen restructure --- .../screens/slot_master_screen.dart | 100 +++++++++++------- .../widgets/edit_slot_dialog.dart | 15 +++ 2 files changed, 76 insertions(+), 39 deletions(-) diff --git a/lib/src/slots/presentation/screens/slot_master_screen.dart b/lib/src/slots/presentation/screens/slot_master_screen.dart index 2d9ede31..21195b6f 100644 --- a/lib/src/slots/presentation/screens/slot_master_screen.dart +++ b/lib/src/slots/presentation/screens/slot_master_screen.dart @@ -16,6 +16,8 @@ class SlotMasterScreen extends StatefulWidget { class _SlotMasterScreenState extends State with AdaptiveState, NoMobile { final searchController = TextEditingController(); + Weekday activeDay = Weekday.monday; + @override void initState() { super.initState(); @@ -50,46 +52,66 @@ class _SlotMasterScreenState extends State with AdaptiveState, return Padding( padding: PaddingAll(), - child: SingleChildScrollView( - child: Column( - spacing: Spacing.largeSpacing, - children: [ - 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'), - ), - ], + child: Column( + children: [ + Row( + spacing: Spacing.mediumSpacing, + children: [ + for (final weekday in Weekday.values) + TextButton( + onPressed: weekday == activeDay + ? null + : () { + setState(() { + activeDay = weekday; + }); + }, + style: TextButton.styleFrom( + backgroundColor: weekday == activeDay ? context.theme.highlightColor : context.theme.cardColor, + ), + child: Text( + weekday.translate(context), + style: context.theme.textTheme.titleLarge, ), - 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(), - ], - ), - ], - ), + ), + ], + ), + Spacing.mediumVertical(), + // TODO(mastermarcohd): add Timeunit subdivisions + SingleChildScrollView( + child: Column( + spacing: Spacing.largeSpacing, + children: [ + // for (final weekday in Weekday.values) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextButton( + onPressed: () => createSlot(activeDay), + child: const Text('New slot'), // TODO(mastermarcohd): translate + ), + Spacing.smallVertical(), + if (groups[activeDay]?.isNotEmpty ?? false) + Wrap( + spacing: Spacing.mediumSpacing, + runSpacing: Spacing.mediumSpacing, + children: [ + // TODO(mastermarcohd): sort slots by time + for (final slot in (groups[activeDay] ?? []).query(searchController.text)) + SizedBox( + key: ValueKey(slot), + width: tileWidth, + height: tileHeight, + child: SlotMasterWidget(slot: slot), + ), + ].show(), + ).stretch(), + ], + ), + ], + ), + ), + ], ), ); } diff --git a/lib/src/slots/presentation/widgets/edit_slot_dialog.dart b/lib/src/slots/presentation/widgets/edit_slot_dialog.dart index ff9cc0cc..00ea185d 100644 --- a/lib/src/slots/presentation/widgets/edit_slot_dialog.dart +++ b/lib/src/slots/presentation/widgets/edit_slot_dialog.dart @@ -516,6 +516,8 @@ class _EditSlotDialogState extends State { ); }, ).expanded(), + + // TODO(mastermarcohd): make remove button IconButton( splashColor: Colors.transparent, highlightColor: Colors.transparent, @@ -561,6 +563,19 @@ class _EditSlotDialogState extends State { ), ].vSpaced(Spacing.smallSpacing), ).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); + }, + icon: const Icon(Icons.add), + ).stretch(), ], ).expanded(), ], From 4bba7bbaf68a4befad27889667ac9ced67c34fb9 Mon Sep 17 00:00:00 2001 From: mcquenji Date: Sun, 10 Aug 2025 07:18:12 +0200 Subject: [PATCH 04/22] fix: disable licensing --- lib/config/echidna.dart | 4 + lib/main.dart | 4 +- lib/src/app/app.dart | 4 +- .../app/presentation/widgets/title_bar.dart | 96 +++++++++---------- lib/src/auth/auth.dart | 7 +- .../repositories/echidna_user_repository.dart | 48 +++++----- pubspec.lock | 26 ----- pubspec.yaml | 6 +- 8 files changed, 84 insertions(+), 111 deletions(-) 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/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..5845c110 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'; @@ -77,7 +75,7 @@ class AppModule extends Module { 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/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/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/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/pubspec.lock b/pubspec.lock index 07ced377..777aad88 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -350,32 +350,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: diff --git a/pubspec.yaml b/pubspec.yaml index fea60b13..61b49c4d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 From de40c9c31c4356db67c57ed3ea5c65686963bd5e Mon Sep 17 00:00:00 2001 From: mcquenji Date: Sun, 10 Aug 2025 07:26:10 +0200 Subject: [PATCH 05/22] chore: bump flutter version --- .fvmrc | 2 +- .vscode/settings.json | 2 +- lib/gen/l10n/app_localizations.dart | 35 ++++++---- lib/gen/l10n/app_localizations_de.dart | 69 ++++++++++++------- lib/gen/l10n/app_localizations_en.dart | 60 ++++++++++------ .../widgets/general_settings.dart | 2 +- pubspec.lock | 24 +++---- 7 files changed, 121 insertions(+), 73 deletions(-) diff --git a/.fvmrc b/.fvmrc index 4cfa3d5f..3ca65ffc 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.29.0" + "flutter": "3.32.8" } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f14a2da4..eee92ae1 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.32.8", "dart.lineLength": 150, "conventionalCommits.scopes": [ "auth", diff --git a/lib/gen/l10n/app_localizations.dart b/lib/gen/l10n/app_localizations.dart index 5b8aa568..230ae44a 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, @@ -873,7 +876,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. /// @@ -1038,7 +1042,8 @@ abstract class AppLocalizations { String get global_disclaimer; } -class _AppLocalizationsDelegate extends LocalizationsDelegate { +class _AppLocalizationsDelegate + extends LocalizationsDelegate { const _AppLocalizationsDelegate(); @override @@ -1047,25 +1052,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 70c84ae3..a558d0ee 100644 --- a/lib/gen/l10n/app_localizations_de.dart +++ b/lib/gen/l10n/app_localizations_de.dart @@ -59,7 +59,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 +68,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 +104,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 +123,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 +139,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 +182,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 +213,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 +223,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 +238,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 +259,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 +335,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 +366,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 +380,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 +422,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'; @@ -445,12 +464,14 @@ class AppLocalizationsDe extends AppLocalizations { 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'; @@ -492,7 +513,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 +552,6 @@ 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! ❤️'; } diff --git a/lib/gen/l10n/app_localizations_en.dart b/lib/gen/l10n/app_localizations_en.dart index 1d22fe7b..f8450f92 100644 --- a/lib/gen/l10n/app_localizations_en.dart +++ b/lib/gen/l10n/app_localizations_en.dart @@ -59,7 +59,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 +68,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 +104,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 +123,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 +139,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 +182,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 +213,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 +237,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 +257,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 +333,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 +367,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 +377,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 => '.'; @@ -445,12 +460,14 @@ class AppLocalizationsEn extends AppLocalizations { 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'; @@ -492,7 +509,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 +539,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 +549,6 @@ 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! ❤️'; } diff --git a/lib/src/settings/presentation/widgets/general_settings.dart b/lib/src/settings/presentation/widgets/general_settings.dart index 0fb1fdb8..39635df7 100644 --- a/lib/src/settings/presentation/widgets/general_settings.dart +++ b/lib/src/settings/presentation/widgets/general_settings.dart @@ -102,7 +102,7 @@ class _GeneralSettingsState extends State with AdaptiveState { // iconItem( // title: context.t.settings_general_manageSubscription, // icon: FontAwesome5Solid.credit_card, - // onPressed: () => Modular.to.pushNamed('/subscription'), // TODO(mcquenji): Implement subscription screen + // onPressed: () => Modular.to.pushNamed('/subscription'), // iconSize: 14, // ), diff --git a/pubspec.lock b/pubspec.lock index 777aad88..902905be 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -66,10 +66,10 @@ 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: @@ -370,10 +370,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: @@ -682,10 +682,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: @@ -722,10 +722,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -1389,10 +1389,10 @@ packages: 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: @@ -1549,10 +1549,10 @@ packages: 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: From 78297eca3914dbe7a2d916d089018affad33b369 Mon Sep 17 00:00:00 2001 From: mcquenji Date: Sun, 10 Aug 2025 07:47:20 +0200 Subject: [PATCH 06/22] chore: bump deps --- .../material_theme_generator_service.dart | 2 +- pubspec.lock | 190 +++++++++--------- pubspec.yaml | 4 +- 3 files changed, 102 insertions(+), 94 deletions(-) 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..637979c0 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, diff --git a/pubspec.lock b/pubspec.lock index 902905be..24400da5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -74,18 +74,18 @@ packages: 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: "6e72049be9639599e5f943e4627c8cfe55740081aa5ea188875d746837a6a923" url: "https://pub.dev" source: hosted - version: "2.0.21" + version: "2.0.24" bloc: dependency: "direct main" description: @@ -114,10 +114,10 @@ packages: dependency: transitive description: name: build - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.4" build_config: dependency: transitive description: @@ -138,26 +138,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.5.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" url: "https://pub.dev" source: hosted - version: "2.4.15" + version: "2.5.4" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "9.1.2" built_collection: dependency: transitive description: @@ -170,18 +170,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 +202,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 +214,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 +274,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: @@ -306,10 +314,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 +346,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: @@ -462,19 +470,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: @@ -503,18 +511,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 @@ -554,10 +562,10 @@ packages: dependency: "direct main" description: name: font_awesome_flutter - sha256: d3a89184101baec7f4600d58840a764d2ef760fe1c5a20ef9e6b0e9b24a07a3a + sha256: f50ce90dbe26d977415b9540400d6778bef00894aced6358ae578abd92b14b10 url: "https://pub.dev" source: hosted - version: "10.8.0" + version: "10.9.0" freezed: dependency: "direct dev" description: @@ -602,18 +610,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: @@ -626,26 +634,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: @@ -714,10 +722,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: "81f04dee10969f89f604e1249382d46b97a1ccad53872875369622b5bfc9e58a" + sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c url: "https://pub.dev" source: hosted - version: "6.9.4" + version: "6.9.5" leak_tracker: dependency: transitive description: @@ -846,7 +854,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" @@ -926,10 +934,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -1016,10 +1024,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: @@ -1040,10 +1048,10 @@ packages: dependency: transitive description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5" pub_semver: dependency: transitive description: @@ -1064,10 +1072,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: @@ -1120,10 +1128,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: @@ -1136,34 +1144,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: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.11" shared_preferences_foundation: dependency: transitive description: @@ -1240,10 +1248,10 @@ packages: 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 @@ -1437,26 +1445,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: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656" url: "https://pub.dev" source: hosted - version: "6.3.15" + version: "6.3.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" url_launcher_linux: dependency: transitive description: @@ -1485,10 +1493,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: @@ -1509,18 +1517,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: @@ -1533,10 +1541,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331" url: "https://pub.dev" source: hosted - version: "1.1.16" + version: "1.1.17" vector_math: dependency: transitive description: @@ -1557,10 +1565,10 @@ packages: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" web: dependency: transitive description: @@ -1573,18 +1581,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: @@ -1597,10 +1605,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: @@ -1634,5 +1642,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.0 <4.0.0" - flutter: ">=3.27.0" + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.32.0" diff --git a/pubspec.yaml b/pubspec.yaml index 61b49c4d..7c0dc612 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 From dbf78e58f99b25040c3fb2666cb3cd9afc664910 Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Fri, 5 Sep 2025 00:57:24 +0200 Subject: [PATCH 07/22] feat: improve slotmaster UI --- .../slot_master_slots_repository.dart | 24 ++++++ .../screens/slot_master_screen.dart | 73 +++++++++++-------- 2 files changed, 67 insertions(+), 30 deletions(-) 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..33d9e8be 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,30 @@ 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 groupByDay = state.requireData.groupFoldBy>( + (s) => s.weekday, + (g, s) => [...?g, s], + ); + + final groupByStartUnit = >>{}; + + for (final day in groupByDay.entries) { + groupByStartUnit[day.key] = day.value.groupFoldBy>( + (s) => s.startUnit, + (g, s) => [...?g, s], + ); + } + + return groupByStartUnit; + } + @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 21195b6f..75100d0b 100644 --- a/lib/src/slots/presentation/screens/slot_master_screen.dart +++ b/lib/src/slots/presentation/screens/slot_master_screen.dart @@ -34,10 +34,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, + ), ); } @@ -48,7 +51,9 @@ class _SlotMasterScreenState extends State with AdaptiveState, Widget buildDesktop(BuildContext context) { final slots = context.watch(); - final groups = slots.group(); + final groups = slots.groupByStartUnit(); + + final activeGroup = groups[activeDay] ?? >{}; return Padding( padding: PaddingAll(), @@ -77,40 +82,48 @@ class _SlotMasterScreenState extends State with AdaptiveState, ], ), Spacing.mediumVertical(), - // TODO(mastermarcohd): add Timeunit subdivisions SingleChildScrollView( child: Column( spacing: Spacing.largeSpacing, children: [ - // for (final weekday in Weekday.values) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextButton( - onPressed: () => createSlot(activeDay), - child: const Text('New slot'), // TODO(mastermarcohd): translate - ), - Spacing.smallVertical(), - if (groups[activeDay]?.isNotEmpty ?? false) - Wrap( - spacing: Spacing.mediumSpacing, - runSpacing: Spacing.mediumSpacing, + for (final timeUnit in SlotTimeUnit.values) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - // TODO(mastermarcohd): sort slots by time - for (final slot in (groups[activeDay] ?? []).query(searchController.text)) - SizedBox( - key: ValueKey(slot), - width: tileWidth, - height: tileHeight, - child: SlotMasterWidget(slot: slot), - ), - ].show(), - ).stretch(), - ], - ), + Text( + timeUnit.humanReadable(), + style: context.theme.textTheme.titleMedium, + ), + Spacing.xsHorizontal(), + TextButton( + onPressed: () => createSlot(activeDay, timeUnit), + child: Text(context.t.slots_slotmaster_newSlot), + ), + Spacing.smallVertical(), + ], + ), + if (activeGroup[timeUnit]?.isNotEmpty ?? false) + Wrap( + spacing: Spacing.mediumSpacing, + runSpacing: Spacing.mediumSpacing, + children: [ + // TODO(mastermarcohd): sort slots by roomnr + for (final slot in (activeGroup[timeUnit] ?? []).query(searchController.text)) + SizedBox( + key: ValueKey(slot), + width: tileWidth, + height: tileHeight, + child: SlotMasterWidget(slot: slot), + ), + ].show(), + ).stretch(), + ], + ), ], ), - ), + ).expanded(), ], ), ); From 72dce04c7ec9d8f577dfb3c80f384ceb474be0e3 Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Fri, 5 Sep 2025 00:58:21 +0200 Subject: [PATCH 08/22] feat: make coursemappings editable --- .../widgets/edit_slot_dialog.dart | 299 +++++++++--------- 1 file changed, 150 insertions(+), 149 deletions(-) diff --git a/lib/src/slots/presentation/widgets/edit_slot_dialog.dart b/lib/src/slots/presentation/widgets/edit_slot_dialog.dart index 00ea185d..2ea5031e 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,11 +11,15 @@ 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}) + : 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; @@ -27,8 +30,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 +43,7 @@ class _EditSlotDialogState extends State { List supervisors = []; List mappings = []; + List courseMappings = []; bool submitting = false; @@ -55,12 +57,13 @@ class _EditSlotDialogState extends State { 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 (widget.slot != null) { roomController.text = widget.slot!.room; @@ -78,7 +81,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 +99,10 @@ class _EditSlotDialogState extends State { final repo = context.read(); + for (final element in courseMappings) { + addMapping(element); + } + final slot = widget.slot?.copyWith( room: roomController.text, startUnit: start!, @@ -179,36 +187,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 +210,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, @@ -456,126 +444,107 @@ 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(), - - // TODO(mastermarcohd): make remove button - 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.close), + ), + ], ), + Container( + padding: PaddingAll(Spacing.xsSpacing), + decoration: ShapeDecoration(shape: squircle(), color: context.theme.scaffoldBackgroundColor), + child: IconButton( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + hoverColor: Colors.transparent, + onPressed: () { + setState(() { + courseMappings.add(MappingElement()); + }); + }, + icon: const Icon(Icons.add), + ).stretch(), + ), ].vSpaced(Spacing.smallSpacing), ).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); - }, - icon: const Icon(Icons.add), - ).stretch(), ], ).expanded(), ], @@ -593,3 +562,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; + } +} From 35ccb45365bf1aa2605381a9a3d241ba118d8857 Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:01:04 +0200 Subject: [PATCH 09/22] feat: add duplicate button --- l10n/de.arb | 4 ++++ l10n/en.arb | 4 ++++ lib/gen/l10n/app_localizations.dart | 6 ++++++ lib/gen/l10n/app_localizations_de.dart | 3 +++ lib/gen/l10n/app_localizations_en.dart | 3 +++ .../slots/presentation/widgets/edit_slot_dialog.dart | 9 +++++++-- .../presentation/widgets/slot_master_widget.dart | 11 +++++++++++ 7 files changed, 38 insertions(+), 2 deletions(-) diff --git a/l10n/de.arb b/l10n/de.arb index 7873c6ec..3ec62fab 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." diff --git a/l10n/en.arb b/l10n/en.arb index 0aab37f3..c10e2a82 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." diff --git a/lib/gen/l10n/app_localizations.dart b/lib/gen/l10n/app_localizations.dart index 230ae44a..ac3e1084 100644 --- a/lib/gen/l10n/app_localizations.dart +++ b/lib/gen/l10n/app_localizations.dart @@ -170,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: diff --git a/lib/gen/l10n/app_localizations_de.dart b/lib/gen/l10n/app_localizations_de.dart index a558d0ee..de32841e 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'; diff --git a/lib/gen/l10n/app_localizations_en.dart b/lib/gen/l10n/app_localizations_en.dart index f8450f92..57f90b2e 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'; diff --git a/lib/src/slots/presentation/widgets/edit_slot_dialog.dart b/lib/src/slots/presentation/widgets/edit_slot_dialog.dart index 2ea5031e..e58da33b 100644 --- a/lib/src/slots/presentation/widgets/edit_slot_dialog.dart +++ b/lib/src/slots/presentation/widgets/edit_slot_dialog.dart @@ -11,7 +11,7 @@ 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, this.startUnit}) + 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. @@ -23,6 +23,9 @@ class EditSlotDialog extends StatefulWidget { /// The slot to edit. final Slot? slot; + /// Whether the slot is being duplicated. + final bool duplicate; + @override State createState() => _EditSlotDialogState(); } @@ -51,7 +54,7 @@ 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; @@ -127,6 +130,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); } diff --git a/lib/src/slots/presentation/widgets/slot_master_widget.dart b/lib/src/slots/presentation/widgets/slot_master_widget.dart index 11609d7c..554753bf 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, @@ -167,6 +174,10 @@ class _SlotMasterWidgetState extends State { onPressed: deleteSlot, child: Text(context.t.global_delete), ), + TextButton( + onPressed: duplicateSlot, + child: Text(context.t.global_duplicate), + ), TextButton( onPressed: editSlot, child: Text(context.t.global_edit), From 9f23a187f9170ae439f77464ed1b81456e5e1bf7 Mon Sep 17 00:00:00 2001 From: McQuenji <60017181+mcquenji@users.noreply.github.com> Date: Mon, 8 Sep 2025 18:44:20 +0200 Subject: [PATCH 10/22] feat(kanban): implement kanban board (#40) Co-authored-by: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .fvmrc | 2 +- .vscode/settings.json | 2 +- build.yaml | 10 +- l10n.yaml | 1 - l10n/de.arb | 66 ++++ l10n/en.arb | 66 ++++ lib/eduplanner.dart | 1 + lib/gen/l10n/app_localizations.dart | 112 +++++- lib/gen/l10n/app_localizations_de.dart | 52 ++- lib/gen/l10n/app_localizations_en.dart | 51 ++- lib/src/app/app.dart | 10 + lib/src/app/presentation/widgets/sidebar.dart | 13 +- .../presentation/widgets/sidebar_target.dart | 38 +- lib/src/app/utils/animate_utils.dart | 8 +- lib/src/auth/domain/models/token.freezed.dart | 26 +- lib/src/auth/domain/models/user.dart | 12 + lib/src/auth/domain/models/user.freezed.dart | 223 +++++++++-- lib/src/auth/domain/models/user.g.dart | 26 ++ .../datasources/moodle_user_datasource.dart | 6 +- .../repositories/user_repository.dart | 152 ++++++-- .../utils/kanban_column_converter_utils.dart | 22 ++ lib/src/auth/utils/utils.dart | 1 + .../domain/models/calendar_plan.freezed.dart | 30 +- .../domain/models/calendar_plan.g.dart | 4 +- .../domain/models/plan_deadline.freezed.dart | 28 +- .../domain/models/plan_invite.freezed.dart | 34 +- .../domain/models/plan_member.freezed.dart | 26 +- .../calendar_plan_repository.dart | 2 +- .../domain/datasources/datasources.dart | 1 + .../domain/datasources/kanban_datasource.dart | 19 + lib/src/kanban/domain/domain.dart | 3 + .../kanban/domain/models/kanban_board.dart | 61 +++ .../domain/models/kanban_board.freezed.dart | 254 +++++++++++++ .../kanban/domain/models/kanban_board.g.dart | 32 ++ lib/src/kanban/domain/models/models.dart | 1 + lib/src/kanban/domain/services/services.dart | 1 + .../kanban/infra/datasources/datasources.dart | 2 + .../datasources/local_kanban_datasource.dart | 49 +++ .../datasources/moodle_kanban_datasource.dart | 70 ++++ lib/src/kanban/infra/infra.dart | 2 + lib/src/kanban/infra/services/services.dart | 1 + lib/src/kanban/kanban.dart | 36 ++ .../kanban/presentation/guards/guards.dart | 1 + lib/src/kanban/presentation/presentation.dart | 4 + .../repositories/kanban_repository.dart | 200 ++++++++++ .../repositories/repositories.dart | 1 + .../presentation/screens/kanban_screen.dart | 89 +++++ .../kanban/presentation/screens/screens.dart | 1 + .../presentation/widgets/kanban_card.dart | 137 +++++++ .../widgets/kanban_column_widget.dart | 117 ++++++ .../kanban/presentation/widgets/widgets.dart | 2 + lib/src/kanban/utils/utils.dart | 1 + .../domain/models/moodle_course.freezed.dart | 32 +- .../domain/models/moodle_task.freezed.dart | 36 +- .../moodle_courses_repository.dart | 14 + .../repositories/moodle_tasks_repository.dart | 19 + .../domain/models/notification.freezed.dart | 34 +- .../presentation/screens/settings_screen.dart | 4 + .../presentation/widgets/feedback_widget.dart | 1 - .../widgets/general_settings.dart | 169 ++------- .../widgets/generic_settings.dart | 351 ++++++++++++++++++ .../presentation/widgets/kanban_settings.dart | 70 ++++ .../presentation/widgets/widgets.dart | 1 + .../domain/models/course_to_slot.freezed.dart | 30 +- .../domain/models/reservation.freezed.dart | 32 +- lib/src/slots/domain/models/slot.freezed.dart | 42 +-- lib/src/slots/domain/models/slot.g.dart | 2 +- .../domain/models/chart_value.freezed.dart | 26 +- .../models/status_aggregate.freezed.dart | 30 +- .../domain/models/task_aggregate.freezed.dart | 30 +- .../domain/models/task_aggregate.g.dart | 4 +- .../domain/models/type_aggregate.freezed.dart | 32 +- .../domain/models/theme_base.freezed.dart | 46 +-- .../services/theme_generator_service.dart | 2 +- pubspec.lock | 149 ++++---- pubspec.yaml | 6 +- test/kanban_module_test.dart | 18 + 77 files changed, 2625 insertions(+), 664 deletions(-) create mode 100644 lib/src/auth/utils/kanban_column_converter_utils.dart create mode 100644 lib/src/kanban/domain/datasources/datasources.dart create mode 100644 lib/src/kanban/domain/datasources/kanban_datasource.dart create mode 100644 lib/src/kanban/domain/domain.dart create mode 100644 lib/src/kanban/domain/models/kanban_board.dart create mode 100644 lib/src/kanban/domain/models/kanban_board.freezed.dart create mode 100644 lib/src/kanban/domain/models/kanban_board.g.dart create mode 100644 lib/src/kanban/domain/models/models.dart create mode 100644 lib/src/kanban/domain/services/services.dart create mode 100644 lib/src/kanban/infra/datasources/datasources.dart create mode 100644 lib/src/kanban/infra/datasources/local_kanban_datasource.dart create mode 100644 lib/src/kanban/infra/datasources/moodle_kanban_datasource.dart create mode 100644 lib/src/kanban/infra/infra.dart create mode 100644 lib/src/kanban/infra/services/services.dart create mode 100644 lib/src/kanban/kanban.dart create mode 100644 lib/src/kanban/presentation/guards/guards.dart create mode 100644 lib/src/kanban/presentation/presentation.dart create mode 100644 lib/src/kanban/presentation/repositories/kanban_repository.dart create mode 100644 lib/src/kanban/presentation/repositories/repositories.dart create mode 100644 lib/src/kanban/presentation/screens/kanban_screen.dart create mode 100644 lib/src/kanban/presentation/screens/screens.dart create mode 100644 lib/src/kanban/presentation/widgets/kanban_card.dart create mode 100644 lib/src/kanban/presentation/widgets/kanban_column_widget.dart create mode 100644 lib/src/kanban/presentation/widgets/widgets.dart create mode 100644 lib/src/kanban/utils/utils.dart create mode 100644 lib/src/settings/presentation/widgets/generic_settings.dart create mode 100644 lib/src/settings/presentation/widgets/kanban_settings.dart create mode 100644 test/kanban_module_test.dart diff --git a/.fvmrc b/.fvmrc index 3ca65ffc..1efca67f 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.32.8" + "flutter": "3.35.0" } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index eee92ae1..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.32.8", + "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 3ec62fab..aa8aecb7 100644 --- a/l10n/de.arb +++ b/l10n/de.arb @@ -733,5 +733,71 @@ "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_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 verstecken", + "@kanban_screen_hideBacklog": { + "description": "Label for the hide backlog button." + }, + "kanban_screen_showBacklog": "Backlog anzeigen", + "@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": "Spalten Farben", + "@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 c10e2a82..9ff0bae7 100644 --- a/l10n/en.arb +++ b/l10n/en.arb @@ -741,5 +741,71 @@ "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_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/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 ac3e1084..45f12dea 100644 --- a/lib/gen/l10n/app_localizations.dart +++ b/lib/gen/l10n/app_localizations.dart @@ -63,7 +63,7 @@ import 'app_localizations_en.dart'; /// property. abstract class AppLocalizations { AppLocalizations(String locale) - : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; @@ -86,16 +86,16 @@ abstract class AppLocalizations { /// of delegates is preferred or required. static const List> localizationsDelegates = >[ - delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ]; + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; /// A list of this localizations delegate's supported locales. static const List supportedLocales = [ Locale('de'), - Locale('en') + Locale('en'), ]; /// Global confirmation button label. @@ -883,7 +883,10 @@ 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 room, + String startUnit, + String endUnit, + ); /// Confirmation message for deleting a slot. /// @@ -1046,6 +1049,90 @@ 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; + + /// 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 @@ -1075,8 +1162,9 @@ AppLocalizations lookupAppLocalizations(Locale locale) { } 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 de32841e..750851f6 100644 --- a/lib/gen/l10n/app_localizations_de.dart +++ b/lib/gen/l10n/app_localizations_de.dart @@ -468,7 +468,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String slots_slotmaster_deleteSlot_title( - String room, String startUnit, String endUnit) { + String room, + String startUnit, + String endUnit, + ) { return 'Slot $room $startUnit - $endUnit löschen?'; } @@ -557,4 +560,51 @@ class AppLocalizationsDe extends AppLocalizations { @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! ❤️'; + + @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 verstecken'; + + @override + String get kanban_screen_showBacklog => 'Backlog anzeigen'; + + @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 => 'Spalten Farben'; } diff --git a/lib/gen/l10n/app_localizations_en.dart b/lib/gen/l10n/app_localizations_en.dart index 57f90b2e..750ff626 100644 --- a/lib/gen/l10n/app_localizations_en.dart +++ b/lib/gen/l10n/app_localizations_en.dart @@ -464,7 +464,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String slots_slotmaster_deleteSlot_title( - String room, String startUnit, String endUnit) { + String room, + String startUnit, + String endUnit, + ) { return 'Delete slot $room $startUnit - $endUnit?'; } @@ -554,4 +557,50 @@ class AppLocalizationsEn extends AppLocalizations { @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! ❤️'; + + @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/src/app/app.dart b/lib/src/app/app.dart index 5845c110..54dcfd43 100644 --- a/lib/src/app/app.dart +++ b/lib/src/app/app.dart @@ -31,6 +31,7 @@ class AppModule extends Module { UpdaterModule(), MoodleModule(), CalendarModule(), + KanbanModule(), ]; @override @@ -69,6 +70,15 @@ class AppModule extends Module { AuthGuard(redirectTo: '/auth/'), ], ), + ModuleRoute( + '/kanban/', + module: KanbanModule(), + guards: [ + // FeatureGuard([kCalendarPlanFeatureID], redirectTo: '/settings/'), + CapabilityGuard({UserCapability.student}, redirectTo: '/slots/'), + AuthGuard(redirectTo: '/auth/'), + ], + ), ModuleRoute( '/calendar/', module: CalendarModule(), 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/utils/animate_utils.dart b/lib/src/app/utils/animate_utils.dart index 18de74ce..f1019bf4 100644 --- a/lib/src/app/utils/animate_utils.dart +++ b/lib/src/app/utils/animate_utils.dart @@ -22,6 +22,7 @@ extension AnimateUtils on List { double begin = 2, double end = 0, int limit = 16, + String? keyPrefex, }) { assert(limit >= 0, 'Limit must be positive'); @@ -54,7 +55,9 @@ extension AnimateUtils on List { widgets.add( this[i] - .animate() + .animate( + key: keyPrefex != null ? ValueKey('$keyPrefex-$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/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..cb860b84 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 in-progress 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..5321e06d 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 in-progress 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 in-progress 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 in-progress 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/user_repository.dart b/lib/src/auth/presentation/repositories/user_repository.dart index 04ffc3ec..65971844 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, + 'auto_move_submitted_tasks': user.autoMoveSubmittedTasksTo, + 'auto_move_overdue_tasks': user.autoMoveOverdueTasksTo, }, ); @@ -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( - patch, - ); - - await _userDatasource.updateUser( - _auth.state.requireData[Webservice.lb_planner_api], + await _updateUser( patch, + transaction, ); - 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,112 @@ 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 { + 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/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..d2cd2d88 --- /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) => ('Kanban Board', 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 39635df7..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'), - // 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..d76e752d --- /dev/null +++ b/lib/src/settings/presentation/widgets/kanban_settings.dart @@ -0,0 +1,70 @@ +import 'package:eduplanner/eduplanner.dart'; +import 'package:eduplanner/src/auth/auth.dart'; +import 'package:eduplanner/src/kanban/kanban.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: '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/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/pubspec.lock b/pubspec.lock index 24400da5..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: @@ -82,10 +77,10 @@ packages: dependency: "direct main" description: name: awesome_extensions - sha256: "6e72049be9639599e5f943e4627c8cfe55740081aa5ea188875d746837a6a923" + sha256: d61c85a583c753e106fcbff392c705c8cd72f6fcacc86ddd1bdcc0a6f498efb3 url: "https://pub.dev" source: hosted - version: "2.0.24" + version: "2.0.25" bloc: dependency: "direct main" description: @@ -114,10 +109,10 @@ packages: dependency: transitive description: name: build - sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.4.1" build_config: dependency: transitive description: @@ -138,26 +133,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "9.1.2" + version: "7.3.2" built_collection: dependency: transitive description: @@ -298,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: @@ -562,18 +557,18 @@ packages: dependency: "direct main" description: name: font_awesome_flutter - sha256: f50ce90dbe26d977415b9540400d6778bef00894aced6358ae578abd92b14b10 + sha256: b738e35f8bb4957896c34957baf922f99c5d415b38ddc8b070d14b7fa95715d4 url: "https://pub.dev" source: hosted - version: "10.9.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: @@ -722,34 +717,34 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b url: "https://pub.dev" source: hosted - version: "6.9.5" + version: "6.8.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" url: "https://pub.dev" source: hosted - version: "10.0.9" + 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: @@ -766,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: @@ -894,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: @@ -934,18 +921,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db" url: "https://pub.dev" source: hosted - version: "2.2.17" + 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: @@ -974,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: @@ -1048,10 +1035,10 @@ packages: dependency: transitive description: name: provider - sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.1.5+1" pub_semver: dependency: transitive description: @@ -1168,10 +1155,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e" + sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74 url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.4.12" shared_preferences_foundation: dependency: transitive description: @@ -1240,10 +1227,10 @@ 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: @@ -1269,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: @@ -1373,26 +1360,26 @@ 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: @@ -1453,18 +1440,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656" + sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7" url: "https://pub.dev" source: hosted - version: "6.3.17" + version: "6.3.18" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 url: "https://pub.dev" source: hosted - version: "6.3.3" + version: "6.3.4" url_launcher_linux: dependency: transitive description: @@ -1477,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: @@ -1541,18 +1528,18 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331" + sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc url: "https://pub.dev" source: hosted - version: "1.1.17" + 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: @@ -1565,10 +1552,10 @@ packages: dependency: transitive description: name: watcher - sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" + sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" web: dependency: transitive description: @@ -1629,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: @@ -1642,5 +1629,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0 <4.0.0" - flutter: ">=3.32.0" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 7c0dc612..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 @@ -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. +} From dc39f5b09483102b1874387b8f232db16f6c8c37 Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Tue, 16 Sep 2025 00:02:24 +0200 Subject: [PATCH 11/22] feat: improve slot editing UX --- l10n/de.arb | 4 +++ l10n/en.arb | 4 +++ lib/gen/l10n/app_localizations.dart | 34 ++++++++++--------- lib/gen/l10n/app_localizations_de.dart | 8 ++--- lib/gen/l10n/app_localizations_en.dart | 8 ++--- .../presentation/widgets/kanban_settings.dart | 2 -- .../widgets/edit_slot_dialog.dart | 12 +++---- 7 files changed, 39 insertions(+), 33 deletions(-) diff --git a/l10n/de.arb b/l10n/de.arb index aa8aecb7..9375457a 100644 --- a/l10n/de.arb +++ b/l10n/de.arb @@ -671,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." diff --git a/l10n/en.arb b/l10n/en.arb index 9ff0bae7..535d6064 100644 --- a/l10n/en.arb +++ b/l10n/en.arb @@ -673,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." diff --git a/lib/gen/l10n/app_localizations.dart b/lib/gen/l10n/app_localizations.dart index 45f12dea..cae659f9 100644 --- a/lib/gen/l10n/app_localizations.dart +++ b/lib/gen/l10n/app_localizations.dart @@ -63,7 +63,7 @@ import 'app_localizations_en.dart'; /// property. abstract class AppLocalizations { AppLocalizations(String locale) - : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; @@ -86,16 +86,16 @@ abstract class AppLocalizations { /// of delegates is preferred or required. static const List> localizationsDelegates = >[ - delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ]; + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; /// A list of this localizations delegate's supported locales. static const List supportedLocales = [ Locale('de'), - Locale('en'), + Locale('en') ]; /// Global confirmation button label. @@ -883,10 +883,7 @@ 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 room, String startUnit, String endUnit); /// Confirmation message for deleting a slot. /// @@ -954,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: @@ -1162,9 +1165,8 @@ AppLocalizations lookupAppLocalizations(Locale locale) { } 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 750851f6..8825126d 100644 --- a/lib/gen/l10n/app_localizations_de.dart +++ b/lib/gen/l10n/app_localizations_de.dart @@ -468,10 +468,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String slots_slotmaster_deleteSlot_title( - String room, - String startUnit, - String endUnit, - ) { + String room, String startUnit, String endUnit) { return 'Slot $room $startUnit - $endUnit löschen?'; } @@ -509,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'; diff --git a/lib/gen/l10n/app_localizations_en.dart b/lib/gen/l10n/app_localizations_en.dart index 750ff626..198abc8e 100644 --- a/lib/gen/l10n/app_localizations_en.dart +++ b/lib/gen/l10n/app_localizations_en.dart @@ -464,10 +464,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String slots_slotmaster_deleteSlot_title( - String room, - String startUnit, - String endUnit, - ) { + String room, String startUnit, String endUnit) { return 'Delete slot $room $startUnit - $endUnit?'; } @@ -505,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'; diff --git a/lib/src/settings/presentation/widgets/kanban_settings.dart b/lib/src/settings/presentation/widgets/kanban_settings.dart index d76e752d..cc0a05b5 100644 --- a/lib/src/settings/presentation/widgets/kanban_settings.dart +++ b/lib/src/settings/presentation/widgets/kanban_settings.dart @@ -1,6 +1,4 @@ import 'package:eduplanner/eduplanner.dart'; -import 'package:eduplanner/src/auth/auth.dart'; -import 'package:eduplanner/src/kanban/kanban.dart'; import 'package:eduplanner/src/settings/presentation/widgets/generic_settings.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; diff --git a/lib/src/slots/presentation/widgets/edit_slot_dialog.dart b/lib/src/slots/presentation/widgets/edit_slot_dialog.dart index e58da33b..a4a6569d 100644 --- a/lib/src/slots/presentation/widgets/edit_slot_dialog.dart +++ b/lib/src/slots/presentation/widgets/edit_slot_dialog.dart @@ -67,6 +67,7 @@ class _EditSlotDialogState extends State { 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; @@ -433,7 +434,7 @@ class _EditSlotDialogState extends State { splashColor: Colors.transparent, highlightColor: Colors.transparent, hoverColor: Colors.transparent, - icon: const Icon(Icons.close), + icon: const Icon(Icons.delete), onPressed: () => removeSupervisor(supervisor.id), ), ], @@ -529,23 +530,20 @@ class _EditSlotDialogState extends State { courseMappings.removeWhere((e) => e.id == element.id); }); }, - icon: const Icon(Icons.close), + icon: const Icon(Icons.delete), ), ], ), Container( padding: PaddingAll(Spacing.xsSpacing), decoration: ShapeDecoration(shape: squircle(), color: context.theme.scaffoldBackgroundColor), - child: IconButton( - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - hoverColor: Colors.transparent, + child: TextButton( + child: Text(context.t.slots_edit_addCourseMapping), onPressed: () { setState(() { courseMappings.add(MappingElement()); }); }, - icon: const Icon(Icons.add), ).stretch(), ), ].vSpaced(Spacing.smallSpacing), From 636257c3f0291114faad46dfc6b9534ec0589c58 Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Tue, 16 Sep 2025 00:03:14 +0200 Subject: [PATCH 12/22] feat: implement TabView in SlotMasterScreen --- .../screens/slot_master_screen.dart | 161 +++++++++++------- 1 file changed, 97 insertions(+), 64 deletions(-) diff --git a/lib/src/slots/presentation/screens/slot_master_screen.dart b/lib/src/slots/presentation/screens/slot_master_screen.dart index 75100d0b..585cf5e4 100644 --- a/lib/src/slots/presentation/screens/slot_master_screen.dart +++ b/lib/src/slots/presentation/screens/slot_master_screen.dart @@ -1,4 +1,5 @@ import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:collection/collection.dart'; import 'package:data_widget/data_widget.dart'; import 'package:eduplanner/eduplanner.dart'; import 'package:flutter/material.dart'; @@ -13,7 +14,8 @@ 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(); Weekday activeDay = Weekday.monday; @@ -22,11 +24,19 @@ class _SlotMasterScreenState extends State with AdaptiveState, void initState() { super.initState(); + _tabController = TabController(length: 7, vsync: this); + searchController.addListener(() { setState(() {}); }); } + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -53,79 +63,102 @@ class _SlotMasterScreenState extends State with AdaptiveState, final groups = slots.groupByStartUnit(); - final activeGroup = groups[activeDay] ?? >{}; + // final activeGroup = groups[activeDay] ?? >{}; return Padding( padding: PaddingAll(), + child: Scaffold( + appBar: TabBar( + controller: _tabController, + tabs: [ + for (final weekday in Weekday.values) + Tab( + text: weekday.translate(context), + ), + ], + ), + body: TabBarView( + controller: _tabController, + children: [ + for (final weekday in Weekday.values) slotTimeTable(groups[weekday] ?? >{}, weekday), + ], + ), + ), + ); + // Padding( + // padding: PaddingAll(), + // child: Column( + // children: [ + // Row( + // spacing: Spacing.mediumSpacing, + // children: [ + // for (final weekday in Weekday.values) + // TextButton( + // onPressed: weekday == activeDay + // ? null + // : () { + // setState(() { + // activeDay = weekday; + // }); + // }, + // style: TextButton.styleFrom( + // backgroundColor: weekday == activeDay ? context.theme.highlightColor : context.theme.cardColor, + // ), + // child: Text( + // weekday.translate(context), + // style: context.theme.textTheme.titleLarge, + // ), + // ), + // ], + // ), + // Spacing.mediumVertical(), + // ], + // ), + // ); + } + + Widget slotTimeTable(Map> activeGroup, Weekday weekday) { + return SingleChildScrollView( child: Column( + spacing: Spacing.largeSpacing, children: [ - Row( - spacing: Spacing.mediumSpacing, - children: [ - for (final weekday in Weekday.values) - TextButton( - onPressed: weekday == activeDay - ? null - : () { - setState(() { - activeDay = weekday; - }); - }, - style: TextButton.styleFrom( - backgroundColor: weekday == activeDay ? context.theme.highlightColor : context.theme.cardColor, - ), - child: Text( - weekday.translate(context), - style: context.theme.textTheme.titleLarge, - ), - ), - ], - ), - Spacing.mediumVertical(), - SingleChildScrollView( - child: Column( - spacing: Spacing.largeSpacing, + for (final timeUnit in SlotTimeUnit.values) + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - for (final timeUnit in SlotTimeUnit.values) - Column( - crossAxisAlignment: CrossAxisAlignment.start, + Row( + children: [ + Text( + timeUnit.humanReadable(), + style: context.theme.textTheme.titleMedium, + ), + Spacing.xsHorizontal(), + TextButton( + onPressed: () => createSlot(weekday, timeUnit), + child: Text(context.t.slots_slotmaster_newSlot), + ), + Spacing.smallVertical(), + ], + ), + if (activeGroup[timeUnit]?.isNotEmpty ?? false) + Wrap( + spacing: Spacing.mediumSpacing, + runSpacing: Spacing.mediumSpacing, children: [ - Row( - children: [ - Text( - timeUnit.humanReadable(), - style: context.theme.textTheme.titleMedium, - ), - Spacing.xsHorizontal(), - TextButton( - onPressed: () => createSlot(activeDay, timeUnit), - child: Text(context.t.slots_slotmaster_newSlot), - ), - Spacing.smallVertical(), - ], - ), - if (activeGroup[timeUnit]?.isNotEmpty ?? false) - Wrap( - spacing: Spacing.mediumSpacing, - runSpacing: Spacing.mediumSpacing, - children: [ - // TODO(mastermarcohd): sort slots by roomnr - for (final slot in (activeGroup[timeUnit] ?? []).query(searchController.text)) - SizedBox( - key: ValueKey(slot), - width: tileWidth, - height: tileHeight, - child: SlotMasterWidget(slot: slot), - ), - ].show(), - ).stretch(), - ], - ), + // 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(), ], ), - ); + ).expanded(); } } From d15161b31fa0010971fdd9c5ddd45961f241ca10 Mon Sep 17 00:00:00 2001 From: mcquenji <60017181+mcquenji@users.noreply.github.com> Date: Tue, 16 Sep 2025 09:48:53 +0200 Subject: [PATCH 13/22] fix: improve slotmaster styling --- .../presentation/widgets/plan_popup.dart | 91 ++++++++++--------- .../screens/slot_master_screen.dart | 19 ++-- .../widgets/edit_slot_dialog.dart | 27 +++--- .../widgets/slot_master_widget.dart | 1 + .../material_theme_generator_service.dart | 10 ++ 5 files changed, 84 insertions(+), 64 deletions(-) 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/slots/presentation/screens/slot_master_screen.dart b/lib/src/slots/presentation/screens/slot_master_screen.dart index 585cf5e4..40390f7c 100644 --- a/lib/src/slots/presentation/screens/slot_master_screen.dart +++ b/lib/src/slots/presentation/screens/slot_master_screen.dart @@ -1,8 +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. @@ -24,7 +25,7 @@ class _SlotMasterScreenState extends State with AdaptiveState, void initState() { super.initState(); - _tabController = TabController(length: 7, vsync: this); + _tabController = TabController(length: Weekday.values.length, vsync: this, animationDuration: 500.ms); searchController.addListener(() { setState(() {}); @@ -77,11 +78,14 @@ class _SlotMasterScreenState extends State with AdaptiveState, ), ], ), - body: TabBarView( - controller: _tabController, - children: [ - for (final weekday in Weekday.values) slotTimeTable(groups[weekday] ?? >{}, weekday), - ], + body: Padding( + padding: PaddingTop(Spacing.mediumSpacing).Horizontal(Spacing.smallSpacing), + child: TabBarView( + controller: _tabController, + children: [ + for (final weekday in Weekday.values) slotTimeTable(groups[weekday] ?? >{}, weekday), + ], + ), ), ), ); @@ -124,6 +128,7 @@ class _SlotMasterScreenState extends State with AdaptiveState, children: [ for (final timeUnit in SlotTimeUnit.values) Column( + spacing: Spacing.smallSpacing, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( diff --git a/lib/src/slots/presentation/widgets/edit_slot_dialog.dart b/lib/src/slots/presentation/widgets/edit_slot_dialog.dart index a4a6569d..baf035cb 100644 --- a/lib/src/slots/presentation/widgets/edit_slot_dialog.dart +++ b/lib/src/slots/presentation/widgets/edit_slot_dialog.dart @@ -434,7 +434,7 @@ class _EditSlotDialogState extends State { splashColor: Colors.transparent, highlightColor: Colors.transparent, hoverColor: Colors.transparent, - icon: const Icon(Icons.delete), + icon: const Icon(Icons.remove_circle_outline_rounded), onPressed: () => removeSupervisor(supervisor.id), ), ], @@ -534,18 +534,19 @@ class _EditSlotDialogState extends State { ), ], ), - Container( - padding: PaddingAll(Spacing.xsSpacing), - decoration: ShapeDecoration(shape: squircle(), color: context.theme.scaffoldBackgroundColor), - child: TextButton( - child: Text(context.t.slots_edit_addCourseMapping), - onPressed: () { - setState(() { - courseMappings.add(MappingElement()); - }); - }, - ).stretch(), - ), + // Container( + // padding: PaddingAll(Spacing.xsSpacing), + // decoration: ShapeDecoration(shape: squircle(), color: context.theme.scaffoldBackgroundColor), + // child: TextButton().stretch(), + // ), + FilledButton( + onPressed: () { + setState(() { + courseMappings.add(MappingElement()); + }); + }, + child: Text(context.t.slots_edit_addCourseMapping), + ).stretch() ].vSpaced(Spacing.smallSpacing), ).expanded(), ], diff --git a/lib/src/slots/presentation/widgets/slot_master_widget.dart b/lib/src/slots/presentation/widgets/slot_master_widget.dart index 554753bf..72c30b4d 100644 --- a/lib/src/slots/presentation/widgets/slot_master_widget.dart +++ b/lib/src/slots/presentation/widgets/slot_master_widget.dart @@ -172,6 +172,7 @@ class _SlotMasterWidgetState extends State { children: [ TextButton( onPressed: deleteSlot, + style: TextButton.styleFrom(foregroundColor: context.theme.colorScheme.error), child: Text(context.t.global_delete), ), TextButton( 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 637979c0..063c59f2 100644 --- a/lib/src/theming/infra/services/material_theme_generator_service.dart +++ b/lib/src/theming/infra/services/material_theme_generator_service.dart @@ -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) { From 8afc9cf94bf4bd80f8a20e83a0c8de34a1f4350d Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Sun, 21 Sep 2025 18:30:57 +0200 Subject: [PATCH 14/22] fix: prevent coursemapping duplication --- lib/src/slots/presentation/widgets/edit_slot_dialog.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/slots/presentation/widgets/edit_slot_dialog.dart b/lib/src/slots/presentation/widgets/edit_slot_dialog.dart index baf035cb..196aa61e 100644 --- a/lib/src/slots/presentation/widgets/edit_slot_dialog.dart +++ b/lib/src/slots/presentation/widgets/edit_slot_dialog.dart @@ -103,6 +103,7 @@ class _EditSlotDialogState extends State { final repo = context.read(); + mappings = []; for (final element in courseMappings) { addMapping(element); } @@ -301,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(), ); From 4978af7727f8bd81645494d4436a616ca6b8e8e2 Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Sun, 21 Sep 2025 19:36:02 +0200 Subject: [PATCH 15/22] fix: fix autocomplete dropdown dissappearing --- .../widgets/edit_slot_dialog.dart | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/lib/src/slots/presentation/widgets/edit_slot_dialog.dart b/lib/src/slots/presentation/widgets/edit_slot_dialog.dart index 196aa61e..923fd16c 100644 --- a/lib/src/slots/presentation/widgets/edit_slot_dialog.dart +++ b/lib/src/slots/presentation/widgets/edit_slot_dialog.dart @@ -329,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), + ), + ), + ); + }, + ); + }, ), ), ), From aa52fba4a4903cbed748ae810e9d0dae86852831 Mon Sep 17 00:00:00 2001 From: McQuenji <60017181+mcquenji@users.noreply.github.com> Date: Tue, 23 Sep 2025 09:11:57 +0200 Subject: [PATCH 16/22] Update lib/src/app/app.dart Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- lib/src/app/app.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/app/app.dart b/lib/src/app/app.dart index 54dcfd43..987af261 100644 --- a/lib/src/app/app.dart +++ b/lib/src/app/app.dart @@ -73,6 +73,8 @@ class AppModule extends Module { ModuleRoute( '/kanban/', module: KanbanModule(), + transition: TransitionType.custom, + customTransition: defaultTransition, guards: [ // FeatureGuard([kCalendarPlanFeatureID], redirectTo: '/settings/'), CapabilityGuard({UserCapability.student}, redirectTo: '/slots/'), From 63beecc80c42c8d3b3de09c43cc7328470f7f192 Mon Sep 17 00:00:00 2001 From: McQuenji <60017181+mcquenji@users.noreply.github.com> Date: Tue, 23 Sep 2025 09:12:23 +0200 Subject: [PATCH 17/22] Update lib/src/slots/presentation/widgets/slot_master_widget.dart Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- lib/src/slots/presentation/widgets/slot_master_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/slots/presentation/widgets/slot_master_widget.dart b/lib/src/slots/presentation/widgets/slot_master_widget.dart index 72c30b4d..eda89393 100644 --- a/lib/src/slots/presentation/widgets/slot_master_widget.dart +++ b/lib/src/slots/presentation/widgets/slot_master_widget.dart @@ -176,7 +176,7 @@ class _SlotMasterWidgetState extends State { child: Text(context.t.global_delete), ), TextButton( - onPressed: duplicateSlot, + onPressed: isDeleting ? null : duplicateSlot, child: Text(context.t.global_duplicate), ), TextButton( From d35c340e33133b989e05f6bea7e12cbf7cb82c33 Mon Sep 17 00:00:00 2001 From: McQuenji <60017181+mcquenji@users.noreply.github.com> Date: Tue, 23 Sep 2025 09:15:10 +0200 Subject: [PATCH 18/22] Update lib/src/app/utils/animate_utils.dart Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- lib/src/app/utils/animate_utils.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/app/utils/animate_utils.dart b/lib/src/app/utils/animate_utils.dart index f1019bf4..52405ea1 100644 --- a/lib/src/app/utils/animate_utils.dart +++ b/lib/src/app/utils/animate_utils.dart @@ -22,7 +22,9 @@ extension AnimateUtils on List { double begin = 2, double end = 0, int limit = 16, - String? keyPrefex, + String? keyPrefix, + + key: keyPrefix != null ? ValueKey('$keyPrefix-$i') : null, }) { assert(limit >= 0, 'Limit must be positive'); From 93bf70cfb5a04e0c6363ca0b2e9d29599f2de59b Mon Sep 17 00:00:00 2001 From: McQuenji <60017181+mcquenji@users.noreply.github.com> Date: Tue, 23 Sep 2025 09:16:38 +0200 Subject: [PATCH 19/22] Update lib/src/auth/presentation/repositories/user_repository.dart Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- lib/src/auth/presentation/repositories/user_repository.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/auth/presentation/repositories/user_repository.dart b/lib/src/auth/presentation/repositories/user_repository.dart index 65971844..8ec87f9d 100644 --- a/lib/src/auth/presentation/repositories/user_repository.dart +++ b/lib/src/auth/presentation/repositories/user_repository.dart @@ -70,9 +70,9 @@ class UserRepository extends Repository> with Tracable { 'optional_tasks_enabled': user.optionalTasksEnabled, 'display_task_count': user.displayTaskCount, 'show_column_colors': user.showColumnColors, - 'auto_move_completed_tasks': user.autoMoveCompletedTasksTo, - 'auto_move_submitted_tasks': user.autoMoveSubmittedTasksTo, - 'auto_move_overdue_tasks': user.autoMoveOverdueTasksTo, + 'auto_move_completed_tasks': user.autoMoveCompletedTasksTo?.name, + 'auto_move_submitted_tasks': user.autoMoveSubmittedTasksTo?.name, + 'auto_move_overdue_tasks': user.autoMoveOverdueTasksTo?.name, }, ); From 156d704646a00171abc1c875838f914269e506a0 Mon Sep 17 00:00:00 2001 From: McQuenji <60017181+mcquenji@users.noreply.github.com> Date: Tue, 23 Sep 2025 09:17:13 +0200 Subject: [PATCH 20/22] Update lib/src/auth/presentation/repositories/user_repository.dart Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- lib/src/auth/presentation/repositories/user_repository.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/auth/presentation/repositories/user_repository.dart b/lib/src/auth/presentation/repositories/user_repository.dart index 8ec87f9d..ed8f40e5 100644 --- a/lib/src/auth/presentation/repositories/user_repository.dart +++ b/lib/src/auth/presentation/repositories/user_repository.dart @@ -247,6 +247,10 @@ class UserRepository extends Repository> with Tracable { /// 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, ); From 010bf195cf016749e7d23fceca3763499952f406 Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Tue, 23 Sep 2025 13:13:27 +0200 Subject: [PATCH 21/22] Update lib/src/app/utils/animate_utils.dart Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- lib/src/app/utils/animate_utils.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/app/utils/animate_utils.dart b/lib/src/app/utils/animate_utils.dart index 52405ea1..80cdf53f 100644 --- a/lib/src/app/utils/animate_utils.dart +++ b/lib/src/app/utils/animate_utils.dart @@ -26,7 +26,7 @@ extension AnimateUtils on List { key: keyPrefix != null ? ValueKey('$keyPrefix-$i') : null, }) { - assert(limit >= 0, 'Limit must be positive'); + assert(limit >= 0, 'Limit must be non-negative'); stagger ??= AnimationStagger(); From b3ab9790fbe8f2f513b81da33f059f74f931e83c Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Tue, 23 Sep 2025 16:16:40 +0200 Subject: [PATCH 22/22] chore: appease coderabbit pr review --- l10n/de.arb | 10 ++++-- l10n/en.arb | 4 +++ lib/gen/l10n/app_localizations.dart | 6 ++++ lib/gen/l10n/app_localizations_de.dart | 9 +++-- lib/gen/l10n/app_localizations_en.dart | 3 ++ lib/src/app/utils/animate_utils.dart | 4 +-- lib/src/auth/domain/models/user.dart | 2 +- lib/src/auth/domain/models/user.freezed.dart | 6 ++-- lib/src/kanban/kanban.dart | 2 +- .../presentation/widgets/kanban_settings.dart | 2 +- .../slot_master_slots_repository.dart | 18 +++------- .../screens/slot_master_screen.dart | 36 +------------------ .../widgets/edit_slot_dialog.dart | 9 ++--- .../widgets/slot_master_widget.dart | 4 +-- 14 files changed, 42 insertions(+), 73 deletions(-) diff --git a/l10n/de.arb b/l10n/de.arb index 9375457a..25359800 100644 --- a/l10n/de.arb +++ b/l10n/de.arb @@ -738,6 +738,10 @@ "@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.", @@ -756,11 +760,11 @@ } } }, - "kanban_screen_hideBacklog": "Backlog verstecken", + "kanban_screen_hideBacklog": "Backlog ausblenden", "@kanban_screen_hideBacklog": { "description": "Label for the hide backlog button." }, - "kanban_screen_showBacklog": "Backlog anzeigen", + "kanban_screen_showBacklog": "Backlog einblenden", "@kanban_screen_showBacklog": { "description": "Label for the show backlog button" }, @@ -800,7 +804,7 @@ "@kanban_settings_moveCompletedTasks": { "description": "Label for the move completed tasks setting." }, - "kanban_settings_columnColors": "Spalten Farben", + "kanban_settings_columnColors": "Spaltenfarben", "@kanban_settings_columnColors": { "description": "Label for the column colors setting." } diff --git a/l10n/en.arb b/l10n/en.arb index 535d6064..a2473f95 100644 --- a/l10n/en.arb +++ b/l10n/en.arb @@ -746,6 +746,10 @@ "@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.", diff --git a/lib/gen/l10n/app_localizations.dart b/lib/gen/l10n/app_localizations.dart index cae659f9..64a2be90 100644 --- a/lib/gen/l10n/app_localizations.dart +++ b/lib/gen/l10n/app_localizations.dart @@ -1053,6 +1053,12 @@ abstract class AppLocalizations { /// **'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: diff --git a/lib/gen/l10n/app_localizations_de.dart b/lib/gen/l10n/app_localizations_de.dart index 8825126d..2aba1bc1 100644 --- a/lib/gen/l10n/app_localizations_de.dart +++ b/lib/gen/l10n/app_localizations_de.dart @@ -561,6 +561,9 @@ class AppLocalizationsDe extends AppLocalizations { 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'; @@ -572,10 +575,10 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get kanban_screen_hideBacklog => 'Backlog verstecken'; + String get kanban_screen_hideBacklog => 'Backlog ausblenden'; @override - String get kanban_screen_showBacklog => 'Backlog anzeigen'; + String get kanban_screen_showBacklog => 'Backlog einblenden'; @override String get kanban_screen_backlog => 'Backlog'; @@ -606,5 +609,5 @@ class AppLocalizationsDe extends AppLocalizations { String get kanban_settings_moveCompletedTasks => 'Erledigte Aufgaben bewegen'; @override - String get kanban_settings_columnColors => 'Spalten Farben'; + 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 198abc8e..ecf338ff 100644 --- a/lib/gen/l10n/app_localizations_en.dart +++ b/lib/gen/l10n/app_localizations_en.dart @@ -558,6 +558,9 @@ class AppLocalizationsEn extends AppLocalizations { 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'; diff --git a/lib/src/app/utils/animate_utils.dart b/lib/src/app/utils/animate_utils.dart index 80cdf53f..e27b1b3e 100644 --- a/lib/src/app/utils/animate_utils.dart +++ b/lib/src/app/utils/animate_utils.dart @@ -23,8 +23,6 @@ extension AnimateUtils on List { double end = 0, int limit = 16, String? keyPrefix, - - key: keyPrefix != null ? ValueKey('$keyPrefix-$i') : null, }) { assert(limit >= 0, 'Limit must be non-negative'); @@ -58,7 +56,7 @@ extension AnimateUtils on List { widgets.add( this[i] .animate( - key: keyPrefex != null ? ValueKey('$keyPrefex-$i') : null, + key: keyPrefix != null ? ValueKey('$keyPrefix-$i') : null, ) .slideY( begin: begin, diff --git a/lib/src/auth/domain/models/user.dart b/lib/src/auth/domain/models/user.dart index cb860b84..3811f066 100644 --- a/lib/src/auth/domain/models/user.dart +++ b/lib/src/auth/domain/models/user.dart @@ -57,7 +57,7 @@ class User with _$User { /// The column to auto-move submitted tasks to @Default(null) @JsonKey(name: 'automovesubmittedtasks') @KanbanColumnConverter() KanbanColumn? autoMoveSubmittedTasksTo, - /// The column to auto-move in-progress tasks to + /// The column to auto-move overdue tasks to @Default(null) @JsonKey(name: 'automoveoverduetasks') @KanbanColumnConverter() KanbanColumn? autoMoveOverdueTasksTo, /// The vintage of the user diff --git a/lib/src/auth/domain/models/user.freezed.dart b/lib/src/auth/domain/models/user.freezed.dart index 5321e06d..0b8a497a 100644 --- a/lib/src/auth/domain/models/user.freezed.dart +++ b/lib/src/auth/domain/models/user.freezed.dart @@ -83,7 +83,7 @@ mixin _$User { KanbanColumn? get autoMoveSubmittedTasksTo => throw _privateConstructorUsedError; - /// The column to auto-move in-progress tasks to + /// The column to auto-move overdue tasks to @JsonKey(name: 'automoveoverduetasks') @KanbanColumnConverter() KanbanColumn? get autoMoveOverdueTasksTo => @@ -493,7 +493,7 @@ class _$UserImpl extends _User { @KanbanColumnConverter() final KanbanColumn? autoMoveSubmittedTasksTo; - /// The column to auto-move in-progress tasks to + /// The column to auto-move overdue tasks to @override @JsonKey(name: 'automoveoverduetasks') @KanbanColumnConverter() @@ -696,7 +696,7 @@ abstract class _User extends User { KanbanColumn? get autoMoveSubmittedTasksTo; @override - /// The column to auto-move in-progress tasks to + /// The column to auto-move overdue tasks to @JsonKey(name: 'automoveoverduetasks') @KanbanColumnConverter() KanbanColumn? get autoMoveOverdueTasksTo; diff --git a/lib/src/kanban/kanban.dart b/lib/src/kanban/kanban.dart index d2cd2d88..d737185f 100644 --- a/lib/src/kanban/kanban.dart +++ b/lib/src/kanban/kanban.dart @@ -22,7 +22,7 @@ class KanbanModule extends Module { void binds(Injector i) { i ..add(MoodleKanbanDatasource.new) - ..add(() => (BuildContext context) => ('Kanban Board', null)) + ..add(() => (BuildContext context) => (context.t.kanban_title, null)) ..addRepository(KanbanRepository.new); } diff --git a/lib/src/settings/presentation/widgets/kanban_settings.dart b/lib/src/settings/presentation/widgets/kanban_settings.dart index cc0a05b5..02ae9302 100644 --- a/lib/src/settings/presentation/widgets/kanban_settings.dart +++ b/lib/src/settings/presentation/widgets/kanban_settings.dart @@ -40,7 +40,7 @@ class KanbanSettings extends StatelessWidget { } return GenericSettings( - title: 'Kanban', + title: context.t.kanban_settings_kanban, items: [ autoMoveItem( name: context.t.kanban_settings_moveSubmittedTasks, 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 33d9e8be..602497bc 100644 --- a/lib/src/slots/presentation/repositories/slot_master_slots_repository.dart +++ b/lib/src/slots/presentation/repositories/slot_master_slots_repository.dart @@ -353,21 +353,11 @@ class SlotMasterSlotsRepository extends Repository>> with return {}; } - final groupByDay = state.requireData.groupFoldBy>( - (s) => s.weekday, - (g, s) => [...?g, s], - ); - - final groupByStartUnit = >>{}; - - for (final day in groupByDay.entries) { - groupByStartUnit[day.key] = day.value.groupFoldBy>( - (s) => s.startUnit, - (g, s) => [...?g, s], - ); - } + final byDay = state.requireData.groupListsBy((s) => s.weekday); - return groupByStartUnit; + return { + for (final e in byDay.entries) e.key: e.value.groupListsBy((s) => s.startUnit), + }; } @override diff --git a/lib/src/slots/presentation/screens/slot_master_screen.dart b/lib/src/slots/presentation/screens/slot_master_screen.dart index 40390f7c..0b49aead 100644 --- a/lib/src/slots/presentation/screens/slot_master_screen.dart +++ b/lib/src/slots/presentation/screens/slot_master_screen.dart @@ -19,8 +19,6 @@ class _SlotMasterScreenState extends State with AdaptiveState, late final TabController _tabController; final searchController = TextEditingController(); - Weekday activeDay = Weekday.monday; - @override void initState() { super.initState(); @@ -35,6 +33,7 @@ class _SlotMasterScreenState extends State with AdaptiveState, @override void dispose() { _tabController.dispose(); + searchController.dispose(); super.dispose(); } @@ -64,8 +63,6 @@ class _SlotMasterScreenState extends State with AdaptiveState, final groups = slots.groupByStartUnit(); - // final activeGroup = groups[activeDay] ?? >{}; - return Padding( padding: PaddingAll(), child: Scaffold( @@ -89,36 +86,6 @@ class _SlotMasterScreenState extends State with AdaptiveState, ), ), ); - // Padding( - // padding: PaddingAll(), - // child: Column( - // children: [ - // Row( - // spacing: Spacing.mediumSpacing, - // children: [ - // for (final weekday in Weekday.values) - // TextButton( - // onPressed: weekday == activeDay - // ? null - // : () { - // setState(() { - // activeDay = weekday; - // }); - // }, - // style: TextButton.styleFrom( - // backgroundColor: weekday == activeDay ? context.theme.highlightColor : context.theme.cardColor, - // ), - // child: Text( - // weekday.translate(context), - // style: context.theme.textTheme.titleLarge, - // ), - // ), - // ], - // ), - // Spacing.mediumVertical(), - // ], - // ), - // ); } Widget slotTimeTable(Map> activeGroup, Weekday weekday) { @@ -142,7 +109,6 @@ class _SlotMasterScreenState extends State with AdaptiveState, onPressed: () => createSlot(weekday, timeUnit), child: Text(context.t.slots_slotmaster_newSlot), ), - Spacing.smallVertical(), ], ), if (activeGroup[timeUnit]?.isNotEmpty ?? false) diff --git a/lib/src/slots/presentation/widgets/edit_slot_dialog.dart b/lib/src/slots/presentation/widgets/edit_slot_dialog.dart index 923fd16c..9b0b12d8 100644 --- a/lib/src/slots/presentation/widgets/edit_slot_dialog.dart +++ b/lib/src/slots/presentation/widgets/edit_slot_dialog.dart @@ -291,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, @@ -553,11 +553,6 @@ class _EditSlotDialogState extends State { ), ], ), - // Container( - // padding: PaddingAll(Spacing.xsSpacing), - // decoration: ShapeDecoration(shape: squircle(), color: context.theme.scaffoldBackgroundColor), - // child: TextButton().stretch(), - // ), FilledButton( onPressed: () { setState(() { @@ -565,7 +560,7 @@ class _EditSlotDialogState extends State { }); }, child: Text(context.t.slots_edit_addCourseMapping), - ).stretch() + ).stretch(), ].vSpaced(Spacing.smallSpacing), ).expanded(), ], diff --git a/lib/src/slots/presentation/widgets/slot_master_widget.dart b/lib/src/slots/presentation/widgets/slot_master_widget.dart index eda89393..1daabb93 100644 --- a/lib/src/slots/presentation/widgets/slot_master_widget.dart +++ b/lib/src/slots/presentation/widgets/slot_master_widget.dart @@ -171,7 +171,7 @@ 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), ), @@ -180,7 +180,7 @@ class _SlotMasterWidgetState extends State { child: Text(context.t.global_duplicate), ), TextButton( - onPressed: editSlot, + onPressed: isDeleting ? null : editSlot, child: Text(context.t.global_edit), ), ],