migrated Marianum Message module to the Marianum-Connect API and implemented push notification deep linking
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
+48
-8
@@ -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);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user