migrated Marianum Message module to the Marianum-Connect API and implemented push notification deep linking

This commit is contained in:
2026-07-11 17:37:50 +02:00
parent ff23199345
commit c444ed54a5
13 changed files with 237 additions and 78 deletions
+5 -3
View File
@@ -1,3 +1,5 @@
import 'package:intl/intl.dart';
import '../../../state/app/modules/marianum_message/bloc/marianum_message_state.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 /// Demo fixtures for the info messages — a single friendly welcome entry so the
@@ -6,16 +8,16 @@ class DemoMarianumMessage {
const DemoMarianumMessage._(); const DemoMarianumMessage._();
static MarianumMessageList list() { static MarianumMessageList list() {
final today = DateTime.now(); final date = DateFormat.yMMMM('de').format(DateTime.now());
final date =
'${today.day.toString().padLeft(2, '0')}.${today.month.toString().padLeft(2, '0')}.${today.year}';
return MarianumMessageList( return MarianumMessageList(
base: 'https://marianum-fulda.de', base: 'https://marianum-fulda.de',
messages: [ messages: [
MarianumMessage( MarianumMessage(
id: 'demo',
name: 'Willkommen in der Marianum-App', name: 'Willkommen in der Marianum-App',
date: date, date: date,
description: 'Eine kurze Begrüßung zum Ausprobieren.',
url: 'https://marianum-fulda.de', url: 'https://marianum-fulda.de',
), ),
], ],
@@ -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<Uint8List> run() async {
try {
final response = await _dio.get<List<int>>(
MarianumConnectEndpoint.resolve(
'newsletter/${Uri.encodeComponent(id)}/file',
),
options: Options(responseType: ResponseType.bytes),
);
return Uint8List.fromList(response.data!);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
}
+9
View File
@@ -93,6 +93,13 @@ class _AppState extends State<App> with WidgetsBindingObserver {
NotificationTasks.navigateToTalk(context, chatToken: token); 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<void> _handlePendingWidgetNavigation() async { Future<void> _handlePendingWidgetNavigation() async {
final pending = await WidgetNavigation.consumePendingTimetableTap(); final pending = await WidgetNavigation.consumePendingTimetableTap();
if (!pending || !mounted) return; if (!pending || !mounted) return;
@@ -191,6 +198,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
// Android renders pushes locally, so a tap arrives via the local // Android renders pushes locally, so a tap arrives via the local
// notifications callback (PushTapRouter) rather than onMessageOpenedApp. // notifications callback (PushTapRouter) rather than onMessageOpenedApp.
PushTapRouter.pendingChatToken.addListener(_onPushTapPending); PushTapRouter.pendingChatToken.addListener(_onPushTapPending);
PushTapRouter.pendingNewsletterId.addListener(_onNewsletterTapPending);
_onMessageSub = FirebaseMessaging.onMessage.listen((message) { _onMessageSub = FirebaseMessaging.onMessage.listen((message) {
if (!mounted) return; if (!mounted) return;
@@ -221,6 +229,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
_onMessageOpenedAppSub?.cancel(); _onMessageOpenedAppSub?.cancel();
_fcmTokenRefreshSub?.cancel(); _fcmTokenRefreshSub?.cancel();
PushTapRouter.pendingChatToken.removeListener(_onPushTapPending); PushTapRouter.pendingChatToken.removeListener(_onPushTapPending);
PushTapRouter.pendingNewsletterId.removeListener(_onNewsletterTapPending);
ShareIntentListener.pending.removeListener(_handlePendingShare); ShareIntentListener.pending.removeListener(_handlePendingShare);
ShareIntentListener.instance.detach(); ShareIntentListener.instance.detach();
Main.bottomNavigator.removeListener(_onTabControllerChanged); Main.bottomNavigator.removeListener(_onTabControllerChanged);
+19 -4
View File
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../push/push_message_handler.dart'; import '../push/push_message_handler.dart';
import '../routing/app_routes.dart';
import '../state/app/modules/chat/bloc/chat_bloc.dart'; import '../state/app/modules/chat/bloc/chat_bloc.dart';
import '../widget/debug/debug_tile.dart'; import '../widget/debug/debug_tile.dart';
import '../widget/debug/json_viewer.dart'; import '../widget/debug/json_viewer.dart';
@@ -41,10 +42,19 @@ class NotificationController {
RemoteMessage message, RemoteMessage message,
BuildContext context, BuildContext context,
) async { ) async {
NotificationTasks.navigateToTalk( final newsletterId = _extractNewsletterId(message);
context, if (newsletterId != null) {
chatToken: _extractChatToken(message), AppRoutes.openNewsletterById(
); context,
id: newsletterId,
title: message.notification?.title,
);
} else {
NotificationTasks.navigateToTalk(
context,
chatToken: _extractChatToken(message),
);
}
NotificationTasks.updateProviders(context); NotificationTasks.updateProviders(context);
unawaited(NotificationTasks.refreshBadge()); unawaited(NotificationTasks.refreshBadge());
@@ -66,4 +76,9 @@ class NotificationController {
} }
return null; return null;
} }
static String? _extractNewsletterId(RemoteMessage message) {
final value = message.data['newsletterId'];
return value is String && value.isNotEmpty ? value : null;
}
} }
+19 -5
View File
@@ -16,6 +16,10 @@ class PushTapRouter {
/// listens to this and opens the chat, then resets it to null. /// listens to this and opens the chat, then resets it to null.
static final ValueNotifier<String?> pendingChatToken = ValueNotifier(null); static final ValueNotifier<String?> 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<String?> pendingNewsletterId = ValueNotifier(null);
static void handleResponse(NotificationResponse response) { static void handleResponse(NotificationResponse response) {
final actionId = response.actionId; final actionId = response.actionId;
if (actionId == kTalkReplyActionId || actionId == kTalkMarkReadActionId) { if (actionId == kTalkReplyActionId || actionId == kTalkMarkReadActionId) {
@@ -23,18 +27,28 @@ class PushTapRouter {
PushActions.handleBackgroundResponse(response); PushActions.handleBackgroundResponse(response);
return; 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; if (token != null) pendingChatToken.value = token;
} }
static String? _chatTokenFrom(String? payload) { static Map<String, dynamic>? _payloadMap(String? payload) {
if (payload == null || payload.isEmpty) return null; if (payload == null || payload.isEmpty) return null;
try { try {
final map = jsonDecode(payload) as Map<String, dynamic>; return jsonDecode(payload) as Map<String, dynamic>;
final token = map['chatToken'];
return token is String && token.isNotEmpty ? token : null;
} on Object { } on Object {
return null; return null;
} }
} }
static String? _stringValue(Map<String, dynamic> map, String key) {
final value = map[key];
return value is String && value.isNotEmpty ? value : null;
}
} }
+18 -2
View File
@@ -131,13 +131,29 @@ class AppRoutes {
static void openMarianumMessage( static void openMarianumMessage(
BuildContext context, BuildContext context,
String basePath,
MarianumMessage message, MarianumMessage message,
) { ) {
pushScreen( pushScreen(
context, context,
withNavBar: false, 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,
),
); );
} }
@@ -27,8 +27,10 @@ abstract class MarianumMessageList with _$MarianumMessageList {
@freezed @freezed
abstract class MarianumMessage with _$MarianumMessage { abstract class MarianumMessage with _$MarianumMessage {
const factory MarianumMessage({ const factory MarianumMessage({
@Default('') String id,
required String name, required String name,
required String date, required String date,
@Default('') String description,
required String url, required String url,
}) = _MarianumMessage; }) = _MarianumMessage;
@@ -568,7 +568,7 @@ as List<MarianumMessage>,
/// @nodoc /// @nodoc
mixin _$MarianumMessage { 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 /// Create a copy of MarianumMessage
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -581,16 +581,16 @@ $MarianumMessageCopyWith<MarianumMessage> get copyWith => _$MarianumMessageCopyW
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,name,date,url); int get hashCode => Object.hash(runtimeType,id,name,date,description,url);
@override @override
String toString() { 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; factory $MarianumMessageCopyWith(MarianumMessage value, $Res Function(MarianumMessage) _then) = _$MarianumMessageCopyWithImpl;
@useResult @useResult
$Res call({ $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 /// Create a copy of MarianumMessage
/// with the given fields replaced by the non-null parameter values. /// 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( 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,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,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String, as String,
)); ));
@@ -708,10 +710,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String date, String url)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String date, String description, String url)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _MarianumMessage() when $default != null: 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(); return orElse();
} }
@@ -729,10 +731,10 @@ return $default(_that.name,_that.date,_that.url);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String date, String url) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String date, String description, String url) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _MarianumMessage(): 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'); throw StateError('Unexpected subclass');
} }
@@ -749,10 +751,10 @@ return $default(_that.name,_that.date,_that.url);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String date, String url)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String date, String description, String url)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _MarianumMessage() when $default != null: 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; return null;
} }
@@ -764,11 +766,13 @@ return $default(_that.name,_that.date,_that.url);case _:
@JsonSerializable() @JsonSerializable()
class _MarianumMessage implements MarianumMessage { 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<String, dynamic> json) => _$MarianumMessageFromJson(json); factory _MarianumMessage.fromJson(Map<String, dynamic> json) => _$MarianumMessageFromJson(json);
@override@JsonKey() final String id;
@override final String name; @override final String name;
@override final String date; @override final String date;
@override@JsonKey() final String description;
@override final String url; @override final String url;
/// Create a copy of MarianumMessage /// Create a copy of MarianumMessage
@@ -784,16 +788,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,name,date,url); int get hashCode => Object.hash(runtimeType,id,name,date,description,url);
@override @override
String toString() { 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; factory _$MarianumMessageCopyWith(_MarianumMessage value, $Res Function(_MarianumMessage) _then) = __$MarianumMessageCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 /// Create a copy of MarianumMessage
/// with the given fields replaced by the non-null parameter values. /// 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( 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,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,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String, as String,
)); ));
@@ -32,14 +32,18 @@ Map<String, dynamic> _$MarianumMessageListToJson(
_MarianumMessage _$MarianumMessageFromJson(Map<String, dynamic> json) => _MarianumMessage _$MarianumMessageFromJson(Map<String, dynamic> json) =>
_MarianumMessage( _MarianumMessage(
id: json['id'] as String? ?? '',
name: json['name'] as String, name: json['name'] as String,
date: json['date'] as String, date: json['date'] as String,
description: json['description'] as String? ?? '',
url: json['url'] as String, url: json['url'] as String,
); );
Map<String, dynamic> _$MarianumMessageToJson(_MarianumMessage instance) => Map<String, dynamic> _$MarianumMessageToJson(_MarianumMessage instance) =>
<String, dynamic>{ <String, dynamic>{
'id': instance.id,
'name': instance.name, 'name': instance.name,
'date': instance.date, 'date': instance.date,
'description': instance.description,
'url': instance.url, 'url': instance.url,
}; };
@@ -1,13 +1,53 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:intl/intl.dart';
import '../../../basis/dataloader/mhsl_data_loader.dart'; import '../../../../../api/marianumconnect/errors/marianumconnect_error.dart';
import '../../../infrastructure/data_loader/data_loader.dart'; import '../../../../../api/marianumconnect/marianumconnect_api.dart';
import '../../../../../api/marianumconnect/marianumconnect_endpoint.dart';
import '../bloc/marianum_message_state.dart'; import '../bloc/marianum_message_state.dart';
class MarianumMessageGetMessages extends MhslDataLoader<MarianumMessageList> { /// Loads the "Marianum Message" list from `GET /api/mobile/v1/newsletter`
@override /// (formerly the mhsl.eu `message/messages.json` endpoint). Bearer token is
Future<Response<String>> fetch() async => dio.get('/message/messages.json'); /// attached by the shared Marianum-Connect dio.
@override class MarianumMessageGetMessages {
MarianumMessageList assemble(DataLoaderResult data) => final Dio _dio;
MarianumMessageList.fromJson(data.asMap());
MarianumMessageGetMessages({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
Future<MarianumMessageList> run() async {
try {
final response = await _dio.get<List<dynamic>>(
MarianumConnectEndpoint.resolve('newsletter'),
);
final messages = (response.data ?? const [])
.cast<Map<String, dynamic>>()
.map(_mapItem)
.toList();
return MarianumMessageList(
base: MarianumConnectEndpoint.current(),
messages: messages,
);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
MarianumMessage _mapItem(Map<String, dynamic> 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);
}
} }
@@ -47,14 +47,21 @@ class MarianumMessageListView extends StatelessWidget {
children: [Icon(Icons.newspaper)], children: [Icon(Icons.newspaper)],
), ),
title: Text(message.name, overflow: TextOverflow.ellipsis), 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), trailing: const Icon(Icons.arrow_right),
onTap: () { onTap: () {
AppRoutes.openMarianumMessage( AppRoutes.openMarianumMessage(context, message);
context,
state.messageList.base,
message,
);
}, },
); );
}, },
@@ -1,47 +1,57 @@
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
import 'package:url_launcher/url_launcher.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/confirm_dialog.dart';
import '../../../widget/info_dialog.dart'; import '../../../widget/placeholder_view.dart';
class MessageView extends StatefulWidget { class MessageView extends StatefulWidget {
final String basePath; final String id;
final MarianumMessage message; final String title;
const MessageView({super.key, required this.basePath, required this.message}); const MessageView({super.key, required this.id, required this.title});
@override @override
State<MessageView> createState() => _MessageViewState(); State<MessageView> createState() => _MessageViewState();
} }
class _MessageViewState extends State<MessageView> { class _MessageViewState extends State<MessageView> {
late final Future<Uint8List> _pdf = GetNewsletterFile(widget.id).run();
@override @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text(widget.message.name)), appBar: AppBar(title: Text(widget.title)),
body: SfPdfViewer.network( body: FutureBuilder<Uint8List>(
widget.basePath + widget.message.url, future: _pdf,
enableHyperlinkNavigation: true, builder: (context, snapshot) {
onDocumentLoadFailed: (PdfDocumentLoadFailedDetails e) { if (snapshot.hasError) {
Navigator.of(context).pop(); return const PlaceholderView(
InfoDialog.show( icon: Icons.error_outline,
context, text: 'Das Dokument konnte nicht geladen werden.',
"Dokument '${widget.message.name}' konnte nicht geladen werden:\n${e.description}", );
title: 'Fehler beim öffnen', }
); if (!snapshot.hasData) {
}, return const Center(child: CircularProgressIndicator());
onHyperlinkClicked: (PdfHyperlinkClickedDetails e) { }
showDialog( return SfPdfViewer.memory(
context: context, snapshot.data!,
builder: (context) => ConfirmDialog( enableHyperlinkNavigation: true,
title: 'Link öffnen', onHyperlinkClicked: (PdfHyperlinkClickedDetails e) {
content: 'Möchtest du den folgenden Link öffnen?\n${e.uri}', showDialog(
confirmButton: 'Öffnen', context: context,
onConfirm: () => launchUrl( builder: (context) => ConfirmDialog(
Uri.parse(e.uri), title: 'Link öffnen',
mode: LaunchMode.externalApplication, content: 'Möchtest du den folgenden Link öffnen?\n${e.uri}',
), confirmButton: 'Öffnen',
), onConfirm: () => launchUrl(
Uri.parse(e.uri),
mode: LaunchMode.externalApplication,
),
),
);
},
); );
}, },
), ),
@@ -54,7 +54,7 @@ class SearchMarianumMessages extends SearchDelegate<MarianumMessage?> {
trailing: const Icon(Icons.arrow_right), trailing: const Icon(Icons.arrow_right),
onTap: () { onTap: () {
close(context, message); close(context, message);
AppRoutes.openMarianumMessage(context, base, message); AppRoutes.openMarianumMessage(context, message);
}, },
); );
}, },