Merge branch 'develop' into develop-widgets

# Conflicts:
#	lib/view/pages/timetable/timetable.dart
#	pubspec.yaml
This commit is contained in:
2025-09-07 11:06:49 +02:00
21 changed files with 333 additions and 83 deletions

View File

@@ -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'
}

View File

@@ -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"

View File

@@ -18,7 +18,7 @@ class AutocompleteResponseObject {
String label;
String? icon;
String? source;
List<String>? status;
String? status;
String? subline;
String? shareWithDisplayNameUniqe;

View File

@@ -28,7 +28,7 @@ AutocompleteResponseObject _$AutocompleteResponseObjectFromJson(
json['label'] as String,
json['icon'] as String?,
json['source'] as String?,
(json['status'] as List<dynamic>?)?.map((e) => e as String).toList(),
json['status'] as String?,
json['subline'] as String?,
json['shareWithDisplayNameUniqe'] as String?,
);

View File

@@ -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 {

View File

@@ -110,6 +110,7 @@ enum GetRoomResponseObjectConversationType {
@JsonValue(3) public,
@JsonValue(4) changelog,
@JsonValue(5) deleted,
@JsonValue(6) noteToSelf,
}
enum GetRoomResponseObjectParticipantNotificationLevel {

View File

@@ -102,6 +102,7 @@ const _$GetRoomResponseObjectConversationTypeEnumMap = {
GetRoomResponseObjectConversationType.public: 3,
GetRoomResponseObjectConversationType.changelog: 4,
GetRoomResponseObjectConversationType.deleted: 5,
GetRoomResponseObjectConversationType.noteToSelf: 6,
};
const _$GetRoomResponseObjectParticipantNotificationLevelEnumMap = {

View File

@@ -58,11 +58,9 @@ abstract class TalkApi<T extends ApiResponse?> 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');
}
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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';

View File

@@ -66,7 +66,7 @@ class _ChatInfoState extends State<ChatInfo> {
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!)),
),

View File

@@ -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<ParticipantsListView> createState() => _ParticipantsListViewState();
}
Widget build(BuildContext context) {
lastname(participant) => participant.displayName.toString().split(' ').last;
class _ParticipantsListViewState extends State<ParticipantsListView> {
@override
Widget build(BuildContext context) => Scaffold(
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);
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(),
],
))
],
)
);
}
}

View File

@@ -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<ChatBubble> 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<ChatBubble> 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<ChatBubble> with SingleTickerProviderStateM
bottom: 0,
right: 0,
left: 0,
child: LinearProgressIndicator(value: downloadProgress/100),
child: LinearProgressIndicator(value: downloadProgress == 1 ? null : downloadProgress/100),
),
),
],

View File

@@ -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<void> onOpen(LinkableElement link) async {

View File

@@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -38,7 +37,7 @@ class _MessageReactionsState extends State<MessageReactions> {
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<Widget>((entry) => ExpansionTile(

View File

@@ -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<Timetable> {
@override
void initState() {
settings = Provider.of<SettingsProvider>(context, listen: false);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Provider.of<TimetableProps>(context, listen: false).run();
});
@@ -131,4 +139,212 @@ class _TimetableState extends State<Timetable> {
updateTimings.cancel();
super.dispose();
}
List<TimeRegion> _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<DateTime>.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<GetTimetableResponseObject> _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;
}
}

View File

@@ -32,7 +32,7 @@ class DefaultSettings {
hiddenModules: [],
),
timetableSettings: TimetableSettings(
connectDoubleLessons: false,
connectDoubleLessons: true,
timetableNameMode: TimetableNameMode.name
),
talkSettings: TalkSettings(

View File

@@ -246,7 +246,7 @@ class _SettingsState extends State<Settings> {
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)
),

View File

@@ -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<FileViewer> {
);
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(

View File

@@ -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