Files
xianyan/lib/core/services/notification/notification_center.dart
Developer 83720002e6 feat: 新增工作台模式、系统托盘,修复多平台兼容性问题
1. 新增工作台三栏布局模式,适配宽屏设备
2. 添加跨平台系统托盘支持,新增托盘图标资源
3. 修复工作台模式下导航返回异常问题
4. 统一JSON类型安全解析,替换硬类型转换
5. 增加macOS深度链接支持,统一渠道分发信息
6. 优化部分页面生命周期和状态加载逻辑
7. 移除废弃的nearby_connections依赖
2026-06-19 06:43:55 +08:00

568 lines
16 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-22
/// 更新时间: 2026-06-19
/// 作用: 合并 NotificationScheduler + DailyNotifyService统一管理所有本地通知调度
/// 上次更新: 类型安全修复(int vs num): 节气日期 year/month/day 使用 SafeJson.parseInt
/// ============================================================
import 'package:xianyan/core/utils/safe_json.dart';
import 'local_notification_service.dart';
import '../../storage/kv_storage.dart';
import '../../utils/logger.dart';
class NotificationCenter {
NotificationCenter._();
// ── 通知ID命名空间 ──
static const int _idDailyRecommend = 1001;
static const int _idSigninReminder = 1002;
static const int _idFortune = 1003;
static const int _idStudyProgress = 1004;
static const int _idSolarTerm = 2001;
// ── 存储键 ──
static const _keyNotificationsEnabled = 'notifications_enabled';
static const _keyDailyRecommendEnabled = 'daily_sentence_enabled';
static const _keyDailyRecommendHour = 'daily_sentence_hour';
static const _keyDailyRecommendMinute = 'daily_sentence_minute';
static const _keySigninReminderEnabled = 'signin_reminder_enabled';
static const _keySigninReminderHour = 'signin_reminder_hour';
static const _keySigninReminderMinute = 'signin_reminder_minute';
static const _keySolarTermEnabled = 'solar_term_enabled';
static const _keyFortuneEnabled = 'fortune_enabled';
static const _keyFortuneHour = 'fortune_hour';
static const _keyFortuneMinute = 'fortune_minute';
static const _keyStudyProgressEnabled = 'study_progress_enabled';
static const _keyReadlaterEnabled = 'notif_charging_readlater';
static const _keyMarketingPushEnabled = 'marketing_push_enabled';
// ── 推送计数器存储键 ──
static const _keyPushCountDailyRecommend = 'push_count_daily_recommend';
static const _keyPushCountSignin = 'push_count_signin';
static const _keyPushCountStudyProgress = 'push_count_study_progress';
static const _keyPushCountReadlater = 'push_count_readlater';
static const _keyPushCountFortune = 'push_count_fortune';
static const _keyPushCountMarketing = 'push_count_marketing';
static const _keyClickCountDailyRecommend = 'click_count_daily_recommend';
static const _keyClickCountSignin = 'click_count_signin';
static const _keyClickCountStudyProgress = 'click_count_study_progress';
static const _keyClickCountReadlater = 'click_count_readlater';
static const _keyClickCountFortune = 'click_count_fortune';
static const _keyClickCountMarketing = 'click_count_marketing';
// ── 全局通知开关 ──
static bool get isNotificationsEnabled =>
KvStorage.getBool(_keyNotificationsEnabled) ?? false;
static Future<void> setNotificationsEnabled(bool v) async {
await KvStorage.setBool(_keyNotificationsEnabled, v);
await configureAll();
}
// ── 每日推荐 ──
static bool get isDailyRecommendEnabled =>
KvStorage.getBool(_keyDailyRecommendEnabled) ?? true;
static int get dailyRecommendHour =>
KvStorage.getInt(_keyDailyRecommendHour) ?? 8;
static int get dailyRecommendMinute =>
KvStorage.getInt(_keyDailyRecommendMinute) ?? 0;
static Future<void> setDailyRecommendEnabled(bool v) async {
await KvStorage.setBool(_keyDailyRecommendEnabled, v);
await configureAll();
}
static Future<void> setDailyRecommendTime(int hour, int minute) async {
await KvStorage.setInt(_keyDailyRecommendHour, hour);
await KvStorage.setInt(_keyDailyRecommendMinute, minute);
await configureAll();
}
// ── 签到提醒 ──
static bool get isSigninReminderEnabled =>
KvStorage.getBool(_keySigninReminderEnabled) ?? true;
static int get signinReminderHour =>
KvStorage.getInt(_keySigninReminderHour) ?? 20;
static int get signinReminderMinute =>
KvStorage.getInt(_keySigninReminderMinute) ?? 0;
static Future<void> setSigninReminderEnabled(bool v) async {
await KvStorage.setBool(_keySigninReminderEnabled, v);
await configureAll();
}
static Future<void> setSigninReminderTime(int hour, int minute) async {
await KvStorage.setInt(_keySigninReminderHour, hour);
await KvStorage.setInt(_keySigninReminderMinute, minute);
await configureAll();
}
// ── 节气通知 ──
static bool get isSolarTermEnabled =>
KvStorage.getBool(_keySolarTermEnabled) ?? true;
static Future<void> setSolarTermEnabled(bool v) async {
await KvStorage.setBool(_keySolarTermEnabled, v);
await configureAll();
}
// ── 每日运势 ──
static bool get isFortuneEnabled =>
KvStorage.getBool(_keyFortuneEnabled) ?? false;
static int get fortuneHour => KvStorage.getInt(_keyFortuneHour) ?? 8;
static int get fortuneMinute => KvStorage.getInt(_keyFortuneMinute) ?? 0;
static Future<void> setFortuneEnabled(bool v) async {
await KvStorage.setBool(_keyFortuneEnabled, v);
await configureAll();
}
static Future<void> setFortuneTime(int hour, int minute) async {
await KvStorage.setInt(_keyFortuneHour, hour);
await KvStorage.setInt(_keyFortuneMinute, minute);
await configureAll();
}
// ── 学习进度 ──
static bool get isStudyProgressEnabled =>
KvStorage.getBool(_keyStudyProgressEnabled) ?? false;
static Future<void> setStudyProgressEnabled(bool v) async {
await KvStorage.setBool(_keyStudyProgressEnabled, v);
await configureAll();
}
// ── 稍后读提醒 ──
static bool get isReadlaterEnabled =>
KvStorage.getBool(_keyReadlaterEnabled) ?? false;
static Future<void> setReadlaterEnabled(bool v) async {
await KvStorage.setBool(_keyReadlaterEnabled, v);
await configureAll();
}
// ── 营销推送 ──
static bool get isMarketingPushEnabled =>
KvStorage.getBool(_keyMarketingPushEnabled) ?? false;
static Future<void> setMarketingPushEnabled(bool v) async {
await KvStorage.setBool(_keyMarketingPushEnabled, v);
}
// ── 推送计数器 ──
/// 各类型推送次数
static int get pushCountDailyRecommend =>
KvStorage.getInt(_keyPushCountDailyRecommend) ?? 0;
static int get pushCountSignin =>
KvStorage.getInt(_keyPushCountSignin) ?? 0;
static int get pushCountStudyProgress =>
KvStorage.getInt(_keyPushCountStudyProgress) ?? 0;
static int get pushCountReadlater =>
KvStorage.getInt(_keyPushCountReadlater) ?? 0;
static int get pushCountFortune =>
KvStorage.getInt(_keyPushCountFortune) ?? 0;
static int get pushCountMarketing =>
KvStorage.getInt(_keyPushCountMarketing) ?? 0;
/// 各类型点击次数
static int get clickCountDailyRecommend =>
KvStorage.getInt(_keyClickCountDailyRecommend) ?? 0;
static int get clickCountSignin =>
KvStorage.getInt(_keyClickCountSignin) ?? 0;
static int get clickCountStudyProgress =>
KvStorage.getInt(_keyClickCountStudyProgress) ?? 0;
static int get clickCountReadlater =>
KvStorage.getInt(_keyClickCountReadlater) ?? 0;
static int get clickCountFortune =>
KvStorage.getInt(_keyClickCountFortune) ?? 0;
static int get clickCountMarketing =>
KvStorage.getInt(_keyClickCountMarketing) ?? 0;
/// 递增推送计数
static Future<void> incrementPushCount(String type) async {
final key = 'push_count_$type';
final current = KvStorage.getInt(key) ?? 0;
await KvStorage.setInt(key, current + 1);
}
/// 递增点击计数
static Future<void> incrementClickCount(String type) async {
final key = 'click_count_$type';
final current = KvStorage.getInt(key) ?? 0;
await KvStorage.setInt(key, current + 1);
}
/// 重置所有计数器
static Future<void> resetAllCounts() async {
final keys = [
_keyPushCountDailyRecommend,
_keyPushCountSignin,
_keyPushCountStudyProgress,
_keyPushCountReadlater,
_keyPushCountFortune,
_keyPushCountMarketing,
_keyClickCountDailyRecommend,
_keyClickCountSignin,
_keyClickCountStudyProgress,
_keyClickCountReadlater,
_keyClickCountFortune,
_keyClickCountMarketing,
];
for (final key in keys) {
await KvStorage.setInt(key, 0);
}
}
// ── 核心调度 ──
static Future<void> configureAll() async {
final enabled = KvStorage.getBool(_keyNotificationsEnabled) ?? false;
if (!enabled) {
await cancelAllManaged();
Log.i('NotificationCenter: 通知已关闭,取消所有调度');
return;
}
await cancelAllManaged();
await _configureDailyRecommend();
await _configureSigninReminder();
await _configureSolarTerm();
await _configureFortune();
await _configureStudyProgress();
Log.i('NotificationCenter: 所有通知已配置');
}
static Future<void> cancelAllManaged() async {
await LocalNotificationService.cancel(_idDailyRecommend);
await LocalNotificationService.cancel(_idSigninReminder);
await LocalNotificationService.cancel(_idFortune);
await LocalNotificationService.cancel(_idStudyProgress);
await LocalNotificationService.cancel(_idSolarTerm);
}
// ── 各通知调度 ──
static Future<void> _configureDailyRecommend() async {
final enabled = KvStorage.getBool(_keyDailyRecommendEnabled) ?? true;
if (!enabled) return;
final hour = KvStorage.getInt(_keyDailyRecommendHour) ?? 8;
final minute = KvStorage.getInt(_keyDailyRecommendMinute) ?? 0;
await LocalNotificationService.scheduleDaily(
id: _idDailyRecommend,
title: '闲言每日一句',
body: '今天的句子已准备好,来看看吧 ✨',
hour: hour,
minute: minute,
payload: 'daily_sentence',
);
await incrementPushCount('daily_recommend');
}
static Future<void> _configureSigninReminder() async {
final enabled = KvStorage.getBool(_keySigninReminderEnabled) ?? true;
if (!enabled) return;
final hour = KvStorage.getInt(_keySigninReminderHour) ?? 20;
final minute = KvStorage.getInt(_keySigninReminderMinute) ?? 0;
await LocalNotificationService.scheduleDaily(
id: _idSigninReminder,
title: '闲言 · 签到提醒',
body: '别忘了今日签到哦 📝',
hour: hour,
minute: minute,
payload: 'signin_reminder',
);
await incrementPushCount('signin');
}
static Future<void> _configureSolarTerm() async {
final enabled = KvStorage.getBool(_keySolarTermEnabled) ?? true;
if (!enabled) return;
final nextTerm = _getNextSolarTerm();
if (nextTerm == null) return;
final scheduledTime = DateTime(
SafeJson.parseInt(nextTerm['year']),
SafeJson.parseInt(nextTerm['month']),
SafeJson.parseInt(nextTerm['day']),
8,
);
await LocalNotificationService.scheduleOnce(
id: _idSolarTerm,
title: '闲言 · ${nextTerm['emoji']} ${nextTerm['name']}',
body: '今日${nextTerm['name']}${nextTerm['poem']}',
scheduledTime: scheduledTime,
payload: 'solar_term',
);
}
static Future<void> _configureFortune() async {
final enabled = KvStorage.getBool(_keyFortuneEnabled) ?? false;
if (!enabled) return;
final hour = KvStorage.getInt(_keyFortuneHour) ?? 8;
final minute = KvStorage.getInt(_keyFortuneMinute) ?? 0;
await LocalNotificationService.scheduleDaily(
id: _idFortune,
title: '闲言 · 🔮 每日运势',
body: '今日运势已生成,快来看看你的运势吧 ✨',
hour: hour,
minute: minute,
payload: 'daily_fortune',
);
await incrementPushCount('fortune');
}
static Future<void> _configureStudyProgress() async {
final enabled = KvStorage.getBool(_keyStudyProgressEnabled) ?? false;
if (!enabled) return;
await LocalNotificationService.scheduleDaily(
id: _idStudyProgress,
title: '闲言 · 学习进度',
body: '该复习今天的学习内容了 📊',
hour: 20,
payload: 'study_progress',
);
await incrementPushCount('study_progress');
}
// ── 节气数据 ──
static Map<String, dynamic>? _getNextSolarTerm() {
final now = DateTime.now();
final terms = _solarTerms2026;
for (final term in terms) {
final date = DateTime(
SafeJson.parseInt(term['year']),
SafeJson.parseInt(term['month']),
SafeJson.parseInt(term['day']),
);
if (date.isAfter(now)) return term;
}
return terms.isNotEmpty ? terms.first : null;
}
static final List<Map<String, dynamic>> _solarTerms2026 = [
{
'year': 2026,
'month': 1,
'day': 5,
'name': '小寒',
'emoji': '❄️',
'poem': '小寒连大吕,欢鹊垒新巢',
},
{
'year': 2026,
'month': 1,
'day': 20,
'name': '大寒',
'emoji': '🧊',
'poem': '大寒须守火,无事不出门',
},
{
'year': 2026,
'month': 2,
'day': 4,
'name': '立春',
'emoji': '🌱',
'poem': '春风如贵客,一到便繁华',
},
{
'year': 2026,
'month': 2,
'day': 18,
'name': '雨水',
'emoji': '🌧️',
'poem': '好雨知时节,当春乃发生',
},
{
'year': 2026,
'month': 3,
'day': 5,
'name': '惊蛰',
'emoji': '',
'poem': '微雨众卉新,一雷惊蛰始',
},
{
'year': 2026,
'month': 3,
'day': 20,
'name': '春分',
'emoji': '🌸',
'poem': '雪入春分省见稀,半开桃李不胜威',
},
{
'year': 2026,
'month': 4,
'day': 5,
'name': '清明',
'emoji': '🍃',
'poem': '清明时节雨纷纷,路上行人欲断魂',
},
{
'year': 2026,
'month': 4,
'day': 20,
'name': '谷雨',
'emoji': '🌾',
'poem': '谷雨如丝复似尘,煮瓶浮蜡正尝新',
},
{
'year': 2026,
'month': 5,
'day': 5,
'name': '立夏',
'emoji': '☀️',
'poem': '绿树阴浓夏日长,楼台倒影入池塘',
},
{
'year': 2026,
'month': 5,
'day': 21,
'name': '小满',
'emoji': '🌿',
'poem': '夜莺啼绿柳,皓月醒长空',
},
{
'year': 2026,
'month': 6,
'day': 5,
'name': '芒种',
'emoji': '🌻',
'poem': '时雨及芒种,四野皆插秧',
},
{
'year': 2026,
'month': 6,
'day': 21,
'name': '夏至',
'emoji': '🌞',
'poem': '昼晷已云极,宵漏自此长',
},
{
'year': 2026,
'month': 7,
'day': 7,
'name': '小暑',
'emoji': '🌡️',
'poem': '倏忽温风至,因循小暑来',
},
{
'year': 2026,
'month': 7,
'day': 22,
'name': '大暑',
'emoji': '🔥',
'poem': '大暑三秋近,林钟九夏移',
},
{
'year': 2026,
'month': 8,
'day': 7,
'name': '立秋',
'emoji': '🍂',
'poem': '乳鸦啼散玉屏空,一枕新凉一扇风',
},
{
'year': 2026,
'month': 8,
'day': 23,
'name': '处暑',
'emoji': '🎐',
'poem': '处暑无三日,新凉直万金',
},
{
'year': 2026,
'month': 9,
'day': 7,
'name': '白露',
'emoji': '💎',
'poem': '露从今夜白,月是故乡明',
},
{
'year': 2026,
'month': 9,
'day': 23,
'name': '秋分',
'emoji': '🍁',
'poem': '金气秋分,风清露冷秋期半',
},
{
'year': 2026,
'month': 10,
'day': 8,
'name': '寒露',
'emoji': '💧',
'poem': '袅袅凉风动,凄凄寒露零',
},
{
'year': 2026,
'month': 10,
'day': 23,
'name': '霜降',
'emoji': '🧊',
'poem': '霜降碧天静,秋事促西风',
},
{
'year': 2026,
'month': 11,
'day': 7,
'name': '立冬',
'emoji': '🧣',
'poem': '冻笔新诗懒写,寒炉美酒时温',
},
{
'year': 2026,
'month': 11,
'day': 22,
'name': '小雪',
'emoji': '🌨️',
'poem': '片片互玲珑,飞扬玉漏终',
},
{
'year': 2026,
'month': 12,
'day': 7,
'name': '大雪',
'emoji': '❄️',
'poem': '大雪江南见未曾,今年方始是严凝',
},
{
'year': 2026,
'month': 12,
'day': 21,
'name': '冬至',
'emoji': '🥟',
'poem': '天时人事日相催,冬至阳生春又来',
},
];
}