build local notification with downloaded image; PDF via local file
This commit is contained in:
parent
d5a6007f7d
commit
339499501f
|
|
@ -4,6 +4,7 @@ import 'dart:io';
|
|||
import 'package:app_links/app_links.dart';
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_downloader/flutter_downloader.dart';
|
||||
|
|
@ -46,6 +47,30 @@ Future<void> _backgroundCallbackHomeWidget(Uri? uri) async {
|
|||
}
|
||||
}
|
||||
|
||||
/// Firebase background message handler.
|
||||
///
|
||||
/// Runs in a separate isolate when the app is killed/background and a
|
||||
/// data-only push arrives (which is how content-service broadcasts ship
|
||||
/// notifications now). Must be a top-level function annotated with
|
||||
/// `vm:entry-point` so Flutter's tree-shaker keeps it.
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
try {
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
} catch (e) {
|
||||
if (kDebugMode) print('bg handler: Firebase.initializeApp failed: $e');
|
||||
}
|
||||
try {
|
||||
await NotificationService.initializeNotification();
|
||||
await NotificationService.showFirebaseNotification(message);
|
||||
} catch (e) {
|
||||
if (kDebugMode) print('bg handler: showFirebaseNotification failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
runZonedGuarded(
|
||||
() async {
|
||||
|
|
@ -89,6 +114,15 @@ void main() async {
|
|||
},
|
||||
);
|
||||
|
||||
// Register the background handler before any other Firebase
|
||||
// messaging setup. Content-service ships data-only pushes so we
|
||||
// need to build the notification ourselves when the app is killed.
|
||||
if (!kIsWeb) {
|
||||
FirebaseMessaging.onBackgroundMessage(
|
||||
_firebaseMessagingBackgroundHandler,
|
||||
);
|
||||
}
|
||||
|
||||
if (!kIsWeb) {
|
||||
await FirebaseApi().initNotification().timeout(
|
||||
const Duration(seconds: 5),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ class NotificationMessage {
|
|||
String? type;
|
||||
String? link;
|
||||
String? image;
|
||||
// Remote cover image URL — set by content-service broadcasts. Used by the
|
||||
// local notification builder to render a BigPictureStyle preview.
|
||||
String? imageUrl;
|
||||
String? photo;
|
||||
String? groupKey;
|
||||
String? userId;
|
||||
|
|
@ -18,6 +21,7 @@ class NotificationMessage {
|
|||
this.type,
|
||||
this.link,
|
||||
this.image,
|
||||
this.imageUrl,
|
||||
this.photo,
|
||||
this.groupKey,
|
||||
this.userId,
|
||||
|
|
@ -31,6 +35,7 @@ class NotificationMessage {
|
|||
type = json['type'];
|
||||
link = json['link'];
|
||||
image = json['image'];
|
||||
imageUrl = json['imageUrl'];
|
||||
photo = json['photo'];
|
||||
groupKey = json['groupKey'];
|
||||
userId = json['userId'];
|
||||
|
|
@ -45,6 +50,7 @@ class NotificationMessage {
|
|||
data['type'] = type;
|
||||
data['link'] = link;
|
||||
data['image'] = image;
|
||||
data['imageUrl'] = imageUrl;
|
||||
data['photo'] = photo;
|
||||
data['groupKey'] = groupKey;
|
||||
data['userId'] = userId;
|
||||
|
|
@ -60,6 +66,7 @@ class NotificationMessage {
|
|||
data['type'] = type ?? '';
|
||||
data['link'] = link ?? '';
|
||||
data['image'] = image ?? '';
|
||||
data['imageUrl'] = imageUrl ?? '';
|
||||
data['photo'] = photo ?? '';
|
||||
data['userId'] = userId ?? '';
|
||||
return data;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:didvan/models/notification_message.dart';
|
||||
|
|
@ -8,6 +9,8 @@ import 'package:didvan/services/storage/storage.dart';
|
|||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class NotificationService {
|
||||
static final FlutterLocalNotificationsPlugin
|
||||
|
|
@ -89,7 +92,9 @@ class NotificationService {
|
|||
print("Notification Type: ${data.notificationType}");
|
||||
}
|
||||
|
||||
if (data.notificationType!.contains('3')) {
|
||||
// Widget-refresh pushes carry notificationType=3 and skip the UI.
|
||||
if (data.notificationType == '3' ||
|
||||
(data.notificationType?.contains('3') ?? false)) {
|
||||
if (kDebugMode) {
|
||||
print("Widget notification - calling fetchWidget()");
|
||||
}
|
||||
|
|
@ -105,8 +110,35 @@ class NotificationService {
|
|||
}
|
||||
}
|
||||
|
||||
/// Downloads [url] into a temp file and returns its absolute path. Used by
|
||||
/// `BigPictureStyleInformation` which needs a local file path. Returns
|
||||
/// null on any failure so we degrade to a no-image notification rather
|
||||
/// than crashing.
|
||||
static Future<String?> _downloadImageToTemp(String url) async {
|
||||
try {
|
||||
final res = await http.get(Uri.parse(url));
|
||||
if (res.statusCode != 200) {
|
||||
if (kDebugMode) {
|
||||
print('image download failed: HTTP ${res.statusCode}');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
final dir = await getTemporaryDirectory();
|
||||
final file = File(
|
||||
'${dir.path}/notif_${DateTime.now().millisecondsSinceEpoch}.img',
|
||||
);
|
||||
await file.writeAsBytes(res.bodyBytes, flush: true);
|
||||
return file.path;
|
||||
} catch (e) {
|
||||
if (kDebugMode) print('image download exception: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> showNotification(NotificationMessage data) async {
|
||||
if (data.notificationType!.contains('2')) {
|
||||
final notifType = data.notificationType ?? '';
|
||||
|
||||
if (notifType.contains('2')) {
|
||||
final String? storedValue = await StorageService.getValue(
|
||||
key: 'notification${AppInitializer.createNotificationId(data)}');
|
||||
if (storedValue != null) {
|
||||
|
|
@ -116,6 +148,26 @@ class NotificationService {
|
|||
}
|
||||
}
|
||||
|
||||
// Resolve the image we want on the notification.
|
||||
// - `data.imageUrl` is the new content-service broadcast field (remote
|
||||
// URL — must be downloaded into the cache before Android can render
|
||||
// it via BigPictureStyle).
|
||||
// - `data.image` is the legacy widget-update field (already a local
|
||||
// path).
|
||||
String? bigPicturePath;
|
||||
if (data.imageUrl != null && data.imageUrl!.isNotEmpty) {
|
||||
bigPicturePath = await _downloadImageToTemp(data.imageUrl!);
|
||||
} else if (data.image != null && data.image!.isNotEmpty) {
|
||||
bigPicturePath = data.image;
|
||||
}
|
||||
|
||||
// Pick the Android style:
|
||||
// - notificationType '1' (legacy "big news" push) → BigPictureStyle
|
||||
// - any push with a resolved image (content broadcasts) →
|
||||
// BigPictureStyle so the cover image shows up
|
||||
// - otherwise → MessagingStyle (compact)
|
||||
final useBigPicture = notifType.contains('1') || bigPicturePath != null;
|
||||
|
||||
final AndroidNotificationDetails androidPlatformChannelSpecifics =
|
||||
AndroidNotificationDetails(
|
||||
'content',
|
||||
|
|
@ -124,11 +176,11 @@ class NotificationService {
|
|||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
playSound: true,
|
||||
styleInformation: data.notificationType!.contains('1')
|
||||
styleInformation: useBigPicture && bigPicturePath != null
|
||||
? BigPictureStyleInformation(
|
||||
FilePathAndroidBitmap(data.image ?? ''),
|
||||
FilePathAndroidBitmap(bigPicturePath),
|
||||
contentTitle: data.title,
|
||||
summaryText: "خبر جدید",
|
||||
summaryText: data.body ?? '',
|
||||
)
|
||||
: MessagingStyleInformation(
|
||||
Person(name: data.title),
|
||||
|
|
@ -139,17 +191,21 @@ class NotificationService {
|
|||
final NotificationDetails platformChannelSpecifics =
|
||||
NotificationDetails(android: androidPlatformChannelSpecifics);
|
||||
|
||||
// Pass the full data map (incl. id/type/link/imageUrl) through the
|
||||
// payload so the onTap handler can route correctly.
|
||||
final payloadJson = jsonEncode(data.toJson());
|
||||
|
||||
await _flutterLocalNotificationsPlugin.show(
|
||||
data.notificationType!.contains('1')
|
||||
useBigPicture
|
||||
? DateTime.now().millisecondsSinceEpoch ~/ 1000
|
||||
: AppInitializer.createNotificationId(data),
|
||||
data.title,
|
||||
data.body,
|
||||
platformChannelSpecifics,
|
||||
payload: data.toPayload().toString(),
|
||||
payload: payloadJson,
|
||||
);
|
||||
|
||||
if (data.notificationType!.contains('2')) {
|
||||
if (notifType.contains('2')) {
|
||||
await StorageService.setValue(
|
||||
key: 'notification${AppInitializer.createNotificationId(data)}',
|
||||
value: data.body);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:didvan/views/widgets/didvan/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:didvan/services/network/request.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
class PdfViewerPage extends StatefulWidget {
|
||||
|
|
@ -24,6 +28,7 @@ class _PdfViewerPageState extends State<PdfViewerPage> {
|
|||
bool _isLoading = true;
|
||||
String? _errorMessage;
|
||||
late String _fullPdfUrl;
|
||||
File? _localPdfFile;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -45,6 +50,52 @@ class _PdfViewerPageState extends State<PdfViewerPage> {
|
|||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_openPdfInBrowser();
|
||||
});
|
||||
} else {
|
||||
// در موبایل: PDF را به temp دانلود میکنیم و سپس با SfPdfViewer.file
|
||||
// نمایش میدهیم. SfPdfViewer.network با presigned URLهای Arvan که
|
||||
// query params زیاد و امضای AWS دارند گاهی fail میشود؛ دانلود
|
||||
// محلی این مشکل را دور میزند.
|
||||
_downloadAndOpen();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _downloadAndOpen() async {
|
||||
try {
|
||||
final headers = (_fullPdfUrl.contains('didvan.com') ||
|
||||
_fullPdfUrl.contains('didvan.app'))
|
||||
? {'Authorization': 'Bearer ${RequestService.token}'}
|
||||
: const <String, String>{};
|
||||
|
||||
final res = await http.get(Uri.parse(_fullPdfUrl), headers: headers);
|
||||
if (res.statusCode != 200) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = 'HTTP ${res.statusCode}';
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final file = File(
|
||||
'${tempDir.path}/pdf_${DateTime.now().millisecondsSinceEpoch}.pdf',
|
||||
);
|
||||
await file.writeAsBytes(res.bodyBytes, flush: true);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_localPdfFile = file;
|
||||
// isLoading remains true until SfPdfViewer's onDocumentLoaded fires
|
||||
});
|
||||
} catch (e) {
|
||||
if (kDebugMode) print('PDF download failed: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = e.toString();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,20 +183,15 @@ class _PdfViewerPageState extends State<PdfViewerPage> {
|
|||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
SfPdfViewer.network(
|
||||
_fullPdfUrl,
|
||||
if (_localPdfFile != null)
|
||||
SfPdfViewer.file(
|
||||
_localPdfFile!,
|
||||
controller: _pdfViewerController,
|
||||
enableDoubleTapZooming: true,
|
||||
enableTextSelection: true,
|
||||
canShowScrollHead: true,
|
||||
canShowScrollStatus: true,
|
||||
pageLayoutMode: PdfPageLayoutMode.continuous,
|
||||
// فقط برای دامنهی خودی توکن میفرستیم؛ presigned S3 با هدر
|
||||
// Authorization پاسخ 403 میدهد.
|
||||
headers: _fullPdfUrl.contains('didvan.com') ||
|
||||
_fullPdfUrl.contains('didvan.app')
|
||||
? {'Authorization': 'Bearer ${RequestService.token}'}
|
||||
: const <String, String>{},
|
||||
onDocumentLoaded: (PdfDocumentLoadedDetails details) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
|
|
|
|||
Loading…
Reference in New Issue