Files
xianyan/lib/core/services/notification/notification_service.dart
Developer a5d8483797 chore: 完成多平台兼容性优化与资源更新
本次提交包含多项改进:
1. 新增Android启动页资源与配色配置,完善多版本主题适配
2. 全量替换Platform.pathSeparator为硬编码斜杠,修复Web平台路径兼容问题
3. 为大量文件系统操作添加kIsWeb守卫,优化Web端表现
4. 替换硬编码平台判断为platform_utils封装,统一平台检测逻辑
5. 移除冗余代码与默认参数,优化小部件性能
6. 新增Web端适配逻辑,处理不支持的原生功能
7. 更新鸿蒙兼容性工具,完善平台识别与路径处理
8. 优化设备注册错误捕获,避免非致命崩溃
9. 添加启动页图标与背景配置,优化首屏体验
2026-06-05 02:31:34 +08:00

302 lines
9.4 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — 本地通知服务
/// 创建时间: 2026-05-10
/// 更新时间: 2026-06-05
/// 作用: 管理本地推送通知(每日推荐/签到提醒/即时通知)
/// 上次更新: Web平台兼容性修复-Platform.isIOS/isMacOS→pu.isIOS/pu.isMacOS
/// ============================================================
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import 'package:xianyan/core/utils/platform/platform_utils.dart' as pu;
import '../../utils/logger.dart';
import 'notification_init_stub.dart';
class NotificationService {
NotificationService._();
static final FlutterLocalNotificationsPlugin _plugin =
FlutterLocalNotificationsPlugin();
static bool _initialized = false;
static const _keyDailyRecommend = 'notif_daily_recommend';
static const _keyDailyRecommendTime = 'notif_daily_recommend_time';
static const _keySigninReminder = 'notif_signin_reminder';
static const _keySigninReminderTime = 'notif_signin_reminder_time';
static const _channelDailyId = 'xianyan_daily_recommend';
static const _channelDailyName = '每日推荐';
static const _channelSigninId = 'xianyan_signin_reminder';
static const _channelSigninName = '签到提醒';
static Future<bool> init() async {
if (_initialized) return true;
try {
tz.initializeTimeZones();
tz.setLocalLocation(tz.getLocation('Asia/Shanghai'));
const androidSettings = AndroidInitializationSettings(
'@mipmap/ic_launcher',
);
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
);
const macOsSettings = DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
);
// 通过桥接方法构建 InitializationSettings
// 官方SDK不含ohos参数鸿蒙端动态注入ohos参数
final settings = buildNotificationInitSettings(
androidSettings: androidSettings,
iosSettings: iosSettings,
macOsSettings: macOsSettings,
);
final result = await _plugin.initialize(
settings: settings,
onDidReceiveNotificationResponse: _onNotificationTapped,
);
if (result ?? false) {
Log.i('通知插件初始化成功');
} else {
Log.w('通知插件初始化返回 false');
}
await _requestPermissions();
_initialized = true;
return true;
} catch (e) {
Log.e('通知插件初始化失败: $e');
return false;
}
}
static Future<void> _requestPermissions() async {
try {
if (pu.isOhos) {
// 鸿蒙端:通过桥接方法动态请求权限
await requestOhosNotificationPermission(_plugin);
}
if (pu.isIOS) {
await _plugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin
>()
?.requestPermissions(alert: true, badge: true, sound: true);
}
if (pu.isMacOS) {
await _plugin
.resolvePlatformSpecificImplementation<
MacOSFlutterLocalNotificationsPlugin
>()
?.requestPermissions(alert: true, badge: true, sound: true);
}
} catch (e) {
Log.w('请求通知权限异常: $e');
}
}
static void _onNotificationTapped(NotificationResponse response) {
Log.i('通知被点击: id=${response.id}, payload=${response.payload}');
}
static Future<bool> scheduleDailyRecommend(TimeOfDay time) async {
try {
if (!_initialized) await init();
const androidDetails = AndroidNotificationDetails(
_channelDailyId,
_channelDailyName,
channelDescription: '每日诗词、成语、名言推荐',
importance: Importance.high,
priority: Priority.high,
);
const iosDetails = DarwinNotificationDetails();
const details = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _plugin.zonedSchedule(
id: 1001,
title: '今日推荐 ✨',
body: '新的诗词、成语、名言等你来看',
scheduledDate: _nextInstanceOfTime(time.hour, time.minute),
notificationDetails: details,
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
matchDateTimeComponents: DateTimeComponents.time,
);
await _saveDailyRecommendPrefs(true, time);
Log.i('每日推荐通知已调度: ${time.hour}:${time.minute}');
return true;
} catch (e) {
Log.e('调度每日推荐通知失败: $e');
return false;
}
}
static Future<bool> scheduleSigninReminder(TimeOfDay time) async {
try {
if (!_initialized) await init();
const androidDetails = AndroidNotificationDetails(
_channelSigninId,
_channelSigninName,
channelDescription: '每日签到提醒',
importance: Importance.high,
priority: Priority.high,
);
const iosDetails = DarwinNotificationDetails();
const details = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _plugin.zonedSchedule(
id: 1002,
title: '签到提醒 📝',
body: '别忘了今日签到,连续签到有惊喜哦',
scheduledDate: _nextInstanceOfTime(time.hour, time.minute),
notificationDetails: details,
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
matchDateTimeComponents: DateTimeComponents.time,
);
await _saveSigninReminderPrefs(true, time);
Log.i('签到提醒通知已调度: ${time.hour}:${time.minute}');
return true;
} catch (e) {
Log.e('调度签到提醒通知失败: $e');
return false;
}
}
static Future<void> cancelAll() async {
try {
await _plugin.cancelAll();
await _saveDailyRecommendPrefs(false, null);
await _saveSigninReminderPrefs(false, null);
Log.i('已取消所有通知');
} catch (e) {
Log.e('取消通知失败: $e');
}
}
static Future<bool> showImmediate(String title, String body) async {
try {
if (!_initialized) await init();
const androidDetails = AndroidNotificationDetails(
'xianyan_immediate',
'即时通知',
channelDescription: '应用内即时消息',
);
const iosDetails = DarwinNotificationDetails();
const details = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
await _plugin.show(
id: now,
title: title,
body: body,
notificationDetails: details,
);
Log.i('即时通知已发送: $title');
return true;
} catch (e) {
Log.e('发送即时通知失败: $e');
return false;
}
}
static Future<bool> isDailyRecommendEnabled() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_keyDailyRecommend) ?? false;
}
static Future<TimeOfDay?> getDailyRecommendTime() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString(_keyDailyRecommendTime);
if (saved == null) return null;
return _parseTimeOfDay(saved);
}
static Future<bool> isSigninReminderEnabled() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_keySigninReminder) ?? false;
}
static Future<TimeOfDay?> getSigninReminderTime() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString(_keySigninReminderTime);
if (saved == null) return null;
return _parseTimeOfDay(saved);
}
static Future<void> _saveDailyRecommendPrefs(
bool enabled,
TimeOfDay? time,
) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_keyDailyRecommend, enabled);
if (time != null) {
await prefs.setString(_keyDailyRecommendTime, _formatTimeOfDay(time));
} else {
await prefs.remove(_keyDailyRecommendTime);
}
}
static Future<void> _saveSigninReminderPrefs(
bool enabled,
TimeOfDay? time,
) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_keySigninReminder, enabled);
if (time != null) {
await prefs.setString(_keySigninReminderTime, _formatTimeOfDay(time));
} else {
await prefs.remove(_keySigninReminderTime);
}
}
static tz.TZDateTime _nextInstanceOfTime(int hour, int minute) {
final now = tz.TZDateTime.now(tz.local);
var scheduled = tz.TZDateTime(
tz.local,
now.year,
now.month,
now.day,
hour,
minute,
);
if (scheduled.isBefore(now)) {
scheduled = scheduled.add(const Duration(days: 1));
}
return scheduled;
}
static String _formatTimeOfDay(TimeOfDay t) =>
'${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
static TimeOfDay _parseTimeOfDay(String s) {
final parts = s.split(':');
return TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1]));
}
}