diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3f7645fd..4e797e9e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,6 +5,9 @@ + + + diff --git a/android/app/src/main/kotlin/de/tum/in/tumcampus/MainActivity.kt b/android/app/src/main/kotlin/de/tum/in/tumcampus/MainActivity.kt index da99a231..e0993657 100644 --- a/android/app/src/main/kotlin/de/tum/in/tumcampus/MainActivity.kt +++ b/android/app/src/main/kotlin/de/tum/in/tumcampus/MainActivity.kt @@ -21,6 +21,6 @@ class MainActivity : FlutterActivity() { fun isPhone(context: Context): Boolean { val resources = context.resources val configuration = resources.configuration - val screenWidthDp = configuration.screenWidthDp - return screenWidthDp <= resources.getDimension(R.dimen.min_tablet_width_dp) + val screenWidthDp = configuration.smallestScreenWidthDp + return screenWidthDp <= resources.getInteger(R.integer.min_tablet_width_dp) } \ No newline at end of file diff --git a/android/app/src/main/res/values/dimens.xml b/android/app/src/main/res/values/dimens.xml index 250f171b..c39c1960 100644 --- a/android/app/src/main/res/values/dimens.xml +++ b/android/app/src/main/res/values/dimens.xml @@ -4,5 +4,5 @@ 8dp 4dp 2dp - 600dp + 600 \ No newline at end of file diff --git a/assets/images/logos/Moodle.png b/assets/images/logos/Moodle.png new file mode 100644 index 00000000..f99da172 Binary files /dev/null and b/assets/images/logos/Moodle.png differ diff --git a/assets/translations/de.json b/assets/translations/de.json index 8948f7ad..d4211719 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -230,6 +230,7 @@ "tumOnlineDegraded": "TUMonline Services sind derzeit beeinträchtigt!", "tumOnlineMaintenance": "TUMonline Services werden derzeit gewartet!", "campus": "Campus", + "moodle": "Moodle", "studies": "Studium", "suggested": "Interessante {}", "more": "Mehr", diff --git a/assets/translations/en.json b/assets/translations/en.json index 09570c12..55ee3b54 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -230,6 +230,7 @@ "tumOnlineDegraded": "TUMonline Services are currently degraded!", "tumOnlineMaintenance": "TUMonline Services are currently under maintenance!", "campus": "Campus", + "moodle" : "Moodle", "studies": "Studies", "suggested": "Suggested {}", "more": "More", diff --git a/lib/base/enums/shortcut_item.dart b/lib/base/enums/shortcut_item.dart index 010d808f..b9cd31a0 100644 --- a/lib/base/enums/shortcut_item.dart +++ b/lib/base/enums/shortcut_item.dart @@ -8,6 +8,7 @@ enum ShortcutItemType { studyRooms(en: "Study Rooms", de: "Lernräume"), calendar(en: "Calendar", de: "Kalendar"), studies(en: "Studies", de: "Studium"), + moodle(en: "Moodle", de: "Moodle"), roomSearch(en: "Room Search", de: "Raumsuche"); final String en; @@ -47,6 +48,8 @@ extension Routing on ShortcutItemType { return routes.calendar; case ShortcutItemType.studies: return routes.studies; + case ShortcutItemType.moodle: + return routes.moodle; case ShortcutItemType.roomSearch: return routes.roomSearch; } diff --git a/lib/base/networking/protocols/api.dart b/lib/base/networking/protocols/api.dart index cc86bf80..c3490e74 100644 --- a/lib/base/networking/protocols/api.dart +++ b/lib/base/networking/protocols/api.dart @@ -1,7 +1,15 @@ +import 'package:campus_flutter/moodleComponent/model/moodle_course.dart'; +import 'package:campus_flutter/moodleComponent/networking/apis/MoodleApi.dart'; +import 'package:campus_flutter/moodleComponent/service/shibboleth_session_generator.dart'; import 'package:dio/dio.dart' as dio; abstract class Api { static String tumToken = ""; + static String tumId = ""; + static Future>? coursesFuture; + static List courses = []; + static ShibbolethSession? session; + static MoodleApi? moodleApi; String get domain; diff --git a/lib/base/routing/router.dart b/lib/base/routing/router.dart index fa07c7a2..5dc6ae36 100644 --- a/lib/base/routing/router.dart +++ b/lib/base/routing/router.dart @@ -14,6 +14,8 @@ import 'package:campus_flutter/homeComponent/view/departure/departures_details_v import 'package:campus_flutter/feedbackComponent/views/feedback_form_view.dart'; import 'package:campus_flutter/feedbackComponent/views/feedback_success_view.dart'; import 'package:campus_flutter/homeComponent/screen/home_screen.dart'; +import 'package:campus_flutter/moodleComponent/view/moodle_course_viewmodel.dart'; +import 'package:campus_flutter/moodleComponent/view/moodle_viewmodel.dart'; import 'package:campus_flutter/navigaTumComponent/model/navigatum_roomfinder_map.dart'; import 'package:campus_flutter/navigaTumComponent/views/navigatum_room_view.dart'; import 'package:campus_flutter/onboardingComponent/views/confirm_view.dart'; @@ -21,6 +23,7 @@ import 'package:campus_flutter/onboardingComponent/views/location_permissions_vi import 'package:campus_flutter/onboardingComponent/views/login_view.dart'; import 'package:campus_flutter/main.dart'; import 'package:campus_flutter/navigation.dart'; +import 'package:campus_flutter/onboardingComponent/views/password_view.dart'; import 'package:campus_flutter/onboardingComponent/views/permission_check_view.dart'; import 'package:campus_flutter/personComponent/views/person_details_view.dart'; import 'package:campus_flutter/placesComponent/model/cafeterias/cafeteria.dart'; @@ -38,6 +41,7 @@ import 'package:campus_flutter/settingsComponent/views/settings_scaffold.dart'; import 'package:campus_flutter/studiesComponent/model/lecture.dart'; import 'package:campus_flutter/studiesComponent/screen/studies_screen.dart'; import 'package:campus_flutter/studiesComponent/view/lectureDetail/lecture_details_view.dart'; +import 'package:flutter/cupertino.dart' show Text; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -54,6 +58,11 @@ final _router = GoRouter( builder: (context, state) => PermissionCheckView(isSettingsView: (state.extra as bool?) ?? false), ), + GoRoute( + path: safetyArea, + builder: (context, state) => + PasswordView(), + ), GoRoute( path: locationPermission, builder: (context, state) => const LocationPermissionView(), @@ -97,6 +106,61 @@ final _router = GoRouter( ), ], ), + StatefulShellBranch( + routes: [ + GoRoute( + path: moodle, + pageBuilder: (context, state) + + => const NoTransitionPage(child: MoodleViewModel()) + ), + GoRoute( + path: "$moodle/viewCourse", + pageBuilder: (context, state) { + final args = state.extra as MoodleCourseArguments?; + + if (args == null) { + return const NoTransitionPage(child: Text("Ein Fehler ist aufgetreten")); + } + + return NoTransitionPage( + child: MoodleCourseViewModel( + args.session, // 1. Argument + args.api, // 2. Argument + args.course // 3. Argument + ) + ); + } + ), + GoRoute( + path: '/webviewPage', + pageBuilder: (context, state) { + final Map? args = state.extra as Map?; + + if (args == null || args['url'] == null) { + return const NoTransitionPage(child: Text("Ein Fehler ist aufgetreten")); + } + + return NoTransitionPage( + child: WebViewPage( + url: args['url'] as String, + // Keine Cookie-Argumente mehr nötig + ), + ); + }, + ), + + GoRoute( + path: '/pdf-viewer', + builder: (context, state) { + + return PdfViewScreen( + stringPathFuture: state.extra as Future, + ); + }, + ), + ], + ), StatefulShellBranch( routes: [ GoRoute( diff --git a/lib/base/routing/routes.dart b/lib/base/routing/routes.dart index 7bdae67d..93f36999 100644 --- a/lib/base/routing/routes.dart +++ b/lib/base/routing/routes.dart @@ -2,6 +2,7 @@ const onboarding = "/onboarding"; const confirm = "/confirm"; const locationPermission = "/locationPermission"; const permissionCheck = "/permissionCheck"; +const safetyArea = "/safetyArea"; /// Home tab const home = "/"; @@ -42,6 +43,9 @@ const search = "/search"; const roomSearch = "/roomSearch"; const personSearch = "/personSearch"; +/// Moodle +const moodle = "/moodle"; + /// General const navigaTum = "/navigaTum"; const personDetails = "/personDetails"; diff --git a/lib/campusComponent/screen/campus_screen.dart b/lib/campusComponent/screen/campus_screen.dart index d7f70766..25045b0d 100644 --- a/lib/campusComponent/screen/campus_screen.dart +++ b/lib/campusComponent/screen/campus_screen.dart @@ -13,14 +13,27 @@ class CampusScreen extends StatelessWidget { if (orientation == Orientation.portrait) { return body(); } else { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Spacer(), - Expanded(flex: 2, child: body()), - const Spacer(), - ], - ); + return SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: Row(children: [ + Expanded(child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column(children: [ + Text('News', style: Theme.of(context).textTheme.headlineSmall), + const SizedBox(height: 10), + NewsWidgetView(), + ],),)), + + const VerticalDivider(width: 0), + Expanded(child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column(children: [ + StudentClubWidgetView(), + const SizedBox(height: 10), + MovieWidgetView(), + ],),)), ] + )); } }, ); diff --git a/lib/homeComponent/screen/home_screen.dart b/lib/homeComponent/screen/home_screen.dart index ba67ffdc..18fbf7f4 100644 --- a/lib/homeComponent/screen/home_screen.dart +++ b/lib/homeComponent/screen/home_screen.dart @@ -1,9 +1,34 @@ +import 'package:campus_flutter/base/networking/protocols/api.dart'; import 'package:campus_flutter/base/util/padded_divider.dart'; import 'package:campus_flutter/homeComponent/view/contactCard/contact_view.dart'; import 'package:campus_flutter/homeComponent/view/widget/widget_screen.dart'; +import 'package:campus_flutter/moodleComponent/model/moodle_course.dart'; +import 'package:campus_flutter/moodleComponent/model/moodle_user.dart'; +import 'package:campus_flutter/moodleComponent/networking/apis/MoodleApi.dart'; +import 'package:campus_flutter/moodleComponent/service/shibboleth_session_generator.dart'; +import 'package:campus_flutter/onboardingComponent/viewModels/onboarding_viewmodel.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +Future> connectToMoodle(WidgetRef ref) async { + var username = Api.tumId; + var password = await ref.read(onboardingViewModel).getPassword(); + ShibbolethSession session = await ShibbolethSessionGenerator().generateSession(username, password).timeout(Duration(seconds: 15), onTimeout: () { + throw WrongTumPasswordSetException(); + }); + try { + var api = MoodleApi(session); + MoodleUser user = await api.getMoodleUser(username); + Api.courses = await api.getCourses(user); + Api.session = session; + Api.moodleApi = api; + return Api.courses; + } + catch(e) { + throw WrongTumPasswordSetException(); + } +} + class HomeScreen extends ConsumerStatefulWidget { const HomeScreen({super.key}); @@ -21,13 +46,17 @@ class _HomeScreenState extends ConsumerState { if (orientation == Orientation.portrait) { return _widgetScrollView(); } else { - return Row( - children: [ - const Spacer(), - Expanded(flex: 2, child: _widgetScrollView()), - const Spacer(), - ], - ); + return SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: Row(children: [ + Expanded(child: ContactScreen()), + const VerticalDivider(width: 0), + Expanded(child: SingleChildScrollView( + controller: scrollController, + scrollDirection: Axis.vertical, + child: WidgetScreen(),)), ] + )); } }, ); diff --git a/lib/homeComponent/view/contactCard/contact_card_view.dart b/lib/homeComponent/view/contactCard/contact_card_view.dart index cb2cf722..a18c12bc 100644 --- a/lib/homeComponent/view/contactCard/contact_card_view.dart +++ b/lib/homeComponent/view/contactCard/contact_card_view.dart @@ -1,8 +1,10 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:campus_flutter/base/enums/device.dart'; import 'package:campus_flutter/base/extensions/base_64_decode_image_data.dart'; +import 'package:campus_flutter/base/networking/protocols/api.dart'; import 'package:campus_flutter/base/services/device_type_service.dart'; import 'package:campus_flutter/base/util/delayed_loading_indicator.dart'; +import 'package:campus_flutter/homeComponent/screen/home_screen.dart'; import 'package:campus_flutter/homeComponent/view/contactCard/contact_card_loading_view.dart'; import 'package:campus_flutter/navigation_service.dart'; import 'package:campus_flutter/personComponent/model/personDetails/person_details.dart'; @@ -39,6 +41,8 @@ class _ContactCardViewState extends ConsumerState { stream: ref.watch(profileDetailsViewModel).personDetails, builder: (context, snapshot) { if (snapshot.hasData || snapshot.hasError) { + Api.tumId = widget.profile.tumID!; + Api.coursesFuture ??= connectToMoodle(ref); return InkWell( onTap: () => NavigationService.openStudentCardSheet(context), child: contactInfo(snapshot.data, widget.profile), diff --git a/lib/moodleComponent/model/moodle_course.dart b/lib/moodleComponent/model/moodle_course.dart new file mode 100644 index 00000000..fa9e3cf4 --- /dev/null +++ b/lib/moodleComponent/model/moodle_course.dart @@ -0,0 +1,224 @@ + +import 'dart:convert'; + +import 'package:campus_flutter/base/routing/routes.dart'; +import 'package:campus_flutter/moodleComponent/model/moodle_section.dart'; +import 'package:campus_flutter/moodleComponent/networking/apis/MoodleApi.dart'; +import 'package:campus_flutter/moodleComponent/service/shibboleth_session_generator.dart'; +import 'package:campus_flutter/moodleComponent/view/moodle_course_viewmodel.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui' as ui; +import 'package:flutter/widgets.dart'; +import 'package:go_router/go_router.dart'; +import 'package:xml/xml.dart' as xml; + +class MoodleCourse{ + final int id; + final String fullname; + final String shortname; + final String idnumber; + final String summary; + final int summaryFormat; + final int startDate; + final int endDate; + final bool visible; + final bool showActivityDates; + final bool showCompletionConditions; + final String pdfExportFont; + final String fullnameDisplay; + final String viewUrl; + final String courseImage; + final int progress; + final bool hasProgress; + final bool isFavourite; + final bool hidden; + final int timeAccess; + final bool showShortname; + final String courseCategory; + MoodleCourseState? state; + + MoodleCourse({ + required this.id, + required this.fullname, + required this.shortname, + required this.idnumber, + required this.summary, + required this.summaryFormat, + required this.startDate, + required this.endDate, + required this.visible, + required this.showActivityDates, + required this.showCompletionConditions, + required this.pdfExportFont, + required this.fullnameDisplay, + required this.viewUrl, + required this.courseImage, + required this.progress, + required this.hasProgress, + required this.isFavourite, + required this.hidden, + required this.timeAccess, + required this.showShortname, + required this.courseCategory, + }); + + Future fetchState(MoodleApi api) async { + state = await api.getCourseStateForCourse(this); + } + + + + Widget createImage(BuildContext ctx) { + double width = 250; + double height = 250; + if(MediaQuery.of(ctx).orientation == Orientation.portrait) { + width = MediaQuery.of(ctx).size.width * 0.08; + height = MediaQuery.of(ctx).size.width * 0.08; + } + if (courseImage.isEmpty) { + return const Icon(Icons.book, size: 40); + } else { + if(courseImage.contains("base64")) { + final base64String = courseImage.split(',').last; + var converted = utf8.decode(base64.decode(base64String)); + converted = converted.replaceAll("100%", "280px"); + return SvgPicture.string( + converted, + width: width, + height: height, + fit: BoxFit.cover, + placeholderBuilder: (context) => const Icon(Icons.book, size: 40), + ); + + } else { + return Image.network( + courseImage, + width: width, + height: height, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return const Icon(Icons.book, size: 40); + }, + ); + } + } + } + + //to and from json + factory MoodleCourse.fromJson(Map json) { + return MoodleCourse( + id: json['id'], + fullname: json['fullname'], + shortname: json['shortname'], + idnumber: json['idnumber'], + summary: json['summary'], + summaryFormat: json['summaryformat'], + startDate: json['startdate'], + endDate: json['enddate'], + visible: json['visible'], + showActivityDates: json['showactivitydates'], + showCompletionConditions: json['showcompletionconditions']??false, + pdfExportFont: json['pdfexportfont'], + fullnameDisplay: json['fullnamedisplay'], + viewUrl: json['viewurl'], + courseImage: json['courseimage'], + progress: json['progress'], + hasProgress: json['hasprogress'], + isFavourite: json['isfavourite'], + hidden: json['hidden'], + timeAccess: json['timeaccess'], + showShortname: json['showshortname'], + courseCategory: json['coursecategory'], + ); + } + + //to string + @override + String toString() { + return 'MoodleCourse{id: $id, fullname: $fullname}'; + } + + Widget build(BuildContext context, {bool withArrowForward = true, bool withArrowBackward = false, ShibbolethSession? session, MoodleApi? api}) { + + //create a smooth design + return SizedBox( + height: MediaQuery.of(context).orientation == Orientation.portrait ? MediaQuery.of(context).size.height * 0.15 : MediaQuery.of(context).size.height*0.8, + width: MediaQuery.of(context).orientation == Orientation.portrait ? MediaQuery.of(context).size.width : MediaQuery.of(context).size.width * 0.25, + child: + Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0), + ), + elevation: 5, + child: InkWell( + borderRadius: BorderRadius.circular(15.0), + onTap: () { + if (withArrowForward) { + final arguments = MoodleCourseArguments( + session: session!, + api: api!, + course: this, + ); + + context.push("$moodle/viewCourse", extra: arguments); + }else if(withArrowBackward) { + context.pop(); + } + }, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: + MediaQuery.of(context).orientation == Orientation.portrait ? + Row( + children: [ + createImage(context), + const SizedBox(width: 10), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + fullname, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 10), + Text( + courseCategory, + style: Theme.of(context).textTheme.titleSmall, + ), + ], + ), + ), + withArrowForward ? Icon(Icons.arrow_forward, color: Theme.of(context).colorScheme.primary): withArrowBackward ? Icon(Icons.arrow_back, color: Theme.of(context).colorScheme.primary): const SizedBox.shrink(), + ], + ) : + Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + fullname, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + createImage(context), + const SizedBox(height: 10), + Text( + courseCategory, + style: Theme.of(context).textTheme.titleSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + withArrowForward ? Icon(Icons.arrow_forward, color: Theme.of(context).colorScheme.primary): withArrowBackward ? Icon(Icons.arrow_back, color: Theme.of(context).colorScheme.primary): const SizedBox.shrink(), + ], + ), + ), + ), + )); + } +} diff --git a/lib/moodleComponent/model/moodle_section.dart b/lib/moodleComponent/model/moodle_section.dart new file mode 100644 index 00000000..1ac70efa --- /dev/null +++ b/lib/moodleComponent/model/moodle_section.dart @@ -0,0 +1,249 @@ +// --- Supporting Class: MoodleCourseDetails --- +import 'package:flutter/cupertino.dart'; + +class MoodleCourseDetails { + final String id; + final int numSections; + final List sectionList; + final bool editMode; + final String highlighted; + final String maxSections; + final String baseurl; + final String stateKey; + final String maxBytes; + final String maxBytesText; + + MoodleCourseDetails({ + required this.id, + required this.numSections, + required this.sectionList, + required this.editMode, + required this.highlighted, + required this.maxSections, + required this.baseurl, + required this.stateKey, + required this.maxBytes, + required this.maxBytesText, + }); + + factory MoodleCourseDetails.fromJson(Map json) { + return MoodleCourseDetails( + id: json['id'] as String, + numSections: int.parse(json['numsections'].toString()), // JSON field is string, but should be int + sectionList: List.from(json['sectionlist'] as List), + editMode: json['editmode'] as bool, + highlighted: json['highlighted'] as String, + maxSections: json['maxsections'] as String, + baseurl: json['baseurl'] as String, + stateKey: json['statekey'] as String, + maxBytes: json['maxbytes'] as String, + maxBytesText: json['maxbytestext'] as String, + ); + } +} + +// --- Supporting Class: MoodleSection --- +class MoodleSection { + + final List monthNamesDe = [ + 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', + 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember' + ]; + final List monthNamesEn = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ]; + + final String id; + final int section; + final int number; + final String title; + final bool hasSummary; + final String? rawTitle; + final List cmList; + final bool visible; + final String sectionUrl; + final bool current; + final bool indexCollapsed; + final bool contentCollapsed; + final bool hasRestrictions; + final bool bulkEditable; + final String? component; + final String? itemId; + final String? parentSectionId; + + MoodleSection({ + required this.id, + required this.section, + required this.number, + required this.title, + required this.hasSummary, + this.rawTitle, + required this.cmList, + required this.visible, + required this.sectionUrl, + required this.current, + required this.indexCollapsed, + required this.contentCollapsed, + required this.hasRestrictions, + required this.bulkEditable, + this.component, + this.itemId, + this.parentSectionId, + }); + + bool isCurrentlyRelevant() { + if(!title.contains("-")) { + return false; + } + var splitTitle = title.split("-"); + try { + String startString = splitTitle[0].trim(); + String endString = splitTitle[1].trim(); + for (int i = 0; i < monthNamesDe.length; i++) { + startString = startString.replaceAll(monthNamesDe[i], (i + 1).toString().padLeft(2, '0')); + endString = endString.replaceAll(monthNamesDe[i], (i + 1).toString().padLeft(2, '0')); + startString = startString.replaceAll(monthNamesEn[i], (i + 1).toString().padLeft(2, '0')); + endString = endString.replaceAll(monthNamesEn[i], (i + 1).toString().padLeft(2, '0')); + } + startString = "${startString.replaceAll(" ", "")}.${DateTime.now().year}".replaceAll(".", "-"); + endString = "${endString.replaceAll(" ", "")}.${DateTime.now().year}".replaceAll(".", "-"); + var startParts = startString.split("-"); + startString = "${startParts[2]}-${startParts[1]}-${startParts[0]}"; + var endParts = endString.split("-"); + endString = "${endParts[2]}-${endParts[1]}-${endParts[0]}"; + debugPrint("Parsed Date String: $startString, $endString for title: $title"); + DateTime parsedStartDate = DateTime.parse(startString); + DateTime parsedEndDate = DateTime.parse(endString); + return (parsedStartDate.isBefore(DateTime.now()) && parsedEndDate.isAfter(DateTime.now())); + } catch (e) { + return false; + } + } + + factory MoodleSection.fromJson(Map json) { + return MoodleSection( + id: json['id'] as String, + section: json['section'] as int, + number: json['number'] as int, + title: json['title'] as String, + hasSummary: json['hassummary'] as bool, + rawTitle: json['rawtitle'] as String?, + cmList: List.from(json['cmlist'] as List), + visible: json['visible'] as bool, + sectionUrl: json['sectionurl'] as String, + current: json['current'] as bool, + indexCollapsed: json['indexcollapsed'] as bool, + contentCollapsed: json['contentcollapsed'] as bool, + hasRestrictions: json['hasrestrictions'] as bool, + bulkEditable: json['bulkeditable'] as bool, + component: json['component'] as String?, + itemId: json['itemid'] as String?, + parentSectionId: json['parentsectionid'] as String?, + ); + } +} + +// --- Supporting Class: MoodleCm (Course Module) --- +class MoodleCm { + final String id; + final String anchor; + final String name; + final bool visible; + final bool stealth; + final String sectionId; + final int sectionNumber; + final bool userVisible; + final bool hasCmRestrictions; + final String modname; + final int indent; + final int groupMode; + final String module; + final String plugin; + final bool hasDelegatedSection; + final bool accessVisible; + final String? url; + final bool isTrackedUser; + final bool allowStealth; + + MoodleCm({ + required this.id, + required this.anchor, + required this.name, + required this.visible, + required this.stealth, + required this.sectionId, + required this.sectionNumber, + required this.userVisible, + required this.hasCmRestrictions, + required this.modname, + required this.indent, + required this.groupMode, + required this.module, + required this.plugin, + required this.hasDelegatedSection, + required this.accessVisible, + this.url, + required this.isTrackedUser, + required this.allowStealth, + }); + + factory MoodleCm.fromJson(Map json) { + return MoodleCm( + id: json['id'] as String, + anchor: json['anchor'] as String, + name: json['name'] as String, + visible: json['visible'] as bool, + stealth: json['stealth'] as bool, + sectionId: json['sectionid'] as String, + sectionNumber: json['sectionnumber'] as int, + userVisible: json['uservisible'] as bool, + hasCmRestrictions: json['hascmrestrictions'] as bool, + modname: json['modname'] as String, + indent: json['indent'] as int, + groupMode: int.parse(json['groupmode'].toString()), // groupmode can be string + module: json['module'] as String, + plugin: json['plugin'] as String, + hasDelegatedSection: json['hasdelegatedsection'] as bool, + accessVisible: json['accessvisible'] as bool, + url: json['url'] as String?, + isTrackedUser: json['istrackeduser'] as bool, + allowStealth: json['allowstealth'] as bool, + ); + } +} + +// =============================================== +// --- Hauptklasse: MoodleCourseState --- +// =============================================== +class MoodleCourseState { + final MoodleCourseDetails course; + final List section; + final List cm; + + MoodleCourseState({ + required this.course, + required this.section, + required this.cm, + }); + + factory MoodleCourseState.fromJson(Map json) { + final MoodleCourseDetails course = MoodleCourseDetails.fromJson(json['course'] as Map); + + final List sectionJson = json['section'] as List; + final List section = sectionJson + .map((s) => MoodleSection.fromJson(s as Map)) + .toList(); + + final List cmJson = json['cm'] as List; + final List cm = cmJson + .map((c) => MoodleCm.fromJson(c as Map)) + .toList(); + + return MoodleCourseState( + course: course, + section: section, + cm: cm, + ); + } +} \ No newline at end of file diff --git a/lib/moodleComponent/model/moodle_user.dart b/lib/moodleComponent/model/moodle_user.dart new file mode 100644 index 00000000..02426a14 --- /dev/null +++ b/lib/moodleComponent/model/moodle_user.dart @@ -0,0 +1,130 @@ +// Supporting class for the preferences list +import 'package:flutter/cupertino.dart'; + +class Preference { + final String name; + final String? value; // Value can sometimes be a number in string form or a complex JSON string + + Preference({ + required this.name, + this.value, + }); + + factory Preference.fromJson(Map json) { + return Preference( + name: json['name'] as String, + // Value can be String, int, or null, so we convert it to String? + value: json['value']?.toString(), + ); + } +} + +// Class representing the Moodle User +class MoodleUser { + final int id; + final String username; + final String fullname; + final String email; + final String department; + final String idnumber; + final String auth; + final bool suspended; + final bool confirmed; + final String lang; + final String theme; + final String timezone; + final int mailformat; + final int trackforums; + final String description; + final int descriptionformat; + final String profileimageurlsmall; + final String profileimageurl; + final List preferences; + + MoodleUser({ + required this.id, + required this.username, + required this.fullname, + required this.email, + required this.department, + required this.idnumber, + required this.auth, + required this.suspended, + required this.confirmed, + required this.lang, + required this.theme, + required this.timezone, + required this.mailformat, + required this.trackforums, + required this.description, + required this.descriptionformat, + required this.profileimageurlsmall, + required this.profileimageurl, + required this.preferences, + }); + + // Factory constructor to create a MoodleUser instance from a JSON map + factory MoodleUser.fromJson(Map data) { + debugPrint("Parsing MoodleUser from JSON: $data"); + var json = (data.entries.last.value as List).first; + // Cast the preferences list of maps into a list of Preference objects + final List preferencesJson = json['preferences'] as List; + final List preferences = preferencesJson + .map((p) => Preference.fromJson(p as Map)) + .toList(); + + return MoodleUser( + id: json['id'] as int, + username: json['username'] as String, + fullname: json['fullname'] as String, + email: json['email'] as String, + department: json['department'] as String, + idnumber: json['idnumber'] as String, + auth: json['auth'] as String, + suspended: json['suspended'] as bool, + confirmed: json['confirmed'] as bool, + lang: json['lang'] as String, + theme: json['theme'] as String, + timezone: json['timezone'] as String, + mailformat: json['mailformat'] as int, + trackforums: json['trackforums'] as int, + description: json['description'] as String, + descriptionformat: json['descriptionformat'] as int, + profileimageurlsmall: json['profileimageurlsmall'] as String, + profileimageurl: json['profileimageurl'] as String, + preferences: preferences, + ); + } + + //toString method for easier debugging + @override + String toString() { + //with all fields + return 'MoodleUser{id: $id, username: $username, fullname: $fullname, email: $email, department: $department, idnumber: $idnumber, auth: $auth, suspended: $suspended, confirmed: $confirmed, lang: $lang, theme: $theme, timezone: $timezone, mailformat: $mailformat, trackforums: $trackforums, description: $description, descriptionformat: $descriptionformat, profileimageurlsmall: $profileimageurlsmall, profileimageurl: $profileimageurl, preferences: $preferences}'; + } +} + +// Class representing the root of the JSON array structure +// Note: The provided JSON is a list containing a single map (the response). +class MoodleUserResponse { + final bool error; + final List data; + + MoodleUserResponse({ + required this.error, + required this.data, + }); + + factory MoodleUserResponse.fromJson(Map json) { + // Cast the data list of maps into a list of MoodleUser objects + final List dataJson = json['data'] as List; + final List data = dataJson + .map((u) => MoodleUser.fromJson(u as Map)) + .toList(); + + return MoodleUserResponse( + error: json['error'] as bool, + data: data, + ); + } +} diff --git a/lib/moodleComponent/networking/apis/MoodleApi.dart b/lib/moodleComponent/networking/apis/MoodleApi.dart new file mode 100644 index 00000000..085a18f4 --- /dev/null +++ b/lib/moodleComponent/networking/apis/MoodleApi.dart @@ -0,0 +1,134 @@ +import 'dart:convert'; + +import 'package:campus_flutter/base/networking/protocols/api.dart'; +import 'package:campus_flutter/moodleComponent/model/moodle_course.dart'; +import 'package:campus_flutter/moodleComponent/model/moodle_section.dart'; +import 'package:campus_flutter/moodleComponent/model/moodle_user.dart'; +import 'package:campus_flutter/moodleComponent/service/shibboleth_session_generator.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/cupertino.dart'; + +class MoodleApi extends Api{ + + ShibbolethSession session; + + + MoodleApi(this.session); + + @override + // TODO: implement domain + String get domain => "moodle.tum.de/lib/ajax/service.php"; + + @override + bool get needsAuth => true; + + @override + Map get parameters => { + "sesskey": session.sessionId, + }; + + @override + String get path => ""; + + @override + String get slug => "XML_DM_REQUEST"; + + Future> getCourses(MoodleUser user) async { + //new dio instance is needed to avoid cookie conflicts + Dio dio = Dio(); + dio.options.headers.addAll({ + "Cookie": session.cookies.entries.map((e) => "${e.key}=${e.value}").toList().join("; "), + "Content-Type": "application/json", + "Accept": "application/json, text/javascript, *; q=0.01", + "X-Requested-With": "XMLHttpRequest", + }); + var data = await dio.post("https://www.moodle.tum.de/lib/ajax/service.php?sesskey=${session.sessionId.toString()}&info=core_course_get_recent_courses", data: + jsonEncode([ + { + "index": 0, + "methodname": "core_course_get_recent_courses", + "args": {"limit": 10, "userid": user.id} + } + ])); + var coursesList = (data.data as List).first.entries.last.value as List; + return coursesList.map((e) => MoodleCourse.fromJson(e)).toList(); + } + + //userName is the username of tum online access +Future getMoodleUser(String userName) async{ + Dio dio = Dio(); + dio.options.headers.addAll({ + "Cookie": session.cookies.entries.map((e) => "${e.key}=${e.value}").toList().join("; "), + "Content-Type": "application/json", + "Accept": "application/json, text/javascript, *; q=0.01", + "X-Requested-With": "XMLHttpRequest", + }); + var data = await dio.post("https://www.moodle.tum.de/lib/ajax/service.php?sesskey=${session.sessionId.toString()}&info=core_course_get_recent_courses", data: + jsonEncode([ + { + "index": 0, + "methodname": "core_user_get_users_by_field", + "args": {"field": "username", "values": [ + userName + ]} + } + ])); + var userMap = (data.data as List).first as Map; + return MoodleUser.fromJson(userMap); +} + +Future getCourseStateForCourse(MoodleCourse course) async { + Dio dio = Dio(); + dio.options.headers.addAll({ + "Cookie": session.cookies.entries + .map((e) => "${e.key}=${e.value}") + .toList() + .join("; "), + "Content-Type": "application/json", + "Accept": "application/json, text/javascript, *; q=0.01", + "X-Requested-With": "XMLHttpRequest", + }); + var data = await dio.post( + "https://www.moodle.tum.de/lib/ajax/service.php?sesskey=${session + .sessionId.toString()}&info=core_courseformat_get_state", data: + jsonEncode([ + { + "index": 0, + "methodname": "core_courseformat_get_state", + "args": {"courseid": course.id} + } + ])); + var courseStateMap = (data.data as List).first as Map; + var jsonData = courseStateMap["data"]; + + return MoodleCourseState.fromJson(jsonDecode(jsonData)); +} + +Future loadHtmlDataForMoodleModule(String cmId) async{ + Dio dio = Dio(); + dio.options.headers.addAll({ + "Cookie": session.cookies.entries + .map((e) => "${e.key}=${e.value}") + .toList() + .join("; "), + "Content-Type": "application/json", + "Accept": "application/json, text/javascript, *; q=0.01", + "X-Requested-With": "XMLHttpRequest", + }); + var data = await dio.post( + "https://www.moodle.tum.de/lib/ajax/service.php?sesskey=${session + .sessionId.toString()}&info=core_course_get_module", data: + jsonEncode([ + { + "index": 0, + "methodname": "core_course_get_module", + "args": {"id": cmId} + } + ])); + var courseStateMap = (data.data as List).first as Map; + var jsonData = courseStateMap["data"]; + + return jsonData; +} + +} \ No newline at end of file diff --git a/lib/moodleComponent/service/shibboleth_session_generator.dart b/lib/moodleComponent/service/shibboleth_session_generator.dart new file mode 100644 index 00000000..a243debd --- /dev/null +++ b/lib/moodleComponent/service/shibboleth_session_generator.dart @@ -0,0 +1,97 @@ +import 'dart:async'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + +class ShibbolethSessionGenerator { + + Future generateSession( + String userName, String password) { + ShibbolethSession session = ShibbolethSession(); + Completer helperCompleter = Completer(); + HeadlessInAppWebView webView = HeadlessInAppWebView( + initialUrlRequest: URLRequest( + url: WebUri( + 'https://www.moodle.tum.de/'), + ), + onTitleChanged: (controller, title) async { + if(title!.startsWith("Startseite")) { + + await controller.evaluateJavascript(source: """ + function cycle() { + document.querySelector("a.btn.btn-primary").click(); + } + setTimeout(cycle, 1500); + """); + }else if(title.startsWith("TUM")) { + await controller.evaluateJavascript(source: """ + + function cycle() { + + function fillInput(input, value) { + + input.focus(); + + input.value = value; + + input.dispatchEvent(new Event('input', { bubbles: true })); + input.dispatchEvent(new Event('change', { bubbles: true })); + + input.blur(); +} + fillInput(document.getElementById("username"), "$userName"); + fillInput(document.getElementById("password"), "$password"); + + setTimeout(() => {document.querySelector("button[type='submit']").click();}, 500); + + } + + cycle(); + """); + } else { + + final tumCookies = await CookieManager.instance() + .getCookies(url: WebUri("https://login.tum.de")); + final moodleCookies = await CookieManager.instance() + .getCookies(url: WebUri("https://www.moodle.tum.de")); + final examMoodleCookies = await CookieManager.instance() + .getCookies(url: WebUri("https://exam.moodle.tum.de")); + + final expiration = tumCookies.first.expiresDate != null ? DateTime.fromMillisecondsSinceEpoch(tumCookies.first.expiresDate!): DateTime.now().add( + const Duration(hours: 8), + ); + session.expiration = expiration; + session.cookies = { + for (var cookie in tumCookies) cookie.name: cookie.value, + for (var cookie in moodleCookies) cookie.name: cookie.value, + for (var cookie in examMoodleCookies) cookie.name: cookie.value, + }; + } + }, + onLoadError: (controller, url, code, message) { + throw Exception("Failed to load page: $message"); + }, + onLoadResource: (InAppWebViewController controller, LoadedResource resource) { + if(resource.url!.toString().startsWith("https://www.moodle.tum.de/lib/ajax/service.php")) { + var sesskey = resource.url!.queryParameters["sesskey"]; + session.sessionId = sesskey!; + debugPrint("Shibboleth session generated with sesskey: ${session.sessionId}"); + if(!helperCompleter.isCompleted) { + helperCompleter.complete(session); + } + } + }, + ); + webView.run(); + return helperCompleter.future; + } + +} + +class ShibbolethSession { + late String userId; + late String sessionId; + Map cookies = {}; + late DateTime expiration; + + ShibbolethSession(); +} \ No newline at end of file diff --git a/lib/moodleComponent/view/moodle_course_viewmodel.dart b/lib/moodleComponent/view/moodle_course_viewmodel.dart new file mode 100644 index 00000000..9c413cd4 --- /dev/null +++ b/lib/moodleComponent/view/moodle_course_viewmodel.dart @@ -0,0 +1,531 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:campus_flutter/moodleComponent/model/moodle_course.dart'; +import 'package:campus_flutter/moodleComponent/model/moodle_section.dart'; +import 'package:campus_flutter/moodleComponent/networking/apis/MoodleApi.dart'; +import 'package:campus_flutter/moodleComponent/service/shibboleth_session_generator.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:gpt_markdown/gpt_markdown.dart'; +import 'package:html2md/html2md.dart' as html2md; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:open_filex/open_filex.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class MoodleCourseViewModel extends ConsumerStatefulWidget { + final MoodleCourse course; + final MoodleApi api; + final ShibbolethSession session; + const MoodleCourseViewModel(this.session, this.api, this.course, {super.key}); + + @override + ConsumerState createState() => + _MoodleCourseViewModelState(); +} + +class _MoodleCourseViewModelState extends ConsumerState { + late Future _future; + Widget? sectionSelection; + int currentIndex = -1; + + @override + void initState() { + super.initState(); + _future = connectToMoodle(); + } + + Future connectToMoodle() async { + return widget.course.fetchState(widget.api); + } + + @override + Widget build(BuildContext context) { + return widget.course.state == null + ? FutureBuilder( + future: _future, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + if(MediaQuery.of(context).orientation == Orientation.portrait) { + return Column( + children: [ + widget.course.build(context, withArrowForward: false, withArrowBackward: true), + const Center(child: CupertinoActivityIndicator()), + ], + ); + } + return Row( + children: [ + widget.course.build(context, withArrowForward: false, withArrowBackward: true), + const Center(child: CupertinoActivityIndicator()), + ], + ); + } else if (snapshot.hasError) { + debugPrintStack(stackTrace: snapshot.stackTrace); + return Center(child: Text("Error: ${snapshot.error}")); + } else { + if(MediaQuery.of(context).orientation == Orientation.portrait) { + return Column( + children: [ + widget.course.build(context, withArrowForward: false, withArrowBackward: true), + const SizedBox(height: 10), + buildCourseContent(), + sectionSelection ?? Container(), + ], + ); + } + return Row( + children: [ + widget.course.build(context, withArrowForward: false, withArrowBackward: true), + const SizedBox(width: 10), + buildCourseContent(), + sectionSelection ?? Container(), + ], + ); + } + }, + ) + : + MediaQuery.of(context).orientation == Orientation.portrait ? + Column( + children: [ + widget.course.build(context, withArrowForward: false, withArrowBackward: true), + buildCourseContent(), + sectionSelection ?? Container(), + ], + ) + : + Row( + children: [ + widget.course.build(context, withArrowForward: false, withArrowBackward: true), + buildCourseContent(), + sectionSelection ?? Container(), + ], + ); + } + + Widget buildCourseContent() { + return SizedBox( + width: MediaQuery.of(context).orientation == Orientation.portrait ? MediaQuery.of(context).size.width : MediaQuery.of(context).size.width * 0.2, + height: MediaQuery.of(context).orientation == Orientation.portrait ? MediaQuery.of(context).size.height * 0.07 : MediaQuery.of(context).size.height, + child: ListView.builder( + itemBuilder: (context, index) { + final content = widget.course.state!.section[index]; + return Card( + shape: content.isCurrentlyRelevant() ? StadiumBorder( + side: BorderSide( + color: Theme.of(context).colorScheme.primaryContainer, + width: 2.0, + ), + ):null, + color: currentIndex == index + ? Theme.of(context).colorScheme.primaryContainer + : null, + child: SizedBox( + width: MediaQuery.of(context).orientation == Orientation.portrait ? MediaQuery.of(context).size.width*0.4 : MediaQuery.of(context).size.width * 0.2, + height: MediaQuery.of(context).size.height * 0.07, + child: ListTile( + title: Text( + content.title, + style: Theme.of(context).textTheme.titleSmall, + textAlign: TextAlign.center, + ), + onTap: () { + setState(() { + currentIndex = index; + sectionSelection = buildSectionWidgetForSection(content); + }); + }, + ), + )); + }, + itemCount: widget.course.state!.section.length, + scrollDirection: MediaQuery.of(context).orientation == Orientation.portrait ? Axis.horizontal : Axis.vertical, + ), + ); + } + + Widget buildSectionWidgetForSection(MoodleSection section) { + var cmList = section.cmList; + return FutureBuilder( + future: getCmListContents(cmList), + builder: (ctx, snap) { + if (snap.connectionState == ConnectionState.waiting) { + return CircularProgressIndicator(); + } else if (snap.hasError) { + return Text("Es ist ein Fehler aufgetreten: ${snap.error}"); + } else { + return SizedBox( + width: MediaQuery.of(context).orientation == Orientation.portrait ? MediaQuery.of(context).size.width : MediaQuery.of(context).size.width * 0.45, + height:MediaQuery.of(context).orientation == Orientation.portrait? MediaQuery.of(context).size.height*0.55: MediaQuery.of(context).size.height * 0.8, + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + children: snap.data! + .map( + (htmlContent) => Column( + children: buildFinalWidgetsFromSegments( + segmentHtmlContent(htmlContent), + ), + ), + ) + .toList(), + ), + ), + ); + } + }, + ); + } + + final RegExp imgTagRegex = RegExp( + '(]*src\s*=\s*["\']([^"\']*)["\'][^>]*>)', + caseSensitive: false, + ); + + List segmentHtmlContent(String fullHtml) { + var split = fullHtml.split(""); + segments.removeAt(i); + segments.insert(i, ""); + if (subvalue.length > 1) { + segments.insert(i + 1, subvalue.sublist(1).join(">")); + } else { + segments.insert(i + 1, ""); + } + } + return segments; + } + + String? extractImageUrl(String imgTag) { + final Match? srcMatch = RegExp( + 'src\s*=\s*["\']([^"\']*)["\']', + caseSensitive: false, + ).firstMatch(imgTag); + return srcMatch?.group(1); + } + + List buildFinalWidgetsFromSegments(List segments) { + final List widgets = []; + + for (final segment in segments) { + if (segment.startsWith('')) { + final String? imageUrl = extractImageUrl(segment); + if (imageUrl != null) { + widgets.add( + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: SvgPicture.network( + imageUrl, + colorFilter: ColorFilter.mode( + Colors.deepOrange, + BlendMode.srcIn, + ), + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.broken_image, color: Colors.red), + ), + ), + ); + } + } else { + final String md = html2md.convert(segment); + widgets.add( + GptMarkdown( + md, + linkBuilder: + ( + BuildContext context, + InlineSpan text, + String url, + TextStyle style, + ) { + return GestureDetector( + onTap: () { + showLinkConfirmationDialog(context, url); + }, + child: Text.rich( + text, + style: style.copyWith( + color: Colors.blue, + decoration: TextDecoration.underline, + ), + ), + ); + }, + ), + ); + } + } + return widgets; + } + + Future showLinkConfirmationDialog( + BuildContext context, + String url, + ) async { + return showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const Row( + children: [ + Icon(Icons.open_in_new, color: Colors.blueAccent), + SizedBox(width: 10), + Text( + 'Link öffnen', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), + ), + ], + ), + + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Möchtest du den folgenden externen Link öffnen?', + style: TextStyle(color: Colors.grey[700]), + ), + + const SizedBox(height: 5), + Text( + url, + style: const TextStyle( + color: Colors.blue, + fontStyle: FontStyle.italic, + decoration: TextDecoration.underline, + ), + ), + ], + ), + + actions: [ + TextButton( + onPressed: () { + Navigator.of(dialogContext).pop(false); + }, + child: const Text('Abbrechen'), + ), + + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () { + Navigator.of(dialogContext).pop(true); + dialogContext.push( + '/webviewPage', + extra: { + 'url': url, + } + ); + + }, + icon: const Icon(Icons.launch, size: 18), + label: const Text('Öffnen'), + ), + ], + ); + }, + ); + } + + Future> getCmListContents(List cmList) async { + List contents = []; + for (var cmId in cmList) { + contents.add(await widget.api.loadHtmlDataForMoodleModule(cmId)); + } + return contents; + } +} + +class MoodleCourseArguments { + final ShibbolethSession session; + final MoodleApi api; + final MoodleCourse course; + + MoodleCourseArguments({ + required this.session, + required this.api, + required this.course, + }); +} + +class WebViewPage extends StatefulWidget { + final String url; + + // Der Konstruktor benötigt nur noch die URL + const WebViewPage({ + required this.url, + super.key, + }); + + @override + State createState() => _WebViewPageState(); +} + +class _WebViewPageState extends State { + final GlobalKey webViewKey = GlobalKey(); + @override + void initState() { + super.initState(); + } + + + + @override + Widget build(BuildContext context) { + return InAppWebView( + key: webViewKey, + initialUrlRequest: URLRequest(url: WebUri(widget.url)), + onDownloadStartRequest: (controller, downloadStartRequest) async { + final String url = downloadStartRequest.url.toString(); + final String fileName = downloadStartRequest.suggestedFilename ?? url.split('/').last; + + final cookieManager = CookieManager.instance(); + final cookies = await cookieManager.getCookies(url: WebUri(url)); + final cookieHeader = cookies.map((c) => '${c.name}=${c.value}').join('; '); + + try { + ScaffoldMessenger.of(context).showSnackBar( + + SnackBar(content: Text('Download von "$fileName" gestartet...'), duration: Duration(seconds: 1),) + ); + await downloadAndOpenFile(url, fileName, cookieHeader); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Datei wird geöffnet...!')) + ); + + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Download fehlgeschlagen: ${e.toString()}')) + ); + } + + return null; + }, + shouldOverrideUrlLoading: (controller, navigationAction) async { + final Uri? url = navigationAction.request.url; + + if (url != null) { + + final String urlString = url.toString(); + if (urlString.toLowerCase().endsWith(".pdf")) { + final cookieManager = CookieManager.instance(); + final cookies = await cookieManager.getCookies(url: WebUri(url.toString())); + final cookieHeader = cookies.map((c) => '${c.name}=${c.value}').join('; '); + final String fileName = urlString.split("/").last; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Download von "$fileName" gestartet. Bitte warten...'), duration: Duration(seconds: 1),) + ); + context.pushReplacement("/pdf-viewer", extra: downloadAndOpenFile(urlString, fileName, cookieHeader)); + return NavigationActionPolicy.CANCEL; + } + } + + return NavigationActionPolicy.ALLOW; + }, + ); + } + + Future downloadAndOpenFile(String url, String fileName, String cookieHeader) async { + + final Directory dir = await getApplicationDocumentsDirectory(); + + + final String savePath = '${dir.path}/$fileName'; + var file = File(savePath); + if(file.existsSync()) { + OpenFilex.open(savePath); + return savePath; + } + + final Dio dio = Dio(); + + await dio.download( + url, + savePath, + options: Options( + headers: { + 'Cookie': cookieHeader, + }, + followRedirects: true, + ), + onReceiveProgress: (received, total) { + if (total != -1) { + } + }, + ); + OpenFilex.open(savePath); + debugPrint('Datei erfolgreich gespeichert unter: $savePath'); + return savePath; + } +} + +class PdfViewScreen extends StatelessWidget { + final Future stringPathFuture; + const PdfViewScreen({ + super.key, + required this.stringPathFuture + }); + + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('PDF Dokument')), + body: Center(child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FutureBuilder(future: stringPathFuture, builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const CircularProgressIndicator(); + } else if (snapshot.hasError) { + return Text("Fehler beim Laden des PDFs: ${snapshot.error}"); + } else { + return ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () { + OpenFilex.open(snapshot.data!); + + }, + icon: const Icon(Icons.settings, size: 18), + label: const Text('Öffnen'), + ); + } + }), + Text("Das PDF-Dokument wird geladen... Bitte warten") + + ], + )) + ); + } +} \ No newline at end of file diff --git a/lib/moodleComponent/view/moodle_viewmodel.dart b/lib/moodleComponent/view/moodle_viewmodel.dart new file mode 100644 index 00000000..8212ec27 --- /dev/null +++ b/lib/moodleComponent/view/moodle_viewmodel.dart @@ -0,0 +1,207 @@ +import 'package:campus_flutter/base/networking/protocols/api.dart'; +import 'package:campus_flutter/base/routing/routes.dart'; +import 'package:campus_flutter/homeComponent/screen/home_screen.dart'; +import 'package:campus_flutter/moodleComponent/model/moodle_course.dart'; +import 'package:campus_flutter/moodleComponent/model/moodle_user.dart'; +import 'package:campus_flutter/moodleComponent/networking/apis/MoodleApi.dart'; +import 'package:campus_flutter/moodleComponent/service/shibboleth_session_generator.dart'; +import 'package:campus_flutter/onboardingComponent/viewModels/onboarding_viewmodel.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +class MoodleViewModel extends ConsumerStatefulWidget { + const MoodleViewModel({super.key}); + + @override + ConsumerState createState()=> _MoodleViewModelState(); + +} + +class _MoodleViewModelState extends ConsumerState{ + + MoodleApi? api; + ShibbolethSession? session; + late Future _future; + var moodleCourses = []; + + @override + void initState() { + super.initState(); + if(Api.courses.isEmpty) { + _future = Api.coursesFuture!.then((value) { + setState(() { + moodleCourses = value; + session = Api.session; + api = Api.moodleApi; + }); + }).catchError((error) { + throw error; + }); + }else { + moodleCourses = Api.courses; + session = Api.session; + api = Api.moodleApi; + } + } + + + @override + Widget build(BuildContext context) { + return Api.courses.isEmpty ? FutureBuilder(future: _future, builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + Text("Verbindung wird hergestellt..."), + ], + )); + } else if (snapshot.hasError) { + if(snapshot.error is NoTumPasswordSetException) { + return Center(child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Kein TUM Password gesetzt. Bitte setze dein Passwort in den Einstellungen."), + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () { + context.push( + safetyArea + ); + + }, + icon: const Icon(Icons.settings, size: 18), + label: const Text('Öffnen'), + ), + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.secondary, + foregroundColor: Theme.of(context).colorScheme.onSecondary, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () async{ + await CookieManager.instance().deleteAllCookies(); + setState(() { + session = null; + api = null; + _future = connectToMoodle(ref).then((value) { + setState(() { + moodleCourses = value; + session = Api.session; + api = Api.moodleApi; + }); + }).catchError((error) { + throw error; + }); + }); + + }, + icon: const Icon(Icons.refresh, size: 18), + label: const Text('Erneut versuchen'), + ), + ], + )); + }else if(snapshot.error is WrongTumPasswordSetException) { + return Center(child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Das gesetzte TUM Password ist möglicherweise falsch. Bitte setze dein Passwort in den Einstellungen."), + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () { + context.push( + safetyArea + ); + + }, + icon: const Icon(Icons.settings, size: 18), + label: const Text('Öffnen'), + ), + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.secondary, + foregroundColor: Theme.of(context).colorScheme.onSecondary, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () async { + await CookieManager.instance().deleteAllCookies(); + setState(() { + session = null; + api = null; + _future = connectToMoodle(ref).then((value) { + setState(() { + moodleCourses = value; + session = Api.session; + api = Api.moodleApi; + }); + }).catchError((error) { + throw error; + }); + }); + + }, + icon: const Icon(Icons.refresh, size: 18), + label: const Text('Erneut versuchen'), + ), + ], + )); + } + debugPrintStack(stackTrace: snapshot.stackTrace); + return Center(child: Text("Error: ${snapshot.error}")); + } else { + return ListView.builder( + scrollDirection: MediaQuery.of(context).orientation == Orientation.landscape ? Axis.horizontal : Axis.vertical, + itemCount: moodleCourses.length, + itemBuilder: (context, index) => SizedBox( + height: MediaQuery.of(context).orientation == Orientation.portrait ? MediaQuery.of(context).size.height * 0.15 : MediaQuery.of(context).size.height*0.8, + width: MediaQuery.of(context).orientation == Orientation.portrait ? MediaQuery.of(context).size.width : MediaQuery.of(context).size.width * 0.25, + child: moodleCourses[index].build(context, session: session!, api: api!) + ) + ); + } + }) : ListView.builder( + scrollDirection: MediaQuery.of(context).orientation == Orientation.landscape ? Axis.horizontal : Axis.vertical, + itemCount: moodleCourses.length, + itemBuilder: (context, index) => SizedBox( + height: MediaQuery.of(context).orientation == Orientation.portrait ? MediaQuery.of(context).size.height * 0.15 : MediaQuery.of(context).size.height*0.8, + width: MediaQuery.of(context).orientation == Orientation.portrait ? MediaQuery.of(context).size.width : MediaQuery.of(context).size.width * 0.25, + child: moodleCourses[index].build(context, session: session!, api: api!) + ) + );; + } +} \ No newline at end of file diff --git a/lib/navigation_service.dart b/lib/navigation_service.dart index eac3f977..4bbeab52 100644 --- a/lib/navigation_service.dart +++ b/lib/navigation_service.dart @@ -49,6 +49,11 @@ class NavigationService { style: Theme.of(context).textTheme.titleLarge, ); case 4: + return Text( + context.tr("moodle"), + style: Theme.of(context).textTheme.titleLarge, + ); + case 5: return Text( context.tr("places"), style: Theme.of(context).textTheme.titleLarge, @@ -164,6 +169,19 @@ class NavigationService { selectedIcon: const Icon(Icons.campaign), label: context.tr("campus"), ), + NavigationDestination( + icon: Image.asset( + 'assets/images/logos/Moodle.png', + fit: BoxFit.cover, + height: 20, + ), + selectedIcon: Image.asset( + 'assets/images/logos/Moodle.png', + fit: BoxFit.cover, + height: 20, + ), + label: context.tr("moodle"), + ), NavigationDestination( icon: const Icon(Icons.place_outlined), selectedIcon: const Icon(Icons.place), diff --git a/lib/onboardingComponent/viewModels/onboarding_viewmodel.dart b/lib/onboardingComponent/viewModels/onboarding_viewmodel.dart index 038e962d..d7822df8 100644 --- a/lib/onboardingComponent/viewModels/onboarding_viewmodel.dart +++ b/lib/onboardingComponent/viewModels/onboarding_viewmodel.dart @@ -23,6 +23,7 @@ import 'package:go_router/go_router.dart'; import 'package:home_widget/home_widget.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:rxdart/rxdart.dart'; +import 'package:xor_encryption/xor_encryption.dart'; final onboardingViewModel = Provider((ref) => OnboardingViewModel()); @@ -34,6 +35,9 @@ class OnboardingViewModel { final TextEditingController textEditingController1 = TextEditingController(); final TextEditingController textEditingController2 = TextEditingController(); final TextEditingController textEditingController3 = TextEditingController(); + + final TextEditingController tumOnlinePasswordController = + TextEditingController(); void clearTextFields() { textEditingController1.clear(); @@ -76,6 +80,24 @@ class OnboardingViewModel { tumIdValid.add(true); } + + void savePassword(String password) { + var xorPassword = XorCipher().encryptData(password, Api.tumToken); + _storage.write(key: "password", value: xorPassword); + } + + void clearPassword() { + _storage.delete(key: "password"); + } + + Future getPassword() async { + var xorPassword = await _storage.read(key: "password"); + if (xorPassword != null) { + return XorCipher().encryptData(xorPassword, Api.tumToken); + } else { + throw NoTumPasswordSetException(); + } + } Future checkLogin() async { return _storage @@ -157,6 +179,7 @@ class OnboardingViewModel { ref.invalidate(studentCardViewModel); await getIt().clearCache(); await _storage.delete(key: "token"); + await _storage.delete(key: "password"); await HomeWidget.saveWidgetData("calendar", null); await HomeWidget.saveWidgetData("calendar_save", null); await HomeWidget.updateWidget( @@ -168,3 +191,17 @@ class OnboardingViewModel { credentials.add(Credentials.none); } } + +class NoTumPasswordSetException implements Exception { + final String message; + NoTumPasswordSetException([this.message = "No TUM Online password set"]); + @override + String toString() => "NoTumPasswordSetException: $message"; +} + +class WrongTumPasswordSetException implements Exception { + final String message; + WrongTumPasswordSetException([this.message = "Wrong TUM Online password set"]); + @override + String toString() => "WrongumPasswordSetException: $message"; +} diff --git a/lib/onboardingComponent/views/password_view.dart b/lib/onboardingComponent/views/password_view.dart new file mode 100644 index 00000000..318696f5 --- /dev/null +++ b/lib/onboardingComponent/views/password_view.dart @@ -0,0 +1,194 @@ +import 'package:campus_flutter/base/networking/protocols/api.dart'; +import 'package:campus_flutter/base/util/custom_back_button.dart'; +import 'package:campus_flutter/base/routing/routes.dart'; +import 'package:campus_flutter/calendarComponent/services/calendar_service.dart'; +import 'package:campus_flutter/onboardingComponent/viewModels/onboarding_viewmodel.dart'; +import 'package:campus_flutter/personComponent/services/profile_service.dart'; +import 'package:campus_flutter/studentCardComponent/viewModel/student_card_viewmodel.dart'; +import 'package:campus_flutter/studiesComponent/service/grade_service.dart'; +import 'package:campus_flutter/studiesComponent/service/lecture_service.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +class PasswordView extends ConsumerWidget { + var text = ""; + PasswordView({super.key}); + + + @override + Widget build(BuildContext context, WidgetRef ref) { + final backgroundColor = + Theme.of(context).brightness == Brightness.dark + ? Theme.of(context).canvasColor + : Colors.white; + return Scaffold( + backgroundColor: backgroundColor, + appBar: AppBar( + leading: const CustomBackButton(), + title: Text(context.tr("checkPermissions")), + backgroundColor: backgroundColor, + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + Expanded( + flex: 0, + child: Text( + "Setze hier deine Login-Daten für Moodle. Deine Daten werden verschlüsselt gespeichert.", + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + ), + const Spacer(), + _tumIdTextFields(context, ref), + const Spacer(), + _tumPasswordField(context, ref), + const Spacer(), + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.greenAccent, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () { + CookieManager.instance().deleteAllCookies(); + ref.read(onboardingViewModel).savePassword(text); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.tr("passwordSaved")), + backgroundColor: Colors.greenAccent, + ), + ); + }, + icon: const Icon(Icons.save, size: 18), + label: const Text('Passwort speichern'), + ), + const Spacer(), + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () { + CookieManager.instance().deleteAllCookies(); + ref.read(onboardingViewModel).clearPassword(); + ref.read(onboardingViewModel).tumOnlinePasswordController.clear(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.tr("passwordReset")), + ), + ); + }, + icon: const Icon(Icons.delete, size: 18), + label: const Text('Passwort zurücksetzen'), + ), + const Spacer(flex: 3), + ], + ), + ), + ); + } + + Widget _tumPasswordField(BuildContext context, WidgetRef ref) { + return TextField( + decoration: InputDecoration( + hintText: context.tr("password"), + border: const OutlineInputBorder(), + ), + obscureText: true, + controller: ref.read(onboardingViewModel).tumOnlinePasswordController, + onChanged: (text) { + this.text = text; + }, + enableSuggestions: false, + autocorrect: false, + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ); + } + + Widget _tumIdTextFields(BuildContext context, WidgetRef ref) { + return Row( + children: [ + const Spacer(), + Expanded( + child: _loginTextField( + "go", + TextInputType.text, + 2, + TextEditingController(text: Api.tumId[0] + Api.tumId[1]), + ref, context + ), + ), + const Padding(padding: EdgeInsets.symmetric(horizontal: 4.0)), + Expanded( + child: _loginTextField( + "42", + TextInputType.number, + 2, + TextEditingController(text: Api.tumId[2] + Api.tumId[3]), + ref, context + ), + ), + const Padding(padding: EdgeInsets.symmetric(horizontal: 4.0)), + Expanded( + child: _loginTextField( + "tum", + TextInputType.text, + 3, + TextEditingController(text: Api.tumId.substring(4, 7)), + ref, context + ), + ), + const Spacer(), + ], + ); + } + + Widget _loginTextField( + String hintText, + TextInputType keyboardType, + int maxLength, + TextEditingController controller, + WidgetRef ref, + BuildContext context + ) { + return TextField( + decoration: InputDecoration( + hintText: hintText, + hintStyle: TextStyle( + color: MediaQuery.platformBrightnessOf(context) == Brightness.dark + ? Colors.grey.shade700 + : Colors.grey.shade400, + ), + border: const OutlineInputBorder(), + ), + keyboardType: keyboardType, + inputFormatters: [LengthLimitingTextInputFormatter(maxLength)], + controller: controller, + onChanged: null, + enableSuggestions: false, + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + ); + } + + +} \ No newline at end of file diff --git a/lib/settingsComponent/views/general_settings_view.dart b/lib/settingsComponent/views/general_settings_view.dart index bcb9d350..ccf76cdc 100644 --- a/lib/settingsComponent/views/general_settings_view.dart +++ b/lib/settingsComponent/views/general_settings_view.dart @@ -20,6 +20,7 @@ class GeneralSettingsView extends ConsumerWidget { child: SeparatedList.widgets( widgets: [ _tokenPermission(context), + _safetyArea(context), _localeSelection(context, ref), _moreSettings(context), ], @@ -41,6 +42,19 @@ class GeneralSettingsView extends ConsumerWidget { ); } + Widget _safetyArea(BuildContext context) { + return ListTile( + dense: true, + leading: Icon(Icons.privacy_tip_outlined, size: 20, color: context.primaryColor), + title: Text( + "Sicherheit & Passwörter", + style: Theme.of(context).textTheme.bodyMedium, + ), + trailing: const Icon(Icons.arrow_forward_ios, size: 15), + onTap: () => context.push(safetyArea, extra: true), + ); + } + Widget _localeSelection(BuildContext context, WidgetRef ref) { return ListTile( dense: true, diff --git a/lib/settingsComponent/views/settings_view.dart b/lib/settingsComponent/views/settings_view.dart index c42c92ea..b2a6843e 100644 --- a/lib/settingsComponent/views/settings_view.dart +++ b/lib/settingsComponent/views/settings_view.dart @@ -28,12 +28,13 @@ class SettingsView extends ConsumerWidget { return Row( children: [ const Expanded( - child: Column( + child: SingleChildScrollView(child: Column( children: [ GeneralSettingsView(), AppearanceSettingsView(), CalendarSettingsView(), ], + ) ), ), Expanded( diff --git a/lib/studentCardComponent/views/student_card_view.dart b/lib/studentCardComponent/views/student_card_view.dart index 916da802..2bf5fad0 100644 --- a/lib/studentCardComponent/views/student_card_view.dart +++ b/lib/studentCardComponent/views/student_card_view.dart @@ -1,9 +1,11 @@ import 'package:campus_flutter/base/enums/error_handling_view_type.dart'; import 'package:campus_flutter/base/errorHandling/error_handling_router.dart'; import 'package:campus_flutter/base/extensions/context.dart'; +import 'package:campus_flutter/base/networking/protocols/api.dart'; import 'package:campus_flutter/base/util/card_with_padding.dart'; import 'package:campus_flutter/base/util/delayed_loading_indicator.dart'; import 'package:campus_flutter/base/util/last_updated_text.dart'; +import 'package:campus_flutter/homeComponent/screen/home_screen.dart'; import 'package:campus_flutter/studentCardComponent/viewModel/student_card_viewmodel.dart'; import 'package:campus_flutter/studentCardComponent/views/bar_code_view.dart'; import 'package:campus_flutter/studentCardComponent/views/information_view.dart'; @@ -23,6 +25,8 @@ class StudentCardView extends ConsumerWidget { if (snapshot.hasData) { if (snapshot.data!.isNotEmpty) { var data = snapshot.data!.first; + Api.tumId = data.studyID; + Api.coursesFuture ??= connectToMoodle(ref); final lastFetched = ref .read(studentCardViewModel) .lastFetched diff --git a/pubspec.lock b/pubspec.lock index 18435373..e2876396 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -471,6 +471,70 @@ packages: url: "https://github.com/mchome/flutter_colorpicker.git" source: git version: "1.1.0" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 + url: "https://pub.dev" + source: hosted + version: "1.3.0+1" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_windows: + dependency: transitive + description: + name: flutter_inappwebview_windows + sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" + url: "https://pub.dev" + source: hosted + version: "0.6.0" flutter_linkify: dependency: "direct main" description: @@ -493,6 +557,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_math_fork: + dependency: transitive + description: + name: flutter_math_fork + sha256: "6d5f2f1aa57ae539ffb0a04bb39d2da67af74601d685a161aff7ce5bda5fa407" + url: "https://pub.dev" + source: hosted + version: "0.7.4" flutter_native_splash: dependency: "direct main" description: @@ -577,10 +649,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 @@ -751,6 +823,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + gpt_markdown: + dependency: "direct main" + description: + name: gpt_markdown + sha256: "68d5337c8a00fc03a37dbddf84a6fd90401c30e99b6baf497ef9522a81fc34ee" + url: "https://pub.dev" + source: hosted + version: "1.1.2" graphs: dependency: transitive description: @@ -792,6 +872,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.6" + html2md: + dependency: "direct main" + description: + name: html2md + sha256: "465cf8ffa1b510fe0e97941579bf5b22e2d575f2cecb500a9c0254efe33a8036" + url: "https://pub.dev" + source: hosted + version: "1.3.2" http: dependency: transitive description: @@ -968,6 +1056,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" node_preamble: dependency: transitive description: @@ -984,6 +1080,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + open_filex: + dependency: "direct main" + description: + name: open_filex + sha256: "9976da61b6a72302cf3b1efbce259200cd40232643a467aac7370addf94d6900" + url: "https://pub.dev" + source: hosted + version: "4.7.0" package_config: dependency: transitive description: @@ -1168,6 +1272,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + provider: + dependency: transitive + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" pub_semver: dependency: transitive description: @@ -1541,6 +1653,30 @@ packages: url: "https://pub.dev" source: hosted version: "30.1.39" + syncfusion_flutter_pdf: + dependency: transitive + description: + name: syncfusion_flutter_pdf + sha256: c2f3e4e5febeaf68e84c9bcfe591088bd3b993acadad61b11c052eac3c8b1c7a + url: "https://pub.dev" + source: hosted + version: "30.1.39" + syncfusion_flutter_pdfviewer: + dependency: "direct main" + description: + name: syncfusion_flutter_pdfviewer + sha256: b06590a752c9e66273179a531a9cbf7fca61ca00a3411cfbe8fb1f67b72ec4a6 + url: "https://pub.dev" + source: hosted + version: "30.1.39" + syncfusion_flutter_signaturepad: + dependency: transitive + description: + name: syncfusion_flutter_signaturepad + sha256: d3ff61c7c0e6fcb8e5b712dcc2faa4a98503bb3b958c6e5182dc19e7c64366b8 + url: "https://pub.dev" + source: hosted + version: "30.1.39" syncfusion_localizations: dependency: transitive description: @@ -1549,6 +1685,46 @@ packages: url: "https://pub.dev" source: hosted version: "30.1.39" + syncfusion_pdfviewer_linux: + dependency: transitive + description: + name: syncfusion_pdfviewer_linux + sha256: "3dd5db820e26e6735a9e1c67d5e1f6ac9bb9d522ae284f46e3ce371319f75c5b" + url: "https://pub.dev" + source: hosted + version: "30.2.7" + syncfusion_pdfviewer_macos: + dependency: transitive + description: + name: syncfusion_pdfviewer_macos + sha256: "40b1e3e61366f3951a8b12ba8813c35ae70b1c8d536b2b417d689d2438d9c184" + url: "https://pub.dev" + source: hosted + version: "30.2.7" + syncfusion_pdfviewer_platform_interface: + dependency: transitive + description: + name: syncfusion_pdfviewer_platform_interface + sha256: ae5f4c5b2b7f5703d5ebb646641481dc58ae79fa5e6fd4ea86fa5c65a9556786 + url: "https://pub.dev" + source: hosted + version: "30.2.7" + syncfusion_pdfviewer_web: + dependency: transitive + description: + name: syncfusion_pdfviewer_web + sha256: "8020d0175047e24caf80341587d7b4be424c5f648bc4763c710cf59487ef16e2" + url: "https://pub.dev" + source: hosted + version: "30.2.7" + syncfusion_pdfviewer_windows: + dependency: transitive + description: + name: syncfusion_pdfviewer_windows + sha256: f46813908b065b74d151a71d4a1155fd5f9d8c59a5765cb6165a4949e626702c + url: "https://pub.dev" + source: hosted + version: "30.2.7" synchronized: dependency: transitive description: @@ -1613,6 +1789,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: @@ -1862,6 +2046,14 @@ packages: url: "https://github.com/jakobkoerber/xml2json.git" source: git version: "6.2.7" + xor_encryption: + dependency: "direct main" + description: + name: xor_encryption + sha256: "535520498dabddbd1818a694a6c6f9372b331f858bf52d9dccc80a1784a8ddd0" + url: "https://pub.dev" + source: hosted + version: "0.0.5" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c1fba03d..fa65b8f0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,7 +32,7 @@ dependencies: # user interface shimmer: ^3.0.0 flutter_staggered_grid_view: ^0.7.0 - flutter_svg: ^2.0.9 + flutter_svg: ^2.2.1 flutter_linkify: ^6.0.0 home_widget: ^0.8.0 auto_size_text: ^3.0.0 @@ -77,6 +77,16 @@ dependencies: easy_logger: ^0.0.2 intl: ^0.20.2 + # shibboleth/moodle + flutter_inappwebview: ^6.1.5 + html2md: ^1.3.2 + gpt_markdown: ^1.1.2 + syncfusion_flutter_pdfviewer: ^30.1.39 + open_filex: ^4.7.0 + + #password in settings_ui: + xor_encryption: ^0.0.5 + dependency_overrides: xml2json: git: @@ -113,6 +123,7 @@ flutter: - assets/images/logos/tum-logo-blue.png - assets/images/logos/tum-logo-blue-text.png - assets/images/logos/tum-logo-rainbow.png + - assets/images/logos/Moodle.png - assets/images/placeholders/portrait_placeholder.png - assets/images/placeholders/movie_placeholder.png - assets/images/placeholders/news_placeholder.png