From 2e80f5fd3d5fd68d52bb14b9db82a76245b03f11 Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Tue, 30 Sep 2025 21:30:07 +0200 Subject: [PATCH 01/14] fix: unbooking slots now works again --- lib/src/slots/domain/models/slot.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/slots/domain/models/slot.dart b/lib/src/slots/domain/models/slot.dart index 7756b3ff..82aff301 100644 --- a/lib/src/slots/domain/models/slot.dart +++ b/lib/src/slots/domain/models/slot.dart @@ -131,7 +131,7 @@ enum Weekday { for (var i = 0; i < 7; i++) { final date = now.add(Duration(days: i)); if (date.weekday == index + 1) { - return date; + return date.copyWith(hour: 0, minute: 0, second: 0, millisecond: 0, microsecond: 0); } } throw StateError('Could not find next date for $this'); From 67b0c493145b26bc26ee1ed8572c7b03c94f486d Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Tue, 30 Sep 2025 21:30:31 +0200 Subject: [PATCH 02/14] fix: change path strategy --- lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 068d3fff..2f665b4d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -101,7 +101,7 @@ void main() async { Adaptive.ignoreHeight = true; - setPathUrlStrategy(); + // setPathUrlStrategy(); // initializeEchidnaApi(baseUrl: kEchidnaHost, clientKey: kEchidnaClientKey, clientId: kEchidnaClientID); From 85e358a90fa3f4a5786bd70ad2772a64183a6aa1 Mon Sep 17 00:00:00 2001 From: mcquenji Date: Tue, 30 Sep 2025 21:50:11 +0200 Subject: [PATCH 03/14] feat: make slots searchable by all their properties --- .../slot_master_slots_repository.dart | 43 ++++++++++++++++++- .../screens/slot_master_screen.dart | 6 +-- 2 files changed, 45 insertions(+), 4 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 602497bc..d5c930ea 100644 --- a/lib/src/slots/presentation/repositories/slot_master_slots_repository.dart +++ b/lib/src/slots/presentation/repositories/slot_master_slots_repository.dart @@ -12,10 +12,14 @@ import 'package:mcquenji_core/mcquenji_core.dart'; class SlotMasterSlotsRepository extends Repository>> with Tracable { final AuthRepository _auth; final SlotsDatasource _datasource; + final MoodleCoursesRepository _courses; + final UsersRepository _users; /// Provides data for the slot master screen. - SlotMasterSlotsRepository(this._auth, this._datasource) : super(AsyncValue.loading()) { + SlotMasterSlotsRepository(this._auth, this._datasource, this._courses, this._users) : super(AsyncValue.loading()) { watchAsync(_auth); + watchAsync(_courses); + watchAsync(_users); _datasource.parent = this; } @@ -360,6 +364,42 @@ class SlotMasterSlotsRepository extends Repository>> with }; } + /// Queries the slots for the given [query]. + /// + /// A query matches a slot if the room, any course name, any vintage or any supervisor's name contains the query (case-insensitive). + List query(String query, {Iterable? slots}) { + final _slots = slots ?? state.data; + + if (_slots == null) { + log('Cannot query slots: No data'); + return []; + } + + if (query.isEmpty) { + return _slots.toList(); + } + + return _slots.where((slot) { + if (slot.room.containsIgnoreCase(query)) { + return true; + } + + if (slot.mappings.any((m) => m.vintage.humanReadable.containsIgnoreCase(query))) { + return true; + } + + if (slot.mappings.any((m) => _courses.getById(m.courseId)?.name.containsIgnoreCase(query) ?? false)) { + return true; + } + + if (_users.filter(ids: slot.supervisors).any((u) => u.fullname.containsIgnoreCase(query))) { + return true; + } + + return false; + }).toList(); + } + @override void dispose() { _datasource.dispose(); @@ -368,6 +408,7 @@ class SlotMasterSlotsRepository extends Repository>> with } /// Adds a query method to the [Slot] iterable. +@Deprecated('Use SlotsRepository.query instead') extension QueryX on Iterable { /// Queries the slots for the given [query]. List query(String query) { diff --git a/lib/src/slots/presentation/screens/slot_master_screen.dart b/lib/src/slots/presentation/screens/slot_master_screen.dart index 116f7af4..ea0dc470 100644 --- a/lib/src/slots/presentation/screens/slot_master_screen.dart +++ b/lib/src/slots/presentation/screens/slot_master_screen.dart @@ -80,7 +80,7 @@ class _SlotMasterScreenState extends State with AdaptiveState, child: TabBarView( controller: _tabController, children: [ - for (final weekday in Weekday.values) slotTimeTable(groups[weekday] ?? >{}, weekday), + for (final weekday in Weekday.values) slotTimeTable(groups[weekday] ?? >{}, weekday, slots), ], ), ), @@ -88,7 +88,7 @@ class _SlotMasterScreenState extends State with AdaptiveState, ); } - Widget slotTimeTable(Map> activeGroup, Weekday weekday) { + Widget slotTimeTable(Map> activeGroup, Weekday weekday, SlotMasterSlotsRepository repo) { return SingleChildScrollView( child: Column( spacing: Spacing.largeSpacing, @@ -117,7 +117,7 @@ class _SlotMasterScreenState extends State with AdaptiveState, runSpacing: Spacing.mediumSpacing, children: [ // TODO(mastermarcohd): implement more intelligent sorting to account for building and floor. - for (final slot in (activeGroup[timeUnit] ?? []).query(searchController.text).sortedBy((s) => s.room)) + for (final slot in repo.query(searchController.text, slots: activeGroup[timeUnit] ?? []).sortedBy((s) => s.room)) SizedBox( key: ValueKey(slot), width: tileWidth, From 26d9606274671cee68e1bc9e52d3a4a268ab867c Mon Sep 17 00:00:00 2001 From: mcquenji Date: Tue, 30 Sep 2025 22:14:53 +0200 Subject: [PATCH 04/14] fix: update SlotMasterSlotsRepository to use SlotMasterCoursesRepository --- .../slot_master_courses_repository.dart | 16 ++++++++++++++++ .../slot_master_slots_repository.dart | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/src/slots/presentation/repositories/slot_master_courses_repository.dart b/lib/src/slots/presentation/repositories/slot_master_courses_repository.dart index c7bc367a..4aa09557 100644 --- a/lib/src/slots/presentation/repositories/slot_master_courses_repository.dart +++ b/lib/src/slots/presentation/repositories/slot_master_courses_repository.dart @@ -59,6 +59,22 @@ class SlotMasterCoursesRepository extends Repository element.id == id); + } catch (e) { + return null; + } + } + @override void dispose() { super.dispose(); 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 d5c930ea..5407bd63 100644 --- a/lib/src/slots/presentation/repositories/slot_master_slots_repository.dart +++ b/lib/src/slots/presentation/repositories/slot_master_slots_repository.dart @@ -12,7 +12,7 @@ import 'package:mcquenji_core/mcquenji_core.dart'; class SlotMasterSlotsRepository extends Repository>> with Tracable { final AuthRepository _auth; final SlotsDatasource _datasource; - final MoodleCoursesRepository _courses; + final SlotMasterCoursesRepository _courses; final UsersRepository _users; /// Provides data for the slot master screen. From 44eaf0647ff326b34b4bd2b6222e15c1d2abd715 Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Wed, 1 Oct 2025 00:45:10 +0200 Subject: [PATCH 05/14] feat: implement expandable supervisor and mapping widget --- .../widgets/slot_data_pop_over.dart | 109 ++++++++++++++++++ .../widgets/slot_master_widget.dart | 25 +--- .../slots/presentation/widgets/widgets.dart | 1 + 3 files changed, 115 insertions(+), 20 deletions(-) create mode 100644 lib/src/slots/presentation/widgets/slot_data_pop_over.dart diff --git a/lib/src/slots/presentation/widgets/slot_data_pop_over.dart b/lib/src/slots/presentation/widgets/slot_data_pop_over.dart new file mode 100644 index 00000000..28ebe0f7 --- /dev/null +++ b/lib/src/slots/presentation/widgets/slot_data_pop_over.dart @@ -0,0 +1,109 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:eduplanner/eduplanner.dart'; +import 'package:flutter/material.dart'; +import 'package:popover/popover.dart'; + +class SlotDataPopOver extends StatefulWidget { + const SlotDataPopOver({super.key, required this.contentList}); + + final List contentList; + + @override + State createState() => _SlotDataPopOverState(); +} + +class _SlotDataPopOverState extends State { + BuildContext? popupContext; + bool parentHover = true; + bool childHover = true; + + void closePopUp() { + print("Penis $parentHover"); + + if (popupContext == null) return; + + if (parentHover || childHover) return; + + Navigator.of(popupContext!).pop(); + + popupContext = null; + } + + @override + Widget build(BuildContext context) { + return HoverBuilder( + // TODO(mastermarcohd): fix popOver hover + // Hoverbuilder doesnt work right due to PopOver barrier + builder: (context, isHovering) { + parentHover = isHovering; + Future.delayed(const Duration(milliseconds: 300), closePopUp); + return GestureDetector( + // ignore: sort_child_properties_last + child: Row( + children: [ + widget.contentList[0], + if (widget.contentList.length > 1) ...[ + Spacing.smallHorizontal(), + Text("+${widget.contentList.length - 1} ..."), + ], + ], + ), + onTap: widget.contentList.length > 1 + ? () { + showPopover( + context: context, + bodyBuilder: (context) { + popupContext = context; + return HoverBuilder( + builder: (context, isHovering) { + childHover = isHovering; + Future.delayed(const Duration(milliseconds: 300), closePopUp); + return Card( + elevation: 16, + shape: squircle(side: BorderSide(color: context.theme.dividerColor)), + color: context.theme.cardColor, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: widget.contentList.spaced(Spacing.smallSpacing), + ), + ), + ), + ); + }, + ); + }, + arrowHeight: 0, + arrowWidth: 0, + // allowClicksOnBackground: true, + direction: PopoverDirection.bottom, + transition: PopoverTransition.other, + // contentDxOffset: -width + (context.size?.width ?? 0), + barrierDismissible: true, + barrierColor: Colors.transparent, + backgroundColor: Colors.transparent, + transitionDuration: const Duration(milliseconds: 300), + popoverTransitionBuilder: (animation, child) { + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: animation.drive( + Tween( + begin: const Offset(0, -0.02), + end: Offset.zero, + ), + ), + child: child, + ), + ); + }, + ); + } + : null, + ); + }, + ); + } +} diff --git a/lib/src/slots/presentation/widgets/slot_master_widget.dart b/lib/src/slots/presentation/widgets/slot_master_widget.dart index 1daabb93..e4dce0a1 100644 --- a/lib/src/slots/presentation/widgets/slot_master_widget.dart +++ b/lib/src/slots/presentation/widgets/slot_master_widget.dart @@ -1,5 +1,4 @@ import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:carousel_slider/carousel_slider.dart'; import 'package:eduplanner/eduplanner.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; @@ -121,15 +120,8 @@ class _SlotMasterWidgetState extends State { ), Padding( padding: PaddingLeft(Spacing.mediumSpacing), - child: CarouselSlider( - disableGesture: true, - options: CarouselOptions( - autoPlay: true, - enableInfiniteScroll: false, - height: 50, - scrollDirection: Axis.vertical, - ), - items: [ + child: SlotDataPopOver( + contentList: [ for (final supervisor in supervisors) UserWidget(user: supervisor), ], ), @@ -143,17 +135,11 @@ class _SlotMasterWidgetState extends State { ), Padding( padding: PaddingLeft(Spacing.mediumSpacing), - child: CarouselSlider( - disableGesture: true, - options: CarouselOptions( - autoPlay: true, - enableInfiniteScroll: false, - scrollDirection: Axis.vertical, - height: 50, - ), - items: [ + child: SlotDataPopOver( + contentList: [ for (final (course, vintage) in courseVintage) Row( + mainAxisSize: MainAxisSize.min, children: [ CourseTag(course: course!), Spacing.xsHorizontal(), @@ -166,7 +152,6 @@ class _SlotMasterWidgetState extends State { ], ), ).expanded(), - // IconButton(onPressed: deleteSlot, icon: Icon(Icons.delete)) Row( mainAxisAlignment: MainAxisAlignment.end, children: [ diff --git a/lib/src/slots/presentation/widgets/widgets.dart b/lib/src/slots/presentation/widgets/widgets.dart index 67104764..60fea81c 100644 --- a/lib/src/slots/presentation/widgets/widgets.dart +++ b/lib/src/slots/presentation/widgets/widgets.dart @@ -3,3 +3,4 @@ export 'number_spinner.dart'; export 'slot_master_widget.dart'; export 'slot_overview_widget.dart'; export 'slot_widget.dart'; +export 'slot_data_pop_over.dart'; \ No newline at end of file From 9fc744693e19d3cadc2e7534ca6a530cab216519 Mon Sep 17 00:00:00 2001 From: mcquenji Date: Wed, 1 Oct 2025 07:55:58 +0200 Subject: [PATCH 06/14] chore: add slot view switcher widget --- .../widgets/slots_view_switcher.dart | 24 +++++++++++++++++++ .../slots/presentation/widgets/widgets.dart | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 lib/src/slots/presentation/widgets/slots_view_switcher.dart diff --git a/lib/src/slots/presentation/widgets/slots_view_switcher.dart b/lib/src/slots/presentation/widgets/slots_view_switcher.dart new file mode 100644 index 00000000..4465e139 --- /dev/null +++ b/lib/src/slots/presentation/widgets/slots_view_switcher.dart @@ -0,0 +1,24 @@ +import 'package:eduplanner/eduplanner.dart'; +import 'package:mcquenji_core/mcquenji_core.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:flutter/material.dart'; +import 'package:eduplanner/src/slots/slots.dart'; +import 'package:eduplanner/gen/assets/assets.gen.dart'; +import 'package:flutter_animate/flutter_animate.dart'; + +class SlotsViewSwitcher extends StatelessWidget { + const SlotsViewSwitcher({super.key}); + + static const capabilityToRoute = { + UserCapability.slotMaster: '/slots/', + UserCapability.teacher: '/slots/overview/', + UserCapability.student: '/slots/book/', + }; + + @override + Widget build(BuildContext context) { + final user = context.watch(); + + return Container(); + } +} diff --git a/lib/src/slots/presentation/widgets/widgets.dart b/lib/src/slots/presentation/widgets/widgets.dart index 60fea81c..d96f7a03 100644 --- a/lib/src/slots/presentation/widgets/widgets.dart +++ b/lib/src/slots/presentation/widgets/widgets.dart @@ -1,6 +1,7 @@ export 'edit_slot_dialog.dart'; export 'number_spinner.dart'; +export 'slot_data_pop_over.dart'; export 'slot_master_widget.dart'; export 'slot_overview_widget.dart'; export 'slot_widget.dart'; -export 'slot_data_pop_over.dart'; \ No newline at end of file +export 'slots_view_switcher.dart'; From 30bf3d9006c76b12bde09f2fc42e039940abe4a7 Mon Sep 17 00:00:00 2001 From: mcquenji <60017181+mcquenji@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:50:19 +0200 Subject: [PATCH 07/14] feat: add a way to switch between different slot views --- .../app/presentation/widgets/title_bar.dart | 3 +- .../screens/slot_details_screen.dart | 4 +- .../screens/slot_master_screen.dart | 4 +- .../screens/slot_overview_screen.dart | 17 +++- .../screens/slot_reservation_screen.dart | 19 +++- .../widgets/slots_view_switcher.dart | 89 ++++++++++++++++--- .../material_theme_generator_service.dart | 2 +- 7 files changed, 117 insertions(+), 21 deletions(-) diff --git a/lib/src/app/presentation/widgets/title_bar.dart b/lib/src/app/presentation/widgets/title_bar.dart index bdb03d3a..359e74e8 100644 --- a/lib/src/app/presentation/widgets/title_bar.dart +++ b/lib/src/app/presentation/widgets/title_bar.dart @@ -210,6 +210,7 @@ class TitleBarState extends State with WindowListener, RouteAware, Ada crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( + spacing: Spacing.mediumSpacing, children: [ ConditionalWrapper( condition: _parentRoute != null, @@ -314,7 +315,7 @@ class TitleBarState extends State with WindowListener, RouteAware, Ada if (user.vintage != null) user.vintage!.humanReadable.text.color(context.theme.colorScheme.primary).color(context.theme.colorScheme.primary) else if (user.capabilities.isNotEmpty) - Text(user.capabilities.highest.translate(context)).color(context.theme.colorScheme.primary), + Text(user.capabilities.map((c) => c.translate(context)).join(', ')).color(context.theme.colorScheme.primary), ], ), ].show(), diff --git a/lib/src/slots/presentation/screens/slot_details_screen.dart b/lib/src/slots/presentation/screens/slot_details_screen.dart index c3f6db27..f9be4011 100644 --- a/lib/src/slots/presentation/screens/slot_details_screen.dart +++ b/lib/src/slots/presentation/screens/slot_details_screen.dart @@ -23,7 +23,9 @@ class _SlotDetailsScreenState extends State with AdaptiveStat void didChangeDependencies() { super.didChangeDependencies(); - Data.of(context).setParentRoute('/slots/overview/'); + Data.of(context) + ..setParentRoute('/slots/overview/') + ..setTrailingWidget(const SlotsViewSwitcher()); } @override diff --git a/lib/src/slots/presentation/screens/slot_master_screen.dart b/lib/src/slots/presentation/screens/slot_master_screen.dart index ea0dc470..52bb47bf 100644 --- a/lib/src/slots/presentation/screens/slot_master_screen.dart +++ b/lib/src/slots/presentation/screens/slot_master_screen.dart @@ -41,7 +41,9 @@ class _SlotMasterScreenState extends State with AdaptiveState, void didChangeDependencies() { super.didChangeDependencies(); - Data.of(context).setSearchController(searchController); + Data.of(context) + ..setSearchController(searchController) + ..setTrailingWidget(const SlotsViewSwitcher()); } void createSlot(Weekday weekday, SlotTimeUnit startUnit) { diff --git a/lib/src/slots/presentation/screens/slot_overview_screen.dart b/lib/src/slots/presentation/screens/slot_overview_screen.dart index e1d94181..8eb28121 100644 --- a/lib/src/slots/presentation/screens/slot_overview_screen.dart +++ b/lib/src/slots/presentation/screens/slot_overview_screen.dart @@ -1,19 +1,32 @@ import 'dart:collection'; import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:data_widget/data_widget.dart'; import 'package:eduplanner/eduplanner.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:intl/intl.dart'; /// Displays an overview of all slots for a supervisor. -class SlotOverviewScreen extends StatelessWidget with AdaptiveWidget, NoMobile { +class SlotOverviewScreen extends StatefulWidget { /// Displays an overview of all slots for a supervisor. const SlotOverviewScreen({super.key}); /// The date formatter. static final formatter = DateFormat('dd.MM.yyyy'); + @override + State createState() => _SlotOverviewScreenState(); +} + +class _SlotOverviewScreenState extends State with AdaptiveState, NoMobile { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + Data.of(context).setTrailingWidget(const SlotsViewSwitcher()); + } + @override Widget buildDesktop(BuildContext context) { final slots = context.watch(); @@ -34,7 +47,7 @@ class SlotOverviewScreen extends StatelessWidget with AdaptiveWidget, NoMobile { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '${group.key.translate(context)} ${formatter.format(group.key.nextDate)}', + '${group.key.translate(context)} ${SlotOverviewScreen.formatter.format(group.key.nextDate)}', style: context.theme.textTheme.titleLarge, ), Spacing.mediumVertical(), diff --git a/lib/src/slots/presentation/screens/slot_reservation_screen.dart b/lib/src/slots/presentation/screens/slot_reservation_screen.dart index 70ba814d..85d16d51 100644 --- a/lib/src/slots/presentation/screens/slot_reservation_screen.dart +++ b/lib/src/slots/presentation/screens/slot_reservation_screen.dart @@ -1,6 +1,7 @@ import 'dart:collection'; import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:data_widget/data_widget.dart'; import 'package:eduplanner/eduplanner.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; @@ -9,13 +10,25 @@ import 'package:intl/intl.dart'; import 'package:sliver_tools/sliver_tools.dart'; /// A screen for reserving slots. -class SlotReservationScreen extends StatelessWidget with AdaptiveWidget { +class SlotReservationScreen extends StatefulWidget { /// A screen for reserving slots. const SlotReservationScreen({super.key}); /// The date formatter. static final formatter = DateFormat('dd.MM.yyyy'); + @override + State createState() => _SlotReservationScreenState(); +} + +class _SlotReservationScreenState extends State with AdaptiveState { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + Data.of(context).setTrailingWidget(const SlotsViewSwitcher()); + } + @override Widget buildDesktop(BuildContext context) { final slots = context.watch(); @@ -41,7 +54,7 @@ class SlotReservationScreen extends StatelessWidget with AdaptiveWidget { Padding( padding: PaddingLeft(), child: Text( - '${group.key.translate(context)} ${formatter.format(group.key.nextDate)}', + '${group.key.translate(context)} ${SlotReservationScreen.formatter.format(group.key.nextDate)}', style: context.theme.textTheme.titleMedium, ).bold(), ), @@ -107,7 +120,7 @@ class SlotReservationScreen extends StatelessWidget with AdaptiveWidget { padding: PaddingLeft().Vertical(Spacing.smallSpacing), color: context.theme.scaffoldBackgroundColor, child: Text( - '${group.key.translate(context)} ${formatter.format(group.key.nextDate)}', + '${group.key.translate(context)} ${SlotReservationScreen.formatter.format(group.key.nextDate)}', style: context.theme.textTheme.titleMedium, ).bold(), ).show(stagger), diff --git a/lib/src/slots/presentation/widgets/slots_view_switcher.dart b/lib/src/slots/presentation/widgets/slots_view_switcher.dart index 4465e139..b8266849 100644 --- a/lib/src/slots/presentation/widgets/slots_view_switcher.dart +++ b/lib/src/slots/presentation/widgets/slots_view_switcher.dart @@ -1,24 +1,89 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:eduplanner/eduplanner.dart'; -import 'package:mcquenji_core/mcquenji_core.dart'; -import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter/material.dart'; -import 'package:eduplanner/src/slots/slots.dart'; -import 'package:eduplanner/gen/assets/assets.gen.dart'; -import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:flutter_vector_icons/flutter_vector_icons.dart'; +/// Provides a [DropdownMenu] that allows the user to switch between different slot views based on their [UserCapability]s. +/// +/// If there is <=1 options this returns [SizedBox.shrink]. class SlotsViewSwitcher extends StatelessWidget { + /// Provides a [DropdownMenu] that allows the user to switch between different slot views based on their [UserCapability]s. + /// + /// If there is <=1 options this returns [SizedBox.shrink]. const SlotsViewSwitcher({super.key}); - static const capabilityToRoute = { - UserCapability.slotMaster: '/slots/', - UserCapability.teacher: '/slots/overview/', - UserCapability.student: '/slots/book/', - }; - @override Widget build(BuildContext context) { final user = context.watch(); - return Container(); + final currentRoute = Modular.to.path; + final capabilities = user.state.data?.capabilities.toList() ?? []; + + if (capabilities.length <= 1) return const SizedBox.shrink(); + + return DropdownMenu( + inputDecorationTheme: context.theme.dropdownMenuTheme.inputDecorationTheme + ?.copyWith(fillColor: context.theme.cardColor, contentPadding: PaddingHorizontal(Spacing.smallSpacing)), + dropdownMenuEntries: [ + for (final r in capabilities) + DropdownMenuEntry( + value: r, + label: r.translateSlotRoute(context), + ), + ], + + trailingIcon: const Icon( + FontAwesome5Solid.chevron_down, + size: 13, + ), + requestFocusOnTap: false, // disable text input + initialSelection: SlotUserCapabilityX.capabilityFromRoute(currentRoute), + onSelected: (r) { + if (r == null) return; + + Modular.to.navigate(r.slotRoute); + }, + ); + } +} + +/// Extension on [UserCapability] for [SlotsViewSwitcher] to determine routes based on capabilities. +extension SlotUserCapabilityX on UserCapability { + /// Returns the route (from root) to the screen slot screen linked to this capability. + String get slotRoute { + return switch (this) { + UserCapability.teacher => '/slots/overview/', + UserCapability.student => '/slots/book/', + UserCapability.slotMaster => '/slots/', + }; + } + + /// Returns the [UserCapability] linked to the given [route]. + /// + /// If no matching capability is found this throws. + static UserCapability capabilityFromRoute(String route) { + if (route.startsWith(UserCapability.teacher.slotRoute)) { + return UserCapability.teacher; + } + if (route.startsWith(UserCapability.student.slotRoute)) { + return UserCapability.student; + } + if (route.startsWith(UserCapability.slotMaster.slotRoute)) { + return UserCapability.slotMaster; + } + + throw ArgumentError.value(route, 'route', 'Unkown route'); + } + + /// Returns a human readable name for the items in the dropdown menu. + String translateSlotRoute(BuildContext context) { + // TODO(mastermarcohd): localize values + + return switch (this) { + UserCapability.teacher => 'View Reservations', + UserCapability.student => 'Book Slots', + UserCapability.slotMaster => 'Manage Slots', + }; } } 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 063c59f2..07a70aca 100644 --- a/lib/src/theming/infra/services/material_theme_generator_service.dart +++ b/lib/src/theming/infra/services/material_theme_generator_service.dart @@ -166,7 +166,7 @@ class MaterialThemeGeneratorService extends ThemeGeneratorService { menuStyle: MenuStyle( padding: WidgetStatePropertyAll(PaddingAll(Spacing.smallSpacing).Vertical(Spacing.mediumSpacing)), backgroundColor: WidgetStatePropertyAll(themeBase.secondaryColor), - shape: WidgetStatePropertyAll(squircle()), + shape: WidgetStatePropertyAll(squircle(side: BorderSide(color: themeBase.tertiaryColor))), elevation: const WidgetStatePropertyAll(8), ), ), From 9f4db577e6e3e176e3cf257b680f6a53c2b85309 Mon Sep 17 00:00:00 2001 From: mcquenji <60017181+mcquenji@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:01:34 +0200 Subject: [PATCH 08/14] fix: do not crash the app when switching slots view --- lib/src/slots/presentation/widgets/slots_view_switcher.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/slots/presentation/widgets/slots_view_switcher.dart b/lib/src/slots/presentation/widgets/slots_view_switcher.dart index b8266849..63721f1e 100644 --- a/lib/src/slots/presentation/widgets/slots_view_switcher.dart +++ b/lib/src/slots/presentation/widgets/slots_view_switcher.dart @@ -42,7 +42,8 @@ class SlotsViewSwitcher extends StatelessWidget { onSelected: (r) { if (r == null) return; - Modular.to.navigate(r.slotRoute); + /// Do not use `navigate` as this will replace all loaded repositories. + Modular.to.pushNamed(r.slotRoute); }, ); } From 8e99680d00bfc6481463aab881af5f4b054a8292 Mon Sep 17 00:00:00 2001 From: mcquenji <60017181+mcquenji@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:14:59 +0200 Subject: [PATCH 09/14] feat: add icons to slotsviewswitcher --- .../presentation/widgets/slots_view_switcher.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/src/slots/presentation/widgets/slots_view_switcher.dart b/lib/src/slots/presentation/widgets/slots_view_switcher.dart index 63721f1e..09bf0a58 100644 --- a/lib/src/slots/presentation/widgets/slots_view_switcher.dart +++ b/lib/src/slots/presentation/widgets/slots_view_switcher.dart @@ -30,6 +30,8 @@ class SlotsViewSwitcher extends StatelessWidget { DropdownMenuEntry( value: r, label: r.translateSlotRoute(context), + labelWidget: Text(r.translateSlotRoute(context)), + leadingIcon: Icon(r.slotIcon, size: 16), ), ], @@ -37,6 +39,10 @@ class SlotsViewSwitcher extends StatelessWidget { FontAwesome5Solid.chevron_down, size: 13, ), + leadingIcon: Icon( + SlotUserCapabilityX.capabilityFromRoute(currentRoute).slotIcon, + size: 16, + ), requestFocusOnTap: false, // disable text input initialSelection: SlotUserCapabilityX.capabilityFromRoute(currentRoute), onSelected: (r) { @@ -87,4 +93,13 @@ extension SlotUserCapabilityX on UserCapability { UserCapability.slotMaster => 'Manage Slots', }; } + + /// The icon used to represent this view in the dropdown. + IconData get slotIcon { + return switch (this) { + UserCapability.teacher => FontAwesome5Solid.chalkboard_teacher, + UserCapability.student => FontAwesome5Solid.book, + UserCapability.slotMaster => FontAwesome5Solid.cogs, + }; + } } From 311b2b99f129a0ce31142aadd072d87e290098df Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Wed, 1 Oct 2025 18:48:09 +0200 Subject: [PATCH 10/14] feat: improve slot widget popOvers --- .../presentation/widgets/user_widget.dart | 21 ++- .../screens/slot_details_screen.dart | 21 +-- .../presentation/widgets/mapping_widget.dart | 27 ++++ .../widgets/slot_data_pop_over.dart | 144 +++++++++--------- .../widgets/slot_master_widget.dart | 17 +-- .../widgets/slot_overview_widget.dart | 28 +--- .../presentation/widgets/slot_widget.dart | 2 +- .../slots/presentation/widgets/widgets.dart | 1 + 8 files changed, 123 insertions(+), 138 deletions(-) create mode 100644 lib/src/slots/presentation/widgets/mapping_widget.dart diff --git a/lib/src/moodle/presentation/widgets/user_widget.dart b/lib/src/moodle/presentation/widgets/user_widget.dart index d6ef2991..7a2be02f 100644 --- a/lib/src/moodle/presentation/widgets/user_widget.dart +++ b/lib/src/moodle/presentation/widgets/user_widget.dart @@ -1,10 +1,11 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:eduplanner/eduplanner.dart'; import 'package:flutter/material.dart'; /// Displays the user's profile image and name. class UserWidget extends StatelessWidget { /// Displays the user's profile image and name. - const UserWidget({super.key, required this.user, this.size = 20, this.style, this.expand = false}); + const UserWidget({super.key, required this.user, this.size = 20, this.style, this.flexible = false}); /// The user to display. final User user; @@ -16,27 +17,23 @@ class UserWidget extends StatelessWidget { final TextStyle? style; /// If true the widget will expand to fill the available space. - final bool expand; + final bool flexible; @override Widget build(BuildContext context) { return Row( - mainAxisSize: expand ? MainAxisSize.max : MainAxisSize.min, + mainAxisSize: MainAxisSize.min, children: [ UserProfileImage( userId: user.id, size: size, ), Spacing.smallHorizontal(), - ConditionalWrapper( - condition: expand, - wrapper: (context, child) => Expanded(child: child), - child: Text( - user.fullname, - style: style, - overflow: TextOverflow.ellipsis, - ), - ), + Text( + user.fullname, + style: style, + overflow: TextOverflow.ellipsis, + ).flexible(), ], ); } diff --git a/lib/src/slots/presentation/screens/slot_details_screen.dart b/lib/src/slots/presentation/screens/slot_details_screen.dart index f9be4011..b40b6e09 100644 --- a/lib/src/slots/presentation/screens/slot_details_screen.dart +++ b/lib/src/slots/presentation/screens/slot_details_screen.dart @@ -46,7 +46,7 @@ class _SlotDetailsScreenState extends State with AdaptiveStat final courseVintage = slot.mappings .map((m) { - final course = courseRepository.filter(id: m.courseId).firstOrNull; + final course = courseRepository.getById(m.courseId); final vintage = m.vintage; return (course, vintage); @@ -105,7 +105,7 @@ class _SlotDetailsScreenState extends State with AdaptiveStat children: [ for (final supervisor in supervisors) UserWidget(user: supervisor), ], - ).stretch(), + ).expanded(), ], ), ), @@ -123,20 +123,9 @@ class _SlotDetailsScreenState extends State with AdaptiveStat spacing: Spacing.mediumSpacing, runSpacing: Spacing.mediumSpacing, children: [ - for (final (course, vintage) in courseVintage) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - CourseTag(course: course!), - Spacing.xsHorizontal(), - Text(course.name), - Spacing.smallHorizontal(), - Text(vintage.humanReadable), - Spacing.mediumHorizontal(), - ], - ), + for (final (course, vintage) in courseVintage) MappingWidget(course: course!, vintage: vintage), ], - ).stretch(), + ).expanded(), ], ), ), @@ -182,7 +171,7 @@ class _SlotDetailsScreenState extends State with AdaptiveStat ), ], ), - ), + ).stretch(), ), ).expanded(flex: 6), ], diff --git a/lib/src/slots/presentation/widgets/mapping_widget.dart b/lib/src/slots/presentation/widgets/mapping_widget.dart new file mode 100644 index 00000000..002bb895 --- /dev/null +++ b/lib/src/slots/presentation/widgets/mapping_widget.dart @@ -0,0 +1,27 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:eduplanner/src/moodle/moodle.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_utils/flutter_utils.dart'; + +class MappingWidget extends StatelessWidget { + const MappingWidget({super.key, required this.course, required this.vintage}); + + final MoodleCourse course; + + final Vintage vintage; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + CourseTag(course: course), + Spacing.xsHorizontal(), + Text(course.name, overflow: TextOverflow.ellipsis).flexible(flex: 3), + Spacing.smallHorizontal(), + Text(vintage.humanReadable, overflow: TextOverflow.ellipsis).flexible(flex: 1), + // Spacing.mediumHorizontal(), + ], + ); + } +} diff --git a/lib/src/slots/presentation/widgets/slot_data_pop_over.dart b/lib/src/slots/presentation/widgets/slot_data_pop_over.dart index 28ebe0f7..29c8b98f 100644 --- a/lib/src/slots/presentation/widgets/slot_data_pop_over.dart +++ b/lib/src/slots/presentation/widgets/slot_data_pop_over.dart @@ -17,12 +17,10 @@ class _SlotDataPopOverState extends State { bool parentHover = true; bool childHover = true; - void closePopUp() { - print("Penis $parentHover"); - + void closePopUp({bool forceClose = false}) { if (popupContext == null) return; - if (parentHover || childHover) return; + if ((parentHover || childHover) && !forceClose) return; Navigator.of(popupContext!).pop(); @@ -32,76 +30,84 @@ class _SlotDataPopOverState extends State { @override Widget build(BuildContext context) { return HoverBuilder( - // TODO(mastermarcohd): fix popOver hover - // Hoverbuilder doesnt work right due to PopOver barrier + cursor: SystemMouseCursors.basic, builder: (context, isHovering) { parentHover = isHovering; Future.delayed(const Duration(milliseconds: 300), closePopUp); - return GestureDetector( - // ignore: sort_child_properties_last - child: Row( - children: [ - widget.contentList[0], - if (widget.contentList.length > 1) ...[ - Spacing.smallHorizontal(), - Text("+${widget.contentList.length - 1} ..."), - ], - ], - ), - onTap: widget.contentList.length > 1 - ? () { - showPopover( - context: context, - bodyBuilder: (context) { - popupContext = context; - return HoverBuilder( - builder: (context, isHovering) { - childHover = isHovering; - Future.delayed(const Duration(milliseconds: 300), closePopUp); - return Card( - elevation: 16, - shape: squircle(side: BorderSide(color: context.theme.dividerColor)), - color: context.theme.cardColor, - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: widget.contentList.spaced(Spacing.smallSpacing), + return Row( + children: [ + Expanded( + child: widget.contentList[0], + ), + if (widget.contentList.length > 1) ...[ + Spacing.xsHorizontal(), + GestureDetector( + // ignore: sort_child_properties_last + child: Chip( + label: Text('+${widget.contentList.length - 1}'), + ), + onTap: widget.contentList.length > 1 + ? () { + if (popupContext != null) { + closePopUp(forceClose: true); + return; + } + showPopover( + context: context, + bodyBuilder: (context) { + popupContext = context; + return HoverBuilder( + cursor: SystemMouseCursors.basic, + builder: (context, isHovering) { + childHover = isHovering; + Future.delayed(const Duration(milliseconds: 300), closePopUp); + return Card( + elevation: 16, + shape: squircle(side: BorderSide(color: context.theme.dividerColor)), + color: context.theme.cardColor, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: widget.contentList.spaced(Spacing.smallSpacing), + ), + ), + ), + ); + }, + ); + }, + arrowHeight: 0, + arrowWidth: 0, + allowClicksOnBackground: true, + direction: PopoverDirection.bottom, + transition: PopoverTransition.other, + // contentDxOffset: -width + (context.size?.width ?? 0), + barrierDismissible: true, + barrierColor: Colors.transparent, + backgroundColor: Colors.transparent, + transitionDuration: const Duration(milliseconds: 300), + popoverTransitionBuilder: (animation, child) { + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: animation.drive( + Tween( + begin: const Offset(0, -0.02), + end: Offset.zero, + ), ), + child: child, ), - ), - ); - }, - ); - }, - arrowHeight: 0, - arrowWidth: 0, - // allowClicksOnBackground: true, - direction: PopoverDirection.bottom, - transition: PopoverTransition.other, - // contentDxOffset: -width + (context.size?.width ?? 0), - barrierDismissible: true, - barrierColor: Colors.transparent, - backgroundColor: Colors.transparent, - transitionDuration: const Duration(milliseconds: 300), - popoverTransitionBuilder: (animation, child) { - return FadeTransition( - opacity: animation, - child: SlideTransition( - position: animation.drive( - Tween( - begin: const Offset(0, -0.02), - end: Offset.zero, - ), - ), - child: child, - ), - ); - }, - ); - } - : null, + ); + }, + ); + } + : null, + ), + ], + ], ); }, ); diff --git a/lib/src/slots/presentation/widgets/slot_master_widget.dart b/lib/src/slots/presentation/widgets/slot_master_widget.dart index e4dce0a1..dd2686f6 100644 --- a/lib/src/slots/presentation/widgets/slot_master_widget.dart +++ b/lib/src/slots/presentation/widgets/slot_master_widget.dart @@ -75,7 +75,7 @@ class _SlotMasterWidgetState extends State { final courseVintage = widget.slot.mappings .map((m) { - final course = courseRepository.filter(id: m.courseId).firstOrNull; + final course = courseRepository.getById(m.courseId); final vintage = m.vintage; return (course, vintage); @@ -136,20 +136,7 @@ class _SlotMasterWidgetState extends State { Padding( padding: PaddingLeft(Spacing.mediumSpacing), child: SlotDataPopOver( - contentList: [ - for (final (course, vintage) in courseVintage) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - CourseTag(course: course!), - Spacing.xsHorizontal(), - Text(course.name), - Spacing.smallHorizontal(), - Text(vintage.humanReadable), - Spacing.mediumHorizontal(), - ], - ), - ], + contentList: [for (final (course, vintage) in courseVintage) MappingWidget(course: course!, vintage: vintage)], ), ).expanded(), Row( diff --git a/lib/src/slots/presentation/widgets/slot_overview_widget.dart b/lib/src/slots/presentation/widgets/slot_overview_widget.dart index 0fd32d45..b6821ac1 100644 --- a/lib/src/slots/presentation/widgets/slot_overview_widget.dart +++ b/lib/src/slots/presentation/widgets/slot_overview_widget.dart @@ -1,5 +1,4 @@ import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:carousel_slider/carousel_slider.dart'; import 'package:eduplanner/eduplanner.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; @@ -27,7 +26,7 @@ class _SlotOverviewWidgetState extends State { final courseVintage = widget.slot.mappings .map((m) { - final course = courseRepository.filter(id: m.courseId).firstOrNull; + final course = courseRepository.getById(m.courseId); final vintage = m.vintage; return (course, vintage); @@ -73,29 +72,8 @@ class _SlotOverviewWidgetState extends State { Text(context.t.slots_details_mappingsCount(courseVintage.length)), ], ), - CarouselSlider( - disableGesture: true, - options: CarouselOptions( - autoPlay: true, - enableInfiniteScroll: false, - height: 40, - scrollDirection: Axis.vertical, - ), - items: [ - for (final (course, vintage) in courseVintage) - Row( - children: [ - CourseTag(course: course!), - Spacing.xsHorizontal(), - Text( - course.name, - overflow: TextOverflow.ellipsis, - ).expanded(flex: 3), - Spacing.smallHorizontal(), - Text(vintage.humanReadable).expanded(), - ], - ), - ], + SlotDataPopOver( + contentList: [for (final (course, vintage) in courseVintage) MappingWidget(course: course!, vintage: vintage)], ), ], ), diff --git a/lib/src/slots/presentation/widgets/slot_widget.dart b/lib/src/slots/presentation/widgets/slot_widget.dart index 838b880e..c44c873f 100644 --- a/lib/src/slots/presentation/widgets/slot_widget.dart +++ b/lib/src/slots/presentation/widgets/slot_widget.dart @@ -285,7 +285,7 @@ class _SlotWidgetState extends State with AdaptiveState { for (final supervisor in supervisors) UserWidget( user: supervisor, - expand: true, + flexible: true, ), ], ).expanded(), diff --git a/lib/src/slots/presentation/widgets/widgets.dart b/lib/src/slots/presentation/widgets/widgets.dart index d96f7a03..0cedd767 100644 --- a/lib/src/slots/presentation/widgets/widgets.dart +++ b/lib/src/slots/presentation/widgets/widgets.dart @@ -1,4 +1,5 @@ export 'edit_slot_dialog.dart'; +export 'mapping_widget.dart'; export 'number_spinner.dart'; export 'slot_data_pop_over.dart'; export 'slot_master_widget.dart'; From 1cfd24f79b701acf7c44acc849d8b945ad5543bd Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Wed, 1 Oct 2025 18:49:10 +0200 Subject: [PATCH 11/14] fix: prevent unitialized repositories --- lib/src/app/presentation/widgets/sidebar_target.dart | 2 +- lib/src/app/presentation/widgets/title_bar.dart | 4 ++-- lib/src/app/utils/no_mobile_utils.dart | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/app/presentation/widgets/sidebar_target.dart b/lib/src/app/presentation/widgets/sidebar_target.dart index d6aac1f9..47487218 100644 --- a/lib/src/app/presentation/widgets/sidebar_target.dart +++ b/lib/src/app/presentation/widgets/sidebar_target.dart @@ -49,7 +49,7 @@ class _SidebarTargetState extends State with AdaptiveState { if (Modular.to.isActive(route)) return; widget.onTap?.call(); - Modular.to.navigate(widget.route); + Modular.to.pushNamed(widget.route); setState(() { isActive = true; diff --git a/lib/src/app/presentation/widgets/title_bar.dart b/lib/src/app/presentation/widgets/title_bar.dart index 359e74e8..96efe431 100644 --- a/lib/src/app/presentation/widgets/title_bar.dart +++ b/lib/src/app/presentation/widgets/title_bar.dart @@ -215,7 +215,7 @@ class TitleBarState extends State with WindowListener, RouteAware, Ada ConditionalWrapper( condition: _parentRoute != null, wrapper: (_, child) => TextButton( - onPressed: () => Modular.to.navigate(_parentRoute!), + onPressed: () => Modular.to.pushNamed(_parentRoute!), child: Row( children: [ const Icon(FontAwesome5Solid.angle_left), @@ -368,7 +368,7 @@ class TitleBarState extends State with WindowListener, RouteAware, Ada ConditionalWrapper( condition: _parentRoute != null, wrapper: (_, child) => TextButton( - onPressed: () => Modular.to.navigate(_parentRoute!), + onPressed: () => Modular.to.pushNamed(_parentRoute!), child: Row( children: [ const Icon(FontAwesome5Solid.angle_left), diff --git a/lib/src/app/utils/no_mobile_utils.dart b/lib/src/app/utils/no_mobile_utils.dart index bb619bcd..2ad83f93 100644 --- a/lib/src/app/utils/no_mobile_utils.dart +++ b/lib/src/app/utils/no_mobile_utils.dart @@ -19,7 +19,7 @@ mixin NoMobile on Adaptive { image: Assets.mobile, ).expanded(), ElevatedButton( - onPressed: () => Modular.to.navigate('/dashboard/'), + onPressed: () => Modular.to.pushNamed('/dashboard/'), child: Text(context.t.app_noMobile_goBack), ), ], From ad819281e3528badd27315939d90e2d87c94d914 Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Thu, 2 Oct 2025 04:32:07 +0200 Subject: [PATCH 12/14] fix: adjust alignment in slot overview screen --- .../screens/slot_overview_screen.dart | 85 ++++++++++--------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/lib/src/slots/presentation/screens/slot_overview_screen.dart b/lib/src/slots/presentation/screens/slot_overview_screen.dart index 8eb28121..60bbe9e4 100644 --- a/lib/src/slots/presentation/screens/slot_overview_screen.dart +++ b/lib/src/slots/presentation/screens/slot_overview_screen.dart @@ -38,47 +38,50 @@ class _SlotOverviewScreenState extends State with AdaptiveSt return Padding( padding: PaddingAll(), - child: SingleChildScrollView( - child: Column( - spacing: Spacing.largeSpacing, - children: [ - for (final group in groups.entries) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${group.key.translate(context)} ${SlotOverviewScreen.formatter.format(group.key.nextDate)}', - style: context.theme.textTheme.titleLarge, - ), - Spacing.mediumVertical(), - for (final timespan in group.value.entries) - Column( - children: [ - Row( - children: [ - const Icon(Icons.access_time), - Spacing.xsHorizontal(), - Text('${timespan.key.$1.humanReadable()} - ${timespan.key.$2.humanReadable()}'), - ], - ), - Spacing.smallVertical(), - Wrap( - spacing: Spacing.mediumSpacing, - runSpacing: Spacing.mediumSpacing, - children: [ - for (final slot in timespan.value) - SizedBox( - key: ValueKey(slot), - width: 300, - child: SlotOverviewWidget(slot: slot), - ), - ], - ).stretch(), - ], - ).paddingOnly(bottom: Spacing.largeSpacing), - ], - ), - ], + child: Align( + alignment: Alignment.topLeft, + child: SingleChildScrollView( + child: Column( + spacing: Spacing.largeSpacing, + children: [ + for (final group in groups.entries) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${group.key.translate(context)} ${SlotOverviewScreen.formatter.format(group.key.nextDate)}', + style: context.theme.textTheme.titleLarge, + ), + Spacing.mediumVertical(), + for (final timespan in group.value.entries) + Column( + children: [ + Row( + children: [ + const Icon(Icons.access_time), + Spacing.xsHorizontal(), + Text('${timespan.key.$1.humanReadable()} - ${timespan.key.$2.humanReadable()}'), + ], + ), + Spacing.smallVertical(), + Wrap( + spacing: Spacing.mediumSpacing, + runSpacing: Spacing.mediumSpacing, + children: [ + for (final slot in timespan.value) + SizedBox( + key: ValueKey(slot), + width: 300, + child: SlotOverviewWidget(slot: slot), + ), + ], + ).stretch(), + ], + ).paddingOnly(bottom: Spacing.largeSpacing), + ], + ), + ], + ), ), ), ); From 681339c406e56dc1ac43cda4bb75ded88b4ac2a7 Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Thu, 2 Oct 2025 18:09:08 +0200 Subject: [PATCH 13/14] fix: titlebar no longer displays wrong screen name --- lib/src/app/presentation/widgets/sidebar_target.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/app/presentation/widgets/sidebar_target.dart b/lib/src/app/presentation/widgets/sidebar_target.dart index 47487218..d6aac1f9 100644 --- a/lib/src/app/presentation/widgets/sidebar_target.dart +++ b/lib/src/app/presentation/widgets/sidebar_target.dart @@ -49,7 +49,7 @@ class _SidebarTargetState extends State with AdaptiveState { if (Modular.to.isActive(route)) return; widget.onTap?.call(); - Modular.to.pushNamed(widget.route); + Modular.to.navigate(widget.route); setState(() { isActive = true; From e96b07df0a055bd2b15074a9f04d29867e622148 Mon Sep 17 00:00:00 2001 From: MasterMarcoHD <57987974+MasterMarcoHD@users.noreply.github.com> Date: Thu, 2 Oct 2025 18:57:32 +0200 Subject: [PATCH 14/14] fix: align student slot view to top left --- .../screens/course_overview_screen.dart | 2 +- .../screens/courses_overview_screen.dart | 3 +- .../widgets/slot_data_pop_over.dart | 4 ++ pubspec.lock | 50 +++++++++---------- 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/lib/src/course_overview/presentation/screens/course_overview_screen.dart b/lib/src/course_overview/presentation/screens/course_overview_screen.dart index 43c67fff..352eb846 100644 --- a/lib/src/course_overview/presentation/screens/course_overview_screen.dart +++ b/lib/src/course_overview/presentation/screens/course_overview_screen.dart @@ -162,7 +162,7 @@ class _CourseOverviewScreenState extends State with Adapti ).toList(), ), ), - ), + ).greedy(), ); } } diff --git a/lib/src/course_overview/presentation/screens/courses_overview_screen.dart b/lib/src/course_overview/presentation/screens/courses_overview_screen.dart index 63c025cf..6dc44452 100644 --- a/lib/src/course_overview/presentation/screens/courses_overview_screen.dart +++ b/lib/src/course_overview/presentation/screens/courses_overview_screen.dart @@ -40,6 +40,7 @@ class _CoursesOverviewScreenState extends State with Adap return Padding( padding: PaddingAll(), child: SingleChildScrollView( + clipBehavior: Clip.none, child: Wrap( runSpacing: Spacing.mediumSpacing, spacing: Spacing.mediumSpacing, @@ -51,7 +52,7 @@ class _CoursesOverviewScreenState extends State with Adap ), ].show(), ), - ), + ).greedy(), ); } } diff --git a/lib/src/slots/presentation/widgets/slot_data_pop_over.dart b/lib/src/slots/presentation/widgets/slot_data_pop_over.dart index 29c8b98f..be33b205 100644 --- a/lib/src/slots/presentation/widgets/slot_data_pop_over.dart +++ b/lib/src/slots/presentation/widgets/slot_data_pop_over.dart @@ -34,6 +34,10 @@ class _SlotDataPopOverState extends State { builder: (context, isHovering) { parentHover = isHovering; Future.delayed(const Duration(milliseconds: 300), closePopUp); + if (widget.contentList.isEmpty) { + closePopUp(forceClose: true); + return const SizedBox.shrink(); + } return Row( children: [ Expanded( diff --git a/pubspec.lock b/pubspec.lock index fb69a662..a156c9d9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -77,10 +77,10 @@ packages: dependency: "direct main" description: name: awesome_extensions - sha256: d61c85a583c753e106fcbff392c705c8cd72f6fcacc86ddd1bdcc0a6f498efb3 + sha256: "41eecb104e84df80f7f51d01cefb4c4046dc5ca40c2f4379745a472140e79fad" url: "https://pub.dev" source: hosted - version: "2.0.25" + version: "2.0.26" bloc: dependency: "direct main" description: @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: built_value - sha256: ba95c961bafcd8686d1cf63be864eb59447e795e124d98d6a27d91fcd13602fb + sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d url: "https://pub.dev" source: hosted - version: "8.11.1" + version: "8.12.0" carousel_slider: dependency: "direct main" description: @@ -229,10 +229,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" url: "https://pub.dev" source: hosted - version: "4.10.1" + version: "4.11.0" collection: dependency: "direct main" description: @@ -514,10 +514,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845 + sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" flutter_test: dependency: "direct dev" description: flutter @@ -528,7 +528,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "76cb50a736e826936f4678eeee2b0b04614094a7" + resolved-ref: be90927b2e5d2f04ce0398de99a532fccfb85600 url: "https://github.com/mcquenji/flutter_utils.git" source: git version: "0.0.2" @@ -557,10 +557,10 @@ packages: dependency: "direct main" description: name: font_awesome_flutter - sha256: b738e35f8bb4957896c34957baf922f99c5d415b38ddc8b070d14b7fa95715d4 + sha256: "27af5982e6c510dec1ba038eff634fa284676ee84e3fd807225c80c4ad869177" url: "https://pub.dev" source: hosted - version: "10.9.1" + version: "10.10.0" freezed: dependency: "direct dev" description: @@ -725,10 +725,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "11.0.1" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: @@ -985,10 +985,10 @@ packages: dependency: transitive description: name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.5.2" popover: dependency: "direct main" description: @@ -1155,10 +1155,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74 + sha256: "0b0f98d535319cb5cdd4f65783c2a54ee6d417a2f093dbb18be3e36e4c3d181f" url: "https://pub.dev" source: hosted - version: "2.4.12" + version: "2.4.14" shared_preferences_foundation: dependency: transitive description: @@ -1344,10 +1344,10 @@ packages: dependency: transitive description: name: system_info2 - sha256: "65206bbef475217008b5827374767550a5420ce70a04d2d7e94d1d2253f3efc9" + sha256: b937736ecfa63c45b10dde1ceb6bb30e5c0c340e14c441df024150679d65ac43 url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.1.0" term_glyph: dependency: transitive description: @@ -1440,10 +1440,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7" + sha256: c0fb544b9ac7efa10254efaf00a951615c362d1ea1877472f8f6c0fa00fcf15b url: "https://pub.dev" source: hosted - version: "6.3.18" + version: "6.3.23" url_launcher_ios: dependency: transitive description: @@ -1544,18 +1544,18 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "15.0.2" watcher: dependency: transitive description: name: watcher - sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c" + sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.1.4" web: dependency: transitive description: