1. 新增工作台三栏布局模式,适配宽屏设备 2. 添加跨平台系统托盘支持,新增托盘图标资源 3. 修复工作台模式下导航返回异常问题 4. 统一JSON类型安全解析,替换硬类型转换 5. 增加macOS深度链接支持,统一渠道分发信息 6. 优化部分页面生命周期和状态加载逻辑 7. 移除废弃的nearby_connections依赖
568 lines
16 KiB
Dart
568 lines
16 KiB
Dart
/// ============================================================
|
||
/// 闲言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': '天时人事日相催,冬至阳生春又来',
|
||
},
|
||
];
|
||
}
|