chore: 批量完成2026-05-24版本迭代更新

本次提交涵盖多项功能优化与重构:
1. 重构pro_image_editor依赖为官方托管版本,移除本地包引用
2. 拆分角色表情枚举至独立文件,优化代码复用性
3. 新增壁纸收藏、预加载、健康检测服务与本地存储支持
4. 完善API响应类型安全检查与排行榜服务能力
5. 新增应用锁设置路由与页面支持
6. 优化路由跳转使用常量路径替代硬编码字符串
7. 新增阅读报告分享功能与设置变更日志服务
8. 修复多处类型转换与空指针风险问题
9. 调整API超时时间优化网络请求表现
10. 统一文件头格式与部分UI组件样式
This commit is contained in:
Developer
2026-05-24 04:00:49 +08:00
parent f5a75cb6d3
commit df1f127a12
84 changed files with 11368 additions and 2176 deletions

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — 权限管理服务
/// 创建时间: 2026-04-23
/// 更新时间: 2026-05-22
/// 更新时间: 2026-05-24
/// 作用: 统一管理应用权限请求,支持相机/相册/通知/位置/蓝牙/附近设备/麦克风/存储/网络/剪贴板权限
/// 上次更新: 移除3个高敏感权限(悬浮窗/通讯录/忽略电池优化),降低应用商店审核风险
/// 上次更新: 增加PermissionGroup权限分组枚举AppPermission添加group字段
/// ============================================================
import 'dart:io';
@@ -36,6 +36,17 @@ enum AppPermissionStatus {
}
}
/// 权限分组枚举
enum PermissionGroup {
required('必要权限', CupertinoIcons.lock_shield_fill),
optional('可选权限', CupertinoIcons.hand_raised_fill),
system('系统权限', CupertinoIcons.gear_solid);
const PermissionGroup(this.label, this.icon);
final String label;
final IconData icon;
}
/// 权限类型枚举
enum AppPermission {
camera(
@@ -45,6 +56,7 @@ enum AppPermission {
'用于拍照制作壁纸、扫描二维码、文件传输扫码配对。仅在您主动使用相关功能时请求,不会后台调用。',
Color(0xFF34C759),
isRequired: true,
group: PermissionGroup.required,
usageScenes: ['壁纸制作 — 拍照添加图片', '二维码 — 扫描登录/配对', '文件传输 — 扫码连接设备'],
),
photos(
@@ -54,6 +66,7 @@ enum AppPermission {
'用于选择图片制作壁纸、保存作品到本地相册、设置头像。仅访问您选中的图片,不会读取全部相册。',
Color(0xFF6C63FF),
isRequired: true,
group: PermissionGroup.required,
usageScenes: ['壁纸制作 — 选择图片', '保存卡片 — 保存到相册', '个人中心 — 设置头像'],
),
notification(
@@ -63,6 +76,7 @@ enum AppPermission {
'用于推送每日推荐句子、签到提醒、系统公告、文件传输状态和互动消息。您可以随时在系统设置中关闭。',
Color(0xFFFF3B30),
isRequired: true,
group: PermissionGroup.required,
usageScenes: ['每日推荐 — 定时推送', '签到提醒 — 每日提醒', '文件传输 — 传输状态', '互动消息 — 点赞评论'],
),
location(
@@ -71,6 +85,7 @@ enum AppPermission {
CupertinoIcons.location_fill,
'用于获取天气信息和节气提醒,仅使用粗略位置(城市级),不获取精确位置,不会后台追踪。',
Color(0xFF007AFF),
group: PermissionGroup.optional,
usageScenes: ['天气信息 — 当前城市天气', '节气提醒 — 当地节气推送'],
),
bluetooth(
@@ -79,6 +94,7 @@ enum AppPermission {
CupertinoIcons.bluetooth,
'用于文件传输助手的蓝牙配对和设备发现,仅在您使用文件传输功能时请求。',
Color(0xFF5AC8FA),
group: PermissionGroup.optional,
usageScenes: ['文件传输 — 蓝牙配对', '设备发现 — 附近设备搜索'],
),
nearbyDevices(
@@ -87,6 +103,7 @@ enum AppPermission {
CupertinoIcons.antenna_radiowaves_left_right,
'用于文件传输助手的局域网设备发现和连接,仅在您使用文件传输功能时请求。',
Color(0xFF64D2FF),
group: PermissionGroup.optional,
usageScenes: ['文件传输 — 局域网发现', '设备连接 — WiFi直连'],
),
microphone(
@@ -95,6 +112,7 @@ enum AppPermission {
CupertinoIcons.mic_fill,
'用于语音朗读句子、语音搜索、AI对话语音输入。仅在您主动使用语音功能时请求不会后台录音。',
Color(0xFFFF3B30),
group: PermissionGroup.optional,
usageScenes: ['语音朗读 — 朗读句子', '语音搜索 — 语音输入关键词', 'AI对话 — 语音输入消息'],
),
storage(
@@ -103,6 +121,7 @@ enum AppPermission {
CupertinoIcons.folder_fill,
'用于保存编辑的卡片、壁纸到本地导出字体文件和数据。Android 12及以下版本需要此权限。',
Color(0xFFFF9500),
group: PermissionGroup.optional,
usageScenes: ['保存卡片 — 导出到本地', '字体管理 — 下载字体文件', '数据导出 — 导出用户数据'],
),
network(
@@ -113,6 +132,7 @@ enum AppPermission {
Color(0xFF007AFF),
isRequired: true,
isVirtual: true,
group: PermissionGroup.system,
usageScenes: ['句子获取 — 加载每日推荐', '数据同步 — 云端同步', '推送通知 — 接收消息'],
),
clipboard(
@@ -122,6 +142,7 @@ enum AppPermission {
'用于复制句子到剪贴板、粘贴文本到编辑器。应用仅在您主动操作时访问剪贴板,不会自动读取。',
Color(0xFFAF52DE),
isVirtual: true,
group: PermissionGroup.system,
usageScenes: ['复制句子 — 一键复制', '编辑器 — 粘贴文本', '搜索 — 粘贴关键词'],
);
@@ -133,6 +154,7 @@ enum AppPermission {
this.color, {
this.isRequired = false,
this.isVirtual = false,
this.group = PermissionGroup.optional,
this.usageScenes = const [],
});
@@ -143,6 +165,7 @@ enum AppPermission {
final Color color;
final bool isRequired;
final bool isVirtual;
final PermissionGroup group;
final List<String> usageScenes;
/// Android 13+ 不需要 storage 权限(由 photos 替代)

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// ============================================================
/// 闲言APP — 桌面小组件数据管理服务
/// 创建时间: 2026-05-15
/// 更新时间: 2026-05-20
/// 更新时间: 2026-05-24
/// 作用: 基于home_widget库管理桌面小组件数据推送与交互
/// 上次更新: 新增拾光角色小组件数据键和更新方法
/// 上次更新: 修复鸿蒙端双重更新+小组件点击导航+后台回调实际路由跳转
/// ============================================================
import 'package:home_widget/home_widget.dart';
@@ -123,12 +123,49 @@ class HomeWidgetService {
final data = await HomeWidget.getWidgetData<String>('clicked_data');
if (data != null && data.isNotEmpty) {
Log.i('HomeWidgetService: 小组件点击数据 — $data');
_navigateFromWidgetData(data);
}
} catch (e) {
Log.e('HomeWidgetService: 获取小组件点击数据失败', e);
}
}
static String? _resolveWidgetRoute(String data) {
final uri = Uri.tryParse(data);
if (uri == null) return null;
final action = uri.host;
return switch (action) {
'open_readlater' => '/home?tab=readlater',
'open_sentence' => '/home',
'open_fortune' => '/daily-fortune',
'open_countdown' => '/countdown',
'open_pomodoro' => '/pomodoro',
'open_solar_term' => '/solar-term',
'open_checkin' => '/signin',
'open_daily_with_character' => '/home',
_ => null,
};
}
static void _navigateFromWidgetData(String data) {
final route = _resolveWidgetRoute(data);
if (route == null) {
Log.w('HomeWidgetService: 无法解析点击数据为路由 — $data');
return;
}
Log.i('HomeWidgetService: 导航到 $route');
_pendingNavigationRoute = route;
}
static String? _pendingNavigationRoute;
static String? consumePendingNavigation() {
final route = _pendingNavigationRoute;
_pendingNavigationRoute = null;
return route;
}
// ============================================================
// 注册交互回调
// ============================================================
@@ -151,34 +188,23 @@ class HomeWidgetService {
Log.i('HomeWidgetService: 后台回调 — ${uri.toString()}');
final action = uri.host;
switch (action) {
case 'open_readlater':
Log.i('HomeWidgetService: 打开稍后读列表');
break;
case 'open_sentence':
final id = uri.queryParameters['id'];
Log.i('HomeWidgetService: 打开句子详情 id=$id');
break;
case 'open_fortune':
Log.i('HomeWidgetService: 打开运势详情');
break;
case 'open_countdown':
Log.i('HomeWidgetService: 打开倒计时设置');
break;
case 'open_pomodoro':
Log.i('HomeWidgetService: 打开番茄钟');
break;
case 'open_solar_term':
Log.i('HomeWidgetService: 打开节气诗词');
break;
case 'open_checkin':
Log.i('HomeWidgetService: 打开签到页面');
break;
case 'open_daily_with_character':
Log.i('HomeWidgetService: 打开拾光每日一句');
break;
default:
Log.i('HomeWidgetService: 未知操作 — $action');
final route = switch (action) {
'open_readlater' => '/home?tab=readlater',
'open_sentence' => '/home',
'open_fortune' => '/daily-fortune',
'open_countdown' => '/countdown',
'open_pomodoro' => '/pomodoro',
'open_solar_term' => '/solar-term',
'open_checkin' => '/signin',
'open_daily_with_character' => '/home',
_ => null,
};
if (route != null) {
Log.i('HomeWidgetService: 后台回调导航到 $route');
_pendingNavigationRoute = route;
} else {
Log.i('HomeWidgetService: 未知操作 — $action');
}
}
@@ -302,11 +328,6 @@ class HomeWidgetService {
}
}
await HomeWidget.updateWidget(
qualifiedAndroidName: type.qualifiedAndroidName,
androidName: type.androidProviderName,
iOSName: type.iosWidgetKind,
).timeout(const Duration(seconds: 3));
if (pu.isOhos) {
try {
const dynamic updateWidget = HomeWidget.updateWidget;
@@ -317,7 +338,20 @@ class HomeWidgetService {
iOSName: type.iosWidgetKind,
ohosName: type.ohosFormName,
).timeout(const Duration(seconds: 3));
} catch (_) {}
} catch (e) {
Log.w('HomeWidgetService: 鸿蒙端 ohosName 刷新失败降级标准API', e);
await HomeWidget.updateWidget(
qualifiedAndroidName: type.qualifiedAndroidName,
androidName: type.androidProviderName,
iOSName: type.iosWidgetKind,
).timeout(const Duration(seconds: 3));
}
} else {
await HomeWidget.updateWidget(
qualifiedAndroidName: type.qualifiedAndroidName,
androidName: type.androidProviderName,
iOSName: type.iosWidgetKind,
).timeout(const Duration(seconds: 3));
}
Log.i('HomeWidgetService: ${type.title} 小部件已刷新');
} catch (e) {

View File

@@ -1,57 +1,165 @@
/// ============================================================
/// 闲言APP — 应用锁服务
/// 创建时间: 2026-05-07
/// 更新时间: 2026-05-14
/// 作用: 生物识别解锁支持面容ID/指纹,使用 local_auth
/// 上次更新: 修复onAppResumed中authenticate()未捕获异常导致崩溃的问题
/// 更新时间: 2026-05-24
/// 作用: 应用锁核心服务 — 支持九宫格手势/数字密码/指纹面容三种解锁方式
/// 上次更新: 修复生物识别失败不增加失败计数的安全隐患
/// ============================================================
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:local_auth/local_auth.dart';
import '../../storage/kv_storage.dart';
import '../../storage/secure_storage.dart';
import '../../utils/logger.dart';
enum AppLockMethod {
none('none', '未启用'),
pattern('pattern', '手势密码'),
pin('pin', '数字密码'),
biometric('biometric', '生物识别');
const AppLockMethod(this.id, this.label);
final String id;
final String label;
static AppLockMethod fromId(String id) =>
AppLockMethod.values.firstWhere((m) => m.id == id, orElse: () => AppLockMethod.none);
}
class AppLockService {
AppLockService._();
static final LocalAuthentication _localAuth = LocalAuthentication();
static const _keyEnabled = 'general_app_lock';
static const _keyMethod = 'app_lock_method';
static const _keyPattern = 'app_lock_pattern';
static const _keyPin = 'app_lock_pin';
static const _keyFailedAttempts = 'app_lock_failed_attempts';
static const _keyLockoutUntil = 'app_lock_lockout_until';
static bool _isLocked = false;
static bool _isAuthenticating = false;
static bool? _deviceSupported;
static int _failedAttempts = 0;
static DateTime? _lockoutUntil;
static final _lockNotifier = ValueNotifier<bool>(false);
static ValueNotifier<bool> get lockNotifier => _lockNotifier;
static bool get isEnabled => KvStorage.getBool(_keyEnabled) ?? false;
static bool get isLocked => _isLocked;
static bool get isAuthenticating => _isAuthenticating;
static AppLockMethod get method => AppLockMethod.fromId(
KvStorage.getString(_keyMethod) ?? 'none',
);
static int get failedAttempts => _failedAttempts;
static bool get isLockedOut {
if (_lockoutUntil == null) return false;
if (DateTime.now().isAfter(_lockoutUntil!)) {
_lockoutUntil = null;
_failedAttempts = 0;
return false;
}
return true;
}
static Duration? get lockoutRemaining {
if (_lockoutUntil == null) return null;
final diff = _lockoutUntil!.difference(DateTime.now());
return diff.isNegative ? null : diff;
}
static void setEnabled(bool v) {
KvStorage.setBool(_keyEnabled, v);
if (!v) {
_isLocked = false;
_failedAttempts = 0;
_lockoutUntil = null;
_lockNotifier.value = false;
}
}
static void lock() {
if (!isEnabled) return;
_isLocked = true;
Log.i('应用已锁定');
static void setMethod(AppLockMethod m) {
KvStorage.setString(_keyMethod, m.id);
}
static void unlock() {
_isLocked = false;
Log.i('应用已解锁');
static Future<void> setPattern(String pattern) async {
await SecureStorage.write(_keyPattern, pattern);
setMethod(AppLockMethod.pattern);
Log.i('应用锁: 手势密码已设置');
}
static Future<void> setPin(String pin) async {
await SecureStorage.write(_keyPin, pin);
setMethod(AppLockMethod.pin);
Log.i('应用锁: 数字密码已设置');
}
static Future<String?> getStoredPattern() async {
return await SecureStorage.read(_keyPattern);
}
static Future<String?> getStoredPin() async {
return await SecureStorage.read(_keyPin);
}
static Future<bool> hasAnyPasswordSet() async {
final pattern = await SecureStorage.read(_keyPattern);
final pin = await SecureStorage.read(_keyPin);
return (pattern != null && pattern.isNotEmpty) ||
(pin != null && pin.isNotEmpty);
}
static Future<void> clearAllPasswords() async {
await SecureStorage.delete(_keyPattern);
await SecureStorage.delete(_keyPin);
setMethod(AppLockMethod.none);
setEnabled(false);
Log.i('应用锁: 所有密码已清除');
}
static Future<bool> verifyPattern(String pattern) async {
final stored = await SecureStorage.read(_keyPattern);
if (stored == null) return false;
final success = pattern == stored;
_handleAuthResult(success);
return success;
}
static Future<bool> verifyPin(String pin) async {
final stored = await SecureStorage.read(_keyPin);
if (stored == null) return false;
final success = pin == stored;
_handleAuthResult(success);
return success;
}
static void _handleAuthResult(bool success) {
if (success) {
_failedAttempts = 0;
_lockoutUntil = null;
unlock();
} else {
_failedAttempts++;
if (_failedAttempts >= 5) {
_lockoutUntil = DateTime.now().add(const Duration(minutes: 5));
Log.w('应用锁: 连续失败$_failedAttempts次锁定5分钟');
}
}
saveLockoutState();
}
static Future<bool> isDeviceSupported() async {
if (_deviceSupported != null) return _deviceSupported!;
try {
final isSupported = await _localAuth.isDeviceSupported();
_deviceSupported = isSupported;
return isSupported;
return await _localAuth.isDeviceSupported();
} on PlatformException catch (e) {
Log.e('设备支持检测失败: ${e.message}');
_deviceSupported = false;
return false;
}
}
@@ -74,18 +182,17 @@ class AppLockService {
}
}
static const _authTimeout = Duration(seconds: 10);
static Future<bool> authenticate({String reason = '请验证身份以解锁闲言'}) async {
static Future<bool> authenticateBiometric({
String reason = '请验证身份以解锁闲言',
}) async {
if (_isAuthenticating) return false;
_isAuthenticating = true;
try {
final isSupported = await isDeviceSupported();
if (!isSupported) {
Log.w('设备不支持生物识别,自动解锁');
unlock();
return true;
Log.w('设备不支持生物识别');
return false;
}
bool didAuthenticate;
@@ -96,47 +203,75 @@ class AppLockService {
persistAcrossBackgrounding: true,
)
.timeout(
_authTimeout,
onTimeout: () {
Log.w('生物识别超时(${_authTimeout.inSeconds}s),自动解锁');
return false;
},
const Duration(seconds: 10),
onTimeout: () => false,
);
} on PlatformException catch (e) {
Log.e('生物识别异常: ${e.code} - ${e.message},自动解锁');
unlock();
return true;
Log.e('生物识别异常: ${e.code} - ${e.message}');
// 用户取消或设备锁定不增加失败计数
final isUserCanceled = e.code == 'userCanceled' ||
e.code == 'cancelled';
final isLockedOut = e.code == 'lockedOut' ||
e.code == 'lockout';
if (!isUserCanceled && !isLockedOut) {
_handleAuthResult(false);
}
return false;
}
if (didAuthenticate) {
unlock();
_handleAuthResult(true);
return true;
}
// 生物识别返回false认证失败非取消增加失败计数
_handleAuthResult(false);
return false;
} on PlatformException catch (e) {
Log.e('生物识别失败: ${e.code} - ${e.message},自动解锁');
unlock();
return true;
} finally {
_isAuthenticating = false;
}
}
static void lock() {
if (!isEnabled) return;
_isLocked = true;
_lockNotifier.value = true;
Log.i('应用已锁定');
}
static void unlock() {
_isLocked = false;
_lockNotifier.value = false;
Log.i('应用已解锁');
}
static void onAppPaused() {
if (isEnabled) {
lock();
}
}
static void onAppResumed() {
if (_isLocked && isEnabled) {
Future.microtask(() async {
try {
await authenticate();
} catch (e) {
Log.e('应用锁认证失败', e);
}
});
static void onAppResumed() {}
static Future<void> loadLockoutState() async {
final failed = KvStorage.getInt(_keyFailedAttempts) ?? 0;
final lockoutMs = KvStorage.getInt(_keyLockoutUntil) ?? 0;
_failedAttempts = failed;
if (lockoutMs > 0) {
_lockoutUntil = DateTime.fromMillisecondsSinceEpoch(lockoutMs);
if (DateTime.now().isAfter(_lockoutUntil!)) {
_lockoutUntil = null;
_failedAttempts = 0;
}
}
}
static Future<void> saveLockoutState() async {
KvStorage.setInt(_keyFailedAttempts, _failedAttempts);
if (_lockoutUntil != null) {
KvStorage.setInt(
_keyLockoutUntil, _lockoutUntil!.millisecondsSinceEpoch);
} else {
KvStorage.setInt(_keyLockoutUntil, 0);
}
}
}

View File

@@ -113,23 +113,23 @@ class LocalNotificationService {
switch (payload) {
case 'daily_sentence':
context.appGo('/home');
context.appGo(AppRoutes.home);
case 'signin_reminder':
context.appGo('/signin');
context.appGo(AppRoutes.signin);
case 'solar_term':
context.appGo('/solar-term');
context.appGo(AppRoutes.solarTerm);
case 'pomodoro_break':
context.appGo('/pomodoro');
context.appGo(AppRoutes.pomodoro);
case 'countdown':
context.appGo('/countdown');
context.appGo(AppRoutes.countdown);
case 'daily_fortune':
context.appGo('/daily-fortune');
context.appGo(AppRoutes.dailyFortune);
case 'study_progress':
context.appGo('/home');
context.appGo(AppRoutes.home);
case 'readlater':
context.appGo('/readlater-chat');
context.appGo(AppRoutes.readlaterChat);
default:
context.appGo('/home');
context.appGo(AppRoutes.home);
}
}

View File

@@ -1,9 +1,9 @@
// ============================================================
// 闲言APP — 特效互斥调度器
// 创建时间: 2026-05-22
// 更新时间: 2026-05-22
// 更新时间: 2026-05-24
// 作用: 限制同一时刻运行的重量级特效数量防止GPU过载
// 上次更新: 初始创建
// 上次更新: 添加acquire()超时机制,防止永久阻塞
// ============================================================
import 'dart:async';
@@ -51,9 +51,11 @@ class EffectMutex {
///
/// 如果当前活跃特效数未达上限,立即返回 token
/// 否则等待直到有槽位释放。
/// [timeout] 不为 null 时,超时后从等待队列移除并抛出 TimeoutException。
Future<EffectToken> acquire(
String name, {
EffectPriority priority = EffectPriority.decoration,
Duration? timeout,
}) async {
if (_active.length < maxConcurrent) {
final token = EffectToken._(this, name);
@@ -65,10 +67,26 @@ class EffectMutex {
}
final completer = Completer<EffectToken>();
_pending.add(_PendingEffect(name, priority, completer));
final pending = _PendingEffect(name, priority, completer);
_pending.add(pending);
Log.i(
'EffectMutex: "$name" 排队等待 (优先级=${priority.name}, 队列=${_pending.length})',
);
if (timeout != null) {
pending._timer = Timer(timeout, () {
if (!completer.isCompleted) {
_pending.remove(pending);
completer.completeError(
TimeoutException(
'EffectMutex: "$name" 等待超时 (${timeout.inMilliseconds}ms)',
),
);
Log.i('EffectMutex: "$name" 等待超时');
}
});
}
return completer.future;
}
@@ -81,6 +99,7 @@ class EffectMutex {
if (_pending.isNotEmpty) {
_pending.sort((a, b) => a.priority.value.compareTo(b.priority.value));
final next = _pending.removeAt(0);
next._timer?.cancel();
final newToken = EffectToken._(this, next.name);
_active.add(newToken);
next.completer.complete(newToken);
@@ -96,4 +115,5 @@ class _PendingEffect {
final String name;
final EffectPriority priority;
final Completer<EffectToken> completer;
Timer? _timer;
}