From c444ed54a5b57bce966fc996faabbc78297055e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 11 Jul 2026 17:37:50 +0200 Subject: [PATCH] migrated Marianum Message module to the Marianum-Connect API and implemented push notification deep linking --- lib/api/demo/data/demo_marianum_message.dart | 8 ++- .../get_newsletter_file.dart | 34 ++++++++++ lib/app.dart | 9 +++ lib/notification/notification_controller.dart | 23 +++++-- lib/push/push_tap_router.dart | 24 +++++-- lib/routing/app_routes.dart | 20 +++++- .../bloc/marianum_message_state.dart | 2 + .../bloc/marianum_message_state.freezed.dart | 46 +++++++------ .../bloc/marianum_message_state.g.dart | 4 ++ .../marianum_message_get_messages.dart | 56 ++++++++++++--- .../marianum_message_list_view.dart | 19 ++++-- .../marianum_message_view.dart | 68 +++++++++++-------- .../search_marianum_messages.dart | 2 +- 13 files changed, 237 insertions(+), 78 deletions(-) create mode 100644 lib/api/marianumconnect/queries/get_newsletter_file/get_newsletter_file.dart diff --git a/lib/api/demo/data/demo_marianum_message.dart b/lib/api/demo/data/demo_marianum_message.dart index 071c8d5..0650dfa 100644 --- a/lib/api/demo/data/demo_marianum_message.dart +++ b/lib/api/demo/data/demo_marianum_message.dart @@ -1,3 +1,5 @@ +import 'package:intl/intl.dart'; + import '../../../state/app/modules/marianum_message/bloc/marianum_message_state.dart'; /// Demo fixtures for the info messages — a single friendly welcome entry so the @@ -6,16 +8,16 @@ class DemoMarianumMessage { const DemoMarianumMessage._(); static MarianumMessageList list() { - final today = DateTime.now(); - final date = - '${today.day.toString().padLeft(2, '0')}.${today.month.toString().padLeft(2, '0')}.${today.year}'; + final date = DateFormat.yMMMM('de').format(DateTime.now()); return MarianumMessageList( base: 'https://marianum-fulda.de', messages: [ MarianumMessage( + id: 'demo', name: 'Willkommen in der Marianum-App', date: date, + description: 'Eine kurze Begrüßung zum Ausprobieren.', url: 'https://marianum-fulda.de', ), ], diff --git a/lib/api/marianumconnect/queries/get_newsletter_file/get_newsletter_file.dart b/lib/api/marianumconnect/queries/get_newsletter_file/get_newsletter_file.dart new file mode 100644 index 0000000..4180c93 --- /dev/null +++ b/lib/api/marianumconnect/queries/get_newsletter_file/get_newsletter_file.dart @@ -0,0 +1,34 @@ +import 'dart:typed_data'; + +import 'package:dio/dio.dart'; + +import '../../errors/marianumconnect_error.dart'; +import '../../marianumconnect_api.dart'; +import '../../marianumconnect_endpoint.dart'; + +/// Downloads the raw PDF bytes of a Marianum Message from +/// `GET /api/mobile/v1/newsletter/{id}/file`. +/// +/// Goes through the shared MC dio so the bearer token is attached automatically; +/// the bytes are handed to `SfPdfViewer.memory` so no auth header has to be +/// plumbed into the viewer itself. +class GetNewsletterFile { + final String id; + final Dio _dio; + + GetNewsletterFile(this.id, {Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); + + Future run() async { + try { + final response = await _dio.get>( + MarianumConnectEndpoint.resolve( + 'newsletter/${Uri.encodeComponent(id)}/file', + ), + options: Options(responseType: ResponseType.bytes), + ); + return Uint8List.fromList(response.data!); + } on DioException catch (e) { + throw mapMarianumConnectError(e); + } + } +} diff --git a/lib/app.dart b/lib/app.dart index c1c4f35..40d1915 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -93,6 +93,13 @@ class _AppState extends State with WidgetsBindingObserver { NotificationTasks.navigateToTalk(context, chatToken: token); } + void _onNewsletterTapPending() { + final id = PushTapRouter.pendingNewsletterId.value; + if (id == null || !mounted) return; + PushTapRouter.pendingNewsletterId.value = null; + AppRoutes.openNewsletterById(context, id: id); + } + Future _handlePendingWidgetNavigation() async { final pending = await WidgetNavigation.consumePendingTimetableTap(); if (!pending || !mounted) return; @@ -191,6 +198,7 @@ class _AppState extends State with WidgetsBindingObserver { // Android renders pushes locally, so a tap arrives via the local // notifications callback (PushTapRouter) rather than onMessageOpenedApp. PushTapRouter.pendingChatToken.addListener(_onPushTapPending); + PushTapRouter.pendingNewsletterId.addListener(_onNewsletterTapPending); _onMessageSub = FirebaseMessaging.onMessage.listen((message) { if (!mounted) return; @@ -221,6 +229,7 @@ class _AppState extends State with WidgetsBindingObserver { _onMessageOpenedAppSub?.cancel(); _fcmTokenRefreshSub?.cancel(); PushTapRouter.pendingChatToken.removeListener(_onPushTapPending); + PushTapRouter.pendingNewsletterId.removeListener(_onNewsletterTapPending); ShareIntentListener.pending.removeListener(_handlePendingShare); ShareIntentListener.instance.detach(); Main.bottomNavigator.removeListener(_onTabControllerChanged); diff --git a/lib/notification/notification_controller.dart b/lib/notification/notification_controller.dart index 33d2920..57f4d8a 100644 --- a/lib/notification/notification_controller.dart +++ b/lib/notification/notification_controller.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../push/push_message_handler.dart'; +import '../routing/app_routes.dart'; import '../state/app/modules/chat/bloc/chat_bloc.dart'; import '../widget/debug/debug_tile.dart'; import '../widget/debug/json_viewer.dart'; @@ -41,10 +42,19 @@ class NotificationController { RemoteMessage message, BuildContext context, ) async { - NotificationTasks.navigateToTalk( - context, - chatToken: _extractChatToken(message), - ); + final newsletterId = _extractNewsletterId(message); + if (newsletterId != null) { + AppRoutes.openNewsletterById( + context, + id: newsletterId, + title: message.notification?.title, + ); + } else { + NotificationTasks.navigateToTalk( + context, + chatToken: _extractChatToken(message), + ); + } NotificationTasks.updateProviders(context); unawaited(NotificationTasks.refreshBadge()); @@ -66,4 +76,9 @@ class NotificationController { } return null; } + + static String? _extractNewsletterId(RemoteMessage message) { + final value = message.data['newsletterId']; + return value is String && value.isNotEmpty ? value : null; + } } diff --git a/lib/push/push_tap_router.dart b/lib/push/push_tap_router.dart index 80db51c..b2ff32e 100644 --- a/lib/push/push_tap_router.dart +++ b/lib/push/push_tap_router.dart @@ -16,6 +16,10 @@ class PushTapRouter { /// listens to this and opens the chat, then resets it to null. static final ValueNotifier pendingChatToken = ValueNotifier(null); + /// Newsletter id of the most recently tapped Marianum-Message notification, + /// or null. [App] listens to this and opens the message, then resets it. + static final ValueNotifier pendingNewsletterId = ValueNotifier(null); + static void handleResponse(NotificationResponse response) { final actionId = response.actionId; if (actionId == kTalkReplyActionId || actionId == kTalkMarkReadActionId) { @@ -23,18 +27,28 @@ class PushTapRouter { PushActions.handleBackgroundResponse(response); return; } - final token = _chatTokenFrom(response.payload); + final map = _payloadMap(response.payload); + if (map == null) return; + final newsletterId = _stringValue(map, 'newsletterId'); + if (newsletterId != null) { + pendingNewsletterId.value = newsletterId; + return; + } + final token = _stringValue(map, 'chatToken'); if (token != null) pendingChatToken.value = token; } - static String? _chatTokenFrom(String? payload) { + static Map? _payloadMap(String? payload) { if (payload == null || payload.isEmpty) return null; try { - final map = jsonDecode(payload) as Map; - final token = map['chatToken']; - return token is String && token.isNotEmpty ? token : null; + return jsonDecode(payload) as Map; } on Object { return null; } } + + static String? _stringValue(Map map, String key) { + final value = map[key]; + return value is String && value.isNotEmpty ? value : null; + } } diff --git a/lib/routing/app_routes.dart b/lib/routing/app_routes.dart index a55314a..4a47240 100644 --- a/lib/routing/app_routes.dart +++ b/lib/routing/app_routes.dart @@ -131,13 +131,29 @@ class AppRoutes { static void openMarianumMessage( BuildContext context, - String basePath, MarianumMessage message, ) { pushScreen( context, withNavBar: false, - screen: MessageView(basePath: basePath, message: message), + screen: MessageView(id: message.id, title: message.name), + ); + } + + /// Opens a Marianum Message by id — used for push deep links where only the + /// id (and the notification title) are known. + static void openNewsletterById( + BuildContext context, { + required String id, + String? title, + }) { + pushScreen( + context, + withNavBar: false, + screen: MessageView( + id: id, + title: title == null || title.isEmpty ? 'Marianum Message' : title, + ), ); } diff --git a/lib/state/app/modules/marianum_message/bloc/marianum_message_state.dart b/lib/state/app/modules/marianum_message/bloc/marianum_message_state.dart index 9421618..4a0742c 100644 --- a/lib/state/app/modules/marianum_message/bloc/marianum_message_state.dart +++ b/lib/state/app/modules/marianum_message/bloc/marianum_message_state.dart @@ -27,8 +27,10 @@ abstract class MarianumMessageList with _$MarianumMessageList { @freezed abstract class MarianumMessage with _$MarianumMessage { const factory MarianumMessage({ + @Default('') String id, required String name, required String date, + @Default('') String description, required String url, }) = _MarianumMessage; diff --git a/lib/state/app/modules/marianum_message/bloc/marianum_message_state.freezed.dart b/lib/state/app/modules/marianum_message/bloc/marianum_message_state.freezed.dart index aee5c09..c21533b 100644 --- a/lib/state/app/modules/marianum_message/bloc/marianum_message_state.freezed.dart +++ b/lib/state/app/modules/marianum_message/bloc/marianum_message_state.freezed.dart @@ -568,7 +568,7 @@ as List, /// @nodoc mixin _$MarianumMessage { - String get name; String get date; String get url; + String get id; String get name; String get date; String get description; String get url; /// Create a copy of MarianumMessage /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -581,16 +581,16 @@ $MarianumMessageCopyWith get copyWith => _$MarianumMessageCopyW @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is MarianumMessage&&(identical(other.name, name) || other.name == name)&&(identical(other.date, date) || other.date == date)&&(identical(other.url, url) || other.url == url)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is MarianumMessage&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.date, date) || other.date == date)&&(identical(other.description, description) || other.description == description)&&(identical(other.url, url) || other.url == url)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,name,date,url); +int get hashCode => Object.hash(runtimeType,id,name,date,description,url); @override String toString() { - return 'MarianumMessage(name: $name, date: $date, url: $url)'; + return 'MarianumMessage(id: $id, name: $name, date: $date, description: $description, url: $url)'; } @@ -601,7 +601,7 @@ abstract mixin class $MarianumMessageCopyWith<$Res> { factory $MarianumMessageCopyWith(MarianumMessage value, $Res Function(MarianumMessage) _then) = _$MarianumMessageCopyWithImpl; @useResult $Res call({ - String name, String date, String url + String id, String name, String date, String description, String url }); @@ -618,10 +618,12 @@ class _$MarianumMessageCopyWithImpl<$Res> /// Create a copy of MarianumMessage /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? date = null,Object? url = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? date = null,Object? description = null,Object? url = null,}) { return _then(_self.copyWith( -name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable +as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable as String, )); @@ -708,10 +710,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String name, String date, String url)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String name, String date, String description, String url)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _MarianumMessage() when $default != null: -return $default(_that.name,_that.date,_that.url);case _: +return $default(_that.id,_that.name,_that.date,_that.description,_that.url);case _: return orElse(); } @@ -729,10 +731,10 @@ return $default(_that.name,_that.date,_that.url);case _: /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String name, String date, String url) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String id, String name, String date, String description, String url) $default,) {final _that = this; switch (_that) { case _MarianumMessage(): -return $default(_that.name,_that.date,_that.url);case _: +return $default(_that.id,_that.name,_that.date,_that.description,_that.url);case _: throw StateError('Unexpected subclass'); } @@ -749,10 +751,10 @@ return $default(_that.name,_that.date,_that.url);case _: /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String name, String date, String url)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String name, String date, String description, String url)? $default,) {final _that = this; switch (_that) { case _MarianumMessage() when $default != null: -return $default(_that.name,_that.date,_that.url);case _: +return $default(_that.id,_that.name,_that.date,_that.description,_that.url);case _: return null; } @@ -764,11 +766,13 @@ return $default(_that.name,_that.date,_that.url);case _: @JsonSerializable() class _MarianumMessage implements MarianumMessage { - const _MarianumMessage({required this.name, required this.date, required this.url}); + const _MarianumMessage({this.id = '', required this.name, required this.date, this.description = '', required this.url}); factory _MarianumMessage.fromJson(Map json) => _$MarianumMessageFromJson(json); +@override@JsonKey() final String id; @override final String name; @override final String date; +@override@JsonKey() final String description; @override final String url; /// Create a copy of MarianumMessage @@ -784,16 +788,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _MarianumMessage&&(identical(other.name, name) || other.name == name)&&(identical(other.date, date) || other.date == date)&&(identical(other.url, url) || other.url == url)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _MarianumMessage&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.date, date) || other.date == date)&&(identical(other.description, description) || other.description == description)&&(identical(other.url, url) || other.url == url)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,name,date,url); +int get hashCode => Object.hash(runtimeType,id,name,date,description,url); @override String toString() { - return 'MarianumMessage(name: $name, date: $date, url: $url)'; + return 'MarianumMessage(id: $id, name: $name, date: $date, description: $description, url: $url)'; } @@ -804,7 +808,7 @@ abstract mixin class _$MarianumMessageCopyWith<$Res> implements $MarianumMessage factory _$MarianumMessageCopyWith(_MarianumMessage value, $Res Function(_MarianumMessage) _then) = __$MarianumMessageCopyWithImpl; @override @useResult $Res call({ - String name, String date, String url + String id, String name, String date, String description, String url }); @@ -821,10 +825,12 @@ class __$MarianumMessageCopyWithImpl<$Res> /// Create a copy of MarianumMessage /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? date = null,Object? url = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? date = null,Object? description = null,Object? url = null,}) { return _then(_MarianumMessage( -name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable +as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable as String, )); diff --git a/lib/state/app/modules/marianum_message/bloc/marianum_message_state.g.dart b/lib/state/app/modules/marianum_message/bloc/marianum_message_state.g.dart index ddadb8b..e6c21ec 100644 --- a/lib/state/app/modules/marianum_message/bloc/marianum_message_state.g.dart +++ b/lib/state/app/modules/marianum_message/bloc/marianum_message_state.g.dart @@ -32,14 +32,18 @@ Map _$MarianumMessageListToJson( _MarianumMessage _$MarianumMessageFromJson(Map json) => _MarianumMessage( + id: json['id'] as String? ?? '', name: json['name'] as String, date: json['date'] as String, + description: json['description'] as String? ?? '', url: json['url'] as String, ); Map _$MarianumMessageToJson(_MarianumMessage instance) => { + 'id': instance.id, 'name': instance.name, 'date': instance.date, + 'description': instance.description, 'url': instance.url, }; diff --git a/lib/state/app/modules/marianum_message/data_provider/marianum_message_get_messages.dart b/lib/state/app/modules/marianum_message/data_provider/marianum_message_get_messages.dart index a8eb24a..a0f7e69 100644 --- a/lib/state/app/modules/marianum_message/data_provider/marianum_message_get_messages.dart +++ b/lib/state/app/modules/marianum_message/data_provider/marianum_message_get_messages.dart @@ -1,13 +1,53 @@ import 'package:dio/dio.dart'; +import 'package:intl/intl.dart'; -import '../../../basis/dataloader/mhsl_data_loader.dart'; -import '../../../infrastructure/data_loader/data_loader.dart'; +import '../../../../../api/marianumconnect/errors/marianumconnect_error.dart'; +import '../../../../../api/marianumconnect/marianumconnect_api.dart'; +import '../../../../../api/marianumconnect/marianumconnect_endpoint.dart'; import '../bloc/marianum_message_state.dart'; -class MarianumMessageGetMessages extends MhslDataLoader { - @override - Future> fetch() async => dio.get('/message/messages.json'); - @override - MarianumMessageList assemble(DataLoaderResult data) => - MarianumMessageList.fromJson(data.asMap()); +/// Loads the "Marianum Message" list from `GET /api/mobile/v1/newsletter` +/// (formerly the mhsl.eu `message/messages.json` endpoint). Bearer token is +/// attached by the shared Marianum-Connect dio. +class MarianumMessageGetMessages { + final Dio _dio; + + MarianumMessageGetMessages({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); + + Future run() async { + try { + final response = await _dio.get>( + MarianumConnectEndpoint.resolve('newsletter'), + ); + final messages = (response.data ?? const []) + .cast>() + .map(_mapItem) + .toList(); + return MarianumMessageList( + base: MarianumConnectEndpoint.current(), + messages: messages, + ); + } on DioException catch (e) { + throw mapMarianumConnectError(e); + } + } + + MarianumMessage _mapItem(Map map) => MarianumMessage( + id: map['id'] as String, + name: map['title'] as String? ?? '', + date: _formatDate(map['date'] as String?), + description: map['description'] as String? ?? '', + url: map['fileUrl'] as String? ?? '', + ); + + static final DateFormat _monthYear = DateFormat.yMMMM('de'); + + /// Server sends ISO `yyyy-MM-dd`; the list shows only the German month + year + /// (e.g. "April 2024"). + static String _formatDate(String? iso) { + if (iso == null || iso.isEmpty) return ''; + final parsed = DateTime.tryParse(iso); + if (parsed == null) return iso; + return _monthYear.format(parsed); + } } diff --git a/lib/view/pages/marianum_message/marianum_message_list_view.dart b/lib/view/pages/marianum_message/marianum_message_list_view.dart index 733db5d..3c50cb3 100644 --- a/lib/view/pages/marianum_message/marianum_message_list_view.dart +++ b/lib/view/pages/marianum_message/marianum_message_list_view.dart @@ -47,14 +47,21 @@ class MarianumMessageListView extends StatelessWidget { children: [Icon(Icons.newspaper)], ), title: Text(message.name, overflow: TextOverflow.ellipsis), - subtitle: Text('vom ${message.date}'), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (message.description.isNotEmpty) + Text( + message.description, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Text('vom ${message.date}'), + ], + ), trailing: const Icon(Icons.arrow_right), onTap: () { - AppRoutes.openMarianumMessage( - context, - state.messageList.base, - message, - ); + AppRoutes.openMarianumMessage(context, message); }, ); }, diff --git a/lib/view/pages/marianum_message/marianum_message_view.dart b/lib/view/pages/marianum_message/marianum_message_view.dart index 1ce7b40..d255391 100644 --- a/lib/view/pages/marianum_message/marianum_message_view.dart +++ b/lib/view/pages/marianum_message/marianum_message_view.dart @@ -1,47 +1,57 @@ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../state/app/modules/marianum_message/bloc/marianum_message_state.dart'; +import '../../../api/marianumconnect/queries/get_newsletter_file/get_newsletter_file.dart'; import '../../../widget/confirm_dialog.dart'; -import '../../../widget/info_dialog.dart'; +import '../../../widget/placeholder_view.dart'; class MessageView extends StatefulWidget { - final String basePath; - final MarianumMessage message; - const MessageView({super.key, required this.basePath, required this.message}); + final String id; + final String title; + const MessageView({super.key, required this.id, required this.title}); @override State createState() => _MessageViewState(); } class _MessageViewState extends State { + late final Future _pdf = GetNewsletterFile(widget.id).run(); + @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar(title: Text(widget.message.name)), - body: SfPdfViewer.network( - widget.basePath + widget.message.url, - enableHyperlinkNavigation: true, - onDocumentLoadFailed: (PdfDocumentLoadFailedDetails e) { - Navigator.of(context).pop(); - InfoDialog.show( - context, - "Dokument '${widget.message.name}' konnte nicht geladen werden:\n${e.description}", - title: 'Fehler beim öffnen', - ); - }, - onHyperlinkClicked: (PdfHyperlinkClickedDetails e) { - showDialog( - context: context, - builder: (context) => ConfirmDialog( - title: 'Link öffnen', - content: 'Möchtest du den folgenden Link öffnen?\n${e.uri}', - confirmButton: 'Öffnen', - onConfirm: () => launchUrl( - Uri.parse(e.uri), - mode: LaunchMode.externalApplication, - ), - ), + appBar: AppBar(title: Text(widget.title)), + body: FutureBuilder( + future: _pdf, + builder: (context, snapshot) { + if (snapshot.hasError) { + return const PlaceholderView( + icon: Icons.error_outline, + text: 'Das Dokument konnte nicht geladen werden.', + ); + } + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator()); + } + return SfPdfViewer.memory( + snapshot.data!, + enableHyperlinkNavigation: true, + onHyperlinkClicked: (PdfHyperlinkClickedDetails e) { + showDialog( + context: context, + builder: (context) => ConfirmDialog( + title: 'Link öffnen', + content: 'Möchtest du den folgenden Link öffnen?\n${e.uri}', + confirmButton: 'Öffnen', + onConfirm: () => launchUrl( + Uri.parse(e.uri), + mode: LaunchMode.externalApplication, + ), + ), + ); + }, ); }, ), diff --git a/lib/view/pages/marianum_message/search_marianum_messages.dart b/lib/view/pages/marianum_message/search_marianum_messages.dart index effba1c..31de81a 100644 --- a/lib/view/pages/marianum_message/search_marianum_messages.dart +++ b/lib/view/pages/marianum_message/search_marianum_messages.dart @@ -54,7 +54,7 @@ class SearchMarianumMessages extends SearchDelegate { trailing: const Icon(Icons.arrow_right), onTap: () { close(context, message); - AppRoutes.openMarianumMessage(context, base, message); + AppRoutes.openMarianumMessage(context, message); }, ); },