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:
@@ -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 替代)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user