diff --git a/android/fastlane/metadata/android/en-GB/changelogs/10004.txt b/android/fastlane/metadata/android/en-GB/changelogs/10004.txt new file mode 100644 index 000000000..07a265811 --- /dev/null +++ b/android/fastlane/metadata/android/en-GB/changelogs/10004.txt @@ -0,0 +1,6 @@ +Fixed + - Error message shown when cancelling profile picture selection + +Added + - Card containing the class name and its associated lecturer on the class info page + - Ability to click on a lecturer from an event and display more details about them diff --git a/android/fastlane/metadata/android/en-US/changelogs/10004.txt b/android/fastlane/metadata/android/en-US/changelogs/10004.txt new file mode 100644 index 000000000..07a265811 --- /dev/null +++ b/android/fastlane/metadata/android/en-US/changelogs/10004.txt @@ -0,0 +1,6 @@ +Fixed + - Error message shown when cancelling profile picture selection + +Added + - Card containing the class name and its associated lecturer on the class info page + - Ability to click on a lecturer from an event and display more details about them diff --git a/android/fastlane/metadata/android/ro/changelogs/10004.txt b/android/fastlane/metadata/android/ro/changelogs/10004.txt new file mode 100644 index 000000000..309f70ffd --- /dev/null +++ b/android/fastlane/metadata/android/ro/changelogs/10004.txt @@ -0,0 +1,7 @@ +Rezolvat + - Mesaj de eroare afișat la anularea selectării imaginii de profil + +Adăugat + - Card care conține numele materiei și cadrul didactic titular pe pagina de informații a unei materii + - Posibilitatea de a apăsa pe un profesor asociat unui eveniment și de a afișa mai multe + detalii despre acesta diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 8972462bf..1840bb7cf 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -204,6 +204,7 @@ class MessageLookup extends MessageLookupByLibrary { "messageWelcomeSimple" : MessageLookupByLibrary.simpleMessage("Welcome!"), "messageYouCanContribute" : MessageLookupByLibrary.simpleMessage("You can contribute to the app data, but you first need to request permissions."), "navigationAskPermissions" : MessageLookupByLibrary.simpleMessage("Ask for permissions"), + "navigationClassInfo" : MessageLookupByLibrary.simpleMessage("Class information"), "navigationClasses" : MessageLookupByLibrary.simpleMessage("Classes"), "navigationEventDetails" : MessageLookupByLibrary.simpleMessage("Event details"), "navigationFilter" : MessageLookupByLibrary.simpleMessage("Filter"), diff --git a/lib/generated/intl/messages_ro.dart b/lib/generated/intl/messages_ro.dart index c7f746076..09dde5326 100644 --- a/lib/generated/intl/messages_ro.dart +++ b/lib/generated/intl/messages_ro.dart @@ -204,6 +204,7 @@ class MessageLookup extends MessageLookupByLibrary { "messageWelcomeSimple" : MessageLookupByLibrary.simpleMessage("Bine ai venit!"), "messageYouCanContribute" : MessageLookupByLibrary.simpleMessage("Poți contribui la datele din aplicație, dar trebuie mai întâi să ceri permisiuni."), "navigationAskPermissions" : MessageLookupByLibrary.simpleMessage("Cere permisiuni"), + "navigationClassInfo" : MessageLookupByLibrary.simpleMessage("Informații materie"), "navigationClasses" : MessageLookupByLibrary.simpleMessage("Materii"), "navigationEventDetails" : MessageLookupByLibrary.simpleMessage("Detalii eveniment"), "navigationFilter" : MessageLookupByLibrary.simpleMessage("Filtru"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 0c9380995..a3384f510 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -1583,6 +1583,16 @@ class S { ); } + /// `Class information` + String get navigationClassInfo { + return Intl.message( + 'Class information', + name: 'navigationClassInfo', + desc: '', + args: [], + ); + } + /// `Show all` String get filterMenuShowAll { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 179086718..426f3b188 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -162,6 +162,7 @@ "navigationClasses": "Classes", "navigationEventDetails": "Event details", "navigationNewsFeed": "News feed", + "navigationClassInfo": "Class information", "filterMenuShowAll": "Show all", "filterMenuShowMine": "Show only mine", diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb index 5e8bf8d16..6114c9310 100644 --- a/lib/l10n/intl_ro.arb +++ b/lib/l10n/intl_ro.arb @@ -162,6 +162,7 @@ "navigationClasses": "Materii", "navigationEventDetails": "Detalii eveniment", "navigationNewsFeed": "Știri", + "navigationClassInfo": "Informații materie", "filterMenuShowAll": "Arată tot", "filterMenuShowMine": "Arată doar pe ale mele", diff --git a/lib/pages/classes/view/class_view.dart b/lib/pages/classes/view/class_view.dart index df24085cf..ba687e22f 100644 --- a/lib/pages/classes/view/class_view.dart +++ b/lib/pages/classes/view/class_view.dart @@ -4,10 +4,14 @@ import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; import 'package:acs_upb_mobile/pages/classes/view/grading_view.dart'; import 'package:acs_upb_mobile/pages/classes/view/shortcut_view.dart'; +import 'package:acs_upb_mobile/pages/people/service/person_provider.dart'; +import 'package:acs_upb_mobile/pages/people/view/person_view.dart'; import 'package:acs_upb_mobile/resources/custom_icons.dart'; import 'package:acs_upb_mobile/resources/utils.dart'; import 'package:acs_upb_mobile/widgets/button.dart'; +import 'package:acs_upb_mobile/widgets/class_icon.dart'; import 'package:acs_upb_mobile/widgets/dialog.dart'; +import 'package:acs_upb_mobile/widgets/icon_text.dart'; import 'package:acs_upb_mobile/widgets/scaffold.dart'; import 'package:acs_upb_mobile/widgets/toast.dart'; import 'package:flutter/cupertino.dart'; @@ -15,24 +19,6 @@ import 'package:flutter/material.dart'; import 'package:positioned_tap_detector/positioned_tap_detector.dart'; import 'package:provider/provider.dart'; -extension ClassExtension on ClassHeader { - Color get colorFromAcronym { - int r = 0, g = 0, b = 0; - if (acronym.isNotEmpty) { - b = acronym[0].codeUnitAt(0); - if (acronym.length >= 2) { - g = acronym[1].codeUnitAt(0); - if (acronym.length >= 3) { - r = acronym[2].codeUnitAt(0); - } - } - } - const int brightnessFactor = 2; - return Color.fromRGBO( - r * brightnessFactor, g * brightnessFactor, b * brightnessFactor, 1); - } -} - class ClassView extends StatefulWidget { const ClassView({Key key, this.classHeader}) : super(key: key); @@ -50,7 +36,7 @@ class _ClassViewState extends State { final classProvider = Provider.of(context); return AppScaffold( - title: Text(widget.classHeader.name), + title: Text(S.of(context).navigationClassInfo), body: FutureBuilder( future: classProvider.fetchClassInfo(widget.classHeader, context: context), @@ -64,6 +50,9 @@ class _ClassViewState extends State { padding: const EdgeInsets.symmetric(horizontal: 8), child: Column( children: [ + const SizedBox(height: 8), + lecturerCard(context), + const SizedBox(height: 8), shortcuts(context), const SizedBox(height: 8), GradingChart( @@ -241,4 +230,67 @@ class _ClassViewState extends State { ), ); } + + Widget lecturerCard(BuildContext context) { + final personProvider = Provider.of(context); + + return Card( + key: const Key('LecturerCard'), + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + ClassIcon(classHeader: widget.classHeader), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IconText( + icon: Icons.class_, + text: widget.classHeader.name ?? '-', + style: Theme.of(context).textTheme.bodyText1, + ), + FutureBuilder( + future: personProvider + .mostRecentLecturer(widget.classHeader.id), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + final lecturerName = snapshot.data; + return GestureDetector( + onTap: () async { + final lecturer = + await personProvider.fetchPerson(lecturerName); + if (lecturer != null) { + await showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (BuildContext buildContext) => + PersonView(person: lecturer)); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IconText( + icon: Icons.person, + text: lecturerName ?? '-', + style: Theme.of(context).textTheme.bodyText1, + ), + ], + ), + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ], + ), + ), + ], + ), + ), + ); + } } diff --git a/lib/pages/classes/view/classes_page.dart b/lib/pages/classes/view/classes_page.dart index 9745f8b6d..a833efd6d 100644 --- a/lib/pages/classes/view/classes_page.dart +++ b/lib/pages/classes/view/classes_page.dart @@ -3,12 +3,11 @@ import 'package:acs_upb_mobile/generated/l10n.dart'; import 'package:acs_upb_mobile/pages/classes/model/class.dart'; import 'package:acs_upb_mobile/pages/classes/service/class_provider.dart'; import 'package:acs_upb_mobile/pages/classes/view/class_view.dart'; +import 'package:acs_upb_mobile/widgets/class_icon.dart'; import 'package:acs_upb_mobile/widgets/error_page.dart'; import 'package:acs_upb_mobile/widgets/icon_text.dart'; import 'package:acs_upb_mobile/widgets/scaffold.dart'; import 'package:acs_upb_mobile/widgets/spoiler.dart'; -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:black_hole_flutter/black_hole_flutter.dart'; import 'package:flutter/material.dart'; import 'package:pedantic/pedantic.dart'; import 'package:provider/provider.dart'; @@ -383,29 +382,9 @@ class _ClassListItemState extends State { @override Widget build(BuildContext context) { return ListTile( - leading: CircleAvatar( - backgroundColor: widget.classHeader.colorFromAcronym, - child: Container( - width: 30, - child: (widget.selectable && selected) - ? Icon( - Icons.check, - color: - widget.classHeader.colorFromAcronym.highEmphasisOnColor, - ) - : Align( - alignment: Alignment.center, - child: AutoSizeText( - widget.classHeader.acronym, - minFontSize: 0, - maxLines: 1, - style: TextStyle( - color: widget - .classHeader.colorFromAcronym.highEmphasisOnColor, - ), - ), - ), - ), + leading: ClassIcon( + classHeader: widget.classHeader, + selected: widget.selectable && selected, ), title: Text( widget.classHeader.name, diff --git a/lib/pages/people/service/person_provider.dart b/lib/pages/people/service/person_provider.dart index e0c4bcf95..aafa2aa67 100644 --- a/lib/pages/people/service/person_provider.dart +++ b/lib/pages/people/service/person_provider.dart @@ -56,4 +56,28 @@ class PersonProvider with ChangeNotifier { return null; } } + + Future mostRecentLecturer(String classId, + {BuildContext context}) async { + try { + final QuerySnapshot query = await FirebaseFirestore.instance + .collection('events') + .where('class', isEqualTo: classId) + .where('type', isEqualTo: 'lecture') + .orderBy('start', descending: true) + .limit(1) + .get(); + + if (query == null || query.docs.isEmpty) { + return null; + } + return query.docs.first.get('teacher'); + } catch (e) { + print(e); + if (context != null) { + AppToast.show(S.of(context).errorSomethingWentWrong); + } + return null; + } + } } diff --git a/lib/pages/timetable/view/events/event_view.dart b/lib/pages/timetable/view/events/event_view.dart index 78ecd1e57..2573cda18 100644 --- a/lib/pages/timetable/view/events/event_view.dart +++ b/lib/pages/timetable/view/events/event_view.dart @@ -5,6 +5,7 @@ import 'package:acs_upb_mobile/pages/classes/view/class_view.dart'; import 'package:acs_upb_mobile/pages/classes/view/classes_page.dart'; import 'package:acs_upb_mobile/pages/filter/model/filter.dart'; import 'package:acs_upb_mobile/pages/filter/service/filter_provider.dart'; +import 'package:acs_upb_mobile/pages/people/view/person_view.dart'; import 'package:acs_upb_mobile/pages/timetable/model/events/class_event.dart'; import 'package:acs_upb_mobile/pages/timetable/model/events/recurring_event.dart'; import 'package:acs_upb_mobile/pages/timetable/model/events/uni_event.dart'; @@ -174,20 +175,34 @@ class _EventViewState extends State { if (widget.eventInstance.mainEvent is ClassEvent) Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), - child: Row( - children: [ - const Padding( - padding: EdgeInsets.all(8), - child: Icon(Icons.person), - ), - const SizedBox(width: 16), - Text( - (widget.eventInstance.mainEvent as ClassEvent) - .teacher - .name ?? - S.of(context).labelUnknown, - style: Theme.of(context).textTheme.subtitle1), - ], + child: GestureDetector( + onTap: () { + if ((widget.eventInstance.mainEvent as ClassEvent).teacher != + null) { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (BuildContext buildContext) => PersonView( + person: + (widget.eventInstance.mainEvent as ClassEvent) + .teacher)); + } + }, + child: Row( + children: [ + const Padding( + padding: EdgeInsets.all(8), + child: Icon(Icons.person), + ), + const SizedBox(width: 16), + Text( + (widget.eventInstance.mainEvent as ClassEvent) + .teacher + .name ?? + S.of(context).labelUnknown, + style: Theme.of(context).textTheme.subtitle1), + ], + ), ), ), ]), diff --git a/lib/widgets/class_icon.dart b/lib/widgets/class_icon.dart new file mode 100644 index 000000000..3bbbe4f23 --- /dev/null +++ b/lib/widgets/class_icon.dart @@ -0,0 +1,59 @@ +import 'package:acs_upb_mobile/pages/classes/model/class.dart'; +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:black_hole_flutter/black_hole_flutter.dart'; + +extension ClassExtension on ClassHeader { + Color get colorFromAcronym { + int r = 0, g = 0, b = 0; + if (acronym.isNotEmpty) { + b = acronym[0].codeUnitAt(0); + if (acronym.length >= 2) { + g = acronym[1].codeUnitAt(0); + if (acronym.length >= 3) { + r = acronym[2].codeUnitAt(0); + } + } + } + const int brightnessFactor = 2; + return Color.fromRGBO( + r * brightnessFactor, g * brightnessFactor, b * brightnessFactor, 1); + } +} + +class ClassIcon extends StatelessWidget { + const ClassIcon({ + @required this.classHeader, + Key key, + this.selected = false, + }) : super(key: key); + + final ClassHeader classHeader; + final bool selected; + + @override + Widget build(BuildContext context) { + return CircleAvatar( + backgroundColor: classHeader.colorFromAcronym, + child: Container( + width: 30, + child: selected + ? Icon( + Icons.check, + color: classHeader.colorFromAcronym.highEmphasisOnColor, + ) + : Align( + alignment: Alignment.center, + child: AutoSizeText( + classHeader.acronym, + minFontSize: 0, + maxLines: 1, + style: TextStyle( + color: classHeader.colorFromAcronym.highEmphasisOnColor, + ), + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 150863c5d..9483c625c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -98,7 +98,7 @@ packages: name: cached_network_image url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "2.5.1" characters: dependency: transitive description: @@ -154,7 +154,7 @@ packages: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "3.6.0" + version: "3.7.0" collection: dependency: transitive description: @@ -383,7 +383,7 @@ packages: name: flutter_cache_manager url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" flutter_colorpicker: dependency: "direct main" description: @@ -678,7 +678,7 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "1.6.27" + version: "1.6.28" path_provider_linux: dependency: transitive description: @@ -886,7 +886,7 @@ packages: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "1.0.3+1" + version: "1.0.3+3" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ac1576c53..acbca827d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ description: A mobile application for students at ACS UPB. # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # # ACS UPB Mobile uses semantic versioning. You can read more in the CONTRIBUTING.md file. -version: 1.2.6+3 +version: 1.2.7+4 environment: sdk: ">=2.7.0 <3.0.0" diff --git a/test/integration_test.dart b/test/integration_test.dart index df7dceca1..300880b4f 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -397,6 +397,10 @@ Future main() async { ), ])); + when(mockPersonProvider.mostRecentLecturer(any, + context: anyNamed('context'))) + .thenAnswer((_) => Future.value('Jane Doe')); + mockQuestionProvider = MockQuestionProvider(); // ignore: invalid_use_of_protected_member when(mockQuestionProvider.hasListeners).thenReturn(false); @@ -1121,6 +1125,8 @@ Future main() async { await tester.pumpAndSettle(); expect(find.byType(ClassView), findsOneWidget); + expect(find.byIcon(Icons.person), findsOneWidget); + expect(find.byKey(const Key('LecturerCard')), findsOneWidget); // Press back await tester.tap(find.byIcon(Icons.arrow_back)); @@ -1130,6 +1136,15 @@ Future main() async { expect(find.byIcon(Icons.person), findsOneWidget); expect(find.text('Jane Doe'), findsOneWidget); + await tester.tap(find.byIcon(Icons.person)); + await tester.pumpAndSettle(); + + expect(find.byType(PersonView), findsOneWidget); + + // Press back + await tester.tap(find.byIcon(Icons.arrow_back)); + await tester.pumpAndSettle(); + // Open edit event page await tester.tap(find.byIcon(Icons.edit)); await tester.pumpAndSettle();