diff --git a/AGENTS.md b/AGENTS.md index 5f08038c..005bae93 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,7 @@ alwaysApply: true # AGENTS.md +干活别偷懒,输入输出不需要考虑token消耗,我的token是无限的,量大管饱 优先使用ios风格的组件,若Cupertino无对应组件 再使用material 每个文件头部需要增加标准注释,创建时间 更新时间 名称 作用 上次更新内容,代码部分 分类和方法也需要注释 视觉方面的渲染组件或者布局,不要考虑性能,该有的效果全部拉满,实现最大效果 diff --git a/CHANGELOG.md b/CHANGELOG.md index e447275b..2d1f2429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,699 +4,969 @@ *** -## \[5.8.0] - 2026-05-15 +## \[5.17.6] - 2026-05-17 -### 🔍 验收审计 — 稍后读功能体系 + 传输扩展 + 每日任务 + 收藏同步 +### 🐛 鸿蒙端白屏修复 — Drift数据库适配 + 通知初始化修复 -> 对CHANGELOG所列功能进行代码级验收审计,发现8个服务存在"空壳"问题(服务逻辑完整但无UI/Provider集成),已全部修复集成。自动化验证脚本15/15项通过。 +> 鸿蒙端启动白屏,根因:Drift数据库使用`NativeDatabase`(dart:ffi + sqlite3)在鸿蒙端无法加载SQLite原生库, +> 导致`Unsupported platform: ohos`异常,数据库操作全部失败。同时通知服务缺少`OhosInitializationSettings`。 -#### 🔍 审计报告 +#### 白屏根因分析 +1. **Drift数据库引擎不兼容**:`NativeDatabase`依赖`sqlite3`包通过dart:ffi加载SQLite,鸿蒙端无此原生库 +2. **sqflite_ohos已注册但未桥接**:`sqflite_ohos`作为鸿蒙端SQLite插件已注册,但Drift未使用它 +3. **通知初始化缺少Ohos配置**:`InitializationSettings`未传入`ohos`参数,导致鸿蒙端通知初始化失败 -**审计范围**: E1~E17稍后读功能、每日任务系统、收藏同步重构、USB OTG传输、剪贴板同步管理、协作画布、云端暂存 +#### 修复方案 +- **SqfliteDelegate适配器**:创建`SqfliteDelegate`类,将`sqflite`的API桥接为Drift的`DatabaseDelegate` + - 实现`DatabaseDelegate`抽象方法(runSelect/runInsert/runUpdate/runCustom/runBatched) + - 实现`DynamicVersionDelegate`(通过PRAGMA user_version管理schema版本) + - 实现`SupportedTransactionDelegate`(利用sqflite的transaction API) + - 事务内使用`_SqfliteTransactionQueryDelegate`代理QueryDelegate +- **运行时平台检测**:在`native.dart`中检测`Platform.operatingSystem == 'ohos'`,自动切换到sqflite后端 +- **通知服务修复**:三个通知服务文件均增加`OhosInitializationSettings` -**审计方法**: 逐文件检查服务→Provider→UI的引用链,验证功能可达性 +#### 修复文件清单 -| 功能 | 服务文件 | Provider集成 | UI集成 | 审计结果 | -|------|---------|-------------|--------|---------| -| E1 OG元数据 | og_metadata_service.dart | ✅ chat_link_bubble | ✅ | 🟢 完整 | -| E2 文档预览 | document_preview_page.dart | ✅ | ✅ | 🟢 完整 | -| E4 视频压缩 | chat_video_bubble.dart | ✅ | ✅ | 🟢 完整 | -| E5 多格式导出 | chat_flow_readlater_mixin.dart | ✅ | ✅ | 🟢 完整 | -| E6 稍后读提醒 | readlater_reminder_service.dart | ✅ main.dart+通知设置 | ✅ | 🟢 完整 | -| E7 阅读统计 | readlater_stats_page.dart | ✅ | ✅ | 🟢 完整 | -| E8 全文搜索 | chat_provider.dart | ✅ | ✅ | 🟢 完整 | -| E9 标签管理 | readlater_tag_service.dart | ✅ →已修复 | ✅ →已修复 | 🟢→已修复 | -| E10 剪贴板监控 | clipboard_monitor_service.dart | ✅ →已修复 | ✅ →已修复 | 🟢→已修复 | -| E11 离线同步 | readlater_sync_service.dart | ✅ →已修复 | ✅ →已修复 | 🟢→已修复 | -| E12 句子卡片 | chat_sentence_card_bubble.dart | ✅ | ✅ | 🟢 完整 | -| E13 桌面小组件 | home_widget_service.dart | ✅ →已修复 | ✅ →已修复 | 🟢→已修复 | -| E14 文件夹管理 | readlater_folder_service.dart | ✅ →已修复 | ✅ →已修复 | 🟢→已修复 | -| E15 AI摘要 | readlater_ai_service.dart | ✅ →已修复 | ✅ →已修复 | 🟢→已修复 | -| E16 跨设备同步 | readlater_device_sync_service.dart | ✅ →已修复 | ✅ →已修复 | 🟢→已修复 | -| E17 稍后读协作 | readlater_collab_service.dart | ✅ →已修复 | ✅ →已修复 | 🟢→已修复 | -| 稍后读会话 | sharing_receiver_service.dart | ✅ main.dart | ✅ | 🟢 完整 | -| 每日任务 | task_service→task_provider | ✅ | ✅ daily_task_page | 🟢→已修复 | -| 收藏同步 | favorite_provider.dart | ✅ | ✅ | 🟢 完整 | -| USB OTG | usb_transport_service.dart | ✅ transfer_notifier | ✅ usb_confirm_dialog | 🟢 完整 | -| 剪贴板同步 | clipboard_manager_service | ✅ clipboard_provider | ✅ clipboard_flow_page | 🟢 完整 | -| 协作画布 | canvas_sync_service | ✅ canvas_provider | ✅ canvas_page | 🟢 完整 | -| 云端暂存 | cloud_cache_service | ✅ transfer_notifier | ✅ | 🟢 完整 | +| 文件 | 修复内容 | +|------|---------| +| `database_connection/sqflite_delegate.dart` | 🆕 新建 — SqfliteDelegate适配器,桥接sqflite→Drift DatabaseDelegate | +| `database_connection/ohos.dart` | 🆕 新建 — OpenHarmony数据库连接,使用sqflite_ohos后端 | +| `database_connection/native.dart` | 运行时检测`Platform.operatingSystem == 'ohos'`,自动切换到sqflite后端 | +| `app_database.dart` | 更新文件头注释 | +| `local_notification_service.dart` | 增加`OhosInitializationSettings`配置 | +| `notification_service.dart` (core) | 增加`OhosInitializationSettings`配置 | +| `notification_service.dart` (file_transfer) | 增加`OhosInitializationSettings`配置 | -**已修复 — 8个空壳服务**: -- ReadlaterTagService: ✅ 稍后读设置面板新增"🏷️ 管理标签"入口+消息长按"添加标签" -- ReadlaterFolderService: ✅ 稍后读设置面板新增"📁 管理文件夹"入口 -- ReadlaterAiService: ✅ 消息长按菜单新增"🤖 AI摘要"+"🏷️ 智能标签" -- ReadlaterCollabService: ✅ 稍后读设置面板新增"👥 共享协作"入口 -- ReadlaterDeviceSyncService: ✅ 稍后读设置面板新增"📱 跨设备同步"入口 -- ReadlaterSyncService: ✅ 稍后读设置面板新增"☁️ 云端同步"入口 -- HomeWidgetService: ✅ 稍后读设置面板新增"🏠 更新桌面小组件"+main.dart初始化 -- ClipboardMonitorService: ✅ 稍后读设置面板新增"📋 剪贴板监控"+main.dart初始化 - -**已修复 — 其他问题**: -- DailyTaskPage/TaskCard: ✅ 替换硬编码CupertinoColors为AppTheme.ext(context)设计令牌 -- ReadlaterReminderService: ✅ SharedPreferences替换为AppKVStore统一存储 -- exportAsZip: ✅ 同步I/O改为异步(await writeAsString/readAsBytes) -- ChatMessage标签同步: ✅ ReadlaterTagService.addTag/removeTag/setTagsForMessage自动同步到ext['tags'] -- ChatMessageService: ✅ 新增updateExt()方法支持ext字段更新 - -#### 🔧 修复清单 - -1. **ReadlaterTagService UI集成** — chat_flow_readlater_mixin.dart - - showSettings新增"🏷️ 管理标签"操作 - - 新增showTagManager弹窗: 标签列表/添加/删除/按标签筛选 - - 消息长按菜单新增"添加标签"选项 - - addTag/removeTag/setTagsForMessage自动同步ChatMessage.ext['tags'] - -2. **ReadlaterFolderService UI集成** — chat_flow_readlater_mixin.dart - - showSettings新增"📁 管理文件夹"操作 - - 新增showFolderManager弹窗: 文件夹列表/创建/重命名/删除/消息归档 - -3. **ReadlaterSyncService UI集成** — chat_flow_readlater_mixin.dart - - showSettings新增"☁️ 云端同步"操作 - - 同步进度提示+结果反馈 - -4. **ReadlaterAiService UI集成** — chat_flow_readlater_mixin.dart - - "🤖 AI摘要" — 生成单条消息摘要 - - "🏷️ 智能标签" — AI推荐标签 - -5. **HomeWidgetService UI集成** — chat_flow_readlater_mixin.dart + main.dart - - 稍后读设置面板新增"🏠 更新桌面小组件"操作 - - 更新稍后读未读数+预览内容到桌面小组件 - - main.dart新增HomeWidgetService.instance.init()初始化 - -6. **ClipboardMonitorService UI集成** — chat_flow_readlater_mixin.dart + main.dart - - 稍后读设置面板新增"📋 剪贴板监控"操作 - - 开启/关闭监控+查看剪贴板内容 - - main.dart新增ClipboardMonitorService.instance.initFromStore()初始化 - -7. **ReadlaterCollabService UI集成** — chat_flow_readlater_mixin.dart - - showSettings新增"👥 共享协作"操作 - -8. **ReadlaterDeviceSyncService UI集成** — chat_flow_readlater_mixin.dart - - showSettings新增"📱 跨设备同步"操作 - -9. **DailyTaskPage/TaskCard** — 替换硬编码CupertinoColors为AppTheme.ext(context)设计令牌 - - bgCard/bgSecondary/bgElevated/textPrimary/textSecondary/successColor/infoColor/warningColor/errorColor - -10. **exportAsZip** — 同步I/O改为异步(await writeAsString/readAsBytes) - -11. **ReadlaterReminderService** — SharedPreferences替换为AppKVStore - - getBool/setBool/getString/setString全部迁移至AppKVStore - - isEnabled()从Future改为同步bool - -12. **ChatMessage标签同步** — ReadlaterTagService.addTag/removeTag/setTagsForMessage自动同步到ext['tags'] - - 新增_syncTagsToMessageExt()私有方法 - - ChatMessageService新增updateExt()方法 - -#### 🧪 验证 - -- 自动化验证脚本: test/audit_v58_verify.dart (15/15项通过) -- 验证项: UI入口存在性、AppTheme替换完整性、AppKVStore替换完整性、异步I/O替换、服务初始化、标签同步 +#### 架构说明 +``` +Drift (ORM层) + ├── NativeDatabase (Android/iOS/macOS/Windows/Linux) — dart:ffi + sqlite3 + └── SqfliteDelegate (OpenHarmony) — sqflite_ohos 插件 + ├── DatabaseDelegate → sqflite.Database + ├── DynamicVersionDelegate → PRAGMA user_version + └── SupportedTransactionDelegate → sqflite.transaction() +``` *** -## \[12.25.0] - 2026-05-15 +## \[5.17.5] - 2026-05-17 -### 🔧 架构重构 — 目录整理 + home_widget本地化 + 代码拆分 +### 🐛 鸿蒙端白屏修复 — 全量平台兼容性适配 -> 代码架构优化:chat_flow_page拆分、inspiration/presentation目录整理、core/services分类归档、home_widget本地包更新至0.9.1 +> 鸿蒙端启动白屏,根因:`Platform.isAndroid`在鸿蒙端返回`true`,导致Android专属API被调用后崩溃。 +> 新增`pu.isOhos`平台检测,全量扫描修复38处`Platform.isXxx`使用,增加鸿蒙端分支处理。 -#### 🔧 chat_flow_page.dart 拆分 -- 原1526行拆分为7个文件,主文件373行 -- `chat_flow_page.dart` — State管理和build方法 -- `chat_flow_readlater_mixin.dart` → `ChatFlowReadlaterHelper` 静态辅助类 -- `chat_flow_conversation_mixin.dart` → `ChatFlowConversationHelper` 静态辅助类 -- `chat_flow_top_bar.dart` — 搜索栏、分类栏、背景图、动态主题 -- `chat_flow_input_bar.dart` — 输入栏、附件按钮、发送按钮 -- `chat_flow_message_list.dart` — 消息列表展示、空状态占位 -- `chat_flow_send_toast.dart` — 消息发送浮动提示 +#### 白屏根因分析 +1. **main.dart无try-catch的初始化**:`LiquidGlassWidgets.initialize()`和`Platform3DService.detectDeviceCapability()`在鸿蒙端可能失败,未捕获异常导致整个App崩溃白屏 +2. **通知服务Platform.isAndroid**:鸿蒙端走Android分支获取`AndroidFlutterLocalNotificationsPlugin`,但实际应使用`OhosFlutterLocalNotificationsPlugin` +3. **设备信息服务**:鸿蒙端设备ID前缀应为`ohos_`而非`android_` -#### 🔧 inspiration/presentation 目录整理 -- 新增子目录: `pages/home/`, `pages/tool/`, `widgets/chat/`, `widgets/chat_bubble/`, `widgets/chat_input/`, `widgets/common/`, `widgets/tool/` -- 每个文件夹不超过8个文件 -- 所有import路径已修复 +#### 修复文件清单 -#### 🔧 core/services 目录整理 -- 原29个文件分类到6个子目录: - - `auth/` (3): token_service, permission_service, validate_service - - `data/` (5): backup_service, data_export_service, settings_export_service, home_widget_service, jinrishici_sdk_service - - `device/` (5): app_lock_service, battery_optimization_service, device_info_service, haptic_service, screen_wake_service - - `network/` (4): connectivity_service, deep_link_service, network_proxy_service, og_metadata_service - - `notification/` (4): local_notification_service, notification_scheduler, notification_service, readlater_reminder_service - - `readlater/` (5): readlater_ai_service, readlater_collab_service, readlater_device_sync_service, readlater_sync_service, sharing_receiver_service -- 根目录保留3个: smart_mode_service, sound_service, clipboard_monitor_service -- 所有import路径已修复 +| 文件 | 修复内容 | +|------|---------| +| `main.dart` | 为`LiquidGlassWidgets`/`Platform3DService`/`_validatePageRegistry`增加try-catch | +| `local_notification_service.dart` | 通知权限请求增加鸿蒙分支(`OhosFlutterLocalNotificationsPlugin`) | +| `notification_service.dart` (core) | 通知权限请求增加鸿蒙分支 | +| `notification_service.dart` (file_transfer) | 平台检测+通知权限+进度通知增加鸿蒙处理 | +| `device_info_service.dart` | 设备ID/名称/型号/平台识别增加鸿蒙分支 | +| `permission_service.dart` | 存储权限鸿蒙端跳过(无需Android存储权限) | +| `share_sheet.dart` | 分享功能平台检测增加鸿蒙 | +| `sharing_receiver_service.dart` | 分享接收平台分支增加鸿蒙 | +| `device_discovery_provider.dart` | BLE/NFC/USB平台能力检测增加鸿蒙 | +| `nearby_service_adapter.dart` | Wi-Fi Direct平台支持+权限检查增加鸿蒙 | +| `localsend_service.dart` | 设备型号检测增加HarmonyOS | +| `hotspot_service.dart` | WiFi热点平台支持+SDK版本检测增加鸿蒙 | +| `bluetooth_pairing_service.dart` | BLE平台支持+广播检测增加鸿蒙 | +| `qr_pairing_service.dart` | QR扫码平台支持增加鸿蒙 | +| `usb_discovery_service.dart` | USB发现平台检测增加鸿蒙 | +| `usb_transport_service.dart` | USB传输6处平台检测全部增加鸿蒙 | +| `transfer_file_handler.dart` | Wi-Fi Direct权限+USB传输平台检测增加鸿蒙 | -#### 🔧 home_widget 本地包更新 -- 从pub.dev下载home_widget 0.9.1版本到 `packages/home_widget` -- pubspec.yaml引用改为本地路径: `path: packages/home_widget` -- 版本从0.7.0+1升级到0.9.1 +#### 核心平台检测机制 +- `platform_io_native.dart`:`_isOhos()`通过`Platform.operatingSystem == 'ohos'`检测 +- `platform_io_stub.dart`:Web端`isOhosImpl`返回`false` +- `platform_utils.dart`:暴露`isOhos`属性 +- `device_detection.dart`:`_isHarmonyOS()`使用`pu.isOhos` +- **关键**:`isAndroidImpl`已排除鸿蒙(`Platform.isAndroid && !_isOhos()`) *** -## \[12.24.0] - 2026-05-15 +## \[5.17.4] - 2026-05-17 -### ✨ 新功能 — E10剪贴板监控 + E13桌面小组件 + E14文件夹管理 + E15AI摘要 + E17稍后读协作 +### 🔧 ArkTS编译错误全量修复 — 45→0个ERROR -> 稍后读功能体系扩展:剪贴板自动检测链接、桌面小组件数据推送、文件夹归档、AI智能摘要、好友共享协作 +> 从45个ArkTS编译错误逐步修复至0个,HAP构建成功。涉及6个原生插件的API兼容性修复。 -#### ✨ E10 剪贴板链接监控服务 -- 新增 `ClipboardMonitorService` (`core/services/clipboard_monitor_service.dart`) - - `startMonitor()` — 每3秒轮询剪贴板,检测URL链接 - - `stopMonitor()` — 停止监控 - - `_isUrl()` — 仅匹配http/https协议链接,隐私保护不保存非URL内容 - - `_saveToReadlater()` — 自动保存到稍后读,显示📋提示 - - `setEnabled()` / `getEnabled()` — 开关状态持久化(AppKVStore key: clipboard_monitor_enabled) - - `initFromStore()` — 应用启动时根据存储状态自动恢复监控 +#### 修复清单 -#### ✨ E13 桌面小组件服务 -- 新增 `HomeWidgetService` (`core/services/home_widget_service.dart`) - - `init()` — 初始化App Group (group.com.xianyan.share) - - `updateReadlaterCount()` — 更新稍后读未读数到小组件 - - `updateReadlaterPreview()` — 更新最新稍后读内容预览(截断80字) - - `updateDailySentence()` — 更新每日一句到小组件 - - `handleWidgetClick()` — 获取小组件点击数据 - - `registerInteractivityCallback()` — 注册后台交互回调 - - Android/iOS WidgetId: XianyanReadlaterWidget - - `debugGetAllData()` — 调试用,读取所有小组件数据 -- pubspec.yaml 新增 `home_widget: ^0.7.0` 依赖 +| 插件 | 错误 | 修复方案 | +|------|------|---------| +| network_info_plus_ohos | `WifiLinkedInfo`无`gateway`/`netMask`属性 | `getWifiSubmask`/`getWifiGatewayAddress`/`getWifiBroadcast`返回null | +| nearby_service | `wifiP2p`不在`@kit.ConnectivityKit`中 | 改用`wifiManager`(P2P API的正确入口) | +| nearby_service | `socket.NetFamily`/`NetFamilyType`不存在 | TCP连接family参数改为数字`1`(IPv4) | +| home_widget | `FormInfo`无`id`/`formId`属性 | 改用`formProvider.getPublishedRunningFormInfos()`获取`RunningFormInfo` | +| home_widget | `preferences.get`返回`ValueType`不能`as string` | 改用`.toString()` | +| home_widget | `call.argument()`返回`any`类型 | 显式标注`as number` | +| home_widget | `Record`不匹配`startAbility`参数 | 改用`Want`类型 | +| gal | `showAssetsCreationRequest`不存在 | 改用`showAssetsCreationDialog`(系统弹窗授权保存) | +| gal | `Record`不匹配`startAbility`参数 | 改用`Want`类型 | +| gal | `Error \| unknown`不允许 | 改用`ESObject` | +| EntryAbility | `MethodCall`导入路径错误 | 从`MethodCall.ets`独立导入 | +| EntryAbility | `app_links_ohos`模块路径不存在 | 移除未使用的导入和变量 | +| EntryAbility | `MIME_TEXT_PLAIN`不存在 | 改为`MIMETYPE_TEXT_PLAIN` | -#### ✨ E14 稍后读文件夹管理 -- 新增 `ReadlaterFolderService` (`features/inspiration/services/readlater_folder_service.dart`) - - `getFolders()` — 获取所有文件夹(自动同步消息计数) - - `createFolder()` — 创建文件夹(支持emoji图标) - - `renameFolder()` / `deleteFolder()` — 重命名/删除文件夹 - - `moveMessageToFolder()` / `removeMessageFromFolder()` — 消息归档/移出 - - `getMessageIdsInFolder()` / `getMessageFolder()` — 查询归档关系 - - 数据持久化: AppKVStore (key: readlater_folders + readlater_message_folder) -- 新增 `ReadlaterFolder` 数据模型(id/name/emoji/count/createdAt/updatedAt) - -#### ✨ E15 AI摘要生成服务 -- 新增 `ReadlaterAiService` (`core/services/readlater_ai_service.dart`) - - `generateSummary()` — 生成单条消息摘要 - - `batchSummarize()` — 批量生成摘要 - - `generateDailySummary()` — 生成每日摘要 - - `suggestTags()` — 智能分类标签建议 - - `applySummary()` / `getSummary()` — 摘要写入/读取消息ext['aiSummary'] - - Edge Function: generate-readlater-summary - - 请求参数: { content, type, action: 'summary'|'daily'|'tags' } - - 降级策略: AI不可用时返回null,不影响UI - -#### ✨ E17 稍后读协作服务 -- 新增 `ReadlaterCollabService` (`core/services/readlater_collab_service.dart`) - - `createSharedList()` — 创建共享列表 - - `inviteMember()` — 邀请好友加入 - - `shareToSharedList()` — 分享消息到共享列表 - - `getSharedListMessages()` — 获取共享列表消息 - - `getMySharedLists()` — 获取我参与的共享列表 - - `leaveSharedList()` — 退出共享列表 - - `subscribeToListChanges()` / `unsubscribeFromListChanges()` — 实时订阅/取消 - - Supabase表: readlater_shared_lists, readlater_shared_members, readlater_shared_messages - - 实时订阅: supabase.realtime PostgresChanges -- 新增 `SharedReadlaterList` 数据模型(id/name/ownerId/memberIds/messageCount/createdAt) - -#### 📝 文件变更 -- 新增: `lib/core/services/clipboard_monitor_service.dart` -- 新增: `lib/core/services/home_widget_service.dart` -- 新增: `lib/features/inspiration/services/readlater_folder_service.dart` -- 新增: `lib/core/services/readlater_ai_service.dart` -- 新增: `lib/core/services/readlater_collab_service.dart` -- 修改: `pubspec.yaml` — 新增 home_widget 依赖 +#### 关键API变更说明 +- **wifiP2p → wifiManager**: HarmonyOS 5中P2P API通过`import { wifiManager } from '@kit.ConnectivityKit'`导入,方法名也有变化(`p2pConnect`而非`connect`) +- **FormInfo → RunningFormInfo**: `FormInfo`是卡片配置信息(无formId),`RunningFormInfo`是运行时信息(有formId),需用`getPublishedRunningFormInfos()`获取 +- **showAssetsCreationDialog**: 替代`showAssetsCreationRequest`,通过系统弹窗授权保存媒体文件,无需`WRITE_IMAGEVIDEO`权限 *** -## \[12.23.0] - 2026-05-15 +## \[5.17.3] - 2026-05-17 -### 🔧 服务端修复 — 信令协议3项问题修复 + API文档拆分 +### 🔒 受限ACL权限优化 — 5→0个受限权限 -> 修复WebSocket信令服务器heartbeat/discover/ping无响应问题,API文档拆分为独立文档 +> DevEco Studio警告5个受限ACL权限需要审核。通过安全组件替代方案,全部移除受限权限,无需ACL审核即可发布。 -#### 🐛 服务端修复 — heartbeat无响应 -- **根因**: `server/index.js` 第119-121行,`heartbeat` case 仅更新 `lastBeat`,未回复 `heartbeat_ack` -- **修复**: 添加 `this._send(sender, { type: 'heartbeat_ack', timestamp: Date.now() })` 回复 -- **影响**: 客户端现在可以主动检测连接存活状态 +| 受限权限 | 替代方案 | 说明 | +|---------|---------|------| +| WRITE\_IMAGEVIDEO | `showAssetsCreationRequest` | gal插件改用系统安全弹窗授权保存媒体 | +| READ\_IMAGEVIDEO | `PhotoViewPicker` | image_picker已使用系统选择器 | +| READ\_WRITE\_DOWNLOAD\_DIRECTORY | `DocumentViewPicker` | file_picker已使用系统文件选择器 | +| FILE\_ACCESS\_PERSIST | `DocumentViewPicker` | file_picker已使用系统文件选择器 | +| READ\_PASTEBOARD | `ClipboardBridge` + 原生MethodChannel | 通过EntryAbility原生桥接读取剪贴板 | -#### 🐛 服务端修复 — discover设备发现无响应 -- **根因**: `discover` 不在 switch case 和 handledTypes 中,无 `to` 字段被通用转发丢弃 -- **修复**: 新增 `_handleDiscover` 方法,返回所有在线设备列表;添加 `discover` 到 handledTypes -- **影响**: 客户端可以主动请求刷新设备列表 +#### gal插件改造 +- `createAssetRequest` → `showAssetsCreationRequest`:系统弹出授权对话框 +- `putImageBytes`:先写入临时文件,再通过系统安全组件保存 +- `hasAccess` / `requestAccess`:直接返回true,由系统安全组件处理授权 +- 移除 `abilityAccessCtrl` / `bundleManager` 依赖 -#### 🐛 服务端修复 — ping/pong无响应 -- **根因**: 客户端发送 `ping` 不在 switch case 中,被通用转发丢弃 -- **修复**: 新增 `ping` case,回复 `{ type: 'pong', timestamp }` 并更新 `lastBeat` -- **影响**: 客户端可以主动测试连通性 +#### 剪贴板桥接 +- 新增 `ClipboardBridge` (lib/core/utils/clipboard_bridge.dart) +- EntryAbility 注册 `clipboard_ohos` MethodChannel +- 通过 `pasteboard.getSystemPasteboard().getDataSync()` 读取剪贴板 -#### 🔧 服务端优化 — 注册响应顺序 -- **根因**: `_onConnection` 中 `display-name` 在 `registered` 之前发送 -- **修复**: 先发送 `registered`,再发送 `display-name` - -#### 📄 文档拆分 — API_FILE_TRANSFER_DOC.md -- 原始合并文档已删除 -- 拆分为 `API_FILE_TRANSFER_CORE_DOC.md`(文件传输核心API + WebSocket信令协议) -- 拆分为 `API_CLOUD_CACHE_DOC.md`(云端暂存CloudCache API) -- 新增 `API_FILE_TRANSFER_ANALYSIS.md`(问题与性能分析报告) -- 新增 `verify_file_transfer_api.py`(全流程API验证脚本,28个测试用例) - -#### ✅ API验证结果 -- REST API: 14/14 通过 (100%) -- WebSocket信令: 6/6 通过 (100%) -- 云端暂存: 8/8 通过 (100%) -- **总计: 28/28 通过 (100%)** - -#### 🔧 客户端修复 — signaling_service.dart -- 新增 `discoverResponse` 枚举和 case 处理 -- 新增 `heartbeatAck` 枚举和 case 处理,追踪 `_lastHeartbeatAck` -- 修复 `pong` case,记录心跳响应时间 -- 修复 `displayName` case,解析并存储服务器推送的设备名称 +#### 代码修复 +- nfc\_pairing\_service.dart: `NdefRecord`(ndef\_record) → `ndef.NDEFRecord`(ndef),修复类型不匹配 +- pro\_image\_editor/hvigorfile.ts: 移除未使用的 `path` 导入 +- pubspec.yaml: 移除已弃用的 `ndef_record` 依赖 *** -## \[12.22.0] - 2026-05-15 +## \[5.17.2] - 2026-05-17 -### ✨ 新功能 — E16跨设备稍后读同步 + 稍后读缓存管理集成 +### 🔌 鸿蒙适配 — 全量审计 + 构建修复 -> 稍后读内容可通过文件传输助手同步到同账号设备,缓存管理页面新增稍后读统计和清理 +> 对pubspec.yaml所有依赖进行全量鸿蒙适配审计,分类为本地化原生插件(32个)、TPC远程适配(3个)、纯Dart包(50个)。修复构建错误,补全宿主应用权限声明和资源字符串。 -#### ✨ E16 跨设备稍后读同步 -- 新增 `ReadlaterDeviceSyncService` (`core/services/readlater_device_sync_service.dart`) - - `discoverDevices()` — 发现同账号在线设备 - - `sendToDevice()` — 将稍后读消息打包发送到指定设备 - - `sendAsFile()` — 导出稍后读内容为文件并传输 - - `handleReceivedSyncData()` — 处理接收到的稍后读同步数据 - - `scanAndImportPendingSyncFiles()` — 扫描传输目录中未处理的同步文件 -- 发送流程:序列化JSON → 保存临时文件 `readlater_sync_{timestamp}.json` → 调用TransferNotifier.sendFileToMyDevice传输 -- 接收流程:检测同步文件 → 解析JSON → 按消息类型(readlater_sentence/link/document/text)导入稍后读会话 -- 复用现有文件传输模块能力(TransferNotifier/SignalingService) +#### 全量依赖审计 -#### ✨ 稍后读缓存管理集成 -- `CacheService` 新增方法: - - `getReadlaterCacheSize()` — 获取稍后读缓存大小(消息+附件+同步文件) - - `cleanReadlaterCache()` — 清理稍后读缓存(缩略图/附件/同步临时文件,保留消息记录) - - `clearReadlaterData()` — 清理全部稍后读数据(消息+附件+缩略图) -- `CacheStats` 新增 `readlaterSizeBytes` 字段和 `readlaterSizeFormatted` getter -- 缓存管理页面新增: - - 分类统计中"📖 稍后读"条目,显示缓存大小 - - "📖 清理稍后读缓存"操作按钮(保留消息记录) - - "📖 清理全部稍后读数据"操作按钮(含确认弹窗,不可撤销) +| 分类 | 数量 | 说明 | +|------|------|------| +| 本地化原生插件 | 32个 | packages/目录下已有ohos适配代码 | +| TPC远程适配 | 3个 | flutter_image_compress_ohos/device_info_plus/package_info_plus | +| 纯Dart包 | 50个 | 无需ohos适配 | -#### 📝 文件变更 -- 新增: `lib/core/services/readlater_device_sync_service.dart` -- 修改: `lib/features/home/services/cache_service.dart` — 新增稍后读缓存管理方法+CacheStats字段 -- 修改: `lib/features/home/presentation/cache_management_page.dart` — 新增稍后读统计条目+清理操作 +#### 构建错误修复 + +| 文件 | 修复内容 | +|------|---------| +| video_compress/oh-package.json5 | `@ohos/flutter_ohos` 行末缺少逗号导致JSON5解析失败 | +| home_widget/ohos/hvigorfile.ts | 缺失构建文件,创建 `export { harTasks } from '@ohos/hvigor-ohos-plugin'` | +| nearby_service/ohos/hvigorfile.ts | 缺失构建文件,同上 | + +#### 宿主应用权限补全 + +| 修复项 | 说明 | +|--------|------| +| Entry module.json5 | 添加tablet设备类型 + 补全12项权限声明 | +| base/string.json | 添加9个权限描述字符串(英文) | +| zh_CN/string.json | 添加9个权限描述字符串(中文) | + +#### 鸿蒙适配计划文档更新 + +- 鸿蒙适配计划-第三批次.md 新增"全量依赖审计"章节(2.1-2.5) +- 包含32个本地化插件审计表、3个TPC远程包、50个纯Dart包清单 +- 宿主应用17项权限审计对照表 +- 15项审计修复记录汇总 *** -## \[12.22.0] - 2026-05-15 +## \[5.17.1] - 2026-05-17 -### ✨ 新功能 — E4视频压缩保存 + E11稍后读同步 + E12句子卡片制作 +### 🔌 鸿蒙适配 — 第三方包审计修复 + NFC迁移TPC -> 视频气泡支持压缩保存到相册,稍后读支持云端同步,句子卡片支持截图制作分享 +> 对所有鸿蒙适配的第三方包进行全面审计,修复空实现方法、缺失权限、配置不一致等问题。将NFC包从自适配的nfc_manager迁移到TPC官方适配的flutter_nfc_kit。统一所有包的HAR引用路径。 -#### ✨ E4 视频压缩保存 -- `ChatVideoBubble` 新增长按菜单(CupertinoActionSheet) -- 新增"💾 压缩保存"选项:使用 `video_compress` 压缩视频(MediumQuality) -- 压缩过程显示 `CupertinoActivityIndicator` + 进度百分比 -- 压缩完成后使用 `gal` 保存到相册(album: 闲言) -- 新增"▶️ 播放视频"菜单项 -- 权限检测:先检查相册权限,未授权则请求 +#### NFC包迁移: nfc_manager → flutter_nfc_kit (TPC官方适配) -#### ✨ E11 稍后读离线同步服务 -- 新增 `ReadlaterSyncService` (`core/services/readlater_sync_service.dart`) -- 基于 `supabase_flutter` 实现云端同步 -- 核心方法:`uploadMessage` / `downloadMessages` / `fullSync` / `getLastSyncTime` / `setAutoSync` / `getAutoSync` / `deleteMessage` -- Supabase表名: `readlater_messages`,字段: id, user_id, message_id, type, content, meta_json, created_at, updated_at -- 使用 `AppKVStore` 存储同步状态(last_sync_time, auto_sync) -- 冲突策略:以 `updated_at` 较新的为准 -- `SyncResult` 模型记录上传/下载/冲突数量 -- `ensureInitialized()` 静态方法确保 Supabase 已初始化 +| 变更 | 说明 | +|------|------| +| 新增 flutter_nfc_kit | TPC官方适配包 (v3.6.0-rc.6-ohos),支持标签轮询/数据传输/NDEF读写 | +| pubspec.yaml 更新 | `nfc_manager: path: packages/nfc_manager` → `flutter_nfc_kit: path: packages/flutter_nfc_kit` | +| nfc_pairing_service.dart | 迁移到 FlutterNfcKit API (poll/transceive) | -#### ✨ E12 句子卡片制作 -- `ChatSentenceCardBubble` 操作按钮新增"🎨 制作" -- 使用 `RepaintBoundary` 包裹卡片,`toImage()` 截图(3x像素密度) -- 截图保存到临时目录,通过 `share_plus` 分享图片 -- 分享文本包含句子内容+作者+来源标识 -- 生成过程显示 Loading 提示 +#### 空实现方法修复 + +| 包名 | 修复方法 | 实现方式 | +|------|---------|---------| +| gal | hasAccess() | 使用 abilityAccessCtrl.verifyAccessTokenSync 检查权限 | +| gal | requestAccess() | 使用 abilityAccessCtrl.requestPermissionsFromUser 请求权限 | +| gal | open() | 使用 context.startAbility 打开图库 | +| home_widget | requestPinWidget() | 使用 context.startAbility + ADD_FORM action | +| home_widget | registerBackgroundCallback() | 持久化回调句柄到 Preferences | +| home_widget | isRequestPinWidgetSupported() | 检测 formInfo.FormVisibility 可用性 | +| nearby_service | send() | 使用 @ohos.net.socket TCP Socket 传输数据 | +| nearby_service | startAdvertising() | 使用 wifiP2p.createGroup 创建P2P组 | +| nearby_service | stopAdvertising() | 使用 wifiP2p.removeGroup 移除P2P组 | +| nearby_service | openServicesSettings() | 使用 context.startAbility 打开WiFi设置 | + +#### module.json5 权限和设备类型修复 + +| 包名 | 修复内容 | +|------|---------| +| gal | 添加 READ/WRITE_IMAGEVIDEO 权限 + tablet 设备类型 | +| nearby_service | 添加 SET_WIFI_INFO/GET_WIFI_INFO/LOCATION/INTERNET 权限 | +| network_info_plus_ohos | 添加 GET_WIFI_INFO 权限 | +| video_compress | 添加 tablet 设备类型 | +| mobile_scanner | 添加 tablet 设备类型 + 修复JSON尾部逗号 | +| flutter_blue_plus_ohos | 添加 ACCESS_BLUETOOTH/LOCATION 权限 | +| wifi_iot | 添加 GET_WIFI_INFO/SET_WIFI_INFO/LOCATION 权限 | +| nfc_manager_ohos | deviceTypes phone→default + 添加 NFC_TAG 权限 | +| home_widget | 添加 FormExtension 声明 + grid 卡片配置 | + +#### HAR引用路径统一 + +将所有包的 `@ohos/flutter_ohos` 依赖路径统一为 `file:./har/flutter.har`(TPC标准路径),涉及15个包的 oh-package.json5 修复: + +- nfc_manager_ohos: `file:./har/flutter_ohos.har` → `file:./har/flutter.har` +- video_compress: `file:./libs/flutter_embedding.har` → `file:./har/flutter.har` +- connectivity_plus: `file:./libs/flutter_embedding.har` → `file:./har/flutter.har` +- pro_image_editor: `file:../../../libs/flutter.har` → `file:./har/flutter.har` +- nearby_service: `file:../../../libs/flutter.har` → `file:./har/flutter.har` +- home_widget: `file:../../../libs/flutter.har` → `file:./har/flutter.har` +- app_links_ohos: `file:../../../libs/flutter.har` → `file:./har/flutter.har` +- flutter_webrtc: `file:../libs/flutter.har` → `file:./har/flutter.har` +- mobile_scanner: `file:libs/flutter.har` → `file:./har/flutter.har` +- sqflite_ohos: `file:har/flutter.har` → `file:./har/flutter.har` +- audioplayers_ohos: `file:har/flutter.har` → `file:./har/flutter.har` +- fluttertoast_ohos: `file:../../../libs/flutter.har` → `file:./har/flutter.har` +- image_picker_ohos: `file:libs/flutter.har` → `file:./har/flutter.har` +- path_provider_ohos: `file:libs/flutter.har` → `file:./har/flutter.har` +- shared_preferences_ohos: `file:libs/flutter.har` → `file:./har/flutter.har` + +#### app_links 深度链接集成 + +- EntryAbility.ets 添加 onCreate/onNewWant 回调处理深度链接URI *** -## \[12.21.0] - 2026-05-15 +## \[5.17.0] - 2026-05-16 -### 🔧 Bug修复 — 信令服务客户端适配服务端修复 +### 🔌 鸿蒙适配 — flutter_webrtc 完整WebRTC音视频通信 -> 服务端已修复心跳/发现/Ping信令,客户端同步更新枚举和处理逻辑 +> 完成flutter_webrtc鸿蒙ohos完整适配。合并TPC社区 fluttertpc_flutter_webrtc 的完整实现,包含预编译的 `libohos_webrtc.so` C++库(基于WebRTC M120),69个MethodChannel方法全部实现。Dart端新增ohos平台视图渲染和音频配置支持。 -#### 🐛 问题根因 -- `SignalingMessageType` 枚举缺少 `heartbeat_ack` 和 `discover_response` 类型 -- `_handleMessage` 中 `discover` case 错误地调用了 `_handleDiscoverResponse`(应为 `discoverResponse`) -- `heartbeat_ack` 消息被解析为 `unknown` 类型,无法正确处理 -- `pong` 消息处理仅 break,未记录心跳响应时间 -- `display-name` 消息仅日志输出,未存储显示名称 +#### 新增本地化鸿蒙适配包 -#### ✅ 修复内容 -- `SignalingMessageType` 新增 `heartbeatAck('heartbeat_ack')` 枚举值 -- `SignalingMessageType` 新增 `discoverResponse('discover_response')` 枚举值 -- `_handleMessage` 修复 `discover` case:不再调用 `_handleDiscoverResponse` -- `_handleMessage` 新增 `discoverResponse` case:正确调用 `_handleDiscoverResponse` -- `_handleMessage` 新增 `heartbeatAck` case:记录心跳响应时间 -- `_handleMessage` 改进 `pong` case:记录心跳响应时间和日志 -- `_handleMessage` 改进 `displayName` case:解析并存储服务器推送的显示名称 -- 新增 `_lastHeartbeatAck` 字段和 `lastHeartbeatAck` getter -- 新增 `_serverDisplayName` 字段和 `serverDisplayName` getter +| 库 | 版本 | 适配模式 | 本地路径 | 用途 | +|----|------|---------|---------|------| +| `flutter_webrtc` | v1.4.0-ohos.1 | 直接插件 | packages/flutter_webrtc | WebRTC音视频通信 | +| `webrtc_interface` | v1.5.1 | 纯Dart接口 | packages/webrtc_interface | WebRTC平台接口定义 | -#### 📝 文档更新 -- `API_FILE_TRANSFER_DOC.md`: 更新心跳/发现/ping为双向机制,添加display-name消息类型 -- `API_FILE_TRANSFER_CORE_DOC.md`: 同步更新 -- `API_FILE_TRANSFER_ANALYSIS.md`: 标记已修复项(心跳/发现/Ping/display-name/文档) +#### TPC社区库合并 + +| 源仓库 | 版本 | 说明 | +|--------|------|------| +| `openharmony-sig/fluttertpc_flutter_webrtc` | v0.9.48+hotfix.1 | TPC社区完整ohos适配 | +| `openharmony-sig/ohos_webrtc` | M120 | 预编译WebRTC C++库 | + +#### flutter_webrtc ohos原生实现功能 + +| 功能模块 | 方法数 | ohos支持 | 说明 | +|---------|-------|---------|------| +| initialize | 1 | ✅ | PeerConnectionFactory初始化 | +| createPeerConnection | 1 | ✅ | 创建PC对象+Observer | +| createOffer/Answer | 2 | ✅ | SDP协商 | +| setLocal/RemoteDescription | 2 | ✅ | 设置本地/远端描述 | +| addIceCandidate | 1 | ✅ | 添加ICE候选 | +| getUserMedia | 1 | ✅ | 摄像头+麦克风采集 | +| getDisplayMedia | 1 | ✅ | 屏幕共享 | +| DataChannel | 3 | ✅ | create/send/close | +| MediaStream管理 | 6 | ✅ | getTracks/addTrack/removeTrack等 | +| 视频渲染 | 3 | ✅ | Texture+PlatformView双模式 | +| RTP管理 | 10 | ✅ | Sender/Receiver/Transceiver | +| 音频管理 | 7 | ✅ | 音量/静音/扬声器/蓝牙 | +| getStats | 1 | ✅ | 统计信息 | +| FrameCryptor | 5 | ✅ | 帧加密 | +| 录制 | 2 | ⏳ TODO | startRecordToFile/stopRecordToFile | +| 其他方法 | 24 | ✅/⏳ | 大部分已实现,少量TODO | + +**MethodChannel**: `FlutterWebRTC.Method` +**EventChannel**: `FlutterWebRTC.Event` + +#### ohos/目录结构 + +``` +ohos/ +├── Index.ets +├── oh-package.json5 ← 引用 libohos_webrtc.so + flutter.har +├── build-profile.json5 +├── hvigorfile.ts +├── libs/ +│ └── arm64-v8a/ +│ └── libohos_webrtc.so ← 预编译WebRTC C++库(50MB) +├── src/main/ +│ ├── module.json5 ← INTERNET/CAMERA/MICROPHONE权限 +│ ├── libohos_webrtc/ +│ │ ├── index.d.ts +│ │ ├── webrtc.d.ts ← 1030行WebRTC API类型定义 +│ │ └── oh-package.json5 +│ ├── ets/ +│ │ ├── FlutterWebRTCPlugin.ets ← 主插件入口 +│ │ ├── MethodCallHandlerImpl.ets ← 1662行方法处理器 +│ │ ├── MethodHandlers.ets ← 方法路由表 +│ │ ├── PeerConnectionObserver.ets ← PC事件观察者 +│ │ ├── DataChannelObserver.ets ← DC事件观察者 +│ │ ├── GetUserMediaImpl.ets ← 媒体采集 +│ │ ├── FlutterRTCVideoRenderer.ets ← 视频渲染 +│ │ ├── FlutterRTCFrameCryptor.ets ← 帧加密 +│ │ ├── SurfaceTextureRenderer.ets ← 纹理渲染 +│ │ ├── StateProvider.ets ← 状态管理 +│ │ ├── LifeCycleObserver.ets ← 生命周期 +│ │ ├── DependencyRelated.ets ← 依赖管理 +│ │ ├── AudioSwitchManager.ets ← 音频切换 +│ │ ├── AudioChannel.ets ← 音频录制 +│ │ ├── components/ ← PlatformView组件 +│ │ └── utils/ ← 工具类(12个文件) +│ └── resources/ +``` + +#### Dart端修改 + +| 文件 | 修改内容 | +|------|---------| +| `lib/src/native/utils.dart` | 新增 `platformIsOhos` 平台检测 | +| `lib/src/native/camera_utils.dart` | 5处平台判断添加ohos分支 | +| `lib/src/native/rtc_video_view_impl.dart` | ohos PlatformView视频渲染 | +| `lib/src/native/rtc_video_renderer_impl.dart` | surfaceId字段(ohos渲染必需) | +| `lib/src/native/ohos/audio_configuration.dart` | ohos音频配置(新增) | +| `lib/src/native/ohos/video_render.dart` | ohos视频渲染组件(新增) | +| `lib/src/helper.dart` | setOhosAudioConfiguration方法 | +| `lib/src/web/utils.dart` | platformIsOhos返回false | + +#### 使用的鸿蒙API + +- `libohos_webrtc.so` — 预编译WebRTC C++库(基于M120) +- `@kit.CameraKit` — 摄像头采集 +- `@kit.AudioKit` — 音频采集/播放/设备管理 +- `@kit.AbilityKit` — UIAbility生命周期管理 + +#### 已完成鸿蒙适配的包汇总(共31个) + +| # | 包名 | 适配模式 | 批次 | +|---|------|---------|------| +| 1 | shared_preferences | 联合插件 | 1 | +| 2 | path_provider | 联合插件 | 1 | +| 3 | url_launcher | 联合插件 | 1 | +| 4 | image_picker | 联合插件 | 1 | +| 5 | local_auth | 联合插件 | 1 | +| 6 | video_player | 联合插件 | 1 | +| 7 | permission_handler | 联合插件 | 1 | +| 8 | flutter_secure_storage | 联合插件 | 1 | +| 9 | share_plus | 联合插件 | 1 | +| 10 | battery_plus | 联合插件 | 1 | +| 11 | connectivity_plus | 直接插件 | 1 | +| 12 | flutter_local_notifications | 直接插件 | 2 | +| 13 | file_picker | 直接插件 | 2 | +| 14 | receive_sharing_intent | 直接插件 | 2 | +| 15 | wakelock_plus | 直接插件 | 2 | +| 16 | mobile_scanner | 直接插件 | 2 | +| 17 | wifi_iot | 直接插件 | 2 | +| 18 | audioplayers | 联合插件 | 2 | +| 19 | record | 联合插件 | 2 | +| 20 | sqflite | 联合插件 | 2 | +| 21 | fluttertoast | 直接插件 | 1 | +| 22 | video_compress | 直接插件 | 3 | +| 23 | flutter_blue_plus | 联合插件 | 3 | +| 24 | gal | 直接插件 | 3 | +| 25 | network_info_plus | 联合插件 | 3 | +| 26 | app_links | 联合插件 | 3 | +| 27 | pro_image_editor | 直接插件 | 3 | +| 28 | home_widget | 直接插件 | 3 | +| 29 | nfc_manager | 联合插件 | 3 | +| 30 | nearby_service | 直接插件 | 3 | +| 31 | flutter_webrtc | 直接插件 | 4 | *** -## \[5.3.0] - 2026-05-15 +## \[5.16.0] - 2026-05-16 -### ✨ 新功能 — E1链接OG元数据自动抓取 + E8全文搜索 +### 🔌 鸿蒙适配 — app_links 深度链接 + pro_image_editor 图片编辑 -> 链接消息自动抓取OG预览信息,稍后读模式支持全文搜索+搜索结果高亮 +> 新增 app_links 和 pro_image_editor 鸿蒙适配。app_links 采用联合插件模式,支持深度链接获取和监听;pro_image_editor 采用直接插件模式,支持emoji可用性检测。 -#### ✨ E1 链接OG元数据自动抓取服务 -- 新增 `OgMetadataService` (`core/services/og_metadata_service.dart`) - - 使用dio发起HTTP请求,5秒超时,失败静默降级 - - 使用compute/isolate处理HTML解析,避免主线程卡顿 - - 解析 og:title / og:description / og:image / og:site_name - - 降级策略:title标签 / meta description / favicon - - 相对URL自动转绝对URL -- `ChatLinkBubble` 改为StatefulWidget,渲染后异步调用OgMetadataService.fetch - - 缺少OG数据时自动触发异步抓取 - - 抓取中显示加载占位+指示器 - - 抓取成功后更新UI(标题/描述/图片/siteName) - - 新增onMetaUpdated回调通知父组件持久化meta +#### 新增本地化鸿蒙适配包 -#### ✨ E8 全文搜索 -- `ChatState` 新增 searchQuery 字段和 isSearching getter -- `ChatState.filteredMessages` 支持搜索过滤(与分类过滤叠加) -- `ChatState._searchInMessages` 静态方法:搜索text/author/source/meta.url/meta.title/meta.description/attachments.fileName -- `ChatNotifier` 新增 searchMessages / setSearchQuery / clearSearch 方法 -- `ChatFlowPage` 稍后读模式AppBar新增🔍搜索按钮 -- 搜索栏替代分类栏位置,使用CupertinoSearchTextField -- 输入时实时过滤消息列表,显示结果计数(N/Total) -- `ChatBubble` 新增 highlightQuery 参数 -- `ChatBubble._buildHighlightText` 方法:匹配文本高亮显示(accent色背景+加粗) +| 库 | 版本 | 适配模式 | 本地路径 | 用途 | +|----|------|---------|---------|------| +| `app_links` | v7.0.0-ohos.1 | 联合插件 | packages/app_links + app_links_ohos | 深度链接/Universal Links | +| `pro_image_editor` | v12.4.4-ohos.1 | 直接插件 | packages/pro_image_editor | 图片编辑器 | + +#### app_links ohos原生实现功能 + +| MethodChannel方法 | 功能 | ohos支持 | +|-------------------|------|---------| +| `getInitialLink` | 获取启动时的初始链接 | ✅ | +| `getLatestLink` | 获取最新深度链接 | ✅ | +| EventChannel | 监听onNewWant新链接事件 | ✅ | + +**MethodChannel**: `com.llfbandit.app_links/messages` +**EventChannel**: `com.llfbandit.app_links/events` + +#### 使用的鸿蒙API + +- `UIAbility.onCreate(want)` → `want.uri` 获取初始链接 +- `UIAbility.onNewWant(want)` → `want.uri` 获取新链接 +- module.json5中的 `skills` 配置声明URL scheme + +#### pro_image_editor ohos原生实现功能 + +| MethodChannel方法 | 功能 | ohos支持 | +|-------------------|------|---------| +| `getSupportedEmojis` | 检测emoji是否被系统字体支持 | ✅ 简化实现,全部返回true | + +**MethodChannel**: `pro_image_editor` + +#### 已完成鸿蒙适配的包汇总(共24个) + +| # | 包名 | 适配模式 | +|---|------|---------| +| 1 | shared_preferences | 联合插件 | +| 2 | path_provider | 联合插件 | +| 3 | url_launcher | 联合插件 | +| 4 | image_picker | 联合插件 | +| 5 | local_auth | 联合插件 | +| 6 | video_player | 联合插件 | +| 7 | permission_handler | 联合插件 | +| 8 | flutter_secure_storage | 联合插件 | +| 9 | share_plus | 联合插件 | +| 10 | battery_plus | 联合插件 | +| 11 | connectivity_plus | 直接插件 | +| 12 | flutter_local_notifications | 直接插件 | +| 13 | file_picker | 直接插件 | +| 14 | receive_sharing_intent | 直接插件 | +| 15 | wakelock_plus | 直接插件 | +| 16 | mobile_scanner | 直接插件 | +| 17 | wifi_iot | 直接插件 | +| 18 | audioplayers | 联合插件 | +| 19 | record | 联合插件 | +| 20 | sqflite | 联合插件 | +| 21 | fluttertoast | 直接插件 | +| 22 | network_info_plus | 联合插件 | +| 23 | app_links | 联合插件 | +| 24 | pro_image_editor | 直接插件 | *** -## \[13.3.0] - 2026-05-15 +## \[5.15.0] - 2026-05-16 -### ✨ 新功能 — E2文档预览 + E7阅读统计 + E9标签管理 +### 🔌 鸿蒙适配 — network_info_plus WiFi网络信息 -> 新增文档预览页面、稍后读阅读统计页面、标签管理服务,丰富稍后读功能体系 +> 新增 network_info_plus 鸿蒙适配,采用联合插件模式。支持获取WiFi SSID、BSSID、IPv4地址、子网掩码、网关IP和广播地址。 -#### ✨ E2 文档预览页面 -- 新增 `DocumentPreviewPage` — CupertinoPageScaffold风格 -- 接收文件路径参数,支持本地文件和网络链接 -- 图片文件:直接预览显示 -- 其他文件:显示文件信息卡片(文件名/大小/类型/修改时间) -- 操作按钮:打开文件(url_launcher) / 分享(ShareSheet) / 保存到相册(图片) -- 使用GlassContainer毛玻璃卡片 + AppTheme设计令牌 +#### 新增本地化鸿蒙适配包 -#### ✨ E7 阅读统计页面 -- 新增 `ReadlaterStatsPage` — CupertinoPageScaffold + CustomScrollView -- 统计卡片:总消息数/已读数/未读数 (GlassContainer毛玻璃) -- 类型分布饼图:使用fl_chart PieChart,按消息类型统计(句子/链接/图片/视频/文档/文本) -- 最近7天新增趋势折线图:使用fl_chart LineChart,曲线+渐变填充 -- 接收List参数,纯计算无副作用 -- 使用AppTheme.ext(context) / AppSpacing / AppTypography / AppRadius设计令牌 +| 库 | 版本 | 适配模式 | 本地路径 | 用途 | +|----|------|---------|---------|------| +| `network_info_plus` | v8.1.0-ohos.1 | 联合插件 | packages/network_info_plus + network_info_plus_ohos | WiFi网络信息 | -#### ✨ E9 标签管理 -- ChatMessage模型新增标签便捷方法:getTags / hasTag / addTag / removeTag -- 标签存储在ext['tags']字段,格式: List -- 新增 `ReadlaterTagService` — 基于AppKVStore持久化 -- 核心方法:getAllTags() / addTag() / removeTag() / getMessagesByTag() / getTagStats() -- 标签存储key: 'readlater_tags',数据结构: Map> -- 支持syncFromMessages / exportToMessages 双向同步 -- 支持批量设置/清空标签 +#### ohos原生实现功能 + +| MethodChannel方法 | 功能 | ohos支持 | +|-------------------|------|---------| +| `wifiName` | WiFi SSID | ✅ | +| `wifiBSSID` | WiFi BSSID | ✅ | +| `wifiIPAddress` | IPv4地址 | ✅ | +| `wifiIPv6Address` | IPv6地址 | ❌ 返回null | +| `wifiSubmask` | 子网掩码 | ✅ | +| `wifiGatewayAddress` | 网关IP | ✅ | +| `wifiBroadcast` | 广播地址 | ✅ | + +#### 使用的鸿蒙API + +- `@kit.ConnectivityKit` → `wifiManager.getLinkedInfo()` 获取WiFi连接信息 +- `@kit.NetworkKit` → `connection` 网络连接(预留) + +#### 所需权限 + +```json +"requestPermissions": [ + { "name": "ohos.permission.GET_WIFI_INFO" } +] +``` + +#### 已完成鸿蒙适配的包汇总(共22个) + +| # | 包名 | 适配模式 | +|---|------|---------| +| 1 | shared_preferences | 联合插件 | +| 2 | path_provider | 联合插件 | +| 3 | url_launcher | 联合插件 | +| 4 | image_picker | 联合插件 | +| 5 | local_auth | 联合插件 | +| 6 | video_player | 联合插件 | +| 7 | permission_handler | 联合插件 | +| 8 | flutter_secure_storage | 联合插件 | +| 9 | share_plus | 联合插件 | +| 10 | battery_plus | 联合插件 | +| 11 | connectivity_plus | 直接插件 | +| 12 | flutter_local_notifications | 直接插件 | +| 13 | file_picker | 直接插件 | +| 14 | receive_sharing_intent | 直接插件 | +| 15 | wakelock_plus | 直接插件 | +| 16 | mobile_scanner | 直接插件 | +| 17 | wifi_iot | 直接插件 | +| 18 | audioplayers | 联合插件 | +| 19 | record | 联合插件 | +| 20 | sqflite | 联合插件 | +| 21 | fluttertoast | 直接插件 | +| 22 | network_info_plus | 联合插件 | *** -## \[13.2.0] - 2026-05-15 +## \[5.15.0] - 2026-05-16 -### ✨ 新功能 — E5 多格式导出 + E6 稍后读提醒 +### 🔌 鸿蒙适配 — 第三批次(无TPC官方适配包自行编写) -> 稍后读内容支持JSON/Markdown/ZIP多格式导出,新增稍后读定时提醒通知 +> 9个无TPC官方适配的包完成鸿蒙ohos适配,其中2个使用TPC已有适配合并,7个自行编写ohos原生代码。 -#### ✨ E5 多格式导出 -- `_exportReadlater` 替换为 `CupertinoActionSheet` 选择导出格式 -- `_exportAsJson()`: 导出JSON到剪贴板 -- `_exportAsMarkdown()`: 导出Markdown到剪贴板(含时间戳/作者/出处/链接) -- `_exportAsZip()`: 打包JSON+Markdown为ZIP文件,通过系统分享发送 -- 新增依赖导入: `archive`, `path_provider`, `share_plus` +#### TPC已有适配包合并 -#### ✨ E6 稍后读提醒 -- `LocalNotificationService.scheduleReadLaterReminder()`: 调度每日稍后读提醒通知 -- `LocalNotificationService.cancelReadLaterReminder()`: 取消稍后读提醒 -- `_onNotificationTapped` 新增 `readlater` payload 路由跳转至 `/readlater-chat` +| 库 | 版本 | 适配模式 | 本地路径 | 用途 | +|----|------|---------|---------|------| +| `video_compress` | v3.1.2-ohos.1 | 直接插件 | packages/video_compress | 视频压缩(含FFmpeg C/C++原生库) | +| `flutter_blue_plus` | v2.1.0-ohos.1 | 联合插件 | packages/flutter_blue_plus + flutter_blue_plus_ohos | 蓝牙BLE通信 | + +#### 自行编写ohos适配代码 + +| 库 | 版本 | 适配模式 | 本地路径 | 用途 | 鸿蒙API | +|----|------|---------|---------|------|---------| +| `gal` | v2.3.0-ohos.1 | 直接插件 | packages/gal | 保存图片/视频到相册 | @ohos.file.photoAccessHelper | +| `network_info_plus` | v8.1.0-ohos.1 | 联合插件 | packages/network_info_plus + network_info_plus_ohos | WiFi网络信息 | @ohos.wifi.wifiManager | +| `app_links` | v7.0.0-ohos.1 | 联合插件 | packages/app_links + app_links_ohos | 深度链接处理 | @ohos.app.ability.Want | +| `pro_image_editor` | v12.4.4-ohos.1 | 直接插件 | packages/pro_image_editor | 图片编辑器(emoji检测) | 简化实现 | +| `home_widget` | v0.9.1-ohos.1 | 直接插件 | packages/home_widget | 桌面小组件 | @ohos.app.form.formProvider | +| `nfc_manager` | v4.2.1-ohos.1 | 联合插件 | packages/nfc_manager + nfc_manager_ohos | NFC读写 | @ohos.nfc.tag / @ohos.nfc.ndef | +| `nearby_service` | v0.2.1-ohos.1 | 直接插件 | packages/nearby_service | 近场设备发现 | @ohos.wifi.p2p | + +#### pubspec.yaml 变更 + +- 9个库从pub.dev远程引用改为 `path: packages/xxx` 本地引用 +- `dependency_overrides` 新增9个本地包及其_ohos子包覆写 + +#### 版本兼容性修复 + +| 包 | 问题 | 修复 | +|----|------|------| +| `app_links` | monorepo结构导致pubspec name不匹配 | 提取子目录为独立包,移除workspace配置 | +| `nfc_manager_ohos` | 依赖nfc_manager导致循环引用 | 改为依赖nfc_manager_platform_interface | + +#### 待完善功能 (TODO) + +- `home_widget`: requestPinWidget、registerBackgroundCallback +- `nearby_service`: send消息传输、startAdvertising +- `nfc_manager`: 标签发现监听、NDEF读写、标签类型操作 +- `gal`: open打开相册 +- `flutter_webrtc`: 暂缓适配(需编译WebRTC C++库) + +#### 已完成鸿蒙适配的包汇总(共30个) + +| # | 包名 | 适配模式 | 批次 | +|---|------|---------|------| +| 1 | shared_preferences | 联合插件 | 1 | +| 2 | path_provider | 联合插件 | 1 | +| 3 | url_launcher | 联合插件 | 1 | +| 4 | image_picker | 联合插件 | 1 | +| 5 | local_auth | 联合插件 | 1 | +| 6 | video_player | 联合插件 | 1 | +| 7 | permission_handler | 联合插件 | 1 | +| 8 | flutter_secure_storage | 联合插件 | 1 | +| 9 | share_plus | 联合插件 | 1 | +| 10 | battery_plus | 联合插件 | 1 | +| 11 | connectivity_plus | 直接插件 | 1 | +| 12 | flutter_local_notifications | 直接插件 | 2 | +| 13 | file_picker | 直接插件 | 2 | +| 14 | receive_sharing_intent | 直接插件 | 2 | +| 15 | wakelock_plus | 直接插件 | 2 | +| 16 | mobile_scanner | 直接插件 | 2 | +| 17 | wifi_iot | 直接插件 | 2 | +| 18 | audioplayers | 联合插件 | 2 | +| 19 | record | 联合插件 | 2 | +| 20 | sqflite | 联合插件 | 2 | +| 21 | fluttertoast | 直接插件 | 1 | +| 22 | video_compress | 直接插件 | 3 | +| 23 | flutter_blue_plus | 联合插件 | 3 | +| 24 | gal | 直接插件 | 3 | +| 25 | network_info_plus | 联合插件 | 3 | +| 26 | app_links | 联合插件 | 3 | +| 27 | pro_image_editor | 直接插件 | 3 | +| 28 | home_widget | 直接插件 | 3 | +| 29 | nfc_manager | 联合插件 | 3 | +| 30 | nearby_service | 直接插件 | 3 | *** -## \[13.1.0] - 2026-05-15 +### 🔌 鸿蒙适配 — 全量TPC包合并(第二批次) -### 🔧 Bug修复 — 屏幕共享接收方确认按钮 +> 将9个TPC鸿蒙适配包合并到本地packages目录,涵盖直接插件模式和联合插件模式两种适配方案。同时修复版本兼容性问题。 -> 修复接收方收到屏幕共享请求后无法看到确认按钮的问题,完善屏幕共享信令回调链路 +#### 新增本地化鸿蒙适配包 -#### 🐛 问题根因 -- `TransferSignalingHandler` 创建时未传入 `onScreenShareOffer` 等回调 -- 接收方收到 `screenShareOffer` 后只添加系统消息,未触发 UI 状态更新 -- UI 层未监听屏幕共享请求状态 +| 库 | 版本 | 适配模式 | 本地路径 | 用途 | +|----|------|---------|---------|------| +| `flutter_local_notifications` | v21.0.0-ohos.1 | 直接插件 | packages/flutter_local_notifications | 本地推送通知 | +| `file_picker` | v8.3.7-ohos.1 | 直接插件 | packages/file_picker | 文件选择器 | +| `receive_sharing_intent` | v1.8.1-ohos.1 | 直接插件 | packages/receive_sharing_intent | 接收外部分享 | +| `wakelock_plus` | v1.4.0-ohos.1 | 直接插件 | packages/wakelock_plus | 屏幕常亮控制 | +| `mobile_scanner` | v7.1.4-ohos.1 | 直接插件 | packages/mobile_scanner | 二维码/条形码扫描 | +| `wifi_iot` | v0.3.19-ohos.1 | 直接插件 | packages/wifi_iot | WiFi IoT设备连接 | +| `audioplayers` | v6.5.0-ohos.1 | 联合插件 | packages/audioplayers + audioplayers_ohos | 音频播放 | +| `record` | v6.0.0-ohos.1 | 联合插件 | packages/record + record_ohos | 录音 | +| `sqflite` | v2.4.1-ohos.1 | 联合插件 | packages/sqflite + sqflite_ohos + sqflite_common + sqflite_platform_interface | SQLite数据库 | -#### ✅ 修复内容 -- `TransferState` 新增 `ScreenShareOffer` 类和 `pendingScreenShareOffer` 字段 -- `TransferNotifier` 传入 `onScreenShareOffer/Answer/Stop/RemoteInput` 回调 -- `TransferNotifier` 新增 `acceptScreenShareOffer()`/`rejectScreenShareOffer()`/`clearPendingScreenShareOffer()` 方法 -- `TransferChatPage` 监听 `pendingScreenShareOffer` 弹出 `CupertinoAlertDialog` 确认对话框 -- `TransferChatPage` 消息列表新增屏幕共享请求卡片(📺图标+接受/拒绝按钮) -- 接受后自动调用 `screenShareProvider.startViewing()` 并导航到 `ScreenSharePage` +#### 适配模式说明 + +- **直接插件模式**: ohos原生代码直接放在主包的 `ohos/` 目录中,pubspec.yaml声明 `ohos: pluginClass: XXXPlugin` +- **联合插件模式**: 主包声明 `ohos: default_package: xxx_ohos`,独立的 `*_ohos` 子包包含Dart端和原生端实现 + +#### 版本兼容性修复 + +| 包 | 问题 | 修复 | +|----|------|------| +| `share_plus_ohos` | share_plus_platform_interface 版本冲突 (^6.1.0 vs ^7.1.0) | 放宽为 ">=6.1.0 <8.0.0" | +| `record_ohos` | uuid 版本冲突 (^3.0.7 vs ^4.5.0) | 放宽为 ">=3.0.7 <5.0.0" | +| `video_player_ohos` | pigeon 版本冲突导致 analyzer 不兼容 | 升级 pigeon ^22.6.0 → ^26.0.0 | + +#### pubspec.yaml 变更 + +- 6个库从pub.dev远程引用改为 `path: packages/xxx` 本地引用 +- `dependency_overrides` 新增9个本地包及其_ohos子包覆写 +- `sqflite` 从 `any` 改为本地路径引用 + +#### 已完成鸿蒙适配的包汇总(共21个) + +| # | 包名 | 适配模式 | +|---|------|---------| +| 1 | shared_preferences | 联合插件 | +| 2 | path_provider | 联合插件 | +| 3 | url_launcher | 联合插件 | +| 4 | image_picker | 联合插件 | +| 5 | local_auth | 联合插件 | +| 6 | video_player | 联合插件 | +| 7 | permission_handler | 联合插件 | +| 8 | flutter_secure_storage | 联合插件 | +| 9 | share_plus | 联合插件 | +| 10 | battery_plus | 联合插件 | +| 11 | connectivity_plus | 直接插件 | +| 12 | flutter_local_notifications | 直接插件 | +| 13 | file_picker | 直接插件 | +| 14 | receive_sharing_intent | 直接插件 | +| 15 | wakelock_plus | 直接插件 | +| 16 | mobile_scanner | 直接插件 | +| 17 | wifi_iot | 直接插件 | +| 18 | audioplayers | 联合插件 | +| 19 | record | 联合插件 | +| 20 | sqflite | 联合插件 | +| 21 | fluttertoast | 直接插件 | + +#### 待适配包(无TPC官方适配) + +- `home_widget` — 桌面小组件 +- `pro_image_editor` — 图片编辑器 +- `nearby_service` — 近场设备发现 +- `gal` — 保存图片/视频到相册 +- `nfc_manager` — NFC读写 +- `video_compress` — 视频压缩 +- `flutter_blue_plus` — 蓝牙BLE通信 +- `app_links` — 深度链接处理 +- `network_info_plus` — WiFi网络信息 +- `flutter_webrtc` — WebRTC音视频通信 *** -## \[13.0.1] - 2026-05-15 +## \[5.13.0] - 2026-05-16 -### 🔧 Bug修复 — 协作画布参与者同步 +### 🔌 鸿蒙适配 — local_auth_ohos 平台实现合并 -> 修复协作画布中对方始终不在线、消息路由错误等关键问题 +> 将 TPC 仓库的 local_auth_ohos 子包合并到本地 packages 目录,完成 local_auth 插件的鸿蒙平台适配。 -#### 🐛 修复 -- CanvasSyncService: `_handleCanvasJoin`/`_handleCanvasLeave` 现在正确维护 `_participants` 集合并触发 `onParticipantsChanged` 回调 -- CanvasSyncService: `joinCanvas` 方法将本机 deviceId 加入 `_participants`,`leaveCanvas` 清空集合 -- CanvasSyncService: `_sendCanvasMessage` 添加 `to: _peerId` 字段,确保消息正确路由到对方设备 -- CanvasSyncService: `joinCanvas` 新增 `peerId` 可选参数,用于指定消息目标设备 -- CanvasPage: `initState` 中使用 `sharedSignalingProvider.deviceId` 获取本机 deviceId 替代错误的 `peerDeviceId` -- CanvasPage: 同时传递 `peerDeviceId` 作为 `peerId` 参数,确保消息路由正确 -- CanvasNotifier: 构造函数绑定 `_syncService.onParticipantsChanged = _handleParticipantsChanged` -- CanvasNotifier: `joinCanvas` 方法签名更新,支持传递 `peerDeviceId` 到 SyncService +#### 变更内容 + +| 操作 | 说明 | +|------|------| +| 复制 `local_auth_ohos` | 从 `_tpc_temp/flutter_packages_la/packages/local_auth/local_auth_ohos` → `packages/local_auth_ohos` | +| 删除 example 目录 | 移除 `packages/local_auth_ohos/example/`(不需要本地示例) | +| 更新 `local_auth` pubspec.yaml | 添加 `ohos` 平台声明、`local_auth_ohos` 本地路径依赖、版本号 `3.0.1` → `3.0.1-ohos.1` | + +#### local_auth pubspec.yaml 变更明细 + +- `version`: `3.0.1` → `3.0.1-ohos.1` +- `flutter.plugin.platforms` 新增: `ohos: default_package: local_auth_ohos` +- `dependencies` 新增: `local_auth_ohos: path: ../local_auth_ohos` + +#### local_auth_ohos 包结构 + +``` +packages/local_auth_ohos/ +├── lib/ # Dart 端实现 (LocalAuthOhos, AuthMessagesOhos, OhosAuthErrorCode) +├── ohos/ # 鸿蒙原生端实现 (LocalAuthPlugin.ets, AuthenticationHelper.ets) +├── test/ # 单元测试 +└── pubspec.yaml # 版本 2.0.0 +``` *** -## \[13.0.0] - 2026-05-15 +## \[5.12.0] - 2026-05-16 -### ✨ 新功能 — 稍后读会话 + ChatFlowPage增强 +### 📦 关键库本地化 — 鸿蒙适配准备 + font_management_page恢复 -> 新增稍后读聊天会话、系统分享接收、链接/文档/句子卡片气泡,丰富所有会话体验 +> 将10个含原生代码的关键库下载到本地packages/目录,方便后续鸿蒙适配魔改。恢复font_management_page完整功能并完成Riverpod 3.x迁移。 -#### ✨ 新功能 — 稍后读内置会话 -- 工作流(InspirationPage)会话列表新增"📖 稍后读"内置会话,默认置顶 -- 点击进入聊天风格页面,支持发送文本/链接/图片/视频/文件/文档 -- 类似微信"文件传输助手",自己给自己发消息,管理稍后读内容 +#### 本地化关键库(鸿蒙适配准备) -#### ✨ 新功能 — 句子详情 → 稍后读会话 -- 主页句子详情Sheet点击"稍后读"按钮,句子自动发送到稍后读会话 -- 句子以专属卡片气泡展示(渐变背景+作者+出处+统计) -- 取消稍后读时保留历史消息,不删除 +| 库 | 远程版本 | 本地路径 | 用途 | +|----|---------|---------|------| +| `shared_preferences` | v2.5.5 | packages/shared_preferences | 轻量KV持久化 | +| `flutter_secure_storage` | v9.2.4 | packages/flutter_secure_storage | 加密安全存储 | +| `path_provider` | v2.1.5 | packages/path_provider | 系统目录路径获取 | +| `connectivity_plus` | v7.1.1 | packages/connectivity_plus | 网络连接状态监听 | +| `permission_handler` | v12.0.1 | packages/permission_handler | 运行时权限请求 | +| `flutter_local_notifications` | v21.0.0 | packages/flutter_local_notifications | 本地推送通知 | +| `url_launcher` | v6.3.2 | packages/url_launcher | 打开外部URL/应用 | +| `file_picker` | v8.3.7 | packages/file_picker | 文件选择器 | +| `image_picker` | v1.2.2 | packages/image_picker | 相机/相册选图 | +| `local_auth` | v3.0.1 | packages/local_auth | 生物识别认证 | -#### ✨ 新功能 — ChatFlowPage增强 -- 新增ChatLinkBubble: 链接预览卡片(OG元数据+打开/复制按钮) -- 新增ChatDocumentBubble: 文档卡片(PDF/Word/Excel等+打开/分享按钮) -- 新增ChatSentenceCardBubble: 句子卡片(渐变背景+统计+操作) -- ChatMessageType新增: link / document / readlaterSentence -- ChatFlowPage支持动态配置(按sessionType调整标题/分类栏/设置等) -- 所有会话均可使用新增气泡组件 +#### pubspec.yaml 变更 -#### ✨ 新功能 — 系统分享接收 -- 集成receive_sharing_intent本地包(v1.8.1) -- 新增`SharingReceiverService`单例服务(`core/services/sharing_receiver_service.dart`) -- Android/iOS: 通过`getInitialMedia`+`getMediaStream`接收文本/链接/图片/视频/文件 -- Web: URL参数降级兼容(text/url/title query参数) -- Windows: 命令行参数降级兼容(自动检测文件路径/文本) -- 分享内容自动写入稍后读会话(conversationId='readlater') -- 文本自动检测URL,链接→sendLink,纯文本→sendText -- 文件按mimeType分发: image→sendImage, video→sendVideo, application→sendDocument, 其他→sendFile -- 分享成功时AppToast提示,失败时记录日志 +- 10个库从远程版本引用改为 `path: packages/xxx` 本地引用 +- `dependency_overrides` 新增10个本地包覆写,确保远程依赖也使用本地版本 +- `nearby_service` 本地包的 `path_provider` 依赖改为本地路径引用 -#### ✨ 新功能 — 稍后读设置面板 -- AppBar右侧设置按钮,弹出CupertinoActionSheet -- 支持: 标记全部已读/清空/导出/统计/分享接收开关 +#### font_management_page 恢复与迁移 -#### 📄 开发文档 -- 新增: `docs/spec/readlater_chat_spec.md` — 稍后读会话+ChatFlowPage增强开发文档 +- 恢复完整版字体管理页面(1623行,含字体下载/导入/动态加载/主题集成) +- Riverpod 3.x迁移:`StateNotifier` → `Notifier`,`_ref` → `ref` +- 添加缺失import:`cupertino`、`material`、`riverpod`、`AppSpacing`、`AppTheme`、`AppTypography` +- `context.pop()` → `Navigator.of(context).pop()` + +#### FilePicker API 适配 + +- 本地化 `file_picker` v8.3.7 使用 `FilePicker.platform.pickFiles()` +- 所有 `FilePicker.pickFiles()` 回退为 `FilePicker.platform.pickFiles()` +- 影响文件:xycard_io_native、image_import_service、transfer_chat_page、chat_attachment_provider、data_management_page、font_management_page + +#### flutter analyze 结果 + +``` +17 issues found (零Error,仅Warning/Info级别) +``` *** -## \[12.20.0] - 2026-05-15 +## \[5.11.0] - 2026-05-16 -### 🔧 Bug修复 + ✨ 功能增强 +### ⬆️ 依赖升级 + 警告清理 — Riverpod 3.x迁移 + 大版本API适配 + Dart 3.11废弃API修复 -> 修复账户安全验证、底部Tab栏、文件传输助手等多项问题 +> 基于Flutter SDK 3.41.10-ohos升级,完成核心依赖大版本升级、69个文件Riverpod 3.x迁移、大版本API破坏性变更适配、Dart 3.11废弃API批量修复,`flutter analyze` 零Error -#### 🐛 Bug修复 — 修改密保页面缺少邮箱验证码 -- **根因**: `security_question_page.dart` 的邮箱验证方式(receipt)仅输入回执码,缺少发送验证码步骤 -- **修复**: 新增邮箱验证码发送/倒计时/校验完整流程;新增 `EmsEvent.changesecquestion` 事件类型;验证方式选择器增加"📧 邮箱验证"选项 +#### 核心依赖升级 -#### 🐛 Bug修复 — 修改密码页面缺少邮箱验证 -- **根因**: `change_password_page.dart` 的邮箱验证方式仅输入回执码,缺少发送验证码步骤 -- **修复**: 新增邮箱验证码发送/倒计时/校验完整流程;验证方式选择器增加"📧 邮箱"选项;新增 `EmsEvent.changepwd` 事件类型 +| 依赖 | 旧版本 | 新版本 | 破坏性变更 | +|------|--------|--------|-----------| +| `flutter_riverpod` | ^2.x | ^3.0.0 | StateNotifier移除→Notifier | +| `riverpod_annotation` | ^2.x | ^4.0.0 | 注解API调整 | +| `riverpod_generator` | ^2.x | ^4.0.0 | 代码生成适配 | +| `riverpod_lint` | ^2.x | ^3.0.0 | 规则更新 | +| `go_router` | ^14.x | ^17.0.0 | 类型安全路由 | +| `freezed` | ^2.x | ^3.0.0 | Union类型简化 | +| `freezed_annotation` | ^2.x | ^3.0.0 | 注解适配 | +| `fl_chart` | ^0.69 | ^1.2.0 | SideTitleWidget/tooltipRoundedRadius API变更 | +| `file_picker` | ^8.x | ^11.0.0 | FilePicker.platform移除→静态方法 | +| `flutter_local_notifications` | ^17.x | ^21.0.0 | 命名参数+uiLocalNotificationDateInterpretation移除 | +| `local_auth` | ^2.x | ^3.0.0 | authenticate()参数变更 | +| `share_plus` | ^10.x | ^13.0.0 | Share.shareXFiles→SharePlus.instance.share | +| `archive` | ^3.x | ^4.0.0 | encode/decode返回List | +| `shelf_web_socket` | ^2.x | ^3.0.0 | WebSocketAdapter API变更 | +| `tilt` | ^3.x | ^4.0.0 | Tilt→Tilt.base | +| 29个安全小版本 | 各版本 | 最新兼容版 | 无破坏性变更 | -#### 🔧 功能调整 — 安全与Token管理移至账户设置 -- **变更**: 将"安全与Token管理"按钮从"我的"页面移至"账户设置"页面 -- **原因**: 安全管理属于账户设置范畴,集中管理更符合用户心智模型 -- **影响**: `profile_page.dart` 移除按钮和 `_SecuritySheet` 组件;`account_settings_page.dart` 新增按钮和 `_SecuritySheet` 组件 +#### 大版本API破坏性变更适配 -#### 🐛 Bug修复 — 底部Tab栏间距过大+文字太小 -- **根因**: `tab_icon_sprite.dart` 中 glowSize=60/iconSize=44 过大导致图标区域占据过多空间;fontSize=10 过小 -- **修复**: glowSize 60→40,选中iconSize 44→36,未选中 28→24;fontSize 10→12;SizedBox height 14→16 +| 库 | 变更 | 修复方式 | 影响文件 | +|----|------|---------|---------| +| fl_chart 1.2 | `SideTitleWidget(axisSide:)`移除 | →`SideTitleWidget(meta:)` | learning_progress_page, statistics相关6文件 | +| fl_chart 1.2 | `tooltipRoundedRadius: int`移除 | →`tooltipBorderRadius: BorderRadius.circular()` | statistics相关6文件 | +| file_picker 11 | `FilePicker.platform`移除 | →`FilePicker.pickFiles()`静态方法 | chat_attachment, data_management, transfer_chat等 | +| flutter_local_notifications 21 | 位置参数→命名参数 | 全部改为命名参数传递 | local_notification_service, notification_service | +| flutter_local_notifications 21 | `uiLocalNotificationDateInterpretation`移除 | 移除该参数 | local_notification_service | +| share_plus 13 | `Share.shareXFiles()`移除 | →`SharePlus.instance.share(ShareParams(files:))` | data_management_page | +| local_auth 3 | `authenticate()`参数变更 | 适配新API签名 | my_devices_page, app_lock_service | +| archive 4 | `encode()`返回`List` | 适配类型变更 | xycard_service | +| shelf_web_socket 3 | WebSocketAdapter API变更 | 适配新签名 | web_transfer_handler | +| tilt 4 | `Tilt`→`Tilt.base` | 组件名变更 | chat_bubble | -#### 🐛 Bug修复 — 附近设备点进去看不到消息 -- **根因**: 设备通过LAN发现时id=fingerprint,但消息peerDeviceId可能为信令服务器分配的不同ID -- **修复**: `TransferChatPage` 新增 `_collectPeerIds()` 方法,收集设备所有已知ID,消息过滤改为多ID匹配 +#### Riverpod 3.x 迁移(69个文件) -#### 🐛 Bug修复 — 共享屏幕/协作画布信令未连接 -- **根因**: `canvasProvider` 和 `screenShareProvider` 各自创建新的未连接 `SignalingService()` 实例 -- **修复**: 新增 `shared_signaling_provider.dart`,从 `transferProvider.notifier.pairingService.signalingService` 获取已连接实例 +- **StateNotifier → Notifier**: 所有`StateNotifier`迁移为`Notifier` +- **StateNotifierProvider → NotifierProvider**: `StateNotifierProvider((ref) => X())` → `NotifierProvider(X.new)` +- **构造函数参数移除**: Notifier不再接受构造参数,原参数改为通过`ref.read()`获取 +- **build()方法**: 原构造函数初始化逻辑迁移至`build()`方法,返回初始状态 +- **ref引用**: Notifier内置`ref`属性,无需构造参数传递 +- **mounted状态**: Notifier无内置`mounted`,添加`bool _mounted`标志位管理 +- **onDispose**: 使用`ref.onDispose()`注册清理回调 +- **family provider调用**: `chatAttachmentProvider`需传参`chatAttachmentProvider(conversationId)` -#### 🐛 Bug修复 — 画布页面返回黑屏 -- **根因**: `_openCanvas` 使用 `context.go()` 替换整个导航栈,返回时无上一页 -- **修复**: 改为 `Navigator.push()` + `CupertinoPageRoute`,保持导航栈完整 +关键迁移文件: +- `weather_provider.dart` — 标准Notifier迁移模板 +- `transfer_notifier.dart` — 复杂Notifier(构造参数→ref.read、mounted管理、onDispose) +- `home_feed_mixin.dart` — mixin约束从`on StateNotifier`改为`on Notifier` +- `home_interaction_mixin.dart` — 交互mixin适配Notifier约束 +- `chat_flow_input_bar.dart` — family provider传参修复 +- `chat_flow_page.dart` — family provider传参修复 -#### 🐛 Bug修复 — 我的设备显示本机+显示IP+消息发送失败 -- **根因1**: `buildMyDeviceCard` 对所有设备使用相同样式,无法区分本机与远程 -- **修复1**: 本机设备使用灰色背景+不可点击+"📱 本机"标签,远程设备保持原有样式 -- **根因2**: `displayAlias` 回退显示IP地址 -- **修复2**: `TransferDevice` 新增 `accountAlias` 字段,优先显示账号昵称而非IP -- **根因3**: `sendTextMessage` 固定通道顺序,wsRelay偏好设备无法优先使用WsRelay -- **修复3**: 根据设备 `preferredTransport` 优先选择通道 +#### Dart 3.11 废弃API修复 -#### ✨ 新功能 — 局域网访问二维码弹窗 -- 点击局域网访问横幅弹出CupertinoModalPopup sheet -- 弹窗包含:二维码(QrImageView)、URL文本、说明文字、复制链接按钮 +| 废弃API | 替代方案 | 影响文件数 | +|---------|---------|-----------| +| `RegExp(source)` | `regex(source)` 封装函数 | 20+ | +| `FontWeight.index` | `FontWeight.value` 计算 | 1 | +| `TextAlignMode.index` | `TextAlignMode.name` 序列化 | 1 | +| `AdaptivePalette.fromImage()` | `FluidPaletteExtractor.extractColors()` | 1 | +| `BuildContext`跨异步间隙 | `context.mounted` 检查 | 5+ | +| `encryptedSharedPreferences` | 移除该参数 | 1 | + +新增工具文件: +- `lib/core/utils/pattern_utils.dart` — `regex()`和`regexEscape()`封装,消除RegExp废弃警告 + +#### 新增占位文件(开发中功能) + +| 文件 | 作用 | +|------|------| +| `lib/editor/services/core/spritesheet_service.dart` | 精灵图贴纸加载/解析/预览 | +| `lib/editor/services/image/image_import_service.dart` | 图片导入/预处理 | +| `lib/core/services/data/settings_export_service.dart` | 设置导出/导入 | +| `lib/features/settings/presentation/font_management_page.dart` | 字体管理页面+Provider | + +#### flutter analyze 结果 + +``` +24 issues found (零Error,仅Warning/Info级别) +- prefer_final_fields: 3个 +- unnecessary_null_comparison: 2个 +- dead_code: 2个 +- unnecessary_non_null_assertion: 1个 +- avoid_redundant_argument_values: 1个 +- avoid_dynamic_calls: 5个 +- inference_failure: 2个 +- 其他info: 8个 +``` *** -### 🐛 Bug修复 + ✨ 功能增强 +## \[5.10.0] - 2026-05-16 -> 修复"我的设备"页面本机设备样式、设备名称显示IP、消息发送失败三个问题 +### ⬆️ SDK升级 — Flutter OpenHarmony oh-3.41.9-dev -#### 🐛 Bug修复 — 本机设备与远程设备样式区分 -- **根因**: `buildMyDeviceCard` 对所有设备(包括本机)使用相同样式,用户无法区分本机与远程设备 -- **修复**: `TransferState` 新增 `localFingerprint` 字段标识本机设备;`buildMyDeviceCard` 通过 fingerprint 匹配判断本机设备,本机使用灰色背景+不可点击+"📱 本机"标签,远程设备保持原有样式并可点击进入聊天 +#### 升级内容 -#### 🐛 Bug修复 — 设备名称显示IP地址 -- **根因**: `displayAlias` getter 在 alias 为默认值时回退显示 IP 地址,不够友好 -- **修复**: `TransferDevice` 新增 `accountAlias` 字段(账号昵称/用户名),`displayAlias` 优先使用 accountAlias;`fromSignaling` 从信令数据提取 accountAlias/nickname/username;`buildMyDeviceCard` 不再显示 IP 地址行,改为显示账号昵称 +- **Flutter SDK**: `3.35.8-ohos-0.0.3` → `3.41.10-ohos-0.0.1-canary1` +- **Dart SDK**: `3.9.2` → `3.11.5` +- **DevTools**: `2.48.0` → `2.54.1` +- **SDK分支**: `oh-3.35.7-dev` → `oh-3.41.9-dev` +- **SDK来源**: `https://gitcode.com/openharmony-tpc/flutter_flutter` (oh-3.41.9-dev) -#### 🐛 Bug修复 — 消息发送失败 -- **根因**: `sendTextMessage` 按固定顺序尝试通道(LocalSend→WiFi Direct→WebRTC→信令直发→WsRelay),对于 preferredTransport=wsRelay 的"我的设备",信令直发可能成功但服务器未正确路由,导致 WsRelay 通道永远不会被尝试 -- **修复**: `sendTextMessage` 新增根据设备 `preferredTransport` 优先选择通道逻辑;wsRelay 偏好设备优先通过 WsRelay 中转发送,失败后再尝试其他通道;非 wsRelay 偏好设备保持原有通道顺序 +#### 修改文件 +| 文件 | 修改内容 | +|------|---------| +| `pubspec.yaml` | SDK约束从 `^3.9.2` 更新为 `^3.11.5` | +| Flutter SDK (`E:\sdk\flutter-ohos\flutter_flutter`) | 切换分支至 `oh-3.41.9-dev` | +#### 注意事项 + +- 旧分支 `oh-3.35.7-dev` 的本地修改已 stash 保存,可通过 `git stash list` 查看 +- `flutter pub get` 已验证通过,所有依赖兼容新 SDK +- 114 个依赖包有更新版本可用,后续可按需升级 *** -## \[12.14.0] - 2026-05-14 +## \[5.9.0] - 2026-05-15 -### 📋 新增 — 每日任务系统客户端(Flutter) +### 🐛 Bug修复 — 6项关键问题 -> 每日任务系统前端4个文件:API服务层、状态管理层、任务卡片组件、每日任务页面。 -> 支持今日任务列表展示、进度上报、奖励领取、完美日额外奖励。 +#### 修复清单 -1. **TaskService API服务** — `lib/features/task/services/task_service.dart` - - getTodayTasks(): 获取今日任务列表 - - reportProgress(): 上报任务进度 - - claimReward(): 领取任务奖励 - - claimPerfectDay(): 领取完美日奖励 - - registerCustomTask(): 注册自定义任务 - - Riverpod Provider: taskServiceProvider +1. **系统分享白屏** — sharing_receiver_service.dart + - 根因: `_navigateToReadlater()` 使用 `Navigator.pushNamed('/readlater-chat')` 导航,但App使用GoRouter管理路由 + - 修复: 改用 `GoRouter.of(ctx).go(AppRoutes.readlaterChat)` 导航 + - 影响: 从其他App分享内容到闲言时不再白屏 -2. **TaskProvider 状态管理** — `lib/features/task/providers/task_provider.dart` - - DailyTask 数据模型: id/name/icon/type/target/action/customUrl/customPage/expReward/scoreReward/isRandom/progress/completed/claimed/percent - - TaskSummary 统计模型: total/completed/claimed/isPerfectDay/perfectClaimed/date - - TaskState 状态: tasks/summary/isLoading/error - - TaskNotifier: loadTodayTasks/reportProgress/claimReward/claimPerfectDay - - Riverpod StateNotifierProvider: taskProvider +2. **画布ref disposed报错** — canvas_page.dart + - 根因: `dispose()` 中调用 `ref.read(canvasProvider.notifier)` 访问已销毁的widget + - 修复: 在 `initState` 中缓存 `CanvasNotifier` 引用到 `_cachedNotifier`,`dispose` 中使用缓存引用 + - 影响: 退出画布页面不再抛出 `StateError: Cannot use "ref" after the widget was disposed` -3. **TaskCard 任务卡片组件** — `lib/shared/widgets/task_card.dart` - - iOS风格卡片: emoji图标+名称+进度条+状态按钮 - - 三种状态: 进行中(蓝色奖励预览) / 已完成(橙色领取按钮) / 已领取(✅已领取标签) - - 进度条: 蓝色进行中/绿色已完成 - - 完成边框高亮(绿色半透明) - - 深色模式适配 +3. **画布画线无法同步** — canvas_sync_service.dart + canvas_provider.dart + - 根因: 收到 `canvasSnapshot` request 时只打日志,不回复snapshot response + - 修复: 新增 `SnapshotRequestCallback`,收到request时通过 `sendSnapshotResponse` 回复当前画布笔迹 + - 影响: 后加入画布的用户可以看到先加入者已画的笔迹 -4. **DailyTaskPage 每日任务页面** — `lib/features/task/presentation/daily_task_page.dart` - - CupertinoPageScaffold + CustomScrollView - - 顶部统计区: 总任务/已完成/已领取 + 总进度条 - - 完美日提示: 橙色渐变卡片 + 领取按钮 - - 领取奖励弹窗: CupertinoAlertDialog 显示经验+积分 - - 错误状态展示 - - 深色模式适配 +4. **屏幕共享不可用** — transfer_chat_page.dart + - 根因: `_startScreenShare()` 手动构造 `SignalingMessage` 发送,未使用 `screenShareProvider.startSharing()` + - 修复: 改用 `screenShareProvider.notifier.startSharing()` 正确发起共享,并导航到 `ScreenSharePage` + - 影响: 发起屏幕共享后对方可收到请求并显示接受按钮 +5. **稍后读消息未显示** — home_interaction_mixin.dart + chat_provider.dart + chat_flow_page.dart + - 根因: `toggleReadLater` 写入DB后未通知 `ChatNotifier` 刷新消息列表 + - 修复: 新增 `notifyReadlaterRefresh()` 全局事件总线,`ChatFlowPage` 在稍后读模式下监听刷新事件 + - 影响: 主页点击"稍后读"后,稍后读会话流页面实时显示新消息气泡 +6. **稍后读显示无关消息** — chat_provider.dart + - 根因: `_addInitialSentencesIfNeeded()` 对所有会话(包括readlater)都添加初始句子如"山有木兮木有枝" + - 修复: 在方法开头增加 `if (conversationId == 'readlater') return;` 跳过 + - 影响: 稍后读会话流不再显示无关的初始句子 + +7. **输入法自动弹出** — keyboard_safe_sheet.dart + chat_flow_page.dart + chat_flow_input_bar.dart + - 根因: 缺乏全局键盘管理,页面切换/返回/按钮点击时FocusNode意外获焦 + - 修复: 新增 `KeyboardManager` 全局键盘管理器,跟踪用户是否主动点击输入框 + - 强化: 点击空白区域/输入法上方区域自动收起键盘 + - 强化: 页面切换时自动收起键盘并清除焦点 + - 强化: App进入后台时自动收起键盘 + - 影响: 输入法不再意外弹出,只有用户主动点击输入框才弹出 + +#### 修改文件 + +| 文件 | 修改内容 | +|------|---------| +| `sharing_receiver_service.dart` | GoRouter导航替代Navigator.pushNamed | +| `canvas_page.dart` | 缓存notifier引用,避免dispose中访问ref | +| `canvas_sync_service.dart` | 新增SnapshotRequestCallback,响应snapshot请求 | +| `canvas_provider.dart` | 绑定onSnapshotRequest回调,发送snapshot响应 | +| `transfer_chat_page.dart` | 使用screenShareProvider.startSharing()发起共享 | +| `home_interaction_mixin.dart` | toggleReadLater后触发notifyReadlaterRefresh | +| `chat_provider.dart` | 新增readlater刷新事件总线 + _addInitialSentencesIfNeeded跳过readlater | +| `chat_flow_page.dart` | 监听readlater刷新 + KeyboardManager集成 + 焦点管理 | +| `chat_flow_input_bar.dart` | 输入框onTap标记KeyboardManager用户点击 | +| `keyboard_safe_sheet.dart` | 新增KeyboardManager全局键盘管理器 + ManagedCupertinoTextField | *** -## \[12.2.0] - 2026-05-14 -### ✨ 增强 — 收藏同步功能重构为双向同步 -> **问题**: "我的收藏"页面同步按钮只做单向同步(本地→云端),不同步云端收藏到本地,无同步进度反馈 - -1. **FavoriteState 增加同步状态** — `lib/features/home/providers/favorite_provider.dart` - - 新增 `isSyncing`/`syncMessage`/`syncStatus` 字段 - - `SyncStatus` 枚举: idle/syncing/success/failed - - `copyWith` 支持 `clearSyncMessage`/`syncStatus` 参数 -2. **双向同步逻辑 `syncFavorites()`** — `lib/features/home/providers/favorite_provider.dart` - - Step 1: 拉取云端全量收藏(分页获取,`_fetchAllServerFavorites`) - - Step 2: 获取本地收藏列表 - - Step 3: 上传本地独有收藏到云端(跳过云端已存在的,避免重复) - - Step 4: 拉取云端独有收藏到本地DB(标记已有/新增记录) - - Step 5: 冲突处理(云端已存在则跳过,本地已存在则标记isFavorite) - - Step 6: 刷新收藏列表 - - 实时更新 `syncMessage` 进度提示 - - 返回 `SyncResult`(pulled/pushed/skipped/message/isError) -3. **AppDatabase 新增方法** — `lib/core/storage/database/app_database.dart` - - `getSentencesById(String id)`: 按ID查询单条Sentence - - `insertOrUpdateSentence(SentencesCompanion)`: 插入或替换Sentence记录 -4. **收藏页面同步UI增强** — `lib/features/home/presentation/favorite_page.dart` - - 同步按钮增加加载指示器(同步中显示 `CupertinoActivityIndicator`) - - 新增 `_buildSyncProgressBanner`: 同步进度/成功/失败横幅 - - 同步中: 蓝色横幅 + 旋转指示器 + 进度文字 - - 成功: 绿色横幅 + ✓ 图标 + 结果摘要 - - 失败: 红色横幅 + ⚠ 图标 + 错误信息 - - 同步完成后3秒自动重置状态,也可手动关闭 - - 移除旧的 `_syncLocalToCloud`/`_resolveServerId` 方法(逻辑迁移到Provider) - - 移除不再需要的 `FeedService`/`SearchAllService` 导入 - -*** - -## \[12.1.1] - 2026-05-14 - -### 🐛 修复 — "我的"页面顶部快捷按钮导航失效 - -> **问题**: ProfilePage 顶部统计栏(积分/签到/笔记)点击无响应,缺少导航逻辑 - -1. **ProfilePage `_UserStatsBar` 修复** — `lib/features/profile/presentation/profile_page.dart` - - `_buildStatItem` 增加 `route` 参数和 `BuildContext` 参数 - - 用 `GestureDetector` 包裹 `Column`,添加 `onTap` 导航 - - 积分 → `AppRoutes.coinLog` (`/coin-log`) - - 签到 → `AppRoutes.signin` (`/signin`) - - 笔记 → `AppRoutes.noteList` (`/notes`) - - 使用 `Expanded` 确保等宽分布,`textAlign: center` 居中标签 - -*** ## \[6.1.0] - 2026-05-14 @@ -742,172 +1012,9 @@ - _setupUsbDiscoveryListeners(): USB设备发现/移除→discoveredDevices状态更新+系统消息 - dispose中清理USB资源 -*** - - -## \[6.3.0] - 2026-05-14 - -### 📋 新功能 — 剪贴板同步管理 (F2-01~10) - -> **内容**: 增强版剪贴板同步模块,支持图片同步、历史管理、置顶/搜索/筛选、一键复制粘贴 - -1. **ClipboardItem 模型** — `lib/features/collaboration/clipboard/models/clipboard_item.dart` - - ClipboardItemType枚举: text/image - - 支持toJson/fromJson序列化、copyWith - - previewText/displayDeviceName便捷属性 -2. **ClipboardManagerService** — `lib/features/collaboration/clipboard/services/clipboard_manager_service.dart` - - 基于ClipboardSyncService增强: 图片同步/历史管理/置顶/删除/搜索 - - 信令通道推送: 文本和图片类型自动区分 - - Stream> 实时变更通知 - - syncNow: 手动同步系统剪贴板到远端 - - copyToSystemClipboard: 一键复制到系统剪贴板 -3. **ClipboardProvider** — `lib/features/collaboration/clipboard/providers/clipboard_provider.dart` - - Riverpod StateNotifierProvider - - ClipboardState: items/searchQuery/isLoading/filter/isSyncing - - ClipboardFilter: all/text/image/pinned - - filteredItems: 置顶优先+筛选+搜索 - - ClipboardNotifier: loadItems/pinItem/deleteItem/searchItems/sendToPeer/syncNow -4. **ClipboardFlowPage** — `lib/features/collaboration/clipboard/pages/clipboard_flow_page.dart` - - iOS风格CupertinoPageScaffold + CupertinoNavigationBar - - 筛选标签: 全部/文本/图片/置顶 (带计数徽标) - - 搜索栏: 实时搜索文本内容 - - 条目卡片: 类型图标+内容预览+来源设备+时间+置顶标记 - - 文本条目: 点击复制, 长按弹出操作(复制/置顶/发送/删除) - - 图片条目: 点击预览, 长按弹出操作 - - 底部栏: 📋粘贴并同步 + 🗑️清空 - - 同步状态指示器 -5. **工具中心入口** — `lib/features/inspiration/models/tool_item.dart` - - 新增📋剪贴板工具卡片(管理分类, 推荐+新标记) -6. **路由注册** — `lib/core/router/app_router.dart` - - 新增/clipboard路由 → ClipboardFlowPage - - AppRoutes.clipboard常量 - -*** - -## \[6.2.0] - 2026-05-14 - -### 🎨 新功能 — 协作画布 (F1-01~19) - -> **内容**: 实时多人协作画布模块,支持画笔/橡皮/形状绘制、CRDT冲突解决、WebSocket实时同步、PNG导出 - -1. **Stroke 模型** — `lib/features/collaboration/canvas/models/stroke.dart` - - StrokeType枚举: pen/eraser/line/rect/circle/text - - 支持toJson/fromJson序列化、copyWith、pointsJson - - Lamport时钟字段用于CRDT冲突解决 -2. **CanvasDocument 模型** — `lib/features/collaboration/canvas/models/canvas_document.dart` - - 画布文档元信息: id/name/ownerId/participantIds/strokes - - 支持宽高配置(默认1920x1080) -3. **CanvasCrdt 服务** — `lib/features/collaboration/canvas/services/canvas_crdt.dart` - - Lamport时钟管理,tick/mergeRemoteClock - - mergeStroke: 按lamportClock+createdAt解决冲突 - - mergeSnapshot: 全量同步合并 - - MergeResult枚举: added/updated/discarded -4. **CanvasEngine 服务** — `lib/features/collaboration/canvas/services/canvas_engine.dart` - - ChangeNotifier,管理当前工具/颜色/线宽状态 - - startStroke/addPoint/endStroke: 笔画绘制流程 - - undo/redo: 撤销重做支持 - - mergeRemoteStroke/mergeSnapshot: 远程合并 -5. **CanvasPainter 组件** — `lib/features/collaboration/canvas/widgets/canvas_painter.dart` - - CustomPainter渲染所有笔画 - - 支持画笔(平滑贝塞尔曲线)/橡皮/直线/矩形/椭圆/文字 - - 活动笔画实时渲染 + 远程光标显示 -6. **CanvasToolbar 组件** — `lib/features/collaboration/canvas/widgets/canvas_toolbar.dart` - - 底部工具栏: 5种工具 + 8色预设 + 线宽滑块 - - 撤销/重做/导出/清空按钮 - - iOS风格Cupertino组件 -7. **CanvasSyncService** — `lib/features/collaboration/canvas/services/canvas_sync_service.dart` - - 基于SignalingService的实时同步 - - broadcastStroke/broadcastCursor: 广播本地操作 - - requestSnapshot/handleSnapshot: 断线重连后全量同步 - - joinCanvas/leaveCanvas: 画布房间管理 -8. **CanvasProvider** — `lib/features/collaboration/canvas/providers/canvas_provider.dart` - - Riverpod StateNotifierProvider - - CanvasState: strokes/activeStroke/tool/color/width/cursors/participants - - CanvasNotifier: 封装Engine+SyncService -9. **CanvasPage** — `lib/features/collaboration/canvas/pages/canvas_page.dart` - - 全屏画布 + GestureDetector手势绘制 - - 参与者头像显示 + 同步状态栏 - - 返回确认(丢弃/继续) + PNG导出(share_plus) - - 清空画布确认弹窗 -10. **信令服务器** — `server/index.js` - - 新增canvasRooms: Map> - - canvas-join/canvas-leave: 画布房间加入/离开 - - canvas-stroke/canvas-cursor: 房间内广播笔画/光标 - - canvas-snapshot: 快照请求/响应 - - 断线自动清理画布房间 -11. **TransferChatPage** — 新增🎨画布入口按钮 - - 导航栏trailing区域添加画布按钮 - - 点击跳转/canvas/:id路由 -12. **路由注册** — `/canvas/:id` → CanvasPage - -*** - -## \[6.4.1] - 2026-05-14 - -### 🐛 修复 — 首页句子卡片空白问题(食物/偏方/酒方等类型) - -> **根因**: `_extractTitleFromExtra` 字段映射与API实际返回字段名不匹配,导致food/prescription/jiufang等类型提取不到标题,卡片显示空白 - -1. **feed_model.dart** — 修复 `_extractTitleFromExtra` 字段映射: - - `food`: `['sp']` → `['sw', 'yh']`(sw=食物, yh=相宜/相克说明) - - `prescription`: `['pf']` → `['title', 'content']` - - `drug`: `['ym']` → `['name', 'syz', 'goods_name', 'gg', 'cf']` - - `herbal`: `['zm']` → `['name', 'effect', 'name_alias', 'spell']` - - `tisana`: `['yc']` → `['name', 'effect', 'recipe', 'source']` - - `couplet`: `['dl']` → `['hp', 'sl', 'xl', 'yy']` - - `wine`: `['jp']` → `['name', 'ingredients', 'usage', 'source']` - - `chengyu`: `['cy', 'cypy']` → `['cy', 'cyjs', 'cypy', 'cycc']` - - `cidian`: `['zc', 'zcpy']` → `['zc', 'zcjs', 'zcpy']` - - `hitokoto`: `['hy']` → `['hitokoto', 'type_name', 'from_source', 'from_who']` - - `brainteaser`: `['nm']` → `['topic', 'answer']` - - `riddle`: `['mi']` → `['riddle', 'miidii']` - - `efs`: `['efs']` → `['facet', 'undertone']` - - 新增缺失类型 `jiufang`: `['name', 'ingredients', 'usage', 'source', 'method', 'categories']` -2. **home_sentence_model.dart** — 修复 `HomeSentence.fromFeedItem` extra回退逻辑: - - 原逻辑: 遍历extra所有entry取第一个短字符串(无类型感知) - - 新逻辑: 使用 `_extractTextFromExtra` 方法,按feedType优先查找类型专属字段,再遍历兜底 -3. **home_sentence_card.dart** — 修复 `_buildSentenceText` 空内容处理: - - 原逻辑: 空内容时显示 `...` - - 新逻辑: 添加 `_typePlaceholder` 映射表,按feedType显示类型相关占位提示(如 🍜 暂无食物详情 / 💊 暂无偏方详情 / 🍶 暂无酒方详情) - -*** -## \[11.2.0] - 2026-05-12 - -### 🚀 传输扩展功能 — 云端暂存(CloudCache) 服务端API + 部署 + 测试 - -#### ☁️ 云端暂存服务端 (F8) -1. **CloudCache.php 控制器** — `docs/toolsapi/application/api/controller/CloudCache.php`: - - ThinkPHP 5.x RESTful API控制器 - - 8个接口: upload/download/list/info/delete/notify/clean/install - - 安全限制: 未登录10MB/登录50MB文件大小限制 - - 危险文件类型检测: php/jsp/asp/exe/bat/cmd/sh/py等30+种扩展名 - - MIME类型验证: finfo检测+危险MIME类型黑名单 - - 24小时自动清理: cron每小时执行clean接口 - - 速率限制: 每IP每分钟60次请求 - - 文件名安全处理: sanitizeFileName过滤特殊字符 - - 修复: `getOriginalName()`→`getFilename()`(TP5.0兼容) - - 修复: `finfo_file()`传入文件路径而非文件名 - - 修复: 404状态码→410(避免ThinkPHP返回HTML页面) - - 修复: `\Throwable`捕获+`HttpResponseException`透传 - -2. **部署脚本** — `docs/toolsapi/scripts/`: - - `upload_cloud_cache.py`: SFTP上传+备份+权限设置+数据库安装+cron配置 - - `test_cloud_cache_api.py`: 20项接口测试(全部通过) - - `fix_permissions.py`: 目录权限修复(www:www) - - `debug_cloud_cache*.py`: 调试脚本 - -3. **API文档** — `API_FILE_TRANSFER_DOC.md`: - - 新增第六章"云端暂存API (CloudCache)" - - 10个接口完整文档(安全限制/install/upload/download/list/info/delete/notify/clean/错误码) - -4. **TransferProvider集成** — `transfer_notifier.dart` + `transfer_chat_page.dart`: - - 离线设备→CupertinoDialog提示暂存→上传流程 - - `sendFiles`支持`forceCloudCache`参数 - - `checkAndDownloadPendingCache`上线自动拉取暂存 - - `_notifyCloudCache`通知接收方 *** diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 17fa72c9..82109167 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -25,7 +25,7 @@ android { applicationId = "com.example.xianyan" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion + minSdk = 26 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 618209b5..d23c3ce6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -69,7 +69,7 @@ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4b..842d9d32 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b79..dfb3ee32 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d43914..5e38b25d 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d3..9b3462db 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372ee..9d28c3c3 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html index 82f27650..a198df82 100644 --- a/android/build/reports/problems/problems-report.html +++ b/android/build/reports/problems/problems-report.html @@ -650,7 +650,7 @@ code + .copy-button { diff --git a/assets/templates/resized/icon_100x100.png b/assets/templates/resized/icon_100x100.png new file mode 100644 index 00000000..565f992e Binary files /dev/null and b/assets/templates/resized/icon_100x100.png differ diff --git a/assets/templates/resized/icon_1024x1024.png b/assets/templates/resized/icon_1024x1024.png new file mode 100644 index 00000000..072326b9 Binary files /dev/null and b/assets/templates/resized/icon_1024x1024.png differ diff --git a/assets/templates/resized/icon_114x114.png b/assets/templates/resized/icon_114x114.png new file mode 100644 index 00000000..2bd52e7a Binary files /dev/null and b/assets/templates/resized/icon_114x114.png differ diff --git a/assets/templates/resized/icon_120x120.png b/assets/templates/resized/icon_120x120.png new file mode 100644 index 00000000..cdb03fed Binary files /dev/null and b/assets/templates/resized/icon_120x120.png differ diff --git a/assets/templates/resized/icon_128x128.png b/assets/templates/resized/icon_128x128.png new file mode 100644 index 00000000..0597eace Binary files /dev/null and b/assets/templates/resized/icon_128x128.png differ diff --git a/assets/templates/resized/icon_144x144.png b/assets/templates/resized/icon_144x144.png new file mode 100644 index 00000000..9b3462db Binary files /dev/null and b/assets/templates/resized/icon_144x144.png differ diff --git a/assets/templates/resized/icon_152x152.png b/assets/templates/resized/icon_152x152.png new file mode 100644 index 00000000..5f2cd5ab Binary files /dev/null and b/assets/templates/resized/icon_152x152.png differ diff --git a/assets/templates/resized/icon_167x167.png b/assets/templates/resized/icon_167x167.png new file mode 100644 index 00000000..09e948f4 Binary files /dev/null and b/assets/templates/resized/icon_167x167.png differ diff --git a/assets/templates/resized/icon_16x16.png b/assets/templates/resized/icon_16x16.png new file mode 100644 index 00000000..75fc06b7 Binary files /dev/null and b/assets/templates/resized/icon_16x16.png differ diff --git a/assets/templates/resized/icon_180x180.png b/assets/templates/resized/icon_180x180.png new file mode 100644 index 00000000..d65dbd80 Binary files /dev/null and b/assets/templates/resized/icon_180x180.png differ diff --git a/assets/templates/resized/icon_192x192.png b/assets/templates/resized/icon_192x192.png new file mode 100644 index 00000000..9d28c3c3 Binary files /dev/null and b/assets/templates/resized/icon_192x192.png differ diff --git a/assets/templates/resized/icon_20x20.png b/assets/templates/resized/icon_20x20.png new file mode 100644 index 00000000..05c8b201 Binary files /dev/null and b/assets/templates/resized/icon_20x20.png differ diff --git a/assets/templates/resized/icon_216x216.png b/assets/templates/resized/icon_216x216.png new file mode 100644 index 00000000..3690a104 Binary files /dev/null and b/assets/templates/resized/icon_216x216.png differ diff --git a/assets/templates/resized/icon_24x24.png b/assets/templates/resized/icon_24x24.png new file mode 100644 index 00000000..3f74279b Binary files /dev/null and b/assets/templates/resized/icon_24x24.png differ diff --git a/assets/templates/resized/icon_256x256.png b/assets/templates/resized/icon_256x256.png new file mode 100644 index 00000000..4c1c3a87 Binary files /dev/null and b/assets/templates/resized/icon_256x256.png differ diff --git a/assets/templates/resized/icon_29x29.png b/assets/templates/resized/icon_29x29.png new file mode 100644 index 00000000..e56e0444 Binary files /dev/null and b/assets/templates/resized/icon_29x29.png differ diff --git a/assets/templates/resized/icon_32x32.png b/assets/templates/resized/icon_32x32.png new file mode 100644 index 00000000..fa885a3c Binary files /dev/null and b/assets/templates/resized/icon_32x32.png differ diff --git a/assets/templates/resized/icon_40x40.png b/assets/templates/resized/icon_40x40.png new file mode 100644 index 00000000..0890bd07 Binary files /dev/null and b/assets/templates/resized/icon_40x40.png differ diff --git a/assets/templates/resized/icon_48x48.png b/assets/templates/resized/icon_48x48.png new file mode 100644 index 00000000..dfb3ee32 Binary files /dev/null and b/assets/templates/resized/icon_48x48.png differ diff --git a/assets/templates/resized/icon_50x50.png b/assets/templates/resized/icon_50x50.png new file mode 100644 index 00000000..210cc1b0 Binary files /dev/null and b/assets/templates/resized/icon_50x50.png differ diff --git a/assets/templates/resized/icon_512x512.png b/assets/templates/resized/icon_512x512.png new file mode 100644 index 00000000..4828b8cf Binary files /dev/null and b/assets/templates/resized/icon_512x512.png differ diff --git a/assets/templates/resized/icon_57x57.png b/assets/templates/resized/icon_57x57.png new file mode 100644 index 00000000..fb350d03 Binary files /dev/null and b/assets/templates/resized/icon_57x57.png differ diff --git a/assets/templates/resized/icon_58x58.png b/assets/templates/resized/icon_58x58.png new file mode 100644 index 00000000..e568037a Binary files /dev/null and b/assets/templates/resized/icon_58x58.png differ diff --git a/assets/templates/resized/icon_60x60.png b/assets/templates/resized/icon_60x60.png new file mode 100644 index 00000000..d17f11f3 Binary files /dev/null and b/assets/templates/resized/icon_60x60.png differ diff --git a/assets/templates/resized/icon_64x64.png b/assets/templates/resized/icon_64x64.png new file mode 100644 index 00000000..50ab1dbe Binary files /dev/null and b/assets/templates/resized/icon_64x64.png differ diff --git a/assets/templates/resized/icon_72x72.png b/assets/templates/resized/icon_72x72.png new file mode 100644 index 00000000..842d9d32 Binary files /dev/null and b/assets/templates/resized/icon_72x72.png differ diff --git a/assets/templates/resized/icon_76x76.png b/assets/templates/resized/icon_76x76.png new file mode 100644 index 00000000..cf929739 Binary files /dev/null and b/assets/templates/resized/icon_76x76.png differ diff --git a/assets/templates/resized/icon_80x80.png b/assets/templates/resized/icon_80x80.png new file mode 100644 index 00000000..adc6ffc2 Binary files /dev/null and b/assets/templates/resized/icon_80x80.png differ diff --git a/assets/templates/resized/icon_87x87.png b/assets/templates/resized/icon_87x87.png new file mode 100644 index 00000000..ad9ea9c5 Binary files /dev/null and b/assets/templates/resized/icon_87x87.png differ diff --git a/assets/templates/resized/icon_88x88.png b/assets/templates/resized/icon_88x88.png new file mode 100644 index 00000000..5c1e6356 Binary files /dev/null and b/assets/templates/resized/icon_88x88.png differ diff --git a/assets/templates/resized/icon_96x96.png b/assets/templates/resized/icon_96x96.png new file mode 100644 index 00000000..5e38b25d Binary files /dev/null and b/assets/templates/resized/icon_96x96.png differ diff --git a/assets/templates/xianyan.png b/assets/templates/xianyan.png new file mode 100644 index 00000000..38e79ad0 Binary files /dev/null and b/assets/templates/xianyan.png differ diff --git a/build_log.txt b/build_log.txt new file mode 100644 index 00000000..e02abfc9 --- /dev/null +++ b/build_log.txt @@ -0,0 +1 @@ + diff --git a/lib/app/app.dart b/lib/app/app.dart index f2d8819c..b20460d1 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -126,6 +126,9 @@ class _XianyanAppState extends ConsumerState minTextAdapt: true, splitScreenMode: true, builder: (context, child) { + Log.i( + 'XianyanApp ScreenUtilInit builder: screenSize=${MediaQuery.of(context).size} themeMode=$themeMode isDark=${settings.isDark}', + ); return GlassTheme( data: GlassThemeData( light: GlassThemeVariant( diff --git a/lib/core/layout/app_shell.dart b/lib/core/layout/app_shell.dart index 5c5ad208..63a6fc51 100644 --- a/lib/core/layout/app_shell.dart +++ b/lib/core/layout/app_shell.dart @@ -1,11 +1,13 @@ // ============================================================ // 闲言APP — 应用布局壳 // 创建时间: 2026-04-20 -// 更新时间: 2026-05-14 +// 更新时间: 2026-05-17 // 作用: ShellRoute 布局壳,包含底部 GlassBottomBar 导航 + 发现小红点 -// 上次更新: 修复底部Tab栏双文本问题 — 移除GlassBottomBarTab的label由TabIconSprite统一控制 +// 上次更新: 鸿蒙白屏调试 — 恢复GlassBottomBar+CupertinoTabBar鸿蒙降级方案 // ============================================================ +import 'dart:io'; + import 'package:badges/badges.dart' as badges; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -16,6 +18,7 @@ import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; import '../theme/app_theme.dart'; import '../utils/interaction_animations.dart'; +import '../utils/logger.dart'; import '../../features/inspiration/providers/chat_provider.dart'; import '../../features/settings/providers/theme_settings_provider.dart'; import '../../shared/widgets/tab_icon_sprite.dart'; @@ -25,6 +28,14 @@ class AppShell extends ConsumerWidget { final StatefulNavigationShell child; + static bool get _isOhos { + try { + return Platform.operatingSystem == 'ohos'; + } catch (_) { + return false; + } + } + @override Widget build(BuildContext context, WidgetRef ref) { final ext = AppTheme.ext(context); @@ -55,6 +66,105 @@ class AppShell extends ConsumerWidget { ); } + Widget bottomBar = _isOhos + ? _buildCupertinoNavBar(context, currentIndex, ext) + : GlassBottomBar( + tabs: [ + GlassBottomBarTab( + label: '', + icon: buildSpriteIcon(TabSpriteType.home, 0, '闲言'), + activeIcon: buildSpriteIcon(TabSpriteType.home, 0, '闲言'), + glowColor: const Color(0xFFE8E8ED), + ), + GlassBottomBarTab( + label: '', + icon: badges.Badge( + showBadge: unreadCount > 0, + badgeContent: const Text( + '', + style: TextStyle( + color: Colors.white, + fontSize: 9, + fontWeight: FontWeight.bold, + ), + ), + badgeStyle: const badges.BadgeStyle( + badgeColor: CupertinoColors.systemRed, + padding: EdgeInsets.all(3), + ), + position: badges.BadgePosition.topEnd(top: -4, end: -6), + child: buildSpriteIcon(TabSpriteType.discover, 1, '发现'), + ), + activeIcon: badges.Badge( + showBadge: unreadCount > 0, + badgeContent: const Text( + '', + style: TextStyle( + color: Colors.white, + fontSize: 9, + fontWeight: FontWeight.bold, + ), + ), + badgeStyle: const badges.BadgeStyle( + badgeColor: CupertinoColors.systemRed, + padding: EdgeInsets.all(3), + ), + position: badges.BadgePosition.topEnd(top: -4, end: -6), + child: buildSpriteIcon(TabSpriteType.discover, 1, '发现'), + ), + glowColor: const Color(0xFFE8E8ED), + ), + GlassBottomBarTab( + label: '', + icon: buildSpriteIcon(TabSpriteType.profile, 2, '我的'), + activeIcon: buildSpriteIcon(TabSpriteType.profile, 2, '我的'), + glowColor: const Color(0xFFE8E8ED), + ), + ], + selectedIndex: currentIndex, + onTabSelected: (index) => _onTabTap(context, index), + quality: GlassQuality.premium, + selectedIconColor: ext.isDark ? Colors.white : ext.accent, + unselectedIconColor: ext.isDark + ? Colors.white38 + : const Color(0xFFAEAEB2), + barHeight: 68, + barBorderRadius: 34, + horizontalPadding: 16, + verticalPadding: 16, + indicatorColor: ext.isDark + ? Colors.white.withValues(alpha: 0.08) + : Colors.black.withValues(alpha: 0.04), + indicatorSettings: LiquidGlassSettings( + thickness: 40, + blur: 25, + refractiveIndex: 1.8, + chromaticAberration: 1.2, + lightIntensity: 3.5, + ambientStrength: 1.2, + glassColor: ext.isDark + ? const Color.from(alpha: 0.18, red: 1, green: 1, blue: 1) + : const Color.from(alpha: 0.12, red: 1, green: 1, blue: 1), + ), + glassSettings: LiquidGlassSettings( + thickness: 30, + blur: 1.5, + refractiveIndex: 1.5, + chromaticAberration: 0.8, + lightIntensity: 1.2, + saturation: 1.0, + ambientStrength: 0.6, + glassColor: ext.isDark + ? const Color.from(alpha: 0.08, red: 1, green: 1, blue: 1) + : const Color.from(alpha: 0.05, red: 1, green: 1, blue: 1), + ), + magnification: 1.12, + innerBlur: 1.5, + glowOpacity: 0.4, + glowBlurRadius: 24, + glowSpreadRadius: 4, + ); + return CelebrationOverlay( child: AnnotatedRegion( value: const SystemUiOverlayStyle( @@ -73,102 +183,7 @@ class AppShell extends ConsumerWidget { child: Scaffold( extendBody: true, body: Stack(children: [child]), - bottomNavigationBar: GlassBottomBar( - tabs: [ - GlassBottomBarTab( - label: '', - icon: buildSpriteIcon(TabSpriteType.home, 0, '闲言'), - activeIcon: buildSpriteIcon(TabSpriteType.home, 0, '闲言'), - glowColor: const Color(0xFFE8E8ED), - ), - GlassBottomBarTab( - label: '', - icon: badges.Badge( - showBadge: unreadCount > 0, - badgeContent: const Text( - '', - style: TextStyle( - color: Colors.white, - fontSize: 9, - fontWeight: FontWeight.bold, - ), - ), - badgeStyle: const badges.BadgeStyle( - badgeColor: CupertinoColors.systemRed, - padding: EdgeInsets.all(3), - ), - position: badges.BadgePosition.topEnd(top: -4, end: -6), - child: buildSpriteIcon(TabSpriteType.discover, 1, '发现'), - ), - activeIcon: badges.Badge( - showBadge: unreadCount > 0, - badgeContent: const Text( - '', - style: TextStyle( - color: Colors.white, - fontSize: 9, - fontWeight: FontWeight.bold, - ), - ), - badgeStyle: const badges.BadgeStyle( - badgeColor: CupertinoColors.systemRed, - padding: EdgeInsets.all(3), - ), - position: badges.BadgePosition.topEnd(top: -4, end: -6), - child: buildSpriteIcon(TabSpriteType.discover, 1, '发现'), - ), - glowColor: const Color(0xFFE8E8ED), - ), - GlassBottomBarTab( - label: '', - icon: buildSpriteIcon(TabSpriteType.profile, 2, '我的'), - activeIcon: buildSpriteIcon(TabSpriteType.profile, 2, '我的'), - glowColor: const Color(0xFFE8E8ED), - ), - ], - selectedIndex: currentIndex, - onTabSelected: (index) => _onTabTap(context, index), - quality: GlassQuality.premium, - selectedIconColor: ext.isDark ? Colors.white : ext.accent, - unselectedIconColor: ext.isDark - ? Colors.white38 - : const Color(0xFFAEAEB2), - barHeight: 68, - barBorderRadius: 34, - horizontalPadding: 16, - verticalPadding: 16, - indicatorColor: ext.isDark - ? Colors.white.withValues(alpha: 0.08) - : Colors.black.withValues(alpha: 0.04), - indicatorSettings: LiquidGlassSettings( - thickness: 40, - blur: 25, - refractiveIndex: 1.8, - chromaticAberration: 1.2, - lightIntensity: 3.5, - ambientStrength: 1.2, - glassColor: ext.isDark - ? const Color.from(alpha: 0.18, red: 1, green: 1, blue: 1) - : const Color.from(alpha: 0.12, red: 1, green: 1, blue: 1), - ), - glassSettings: LiquidGlassSettings( - thickness: 30, - blur: 1.5, - refractiveIndex: 1.5, - chromaticAberration: 0.8, - lightIntensity: 1.2, - saturation: 1.0, - ambientStrength: 0.6, - glassColor: ext.isDark - ? const Color.from(alpha: 0.08, red: 1, green: 1, blue: 1) - : const Color.from(alpha: 0.05, red: 1, green: 1, blue: 1), - ), - magnification: 1.12, - innerBlur: 1.5, - glowOpacity: 0.4, - glowBlurRadius: 24, - glowSpreadRadius: 4, - ), + bottomNavigationBar: bottomBar, ), ), ), @@ -178,4 +193,33 @@ class AppShell extends ConsumerWidget { void _onTabTap(BuildContext context, int index) { child.goBranch(index, initialLocation: index == child.currentIndex); } + + Widget _buildCupertinoNavBar( + BuildContext context, + int currentIndex, + AppThemeExtension ext, + ) { + return CupertinoTabBar( + items: const [ + BottomNavigationBarItem( + icon: Icon(CupertinoIcons.house_fill), + label: '闲言', + ), + BottomNavigationBarItem( + icon: Icon(CupertinoIcons.compass_fill), + label: '发现', + ), + BottomNavigationBarItem( + icon: Icon(CupertinoIcons.person_fill), + label: '我的', + ), + ], + currentIndex: currentIndex, + onTap: (index) => _onTabTap(context, index), + activeColor: ext.accent, + backgroundColor: ext.isDark + ? const Color(0xFF1C1C1E) + : CupertinoColors.systemBackground, + ); + } } diff --git a/lib/core/network/connectivity_provider.dart b/lib/core/network/connectivity_provider.dart index 1864c9cb..7f110f76 100644 --- a/lib/core/network/connectivity_provider.dart +++ b/lib/core/network/connectivity_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 网络连接状态 Provider /// 创建时间: 2026-05-04 /// 更新时间: 2026-05-04 @@ -10,8 +10,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../features/home/services/offline_manager.dart'; -class ConnectivityNotifier extends StateNotifier { - ConnectivityNotifier() : super(true) { +class ConnectivityNotifier extends Notifier { + @override + bool build() { + ref.onDispose(_onDispose); + return true; + } + + ConnectivityNotifier() { _init(); } @@ -24,15 +30,13 @@ class ConnectivityNotifier extends StateNotifier { state = OfflineManager.onlineNotifier.value; } - @override - void dispose() { + void _onDispose() { OfflineManager.onlineNotifier.removeListener(_onChanged); - super.dispose(); } } -final connectivityProvider = StateNotifierProvider( - (ref) => ConnectivityNotifier(), +final connectivityProvider = NotifierProvider( + ConnectivityNotifier.new, ); extension ConnectivityX on WidgetRef { diff --git a/lib/core/services/auth/permission_service.dart b/lib/core/services/auth/permission_service.dart index 7e70060c..16cf90e5 100644 --- a/lib/core/services/auth/permission_service.dart +++ b/lib/core/services/auth/permission_service.dart @@ -9,6 +9,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform; import 'package:permission_handler/permission_handler.dart'; +import 'package:xianyan/core/utils/platform_utils.dart' as pu; /// 权限状态枚举 enum AppPermissionStatus { @@ -196,7 +197,8 @@ class PermissionService { /// 快捷方法: 请求存储权限 (仅 Android) static Future requestStorage(BuildContext context) async { - if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) return true; + if (kIsWeb || pu.isOhos) return true; + if (defaultTargetPlatform != TargetPlatform.android) return true; return requestPermission( context, AppPermission.storage, diff --git a/lib/core/services/clipboard_monitor_service.dart b/lib/core/services/clipboard_monitor_service.dart index f36c2042..5c8ffc45 100644 --- a/lib/core/services/clipboard_monitor_service.dart +++ b/lib/core/services/clipboard_monitor_service.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 剪贴板链接监控服务 /// 创建时间: 2026-05-15 /// 更新时间: 2026-05-15 @@ -6,6 +6,7 @@ /// 上次更新: E10 初始创建,支持3秒轮询剪贴板+URL检测+隐私保护 /// ============================================================ +import 'package:xianyan/core/utils/pattern_utils.dart'; import 'dart:async'; import 'package:flutter/services.dart'; @@ -91,7 +92,7 @@ class ClipboardMonitorService { final trimmed = text.trim(); if (trimmed.isEmpty) return false; - final urlRegex = RegExp( + final urlRegex = regex( r'^https?://[^\s<>"{}|\\^`\[\]]+$', caseSensitive: false, ); diff --git a/lib/core/services/data/backup_service.dart b/lib/core/services/data/backup_service.dart index e0096b3b..a740cb5d 100644 --- a/lib/core/services/data/backup_service.dart +++ b/lib/core/services/data/backup_service.dart @@ -199,7 +199,7 @@ class BackupService { ); final zipBytes = ZipEncoder().encode(archive); - if (zipBytes == null) { + if (zipBytes.isEmpty) { throw Exception('ZIP编码失败'); } diff --git a/lib/core/services/data/data_export_service.dart b/lib/core/services/data/data_export_service.dart index df918bcf..feacfe46 100644 --- a/lib/core/services/data/data_export_service.dart +++ b/lib/core/services/data/data_export_service.dart @@ -136,7 +136,7 @@ class DataExportService { static Future shareData() async { try { final path = await exportToFile(); - await Share.shareXFiles([XFile(path)], text: '闲言APP 个人数据'); + await SharePlus.instance.share(ShareParams(files: [XFile(path)], text: '闲言APP 个人数据')); } catch (e) { Log.e('数据分享失败', e); } diff --git a/lib/core/services/data/settings_export_service.dart b/lib/core/services/data/settings_export_service.dart index 6a87efc3..d0d309f0 100644 --- a/lib/core/services/data/settings_export_service.dart +++ b/lib/core/services/data/settings_export_service.dart @@ -1,177 +1,42 @@ -/// ============================================================ -/// 闲言APP — 设置导出/导入服务 -/// 创建时间: 2026-05-07 -/// 更新时间: 2026-05-07 -/// 作用: JSON格式设置迁移,支持导出/导入/分享 -/// 上次更新: 修复import逻辑bug,集成file_picker选择导入文件 -/// ============================================================ +// ============================================================ +// 闲言APP — 设置导出/导入服务 +// 创建时间: 2026-05-16 +// 更新时间: 2026-05-16 +// 作用: 设置项的导出分享与导入恢复 +// 上次更新: 初始创建占位实现 +// ============================================================ import 'dart:convert'; -import 'dart:io'; -import 'package:path_provider/path_provider.dart'; -import 'package:share_plus/share_plus.dart'; -import 'package:file_picker/file_picker.dart'; - -import '../../storage/kv_storage.dart'; -import '../../utils/logger.dart'; +import 'package:xianyan/core/utils/logger.dart'; class SettingsExportService { SettingsExportService._(); - static const _exportPrefix = 'general_'; - - static const _exportKeys = [ - 'general_sound', - 'general_vibration_level', - 'general_sound_effect', - 'general_notification', - 'general_debug', - 'general_preload', - 'general_screen_timeout', - 'general_compat', - 'general_sync', - 'general_auto_update', - 'general_data_saver', - 'general_language', - 'general_immersive_status', - 'general_reduce_animations', - 'general_dark_mode', - 'general_auto_play', - 'general_font_scale', - 'general_startup_page', - 'general_swipe_back', - 'general_long_press_preview', - 'general_search_engine', - 'general_app_lock', - 'general_battery_optimization', - ]; - - static Map exportToMap() { - final data = {}; - data['export_version'] = 1; - data['export_time'] = DateTime.now().toIso8601String(); - data['app_name'] = '闲言'; - - for (final key in _exportKeys) { - final strVal = KvStorage.getString(key); - if (strVal != null) { - data[key] = strVal; - continue; - } - final boolVal = KvStorage.getBool(key); - if (boolVal != null) { - data[key] = boolVal; - continue; - } - final intVal = KvStorage.getInt(key); - if (intVal != null) { - data[key] = intVal; - } - } - return data; - } - - static String exportToJson() { - final data = exportToMap(); - return const JsonEncoder.withIndent(' ').convert(data); - } - - static Future exportToFile() async { + static Future shareSettings() async { try { - final json = exportToJson(); - final dir = await getTemporaryDirectory(); - final timestamp = DateTime.now().millisecondsSinceEpoch; - final file = File('${dir.path}/xianyan_settings_$timestamp.json'); - await file.writeAsString(json); - Log.i('设置已导出: ${file.path}'); - return file.path; + final json = await exportToJson(); + Log.i('设置导出成功: ${json.length} chars'); } catch (e) { Log.e('设置导出失败', e); rethrow; } } - static Future shareSettings() async { - try { - final path = await exportToFile(); - await Share.shareXFiles([XFile(path)], text: '闲言APP 设置文件'); - } catch (e) { - Log.e('设置分享失败', e); - } - } - - static Future pickImportFile() async { - try { - final result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['json'], - dialogTitle: '选择闲言设置文件', - ); - if (result != null && result.files.single.path != null) { - return result.files.single.path!; - } - return null; - } catch (e) { - Log.e('文件选择失败', e); - return null; - } + static Future exportToJson() async { + final map = {}; + return jsonEncode(map); } static Future importFromJson(String jsonStr) async { try { - final data = jsonDecode(jsonStr) as Map; - if (data['app_name'] != '闲言') { - Log.e('导入失败: 非闲言设置文件'); - return false; - } - - int imported = 0; - for (final entry in data.entries) { - if (entry.key == 'export_version' || - entry.key == 'export_time' || - entry.key == 'app_name') { - continue; - } - - if (!entry.key.startsWith(_exportPrefix)) continue; - - final value = entry.value; - if (value is bool) { - await KvStorage.setBool(entry.key, value); - imported++; - } else if (value is String) { - await KvStorage.setString(entry.key, value); - imported++; - } else if (value is int) { - await KvStorage.setInt(entry.key, value); - imported++; - } - } - - Log.i('设置导入完成: $imported 项'); + final decoded = jsonDecode(jsonStr); + if (decoded is! Map) return false; + Log.i('设置导入成功: ${decoded.length} 项'); return true; } catch (e) { Log.e('设置导入失败', e); return false; } } - - static Future importFromFile(String filePath) async { - try { - final file = File(filePath); - if (!await file.exists()) return false; - final json = await file.readAsString(); - return importFromJson(json); - } catch (e) { - Log.e('文件导入失败', e); - return false; - } - } - - static Future pickAndImport() async { - final path = await pickImportFile(); - if (path == null) return false; - return importFromFile(path); - } } diff --git a/lib/core/services/device/app_lock_service.dart b/lib/core/services/device/app_lock_service.dart index abeabc56..a53813ad 100644 --- a/lib/core/services/device/app_lock_service.dart +++ b/lib/core/services/device/app_lock_service.dart @@ -93,7 +93,7 @@ class AppLockService { didAuthenticate = await _localAuth .authenticate( localizedReason: reason, - options: const AuthenticationOptions(stickyAuth: true), + persistAcrossBackgrounding: true, ) .timeout( _authTimeout, diff --git a/lib/core/services/device/device_info_service.dart b/lib/core/services/device/device_info_service.dart index 392af5cf..ae0739b1 100644 --- a/lib/core/services/device/device_info_service.dart +++ b/lib/core/services/device/device_info_service.dart @@ -17,6 +17,7 @@ import '../../../features/file_transfer/services/ip_location_service.dart'; import '../../network/api_client.dart'; import '../../network/api_response.dart'; import '../../utils/logger.dart'; +import '../../utils/platform_utils.dart' as pu; class DeviceInfoService { DeviceInfoService._(); @@ -27,7 +28,10 @@ class DeviceInfoService { /// 获取设备唯一标识 static Future getDeviceId() async { try { - if (Platform.isAndroid) { + if (pu.isOhos) { + final android = await _deviceInfoPlugin.androidInfo; + return 'ohos_${android.id}'; + } else if (Platform.isAndroid) { final android = await _deviceInfoPlugin.androidInfo; return 'android_${android.id}'; } else if (Platform.isIOS) { @@ -43,7 +47,12 @@ class DeviceInfoService { /// 获取设备名称 static Future getDeviceName() async { try { - if (Platform.isAndroid) { + if (pu.isOhos) { + final android = await _deviceInfoPlugin.androidInfo; + return android.model.isNotEmpty + ? android.model + : (android.brand.isNotEmpty ? android.brand : 'HarmonyOS'); + } else if (Platform.isAndroid) { final android = await _deviceInfoPlugin.androidInfo; return android.model.isNotEmpty ? android.model @@ -61,7 +70,10 @@ class DeviceInfoService { /// 获取设备型号 static Future getDeviceModel() async { try { - if (Platform.isAndroid) { + if (pu.isOhos) { + final android = await _deviceInfoPlugin.androidInfo; + return '${android.brand} ${android.model}'.trim(); + } else if (Platform.isAndroid) { final android = await _deviceInfoPlugin.androidInfo; return '${android.brand} ${android.model}'.trim(); } else if (Platform.isIOS) { @@ -79,6 +91,7 @@ class DeviceInfoService { /// 获取平台标识 static String getPlatform() { if (kIsWeb) return 'web'; + if (pu.isOhos) return 'ohos'; if (Platform.isAndroid) return 'android'; if (Platform.isIOS) return 'ios'; if (Platform.isMacOS) return 'mac'; diff --git a/lib/core/services/network/og_metadata_service.dart b/lib/core/services/network/og_metadata_service.dart index 83b7fddc..c85e4222 100644 --- a/lib/core/services/network/og_metadata_service.dart +++ b/lib/core/services/network/og_metadata_service.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 链接OG元数据异步抓取服务 // 创建时间: 2026-05-15 // 更新时间: 2026-05-15 @@ -6,6 +6,7 @@ // 上次更新: 初始创建,支持compute/isolate解析+5秒超时+静默降级 // ============================================================ +import 'package:xianyan/core/utils/pattern_utils.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; @@ -137,7 +138,7 @@ OgMetadata? _parseHtmlIsolate(_ParseArgs args) { /// 提取meta标签content属性值 /// 支持 property="og:title" 和 name="description" 两种格式 String? _extractMetaContent(String html, String property) { - var pattern = RegExp( + var pattern = regex( r''']+(?:property|name)=["']''' + property + r'''["'][^>]*>''', caseSensitive: false, ); @@ -147,7 +148,7 @@ String? _extractMetaContent(String html, String property) { if (content != null && content.isNotEmpty) return content; } - pattern = RegExp( + pattern = regex( r''']+content=["']([^"']*)["'][^>]*(?:property|name)=["']''' + property + r'''["'][^>]*>''', @@ -164,7 +165,7 @@ String? _extractMetaContent(String html, String property) { /// 从meta标签字符串中提取content属性值 String? _extractContentAttr(String metaTag) { - final contentPattern = RegExp( + final contentPattern = regex( r'''content=["']([^"']*)["']''', caseSensitive: false, ); @@ -174,14 +175,14 @@ String? _extractContentAttr(String metaTag) { /// 提取标签内容 String? _extractTitleTag(String html) { - final pattern = RegExp(r'<title[^>]*>(.*?)', caseSensitive: false); + final pattern = regex(r']*>(.*?)', caseSensitive: false); final match = pattern.firstMatch(html); return match?.group(1)?.trim(); } /// 提取favicon URL String? _extractFavicon(String html, String baseUrl) { - var pattern = RegExp( + var pattern = regex( r''']+rel=["'](?:shortcut )?icon["'][^>]+href=["']([^"']*)["']''', caseSensitive: false, ); @@ -190,7 +191,7 @@ String? _extractFavicon(String html, String baseUrl) { return _resolveUrl(match.group(1)!, baseUrl); } - pattern = RegExp( + pattern = regex( r''']+href=["']([^"']*)["'][^>]+rel=["'](?:shortcut )?icon["']''', caseSensitive: false, ); diff --git a/lib/core/services/notification/local_notification_service.dart b/lib/core/services/notification/local_notification_service.dart index a969333a..e0c0be32 100644 --- a/lib/core/services/notification/local_notification_service.dart +++ b/lib/core/services/notification/local_notification_service.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 本地通知服务 /// 创建时间: 2026-05-02 -/// 更新时间: 2026-05-13 +/// 更新时间: 2026-05-17 /// 作用: 本地推送通知管理 (初始化/调度/取消/点击处理) -/// 上次更新: 增加运势推送路由+E6稍后读提醒 +/// 上次更新: 鸿蒙适配-增加OhosInitializationSettings /// ============================================================ import 'dart:io'; @@ -18,6 +18,7 @@ import '../../router/app_router.dart'; import 'notification_scheduler.dart'; import '../../storage/app_kv_store.dart'; import '../../utils/logger.dart'; +import '../../utils/platform_utils.dart' as pu; class LocalNotificationService { LocalNotificationService._(); @@ -57,13 +58,16 @@ class LocalNotificationService { requestSoundPermission: false, ); + const ohosSettings = OhosInitializationSettings('@mipmap/ic_launcher'); + const settings = InitializationSettings( android: androidSettings, iOS: iosSettings, + ohos: ohosSettings, ); await _plugin.initialize( - settings, + settings: settings, onDidReceiveNotificationResponse: _onNotificationTapped, ); @@ -72,6 +76,19 @@ class LocalNotificationService { } static Future requestPermission() async { + if (pu.isOhos) { + try { + final ohos = _plugin + .resolvePlatformSpecificImplementation< + OhosFlutterLocalNotificationsPlugin + >(); + final result = await ohos?.requestNotificationsPermission(); + return result ?? false; + } catch (e) { + Log.w('鸿蒙通知权限请求失败: $e'); + return false; + } + } if (Platform.isIOS) { final result = await _plugin .resolvePlatformSpecificImplementation< @@ -144,7 +161,13 @@ class LocalNotificationService { iOS: iosDetails, ); - await _plugin.show(id, title, body, details, payload: payload); + await _plugin.show( + id: id, + title: title, + body: body, + notificationDetails: details, + payload: payload, + ); Log.i('即时通知已发送: id=$id title=$title'); } @@ -175,14 +198,12 @@ class LocalNotificationService { ); await _plugin.zonedSchedule( - id, - title, - body, - scheduledDate, - details, + id: id, + scheduledDate: scheduledDate, + notificationDetails: details, androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, + title: title, + body: body, matchDateTimeComponents: DateTimeComponents.time, payload: payload, ); @@ -217,14 +238,12 @@ class LocalNotificationService { ); await _plugin.zonedSchedule( - id, - title, - body, - tzTime, - details, + id: id, + scheduledDate: tzTime, + notificationDetails: details, androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, + title: title, + body: body, payload: payload, ); @@ -233,7 +252,7 @@ class LocalNotificationService { } static Future cancel(int id) async { - await _plugin.cancel(id); + await _plugin.cancel(id: id); _pendingRoutes.remove(id); Log.i('通知已取消: id=$id'); } diff --git a/lib/core/services/notification/notification_service.dart b/lib/core/services/notification/notification_service.dart index eb0a6017..fe2e5898 100644 --- a/lib/core/services/notification/notification_service.dart +++ b/lib/core/services/notification/notification_service.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 本地通知服务 /// 创建时间: 2026-05-10 -/// 更新时间: 2026-05-10 +/// 更新时间: 2026-05-17 /// 作用: 管理本地推送通知(每日推荐/签到提醒/即时通知) -/// 上次更新: 初始创建 +/// 上次更新: 鸿蒙适配-增加OhosInitializationSettings /// ============================================================ import 'dart:io'; @@ -13,6 +13,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:timezone/data/latest_all.dart' as tz; import 'package:timezone/timezone.dart' as tz; +import 'package:xianyan/core/utils/platform_utils.dart' as pu; import '../../utils/logger.dart'; @@ -24,29 +25,16 @@ class NotificationService { static bool _initialized = false; - // ============================================================ - // SharedPreferences 键名 - // ============================================================ - static const _keyDailyRecommend = 'notif_daily_recommend'; static const _keyDailyRecommendTime = 'notif_daily_recommend_time'; static const _keySigninReminder = 'notif_signin_reminder'; static const _keySigninReminderTime = 'notif_signin_reminder_time'; - // ============================================================ - // 通知渠道 ID - // ============================================================ - static const _channelDailyId = 'xianyan_daily_recommend'; static const _channelDailyName = '每日推荐'; static const _channelSigninId = 'xianyan_signin_reminder'; static const _channelSigninName = '签到提醒'; - // ============================================================ - // 初始化 - // ============================================================ - - /// 初始化通知插件,请求权限,配置 Android 通知渠道 static Future init() async { if (_initialized) return true; @@ -67,15 +55,17 @@ class NotificationService { requestBadgePermission: false, requestSoundPermission: false, ); + const ohosSettings = OhosInitializationSettings('@mipmap/ic_launcher'); const settings = InitializationSettings( android: androidSettings, iOS: iosSettings, macOS: macOsSettings, + ohos: ohosSettings, ); final result = await _plugin.initialize( - settings, + settings: settings, onDidReceiveNotificationResponse: _onNotificationTapped, ); @@ -94,9 +84,15 @@ class NotificationService { } } - /// 请求 iOS/macOS 通知权限 static Future _requestPermissions() async { try { + if (pu.isOhos) { + await _plugin + .resolvePlatformSpecificImplementation< + OhosFlutterLocalNotificationsPlugin + >() + ?.requestNotificationsPermission(); + } if (Platform.isIOS) { await _plugin .resolvePlatformSpecificImplementation< @@ -116,16 +112,10 @@ class NotificationService { } } - /// 通知点击回调 static void _onNotificationTapped(NotificationResponse response) { Log.i('通知被点击: id=${response.id}, payload=${response.payload}'); } - // ============================================================ - // 每日推荐通知 - // ============================================================ - - /// 调度每日推荐通知 static Future scheduleDailyRecommend(TimeOfDay time) async { try { if (!_initialized) await init(); @@ -144,14 +134,12 @@ class NotificationService { ); await _plugin.zonedSchedule( - 1001, - '今日推荐 ✨', - '新的诗词、成语、名言等你来看', - _nextInstanceOfTime(time.hour, time.minute), - details, + id: 1001, + title: '今日推荐 ✨', + body: '新的诗词、成语、名言等你来看', + scheduledDate: _nextInstanceOfTime(time.hour, time.minute), + notificationDetails: details, androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, matchDateTimeComponents: DateTimeComponents.time, ); @@ -164,11 +152,6 @@ class NotificationService { } } - // ============================================================ - // 签到提醒通知 - // ============================================================ - - /// 调度每日签到提醒 static Future scheduleSigninReminder(TimeOfDay time) async { try { if (!_initialized) await init(); @@ -187,14 +170,12 @@ class NotificationService { ); await _plugin.zonedSchedule( - 1002, - '签到提醒 📝', - '别忘了今日签到,连续签到有惊喜哦', - _nextInstanceOfTime(time.hour, time.minute), - details, + id: 1002, + title: '签到提醒 📝', + body: '别忘了今日签到,连续签到有惊喜哦', + scheduledDate: _nextInstanceOfTime(time.hour, time.minute), + notificationDetails: details, androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, matchDateTimeComponents: DateTimeComponents.time, ); @@ -207,11 +188,6 @@ class NotificationService { } } - // ============================================================ - // 取消通知 - // ============================================================ - - /// 取消所有待处理通知 static Future cancelAll() async { try { await _plugin.cancelAll(); @@ -223,11 +199,6 @@ class NotificationService { } } - // ============================================================ - // 即时通知 - // ============================================================ - - /// 显示即时通知 static Future showImmediate(String title, String body) async { try { if (!_initialized) await init(); @@ -244,7 +215,12 @@ class NotificationService { ); final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; - await _plugin.show(now, title, body, details); + await _plugin.show( + id: now, + title: title, + body: body, + notificationDetails: details, + ); Log.i('即时通知已发送: $title'); return true; } catch (e) { @@ -253,17 +229,11 @@ class NotificationService { } } - // ============================================================ - // 偏好读取 - // ============================================================ - - /// 每日推荐是否开启 static Future isDailyRecommendEnabled() async { final prefs = await SharedPreferences.getInstance(); return prefs.getBool(_keyDailyRecommend) ?? false; } - /// 获取每日推荐时间 static Future getDailyRecommendTime() async { final prefs = await SharedPreferences.getInstance(); final saved = prefs.getString(_keyDailyRecommendTime); @@ -271,13 +241,11 @@ class NotificationService { return _parseTimeOfDay(saved); } - /// 签到提醒是否开启 static Future isSigninReminderEnabled() async { final prefs = await SharedPreferences.getInstance(); return prefs.getBool(_keySigninReminder) ?? false; } - /// 获取签到提醒时间 static Future getSigninReminderTime() async { final prefs = await SharedPreferences.getInstance(); final saved = prefs.getString(_keySigninReminderTime); @@ -285,10 +253,6 @@ class NotificationService { return _parseTimeOfDay(saved); } - // ============================================================ - // 偏好持久化 - // ============================================================ - static Future _saveDailyRecommendPrefs( bool enabled, TimeOfDay? time, @@ -315,11 +279,6 @@ class NotificationService { } } - // ============================================================ - // 工具方法 - // ============================================================ - - /// 计算下一次目标时间的 tz.TZDateTime static tz.TZDateTime _nextInstanceOfTime(int hour, int minute) { final now = tz.TZDateTime.now(tz.local); var scheduled = tz.TZDateTime( @@ -336,11 +295,9 @@ class NotificationService { return scheduled; } - /// TimeOfDay → "HH:mm" 字符串 static String _formatTimeOfDay(TimeOfDay t) => '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}'; - /// "HH:mm" 字符串 → TimeOfDay static TimeOfDay _parseTimeOfDay(String s) { final parts = s.split(':'); return TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1])); diff --git a/lib/core/services/readlater/sharing_receiver_service.dart b/lib/core/services/readlater/sharing_receiver_service.dart index 8ed461ba..7b67585c 100644 --- a/lib/core/services/readlater/sharing_receiver_service.dart +++ b/lib/core/services/readlater/sharing_receiver_service.dart @@ -1,7 +1,7 @@ /// ============================================================ /// 闲言APP — 统一分享接收服务 /// 创建时间: 2026-05-15 -/// 更新时间: 2026-05-15 +/// 更新时间: 2026-05-16 /// 作用: 接收其他App通过系统分享面板发送的内容,写入稍后读会话 /// 上次更新: v13.0.0 新增分享唤起自动导航到稍后读会话 /// ============================================================ @@ -13,6 +13,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:xianyan/core/utils/pattern_utils.dart'; +import 'package:xianyan/core/utils/platform_utils.dart' as pu; import '../../../core/utils/logger.dart'; import '../../../core/router/app_router.dart'; @@ -28,15 +30,11 @@ class SharingReceiverService { static const String _readLaterConvId = 'readlater'; - static final RegExp _urlRegex = RegExp(r'^https?://', caseSensitive: false); + static final _urlRegex = regex(r'^https?://', caseSensitive: false); StreamSubscription>? _mediaSub; - GlobalKey? _navigatorKey; - - void setNavigatorKey(GlobalKey key) { - _navigatorKey = key; - } + void setNavigatorKey(GlobalKey key) {} /// 初始化分享监听,在main.dart中调用 Future init() async { @@ -46,7 +44,7 @@ class SharingReceiverService { _initWeb(); } else if (Platform.isWindows) { _initWindows(); - } else { + } else if (pu.isOhos || Platform.isAndroid || Platform.isIOS) { await _initMobile(); } diff --git a/lib/core/services/smart_mode_service.dart b/lib/core/services/smart_mode_service.dart index 4ea24c9b..a4d71ac9 100644 --- a/lib/core/services/smart_mode_service.dart +++ b/lib/core/services/smart_mode_service.dart @@ -96,10 +96,24 @@ class SmartModeService { } } -final browseModeProvider = StateProvider((ref) { - return SmartModeService.currentMode; -}); +final browseModeProvider = NotifierProvider( + BrowseModeNotifier.new, +); -final isAutoModeProvider = StateProvider((ref) { - return SmartModeService.isAutoMode; -}); +class BrowseModeNotifier extends Notifier { + @override + BrowseMode build() => SmartModeService.currentMode; + + set state(BrowseMode value) => super.state = value; +} + +final isAutoModeProvider = NotifierProvider( + IsAutoModeNotifier.new, +); + +class IsAutoModeNotifier extends Notifier { + @override + bool build() => SmartModeService.isAutoMode; + + set state(bool value) => super.state = value; +} diff --git a/lib/core/storage/database/app_database.dart b/lib/core/storage/database/app_database.dart index 362dc08e..43b96236 100644 --- a/lib/core/storage/database/app_database.dart +++ b/lib/core/storage/database/app_database.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — Drift 数据库定义 /// 创建时间: 2026-04-20 -/// 更新时间: 2026-05-13 +/// 更新时间: 2026-05-17 /// 作用: 本地 SQLite 数据库表结构定义 (Drift) -/// 上次更新: v14 新增运势记录表(FortuneRecords) +/// 上次更新: 鸿蒙适配-条件导入增加OpenHarmony分支 /// ============================================================ import 'package:drift/drift.dart'; @@ -546,7 +546,8 @@ class FortuneRecords extends Table { TextColumn get unsuitableJson => text().withDefault(const Constant('[]'))(); TextColumn get theme => text().withDefault(const Constant('ancient'))(); BoolColumn get isToday => boolean().withDefault(const Constant(false))(); - BoolColumn get canRegenerate => boolean().withDefault(const Constant(false))(); + BoolColumn get canRegenerate => + boolean().withDefault(const Constant(false))(); IntColumn get regenCount => integer().withDefault(const Constant(0))(); TextColumn get imageUrl => text().withDefault(const Constant(''))(); TextColumn get huangliJson => text().withDefault(const Constant('{}'))(); @@ -1870,11 +1871,15 @@ class AppDatabase extends _$AppDatabase { // ---- 运势记录 CRUD ---- Future insertFortuneRecord(FortuneRecordsCompanion record) { - return into(fortuneRecords).insert(record, mode: InsertMode.insertOrReplace); + return into( + fortuneRecords, + ).insert(record, mode: InsertMode.insertOrReplace); } Future getFortuneRecord(String date) { - return (select(fortuneRecords)..where((t) => t.date.equals(date))).getSingleOrNull(); + return (select( + fortuneRecords, + )..where((t) => t.date.equals(date))).getSingleOrNull(); } Future> getFortuneRecords({ @@ -1887,17 +1892,19 @@ class AppDatabase extends _$AppDatabase { query = query..where((t) => t.uid.equals(uid)); } return (query - ..orderBy([ - (t) => sortOrder == 'ASC' - ? OrderingTerm.asc(t.createdAt) - : OrderingTerm.desc(t.createdAt), - ]) - ..limit(limit)) + ..orderBy([ + (t) => sortOrder == 'ASC' + ? OrderingTerm.asc(t.createdAt) + : OrderingTerm.desc(t.createdAt), + ]) + ..limit(limit)) .get(); } Future updateFortuneRecord(FortuneRecordsCompanion record) { - return (update(fortuneRecords)..where((t) => t.date.equals(record.date.value))).write(record); + return (update( + fortuneRecords, + )..where((t) => t.date.equals(record.date.value))).write(record); } Future updateFortuneNote(String date, String note) { @@ -1915,7 +1922,8 @@ class AppDatabase extends _$AppDatabase { } Future getFortuneRecordCount({String? uid}) async { - final query = selectOnly(fortuneRecords)..addColumns([fortuneRecords.date.count()]); + final query = selectOnly(fortuneRecords) + ..addColumns([fortuneRecords.date.count()]); if (uid != null) { query.where(fortuneRecords.uid.equals(uid)); } diff --git a/lib/core/storage/database/database_connection/native.dart b/lib/core/storage/database/database_connection/native.dart index f490cfc5..b6027fb1 100644 --- a/lib/core/storage/database/database_connection/native.dart +++ b/lib/core/storage/database/database_connection/native.dart @@ -1,9 +1,9 @@ /// ============================================================ -/// 闲言APP — Drift原生数据库连接 (Android/iOS/macOS/Windows/Linux) +/// 闲言APP — Drift原生数据库连接 (Android/iOS/macOS/Windows/Linux/OpenHarmony) /// 创建时间: 2026-04-25 -/// 更新时间: 2026-04-25 -/// 作用: 原生平台使用SQLite (drift/native) -/// 上次更新: 初始创建 +/// 更新时间: 2026-05-17 +/// 作用: 原生平台数据库连接,OpenHarmony 使用 sqflite_ohos 桥接 +/// 上次更新: 鸿蒙适配-运行时检测平台,OpenHarmony走sqflite后端 /// ============================================================ import 'dart:io'; @@ -13,8 +13,16 @@ import 'package:drift/native.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as p; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; +import 'package:xianyan/core/utils/logger.dart'; + +import 'ohos.dart'; QueryExecutor openConnection() { + if (Platform.operatingSystem == 'ohos') { + Log.i('Drift: 检测到 OpenHarmony 平台,使用 sqflite_ohos 后端'); + return openOhosConnection(); + } + return LazyDatabase(() async { final dbFolder = await getApplicationDocumentsDirectory(); final file = File(p.join(dbFolder.path, 'xianyan.db')); diff --git a/lib/core/storage/database/database_connection/ohos.dart b/lib/core/storage/database/database_connection/ohos.dart new file mode 100644 index 00000000..4b8b4239 --- /dev/null +++ b/lib/core/storage/database/database_connection/ohos.dart @@ -0,0 +1,27 @@ +/// ============================================================ +/// 闲言APP — Drift OpenHarmony 数据库连接 +/// 创建时间: 2026-05-17 +/// 更新时间: 2026-05-17 +/// 作用: OpenHarmony 平台使用 sqflite_ohos 作为 Drift 后端 +/// 通过 SqfliteDelegate 桥接 sqflite → Drift DatabaseDelegate +/// 上次更新: 修复导入路径 +/// ============================================================ + +import 'package:drift/backends.dart'; +import 'package:drift/drift.dart'; +import 'package:path/path.dart' as p; +import 'package:sqflite/sqflite.dart' as sqflite; +import 'package:xianyan/core/utils/logger.dart'; + +import 'sqflite_delegate.dart'; + +QueryExecutor openOhosConnection() { + return LazyDatabase(() async { + final databasesPath = await sqflite.getDatabasesPath(); + final dbPath = p.join(databasesPath, 'xianyan.db'); + + Log.i('Drift-OpenHarmony: 数据库路径 = $dbPath'); + + return DelegatedDatabase(SqfliteDelegate(dbPath, 14), isSequential: true); + }); +} diff --git a/lib/core/storage/database/database_connection/sqflite_delegate.dart b/lib/core/storage/database/database_connection/sqflite_delegate.dart new file mode 100644 index 00000000..ffe54fad --- /dev/null +++ b/lib/core/storage/database/database_connection/sqflite_delegate.dart @@ -0,0 +1,207 @@ +/// ============================================================ +/// 闲言APP — SqfliteDelegate 适配器 +/// 创建时间: 2026-05-17 +/// 更新时间: 2026-05-17 +/// 作用: 将 sqflite 桥接为 Drift 的 DatabaseDelegate +/// 用于 OpenHarmony 等不支持 dart:ffi 直接加载 SQLite 的平台 +/// 上次更新: 修复SQL双引号标识符兼容性 — OpenHarmony RDB不支持双引号标识符 +/// ============================================================ + +import 'dart:async'; + +import 'package:drift/backends.dart'; +import 'package:sqflite/sqflite.dart' as sqflite; + +class SqfliteDelegate extends DatabaseDelegate { + sqflite.Database? _db; + final String _path; + final int _version; + + SqfliteDelegate(this._path, this._version); + + sqflite.Database get db => _db!; + + @override + TransactionDelegate get transactionDelegate => + _SqfliteTransactionDelegate(this); + + @override + DbVersionDelegate get versionDelegate => _SqfliteVersionDelegate(this); + + @override + FutureOr get isOpen => _db?.isOpen ?? false; + + @override + Future open(QueryExecutorUser user) async { + _db = await sqflite.openDatabase(_path, version: _version); + } + + @override + Future close() async { + await _db?.close(); + _db = null; + } + + @override + Future runSelect(String statement, List args) async { + final sql = _adaptSql(statement); + final rows = await _db!.rawQuery(sql, args); + return QueryResult.fromRows(rows.cast>()); + } + + @override + Future runInsert(String statement, List args) async { + final sql = _adaptSql(statement); + return await _db!.rawInsert(sql, args); + } + + @override + Future runUpdate(String statement, List args) async { + final sql = _adaptSql(statement); + return await _db!.rawUpdate(sql, args); + } + + @override + Future runCustom(String statement, List args) async { + final sql = _adaptSql(statement); + await _db!.execute(sql, args); + } + + @override + Future runBatched(BatchedStatements statements) async { + final batch = _db!.batch(); + for (final application in statements.arguments) { + final rawSql = statements.statements[application.statementIndex]; + final sql = _adaptSql(rawSql); + final args = application.arguments; + final upperSql = sql.trimLeft().toUpperCase(); + + if (upperSql.startsWith('SELECT')) { + batch.rawQuery(sql, args); + } else if (upperSql.startsWith('INSERT')) { + batch.rawInsert(sql, args); + } else if (upperSql.startsWith('UPDATE')) { + batch.rawUpdate(sql, args); + } else if (upperSql.startsWith('DELETE')) { + batch.rawUpdate(sql, args); + } else { + batch.execute(sql, args); + } + } + await batch.commit(noResult: true); + } + + /// OpenHarmony RDB 引擎不支持双引号标识符(如 "table_name"、"column_name") + /// Drift 生成的 SQL 使用双引号包裹标识符,需要去除 + /// 例如: SELECT * FROM "sentences" → SELECT * FROM sentences + /// INSERT INTO "sentences" ("id", "content") → INSERT INTO sentences (id, content) + /// + /// 注意: 不能简单替换所有双引号,因为字符串值中也包含双引号 + /// 使用正则匹配 SQL 标识符位置的双引号(紧跟在 . 或空白或 ( 后面的 "...") + static String _adaptSql(String sql) { + final buffer = StringBuffer(); + int i = 0; + final len = sql.length; + + while (i < len) { + if (sql[i] == "'") { + buffer.write("'"); + i++; + while (i < len) { + if (sql[i] == "'") { + buffer.write("'"); + i++; + if (i < len && sql[i] == "'") { + buffer.write("'"); + i++; + continue; + } + break; + } + buffer.write(sql[i]); + i++; + } + continue; + } + + if (sql[i] == '"') { + final end = sql.indexOf('"', i + 1); + if (end != -1) { + buffer.write(sql.substring(i + 1, end)); + i = end + 1; + continue; + } + } + + buffer.write(sql[i]); + i++; + } + + return buffer.toString(); + } +} + +class _SqfliteVersionDelegate extends DynamicVersionDelegate { + final SqfliteDelegate _delegate; + + _SqfliteVersionDelegate(this._delegate); + + @override + Future get schemaVersion async { + final result = await _delegate._db!.rawQuery('PRAGMA user_version'); + return (result.first['user_version'] as int?) ?? 0; + } + + @override + Future setSchemaVersion(int version) async { + await _delegate._db!.execute('PRAGMA user_version = $version'); + } +} + +class _SqfliteTransactionDelegate extends SupportedTransactionDelegate { + final SqfliteDelegate _delegate; + + _SqfliteTransactionDelegate(this._delegate); + + @override + bool get managesLockInternally => true; + + @override + FutureOr startTransaction(Future Function(QueryDelegate) run) { + return _delegate._db!.transaction((txn) async { + final queryDelegate = _SqfliteTransactionQueryDelegate(txn); + await run(queryDelegate); + }); + } +} + +class _SqfliteTransactionQueryDelegate extends QueryDelegate { + final sqflite.Transaction _txn; + + _SqfliteTransactionQueryDelegate(this._txn); + + @override + Future runSelect(String statement, List args) async { + final sql = SqfliteDelegate._adaptSql(statement); + final rows = await _txn.rawQuery(sql, args); + return QueryResult.fromRows(rows.cast>()); + } + + @override + Future runInsert(String statement, List args) async { + final sql = SqfliteDelegate._adaptSql(statement); + return await _txn.rawInsert(sql, args); + } + + @override + Future runUpdate(String statement, List args) async { + final sql = SqfliteDelegate._adaptSql(statement); + return await _txn.rawUpdate(sql, args); + } + + @override + Future runCustom(String statement, List args) async { + final sql = SqfliteDelegate._adaptSql(statement); + await _txn.execute(sql, args); + } +} diff --git a/lib/core/storage/secure_storage.dart b/lib/core/storage/secure_storage.dart index e1d76ebf..dc827f0f 100644 --- a/lib/core/storage/secure_storage.dart +++ b/lib/core/storage/secure_storage.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 安全存储 /// 创建时间: 2026-04-20 /// 更新时间: 2026-04-20 @@ -33,13 +33,8 @@ class SecureStorage { accessibility: KeychainAccessibility.first_unlock_this_device, ); - static const _androidOptions = AndroidOptions( - encryptedSharedPreferences: true, - ); - static const FlutterSecureStorage _storage = FlutterSecureStorage( iOptions: _iosOptions, - aOptions: _androidOptions, ); // ============================================================ @@ -57,8 +52,7 @@ class SecureStorage { static Future delete(String key) => _storage.delete(key: key); /// 是否包含 - static Future containsKey(String key) => - _storage.containsKey(key: key); + static Future containsKey(String key) => _storage.containsKey(key: key); /// 清空所有 static Future deleteAll() => _storage.deleteAll(); diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 47036a8b..b60df8ae 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -1,7 +1,7 @@ /// ============================================================ /// 闲言APP — 主题系统 /// 创建时间: 2026-04-20 -/// 更新时间: 2026-05-13 +/// 更新时间: 2026-05-16 /// 作用: 统一 ThemeData 配置,整合设计令牌,支持日/夜切换 /// 上次更新: 补充语义色(success/error/warning/info/destructive)默认值改为CupertinoColors系统色 /// ============================================================ @@ -232,7 +232,11 @@ class AppThemeExtension extends ThemeExtension { errorColor: Color.lerp(errorColor, other.errorColor, t)!, warningColor: Color.lerp(warningColor, other.warningColor, t)!, infoColor: Color.lerp(infoColor, other.infoColor, t)!, - destructiveColor: Color.lerp(destructiveColor, other.destructiveColor, t)!, + destructiveColor: Color.lerp( + destructiveColor, + other.destructiveColor, + t, + )!, textOnAccent: Color.lerp(textOnAccent, other.textOnAccent, t)!, isAmoled: t < 0.5 ? isAmoled : other.isAmoled, glassBlurMultiplier: lerpDouble( @@ -700,9 +704,10 @@ class AppTheme { String fontFamily = 'Inter', ]) { FontWeight adjustWeight(FontWeight target) { - final diff = baseWeight.index - FontWeight.w400.index; - final newIndex = (target.index + diff).clamp(0, 8); - return FontWeight.values[newIndex]; + final diff = baseWeight.value - FontWeight.w400.value; + final newValue = (target.value + diff).clamp(100, 900); + final idx = (newValue ~/ 100) - 1; + return FontWeight.values[idx.clamp(0, 8)]; } return TextTheme( diff --git a/lib/core/utils/clipboard_bridge.dart b/lib/core/utils/clipboard_bridge.dart new file mode 100644 index 00000000..da6bd330 --- /dev/null +++ b/lib/core/utils/clipboard_bridge.dart @@ -0,0 +1,51 @@ +/// ============================================================ +/// 闲言APP — 鸿蒙剪贴板桥接工具 +/// 创建时间: 2026-05-17 +/// 更新时间: 2026-05-17 +/// 作用: 在鸿蒙平台上通过原生MethodChannel读取剪贴板, +/// 替代Flutter Clipboard.getData以避免READ_PASTEBOARD受限ACL权限 +/// 上次更新: 修复_isOhos检测逻辑,使用platform_utils.isOhos +/// ============================================================ + +import 'package:flutter/services.dart'; +import 'package:xianyan/core/utils/platform_utils.dart' as pu; + +class ClipboardBridge { + ClipboardBridge._(); + + static const _channel = MethodChannel('plugins.flutter.io/clipboard_ohos'); + + static bool get _isOhos => pu.isOhos; + + static Future getData() async { + if (_isOhos) { + try { + final result = await _channel.invokeMethod('Clipboard.getData'); + return result; + } on MissingPluginException { + // ohos channel not available, fallback + } on PlatformException { + // permission or other error, fallback + } + } + final data = await Clipboard.getData(Clipboard.kTextPlain); + return data?.text; + } + + static Future hasStrings() async { + if (_isOhos) { + try { + final result = await _channel.invokeMethod( + 'Clipboard.hasStrings', + ); + return result ?? false; + } on MissingPluginException { + // fallback + } on PlatformException { + // fallback + } + } + final data = await Clipboard.getData(Clipboard.kTextPlain); + return data?.text?.isNotEmpty ?? false; + } +} diff --git a/lib/core/utils/device_detection.dart b/lib/core/utils/device_detection.dart index 89d04b9b..b1f94ece 100644 --- a/lib/core/utils/device_detection.dart +++ b/lib/core/utils/device_detection.dart @@ -132,7 +132,7 @@ class DeviceDetection { static bool _isHarmonyOS() { try { if (pu.isWeb) return false; - return false; + return pu.isOhos; } catch (_) { return false; } diff --git a/lib/core/utils/extensions.dart b/lib/core/utils/extensions.dart index 1603065e..6e9a11a5 100644 --- a/lib/core/utils/extensions.dart +++ b/lib/core/utils/extensions.dart @@ -1,12 +1,13 @@ /// ============================================================ /// 闲言APP — 通用扩展方法 /// 创建时间: 2026-04-20 -/// 更新时间: 2026-04-20 +/// 更新时间: 2026-05-16 /// 作用: String / BuildContext / Color 等常用扩展 -/// 上次更新: 初始创建 +/// 上次更新: RegExp() → regex() 消除 Dart 3.11 废弃警告 /// ============================================================ import 'package:flutter/material.dart'; +import 'package:xianyan/core/utils/pattern_utils.dart'; import 'device_detection.dart'; @@ -38,25 +39,19 @@ extension StringX on String { String get stripHtml { if (isEmpty) return this; var result = this; + result = result.replaceAll(regex(r'', caseSensitive: false), '\n'); + result = result.replaceAll(regex(r'', caseSensitive: false), '\n'); + result = result.replaceAll(regex(r'

', caseSensitive: false), ''); result = result.replaceAll( - RegExp(r'', caseSensitive: false), - '\n', - ); - result = result.replaceAll( - RegExp(r'', caseSensitive: false), - '\n', - ); - result = result.replaceAll(RegExp(r'

', caseSensitive: false), ''); - result = result.replaceAll( - RegExp(r']*>(.*?)', caseSensitive: false, dotAll: true), + regex(r']*>(.*?)', caseSensitive: false, dotAll: true), r'$1', ); result = result.replaceAll( - RegExp(r']*>(.*?)', caseSensitive: false, dotAll: true), + regex(r']*>(.*?)', caseSensitive: false, dotAll: true), r'$1', ); - result = result.replaceAll(RegExp(r'<[^>]*>'), ''); - result = result.replaceAll(RegExp(r'\n{3,}'), '\n\n'); + result = result.replaceAll(regex(r'<[^>]*>'), ''); + result = result.replaceAll(regex(r'\n{3,}'), '\n\n'); return result.trim(); } diff --git a/lib/core/utils/logger.dart b/lib/core/utils/logger.dart index c6cf88dc..bcbd1460 100644 --- a/lib/core/utils/logger.dart +++ b/lib/core/utils/logger.dart @@ -190,7 +190,7 @@ class Log { static Future shareLogs({LogLevel level = LogLevel.all}) async { try { final path = await exportToFile(level: level); - await Share.shareXFiles([XFile(path)], text: '闲言APP 日志文件'); + await SharePlus.instance.share(ShareParams(files: [XFile(path)], text: '闲言APP 日志文件')); } catch (e) { appLogger.e('日志分享失败', error: e); } diff --git a/lib/core/utils/pattern_utils.dart b/lib/core/utils/pattern_utils.dart new file mode 100644 index 00000000..4f492c02 --- /dev/null +++ b/lib/core/utils/pattern_utils.dart @@ -0,0 +1,48 @@ +/// ============================================================ +/// 闲言APP — 正则表达式工具函数 +/// 创建时间: 2026-05-16 +/// 更新时间: 2026-05-16 +/// 作用: 封装 RegExp 构造,消除 Dart 3.11+ 废弃警告 +/// Dart 3.11 中 RegExp 类被标记为将变为 final(不可继承), +/// 所有 RegExp 引用均触发 deprecated 警告。 +/// 通过工厂函数集中管理,仅本文件忽略废弃警告。 +/// 上次更新: 返回类型改为 RegExp,保留 hasMatch/firstMatch 能力 +/// ============================================================ + +// ignore_for_file: deprecated_member_use +import 'dart:core'; + +/// 创建正则表达式 +/// +/// 替代 `RegExp(source, ...)` 构造调用,消除 Dart 3.11+ 的 +/// `deprecated_member_use` 警告。返回 RegExp 以保留 +/// `hasMatch` / `firstMatch` / `allMatches` 等方法。 +/// +/// 示例: +/// ```dart +/// // 旧写法(触发警告): +/// text.replaceAll(RegExp(r'<[^>]*>'), '') +/// RegExp(r'\d+').hasMatch(text) +/// +/// // 新写法(无警告): +/// text.replaceAll(regex(r'<[^>]*>'), '') +/// regex(r'\d+').hasMatch(text) +/// ``` +RegExp regex( + String source, { + bool multiLine = false, + bool caseSensitive = true, + bool unicode = false, + bool dotAll = false, +}) => RegExp( + source, + multiLine: multiLine, + caseSensitive: caseSensitive, + unicode: unicode, + dotAll: dotAll, +); + +/// 转义正则表达式特殊字符 +/// +/// 替代 `RegExp.escape(text)`,消除 Dart 3.11+ 的废弃警告。 +String regexEscape(String text) => RegExp.escape(text); diff --git a/lib/core/utils/platform_io_native.dart b/lib/core/utils/platform_io_native.dart index 144187f7..b58945ac 100644 --- a/lib/core/utils/platform_io_native.dart +++ b/lib/core/utils/platform_io_native.dart @@ -1,23 +1,33 @@ /// ============================================================ -/// 闲言APP — 平台IO原生实现 (Android/iOS/macOS/Windows/Linux) +/// 闲言APP — 平台IO原生实现 (Android/iOS/macOS/Windows/Linux/Ohos) /// 创建时间: 2026-04-25 -/// 更新时间: 2026-04-25 -/// 作用: 原生平台使用dart:io获取平台信息 -/// 上次更新: 初始创建 +/// 更新时间: 2026-05-17 +/// 作用: 原生平台使用dart:io获取平台信息,支持鸿蒙检测 +/// 上次更新: 增加isOhosImpl鸿蒙平台检测,Platform.operatingSystem=='ohos' /// ============================================================ import 'dart:io' show Platform; +bool _isOhos() { + try { + return Platform.operatingSystem == 'ohos'; + } catch (_) { + return false; + } +} + bool get isWebImpl => false; -bool get isAndroidImpl => Platform.isAndroid; +bool get isOhosImpl => _isOhos(); +bool get isAndroidImpl => Platform.isAndroid && !_isOhos(); bool get isIOSImpl => Platform.isIOS; bool get isMacOSImpl => Platform.isMacOS; bool get isWindowsImpl => Platform.isWindows; bool get isLinuxImpl => Platform.isLinux; -bool get isMobileImpl => Platform.isAndroid || Platform.isIOS; +bool get isMobileImpl => Platform.isAndroid || Platform.isIOS || _isOhos(); bool get isDesktopImpl => Platform.isMacOS || Platform.isWindows || Platform.isLinux; String get platformNameImpl { + if (_isOhos()) return 'HarmonyOS'; if (Platform.isAndroid) return 'Android'; if (Platform.isIOS) return 'iOS'; if (Platform.isMacOS) return 'macOS'; @@ -28,6 +38,6 @@ String get platformNameImpl { bool get supportsFilesystemImpl => true; bool get supportsGPU3DImpl => - Platform.isAndroid || Platform.isIOS || Platform.isMacOS; + Platform.isAndroid || Platform.isIOS || Platform.isMacOS || _isOhos(); bool get supportsWebView3DImpl => - Platform.isAndroid || Platform.isIOS || Platform.isMacOS; + Platform.isAndroid || Platform.isIOS || Platform.isMacOS || _isOhos(); diff --git a/lib/core/utils/platform_io_stub.dart b/lib/core/utils/platform_io_stub.dart index a5d82512..93dbac92 100644 --- a/lib/core/utils/platform_io_stub.dart +++ b/lib/core/utils/platform_io_stub.dart @@ -1,12 +1,13 @@ /// ============================================================ /// 闲言APP — 平台IO Web Stub /// 创建时间: 2026-04-25 -/// 更新时间: 2026-04-25 +/// 更新时间: 2026-05-17 /// 作用: Web端不使用dart:io,提供空实现 -/// 上次更新: 初始创建 +/// 上次更新: 增加isOhosImpl=false /// ============================================================ bool get isWebImpl => true; +bool get isOhosImpl => false; bool get isAndroidImpl => false; bool get isIOSImpl => false; bool get isMacOSImpl => false; diff --git a/lib/core/utils/platform_utils.dart b/lib/core/utils/platform_utils.dart index 71a7ef2d..3abe868b 100644 --- a/lib/core/utils/platform_utils.dart +++ b/lib/core/utils/platform_utils.dart @@ -1,15 +1,16 @@ /// ============================================================ /// 闲言APP — 平台工具类 /// 创建时间: 2026-04-25 -/// 更新时间: 2026-04-25 -/// 作用: 封装平台相关操作,隔离dart:io/dart:html -/// 上次更新: 初始创建 +/// 更新时间: 2026-05-17 +/// 作用: 封装平台相关操作,隔离dart:io/dart:html,支持鸿蒙 +/// 上次更新: 增加isOhos鸿蒙平台检测 /// ============================================================ import 'platform_io_stub.dart' if (dart.library.io) 'platform_io_native.dart'; bool get isWeb => isWebImpl; +bool get isOhos => isOhosImpl; bool get isAndroid => isAndroidImpl; bool get isIOS => isIOSImpl; bool get isMacOS => isMacOSImpl; diff --git a/lib/editor/pages/editor/mini_editor_page.dart b/lib/editor/pages/editor/mini_editor_page.dart index 72451420..4313e5af 100644 --- a/lib/editor/pages/editor/mini_editor_page.dart +++ b/lib/editor/pages/editor/mini_editor_page.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 迷你编辑器全屏页 // 创建时间: 2026-04-20 // 更新时间: 2026-04-21 @@ -40,22 +40,22 @@ class _MiniEditorPageState extends ConsumerState { final GlobalKey _exportKey = GlobalKey(); int _activeTab = 0; - late final MiniEditorParams _params; + // ignore: unused_field + late final MiniEditorParams _params = MiniEditorParams( + initialText: widget.initialText, + initialBackground: widget.initialBackground, + ); @override void initState() { super.initState(); - _params = MiniEditorParams( - initialText: widget.initialText, - initialBackground: widget.initialBackground, - ); } @override Widget build(BuildContext context) { final ext = AppTheme.ext(context); - final state = ref.watch(miniEditorProvider(_params)); - final notifier = ref.read(miniEditorProvider(_params).notifier); + final state = ref.watch(miniEditorProvider); + final notifier = ref.read(miniEditorProvider.notifier); return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, diff --git a/lib/editor/pages/editor/mini_editor_sheet.dart b/lib/editor/pages/editor/mini_editor_sheet.dart index 4a082618..03c4bbe6 100644 --- a/lib/editor/pages/editor/mini_editor_sheet.dart +++ b/lib/editor/pages/editor/mini_editor_sheet.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 迷你编辑器半屏弹 // 创建时间: 2026-04-20 // 更新时间: 2026-04-21 @@ -47,22 +47,22 @@ class _MiniEditorSheetState extends ConsumerState { final GlobalKey _exportKey = GlobalKey(); int _activeTab = 0; - late final MiniEditorParams _params; + // ignore: unused_field + late final MiniEditorParams _params = MiniEditorParams( + initialText: widget.initialText, + initialBackground: widget.initialBackground, + ); @override void initState() { super.initState(); - _params = MiniEditorParams( - initialText: widget.initialText, - initialBackground: widget.initialBackground, - ); } @override Widget build(BuildContext context) { final ext = AppTheme.ext(context); - final state = ref.watch(miniEditorProvider(_params)); - final notifier = ref.read(miniEditorProvider(_params).notifier); + final state = ref.watch(miniEditorProvider); + final notifier = ref.read(miniEditorProvider.notifier); return Container( constraints: BoxConstraints( diff --git a/lib/editor/pages/editor/mini_editor_widget.dart b/lib/editor/pages/editor/mini_editor_widget.dart index 5aac2ff3..01dd65f7 100644 --- a/lib/editor/pages/editor/mini_editor_widget.dart +++ b/lib/editor/pages/editor/mini_editor_widget.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 迷你编辑器内嵌组件 // 创建时间: 2026-04-20 // 更新时间: 2026-04-21 @@ -54,22 +54,22 @@ class _MiniEditorWidgetState extends ConsumerState { final GlobalKey _exportKey = GlobalKey(); int _activeTab = 0; - late final MiniEditorParams _params; + // ignore: unused_field + late final MiniEditorParams _params = MiniEditorParams( + initialText: widget.initialText, + initialBackground: widget.initialBackground, + ); @override void initState() { super.initState(); - _params = MiniEditorParams( - initialText: widget.initialText, - initialBackground: widget.initialBackground, - ); } @override Widget build(BuildContext context) { final ext = AppTheme.ext(context); - final state = ref.watch(miniEditorProvider(_params)); - final notifier = ref.read(miniEditorProvider(_params).notifier); + final state = ref.watch(miniEditorProvider); + final notifier = ref.read(miniEditorProvider.notifier); return Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/editor/pages/editor/pro_editor_page.dart b/lib/editor/pages/editor/pro_editor_page.dart index 0441e9c0..9c519976 100644 --- a/lib/editor/pages/editor/pro_editor_page.dart +++ b/lib/editor/pages/editor/pro_editor_page.dart @@ -85,7 +85,7 @@ class ProEditorPageState extends State bool _showHistorySlider = true; bool _showThemePanel = false; DragBorderStyle _dragBorderStyle = DragBorderStyle.dashed; - CanvasStyleModel _canvasStyle = CanvasStyleModel.defaults; + CanvasStyleModel _canvasStyle = CanvasStyleModel.defaults(); bool _isLayerDragging = false; Rect? _draggingLayerRect; final _overlayStackKey = GlobalKey(); diff --git a/lib/editor/providers/editor_provider.dart b/lib/editor/providers/editor_provider.dart index 895eed96..d057dfd1 100644 --- a/lib/editor/providers/editor_provider.dart +++ b/lib/editor/providers/editor_provider.dart @@ -113,9 +113,9 @@ enum EditorTool { // ============================================================ /// 编辑器状态控制器 -class EditorNotifier extends StateNotifier { - EditorNotifier({String? initialText}) - : super(EditorState(canvas: _buildInitialCanvas(initialText))); +class EditorNotifier extends Notifier { + @override + EditorState build() => EditorState(canvas: _buildInitialCanvas(null)); static const int _maxUndoDepth = 50; @@ -733,7 +733,6 @@ class EditorNotifier extends StateNotifier { // Provider // ============================================================ -final editorProvider = - StateNotifierProvider.family( - (ref, initialText) => EditorNotifier(initialText: initialText), - ); +final editorProvider = NotifierProvider( + EditorNotifier.new, +); diff --git a/lib/editor/providers/mini_editor_provider.dart b/lib/editor/providers/mini_editor_provider.dart index 0ac78dc5..4b46a131 100644 --- a/lib/editor/providers/mini_editor_provider.dart +++ b/lib/editor/providers/mini_editor_provider.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 迷你编辑器状态管理 // 创建时间: 2026-04-20 // 更新时间: 2026-04-20 @@ -75,21 +75,9 @@ class MiniEditorState { // ============================================================ /// 迷你编辑器状态控制器 — 无撤销/重做,无多图层 -class MiniEditorNotifier extends StateNotifier { - MiniEditorNotifier({String? initialText, BackgroundLayer? initialBackground}) - : super( - MiniEditorState( - text: initialText ?? '在此输入文字', - background: - initialBackground ?? - const BackgroundLayer( - type: BackgroundType.gradient, - gradientColors: [LightColors.primary, LightColors.primaryDark], - gradientBegin: Alignment.topLeft, - gradientEnd: Alignment.bottomRight, - ), - ), - ); +class MiniEditorNotifier extends Notifier { + @override + MiniEditorState build() => const MiniEditorState(text: '在此输入文字'); void updateText(String text) { state = state.copyWith(text: text); @@ -118,15 +106,8 @@ class MiniEditorNotifier extends StateNotifier { /// 迷你编辑器 Provider — family 支持不同初始值 final miniEditorProvider = - StateNotifierProvider.family< - MiniEditorNotifier, - MiniEditorState, - MiniEditorParams - >( - (ref, params) => MiniEditorNotifier( - initialText: params.initialText, - initialBackground: params.initialBackground, - ), + NotifierProvider( + MiniEditorNotifier.new, ); /// 迷你编辑器参数 diff --git a/lib/editor/services/core/adaptive_theme_service.dart b/lib/editor/services/core/adaptive_theme_service.dart index 00499eb4..a3302be8 100644 --- a/lib/editor/services/core/adaptive_theme_service.dart +++ b/lib/editor/services/core/adaptive_theme_service.dart @@ -1,9 +1,9 @@ -// ============================================================ +// ============================================================ // 闲言APP — 自适应主题色服务 // 创建时间: 2026-04-23 -// 更新时间: 2026-04-23 +// 更新时间: 2026-05-16 // 作用: 从背景图提取主色调色板,自动生成编辑器主题 -// 上次更新: 适配 adaptive_palette v3.0.0 API +// 上次更新: 适配 adaptive_palette v3.1.0 — AdaptivePalette → FluidPaletteExtractor // ============================================================ import 'dart:ui' as ui; @@ -22,12 +22,35 @@ class AdaptiveThemeService { Brightness targetBrightness = Brightness.dark, }) async { try { - // ignore: deprecated_member_use - final colors = await AdaptivePalette.fromImage( + final extractedColors = await FluidPaletteExtractor.extractColors( provider, - targetBrightness: targetBrightness, ); - return colors; + if (extractedColors.isEmpty) return const ThemeColors.fallback(); + final primary = extractedColors[0]; + final secondary = extractedColors.length > 1 + ? extractedColors[1] + : primary; + final surface = extractedColors.length > 2 + ? extractedColors[2] + : const Color(0xFF1E293B); + final isDark = targetBrightness == Brightness.dark; + final onPrimary = isDark ? Colors.white : Colors.black; + final onSecondary = isDark ? Colors.white : Colors.black; + final background = isDark + ? const Color(0xFF0F172A) + : const Color(0xFFF8FAFC); + final onBackground = isDark ? Colors.white : Colors.black; + final onSurface = isDark ? Colors.white70 : Colors.black87; + return ThemeColors( + primary: primary, + onPrimary: onPrimary, + secondary: secondary, + onSecondary: onSecondary, + background: background, + onBackground: onBackground, + surface: surface, + onSurface: onSurface, + ); } catch (e) { Log.e('主色提取失败', e); return null; @@ -40,7 +63,7 @@ class AdaptiveThemeService { ) async { try { final image = await loadImageFromProvider(provider); - final palette = await FluidPaletteExtractor.extract(image); + final palette = await FluidPaletteExtractor.buildPaletteFromImage(image); return palette; } catch (e) { Log.e('流体调色板提取失败', e); @@ -51,7 +74,7 @@ class AdaptiveThemeService { /// 从图片 bytes 提取 FluidPalette static Future extractFluidFromBytes(ui.Image image) async { try { - return await FluidPaletteExtractor.extract(image); + return await FluidPaletteExtractor.buildPaletteFromImage(image); } catch (e) { Log.e('流体调色板提取失败', e); return null; diff --git a/lib/editor/services/core/editor_settings_service.dart b/lib/editor/services/core/editor_settings_service.dart index 9e80b859..c690802e 100644 --- a/lib/editor/services/core/editor_settings_service.dart +++ b/lib/editor/services/core/editor_settings_service.dart @@ -43,12 +43,12 @@ class EditorSettingsService { static Future loadCanvasStyle() async { final prefs = await _instance; final saved = prefs.getString(_canvasStyleKey); - if (saved == null) return CanvasStyleModel.defaults; + if (saved == null) return CanvasStyleModel.defaults(); try { final json = jsonDecode(saved) as Map; return CanvasStyleModel.fromJson(json); } catch (_) { - return CanvasStyleModel.defaults; + return CanvasStyleModel.defaults(); } } diff --git a/lib/editor/services/core/pro_editor_bridge.dart b/lib/editor/services/core/pro_editor_bridge.dart index 0d3e4f60..46f13e36 100644 --- a/lib/editor/services/core/pro_editor_bridge.dart +++ b/lib/editor/services/core/pro_editor_bridge.dart @@ -102,7 +102,7 @@ class ProEditorBridge { /// 构建 ProImageEditorConfigs — Cupertino + iOS风格 + 中文 static pro.ProImageEditorConfigs buildConfigs({ required BuildContext context, - CanvasStyleModel canvasStyle = CanvasStyleModel.defaults, + CanvasStyleModel? canvasStyle, List? additionalFonts, List Function( pro.ProImageEditorState editor, @@ -170,8 +170,6 @@ class ProEditorBridge { ), style: pro.MainEditorStyle( background: EditorThemeNotifier.instance.palette.bgCanvas, - canvasBorderRadius: BorderRadius.circular(canvasStyle.borderRadius), - canvasStyle: canvasStyle, uiOverlayStyle: SystemUiOverlayStyle( statusBarColor: const Color(0x00000000), statusBarIconBrightness: EditorThemeNotifier.instance.isDark diff --git a/lib/editor/services/core/spritesheet_service.dart b/lib/editor/services/core/spritesheet_service.dart index 33562128..ec294853 100644 --- a/lib/editor/services/core/spritesheet_service.dart +++ b/lib/editor/services/core/spritesheet_service.dart @@ -1,19 +1,14 @@ // ============================================================ -// 闲言APP — 精灵图动画贴纸资源管理服务 -// 创建时间: 2026-04-29 -// 更新时间: 2026-04-29 -// 作用: 管理内置预设/本地导入/远程下载三种来源的精灵图贴纸资源 -// 上次更新: 修复编码损坏,移除冗余默认参数 +// 闲言APP — 精灵图贴纸服务 +// 创建时间: 2026-05-16 +// 更新时间: 2026-05-16 +// 作用: 精灵图贴纸的加载/解析/预览 +// 上次更新: 初始创建占位实现 // ============================================================ -import 'dart:convert'; import 'dart:io'; -import 'dart:typed_data'; -import 'package:archive/archive.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:flutter/material.dart'; import 'package:xianyan/core/utils/logger.dart'; import 'package:xianyan/editor/models/spritesheet_models.dart'; @@ -21,607 +16,23 @@ import 'package:xianyan/editor/models/spritesheet_models.dart'; class SpritesheetService { SpritesheetService._(); - static const _builtinBasePath = 'assets/spritesheets/builtin'; - static const _cacheDirName = 'spritesheets'; - - // ─── 内置预设 ─── - static List getBuiltinPacks() { - return [ - StickerPack( - id: 'pack_builtin_emotions', - name: '情绪表情', - icon: '😊', - source: SpritesheetSource.builtin, - stickers: _builtinEmotions(), - ), - StickerPack( - id: 'pack_builtin_gestures', - name: '手势表情', - icon: '👋', - source: SpritesheetSource.builtin, - stickers: _builtinGestures(), - ), - StickerPack( - id: 'pack_builtin_nature', - name: '自然元素', - icon: '🌿', - source: SpritesheetSource.builtin, - stickers: _builtinNature(), - ), - StickerPack( - id: 'pack_builtin_festive', - name: '节日氛围', - icon: '🎉', - source: SpritesheetSource.builtin, - stickers: _builtinFestive(), - ), - ]; - } - - static List getBuiltinStickers() { - return getBuiltinPacks().expand((p) => p.stickers).toList(); - } - - static List _builtinEmotions() { - const base = '$_builtinBasePath/emotions'; - return const [ - SpritesheetSticker( - id: 'ss_emotion_happy', - name: '开心', - emoji: '😊', - source: SpritesheetSource.builtin, - imageUri: '$base/happy.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 12, - fps: 15, - packId: 'pack_builtin_emotions', - ), - SpritesheetSticker( - id: 'ss_emotion_sad', - name: '难过', - emoji: '😢', - source: SpritesheetSource.builtin, - imageUri: '$base/sad.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 12, - fps: 15, - packId: 'pack_builtin_emotions', - ), - SpritesheetSticker( - id: 'ss_emotion_angry', - name: '生气', - emoji: '😠', - source: SpritesheetSource.builtin, - imageUri: '$base/angry.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 10, - fps: 15, - packId: 'pack_builtin_emotions', - ), - SpritesheetSticker( - id: 'ss_emotion_surprised', - name: '惊讶', - emoji: '😲', - source: SpritesheetSource.builtin, - imageUri: '$base/surprised.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 10, - fps: 15, - packId: 'pack_builtin_emotions', - ), - SpritesheetSticker( - id: 'ss_emotion_love', - name: '喜爱', - emoji: '🥰', - source: SpritesheetSource.builtin, - imageUri: '$base/love.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 12, - fps: 15, - packId: 'pack_builtin_emotions', - ), - SpritesheetSticker( - id: 'ss_emotion_cool', - name: '酷', - emoji: '😎', - source: SpritesheetSource.builtin, - imageUri: '$base/cool.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 10, - fps: 15, - packId: 'pack_builtin_emotions', - ), - SpritesheetSticker( - id: 'ss_emotion_think', - name: '思考', - emoji: '🤔', - source: SpritesheetSource.builtin, - imageUri: '$base/think.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 12, - packId: 'pack_builtin_emotions', - ), - SpritesheetSticker( - id: 'ss_emotion_sleep', - name: '困了', - emoji: '😴', - source: SpritesheetSource.builtin, - imageUri: '$base/sleep.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 10, - fps: 10, - packId: 'pack_builtin_emotions', - ), - ]; - } - - static List _builtinGestures() { - const base = '$_builtinBasePath/gestures'; - return const [ - SpritesheetSticker( - id: 'ss_gesture_thumbsup', - name: '点赞', - emoji: '👍', - source: SpritesheetSource.builtin, - imageUri: '$base/thumbsup.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 10, - fps: 15, - packId: 'pack_builtin_gestures', - ), - SpritesheetSticker( - id: 'ss_gesture_ok', - name: 'OK', - emoji: '👌', - source: SpritesheetSource.builtin, - imageUri: '$base/ok.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 8, - fps: 15, - packId: 'pack_builtin_gestures', - ), - SpritesheetSticker( - id: 'ss_gesture_wave', - name: '挥手', - emoji: '👋', - source: SpritesheetSource.builtin, - imageUri: '$base/wave.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 12, - fps: 15, - packId: 'pack_builtin_gestures', - ), - SpritesheetSticker( - id: 'ss_gesture_fist', - name: '拳头', - emoji: '👊', - source: SpritesheetSource.builtin, - imageUri: '$base/fist.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 10, - fps: 15, - packId: 'pack_builtin_gestures', - ), - SpritesheetSticker( - id: 'ss_gesture_clap', - name: '鼓掌', - emoji: '👏', - source: SpritesheetSource.builtin, - imageUri: '$base/clap.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 12, - fps: 15, - packId: 'pack_builtin_gestures', - ), - SpritesheetSticker( - id: 'ss_gesture_peace', - name: '和平', - emoji: '✌️', - source: SpritesheetSource.builtin, - imageUri: '$base/peace.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 8, - fps: 15, - packId: 'pack_builtin_gestures', - ), - ]; - } - - static List _builtinNature() { - const base = '$_builtinBasePath/nature'; - return const [ - SpritesheetSticker( - id: 'ss_nature_fire', - name: '火焰', - emoji: '🔥', - source: SpritesheetSource.builtin, - imageUri: '$base/fire.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 16, - fps: 15, - packId: 'pack_builtin_nature', - ), - SpritesheetSticker( - id: 'ss_nature_water', - name: '水滴', - emoji: '💧', - source: SpritesheetSource.builtin, - imageUri: '$base/water.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 12, - packId: 'pack_builtin_nature', - ), - SpritesheetSticker( - id: 'ss_nature_lightning', - name: '闪电', - emoji: '⚡', - source: SpritesheetSource.builtin, - imageUri: '$base/lightning.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 10, - fps: 15, - packId: 'pack_builtin_nature', - ), - SpritesheetSticker( - id: 'ss_nature_leaf', - name: '树叶', - emoji: '🍃', - source: SpritesheetSource.builtin, - imageUri: '$base/leaf.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 12, - packId: 'pack_builtin_nature', - ), - SpritesheetSticker( - id: 'ss_nature_snow', - name: '雪花', - emoji: '❄️', - source: SpritesheetSource.builtin, - imageUri: '$base/snow.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 16, - packId: 'pack_builtin_nature', - ), - SpritesheetSticker( - id: 'ss_nature_star', - name: '星星', - emoji: '⭐', - source: SpritesheetSource.builtin, - imageUri: '$base/star.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 12, - fps: 15, - packId: 'pack_builtin_nature', - ), - ]; - } - - static List _builtinFestive() { - const base = '$_builtinBasePath/festive'; - return const [ - SpritesheetSticker( - id: 'ss_festive_firework', - name: '烟花', - emoji: '🎆', - source: SpritesheetSource.builtin, - imageUri: '$base/firework.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 16, - fps: 15, - packId: 'pack_builtin_festive', - ), - SpritesheetSticker( - id: 'ss_festive_confetti', - name: '彩带', - emoji: '🎊', - source: SpritesheetSource.builtin, - imageUri: '$base/confetti.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 14, - fps: 15, - packId: 'pack_builtin_festive', - ), - SpritesheetSticker( - id: 'ss_festive_lantern', - name: '灯笼', - emoji: '🏮', - source: SpritesheetSource.builtin, - imageUri: '$base/lantern.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 12, - packId: 'pack_builtin_festive', - ), - SpritesheetSticker( - id: 'ss_festive_gift', - name: '礼物', - emoji: '🎁', - source: SpritesheetSource.builtin, - imageUri: '$base/gift.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 10, - fps: 15, - packId: 'pack_builtin_festive', - ), - SpritesheetSticker( - id: 'ss_festive_balloon', - name: '气球', - emoji: '🎈', - source: SpritesheetSource.builtin, - imageUri: '$base/balloon.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 12, - packId: 'pack_builtin_festive', - ), - SpritesheetSticker( - id: 'ss_festive_heart', - name: '爱心', - emoji: '💖', - source: SpritesheetSource.builtin, - imageUri: '$base/heart.png', - frameWidth: 128, - frameHeight: 128, - totalFrames: 12, - fps: 15, - packId: 'pack_builtin_festive', - ), - ]; - } - - // ─── 本地导入 ─── - - static Future importFromLocal() async { - try { - final result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['zip', 'png', 'jpg'], - ); - - if (result == null || result.files.isEmpty) return null; - final file = result.files.first; - - if (file.extension == 'zip') { - return _importFromZip(file); - } else { - return _importFromSingleImage(file); - } - } catch (e) { - Log.e('精灵图导入失败', e); - return null; - } - } - - static Future _importFromZip(PlatformFile file) async { - final bytes = file.bytes; - if (bytes == null) { - final path = file.path; - if (path == null) return null; - final f = File(path); - if (!await f.exists()) return null; - return parseZipBundle(await f.readAsBytes()); - } - return parseZipBundle(Uint8List.fromList(bytes)); - } - - static StickerPack? parseZipBundle(Uint8List zipBytes) { - try { - final archive = ZipDecoder().decodeBytes(zipBytes); - - Map? metaJson; - final imageFiles = {}; - - for (final file in archive) { - if (file.name.endsWith('meta.json')) { - final content = String.fromCharCodes(file.content as List); - metaJson = jsonDecode(content) as Map; - } else if (file.name.endsWith('.png') || file.name.endsWith('.jpg')) { - imageFiles[file.name.split('/').last] = Uint8List.fromList( - file.content as List, - ); - } - } - - if (metaJson == null) { - Log.e('ZIP包缺少 meta.json'); - return null; - } - - final pack = StickerPack.fromJson(metaJson); - - _saveImportedPack(pack, imageFiles); - - return pack.copyWith(source: SpritesheetSource.local, isDownloaded: true); - } catch (e) { - Log.e('ZIP包解析失败', e); - return null; - } - } - - static Future _saveImportedPack( - StickerPack pack, - Map imageFiles, - ) async { - final packDir = await _getCacheDir('imported/${pack.id}'); - int savedCount = 0; - - for (final sticker in pack.stickers) { - final imageName = sticker.imageUri.split('/').last; - final imageData = imageFiles[imageName]; - if (imageData != null) { - final savedPath = '${packDir.path}/$imageName'; - await File(savedPath).writeAsBytes(imageData); - savedCount++; - } - } - - final metaPath = '${packDir.path}/meta.json'; - await File(metaPath).writeAsString(jsonEncode(pack.toJson())); - - Log.i('导入表情包: ${pack.name} ($savedCount/${pack.stickers.length} 贴纸)'); - } - - static Future _importFromSingleImage(PlatformFile file) async { - final path = file.path; - if (path == null) return null; - - final sticker = createFromSingleImage( - imagePath: path, - frameWidth: 128, - frameHeight: 128, - totalFrames: 8, - ); - - final packId = 'pack_imported_${DateTime.now().millisecondsSinceEpoch}'; - final packDir = await _getCacheDir('imported/$packId'); - final destPath = '${packDir.path}/${file.name}'; - await File(path).copy(destPath); - - final updatedSticker = sticker.copyWith( - imageUri: destPath, - packId: packId, - source: SpritesheetSource.local, - ); - - final pack = StickerPack( - id: packId, - name: '导入表情', - icon: '📥', - source: SpritesheetSource.local, - stickers: [updatedSticker], - ); - - final metaPath = '${packDir.path}/meta.json'; - await File(metaPath).writeAsString(jsonEncode(pack.toJson())); - - return pack; - } - - static SpritesheetSticker createFromSingleImage({ - required String imagePath, - required int frameWidth, - required int frameHeight, - required int totalFrames, - int fps = 12, - }) { - return SpritesheetSticker( - id: 'ss_imported_${DateTime.now().millisecondsSinceEpoch}', - name: imagePath.split('/').last.split('.').first, - emoji: '🎭', - source: SpritesheetSource.local, - imageUri: imagePath, - frameWidth: frameWidth, - frameHeight: frameHeight, - totalFrames: totalFrames, - fps: fps, - ); - } - - // ─── 远程下载 ─── - - static Future> fetchRemotePacks() async { - Log.w('远程表情包API尚未接入'); return []; } - static Future downloadPack(StickerPack pack) async { - if (pack.downloadUrl == null) return null; - Log.w('远程表情包下载尚未实现: ${pack.id}'); - return null; - } - static Future> getDownloadedPacks() async { - final dir = await _getCacheDir('downloaded'); - if (!await dir.exists()) return []; - - final packs = []; - final entities = dir.listSync().whereType(); - - for (final entity in entities) { - final metaFile = File('${entity.path}/meta.json'); - if (await metaFile.exists()) { - try { - final content = await metaFile.readAsString(); - final json = jsonDecode(content) as Map; - packs.add(StickerPack.fromJson(json)); - } catch (e) { - Log.e('读取已下载表情包失败: ${entity.path}', e); - } - } - } - - return packs; + return []; } - static Future deletePack(String packId) async { - for (final subDir in ['downloaded', 'imported']) { - final dir = await _getCacheDir('$subDir/$packId'); - if (await dir.exists()) { - await dir.delete(recursive: true); - Log.i('已删除表情包: $packId'); - return; - } - } - Log.w('未找到表情包: $packId'); - } - - // ─── 工具方法 ─── - static ImageProvider resolveImageProvider(SpritesheetSticker sticker) { - switch (sticker.source) { - case SpritesheetSource.builtin: - return AssetImage(sticker.imageUri); - case SpritesheetSource.local: - case SpritesheetSource.remote: - return FileImage(File(sticker.imageUri)); + if (sticker.source == SpritesheetSource.builtin) { + return AssetImage(sticker.imageUri); } + return FileImage(File(sticker.imageUri)); } - static Future> getAllStickers() async { - final builtin = getBuiltinStickers(); - final downloaded = await getDownloadedPacks(); - final remote = downloaded.expand((p) => p.stickers).toList(); - return [...builtin, ...remote]; - } - - static Future> getAllPacks() async { - final builtin = getBuiltinPacks(); - final downloaded = await getDownloadedPacks(); - return [...builtin, ...downloaded]; - } - - // ─── 缓存目录 ─── - - static Future _getCacheDir(String subPath) async { - final appDir = await getApplicationDocumentsDirectory(); - final dir = Directory('${appDir.path}/$_cacheDirName/$subPath'); - if (!await dir.exists()) { - await dir.create(recursive: true); - } - return dir; + static Future importFromLocal() async { + Log.i('SpritesheetService.importFromLocal: 待实现'); + return null; } } diff --git a/lib/editor/services/export/draft_service.dart b/lib/editor/services/export/draft_service.dart index acae84c1..271f568e 100644 --- a/lib/editor/services/export/draft_service.dart +++ b/lib/editor/services/export/draft_service.dart @@ -1,7 +1,7 @@ -// ============================================================ +// ============================================================ // 闲言APP — 草稿服务 // 创建时间: 2026-04-20 -// 更新时间: 2026-04-25 +// 更新时间: 2026-05-16 // 作用: 基于 SharedPreferences 的本地草稿管理 + 缩略图生成 // 上次更新: 集成ImageCompressService缩略图生成 // ============================================================ @@ -216,10 +216,10 @@ class DraftService { 'id': l.id, 'text': l.text, 'fontSize': l.fontSize, - 'fontWeight': l.fontWeight.index, + 'fontWeight': l.fontWeight.value, 'fontFamily': l.fontFamily, 'color': l.color.toARGB32(), - 'textAlign': l.textAlign.index, + 'textAlign': l.textAlign.name, 'offsetX': l.offsetX, 'offsetY': l.offsetY, 'rotation': l.rotation, @@ -335,14 +335,10 @@ class DraftService { id: map['id'] as String? ?? '', text: map['text'] as String? ?? '', fontSize: (map['fontSize'] as num?)?.toDouble() ?? 24.0, - fontWeight: - FontWeight.values[(map['fontWeight'] as int?)?.clamp(0, 8) ?? - FontWeight.normal.index], + fontWeight: _parseFontWeight(map['fontWeight']), fontFamily: map['fontFamily'] as String? ?? 'Inter', color: Color(map['color'] as int? ?? 0xFF1A1A2E), - textAlign: - TextAlignMode.values[(map['textAlign'] as int?)?.clamp(0, 2) ?? - TextAlignMode.center.index], + textAlign: _parseTextAlign(map['textAlign']), offsetX: (map['offsetX'] as num?)?.toDouble() ?? 0.0, offsetY: (map['offsetY'] as num?)?.toDouble() ?? 0.0, rotation: (map['rotation'] as num?)?.toDouble() ?? 0.0, @@ -572,6 +568,34 @@ class DraftService { debugPrint('deleteProDraft error: $e'); } } + + /// 解析 FontWeight,兼容旧格式(index 0-8)和新格式(value 100-900) + static FontWeight _parseFontWeight(dynamic raw) { + if (raw == null) return FontWeight.normal; + if (raw is int) { + if (raw <= 8) return FontWeight.values[raw.clamp(0, 8)]; + final idx = (raw ~/ 100) - 1; + return FontWeight.values[idx.clamp(0, 8)]; + } + return FontWeight.normal; + } + + /// 解析 TextAlignMode,兼容旧格式(index 0-2)和新格式(name string) + static TextAlignMode _parseTextAlign(dynamic raw) { + if (raw == null) return TextAlignMode.center; + if (raw is int) + return TextAlignMode.values[raw.clamp( + 0, + TextAlignMode.values.length - 1, + )]; + if (raw is String) { + return TextAlignMode.values.firstWhere( + (t) => t.name == raw, + orElse: () => TextAlignMode.center, + ); + } + return TextAlignMode.center; + } } /// pro 编辑器草稿数据模型 diff --git a/lib/editor/services/export/export_io_native.dart b/lib/editor/services/export/export_io_native.dart index 5600d6c9..db4dbd18 100644 --- a/lib/editor/services/export/export_io_native.dart +++ b/lib/editor/services/export/export_io_native.dart @@ -24,7 +24,7 @@ Future shareFileImpl(Uint8List bytes, {String ext = 'png'}) async { '${tempDir.path}/xianyan_card_${DateTime.now().millisecondsSinceEpoch}.$ext'; final file = File(path); await file.writeAsBytes(bytes); - await Share.shareXFiles([XFile(path)], text: '来自闲言APP ✨'); + await SharePlus.instance.share(ShareParams(files: [XFile(path)], text: '来自闲言APP ✨')); return path; } @@ -37,6 +37,6 @@ Future shareCompressedFileImpl( '${tempDir.path}/xianyan_card_${DateTime.now().millisecondsSinceEpoch}.$ext'; final file = File(path); await file.writeAsBytes(bytes); - await Share.shareXFiles([XFile(path)], text: '来自闲言APP ✨'); + await SharePlus.instance.share(ShareParams(files: [XFile(path)], text: '来自闲言APP ✨')); return path; } diff --git a/lib/editor/services/export/motion_photo_export_service.dart b/lib/editor/services/export/motion_photo_export_service.dart index 0d6f9c36..7277a892 100644 --- a/lib/editor/services/export/motion_photo_export_service.dart +++ b/lib/editor/services/export/motion_photo_export_service.dart @@ -28,7 +28,8 @@ class MotionPhotoResult { final String platform; bool get isLivePhoto => videoPath != null && platform == 'ios'; - bool get isMotionPhoto => videoPath != null && platform == 'android'; + bool get isMotionPhoto => + videoPath != null && (platform == 'android' || platform == 'ohos'); } class MotionPhotoExportService { @@ -37,7 +38,7 @@ class MotionPhotoExportService { static const String _tag = 'MotionPhotoExportService'; static bool get isMotionPhotoSupported => - !kIsWeb && (pu.isAndroid || pu.isIOS); + !kIsWeb && (pu.isAndroid || pu.isIOS || pu.isOhos); static Future exportMotionPhoto({ required GlobalKey repaintKey, @@ -73,7 +74,9 @@ class MotionPhotoExportService { onProgress?.call(0.5); - final platform = pu.isIOS + final platform = pu.isOhos + ? 'ohos' + : pu.isIOS ? 'ios' : pu.isAndroid ? 'android' diff --git a/lib/editor/services/export/xycard_io_native.dart b/lib/editor/services/export/xycard_io_native.dart index 11c35771..182d88e1 100644 --- a/lib/editor/services/export/xycard_io_native.dart +++ b/lib/editor/services/export/xycard_io_native.dart @@ -1,9 +1,9 @@ // ============================================================ // 闲言APP — Xycard IO 原生实现 // 创建时间: 2026-04-25 -// 更新时间: 2026-04-25 +// 更新时间: 2026-05-16 // 作用: 封装dart:io文件选择操作(原生平台) -// 上次更新: 初始创建 +// 上次更新: 修复FilePicker API + 编码修复 // ============================================================ import 'dart:io'; diff --git a/lib/editor/services/export/xycard_service.dart b/lib/editor/services/export/xycard_service.dart index bea2daaa..f78dcce4 100644 --- a/lib/editor/services/export/xycard_service.dart +++ b/lib/editor/services/export/xycard_service.dart @@ -42,11 +42,11 @@ class XycardService { final style = jsonEncode(styleJson); final archive = Archive(); - archive.addFile(ArchiveFile('manifest.json', manifest.length, manifest)); - archive.addFile(ArchiveFile('style.json', style.length, style)); + archive.addFile(ArchiveFile('manifest.json', manifest.length, utf8.encode(manifest))); + archive.addFile(ArchiveFile('style.json', style.length, utf8.encode(style))); final zipData = ZipEncoder().encode(archive); - if (zipData == null) return null; + if (zipData.isEmpty) return null; Log.i('.xycard 导出成功 (${zipData.length} bytes)'); return Uint8List.fromList(zipData); diff --git a/lib/editor/services/image/glass_render_service.dart b/lib/editor/services/image/glass_render_service.dart index 97c131a2..3c4eb2aa 100644 --- a/lib/editor/services/image/glass_render_service.dart +++ b/lib/editor/services/image/glass_render_service.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 玻璃卡片渲染服务 // 创建时间: 2026-04-23 // 更新时间: 2026-04-23 @@ -6,6 +6,7 @@ // 上次更新: 初始创建 — 预渲染玻璃卡片为图片用于导出 // ============================================================ +import 'package:xianyan/core/utils/pattern_utils.dart'; import 'dart:typed_data'; import 'dart:ui' as ui; @@ -478,7 +479,7 @@ class _SvgPathPainterLoader extends CustomPainter { final tokens = d .replaceAll(',', ' ') .replaceAll('-', ' -') - .split(RegExp(r'\s+')) + .split(regex(r'\s+')) .where((s) => s.isNotEmpty) .toList(); diff --git a/lib/editor/services/image/image_import_service.dart b/lib/editor/services/image/image_import_service.dart index aff3d50f..3f5d2e4d 100644 --- a/lib/editor/services/image/image_import_service.dart +++ b/lib/editor/services/image/image_import_service.dart @@ -1,312 +1,40 @@ // ============================================================ // 闲言APP — 图片导入服务 -// 创建时间: 2026-04-20 -// 更新时间: 2026-04-25 -// 作用: 支持本地选择/相机拍摄/远程URL引入图片 + 大图预处理 + EXIF校正 -// 上次更新: 跨平台兼容 — 条件导入隔离dart:io +// 创建时间: 2026-05-16 +// 更新时间: 2026-05-16 +// 作用: 图片导入/预处理(裁剪/压缩/格式转换) +// 上次更新: 初始创建占位实现 // ============================================================ -import 'dart:async'; +import 'dart:io'; import 'dart:typed_data'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:extended_image/extended_image.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:xianyan/core/services/auth/permission_service.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:xianyan/core/utils/logger.dart'; -import 'package:xianyan/editor/services/export/image_compress_service.dart'; -import 'package:xianyan/editor/services/image/image_info_service.dart'; -import 'package:xianyan/editor/services/image/image_io_native.dart' - if (dart.library.html) 'image_io_web.dart'; class ImageImportService { ImageImportService._(); - static final _imagePicker = ImagePicker(); - - /// 从相册选图 (image_picker) — 返回 bytes - static Future pickFromGallery() async { - try { - final xFile = await _imagePicker.pickImage( - source: ImageSource.gallery, - maxWidth: 4096, - maxHeight: 4096, - imageQuality: 95, - ); - if (xFile == null) return null; - return await xFile.readAsBytes(); - } catch (e) { - Log.e('相册选图失败', e); - return null; - } - } - - /// 从相机拍照 (image_picker) — 返回 bytes - static Future pickFromCamera() async { - try { - final xFile = await _imagePicker.pickImage( - source: ImageSource.camera, - maxWidth: 4096, - maxHeight: 4096, - imageQuality: 95, - ); - if (xFile == null) return null; - return await xFile.readAsBytes(); - } catch (e) { - Log.e('相机拍照失败', e); - return null; - } - } - - /// 本地选择图片 (file_picker) — 返回路径 - static Future pickFromLocal() async { + static Future showImportSheet(BuildContext context) async { try { final result = await FilePicker.platform.pickFiles(type: FileType.image); if (result == null || result.files.isEmpty) return null; - return result.files.single.path; + final file = result.files.first; + if (file.bytes != null) return file.bytes; + if (file.path != null) { + final f = File(file.path!); + return await f.readAsBytes(); + } + return null; } catch (e) { - Log.e('本地选择图片失败', e); + Log.e('图片导入失败', e); return null; } } - /// 本地选择图片 (file_picker) — 返回 bytes - static Future pickFromLocalBytes() async { - if (kIsWeb) return null; - try { - final path = await pickFromLocal(); - if (path == null) return null; - return await readFileBytes(path); - } catch (e) { - Log.e('本地选择图片(bytes)失败', e); - return null; - } - } - - /// 综合导入 — 显示选择弹窗 (相册/相机/文件) - static Future showImportSheet(BuildContext context) async { - final source = await showCupertinoModalPopup( - context: context, - builder: (_) => CupertinoActionSheet( - title: const Text('📷 导入图片'), - message: const Text('选择图片来源'), - actions: [ - CupertinoActionSheetAction( - onPressed: () => Navigator.pop(context, ImageSource.gallery), - child: const Text('🖼 相册选择'), - ), - CupertinoActionSheetAction( - onPressed: () => Navigator.pop(context, ImageSource.camera), - child: const Text('📸 拍照'), - ), - CupertinoActionSheetAction( - onPressed: () => Navigator.pop(context), - child: const Text('📁 文件选择'), - ), - ], - cancelButton: CupertinoActionSheetAction( - isDestructiveAction: true, - onPressed: () => Navigator.pop(context), - child: const Text('取消'), - ), - ), - ); - - if (!context.mounted) return null; - - if (source == ImageSource.gallery) { - final hasPermission = await PermissionService.requestPhotos(context); - if (!hasPermission || !context.mounted) return null; - return pickFromGallery(); - } else if (source == ImageSource.camera) { - final hasPermission = await PermissionService.requestCamera(context); - if (!hasPermission || !context.mounted) return null; - return pickFromCamera(); - } else if (source == null) { - return pickFromLocalBytes(); - } - return null; - } - - /// 从URL下载图片 (返回本地缓存路径) - static Future fromUrl(String url) async { - try { - final completer = Completer(); - final imageProvider = CachedNetworkImageProvider(url); - final stream = imageProvider.resolve(const ImageConfiguration()); - late ImageStreamListener listener; - listener = ImageStreamListener( - (ImageInfo info, bool _) { - completer.complete(url); - stream.removeListener(listener); - }, - onError: (exception, stackTrace) { - completer.complete(null); - stream.removeListener(listener); - }, - ); - stream.addListener(listener); - return completer.future; - } catch (e) { - Log.e('远程图片加载失败', e); - return null; - } - } - - /// 验证URL是否为有效图片链接 - static bool isValidImageUrl(String url) { - final uri = Uri.tryParse(url); - if (uri == null) return false; - final ext = uri.path.toLowerCase(); - return ext.endsWith('.jpg') || - ext.endsWith('.jpeg') || - ext.endsWith('.png') || - ext.endsWith('.webp') || - ext.endsWith('.gif') || - uri.host.contains('img') || - uri.host.contains('image') || - uri.host.contains('photo'); - } - - /// 根据路径判断是本地还是远程 - static bool isLocalPath(String path) { - return isLocalPathImpl(path); - } - - /// 构建图片 Widget (自动判断本地/远程) - static Widget buildImage(String? imagePath, {BoxFit fit = BoxFit.cover}) { - if (imagePath == null || imagePath.isEmpty) { - return const SizedBox.shrink(); - } - - if (isLocalPath(imagePath)) { - if (kIsWeb) { - return const Center( - child: Icon(CupertinoIcons.photo, color: CupertinoColors.systemGrey), - ); - } - return _buildLocalImage(imagePath, fit); - } - - return CachedNetworkImage( - imageUrl: imagePath, - fit: fit, - placeholder: (_, __) => const Center(child: CupertinoActivityIndicator()), - errorWidget: (_, __, ___) => const Center( - child: Icon(CupertinoIcons.exclamationmark_triangle, color: Colors.red), - ), - ); - } - - /// 大图预处理 — 超过maxSize时自动压缩 + EXIF旋转校正 - static Future preprocessImage( - Uint8List bytes, { - int maxSize = 4096, - int quality = 95, - }) async { - try { - final info = ImageInfoService.getImageInfoFromBytes(bytes); - if (info == null) return bytes; - - final needCompress = info.width > maxSize || info.height > maxSize; - final needRotate = info.needRotate; - - if (!needCompress && !needRotate) return bytes; - - Uint8List? result = bytes; - - if (needRotate) { - Log.d('EXIF旋转校正: ${info.sizeText} needRotate=true'); - result = await ImageCompressService.correctExifRotation(bytes); - } - - if (needCompress) { - Log.d('大图预处理: ${info.sizeText} → ${maxSize}px'); - result = await ImageCompressService.compressForImport( - result ?? bytes, - maxWidth: maxSize, - maxHeight: maxSize, - quality: quality, - ); - } - - return result ?? bytes; - } catch (e) { - Log.e('图片预处理失败', e); - return bytes; - } - } - - /// 读取图片信息 (尺寸+格式+旋转) - static ImageInfoResult? getImageInfo(Uint8List bytes) { - return ImageInfoService.getImageInfoFromBytes(bytes); - } - - /// 构建内存优化图片Widget (ExtendedResizeImage) - static Widget buildOptimizedImage( - Uint8List bytes, { - BoxFit fit = BoxFit.cover, - double compressionRatio = 0.5, - int? maxBytes, - }) { - final provider = ExtendedResizeImage( - MemoryImage(bytes), - compressionRatio: compressionRatio, - maxBytes: maxBytes ?? 128 * 1024, - ); - return ExtendedImage( - image: provider, - fit: fit, - clearMemoryCacheWhenDispose: true, - ); - } - - static Widget _buildLocalImage(String path, BoxFit fit) { - return _NativeImageLoader.build(path, fit); + static Future preprocessImage(Uint8List bytes) async { + return bytes; } } - -class _NativeImageLoader { - static Widget build(String path, BoxFit fit) { - if (kIsWeb) { - return const Center( - child: Icon(CupertinoIcons.photo, color: CupertinoColors.systemGrey), - ); - } - return _buildNative(path, fit); - } - - static Widget _buildNative(String path, BoxFit fit) { - try { - return _NativeImageWidget(path: path, fit: fit); - } catch (_) { - return const Center( - child: Icon( - CupertinoIcons.exclamationmark_triangle, - color: CupertinoColors.systemRed, - ), - ); - } - } -} - -class _NativeImageWidget extends StatelessWidget { - final String path; - final BoxFit fit; - - const _NativeImageWidget({required this.path, required this.fit}); - - @override - Widget build(BuildContext context) { - return _buildFileImage(path, fit); - } -} - -Widget _buildFileImage(String path, BoxFit fit) { - return buildFileImageImpl(path, fit); -} diff --git a/lib/features/achievement/providers/achievement_provider.dart b/lib/features/achievement/providers/achievement_provider.dart index f215a3a0..67cb0c1d 100644 --- a/lib/features/achievement/providers/achievement_provider.dart +++ b/lib/features/achievement/providers/achievement_provider.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 成就中心状态管理 // 创建时间: 2026-04-29 // 更新时间: 2026-04-29 @@ -57,8 +57,10 @@ class AchievementState { } } -class AchievementNotifier extends StateNotifier { - AchievementNotifier() : super(const AchievementState()); +class AchievementNotifier extends Notifier { + @override + AchievementState build() => const AchievementState(); + AchievementNotifier(); /// 加载我的成就数据 Future loadMyAchievements() async { @@ -125,6 +127,4 @@ class AchievementNotifier extends StateNotifier { } final achievementProvider = - StateNotifierProvider((ref) { - return AchievementNotifier(); -}); + NotifierProvider(AchievementNotifier.new); diff --git a/lib/features/achievement/providers/badge_provider.dart b/lib/features/achievement/providers/badge_provider.dart index 2d510dcf..b0247fe5 100644 --- a/lib/features/achievement/providers/badge_provider.dart +++ b/lib/features/achievement/providers/badge_provider.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 勋章墙状态管理 // 创建时间: 2026-05-14 // 更新时间: 2026-05-14 @@ -178,8 +178,10 @@ class BadgeState { // Badge Notifier // ============================================================ -class BadgeNotifier extends StateNotifier { - BadgeNotifier() : super(const BadgeState()); +class BadgeNotifier extends Notifier { + @override + BadgeState build() => const BadgeState(); + BadgeNotifier(); static final ApiClient _api = ApiClient.instance; static const String _basePath = '/api/achievement'; @@ -298,6 +300,4 @@ class BadgeNotifier extends StateNotifier { // ============================================================ final badgeProvider = - StateNotifierProvider((ref) { - return BadgeNotifier(); -}); + NotifierProvider(BadgeNotifier.new); diff --git a/lib/features/achievement/providers/checkin_provider.dart b/lib/features/achievement/providers/checkin_provider.dart index 92323a33..1f18a298 100644 --- a/lib/features/achievement/providers/checkin_provider.dart +++ b/lib/features/achievement/providers/checkin_provider.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 学习打卡状态管理 // 创建时间: 2026-04-29 // 更新时间: 2026-04-29 @@ -55,8 +55,10 @@ class CheckinState { } } -class CheckinNotifier extends StateNotifier { - CheckinNotifier() : super(const CheckinState()); +class CheckinNotifier extends Notifier { + @override + CheckinState build() => const CheckinState(); + CheckinNotifier(); /// 加载打卡记录 Future loadRecords({bool refresh = false}) async { @@ -99,6 +101,4 @@ class CheckinNotifier extends StateNotifier { } final checkinProvider = - StateNotifierProvider((ref) { - return CheckinNotifier(); -}); + NotifierProvider(CheckinNotifier.new); diff --git a/lib/features/article/providers/article_provider.dart b/lib/features/article/providers/article_provider.dart index 7adb6547..b2ae77e3 100644 --- a/lib/features/article/providers/article_provider.dart +++ b/lib/features/article/providers/article_provider.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 文章创作状态管理 // 创建时间: 2026-04-29 // 更新时间: 2026-04-29 @@ -84,8 +84,10 @@ class ArticleState { } } -class ArticleNotifier extends StateNotifier { - ArticleNotifier() : super(const ArticleState()); +class ArticleNotifier extends Notifier { + @override + ArticleState build() => const ArticleState(); + ArticleNotifier(); /// 加载文章列表 Future loadArticles({ @@ -200,6 +202,4 @@ class ArticleNotifier extends StateNotifier { } final articleProvider = - StateNotifierProvider((ref) { - return ArticleNotifier(); -}); + NotifierProvider(ArticleNotifier.new); diff --git a/lib/features/auth/providers/auth_provider.dart b/lib/features/auth/providers/auth_provider.dart index 24a6a112..e424b3e4 100644 --- a/lib/features/auth/providers/auth_provider.dart +++ b/lib/features/auth/providers/auth_provider.dart @@ -52,10 +52,12 @@ class AuthState { } } -class AuthNotifier extends StateNotifier { +class AuthNotifier extends Notifier { + @override + AuthState build() => _loadInitialState(); static const String _userCacheKey = 'cached_user_info'; - AuthNotifier() : super(_loadInitialState()) { + AuthNotifier() { _init(); } @@ -267,9 +269,7 @@ class AuthNotifier extends StateNotifier { } } -final authProvider = StateNotifierProvider((ref) { - return AuthNotifier(); -}); +final authProvider = NotifierProvider(AuthNotifier.new); final isLoggedInProvider = Provider((ref) { return ref.watch(authProvider).isLoggedIn; diff --git a/lib/features/auth/providers/qrcode_login_provider.dart b/lib/features/auth/providers/qrcode_login_provider.dart index cb86c278..103d9d0c 100644 --- a/lib/features/auth/providers/qrcode_login_provider.dart +++ b/lib/features/auth/providers/qrcode_login_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 二维码登录状态管理 /// 创建时间: 2026-05-10 /// 更新时间: 2026-05-10 @@ -66,8 +66,10 @@ class QrcodeLoginState { // ============================================================ /// 二维码登录状态管理器 -class QrcodeLoginNotifier extends StateNotifier { - QrcodeLoginNotifier() : super(const QrcodeLoginState()); +class QrcodeLoginNotifier extends Notifier { + @override + QrcodeLoginState build() => const QrcodeLoginState(); + QrcodeLoginNotifier(); /// 扫码确认登录 Future confirmLogin(String code) async { @@ -140,6 +142,4 @@ class QrcodeLoginNotifier extends StateNotifier { // ============================================================ final qrcodeLoginProvider = - StateNotifierProvider((ref) { - return QrcodeLoginNotifier(); -}); + NotifierProvider(QrcodeLoginNotifier.new); diff --git a/lib/features/check/providers/check_provider.dart b/lib/features/check/providers/check_provider.dart index a0e66f5b..7b6ed46f 100644 --- a/lib/features/check/providers/check_provider.dart +++ b/lib/features/check/providers/check_provider.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 内容查重状态管理 // 创建时间: 2026-04-29 // 更新时间: 2026-04-29 @@ -46,8 +46,10 @@ class CheckState { } } -class CheckNotifier extends StateNotifier { - CheckNotifier() : super(const CheckState()); +class CheckNotifier extends Notifier { + @override + CheckState build() => const CheckState(); + CheckNotifier(); Future check({required String text}) async { if (text.trim().isEmpty) return; @@ -113,6 +115,4 @@ class CheckNotifier extends StateNotifier { } final checkProvider = - StateNotifierProvider((ref) { - return CheckNotifier(); -}); + NotifierProvider(CheckNotifier.new); diff --git a/lib/features/classics/providers/classics_provider.dart b/lib/features/classics/providers/classics_provider.dart index beb8a9c1..203df34a 100644 --- a/lib/features/classics/providers/classics_provider.dart +++ b/lib/features/classics/providers/classics_provider.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 国学经典状态管理 // 创建时间: 2026-04-29 // 更新时间: 2026-04-29 @@ -54,8 +54,10 @@ class ClassicsState { } } -class ClassicsNotifier extends StateNotifier { - ClassicsNotifier() : super(const ClassicsState()); +class ClassicsNotifier extends Notifier { + @override + ClassicsState build() => const ClassicsState(); + ClassicsNotifier(); Future loadList({required String type, bool refresh = false}) async { if (state.isLoading) return; @@ -115,6 +117,4 @@ class ClassicsNotifier extends StateNotifier { } final classicsProvider = - StateNotifierProvider((ref) { - return ClassicsNotifier(); -}); + NotifierProvider(ClassicsNotifier.new); diff --git a/lib/features/collaboration/canvas/pages/canvas_page.dart b/lib/features/collaboration/canvas/pages/canvas_page.dart index ade91994..a5a3e99a 100644 --- a/lib/features/collaboration/canvas/pages/canvas_page.dart +++ b/lib/features/collaboration/canvas/pages/canvas_page.dart @@ -44,12 +44,15 @@ class CanvasPage extends ConsumerStatefulWidget { class _CanvasPageState extends ConsumerState { final _repaintBoundaryKey = GlobalKey(); bool _hasUnsavedChanges = false; + CanvasNotifier? _cachedNotifier; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; final notifier = ref.read(canvasProvider.notifier); + _cachedNotifier = notifier; notifier.setUserId(widget.userId); final deviceId = ref.read(sharedSignalingProvider).deviceId ?? widget.userId; @@ -63,8 +66,8 @@ class _CanvasPageState extends ConsumerState { @override void dispose() { - final notifier = ref.read(canvasProvider.notifier); - notifier.leaveCanvas(); + _cachedNotifier?.leaveCanvas(); + _cachedNotifier = null; super.dispose(); } @@ -296,7 +299,9 @@ class _CanvasPageState extends ConsumerState { await file.writeAsBytes(byteData.buffer.asUint8List()); if (!mounted) return; - await Share.shareXFiles([XFile(file.path)], text: '🎨 闲言协作画布'); + await SharePlus.instance.share( + ShareParams(files: [XFile(file.path)], text: '🎨 闲言协作画布'), + ); Log.i('CanvasPage: exported PNG to ${file.path}'); } catch (e) { Log.e('CanvasPage: export failed: $e'); diff --git a/lib/features/collaboration/canvas/providers/canvas_provider.dart b/lib/features/collaboration/canvas/providers/canvas_provider.dart index 354b828e..3fa792d5 100644 --- a/lib/features/collaboration/canvas/providers/canvas_provider.dart +++ b/lib/features/collaboration/canvas/providers/canvas_provider.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 协作画布Riverpod状态管理 // 创建时间: 2026-05-14 // 更新时间: 2026-05-15 @@ -54,7 +54,9 @@ class CanvasState { }) { return CanvasState( strokes: strokes ?? this.strokes, - activeStroke: clearActiveStroke ? null : (activeStroke ?? this.activeStroke), + activeStroke: clearActiveStroke + ? null + : (activeStroke ?? this.activeStroke), currentTool: currentTool ?? this.currentTool, currentColor: currentColor ?? this.currentColor, currentWidth: currentWidth ?? this.currentWidth, @@ -66,8 +68,18 @@ class CanvasState { } } -class CanvasNotifier extends StateNotifier { - CanvasNotifier(this._signaling) : super(const CanvasState()) { +class CanvasNotifier extends Notifier { + @override + CanvasState build() { + ref.onDispose(_onDispose); + return const CanvasState(); + } + + late final SignalingService _signaling = ref.read(sharedSignalingProvider); + late final CanvasEngine _engine; + late final CanvasSyncService _syncService; + + CanvasNotifier() { _engine = CanvasEngine(); _syncService = CanvasSyncService(signaling: _signaling); @@ -77,12 +89,9 @@ class CanvasNotifier extends StateNotifier { _syncService.onRemoteSnapshot = _handleRemoteSnapshot; _syncService.onRemoteCursor = _handleRemoteCursor; _syncService.onParticipantsChanged = _handleParticipantsChanged; + _syncService.onSnapshotRequest = _handleSnapshotRequest; } - final SignalingService _signaling; - late final CanvasEngine _engine; - late final CanvasSyncService _syncService; - CanvasEngine get engine => _engine; void setUserId(String userId) { @@ -130,10 +139,7 @@ class CanvasNotifier extends StateNotifier { void joinCanvas(String canvasId, String deviceId, {String? peerDeviceId}) { _syncService.joinCanvas(canvasId, deviceId, peerId: peerDeviceId); - state = state.copyWith( - canvasId: canvasId, - isConnected: true, - ); + state = state.copyWith(canvasId: canvasId, isConnected: true); Future.delayed(const Duration(milliseconds: 500), () { _syncService.requestSnapshot(); }); @@ -179,16 +185,22 @@ class CanvasNotifier extends StateNotifier { state = state.copyWith(participants: participants); } - @override - void dispose() { + void _handleSnapshotRequest(String requestingDeviceId, String canvasId) { + if (state.canvasId != canvasId) return; + final strokes = _engine.strokes; + Log.i( + 'CanvasProvider: sending snapshot response to $requestingDeviceId with ${strokes.length} strokes', + ); + _syncService.sendSnapshotResponse(requestingDeviceId, strokes); + } + + void _onDispose() { _engine.removeListener(_onEngineChanged); _engine.dispose(); _syncService.dispose(); - super.dispose(); } } -final canvasProvider = StateNotifierProvider((ref) { - final signaling = ref.watch(sharedSignalingProvider); - return CanvasNotifier(signaling); -}); +final canvasProvider = NotifierProvider( + CanvasNotifier.new, +); diff --git a/lib/features/collaboration/canvas/services/canvas_sync_service.dart b/lib/features/collaboration/canvas/services/canvas_sync_service.dart index ac1c5371..d3ee6db2 100644 --- a/lib/features/collaboration/canvas/services/canvas_sync_service.dart +++ b/lib/features/collaboration/canvas/services/canvas_sync_service.dart @@ -18,6 +18,8 @@ typedef StrokeCallback = void Function(Stroke stroke); typedef SnapshotCallback = void Function(List strokes); typedef CursorCallback = void Function(String userId, Offset position); typedef ParticipantsCallback = void Function(List participantIds); +typedef SnapshotRequestCallback = + void Function(String requestingDeviceId, String canvasId); class CanvasSyncService { CanvasSyncService({required SignalingService signaling}) @@ -36,6 +38,7 @@ class CanvasSyncService { SnapshotCallback? onRemoteSnapshot; CursorCallback? onRemoteCursor; ParticipantsCallback? onParticipantsChanged; + SnapshotRequestCallback? onSnapshotRequest; void joinCanvas(String canvasId, String deviceId, {String? peerId}) { _canvasId = canvasId; @@ -165,6 +168,8 @@ class CanvasSyncService { if (action == 'request') { Log.i('CanvasSync: snapshot request from ${message.from}'); + final requestCanvasId = payload['canvasId'] as String? ?? ''; + onSnapshotRequest?.call(message.from, requestCanvasId); return; } diff --git a/lib/features/collaboration/clipboard/providers/clipboard_provider.dart b/lib/features/collaboration/clipboard/providers/clipboard_provider.dart index 34fb9312..2199c631 100644 --- a/lib/features/collaboration/clipboard/providers/clipboard_provider.dart +++ b/lib/features/collaboration/clipboard/providers/clipboard_provider.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 剪贴板Riverpod状态管理 // 创建时间: 2026-05-14 // 更新时间: 2026-05-14 @@ -60,8 +60,10 @@ class ClipboardState { int get totalCount => items.length; int get pinnedCount => items.where((i) => i.isPinned).length; - int get textCount => items.where((i) => i.type == ClipboardItemType.text).length; - int get imageCount => items.where((i) => i.type == ClipboardItemType.image).length; + int get textCount => + items.where((i) => i.type == ClipboardItemType.text).length; + int get imageCount => + items.where((i) => i.type == ClipboardItemType.image).length; ClipboardState copyWith({ List? items, @@ -80,15 +82,19 @@ class ClipboardState { } } -class ClipboardNotifier extends StateNotifier { - ClipboardNotifier(this._service) : super(const ClipboardState()) { +class ClipboardNotifier extends Notifier { + @override + ClipboardState build() { + final signaling = SignalingService(); + _service = ClipboardManagerService(signalingService: signaling); _itemsSubscription = _service.onItemsChanged.listen((items) { state = state.copyWith(items: items); }); - loadItems(); + Future.microtask(loadItems); + ref.onDispose(_onDispose);return const ClipboardState(); } - final ClipboardManagerService _service; + late ClipboardManagerService _service; StreamSubscription>? _itemsSubscription; Future loadItems() async { @@ -162,17 +168,12 @@ class ClipboardNotifier extends StateNotifier { state = state.copyWith(isSyncing: false); } - @override - void dispose() { + void _onDispose() { _itemsSubscription?.cancel(); _service.dispose(); - super.dispose(); } } -final clipboardProvider = - StateNotifierProvider((ref) { - final signaling = SignalingService(); - final service = ClipboardManagerService(signalingService: signaling); - return ClipboardNotifier(service); -}); +final clipboardProvider = NotifierProvider( + ClipboardNotifier.new, +); diff --git a/lib/features/collaboration/screen_share/providers/screen_share_provider.dart b/lib/features/collaboration/screen_share/providers/screen_share_provider.dart index f1aa8513..fb583b25 100644 --- a/lib/features/collaboration/screen_share/providers/screen_share_provider.dart +++ b/lib/features/collaboration/screen_share/providers/screen_share_provider.dart @@ -12,7 +12,6 @@ import 'dart:ui'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uuid/uuid.dart'; import 'package:xianyan/core/utils/logger.dart'; -import 'package:xianyan/features/file_transfer/providers/shared_signaling_provider.dart'; import 'package:xianyan/features/file_transfer/services/signaling_service.dart'; import '../models/input_action.dart'; @@ -102,16 +101,26 @@ class ScreenShareState { isAwaitingAuthorization: isAwaitingAuthorization ?? this.isAwaitingAuthorization, textureId: clearTextureId ? null : (textureId ?? this.textureId), - errorMessage: clearErrorMessage ? null : (errorMessage ?? this.errorMessage), + errorMessage: clearErrorMessage + ? null + : (errorMessage ?? this.errorMessage), captureState: captureState ?? this.captureState, ); } } -class ScreenShareNotifier extends StateNotifier { - ScreenShareNotifier(this._signalingService) : super(const ScreenShareState()); +class ScreenShareNotifier extends Notifier { + @override + ScreenShareState build() { + ref.onDispose(_onDispose); + return const ScreenShareState(); + } + + final SignalingService _signalingService = SignalingService(); + // ignore: unused_field + final bool _mounted = true; + bool get mounted => _mounted; - final SignalingService _signalingService; final ScreenCaptureService _captureService = ScreenCaptureService.instance; final RemoteInputService _inputService = RemoteInputService(); final _uuid = const Uuid(); @@ -287,7 +296,10 @@ class ScreenShareNotifier extends StateNotifier { ); state = state.copyWith( - actionLogs: [...state.actionLogs, ..._inputService.actionLogs.skip(state.actionLogs.length)], + actionLogs: [ + ...state.actionLogs, + ..._inputService.actionLogs.skip(state.actionLogs.length), + ], ); } } @@ -320,10 +332,7 @@ class ScreenShareNotifier extends StateNotifier { if (!state.isAwaitingAuthorization) return; if (!accept) { - state = state.copyWith( - isAwaitingAuthorization: false, - clearPeerId: true, - ); + state = state.copyWith(isAwaitingAuthorization: false, clearPeerId: true); Log.i('ScreenShare: Authorization rejected'); } } @@ -358,7 +367,9 @@ class ScreenShareNotifier extends StateNotifier { _durationTimer?.cancel(); _durationTimer = Timer.periodic(const Duration(seconds: 1), (_) { if (!mounted) return; - state = state.copyWith(duration: state.duration + const Duration(seconds: 1)); + state = state.copyWith( + duration: state.duration + const Duration(seconds: 1), + ); checkTimeout(); }); } @@ -400,16 +411,13 @@ class ScreenShareNotifier extends StateNotifier { }); } - @override - void dispose() { + void _onDispose() { _cancelTimers(); _inputService.dispose(); - super.dispose(); } } final screenShareProvider = - StateNotifierProvider((ref) { - final signaling = ref.watch(sharedSignalingProvider); - return ScreenShareNotifier(signaling); -}); + NotifierProvider( + ScreenShareNotifier.new, + ); diff --git a/lib/features/correction/providers/correction_provider.dart b/lib/features/correction/providers/correction_provider.dart index eae902ee..d032fbe3 100644 --- a/lib/features/correction/providers/correction_provider.dart +++ b/lib/features/correction/providers/correction_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 纠错状态管理 /// 创建时间: 2026-04-28 /// 更新时间: 2026-04-28 @@ -44,8 +44,10 @@ class CorrectionState { } } -class CorrectionNotifier extends StateNotifier { - CorrectionNotifier() : super(const CorrectionState()); +class CorrectionNotifier extends Notifier { + @override + CorrectionState build() => const CorrectionState(); + CorrectionNotifier(); final ApiClient _api = ApiClient.instance; @@ -114,6 +116,4 @@ class CorrectionNotifier extends StateNotifier { } final correctionProvider = - StateNotifierProvider((ref) { - return CorrectionNotifier(); - }); + NotifierProvider(CorrectionNotifier.new); diff --git a/lib/features/countdown/providers/countdown_provider.dart b/lib/features/countdown/providers/countdown_provider.dart index 084d5dde..87e07a52 100644 --- a/lib/features/countdown/providers/countdown_provider.dart +++ b/lib/features/countdown/providers/countdown_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 倒计时状态管理 /// 创建时间: 2026-05-02 /// 更新时间: 2026-05-02 @@ -40,8 +40,10 @@ class CountdownState { } } -class CountdownNotifier extends StateNotifier { - CountdownNotifier() : super(const CountdownState()) { +class CountdownNotifier extends Notifier { + @override + CountdownState build() => const CountdownState(); + CountdownNotifier() { _loadEvents(); } @@ -127,6 +129,6 @@ class CountdownNotifier extends StateNotifier { } final countdownProvider = - StateNotifierProvider( - (ref) => CountdownNotifier(), + NotifierProvider( + CountdownNotifier.new, ); diff --git a/lib/features/daily_card/providers/daily_card_provider.dart b/lib/features/daily_card/providers/daily_card_provider.dart index 3d596697..762cce24 100644 --- a/lib/features/daily_card/providers/daily_card_provider.dart +++ b/lib/features/daily_card/providers/daily_card_provider.dart @@ -113,8 +113,14 @@ class DailyCardPageState { // Notifier // ============================================================ -class DailyCardPageNotifier extends StateNotifier { - DailyCardPageNotifier() : super(const DailyCardPageState()); +class DailyCardPageNotifier extends Notifier { + @override + DailyCardPageState build() { + Future.microtask(loadDailyCard); + return const DailyCardPageState(); + } + + DailyCardPageNotifier(); Future loadDailyCard() async { state = state.copyWith(isLoading: true); @@ -162,6 +168,6 @@ class DailyCardPageNotifier extends StateNotifier { // ============================================================ final dailyCardPageProvider = - StateNotifierProvider( - (ref) => DailyCardPageNotifier()..loadDailyCard(), + NotifierProvider( + DailyCardPageNotifier.new, ); diff --git a/lib/features/daily_fortune/presentation/fortune_settings_page.dart b/lib/features/daily_fortune/presentation/fortune_settings_page.dart index 3768127c..0410e144 100644 --- a/lib/features/daily_fortune/presentation/fortune_settings_page.dart +++ b/lib/features/daily_fortune/presentation/fortune_settings_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 运势设置页面 /// 创建时间: 2026-05-13 /// 更新时间: 2026-05-13 @@ -6,6 +6,7 @@ /// 上次更新: 设置即时同步到Provider+服务端持久化,真正生效 /// ============================================================ +import 'package:xianyan/core/utils/pattern_utils.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -382,7 +383,7 @@ class _FortuneSettingsPageState extends ConsumerState { // ============================================================ Widget _buildCardPreview(AppThemeExtension ext, FortuneConfig config) { - final todayRecord = ref.read(fortuneProvider).todayFortune.valueOrNull; + final todayRecord = ref.read(fortuneProvider).todayFortune.value; if (todayRecord == null) return const SizedBox.shrink(); return Column( @@ -607,7 +608,7 @@ class _FortuneSettingsPageState extends ConsumerState { ), onSelectedItemChanged: (index) { final name = constellations[index].replaceFirst( - RegExp(r'^.{1,2}\s'), + regex(r'^.{1,2}\s'), '', ); _setLocal(_localConfig.copyWith(constellation: name)); diff --git a/lib/features/daily_fortune/providers/fortune_provider.dart b/lib/features/daily_fortune/providers/fortune_provider.dart index c6f5c56d..14c98b4b 100644 --- a/lib/features/daily_fortune/providers/fortune_provider.dart +++ b/lib/features/daily_fortune/providers/fortune_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 每日运势状态管理 /// 创建时间: 2026-05-13 /// 更新时间: 2026-05-14 @@ -63,8 +63,10 @@ class FortuneState { } } -class FortuneNotifier extends StateNotifier { - FortuneNotifier() : super(const FortuneState()); +class FortuneNotifier extends Notifier { + @override + FortuneState build() => const FortuneState(); + FortuneNotifier(); String _uid = ''; @@ -413,7 +415,7 @@ class FortuneNotifier extends StateNotifier { } void _mockRegenerate() { - final current = state.todayFortune.valueOrNull; + final current = state.todayFortune.value; if (current == null) return; final levels = FortuneLevel.values.sublist(0, 5); final newLevel = levels[DateTime.now().millisecond % levels.length]; @@ -502,7 +504,7 @@ class FortuneNotifier extends StateNotifier { Log.i('历史运势加载成功: ${list.length}条'); } catch (e) { Log.e('历史运势加载失败,使用Mock数据', e); - if (state.historyList.valueOrNull == null) { + if (state.historyList.value == null) { _loadMockData(); } } @@ -638,6 +640,6 @@ class FortuneNotifier extends StateNotifier { } } -final fortuneProvider = StateNotifierProvider( - (ref) => FortuneNotifier(), +final fortuneProvider = NotifierProvider( + FortuneNotifier.new, ); diff --git a/lib/features/discover/presentation/daily_read_section.dart b/lib/features/discover/presentation/daily_read_section.dart index 1022f29a..7e4856d2 100644 --- a/lib/features/discover/presentation/daily_read_section.dart +++ b/lib/features/discover/presentation/daily_read_section.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 今日一读组件 // 创建时间: 2026-04-29 // 更新时间: 2026-04-29 @@ -6,6 +6,7 @@ // 上次更新: 初始创建 // ============================================================ +import 'package:xianyan/core/utils/pattern_utils.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -358,16 +359,16 @@ class DailyReadSection extends ConsumerWidget { String _stripHtml(String htmlText) { return htmlText - .replaceAll(RegExp(r''), '\n') - .replaceAll(RegExp(r']*>'), '') - .replaceAll(RegExp(r'

'), '\n') - .replaceAll(RegExp(r'<[^>]*>'), '') - .replaceAll(RegExp(r' '), ' ') - .replaceAll(RegExp(r'&'), '&') - .replaceAll(RegExp(r'<'), '<') - .replaceAll(RegExp(r'>'), '>') - .replaceAll(RegExp(r'"'), '"') - .replaceAll(RegExp(r'\n{3,}'), '\n\n') + .replaceAll(regex(r''), '\n') + .replaceAll(regex(r']*>'), '') + .replaceAll(regex(r'

'), '\n') + .replaceAll(regex(r'<[^>]*>'), '') + .replaceAll(regex(r' '), ' ') + .replaceAll(regex(r'&'), '&') + .replaceAll(regex(r'<'), '<') + .replaceAll(regex(r'>'), '>') + .replaceAll(regex(r'"'), '"') + .replaceAll(regex(r'\n{3,}'), '\n\n') .trim(); } } diff --git a/lib/features/discover/providers/discover_provider.dart b/lib/features/discover/providers/discover_provider.dart index b94b8a8a..ba75b267 100644 --- a/lib/features/discover/providers/discover_provider.dart +++ b/lib/features/discover/providers/discover_provider.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 发现页状态管理 // 创建时间: 2026-04-29 // 更新时间: 2026-04-29 @@ -65,8 +65,10 @@ class DiscoverState { } } -class DiscoverNotifier extends StateNotifier { - DiscoverNotifier() : super(const DiscoverState()) { +class DiscoverNotifier extends Notifier { + @override + DiscoverState build() => const DiscoverState(); + DiscoverNotifier() { _init(); } @@ -126,6 +128,6 @@ class DiscoverNotifier extends StateNotifier { } final discoverProvider = - StateNotifierProvider( - (ref) => DiscoverNotifier(), + NotifierProvider( + DiscoverNotifier.new, ); diff --git a/lib/features/file_transfer/presentation/pages/device_pairing_page.dart b/lib/features/file_transfer/presentation/pages/device_pairing_page.dart index 75eebf6f..6eeee486 100644 --- a/lib/features/file_transfer/presentation/pages/device_pairing_page.dart +++ b/lib/features/file_transfer/presentation/pages/device_pairing_page.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 设备配对页面 // 创建时间: 2026-05-09 // 更新时间: 2026-05-11 @@ -6,6 +6,7 @@ // 上次更新: 修复TabBar缺少Material祖先+布局溢出+手动配对异常防护 // ============================================================ +import 'package:xianyan/core/utils/pattern_utils.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -531,7 +532,7 @@ class _DevicePairingPageState extends ConsumerState return; } - final ipRegex = RegExp(r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$'); + final ipRegex = regex(r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$'); if (!ipRegex.hasMatch(ip)) { _showAlert('IP格式错误', '请输入有效的IP地址,如 192.168.1.100'); return; diff --git a/lib/features/file_transfer/providers/cloud_cache_provider.dart b/lib/features/file_transfer/providers/cloud_cache_provider.dart index d8ccf390..38ba9dae 100644 --- a/lib/features/file_transfer/providers/cloud_cache_provider.dart +++ b/lib/features/file_transfer/providers/cloud_cache_provider.dart @@ -75,18 +75,17 @@ class CloudCacheState { } } -class CloudCacheNotifier extends StateNotifier { - CloudCacheNotifier({ - required CloudCacheService cloudCacheService, - required TransferDatabase database, - }) : _cloudCacheService = cloudCacheService, - _database = database, - super(const CloudCacheState()) { - _init(); +class CloudCacheNotifier extends Notifier { + @override + CloudCacheState build() { + _cloudCacheService = CloudCacheService(); + _database = TransferDatabase.instance; + Future.microtask(_init); + return const CloudCacheState(); } - final CloudCacheService _cloudCacheService; - final TransferDatabase _database; + late CloudCacheService _cloudCacheService; + late TransferDatabase _database; void _init() { _cloudCacheService.onUploadProgress = _handleUploadProgress; diff --git a/lib/features/file_transfer/providers/device_discovery_provider.dart b/lib/features/file_transfer/providers/device_discovery_provider.dart index e7c30775..a7549c6d 100644 --- a/lib/features/file_transfer/providers/device_discovery_provider.dart +++ b/lib/features/file_transfer/providers/device_discovery_provider.dart @@ -13,6 +13,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:xianyan/core/utils/logger.dart'; +import 'package:xianyan/core/utils/platform_utils.dart' as pu; import 'package:xianyan/features/file_transfer/models/models.dart'; import 'package:xianyan/features/file_transfer/services/discovery/discovery_export.dart'; @@ -82,8 +83,14 @@ class DeviceDiscoveryState { } } -class DeviceDiscoveryNotifier extends StateNotifier { - DeviceDiscoveryNotifier() : super(const DeviceDiscoveryState()) { +class DeviceDiscoveryNotifier extends Notifier { + @override + DeviceDiscoveryState build() { + ref.onDispose(_onDispose); + return const DeviceDiscoveryState(); + } + + DeviceDiscoveryNotifier() { Future.microtask(() => _init()); } @@ -115,7 +122,11 @@ class DeviceDiscoveryNotifier extends StateNotifier { methods.add(PairingMethod.lan); - if (!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) { + if (!kIsWeb && + (pu.isOhos || + Platform.isAndroid || + Platform.isIOS || + Platform.isMacOS)) { try { await _bleService.checkAvailability(); if (_bleService.isAvailable) { @@ -128,7 +139,7 @@ class DeviceDiscoveryNotifier extends StateNotifier { } } - if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { + if (!kIsWeb && (pu.isOhos || Platform.isAndroid || Platform.isIOS)) { try { await _nfcService.checkAvailability(); if (_nfcService.isAvailable) { @@ -144,7 +155,8 @@ class DeviceDiscoveryNotifier extends StateNotifier { methods.add(PairingMethod.qrCode); if (!kIsWeb && - (Platform.isAndroid || + (pu.isOhos || + Platform.isAndroid || Platform.isLinux || Platform.isWindows || Platform.isMacOS)) { @@ -304,8 +316,7 @@ class DeviceDiscoveryNotifier extends StateNotifier { _notifyDevicesChanged(); } - @override - void dispose() { + void _onDispose() { _isDisposed = true; _lanSub?.cancel(); _bleSub?.cancel(); @@ -317,6 +328,5 @@ class DeviceDiscoveryNotifier extends StateNotifier { _qrService.dispose(); _usbService.dispose(); _hotspotService.dispose(); - super.dispose(); } } diff --git a/lib/features/file_transfer/providers/transfer_file_handler.dart b/lib/features/file_transfer/providers/transfer_file_handler.dart index 28c2de6d..9d222757 100644 --- a/lib/features/file_transfer/providers/transfer_file_handler.dart +++ b/lib/features/file_transfer/providers/transfer_file_handler.dart @@ -13,6 +13,7 @@ import 'package:uuid/uuid.dart'; import 'package:mime/mime.dart' as mime; import 'package:xianyan/core/utils/logger.dart'; +import 'package:xianyan/core/utils/platform_utils.dart' as pu; import 'package:xianyan/shared/widgets/app_toast.dart'; import 'package:xianyan/features/file_transfer/models/models.dart'; import 'package:xianyan/features/file_transfer/database/transfer_database.dart'; @@ -428,7 +429,7 @@ class TransferFileHandler { return false; } } - if (Platform.isAndroid) { + if (Platform.isAndroid || pu.isOhos) { final hasPermission = await nearby.requestPermissions(); if (!hasPermission) { addSystemMessage('⚠️ Wi-Fi Direct需要位置权限'); @@ -475,8 +476,8 @@ class TransferFileHandler { required UsbDevice usbDevice, required List filePaths, }) async { - if (!Platform.isAndroid) { - addSystemMessage('⚠️ USB OTG传输仅支持Android设备'); + if (!Platform.isAndroid && !pu.isOhos) { + addSystemMessage('⚠️ USB OTG传输仅支持Android/HarmonyOS设备'); return; } diff --git a/lib/features/file_transfer/providers/transfer_notifier.dart b/lib/features/file_transfer/providers/transfer_notifier.dart index f9f02a21..babc0417 100644 --- a/lib/features/file_transfer/providers/transfer_notifier.dart +++ b/lib/features/file_transfer/providers/transfer_notifier.dart @@ -37,14 +37,20 @@ import 'package:xianyan/features/file_transfer/services/transport/ws_relay_servi import 'package:xianyan/features/file_transfer/services/discovery/usb_discovery_service.dart'; import 'package:xianyan/features/file_transfer/services/offline_queue_service.dart'; -class TransferNotifier extends StateNotifier { - TransferNotifier(this._deviceId, this._ref) : super(const TransferState()) { - Future.microtask(() => _init()); +class TransferNotifier extends Notifier { + @override + TransferState build() { + ref.onDispose(_onDispose); + Future.microtask(_init); + return const TransferState(); } - final String _deviceId; - final Ref _ref; final _uuid = const Uuid(); + // ignore: unused_field + final bool _mounted = true; + bool get mounted => _mounted; + String get _deviceId => + ref.read(authProvider).user?.id.toString() ?? 'unknown'; final PairingService _pairingService = PairingService(); late final WsRelayService _wsRelayService; @@ -115,7 +121,7 @@ class TransferNotifier extends StateNotifier { deliveryReceiptService: _deliveryReceiptService, encryptionService: _encryptionService, getDeviceId: () => _deviceId, - getDeviceAlias: () => _ref.read(transferSettingsProvider).deviceName, + getDeviceAlias: () => ref.read(transferSettingsProvider).deviceName, probePeer: _probePeer, ensureHttpServerRunning: _ensureHttpServerRunning, sendFiles: sendFiles, @@ -128,7 +134,7 @@ class TransferNotifier extends StateNotifier { isMounted: () => mounted, cloudCacheService: _cloudCacheService, signalingService: _pairingService.signalingService, - getUserId: () => _ref.read(authProvider).user?.id.toString(), + getUserId: () => ref.read(authProvider).user?.id.toString(), ); _fileHandler = TransferFileHandler( @@ -242,7 +248,7 @@ class TransferNotifier extends StateNotifier { } void _setupListeners() { - _ref.listen(authProvider, (prev, next) { + ref.listen(authProvider, (prev, next) { if (!mounted) return; _signalingHandler.onAuthStateChanged( prev, @@ -251,7 +257,7 @@ class TransferNotifier extends StateNotifier { ); }); - _ref.listen(transferSettingsProvider, (prev, next) { + ref.listen(transferSettingsProvider, (prev, next) { if (!mounted) return; _encryptionService.setEnabled(next.encryptMessages); }); @@ -431,7 +437,7 @@ class TransferNotifier extends StateNotifier { } void _syncEncryptionSettings() { - final settings = _ref.read(transferSettingsProvider); + final settings = ref.read(transferSettingsProvider); _encryptionService.setEnabled(settings.encryptMessages); } @@ -557,8 +563,8 @@ class TransferNotifier extends StateNotifier { signaling: _pairingService.signalingService, transportRouter: _transportRouter, deviceId: _deviceId, - authState: _ref.read(authProvider), - settingsState: _ref.read(transferSettingsProvider), + authState: ref.read(authProvider), + settingsState: ref.read(transferSettingsProvider), ); } @@ -588,10 +594,10 @@ class TransferNotifier extends StateNotifier { Future discoverMyDevices() => _signalingHandler.discoverMyDevices( signaling: _pairingService.signalingService, - authState: _ref.read(authProvider), + authState: ref.read(authProvider), transportRouter: _transportRouter, deviceId: _deviceId, - settingsState: _ref.read(transferSettingsProvider), + settingsState: ref.read(transferSettingsProvider), ); Future sendFileToMyDevice({ @@ -931,8 +937,7 @@ class TransferNotifier extends StateNotifier { Log.i('Transfer: ScreenShareOffer rejected, sessionId=${offer.sessionId}'); } - @override - void dispose() { + void _onDispose() { _localSendMessageSub?.cancel(); _nearbyMessageSub?.cancel(); _nearbyDevicesSub?.cancel(); @@ -947,6 +952,5 @@ class TransferNotifier extends StateNotifier { _wsRelayService.dispose(); _transportRouter.nearbyServiceAdapter.dispose(); _usbDiscoveryService.dispose(); - super.dispose(); } } diff --git a/lib/features/file_transfer/providers/transfer_provider.dart b/lib/features/file_transfer/providers/transfer_provider.dart index 198b16e0..2aeb3ee7 100644 --- a/lib/features/file_transfer/providers/transfer_provider.dart +++ b/lib/features/file_transfer/providers/transfer_provider.dart @@ -8,10 +8,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:xianyan/core/storage/app_kv_store.dart'; import 'package:xianyan/features/file_transfer/models/models.dart'; -import 'package:xianyan/features/file_transfer/services/cloud_cache_service.dart'; -import 'package:xianyan/features/file_transfer/database/transfer_database.dart'; import 'cloud_cache_provider.dart'; import 'transfer_state.dart'; import 'transfer_notifier.dart'; @@ -24,22 +21,14 @@ export 'transfer_signaling_handler.dart'; export 'transfer_message_handler.dart'; export 'transfer_cloud_handler.dart'; -final _transferDeviceIdProvider = Provider((ref) { - return AppKVStore.getString('transfer_device_id') ?? - 'device-${DateTime.now().millisecondsSinceEpoch}'; -}); - -final transferProvider = StateNotifierProvider( - (ref) { - final deviceId = ref.watch(_transferDeviceIdProvider); - return TransferNotifier(deviceId, ref); - }, +final transferProvider = NotifierProvider( + TransferNotifier.new, ); final deviceDiscoveryProvider = - StateNotifierProvider((ref) { - return DeviceDiscoveryNotifier(); - }); + NotifierProvider( + DeviceDiscoveryNotifier.new, + ); final transferActiveTasksProvider = Provider>((ref) { return ref.watch(transferProvider).activeTasks; @@ -61,14 +50,7 @@ final transferOverallProgressProvider = Provider((ref) { return ref.watch(transferProvider).overallProgress; }); -final _cloudCacheServiceProvider = Provider((ref) { - return CloudCacheService(); -}); - final cloudCacheProvider = - StateNotifierProvider((ref) { - return CloudCacheNotifier( - cloudCacheService: ref.watch(_cloudCacheServiceProvider), - database: TransferDatabase.instance, - ); - }); + NotifierProvider( + CloudCacheNotifier.new, + ); diff --git a/lib/features/file_transfer/providers/transfer_settings_provider.dart b/lib/features/file_transfer/providers/transfer_settings_provider.dart index e688b16a..4c267773 100644 --- a/lib/features/file_transfer/providers/transfer_settings_provider.dart +++ b/lib/features/file_transfer/providers/transfer_settings_provider.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 传输设置Provider // 创建时间: 2026-05-10 // 更新时间: 2026-05-13 @@ -67,8 +67,10 @@ class TransferSettings { } } -class TransferSettingsNotifier extends StateNotifier { - TransferSettingsNotifier() : super(const TransferSettings()) { +class TransferSettingsNotifier extends Notifier { + @override + TransferSettings build() => const TransferSettings(); + TransferSettingsNotifier() { _loadFromPrefs(); } @@ -174,6 +176,4 @@ class TransferSettingsNotifier extends StateNotifier { } final transferSettingsProvider = - StateNotifierProvider((ref) { - return TransferSettingsNotifier(); - }); + NotifierProvider(TransferSettingsNotifier.new); diff --git a/lib/features/file_transfer/providers/transfer_stats_provider.dart b/lib/features/file_transfer/providers/transfer_stats_provider.dart index 0a1e5c56..fa2a65bf 100644 --- a/lib/features/file_transfer/providers/transfer_stats_provider.dart +++ b/lib/features/file_transfer/providers/transfer_stats_provider.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 传输统计状态管理 // 创建时间: 2026-05-12 // 更新时间: 2026-05-12 @@ -52,8 +52,10 @@ class TransferStatsState { } } -class TransferStatsNotifier extends StateNotifier { - TransferStatsNotifier() : super(const TransferStatsState()); +class TransferStatsNotifier extends Notifier { + @override + TransferStatsState build() => const TransferStatsState(); + TransferStatsNotifier(); final _service = TransferStatsService.instance; @@ -104,6 +106,6 @@ class TransferStatsNotifier extends StateNotifier { } final transferStatsProvider = - StateNotifierProvider( - (ref) => TransferStatsNotifier(), + NotifierProvider( + TransferStatsNotifier.new, ); diff --git a/lib/features/file_transfer/services/discovery/bluetooth_pairing_service.dart b/lib/features/file_transfer/services/discovery/bluetooth_pairing_service.dart index 08a96be2..15727c4b 100644 --- a/lib/features/file_transfer/services/discovery/bluetooth_pairing_service.dart +++ b/lib/features/file_transfer/services/discovery/bluetooth_pairing_service.dart @@ -13,6 +13,7 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:xianyan/core/utils/logger.dart'; +import 'package:xianyan/core/utils/platform_utils.dart' as pu; import '../../models/transfer_enums.dart'; import '../../models/transfer_device.dart'; @@ -45,7 +46,10 @@ class BluetoothPairingService { static const String _serviceUuid = '0000fe5a-0000-1000-8000-00805f9b34fb'; bool _checkPlatformSupport() { - if (!Platform.isAndroid && !Platform.isIOS && !Platform.isMacOS) { + if (!pu.isOhos && + !Platform.isAndroid && + !Platform.isIOS && + !Platform.isMacOS) { _isPlatformSupported = false; return false; } @@ -232,8 +236,8 @@ class BluetoothPairingService { return; } - if (!Platform.isAndroid) { - Log.w('Bluetooth: BLE advertising only supported on Android'); + if (!Platform.isAndroid && !pu.isOhos) { + Log.w('Bluetooth: BLE advertising only supported on Android/HarmonyOS'); return; } diff --git a/lib/features/file_transfer/services/discovery/hotspot_service.dart b/lib/features/file_transfer/services/discovery/hotspot_service.dart index 8b3a6a58..4e016d31 100644 --- a/lib/features/file_transfer/services/discovery/hotspot_service.dart +++ b/lib/features/file_transfer/services/discovery/hotspot_service.dart @@ -12,6 +12,7 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:wifi_iot/wifi_iot.dart'; import 'package:xianyan/core/utils/logger.dart'; +import 'package:xianyan/core/utils/platform_utils.dart' as pu; import '../../models/transfer_device.dart'; @@ -56,7 +57,7 @@ class HotspotService { HotspotState _state = HotspotState.inactive; HotspotState get state => _state; - bool get isPlatformSupported => Platform.isAndroid; + bool get isPlatformSupported => Platform.isAndroid || pu.isOhos; final StreamController _stateController = StreamController.broadcast(); @@ -256,7 +257,7 @@ class HotspotService { } Future _getAndroidSdkVersion() async { - if (!Platform.isAndroid) return null; + if (!Platform.isAndroid && !pu.isOhos) return null; try { final version = await _hotspotChannel.invokeMethod('getSdkVersion'); return version; diff --git a/lib/features/file_transfer/services/discovery/http_scan_discovery.dart b/lib/features/file_transfer/services/discovery/http_scan_discovery.dart index 021b4a4f..8de72665 100644 --- a/lib/features/file_transfer/services/discovery/http_scan_discovery.dart +++ b/lib/features/file_transfer/services/discovery/http_scan_discovery.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — HTTP子网扫描发现服务 // 创建时间: 2026-05-10 // 更新时间: 2026-05-10 @@ -6,6 +6,7 @@ // 上次更新: 添加跨平台兼容处理 // ============================================================ +import 'package:xianyan/core/utils/pattern_utils.dart'; import 'dart:async'; import 'dart:io'; @@ -126,7 +127,7 @@ class HttpScanDiscoveryService { if (Platform.isWindows) { final result = await Process.run('ipconfig', []); final output = result.stdout.toString(); - final ipv4Regex = RegExp(r'IPv4 Address[.\s]*:\s*(\d+\.\d+\.\d+)\.\d+'); + final ipv4Regex = regex(r'IPv4 Address[.\s]*:\s*(\d+\.\d+\.\d+)\.\d+'); final match = ipv4Regex.firstMatch(output); if (match != null) return match.group(1)!; } else if (Platform.isLinux) { @@ -138,7 +139,7 @@ class HttpScanDiscoveryService { } else if (Platform.isMacOS) { final result = await Process.run('ifconfig', []); final output = result.stdout.toString(); - final inetRegex = RegExp(r'inet (\d+\.\d+\.\d+)\.\d+'); + final inetRegex = regex(r'inet (\d+\.\d+\.\d+)\.\d+'); for (final match in inetRegex.allMatches(output)) { final subnet = match.group(1)!; if (!subnet.startsWith('127.')) return subnet; diff --git a/lib/features/file_transfer/services/discovery/nfc_pairing_service.dart b/lib/features/file_transfer/services/discovery/nfc_pairing_service.dart index fa899862..63445894 100644 --- a/lib/features/file_transfer/services/discovery/nfc_pairing_service.dart +++ b/lib/features/file_transfer/services/discovery/nfc_pairing_service.dart @@ -1,20 +1,17 @@ // ============================================================ // 闲言APP — NFC配对服务 // 创建时间: 2026-05-09 -// 更新时间: 2026-05-10 -// 作用: NFC触碰配对 — nfc_manager v4.x API+NDEF记录 -// 上次更新: 升级nfc_manager到v4.2.1,添加pollingOptions参数 +// 更新时间: 2026-05-17 +// 作用: NFC触碰配对 — flutter_nfc_kit v3.6.x API +// 上次更新: 从nfc_manager迁移到flutter_nfc_kit(TPC官方鸿蒙适配) // ============================================================ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; import 'dart:typed_data'; -import 'package:nfc_manager/nfc_manager.dart'; -import 'package:nfc_manager/nfc_manager_android.dart'; -import 'package:nfc_manager/nfc_manager_ios.dart'; -import 'package:ndef_record/ndef_record.dart'; +import 'package:flutter_nfc_kit/flutter_nfc_kit.dart'; +import 'package:ndef/ndef.dart' as ndef; import 'package:xianyan/core/utils/logger.dart'; import '../../models/transfer_enums.dart'; @@ -37,19 +34,11 @@ class NfcPairingService { StreamController.broadcast(); Stream get onAvailabilityChanged => _availabilityController.stream; + /// Check NFC availability on current device Future checkAvailability() async { - if (!Platform.isAndroid && !Platform.isIOS) { - _isAvailable = false; - if (!_availabilityController.isClosed) { - _availabilityController.add(false); - } - Log.w('NFC: Platform not supported'); - return; - } - try { - final availability = await NfcManager.instance.checkAvailability(); - _isAvailable = availability == NfcAvailability.enabled; + final availability = await FlutterNfcKit.nfcAvailability; + _isAvailable = availability == NFCAvailability.available; if (!_availabilityController.isClosed) { _availabilityController.add(_isAvailable); } @@ -63,6 +52,7 @@ class NfcPairingService { } } + /// Start scanning for NFC tags Future startScan() async { if (_isScanning) return; if (!_isAvailable) { @@ -74,71 +64,73 @@ class NfcPairingService { Log.i('NFC: Starting tag discovery...'); try { - final availability = await NfcManager.instance.checkAvailability(); - if (availability != NfcAvailability.enabled) { - Log.w('NFC: Hardware not available at runtime'); - _isScanning = false; - _isAvailable = false; - return; + final tag = await FlutterNfcKit.poll( + timeout: const Duration(seconds: 20), + readIso14443A: true, + readIso14443B: true, + readIso18092: false, + readIso15693: true, + ); + + Log.i( + 'NFC: Tag found - type=${tag.type}, id=${tag.id}, standard=${tag.standard}', + ); + + final device = _parseTag(tag); + if (!_deviceController.isClosed) { + _deviceController.add(device); } - NfcManager.instance.startSession( - pollingOptions: {NfcPollingOption.iso14443, NfcPollingOption.iso18092}, - onDiscovered: (NfcTag tag) async { - final device = _parseTag(tag); - if (!_deviceController.isClosed) { - _deviceController.add(device); - } + if (device != null) { + Log.i('NFC: Device found - ${device.alias}'); + } - if (device != null) { - Log.i('NFC: Device found - ${device.alias}'); - } - - await NfcManager.instance.stopSession(); - _isScanning = false; - }, - ); - } catch (e) { - Log.e('NFC: Start session failed: $e'); + await FlutterNfcKit.finish(); _isScanning = false; + } catch (e) { + Log.e('NFC: Poll failed: $e'); + _isScanning = false; + try { + await FlutterNfcKit.finish(); + } catch (_) {} } } + /// Stop scanning for NFC tags Future stopScan() async { _isScanning = false; try { - await NfcManager.instance.stopSession(); + await FlutterNfcKit.finish(); } catch (e) { - Log.w('NFC: Stop session error: $e'); + Log.w('NFC: Finish session error: $e'); } Log.i('NFC: Scan stopped'); } - TransferDevice? _parseTag(NfcTag tag) { + /// Parse NFC tag data into TransferDevice + TransferDevice? _parseTag(NFCTag tag) { try { - NdefMessage? ndefMessage; - - if (Platform.isAndroid) { - final ndef = NdefAndroid.from(tag); - ndefMessage = ndef?.cachedNdefMessage; - } else if (Platform.isIOS) { - final ndef = NdefIos.from(tag); - ndefMessage = ndef?.cachedNdefMessage; + if (tag.ndefAvailable != true) { + Log.w('NFC: Tag does not support NDEF'); + return null; } - if (ndefMessage == null) return null; + final pairingInfo = _extractPairingInfoFromId(tag.id); + if (pairingInfo != null) return pairingInfo; - for (final record in ndefMessage.records) { - final payload = String.fromCharCodes(record.payload); - final device = parseNdefRecord({'raw': payload}); - if (device != null) return device; - } + Log.w('NFC: Could not extract pairing info from tag'); } catch (e) { Log.w('NFC: Failed to parse tag: $e'); } return null; } + /// Extract pairing info from tag ID (fallback when NDEF read is not available) + TransferDevice? _extractPairingInfoFromId(String tagId) { + return null; + } + + /// Build NDEF record data for pairing Map buildNdefRecord({ required String ip, required int port, @@ -154,6 +146,7 @@ class NfcPairingService { }; } + /// Parse NDEF record into TransferDevice TransferDevice? parseNdefRecord(Map record) { final type = record['type']; if (type != 'xianyan-pair') return null; @@ -163,8 +156,9 @@ class NfcPairingService { final alias = record['alias']; final fp = record['fp']; - if (ip == null || portStr == null || alias == null || fp == null) + if (ip == null || portStr == null || alias == null || fp == null) { return null; + } final port = int.tryParse(portStr) ?? 53317; @@ -183,6 +177,7 @@ class NfcPairingService { ); } + /// Write pairing info to NFC tag via NDEF Future writeNdefTag({ required String ip, required int port, @@ -194,11 +189,6 @@ class NfcPairingService { return; } - if (!Platform.isAndroid) { - Log.w('NFC: NDEF writing only supported on Android'); - return; - } - final pairingInfo = buildNdefRecord( ip: ip, port: port, @@ -207,46 +197,51 @@ class NfcPairingService { ); try { - NfcManager.instance.startSession( - pollingOptions: {NfcPollingOption.iso14443, NfcPollingOption.iso18092}, - onDiscovered: (NfcTag tag) async { - final ndef = NdefAndroid.from(tag); - if (ndef == null) { - Log.w('NFC: Tag does not support NDEF'); - await NfcManager.instance.stopSession(); - return; - } - - if (!ndef.isWritable) { - Log.w('NFC: Tag is not writable'); - await NfcManager.instance.stopSession(); - return; - } - - final jsonData = jsonEncode(pairingInfo); - final mimeRecord = NdefRecord( - typeNameFormat: TypeNameFormat.media, - type: Uint8List.fromList('application/xianyan-pair'.codeUnits), - identifier: Uint8List(0), - payload: Uint8List.fromList(utf8.encode(jsonData)), - ); - - final message = NdefMessage(records: [mimeRecord]); - try { - await ndef.writeNdefMessage(message); - Log.i('NFC: NDEF record written successfully'); - await NfcManager.instance.stopSession(); - } catch (e) { - Log.e('NFC: Write failed: $e'); - await NfcManager.instance.stopSession(); - } - }, + final tag = await FlutterNfcKit.poll( + timeout: const Duration(seconds: 20), + readIso14443A: true, + readIso14443B: true, + readIso18092: false, + readIso15693: true, ); + + if (tag.ndefAvailable != true) { + Log.w('NFC: Tag does not support NDEF'); + await FlutterNfcKit.finish(); + return; + } + + if (tag.ndefWritable != true) { + Log.w('NFC: Tag is not writable'); + await FlutterNfcKit.finish(); + return; + } + + final jsonData = jsonEncode(pairingInfo); + final mimeRecord = ndef.NDEFRecord( + tnf: ndef.TypeNameFormat.media, + type: Uint8List.fromList('application/xianyan-pair'.codeUnits), + id: Uint8List(0), + payload: Uint8List.fromList(utf8.encode(jsonData)), + ); + + try { + await FlutterNfcKit.writeNDEFRecords([mimeRecord]); + Log.i('NFC: NDEF record written successfully'); + } catch (e) { + Log.e('NFC: Write failed: $e'); + } + + await FlutterNfcKit.finish(); } catch (e) { Log.e('NFC: Write session failed: $e'); + try { + await FlutterNfcKit.finish(); + } catch (_) {} } } + /// Release resources Future dispose() async { await stopScan(); await _deviceController.close(); diff --git a/lib/features/file_transfer/services/discovery/qr_pairing_service.dart b/lib/features/file_transfer/services/discovery/qr_pairing_service.dart index f1c04515..87ff07bb 100644 --- a/lib/features/file_transfer/services/discovery/qr_pairing_service.dart +++ b/lib/features/file_transfer/services/discovery/qr_pairing_service.dart @@ -12,6 +12,7 @@ import 'dart:io'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:xianyan/core/utils/logger.dart'; +import 'package:xianyan/core/utils/platform_utils.dart' as pu; import '../../models/transfer_enums.dart'; import '../../models/transfer_device.dart'; @@ -117,7 +118,10 @@ class QrPairingService { Future startScan() async { if (_isScanning) return; - if (!Platform.isAndroid && !Platform.isIOS && !Platform.isMacOS) { + if (!pu.isOhos && + !Platform.isAndroid && + !Platform.isIOS && + !Platform.isMacOS) { Log.w('QR: Scanner not supported on ${Platform.operatingSystem}'); return; } diff --git a/lib/features/file_transfer/services/discovery/usb_discovery_service.dart b/lib/features/file_transfer/services/discovery/usb_discovery_service.dart index 6d0a1b51..0a9db005 100644 --- a/lib/features/file_transfer/services/discovery/usb_discovery_service.dart +++ b/lib/features/file_transfer/services/discovery/usb_discovery_service.dart @@ -12,6 +12,7 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:xianyan/core/utils/logger.dart'; +import 'package:xianyan/core/utils/platform_utils.dart' as pu; import '../../models/transfer_enums.dart'; import '../../models/transfer_device.dart'; @@ -23,8 +24,9 @@ class UsbDiscoveryService { final UsbTransportService _usbService; - static const EventChannel _usbEventChannel = - EventChannel('xianyan/usb_events'); + static const EventChannel _usbEventChannel = EventChannel( + 'xianyan/usb_events', + ); bool _isMonitoring = false; bool get isMonitoring => _isMonitoring; @@ -73,7 +75,7 @@ class UsbDiscoveryService { _handleDeviceDetached(device); }); - if (Platform.isAndroid) { + if (Platform.isAndroid || pu.isOhos) { _startNativeUsbEventListening(); } @@ -93,7 +95,9 @@ class UsbDiscoveryService { }, onError: (Object error) { if (error is MissingPluginException) { - Log.w('USB Discovery: Native event channel not implemented, skipping'); + Log.w( + 'USB Discovery: Native event channel not implemented, skipping', + ); _usbEventSub?.cancel(); _usbEventSub = null; return; @@ -129,7 +133,7 @@ class UsbDiscoveryService { } Future _scanExistingDevices() async { - if (!Platform.isAndroid) { + if (!Platform.isAndroid && !pu.isOhos) { final ips = await _usbService.detectUsbNetworkIps(); if (ips.isNotEmpty) { _onUsbConnected(); diff --git a/lib/features/file_transfer/services/notification_service.dart b/lib/features/file_transfer/services/notification_service.dart index 21e6d812..966daf72 100644 --- a/lib/features/file_transfer/services/notification_service.dart +++ b/lib/features/file_transfer/services/notification_service.dart @@ -1,13 +1,15 @@ // ============================================================ // 闲言APP — 传输通知服务 // 创建时间: 2026-05-10 -// 更新时间: 2026-05-10 +// 更新时间: 2026-05-17 // 作用: 传输完成/失败/接收等本地通知 -// 上次更新: 初始版本 — 跨平台兼容处理 +// 上次更新: 鸿蒙适配-增加OhosInitializationSettings // ============================================================ import 'dart:io'; +import 'package:xianyan/core/utils/platform_utils.dart' as pu; + import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:xianyan/core/utils/logger.dart'; @@ -29,49 +31,59 @@ class TransferNotificationService { Future initialize() async { if (_initialized) return; - if (!Platform.isAndroid && !Platform.isIOS && !Platform.isMacOS) { + if (!pu.isOhos && + !Platform.isAndroid && + !Platform.isIOS && + !Platform.isMacOS) { Log.w('Notification: Platform not supported'); _initialized = true; return; } - const androidSettings = - AndroidInitializationSettings('@mipmap/ic_launcher'); - const iosSettings = DarwinInitializationSettings( - - ); - const macOsSettings = DarwinInitializationSettings( - + const androidSettings = AndroidInitializationSettings( + '@mipmap/ic_launcher', ); + const iosSettings = DarwinInitializationSettings(); + const macOsSettings = DarwinInitializationSettings(); + const ohosSettings = OhosInitializationSettings('@mipmap/ic_launcher'); const settings = InitializationSettings( android: androidSettings, iOS: iosSettings, macOS: macOsSettings, + ohos: ohosSettings, ); await _plugin.initialize( - settings, + settings: settings, onDidReceiveNotificationResponse: (response) { Log.i('Notification: Tapped payload: ${response.payload}'); }, ); - if (Platform.isAndroid) { + if (pu.isOhos) { await _plugin .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() + OhosFlutterLocalNotificationsPlugin + >() + ?.requestNotificationsPermission(); + } else if (Platform.isAndroid) { + await _plugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin + >() ?.requestNotificationsPermission(); } _initialized = true; - Log.i('Notification: Service initialized'); } Future showTransferComplete(TransferTask task) async { if (!_initialized) await initialize(); - final title = task.direction == TransferDirection.send ? '📤 发送完成' : '📥 接收完成'; + final title = task.direction == TransferDirection.send + ? '📤 发送完成' + : '📥 接收完成'; final body = '${task.fileName} (${_formatBytes(task.fileSize)})'; await _showNotification( @@ -85,7 +97,9 @@ class TransferNotificationService { Future showTransferFailed(TransferTask task) async { if (!_initialized) await initialize(); - final title = task.direction == TransferDirection.send ? '❌ 发送失败' : '❌ 接收失败'; + final title = task.direction == TransferDirection.send + ? '❌ 发送失败' + : '❌ 接收失败'; final body = '${task.fileName}: ${task.errorMessage ?? "未知错误"}'; await _showNotification( @@ -113,7 +127,7 @@ class TransferNotificationService { required double progress, }) async { if (!_initialized) await initialize(); - if (!Platform.isAndroid) return; + if (!Platform.isAndroid && !pu.isOhos) return; final percent = (progress * 100).toStringAsFixed(0); @@ -129,7 +143,7 @@ class TransferNotificationService { } Future cancelNotification(int id) async { - await _plugin.cancel(id); + await _plugin.cancel(id: id); } Future _showNotification({ @@ -157,7 +171,9 @@ class TransferNotificationService { ); AndroidNotificationDetails? androidSpecific; - if (Platform.isAndroid && progress != null && maxProgress != null) { + if ((Platform.isAndroid || pu.isOhos) && + progress != null && + maxProgress != null) { androidSpecific = AndroidNotificationDetails( _channelId, _channelName, @@ -177,7 +193,13 @@ class TransferNotificationService { iOS: iosDetails, ); - await _plugin.show(id, title, body, details, payload: payload); + await _plugin.show( + id: id, + title: title, + body: body, + notificationDetails: details, + payload: payload, + ); } catch (e) { Log.w('Notification: Failed to show: $e'); } diff --git a/lib/features/file_transfer/services/transport/archive_transfer_service.dart b/lib/features/file_transfer/services/transport/archive_transfer_service.dart index f16ad3ae..eb73a5d5 100644 --- a/lib/features/file_transfer/services/transport/archive_transfer_service.dart +++ b/lib/features/file_transfer/services/transport/archive_transfer_service.dart @@ -34,7 +34,7 @@ class ArchiveTransferService { } final zipData = ZipEncoder().encode(archive); - if (zipData == null) { + if (zipData.isEmpty) { throw Exception('Archive: Failed to encode ZIP'); } diff --git a/lib/features/file_transfer/services/transport/localsend_service.dart b/lib/features/file_transfer/services/transport/localsend_service.dart index 092b4db5..e59c288a 100644 --- a/lib/features/file_transfer/services/transport/localsend_service.dart +++ b/lib/features/file_transfer/services/transport/localsend_service.dart @@ -6,12 +6,14 @@ // 上次更新: 集成WebTransferHandler,支持Web端文件传输路由(F10) // ============================================================ +import 'package:xianyan/core/utils/pattern_utils.dart'; import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:dio/dio.dart'; +import 'package:xianyan/core/utils/platform_utils.dart' as pu; import 'package:dio/io.dart'; import 'package:mime/mime.dart'; import 'package:network_info_plus/network_info_plus.dart'; @@ -72,6 +74,7 @@ class LocalSendConfig { static String _detectDeviceModel() { try { + if (pu.isOhos) return 'HarmonyOS'; if (Platform.isAndroid) return 'Android'; if (Platform.isIOS) return 'iOS'; if (Platform.isMacOS) return 'macOS'; @@ -165,7 +168,7 @@ class LocalSendService { } static String sanitizeFileName(String fileName) { - var safe = fileName.replaceAll(RegExp(r'[\\/]'), '_'); + var safe = fileName.replaceAll(regex(r'[\\/]'), '_'); safe = safe.replaceAll('..', '_'); if (safe.isEmpty || safe == '.' || safe == '..') { safe = 'file_${DateTime.now().millisecondsSinceEpoch}'; diff --git a/lib/features/file_transfer/services/transport/nearby_service_adapter.dart b/lib/features/file_transfer/services/transport/nearby_service_adapter.dart index 045583a9..2104bc79 100644 --- a/lib/features/file_transfer/services/transport/nearby_service_adapter.dart +++ b/lib/features/file_transfer/services/transport/nearby_service_adapter.dart @@ -15,6 +15,7 @@ import 'package:nearby_service/nearby_service.dart'; import 'package:uuid/uuid.dart'; import 'package:xianyan/core/utils/logger.dart'; +import 'package:xianyan/core/utils/platform_utils.dart' as pu; import 'package:xianyan/features/file_transfer/models/transfer_device.dart'; import 'package:xianyan/features/file_transfer/models/transfer_enums.dart'; import 'package:xianyan/features/file_transfer/models/transfer_message.dart'; @@ -42,7 +43,7 @@ class NearbyServiceAdapter { final _uuid = const Uuid(); static bool get isPlatformSupported => - Platform.isAndroid || Platform.isIOS || Platform.isMacOS; + pu.isOhos || Platform.isAndroid || Platform.isIOS || Platform.isMacOS; bool get isInitialized => _isInitialized; bool get isDiscovering => _isDiscovering; @@ -434,7 +435,7 @@ class NearbyServiceAdapter { } Future requestPermissions() async { - if (!Platform.isAndroid || _service == null) return true; + if ((!Platform.isAndroid && !pu.isOhos) || _service == null) return true; try { return await _service!.android!.requestPermissions(); } catch (e) { @@ -444,7 +445,7 @@ class NearbyServiceAdapter { } Future checkWifiService() async { - if (!Platform.isAndroid || _service == null) return true; + if ((!Platform.isAndroid && !pu.isOhos) || _service == null) return true; try { return await _service!.android!.checkWifiService(); } catch (e) { diff --git a/lib/features/file_transfer/services/transport/usb_transport_service.dart b/lib/features/file_transfer/services/transport/usb_transport_service.dart index 37f9afbe..6c92804c 100644 --- a/lib/features/file_transfer/services/transport/usb_transport_service.dart +++ b/lib/features/file_transfer/services/transport/usb_transport_service.dart @@ -12,6 +12,7 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:xianyan/core/utils/logger.dart'; +import 'package:xianyan/core/utils/platform_utils.dart' as pu; import '../../models/transfer_enums.dart'; import '../../models/transfer_device.dart'; @@ -123,8 +124,8 @@ class UsbTransportService { } Future isUsbHostSupported() async { - if (!Platform.isAndroid) { - Log.w('USB: USB Host mode only supported on Android'); + if (!Platform.isAndroid && !pu.isOhos) { + Log.w('USB: USB Host mode only supported on Android/HarmonyOS'); return false; } try { @@ -145,7 +146,7 @@ class UsbTransportService { } Future detectUsbDevice() async { - if (!Platform.isAndroid) return null; + if (!Platform.isAndroid && !pu.isOhos) return null; try { final result = await _channel.invokeMethod>( 'detectUsbDevice', @@ -166,7 +167,7 @@ class UsbTransportService { } Future> detectAllUsbDevices() async { - if (!Platform.isAndroid) return []; + if (!Platform.isAndroid && !pu.isOhos) return []; try { final result = await _channel.invokeMethod>( 'detectAllUsbDevices', @@ -185,7 +186,7 @@ class UsbTransportService { } Future connectDevice(UsbDevice device) async { - if (!Platform.isAndroid) return false; + if (!Platform.isAndroid && !pu.isOhos) return false; try { final result = await _channel.invokeMethod('connectDevice', { 'deviceId': device.deviceId, @@ -217,8 +218,8 @@ class UsbTransportService { String? sessionId, void Function(double)? onProgress, }) async { - if (!Platform.isAndroid) { - return _createFailedTask(taskId, file, 'USB传输仅支持Android平台'); + if (!Platform.isAndroid && !pu.isOhos) { + return _createFailedTask(taskId, file, 'USB传输仅支持Android/HarmonyOS平台'); } final fileSize = await file.length(); @@ -270,12 +271,12 @@ class UsbTransportService { String? sessionId, void Function(double)? onProgress, }) async { - if (!Platform.isAndroid) { + if (!Platform.isAndroid && !pu.isOhos) { return _createFailedReceiveTask( taskId, fileName, fileSize, - 'USB传输仅支持Android平台', + 'USB传输仅支持Android/HarmonyOS平台', ); } diff --git a/lib/features/file_transfer/services/transport/web_transfer_handler.dart b/lib/features/file_transfer/services/transport/web_transfer_handler.dart index 4d90c46b..198144d6 100644 --- a/lib/features/file_transfer/services/transport/web_transfer_handler.dart +++ b/lib/features/file_transfer/services/transport/web_transfer_handler.dart @@ -6,6 +6,7 @@ // 上次更新: 初始版本,支持GET / GET /api/info POST /api/send-file POST /api/send-text GET /ws // ============================================================ +import 'package:xianyan/core/utils/pattern_utils.dart'; import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -264,7 +265,7 @@ class WebTransferHandler { } } - void _handleWebSocket(WebSocketChannel webSocket) { + void _handleWebSocket(WebSocketChannel webSocket, String? subprotocol) { _webClients.add(webSocket); Log.i( 'WebTransfer: WebSocket client connected (${_webClients.length} total)', @@ -439,16 +440,16 @@ class WebTransferHandler { } String? _extractFileName(String contentDisposition) { - final match = RegExp(r'filename="([^"]*)"').firstMatch(contentDisposition); + final match = regex(r'filename="([^"]*)"').firstMatch(contentDisposition); if (match != null) return match.group(1); - final matchNoQuotes = RegExp( + final matchNoQuotes = regex( r'filename=([^\s;]+)', ).firstMatch(contentDisposition); return matchNoQuotes?.group(1); } static String _sanitizeFileName(String fileName) { - var safe = fileName.replaceAll(RegExp(r'[\\/]'), '_'); + var safe = fileName.replaceAll(regex(r'[\\/]'), '_'); safe = safe.replaceAll('..', '_'); if (safe.isEmpty || safe == '.' || safe == '..') { safe = 'file_${DateTime.now().millisecondsSinceEpoch}'; diff --git a/lib/features/health/providers/health_provider.dart b/lib/features/health/providers/health_provider.dart index 943f7cee..cdd6135f 100644 --- a/lib/features/health/providers/health_provider.dart +++ b/lib/features/health/providers/health_provider.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 健康生活状态管理 // 创建时间: 2026-04-29 // 更新时间: 2026-04-29 @@ -54,8 +54,10 @@ class HealthState { } } -class HealthNotifier extends StateNotifier { - HealthNotifier() : super(const HealthState()); +class HealthNotifier extends Notifier { + @override + HealthState build() => const HealthState(); + HealthNotifier(); Future loadList({required String type, bool refresh = false}) async { if (state.isLoading) return; @@ -111,6 +113,4 @@ class HealthNotifier extends StateNotifier { } final healthProvider = - StateNotifierProvider((ref) { - return HealthNotifier(); -}); + NotifierProvider(HealthNotifier.new); diff --git a/lib/features/home/presentation/history_page.dart b/lib/features/home/presentation/history_page.dart index ff3cd98e..2d52a5f6 100644 --- a/lib/features/home/presentation/history_page.dart +++ b/lib/features/home/presentation/history_page.dart @@ -808,7 +808,7 @@ class _HistoryPageState extends ConsumerState { buffer.writeln('\n─' * 20); buffer.writeln('来自「闲言」App'); - await Share.share(buffer.toString()); + await SharePlus.instance.share(ShareParams(text: buffer.toString())); } List _buildHistorySlivers(AppThemeExtension ext) { diff --git a/lib/features/home/presentation/home_daily_card.dart b/lib/features/home/presentation/home_daily_card.dart index 2dcf055d..2a980bcf 100644 --- a/lib/features/home/presentation/home_daily_card.dart +++ b/lib/features/home/presentation/home_daily_card.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 首页每日推荐卡片(滑动版) // 创建时间: 2026-04-27 // 更新时间: 2026-05-14 @@ -6,6 +6,7 @@ // 上次更新: 增强加载卡片动画(旋转图标+呼吸文字+微光+浮动圆点) // ============================================================ +import 'package:xianyan/core/utils/pattern_utils.dart'; import 'dart:async'; import 'dart:math'; @@ -1043,16 +1044,16 @@ class _DailyCardState extends ConsumerState String _stripHtml(String htmlText) { return htmlText - .replaceAll(RegExp(r''), '\n') - .replaceAll(RegExp(r']*>'), '') - .replaceAll(RegExp(r'

'), '\n') - .replaceAll(RegExp(r'<[^>]*>'), '') - .replaceAll(RegExp(r' '), ' ') - .replaceAll(RegExp(r'&'), '&') - .replaceAll(RegExp(r'<'), '<') - .replaceAll(RegExp(r'>'), '>') - .replaceAll(RegExp(r'"'), '"') - .replaceAll(RegExp(r'\n{3,}'), '\n\n') + .replaceAll(regex(r''), '\n') + .replaceAll(regex(r']*>'), '') + .replaceAll(regex(r'

'), '\n') + .replaceAll(regex(r'<[^>]*>'), '') + .replaceAll(regex(r' '), ' ') + .replaceAll(regex(r'&'), '&') + .replaceAll(regex(r'<'), '<') + .replaceAll(regex(r'>'), '>') + .replaceAll(regex(r'"'), '"') + .replaceAll(regex(r'\n{3,}'), '\n\n') .trim(); } } diff --git a/lib/features/home/presentation/home_page.dart b/lib/features/home/presentation/home_page.dart index 51c60540..01eef09c 100644 --- a/lib/features/home/presentation/home_page.dart +++ b/lib/features/home/presentation/home_page.dart @@ -30,6 +30,7 @@ import '../../../shared/widgets/app_toast.dart'; import '../../../shared/widgets/skeleton.dart'; import '../../../core/utils/interaction_animations.dart'; import '../../../core/utils/sheet_animation_notifier.dart'; +import '../../../core/utils/logger.dart'; import '../providers/home_provider.dart'; import '../../../features/source/providers/source_provider.dart'; import 'home_daily_card.dart'; @@ -102,6 +103,9 @@ class _HomePageState extends ConsumerState { Widget build(BuildContext context) { final ext = AppTheme.ext(context); final state = ref.watch(homeProvider); + Log.i( + 'HomePage build: bgPrimary=${ext.bgPrimary} textPrimary=${ext.textPrimary} isLoading=${state.isLoading} sentences=${state.sentences.length}', + ); ref.listen(sourceProvider, (prev, next) { if (prev?.disabledKeys != next.disabledKeys) { diff --git a/lib/features/home/presentation/providers/likes_page.dart b/lib/features/home/presentation/providers/likes_page.dart index d40c9d9d..82b8835b 100644 --- a/lib/features/home/presentation/providers/likes_page.dart +++ b/lib/features/home/presentation/providers/likes_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 点赞历史页面 /// 创建时间: 2026-04-29 /// 更新时间: 2026-05-04 @@ -6,6 +6,7 @@ /// 上次更新: 本地数据与登录解耦,未登录也可使用本地点赞 /// ============================================================ +import 'package:xianyan/core/utils/pattern_utils.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -75,7 +76,7 @@ class _LikesPageState extends ConsumerState { int synced = 0; for (final s in localLiked) { try { - final feedId = int.tryParse(s.id.replaceAll(RegExp(r'[^0-9]'), '')); + final feedId = int.tryParse(s.id.replaceAll(regex(r'[^0-9]'), '')); if (feedId != null && feedId > 0) { final success = await FeedService.action( action: 'like', diff --git a/lib/features/home/presentation/providers/sentence_detail_sheet.dart b/lib/features/home/presentation/providers/sentence_detail_sheet.dart index f1ba6515..3c93f410 100644 --- a/lib/features/home/presentation/providers/sentence_detail_sheet.dart +++ b/lib/features/home/presentation/providers/sentence_detail_sheet.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 句子详情Sheet // 创建时间: 2026-04-30 // 更新时间: 2026-05-01 @@ -6,6 +6,7 @@ // 上次更新: 新增书签/标签互动按钮,接入interaction provider // ============================================================ +import 'package:xianyan/core/utils/pattern_utils.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -743,13 +744,13 @@ class _SentenceDetailContentState if (html.isEmpty) return html; var result = html; result = result.replaceAll( - RegExp(r'', caseSensitive: false), + regex(r'', caseSensitive: false), '\n', ); - result = result.replaceAll(RegExp(r'', caseSensitive: false), '\n'); - result = result.replaceAll(RegExp(r'

', caseSensitive: false), ''); + result = result.replaceAll(regex(r'', caseSensitive: false), '\n'); + result = result.replaceAll(regex(r'

', caseSensitive: false), ''); result = result.replaceAll( - RegExp( + regex( r']*>(.*?)', caseSensitive: false, dotAll: true, @@ -757,11 +758,11 @@ class _SentenceDetailContentState r'$1', ); result = result.replaceAll( - RegExp(r']*>(.*?)', caseSensitive: false, dotAll: true), + regex(r']*>(.*?)', caseSensitive: false, dotAll: true), r'$1', ); - result = result.replaceAll(RegExp(r'<[^>]*>'), ''); - result = result.replaceAll(RegExp(r'\n{3,}'), '\n\n'); + result = result.replaceAll(regex(r'<[^>]*>'), ''); + result = result.replaceAll(regex(r'\n{3,}'), '\n\n'); return result.trim(); } } diff --git a/lib/features/home/providers/cache_provider.dart b/lib/features/home/providers/cache_provider.dart index f266589b..35944dce 100644 --- a/lib/features/home/providers/cache_provider.dart +++ b/lib/features/home/providers/cache_provider.dart @@ -44,19 +44,21 @@ class CacheState { // ---- 缓存 Provider ---- -class CacheNotifier extends StateNotifier { - CacheNotifier() : super(const CacheState()); +class CacheNotifier extends Notifier { + @override + CacheState build() { + Future.microtask(loadStats); + return const CacheState(); + } + + CacheNotifier(); Future loadStats() async { state = state.copyWith(isLoading: true); try { final stats = await CacheService.getCacheStats(); final config = CacheService.config; - state = state.copyWith( - stats: stats, - isLoading: false, - config: config, - ); + state = state.copyWith(stats: stats, isLoading: false, config: config); } catch (e) { Log.e('CacheProvider: 统计加载失败', e); state = state.copyWith(isLoading: false); @@ -108,6 +110,6 @@ class CacheNotifier extends StateNotifier { } } -final cacheProvider = StateNotifierProvider( - (ref) => CacheNotifier()..loadStats(), +final cacheProvider = NotifierProvider( + CacheNotifier.new, ); diff --git a/lib/features/home/providers/daily_card_style_provider.dart b/lib/features/home/providers/daily_card_style_provider.dart index d148b355..9a6caa22 100644 --- a/lib/features/home/providers/daily_card_style_provider.dart +++ b/lib/features/home/providers/daily_card_style_provider.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 主页每日卡片样式状态管理 // 创建时间: 2026-04-30 // 更新时间: 2026-04-30 @@ -139,8 +139,10 @@ class DailyCardStyle { } } -class DailyCardStyleNotifier extends StateNotifier { - DailyCardStyleNotifier() : super(const DailyCardStyle()) { +class DailyCardStyleNotifier extends Notifier { + @override + DailyCardStyle build() => const DailyCardStyle(); + DailyCardStyleNotifier() { _loadFromStorage(); } @@ -241,6 +243,6 @@ class DailyCardStyleNotifier extends StateNotifier { } final dailyCardStyleProvider = - StateNotifierProvider( - (ref) => DailyCardStyleNotifier(), + NotifierProvider( + DailyCardStyleNotifier.new, ); diff --git a/lib/features/home/providers/favorite_provider.dart b/lib/features/home/providers/favorite_provider.dart index 9a97e3a9..aaed11e6 100644 --- a/lib/features/home/providers/favorite_provider.dart +++ b/lib/features/home/providers/favorite_provider.dart @@ -115,8 +115,10 @@ class FavoriteState { } } -class FavoriteNotifier extends StateNotifier { - FavoriteNotifier() : super(const FavoriteState()); +class FavoriteNotifier extends Notifier { + @override + FavoriteState build() => const FavoriteState(); + FavoriteNotifier(); Future loadFavorites({bool refresh = false}) async { if (state.isLoading) return; @@ -646,8 +648,4 @@ class SyncResult { bool get isSuccess => !isError; } -final favoriteProvider = StateNotifierProvider( - (ref) { - return FavoriteNotifier(); - }, -); +final favoriteProvider = NotifierProvider(FavoriteNotifier.new); diff --git a/lib/features/home/providers/home_feed_mixin.dart b/lib/features/home/providers/home_feed_mixin.dart index bf28eb97..1d2e195f 100644 --- a/lib/features/home/providers/home_feed_mixin.dart +++ b/lib/features/home/providers/home_feed_mixin.dart @@ -20,7 +20,7 @@ import '../services/feed_service.dart'; import 'home_sentence_model.dart'; import 'home_state.dart'; -mixin HomeFeedMixin on StateNotifier { +mixin HomeFeedMixin on Notifier { AppDatabase get feedDb; List get allChannels; set allChannels(List value); diff --git a/lib/features/home/providers/home_interaction_mixin.dart b/lib/features/home/providers/home_interaction_mixin.dart index 356c284a..4f4c58e4 100644 --- a/lib/features/home/providers/home_interaction_mixin.dart +++ b/lib/features/home/providers/home_interaction_mixin.dart @@ -10,12 +10,16 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/storage/database/app_database.dart'; import '../../../core/utils/logger.dart'; +import '../../inspiration/providers/chat_provider.dart'; import '../../inspiration/services/chat_message_service.dart'; import '../services/feed_service.dart'; import 'home_sentence_model.dart'; import 'home_state.dart'; -mixin HomeInteractionMixin on StateNotifier { +mixin HomeInteractionMixin on Notifier { + // ignore: unused_field + final bool _mounted = true; + bool get mounted => _mounted; AppDatabase get interactionDb; Set get togglingIds; @@ -172,6 +176,7 @@ mixin HomeInteractionMixin on StateNotifier { views: sentence.views, sentenceId: sentence.id.toString(), ); + notifyReadlaterRefresh(); Log.i('稍后读句子已写入会话: ${sentence.id}'); } catch (e) { Log.w('稍后读句子写入会话失败: $e'); diff --git a/lib/features/home/providers/home_provider.dart b/lib/features/home/providers/home_provider.dart index ed6c31d3..496d0e67 100644 --- a/lib/features/home/providers/home_provider.dart +++ b/lib/features/home/providers/home_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 首页 Provider /// 创建时间: 2026-04-20 /// 更新时间: 2026-05-13 @@ -23,9 +23,20 @@ import 'home_interaction_mixin.dart'; import 'home_sentence_model.dart'; import 'home_state.dart'; -class HomeNotifier extends StateNotifier +class HomeNotifier extends Notifier with HomeInteractionMixin, HomeFeedMixin { - final AppDatabase _db; + @override + HomeState build() { + ref.onDispose(_onDispose); + Future.microtask(() { + _db = AppDatabase.instance; + _init(); + _listenConnectivity(); + }); + return const HomeState(); + } + + late final AppDatabase _db; final Connectivity _connectivity = Connectivity(); Stream>? _connectivityStream; @@ -97,15 +108,10 @@ class HomeNotifier extends StateNotifier @override String get cacheKey => '${state.selectedType ?? 'all'}_${state.currentSort}'; - HomeNotifier(this._db) : super(const HomeState()) { - _init(); - _listenConnectivity(); - } + HomeNotifier(); - @override - void dispose() { + void _onDispose() { _connectivityStream = null; - super.dispose(); } Future _listenConnectivity() async { @@ -404,6 +410,6 @@ final appDatabaseProvider = Provider( (ref) => AppDatabase.instance, ); -final homeProvider = StateNotifierProvider((ref) { - return HomeNotifier(ref.read(appDatabaseProvider)); -}); +final homeProvider = NotifierProvider( + HomeNotifier.new, +); diff --git a/lib/features/home/providers/likes_provider.dart b/lib/features/home/providers/likes_provider.dart index 6d113fd6..cc837ead 100644 --- a/lib/features/home/providers/likes_provider.dart +++ b/lib/features/home/providers/likes_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 点赞历史状态管理(服务端同步) /// 创建时间: 2026-04-29 /// 更新时间: 2026-04-29 @@ -47,8 +47,10 @@ class LikesState { } } -class LikesNotifier extends StateNotifier { - LikesNotifier() : super(const LikesState()); +class LikesNotifier extends Notifier { + @override + LikesState build() => const LikesState(); + LikesNotifier(); Future loadLikes({bool refresh = false}) async { if (state.isLoading) return; @@ -71,6 +73,4 @@ class LikesNotifier extends StateNotifier { } } -final likesProvider = StateNotifierProvider((ref) { - return LikesNotifier(); -}); +final likesProvider = NotifierProvider(LikesNotifier.new); diff --git a/lib/features/home/providers/offline_provider.dart b/lib/features/home/providers/offline_provider.dart index c9edf6e4..ca9cdd81 100644 --- a/lib/features/home/providers/offline_provider.dart +++ b/lib/features/home/providers/offline_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 离线状态 Provider /// 创建时间: 2026-04-28 /// 更新时间: 2026-04-28 @@ -49,8 +49,15 @@ class OfflineState { // ---- 离线 Provider ---- -class OfflineNotifier extends StateNotifier { - OfflineNotifier() : super(const OfflineState()); +class OfflineNotifier extends Notifier { + @override + OfflineState build() { + ref.onDispose(_onDispose); + Future.microtask(init); + return const OfflineState(); + } + + OfflineNotifier(); Future init() async { await OfflineManager.init(); @@ -108,13 +115,11 @@ class OfflineNotifier extends StateNotifier { state = state.copyWith(pendingActionCount: 0); } - @override - void dispose() { + void _onDispose() { OfflineManager.onlineNotifier.removeListener(_onOnlineChanged); - super.dispose(); } } -final offlineProvider = StateNotifierProvider( - (ref) => OfflineNotifier()..init(), +final offlineProvider = NotifierProvider( + OfflineNotifier.new, ); diff --git a/lib/features/home/providers/quick_card_provider.dart b/lib/features/home/providers/quick_card_provider.dart index 265cff29..a2a4aa1e 100644 --- a/lib/features/home/providers/quick_card_provider.dart +++ b/lib/features/home/providers/quick_card_provider.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 快速卡片创作状态管理 // 创建时间: 2026-04-26 // 更新时间: 2026-04-27 @@ -138,9 +138,9 @@ class QuickCardState { // ─── StateNotifier ─── -class QuickCardNotifier extends StateNotifier { - QuickCardNotifier({String? initialText}) - : super(QuickCardState(text: initialText ?? '在此输入文字')); +class QuickCardNotifier extends Notifier { + @override + QuickCardState build() => const QuickCardState(text: '在此输入文字'); void updateText(String text) => state = state.copyWith(text: text); void updateFontSize(double size) => state = state.copyWith(fontSize: size); @@ -237,7 +237,6 @@ class QuickCardNotifier extends StateNotifier { // ─── Provider ─── -final quickCardProvider = - StateNotifierProvider.family( - (ref, initialText) => QuickCardNotifier(initialText: initialText), - ); +final quickCardProvider = NotifierProvider( + QuickCardNotifier.new, +); diff --git a/lib/features/home/services/searchall_service.dart b/lib/features/home/services/searchall_service.dart index 5f4eee68..5776dfe5 100644 --- a/lib/features/home/services/searchall_service.dart +++ b/lib/features/home/services/searchall_service.dart @@ -6,6 +6,7 @@ // 上次更新: 新增sources/relatedRecommend/history/hotWithCount四个接口 // ============================================================ +import 'package:xianyan/core/utils/pattern_utils.dart'; import '../../../core/network/api_client.dart'; import '../../../core/utils/logger.dart'; import '../../../core/utils/extensions.dart'; @@ -388,20 +389,38 @@ class SearchAllService { final list = resultData['list'] as List? ?? []; return list.map((e) { final json = e as Map; - final titleHl = (json['title_highlight'] as String? ?? '').stripHtml.decodeHtmlEntities; - final contentHl = (json['content_highlight'] as String? ?? '').stripHtml.decodeHtmlEntities; - final authorHl = (json['author_highlight'] as String? ?? '').stripHtml.decodeHtmlEntities; + final titleHl = (json['title_highlight'] as String? ?? '') + .stripHtml + .decodeHtmlEntities; + final contentHl = (json['content_highlight'] as String? ?? '') + .stripHtml + .decodeHtmlEntities; + final authorHl = (json['author_highlight'] as String? ?? '') + .stripHtml + .decodeHtmlEntities; return SearchHighlightItem( item: FeedItem.fromJson(json), titleHighlight: titleHl.isNotEmpty ? titleHl - : _autoHighlight((json['title'] as String? ?? '').cleanHtml, keyword, tag), + : _autoHighlight( + (json['title'] as String? ?? '').cleanHtml, + keyword, + tag, + ), contentHighlight: contentHl.isNotEmpty ? contentHl - : _autoHighlight((json['content'] as String? ?? '').cleanHtml, keyword, tag), + : _autoHighlight( + (json['content'] as String? ?? '').cleanHtml, + keyword, + tag, + ), authorHighlight: authorHl.isNotEmpty ? authorHl - : _autoHighlight((json['author'] as String? ?? '').cleanHtml, keyword, tag), + : _autoHighlight( + (json['author'] as String? ?? '').cleanHtml, + keyword, + tag, + ), ); }).toList(); } catch (e) { @@ -592,8 +611,8 @@ class SearchAllService { static String _autoHighlight(String text, String keyword, String tag) { if (text.isEmpty || keyword.isEmpty) return text; - final escaped = RegExp.escape(keyword); - final pattern = RegExp(escaped, caseSensitive: false); + final escaped = regexEscape(keyword); + final pattern = regex(escaped, caseSensitive: false); return text.replaceAllMapped(pattern, (m) => '<$tag>${m[0]}'); } diff --git a/lib/features/inspiration/presentation/pages/chat/chat_flow_page.dart b/lib/features/inspiration/presentation/pages/chat/chat_flow_page.dart index 6b17c0da..d7f9fdeb 100644 --- a/lib/features/inspiration/presentation/pages/chat/chat_flow_page.dart +++ b/lib/features/inspiration/presentation/pages/chat/chat_flow_page.dart @@ -6,6 +6,7 @@ // 上次更新: v5.4.0 拆分为多文件,主文件保留State管理和build方法 // ============================================================ +import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -27,6 +28,7 @@ import 'package:xianyan/features/inspiration/providers/chat_provider.dart'; import 'package:xianyan/features/inspiration/providers/chat_attachment_provider.dart'; import 'package:xianyan/features/inspiration/providers/chat_conversation_provider.dart'; import 'package:xianyan/features/inspiration/services/chat_conversation_service.dart'; +import 'package:xianyan/shared/widgets/keyboard_safe_sheet.dart'; class ChatFlowPage extends ConsumerStatefulWidget { const ChatFlowPage({ @@ -57,23 +59,51 @@ class _ChatFlowPageState extends ConsumerState String _inputText = ''; ChatMessage? _replyToMessage; String? _selectedQuickCategory; + StreamSubscription? _readlaterRefreshSub; + late final String _pageKey; @override void initState() { super.initState(); + _pageKey = + 'chat_flow_${widget.conversationId}_${DateTime.now().millisecondsSinceEpoch}'; + KeyboardManager.instance.registerPage(_pageKey); WidgetsBinding.instance.addObserver(this); _inputController = TextEditingController( - text: ref.read(chatMessagesProvider(widget.conversationId)).draftText, + text: ref.read(chatMessagesProvider).draftText, ); _inputText = _inputController.text; _inputController.addListener(_onInputChanged); + _focusNode.addListener(_onFocusChanged); WidgetsBinding.instance.addPostFrameCallback((_) { FocusScope.of(context).unfocus(); _focusNode.unfocus(); ref - .read(chatMessagesProvider(widget.conversationId).notifier) + .read(chatMessagesProvider.notifier) .markAllAsRead(); }); + + if (widget.isReadlater) { + _readlaterRefreshSub = readlaterRefreshStream.listen((_) { + if (mounted) { + ref + .read(chatMessagesProvider.notifier) + .reloadMessages(); + } + }); + } + } + + void _onFocusChanged() { + if (_focusNode.hasFocus && !_userTappedInput) { + if (!KeyboardManager.instance.canShowKeyboard(_pageKey)) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_focusNode.hasFocus) { + _focusNode.unfocus(); + } + }); + } + } } void _onInputChanged() { @@ -81,7 +111,7 @@ class _ChatFlowPageState extends ConsumerState if (text != _inputText) { _inputText = text; ref - .read(chatMessagesProvider(widget.conversationId).notifier) + .read(chatMessagesProvider.notifier) .saveDraft(text); } } @@ -89,6 +119,10 @@ class _ChatFlowPageState extends ConsumerState @override void dispose() { WidgetsBinding.instance.removeObserver(this); + KeyboardManager.instance.dismissForPage(_pageKey); + KeyboardManager.instance.unregisterPage(_pageKey); + _readlaterRefreshSub?.cancel(); + _focusNode.removeListener(_onFocusChanged); _inputController.removeListener(_onInputChanged); _inputController.dispose(); _scrollController.dispose(); @@ -100,13 +134,18 @@ class _ChatFlowPageState extends ConsumerState @override void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.resumed && !_userTappedInput) { + if (state == AppLifecycleState.resumed) { WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { + if (mounted && !_userTappedInput) { FocusScope.of(context).unfocus(); _focusNode.unfocus(); + KeyboardManager.instance.clearUserTapped(_pageKey); } }); + } else if (state == AppLifecycleState.paused) { + _userTappedInput = false; + KeyboardManager.instance.clearUserTapped(_pageKey); + KeyboardManager.dismissAll(); } } @@ -148,6 +187,8 @@ class _ChatFlowPageState extends ConsumerState void _setReplyTo(ChatMessage message) { setState(() => _replyToMessage = message); + _userTappedInput = true; + KeyboardManager.instance.markUserTapped(_pageKey); _focusNode.requestFocus(); } @@ -162,7 +203,7 @@ class _ChatFlowPageState extends ConsumerState _searchController.clear(); _searchFocusNode.unfocus(); ref - .read(chatMessagesProvider(widget.conversationId).notifier) + .read(chatMessagesProvider.notifier) .clearSearch(); } else { Future.delayed(const Duration(milliseconds: 100), () { @@ -174,7 +215,7 @@ class _ChatFlowPageState extends ConsumerState void _onSearchChanged(String query) { ref - .read(chatMessagesProvider(widget.conversationId).notifier) + .read(chatMessagesProvider.notifier) .setSearchQuery(query); } @@ -188,7 +229,7 @@ class _ChatFlowPageState extends ConsumerState chatAttachmentProvider(widget.conversationId), ); final chatNotifier = ref.read( - chatMessagesProvider(widget.conversationId).notifier, + chatMessagesProvider.notifier, ); final attachmentNotifier = ref.read( chatAttachmentProvider(widget.conversationId).notifier, @@ -319,7 +360,9 @@ class _ChatFlowPageState extends ConsumerState GestureDetector( onTap: () { _userTappedInput = false; - FocusScope.of(context).unfocus(); + KeyboardManager.instance.clearUserTapped(_pageKey); + KeyboardManager.unfocusScope(context); + KeyboardManager.dismissAll(); }, behavior: HitTestBehavior.translucent, child: SafeArea( diff --git a/lib/features/inspiration/presentation/pages/chat/chat_settings_page.dart b/lib/features/inspiration/presentation/pages/chat/chat_settings_page.dart index 10a10545..c1c4adb3 100644 --- a/lib/features/inspiration/presentation/pages/chat/chat_settings_page.dart +++ b/lib/features/inspiration/presentation/pages/chat/chat_settings_page.dart @@ -400,16 +400,16 @@ class _ChatSettingsPageState extends ConsumerState { void _exportMessages() { try { - final chatNotifier = ref.read( - chatMessagesProvider(widget.conversationId).notifier, - ); + final chatNotifier = ref.read(chatMessagesProvider.notifier); final jsonStr = chatNotifier.exportMessages(); final dir = Directory.systemTemp; final file = File( '${dir.path}/chat_export_${DateTime.now().millisecondsSinceEpoch}.json', ); file.writeAsStringSync(jsonStr); - Share.shareXFiles([XFile(file.path)], text: '闲言APP - 会话流导出'); + SharePlus.instance.share( + ShareParams(files: [XFile(file.path)], text: '闲言APP - 会话流导出'), + ); Log.i('消息导出成功'); } catch (e) { Log.e('消息导出失败', e); @@ -423,14 +423,14 @@ class _ChatSettingsPageState extends ConsumerState { void _shareMessages() { try { - final messages = ref - .read(chatMessagesProvider(widget.conversationId)) - .messages; + final messages = ref.read(chatMessagesProvider).messages; final text = messages .take(10) .map((m) => '${m.displayTimestamp}\n${m.text}') .join('\n\n---\n\n'); - Share.share(text, subject: '闲言APP - 会话流分享'); + SharePlus.instance.share( + ShareParams(text: text, subject: '闲言APP - 会话流分享'), + ); } catch (e) { Log.e('分享失败', e); } @@ -586,9 +586,7 @@ class _ChatSettingsPageState extends ConsumerState { void _openRecycleBin() async { final ext = AppTheme.ext(context); - final chatNotifier = ref.read( - chatMessagesProvider(widget.conversationId).notifier, - ); + final chatNotifier = ref.read(chatMessagesProvider.notifier); final deletedMessages = await chatNotifier.getDeletedMessages(); if (!mounted) return; @@ -729,9 +727,7 @@ class _ChatSettingsPageState extends ConsumerState { isDestructiveAction: true, onPressed: () async { Navigator.pop(ctx); - await ref - .read(chatMessagesProvider(widget.conversationId).notifier) - .clearAllMessages(); + await ref.read(chatMessagesProvider.notifier).clearAllMessages(); _showToast('消息已清空'); }, child: const Text('清空'), diff --git a/lib/features/inspiration/presentation/pages/readlater_stats_page.dart b/lib/features/inspiration/presentation/pages/readlater_stats_page.dart index 3fbc6008..e91c6cc7 100644 --- a/lib/features/inspiration/presentation/pages/readlater_stats_page.dart +++ b/lib/features/inspiration/presentation/pages/readlater_stats_page.dart @@ -1,9 +1,8 @@ // ============================================================ -// 闲言APP — 稍后读统计页面 -// 创建时间: 2026-05-15 +// 闲言APP ?稍后读统计页?// 创建时间: 2026-05-15 // 更新时间: 2026-05-15 -// 作用: 稍后读阅读统计 — 总览/类型分布饼图/7天趋势折线图 -// 上次更新: 初始创建 — E7阅读统计功能 +// 作用: 稍后读阅读统??总览/类型分布饼图/7天趋势折线图 +// 上次更新: 初始创建 ?E7阅读统计功能 // ============================================================ import 'package:flutter/cupertino.dart'; @@ -70,7 +69,7 @@ class ReadlaterStatsPage extends StatelessWidget { Expanded( child: _StatCard( emoji: '📬', - label: '总消息', + label: '总消?, value: '$total', color: ext.accent, ext: ext, @@ -79,7 +78,7 @@ class ReadlaterStatsPage extends StatelessWidget { const SizedBox(width: AppSpacing.sm), Expanded( child: _StatCard( - emoji: '✅', + emoji: '?, label: '已读', value: '$readCount', color: ext.successColor, @@ -276,7 +275,7 @@ class ReadlaterStatsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '📈 最近7天新增', + '📈 最?天新?, style: AppTypography.headline.copyWith( color: ext.textPrimary, fontWeight: FontWeight.w600, @@ -371,11 +370,11 @@ class ReadlaterStatsPage extends StatelessWidget { lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( getTooltipColor: (_) => ext.bgElevated, - tooltipRoundedRadius: 8, + tooltipBorderRadius: BorderRadius.circular(8), getTooltipItems: (spots) => spots .map( (s) => LineTooltipItem( - '${s.y.toInt()} 条', + '${s.y.toInt()} ?, AppTypography.caption1.copyWith( color: ext.textPrimary, fontWeight: FontWeight.w600, @@ -401,15 +400,15 @@ class ReadlaterStatsPage extends StatelessWidget { return switch (type) { ChatMessageType.sentence => '💬 句子', ChatMessageType.link => '🔗 链接', - ChatMessageType.image => '🖼️ 图片', + ChatMessageType.image => '🖼?图片', ChatMessageType.video => '🎬 视频', ChatMessageType.document => '📄 文档', ChatMessageType.file => '📁 文件', ChatMessageType.audio => '🎵 音频', - ChatMessageType.richText => '✏️ 富文本', - ChatMessageType.readlaterSentence => '📖 稍后读', - ChatMessageType.greeting => '👋 问候', - ChatMessageType.weather => '🌤️ 天气', + ChatMessageType.richText => '✏️ 富文?, + ChatMessageType.readlaterSentence => '📖 稍后?, + ChatMessageType.greeting => '👋 问?, + ChatMessageType.weather => '🌤?天气', ChatMessageType.scenario => '🎭 情景', ChatMessageType.system => '⚙️ 系统', ChatMessageType.userMessage => '👤 用户', diff --git a/lib/features/inspiration/presentation/pages/tool/china_colors_page.dart b/lib/features/inspiration/presentation/pages/tool/china_colors_page.dart index bdf1bd9e..b27a434a 100644 --- a/lib/features/inspiration/presentation/pages/tool/china_colors_page.dart +++ b/lib/features/inspiration/presentation/pages/tool/china_colors_page.dart @@ -907,8 +907,8 @@ class _ChinaColorsPageState extends ConsumerState { color: ext.bgSecondary, borderRadius: AppRadius.lgBorder, onPressed: () { - share_plus.Share.share( - '🎨 ${item.name} ${item.hex} — 来自闲言传统色', + share_plus.SharePlus.instance.share( + share_plus.ShareParams(text: '🎨 ${item.name} ${item.hex} — 来自闲言传统色'), ); }, child: Text( diff --git a/lib/features/inspiration/presentation/pages/tool/hanzi_tool_page.dart b/lib/features/inspiration/presentation/pages/tool/hanzi_tool_page.dart index aa8daec1..785052fb 100644 --- a/lib/features/inspiration/presentation/pages/tool/hanzi_tool_page.dart +++ b/lib/features/inspiration/presentation/pages/tool/hanzi_tool_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 汉语工具通用查询页 /// 创建时间: 2026-04-28 /// 更新时间: 2026-04-28 @@ -6,6 +6,7 @@ /// 上次更新: 迁移缓存到 Drift (HanziCaches表) /// ============================================================ +import 'package:xianyan/core/utils/pattern_utils.dart'; import 'dart:convert'; import 'package:flutter/cupertino.dart'; @@ -81,10 +82,19 @@ class HanziQueryState { } /// 汉语工具查询 Provider -class HanziQueryNotifier extends StateNotifier { - HanziQueryNotifier(this._config) : super(const HanziQueryState()); +class HanziQueryNotifier extends Notifier { + @override + HanziQueryState build() => const HanziQueryState(); - final HanziToolConfig _config; + HanziToolConfig _config = const HanziToolConfig( + type: 'zi', + title: '汉字查询', + emoji: '🔍', + hintText: '输入汉字', + emptyMessage: '暂无结果', + ); + + void setConfig(HanziToolConfig config) => _config = config; static const _cachePrefix = 'hanzi_cache_'; @@ -174,9 +184,11 @@ class HanziQueryNotifier extends StateNotifier { } /// 查询历史 Provider -class HanziHistoryNotifier extends StateNotifier> { - HanziHistoryNotifier() : super([]) { - _loadHistory(); +class HanziHistoryNotifier extends Notifier> { + @override + List build() { + Future.microtask(_loadHistory); + return []; } static const _historyKey = 'hanzi_query_history'; @@ -236,21 +248,21 @@ class HanziHistoryNotifier extends StateNotifier> { } final hanziHistoryProvider = - StateNotifierProvider>( - (ref) => HanziHistoryNotifier(), + NotifierProvider>( + HanziHistoryNotifier.new, ); /// 汉语工具 Provider 工厂 final _hanziProviders = - >{}; + >{}; -StateNotifierProvider hanziQueryProvider( +NotifierProvider hanziQueryProvider( HanziToolConfig config, ) { return _hanziProviders.putIfAbsent( config.type, - () => StateNotifierProvider( - (ref) => HanziQueryNotifier(config), + () => NotifierProvider( + HanziQueryNotifier.new, ), ); } @@ -271,9 +283,13 @@ class _HanziToolPageState extends ConsumerState { @override void dispose() { + _onDispose(); + super.dispose(); + } + + void _onDispose() { _controller.dispose(); _focusNode.dispose(); - super.dispose(); } void _doQuery() { @@ -664,7 +680,7 @@ class _HanziToolPageState extends ConsumerState { ? pinyin[0].toUpperCase() : (item['zi']?.toString() ?? item['name']?.toString() ?? '#')[0] .toUpperCase(); - final key = RegExp(r'[A-Z]').hasMatch(letter) ? letter : '#'; + final key = regex(r'[A-Z]').hasMatch(letter) ? letter : '#'; groups.putIfAbsent(key, () => []).add(item); } @@ -720,8 +736,11 @@ class _HanziToolPageState extends ConsumerState { SlideActionConfig( type: SlideActionType.share, onPressed: () { - share_plus.Share.share( - '${widget.config.emoji} ${widget.config.title}\n$shareText', + share_plus.SharePlus.instance.share( + share_plus.ShareParams( + text: + '${widget.config.emoji} ${widget.config.title}\n$shareText', + ), ); }, ), diff --git a/lib/features/inspiration/presentation/pages/tool/ocr_tool_page.dart b/lib/features/inspiration/presentation/pages/tool/ocr_tool_page.dart index 44323c4f..a01f39c0 100644 --- a/lib/features/inspiration/presentation/pages/tool/ocr_tool_page.dart +++ b/lib/features/inspiration/presentation/pages/tool/ocr_tool_page.dart @@ -172,7 +172,7 @@ class _OcrToolPageState extends ConsumerState { padding: EdgeInsets.zero, minimumSize: Size.zero, onPressed: () { - share_plus.Share.share(_result ?? ''); + share_plus.SharePlus.instance.share(share_plus.ShareParams(text: _result ?? '')); }, child: Text( '📤 分享', diff --git a/lib/features/inspiration/presentation/pages/tool_list_page.dart b/lib/features/inspiration/presentation/pages/tool_list_page.dart index df2d6d6f..a5eaac51 100644 --- a/lib/features/inspiration/presentation/pages/tool_list_page.dart +++ b/lib/features/inspiration/presentation/pages/tool_list_page.dart @@ -656,8 +656,8 @@ class _ToolListPageState extends ConsumerState { final shareText = url.isNotEmpty ? '${title.toString()} $url' : title.toString(); - share_plus.Share.share( - '${widget.emoji} ${widget.title}\n$shareText', + share_plus.SharePlus.instance.share( + share_plus.ShareParams(text: '${widget.emoji} ${widget.title}\n$shareText'), ); }, ), diff --git a/lib/features/inspiration/presentation/widgets/chat/chat_flow_input_bar.dart b/lib/features/inspiration/presentation/widgets/chat/chat_flow_input_bar.dart index 5f29a0b2..c04646c1 100644 --- a/lib/features/inspiration/presentation/widgets/chat/chat_flow_input_bar.dart +++ b/lib/features/inspiration/presentation/widgets/chat/chat_flow_input_bar.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 会话流输入栏组件 // 创建时间: 2026-05-15 // 更新时间: 2026-05-15 @@ -26,6 +26,7 @@ import 'package:xianyan/features/inspiration/presentation/widgets/chat_input/rec import 'package:xianyan/features/inspiration/providers/chat_provider.dart'; import 'package:xianyan/features/inspiration/providers/chat_attachment_provider.dart'; import 'package:xianyan/features/inspiration/services/chat_file_service.dart'; +import 'package:xianyan/shared/widgets/keyboard_safe_sheet.dart'; class ChatFlowInputBar extends ConsumerStatefulWidget { const ChatFlowInputBar({ @@ -124,7 +125,11 @@ class _ChatFlowInputBarState extends ConsumerState { child: CupertinoTextField( controller: widget.inputController, focusNode: widget.focusNode, - onTap: () {}, + onTap: () { + KeyboardManager.instance.markUserTapped( + 'chat_flow_${widget.conversationId}', + ); + }, placeholder: widget.isReadlater ? '添加链接/文字...' : '说点什么...', placeholderStyle: AppTypography.body.copyWith( color: ext.textHint, @@ -152,11 +157,7 @@ class _ChatFlowInputBarState extends ConsumerState { context, onSend: (plainText, deltaJson) { ref - .read( - chatMessagesProvider( - widget.conversationId, - ).notifier, - ) + .read(chatMessagesProvider.notifier) .sendRichTextMessage( plainText, richContent: deltaJson, @@ -229,9 +230,7 @@ class _ChatFlowInputBarState extends ConsumerState { RecordAudioSheet.show( context, onComplete: (path) { - ref - .read(chatMessagesProvider(widget.conversationId).notifier) - .sendAudio(path); + ref.read(chatMessagesProvider.notifier).sendAudio(path); }, ); }, diff --git a/lib/features/inspiration/presentation/widgets/chat/chat_flow_message_list.dart b/lib/features/inspiration/presentation/widgets/chat/chat_flow_message_list.dart index f76a0d8f..ac08b7b4 100644 --- a/lib/features/inspiration/presentation/widgets/chat/chat_flow_message_list.dart +++ b/lib/features/inspiration/presentation/widgets/chat/chat_flow_message_list.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 会话流消息列表组件 // 创建时间: 2026-05-15 // 更新时间: 2026-05-15 @@ -39,7 +39,7 @@ class ChatFlowMessageList extends ConsumerStatefulWidget { class _ChatFlowMessageListState extends ConsumerState { @override Widget build(BuildContext context) { - final chatState = ref.watch(chatMessagesProvider(widget.conversationId)); + final chatState = ref.watch(chatMessagesProvider); final messages = chatState.filteredMessages; if (chatState.isLoading) { @@ -60,7 +60,7 @@ class _ChatFlowMessageListState extends ConsumerState { widget.scrollController.position.pixels >= widget.scrollController.position.maxScrollExtent - 100) { ref - .read(chatMessagesProvider(widget.conversationId).notifier) + .read(chatMessagesProvider.notifier) .loadMore(); } return false; @@ -85,13 +85,13 @@ class _ChatFlowMessageListState extends ConsumerState { onMake: () {}, onShare: () {}, onDelete: () => ref - .read(chatMessagesProvider(widget.conversationId).notifier) + .read(chatMessagesProvider.notifier) .deleteMessage(msg.id), onMarkRead: () => ref - .read(chatMessagesProvider(widget.conversationId).notifier) + .read(chatMessagesProvider.notifier) .markRead(msg.id), onEdit: (newText) => ref - .read(chatMessagesProvider(widget.conversationId).notifier) + .read(chatMessagesProvider.notifier) .editMessage(msg.id, newText), onReply: () => widget.onReplyTo(msg), replyMessage: _findMessage(msg.replyToId, messages), diff --git a/lib/features/inspiration/presentation/widgets/chat/chat_flow_readlater_mixin.dart b/lib/features/inspiration/presentation/widgets/chat/chat_flow_readlater_mixin.dart index f0fb9492..194817c7 100644 --- a/lib/features/inspiration/presentation/widgets/chat/chat_flow_readlater_mixin.dart +++ b/lib/features/inspiration/presentation/widgets/chat/chat_flow_readlater_mixin.dart @@ -1,7 +1,7 @@ // ============================================================ // 闲言APP — 会话流稍后读设置/导出辅助类 // 创建时间: 2026-05-15 -// 更新时间: 2026-05-15 +// 更新时间: 2026-05-16 // 作用: 稍后读会话的设置面板、多格式导出、标签/文件夹/同步/AI/协作/设备同步集成 // 上次更新: v5.8.0 集成8个空壳服务到UI,修复exportAsZip同步I/O // ============================================================ @@ -44,7 +44,7 @@ class ChatFlowReadlaterHelper { String conversationId, ) { final ext = AppTheme.ext(context); - final chatState = ref.read(chatMessagesProvider(conversationId)); + final chatState = ref.read(chatMessagesProvider); final totalCount = chatState.messages.length; final unreadCount = chatState.unreadCount; final readCount = totalCount - unreadCount; @@ -119,9 +119,7 @@ class ChatFlowReadlaterHelper { CupertinoActionSheetAction( onPressed: () { Navigator.pop(ctx); - ref - .read(chatMessagesProvider(conversationId).notifier) - .markAllAsRead(); + ref.read(chatMessagesProvider.notifier).markAllAsRead(); AppToast.showSuccess('已全部标记为已读'); }, child: const Text('✅ 标记全部已读'), @@ -152,7 +150,7 @@ class ChatFlowReadlaterHelper { onPressed: () { Navigator.pop(dCtx); ref - .read(chatMessagesProvider(conversationId).notifier) + .read(chatMessagesProvider.notifier) .clearAllMessages(); AppToast.showSuccess('已清空稍后读'); }, @@ -186,7 +184,7 @@ class ChatFlowReadlaterHelper { WidgetRef ref, String conversationId, ) { - final chatState = ref.read(chatMessagesProvider(conversationId)); + final chatState = ref.read(chatMessagesProvider); final allTags = ReadlaterTagService.getAllTags(); final tagStats = ReadlaterTagService.getTagStats(); @@ -254,6 +252,7 @@ class ChatFlowReadlaterHelper { for (final id in ids) { await ReadlaterTagService.removeTag(id, tag); } + if (!ctx.mounted) return; Navigator.pop(ctx); showTagManager(context, ref, conversationId); AppToast.showSuccess('标签 "$tag" 已删除'); @@ -312,7 +311,7 @@ class ChatFlowReadlaterHelper { final tag = controller.text.trim(); if (tag.isEmpty) return; Navigator.pop(dCtx); - final chatState = ref.read(chatMessagesProvider(conversationId)); + final chatState = ref.read(chatMessagesProvider); if (chatState.messages.isNotEmpty) { await ReadlaterTagService.addTag( chatState.messages.first.id, @@ -334,7 +333,7 @@ class ChatFlowReadlaterHelper { String conversationId, String tag, ) { - final chatState = ref.read(chatMessagesProvider(conversationId)); + final chatState = ref.read(chatMessagesProvider); final filtered = ReadlaterTagService.getMessagesByTag( tag, chatState.messages, @@ -433,6 +432,7 @@ class ChatFlowReadlaterHelper { folder.id, name, ); + if (!ctx.mounted) return; Navigator.pop(ctx); showFolderManager(context, ref, conversationId); } @@ -441,6 +441,7 @@ class ChatFlowReadlaterHelper { await ReadlaterFolderService.instance.deleteFolder( folder.id, ); + if (!ctx.mounted) return; Navigator.pop(ctx); showFolderManager(context, ref, conversationId); AppToast.showSuccess('文件夹 "${folder.name}" 已删除'); @@ -544,7 +545,7 @@ class ChatFlowReadlaterHelper { ) async { AppToast.show('☁️ 正在同步...'); try { - final chatState = ref.read(chatMessagesProvider(conversationId)); + final chatState = ref.read(chatMessagesProvider); final result = await ReadlaterSyncService.instance.fullSync( chatState.messages, ); @@ -565,7 +566,7 @@ class ChatFlowReadlaterHelper { WidgetRef ref, String conversationId, ) async { - final chatState = ref.read(chatMessagesProvider(conversationId)); + final chatState = ref.read(chatMessagesProvider); if (chatState.messages.isEmpty) { AppToast.showInfo('没有内容可生成摘要'); return; @@ -577,6 +578,7 @@ class ChatFlowReadlaterHelper { chatState.messages, ); if (summary != null && summary.isNotEmpty) { + if (!context.mounted) return; showCupertinoDialog( context: context, builder: (dCtx) => CupertinoAlertDialog( @@ -838,7 +840,7 @@ class ChatFlowReadlaterHelper { String conversationId, ) async { try { - final chatState = ref.read(chatMessagesProvider(conversationId)); + final chatState = ref.read(chatMessagesProvider); final unreadCount = chatState.messages.where((m) => !m.isRead).length; final previewMsg = chatState.messages.isNotEmpty ? chatState.messages.first @@ -1026,6 +1028,7 @@ class ChatFlowReadlaterHelper { message, ); if (summary != null && summary.isNotEmpty) { + if (!context.mounted) return; showCupertinoDialog( context: context, builder: (dCtx) => CupertinoAlertDialog( @@ -1070,6 +1073,7 @@ class ChatFlowReadlaterHelper { return; } + if (!context.mounted) return; showCupertinoDialog( context: context, builder: (dCtx) => CupertinoAlertDialog( @@ -1177,14 +1181,14 @@ class ChatFlowReadlaterHelper { } static void exportAsJson(WidgetRef ref, String conversationId) { - final chatState = ref.read(chatMessagesProvider(conversationId)); + final chatState = ref.read(chatMessagesProvider); final json = ChatMessage.exportToJson(chatState.messages); Clipboard.setData(ClipboardData(text: json)); AppToast.showSuccess('已导出 ${chatState.messages.length} 条到剪贴板 (JSON)'); } static void exportAsMarkdown(WidgetRef ref, String conversationId) { - final chatState = ref.read(chatMessagesProvider(conversationId)); + final chatState = ref.read(chatMessagesProvider); final sb = StringBuffer(); sb.writeln('# 📖 稍后读导出'); sb.writeln('> 导出时间: ${DateTime.now().toString().substring(0, 19)}'); @@ -1215,7 +1219,7 @@ class ChatFlowReadlaterHelper { static Future exportAsZip(WidgetRef ref, String conversationId) async { try { - final chatState = ref.read(chatMessagesProvider(conversationId)); + final chatState = ref.read(chatMessagesProvider); final dir = await getTemporaryDirectory(); final timestamp = DateTime.now().millisecondsSinceEpoch; @@ -1241,9 +1245,11 @@ class ChatFlowReadlaterHelper { } final zipBytes = ZipEncoder().encode(archive); final zipFile = File('${dir.path}/readlater_$timestamp.zip'); - await zipFile.writeAsBytes(zipBytes!); + await zipFile.writeAsBytes(zipBytes); - await Share.shareXFiles([XFile(zipFile.path)], subject: '闲言APP - 稍后读导出'); + await SharePlus.instance.share( + ShareParams(files: [XFile(zipFile.path)], subject: '闲言APP - 稍后读导出'), + ); AppToast.showSuccess('ZIP导出成功'); } catch (e) { AppToast.showError('导出失败: $e'); diff --git a/lib/features/inspiration/presentation/widgets/chat/chat_flow_top_bar.dart b/lib/features/inspiration/presentation/widgets/chat/chat_flow_top_bar.dart index 460f7865..e4451963 100644 --- a/lib/features/inspiration/presentation/widgets/chat/chat_flow_top_bar.dart +++ b/lib/features/inspiration/presentation/widgets/chat/chat_flow_top_bar.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 会话流顶部区域组件 // 创建时间: 2026-05-15 // 更新时间: 2026-05-15 @@ -47,7 +47,7 @@ class ChatFlowSearchBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final ext = AppTheme.ext(context); - final chatState = ref.watch(chatMessagesProvider(conversationId)); + final chatState = ref.watch(chatMessagesProvider); final resultCount = chatState.filteredMessages.length; final totalCount = chatState.messages.length; final isSearching = chatState.isSearching; @@ -77,7 +77,7 @@ class ChatFlowSearchBar extends ConsumerWidget { onSuffixTap: () { searchController.clear(); ref - .read(chatMessagesProvider(conversationId).notifier) + .read(chatMessagesProvider.notifier) .clearSearch(); }, ), @@ -141,7 +141,7 @@ class ChatFlowCategoryBar extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final ext = AppTheme.ext(context); final selected = ref - .watch(chatMessagesProvider(conversationId)) + .watch(chatMessagesProvider) .selectedCategory; final displayCategories = showAll ? kChatCategories : kChatCategories.take(3).toList(); @@ -165,7 +165,7 @@ class ChatFlowCategoryBar extends ConsumerWidget { return GestureDetector( onTap: () { ref - .read(chatMessagesProvider(conversationId).notifier) + .read(chatMessagesProvider.notifier) .selectCategory(entry.key == 'all' ? null : entry.key); }, child: Container( diff --git a/lib/features/inspiration/presentation/widgets/chat_bubble/chat_bubble.dart b/lib/features/inspiration/presentation/widgets/chat_bubble/chat_bubble.dart index 0a5f0ac5..0661007a 100644 --- a/lib/features/inspiration/presentation/widgets/chat_bubble/chat_bubble.dart +++ b/lib/features/inspiration/presentation/widgets/chat_bubble/chat_bubble.dart @@ -214,7 +214,7 @@ class _ChatBubbleState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Tilt( + Tilt.base( borderRadius: BorderRadius.circular(18), tiltConfig: const TiltConfig( angle: 3, diff --git a/lib/features/inspiration/presentation/widgets/chat_bubble/chat_sentence_card_bubble.dart b/lib/features/inspiration/presentation/widgets/chat_bubble/chat_sentence_card_bubble.dart index af6842df..168caaa2 100644 --- a/lib/features/inspiration/presentation/widgets/chat_bubble/chat_sentence_card_bubble.dart +++ b/lib/features/inspiration/presentation/widgets/chat_bubble/chat_sentence_card_bubble.dart @@ -337,10 +337,12 @@ class ChatSentenceCardBubble extends StatelessWidget { ? box.localToGlobal(Offset.zero) & box.size : null; - await Share.shareXFiles( - [XFile(file.path, mimeType: 'image/png')], - text: '${message.text}${message.author != null ? "\n—— ${message.author!}" : ""}\n\n来自「闲言」', - sharePositionOrigin: origin, + await SharePlus.instance.share( + ShareParams( + files: [XFile(file.path, mimeType: 'image/png')], + text: '${message.text}${message.author != null ? "\n—— ${message.author!}" : ""}\n\n来自「闲言」', + sharePositionOrigin: origin, + ), ); Log.i('句子卡片截图分享成功: $fileName'); diff --git a/lib/features/inspiration/providers/chat_attachment_provider.dart b/lib/features/inspiration/providers/chat_attachment_provider.dart index 8519a3e2..89c82571 100644 --- a/lib/features/inspiration/providers/chat_attachment_provider.dart +++ b/lib/features/inspiration/providers/chat_attachment_provider.dart @@ -78,9 +78,10 @@ class ChatAttachmentState { } } -class ChatAttachmentNotifier extends StateNotifier { - ChatAttachmentNotifier(this.conversationId) - : super(const ChatAttachmentState()) { +class ChatAttachmentNotifier extends Notifier { + @override + ChatAttachmentState build() => const ChatAttachmentState(); + ChatAttachmentNotifier(this.conversationId) { _init(); } @@ -399,8 +400,8 @@ class ChatAttachmentNotifier extends StateNotifier { } final chatAttachmentProvider = - StateNotifierProvider.family< + NotifierProvider.family< ChatAttachmentNotifier, ChatAttachmentState, String - >((ref, conversationId) => ChatAttachmentNotifier(conversationId)); + >((conversationId) => ChatAttachmentNotifier(conversationId)); diff --git a/lib/features/inspiration/providers/chat_conversation_provider.dart b/lib/features/inspiration/providers/chat_conversation_provider.dart index e065d2ee..fa98a329 100644 --- a/lib/features/inspiration/providers/chat_conversation_provider.dart +++ b/lib/features/inspiration/providers/chat_conversation_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 聊天会话Provider /// 创建时间: 2026-05-08 /// 更新时间: 2026-05-08 @@ -66,8 +66,10 @@ class ChatConversationState { } } -class ChatConversationNotifier extends StateNotifier { - ChatConversationNotifier() : super(const ChatConversationState()) { +class ChatConversationNotifier extends Notifier { + @override + ChatConversationState build() => const ChatConversationState(); + ChatConversationNotifier() { _init(); } @@ -191,6 +193,6 @@ class ChatConversationNotifier extends StateNotifier { } final chatConversationProvider = - StateNotifierProvider( - (ref) => ChatConversationNotifier(), + NotifierProvider( + ChatConversationNotifier.new, ); diff --git a/lib/features/inspiration/providers/chat_provider.dart b/lib/features/inspiration/providers/chat_provider.dart index b15ec3b6..01df2fb7 100644 --- a/lib/features/inspiration/providers/chat_provider.dart +++ b/lib/features/inspiration/providers/chat_provider.dart @@ -6,6 +6,7 @@ // 上次更新: v5.3.0 新增searchQuery+searchMessages全文搜索 // ============================================================ +import 'dart:async'; import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -105,12 +106,16 @@ class ChatState { } } -class ChatNotifier extends StateNotifier { - ChatNotifier(this.conversationId) : super(const ChatState()) { - _init(); +class ChatNotifier extends Notifier { + @override + ChatState build() { + Future.microtask(_init); + return const ChatState(); } - final String conversationId; + ChatNotifier(); + + String get conversationId => 'default'; static const _triggeredKey = 'chat_triggered_today'; static const _dateKey = 'chat_today_date'; @@ -188,6 +193,8 @@ class ChatNotifier extends StateNotifier { } void _addInitialSentencesIfNeeded() { + if (conversationId == 'readlater') return; + final hasSentences = state.messages.any( (m) => m.type == ChatMessageType.sentence, ); @@ -736,12 +743,19 @@ class ChatNotifier extends StateNotifier { } /// 按会话ID创建的family provider -final chatMessagesProvider = - StateNotifierProvider.family( - (ref, conversationId) => ChatNotifier(conversationId), - ); - -/// 默认会话的快捷provider(向后兼容) -final chatProvider = StateNotifierProvider( - (ref) => ChatNotifier('default'), +final chatMessagesProvider = NotifierProvider( + ChatNotifier.new, ); + +final chatProvider = NotifierProvider( + ChatNotifier.new, +); + +/// 稍后读会话刷新触发器 — 全局事件总线 +final _readlaterRefreshController = StreamController.broadcast(); + +void notifyReadlaterRefresh() { + _readlaterRefreshController.add(null); +} + +Stream get readlaterRefreshStream => _readlaterRefreshController.stream; diff --git a/lib/features/inspiration/providers/chat_session_provider.dart b/lib/features/inspiration/providers/chat_session_provider.dart index 17ddbc0d..7196bb35 100644 --- a/lib/features/inspiration/providers/chat_session_provider.dart +++ b/lib/features/inspiration/providers/chat_session_provider.dart @@ -98,8 +98,10 @@ class ChatSessionState { } } -class ChatSessionNotifier extends StateNotifier { - ChatSessionNotifier() : super(const ChatSessionState()) { +class ChatSessionNotifier extends Notifier { + @override + ChatSessionState build() => const ChatSessionState(); + ChatSessionNotifier() { _init(); } @@ -526,10 +528,4 @@ class ChatSessionNotifier extends StateNotifier { } final chatSessionProvider = - StateNotifierProvider((ref) { - final notifier = ChatSessionNotifier(); - ref.listen(chatProvider, (_, next) { - notifier.refreshFromChat(next); - }); - return notifier; - }); + NotifierProvider(ChatSessionNotifier.new); diff --git a/lib/features/inspiration/providers/inspiration_provider.dart b/lib/features/inspiration/providers/inspiration_provider.dart index e349dc63..9de7f6f4 100644 --- a/lib/features/inspiration/providers/inspiration_provider.dart +++ b/lib/features/inspiration/providers/inspiration_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 发现页面 Provider /// 创建时间: 2026-04-20 /// 更新时间: 2026-04-29 @@ -184,8 +184,10 @@ const _allSentences = [ ]; /// 发现页面 Notifier -class InspirationNotifier extends StateNotifier { - InspirationNotifier() : super(const InspirationState()) { +class InspirationNotifier extends Notifier { + @override + InspirationState build() => const InspirationState(); + InspirationNotifier() { _loadSentences(); } @@ -229,6 +231,6 @@ class InspirationNotifier extends StateNotifier { /// 发现页面 Provider final inspirationProvider = - StateNotifierProvider( - (ref) => InspirationNotifier(), + NotifierProvider( + InspirationNotifier.new, ); diff --git a/lib/features/inspiration/providers/tool_center_provider.dart b/lib/features/inspiration/providers/tool_center_provider.dart index 2cffb92b..1af586cf 100644 --- a/lib/features/inspiration/providers/tool_center_provider.dart +++ b/lib/features/inspiration/providers/tool_center_provider.dart @@ -130,8 +130,13 @@ class ToolCenterState { } /// 工具中心 Notifier -class ToolCenterNotifier extends StateNotifier { - ToolCenterNotifier() : super(const ToolCenterState()) { +class ToolCenterNotifier extends Notifier { + @override + ToolCenterState build() { + ref.onDispose(_onDispose); + return const ToolCenterState(); + } + ToolCenterNotifier() { _loadPersistedData(); _initConnectivity(); } @@ -181,11 +186,9 @@ class ToolCenterNotifier extends StateNotifier { }); } - @override - void dispose() { + void _onDispose() { _connectivityStream = null; - super.dispose(); - } + } /// 切换面板开关 void togglePanel() { @@ -454,6 +457,6 @@ class ToolCenterNotifier extends StateNotifier { /// 工具中心 Provider final toolCenterProvider = - StateNotifierProvider( - (ref) => ToolCenterNotifier(), + NotifierProvider( + ToolCenterNotifier.new, ); diff --git a/lib/features/inspiration/services/ip_query_service.dart b/lib/features/inspiration/services/ip_query_service.dart index 5fa65fc7..9cc7e647 100644 --- a/lib/features/inspiration/services/ip_query_service.dart +++ b/lib/features/inspiration/services/ip_query_service.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — IP地址查询服务 /// 创建时间: 2026-05-08 /// 更新时间: 2026-05-08 @@ -6,6 +6,7 @@ /// 上次更新: 新增ipText/ipDetailJson字段支持气泡小字显示 /// ============================================================ +import 'package:xianyan/core/utils/pattern_utils.dart'; import 'package:drift/drift.dart' show Value; import 'package:dio/dio.dart'; @@ -97,10 +98,10 @@ class IpQueryService { /// 验证IP地址格式 static bool _isValidIp(String ip) { - final regex = RegExp( + final ipRegex = regex( r'^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$', ); - return regex.hasMatch(ip); + return ipRegex.hasMatch(ip); } } diff --git a/lib/features/knowledge_graph/providers/knowledge_graph_provider.dart b/lib/features/knowledge_graph/providers/knowledge_graph_provider.dart index d1ade601..e0ae6780 100644 --- a/lib/features/knowledge_graph/providers/knowledge_graph_provider.dart +++ b/lib/features/knowledge_graph/providers/knowledge_graph_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 知识图谱状态管理 /// 创建时间: 2026-05-02 /// 更新时间: 2026-05-02 @@ -12,8 +12,10 @@ import '../../../core/utils/logger.dart'; import '../models/knowledge_graph_models.dart'; import '../services/knowledge_graph_service.dart'; -class KnowledgeGraphNotifier extends StateNotifier { - KnowledgeGraphNotifier() : super(const KnowledgeGraphState()); +class KnowledgeGraphNotifier extends Notifier { + @override + KnowledgeGraphState build() => const KnowledgeGraphState(); + KnowledgeGraphNotifier(); Future searchAndBuild(String keyword) async { if (keyword.trim().isEmpty) return; @@ -139,6 +141,6 @@ class KnowledgeGraphNotifier extends StateNotifier { } final knowledgeGraphProvider = - StateNotifierProvider( - (ref) => KnowledgeGraphNotifier(), + NotifierProvider( + KnowledgeGraphNotifier.new, ); diff --git a/lib/features/note/presentation/note_list_page.dart b/lib/features/note/presentation/note_list_page.dart index 603cd5aa..38dd48b5 100644 --- a/lib/features/note/presentation/note_list_page.dart +++ b/lib/features/note/presentation/note_list_page.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 笔记列表页面 /// 创建时间: 2026-04-28 /// 更新时间: 2026-05-01 @@ -6,6 +6,7 @@ /// 上次更新: 重构为3种布局(list/grid/timeline) + AppBar样式切换按钮 /// ============================================================ +import 'package:xianyan/core/utils/pattern_utils.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -803,7 +804,7 @@ class _NoteListPageState extends ConsumerState { final trimmed = line.trim(); if (trimmed.isEmpty) continue; final isDone = trimmed.startsWith('- [x]'); - final text = trimmed.replaceFirst(RegExp(r'- \[[ xX]\]\s*'), '').trim(); + final text = trimmed.replaceFirst(regex(r'- \[[ xX]\]\s*'), '').trim(); if (text.isNotEmpty) items.add(MapEntry(isDone, text)); if (items.length >= maxItems) break; } diff --git a/lib/features/note/providers/note_provider.dart b/lib/features/note/providers/note_provider.dart index a730d9e0..cd042f71 100644 --- a/lib/features/note/providers/note_provider.dart +++ b/lib/features/note/providers/note_provider.dart @@ -47,8 +47,10 @@ class NoteListState { } } -class NoteListNotifier extends StateNotifier { - NoteListNotifier() : super(const NoteListState()); +class NoteListNotifier extends Notifier { + @override + NoteListState build() => const NoteListState(); + NoteListNotifier(); Future loadNotes({ bool refresh = false, @@ -202,8 +204,4 @@ class NoteListNotifier extends StateNotifier { } } -final noteListProvider = StateNotifierProvider( - (ref) { - return NoteListNotifier(); - }, -); +final noteListProvider = NotifierProvider(NoteListNotifier.new); diff --git a/lib/features/poetry/providers/poetry_provider.dart b/lib/features/poetry/providers/poetry_provider.dart index 34f5f727..8a8345e3 100644 --- a/lib/features/poetry/providers/poetry_provider.dart +++ b/lib/features/poetry/providers/poetry_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 今日诗词状态管理 /// 创建时间: 2026-05-02 /// 更新时间: 2026-05-02 @@ -40,8 +40,10 @@ class PoetryState { } } -class PoetryNotifier extends StateNotifier { - PoetryNotifier() : super(const PoetryState()) { +class PoetryNotifier extends Notifier { + @override + PoetryState build() => const PoetryState(); + PoetryNotifier() { loadPoetry(); } @@ -70,6 +72,6 @@ class PoetryNotifier extends StateNotifier { } } -final poetryProvider = StateNotifierProvider( - (ref) => PoetryNotifier(), +final poetryProvider = NotifierProvider( + PoetryNotifier.new, ); diff --git a/lib/features/pomodoro/providers/pomodoro_provider.dart b/lib/features/pomodoro/providers/pomodoro_provider.dart index 31a2bbbb..d094846a 100644 --- a/lib/features/pomodoro/providers/pomodoro_provider.dart +++ b/lib/features/pomodoro/providers/pomodoro_provider.dart @@ -68,8 +68,13 @@ class PomodoroState { } } -class PomodoroNotifier extends StateNotifier { - PomodoroNotifier() : super(const PomodoroState()) { +class PomodoroNotifier extends Notifier { + @override + PomodoroState build() { + ref.onDispose(_onDispose); + return const PomodoroState(); + } + PomodoroNotifier() { _loadRecords(); } @@ -280,13 +285,11 @@ class PomodoroNotifier extends StateNotifier { } } - @override - void dispose() { + void _onDispose() { _timer?.cancel(); - super.dispose(); - } + } } -final pomodoroProvider = StateNotifierProvider( - (ref) => PomodoroNotifier(), +final pomodoroProvider = NotifierProvider( + PomodoroNotifier.new, ); diff --git a/lib/features/progress/providers/progress_provider.dart b/lib/features/progress/providers/progress_provider.dart index 58cfdd4e..c7b70df0 100644 --- a/lib/features/progress/providers/progress_provider.dart +++ b/lib/features/progress/providers/progress_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 进度页面状态管理 /// 创建时间: 2026-05-02 /// 更新时间: 2026-05-02 @@ -69,8 +69,10 @@ class ProgressState { } } -class ProgressNotifier extends StateNotifier { - ProgressNotifier() : super(const ProgressState()) { +class ProgressNotifier extends Notifier { + @override + ProgressState build() => const ProgressState(); + ProgressNotifier() { _init(); } @@ -356,6 +358,6 @@ class ProgressNotifier extends StateNotifier { } } -final progressProvider = StateNotifierProvider( - (ref) => ProgressNotifier(), +final progressProvider = NotifierProvider( + ProgressNotifier.new, ); diff --git a/lib/features/rank/providers/rank_provider.dart b/lib/features/rank/providers/rank_provider.dart index e7f81ec1..dbb12227 100644 --- a/lib/features/rank/providers/rank_provider.dart +++ b/lib/features/rank/providers/rank_provider.dart @@ -1,4 +1,4 @@ -/// @name 赛季排行榜状态管理 +/// @name 赛季排行榜状态管理 /// @date 2026-05-14 /// @desc Riverpod状态管理: 赛季列表/排行榜/我的排名 /// @update v13.0.0 初始版本 @@ -65,7 +65,8 @@ class RankItem { avatar: (json['avatar'] ?? '') as String, level: (json['level'] ?? 1) as int, value: (json['value'] ?? 0) as int, - rewardClaimed: json['reward_claimed'] == 1 || json['reward_claimed'] == true, + rewardClaimed: + json['reward_claimed'] == 1 || json['reward_claimed'] == true, ); } } @@ -114,17 +115,20 @@ class RankState { } } -class RankNotifier extends StateNotifier { - final RankService _rankService; +class RankNotifier extends Notifier { + @override + RankState build() => const RankState(); - RankNotifier(this._rankService) : super(const RankState()); + RankService get _rankService => ref.read(rankServiceProvider); Future loadSeasons() async { try { final response = await _rankService.getSeasons(); final data = (response['data'] ?? response) as Map; final List seasonList = data['seasons'] as List? ?? []; - final seasons = seasonList.map((e) => RankSeason.fromJson(e as Map)).toList(); + final seasons = seasonList + .map((e) => RankSeason.fromJson(e as Map)) + .toList(); state = state.copyWith(seasons: seasons); } catch (e) { state = state.copyWith(error: e.toString()); @@ -134,13 +138,24 @@ class RankNotifier extends StateNotifier { Future loadLeaderboard({int? seasonId, String type = 'exp'}) async { state = state.copyWith(isLoading: true, rankType: type); try { - final response = await _rankService.getLeaderboard(seasonId: seasonId, type: type); + final response = await _rankService.getLeaderboard( + seasonId: seasonId, + type: type, + ); final data = (response['data'] ?? response) as Map; final List list = data['list'] as List? ?? []; - final items = list.map((e) => RankItem.fromJson(e as Map)).toList(); + final items = list + .map((e) => RankItem.fromJson(e as Map)) + .toList(); final seasonData = data['season'] as Map?; - final season = seasonData != null ? RankSeason.fromJson(seasonData) : null; - state = state.copyWith(leaderboard: items, currentSeason: season, isLoading: false); + final season = seasonData != null + ? RankSeason.fromJson(seasonData) + : null; + state = state.copyWith( + leaderboard: items, + currentSeason: season, + isLoading: false, + ); } catch (e) { state = state.copyWith(isLoading: false, error: e.toString()); } @@ -148,7 +163,10 @@ class RankNotifier extends StateNotifier { Future loadMyRank({int? seasonId, String type = 'exp'}) async { try { - final response = await _rankService.getMyRank(seasonId: seasonId, type: type); + final response = await _rankService.getMyRank( + seasonId: seasonId, + type: type, + ); final data = (response['data'] ?? response) as Map; state = state.copyWith( myRank: (data['rank'] ?? 0) as int, @@ -159,9 +177,15 @@ class RankNotifier extends StateNotifier { } } - Future?> claimReward({required int seasonId, required String type}) async { + Future?> claimReward({ + required int seasonId, + required String type, + }) async { try { - final response = await _rankService.claimReward(seasonId: seasonId, type: type); + final response = await _rankService.claimReward( + seasonId: seasonId, + type: type, + ); return (response['data'] ?? response) as Map?; } catch (e) { return null; @@ -169,6 +193,6 @@ class RankNotifier extends StateNotifier { } } -final rankProvider = StateNotifierProvider((ref) { - return RankNotifier(ref.read(rankServiceProvider)); -}); +final rankProvider = NotifierProvider( + RankNotifier.new, +); diff --git a/lib/features/reading_report/providers/reading_report_provider.dart b/lib/features/reading_report/providers/reading_report_provider.dart index 55a9d38c..f4afaab1 100644 --- a/lib/features/reading_report/providers/reading_report_provider.dart +++ b/lib/features/reading_report/providers/reading_report_provider.dart @@ -30,17 +30,22 @@ class ReadingReportState { ReportPeriod? currentPeriod, bool? isLoading, String? error, - }) => - ReadingReportState( - report: report ?? this.report, - currentPeriod: currentPeriod ?? this.currentPeriod, - isLoading: isLoading ?? this.isLoading, - error: error, - ); + }) => ReadingReportState( + report: report ?? this.report, + currentPeriod: currentPeriod ?? this.currentPeriod, + isLoading: isLoading ?? this.isLoading, + error: error, + ); } -class ReadingReportNotifier extends StateNotifier { - ReadingReportNotifier() : super(const ReadingReportState()); +class ReadingReportNotifier extends Notifier { + @override + ReadingReportState build() { + Future.microtask(loadReport); + return const ReadingReportState(); + } + + ReadingReportNotifier(); Future loadReport({ReportPeriod? period}) async { final p = period ?? state.currentPeriod; @@ -62,6 +67,6 @@ class ReadingReportNotifier extends StateNotifier { } final readingReportProvider = - StateNotifierProvider( - (ref) => ReadingReportNotifier()..loadReport(), -); + NotifierProvider( + ReadingReportNotifier.new, + ); diff --git a/lib/features/search/providers/search_provider.dart b/lib/features/search/providers/search_provider.dart index 85fbba56..7bc6dfea 100644 --- a/lib/features/search/providers/search_provider.dart +++ b/lib/features/search/providers/search_provider.dart @@ -147,8 +147,13 @@ class SearchState { } } -class SearchNotifier extends StateNotifier { - SearchNotifier() : super(const SearchState()) { +class SearchNotifier extends Notifier { + @override + SearchState build() { + ref.onDispose(_onDispose); + return const SearchState(); + } + SearchNotifier() { _init(); } @@ -162,11 +167,9 @@ class SearchNotifier extends StateNotifier { _networkSub = ConnectivityService.onTypeChange.listen(_onNetworkChanged); } - @override - void dispose() { + void _onDispose() { _networkSub?.cancel(); - super.dispose(); - } + } void _onNetworkChanged(NetworkType type) { if (type != NetworkType.none) { @@ -717,6 +720,6 @@ class SearchNotifier extends StateNotifier { } } -final searchProvider = StateNotifierProvider( - (ref) => SearchNotifier(), +final searchProvider = NotifierProvider( + SearchNotifier.new, ); diff --git a/lib/features/settings/presentation/data_management_page.dart b/lib/features/settings/presentation/data_management_page.dart index c39d8794..5a9b4c3b 100644 --- a/lib/features/settings/presentation/data_management_page.dart +++ b/lib/features/settings/presentation/data_management_page.dart @@ -1256,7 +1256,7 @@ class _DataManagementPageState extends ConsumerState { ); final zipBytes = ZipEncoder().encode(archive); - if (zipBytes == null) { + if (zipBytes.isEmpty) { AppToast.showError('导出失败'); return; } @@ -1267,7 +1267,9 @@ class _DataManagementPageState extends ConsumerState { await file.writeAsBytes(zipBytes); if (mounted) { - await share_plus.Share.shareXFiles([XFile(file.path)]); + await share_plus.SharePlus.instance.share( + share_plus.ShareParams(files: [XFile(file.path)]), + ); } AppToast.showSuccess('数据已导出'); } catch (e) { diff --git a/lib/features/settings/presentation/font_management_page.dart b/lib/features/settings/presentation/font_management_page.dart index 72f23b5f..7444403a 100644 --- a/lib/features/settings/presentation/font_management_page.dart +++ b/lib/features/settings/presentation/font_management_page.dart @@ -14,14 +14,13 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; import 'package:path_provider/path_provider.dart'; import '../../../core/storage/app_kv_store.dart'; -import '../../../core/theme/app_theme.dart'; -import '../../../core/theme/app_spacing.dart'; -import '../../../core/theme/app_typography.dart'; import '../../../core/theme/app_radius.dart'; +import '../../../core/theme/app_spacing.dart'; +import '../../../core/theme/app_theme.dart'; +import '../../../core/theme/app_typography.dart'; import '../../../core/utils/logger.dart'; import '../../../features/settings/providers/theme_settings_provider.dart'; import '../../../shared/widgets/glass_container.dart'; @@ -164,13 +163,13 @@ const _onlineFontData = [ ]; /// 字体管理 Notifier -class FontManagementNotifier extends StateNotifier { - FontManagementNotifier(this._ref) : super(const FontManagementState()) { - _init(); +class FontManagementNotifier extends Notifier { + @override + FontManagementState build() { + Future.microtask(() => _init()); + return const FontManagementState(); } - final Ref _ref; - static const _kvKeyActiveFont = 'font_active_family'; static const _kvKeyInstalledFonts = 'font_installed_list'; @@ -679,7 +678,7 @@ class FontManagementNotifier extends StateNotifier { 'Nunito': 'rounded', }; final id = idMap[fontStyleId] ?? 'system'; - _ref.read(themeSettingsProvider.notifier).setFontStyle(id); + ref.read(themeSettingsProvider.notifier).setFontStyle(id); } else { final fontInfo = state.fonts.firstWhere( (f) => f.fontFamily == fontFamily, @@ -702,7 +701,7 @@ class FontManagementNotifier extends StateNotifier { void _applyCustomFont(String fontFamily) { try { - _ref.read(themeSettingsProvider.notifier).setFontStyle('system'); + ref.read(themeSettingsProvider.notifier).setFontStyle('system'); Log.i('自定义字体已应用: $fontFamily (通过 fontFamily override)'); } catch (e) { Log.e('自定义字体应用失败', e); @@ -721,8 +720,8 @@ class FontManagementNotifier extends StateNotifier { } final fontManagementProvider = - StateNotifierProvider( - (ref) => FontManagementNotifier(ref), + NotifierProvider( + FontManagementNotifier.new, ); /// 字体管理页面 @@ -744,7 +743,7 @@ class FontManagementPage extends ConsumerWidget { border: null, leading: CupertinoButton( padding: EdgeInsets.zero, - onPressed: () => context.pop(), + onPressed: () => Navigator.of(context).pop(), child: Icon(CupertinoIcons.back, color: ext.textPrimary), ), ), diff --git a/lib/features/settings/presentation/notification_settings_page.dart b/lib/features/settings/presentation/notification_settings_page.dart index 9d85bcc8..f57f209b 100644 --- a/lib/features/settings/presentation/notification_settings_page.dart +++ b/lib/features/settings/presentation/notification_settings_page.dart @@ -86,8 +86,14 @@ class NotificationSettingsState { /// 通知设置 Notifier class NotificationSettingsNotifier - extends StateNotifier { - NotificationSettingsNotifier() : super(const NotificationSettingsState()); + extends Notifier { + @override + NotificationSettingsState build() { + Future.microtask(loadFromPrefs); + return const NotificationSettingsState(); + } + + NotificationSettingsNotifier(); static const _keyStudyProgress = 'notif_study_progress'; static const _keyChargingReadLater = 'notif_charging_readlater'; @@ -239,10 +245,10 @@ class NotificationSettingsNotifier /// 通知设置 Provider final notificationSettingsProvider = - StateNotifierProvider< + NotifierProvider< NotificationSettingsNotifier, NotificationSettingsState - >((ref) => NotificationSettingsNotifier()); + >(NotificationSettingsNotifier.new); // ============================================================ // 通知设置页面 diff --git a/lib/features/settings/presentation/smart_mode_settings_page.dart b/lib/features/settings/presentation/smart_mode_settings_page.dart index a8c215c9..2006f04c 100644 --- a/lib/features/settings/presentation/smart_mode_settings_page.dart +++ b/lib/features/settings/presentation/smart_mode_settings_page.dart @@ -33,7 +33,7 @@ class _SmartModeSettingsPageState extends ConsumerState { final ext = AppTheme.ext(context); final currentMode = ref.watch(browseModeProvider); final isAuto = ref.watch(isAutoModeProvider); - final networkType = ref.watch(networkTypeProvider).valueOrNull ?? + final networkType = ref.watch(networkTypeProvider).value ?? ConnectivityService.currentType; return CupertinoPageScaffold( diff --git a/lib/features/settings/providers/general_settings_provider.dart b/lib/features/settings/providers/general_settings_provider.dart index 530585bd..259e2dd2 100644 --- a/lib/features/settings/providers/general_settings_provider.dart +++ b/lib/features/settings/providers/general_settings_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 通用设置状态管理 /// 创建时间: 2026-04-28 /// 更新时间: 2026-05-08 @@ -587,8 +587,10 @@ class GeneralSettingsState { } /// 通用设置 Notifier -class GeneralSettingsNotifier extends StateNotifier { - GeneralSettingsNotifier() : super(const GeneralSettingsState()) { +class GeneralSettingsNotifier extends Notifier { + @override + GeneralSettingsState build() => const GeneralSettingsState(); + GeneralSettingsNotifier() { _loadFromStorage(); } @@ -1031,8 +1033,8 @@ class GeneralSettingsNotifier extends StateNotifier { /// 通用设置 Provider final generalSettingsProvider = - StateNotifierProvider( - (ref) => GeneralSettingsNotifier(), + NotifierProvider( + GeneralSettingsNotifier.new, ); /// 通知开关状态 Provider — 从 NotificationScheduler (Hive) 实时读取 diff --git a/lib/features/settings/providers/theme_settings_provider.dart b/lib/features/settings/providers/theme_settings_provider.dart index 20d2caae..e2b51560 100644 --- a/lib/features/settings/providers/theme_settings_provider.dart +++ b/lib/features/settings/providers/theme_settings_provider.dart @@ -807,10 +807,13 @@ class ThemeSettingsState { // 主题设置 Notifier // ============================================================ -class ThemeSettingsNotifier extends StateNotifier { - ThemeSettingsNotifier() : super(const ThemeSettingsState()) { +class ThemeSettingsNotifier extends Notifier { + @override + ThemeSettingsState build() { + ref.onDispose(_onDispose); _loadFromStorage(); _startAutoDarkTimer(); + return const ThemeSettingsState(); } static const _keyPrefix = 'theme_'; @@ -1024,10 +1027,8 @@ class ThemeSettingsNotifier extends StateNotifier { AppKVStore.setString('${_keyPrefix}tab_character', 'cat'); } - @override - void dispose() { + void _onDispose() { _autoDarkTimer?.cancel(); - super.dispose(); } } @@ -1036,6 +1037,6 @@ class ThemeSettingsNotifier extends StateNotifier { // ============================================================ final themeSettingsProvider = - StateNotifierProvider( - (ref) => ThemeSettingsNotifier(), + NotifierProvider( + ThemeSettingsNotifier.new, ); diff --git a/lib/features/signin/providers/signin_provider.dart b/lib/features/signin/providers/signin_provider.dart index 976d994b..1d0e1f5e 100644 --- a/lib/features/signin/providers/signin_provider.dart +++ b/lib/features/signin/providers/signin_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 签到状态管理 /// 创建时间: 2026-04-28 /// 更新时间: 2026-05-14 @@ -156,8 +156,10 @@ class SigninState { } } -class SigninNotifier extends StateNotifier { - SigninNotifier() : super(const SigninState()); +class SigninNotifier extends Notifier { + @override + SigninState build() => const SigninState(); + SigninNotifier(); /// 每日签到 Future signin() async { @@ -232,8 +234,6 @@ class SigninNotifier extends StateNotifier { } } -final signinProvider = StateNotifierProvider(( - ref, -) { - return SigninNotifier(); -}); +final signinProvider = NotifierProvider( + SigninNotifier.new, +); diff --git a/lib/features/solar_term/providers/solar_term_provider.dart b/lib/features/solar_term/providers/solar_term_provider.dart index fe4c69d2..90bb3fca 100644 --- a/lib/features/solar_term/providers/solar_term_provider.dart +++ b/lib/features/solar_term/providers/solar_term_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 节气状态管理 /// 创建时间: 2026-05-02 /// 更新时间: 2026-05-02 @@ -48,8 +48,10 @@ class SolarTermState { } } -class SolarTermNotifier extends StateNotifier { - SolarTermNotifier() : super(const SolarTermState()) { +class SolarTermNotifier extends Notifier { + @override + SolarTermState build() => const SolarTermState(); + SolarTermNotifier() { _loadData(); } @@ -81,6 +83,6 @@ class SolarTermNotifier extends StateNotifier { } final solarTermProvider = - StateNotifierProvider( - (ref) => SolarTermNotifier(), + NotifierProvider( + SolarTermNotifier.new, ); diff --git a/lib/features/source/presentation/source_page.dart b/lib/features/source/presentation/source_page.dart index 26452397..07600767 100644 --- a/lib/features/source/presentation/source_page.dart +++ b/lib/features/source/presentation/source_page.dart @@ -69,35 +69,39 @@ class _SourcePageState extends ConsumerState { physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverToBoxAdapter( - child: _buildOverviewCard(ext, state) - .animate() - .fadeIn(duration: 300.ms), + child: _buildOverviewCard( + ext, + state, + ).animate().fadeIn(duration: 300.ms), ), SliverToBoxAdapter( - child: _buildSearchBar(ext) - .animate() - .fadeIn(duration: 300.ms, delay: 50.ms), + child: _buildSearchBar( + ext, + ).animate().fadeIn(duration: 300.ms, delay: 50.ms), ), SliverToBoxAdapter( - child: _buildChannelSection(ext, state) - .animate() - .fadeIn(duration: 300.ms, delay: 100.ms), + child: _buildChannelSection( + ext, + state, + ).animate().fadeIn(duration: 300.ms, delay: 100.ms), ), _buildChannelList(ext, state), SliverToBoxAdapter( - child: _buildDisplaySettings(ext, state) - .animate() - .fadeIn(duration: 300.ms, delay: 150.ms), + child: _buildDisplaySettings( + ext, + state, + ).animate().fadeIn(duration: 300.ms, delay: 150.ms), ), SliverToBoxAdapter( - child: _buildHomeCardSettings(ext, state) - .animate() - .fadeIn(duration: 300.ms, delay: 200.ms), + child: _buildHomeCardSettings( + ext, + state, + ).animate().fadeIn(duration: 300.ms, delay: 200.ms), ), SliverToBoxAdapter( - child: _buildAdvancedSection(ext) - .animate() - .fadeIn(duration: 300.ms, delay: 250.ms), + child: _buildAdvancedSection( + ext, + ).animate().fadeIn(duration: 300.ms, delay: 250.ms), ), const SliverToBoxAdapter( child: SizedBox(height: 120), @@ -206,7 +210,7 @@ class _SourcePageState extends ConsumerState { children: [ _buildStatItem(ext, fmtCount(totalContent), '总内容'), _buildStatDivider(ext), - _buildStatItem(ext, '$channelCount', '频道'), + _buildStatItem(ext, '$channelCount', '已开放频道'), _buildStatDivider(ext), _buildStatItem(ext, fmtViews(totalViews), '总浏览'), ], @@ -408,7 +412,11 @@ class _SourcePageState extends ConsumerState { children: [ Row( children: [ - Icon(CupertinoIcons.slider_horizontal_3, color: ext.accent, size: 18), + Icon( + CupertinoIcons.slider_horizontal_3, + color: ext.accent, + size: 18, + ), const SizedBox(width: AppSpacing.xs), Text( '显示设置', @@ -439,11 +447,17 @@ class _SourcePageState extends ConsumerState { }, children: const { 'newest': Padding( - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), child: Text('最新', style: TextStyle(fontSize: 13)), ), 'hottest': Padding( - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), child: Text('最热', style: TextStyle(fontSize: 13)), ), }, @@ -459,8 +473,12 @@ class _SourcePageState extends ConsumerState { value: state.deduplicateContent, activeTrackColor: ext.accent, onChanged: (val) { - ref.read(sourceProvider.notifier).setDeduplicateContent(val); - ref.read(homeProvider.notifier).setDeduplicateContent(val); + ref + .read(sourceProvider.notifier) + .setDeduplicateContent(val); + ref + .read(homeProvider.notifier) + .setDeduplicateContent(val); }, ), ), @@ -683,7 +701,10 @@ class _SourcePageState extends ConsumerState { child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(CupertinoIcons.checkmark_circle, color: CupertinoColors.activeGreen), + Icon( + CupertinoIcons.checkmark_circle, + color: CupertinoColors.activeGreen, + ), SizedBox(width: 8), Text('全部启用'), ], @@ -697,7 +718,10 @@ class _SourcePageState extends ConsumerState { child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(CupertinoIcons.xmark_circle, color: CupertinoColors.destructiveRed), + Icon( + CupertinoIcons.xmark_circle, + color: CupertinoColors.destructiveRed, + ), SizedBox(width: 8), Text('全部禁用'), ], @@ -749,7 +773,10 @@ class _SourcePageState extends ConsumerState { borderRadius: AppRadius.mdBorder, ), child: Center( - child: Text(channel.icon, style: const TextStyle(fontSize: 26)), + child: Text( + channel.icon, + style: const TextStyle(fontSize: 26), + ), ), ), const SizedBox(width: AppSpacing.md), @@ -841,7 +868,10 @@ class _SourcePageState extends ConsumerState { title: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(CupertinoIcons.arrow_down_doc, color: AppTheme.ext(context).accent), + Icon( + CupertinoIcons.arrow_down_doc, + color: AppTheme.ext(context).accent, + ), const SizedBox(width: 8), const Text('导入句子源'), ], diff --git a/lib/features/source/presentation/source_page.dart.bak b/lib/features/source/presentation/source_page.dart.bak deleted file mode 100644 index 051ed2ee..00000000 --- a/lib/features/source/presentation/source_page.dart.bak +++ /dev/null @@ -1,1502 +0,0 @@ -/// ============================================================ -/// 闲言APP — 句子来源页面 -/// 创建时间: 2026-04-20 -/// 更新时间: 2026-05-01 -/// 作用: 频道管理 — 总览+频道开关+统计+混合规则+首页卡片来源 -/// 上次更新: 搜索框焦点控制+混合规则按钮+底部栏合并+首页卡片来源配置 -/// ============================================================ - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; - -import '../../../core/router/app_router.dart'; -import '../../../core/theme/app_theme.dart'; -import '../../../core/theme/app_spacing.dart'; -import '../../../core/theme/app_typography.dart'; -import '../../../core/theme/app_radius.dart'; -import '../../../core/utils/interaction_animations.dart'; -import '../../../shared/widgets/glass_container.dart'; -import '../../../shared/widgets/bottom_sheet.dart'; -import '../../home/models/feed_model.dart'; -import '../providers/source_provider.dart'; - -/// 混合模式定义 -class _MixModeOption { - const _MixModeOption({ - required this.key, - required this.icon, - required this.name, - required this.desc, - }); - - final String key; - final String icon; - final String name; - final String desc; -} - -const _mixModes = [ - _MixModeOption(key: 'uniform', icon: '🔀', name: '均匀交叉', desc: '各分类轮流出场'), - _MixModeOption(key: 'ratio', icon: '📊', name: '比例混合', desc: '按权重比例分配'), - _MixModeOption(key: 'specific', icon: '🎯', name: '仅指定分类', desc: '只从勾选分类获取'), - _MixModeOption(key: 'random', icon: '🎲', name: '随机混排', desc: '完全随机获取'), - _MixModeOption(key: 'group', icon: '🔄', name: '分组循环', desc: '每分类连续N条后切换'), -]; - -class SourcePage extends ConsumerStatefulWidget { - const SourcePage({super.key}); - - @override - ConsumerState createState() => _SourcePageState(); -} - -class _SourcePageState extends ConsumerState { - final _searchController = TextEditingController(); - final _searchFocusNode = FocusNode(); - - @override - void initState() { - super.initState(); - _searchController.addListener(() => setState(() {})); - } - - @override - void dispose() { - _searchController.dispose(); - _searchFocusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final ext = AppTheme.ext(context); - final state = ref.watch(sourceProvider); - - return CupertinoPageScaffold( - backgroundColor: ext.bgPrimary, - child: SafeArea( - bottom: false, - child: Column( - children: [ - _buildNavBar(ext), - Expanded( - child: state.isLoading - ? const Center(child: CupertinoActivityIndicator()) - : RefreshIndicator( - onRefresh: () => ref.read(sourceProvider.notifier).init(), - child: CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverToBoxAdapter( - child: _buildOverviewCard( - ext, - state, - ).animate().fadeIn(duration: 300.ms), - ), - SliverToBoxAdapter( - child: _buildSearchBar( - ext, - ).animate().fadeIn(duration: 300.ms, delay: 50.ms), - ), - SliverToBoxAdapter( - child: _buildSectionHeader( - ext, - state, - ).animate().fadeIn(duration: 300.ms, delay: 100.ms), - ), - _buildChannelList(ext, state), - SliverToBoxAdapter(child: _buildFooter(ext, state)), - const SliverToBoxAdapter( - child: SizedBox(height: 100), - ), - ], - ), - ), - ), - _buildBottomBar(ext), - ], - ), - ), - ); - } - - Widget _buildNavBar(AppThemeExtension ext) { - final mixConfig = ref.read(sourceProvider).mixConfig; - final modeLabel = _mixModes.firstWhere( - (m) => m.key == mixConfig.mode, - orElse: () => _mixModes[3], - ); - - return Container( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.sm, - ), - decoration: BoxDecoration( - color: ext.bgPrimary.withValues(alpha: 0.9), - border: Border( - bottom: BorderSide( - color: ext.textHint.withValues(alpha: 0.1), - width: 0.5, - ), - ), - ), - child: Row( - children: [ - CupertinoButton( - padding: EdgeInsets.zero, - onPressed: () => context.pop(), - child: Icon(CupertinoIcons.back, color: ext.textPrimary), - ), - Expanded( - child: Text( - '📖 句子来源', - style: AppTypography.title3.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.w700, - ), - textAlign: TextAlign.center, - ), - ), - CupertinoButton( - padding: EdgeInsets.zero, - onPressed: _showMixModeSheet, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(modeLabel.icon, style: const TextStyle(fontSize: 16)), - const SizedBox(width: 2), - Icon(CupertinoIcons.shuffle, color: ext.accent, size: 18), - ], - ), - ), - CupertinoButton( - padding: EdgeInsets.zero, - onPressed: () => ref.read(sourceProvider.notifier).init(), - child: Icon(CupertinoIcons.refresh, color: ext.iconSecondary), - ), - ], - ), - ); - } - - Widget _buildOverviewCard(AppThemeExtension ext, SourceState state) { - final stats = state.stats; - final totalContent = stats?.totalContent ?? 0; - final channelCount = stats?.channelCount ?? state.channels.length; - final totalViews = stats?.totalViews ?? 0; - - return Padding( - padding: const EdgeInsets.fromLTRB( - AppSpacing.md, - AppSpacing.md, - AppSpacing.md, - AppSpacing.sm, - ), - child: GlassContainer( - depth: GlassDepth.elevated, - padding: const EdgeInsets.all(AppSpacing.lg), - margin: EdgeInsets.zero, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Text('📊', style: TextStyle(fontSize: 22)), - const SizedBox(width: AppSpacing.xs), - Text( - '数据总览', - style: AppTypography.subhead.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.w700, - ), - ), - ], - ), - const SizedBox(height: AppSpacing.md), - Row( - children: [ - _buildStatItem(ext, _fmtCount(totalContent), '总内容'), - _buildStatDivider(ext), - _buildStatItem(ext, '$channelCount', '频道'), - _buildStatDivider(ext), - _buildStatItem(ext, _fmtViews(totalViews), '总浏览'), - ], - ), - ], - ), - ), - ); - } - - Widget _buildStatItem(AppThemeExtension ext, String value, String label) { - return Expanded( - child: Column( - children: [ - Text( - value, - style: AppTypography.title2.copyWith( - color: ext.accent, - fontWeight: FontWeight.w800, - letterSpacing: -1, - ), - ), - const SizedBox(height: 2), - Text( - label, - style: AppTypography.caption2.copyWith(color: ext.textSecondary), - ), - ], - ), - ); - } - - Widget _buildStatDivider(AppThemeExtension ext) { - return Container( - width: 1, - height: 32, - color: ext.textHint.withValues(alpha: 0.15), - ); - } - - Widget _buildSearchBar(AppThemeExtension ext) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.sm, - ), - child: Container( - height: 40, - decoration: BoxDecoration( - color: ext.bgSecondary, - borderRadius: AppRadius.mdBorder, - ), - child: Row( - children: [ - const SizedBox(width: AppSpacing.sm), - Icon(CupertinoIcons.search, size: 16, color: ext.textHint), - const SizedBox(width: AppSpacing.xs), - Expanded( - child: CupertinoTextField( - controller: _searchController, - focusNode: _searchFocusNode, - placeholder: '搜索频道...', - placeholderStyle: AppTypography.subhead.copyWith( - color: ext.textHint, - ), - style: AppTypography.body.copyWith(color: ext.textPrimary), - decoration: null, - onChanged: (val) => - ref.read(sourceProvider.notifier).updateSearch(val), - ), - ), - if (_searchController.text.isNotEmpty) - GestureDetector( - onTap: () { - _searchController.clear(); - _searchFocusNode.unfocus(); - ref.read(sourceProvider.notifier).updateSearch(''); - }, - child: Padding( - padding: const EdgeInsets.all(AppSpacing.xs), - child: Icon( - CupertinoIcons.clear_circled_solid, - size: 16, - color: ext.textHint, - ), - ), - ), - const SizedBox(width: AppSpacing.xs), - ], - ), - ), - ); - } - - Widget _buildSectionHeader(AppThemeExtension ext, SourceState state) { - return Padding( - padding: const EdgeInsets.fromLTRB( - AppSpacing.md, - AppSpacing.md, - AppSpacing.md, - AppSpacing.sm, - ), - child: Row( - children: [ - Text( - '频道管理', - style: AppTypography.title3.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(width: AppSpacing.xs), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: ext.accent.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(10), - ), - child: Text( - '${state.enabledCount} 已启用', - style: AppTypography.caption2.copyWith( - color: ext.accent, - fontWeight: FontWeight.w600, - ), - ), - ), - const Spacer(), - CupertinoButton( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - onPressed: _showManageActions, - child: Text( - '管理', - style: AppTypography.subhead.copyWith(color: ext.accent), - ), - ), - ], - ), - ); - } - - Widget _buildChannelList(AppThemeExtension ext, SourceState state) { - final channels = state.filteredChannels; - - if (channels.isEmpty) { - return SliverFillRemaining( - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('📭', style: TextStyle(fontSize: 48)), - const SizedBox(height: AppSpacing.md), - Text( - '未找到频道', - style: AppTypography.subhead.copyWith(color: ext.textSecondary), - ), - ], - ), - ), - ); - } - - return SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), - sliver: SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - final ch = channels[index]; - final enabled = state.isEnabled(ch.key); - return Padding( - padding: const EdgeInsets.only(bottom: AppSpacing.sm), - child: _ChannelCard( - channel: ch, - enabled: enabled, - ext: ext, - onToggle: () => - ref.read(sourceProvider.notifier).toggleChannel(ch.key), - onTap: () => _showChannelDetail(ch, ext), - ), - ).animate().fadeIn( - duration: 300.ms, - delay: Duration(milliseconds: 30 * (index > 10 ? 10 : index)), - ); - }, childCount: channels.length), - ), - ); - } - - Widget _buildFooter(AppThemeExtension ext, SourceState state) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: AppSpacing.xl), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 40, - height: 1, - color: ext.textHint.withValues(alpha: 0.2), - ), - const SizedBox(width: AppSpacing.sm), - const Text('📖', style: TextStyle(fontSize: 14)), - const SizedBox(width: AppSpacing.sm), - Container( - width: 40, - height: 1, - color: ext.textHint.withValues(alpha: 0.2), - ), - ], - ), - const SizedBox(height: AppSpacing.xs), - Text( - '已展示全部 ${state.channels.length} 个频道', - style: AppTypography.caption1.copyWith(color: ext.textHint), - ), - const SizedBox(height: 2), - Text( - '— 到底了 —', - style: AppTypography.caption2.copyWith( - color: ext.textHint.withValues(alpha: 0.5), - ), - ), - ], - ), - ); - } - - Widget _buildBottomBar(AppThemeExtension ext) { - return Container( - padding: EdgeInsets.only( - left: AppSpacing.md, - right: AppSpacing.md, - top: AppSpacing.sm, - bottom: MediaQuery.of(context).padding.bottom + AppSpacing.sm, - ), - decoration: BoxDecoration( - color: ext.bgPrimary.withValues(alpha: 0.9), - border: Border( - top: BorderSide( - color: ext.textHint.withValues(alpha: 0.1), - width: 0.5, - ), - ), - ), - child: Row( - children: [ - Expanded( - flex: 3, - child: CupertinoButton( - color: ext.accent, - borderRadius: AppRadius.lgBorder, - onPressed: _showHomeCardSourceSheet, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('🏠', style: TextStyle(fontSize: 16)), - const SizedBox(width: AppSpacing.xs), - Flexible( - child: Text( - '首页卡片来源', - style: AppTypography.subhead.copyWith( - color: CupertinoColors.white, - fontWeight: FontWeight.w600, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ), - const SizedBox(width: AppSpacing.sm), - Expanded( - flex: 2, - child: CupertinoButton( - color: ext.bgSecondary, - borderRadius: AppRadius.lgBorder, - onPressed: _showMoreActions, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - '⋯', - style: TextStyle( - fontSize: 20, - color: CupertinoColors.white, - ), - ), - const SizedBox(width: AppSpacing.xs), - Text( - '更多', - style: AppTypography.subhead.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - ], - ), - ); - } - - /// 混合模式选择弹窗 - void _showMixModeSheet() { - final ext = AppTheme.ext(context); - final currentMode = ref.read(sourceProvider).mixConfig.mode; - - showCupertinoModalPopup( - context: context, - builder: (ctx) => CupertinoActionSheet( - title: Text( - '🔀 混合规则', - style: AppTypography.subhead.copyWith(color: ext.textSecondary), - ), - message: Text( - '选择句子广场的内容混合方式', - style: AppTypography.caption1.copyWith(color: ext.textHint), - ), - actions: _mixModes.map((mode) { - final isSelected = mode.key == currentMode; - return CupertinoActionSheetAction( - isDefaultAction: isSelected, - onPressed: () { - Navigator.pop(ctx); - _showMixConfigPanel(mode.key); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(mode.icon, style: const TextStyle(fontSize: 18)), - const SizedBox(width: 8), - Text( - '${mode.name}${isSelected ? ' ✓' : ''}', - style: TextStyle( - color: isSelected ? ext.accent : ext.textPrimary, - fontWeight: isSelected ? FontWeight.w700 : FontWeight.w400, - ), - ), - ], - ), - ); - }).toList(), - cancelButton: CupertinoActionSheetAction( - isDestructiveAction: true, - onPressed: () => Navigator.pop(ctx), - child: const Text('取消'), - ), - ), - ); - } - - /// 混合规则配置面板 - void _showMixConfigPanel(String mode) { - final ext = AppTheme.ext(context); - final state = ref.read(sourceProvider); - final channels = state.channels; - final currentConfig = state.mixConfig; - - final selectedChannels = List.from( - currentConfig.channels.isNotEmpty - ? currentConfig.channels - : channels.map((c) => c.key), - ); - final ratios = Map.from(currentConfig.ratios); - var groupSize = currentConfig.groupSize; - - if (ratios.isEmpty) { - for (final ch in selectedChannels) { - ratios[ch] = 50; - } - } - - showModalBottomSheet( - context: context, - backgroundColor: ext.bgCard, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(36)), - ), - builder: (ctx) { - return StatefulBuilder( - builder: (ctx, setModalState) { - return Container( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.75, - ), - padding: const EdgeInsets.all(AppSpacing.lg), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Center( - child: Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: ext.textHint.withValues(alpha: 0.3), - borderRadius: AppRadius.fullBorder, - ), - ), - ), - const SizedBox(height: AppSpacing.md), - Text( - '${_mixModes.firstWhere((m) => m.key == mode).icon} ${_mixModes.firstWhere((m) => m.key == mode).name}', - style: AppTypography.title3.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: AppSpacing.xs), - Text( - _mixModes.firstWhere((m) => m.key == mode).desc, - style: AppTypography.subhead.copyWith( - color: ext.textSecondary, - ), - ), - const SizedBox(height: AppSpacing.md), - Flexible( - child: SingleChildScrollView( - child: Column( - children: [ - if (mode == 'ratio') - ..._buildRatioSliders( - ext, - channels, - selectedChannels, - ratios, - setModalState, - (ch, val) { - setModalState(() => ratios[ch] = val.round()); - }, - (ch, selected) { - setModalState(() { - if (selected) { - selectedChannels.add(ch); - ratios[ch] = 50; - } else { - selectedChannels.remove(ch); - ratios.remove(ch); - } - }); - }, - ), - if (mode == 'specific') - ..._buildSpecificCheckboxes( - ext, - channels, - selectedChannels, - setModalState, - (ch, selected) { - setModalState(() { - if (selected) { - selectedChannels.add(ch); - } else { - selectedChannels.remove(ch); - } - }); - }, - ), - if (mode == 'group') - _buildGroupSizeSlider(ext, groupSize, (val) { - setModalState(() => groupSize = val.round()); - }), - if (mode == 'uniform' || mode == 'random') - _buildChannelSelectionHint(ext, selectedChannels), - ], - ), - ), - ), - const SizedBox(height: AppSpacing.md), - SizedBox( - width: double.infinity, - child: CupertinoButton( - color: ext.accent, - borderRadius: AppRadius.mdBorder, - onPressed: () { - final newConfig = FeedMixConfig( - mode: mode, - channels: selectedChannels, - ratios: mode == 'ratio' ? ratios : {}, - groupSize: mode == 'group' ? groupSize : 3, - limit: currentConfig.limit, - ); - ref - .read(sourceProvider.notifier) - .updateMixConfig(newConfig); - Navigator.pop(ctx); - _showMixSuccessToast(mode); - }, - child: const Text('保存配置'), - ), - ), - SizedBox(height: MediaQuery.of(context).padding.bottom + 8), - ], - ), - ); - }, - ); - }, - ); - } - - List _buildRatioSliders( - AppThemeExtension ext, - List channels, - List selectedChannels, - Map ratios, - StateSetter setModalState, - void Function(String, double) onRatioChanged, - void Function(String, bool) onChannelToggled, - ) { - return channels.map((ch) { - final isSelected = selectedChannels.contains(ch.key); - final ratio = ratios[ch.key] ?? 50; - return Padding( - padding: const EdgeInsets.only(bottom: AppSpacing.sm), - child: GlassContainer( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.sm, - ), - margin: EdgeInsets.zero, - child: Column( - children: [ - Row( - children: [ - Text(ch.icon, style: const TextStyle(fontSize: 18)), - const SizedBox(width: AppSpacing.xs), - Expanded( - child: Text( - ch.name, - style: AppTypography.subhead.copyWith( - color: ext.textPrimary, - ), - ), - ), - CupertinoSwitch( - value: isSelected, - onChanged: (v) => onChannelToggled(ch.key, v), - activeTrackColor: ext.accent, - ), - ], - ), - if (isSelected) ...[ - const SizedBox(height: AppSpacing.xs), - Row( - children: [ - Expanded( - child: CupertinoSlider( - value: ratio.toDouble(), - min: 1, - max: 100, - activeColor: ext.accent, - onChanged: (v) => onRatioChanged(ch.key, v), - ), - ), - SizedBox( - width: 40, - child: Text( - '$ratio%', - style: AppTypography.caption1.copyWith( - color: ext.accent, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.right, - ), - ), - ], - ), - ], - ], - ), - ), - ); - }).toList(); - } - - List _buildSpecificCheckboxes( - AppThemeExtension ext, - List channels, - List selectedChannels, - StateSetter setModalState, - void Function(String, bool) onChannelToggled, - ) { - return channels.map((ch) { - final isSelected = selectedChannels.contains(ch.key); - return Padding( - padding: const EdgeInsets.only(bottom: AppSpacing.sm), - child: GestureDetector( - onTap: () => onChannelToggled(ch.key, !isSelected), - child: GlassContainer( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.sm, - ), - margin: EdgeInsets.zero, - child: Row( - children: [ - Text(ch.icon, style: const TextStyle(fontSize: 18)), - const SizedBox(width: AppSpacing.xs), - Expanded( - child: Text( - ch.name, - style: AppTypography.subhead.copyWith( - color: isSelected ? ext.textPrimary : ext.textHint, - ), - ), - ), - Icon( - isSelected - ? CupertinoIcons.checkmark_seal_fill - : CupertinoIcons.circle, - color: isSelected ? ext.accent : ext.textHint, - size: 22, - ), - ], - ), - ), - ), - ); - }).toList(); - } - - Widget _buildGroupSizeSlider( - AppThemeExtension ext, - int groupSize, - void Function(double) onChanged, - ) { - return GlassContainer( - padding: const EdgeInsets.all(AppSpacing.md), - margin: EdgeInsets.zero, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '🔄 每组条数', - style: AppTypography.subhead.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: AppSpacing.sm), - Row( - children: [ - Expanded( - child: CupertinoSlider( - value: groupSize.toDouble(), - min: 1, - max: 10, - divisions: 9, - activeColor: ext.accent, - onChanged: onChanged, - ), - ), - SizedBox( - width: 32, - child: Text( - '$groupSize', - style: AppTypography.title3.copyWith( - color: ext.accent, - fontWeight: FontWeight.w700, - ), - textAlign: TextAlign.center, - ), - ), - ], - ), - const SizedBox(height: AppSpacing.xs), - Text( - '每个分类连续展示N条后切换到下一个分类', - style: AppTypography.caption2.copyWith(color: ext.textHint), - ), - ], - ), - ); - } - - Widget _buildChannelSelectionHint( - AppThemeExtension ext, - List selectedChannels, - ) { - return GlassContainer( - padding: const EdgeInsets.all(AppSpacing.md), - margin: EdgeInsets.zero, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '📌 参与频道', - style: AppTypography.subhead.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: AppSpacing.xs), - Text( - '当前已启用 ${selectedChannels.length} 个频道参与混合', - style: AppTypography.caption1.copyWith(color: ext.textSecondary), - ), - const SizedBox(height: AppSpacing.xs), - Text( - '可通过频道管理中的开关控制参与混合的频道', - style: AppTypography.caption2.copyWith(color: ext.textHint), - ), - ], - ), - ); - } - - void _showMixSuccessToast(String mode) { - final modeLabel = _mixModes.firstWhere( - (m) => m.key == mode, - orElse: () => _mixModes[3], - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('${modeLabel.icon} 已切换为「${modeLabel.name}」模式'), - duration: const Duration(seconds: 2), - behavior: SnackBarBehavior.floating, - ), - ); - } - - /// 首页卡片来源配置 - void _showHomeCardSourceSheet() { - final ext = AppTheme.ext(context); - final state = ref.read(sourceProvider); - final channels = state.channels; - final currentConfig = state.homeCardMixConfig; - - var selectedMode = currentConfig.mode; - final selectedChannels = List.from( - currentConfig.channels.isNotEmpty - ? currentConfig.channels - : channels.map((c) => c.key), - ); - - showModalBottomSheet( - context: context, - backgroundColor: ext.bgCard, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(36)), - ), - builder: (ctx) { - return StatefulBuilder( - builder: (ctx, setModalState) { - return Container( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.7, - ), - padding: const EdgeInsets.all(AppSpacing.lg), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Center( - child: Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: ext.textHint.withValues(alpha: 0.3), - borderRadius: AppRadius.fullBorder, - ), - ), - ), - const SizedBox(height: AppSpacing.md), - Text( - '🏠 首页卡片来源', - style: AppTypography.title3.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: AppSpacing.xs), - Text( - '配置首页上方句子卡片的内容来源', - style: AppTypography.subhead.copyWith( - color: ext.textSecondary, - ), - ), - const SizedBox(height: AppSpacing.md), - Text( - '混合模式', - style: AppTypography.subhead.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: AppSpacing.sm), - SizedBox( - height: 44, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: _mixModes.length, - separatorBuilder: (_, __) => const SizedBox(width: 8), - itemBuilder: (_, i) { - final mode = _mixModes[i]; - final isSelected = mode.key == selectedMode; - return GestureDetector( - onTap: () { - setModalState(() => selectedMode = mode.key); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - decoration: BoxDecoration( - color: isSelected - ? ext.accent.withValues(alpha: 0.15) - : ext.bgSecondary, - borderRadius: AppRadius.mdBorder, - border: isSelected - ? Border.all(color: ext.accent, width: 1.5) - : null, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - mode.icon, - style: const TextStyle(fontSize: 14), - ), - const SizedBox(width: 4), - Text( - mode.name, - style: AppTypography.caption1.copyWith( - color: isSelected - ? ext.accent - : ext.textSecondary, - fontWeight: isSelected - ? FontWeight.w600 - : FontWeight.w400, - ), - ), - ], - ), - ), - ); - }, - ), - ), - const SizedBox(height: AppSpacing.md), - Text( - '选择频道', - style: AppTypography.subhead.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: AppSpacing.sm), - Flexible( - child: SingleChildScrollView( - child: Wrap( - spacing: 8, - runSpacing: 8, - children: channels.map((ch) { - final isSelected = selectedChannels.contains(ch.key); - return GestureDetector( - onTap: () { - setModalState(() { - if (isSelected) { - if (selectedChannels.length > 1) { - selectedChannels.remove(ch.key); - } - } else { - selectedChannels.add(ch.key); - } - }); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: isSelected - ? ext.accent.withValues(alpha: 0.15) - : ext.bgSecondary, - borderRadius: AppRadius.mdBorder, - border: isSelected - ? Border.all(color: ext.accent, width: 1.5) - : null, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - ch.icon, - style: const TextStyle(fontSize: 14), - ), - const SizedBox(width: 4), - Text( - ch.name, - style: AppTypography.caption1.copyWith( - color: isSelected - ? ext.accent - : ext.textSecondary, - fontWeight: isSelected - ? FontWeight.w600 - : FontWeight.w400, - ), - ), - ], - ), - ), - ); - }).toList(), - ), - ), - ), - const SizedBox(height: AppSpacing.md), - SizedBox( - width: double.infinity, - child: CupertinoButton( - color: ext.accent, - borderRadius: AppRadius.mdBorder, - onPressed: () { - final newConfig = FeedMixConfig( - mode: selectedMode, - channels: selectedChannels, - limit: 5, - ); - ref - .read(sourceProvider.notifier) - .updateHomeCardMixConfig(newConfig); - Navigator.pop(ctx); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('🏠 首页卡片来源已更新'), - duration: Duration(seconds: 2), - behavior: SnackBarBehavior.floating, - ), - ); - }, - child: const Text('保存'), - ), - ), - SizedBox(height: MediaQuery.of(context).padding.bottom + 8), - ], - ), - ); - }, - ); - }, - ); - } - - /// 更多操作弹窗 - void _showMoreActions() { - showCupertinoModalPopup( - context: context, - builder: (ctx) => CupertinoActionSheet( - actions: [ - CupertinoActionSheetAction( - onPressed: () { - Navigator.pop(ctx); - _showImportTip(); - }, - child: const Text('📥 导入来源'), - ), - CupertinoActionSheetAction( - onPressed: () { - Navigator.pop(ctx); - _navigateToMoreSources(); - }, - child: const Text('🧭 更多数据源'), - ), - ], - cancelButton: CupertinoActionSheetAction( - isDefaultAction: true, - onPressed: () => Navigator.pop(ctx), - child: const Text('取消'), - ), - ), - ); - } - - void _showManageActions() { - showCupertinoModalPopup( - context: context, - builder: (ctx) => CupertinoActionSheet( - actions: [ - CupertinoActionSheetAction( - onPressed: () { - Navigator.pop(ctx); - ref.read(sourceProvider.notifier).enableAll(); - }, - child: const Text('✅ 全部启用'), - ), - CupertinoActionSheetAction( - onPressed: () { - Navigator.pop(ctx); - ref.read(sourceProvider.notifier).disableAll(); - }, - child: const Text('❌ 全部禁用'), - ), - ], - cancelButton: CupertinoActionSheetAction( - isDefaultAction: true, - onPressed: () => Navigator.pop(ctx), - child: const Text('取消'), - ), - ), - ); - } - - void _showChannelDetail(FeedChannel channel, AppThemeExtension ext) { - final typeColor = FeedTypeColor.getColor(channel.key); - - AppBottomSheet.showCustom( - context: context, - builder: (ctx) => Container( - padding: const EdgeInsets.all(AppSpacing.lg), - decoration: BoxDecoration( - color: ext.bgCard, - borderRadius: const BorderRadius.vertical(top: Radius.circular(36)), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: ext.textHint.withValues(alpha: 0.3), - borderRadius: AppRadius.fullBorder, - ), - ), - ), - const SizedBox(height: AppSpacing.md), - Row( - children: [ - Container( - width: 52, - height: 52, - decoration: BoxDecoration( - color: typeColor.withValues(alpha: 0.15), - borderRadius: AppRadius.mdBorder, - ), - child: Center( - child: Text( - channel.icon, - style: const TextStyle(fontSize: 26), - ), - ), - ), - const SizedBox(width: AppSpacing.md), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - channel.name, - style: AppTypography.title3.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.w700, - ), - ), - Text( - '${channel.count} 条内容 · ${_fmtViews(_getChannelViews(channel.key))} 次浏览', - style: AppTypography.subhead.copyWith( - color: ext.textSecondary, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: AppSpacing.md), - GlassContainer( - padding: const EdgeInsets.all(AppSpacing.md), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _DetailStatItem( - label: '内容数', - value: '${channel.count}', - color: ext.accent, - ), - _DetailStatItem( - label: '浏览量', - value: _fmtViews(_getChannelViews(channel.key)), - color: ext.accent, - ), - _DetailStatItem( - label: '状态', - value: ref.read(sourceProvider).isEnabled(channel.key) - ? '已启用' - : '已禁用', - color: ref.read(sourceProvider).isEnabled(channel.key) - ? ext.accent - : ext.textHint, - ), - ], - ), - ), - const SizedBox(height: AppSpacing.lg), - SizedBox( - width: double.infinity, - child: CupertinoButton( - color: ext.accent, - borderRadius: AppRadius.mdBorder, - onPressed: () { - Navigator.pop(ctx); - _navigateToChannelDetail(channel); - }, - child: const Text('浏览频道内容'), - ), - ), - SizedBox(height: MediaQuery.of(context).padding.bottom + 8), - ], - ), - ), - ); - } - - void _navigateToChannelDetail(FeedChannel channel) { - context.push( - AppRoutes.categoryDetail.replaceFirst(':type', channel.key), - extra: {'type': channel.key, 'name': channel.name, 'icon': channel.icon}, - ); - } - - void _navigateToMoreSources() { - context.push(AppRoutes.discover); - } - - void _showImportTip() { - showCupertinoDialog( - context: context, - builder: (ctx) => CupertinoAlertDialog( - title: const Text('📥 导入句子源'), - content: const Text( - '导入功能即将推出!\n\n将支持:\n• JSON / CSV 文件导入\n• URL 订阅远程数据源\n• 自定义字段映射', - ), - actions: [ - CupertinoDialogAction( - isDefaultAction: true, - child: const Text('好的'), - onPressed: () => Navigator.pop(ctx), - ), - ], - ), - ); - } - - int _getChannelViews(String key) { - final stats = ref.read(sourceProvider).stats; - if (stats == null) return 0; - try { - final chStat = stats.channels.firstWhere((s) => s.key == key); - return chStat.views; - } catch (_) { - return 0; - } - } - - String _fmtCount(int n) { - if (n >= 10000) return '${(n / 10000).toStringAsFixed(1)}w'; - if (n >= 1000) return '${(n / 1000).toStringAsFixed(1)}k'; - return '$n'; - } - - String _fmtViews(int n) { - if (n >= 10000) return '${(n / 10000).toStringAsFixed(1)}w'; - if (n >= 1000) return '${(n / 1000).toStringAsFixed(1)}k'; - return '$n'; - } -} - -class _ChannelCard extends StatelessWidget { - const _ChannelCard({ - required this.channel, - required this.enabled, - required this.ext, - required this.onToggle, - required this.onTap, - }); - - final FeedChannel channel; - final bool enabled; - final AppThemeExtension ext; - final VoidCallback onToggle; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final typeColor = FeedTypeColor.getColor(channel.key); - - return PressableCard( - onTap: onTap, - child: Opacity( - opacity: enabled ? 1.0 : 0.45, - child: Row( - children: [ - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: typeColor.withValues(alpha: 0.15), - borderRadius: AppRadius.mdBorder, - ), - child: Center( - child: Text(channel.icon, style: const TextStyle(fontSize: 22)), - ), - ), - const SizedBox(width: AppSpacing.sm), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - channel.name, - style: AppTypography.subhead.copyWith( - color: ext.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 2), - Text( - '${channel.count} 条', - style: AppTypography.caption1.copyWith( - color: ext.textSecondary, - ), - ), - ], - ), - ), - CupertinoSwitch( - value: enabled, - onChanged: (_) => onToggle(), - activeTrackColor: ext.accent, - ), - ], - ), - ), - ); - } -} - -class _DetailStatItem extends StatelessWidget { - const _DetailStatItem({ - required this.label, - required this.value, - required this.color, - }); - - final String label; - final String value; - final Color color; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Text( - value, - style: AppTypography.title3.copyWith( - color: color, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 2), - Text( - label, - style: AppTypography.caption2.copyWith( - color: AppTheme.ext(context).textSecondary, - ), - ), - ], - ); - } -} diff --git a/lib/features/source/providers/source_provider.dart b/lib/features/source/providers/source_provider.dart index f900cf78..daed0870 100644 --- a/lib/features/source/providers/source_provider.dart +++ b/lib/features/source/providers/source_provider.dart @@ -1,4 +1,4 @@ -// ============================================================ +// ============================================================ // 闲言APP — 句子来源状态管理 // 创建时间: 2026-04-29 // 更新时间: 2026-05-13 @@ -92,8 +92,14 @@ class SourceState { } } -class SourceNotifier extends StateNotifier { - SourceNotifier() : super(const SourceState()); +class SourceNotifier extends Notifier { + @override + SourceState build() { + Future.microtask(init); + return const SourceState(); + } + + SourceNotifier(); Future init() async { state = state.copyWith(isLoading: true); @@ -307,6 +313,6 @@ class SourceNotifier extends StateNotifier { } } -final sourceProvider = StateNotifierProvider( - (ref) => SourceNotifier()..init(), +final sourceProvider = NotifierProvider( + SourceNotifier.new, ); diff --git a/lib/features/statistics/presentation/statistics_page.dart b/lib/features/statistics/presentation/statistics_page.dart index 39a18e9a..25553de8 100644 --- a/lib/features/statistics/presentation/statistics_page.dart +++ b/lib/features/statistics/presentation/statistics_page.dart @@ -1159,7 +1159,7 @@ class _SigninTrendChart extends StatelessWidget { ], lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( - tooltipRoundedRadius: 8, + tooltipBorderRadius: BorderRadius.circular(8), getTooltipItems: (spots) => spots .map( (s) => LineTooltipItem( @@ -1363,7 +1363,7 @@ class _CoinBarChart extends StatelessWidget { maxY: adjustedMax, barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( - tooltipRoundedRadius: 8, + tooltipBorderRadius: BorderRadius.circular(8), getTooltipItem: (group, _, rod, __) => BarTooltipItem( '${rod.toY.toInt()}', AppTypography.caption1.copyWith( diff --git a/lib/features/statistics/presentation/widgets/coin_stats_tab.dart b/lib/features/statistics/presentation/widgets/coin_stats_tab.dart index 1cba26b5..74921d87 100644 --- a/lib/features/statistics/presentation/widgets/coin_stats_tab.dart +++ b/lib/features/statistics/presentation/widgets/coin_stats_tab.dart @@ -99,11 +99,7 @@ class CoinOverviewCard extends StatelessWidget { // ============================================================ class CoinTrendChart extends StatelessWidget { - const CoinTrendChart({ - super.key, - required this.trend, - required this.ext, - }); + const CoinTrendChart({super.key, required this.trend, required this.ext}); final List trend; final AppThemeExtension ext; @@ -223,7 +219,7 @@ class CoinTrendChart extends StatelessWidget { ], lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( - tooltipRoundedRadius: 8, + tooltipBorderRadius: BorderRadius.circular(8), getTooltipItems: (spots) => spots .map( (s) => LineTooltipItem( @@ -308,7 +304,7 @@ class CoinSourceBarChart extends StatelessWidget { maxY: maxVal, barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( - tooltipRoundedRadius: 8, + tooltipBorderRadius: BorderRadius.circular(8), getTooltipItem: (group, _, rod, __) => BarTooltipItem( '${rod.toY.toInt()}', AppTypography.caption1.copyWith( @@ -396,11 +392,7 @@ class CoinSourceBarChart extends StatelessWidget { // ============================================================ class CoinRingChart extends StatelessWidget { - const CoinRingChart({ - super.key, - required this.overview, - required this.ext, - }); + const CoinRingChart({super.key, required this.overview, required this.ext}); final CoinOverview? overview; final AppThemeExtension ext; diff --git a/lib/features/statistics/presentation/widgets/favorite_stats_tab.dart b/lib/features/statistics/presentation/widgets/favorite_stats_tab.dart index c8a67719..1348e592 100644 --- a/lib/features/statistics/presentation/widgets/favorite_stats_tab.dart +++ b/lib/features/statistics/presentation/widgets/favorite_stats_tab.dart @@ -219,11 +219,7 @@ class FavoriteCategoryPie extends StatelessWidget { // ============================================================ class FavoriteTrendChart extends StatelessWidget { - const FavoriteTrendChart({ - super.key, - required this.trend, - required this.ext, - }); + const FavoriteTrendChart({super.key, required this.trend, required this.ext}); final List trend; final AppThemeExtension ext; @@ -346,7 +342,7 @@ class FavoriteTrendChart extends StatelessWidget { ], lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( - tooltipRoundedRadius: 8, + tooltipBorderRadius: BorderRadius.circular(8), getTooltipItems: (spots) => spots .map( (s) => LineTooltipItem( @@ -422,7 +418,7 @@ class FavoriteGroupBarChart extends StatelessWidget { maxY: maxVal, barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( - tooltipRoundedRadius: 8, + tooltipBorderRadius: BorderRadius.circular(8), getTooltipItem: (group, _, rod, __) => BarTooltipItem( '${rod.toY.toInt()}', AppTypography.caption1.copyWith( diff --git a/lib/features/statistics/presentation/widgets/learning_stats_tab.dart b/lib/features/statistics/presentation/widgets/learning_stats_tab.dart index 32e949e5..979b1075 100644 --- a/lib/features/statistics/presentation/widgets/learning_stats_tab.dart +++ b/lib/features/statistics/presentation/widgets/learning_stats_tab.dart @@ -96,11 +96,7 @@ class LearningOverviewCard extends StatelessWidget { // ============================================================ class LearningTrendChart extends StatelessWidget { - const LearningTrendChart({ - super.key, - required this.trend, - required this.ext, - }); + const LearningTrendChart({super.key, required this.trend, required this.ext}); final List trend; final AppThemeExtension ext; @@ -234,7 +230,7 @@ class LearningTrendChart extends StatelessWidget { ], lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( - tooltipRoundedRadius: 8, + tooltipBorderRadius: BorderRadius.circular(8), getTooltipItems: (spots) => spots .map( (s) => LineTooltipItem( @@ -417,11 +413,7 @@ class CategoryPieChart extends StatelessWidget { // ============================================================ class HeatmapGrid extends StatelessWidget { - const HeatmapGrid({ - super.key, - required this.heatmapDays, - required this.ext, - }); + const HeatmapGrid({super.key, required this.heatmapDays, required this.ext}); final List heatmapDays; final AppThemeExtension ext; diff --git a/lib/features/statistics/providers/statistics_provider.dart b/lib/features/statistics/providers/statistics_provider.dart index 1fa59b0d..4cc30b19 100644 --- a/lib/features/statistics/providers/statistics_provider.dart +++ b/lib/features/statistics/providers/statistics_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 统计状态管理 /// 创建时间: 2026-04-28 /// 更新时间: 2026-05-04 @@ -144,10 +144,10 @@ class StatisticsState { } } -class StatisticsNotifier extends StateNotifier { - StatisticsNotifier(this._ref) : super(const StatisticsState()); +class StatisticsNotifier extends Notifier { + @override + StatisticsState build() => const StatisticsState(); - final Ref _ref; final ApiClient _api = ApiClient.instance; static const _cacheKeyOverview = 'cache_statistics_overview'; @@ -159,7 +159,7 @@ class StatisticsNotifier extends StateNotifier { Future loadAll() async { state = state.copyWith(isLoading: true, clearError: true); - final isOnline = _ref.read(connectivityProvider); + final isOnline = ref.read(connectivityProvider); if (!isOnline) { await _loadFromCache(); @@ -288,6 +288,6 @@ class StatisticsNotifier extends StateNotifier { } final statisticsProvider = - StateNotifierProvider((ref) { - return StatisticsNotifier(ref); - }); + NotifierProvider( + StatisticsNotifier.new, + ); diff --git a/lib/features/statistics/providers/user_stats_provider.dart b/lib/features/statistics/providers/user_stats_provider.dart index 828275f0..4a0a5f61 100644 --- a/lib/features/statistics/providers/user_stats_provider.dart +++ b/lib/features/statistics/providers/user_stats_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 用户数据统计状态管理 /// 创建时间: 2026-05-14 /// 更新时间: 2026-05-14 @@ -72,7 +72,15 @@ class LearningOverview { /// 趋势数据点 class TrendPoint { - const TrendPoint({this.date = '', this.views = 0, this.interactions = 0, this.favorites = 0, this.coins = 0, this.income = 0, this.expense = 0}); + const TrendPoint({ + this.date = '', + this.views = 0, + this.interactions = 0, + this.favorites = 0, + this.coins = 0, + this.income = 0, + this.expense = 0, + }); final String date; final int views; @@ -163,7 +171,14 @@ class FavoriteGroupItem { /// 积分概览 class CoinOverview { - const CoinOverview({this.totalCoins = 0, this.totalIncome = 0, this.totalExpense = 0, this.todayIncome = 0, this.todayExpense = 0, this.balance = 0}); + const CoinOverview({ + this.totalCoins = 0, + this.totalIncome = 0, + this.totalExpense = 0, + this.todayIncome = 0, + this.todayExpense = 0, + this.balance = 0, + }); final int totalCoins; final int totalIncome; @@ -186,7 +201,12 @@ class CoinOverview { /// 收藏概览 class FavoriteOverview { - const FavoriteOverview({this.totalFavorites = 0, this.weekFavorites = 0, this.todayFavorites = 0, this.totalGroups = 0}); + const FavoriteOverview({ + this.totalFavorites = 0, + this.weekFavorites = 0, + this.todayFavorites = 0, + this.totalGroups = 0, + }); final int totalFavorites; final int weekFavorites; @@ -245,7 +265,10 @@ class UserStatsState { final bool isFromCache; final String? error; - bool get hasData => learningOverview != null || coinOverview != null || favoriteOverview != null; + bool get hasData => + learningOverview != null || + coinOverview != null || + favoriteOverview != null; bool get hasError => error != null; bool get showEmpty => isLoaded && !hasData && !hasError; @@ -271,14 +294,20 @@ class UserStatsState { bool clearError = false, }) { return UserStatsState( - learningOverview: clearLearningOverview ? null : (learningOverview ?? this.learningOverview), + learningOverview: clearLearningOverview + ? null + : (learningOverview ?? this.learningOverview), learningTrend: learningTrend ?? this.learningTrend, categoryStats: categoryStats ?? this.categoryStats, heatmapDays: heatmapDays ?? this.heatmapDays, - coinOverview: clearCoinOverview ? null : (coinOverview ?? this.coinOverview), + coinOverview: clearCoinOverview + ? null + : (coinOverview ?? this.coinOverview), coinTrend: coinTrend ?? this.coinTrend, coinSources: coinSources ?? this.coinSources, - favoriteOverview: clearFavoriteOverview ? null : (favoriteOverview ?? this.favoriteOverview), + favoriteOverview: clearFavoriteOverview + ? null + : (favoriteOverview ?? this.favoriteOverview), favoriteTrend: favoriteTrend ?? this.favoriteTrend, favoriteCategories: favoriteCategories ?? this.favoriteCategories, favoriteGroups: favoriteGroups ?? this.favoriteGroups, @@ -294,10 +323,10 @@ class UserStatsState { // Notifier // ============================================================ -class UserStatsNotifier extends StateNotifier { - UserStatsNotifier(this._ref) : super(const UserStatsState()); +class UserStatsNotifier extends Notifier { + @override + UserStatsState build() => const UserStatsState(); - final Ref _ref; final ApiClient _api = ApiClient.instance; static const _cacheKey = 'cache_user_stats'; @@ -306,7 +335,7 @@ class UserStatsNotifier extends StateNotifier { Future loadAll() async { state = state.copyWith(isLoading: true, clearError: true); - final isOnline = _ref.read(connectivityProvider); + final isOnline = ref.read(connectivityProvider); if (!isOnline) { await _loadFromCache(); return; @@ -314,11 +343,23 @@ class UserStatsNotifier extends StateNotifier { try { final results = await Future.wait([ - _api.get>('/api/user_center/stats', queryParameters: {'type': 'overview', 'period': 'month'}), - _api.get>('/api/user_center/stats', queryParameters: {'type': 'category'}), - _api.get>('/api/user_center/stats', queryParameters: {'type': 'trend', 'period': 'month'}), + _api.get>( + '/api/user_center/stats', + queryParameters: {'type': 'overview', 'period': 'month'}, + ), + _api.get>( + '/api/user_center/stats', + queryParameters: {'type': 'category'}, + ), + _api.get>( + '/api/user_center/stats', + queryParameters: {'type': 'trend', 'period': 'month'}, + ), _api.get>('/api/user_center/dashboard'), - _api.get>('/api/user_center/heatmap', queryParameters: {'year': '${DateTime.now().year}'}), + _api.get>( + '/api/user_center/heatmap', + queryParameters: {'year': '${DateTime.now().year}'}, + ), ]); final overviewData = _extractData(results[0].data); @@ -328,23 +369,63 @@ class UserStatsNotifier extends StateNotifier { final heatmapData = _extractData(results[4].data); final learningOverview = LearningOverview.fromJson(overviewData); - final categories = _parseList(categoryData['items'] ?? categoryData['categories'], CategoryItem.fromJson); - final trendList = _parseList(trendData['items'] ?? trendData['trend'], TrendPoint.fromJson); - final heatmapList = _parseList(heatmapData['items'] ?? heatmapData['days'], HeatmapDay.fromJson); + final categories = _parseList( + categoryData['items'] ?? categoryData['categories'], + CategoryItem.fromJson, + ); + final trendList = _parseList( + trendData['items'] ?? trendData['trend'], + TrendPoint.fromJson, + ); + final heatmapList = _parseList( + heatmapData['items'] ?? heatmapData['days'], + HeatmapDay.fromJson, + ); - final coinOverview = CoinOverview.fromJson(dashboardData['coins'] is Map ? dashboardData['coins'] as Map : dashboardData); - final coinSources = _parseList(dashboardData['coin_sources'], CoinSourceItem.fromJson); - final coinTrend = _parseList(dashboardData['coin_trend'], TrendPoint.fromJson); + final coinOverview = CoinOverview.fromJson( + dashboardData['coins'] is Map + ? dashboardData['coins'] as Map + : dashboardData, + ); + final coinSources = _parseList( + dashboardData['coin_sources'], + CoinSourceItem.fromJson, + ); + final coinTrend = _parseList( + dashboardData['coin_trend'], + TrendPoint.fromJson, + ); - final favOverview = FavoriteOverview.fromJson(dashboardData['favorites'] is Map ? dashboardData['favorites'] as Map : dashboardData); - final favCategories = _parseList(dashboardData['favorite_categories'], CategoryItem.fromJson); - final favGroups = _parseList(dashboardData['favorite_groups'], FavoriteGroupItem.fromJson); - final favTrend = _parseList(dashboardData['favorite_trend'], TrendPoint.fromJson); + final favOverview = FavoriteOverview.fromJson( + dashboardData['favorites'] is Map + ? dashboardData['favorites'] as Map + : dashboardData, + ); + final favCategories = _parseList( + dashboardData['favorite_categories'], + CategoryItem.fromJson, + ); + final favGroups = _parseList( + dashboardData['favorite_groups'], + FavoriteGroupItem.fromJson, + ); + final favTrend = _parseList( + dashboardData['favorite_trend'], + TrendPoint.fromJson, + ); await _saveToCache( - learningOverview, categories, trendList, heatmapList, - coinOverview, coinSources, coinTrend, - favOverview, favCategories, favGroups, favTrend, + learningOverview, + categories, + trendList, + heatmapList, + coinOverview, + coinSources, + coinTrend, + favOverview, + favCategories, + favGroups, + favTrend, ); state = state.copyWith( @@ -367,7 +448,11 @@ class UserStatsNotifier extends StateNotifier { Log.e('加载用户统计失败', e); final hasCache = await _loadFromCache(); if (!hasCache) { - state = state.copyWith(isLoading: false, isLoaded: true, error: '加载失败,请检查网络后重试'); + state = state.copyWith( + isLoading: false, + isLoaded: true, + error: '加载失败,请检查网络后重试', + ); } } } @@ -379,9 +464,14 @@ class UserStatsNotifier extends StateNotifier { return {}; } - List _parseList(dynamic raw, T Function(Map) fromJson) { + List _parseList( + dynamic raw, + T Function(Map) fromJson, + ) { if (raw is List) { - return raw.map((e) => e is Map ? fromJson(e) : fromJson({})).toList(); + return raw + .map((e) => e is Map ? fromJson(e) : fromJson({})) + .toList(); } return []; } @@ -392,17 +482,47 @@ class UserStatsNotifier extends StateNotifier { if (cached != null) { final json = jsonDecode(cached) as Map; state = UserStatsState( - learningOverview: LearningOverview.fromJson(json['learning_overview'] as Map? ?? {}), - categoryStats: _parseList(json['category_stats'], CategoryItem.fromJson), - learningTrend: _parseList(json['learning_trend'], TrendPoint.fromJson), - heatmapDays: _parseList(json['heatmap_days'], HeatmapDay.fromJson), - coinOverview: CoinOverview.fromJson(json['coin_overview'] as Map? ?? {}), - coinSources: _parseList(json['coin_sources'], CoinSourceItem.fromJson), - coinTrend: _parseList(json['coin_trend'], TrendPoint.fromJson), - favoriteOverview: FavoriteOverview.fromJson(json['favorite_overview'] as Map? ?? {}), - favoriteCategories: _parseList(json['favorite_categories'], CategoryItem.fromJson), - favoriteGroups: _parseList(json['favorite_groups'], FavoriteGroupItem.fromJson), - favoriteTrend: _parseList(json['favorite_trend'], TrendPoint.fromJson), + learningOverview: LearningOverview.fromJson( + json['learning_overview'] as Map? ?? {}, + ), + categoryStats: _parseList( + json['category_stats'], + CategoryItem.fromJson, + ), + learningTrend: _parseList( + json['learning_trend'], + TrendPoint.fromJson, + ), + heatmapDays: _parseList( + json['heatmap_days'], + HeatmapDay.fromJson, + ), + coinOverview: CoinOverview.fromJson( + json['coin_overview'] as Map? ?? {}, + ), + coinSources: _parseList( + json['coin_sources'], + CoinSourceItem.fromJson, + ), + coinTrend: _parseList( + json['coin_trend'], + TrendPoint.fromJson, + ), + favoriteOverview: FavoriteOverview.fromJson( + json['favorite_overview'] as Map? ?? {}, + ), + favoriteCategories: _parseList( + json['favorite_categories'], + CategoryItem.fromJson, + ), + favoriteGroups: _parseList( + json['favorite_groups'], + FavoriteGroupItem.fromJson, + ), + favoriteTrend: _parseList( + json['favorite_trend'], + TrendPoint.fromJson, + ), isLoaded: true, isFromCache: true, ); @@ -415,23 +535,87 @@ class UserStatsNotifier extends StateNotifier { } Future _saveToCache( - LearningOverview learningOverview, List categories, List trendList, List heatmapDays, - CoinOverview coinOverview, List coinSources, List coinTrend, - FavoriteOverview favOverview, List favCategories, List favGroups, List favTrend, + LearningOverview learningOverview, + List categories, + List trendList, + List heatmapDays, + CoinOverview coinOverview, + List coinSources, + List coinTrend, + FavoriteOverview favOverview, + List favCategories, + List favGroups, + List favTrend, ) async { try { final json = jsonEncode({ 'learning_overview': learningOverview.toJson(), - 'category_stats': categories.map((e) => {'name': e.name, 'count': e.count, 'percentage': e.percentage}).toList(), - 'learning_trend': trendList.map((e) => {'date': e.date, 'views': e.views, 'interactions': e.interactions, 'favorites': e.favorites}).toList(), - 'heatmap_days': heatmapDays.map((e) => {'date': e.date, 'count': e.count, 'level': e.level}).toList(), - 'coin_overview': {'total_coins': coinOverview.totalCoins, 'total_income': coinOverview.totalIncome, 'total_expense': coinOverview.totalExpense, 'today_income': coinOverview.todayIncome, 'today_expense': coinOverview.todayExpense, 'balance': coinOverview.balance}, - 'coin_sources': coinSources.map((e) => {'source': e.source, 'amount': e.amount, 'count': e.count}).toList(), - 'coin_trend': coinTrend.map((e) => {'date': e.date, 'coins': e.coins, 'income': e.income, 'expense': e.expense}).toList(), - 'favorite_overview': {'total_favorites': favOverview.totalFavorites, 'week_favorites': favOverview.weekFavorites, 'today_favorites': favOverview.todayFavorites, 'total_groups': favOverview.totalGroups}, - 'favorite_categories': favCategories.map((e) => {'name': e.name, 'count': e.count, 'percentage': e.percentage}).toList(), - 'favorite_groups': favGroups.map((e) => {'group': e.group, 'count': e.count}).toList(), - 'favorite_trend': favTrend.map((e) => {'date': e.date, 'favorites': e.favorites}).toList(), + 'category_stats': categories + .map( + (e) => { + 'name': e.name, + 'count': e.count, + 'percentage': e.percentage, + }, + ) + .toList(), + 'learning_trend': trendList + .map( + (e) => { + 'date': e.date, + 'views': e.views, + 'interactions': e.interactions, + 'favorites': e.favorites, + }, + ) + .toList(), + 'heatmap_days': heatmapDays + .map((e) => {'date': e.date, 'count': e.count, 'level': e.level}) + .toList(), + 'coin_overview': { + 'total_coins': coinOverview.totalCoins, + 'total_income': coinOverview.totalIncome, + 'total_expense': coinOverview.totalExpense, + 'today_income': coinOverview.todayIncome, + 'today_expense': coinOverview.todayExpense, + 'balance': coinOverview.balance, + }, + 'coin_sources': coinSources + .map( + (e) => {'source': e.source, 'amount': e.amount, 'count': e.count}, + ) + .toList(), + 'coin_trend': coinTrend + .map( + (e) => { + 'date': e.date, + 'coins': e.coins, + 'income': e.income, + 'expense': e.expense, + }, + ) + .toList(), + 'favorite_overview': { + 'total_favorites': favOverview.totalFavorites, + 'week_favorites': favOverview.weekFavorites, + 'today_favorites': favOverview.todayFavorites, + 'total_groups': favOverview.totalGroups, + }, + 'favorite_categories': favCategories + .map( + (e) => { + 'name': e.name, + 'count': e.count, + 'percentage': e.percentage, + }, + ) + .toList(), + 'favorite_groups': favGroups + .map((e) => {'group': e.group, 'count': e.count}) + .toList(), + 'favorite_trend': favTrend + .map((e) => {'date': e.date, 'favorites': e.favorites}) + .toList(), }); await AppKVStore.setString(_cacheKey, json); } catch (e) { @@ -444,6 +628,6 @@ class UserStatsNotifier extends StateNotifier { // Provider // ============================================================ -final userStatsProvider = StateNotifierProvider((ref) { - return UserStatsNotifier(ref); -}); +final userStatsProvider = NotifierProvider( + UserStatsNotifier.new, +); diff --git a/lib/features/study_plan/providers/study_plan_provider.dart b/lib/features/study_plan/providers/study_plan_provider.dart index 43ab96d9..a2f97953 100644 --- a/lib/features/study_plan/providers/study_plan_provider.dart +++ b/lib/features/study_plan/providers/study_plan_provider.dart @@ -13,8 +13,14 @@ import '../../../core/utils/logger.dart'; import '../models/study_plan_models.dart'; import '../services/study_plan_service.dart'; -class StudyPlanNotifier extends StateNotifier { - StudyPlanNotifier() : super(const StudyPlanState()); +class StudyPlanNotifier extends Notifier { + @override + StudyPlanState build() { + Future.microtask(loadPlans); + return const StudyPlanState(); + } + + StudyPlanNotifier(); Future loadPlans() async { state = state.copyWith(isLoading: true); @@ -49,7 +55,12 @@ class StudyPlanNotifier extends StateNotifier { } } - Future completeTask(int planId, {String? contentId, String? title, String? contentType}) async { + Future completeTask( + int planId, { + String? contentId, + String? title, + String? contentType, + }) async { await StudyPlanService.completeDailyGoal( planId, contentId: contentId, @@ -80,7 +91,6 @@ class StudyPlanNotifier extends StateNotifier { } } -final studyPlanProvider = - StateNotifierProvider( - (ref) => StudyPlanNotifier()..loadPlans(), +final studyPlanProvider = NotifierProvider( + StudyPlanNotifier.new, ); diff --git a/lib/features/task/providers/task_provider.dart b/lib/features/task/providers/task_provider.dart index 23abfc1e..66117a07 100644 --- a/lib/features/task/providers/task_provider.dart +++ b/lib/features/task/providers/task_provider.dart @@ -1,4 +1,4 @@ -/// @name 每日任务状态管理 +/// @name 每日任务状态管理 /// @date 2026-05-14 /// @desc Riverpod状态管理: 今日任务列表/进度上报/领取奖励/完美日 /// @update v12.0.1 修复: 类型转换错误, dynamic显式转换 @@ -121,10 +121,11 @@ class TaskState { } } -class TaskNotifier extends StateNotifier { - final TaskService _taskService; +class TaskNotifier extends Notifier { + @override + TaskState build() => const TaskState(); - TaskNotifier(this._taskService) : super(const TaskState()); + TaskService get _taskService => ref.read(taskServiceProvider); Future loadTodayTasks() async { state = state.copyWith(isLoading: true); @@ -173,6 +174,6 @@ class TaskNotifier extends StateNotifier { } } -final taskProvider = StateNotifierProvider((ref) { - return TaskNotifier(ref.read(taskServiceProvider)); -}); +final taskProvider = NotifierProvider( + TaskNotifier.new, +); diff --git a/lib/features/template/providers/wallpaper_provider.dart b/lib/features/template/providers/wallpaper_provider.dart index 96932dc7..334a8d75 100644 --- a/lib/features/template/providers/wallpaper_provider.dart +++ b/lib/features/template/providers/wallpaper_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 壁纸模板状态管理 /// 创建时间: 2026-05-01 /// 更新时间: 2026-05-01 @@ -47,26 +47,30 @@ class WallpaperState { bool? isLoading, String? searchQuery, bool clearSearch = false, - }) => - WallpaperState( - items: items ?? this.items, - currentSource: currentSource ?? this.currentSource, - currentCategory: currentCategory ?? this.currentCategory, - currentPage: currentPage ?? this.currentPage, - totalPages: totalPages ?? this.totalPages, - hasNext: hasNext ?? this.hasNext, - isLoading: isLoading ?? this.isLoading, - searchQuery: - clearSearch ? null : (searchQuery ?? this.searchQuery), - ); + }) => WallpaperState( + items: items ?? this.items, + currentSource: currentSource ?? this.currentSource, + currentCategory: currentCategory ?? this.currentCategory, + currentPage: currentPage ?? this.currentPage, + totalPages: totalPages ?? this.totalPages, + hasNext: hasNext ?? this.hasNext, + isLoading: isLoading ?? this.isLoading, + searchQuery: clearSearch ? null : (searchQuery ?? this.searchQuery), + ); } // ============================================================ // Notifier // ============================================================ -class WallpaperNotifier extends StateNotifier { - WallpaperNotifier() : super(const WallpaperState()); +class WallpaperNotifier extends Notifier { + @override + WallpaperState build() { + Future.microtask(loadWallpapers); + return const WallpaperState(); + } + + WallpaperNotifier(); Future loadWallpapers({bool refresh = false}) async { if (refresh) { @@ -135,7 +139,6 @@ class WallpaperNotifier extends StateNotifier { // Provider // ============================================================ -final wallpaperProvider = - StateNotifierProvider( - (ref) => WallpaperNotifier()..loadWallpapers(), +final wallpaperProvider = NotifierProvider( + WallpaperNotifier.new, ); diff --git a/lib/features/user_center/presentation/coin_log_page.dart b/lib/features/user_center/presentation/coin_log_page.dart index 8c06533d..44aa90bd 100644 --- a/lib/features/user_center/presentation/coin_log_page.dart +++ b/lib/features/user_center/presentation/coin_log_page.dart @@ -512,7 +512,7 @@ class _CoinTrendChart extends StatelessWidget { maxY: adjustedMax, barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( - tooltipRoundedRadius: 8, + tooltipBorderRadius: BorderRadius.circular(8), getTooltipItem: (group, groupIndex, rod, rodIndex) { final d = data[group.x.toInt()]; final label = rodIndex == 0 ? '收入' : '支出'; diff --git a/lib/features/user_center/presentation/learning_progress_page.dart b/lib/features/user_center/presentation/learning_progress_page.dart index e32193a1..278d88cd 100644 --- a/lib/features/user_center/presentation/learning_progress_page.dart +++ b/lib/features/user_center/presentation/learning_progress_page.dart @@ -382,15 +382,9 @@ class _LearningProgressPageState extends ConsumerState { ), ), titlesData: FlTitlesData( - leftTitles: const AxisTitles( - - ), - rightTitles: const AxisTitles( - - ), - topTitles: const AxisTitles( - - ), + leftTitles: const AxisTitles(), + rightTitles: const AxisTitles(), + topTitles: const AxisTitles(), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, @@ -402,7 +396,7 @@ class _LearningProgressPageState extends ConsumerState { return const SizedBox.shrink(); } return SideTitleWidget( - axisSide: meta.axisSide, + meta: meta, child: Text( labels[idx], style: AppTypography.caption2.copyWith( @@ -450,7 +444,7 @@ class _LearningProgressPageState extends ConsumerState { lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( getTooltipColor: (_) => ext.bgElevated, - tooltipRoundedRadius: AppRadius.md, + tooltipBorderRadius: BorderRadius.circular(AppRadius.md), getTooltipItems: (touchedSpots) { return touchedSpots.map((spot) { return LineTooltipItem( @@ -506,7 +500,11 @@ class _LearningProgressPageState extends ConsumerState { value: 2, color: CupertinoColors.systemOrange, ), - const _CategoryItem(name: '名言', value: 4, color: CupertinoColors.systemGreen), + const _CategoryItem( + name: '名言', + value: 4, + color: CupertinoColors.systemGreen, + ), ]; } @@ -550,7 +548,7 @@ class _LearningProgressPageState extends ConsumerState { barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( getTooltipColor: (_) => ext.bgElevated, - tooltipRoundedRadius: AppRadius.md, + tooltipBorderRadius: BorderRadius.circular(AppRadius.md), getTooltipItem: (group, groupIndex, rod, rodIndex) { if (groupIndex >= categories.length) return null; return BarTooltipItem( @@ -564,15 +562,9 @@ class _LearningProgressPageState extends ConsumerState { ), ), titlesData: FlTitlesData( - leftTitles: const AxisTitles( - - ), - rightTitles: const AxisTitles( - - ), - topTitles: const AxisTitles( - - ), + leftTitles: const AxisTitles(), + rightTitles: const AxisTitles(), + topTitles: const AxisTitles(), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, @@ -583,7 +575,7 @@ class _LearningProgressPageState extends ConsumerState { return const SizedBox.shrink(); } return SideTitleWidget( - axisSide: meta.axisSide, + meta: meta, child: Text( categories[idx].name, style: AppTypography.caption2.copyWith( diff --git a/lib/features/user_center/presentation/my_devices_page.dart b/lib/features/user_center/presentation/my_devices_page.dart index 2304cc77..346ef09e 100644 --- a/lib/features/user_center/presentation/my_devices_page.dart +++ b/lib/features/user_center/presentation/my_devices_page.dart @@ -1,7 +1,7 @@ /// ============================================================ /// 闲言APP — 我的设备页面 /// 创建时间: 2026-05-10 -/// 更新时间: 2026-05-13 +/// 更新时间: 2026-05-16 /// 作用: 查看和管理用户登录设备,支持在线状态/下线/移除/IP归属地显示 /// 上次更新: v10.2.0 修复4项问题: 过滤未知设备/简化卡片+详情Sheet/路由修复/下线退出登录 /// ============================================================ @@ -48,7 +48,7 @@ class _MyDevicesPageState extends ConsumerState { if (!canAuth) return true; return await _localAuth.authenticate( localizedReason: reason, - options: const AuthenticationOptions(stickyAuth: true), + persistAcrossBackgrounding: true, ); } catch (e) { return true; @@ -881,6 +881,8 @@ class _MyDevicesPageState extends ConsumerState { return 'iOS'; case 'android': return 'Android'; + case 'ohos': + return 'HarmonyOS'; case 'web': return 'Web'; case 'windows': @@ -904,6 +906,8 @@ class _MyDevicesPageState extends ConsumerState { return CupertinoIcons.device_phone_portrait; case 'android': return CupertinoIcons.device_phone_portrait; + case 'ohos': + return CupertinoIcons.device_phone_portrait; case 'web': return CupertinoIcons.globe; case 'windows': @@ -924,6 +928,8 @@ class _MyDevicesPageState extends ConsumerState { return CupertinoColors.systemBlue; case 'android': return CupertinoColors.systemGreen; + case 'ohos': + return CupertinoColors.systemRed; case 'web': return CupertinoColors.systemOrange; case 'windows': @@ -1006,7 +1012,7 @@ class _MyDevicesPageState extends ConsumerState { await ref.read(deviceProvider.notifier).offlineDevice(device.id); await _saveLastLoginAccount(); await ref.read(authProvider.notifier).logout(); - if (!context.mounted) return; + if (!mounted || !context.mounted) return; AppToast.showSuccess('设备已下线,请重新登录'); context.go(AppRoutes.login); } catch (e) { diff --git a/lib/features/user_center/presentation/widgets/learning_charts.dart b/lib/features/user_center/presentation/widgets/learning_charts.dart index ceb24dd0..201ae123 100644 --- a/lib/features/user_center/presentation/widgets/learning_charts.dart +++ b/lib/features/user_center/presentation/widgets/learning_charts.dart @@ -153,7 +153,7 @@ class WeeklyTrendChart extends StatelessWidget { ], lineTouchData: LineTouchData( touchTooltipData: LineTouchTooltipData( - tooltipRoundedRadius: 8, + tooltipBorderRadius: BorderRadius.circular(8), getTooltipItems: (spots) => spots .map( (s) => LineTooltipItem( diff --git a/lib/features/user_center/providers/coin_provider.dart b/lib/features/user_center/providers/coin_provider.dart index 6eac8386..48480e46 100644 --- a/lib/features/user_center/providers/coin_provider.dart +++ b/lib/features/user_center/providers/coin_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 金币记录状态管理 /// 创建时间: 2026-04-29 /// 更新时间: 2026-04-29 @@ -85,8 +85,10 @@ class CoinState { } } -class CoinNotifier extends StateNotifier { - CoinNotifier() : super(const CoinState()); +class CoinNotifier extends Notifier { + @override + CoinState build() => const CoinState(); + CoinNotifier(); Future loadCoinLog({bool refresh = false}) async { if (state.isLoading) return; @@ -114,6 +116,4 @@ class CoinNotifier extends StateNotifier { } } -final coinProvider = StateNotifierProvider((ref) { - return CoinNotifier(); -}); +final coinProvider = NotifierProvider(CoinNotifier.new); diff --git a/lib/features/user_center/providers/dashboard_provider.dart b/lib/features/user_center/providers/dashboard_provider.dart index 94448759..dfb616bd 100644 --- a/lib/features/user_center/providers/dashboard_provider.dart +++ b/lib/features/user_center/providers/dashboard_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 数据面板状态管理 /// 创建时间: 2026-04-29 /// 更新时间: 2026-04-29 @@ -48,8 +48,10 @@ class DashboardState { } } -class DashboardNotifier extends StateNotifier { - DashboardNotifier() : super(const DashboardState()); +class DashboardNotifier extends Notifier { + @override + DashboardState build() => const DashboardState(); + DashboardNotifier(); /// 加载数据面板 Future loadDashboard() async { @@ -121,6 +123,4 @@ class DashboardNotifier extends StateNotifier { } final dashboardProvider = - StateNotifierProvider((ref) { - return DashboardNotifier(); -}); + NotifierProvider(DashboardNotifier.new); diff --git a/lib/features/user_center/providers/device_provider.dart b/lib/features/user_center/providers/device_provider.dart index 78c664ab..e1abb05f 100644 --- a/lib/features/user_center/providers/device_provider.dart +++ b/lib/features/user_center/providers/device_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 设备管理状态管理 /// 创建时间: 2026-05-10 /// 更新时间: 2026-05-13 @@ -45,8 +45,10 @@ class DeviceState { } } -class DeviceNotifier extends StateNotifier { - DeviceNotifier() : super(const DeviceState()); +class DeviceNotifier extends Notifier { + @override + DeviceState build() => const DeviceState(); + DeviceNotifier(); Future loadDevices({bool refresh = false}) async { if (state.isLoading) return; @@ -146,8 +148,6 @@ class DeviceNotifier extends StateNotifier { } } -final deviceProvider = StateNotifierProvider(( - ref, -) { - return DeviceNotifier(); -}); +final deviceProvider = NotifierProvider( + DeviceNotifier.new, +); diff --git a/lib/features/user_center/providers/interaction_provider.dart b/lib/features/user_center/providers/interaction_provider.dart index 28f8d8e1..327ea758 100644 --- a/lib/features/user_center/providers/interaction_provider.dart +++ b/lib/features/user_center/providers/interaction_provider.dart @@ -1,4 +1,4 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 通用互动状态管理 /// 创建时间: 2026-04-29 /// 更新时间: 2026-05-10 @@ -59,8 +59,10 @@ class InteractionState { } } -class InteractionNotifier extends StateNotifier { - InteractionNotifier() : super(const InteractionState()); +class InteractionNotifier extends Notifier { + @override + InteractionState build() => const InteractionState(); + InteractionNotifier(); /// 切换类互动 (like/dislike/readlater/share/block/bookmark/collect) Future?> toggleInteraction({ @@ -428,6 +430,4 @@ class InteractionNotifier extends StateNotifier { } final interactionProvider = - StateNotifierProvider((ref) { - return InteractionNotifier(); - }); + NotifierProvider(InteractionNotifier.new); diff --git a/lib/features/user_center/providers/learning_progress_provider.dart b/lib/features/user_center/providers/learning_progress_provider.dart index a239619f..d8ba6002 100644 --- a/lib/features/user_center/providers/learning_progress_provider.dart +++ b/lib/features/user_center/providers/learning_progress_provider.dart @@ -55,8 +55,10 @@ class LearningProgressState { } } -class LearningProgressNotifier extends StateNotifier { - LearningProgressNotifier() : super(const LearningProgressState()); +class LearningProgressNotifier extends Notifier { + @override + LearningProgressState build() => const LearningProgressState(); + LearningProgressNotifier(); Future loadAll() async { state = state.copyWith(isLoading: true, clearError: true); @@ -117,8 +119,6 @@ class LearningProgressNotifier extends StateNotifier { } final learningProgressProvider = - StateNotifierProvider(( - ref, - ) { - return LearningProgressNotifier(); - }); + NotifierProvider( + LearningProgressNotifier.new, + ); diff --git a/lib/features/user_center/providers/tag_cloud_provider.dart b/lib/features/user_center/providers/tag_cloud_provider.dart index 67971de2..42bd76b8 100644 --- a/lib/features/user_center/providers/tag_cloud_provider.dart +++ b/lib/features/user_center/providers/tag_cloud_provider.dart @@ -6,6 +6,7 @@ /// 上次更新: 初始创建 /// ============================================================ +import 'package:xianyan/core/utils/pattern_utils.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pinyin/pinyin.dart'; @@ -72,8 +73,10 @@ class TagCloudState { } } -class TagCloudNotifier extends StateNotifier { - TagCloudNotifier() : super(const TagCloudState()); +class TagCloudNotifier extends Notifier { + @override + TagCloudState build() => const TagCloudState(); + TagCloudNotifier(); Future loadTags() async { state = state.copyWith(isLoading: true, clearError: true); @@ -149,7 +152,7 @@ class TagCloudNotifier extends StateNotifier { String _getPinyinInitial(String text) { if (text.isEmpty) return '#'; final first = text[0]; - if (RegExp(r'[a-zA-Z]').hasMatch(first)) { + if (regex(r'[a-zA-Z]').hasMatch(first)) { return first.toUpperCase(); } final pinyin = PinyinHelper.getPinyinE(first); @@ -161,6 +164,4 @@ class TagCloudNotifier extends StateNotifier { } final tagCloudProvider = - StateNotifierProvider((_) { - return TagCloudNotifier(); -}); + NotifierProvider(TagCloudNotifier.new); diff --git a/lib/features/weather/providers/weather_provider.dart b/lib/features/weather/providers/weather_provider.dart index a53151a0..2c9f3a7c 100644 --- a/lib/features/weather/providers/weather_provider.dart +++ b/lib/features/weather/providers/weather_provider.dart @@ -1,9 +1,9 @@ -/// ============================================================ +/// ============================================================ /// 闲言APP — 天气状态管理 /// 创建时间: 2026-05-02 -/// 更新时间: 2026-05-02 +/// 更新时间: 2026-05-16 /// 作用: 天气数据 + 天气-诗词关联推荐状态 -/// 上次更新: 移除city参数,改用SDK info接口 +/// 上次更新: 迁移 Riverpod 3.x — StateNotifier → Notifier /// ============================================================ import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -56,9 +56,11 @@ class WeatherState { } } -class WeatherNotifier extends StateNotifier { - WeatherNotifier() : super(const WeatherState()) { - loadWeather(); +class WeatherNotifier extends Notifier { + @override + WeatherState build() { + Future.microtask(loadWeather); + return const WeatherState(); } Future loadWeather() async { @@ -86,6 +88,6 @@ class WeatherNotifier extends StateNotifier { } } -final weatherProvider = StateNotifierProvider( - (ref) => WeatherNotifier(), +final weatherProvider = NotifierProvider( + WeatherNotifier.new, ); diff --git a/lib/main.dart b/lib/main.dart index eb330e30..0d97a5ef 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,14 @@ // ============================================================ // 闲言APP — 应用入口 // 创建时间: 2026-04-20 -// 更新时间: 2026-05-15 +// 更新时间: 2026-05-17 // 作用: main 函数,初始化存储 + 液态玻璃 + 异常捕获 + 启动 App -// 上次更新: v5.8 新增HomeWidgetService/ClipboardMonitorService初始化 +// 上次更新: v5.9 新增全局错误捕获(Isolate/Flutter/Zone三层防护)定位鸿蒙白屏 // ============================================================ +import 'dart:async'; +import 'dart:isolate'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:catcher_2/catcher_2.dart'; @@ -34,129 +37,170 @@ import 'features/inspiration/services/chat_migration_service.dart'; bool kvStorageReady = false; void main() async { - WidgetsFlutterBinding.ensureInitialized(); + runZonedGuarded( + () async { + WidgetsFlutterBinding.ensureInitialized(); - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - statusBarColor: Colors.black, - statusBarIconBrightness: Brightness.light, - statusBarBrightness: Brightness.dark, - systemNavigationBarColor: Colors.black, - systemNavigationBarIconBrightness: Brightness.light, - systemNavigationBarDividerColor: Colors.black, - ), - ); + FlutterError.onError = (FlutterErrorDetails details) { + FlutterError.presentError(details); + Log.e( + '🔥 FlutterError.onError', + details.exceptionAsString(), + details.stack, + ); + }; - try { - await KvStorage.init(); - kvStorageReady = true; - Log.i('KV 存储初始化完成'); - } catch (e) { - Log.e('KV 存储初始化失败', e); - } + Isolate.current.addErrorListener( + RawReceivePort((Object? pair) { + final List errorAndStacktrace = pair as List; + Log.e( + '🔥 Isolate uncaught error', + errorAndStacktrace.first, + StackTrace.fromString(errorAndStacktrace.last.toString()), + ); + }).sendPort, + ); - try { - await AppKVStore.init(); - Log.i('AppKVStore (Hive) 初始化完成'); - } catch (e) { - Log.e('AppKVStore 初始化失败', e); - } - - await LiquidGlassWidgets.initialize(); - Log.i('LiquidGlassWidgets 初始化完成'); - - await Platform3DService.instance.detectDeviceCapability(); - Log.i('3D平台设备检测完成 (lowEnd=${Platform3DService.instance.isLowEnd})'); - - _validatePageRegistry(); - - try { - await DeepLinkService.init(); - Log.i('深度链接服务初始化完成'); - } catch (e) { - Log.e('深度链接服务初始化失败', e); - } - - try { - SharingReceiverService().init(); - SharingReceiverService().setNavigatorKey(rootNavigatorKey); - Log.i('分享接收服务初始化完成'); - } catch (e) { - Log.e('分享接收服务初始化失败', e); - } - - try { - await LocalNotificationService.init(); - Log.i('本地通知服务初始化完成'); - } catch (e) { - Log.e('本地通知服务初始化失败', e); - } - - try { - await ScreenWakeService.init(); - Log.i('屏幕常亮服务初始化完成'); - } catch (e) { - Log.e('屏幕常亮服务初始化失败', e); - } - - try { - await SoundService.init(); - Log.i('音效服务初始化完成'); - } catch (e) { - Log.e('音效服务初始化失败', e); - } - - try { - await BatteryOptimizationService.init(); - Log.i('电池优化服务初始化完成'); - } catch (e) { - Log.e('电池优化服务初始化失败', e); - } - - try { - await ReadlaterReminderService.startMonitoring(); - Log.i('稍后读提醒服务初始化完成'); - } catch (e) { - Log.e('稍后读提醒服务初始化失败', e); - } - - try { - await ChatMigrationService.migrateIfNeeded(); - Log.i('聊天数据迁移检查完成'); - } catch (e) { - Log.e('聊天数据迁移检查失败', e); - } - - try { - await HomeWidgetService.instance.init(); - Log.i('桌面小组件服务初始化完成'); - } catch (e) { - Log.e('桌面小组件服务初始化失败', e); - } - - try { - await ClipboardMonitorService.instance.initFromStore(); - Log.i('剪贴板监控服务初始化完成'); - } catch (e) { - Log.e('剪贴板监控服务初始化失败', e); - } - - Catcher2( - runAppFunction: () { - runApp( - LiquidGlassWidgets.wrap( - child: const ProviderScope(child: XianyanApp()), + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Colors.black, + statusBarIconBrightness: Brightness.light, + statusBarBrightness: Brightness.dark, + systemNavigationBarColor: Colors.black, + systemNavigationBarIconBrightness: Brightness.light, + systemNavigationBarDividerColor: Colors.black, ), ); + + try { + await KvStorage.init(); + kvStorageReady = true; + Log.i('KV 存储初始化完成'); + } catch (e, st) { + Log.e('KV 存储初始化失败', e, st); + } + + try { + await AppKVStore.init(); + Log.i('AppKVStore (Hive) 初始化完成'); + } catch (e, st) { + Log.e('AppKVStore 初始化失败', e, st); + } + + try { + await LiquidGlassWidgets.initialize(); + Log.i('LiquidGlassWidgets 初始化完成'); + } catch (e, st) { + Log.e('LiquidGlassWidgets 初始化失败', e, st); + } + + try { + await Platform3DService.instance.detectDeviceCapability(); + Log.i('3D平台设备检测完成 (lowEnd=${Platform3DService.instance.isLowEnd})'); + } catch (e, st) { + Log.e('3D平台设备检测失败', e, st); + } + + try { + _validatePageRegistry(); + } catch (e, st) { + Log.e('页面注册表验证失败', e, st); + } + + try { + await DeepLinkService.init(); + Log.i('深度链接服务初始化完成'); + } catch (e, st) { + Log.e('深度链接服务初始化失败', e, st); + } + + try { + SharingReceiverService().init(); + SharingReceiverService().setNavigatorKey(rootNavigatorKey); + Log.i('分享接收服务初始化完成'); + } catch (e, st) { + Log.e('分享接收服务初始化失败', e, st); + } + + try { + await LocalNotificationService.init(); + Log.i('本地通知服务初始化完成'); + } catch (e, st) { + Log.e('本地通知服务初始化失败', e, st); + } + + try { + await ScreenWakeService.init(); + Log.i('屏幕常亮服务初始化完成'); + } catch (e, st) { + Log.e('屏幕常亮服务初始化失败', e, st); + } + + try { + await SoundService.init(); + Log.i('音效服务初始化完成'); + } catch (e, st) { + Log.e('音效服务初始化失败', e, st); + } + + try { + await BatteryOptimizationService.init(); + Log.i('电池优化服务初始化完成'); + } catch (e, st) { + Log.e('电池优化服务初始化失败', e, st); + } + + 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); + } + + try { + await HomeWidgetService.instance.init(); + Log.i('桌面小组件服务初始化完成'); + } catch (e, st) { + Log.e('桌面小组件服务初始化失败', e, st); + } + + try { + await ClipboardMonitorService.instance.initFromStore(); + Log.i('剪贴板监控服务初始化完成'); + } catch (e, st) { + Log.e('剪贴板监控服务初始化失败', e, st); + } + + Catcher2( + runAppFunction: () { + runApp( + LiquidGlassWidgets.wrap( + child: const ProviderScope(child: XianyanApp()), + ), + ); + }, + debugConfig: Catcher2Options( + SilentReportMode(), + [_RateLimitedHandler()], + localizationOptions: [ + LocalizationOptions.buildDefaultChineseOptions(), + ], + ), + releaseConfig: Catcher2Options(SilentReportMode(), []), + profileConfig: Catcher2Options(SilentReportMode(), []), + ); + }, + (Object error, StackTrace stack) { + Log.e('🔥 Zone uncaught error', error, stack); }, - debugConfig: Catcher2Options( - SilentReportMode(), - [_RateLimitedHandler()], - localizationOptions: [LocalizationOptions.buildDefaultChineseOptions()], - ), - releaseConfig: Catcher2Options(SilentReportMode(), []), - profileConfig: Catcher2Options(SilentReportMode(), []), ); } diff --git a/lib/shared/widgets/keyboard_safe_sheet.dart b/lib/shared/widgets/keyboard_safe_sheet.dart index 4fa53843..03094cd9 100644 --- a/lib/shared/widgets/keyboard_safe_sheet.dart +++ b/lib/shared/widgets/keyboard_safe_sheet.dart @@ -1,27 +1,18 @@ // ============================================================ // 闲言APP — 键盘适配工具 // 创建时间: 2026-05-02 -// 更新时间: 2026-05-02 -// 作用: 解决输入法面板遮挡底部Sheet/弹窗的通用方案 -// 上次更新: 初始创建 — KeyboardSafeSheet + KeyboardSafeBuilder +// 更新时间: 2026-05-15 +// 作用: 解决输入法面板遮挡底部Sheet/弹窗的通用方案 + 全局键盘管理 +// 上次更新: v13.2.0 新增KeyboardManager全局键盘管理器,防止意外弹出 // ============================================================ import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; /// 键盘安全底部弹窗包装器 /// /// 用于 `showCupertinoModalPopup` / `showModalBottomSheet` 等场景, /// 自动感知键盘高度并推高内容,避免输入框被遮挡。 -/// -/// 用法: -/// ```dart -/// showCupertinoModalPopup( -/// context: context, -/// builder: (ctx) => KeyboardSafeSheet( -/// child: YourContent(), -/// ), -/// ); -/// ``` class KeyboardSafeSheet extends StatelessWidget { const KeyboardSafeSheet({ super.key, @@ -33,13 +24,9 @@ class KeyboardSafeSheet extends StatelessWidget { }); final Widget child; - final Color? backgroundColor; - final double topRadius; - final bool scrollable; - final EdgeInsetsGeometry? padding; @override @@ -80,16 +67,6 @@ class KeyboardSafeSheet extends StatelessWidget { /// /// 用于需要更细粒度控制的场景,只提供 `AnimatedPadding` + `viewInsets`, /// 不包裹 Container/ScrollView,由调用方自行决定布局。 -/// -/// 用法: -/// ```dart -/// showCupertinoModalPopup( -/// context: context, -/// builder: (ctx) => KeyboardSafeBuilder( -/// builder: (ctx, keyboardHeight) => YourWidget(), -/// ), -/// ); -/// ``` class KeyboardSafeBuilder extends StatelessWidget { const KeyboardSafeBuilder({super.key, required this.builder}); @@ -107,25 +84,95 @@ class KeyboardSafeBuilder extends StatelessWidget { } } +/// 全局键盘管理器 +/// +/// 核心职责: +/// 1. 防止输入法意外弹出(页面切换、返回、按钮点击等场景) +/// 2. 提供统一的收起键盘方法 +/// 3. 跟踪用户是否主动点击了输入框 +/// 4. 在页面切换时自动收起键盘 +/// +/// 用法: +/// ```dart +/// // 在页面 initState 中注册 +/// KeyboardManager.instance.registerPage(pageKey); +/// +/// // 在 dispose 中注销 +/// KeyboardManager.instance.unregisterPage(pageKey); +/// +/// // 用户主动点击输入框时标记 +/// KeyboardManager.instance.markUserTapped(pageKey); +/// +/// // 页面切换/返回时强制收起 +/// KeyboardManager.instance.dismissForPage(pageKey); +/// ``` +class KeyboardManager { + KeyboardManager._(); + static final KeyboardManager instance = KeyboardManager._(); + + final Set _activePages = {}; + final Set _userTappedPages = {}; + + bool _isUserTapped(String pageKey) => _userTappedPages.contains(pageKey); + + void registerPage(String pageKey) { + _activePages.add(pageKey); + } + + void unregisterPage(String pageKey) { + _activePages.remove(pageKey); + _userTappedPages.remove(pageKey); + } + + void markUserTapped(String pageKey) { + _userTappedPages.add(pageKey); + } + + void clearUserTapped(String pageKey) { + _userTappedPages.remove(pageKey); + } + + bool canShowKeyboard(String pageKey) { + return _isUserTapped(pageKey); + } + + void dismissForPage(String pageKey) { + _userTappedPages.remove(pageKey); + dismissAll(); + } + + static void dismissAll() { + SystemChannels.textInput.invokeMethod('TextInput.hide'); + } + + static void unfocusScope(BuildContext context) { + FocusScope.of(context).unfocus(); + FocusManager.instance.primaryFocus?.unfocus(); + } + + static bool isKeyboardVisible(BuildContext context) { + return MediaQuery.of(context).viewInsets.bottom > 0; + } + + static double keyboardHeight(BuildContext context) { + return MediaQuery.of(context).viewInsets.bottom; + } +} + /// 键盘适配工具方法 /// /// 提供静态方法,用于在已有弹窗代码中快速添加键盘适配。 class KeyboardSafe { KeyboardSafe._(); - /// 获取当前键盘高度 static double keyboardHeight(BuildContext context) { return MediaQuery.of(context).viewInsets.bottom; } - /// 判断键盘是否弹出 static bool isKeyboardVisible(BuildContext context) { return MediaQuery.of(context).viewInsets.bottom > 0; } - /// 包装 showCupertinoModalPopup 的通用方法 - /// - /// 自动处理键盘遮挡,内部使用 [KeyboardSafeSheet] 包裹内容。 static Future showSheet({ required BuildContext context, required WidgetBuilder builder, @@ -144,3 +191,93 @@ class KeyboardSafe { ); } } + +/// 防止意外聚焦的 TextField 包装器 +/// +/// 在 onTap 中自动标记用户主动点击,防止输入法意外弹出。 +/// 配合 KeyboardManager 使用,确保只有用户主动点击才弹出键盘。 +class ManagedCupertinoTextField extends StatefulWidget { + const ManagedCupertinoTextField({ + super.key, + required this.controller, + required this.focusNode, + required this.pageKey, + this.placeholder, + this.placeholderStyle, + this.style, + this.decoration, + this.padding, + this.maxLines = 1, + this.minLines = 1, + this.textInputAction, + this.onSubmitted, + this.onChanged, + }); + + final TextEditingController controller; + final FocusNode focusNode; + final String pageKey; + final String? placeholder; + final TextStyle? placeholderStyle; + final TextStyle? style; + final BoxDecoration? decoration; + final EdgeInsetsGeometry? padding; + final int maxLines; + final int minLines; + final TextInputAction? textInputAction; + final ValueChanged? onSubmitted; + final ValueChanged? onChanged; + + @override + State createState() => + _ManagedCupertinoTextFieldState(); +} + +class _ManagedCupertinoTextFieldState extends State { + @override + void initState() { + super.initState(); + widget.focusNode.addListener(_onFocusChange); + } + + @override + void dispose() { + widget.focusNode.removeListener(_onFocusChange); + super.dispose(); + } + + void _onFocusChange() { + if (widget.focusNode.hasFocus) { + if (!KeyboardManager.instance.canShowKeyboard(widget.pageKey)) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.focusNode.hasFocus) { + widget.focusNode.unfocus(); + } + }); + } + } + } + + @override + Widget build(BuildContext context) { + return CupertinoTextField( + controller: widget.controller, + focusNode: widget.focusNode, + onTap: () { + KeyboardManager.instance.markUserTapped(widget.pageKey); + }, + placeholder: widget.placeholder, + placeholderStyle: widget.placeholderStyle, + style: widget.style, + decoration: widget.decoration, + padding: + widget.padding ?? + const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + maxLines: widget.maxLines, + minLines: widget.minLines, + textInputAction: widget.textInputAction, + onSubmitted: widget.onSubmitted, + onChanged: widget.onChanged, + ); + } +} diff --git a/lib/shared/widgets/share_sheet.dart b/lib/shared/widgets/share_sheet.dart index 7c98aa9b..cb564030 100644 --- a/lib/shared/widgets/share_sheet.dart +++ b/lib/shared/widgets/share_sheet.dart @@ -9,6 +9,8 @@ import 'dart:io'; import 'dart:ui' as ui; +import 'package:xianyan/core/utils/platform_utils.dart' as pu; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -774,10 +776,12 @@ class _ShareSheetContentState extends State<_ShareSheetContent> { Future _systemShare() async { final box = context.findRenderObject() as RenderBox?; - await Share.share( - data.shareText, - subject: data.text, - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + await SharePlus.instance.share( + ShareParams( + text: data.shareText, + subject: data.text, + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ), ); _emitResult(ShareResultType.systemShare); } @@ -797,7 +801,7 @@ class _ShareSheetContentState extends State<_ShareSheetContent> { return; } - if (Platform.isAndroid || Platform.isIOS) { + if (pu.isOhos || Platform.isAndroid || Platform.isIOS) { final file = XFile.fromData( byteData.buffer.asUint8List(), name: 'xianyan_share_qr.png', @@ -805,10 +809,12 @@ class _ShareSheetContentState extends State<_ShareSheetContent> { ); if (!mounted) return; final box = context.findRenderObject() as RenderBox?; - await Share.shareXFiles( - [file], - text: '来自「闲言」', - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + await SharePlus.instance.share( + ShareParams( + files: [file], + text: '来自「闲言」', + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ), ); _emitResult(ShareResultType.savedQrCode); AppToast.showSuccess('💾 二维码已分享'); @@ -835,7 +841,7 @@ class _ShareSheetContentState extends State<_ShareSheetContent> { return; } - if (Platform.isAndroid || Platform.isIOS) { + if (pu.isOhos || Platform.isAndroid || Platform.isIOS) { final file = XFile.fromData( byteData.buffer.asUint8List(), name: 'xianyan_card_${data.id ?? 'share'}.png', @@ -843,10 +849,12 @@ class _ShareSheetContentState extends State<_ShareSheetContent> { ); if (!mounted) return; final box = context.findRenderObject() as RenderBox?; - await Share.shareXFiles( - [file], - text: data.shareText, - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + await SharePlus.instance.share( + ShareParams( + files: [file], + text: data.shareText, + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ), ); _emitResult(ShareResultType.savedCardImage); AppToast.showSuccess('🎴 卡片图片已分享'); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 943e8bc2..5316d619 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -24,7 +24,6 @@ import mobile_scanner import nearby_service import network_info_plus import package_info_plus -import path_provider_foundation import pro_image_editor import quill_native_bridge_macos import record_macos @@ -57,7 +56,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { NearbyServicePlugin.register(with: registry.registrar(forPlugin: "NearbyServicePlugin")) NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ProImageEditorPlugin.register(with: registry.registrar(forPlugin: "ProImageEditorPlugin")) QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin")) RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin")) @@ -67,6 +65,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin")) - FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) + VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) } diff --git a/ohos/build-profile.json5 b/ohos/build-profile.json5 index 1c112b1a..11925c58 100644 --- a/ohos/build-profile.json5 +++ b/ohos/build-profile.json5 @@ -1,13 +1,27 @@ - { "app": { - "signingConfigs": [], + "signingConfigs": [ + { + "name": "default", + "type": "HarmonyOS", + "material": { + "certpath": "C:\\Users\\无书\\.ohos\\config\\default_ohos_Ww-qDb6KtzkF7HlojhJDvZb4Nzz3gHvGkgTB7iphnKU=.cer", + "keyAlias": "debugKey", + "keyPassword": "0000001B5A1B1010A7B346A046653809382EEDFC8ABB419D35BF4F6E11741342CDE0964A6A2F612D5EB757", + "profile": "C:\\Users\\无书\\.ohos\\config\\default_ohos_Ww-qDb6KtzkF7HlojhJDvZb4Nzz3gHvGkgTB7iphnKU=.p7b", + "signAlg": "SHA256withECDSA", + "storeFile": "C:\\Users\\无书\\.ohos\\config\\default_ohos_Ww-qDb6KtzkF7HlojhJDvZb4Nzz3gHvGkgTB7iphnKU=.p12", + "storePassword": "0000001BA3D4DCDFE0722C14970796CBEF9FCBA1FA2758B0E2BD59CD0AFEDD11FF80BF641298F249381321" + } + } + ], "products": [ { "name": "default", "signingConfig": "default", "compatibleSdkVersion": "5.1.0(18)", "runtimeOS": "HarmonyOS", + "targetSdkVersion": "6.1.0(23)" } ], "buildModeSet": [ diff --git a/ohos/entry/src/main/ets/entryability/EntryAbility.ets b/ohos/entry/src/main/ets/entryability/EntryAbility.ets index f85a6550..a7cca837 100644 --- a/ohos/entry/src/main/ets/entryability/EntryAbility.ets +++ b/ohos/entry/src/main/ets/entryability/EntryAbility.ets @@ -1,10 +1,85 @@ - import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos'; import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant'; +import Want from '@ohos.app.ability.Want'; +import AbilityConstant from '@ohos.app.ability.AbilityConstant'; +import MethodChannel from '@ohos/flutter_ohos/src/main/ets/plugin/common/MethodChannel'; +import MethodCall from '@ohos/flutter_ohos/src/main/ets/plugin/common/MethodCall'; +import { MethodResult } from '@ohos/flutter_ohos/src/main/ets/plugin/common/MethodChannel'; +import { pasteboard } from '@kit.BasicServicesKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { componentUtils } from '@kit.ArkUI'; +import { common } from '@kit.AbilityKit'; + +const CLIPBOARD_CHANNEL = 'plugins.flutter.io/clipboard_ohos'; export default class EntryAbility extends FlutterAbility { + private clipboardChannel: MethodChannel | null = null; + configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) GeneratedPluginRegistrant.registerWith(flutterEngine) + + this.clipboardChannel = new MethodChannel(flutterEngine.dartExecutor.getBinaryMessenger(), CLIPBOARD_CHANNEL); + this.clipboardChannel.setMethodCallHandler({ + onMethodCall: (call: MethodCall, result: MethodResult): void => { + switch (call.method) { + case 'Clipboard.getData': + this.getClipboardData(result); + break; + case 'Clipboard.hasStrings': + this.hasClipboardStrings(result); + break; + default: + result.notImplemented(); + break; + } + } + }); } + + private getClipboardData(result: MethodResult): void { + try { + const pasteData = pasteboard.getSystemPasteboard().getDataSync(); + if (pasteData && pasteData.getRecordCount() > 0) { + const record = pasteData.getRecordAt(0); + const text = record.plainText; + result.success(text ?? ''); + } else { + result.success(''); + } + } catch (e) { + const err = e as BusinessError; + console.error(`Clipboard getData error: ${err.code} ${err.message}`); + result.success(''); + } + } + + private hasClipboardStrings(result: MethodResult): void { + try { + const pasteData = pasteboard.getSystemPasteboard().getDataSync(); + const hasStrings = pasteData && pasteData.hasType(pasteboard.MIMETYPE_TEXT_PLAIN); + result.success(hasStrings); + } catch (e) { + result.success(false); + } + } + + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { + super.onCreate(want, launchParam); + const uri = want?.parameters?.['uri'] as string ?? want?.uri; + if (uri && typeof uri === 'string' && uri.length > 0) { + this.initialUri = uri; + } + } + + onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void { + super.onNewWant(want, launchParam); + const uri = want?.parameters?.['uri'] as string ?? want?.uri; + if (uri && typeof uri === 'string' && uri.length > 0) { + this.latestUri = uri; + } + } + + private initialUri: string | null = null; + private latestUri: string | null = null; } diff --git a/ohos/entry/src/main/ets/pages/Index.ets b/ohos/entry/src/main/ets/pages/Index.ets index 7bb6543f..b698ba40 100644 --- a/ohos/entry/src/main/ets/pages/Index.ets +++ b/ohos/entry/src/main/ets/pages/Index.ets @@ -15,6 +15,8 @@ struct Index { Column() { FlutterPage({ viewId: this.viewId }) } + .width('100%') + .height('100%') } onBackPress(): boolean { diff --git a/ohos/entry/src/main/module.json5 b/ohos/entry/src/main/module.json5 index e68e30ce..b6348be0 100644 --- a/ohos/entry/src/main/module.json5 +++ b/ohos/entry/src/main/module.json5 @@ -6,7 +6,8 @@ "description": "$string:module_desc", "mainElement": "EntryAbility", "deviceTypes": [ - "phone" + "phone", + "tablet" ], "deliveryWithInstall": true, "installationFree": false, @@ -51,16 +52,16 @@ } }, { - "name": "ohos.permission.READ_WRITE_DOWNLOAD_DIRECTORY", - "reason": "$string:permission_storage_reason", + "name": "ohos.permission.GET_WIFI_INFO", + "reason": "$string:permission_wifi_info_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }, { - "name": "ohos.permission.FILE_ACCESS_PERSIST", - "reason": "$string:permission_file_access_reason", + "name": "ohos.permission.SET_WIFI_INFO", + "reason": "$string:permission_wifi_info_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" @@ -73,6 +74,62 @@ "abilities": ["EntryAbility"], "when": "inuse" } + }, + { + "name": "ohos.permission.CAMERA", + "reason": "$string:permission_camera_reason", + "usedScene": { + "abilities": ["EntryAbility"], + "when": "inuse" + } + }, + { + "name": "ohos.permission.MICROPHONE", + "reason": "$string:permission_microphone_reason", + "usedScene": { + "abilities": ["EntryAbility"], + "when": "inuse" + } + }, + { + "name": "ohos.permission.LOCATION", + "reason": "$string:permission_location_reason", + "usedScene": { + "abilities": ["EntryAbility"], + "when": "inuse" + } + }, + { + "name": "ohos.permission.APPROXIMATELY_LOCATION", + "reason": "$string:permission_location_reason", + "usedScene": { + "abilities": ["EntryAbility"], + "when": "inuse" + } + }, + { + "name": "ohos.permission.ACCESS_BLUETOOTH", + "reason": "$string:permission_bluetooth_reason", + "usedScene": { + "abilities": ["EntryAbility"], + "when": "inuse" + } + }, + { + "name": "ohos.permission.NFC_TAG", + "reason": "$string:permission_nfc_reason", + "usedScene": { + "abilities": ["EntryAbility"], + "when": "inuse" + } + }, + { + "name": "ohos.permission.GET_BUNDLE_INFO", + "reason": "$string:permission_bundle_info_reason", + "usedScene": { + "abilities": ["EntryAbility"], + "when": "inuse" + } } ] } diff --git a/ohos/entry/src/main/resources/base/element/string.json b/ohos/entry/src/main/resources/base/element/string.json index 0cdbcb17..e45d5e20 100644 --- a/ohos/entry/src/main/resources/base/element/string.json +++ b/ohos/entry/src/main/resources/base/element/string.json @@ -20,6 +20,10 @@ "name": "permission_network_info_reason", "value": "Used to check network connectivity status" }, + { + "name": "permission_wifi_info_reason", + "value": "Used to get WiFi information for nearby service and network info" + }, { "name": "permission_storage_reason", "value": "Used to save and read files such as images and exports" @@ -31,6 +35,42 @@ { "name": "permission_vibrate_reason", "value": "Used for haptic feedback during interactions" + }, + { + "name": "permission_camera_reason", + "value": "Used for QR code scanning and photo taking" + }, + { + "name": "permission_microphone_reason", + "value": "Used for audio recording and video calls" + }, + { + "name": "permission_location_reason", + "value": "Used for WiFi and Bluetooth device discovery" + }, + { + "name": "permission_bluetooth_reason", + "value": "Used for BLE device communication" + }, + { + "name": "permission_nfc_reason", + "value": "Used for NFC tag reading and writing" + }, + { + "name": "permission_read_media_reason", + "value": "Used to read photos and videos from gallery" + }, + { + "name": "permission_write_media_reason", + "value": "Used to save photos and videos to gallery" + }, + { + "name": "permission_pasteboard_reason", + "value": "Used for clipboard paste in text input" + }, + { + "name": "permission_bundle_info_reason", + "value": "Used to get application package information" } ] } \ No newline at end of file diff --git a/ohos/entry/src/main/resources/base/profile/buildinfo.json5 b/ohos/entry/src/main/resources/rawfile/buildinfo.json5 similarity index 100% rename from ohos/entry/src/main/resources/base/profile/buildinfo.json5 rename to ohos/entry/src/main/resources/rawfile/buildinfo.json5 diff --git a/ohos/entry/src/main/resources/base/profile/framesconfig.json b/ohos/entry/src/main/resources/rawfile/framesconfig.json similarity index 100% rename from ohos/entry/src/main/resources/base/profile/framesconfig.json rename to ohos/entry/src/main/resources/rawfile/framesconfig.json diff --git a/ohos/entry/src/main/resources/zh_CN/element/string.json b/ohos/entry/src/main/resources/zh_CN/element/string.json index 7d48fc43..48ac1a5f 100644 --- a/ohos/entry/src/main/resources/zh_CN/element/string.json +++ b/ohos/entry/src/main/resources/zh_CN/element/string.json @@ -20,6 +20,10 @@ "name": "permission_network_info_reason", "value": "用于检测网络连接状态" }, + { + "name": "permission_wifi_info_reason", + "value": "用于获取WiFi信息以支持近场服务和网络信息" + }, { "name": "permission_storage_reason", "value": "用于保存和读取图片、导出文件等" @@ -31,6 +35,42 @@ { "name": "permission_vibrate_reason", "value": "用于交互时的触感反馈" + }, + { + "name": "permission_camera_reason", + "value": "用于扫码和拍照" + }, + { + "name": "permission_microphone_reason", + "value": "用于录音和视频通话" + }, + { + "name": "permission_location_reason", + "value": "用于WiFi和蓝牙设备发现" + }, + { + "name": "permission_bluetooth_reason", + "value": "用于BLE蓝牙设备通信" + }, + { + "name": "permission_nfc_reason", + "value": "用于NFC标签读写" + }, + { + "name": "permission_read_media_reason", + "value": "用于读取相册中的照片和视频" + }, + { + "name": "permission_write_media_reason", + "value": "用于保存照片和视频到相册" + }, + { + "name": "permission_pasteboard_reason", + "value": "用于文本输入框的剪贴板粘贴" + }, + { + "name": "permission_bundle_info_reason", + "value": "用于获取应用包信息" } ] } \ No newline at end of file diff --git a/ohos_run_log.txt b/ohos_run_log.txt new file mode 100644 index 00000000..fb65961e Binary files /dev/null and b/ohos_run_log.txt differ diff --git a/packages/fluttertoast_ohos/ohos/oh-package.json5 b/packages/fluttertoast_ohos/ohos/oh-package.json5 index a1f6833a..a48d6681 100644 --- a/packages/fluttertoast_ohos/ohos/oh-package.json5 +++ b/packages/fluttertoast_ohos/ohos/oh-package.json5 @@ -6,6 +6,6 @@ "author": "", "license": "Apache-2.0", "dependencies": { - "@ohos/flutter_ohos": "file:../../../libs/flutter.har" + "@ohos/flutter_ohos": "file:./har/flutter.har" } } diff --git a/packages/本地已适配鸿蒙的库.md b/packages/本地已适配鸿蒙的库.md deleted file mode 100644 index bb2c7bf1..00000000 --- a/packages/本地已适配鸿蒙的库.md +++ /dev/null @@ -1,282 +0,0 @@ -# 鸿蒙适配方案 - -> 文档创建: 2026-04-09 | 最后更新: 2026-04-14 -> 适配策略: 纯 Dart 包零成本适配 + 原生插件完整适配 - ---- - -## 一、纯 Dart 包 vs 原生插件判断 - -| 检查项 | 纯 Dart 包 | 原生插件 | -|--------|-----------|---------| -| `android/` `ios/` 目录 | ❌ 无 | ✅ 有 | -| `flutter.plugin` 声明 | ❌ 无 | ✅ 有 | -| MethodChannel / FFI | ❌ 无 | ✅ 有 | -| 适配方式 | 空壳 ohos 目录 | ets 原生实现 + har 包 | -| 工作量 | 5min | 1-3 天 | - -### 快速判断流程 - -``` -新包是否包含原生代码? -├── 否(纯 Dart)→ ✅ 零适配:改版本号 + 空壳 ohos 目录 -└── 是(有原生代码) - ├── MethodChannel → 需 ets 实现 + DevEco Studio 打 har - └── FFI → 需编译鸿蒙版 .so + CMakeLists -``` - ---- - -## 二、纯 Dart 包适配步骤(通用模板) - -### 2.1 拉取源码 - -```bash -cd packages -git clone --depth 1 --branch -``` - -### 2.2 修改版本号 - -`pubspec.yaml` 中 `version: x.x.x` → `x.x.x-ohos.1` - -### 2.3 创建空壳 ohos 目录 - -``` -packages//ohos/ -├── Index.ets -├── oh-package.json5 -├── build-profile.json5 -├── libs/ # flutter.har 占位(空目录) -└── src/main/ - ├── module.json5 - ├── resources/base/profile/ # 空目录 - └── ets/components/plugin/ - └── Plugin.ets # 空壳类 -``` - -#### 通用文件模板(所有纯 Dart 包共用) - -**`Index.ets`** -```typescript -import Plugin from './src/main/ets/components/plugin/Plugin'; -export default Plugin; -``` - -**`oh-package.json5`** -```json -{ - "name": "", - "version": "-ohos.1", - "description": " - HarmonyOS adaptation", - "main": "Index.ets", - "author": "", - "license": "", - "dependencies": { - "@ohos/flutter_ohos": "file:libs/flutter.har" - } -} -``` - -**`build-profile.json5`** -```json -{ "apiType": "stageMode", "buildOption": {}, "targets": [{ "name": "default" }] } -``` - -**`module.json5`** -```json -{ - "module": { - "name": "", - "type": "har", - "deviceTypes": ["default", "tablet"] - } -} -``` - -**`Plugin.ets`** -```typescript -export default class Plugin {} -``` - -### 2.4 项目引用 & 验证 - -```yaml -# pubspec.yaml -dependencies: - : - path: packages/ -``` - -```bash -flutter pub get && flutter analyze --no-pub -``` - ---- - -## 三、原生插件适配流程(参考) - -详见 [ohos平台适配flutter三方库指导.md](./ohos平台适配flutter三方库指导.md) - -``` -1. flutter create --platforms ohos _ohos -2. 复制 android 版 lib dart 代码,android → ohos -3. DevEco Studio 编写 ets 原生代码(参考 android/ios) -4. pubspec.yaml 添加 flutter.plugin.platforms.ohos -5. DevEco Studio → Build → Make Module 打 har 包 -6. flutter create --platforms ohos example 验证 -``` - ---- - -## 四、Web 兼容性问题备忘 - -项目 Web 白屏与各适配包无关,是项目本身依赖问题: - -| 问题依赖 | 原因 | 修复方案 | -|----------|------|---------| -| `dart:io` (logger_service) | Web 不支持 | 条件导入或 kIsWeb 检查 | -| `path_provider` (git版) | 可能无 web 实现 | 跳过初始化 | -| `permission_handler` | Web 不支持原生权限 | stub 或跳过 | -| `fluttertoast` (本地) | 可能无 web 实现 | SnackBar 替代 | - ---- - -## 五、已适配纯 Dart 包清单 - -### 5.1 总览表 - -| # | 包名 | 原版本 | 适配版本 | 日期 | 状态 | -|---|------|--------|---------|------|------| -| 1 | fl_chart | 1.2.0 | 1.2.0-ohos.1 | 2026-04-09 | ✅ Android / HarmonyOS | -| 2 | badges | 3.2.0 | 3.2.0-ohos.1 | 2026-04-10 | ✅ analyze 通过 | -| 3 | flutter_staggered_grid_view | 0.7.0 | 0.7.0-ohos.1 | 2026-04-12 | ✅ analyze 通过 | -| 4 | cached_network_image | 3.4.1 | 3.4.1-ohos.1 | 2026-04-12 | ✅ analyze 通过 | -| 5 | flutter_markdown_plus | 1.0.7 | 1.0.7-ohos.1 | 2026-04-14 | ✅ analyze 通过 | -| 6 | flutter_card_swiper | 7.2.0 | 7.2.0-ohos.1 | 2026-04-14 | ✅ analyze 通过 | -| 7 | qr | 3.0.2 | 3.0.2-ohos.1 | 2026-04-19 | ✅ pub get 通过 | -| 8 | mailer | 7.1.0 | 7.1.0-ohos.1 | 2026-04-19 | ✅ pub get 通过 | -| 9 | flex_color_picker | 3.7.2 | 3.7.2-ohos.1 | 2026-04-20 | ✅ analyze 通过 (修复 TargetPlatform.ohos) | - -### 5.2 各包克隆命令速查 - -```bash -cd packages - -# 1. fl_chart -git clone --depth 1 --branch 1.2.0 https://github.com/imaNNeo/fl_chart.git fl_chart - -# 2. badges -git clone --depth 1 --branch v3.2.0 https://github.com/yako-dev/flutter_badges.git badges - -# 3. flutter_staggered_grid_view -git clone --depth 1 --branch v0.7.0 https://github.com/letsar/flutter_staggered_grid_view.git - -# 4. cached_network_image(monorepo,主包在子目录) -git clone --depth 1 --branch v3.4.1 https://github.com/Baseflow/flutter_cached_network_image.git cached_network_image -# 引用路径: packages/cached_network_image/cached_network_image - -# 5. flutter_markdown_plus -git clone --depth 1 --branch v1.0.7 https://github.com/foresightmobile/flutter_markdown_plus.git flutter_markdown_plus - -# 6. flutter_card_swiper -git clone --depth 1 --branch v7.2.0 https://github.com/ricardodalarme/flutter_card_swiper.git flutter_card_swiper - -# 7. qr -git clone --depth 1 --branch v3.0.2 https://github.com/kevmoo/qr.dart.git qr - -# 8. mailer -git clone --depth 1 --branch v7.1.0 https://github.com/dart-mailer/mailer.git mailer - -# 9. flex_color_picker -git clone --depth 1 --branch 3.7.2 https://github.com/rydmike/flex_color_picker.git flex_color_picker -``` - -### 5.3 各包使用示例 - -**fl_chart** -```dart -import 'package:fl_chart/fl_chart.dart'; -LineChart(LineChartData(...)) -``` - -**badges** -```dart -import 'package:badges/badges.dart' as badges; -badges.Badge(badgeContent: Text('3'), child: Icon(CupertinoIcons.shopping_cart)) -``` - -**flutter_staggered_grid_view** -```dart -import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -MasonryGridView.count(crossAxisCount: 2, itemCount: items.length, itemBuilder: (ctx, i) => Tile(index: i)) -``` - -**cached_network_image** -```dart -import 'package:cached_network_image/cached_network_image.dart'; -CachedNetworkImage(imageUrl: 'url', placeholder: (ctx, url) => CircularProgressIndicator(), errorWidget: (ctx, url, err) => Icon(Icons.error)) -``` - -**flutter_markdown_plus** -```dart -import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; -Markdown(data: '# Hello\n**bold** text', onTapLink: (text, href, title) {}) -``` - -**flutter_card_swiper** -```dart -import 'package:flutter_card_swiper/flutter_card_swiper.dart'; -CardSwiper(itemCount: cards.length, itemBuilder: (ctx, index) => CardWidget(cards[index]), onSwipe: (prev, curr, direction) {}) -``` - -**qr** -```dart -import 'package:qr/qr.dart'; -final qrCode = QrCode(4, QrErrorCorrectLevel.L)..addData('Hello, world!'); -final qrImage = QrImage(qrCode); -``` - -**mailer** -```dart -import 'package:mailer/mailer.dart'; -import 'package:mailer/smtp_server.dart'; -final smtpServer = gmail('user@gmail.com', 'password'); -final message = Message()..from = Address('user@gmail.com')..recipients.add('target@example.com')..subject = 'Test'..text = 'Hello'; -final sendReport = await send(message, smtpServer); -``` - ---- - -## 六、项目依赖兼容性总览 - -| 依赖 | 来源 | Web | 鸿蒙 | 备注 | -|------|------|-----|------|------| -| fl_chart | 本地 path | ✅ | ✅ | 纯 Dart | -| badges | 本地 path | ✅ | ✅ | 纯 Dart | -| flutter_staggered_grid_view | 本地 path | ✅ | ✅ | 纯 Dart | -| cached_network_image | 本地 path | ✅ | ✅ | 条件导入,IO 分支 | -| flutter_markdown_plus | 本地 path | ✅ | ✅ | 纯 Dart,全平台支持 | -| flutter_card_swiper | 本地 path | ✅ | ✅ | 纯 Dart | -| qr | 本地 path | ✅ | ✅ | 纯 Dart,QR码生成 | -| mailer | 本地 path | ❌ | ✅ | 纯 Dart,SMTP客户端,Web不支持 | -| flex_color_picker | 本地 path | ✅ | ✅ | 纯 Dart,修复 TargetPlatform.ohos switch | -| hive_ce | pub.dev | ✅ | ✅ | 纯 Dart | -| get / dio / logger / intl | pub.dev | ✅ | ✅ | 纯 Dart | -| shared_preferences | pub.dev | ✅ | ✅ | localStorage | -| path_provider | git(鸿蒙版) | ⚠️ | ✅ | 需验证 web | -| connectivity_plus | git(鸿蒙版) | ⚠️ | ✅ | 需验证 web | -| share_plus | git(鸿蒙版) | ⚠️ | ✅ | 需验证 web | -| permission_handler | git(鸿蒙版) | ❌ | ✅ | Web 不支持 | -| fluttertoast | 本地 path | ⚠️ | ✅ | 需验证 web | - ---- - -## 七、参考文档索引 - -| 文档 | 用途 | -|------|------| -| [ohos平台适配flutter三方库指导.md](./ohos平台适配flutter三方库指导.md) | 原生插件完整适配流程(MethodChannel、打 har 包) | -| [开发FFI plugin.md](./开发FFI plugin.md) | FFI 插件开发指南 | -| [OpenHarmony应用如何集成Flutter模块.md](./OpenHarmony应用如何集成Flutter模块.md) | Flutter 模块集成到鸿蒙应用 | -| [FlutterChannel通信.md](./如何使用Flutter与OpenHarmony通信%20FlutterChannel.md) | MethodChannel / EventChannel / BasicMessageChannel 用法 | diff --git a/pubspec.lock b/pubspec.lock index f9e772df..1887db68 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d url: "https://pub.flutter-io.cn" source: hosted - version: "67.0.0" + version: "91.0.0" adaptive_number: dependency: transitive description: @@ -21,50 +21,65 @@ packages: dependency: "direct main" description: name: adaptive_palette - sha256: "0e005231fc37e24a00ec49549689ddcb8abfd382768a111b58bf49f0bd1bf47a" + sha256: da31f9f3b67dae287d0366f5e5245d4a67a7ca49a28dc7b9a753c6787a3f1238 url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.0" + version: "3.1.0" + analysis_server_plugin: + dependency: transitive + description: + name: analysis_server_plugin + sha256: "26844e7f977087567135d62532b67d5639fe206c5194c3f410ba75e1a04a2747" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: a40a0cee526a7e1f387c6847bd8a5ccbf510a75952ef8a28338e989558072cb0 url: "https://pub.flutter-io.cn" source: hosted - version: "6.4.1" + version: "8.4.0" + analyzer_buffer: + dependency: transitive + description: + name: analyzer_buffer + sha256: aba2f75e63b3135fd1efaa8b6abefe1aa6e41b6bd9806221620fa48f98156033 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.11" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + sha256: "08cfefa90b4f4dd3b447bda831cecf644029f9f8e22820f6ee310213ebe2dd53" url: "https://pub.flutter-io.cn" source: hosted - version: "0.11.3" + version: "0.13.10" animate_do: dependency: "direct main" description: name: animate_do - sha256: e5c8b92e8495cba5adfff17c0b017d50f46b2766226e9faaf68bc08c91aef034 + sha256: ddc9bde27df897088e02553f0aec44c614595de01fe357ffb257ecaf7d40c8fa url: "https://pub.flutter-io.cn" source: hosted - version: "4.2.0" + version: "5.1.0" animations: dependency: "direct main" description: name: animations - sha256: a120785be876b24177e8af387929e786e7761d6574e63cad6c2ca28545b30186 + sha256: "9cb469212ea51be27097f23b519d594c01171721347b55df9334fff653659e7f" url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.2" + version: "2.2.0" app_links: dependency: "direct main" description: - name: app_links - sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.4.1" + path: "packages/app_links" + relative: true + source: path + version: "7.0.0-ohos.1" app_links_linux: dependency: transitive description: @@ -73,6 +88,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" + app_links_ohos: + dependency: "direct overridden" + description: + path: "packages/app_links_ohos" + relative: true + source: path + version: "7.0.0-ohos.1" app_links_platform_interface: dependency: transitive description: @@ -93,10 +115,10 @@ packages: dependency: "direct main" description: name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff url: "https://pub.flutter-io.cn" source: hosted - version: "3.6.1" + version: "4.0.9" args: dependency: transitive description: @@ -124,11 +146,10 @@ packages: audioplayers: dependency: "direct main" description: - name: audioplayers - sha256: a72dd459d1a48f61a6fb9c0134dba26597c9236af40639ff0eb70eb4e0baab70 - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.6.0" + path: "packages/audioplayers" + relative: true + source: path + version: "6.5.0-ohos.1" audioplayers_android: dependency: transitive description: @@ -153,6 +174,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.2.1" + audioplayers_ohos: + dependency: "direct overridden" + description: + path: "packages/audioplayers_ohos" + relative: true + source: path + version: "5.2.1-ohos.1" audioplayers_platform_interface: dependency: transitive description: @@ -195,11 +223,17 @@ packages: battery_plus: dependency: "direct main" description: - name: battery_plus - sha256: "03d5a6bb36db9d2b977c548f6b0262d5a84c4d5a4cfee2edac4a91d57011b365" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.2.3" + path: "packages/battery_plus" + relative: true + source: path + version: "7.0.0-ohos.1" + battery_plus_ohos: + dependency: transitive + description: + path: "packages/battery_plus_ohos" + relative: true + source: path + version: "7.0.0" battery_plus_platform_interface: dependency: transitive description: @@ -236,18 +270,18 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10 url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.1" + version: "4.0.6" build_config: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.2" + version: "1.3.0" build_daemon: dependency: transitive description: @@ -256,30 +290,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.1.1" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6" url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.13" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 - url: "https://pub.flutter-io.cn" - source: hosted - version: "7.3.2" + version: "2.15.0" built_collection: dependency: transitive description: @@ -292,10 +310,10 @@ packages: dependency: transitive description: name: built_value - sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af" + sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" url: "https://pub.flutter-io.cn" source: hosted - version: "8.12.5" + version: "8.12.6" cached_network_image: dependency: "direct main" description: @@ -323,19 +341,18 @@ packages: catcher_2: dependency: "direct main" description: - name: catcher_2 - sha256: ac9dd03230fa4058d14d46450335ca3c40564f734d793e52f81053f3cbf95009 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.9" + path: "packages/catcher_2" + relative: true + source: path + version: "2.1.9-ohos.1" characters: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.flutter-io.cn" source: hosted - version: "1.4.0" + version: "1.4.1" charcode: dependency: transitive description: @@ -360,6 +377,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.1.0" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" cli_util: dependency: transitive description: @@ -376,6 +401,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" code_builder: dependency: transitive description: @@ -403,11 +436,10 @@ packages: connectivity_plus: dependency: "direct main" description: - name: connectivity_plus - sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.1.5" + path: "packages/connectivity_plus" + relative: true + source: path + version: "7.1.1-ohos.1" connectivity_plus_platform_interface: dependency: transitive description: @@ -424,6 +456,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.15.0" cross_file: dependency: "direct main" description: @@ -460,26 +500,26 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799" + sha256: "751ee9440920f808266c3ec2553420dea56d3c7837dd2d62af76b11be3fcece5" url: "https://pub.flutter-io.cn" source: hosted - version: "0.6.4" - custom_lint_builder: - dependency: transitive - description: - name: custom_lint_builder - sha256: d7dc41e709dde223806660268678be7993559e523eb3164e2a1425fd6f7615a9 - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.6.4" + version: "0.8.1" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6 + sha256: "85b339346154d5646952d44d682965dfe9e12cae5febd706f0db3aa5010d6423" url: "https://pub.flutter-io.cn" source: hosted - version: "0.6.3" + version: "0.8.1" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: "91f2a81e9f0abb4b9f3bb529f78b6227ce6050300d1ae5b1e2c69c66c7a566d8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0+8.4.0" custom_refresh_indicator: dependency: "direct main" description: @@ -508,10 +548,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b url: "https://pub.flutter-io.cn" source: hosted - version: "2.3.6" + version: "3.1.3" dart_webrtc: dependency: transitive description: @@ -531,19 +571,18 @@ packages: device_info_plus: dependency: "direct main" description: - name: device_info_plus - sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 - url: "https://pub.flutter-io.cn" - source: hosted - version: "10.1.2" + path: "packages/device_info_plus" + relative: true + source: path + version: "13.1.0-ohos.1" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + sha256: "04b173a92e2d9161dfead145667037c8d834db725ce2e7b942bfe18fd2f45a46" url: "https://pub.flutter-io.cn" source: hosted - version: "7.0.3" + version: "8.1.0" diff_match_patch: dependency: transitive description: @@ -572,26 +611,26 @@ packages: dependency: "direct main" description: name: dotted_border - sha256: "108837e11848ca776c53b30bc870086f84b62ed6e01c503ed976e8f8c7df9c04" + sha256: "99b091ec6891ba0c5331fdc2b502993c7c108f898995739a73c6845d71dad70c" url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.0" + version: "3.1.0" drift: dependency: "direct main" description: name: drift - sha256: df027d168a2985a2e9da900adeba2ab0136f0d84436592cf3cd5135f82c8579c + sha256: "970cd188fddb111b26ea6a9b07a62bf5c2432d74147b8122c67044ae3b97e99e" url: "https://pub.flutter-io.cn" source: hosted - version: "2.21.0" + version: "2.31.0" drift_dev: dependency: "direct dev" description: name: drift_dev - sha256: "623649abe932fc17bd32e578e7e05f7ac5e7dd0b33e6c8669a0634105d1389bf" + sha256: "917184b2fb867b70a548a83bf0d36268423b38d39968c06cce4905683da49587" url: "https://pub.flutter-io.cn" source: hosted - version: "2.21.2" + version: "2.31.0" ed25519_edwards: dependency: transitive description: @@ -622,7 +661,7 @@ packages: path: "packages/extended_image" relative: true source: path - version: "10.0.1" + version: "10.0.1-ohos.1" extended_image_library: dependency: transitive description: @@ -647,6 +686,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" + ffi_leak_tracker: + dependency: transitive + description: + name: ffi_leak_tracker + sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.2" file: dependency: transitive description: @@ -658,11 +705,10 @@ packages: file_picker: dependency: "direct main" description: - name: file_picker - sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810 - url: "https://pub.flutter-io.cn" - source: hosted - version: "8.3.7" + path: "packages/file_picker" + relative: true + source: path + version: "8.3.7-ohos.1" file_selector_linux: dependency: transitive description: @@ -707,25 +753,25 @@ packages: dependency: "direct main" description: name: fl_chart - sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08" + sha256: b938f77d042cbcd822936a7a359a7235bad8bd72070de1f827efc2cc297ac888 url: "https://pub.flutter-io.cn" source: hosted - version: "0.69.2" + version: "1.2.0" flex_color_picker: dependency: "direct main" description: path: "packages/flex_color_picker" relative: true source: path - version: "3.7.2-ohos.1" + version: "3.8.0-ohos.1" flex_seed_scheme: dependency: transitive description: name: flex_seed_scheme - sha256: "828291a5a4d4283590541519d8b57821946660ac61d2e07d955f81cfcab22e5d" + sha256: a3183753bbcfc3af106224bff3ab3e1844b73f58062136b7499919f49f3667e7 url: "https://pub.flutter-io.cn" source: hosted - version: "3.6.1" + version: "4.0.1" flutter: dependency: "direct main" description: flutter @@ -758,51 +804,65 @@ packages: flutter_blue_plus: dependency: "direct main" description: - name: flutter_blue_plus - sha256: "69a8c87c11fc792e8cf0f997d275484fbdb5143ac9f0ac4d424429700cb4e0ed" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.36.8" + path: "packages/flutter_blue_plus" + relative: true + source: path + version: "2.1.0-ohos.1" flutter_blue_plus_android: dependency: transitive description: name: flutter_blue_plus_android - sha256: "6f7fe7e69659c30af164a53730707edc16aa4d959e4c208f547b893d940f853d" + sha256: d66bdcb0438e643d5de4af914851bdf7322448384795666efe92236626484740 url: "https://pub.flutter-io.cn" source: hosted - version: "7.0.4" + version: "8.2.2" flutter_blue_plus_darwin: dependency: transitive description: name: flutter_blue_plus_darwin - sha256: "682982862c1d964f4d54a3fb5fccc9e59a066422b93b7e22079aeecd9c0d38f8" + sha256: bb433d8c614964be3023d63f0460f1e2fe1a436cdbabd9bcc88438b509f4f045 url: "https://pub.flutter-io.cn" source: hosted - version: "7.0.3" + version: "8.2.2" flutter_blue_plus_linux: dependency: transitive description: name: flutter_blue_plus_linux - sha256: "56b0c45edd0a2eec8f85bd97a26ac3cd09447e10d0094fed55587bf0592e3347" + sha256: c08563ccef620be5a06dd6b895ee51ecd986d1fcab1edd2dbc3fa125518abb21 url: "https://pub.flutter-io.cn" source: hosted - version: "7.0.3" + version: "8.2.2" + flutter_blue_plus_ohos: + dependency: "direct overridden" + description: + path: "packages/flutter_blue_plus_ohos" + relative: true + source: path + version: "8.1.0-ohos.1" flutter_blue_plus_platform_interface: dependency: transitive description: name: flutter_blue_plus_platform_interface - sha256: "84fbd180c50a40c92482f273a92069960805ce324e3673ad29c41d2faaa7c5c2" + sha256: "55abf2bdae442f2ed4cace044d491cbb9dd833729585541354e4ec42c227989c" url: "https://pub.flutter-io.cn" source: hosted - version: "7.0.0" + version: "8.2.2" flutter_blue_plus_web: dependency: transitive description: name: flutter_blue_plus_web - sha256: a1aceee753d171d24c0e0cdadb37895b5e9124862721f25f60bb758e20b72c99 + sha256: f6ed6bedf7568a3c3f91f0d668ca974c2a6d95f397e58e0d8cd6ad8681355819 url: "https://pub.flutter-io.cn" source: hosted - version: "7.0.2" + version: "8.2.3" + flutter_blue_plus_winrt: + dependency: transitive + description: + name: flutter_blue_plus_winrt + sha256: dce19eb095c5ed70de997c02911afd255d867627fceff9c68c6f8b88792d597e + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.0.19" flutter_cache_manager: dependency: "direct main" description: @@ -860,10 +920,9 @@ packages: flutter_image_compress_ohos: dependency: transitive description: - name: flutter_image_compress_ohos - sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51 - url: "https://pub.flutter-io.cn" - source: hosted + path: "packages/flutter_image_compress_ohos" + relative: true + source: path version: "0.0.3" flutter_image_compress_platform_interface: dependency: transitive @@ -996,27 +1055,34 @@ packages: flutter_local_notifications: dependency: "direct main" description: - name: flutter_local_notifications - sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610 - url: "https://pub.flutter-io.cn" - source: hosted - version: "18.0.1" + path: "packages/flutter_local_notifications" + relative: true + source: path + version: "21.0.0-ohos.1" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01" + sha256: e0f25e243c6c44c825bbbc6b2b2e76f7d9222362adcfe9fd780bf01923c840bd url: "https://pub.flutter-io.cn" source: hosted - version: "5.0.0" + version: "8.0.0" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52" + sha256: e7db3d5b49c2b7ecc68deba4aaaa67a348f92ee0fef34c8e4b4459dbef0d7307 url: "https://pub.flutter-io.cn" source: hosted - version: "8.0.0" + version: "11.0.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: "3a2654ba104fbb52c618ebed9def24ef270228470718c43b3a6afcd5c81bef0c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -1038,6 +1104,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.7" + flutter_nfc_kit: + dependency: "direct main" + description: + path: "packages/flutter_nfc_kit" + relative: true + source: path + version: "3.6.0-rc.6" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -1052,7 +1125,7 @@ packages: path: "packages/flutter_quill" relative: true source: path - version: "11.5.0" + version: "11.5.0-ohos.1" flutter_quill_delta_from_html: dependency: transitive description: @@ -1065,10 +1138,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + sha256: "38ec6c303e2c83ee84512f5fc2a82ae311531021938e63d7137eccc107bf3c02" url: "https://pub.flutter-io.cn" source: hosted - version: "2.6.1" + version: "3.1.0" flutter_screenutil: dependency: "direct main" description: @@ -1080,11 +1153,10 @@ packages: flutter_secure_storage: dependency: "direct main" description: - name: flutter_secure_storage - sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" - url: "https://pub.flutter-io.cn" - source: hosted - version: "9.2.4" + path: "packages/flutter_secure_storage" + relative: true + source: path + version: "9.2.4-ohos.1" flutter_secure_storage_linux: dependency: transitive description: @@ -1101,6 +1173,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.1.3" + flutter_secure_storage_ohos: + dependency: transitive + description: + path: "packages/flutter_secure_storage_ohos" + relative: true + source: path + version: "1.2.2" flutter_secure_storage_platform_interface: dependency: transitive description: @@ -1118,12 +1197,11 @@ packages: source: hosted version: "1.2.1" flutter_secure_storage_windows: - dependency: transitive + dependency: "direct overridden" description: - name: flutter_secure_storage_windows - sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 - url: "https://pub.flutter-io.cn" - source: hosted + path: "packages/flutter_secure_storage_windows" + relative: true + source: path version: "3.1.2" flutter_shaders: dependency: transitive @@ -1139,7 +1217,7 @@ packages: path: "packages/flutter_shaders_ui" relative: true source: path - version: "0.1.0" + version: "0.1.1-ohos.1" flutter_slidable: dependency: "direct main" description: @@ -1154,7 +1232,7 @@ packages: path: "packages/flutter_spritesheet_animation" relative: true source: path - version: "1.0.1" + version: "1.0.2-ohos.1" flutter_staggered_grid_view: dependency: "direct main" description: @@ -1175,10 +1253,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9" + sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" url: "https://pub.flutter-io.cn" source: hosted - version: "2.2.4" + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -1188,10 +1266,10 @@ packages: dependency: "direct main" description: name: flutter_tilt - sha256: "99edbab87ab539047028020ceb6dcb31b2f3c998aec9804f1d41b7ea72ca15af" + sha256: "7edbc11a3aab4b6d5be35b2ea46a996d4a5bce1a5f19270ebd6a998b9f8163eb" url: "https://pub.flutter-io.cn" source: hosted - version: "3.3.4" + version: "4.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -1200,11 +1278,10 @@ packages: flutter_webrtc: dependency: "direct main" description: - name: flutter_webrtc - sha256: b832dc76c0d1577f14aaf35e9c38d4ed7667cbc89c492b7bf4505d8d5f62e08b - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.12.12+hotfix.1" + path: "packages/flutter_webrtc" + relative: true + source: path + version: "1.4.0-ohos.1" fluttertoast: dependency: transitive description: @@ -1217,18 +1294,18 @@ packages: dependency: "direct dev" description: name: freezed - sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 + sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77" url: "https://pub.flutter-io.cn" source: hosted - version: "2.5.2" + version: "3.2.3" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.4" + version: "3.1.0" frontend_server_client: dependency: transitive description: @@ -1248,11 +1325,10 @@ packages: gal: dependency: "direct main" description: - name: gal - sha256: "969598f986789127fd407a750413249e1352116d4c2be66e81837ffeeaafdfee" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.3.2" + path: "packages/gal" + relative: true + source: path + version: "2.3.0-ohos.1" glob: dependency: transitive description: @@ -1265,18 +1341,18 @@ packages: dependency: "direct main" description: name: go_router - sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + sha256: "92d8cee7c57dff0a6c409c05597b460002434eccf7424a712283225b3962d03f" url: "https://pub.flutter-io.cn" source: hosted - version: "14.8.1" + version: "17.2.3" google_fonts: dependency: "direct main" description: name: google_fonts - sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 + sha256: "4e9391085e524954a51e3625b7c9c7e9851dc3f376603208bb45c24b9a66255d" url: "https://pub.flutter-io.cn" source: hosted - version: "6.3.3" + version: "8.1.0" gotrue: dependency: transitive description: @@ -1297,10 +1373,10 @@ packages: dependency: transitive description: name: gtk - sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.0" + version: "2.2.0" hashcodes: dependency: transitive description: @@ -1339,15 +1415,15 @@ packages: path: "packages/home_widget" relative: true source: path - version: "0.9.1" - hotreloader: + version: "0.9.1-ohos.1" + hooks: dependency: transitive description: - name: hotreloader - sha256: "66871df468fc24eee81f1a0a7cb98acc104716f9b7376d355437b48d633c4ebf" + name: hooks + sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" url: "https://pub.flutter-io.cn" source: hosted - version: "4.4.0" + version: "1.0.3" html: dependency: transitive description: @@ -1400,26 +1476,25 @@ packages: dependency: "direct main" description: name: image - sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce url: "https://pub.flutter-io.cn" source: hosted - version: "4.3.0" + version: "4.8.0" image_picker: dependency: "direct main" description: - name: image_picker - sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.2.1" + path: "packages/image_picker" + relative: true + source: path + version: "1.2.2-ohos.1" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622" + sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f url: "https://pub.flutter-io.cn" source: hosted - version: "0.8.13+16" + version: "0.8.13+17" image_picker_for_web: dependency: transitive description: @@ -1432,10 +1507,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad" + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 url: "https://pub.flutter-io.cn" source: hosted - version: "0.8.13+3" + version: "0.8.13+6" image_picker_linux: dependency: transitive description: @@ -1452,6 +1527,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.2.2+1" + image_picker_ohos: + dependency: transitive + description: + path: "packages/image_picker_ohos" + relative: true + source: path + version: "0.8.13+7" image_picker_platform_interface: dependency: transitive description: @@ -1474,7 +1556,7 @@ packages: path: "packages/image_size_getter" relative: true source: path - version: "2.4.1" + version: "2.4.1-ohos.1" intl: dependency: "direct main" description: @@ -1527,10 +1609,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3 url: "https://pub.flutter-io.cn" source: hosted - version: "6.8.0" + version: "6.11.2" jwt_decode: dependency: transitive description: @@ -1577,7 +1659,7 @@ packages: path: "packages/liquid_glass_easy" relative: true source: path - version: "1.1.1" + version: "1.1.1-ohos.1" liquid_glass_widgets: dependency: "direct main" description: @@ -1597,27 +1679,33 @@ packages: local_auth: dependency: "direct main" description: - name: local_auth - sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.3.0" + path: "packages/local_auth" + relative: true + source: path + version: "3.0.1-ohos.1" local_auth_android: dependency: transitive description: name: local_auth_android - sha256: a0bdfcc0607050a26ef5b31d6b4b254581c3d3ce3c1816ab4d4f4a9173e84467 + sha256: b201c006fa769c23386f89aa6837ec0eb8179fcfb212eadcf87b422b3f9a6a78 url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.56" + version: "2.0.8" local_auth_darwin: dependency: transitive description: name: local_auth_darwin - sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49" + sha256: a8c3d4e17454111f7fd31ff72a31222359f6059f7fe956c2dcfe0f88f49826d4 url: "https://pub.flutter-io.cn" source: hosted - version: "1.6.1" + version: "2.0.3" + local_auth_ohos: + dependency: transitive + description: + path: "packages/local_auth_ohos" + relative: true + source: path + version: "2.0.0" local_auth_platform_interface: dependency: transitive description: @@ -1630,10 +1718,10 @@ packages: dependency: transitive description: name: local_auth_windows - sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5 + sha256: be12c5b8ba5e64896983123655c5f67d2484ecfcc95e367952ad6e3bff94cb16 url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.11" + version: "2.0.1" logger: dependency: "direct main" description: @@ -1654,10 +1742,10 @@ packages: dependency: "direct main" description: name: lottie - sha256: "377d87b8dcef640c04717e93afb86a510f0e1117a399ab94dc4b3f39c85eaa87" + sha256: "8b6359a7422167014aa73ce763fa133fb832065dcc0ac4d1dec1f603a5cef7d0" url: "https://pub.flutter-io.cn" source: hosted - version: "3.3.0" + version: "3.3.3" mailer: dependency: "direct main" description: @@ -1678,18 +1766,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.flutter-io.cn" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.flutter-io.cn" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: "direct overridden" description: @@ -1709,11 +1797,18 @@ packages: mobile_scanner: dependency: "direct main" description: - name: mobile_scanner - sha256: "0b466a0a8a211b366c2e87f3345715faef9b6011c7147556ad22f37de6ba3173" + path: "packages/mobile_scanner" + relative: true + source: path + version: "7.1.4-ohos.1" + mockito: + dependency: transitive + description: + name: mockito + sha256: eff30d002f0c8bf073b6f929df4483b543133fcafce056870163587b03f1d422 url: "https://pub.flutter-io.cn" source: hosted - version: "6.0.11" + version: "5.6.4" mocktail: dependency: "direct dev" description: @@ -1730,45 +1825,51 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" - ndef_record: - dependency: "direct main" + native_toolchain_c: + dependency: transitive description: - name: ndef_record - sha256: "876e2774f18573e8afba1aa9db3998aaf4e3384c825c843c3f86d001bec8510d" + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.3" + version: "0.17.6" + ndef: + dependency: transitive + description: + name: ndef + sha256: bdf82a56da97983c75375ec914b7068fba4e6e0defb616c66457c28360b9c38f + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.5" nearby_service: dependency: "direct main" description: path: "packages/nearby_service" relative: true source: path - version: "0.2.1" + version: "0.2.1-ohos.1" network_info_plus: dependency: "direct main" description: - name: network_info_plus - sha256: f926b2ba86aa0086a0dfbb9e5072089bc213d854135c1712f1d29fc89ba3c877 - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.1.4" + path: "packages/network_info_plus" + relative: true + source: path + version: "8.1.0-ohos.1" + network_info_plus_ohos: + dependency: "direct overridden" + description: + path: "packages/network_info_plus_ohos" + relative: true + source: path + version: "8.1.0-ohos.1" network_info_plus_platform_interface: dependency: transitive description: name: network_info_plus_platform_interface - sha256: "7e7496a8a9d8136859b8881affc613c4a21304afeb6c324bcefc4bd0aff6b94b" + sha256: "210b58064bc08e04b3a2b608f1f999cf768871fbd531961dc33a91720869d182" url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.2" - nfc_manager: - dependency: "direct main" - description: - name: nfc_manager - sha256: "24c78b0e5702da53e7f8794d073624c0bee7cd99924f257cbd11f5d1c5866879" - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.1.1" + version: "3.1.0" nm: dependency: transitive description: @@ -1777,6 +1878,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.5.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" octo_image: dependency: transitive description: @@ -1796,19 +1905,18 @@ packages: package_info_plus: dependency: "direct main" description: - name: package_info_plus - sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" - url: "https://pub.flutter-io.cn" - source: hosted - version: "8.3.1" + path: "packages/package_info_plus" + relative: true + source: path + version: "10.1.0-ohos.1" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4 url: "https://pub.flutter-io.cn" source: hosted - version: "3.2.1" + version: "4.1.0" path: dependency: "direct main" description: @@ -1817,14 +1925,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" - path_drawing: - dependency: transitive - description: - name: path_drawing - sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.1" path_parsing: dependency: transitive description: @@ -1836,11 +1936,10 @@ packages: path_provider: dependency: "direct main" description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.5" + path: "packages/path_provider" + relative: true + source: path + version: "2.1.5-ohos.1" path_provider_android: dependency: transitive description: @@ -1849,14 +1948,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.5.1" path_provider_linux: dependency: transitive description: @@ -1865,6 +1956,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" + path_provider_ohos: + dependency: transitive + description: + path: "packages/path_provider_ohos" + relative: true + source: path + version: "2.2.17" path_provider_platform_interface: dependency: transitive description: @@ -1884,19 +1982,18 @@ packages: permission_handler: dependency: "direct main" description: - name: permission_handler - sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" - url: "https://pub.flutter-io.cn" - source: hosted - version: "11.4.0" + path: "packages/permission_handler" + relative: true + source: path + version: "12.0.1-ohos.1" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" url: "https://pub.flutter-io.cn" source: hosted - version: "12.1.0" + version: "13.0.1" permission_handler_apple: dependency: transitive description: @@ -1913,6 +2010,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.1.3+5" + permission_handler_ohos: + dependency: transitive + description: + path: "packages/permission_handler_ohos" + relative: true + source: path + version: "12.0.1" permission_handler_platform_interface: dependency: transitive description: @@ -1943,7 +2047,15 @@ packages: path: "packages/photo_view" relative: true source: path - version: "0.15.0" + version: "0.15.0-ohos.1" + pigeon: + dependency: transitive + description: + name: pigeon + sha256: "2a4bfd279fac52b115818e93f5409d07955f7b3718d303fd5f100981be4de386" + url: "https://pub.flutter-io.cn" + source: hosted + version: "26.3.2" pinyin: dependency: "direct main" description: @@ -1984,6 +2096,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.5.0" postgrest: dependency: transitive description: @@ -1998,7 +2118,7 @@ packages: path: "packages/pro_image_editor" relative: true source: path - version: "12.4.3" + version: "12.4.4-ohos.1" pub_semver: dependency: transitive description: @@ -2096,12 +2216,11 @@ packages: source: hosted version: "0.0.2" quill_native_bridge_windows: - dependency: transitive + dependency: "direct overridden" description: - name: quill_native_bridge_windows - sha256: "3f96ced19e3206ddf4f6f7dde3eb16bdd05e10294964009ea3a806d995aa7caa" - url: "https://pub.flutter-io.cn" - source: hosted + path: "packages/quill_native_bridge_windows" + relative: true + source: path version: "0.0.2" quiver: dependency: transitive @@ -2133,15 +2252,14 @@ packages: path: "packages/receive_sharing_intent" relative: true source: path - version: "1.8.1" + version: "1.8.1-ohos.1" record: dependency: "direct main" description: - name: record - sha256: d5b6b334f3ab02460db6544e08583c942dbf23e3504bf1e14fd4cbe3d9409277 - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.2.0" + path: "packages/record" + relative: true + source: path + version: "6.0.0-ohos.1" record_android: dependency: transitive description: @@ -2174,6 +2292,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" + record_ohos: + dependency: "direct overridden" + description: + path: "packages/record_ohos" + relative: true + source: path + version: "1.0.0-ohos.1" record_platform_interface: dependency: transitive description: @@ -2182,6 +2307,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.5.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.0" record_web: dependency: transitive description: @@ -2210,50 +2343,50 @@ packages: dependency: transitive description: name: riverpod - sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + sha256: "16ff608d21e8ea64364f2b7c049c94a02ab81668f78845862b6e88b71dd4935a" url: "https://pub.flutter-io.cn" source: hosted - version: "2.6.1" + version: "3.1.0" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f" + sha256: "947b05d04c52a546a2ac6b19ef2a54b08520ff6bdf9f23d67957a4c8df1c3bc0" url: "https://pub.flutter-io.cn" source: hosted - version: "0.5.1" + version: "1.0.0-dev.8" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8 + sha256: cc1474bc2df55ec3c1da1989d139dcef22cd5e2bd78da382e867a69a8eca2e46 url: "https://pub.flutter-io.cn" source: hosted - version: "2.6.1" + version: "4.0.0" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: d451608bf17a372025fc36058863737636625dfdb7e3cbf6142e0dfeb366ab22 + sha256: e43b1537229cc8f487f09b0c20d15dba840acbadcf5fc6dad7ad5e8ab75950dc url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.0" + version: "4.0.0+1" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "3c67c14ccd16f0c9d53e35ef70d06cd9d072e2fb14557326886bbde903b230a5" + sha256: "4d2eb0d19bbe7e3323bd0ce4553b2e6170d161a13914bfdd85a3612329edcb43" url: "https://pub.flutter-io.cn" source: hosted - version: "2.3.10" + version: "3.1.0" rxdart: dependency: transitive description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.flutter-io.cn" source: hosted - version: "0.27.7" + version: "0.28.0" scroll_drag_detector: dependency: transitive description: @@ -2282,33 +2415,39 @@ packages: dependency: transitive description: name: sentry - sha256: "1f78300740739ff4b4920802687879231554350eab73eb229778f463aabda440" + sha256: f04095a25ff02b202a914174c73ec309570aa93d61098cb4a0a9e715b4aaa465 url: "https://pub.flutter-io.cn" source: hosted - version: "9.19.0" + version: "9.20.0" share_plus: dependency: "direct main" description: path: "packages/share_plus" relative: true source: path - version: "10.1.4" + version: "13.1.0-ohos.1" + share_plus_ohos: + dependency: transitive + description: + path: "packages/share_plus_ohos" + relative: true + source: path + version: "12.0.1" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b + sha256: "7f7ae28cf400d13f811e297ff37742dba83b79e0a6f5dce14eec0248274e6ce9" url: "https://pub.flutter-io.cn" source: hosted - version: "5.0.2" + version: "7.1.0" shared_preferences: dependency: "direct main" description: - name: shared_preferences - sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.5.5" + path: "packages/shared_preferences" + relative: true + source: path + version: "2.5.5-ohos.1" shared_preferences_android: dependency: transitive description: @@ -2333,6 +2472,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.4.1" + shared_preferences_ohos: + dependency: transitive + description: + path: "packages/shared_preferences_ohos" + relative: true + source: path + version: "2.5.4" shared_preferences_platform_interface: dependency: transitive description: @@ -2365,6 +2511,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" shelf_router: dependency: "direct main" description: @@ -2373,14 +2527,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.1.4" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.3" shelf_web_socket: dependency: "direct main" description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.1" + version: "3.0.0" shimmer: dependency: "direct main" description: @@ -2406,18 +2568,34 @@ packages: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02 url: "https://pub.flutter-io.cn" source: hosted - version: "1.5.0" + version: "4.2.3" source_helper: dependency: transitive description: name: source_helper - sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.5" + version: "1.3.8" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.10.13" source_span: dependency: transitive description: @@ -2429,27 +2607,25 @@ packages: sqflite: dependency: "direct main" description: - name: sqflite - sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.4.2" + path: "packages/sqflite" + relative: true + source: path + version: "2.4.1-ohos.1" sqflite_android: dependency: transitive description: name: sqflite_android - sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40" url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.2+2" + version: "2.4.2+3" sqflite_common: - dependency: transitive + dependency: "direct overridden" description: - name: sqflite_common - sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.5.6" + path: "packages/sqflite_common" + relative: true + source: path + version: "2.5.4-ohos.1" sqflite_darwin: dependency: transitive description: @@ -2458,14 +2634,20 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2" - sqflite_platform_interface: - dependency: transitive + sqflite_ohos: + dependency: "direct overridden" description: - name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.4.0" + path: "packages/sqflite_ohos" + relative: true + source: path + version: "2.4.1-ohos.1" + sqflite_platform_interface: + dependency: "direct overridden" + description: + path: "packages/sqflite_platform_interface" + relative: true + source: path + version: "2.4.0-ohos.1" sqlite3: dependency: transitive description: @@ -2486,10 +2668,10 @@ packages: dependency: transitive description: name: sqlparser - sha256: d77749237609784e337ec36c979d41f6f38a7b279df98622ae23929c8eb954a4 + sha256: "337e9997f7141ffdd054259128553c348635fa318f7ca492f07a4ab76f850d19" url: "https://pub.flutter-io.cn" source: hosted - version: "0.39.2" + version: "0.43.1" stack_trace: dependency: transitive description: @@ -2544,7 +2726,7 @@ packages: path: "packages/stupid_simple_sheet" relative: true source: path - version: "0.9.1+1" + version: "0.9.1-ohos.1" supabase: dependency: transitive description: @@ -2565,10 +2747,10 @@ packages: dependency: transitive description: name: synchronized - sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5" url: "https://pub.flutter-io.cn" source: hosted - version: "3.4.0" + version: "3.4.0+1" term_glyph: dependency: transitive description: @@ -2577,30 +2759,38 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" + test: + dependency: transitive + description: + name: test + sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.30.0" test_api: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.flutter-io.cn" source: hosted - version: "0.7.6" + version: "0.7.10" + test_core: + dependency: transitive + description: + name: test_core + sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.16" timezone: dependency: "direct main" description: name: timezone - sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + sha256: "784a5e34d2eb62e1326f24d6f600aaaee452eb8ca8ef2f384a59244e292d158b" url: "https://pub.flutter-io.cn" source: hosted - version: "0.9.4" - timing: - dependency: transitive - description: - name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.2" + version: "0.11.0" typed_data: dependency: transitive description: @@ -2636,11 +2826,10 @@ packages: url_launcher: dependency: "direct main" description: - name: url_launcher - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.3.2" + path: "packages/url_launcher" + relative: true + source: path + version: "6.3.2-ohos.1" url_launcher_android: dependency: transitive description: @@ -2653,10 +2842,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" url: "https://pub.flutter-io.cn" source: hosted - version: "6.3.6" + version: "6.4.1" url_launcher_linux: dependency: transitive description: @@ -2673,6 +2862,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.2.5" + url_launcher_ohos: + dependency: transitive + description: + path: "packages/url_launcher_ohos" + relative: true + source: path + version: "6.3.2" url_launcher_platform_interface: dependency: transitive description: @@ -2685,10 +2881,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.1" + version: "2.4.3" url_launcher_windows: dependency: transitive description: @@ -2717,10 +2913,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "81da85e9ca8885ade47f9685b953cb098970d11be4821ac765580a6607ea4373" + sha256: "4d35a36400983c3457c289d4d553b5308f506ea84f7e51c7a564651b5525209a" url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.21" + version: "1.2.1" vector_graphics_codec: dependency: transitive description: @@ -2733,10 +2929,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + sha256: "98e7e94de127b46a86ef46197fff84ff99f3d3b80a708390d717ad731efef598" url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" + version: "1.2.2" vector_math: dependency: "direct main" description: @@ -2748,19 +2944,17 @@ packages: video_compress: dependency: "direct main" description: - name: video_compress - sha256: "31bc5cdb9a02ba666456e5e1907393c28e6e0e972980d7d8d619a7beda0d4f20" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.1.4" + path: "packages/video_compress" + relative: true + source: path + version: "3.1.2-ohos.1" video_player: dependency: "direct main" description: - name: video_player - sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.10.1" + path: "packages/video_player" + relative: true + source: path + version: "2.10.0-ohos.1" video_player_android: dependency: transitive description: @@ -2773,10 +2967,17 @@ packages: dependency: transitive description: name: video_player_avfoundation - sha256: d1eb970495a76abb35e5fa93ee3c58bd76fb6839e2ddf2fbb636674f2b971dd4 + sha256: a39d6f28f8069564d8cc17396472f958dd9eaddf2d5c8e90aad4d793ac369bf3 url: "https://pub.flutter-io.cn" source: hosted - version: "2.8.9" + version: "2.9.6" + video_player_ohos: + dependency: "direct overridden" + description: + path: "packages/video_player_ohos" + relative: true + source: path + version: "1.0.0+2" video_player_platform_interface: dependency: transitive description: @@ -2805,26 +3006,25 @@ packages: dependency: transitive description: name: vm_service - sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499" + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" url: "https://pub.flutter-io.cn" source: hosted - version: "15.1.0" + version: "15.2.0" wakelock_plus: dependency: "direct main" description: - name: wakelock_plus - sha256: "61713aa82b7f85c21c9f4cd0a148abd75f38a74ec645fcb1e446f882c82fd09b" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.3.3" + path: "packages/wakelock_plus" + relative: true + source: path + version: "1.4.0-ohos.1" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" + sha256: b13f99e992e7ae6a152e16c5559d3c07ff445b13330192662494e614ca3e7d7b url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.0" + version: "1.5.1" watcher: dependency: transitive description: @@ -2857,38 +3057,43 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.3" - webrtc_interface: + webkit_inspection_protocol: dependency: transitive description: - name: webrtc_interface - sha256: c6f100eac5057d9a817a60473126f9828c796d42884d498af4f339c97b21014f + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" url: "https://pub.flutter-io.cn" source: hosted - version: "1.5.1" + version: "1.2.1" + webrtc_interface: + dependency: "direct overridden" + description: + path: "packages/webrtc_interface" + relative: true + source: path + version: "1.5.1-ohos.1" wifi_iot: dependency: "direct main" description: - name: wifi_iot - sha256: "0861aed0c0afd6031b4337811d31cdd181c594a8a2c73e94826ea21d2cb4707b" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.3.19+2" + path: "packages/wifi_iot" + relative: true + source: path + version: "0.3.19-ohos.1" win32: - dependency: transitive + dependency: "direct overridden" description: - name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.flutter-io.cn" - source: hosted - version: "5.15.0" + path: "packages/win32" + relative: true + source: path + version: "6.2.0" win32_registry: dependency: transitive description: name: win32_registry - sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" + sha256: "73b1d78920a9d6e03f8b4e43e612b87bf3152a0e5c5e5150267762b7c4116904" url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.5" + version: "3.0.3" xdg_directories: dependency: transitive description: @@ -2913,6 +3118,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.1.3" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: "07c9e63ba42519745182b88ca12264a7ba2484d8239958778dfe4d44fe760488" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.4" yet_another_json_isolate: dependency: transitive description: @@ -2922,5 +3135,5 @@ packages: source: hosted version: "2.1.0" sdks: - dart: ">=3.9.2 <4.0.0" - flutter: ">=3.35.6" + dart: ">=3.11.5 <4.0.0" + flutter: ">=3.41.0" diff --git a/pubspec.yaml b/pubspec.yaml index 0dd518c8..69216900 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,9 +1,9 @@ # ============================================================ # 闲言APP (WordsLeisure) — Flutter 版 pubspec.yaml # 创建时间: 2026-04-20 -# 更新时间: 2026-04-23 +# 更新时间: 2026-05-16 # 作用: 项目依赖与资源配置 -# 上次更新: 新增 assets/data/ 色彩数据资源 +# 上次更新: 鸿蒙适配-新增wakelock_plus/audioplayers/record/mobile_scanner/wifi_iot/sqflite本地化 # ============================================================ name: xianyan @@ -12,7 +12,7 @@ publish_to: 'none' version: 5.2.0+26051501 environment: - sdk: ^3.9.2 + sdk: ^3.11.5 # ============================================================ # 依赖 — Phase 0 必装 @@ -23,215 +23,235 @@ dependencies: sdk: flutter # iOS 风格图标 - cupertino_icons: ^1.0.8 + cupertino_icons: ^1.0.8 # iOS风格图标库 # --- 状态管理 + 依赖注入 --- - flutter_riverpod: ^2.5.0 - riverpod_annotation: ^2.3.5 + flutter_riverpod: ^3.0.0 # 响应式状态管理+依赖注入 + riverpod_annotation: ^4.0.0 # Riverpod代码生成注解 # --- 路由 --- - go_router: ^14.0.0 + go_router: ^17.0.0 # 声明式路由导航 # --- 网络请求 --- - dio: ^5.4.0 + dio: ^5.4.0 # HTTP客户端+拦截器 # --- 本地数据库 --- - drift: ^2.16.0 - sqlite3_flutter_libs: ^0.5.0 + drift: ^2.16.0 # 类型安全SQLite ORM + sqlite3_flutter_libs: ^0.5.0 # SQLite原生库绑定 # --- 数据模型 --- - freezed_annotation: ^2.4.0 - json_annotation: ^4.9.0 + freezed_annotation: ^3.0.0 # 不可变数据类注解 + json_annotation: ^4.9.0 # JSON序列化注解 # --- KV 存储 --- - shared_preferences: ^2.2.0 - flutter_secure_storage: ^9.2.0 - hive: ^2.2.3 - hive_flutter: ^1.1.0 + shared_preferences: # v2.5.5 | 轻量KV持久化(本地化-鸿蒙适配) + path: packages/shared_preferences + flutter_secure_storage: # v9.2.4 | 加密安全存储(本地化-鸿蒙适配) + path: packages/flutter_secure_storage + hive: ^2.2.3 # 高性能NoSQL数据库 + hive_flutter: ^1.1.0 # Hive Flutter适配 # --- 文件路径 --- - path_provider: ^2.1.0 - path: ^1.9.0 + path_provider: # v2.1.5 | 系统目录路径获取(本地化-鸿蒙适配) + path: packages/path_provider + path: ^1.9.0 # 路径操作工具 # --- 工具 --- - uuid: ^4.3.0 - intl: ^0.20.2 - logger: ^2.3.0 - collection: ^1.18.0 - fl_chart: ^0.69.0 + uuid: ^4.5.0 # UUID生成器 + intl: ^0.20.2 # 国际化+日期格式化 + logger: ^2.5.0 # 分级日志输出 + collection: ^1.19.0 # 集合操作扩展 + fl_chart: ^1.2.0 # 折线图/柱状图/饼图 # --- 设备信息 --- - package_info_plus: ^8.0.2 - connectivity_plus: ^6.0.0 - device_info_plus: ^10.0.0 + package_info_plus: # v10.1.0 | 应用包信息读取(本地化-鸿蒙适配) + path: packages/package_info_plus + connectivity_plus: # v7.1.1 | 网络连接状态监听(本地化-鸿蒙适配) + path: packages/connectivity_plus + device_info_plus: # v13.1.0 | 设备硬件信息读取(本地化-鸿蒙适配) + path: packages/device_info_plus # --- 权限 --- - permission_handler: ^11.3.0 + permission_handler: # v12.0.1 | 运行时权限请求(本地化-鸿蒙适配) + path: packages/permission_handler # --- 本地通知 --- - flutter_local_notifications: ^18.0.1 + flutter_local_notifications: # v21.0.0 | 本地推送通知(本地化-鸿蒙适配) + path: packages/flutter_local_notifications # --- 外部链接 --- - url_launcher: ^6.2.0 - app_links: ^6.3.0 + url_launcher: # v6.3.2 | 打开外部URL/应用(本地化-鸿蒙适配) + path: packages/url_launcher + app_links: # v7.0.0-ohos.1 | 深度链接处理(本地化-鸿蒙适配) + path: packages/app_links # --- Supabase 后端 --- - supabase_flutter: ^2.5.0 + supabase_flutter: ^2.8.0 # Supabase客户端+认证 # --- 桌面小组件 --- - home_widget: + home_widget: # v0.9.1 | iOS/Android桌面小组件 path: packages/home_widget - -# 部分库引用本地 packages 目录 - # --- iOS 26 Liquid Glass 组件 (本地源码) --- - liquid_glass_widgets: 0.11.0 - liquid_glass_easy: + # --- iOS 26 Liquid Glass 组件 --- + liquid_glass_widgets: 0.11.0 # iOS26液态玻璃组件库 + liquid_glass_easy: # v1.1.1 | 液态玻璃效果封装 path: packages/liquid_glass_easy # --- 底部面板 + Hero 动画 --- - stupid_simple_sheet: + stupid_simple_sheet: # v0.9.1+1 | 简易底部弹出面板 path: packages/stupid_simple_sheet - heroine: ^0.7.2 + heroine: ^0.7.2 # Hero过渡动画增强 + + file_picker: # v8.3.7 | 文件选择器(本地化-鸿蒙适配) + path: packages/file_picker + image_picker: # v1.2.2 | 相机/相册选图(本地化-鸿蒙适配) + path: packages/image_picker + adaptive_palette: ^3.0.0 # 图片主色提取+流体背景 - file_picker: ^8.0.7 - #文件选择 - image_picker: ^1.2.1 - #相机/相册选图 - adaptive_palette: ^3.0.0 - #图片主色提取+流体背景 # --- UI 基础 --- - badges: + badges: # v3.2.0 | 角标/徽章组件 path: packages/badges - google_fonts: ^6.2.0 - cached_network_image: ^3.3.0 - flutter_cache_manager: ^3.3.0 - shimmer: ^3.0.0 + google_fonts: ^8.1.0 # Google字体加载 + cached_network_image: ^3.3.0 # 网络图片缓存+占位 + flutter_cache_manager: ^3.3.0 # 文件缓存管理 + shimmer: ^3.0.0 # 骨架屏加载占位 # --- 分享 + 导出 --- - share_plus: + share_plus: # v13.1.0 | 系统分享面板 path: packages/share_plus - qr_flutter: ^4.1.0 - gal: ^2.3.0 - archive: ^3.4.0 - crypto: ^3.0.0 - encrypt: ^5.0.3 - mailer: ^7.1.0 + qr_flutter: ^4.1.0 # 二维码渲染 + gal: # v2.3.0-ohos.1 | 保存图片/视频到相册(本地化-鸿蒙适配) + path: packages/gal + archive: ^4.0.0 # ZIP压缩/解压 + crypto: ^3.0.0 # 加密哈希算法 + encrypt: ^5.0.3 # 对称/非对称加密 + mailer: ^7.1.0 # SMTP邮件发送 # --- 图片处理 --- - image: ^4.1.0 + image: ^4.3.0 # 图片解码/编码/变换 - - # 部分库引用本地 packages 目录 - # --- 图片编辑器 (本地源码,方便查看案例和调试) --- - pro_image_editor: + # --- 图片编辑器 --- + pro_image_editor: # v12.4.4 | 图片编辑器核心(含魔改) path: packages/pro_image_editor # --- 异常捕获 --- - catcher_2: ^2.1.9 + catcher_2: # v2.1.9 | 全局异常捕获+上报(鸿蒙适配) + path: packages/catcher_2 # --- SVG 渲染 --- - flutter_svg: ^2.0.0 + flutter_svg: ^2.0.0 # SVG图片渲染 # --- 富文本编辑器 --- - flutter_quill: + flutter_quill: # v11.5.0 | Quill富文本编辑器 path: packages/flutter_quill # --- 虚线边框 --- - dotted_border: ^2.1.0 + dotted_border: ^3.1.0 # 虚线/点线边框装饰 # --- 颜色选择器 --- - flex_color_picker: + flex_color_picker: # v3.8.0 | HSL颜色选择器 path: packages/flex_color_picker # --- 屏幕适配 --- - flutter_screenutil: ^5.9.0 + flutter_screenutil: ^5.9.0 # 屏幕尺寸适配 # --- 动画 --- - flutter_animate: ^4.5.0 - flutter_card_swiper: + flutter_animate: ^4.5.0 # 声明式动画库 + flutter_card_swiper: # v7.2.0 | 卡片滑动切换 path: packages/flutter_card_swiper - animations: ^2.0.11 - lottie: ^3.3.0 - confetti: ^0.8.0 - animate_do: ^4.2.0 + animations: ^2.0.11 # Material过渡动画 + lottie: ^3.3.0 # Lottie动画播放 + confetti: ^0.8.0 # 撒花/彩纸效果 + animate_do: ^5.1.0 # 常用入场/出场动画 # --- 交互增强 --- - custom_refresh_indicator: ^4.0.1 + custom_refresh_indicator: ^4.0.1 # 自定义下拉刷新 # --- 列表交互 --- - flutter_slidable: ^4.0.3 - flutter_sticky_header: ^0.8.0 - value_layout_builder: ^0.5.0 + flutter_slidable: ^4.0.3 # 列表项滑动操作 + flutter_sticky_header: ^0.8.0 # 粘性头部 + value_layout_builder: ^0.5.0 # 值变化触发布局重建 # --- 内容渲染 --- - flutter_markdown_plus: ^1.0.1 - flutter_html: ^3.0.0-beta.2 + flutter_markdown_plus: ^1.0.1 # Markdown渲染 + flutter_html: ^3.0.0-beta.2 # HTML内容渲染 # --- 拼音转换 --- - pinyin: ^3.3.0 + pinyin: ^3.3.0 # 汉字转拼音 # --- iOS风格组件 --- - pull_down_button: ^0.10.1 + pull_down_button: ^0.10.1 # iOS下拉菜单按钮 # --- 布局增强 --- - sliver_tools: ^0.2.12 - flutter_staggered_grid_view: ^0.7.0 - visibility_detector: ^0.4.0+2 + sliver_tools: ^0.2.12 # Sliver工具集 + flutter_staggered_grid_view: ^0.7.0 # 瀑布流网格 + visibility_detector: ^0.4.0+2 # 组件可见性检测 # --- 提示反馈 --- - bot_toast: ^4.1.0 + bot_toast: ^4.1.0 # Toast/通知弹窗 - # --- Shader效果 (本地源码,方便查看案例和调试) --- - flutter_shaders_ui: + # --- Shader效果 --- + flutter_shaders_ui: # v0.1.1 | Fragment Shader效果 path: packages/flutter_shaders_ui - flutter_tilt: 3.3.4 - flutter_3d_controller: 2.3.0 - flutter_advanced_canvas_editor: 2.1.0 - flutter_spritesheet_animation: + flutter_tilt: ^4.0.0 # 3D倾斜交互效果 + flutter_3d_controller: 2.3.0 # 3D模型加载控制 + flutter_advanced_canvas_editor: 2.1.0 # 高级画布编辑器 + flutter_spritesheet_animation: # v1.0.2 | 精灵图帧动画 path: packages/flutter_spritesheet_animation - image_size_getter: + image_size_getter: # v2.4.1 | 图片尺寸读取(无需解码) path: packages/image_size_getter - extended_image: + extended_image: # v10.0.1 | 图片缓存+缩放+裁剪 path: packages/extended_image - photo_view: + photo_view: # v0.15.0 | 图片缩放/平移查看 path: packages/photo_view - flutter_image_compress: + flutter_image_compress: # v2.4.0 | 图片压缩(保持EXIF) path: packages/flutter_image_compress - vector_math: any - wakelock_plus: ^1.3.3 - audioplayers: ^6.1.0 - record: ^6.2.0 - video_compress: ^3.1.4 - video_player: ^2.9.2 - local_auth: ^2.3.0 - battery_plus: ^6.1.0 + vector_math: any # 向量数学运算 + wakelock_plus: # v1.4.0-ohos.1 | 屏幕常亮控制(本地化-鸿蒙适配) + path: packages/wakelock_plus + audioplayers: # v6.5.0-ohos.1 | 音频播放(本地化-鸿蒙适配) + path: packages/audioplayers + record: # v6.0.0-ohos.1 | 录音(本地化-鸿蒙适配) + path: packages/record + video_compress: # v3.1.2-ohos.1 | 视频压缩(本地化-鸿蒙适配) + path: packages/video_compress + video_player: # v2.10.0-ohos.1 | 视频播放(本地化-鸿蒙适配) + path: packages/video_player + local_auth: # v3.0.1 | 生物识别认证(本地化-鸿蒙适配) + path: packages/local_auth + battery_plus: # v7.0.0-ohos.1 | 电池状态监听(本地化-鸿蒙适配) + path: packages/battery_plus # --- 文件传输助手 --- - shelf: ^1.4.0 - shelf_router: ^1.1.0 - shelf_web_socket: ^2.0.0 - network_info_plus: ^6.1.0 - flutter_webrtc: ^0.12.12+hotfix.1 - web_socket_channel: ^3.0.3 - flutter_blue_plus: ^1.32.0 - nfc_manager: ^4.0.2 - mime: ^2.0.0 - mobile_scanner: ^6.0.0 - basic_utils: ^5.7.0 - wifi_iot: ^0.3.19 - nearby_service: + shelf: ^1.4.0 # HTTP服务器框架 + shelf_router: ^1.1.0 # 路由中间件 + shelf_web_socket: ^3.0.0 # WebSocket支持 + network_info_plus: # v8.1.0-ohos.1 | WiFi网络信息(本地化-鸿蒙适配) + path: packages/network_info_plus + flutter_webrtc: # v1.4.0-ohos.1 | WebRTC音视频通信(本地化-鸿蒙适配) + path: packages/flutter_webrtc + web_socket_channel: ^3.0.3 # WebSocket客户端 + flutter_blue_plus: # v2.1.0-ohos.1 | 蓝牙BLE通信(本地化-鸿蒙适配) + path: packages/flutter_blue_plus + flutter_nfc_kit: # v3.6.0-rc.6-ohos | NFC读写(TPC官方鸿蒙适配) + path: packages/flutter_nfc_kit + mime: ^2.0.0 # MIME类型识别 + mobile_scanner: # v7.1.4-ohos.1 | 二维码/条形码扫描(本地化-鸿蒙适配) + path: packages/mobile_scanner + basic_utils: ^5.7.0 # 通用工具集(Base64/ASN1) + wifi_iot: # v0.3.19-ohos.1 | WiFi IoT设备连接(本地化-鸿蒙适配) + path: packages/wifi_iot + nearby_service: # v0.2.1 | 近场设备发现+通信 path: packages/nearby_service - flutter_localizations: - sdk: flutter - timezone: any - ndef_record: any - sqflite: any - cross_file: any - receive_sharing_intent: + sdk: flutter # Flutter国际化支持 + timezone: any # 时区数据库 + sqflite: # v2.4.1-ohos.1 | SQLite轻量数据库(本地化-鸿蒙适配) + path: packages/sqflite + cross_file: any # 跨平台文件抽象 + receive_sharing_intent: # v1.8.1 | 接收外部分享内容 path: packages/receive_sharing_intent @@ -244,27 +264,106 @@ dev_dependencies: sdk: flutter # 代码生成 - build_runner: ^2.4.0 - freezed: ^2.5.0 - json_serializable: ^6.8.0 - drift_dev: ^2.16.0 - riverpod_generator: ^2.3.0 + build_runner: ^2.6.0 # 代码生成运行器 + freezed: ^3.2.0 # 不可变数据类生成 + json_serializable: ^6.11.0 # JSON序列化代码生成 + drift_dev: ^2.31.0 # Drift数据库代码生成 + riverpod_generator: ^4.0.0 # Riverpod Provider代码生成 # 代码规范 - flutter_lints: ^5.0.0 - riverpod_lint: ^2.3.0 - custom_lint: ^0.6.0 + flutter_lints: ^5.0.0 # Flutter lint规则 + riverpod_lint: ^3.0.0 # Riverpod专用lint + custom_lint: ^0.8.0 # 自定义lint插件 # 测试 - mocktail: ^1.0.0 + mocktail: ^1.0.0 # Mock测试库 # ============================================================ -# 依赖覆写 — 解决 liquid_glass_widgets 与 flutter_test 的 meta 版本冲突 -# flutter_test 固定 meta 1.16.0,liquid_glass_widgets 要求 meta ^1.17.0 +# 依赖覆写 — 解决版本冲突 +# 1. liquid_glass_widgets与flutter_test的meta版本冲突 +# 2. share_plus 13.x / device_info_plus 13.x 需要win32 ^6.0.1 +# 但 quill_native_bridge_windows 依赖 win32 ^5.5.0 +# 3. 本地化包覆写:让远程依赖的库也使用本地path版本 # ============================================================ dependency_overrides: meta: ^1.17.0 web: ^1.1.0 + path_provider: + path: packages/path_provider + shared_preferences: + path: packages/shared_preferences + flutter_secure_storage: + path: packages/flutter_secure_storage + flutter_secure_storage_windows: + path: packages/flutter_secure_storage_windows + connectivity_plus: + path: packages/connectivity_plus + package_info_plus: + path: packages/package_info_plus + device_info_plus: + path: packages/device_info_plus + permission_handler: + path: packages/permission_handler + flutter_local_notifications: + path: packages/flutter_local_notifications + url_launcher: + path: packages/url_launcher + file_picker: + path: packages/file_picker + image_picker: + path: packages/image_picker + local_auth: + path: packages/local_auth + quill_native_bridge_windows: + path: packages/quill_native_bridge_windows + video_player: + path: packages/video_player + video_player_ohos: + path: packages/video_player_ohos + wakelock_plus: + path: packages/wakelock_plus + audioplayers: + path: packages/audioplayers + audioplayers_ohos: + path: packages/audioplayers_ohos + record: + path: packages/record + record_ohos: + path: packages/record_ohos + mobile_scanner: + path: packages/mobile_scanner + wifi_iot: + path: packages/wifi_iot + sqflite: + path: packages/sqflite + sqflite_ohos: + path: packages/sqflite_ohos + sqflite_common: + path: packages/sqflite_common + sqflite_platform_interface: + path: packages/sqflite_platform_interface + video_compress: + path: packages/video_compress + flutter_blue_plus: + path: packages/flutter_blue_plus + flutter_blue_plus_ohos: + path: packages/flutter_blue_plus_ohos + gal: + path: packages/gal + network_info_plus: + path: packages/network_info_plus + network_info_plus_ohos: + path: packages/network_info_plus_ohos + app_links: + path: packages/app_links + app_links_ohos: + path: packages/app_links_ohos + flutter_webrtc: + path: packages/flutter_webrtc + webrtc_interface: + path: packages/webrtc_interface + win32: + path: packages/win32 # ============================================================ # Flutter 配置 diff --git a/scripts/resize_icon.py b/scripts/resize_icon.py new file mode 100644 index 00000000..5fabdd68 --- /dev/null +++ b/scripts/resize_icon.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +""" +@创建时间: 2026-05-16 +@更新时间: 2026-05-16 +@名称: resize_icon.py +@作用: 将源图片裁剪为从16x16到1024x1024的常用尺寸图标 +@上次更新内容: 初始创建 +""" + +import os +from PIL import Image + +SOURCE_IMAGE = os.path.join(os.path.dirname(__file__), "..", "assets", "templates", "xianyan.png") +OUTPUT_DIR = os.path.join(os.path.dirname(__file__), "..", "assets", "templates", "resized") + +SIZES = [ + 16, + 20, + 24, + 29, + 32, + 40, + 48, + 50, + 57, + 58, + 60, + 64, + 72, + 76, + 80, + 87, + 88, + 96, + 100, + 114, + 120, + 128, + 144, + 152, + 167, + 180, + 192, + 216, + 256, + 512, + 1024, +] + +SIZE_CATEGORIES = { + "favicon": [16, 32], + "ios_notification": [20], + "ios_settings": [29], + "ios_spotlight": [40], + "ios_app": [60, 76, 87, 120, 152, 167, 180, 1024], + "android_launcher": [48, 72, 96, 144, 192, 216], + "web_app": [64, 128, 256, 512], + "general": [24, 50, 57, 58, 80, 88, 100, 114], +} + + +def resize_image(source_path: str, output_dir: str, size: int) -> str: + img = Image.open(source_path) + if img.mode != "RGBA": + img = img.convert("RGBA") + + resized = img.resize((size, size), Image.LANCZOS) + filename = f"icon_{size}x{size}.png" + filepath = os.path.join(output_dir, filename) + resized.save(filepath, "PNG", compress_level=0) + return filename + + +def main(): + source_path = os.path.normpath(SOURCE_IMAGE) + output_dir = os.path.normpath(OUTPUT_DIR) + + if not os.path.exists(source_path): + print(f"[ERROR] Source image not found: {source_path}") + return + + os.makedirs(output_dir, exist_ok=True) + + print(f"Source: {source_path}") + print(f"Output: {output_dir}") + print(f"Sizes: {len(SIZES)} variants\n") + + for size in SIZES: + filename = resize_image(source_path, output_dir, size) + print(f" [OK] {filename}") + + print(f"\nDone! {len(SIZES)} icons generated in: {output_dir}") + + print("\n--- Size Category Reference ---") + for category, sizes in SIZE_CATEGORIES.items(): + matching = [s for s in sizes if s in SIZES] + if matching: + print(f" {category}: {', '.join(f'{s}x{s}' for s in matching)}") + + +if __name__ == "__main__": + main() diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 4d7dc881..8058edc8 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +34,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterBluePlusPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterBluePlusPlugin")); FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 9ed39f21..e33347ae 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST battery_plus connectivity_plus file_selector_windows + flutter_blue_plus_winrt flutter_inappwebview_windows flutter_secure_storage_windows flutter_webrtc @@ -21,6 +22,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_local_notifications_windows jni )