chore: 汇总2026-05-30全量更新
### 详细变更:
1. **文档与配置**:更新AGENTS.md添加命令超时约束,升级Rive依赖至0.14.7并替换平台插件引用
2. **UI优化**:重构AppInfo页面布局、移除图表冗余配置、锁定部分系统设置项
3. **功能增强**:
- 新增工具面板拖拽状态管理与介绍弹窗
- 新增进度页面编辑/重排/清空用户进度功能
- 新增摇一摇路由作用域拦截逻辑
4. **体验优化**:
- 统一外部链接跳转弹窗,添加文件打开确认逻辑
- 修复设备卡片IP溢出、Android权限声明问题
- 后台任务初始化增加协议校验
5. **代码重构**:拆分工具面板配置、拖拽逻辑与动画参数,优化状态管理代码
6. **工具脚本**:新增协议文件上传脚本
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 权限管理服务
|
||||
/// 创建时间: 2026-04-23
|
||||
/// 更新时间: 2026-05-26
|
||||
/// 更新时间: 2026-05-30
|
||||
/// 作用: 统一管理应用权限请求,支持相机/相册/通知/位置/蓝牙/附近设备/麦克风/存储/网络/剪贴板/分享权限
|
||||
/// 上次更新: 新增权限使用统计功能(recordUsage/getUsageStats)
|
||||
/// 上次更新: 更新存储权限描述(区分READ/WRITE的API级别必要性)
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:io';
|
||||
@@ -12,6 +12,7 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
import '../../storage/kv_storage.dart';
|
||||
import '../device/shake_detector.dart';
|
||||
|
||||
/// 权限状态枚举
|
||||
enum AppPermissionStatus {
|
||||
@@ -117,9 +118,14 @@ enum AppPermission {
|
||||
'存储空间',
|
||||
Permission.storage,
|
||||
CupertinoIcons.folder_fill,
|
||||
'用于保存编辑的卡片、壁纸到本地,导出字体文件和数据。Android 12及以下版本需要此权限。',
|
||||
'用于保存编辑的卡片、壁纸到本地,导出字体文件和数据。仅Android 9及以下(API≤29)需要写入权限,Android 10+使用分区存储;Android 12及以下(API≤32)需要读取权限,Android 13+由相册权限替代。',
|
||||
Color(0xFFFF9500),
|
||||
usageScenes: ['保存卡片 — 导出到本地', '字体管理 — 下载字体文件', '数据导出 — 导出用户数据'],
|
||||
usageScenes: [
|
||||
'保存卡片 — 导出到本地',
|
||||
'壁纸设置 — 保存壁纸',
|
||||
'字体管理 — 下载字体文件',
|
||||
'数据导出 — 导出用户数据',
|
||||
],
|
||||
),
|
||||
network(
|
||||
'网络连接',
|
||||
@@ -151,6 +157,15 @@ enum AppPermission {
|
||||
isVirtual: true,
|
||||
group: PermissionGroup.system,
|
||||
usageScenes: ['句子分享 — 分享到微信/QQ', '卡片分享 — 分享到社交平台', '日志导出 — 分享日志文件'],
|
||||
),
|
||||
shake(
|
||||
'摇一摇',
|
||||
Permission.notification,
|
||||
CupertinoIcons.arrow_counterclockwise,
|
||||
'摇晃手机触发特定功能,如换句、刷新等',
|
||||
Color(0xFF5856D6),
|
||||
isVirtual: true,
|
||||
usageScenes: ['切换每日推荐句子', '刷新内容', '互动彩蛋'],
|
||||
);
|
||||
|
||||
const AppPermission(
|
||||
@@ -204,6 +219,24 @@ class PermissionService {
|
||||
|
||||
static const _usageStatsKey = 'permission_usage_stats';
|
||||
|
||||
static const _shakeEnabledKey = 'shake_enabled';
|
||||
|
||||
static bool get isShakeEnabled =>
|
||||
KvStorage.getBool(_shakeEnabledKey, box: HiveBoxNames.userPrefs) ?? true;
|
||||
|
||||
static Future<void> setShakeEnabled(bool enabled) async {
|
||||
await KvStorage.setBool(
|
||||
_shakeEnabledKey,
|
||||
enabled,
|
||||
box: HiveBoxNames.userPrefs,
|
||||
);
|
||||
if (!enabled) {
|
||||
try {
|
||||
ShakeDetector.instance.stop();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
/// 记录权限使用
|
||||
static void recordUsage(AppPermission permission) {
|
||||
try {
|
||||
@@ -283,7 +316,9 @@ class PermissionService {
|
||||
final buffer = StringBuffer();
|
||||
stats.forEach((key, value) {
|
||||
if (buffer.isNotEmpty) buffer.write(';');
|
||||
buffer.write('$key=${value['count']},${value['lastUsed']},${value['firstUsed'] ?? ''}');
|
||||
buffer.write(
|
||||
'$key=${value['count']},${value['lastUsed']},${value['firstUsed'] ?? ''}',
|
||||
);
|
||||
});
|
||||
return buffer.toString();
|
||||
}
|
||||
@@ -332,6 +367,10 @@ class PermissionService {
|
||||
return AppPermissionStatus.granted;
|
||||
case AppPermission.share:
|
||||
return AppPermissionStatus.granted;
|
||||
case AppPermission.shake:
|
||||
return isShakeEnabled
|
||||
? AppPermissionStatus.granted
|
||||
: AppPermissionStatus.denied;
|
||||
default:
|
||||
return AppPermissionStatus.granted;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 后台任务服务
|
||||
/// 创建时间: 2026-05-25
|
||||
/// 更新时间: 2026-05-25
|
||||
/// 更新时间: 2026-05-30
|
||||
/// 作用: 基于workmanager实现后台定时任务
|
||||
/// 上次更新: 修复OHOS端原生通道未注册导致PlatformException
|
||||
/// 上次更新: 增加用户协议同意检查,未同意则跳过后台任务初始化
|
||||
/// ============================================================
|
||||
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
|
||||
import '../../storage/kv_storage.dart';
|
||||
import '../../utils/logger.dart';
|
||||
import '../../utils/platform/platform_utils.dart' as pu;
|
||||
import 'background_callback.dart';
|
||||
@@ -27,6 +28,11 @@ class BackgroundTaskService {
|
||||
Future<void> init() async {
|
||||
if (_initialized) return;
|
||||
|
||||
if (!KvStorage.isOnboardingCompleted) {
|
||||
Log.i('BackgroundTaskService: 用户未同意协议,跳过初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
if (pu.isWeb) {
|
||||
Log.i('BackgroundTaskService: Web平台跳过后台任务初始化');
|
||||
_initialized = true;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// ============================================================
|
||||
// 闲言APP — 摇一摇检测器
|
||||
// 创建时间: 2026-05-20
|
||||
// 更新时间: 2026-05-29
|
||||
// 更新时间: 2026-05-30
|
||||
// 作用: 监听加速度传感器,检测摇一摇手势
|
||||
// 上次更新: 改用处理器栈模式,仅当前可见页面的回调生效
|
||||
// 上次更新: 新增路由作用域检查,仅当前路由在home分支时才响应摇一摇
|
||||
//
|
||||
// 摇一摇生命周期管理(处理器栈模式):
|
||||
//
|
||||
@@ -15,6 +15,12 @@
|
||||
// - 页面 deactivate 时: ShakeDetector.instance.popHandler('/route')
|
||||
// - 仅栈顶 handler 生效,确保只有当前可见页面响应摇一摇
|
||||
//
|
||||
// 路由作用域:
|
||||
// - setScope(scope) 由路由观察者调用,标识当前所在路由分支
|
||||
// - 仅当 _activeScope 匹配栈顶 handler 路由前缀时才触发回调
|
||||
// - 例如: _activeScope='/home' 且栈顶 handler route='/home' → 允许触发
|
||||
// - 例如: _activeScope='/settings' 且栈顶 handler route='/home' → 禁止触发
|
||||
//
|
||||
// 错误用法(已废弃):
|
||||
// - setHomePageActive(true/false) — 在 IndexedStack 中永远为 true
|
||||
// - 在 dispose 中 stop() — 永远不会被调用
|
||||
@@ -48,9 +54,24 @@ class ShakeDetector {
|
||||
|
||||
final List<_ShakeHandlerEntry> _handlerStack = [];
|
||||
|
||||
String? _activeScope;
|
||||
|
||||
bool get isEnabled => _isEnabled;
|
||||
bool get hasActiveHandler => _handlerStack.isNotEmpty;
|
||||
|
||||
void setScope(String? scope) {
|
||||
_activeScope = scope;
|
||||
Log.i('ShakeDetector: setScope scope=$scope');
|
||||
}
|
||||
|
||||
bool _isScopeAllowed() {
|
||||
if (_activeScope == null) return true;
|
||||
if (_activeScope!.isEmpty) return false;
|
||||
if (_handlerStack.isEmpty) return false;
|
||||
final topRoute = _handlerStack.last.route;
|
||||
return topRoute == _activeScope || topRoute.startsWith('$_activeScope/');
|
||||
}
|
||||
|
||||
void pushHandler(String route, ShakeCallback callback) {
|
||||
_handlerStack.removeWhere((e) => e.route == route);
|
||||
_handlerStack.add(_ShakeHandlerEntry(route, callback));
|
||||
@@ -90,6 +111,12 @@ class ShakeDetector {
|
||||
now.difference(_lastShakeTime!) > _minInterval) {
|
||||
_lastShakeTime = now;
|
||||
_consecutiveCount = 0;
|
||||
if (!_isScopeAllowed()) {
|
||||
Log.i(
|
||||
'ShakeDetector: 摇一摇被作用域拦截 (scope=$_activeScope, topRoute=${_handlerStack.last.route})',
|
||||
);
|
||||
return;
|
||||
}
|
||||
Log.i('ShakeDetector: 检测到摇一摇 (acceleration=$acceleration)');
|
||||
_handlerStack.last.callback.call();
|
||||
HapticService.medium();
|
||||
|
||||
132
lib/core/services/post_agreement_initializer.dart
Normal file
132
lib/core/services/post_agreement_initializer.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
// ============================================================
|
||||
// 闲言APP — 协议同意后初始化器
|
||||
// 创建时间: 2026-05-30
|
||||
// 更新时间: 2026-05-30
|
||||
// 作用: 将权限敏感的服务初始化延迟到用户同意协议后执行
|
||||
// 上次更新: 首次创建,从main.dart拆分权限敏感初始化
|
||||
// ============================================================
|
||||
|
||||
import '../storage/kv_storage.dart';
|
||||
import '../utils/logger.dart';
|
||||
import '../utils/platform/platform_utils.dart' as pu;
|
||||
import '../router/app_router.dart' show rootNavigatorKey;
|
||||
import 'network/connectivity_service.dart';
|
||||
import 'clipboard_monitor_service.dart';
|
||||
import 'background/background_task_service.dart';
|
||||
import 'notification/local_notification_service.dart';
|
||||
import 'device/screen_wake_service.dart';
|
||||
import 'device/battery_optimization_service.dart';
|
||||
import 'notification/readlater_reminder_service.dart';
|
||||
import 'data/home_widget_service.dart';
|
||||
import 'readlater/sharing_receiver_service.dart';
|
||||
import '../../features/discover/services/chat_migration_service.dart';
|
||||
|
||||
class PostAgreementInitializer {
|
||||
PostAgreementInitializer._();
|
||||
|
||||
static bool _initialized = false;
|
||||
|
||||
static bool get isInitialized => _initialized;
|
||||
|
||||
static Future<void> init() async {
|
||||
if (_initialized) return;
|
||||
|
||||
Log.i('PostAgreementInitializer: 开始初始化权限敏感服务...');
|
||||
|
||||
if (!pu.isWeb) {
|
||||
try {
|
||||
SharingReceiverService().init();
|
||||
SharingReceiverService().setNavigatorKey(rootNavigatorKey);
|
||||
Log.i('分享接收服务初始化完成');
|
||||
} catch (e, st) {
|
||||
Log.e('分享接收服务初始化失败', e, st);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pu.isWeb) {
|
||||
try {
|
||||
await LocalNotificationService.init();
|
||||
Log.i('本地通知服务初始化完成');
|
||||
} catch (e, st) {
|
||||
Log.e('本地通知服务初始化失败', e, st);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pu.isWeb) {
|
||||
try {
|
||||
await ScreenWakeService.init();
|
||||
Log.i('屏幕常亮服务初始化完成');
|
||||
} catch (e, st) {
|
||||
Log.e('屏幕常亮服务初始化失败', e, st);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pu.isWeb) {
|
||||
try {
|
||||
await BatteryOptimizationService.init();
|
||||
Log.i('电池优化服务初始化完成');
|
||||
} catch (e, st) {
|
||||
Log.e('电池优化服务初始化失败', e, st);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pu.isWeb) {
|
||||
try {
|
||||
await ReadlaterReminderService.startMonitoring();
|
||||
Log.i('稍后读提醒服务初始化完成');
|
||||
} catch (e, st) {
|
||||
Log.e('稍后读提醒服务初始化失败', e, st);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await ChatMigrationService.migrateIfNeeded();
|
||||
Log.i('聊天数据迁移检查完成');
|
||||
} catch (e, st) {
|
||||
Log.e('聊天数据迁移检查失败', e, st);
|
||||
}
|
||||
|
||||
if (!pu.isWeb) {
|
||||
try {
|
||||
await HomeWidgetService.instance.init();
|
||||
Log.i('桌面小组件服务初始化完成');
|
||||
} catch (e, st) {
|
||||
Log.e('桌面小组件服务初始化失败', e, st);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pu.isWeb) {
|
||||
try {
|
||||
await ClipboardMonitorService.instance.initFromStore();
|
||||
Log.i('剪贴板监控服务初始化完成');
|
||||
} catch (e, st) {
|
||||
Log.e('剪贴板监控服务初始化失败', e, st);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pu.isWeb) {
|
||||
try {
|
||||
await ConnectivityService.init();
|
||||
Log.i('网络状态检测服务初始化完成');
|
||||
} catch (e, st) {
|
||||
Log.e('网络状态检测服务初始化失败', e, st);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pu.isWeb) {
|
||||
try {
|
||||
await BackgroundTaskService.instance.init();
|
||||
Log.i('后台任务服务初始化完成');
|
||||
} catch (e, st) {
|
||||
Log.e('后台任务服务初始化失败', e, st);
|
||||
}
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
Log.i('PostAgreementInitializer: 所有权限敏感服务初始化完成 ✓');
|
||||
}
|
||||
|
||||
static bool shouldInit() {
|
||||
return KvStorage.isOnboardingCompleted && !_initialized;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
//// 闲言APP 统一分享接收服务
|
||||
// 创建时间: 2026-05-15
|
||||
/// 更新时间: 2026-05-26
|
||||
/// 更新时间: 2026-05-30
|
||||
/// 作用: 接收其他App通过系统分享面板发送的内容,写入稍后读会话
|
||||
/// 上次更新: 使用SafeSharingReceiver中间件,防止SharedMediaFile空指针崩溃
|
||||
/// 上次更新: 添加确认弹窗,修复图片视频空白问题
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
import 'package:xianyan/core/utils/data/pattern_utils.dart';
|
||||
import 'package:xianyan/core/utils/platform/platform_utils.dart' as pu;
|
||||
import 'package:xianyan/core/router/app_nav_extension.dart';
|
||||
|
||||
import '../../../core/utils/logger.dart';
|
||||
import '../../../core/router/app_router.dart';
|
||||
@@ -170,8 +169,7 @@ class SharingReceiverService {
|
||||
sourceApp: '分享',
|
||||
);
|
||||
Log.i('分享链接已写入稍后读: $text');
|
||||
AppToast.showSuccess('🔗 链接已保存到稍后读');
|
||||
_navigateToReadlater();
|
||||
_showConfirmDialog('🔗 链接已保存到稍后读');
|
||||
} else {
|
||||
await ChatMessageService.sendText(
|
||||
conversationId: _readLaterConvId,
|
||||
@@ -181,8 +179,7 @@ class SharingReceiverService {
|
||||
Log.i(
|
||||
'分享文本已写入稍后读: ${text.length > 50 ? '${text.substring(0, 50)}...' : text}',
|
||||
);
|
||||
AppToast.showSuccess('📝 文本已保存到稍后读');
|
||||
_navigateToReadlater();
|
||||
_showConfirmDialog('📝 文本已保存到稍后读');
|
||||
}
|
||||
} catch (e) {
|
||||
Log.e('分享文本处理失败', e);
|
||||
@@ -211,8 +208,7 @@ class SharingReceiverService {
|
||||
},
|
||||
);
|
||||
Log.i('分享图片已写入稍后读: $fileName');
|
||||
AppToast.showSuccess('🖼️ 图片已保存到稍后读');
|
||||
_navigateToReadlater();
|
||||
_showConfirmDialog('🖼️ 图片已保存到稍后读');
|
||||
} else if (mimeType.startsWith('video/')) {
|
||||
await ChatMessageService.sendVideo(
|
||||
conversationId: _readLaterConvId,
|
||||
@@ -225,8 +221,7 @@ class SharingReceiverService {
|
||||
},
|
||||
);
|
||||
Log.i('分享视频已写入稍后读: $fileName');
|
||||
AppToast.showSuccess('🎬 视频已保存到稍后读');
|
||||
_navigateToReadlater();
|
||||
_showConfirmDialog('🎬 视频已保存到稍后读');
|
||||
} else if (mimeType.startsWith('application/')) {
|
||||
await ChatMessageService.sendDocument(
|
||||
conversationId: _readLaterConvId,
|
||||
@@ -237,8 +232,7 @@ class SharingReceiverService {
|
||||
meta: {'mimeType': mimeType},
|
||||
);
|
||||
Log.i('分享文档已写入稍后读: $fileName');
|
||||
AppToast.showSuccess('📄 文档已保存到稍后读');
|
||||
_navigateToReadlater();
|
||||
_showConfirmDialog('📄 文档已保存到稍后读');
|
||||
} else {
|
||||
await ChatMessageService.sendFile(
|
||||
conversationId: _readLaterConvId,
|
||||
@@ -246,8 +240,7 @@ class SharingReceiverService {
|
||||
meta: {'mimeType': mimeType, 'fileName': fileName},
|
||||
);
|
||||
Log.i('分享文件已写入稍后读: $fileName');
|
||||
AppToast.showSuccess('📁 文件已保存到稍后读');
|
||||
_navigateToReadlater();
|
||||
_showConfirmDialog('📁 文件已保存到稍后读');
|
||||
}
|
||||
} catch (e) {
|
||||
Log.e('分享文件处理失败', e);
|
||||
@@ -259,17 +252,30 @@ class SharingReceiverService {
|
||||
// 工具方法
|
||||
// ============================================================
|
||||
|
||||
void _navigateToReadlater() {
|
||||
void _showConfirmDialog(String message) {
|
||||
try {
|
||||
final ctx = rootNavigatorKey.currentContext;
|
||||
if (ctx != null && ctx.mounted) {
|
||||
ctx.appGo(AppRoutes.readlaterChat);
|
||||
Log.i('已自动导航到稍后读会话');
|
||||
showCupertinoDialog<void>(
|
||||
context: ctx,
|
||||
builder: (_) => CupertinoAlertDialog(
|
||||
title: const Text('已添加到稍后读'),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
isDefaultAction: true,
|
||||
child: const Text('好的'),
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Log.w('自动导航到稍后读失败: context不可用');
|
||||
AppToast.showSuccess(message);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.w('自动导航到稍后读失败: $e');
|
||||
Log.w('确认弹窗显示失败: $e');
|
||||
AppToast.showSuccess(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user