此提交包含多项变更: 1. 新增鸿蒙平台支持,完善设备检测与数据库适配 2. 替换旧版分享插件API为SharePlus 3. 批量迁移StateNotifier到Notifier以适配新版Riverpod 4. 修复zip编码判断、图表API参数等bug 5. 更新应用图标、启动页资源与多尺寸适配图标 6. 调整Android最小SDK版本与应用名称 7. 优化日志打印与正则表达式使用 8. 修正编辑器画布样式初始化与配置逻辑 9. 更新依赖与CI插件配置
306 lines
9.3 KiB
Dart
306 lines
9.3 KiB
Dart
/// ============================================================
|
|
/// 闲言APP — 本地通知服务
|
|
/// 创建时间: 2026-05-10
|
|
/// 更新时间: 2026-05-17
|
|
/// 作用: 管理本地推送通知(每日推荐/签到提醒/即时通知)
|
|
/// 上次更新: 鸿蒙适配-增加OhosInitializationSettings
|
|
/// ============================================================
|
|
|
|
import 'dart:io';
|
|
|
|
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_utils.dart' as pu;
|
|
|
|
import '../../utils/logger.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,
|
|
);
|
|
const ohosSettings = OhosInitializationSettings('@mipmap/ic_launcher');
|
|
|
|
const settings = InitializationSettings(
|
|
android: androidSettings,
|
|
iOS: iosSettings,
|
|
macOS: macOsSettings,
|
|
ohos: ohosSettings,
|
|
);
|
|
|
|
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 _plugin
|
|
.resolvePlatformSpecificImplementation<
|
|
OhosFlutterLocalNotificationsPlugin
|
|
>()
|
|
?.requestNotificationsPermission();
|
|
}
|
|
if (Platform.isIOS) {
|
|
await _plugin
|
|
.resolvePlatformSpecificImplementation<
|
|
IOSFlutterLocalNotificationsPlugin
|
|
>()
|
|
?.requestPermissions(alert: true, badge: true, sound: true);
|
|
}
|
|
if (Platform.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]));
|
|
}
|
|
}
|