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