diff --git a/android/app/build.gradle b/android/app/build.gradle index 77903e3..0649ca9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -28,13 +28,13 @@ android { ndkVersion "27.0.12077973" compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 coreLibraryDesugaringEnabled true - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } sourceSets { @@ -68,5 +68,5 @@ flutter { dependencies { implementation 'com.android.support:multidex:2.0.1' - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4' } diff --git a/android/settings.gradle b/android/settings.gradle index 67bf08a..35491da 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -20,7 +20,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version '8.7.3' apply false - id "org.jetbrains.kotlin.android" version "1.8.10" apply false + id "org.jetbrains.kotlin.android" version "2.1.10" apply false } include ":app" diff --git a/lib/api/marianumcloud/autocomplete/autocompleteResponse.dart b/lib/api/marianumcloud/autocomplete/autocompleteResponse.dart index 15e1ffc..8e72772 100644 --- a/lib/api/marianumcloud/autocomplete/autocompleteResponse.dart +++ b/lib/api/marianumcloud/autocomplete/autocompleteResponse.dart @@ -18,7 +18,7 @@ class AutocompleteResponseObject { String label; String? icon; String? source; - List? status; + String? status; String? subline; String? shareWithDisplayNameUniqe; diff --git a/lib/api/marianumcloud/autocomplete/autocompleteResponse.g.dart b/lib/api/marianumcloud/autocomplete/autocompleteResponse.g.dart index e029e0e..094f0b7 100644 --- a/lib/api/marianumcloud/autocomplete/autocompleteResponse.g.dart +++ b/lib/api/marianumcloud/autocomplete/autocompleteResponse.g.dart @@ -28,7 +28,7 @@ AutocompleteResponseObject _$AutocompleteResponseObjectFromJson( json['label'] as String, json['icon'] as String?, json['source'] as String?, - (json['status'] as List?)?.map((e) => e as String).toList(), + json['status'] as String?, json['subline'] as String?, json['shareWithDisplayNameUniqe'] as String?, ); diff --git a/lib/api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart b/lib/api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart index 1be47ce..3d0e9ff 100644 --- a/lib/api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart +++ b/lib/api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart @@ -55,12 +55,15 @@ class GetParticipantsResponseObject { } enum GetParticipantsResponseObjectParticipantType { - @JsonValue(1) owner, - @JsonValue(2) moderator, - @JsonValue(3) user, - @JsonValue(4) guest, - @JsonValue(5) userFollowingPublicLink, - @JsonValue(6) guestWithModeratorPermissions + @JsonValue(1) owner('Besitzer'), + @JsonValue(2) moderator('Moderator'), + @JsonValue(3) user('Teilnehmer'), + @JsonValue(4) guest('Gast'), + @JsonValue(5) userFollowingPublicLink('Teilnehmer über Link'), + @JsonValue(6) guestWithModeratorPermissions('Gast Moderator'); + + const GetParticipantsResponseObjectParticipantType(this.prettyName); + final String prettyName; } enum GetParticipantsResponseObjectParticipantsInCallFlags { diff --git a/lib/api/marianumcloud/talk/room/getRoomResponse.dart b/lib/api/marianumcloud/talk/room/getRoomResponse.dart index e8b9e27..10ae342 100644 --- a/lib/api/marianumcloud/talk/room/getRoomResponse.dart +++ b/lib/api/marianumcloud/talk/room/getRoomResponse.dart @@ -110,6 +110,7 @@ enum GetRoomResponseObjectConversationType { @JsonValue(3) public, @JsonValue(4) changelog, @JsonValue(5) deleted, + @JsonValue(6) noteToSelf, } enum GetRoomResponseObjectParticipantNotificationLevel { diff --git a/lib/api/marianumcloud/talk/room/getRoomResponse.g.dart b/lib/api/marianumcloud/talk/room/getRoomResponse.g.dart index f937b3b..738fc95 100644 --- a/lib/api/marianumcloud/talk/room/getRoomResponse.g.dart +++ b/lib/api/marianumcloud/talk/room/getRoomResponse.g.dart @@ -102,6 +102,7 @@ const _$GetRoomResponseObjectConversationTypeEnumMap = { GetRoomResponseObjectConversationType.public: 3, GetRoomResponseObjectConversationType.changelog: 4, GetRoomResponseObjectConversationType.deleted: 5, + GetRoomResponseObjectConversationType.noteToSelf: 6, }; const _$GetRoomResponseObjectParticipantNotificationLevelEnumMap = { diff --git a/lib/api/marianumcloud/talk/talkApi.dart b/lib/api/marianumcloud/talk/talkApi.dart index b90af22..e79340f 100644 --- a/lib/api/marianumcloud/talk/talkApi.dart +++ b/lib/api/marianumcloud/talk/talkApi.dart @@ -58,11 +58,9 @@ abstract class TalkApi extends ApiRequest { assembled?.headers = data.headers; return assembled; } catch (e) { - // TODO report error - log('Error assembling Talk API ${T.toString()} message: ${e.toString()} response on ${endpoint.path} with request body: $body and request headers: ${headers.toString()}'); + var message = 'Error assembling Talk API ${T.toString()} message: ${e.toString()} response with request body: $body and request headers: ${headers.toString()}'; + log(message); + throw Exception(message); } - - throw Exception('Error assembling Talk API response'); } - } diff --git a/lib/model/breakers/BreakerProps.dart b/lib/model/breakers/BreakerProps.dart index ec55ada..4386c2f 100644 --- a/lib/model/breakers/BreakerProps.dart +++ b/lib/model/breakers/BreakerProps.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:package_info_plus/package_info_plus.dart'; import '../../api/apiResponse.dart'; @@ -12,6 +13,8 @@ class BreakerProps extends DataHolder { PackageInfo? packageInfo; String? isBlocked(BreakerArea? type) { + if(kDebugMode) return null; + if(packageInfo == null) { PackageInfo.fromPlatform().then((value) => packageInfo = value); return null; diff --git a/lib/state/app/modules/app_modules.dart b/lib/state/app/modules/app_modules.dart index 2af4ebc..744feef 100644 --- a/lib/state/app/modules/app_modules.dart +++ b/lib/state/app/modules/app_modules.dart @@ -31,7 +31,7 @@ class AppModule { var available = { Modules.timetable: AppModule( Modules.timetable, - name: 'Vertretung', + name: 'Stundenplan', icon: () => Icon(Icons.calendar_month), breakerArea: BreakerArea.timetable, create: Timetable.new, @@ -62,7 +62,7 @@ class AppModule { ), Modules.files: AppModule( Modules.files, - name: 'Files', + name: 'Dateien', icon: () => Icon(Icons.folder), breakerArea: BreakerArea.files, create: Files.new, diff --git a/lib/view/pages/files/fileElement.dart b/lib/view/pages/files/fileElement.dart index 96aa306..cd8b772 100644 --- a/lib/view/pages/files/fileElement.dart +++ b/lib/view/pages/files/fileElement.dart @@ -1,11 +1,11 @@ import 'dart:io'; -import 'package:better_open_file/better_open_file.dart'; import 'package:filesize/filesize.dart'; import 'package:flowder/flowder.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; +import 'package:open_filex/open_filex.dart'; import '../../../widget/infoDialog.dart'; import 'package:nextcloud/nextcloud.dart'; import 'package:path_provider/path_provider.dart'; diff --git a/lib/view/pages/talk/chatDetails/chatInfo.dart b/lib/view/pages/talk/chatDetails/chatInfo.dart index 2460704..81ca7a3 100644 --- a/lib/view/pages/talk/chatDetails/chatInfo.dart +++ b/lib/view/pages/talk/chatDetails/chatInfo.dart @@ -66,7 +66,7 @@ class _ChatInfoState extends State { if(participants != null) ...[ ListTile( leading: const Icon(Icons.supervised_user_circle), - title: Text('${participants!.data.length} Teilnehmer'), + title: Text('${participants!.data.length} Mitglieder'), trailing: const Icon(Icons.arrow_right), onTap: () => TalkNavigator.pushSplitView(context, ParticipantsListView(participants!)), ), diff --git a/lib/view/pages/talk/chatDetails/participants/participantsListView.dart b/lib/view/pages/talk/chatDetails/participants/participantsListView.dart index 2bc863b..db25cb7 100644 --- a/lib/view/pages/talk/chatDetails/participants/participantsListView.dart +++ b/lib/view/pages/talk/chatDetails/participants/participantsListView.dart @@ -1,28 +1,44 @@ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import '../../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart'; import '../../../../../widget/userAvatar.dart'; -class ParticipantsListView extends StatefulWidget { +class ParticipantsListView extends StatelessWidget { final GetParticipantsResponse participantsResponse; const ParticipantsListView(this.participantsResponse, {super.key}); @override - State createState() => _ParticipantsListViewState(); -} + Widget build(BuildContext context) { + lastname(participant) => participant.displayName.toString().split(' ').last; + + final participants = participantsResponse.data + .sorted((a, b) => lastname(a).compareTo(lastname(b))) + .sorted((a, b) => a.participantType.index.compareTo(b.participantType.index)); + var groupedParticipants = participants.groupListsBy((participant) => participant.participantType); -class _ParticipantsListViewState extends State { - @override - Widget build(BuildContext context) => Scaffold( + return Scaffold( appBar: AppBar( - title: const Text('Teilnehmende'), + title: const Text('Mitglieder'), ), body: ListView( - children: widget.participantsResponse.data.map((participant) => ListTile( - leading: UserAvatar(id: participant.actorId), - title: Text(participant.displayName), - subtitle: participant.statusMessage != null ? Text(participant.statusMessage!) : null, - )).toList(), - ), + children: [ + ...groupedParticipants.entries.map((entry) => Column( + children: [ + ListTile( + title: Text(entry.key.prettyName), + titleTextStyle: TextStyle(fontWeight: FontWeight.bold), + ), + ...entry.value.map((participant) => ListTile( + leading: UserAvatar(id: participant.actorId), + title: Text(participant.displayName), + subtitle: participant.statusMessage != null ? Text(participant.statusMessage!) : null, + )), + Divider(), + ], + )) + ], + ) ); + } } diff --git a/lib/view/pages/talk/components/chatBubble.dart b/lib/view/pages/talk/components/chatBubble.dart index 6e72b52..fdf5239 100644 --- a/lib/view/pages/talk/components/chatBubble.dart +++ b/lib/view/pages/talk/components/chatBubble.dart @@ -1,4 +1,3 @@ -import 'package:better_open_file/better_open_file.dart'; import 'package:bubble/bubble.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emojis; import 'package:flowder/flowder.dart'; @@ -6,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:jiffy/jiffy.dart'; +import 'package:open_filex/open_filex.dart'; import '../../../../extensions/text.dart'; import 'package:provider/provider.dart'; @@ -206,7 +206,7 @@ class _ChatBubbleState extends State with SingleTickerProviderStateM ), ), Visibility( - visible: !message.containsFile, + visible: widget.bubbleData.message != '{file}', child: ListTile( leading: const Icon(Icons.copy), title: const Text('Nachricht kopieren'), @@ -323,7 +323,9 @@ class _ChatBubbleState extends State with SingleTickerProviderStateM return; } - downloadProgress = 1; + setState(() { + downloadProgress = 1; + }); downloadCore = FileElement.download(context, message.file!.path!, message.file!.name, (progress) { if(progress > 1) { setState(() { @@ -408,7 +410,7 @@ class _ChatBubbleState extends State with SingleTickerProviderStateM bottom: 0, right: 0, left: 0, - child: LinearProgressIndicator(value: downloadProgress/100), + child: LinearProgressIndicator(value: downloadProgress == 1 ? null : downloadProgress/100), ), ), ], diff --git a/lib/view/pages/talk/components/chatMessage.dart b/lib/view/pages/talk/components/chatMessage.dart index 626d011..90abb14 100644 --- a/lib/view/pages/talk/components/chatMessage.dart +++ b/lib/view/pages/talk/components/chatMessage.dart @@ -21,39 +21,49 @@ class ChatMessage { ChatMessage({required this.originalMessage, this.originalData}) { if(originalData?.containsKey('file') ?? false) { file = originalData?['file']; - content = file?.name ?? 'Datei'; - } else { - content = RichObjectStringProcessor.parseToString(originalMessage, originalData); } + content = RichObjectStringProcessor.parseToString(originalMessage, originalData); } Widget getWidget() { - if(file == null) { - return Linkify( - text: content, - onOpen: onOpen, - ); - } + var contentWidget = Linkify( + text: content, + onOpen: onOpen, + ); - return Padding(padding: const EdgeInsets.only(top: 5), child: CachedNetworkImage( - errorWidget: (context, url, error) => Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, + if(file == null) return contentWidget; + + return Padding( + padding: const EdgeInsets.only(top: 5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Icon(Icons.file_open_outlined, size: 35), - const SizedBox(width: 10), - Flexible(child: Text(file!.name, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.bold))), - const SizedBox(width: 10), + CachedNetworkImage( + errorWidget: (context, url, error) => Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.file_open_outlined, size: 35), + const SizedBox(width: 10), + Flexible(child: Text(file!.name, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.bold))), + const SizedBox(width: 10), + ], + ), + alignment: Alignment.center, + placeholder: (context, url) => const Padding(padding: EdgeInsets.all(15), child: SizedBox(width: 50, child: LinearProgressIndicator())), + fadeInDuration: Duration.zero, + fadeOutDuration: Duration.zero, + errorListener: (value) {}, + imageUrl: 'https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=130&y=-1&a=1', + ), + if(originalMessage != '{file}') ...[ + SizedBox(height: 5), + contentWidget + ] ], - ), - alignment: Alignment.center, - placeholder: (context, url) => const Padding(padding: EdgeInsets.all(15), child: SizedBox(width: 50, child: LinearProgressIndicator())), - fadeInDuration: Duration.zero, - fadeOutDuration: Duration.zero, - errorListener: (value) {}, - imageUrl: 'https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=100&y=-1&a=1', - )); + ) + ); } Future onOpen(LinkableElement link) async { diff --git a/lib/view/pages/talk/messageReactions.dart b/lib/view/pages/talk/messageReactions.dart index 1ab77ca..b3aeaf2 100644 --- a/lib/view/pages/talk/messageReactions.dart +++ b/lib/view/pages/talk/messageReactions.dart @@ -1,4 +1,3 @@ - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -38,7 +37,7 @@ class _MessageReactionsState extends State { future: data, builder: (context, snapshot) { if(snapshot.connectionState == ConnectionState.waiting) return const LoadingSpinner(); - if(snapshot.data == null) return const PlaceholderView(icon: Icons.search_off_outlined, text: 'Keine Reaktionen gefunden!'); + if(snapshot.data!.data.isEmpty) return const PlaceholderView(icon: Icons.search_off_outlined, text: 'Keine Reaktionen gefunden!'); return ListView( children: [ ...snapshot.data!.data.entries.map((entry) => ExpansionTile( diff --git a/lib/view/pages/timetable/timetable.dart b/lib/view/pages/timetable/timetable.dart index 8109c51..c55300e 100644 --- a/lib/view/pages/timetable/timetable.dart +++ b/lib/view/pages/timetable/timetable.dart @@ -1,17 +1,24 @@ - import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; +import '../../../api/webuntis/queries/getHolidays/getHolidaysResponse.dart'; +import '../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart'; +import '../../../extensions/dateTime.dart'; import '../../../homescreen_widgets/timetable/timetableHomeWidget.dart'; import '../../../model/timetable/timetableProps.dart'; import '../../../storage/base/settingsProvider.dart'; import '../../../widget/loadingSpinner.dart'; import '../../../widget/placeholderView.dart'; +import 'arbitraryAppointment.dart'; import 'calendar.dart'; +import 'customTimetableColors.dart'; import 'customTimetableEventEditDialog.dart'; +import 'timetableEvents.dart'; +import 'timetableNameMode.dart'; import 'viewCustomTimetableEvents.dart'; class Timetable extends StatefulWidget { @@ -31,6 +38,7 @@ class _TimetableState extends State { @override void initState() { settings = Provider.of(context, listen: false); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { Provider.of(context, listen: false).run(); }); @@ -131,4 +139,212 @@ class _TimetableState extends State { updateTimings.cancel(); super.dispose(); } + + List _buildSpecialTimeRegions(GetHolidaysResponse holidays) { + var lastMonday = DateTime.now().subtract(const Duration(days: 14)).nextWeekday(DateTime.monday); + var firstBreak = lastMonday.copyWith(hour: 10, minute: 15); + var secondBreak = lastMonday.copyWith(hour: 13, minute: 50); + + var holidayList = holidays.result.map((holiday) { + var startDay = _parseWebuntisTimestamp(holiday.startDate, 0); + var dayCount = _parseWebuntisTimestamp(holiday.endDate, 0) + .difference(startDay) + .inDays; + var days = List.generate(dayCount, (index) => startDay.add(Duration(days: index))); + + return days.map((holidayDay) => TimeRegion( + startTime: holidayDay.copyWith(hour: 07, minute: 55), + endTime: holidayDay.copyWith(hour: 16, minute: 30), + text: 'holiday:${holiday.name}', + color: Theme + .of(context) + .disabledColor + .withAlpha(50), + iconData: Icons.holiday_village_outlined + )); + }).expand((e) => e); + + bool isInHoliday(DateTime time) => holidayList.any((element) => element.startTime.isSameDay(time)); + + return [ + ...holidayList, + + if(!isInHoliday(firstBreak)) + TimeRegion( + startTime: firstBreak, + endTime: firstBreak.add(const Duration(minutes: 20)), + recurrenceRule: 'FREQ=DAILY;INTERVAL=1', + text: 'centerIcon', + color: Theme.of(context).primaryColor.withAlpha(50), + iconData: Icons.restaurant + ), + + if(!isInHoliday(secondBreak)) + TimeRegion( + startTime: secondBreak, + endTime: secondBreak.add(const Duration(minutes: 15)), + recurrenceRule: 'FREQ=DAILY;INTERVAL=1', + text: 'centerIcon', + color: Theme.of(context).primaryColor.withAlpha(50), + iconData: Icons.restaurant + ), + ]; + } + + List _removeDuplicates(TimetableProps data, Duration maxTimeBetweenDouble) { + + var timetableList = data.getTimetableResponse.result.toList(); + + if(timetableList.isEmpty) return timetableList; + + timetableList.sort((a, b) => _parseWebuntisTimestamp(a.date, a.startTime).compareTo(_parseWebuntisTimestamp(b.date, b.startTime))); + + var previousElement = timetableList.first; + for(var i = 1; i < timetableList.length; i++) { + var currentElement = timetableList.elementAt(i); + + bool isSameLesson() { + var currentSubjectId = currentElement.su.firstOrNull?.id; + var previousSubjectId = previousElement.su.firstOrNull?.id; + + if(currentSubjectId == null || previousSubjectId == null || currentSubjectId != previousSubjectId) return false; + + var currentRoomId = currentElement.ro.firstOrNull?.id; + var previousRoomId = previousElement.ro.firstOrNull?.id; + + if(currentRoomId != previousRoomId) return false; + + var currentTeacherId = currentElement.te.firstOrNull?.id; + var previousTeacherId = previousElement.te.firstOrNull?.id; + + if(currentTeacherId != previousTeacherId) return false; + + var currentStatusCode = currentElement.code; + var previousStatusCode = previousElement.code; + + if(currentStatusCode != previousStatusCode) return false; + + return true; + } + + bool isNotSeparated() => _parseWebuntisTimestamp(previousElement.date, previousElement.endTime).add(maxTimeBetweenDouble) + .isSameOrAfter(_parseWebuntisTimestamp(currentElement.date, currentElement.startTime)); + + if(isSameLesson() && isNotSeparated()) { + previousElement.endTime = currentElement.endTime; + timetableList.remove(currentElement); + i--; + } else { + previousElement = currentElement; + } + } + + return timetableList; + } + + TimetableEvents _buildTableEvents(TimetableProps data) { + + var timetableList = data.getTimetableResponse.result.toList(); + + if(settings.val().timetableSettings.connectDoubleLessons) { + timetableList = _removeDuplicates(data, const Duration(minutes: 5)); + } + + var appointments = timetableList.map((element) { + + var rooms = data.getRoomsResponse; + var subjects = data.getSubjectsResponse; + + try { + var startTime = _parseWebuntisTimestamp(element.date, element.startTime); + var endTime = _parseWebuntisTimestamp(element.date, element.endTime); + + var subject = subjects.result.firstWhereOrNull((subject) => subject.id == element.su.firstOrNull?.id); + var subjectName = 'Unbekannt'; + if(subject != null) { + subjectName = { + TimetableNameMode.name: subject.name, + TimetableNameMode.longName: subject.longName, + TimetableNameMode.alternateName: subject.alternateName, + }[settings.val().timetableSettings.timetableNameMode]!; + } + + return Appointment( + id: ArbitraryAppointment(webuntis: element), + startTime: startTime, + endTime: endTime, + subject: subjectName, + location: '' + '${rooms.result.firstWhereOrNull((room) => room.id == element.ro.firstOrNull?.id)?.name ?? 'Unbekannt'}' + '\n' + '${element.te.firstOrNull?.longname ?? 'Unbekannt'}', + notes: element.activityType, + color: _getEventColor(element, startTime, endTime), + ); + } catch(e) { + var endTime = _parseWebuntisTimestamp(element.date, element.endTime); + return Appointment( + id: ArbitraryAppointment(webuntis: element), + startTime: _parseWebuntisTimestamp(element.date, element.startTime), + endTime: endTime, + subject: 'Änderung', + notes: element.info, + location: 'Unbekannt', + color: const Color(0xff404040).withAlpha(endTime.isBefore(DateTime.now()) ? 100 : 255), + startTimeZone: '', + endTimeZone: '', + ); + } + }).toList(); + + appointments.addAll(data.getCustomTimetableEventResponse.events.map((customEvent) => Appointment( + id: ArbitraryAppointment(custom: customEvent), + startTime: customEvent.startDate, + endTime: customEvent.endDate, + location: customEvent.description, + subject: customEvent.title, + recurrenceRule: customEvent.rrule, + color: TimetableColors.getColorFromString(customEvent.color ?? TimetableColors.defaultColor.name), + startTimeZone: '', + endTimeZone: '', + ))); + + return TimetableEvents(appointments); + } + + DateTime _parseWebuntisTimestamp(int date, int time) { + var timeString = time.toString().padLeft(4, '0'); + return DateTime.parse('$date ${timeString.substring(0, 2)}:${timeString.substring(2, 4)}'); + } + + Color _getEventColor(GetTimetableResponseObject webuntisElement, DateTime startTime, DateTime endTime) { + // Make element darker, when it already took place + var alpha = endTime.isBefore(DateTime.now()) ? 100 : 255; + + // Cancelled + if(webuntisElement.code == 'cancelled') return const Color(0xff000000).withAlpha(alpha); + + // Any changes or no teacher at this element + if(webuntisElement.code == 'irregular' || webuntisElement.te.first.id == 0) return const Color(0xff8F19B3).withAlpha(alpha); + + // Teacher has changed + if(webuntisElement.te.any((element) => element.orgname != null)) return const Color(0xFF29639B).withAlpha(alpha); + + // Event was in the past + if(endTime.isBefore(DateTime.now())) return Theme.of(context).primaryColor.withAlpha(alpha); + + // Event takes currently place + if(endTime.isAfter(DateTime.now()) && startTime.isBefore(DateTime.now())) return Theme.of(context).primaryColor.withRed(200); + + // Fallback + return Theme.of(context).primaryColor.withAlpha(alpha); + } + + bool _isCrossedOut(CalendarAppointmentDetails calendarEntry) { + var appointment = calendarEntry.appointments.first.id as ArbitraryAppointment; + if(appointment.hasWebuntis()) { + return appointment.webuntis!.code == 'cancelled'; + } + return false; + } } diff --git a/lib/view/settings/defaultSettings.dart b/lib/view/settings/defaultSettings.dart index 36b2289..7b99b25 100644 --- a/lib/view/settings/defaultSettings.dart +++ b/lib/view/settings/defaultSettings.dart @@ -32,7 +32,7 @@ class DefaultSettings { hiddenModules: [], ), timetableSettings: TimetableSettings( - connectDoubleLessons: false, + connectDoubleLessons: true, timetableNameMode: TimetableNameMode.name ), talkSettings: TalkSettings( diff --git a/lib/view/settings/settings.dart b/lib/view/settings/settings.dart index 9784a29..0d3f58d 100644 --- a/lib/view/settings/settings.dart +++ b/lib/view/settings/settings.dart @@ -246,7 +246,7 @@ class _SettingsState extends State { ListTile( leading: const CenteredLeading(Icon(Icons.date_range_outlined)), title: const Text('Infos zu Web-/ Untis'), - subtitle: const Text('Für den Vertretungsplan'), + subtitle: const Text('Für den Stundenplan'), trailing: const Icon(Icons.arrow_right), onTap: () => PrivacyInfo(providerText: 'Untis', imprintUrl: 'https://www.untis.at/impressum', privacyUrl: 'https://www.untis.at/datenschutz-wu-apps').showPopup(context) ), diff --git a/lib/widget/fileViewer.dart b/lib/widget/fileViewer.dart index 8eb01e1..085caf4 100644 --- a/lib/widget/fileViewer.dart +++ b/lib/widget/fileViewer.dart @@ -1,8 +1,8 @@ import 'dart:io'; import 'dart:math'; -import 'package:better_open_file/better_open_file.dart'; import 'package:flutter/material.dart'; +import 'package:open_filex/open_filex.dart'; import 'package:photo_view/photo_view.dart'; import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; @@ -93,7 +93,7 @@ class _FileViewerState extends State { ); default: - OpenFile.open(widget.path).then((result) { + OpenFilex.open(widget.path).then((result) { Navigator.of(context).pop(); if(result.type != ResultType.done) { showDialog(context: context, builder: (context) => AlertDialog( diff --git a/pubspec.yaml b/pubspec.yaml index 5421bc7..5cd1578 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.1.2+40 +version: 0.1.5+43 environment: sdk: '>3.0.0' @@ -42,7 +42,6 @@ dependencies: animated_digit: ^3.2.3 async: ^2.11.0 badges: ^3.1.2 - better_open_file: ^3.6.5 bloc: ^9.0.0 bottom_sheet: ^4.0.4 bubble: ^1.2.1 @@ -55,11 +54,11 @@ dependencies: easy_debounce: ^2.0.3 emoji_picker_flutter: ^4.3.0 fast_rsa: ^3.7.1 - file_picker: ^8.1.7 + file_picker: ^10.3.2 filesize: ^2.0.1 - firebase_core: ^3.10.1 - firebase_in_app_messaging: ^0.8.1+1 - firebase_messaging: ^15.2.1 + firebase_core: ^4.1.0 + firebase_in_app_messaging: ^0.9.0+1 + firebase_messaging: ^16.0.1 flowder: git: url: https://github.com/Harsh223/flowder.git @@ -67,11 +66,11 @@ dependencies: flutter_bloc: ^9.0.0 flutter_launcher_icons: ^0.14.3 flutter_linkify: ^6.0.0 - flutter_local_notifications: ^18.0.1 + flutter_local_notifications: ^19.4.1 flutter_login: ^5.0.0 flutter_native_splash: ^2.4.4 flutter_split_view: ^0.1.2 - freezed_annotation: ^2.4.4 + freezed_annotation: ^3.1.0 http: ^1.3.0 hydrated_bloc: ^10.0.0 image_picker: ^1.1.2 @@ -87,20 +86,22 @@ dependencies: url: https://github.com/provokateurin/nextcloud-neon package_info_plus: ^8.1.3 path_provider: ^2.1.5 - persistent_bottom_nav_bar_v2: ^5.3.1 + persistent_bottom_nav_bar_v2: ^6.1.0 photo_view: ^0.15.0 pretty_json: ^2.0.0 provider: ^6.1.2 qr_flutter: ^4.1.0 rrule: ^0.2.17 rrule_generator: ^0.9.0 - share_plus: ^10.1.4 + share_plus: ^11.1.0 shared_preferences: ^2.3.5 - syncfusion_flutter_calendar: ^28.1.41 - syncfusion_flutter_pdfviewer: ^28.1.41 + syncfusion_flutter_calendar: ^31.1.17 + syncfusion_flutter_pdfviewer: ^31.1.17 time_range_picker: ^2.3.0 url_launcher: ^6.3.1 uuid: ^4.5.1 + open_filex: ^4.7.0 + collection: ^1.19.0 home_widget: ^0.7.0+1 screenshot: ^3.0.0 background_fetch: ^1.3.7