chore: 完成多模块迭代优化与依赖更新
本次提交包含多项更新: 1. 更新file_picker依赖到11.0.0-ohos.1版本 2. 清理SecureStorage、Catcher2配置冗余代码 3. 优化鸿蒙系统下HomeWidget调用方式 4. 重构编辑器导航栏图标与页面路由引用 5. 修复边框样式、简化空值判断逻辑 6. 移除冗余系统UI样式配置 7. 新增共享组件导出与自适应返回按钮 8. 批量替换路由引用为app_routes 9. 标记过时通知服务并补充注释 10. 新增引导页扫一扫功能卡片 11. 完善沉浸式状态栏配置逻辑 12. 为大量页面添加统一自适应返回按钮
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 权限管理服务
|
||||
/// 创建时间: 2026-04-23
|
||||
/// 更新时间: 2026-05-21
|
||||
/// 作用: 统一管理应用权限请求,支持相机/相册/通知/位置/蓝牙/附近设备权限
|
||||
/// 上次更新: 重写AppPermission枚举,移除废弃storage权限,新增isRequired和usageScenes字段
|
||||
/// 更新时间: 2026-05-22
|
||||
/// 作用: 统一管理应用权限请求,支持相机/相册/通知/位置/蓝牙/附近设备/麦克风/存储/网络/剪贴板权限
|
||||
/// 上次更新: 移除3个高敏感权限(悬浮窗/通讯录/忽略电池优化),降低应用商店审核风险
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
@@ -87,7 +89,41 @@ enum AppPermission {
|
||||
Color(0xFF64D2FF),
|
||||
usageScenes: ['文件传输 — 局域网发现', '设备连接 — WiFi直连'],
|
||||
),
|
||||
;
|
||||
microphone(
|
||||
'麦克风',
|
||||
Permission.microphone,
|
||||
CupertinoIcons.mic_fill,
|
||||
'用于语音朗读句子、语音搜索、AI对话语音输入。仅在您主动使用语音功能时请求,不会后台录音。',
|
||||
Color(0xFFFF3B30),
|
||||
usageScenes: ['语音朗读 — 朗读句子', '语音搜索 — 语音输入关键词', 'AI对话 — 语音输入消息'],
|
||||
),
|
||||
storage(
|
||||
'存储空间',
|
||||
Permission.storage,
|
||||
CupertinoIcons.folder_fill,
|
||||
'用于保存编辑的卡片、壁纸到本地,导出字体文件和数据。Android 12及以下版本需要此权限。',
|
||||
Color(0xFFFF9500),
|
||||
usageScenes: ['保存卡片 — 导出到本地', '字体管理 — 下载字体文件', '数据导出 — 导出用户数据'],
|
||||
),
|
||||
network(
|
||||
'网络连接',
|
||||
Permission.notification,
|
||||
CupertinoIcons.wifi,
|
||||
'闲言需要网络连接来获取句子、同步数据和推送通知。请在系统设置中确保网络权限已开启。',
|
||||
Color(0xFF007AFF),
|
||||
isRequired: true,
|
||||
isVirtual: true,
|
||||
usageScenes: ['句子获取 — 加载每日推荐', '数据同步 — 云端同步', '推送通知 — 接收消息'],
|
||||
),
|
||||
clipboard(
|
||||
'剪贴板',
|
||||
Permission.notification,
|
||||
CupertinoIcons.doc_on_clipboard_fill,
|
||||
'用于复制句子到剪贴板、粘贴文本到编辑器。应用仅在您主动操作时访问剪贴板,不会自动读取。',
|
||||
Color(0xFFAF52DE),
|
||||
isVirtual: true,
|
||||
usageScenes: ['复制句子 — 一键复制', '编辑器 — 粘贴文本', '搜索 — 粘贴关键词'],
|
||||
);
|
||||
|
||||
const AppPermission(
|
||||
this.label,
|
||||
@@ -96,6 +132,7 @@ enum AppPermission {
|
||||
this.description,
|
||||
this.color, {
|
||||
this.isRequired = false,
|
||||
this.isVirtual = false,
|
||||
this.usageScenes = const [],
|
||||
});
|
||||
|
||||
@@ -105,7 +142,26 @@ enum AppPermission {
|
||||
final String description;
|
||||
final Color color;
|
||||
final bool isRequired;
|
||||
final bool isVirtual;
|
||||
final List<String> usageScenes;
|
||||
|
||||
/// Android 13+ 不需要 storage 权限(由 photos 替代)
|
||||
bool get isPlatformRelevant {
|
||||
if (this == AppPermission.storage) {
|
||||
if (!Platform.isAndroid) return false;
|
||||
final sdkInt = _androidSdkInt;
|
||||
return sdkInt != null && sdkInt <= 32;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int? get _androidSdkInt {
|
||||
try {
|
||||
return int.tryParse(Platform.version.split('.').first);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 权限管理服务 — iOS 风格权限请求
|
||||
@@ -114,6 +170,9 @@ class PermissionService {
|
||||
|
||||
/// 检查单个权限状态
|
||||
static Future<AppPermissionStatus> checkStatus(AppPermission perm) async {
|
||||
if (perm.isVirtual) {
|
||||
return _checkVirtualStatus(perm);
|
||||
}
|
||||
try {
|
||||
final status = await perm.permission.status;
|
||||
return AppPermissionStatus.fromPermissionStatus(status);
|
||||
@@ -123,11 +182,26 @@ class PermissionService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 批量查询所有权限状态
|
||||
/// 虚拟权限状态检查
|
||||
static Future<AppPermissionStatus> _checkVirtualStatus(
|
||||
AppPermission perm,
|
||||
) async {
|
||||
switch (perm) {
|
||||
case AppPermission.network:
|
||||
return AppPermissionStatus.granted;
|
||||
case AppPermission.clipboard:
|
||||
return AppPermissionStatus.granted;
|
||||
default:
|
||||
return AppPermissionStatus.granted;
|
||||
}
|
||||
}
|
||||
|
||||
/// 批量查询所有权限状态(过滤平台不相关权限)
|
||||
static Future<Map<AppPermission, AppPermissionStatus>>
|
||||
checkAllStatus() async {
|
||||
final results = <AppPermission, AppPermissionStatus>{};
|
||||
for (final perm in AppPermission.values) {
|
||||
if (!perm.isPlatformRelevant) continue;
|
||||
results[perm] = await checkStatus(perm);
|
||||
}
|
||||
return results;
|
||||
@@ -139,6 +213,7 @@ class PermissionService {
|
||||
AppPermission perm, {
|
||||
String? rationale,
|
||||
}) async {
|
||||
if (perm.isVirtual) return true;
|
||||
try {
|
||||
final status = await perm.permission.status;
|
||||
|
||||
@@ -177,6 +252,10 @@ class PermissionService {
|
||||
}) async {
|
||||
final results = <AppPermission, bool>{};
|
||||
for (final perm in permissions) {
|
||||
if (perm.isVirtual) {
|
||||
results[perm] = true;
|
||||
continue;
|
||||
}
|
||||
results[perm] = await requestPermission(
|
||||
context,
|
||||
perm,
|
||||
@@ -216,6 +295,14 @@ class PermissionService {
|
||||
rationale: '需要位置权限才能提供天气和节气信息',
|
||||
);
|
||||
|
||||
/// 快捷方法: 请求麦克风权限
|
||||
static Future<bool> requestMicrophone(BuildContext context) =>
|
||||
requestPermission(
|
||||
context,
|
||||
AppPermission.microphone,
|
||||
rationale: '需要麦克风权限才能使用语音功能',
|
||||
);
|
||||
|
||||
/// 打开系统设置
|
||||
static Future<bool> openSettings() => openAppSettings();
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:catcher_2/model/platform_type.dart';
|
||||
import 'package:catcher_2/model/report.dart';
|
||||
import 'package:catcher_2/model/report_mode.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' show SelectableText;
|
||||
|
||||
@@ -303,7 +303,7 @@ class HomeWidgetService {
|
||||
// 官方SDK下不支持,使用 dynamic 调用绕过编译检查
|
||||
if (pu.isOhos) {
|
||||
try {
|
||||
final dynamic updateWidget = HomeWidget.updateWidget;
|
||||
const dynamic updateWidget = HomeWidget.updateWidget;
|
||||
await updateWidget(
|
||||
qualifiedAndroidName: type.qualifiedAndroidName,
|
||||
androidName: type.androidProviderName,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// ============================================================
|
||||
// 闲言APP — 设备信息服务
|
||||
// 创建时间: 2026-05-10
|
||||
// 更新时间: 2026-05-20
|
||||
// 更新时间: 2026-05-22
|
||||
// 作用: 采集设备信息并自动注册到服务端
|
||||
// 上次更新: v14.31.0 新增cachedDeviceModel/cachedDeviceName缓存+initCache()方法
|
||||
// 上次更新: 新增refresh()/clearCache()方法,支持强制刷新设备缓存
|
||||
// ============================================================
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -24,6 +25,7 @@ class DeviceInfoService {
|
||||
|
||||
static final _deviceInfoPlugin = DeviceInfoPlugin();
|
||||
static const _prefKeyDeviceRegistered = 'device_registered_v2';
|
||||
static const _prefKeyFallbackDeviceId = 'fallback_device_id';
|
||||
|
||||
static String? _cachedDeviceModel;
|
||||
static String? _cachedDeviceName;
|
||||
@@ -36,7 +38,357 @@ class DeviceInfoService {
|
||||
_cachedDeviceName ??= await getDeviceName();
|
||||
}
|
||||
|
||||
/// 获取设备唯一标识
|
||||
// ============================================================
|
||||
// 品牌中文名映射
|
||||
// ============================================================
|
||||
|
||||
static const Map<String, String> _brandNameMap = {
|
||||
'huawei': '华为',
|
||||
'honor': '荣耀',
|
||||
'xiaomi': '小米',
|
||||
'redmi': '红米',
|
||||
'oppo': 'OPPO',
|
||||
'vivo': 'vivo',
|
||||
'oneplus': '一加',
|
||||
'realme': '真我',
|
||||
'meizu': '魅族',
|
||||
'samsung': '三星',
|
||||
'zte': '中兴',
|
||||
'lenovo': '联想',
|
||||
'motorola': '摩托罗拉',
|
||||
'nubia': '努比亚',
|
||||
'iqoo': 'iQOO',
|
||||
'pixel': 'Google Pixel',
|
||||
'apple': 'Apple',
|
||||
'sony': '索尼',
|
||||
'asus': '华硕',
|
||||
'rog': 'ROG',
|
||||
'nothing': 'Nothing',
|
||||
'transsion': '传音',
|
||||
'tecno': 'TECNO',
|
||||
'infinix': 'Infinix',
|
||||
'poco': 'POCO',
|
||||
'sharp': '夏普',
|
||||
'htc': 'HTC',
|
||||
'lg': 'LG',
|
||||
'nokia': '诺基亚',
|
||||
'google': 'Google',
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 热门型号友好名称映射
|
||||
// ============================================================
|
||||
|
||||
static const Map<String, String> _modelNameMap = {
|
||||
// 华为
|
||||
'ELE-AL00': '华为 P40',
|
||||
'ELE-TL00': '华为 P40',
|
||||
'ANA-AL00': '华为 P40 Pro',
|
||||
'ANA-TL00': '华为 P40 Pro',
|
||||
'ELS-AN00': '华为 P40 Pro+',
|
||||
'JAD-AL00': '华为 P50',
|
||||
'JAD-AN00': '华为 P50',
|
||||
'JAD-LX9': '华为 P50',
|
||||
'JAD-AL50': '华为 P50 Pro',
|
||||
'ABR-AL00': '华为 P50 Pro',
|
||||
'ALT-AL00': '华为 P50 Pocket',
|
||||
'MATE-AL00': '华为 Mate 50',
|
||||
'MATE-AN00': '华为 Mate 50',
|
||||
'CET-AL00': '华为 Mate 50 Pro',
|
||||
'CET-AN00': '华为 Mate 50 Pro',
|
||||
'TAH-AN00': '华为 Mate 60',
|
||||
'TAH-AN00m': '华为 Mate 60',
|
||||
'ALT-AN00': '华为 Mate 60 Pro',
|
||||
'BRA-AN00': '华为 Mate 60 Pro+',
|
||||
'ALN-AL00': '华为 Mate 30',
|
||||
'ALN-AN00': '华为 Mate 30',
|
||||
'LIO-AL00': '华为 Mate 30 Pro',
|
||||
'LIO-AN00': '华为 Mate 30 Pro',
|
||||
'OCE-AN10': '华为 Mate 40',
|
||||
'NOH-AN00': '华为 Mate 40 Pro',
|
||||
'NOH-AN01': '华为 Mate 40 Pro',
|
||||
'NOP-AN00': '华为 Mate 40 Pro+',
|
||||
'TET-AN00': '华为 Mate X',
|
||||
'TET-AN10': '华为 Mate X',
|
||||
'TAH-N1H': '华为 Mate X3',
|
||||
'GEM-AN00': '华为 Mate X5',
|
||||
'WGR-AN00': '华为 nova 12',
|
||||
'ADA-AL00': '华为 nova 7',
|
||||
'ANG-AN00': '华为 nova 8',
|
||||
'RTE-AL00': '华为 nova 9',
|
||||
'NAM-AL00': '华为 nova 10',
|
||||
'BZT-AN00': '华为 nova 11',
|
||||
'FIG-AL00': '华为 Mate 20',
|
||||
'FIG-TL00': '华为 Mate 20',
|
||||
'LYA-AL00': '华为 Mate 20 Pro',
|
||||
'LYA-AN00': '华为 Mate 20 Pro',
|
||||
'EVR-AL00': '华为 Mate 20 X',
|
||||
'EVR-AN00': '华为 Mate 20 X',
|
||||
'VOG-AL00': '华为 P30',
|
||||
'VOG-AN00': '华为 P30 Pro',
|
||||
'VCE-AL00': '华为 P30',
|
||||
'MAR-AL00': '华为 P30 lite',
|
||||
// 荣耀
|
||||
'ANY-AN00': '荣耀 30',
|
||||
'BMH-AN10': '荣耀 30S',
|
||||
'EBG-AN00': '荣耀 50',
|
||||
'FNE-AN00': '荣耀 60',
|
||||
'FRI-AN00': '荣耀 70',
|
||||
'VMA-AN00': '荣耀 80',
|
||||
'MGI-AN00': '荣耀 90',
|
||||
'MAA-AN00': '荣耀 Magic5',
|
||||
'BVL-AN00': '荣耀 Magic6',
|
||||
'BVL-N49': '荣耀 Magic6 Pro',
|
||||
// 小米
|
||||
'23127PN0CG': '小米 14',
|
||||
'23116PN5BC': '小米 14 Pro',
|
||||
'23117RA68G': '小米 14 Ultra',
|
||||
'2211133C': '小米 13',
|
||||
'2210132C': '小米 13 Pro',
|
||||
'2304FPN6DG': '小米 13 Ultra',
|
||||
'2201123C': '小米 12',
|
||||
'2201122C': '小米 12 Pro',
|
||||
'2203121C': '小米 12S Ultra',
|
||||
'M2011K2C': '小米 11',
|
||||
'M2012K11AC': '小米 11 Pro',
|
||||
'M2102K1C': '小米 11 Ultra',
|
||||
'M2007J22C': '小米 10',
|
||||
'M2001J2C': '小米 10 Pro',
|
||||
'M2004J11C': '小米 10 Ultra',
|
||||
'M1902F1A': '小米 9',
|
||||
'M1803E1A': '小米 8',
|
||||
'M2101K9C': '小米 11i',
|
||||
'23013RK75C': '小米 Civi 3',
|
||||
'23076MO4BC': '小米 Civi 4 Pro',
|
||||
'24030PN60G': '小米 14 CIVI',
|
||||
'24053PN09G': '小米 15',
|
||||
'24129PN74G': '小米 15 Pro',
|
||||
// 红米
|
||||
'2312DRA50G': '红米 Note 13 Pro',
|
||||
'23090RA98G': '红米 Note 13',
|
||||
'23076RA4DG': '红米 12',
|
||||
'22101316G': '红米 Note 12 Pro',
|
||||
'22111319G': '红米 Note 12',
|
||||
'22071212AG': '红米 Note 11 Pro',
|
||||
'2201117TG': '红米 Note 11',
|
||||
'M2010J19SY': '红米 Note 9 Pro',
|
||||
'M2003J15SC': '红米 Note 9',
|
||||
'23122RAA0G': '红米 K70',
|
||||
'23113RKC6G': '红米 K70 Pro',
|
||||
'23078PND5G': '红米 K60',
|
||||
'22127RK46C': '红米 K60 Pro',
|
||||
'22081212C': '红米 K50 Ultra',
|
||||
'22041211AC': '红米 K50 Pro',
|
||||
'22041216C': '红米 K50',
|
||||
'21081111RG': '红米 K40 Pro',
|
||||
'M2012K11C': '红米 K40',
|
||||
'M2007J22B': '红米 K30 Pro',
|
||||
'24122RKC7C': '红米 K80',
|
||||
'24117RKV7C': '红米 K80 Pro',
|
||||
// OPPO
|
||||
'CPH2591': 'OPPO Find X7',
|
||||
'PHZ110': 'OPPO Find X7 Ultra',
|
||||
'CPH2505': 'OPPO Find X6 Pro',
|
||||
'CPH2449': 'OPPO Find X5 Pro',
|
||||
'CPH2375': 'OPPO Find X3 Pro',
|
||||
'CPH2581': 'OPPO Reno 11 Pro',
|
||||
'CPH2521': 'OPPO Reno 10 Pro',
|
||||
'CPH2441': 'OPPO Reno 9 Pro',
|
||||
'PFDM00': 'OPPO Find N3',
|
||||
'PEQM00': 'OPPO Find N2',
|
||||
'PGBM10': 'OPPO Reno 12 Pro',
|
||||
'PJC110': 'OPPO Find X8',
|
||||
'PJC1': 'OPPO Find X8 Pro',
|
||||
// vivo
|
||||
'V2324A': 'vivo X100',
|
||||
'V2324HA': 'vivo X100 Pro',
|
||||
'V2231A': 'vivo X90 Pro',
|
||||
'V2219A': 'vivo X90',
|
||||
'V2145A': 'vivo X80 Pro',
|
||||
'V2171A': 'vivo X Fold3',
|
||||
'V2307A': 'vivo X Fold3 Pro',
|
||||
'V2338A': 'vivo S18 Pro',
|
||||
'V2355A': 'vivo S19 Pro',
|
||||
'V2408A': 'vivo X200',
|
||||
'V2408CA': 'vivo X200 Pro',
|
||||
// iQOO
|
||||
'I2302': 'iQOO 12',
|
||||
'I2219': 'iQOO 11',
|
||||
'I2123': 'iQOO 10',
|
||||
'I2012': 'iQOO 9 Pro',
|
||||
'V2339A': 'iQOO Neo9',
|
||||
'V2403A': 'iQOO Neo9S Pro',
|
||||
'V2352A': 'iQOO Z9 Turbo',
|
||||
// 一加
|
||||
'CPH2583': '一加 12',
|
||||
'CPH2415': '一加 10 Pro',
|
||||
'PJD110': '一加 13',
|
||||
'PGB110': '一加 Ace 3',
|
||||
'PHB110': '一加 Ace 2 Pro',
|
||||
'PJE110': '一加 Ace 3V',
|
||||
// 真我
|
||||
'RMX3901': '真我 GT5 Pro',
|
||||
'RMX3700': '真我 GT5',
|
||||
'RMX3708': '真我 GT Neo5',
|
||||
'RMX3881': '真我 GT Neo6',
|
||||
'RMX3921': '真我 GT Neo6 SE',
|
||||
'RMX3999': '真我 12 Pro+',
|
||||
'RMX3951': '真我 13 Pro+',
|
||||
// 魅族
|
||||
'MEIZU 21': '魅族 21',
|
||||
'MEIZU 21 Pro': '魅族 21 Pro',
|
||||
'MEIZU 20': '魅族 20',
|
||||
'MEIZU 20 Pro': '魅族 20 Pro',
|
||||
'MEIZU 20 Infinity': '魅族 20 Infinity',
|
||||
'M1916Q': '魅族 18',
|
||||
'M1917Q': '魅族 18 Pro',
|
||||
// 三星
|
||||
'SM-S928B': '三星 Galaxy S25 Ultra',
|
||||
'SM-S926B': '三星 Galaxy S25+',
|
||||
'SM-S921B': '三星 Galaxy S25',
|
||||
'SM-S928U': '三星 Galaxy S25 Ultra',
|
||||
'SM-S928N': '三星 Galaxy S25 Ultra',
|
||||
'SM-S918B': '三星 Galaxy S24 Ultra',
|
||||
'SM-S916B': '三星 Galaxy S24+',
|
||||
'SM-S924B': '三星 Galaxy S24',
|
||||
'SM-S908B': '三星 Galaxy S23 Ultra',
|
||||
'SM-S906B': '三星 Galaxy S23+',
|
||||
'SM-S901B': '三星 Galaxy S23',
|
||||
'SM-S911B': '三星 Galaxy S22 Ultra',
|
||||
'SM-S901E': '三星 Galaxy S22',
|
||||
'SM-G998B': '三星 Galaxy S21 Ultra',
|
||||
'SM-G996B': '三星 Galaxy S21+',
|
||||
'SM-G991B': '三星 Galaxy S21',
|
||||
'SM-N986B': '三星 Galaxy Note20 Ultra',
|
||||
'SM-N981B': '三星 Galaxy Note20',
|
||||
'SM-F731B': '三星 Galaxy Z Flip5',
|
||||
'SM-F946B': '三星 Galaxy Z Fold5',
|
||||
'SM-F721B': '三星 Galaxy Z Flip4',
|
||||
'SM-F936B': '三星 Galaxy Z Fold4',
|
||||
// 中兴
|
||||
'ZTE A2023': '中兴 Axon 40 Ultra',
|
||||
'ZTE V8020': '中兴 Axon 50 Ultra',
|
||||
// 努比亚
|
||||
'NX712J': '努比亚 Z50 Ultra',
|
||||
'NX769J': '努比亚 Z60 Ultra',
|
||||
// 联想
|
||||
'L78032': '联想 拯救者 Y70',
|
||||
'L38111': '联想 拯救者 Y90',
|
||||
// 摩托罗拉
|
||||
'XT2301-5': '摩托罗拉 edge 40 Pro',
|
||||
'XT2401-1': '摩托罗拉 edge 50 Ultra',
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// iOS 设备型号映射 (utsname.machine → 友好名称)
|
||||
// ============================================================
|
||||
|
||||
static const Map<String, String> _iosModelMap = {
|
||||
'iPhone17,1': 'iPhone 16 Pro',
|
||||
'iPhone17,2': 'iPhone 16 Pro Max',
|
||||
'iPhone17,3': 'iPhone 16',
|
||||
'iPhone17,4': 'iPhone 16 Plus',
|
||||
'iPhone17,5': 'iPhone 16e',
|
||||
'iPhone16,1': 'iPhone 15 Pro',
|
||||
'iPhone16,2': 'iPhone 15 Pro Max',
|
||||
'iPhone16,3': 'iPhone 15',
|
||||
'iPhone16,4': 'iPhone 15 Plus',
|
||||
'iPhone15,2': 'iPhone 14 Pro',
|
||||
'iPhone15,3': 'iPhone 14 Pro Max',
|
||||
'iPhone15,4': 'iPhone 14',
|
||||
'iPhone15,5': 'iPhone 14 Plus',
|
||||
'iPhone14,2': 'iPhone 13 Pro',
|
||||
'iPhone14,3': 'iPhone 13 Pro Max',
|
||||
'iPhone14,4': 'iPhone 13 mini',
|
||||
'iPhone14,5': 'iPhone 13',
|
||||
'iPhone14,6': 'iPhone SE (3rd gen)',
|
||||
'iPhone14,7': 'iPhone 13',
|
||||
'iPhone14,8': 'iPhone 13',
|
||||
'iPhone13,2': 'iPhone 12',
|
||||
'iPhone13,3': 'iPhone 12',
|
||||
'iPhone13,4': 'iPhone 12 Pro',
|
||||
'iPhone13,5': 'iPhone 12 Pro',
|
||||
'iPhone13,6': 'iPhone 12 mini',
|
||||
'iPhone13,7': 'iPhone 12 mini',
|
||||
'iPhone12,1': 'iPhone 11',
|
||||
'iPhone12,3': 'iPhone 11 Pro',
|
||||
'iPhone12,5': 'iPhone 11 Pro Max',
|
||||
'iPhone12,8': 'iPhone SE (2nd gen)',
|
||||
'iPhone11,2': 'iPhone XS',
|
||||
'iPhone11,4': 'iPhone XS Max',
|
||||
'iPhone11,8': 'iPhone XR',
|
||||
'iPhone10,3': 'iPhone X',
|
||||
'iPhone10,6': 'iPhone X',
|
||||
'iPad16,1': 'iPad Pro 13" (M4)',
|
||||
'iPad16,2': 'iPad Pro 13" (M4)',
|
||||
'iPad16,3': 'iPad Pro 11" (M4)',
|
||||
'iPad16,4': 'iPad Pro 11" (M4)',
|
||||
'iPad14,1': 'iPad Air (M2)',
|
||||
'iPad14,2': 'iPad Air (M2)',
|
||||
'iPad14,3': 'iPad Pro 12.9" (M2)',
|
||||
'iPad14,4': 'iPad Pro 12.9" (M2)',
|
||||
'iPad14,5': 'iPad Pro 11" (M2)',
|
||||
'iPad14,6': 'iPad Pro 11" (M2)',
|
||||
'iPad13,1': 'iPad Air (5th gen)',
|
||||
'iPad13,2': 'iPad Air (5th gen)',
|
||||
'iPad13,4': 'iPad Pro 12.9" (M1)',
|
||||
'iPad13,5': 'iPad Pro 12.9" (M1)',
|
||||
'iPad13,8': 'iPad Pro 11" (M1)',
|
||||
'iPad13,9': 'iPad Pro 11" (M1)',
|
||||
'iPad13,16': 'iPad (10th gen)',
|
||||
'iPad13,17': 'iPad (10th gen)',
|
||||
'iPad13,18': 'iPad mini (6th gen)',
|
||||
'iPad13,19': 'iPad mini (6th gen)',
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 品牌中文名查询
|
||||
// ============================================================
|
||||
|
||||
static String getBrandChineseName(String brand) {
|
||||
final key = brand.toLowerCase().trim();
|
||||
return _brandNameMap[key] ?? brand;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 型号友好名称查询
|
||||
// ============================================================
|
||||
|
||||
static String? getModelFriendlyName(String model) {
|
||||
final trimmed = model.trim();
|
||||
if (trimmed.isEmpty) return null;
|
||||
if (_modelNameMap.containsKey(trimmed)) return _modelNameMap[trimmed];
|
||||
if (_iosModelMap.containsKey(trimmed)) return _iosModelMap[trimmed];
|
||||
for (final entry in _modelNameMap.entries) {
|
||||
if (trimmed.toUpperCase().contains(entry.key.toUpperCase())) {
|
||||
return entry.value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 生成稳定UUID
|
||||
// ============================================================
|
||||
|
||||
static String _generateStableUuid() {
|
||||
final r = Random();
|
||||
const chars = '0123456789abcdef';
|
||||
const segments = [8, 4, 4, 4, 12];
|
||||
final buf = StringBuffer();
|
||||
for (var i = 0; i < segments.length; i++) {
|
||||
if (i > 0) buf.write('-');
|
||||
for (var j = 0; j < segments[i]; j++) {
|
||||
buf.write(chars[r.nextInt(16)]);
|
||||
}
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 获取设备唯一标识
|
||||
// ============================================================
|
||||
|
||||
static Future<String> getDeviceId() async {
|
||||
try {
|
||||
if (pu.isOhos) {
|
||||
@@ -52,54 +404,87 @@ class DeviceInfoService {
|
||||
} catch (e) {
|
||||
Log.w('获取设备ID失败: $e');
|
||||
}
|
||||
return 'unknown_${DateTime.now().millisecondsSinceEpoch}';
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
var fallbackId = prefs.getString(_prefKeyFallbackDeviceId);
|
||||
if (fallbackId != null && fallbackId.isNotEmpty)
|
||||
return 'fallback_$fallbackId';
|
||||
fallbackId = _generateStableUuid();
|
||||
await prefs.setString(_prefKeyFallbackDeviceId, fallbackId);
|
||||
Log.i('生成稳定回退设备ID: $fallbackId');
|
||||
return 'fallback_$fallbackId';
|
||||
}
|
||||
|
||||
/// 获取设备名称
|
||||
// ============================================================
|
||||
// 获取设备名称 (友好名称)
|
||||
// ============================================================
|
||||
|
||||
static Future<String> getDeviceName() async {
|
||||
if (_cachedDeviceName != null) return _cachedDeviceName!;
|
||||
try {
|
||||
if (pu.isOhos) {
|
||||
final android = await _deviceInfoPlugin.androidInfo;
|
||||
final friendly = _buildFriendlyName(android.brand, android.model);
|
||||
if (friendly != null) return friendly;
|
||||
if (android.model.isNotEmpty) return android.model;
|
||||
if (android.brand.isNotEmpty) return android.brand;
|
||||
if (android.brand.isNotEmpty) return getBrandChineseName(android.brand);
|
||||
if (android.display.isNotEmpty) return android.display;
|
||||
if (android.product.isNotEmpty) return android.product;
|
||||
return 'HarmonyOS Device';
|
||||
return '鸿蒙设备';
|
||||
} else if (Platform.isAndroid) {
|
||||
final android = await _deviceInfoPlugin.androidInfo;
|
||||
return android.model.isNotEmpty
|
||||
? android.model
|
||||
: (android.brand.isNotEmpty ? android.brand : 'Android');
|
||||
final friendly = _buildFriendlyName(android.brand, android.model);
|
||||
if (friendly != null) return friendly;
|
||||
if (android.model.isNotEmpty) return android.model;
|
||||
if (android.brand.isNotEmpty) return getBrandChineseName(android.brand);
|
||||
return 'Android 设备';
|
||||
} else if (Platform.isIOS) {
|
||||
final ios = await _deviceInfoPlugin.iosInfo;
|
||||
return ios.utsname.machine.isNotEmpty ? ios.utsname.machine : 'iPhone';
|
||||
final machine = ios.utsname.machine;
|
||||
if (machine.isNotEmpty) {
|
||||
final friendly = _iosModelMap[machine];
|
||||
if (friendly != null) return friendly;
|
||||
return machine;
|
||||
}
|
||||
return 'iPhone';
|
||||
}
|
||||
} catch (e) {
|
||||
Log.w('获取设备名称失败: $e');
|
||||
}
|
||||
return 'Unknown';
|
||||
return '未知设备';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 获取设备型号 (品牌+型号)
|
||||
// ============================================================
|
||||
|
||||
static Future<String> getDeviceModel() async {
|
||||
if (_cachedDeviceModel != null) return _cachedDeviceModel!;
|
||||
try {
|
||||
if (pu.isOhos) {
|
||||
final android = await _deviceInfoPlugin.androidInfo;
|
||||
final brand = android.brand.isNotEmpty ? android.brand : 'HarmonyOS';
|
||||
final model = android.model.isNotEmpty
|
||||
? android.model
|
||||
: (android.display.isNotEmpty ? android.display : (android.product.isNotEmpty ? android.product : 'Device'));
|
||||
final result = '$brand $model'.trim();
|
||||
return result == 'HarmonyOS Device' ? 'HarmonyOS Device' : result;
|
||||
return _buildDeviceModel(
|
||||
android.brand,
|
||||
android.model,
|
||||
android.display,
|
||||
android.product,
|
||||
isOhos: true,
|
||||
);
|
||||
} else if (Platform.isAndroid) {
|
||||
final android = await _deviceInfoPlugin.androidInfo;
|
||||
return '${android.brand} ${android.model}'.trim();
|
||||
return _buildDeviceModel(
|
||||
android.brand,
|
||||
android.model,
|
||||
android.display,
|
||||
android.product,
|
||||
);
|
||||
} else if (Platform.isIOS) {
|
||||
final ios = await _deviceInfoPlugin.iosInfo;
|
||||
return ios.utsname.machine.isNotEmpty
|
||||
? ios.utsname.machine
|
||||
: 'iOS Device';
|
||||
final machine = ios.utsname.machine;
|
||||
if (machine.isNotEmpty) {
|
||||
final friendly = _iosModelMap[machine];
|
||||
return friendly ?? machine;
|
||||
}
|
||||
return 'iOS Device';
|
||||
}
|
||||
} catch (e) {
|
||||
Log.w('获取设备型号失败: $e');
|
||||
@@ -107,7 +492,57 @@ class DeviceInfoService {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
/// 获取平台标识
|
||||
// ============================================================
|
||||
// 构建友好设备名称
|
||||
// ============================================================
|
||||
|
||||
static String? _buildFriendlyName(String brand, String model) {
|
||||
if (model.isNotEmpty) {
|
||||
final friendly = getModelFriendlyName(model);
|
||||
if (friendly != null) return friendly;
|
||||
}
|
||||
if (brand.isNotEmpty && model.isNotEmpty) {
|
||||
final brandCn = getBrandChineseName(brand);
|
||||
if (brandCn != brand) return '$brandCn $model';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 构建设备型号字符串
|
||||
// ============================================================
|
||||
|
||||
static String _buildDeviceModel(
|
||||
String brand,
|
||||
String model,
|
||||
String display,
|
||||
String product, {
|
||||
bool isOhos = false,
|
||||
}) {
|
||||
final brandCn = brand.isNotEmpty
|
||||
? getBrandChineseName(brand)
|
||||
: (isOhos ? '鸿蒙' : '');
|
||||
final modelFallback = model.isNotEmpty
|
||||
? model
|
||||
: (display.isNotEmpty ? display : (product.isNotEmpty ? product : ''));
|
||||
|
||||
if (model.isNotEmpty) {
|
||||
final friendly = getModelFriendlyName(model);
|
||||
if (friendly != null) return friendly;
|
||||
}
|
||||
|
||||
if (brandCn.isNotEmpty && modelFallback.isNotEmpty) {
|
||||
return '$brandCn $modelFallback';
|
||||
}
|
||||
if (brandCn.isNotEmpty) return brandCn;
|
||||
if (modelFallback.isNotEmpty) return modelFallback;
|
||||
return isOhos ? '鸿蒙设备' : 'Android 设备';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 获取平台标识
|
||||
// ============================================================
|
||||
|
||||
static String getPlatform() {
|
||||
if (kIsWeb) return 'web';
|
||||
if (pu.isOhos) return 'ohos';
|
||||
@@ -119,7 +554,10 @@ class DeviceInfoService {
|
||||
return 'other';
|
||||
}
|
||||
|
||||
/// 获取APP名称+版本
|
||||
// ============================================================
|
||||
// 获取APP名称+版本
|
||||
// ============================================================
|
||||
|
||||
static Future<String> getAppName() async {
|
||||
try {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
@@ -129,7 +567,10 @@ class DeviceInfoService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册设备到服务端(登录后调用)
|
||||
// ============================================================
|
||||
// 注册设备到服务端(登录后调用)
|
||||
// ============================================================
|
||||
|
||||
static Future<bool> registerDeviceIfNeeded() async {
|
||||
try {
|
||||
final deviceId = await getDeviceId();
|
||||
@@ -138,7 +579,6 @@ class DeviceInfoService {
|
||||
final platform = getPlatform();
|
||||
final appName = await getAppName();
|
||||
|
||||
// 查询IP归属地(不阻塞注册,失败时降级)
|
||||
String? ipCity;
|
||||
String? ipRange;
|
||||
try {
|
||||
@@ -185,11 +625,46 @@ class DeviceInfoService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 重置注册状态(退出登录时调用)
|
||||
// ============================================================
|
||||
// 重置注册状态(退出登录时调用)
|
||||
// ============================================================
|
||||
|
||||
static Future<void> resetRegistration() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_prefKeyDeviceRegistered);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 强制刷新设备缓存(重新读取平台信息+重新注册)
|
||||
// ============================================================
|
||||
|
||||
static Future<void> refresh() async {
|
||||
clearCache();
|
||||
_cachedDeviceModel = await getDeviceModel();
|
||||
_cachedDeviceName = await getDeviceName();
|
||||
Log.i('设备缓存已刷新: model=$_cachedDeviceModel, name=$_cachedDeviceName');
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final wasRegistered = prefs.getBool(_prefKeyDeviceRegistered) ?? false;
|
||||
if (wasRegistered) {
|
||||
await registerDeviceIfNeeded();
|
||||
Log.i('设备信息已重新注册到服务端');
|
||||
}
|
||||
} catch (e) {
|
||||
Log.w('设备重新注册失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 仅清除本地缓存(不重新注册)
|
||||
// ============================================================
|
||||
|
||||
static void clearCache() {
|
||||
_cachedDeviceModel = null;
|
||||
_cachedDeviceName = null;
|
||||
Log.d('设备缓存已清除');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
// ============================================================
|
||||
// 闲言APP — 每日定时通知服务
|
||||
// 创建时间: 2026-05-20
|
||||
// 更新时间: 2026-05-20
|
||||
// 更新时间: 2026-05-22
|
||||
// 作用: 每日定时推送一句好句
|
||||
// 上次更新: 初始版本
|
||||
// 上次更新: @Deprecated 已合并至NotificationCenter,请使用NotificationCenter替代
|
||||
// ============================================================
|
||||
|
||||
import '../../utils/logger.dart';
|
||||
import 'local_notification_service.dart';
|
||||
|
||||
@Deprecated('已合并至NotificationCenter,请使用NotificationCenter替代')
|
||||
class DailyNotifyService {
|
||||
DailyNotifyService._();
|
||||
static final DailyNotifyService instance = DailyNotifyService._();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 本地通知服务
|
||||
/// 创建时间: 2026-05-02
|
||||
/// 更新时间: 2026-05-17
|
||||
/// 更新时间: 2026-05-22
|
||||
/// 作用: 本地推送通知管理 (初始化/调度/取消/点击处理)
|
||||
/// 上次更新: 鸿蒙适配-使用桥接方法隔离OhosInitializationSettings
|
||||
/// 上次更新: 添加study_progress通知点击路由
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:io';
|
||||
@@ -15,7 +15,7 @@ import 'package:timezone/data/latest_all.dart' as tz;
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
|
||||
import '../../router/app_router.dart';
|
||||
import 'notification_scheduler.dart';
|
||||
import 'notification_center.dart';
|
||||
import '../../storage/app_kv_store.dart';
|
||||
import '../../utils/logger.dart';
|
||||
import '../../utils/platform_utils.dart' as pu;
|
||||
@@ -121,6 +121,8 @@ class LocalNotificationService {
|
||||
context.appGo('/countdown');
|
||||
case 'daily_fortune':
|
||||
context.appGo('/daily-fortune');
|
||||
case 'study_progress':
|
||||
context.appGo('/home');
|
||||
case 'readlater':
|
||||
context.appGo('/readlater-chat');
|
||||
default:
|
||||
@@ -292,7 +294,7 @@ class LocalNotificationService {
|
||||
}
|
||||
|
||||
static Future<void> setupDailyNotifications(WidgetRef ref) async {
|
||||
if (!NotificationScheduler.isNotificationsEnabled) {
|
||||
if (!NotificationCenter.isNotificationsEnabled) {
|
||||
await cancelAll();
|
||||
return;
|
||||
}
|
||||
|
||||
303
lib/core/services/notification/notification_center.dart
Normal file
303
lib/core/services/notification/notification_center.dart
Normal file
@@ -0,0 +1,303 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 统一通知中心
|
||||
/// 创建时间: 2026-05-22
|
||||
/// 更新时间: 2026-05-22
|
||||
/// 作用: 合并 NotificationScheduler + DailyNotifyService,统一管理所有本地通知调度
|
||||
/// 上次更新: 初始版本,合并每日推荐/签到/节气/运势/学习进度/稍后读通知
|
||||
/// ============================================================
|
||||
|
||||
import 'local_notification_service.dart';
|
||||
import '../../storage/app_kv_store.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 bool get isNotificationsEnabled =>
|
||||
AppKVStore.getBool(_keyNotificationsEnabled) ?? false;
|
||||
|
||||
static Future<void> setNotificationsEnabled(bool v) async {
|
||||
await AppKVStore.setBool(_keyNotificationsEnabled, v);
|
||||
await configureAll();
|
||||
}
|
||||
|
||||
// ── 每日推荐 ──
|
||||
|
||||
static bool get isDailyRecommendEnabled =>
|
||||
AppKVStore.getBool(_keyDailyRecommendEnabled) ?? true;
|
||||
|
||||
static int get dailyRecommendHour =>
|
||||
AppKVStore.getInt(_keyDailyRecommendHour) ?? 8;
|
||||
|
||||
static int get dailyRecommendMinute =>
|
||||
AppKVStore.getInt(_keyDailyRecommendMinute) ?? 0;
|
||||
|
||||
static Future<void> setDailyRecommendEnabled(bool v) async {
|
||||
await AppKVStore.setBool(_keyDailyRecommendEnabled, v);
|
||||
await configureAll();
|
||||
}
|
||||
|
||||
static Future<void> setDailyRecommendTime(int hour, int minute) async {
|
||||
await AppKVStore.setInt(_keyDailyRecommendHour, hour);
|
||||
await AppKVStore.setInt(_keyDailyRecommendMinute, minute);
|
||||
await configureAll();
|
||||
}
|
||||
|
||||
// ── 签到提醒 ──
|
||||
|
||||
static bool get isSigninReminderEnabled =>
|
||||
AppKVStore.getBool(_keySigninReminderEnabled) ?? true;
|
||||
|
||||
static int get signinReminderHour =>
|
||||
AppKVStore.getInt(_keySigninReminderHour) ?? 20;
|
||||
|
||||
static int get signinReminderMinute =>
|
||||
AppKVStore.getInt(_keySigninReminderMinute) ?? 0;
|
||||
|
||||
static Future<void> setSigninReminderEnabled(bool v) async {
|
||||
await AppKVStore.setBool(_keySigninReminderEnabled, v);
|
||||
await configureAll();
|
||||
}
|
||||
|
||||
static Future<void> setSigninReminderTime(int hour, int minute) async {
|
||||
await AppKVStore.setInt(_keySigninReminderHour, hour);
|
||||
await AppKVStore.setInt(_keySigninReminderMinute, minute);
|
||||
await configureAll();
|
||||
}
|
||||
|
||||
// ── 节气通知 ──
|
||||
|
||||
static bool get isSolarTermEnabled =>
|
||||
AppKVStore.getBool(_keySolarTermEnabled) ?? true;
|
||||
|
||||
static Future<void> setSolarTermEnabled(bool v) async {
|
||||
await AppKVStore.setBool(_keySolarTermEnabled, v);
|
||||
await configureAll();
|
||||
}
|
||||
|
||||
// ── 每日运势 ──
|
||||
|
||||
static bool get isFortuneEnabled =>
|
||||
AppKVStore.getBool(_keyFortuneEnabled) ?? false;
|
||||
|
||||
static int get fortuneHour => AppKVStore.getInt(_keyFortuneHour) ?? 8;
|
||||
|
||||
static int get fortuneMinute => AppKVStore.getInt(_keyFortuneMinute) ?? 0;
|
||||
|
||||
static Future<void> setFortuneEnabled(bool v) async {
|
||||
await AppKVStore.setBool(_keyFortuneEnabled, v);
|
||||
await configureAll();
|
||||
}
|
||||
|
||||
static Future<void> setFortuneTime(int hour, int minute) async {
|
||||
await AppKVStore.setInt(_keyFortuneHour, hour);
|
||||
await AppKVStore.setInt(_keyFortuneMinute, minute);
|
||||
await configureAll();
|
||||
}
|
||||
|
||||
// ── 学习进度 ──
|
||||
|
||||
static bool get isStudyProgressEnabled =>
|
||||
AppKVStore.getBool(_keyStudyProgressEnabled) ?? false;
|
||||
|
||||
static Future<void> setStudyProgressEnabled(bool v) async {
|
||||
await AppKVStore.setBool(_keyStudyProgressEnabled, v);
|
||||
await configureAll();
|
||||
}
|
||||
|
||||
// ── 稍后读提醒 ──
|
||||
|
||||
static bool get isReadlaterEnabled =>
|
||||
AppKVStore.getBool(_keyReadlaterEnabled) ?? false;
|
||||
|
||||
static Future<void> setReadlaterEnabled(bool v) async {
|
||||
await AppKVStore.setBool(_keyReadlaterEnabled, v);
|
||||
}
|
||||
|
||||
// ── 核心调度 ──
|
||||
|
||||
static Future<void> configureAll() async {
|
||||
final enabled = AppKVStore.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 = AppKVStore.getBool(_keyDailyRecommendEnabled) ?? true;
|
||||
if (!enabled) return;
|
||||
|
||||
final hour = AppKVStore.getInt(_keyDailyRecommendHour) ?? 8;
|
||||
final minute = AppKVStore.getInt(_keyDailyRecommendMinute) ?? 0;
|
||||
|
||||
await LocalNotificationService.scheduleDaily(
|
||||
id: _idDailyRecommend,
|
||||
title: '闲言每日一句',
|
||||
body: '今天的句子已准备好,来看看吧 ✨',
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
payload: 'daily_sentence',
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _configureSigninReminder() async {
|
||||
final enabled = AppKVStore.getBool(_keySigninReminderEnabled) ?? true;
|
||||
if (!enabled) return;
|
||||
|
||||
final hour = AppKVStore.getInt(_keySigninReminderHour) ?? 20;
|
||||
final minute = AppKVStore.getInt(_keySigninReminderMinute) ?? 0;
|
||||
|
||||
await LocalNotificationService.scheduleDaily(
|
||||
id: _idSigninReminder,
|
||||
title: '闲言 · 签到提醒',
|
||||
body: '别忘了今日签到哦 📝',
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
payload: 'signin_reminder',
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _configureSolarTerm() async {
|
||||
final enabled = AppKVStore.getBool(_keySolarTermEnabled) ?? true;
|
||||
if (!enabled) return;
|
||||
|
||||
final nextTerm = _getNextSolarTerm();
|
||||
if (nextTerm == null) return;
|
||||
|
||||
final scheduledTime = DateTime(
|
||||
nextTerm['year'] as int,
|
||||
nextTerm['month'] as int,
|
||||
nextTerm['day'] as int,
|
||||
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 = AppKVStore.getBool(_keyFortuneEnabled) ?? false;
|
||||
if (!enabled) return;
|
||||
|
||||
final hour = AppKVStore.getInt(_keyFortuneHour) ?? 8;
|
||||
final minute = AppKVStore.getInt(_keyFortuneMinute) ?? 0;
|
||||
|
||||
await LocalNotificationService.scheduleDaily(
|
||||
id: _idFortune,
|
||||
title: '闲言 · 🔮 每日运势',
|
||||
body: '今日运势已生成,快来看看你的运势吧 ✨',
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
payload: 'daily_fortune',
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _configureStudyProgress() async {
|
||||
final enabled = AppKVStore.getBool(_keyStudyProgressEnabled) ?? false;
|
||||
if (!enabled) return;
|
||||
|
||||
await LocalNotificationService.scheduleDaily(
|
||||
id: _idStudyProgress,
|
||||
title: '闲言 · 学习进度',
|
||||
body: '该复习今天的学习内容了 📊',
|
||||
hour: 20,
|
||||
payload: 'study_progress',
|
||||
);
|
||||
}
|
||||
|
||||
// ── 节气数据 ──
|
||||
|
||||
static Map<String, dynamic>? _getNextSolarTerm() {
|
||||
final now = DateTime.now();
|
||||
final terms = _solarTerms2026;
|
||||
for (final term in terms) {
|
||||
final date = DateTime(
|
||||
term['year'] as int,
|
||||
term['month'] as int,
|
||||
term['day'] as int,
|
||||
);
|
||||
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': '天时人事日相催,冬至阳生春又来'},
|
||||
];
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 通知调度器
|
||||
/// 创建时间: 2026-05-02
|
||||
/// 更新时间: 2026-05-13
|
||||
/// 作用: 统一管理本地通知调度 (每日推荐/签到/节气/番茄钟/运势)
|
||||
/// 上次更新: 增加运势推送调度
|
||||
/// 更新时间: 2026-05-22
|
||||
/// 作用: 统一管理本地通知调度 (每日推荐/签到/节气/番茄钟/运势/学习进度)
|
||||
/// 上次更新: @Deprecated 已合并至NotificationCenter,请使用NotificationCenter替代
|
||||
/// ============================================================
|
||||
|
||||
import 'local_notification_service.dart';
|
||||
import '../../storage/app_kv_store.dart';
|
||||
import '../../utils/logger.dart';
|
||||
|
||||
@Deprecated('已合并至NotificationCenter,请使用NotificationCenter替代')
|
||||
class NotificationScheduler {
|
||||
NotificationScheduler._();
|
||||
|
||||
@@ -24,16 +25,25 @@ class NotificationScheduler {
|
||||
static const _keyFortuneEnabled = 'fortune_enabled';
|
||||
static const _keyFortuneHour = 'fortune_hour';
|
||||
static const _keyFortuneMinute = 'fortune_minute';
|
||||
static const _keyStudyProgressEnabled = 'study_progress_enabled';
|
||||
|
||||
static Future<void> _cancelAllManaged() async {
|
||||
await LocalNotificationService.cancel(1001);
|
||||
await LocalNotificationService.cancel(1002);
|
||||
await LocalNotificationService.cancel(1003);
|
||||
await LocalNotificationService.cancel(1004);
|
||||
await LocalNotificationService.cancel(2001);
|
||||
}
|
||||
|
||||
static Future<void> configureAll() async {
|
||||
final enabled = AppKVStore.getBool(_keyNotificationsEnabled) ?? false;
|
||||
if (!enabled) {
|
||||
await LocalNotificationService.cancelAll();
|
||||
await _cancelAllManaged();
|
||||
Log.i('通知调度: 通知已关闭,取消所有调度');
|
||||
return;
|
||||
}
|
||||
|
||||
await LocalNotificationService.cancelAll();
|
||||
await _cancelAllManaged();
|
||||
|
||||
final dailySentence = AppKVStore.getBool(_keyDailySentenceEnabled) ?? true;
|
||||
if (dailySentence) {
|
||||
@@ -41,8 +51,8 @@ class NotificationScheduler {
|
||||
final minute = AppKVStore.getInt(_keyDailySentenceMinute) ?? 0;
|
||||
await LocalNotificationService.scheduleDaily(
|
||||
id: 1001,
|
||||
title: '闲言 · 每日一句',
|
||||
body: '新的一天,送你一句好话 ✨',
|
||||
title: '闲言每日一句',
|
||||
body: '今天的句子已准备好,来看看吧 ✨',
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
payload: 'daily_sentence',
|
||||
@@ -83,6 +93,17 @@ class NotificationScheduler {
|
||||
);
|
||||
}
|
||||
|
||||
final studyProgress = AppKVStore.getBool(_keyStudyProgressEnabled) ?? false;
|
||||
if (studyProgress) {
|
||||
await LocalNotificationService.scheduleDaily(
|
||||
id: 1004,
|
||||
title: '闲言 · 学习进度',
|
||||
body: '该复习今天的学习内容了 📊',
|
||||
hour: 20,
|
||||
payload: 'study_progress',
|
||||
);
|
||||
}
|
||||
|
||||
Log.i('通知调度: 所有通知已配置');
|
||||
}
|
||||
|
||||
@@ -151,6 +172,9 @@ class NotificationScheduler {
|
||||
|
||||
static int get fortuneMinute => AppKVStore.getInt(_keyFortuneMinute) ?? 0;
|
||||
|
||||
static bool get isStudyProgressEnabled =>
|
||||
AppKVStore.getBool(_keyStudyProgressEnabled) ?? false;
|
||||
|
||||
static Future<void> setNotificationsEnabled(bool v) async {
|
||||
await AppKVStore.setBool(_keyNotificationsEnabled, v);
|
||||
await configureAll();
|
||||
@@ -194,6 +218,11 @@ class NotificationScheduler {
|
||||
await configureAll();
|
||||
}
|
||||
|
||||
static Future<void> setStudyProgressEnabled(bool v) async {
|
||||
await AppKVStore.setBool(_keyStudyProgressEnabled, v);
|
||||
await configureAll();
|
||||
}
|
||||
|
||||
static final List<Map<String, dynamic>> _solarTerms2026 = [
|
||||
{
|
||||
'year': 2026,
|
||||
|
||||
65
lib/core/services/ui/status_bar_service.dart
Normal file
65
lib/core/services/ui/status_bar_service.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 状态栏统一管理服务
|
||||
/// 创建时间: 2026-05-22
|
||||
/// 更新时间: 2026-05-22
|
||||
/// 作用: 集中管理状态栏样式,响应主题变化,支持沉浸式模式
|
||||
/// 上次更新: 初始创建,统一所有 SystemUiOverlayStyle 设置
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class StatusBarService {
|
||||
StatusBarService._();
|
||||
|
||||
static SystemUiOverlayStyle resolveStyle({required bool isDark}) {
|
||||
return SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
|
||||
statusBarBrightness: isDark ? Brightness.dark : Brightness.light,
|
||||
systemNavigationBarColor: Colors.transparent,
|
||||
systemNavigationBarIconBrightness:
|
||||
isDark ? Brightness.light : Brightness.dark,
|
||||
systemNavigationBarDividerColor: Colors.transparent,
|
||||
);
|
||||
}
|
||||
|
||||
static void applyStyle({required bool isDark}) {
|
||||
SystemChrome.setSystemUIOverlayStyle(resolveStyle(isDark: isDark));
|
||||
}
|
||||
|
||||
static void setImmersive(bool immersive) {
|
||||
if (immersive) {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.immersiveSticky,
|
||||
);
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.edgeToEdge,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static void enterEdgeToEdge() {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
}
|
||||
}
|
||||
|
||||
class StatusBarStyleRegion extends StatelessWidget {
|
||||
const StatusBarStyleRegion({
|
||||
super.key,
|
||||
required this.isDark,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final bool isDark;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: StatusBarService.resolveStyle(isDark: isDark),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user