本次提交包含多项改进: 1. 新增Android启动页资源与配色配置,完善多版本主题适配 2. 全量替换Platform.pathSeparator为硬编码斜杠,修复Web平台路径兼容问题 3. 为大量文件系统操作添加kIsWeb守卫,优化Web端表现 4. 替换硬编码平台判断为platform_utils封装,统一平台检测逻辑 5. 移除冗余代码与默认参数,优化小部件性能 6. 新增Web端适配逻辑,处理不支持的原生功能 7. 更新鸿蒙兼容性工具,完善平台识别与路径处理 8. 优化设备注册错误捕获,避免非致命崩溃 9. 添加启动页图标与背景配置,优化首屏体验
302 lines
9.4 KiB
Dart
302 lines
9.4 KiB
Dart
/// ============================================================
|
||
/// 闲言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]));
|
||
}
|
||
}
|