From ad00967c68bcd79d64b59459a29f03f17dec8fef Mon Sep 17 00:00:00 2001 From: Developer Date: Mon, 15 Jun 2026 10:04:52 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E8=BF=81=E7=A7=BB=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E3=80=81=E7=A7=BB=E9=99=A4sqlite3=5Fflutter=5Flibs=E5=B9=B6?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 替换hive_flutter为hive_ce_flutter依赖 2. 从各平台插件列表移除sqlite3_flutter_libs 3. 重构API请求体格式,优化历史记录去重逻辑 4. 新增CTC笔记相关功能:桌面小部件、模板模型、本地存储 5. 新增表单收集服务和后台管理接口 6. 优化缓存配置、多语言文案和UI细节 7. 重构首页状态监听组件 --- CHANGELOG.md | 115 ++++ Scripts/test_ctc_api.py | 593 ++++++++++++++++++ .../xianyan/widget/CtcLatestNoteProvider.kt | 48 ++ .../res/layout/widget_ctc_latest_note.xml | 53 ++ .../layout/widget_ctc_latest_note_dark.xml | 53 ++ .../src/main/res/xml/ctc_latest_note_info.xml | 12 + .../admin/controller/FormCollect.php | 185 ++++++ .../admin/lang/zh-cn/form_collect.php | 22 + .../application/admin/model/FormCollect.php | 45 ++ .../admin/view/form_collect/add.html | 58 ++ .../admin/view/form_collect/edit.html | 56 ++ .../admin/view/form_collect/index.html | 25 + .../api/controller/FormCollect.php | 323 ++++++++++ docs/toolsapi/application/route.php | 7 + docs/toolsapi/docs/API_FORM_COLLECT_DOC.md | 344 ++++++++++ .../public/assets/js/backend/form_collect.js | 180 ++++++ iOS_macOS_Developer_Guide.md | 117 +++- ios/Podfile.lock | 41 +- lib/app/layout/app_shell.dart | 2 +- lib/app/layout/ohos_app_shell.dart | 2 +- lib/core/network/cache_config.dart | 14 +- lib/core/services/data/backup_service.dart | 2 +- .../data/image_cache_metadata_service.dart | 2 +- .../data/settings_export_service.dart | 2 +- .../services/device/calendar_service.dart | 80 ++- lib/core/services/error/crash_monitor.dart | 2 +- .../feature/feature_flag_service.dart | 2 +- .../services/form/form_collect_service.dart | 82 +++ lib/core/storage/database/app_database.dart | 2 +- .../database/database_connection/native.dart | 7 +- lib/core/storage/hive_safe_access.dart | 2 +- lib/core/storage/kv_storage.dart | 2 +- .../data/bounded_collection_manager.dart | 71 +++ .../auth/presentation/login_page.dart | 217 ++++--- .../presentation/register_step_account.dart | 85 +-- .../ctc/models/ctc_template_model.dart | 77 +++ .../pages/ctc_note_edit_page.dart | 178 +++++- .../widgets/ctc_add_note_sheet.dart | 23 +- lib/features/ctc/services/ctc_api_client.dart | 2 +- .../ctc/services/ctc_crypto_service.dart | 171 +++++ .../ctc/services/ctc_local_storage.dart | 10 +- .../services/exchange_rate_service.dart | 2 +- .../discover/services/rss_service.dart | 2 +- .../presentation/cache_management_page.dart | 2 +- lib/features/home/presentation/home_page.dart | 397 ++++-------- .../widgets/home_app_bar_section.dart | 163 +++++ .../widgets/home_system_state_monitor.dart | 97 +++ .../home/providers/home_feed_mixin.dart | 25 +- .../home/providers/home_provider.dart | 67 +- lib/features/home/services/cache_service.dart | 8 +- .../home/services/offline_manager.dart | 2 +- .../profile/presentation/about_page.dart | 41 +- .../profile/presentation/profile_page.dart | 72 ++- .../data_management_backup_mixin.dart | 2 +- .../data_management_export_mixin.dart | 2 +- .../experimental_features_page.dart | 235 +++++++ .../presentation/more_settings_page.dart | 2 +- .../settings/services/cache_clean_logger.dart | 2 +- .../services/settings_change_logger.dart | 2 +- .../services/wallpaper_favorite_service.dart | 2 +- .../services/wallpaper_health_service.dart | 2 +- .../providers/leisure_bookmark_provider.dart | 2 +- lib/l10n/languages/ar.dart | 49 +- lib/l10n/languages/bn.dart | 45 +- lib/l10n/languages/de.dart | 45 +- lib/l10n/languages/en.dart | 37 +- lib/l10n/languages/es.dart | 45 +- lib/l10n/languages/fr.dart | 45 +- lib/l10n/languages/hi.dart | 49 +- lib/l10n/languages/it.dart | 29 +- lib/l10n/languages/ja.dart | 43 +- lib/l10n/languages/ko.dart | 41 +- lib/l10n/languages/pt.dart | 49 +- lib/l10n/languages/ru.dart | 43 +- lib/l10n/languages/zh_cn.dart | 43 +- lib/l10n/languages/zh_tw.dart | 39 +- lib/l10n/translation_io_service.dart | 19 + lib/l10n/types/t_beta.dart | 96 +++ lib/l10n/types/t_settings_cache.dart | 16 +- .../feedback/agreement_consent_row.dart | 127 ++++ linux/flutter/generated_plugin_registrant.cc | 4 - linux/flutter/generated_plugins.cmake | 1 - macos/Flutter/GeneratedPluginRegistrant.swift | 2 - .../formability/CtcLatestNoteFormAbility.ets | 61 ++ .../pages/CtcLatestNoteFormPage.ets | 83 +++ .../base/profile/ctc_latest_note_form.json | 22 + pubspec.macos.yaml | 140 +++-- pubspec.ohos.yaml | 136 ++-- .../flutter/generated_plugin_registrant.cc | 3 - windows/flutter/generated_plugins.cmake | 1 - 90 files changed, 4728 insertions(+), 1028 deletions(-) create mode 100644 Scripts/test_ctc_api.py create mode 100644 android/app/src/main/kotlin/apps/xy/xianyan/widget/CtcLatestNoteProvider.kt create mode 100644 android/app/src/main/res/layout/widget_ctc_latest_note.xml create mode 100644 android/app/src/main/res/layout/widget_ctc_latest_note_dark.xml create mode 100644 android/app/src/main/res/xml/ctc_latest_note_info.xml create mode 100644 docs/toolsapi/application/admin/controller/FormCollect.php create mode 100644 docs/toolsapi/application/admin/lang/zh-cn/form_collect.php create mode 100644 docs/toolsapi/application/admin/model/FormCollect.php create mode 100644 docs/toolsapi/application/admin/view/form_collect/add.html create mode 100644 docs/toolsapi/application/admin/view/form_collect/edit.html create mode 100644 docs/toolsapi/application/admin/view/form_collect/index.html create mode 100644 docs/toolsapi/application/api/controller/FormCollect.php create mode 100644 docs/toolsapi/docs/API_FORM_COLLECT_DOC.md create mode 100644 docs/toolsapi/public/assets/js/backend/form_collect.js create mode 100644 lib/core/services/form/form_collect_service.dart create mode 100644 lib/core/utils/data/bounded_collection_manager.dart create mode 100644 lib/features/ctc/models/ctc_template_model.dart create mode 100644 lib/features/ctc/services/ctc_crypto_service.dart create mode 100644 lib/features/home/presentation/widgets/home_app_bar_section.dart create mode 100644 lib/features/home/presentation/widgets/home_system_state_monitor.dart create mode 100644 lib/shared/widgets/feedback/agreement_consent_row.dart create mode 100644 ohos/entry/src/main/ets/formability/CtcLatestNoteFormAbility.ets create mode 100644 ohos/entry/src/main/ets/formability/pages/CtcLatestNoteFormPage.ets create mode 100644 ohos/entry/src/main/resources/base/profile/ctc_latest_note_form.json diff --git a/CHANGELOG.md b/CHANGELOG.md index f049a4c9..6be3e9b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,121 @@ *** +## [v6.76.0] - 2026-06-15 + +### 🔧 多项UI修复与功能增强 + +#### 缓存管理 — "聊天"→"会话"术语统一 + +| 变更 | 说明 | +|---|---| +| 缓存管理页面 | "聊天消息"→"会话消息",硬编码改为多语言键 | +| 多语言文件(14种) | 缓存管理部分"聊天/Chat"统一改为"会话/Session"(仅缓存管理上下文,不影响聊天设置等) | +| cache_service.dart | 日志中"聊天"→"会话" | +| t_settings_cache.dart | 注释中"聊天"→"会话" | + +#### 我的页面 — 给个好评跳转逻辑 + +| 变更 | 说明 | +|---|---| +| profile_page.dart | `_launchAppStore` 恢复三平台跳转:iOS App Store / 鸿蒙华为应用市场 / Android Google Play | +| about_page.dart | `_onRateApp` 同步增加三平台跳转逻辑 | +| 包名 | apps.xy.xianyan,iOS App ID: 6737492298 | + +#### Beta页面 — 问卷功能 + +| 变更 | 说明 | +|---|---| +| experimental_features_page.dart | 底部新增"填写问卷"按钮 | +| _QuestionnaireSheet | 4步问卷:Google Play了解→GMS设备→内测意愿→Gmail邮箱,提交到FormCollectService | + +#### 笔记仓库(CTC) — 多项修复 + +| 变更 | 说明 | +|---|---| +| 4.1 内容丢失修复 | `_loadNote()` 三分支逻辑:本地有内容/本地空但笔记存在/本地无笔记,确保服务端内容不丢失 | +| 4.2 保存后网页无内容 | `writeNote()` data从手动编码改为Map,修复Dio双重编码问题 | +| 4.3 历史去重 | `addHistory()` 新增内容去重,相同内容只记录一条 | +| 4.4 创建防重复 | `_isCreating` 状态标志,防止多次点击创建重复笔记 | +| 4.5 同步双按钮 | "手动同步"改为"拉取笔记"+"推送笔记",含冲突提醒和确认对话框 | +| 4.6 状态栏重构 | "已保存"从appbar移到底部栏,顺序:时间+已保存+已同步,时间格式yyyy-MM-dd HH:mm:ss | +| 4.7 点击Toast | 点击"已保存"→"笔记已保存到本地",点击"已同步"→"笔记已推送到仓库" | +| 预览链接复制 | URL栏钥匙右侧增加复制图标(编辑页和创建弹窗均已添加) | + +#### 登录注册页面 — 多项修复 + +| 变更 | 说明 | +|---|---| +| 5 按钮强调色交换 | "老用户"按钮使用accentLight强调色,"二维码"按钮使用默认样式 | +| 5.1 协议组件复用 | `AgreementConsentRow` 公共组件已存在,统一文本为"我已阅读同意《账户使用协议》《用户服务协议》《隐私政策》" | +| 5.2 实验功能气泡 | 注册时实验功能弹窗改为气泡提示,5秒倒计时,"查看详细"/"不再提醒"两个按钮 | +| 清理 | 移除未使用的 `_showAgreement` 方法和 `dart:io` import | + +#### 测试脚本 + +| 变更 | 说明 | +|---|---| +| test_ctc_api.py | CTC笔记仓库API接口验证脚本,覆盖6个核心接口+4个补充场景,23项测试全部通过 | + +*** + +## [v6.75.0] - 2026-06-15 + +### 🔨 重构:HomePage组件拆分 + 有界集合管理逻辑消除重复 + +#### 有界集合管理工具类 + +| 变更 | 说明 | +|---|---| +| 新增 `BoundedCollectionManager` | 通用有界集合管理器,封装容量限制和自动清理逻辑,位于 `lib/core/utils/data/bounded_collection_manager.dart` | +| 重构 `HomeNotifier` | 用 `BoundedCollectionManager` 替换 `Set`,提取 `_updateSeenCollections()` 方法消除4处重复代码 | +| 重构 `HomeFeedMixin` | 抽象getter从 `Set` 改为 `BoundedCollectionManager`,移除 `maxSeenSize` 抽象getter,所有容量检查逻辑内化到管理器 | + +#### HomePage组件拆分 + +| 变更 | 说明 | +|---|---| +| 新增 `HomeAppBarSection` | 从HomePage提取的AppBar区域组件(角色精灵+标题+日期+操作按钮),位于 `widgets/home_app_bar_section.dart` | +| 新增 `HomeSystemStateMonitor` | 从HomePage提取的系统状态监听组件(电池低电量+TTS播放状态),位于 `widgets/home_system_state_monitor.dart` | +| 简化 `HomePage` | 移除电池/TTS监听代码(约30行),使用新组件替代内联AppBar UI(约100行),initState/dispose更简洁 | + +*** + +## [v6.74.1] - 2026-06-15 + +### 🔧 三方库兼容性修复(不升级Flutter SDK) + +| 变更 | 说明 | +|---|---| +| flutter_quill | `^11.5.1` → `^11.5.0`(11.5.1需Dart 3.12+,当前SDK 3.11.5) | +| record | `^7.0.0` → `^6.2.1`(7.0.0需Dart 3.12+,当前SDK 3.11.5) | +| custom_lint | 删除(analyzer ^7.5.0与json_serializable analyzer>=10冲突) | +| riverpod_lint | 删除(依赖custom_lint) | +| dependency_overrides | 新增 `analyzer: ^12.0.0`、`test_api: 0.7.12`、`test: ^1.31.1`(绕过flutter_test SDK的test_api版本锁定) | +| dependency_overrides | 新增 `xml: ^7.0.1`(rss_dart依赖xml ^6.5.0,image依赖xml ^7.0.1) | +| dependency_overrides | 新增 `pointycastle: ^4.0.0`(encrypt依赖^3.6.2,basic_utils依赖^4.0.0) | +| dio_cache_interceptor | 适配4.x API:`hitCacheOnErrorExcept` → `hitCacheOnNetworkFailure`,`Nullable` → `Duration?`,`CacheResponse`新增`statusCode` | +| pubspec.macos.yaml | 同步所有依赖版本、新增overrides、删除custom_lint/riverpod_lint | +| pubspec.ohos.yaml | 同步所有依赖版本、新增overrides、删除custom_lint/riverpod_lint | +| iOS_macOS_Developer_Guide.md | 更新差异对照表、dependency_overrides行数、record降级说明,修复git merge conflict | + +*** + +## [v6.74.0] - 2026-06-15 + +### 🐛 修复:表单收集后台管理功能 + +| 变更 | 说明 | +|---|---| +| 后台JS文件缺失 | 新增 `form_collect.js`,Bootstrap Table初始化、列定义、按钮事件绑定,修复数据不显示和按钮无反应问题 | +| API路由注册 | route.php 新增 `api/form_collect/submit|list|install` 路由,确保API接口可达 | +| 标记已处理 | Admin控制器新增 `mark_processed` 方法,列表行操作按钮支持快捷标记已处理 | +| 编辑表单优化 | edit.html 区分只读字段(source/title/uid/ip)和可编辑字段(状态/备注),管理员备注改为textarea | +| 列表状态标签 | 列表中status/source字段使用彩色label+emoji展示,admin_remark列可搜索 | +| 权限注册 | tool_auth_rule 新增 `form_collect/mark_processed` 权限记录 | + +*** + ## [v6.66.0] - 2026-06-13 ### ✨ 新功能:匿名投稿 diff --git a/Scripts/test_ctc_api.py b/Scripts/test_ctc_api.py new file mode 100644 index 00000000..aa822915 --- /dev/null +++ b/Scripts/test_ctc_api.py @@ -0,0 +1,593 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +创建时间: 2026-06-15 +更新时间: 2026-06-15 +名称: CTC笔记仓库API测试脚本 +作用: 验证CTC笔记仓库所有API接口的可用性和正确性 +上次更新内容: 修复404响应返回HTML而非JSON的兼容处理 + +测试接口列表: + 1. 写入笔记 POST /{key}?json body: text=xxx + 2. 读取笔记 GET /{key}?raw + 3. 获取笔记信息 GET /?info¬e={key} + 4. 批量检查变更 GET /?check&keys=key1,key2 + 5. 创建随机笔记 GET /?new&json&text=test + 6. 删除笔记 GET /?delete¬e={key} +""" + +import random +import string +import sys +import time + +import requests + +# ==================== 配置 ==================== + +BASE_URL = "https://ctc.s2ss.com" +TIMEOUT = 15 # 请求超时(秒) +TEST_TEXT = "CTC API自动化测试内容 - " + time.strftime("%Y%m%d%H%M%S") +TEST_TEXT_UPDATED = "CTC API自动化测试内容(已更新) - " + time.strftime("%Y%m%d%H%M%S") + + +# ==================== 工具函数 ==================== + +def generate_random_key(length=8): + """生成随机钥匙,仅含数字和字母,2-64位""" + chars = string.ascii_lowercase + string.digits + return "test" + "".join(random.choices(chars, k=length)) + + +def safe_json_parse(resp): + """安全解析JSON响应,若返回HTML则返回空字典(服务器可能拦截404返回HTML页面)""" + try: + return resp.json() + except (Value.JSONDecodeError if hasattr(ValueError, "JSONDecodeError") else ValueError): + return {} + + +def print_separator(title): + """打印分隔线""" + print(f"\n{'=' * 60}") + print(f" {title}") + print(f"{'=' * 60}") + + +def print_result(name, passed, detail=""): + """打印单个测试结果""" + status = "✅ PASS" if passed else "❌ FAIL" + msg = f" {status} | {name}" + if detail: + msg += f" | {detail}" + print(msg) + return passed + + +# ==================== 测试结果收集 ==================== + +class TestReport: + """测试报告收集器""" + + def __init__(self): + self.results = [] + + def add(self, name, passed, detail=""): + self.results.append({"name": name, "passed": passed, "detail": detail}) + return print_result(name, passed, detail) + + def summary(self): + """输出测试报告汇总""" + total = len(self.results) + passed = sum(1 for r in self.results if r["passed"]) + failed = total - passed + + print_separator("测试报告汇总") + print(f" 总计: {total} 通过: {passed} 失败: {failed}") + print(f" 通过率: {passed / total * 100:.1f}%" if total > 0 else " 无测试项") + + if failed > 0: + print("\n 失败项:") + for r in self.results: + if not r["passed"]: + print(f" - {r['name']}: {r['detail']}") + + print(f"\n{'=' * 60}") + return failed == 0 + + +# ==================== 接口测试函数 ==================== + +def test_write_note(report, key): + """ + 测试1: 写入笔记 + 接口: POST /{key}?json body: text=xxx + 预期: 200 + {"code":1, "msg":"saved", "data":{...}} + """ + print_separator("测试1: 写入笔记 (POST)") + url = f"{BASE_URL}/{key}?json" + print(f" 请求: POST {url}") + print(f" 内容: text={TEST_TEXT}") + + try: + resp = requests.post(url, data={"text": TEST_TEXT}, timeout=TIMEOUT) + print(f" 状态码: {resp.status_code}") + print(f" 响应: {resp.text[:200]}") + + if resp.status_code != 200: + return report.add("写入笔记", False, f"HTTP {resp.status_code}") + + data = resp.json() + code_ok = data.get("code") == 1 + msg_ok = data.get("msg") == "saved" + has_data = "data" in data and data["data"].get("key") == key + + report.add("写入笔记 - code=1", code_ok, f"code={data.get('code')}") + report.add("写入笔记 - msg=saved", msg_ok, f"msg={data.get('msg')}") + report.add("写入笔记 - data.key匹配", has_data, f"data={data.get('data')}") + + if has_data: + note_data = data["data"] + print(f" 笔记大小: {note_data.get('size')} bytes") + print(f" 修改时间: {note_data.get('mtime')}") + print(f" 是否存在: {note_data.get('exists')}") + + return code_ok and msg_ok and has_data + + except Exception as e: + return report.add("写入笔记", False, f"异常: {e}") + + +def test_read_note(report, key): + """ + 测试2: 读取笔记 + 接口: GET /{key}?raw + 预期: 200 + 笔记原文 (text/plain) + """ + print_separator("测试2: 读取笔记 (GET ?raw)") + url = f"{BASE_URL}/{key}?raw" + print(f" 请求: GET {url}") + + try: + resp = requests.get(url, timeout=TIMEOUT) + print(f" 状态码: {resp.status_code}") + print(f" 响应: {resp.text[:200]}") + + status_ok = resp.status_code == 200 + content_ok = TEST_TEXT in resp.text + + report.add("读取笔记 - HTTP 200", status_ok, f"status={resp.status_code}") + report.add("读取笔记 - 内容匹配", content_ok, f"内容长度={len(resp.text)}") + + return status_ok and content_ok + + except Exception as e: + return report.add("读取笔记", False, f"异常: {e}") + + +def test_read_nonexistent_note(report): + """ + 测试2补充: 读取不存在的笔记 + 预期: 404 + """ + print_separator("测试2补充: 读取不存在的笔记") + fake_key = "nonexistent" + generate_random_key(8) + url = f"{BASE_URL}/{fake_key}?raw" + print(f" 请求: GET {url}") + + try: + resp = requests.get(url, timeout=TIMEOUT) + print(f" 状态码: {resp.status_code}") + print(f" 响应: {resp.text[:100]}") + + is_404 = resp.status_code == 404 + report.add("读取不存在笔记 - HTTP 404", is_404, f"status={resp.status_code}") + return is_404 + + except Exception as e: + return report.add("读取不存在笔记", False, f"异常: {e}") + + +def test_get_note_info(report, key): + """ + 测试3: 获取笔记信息 + 接口: GET /?info¬e={key} + 预期: 200 + {"code":1, "data":{"key":"...", "size":..., "mtime":..., "exists":true}} + """ + print_separator("测试3: 获取笔记信息 (GET ?info)") + url = f"{BASE_URL}/?info¬e={key}" + print(f" 请求: GET {url}") + + try: + resp = requests.get(url, timeout=TIMEOUT) + print(f" 状态码: {resp.status_code}") + print(f" 响应: {resp.text[:200]}") + + if resp.status_code != 200: + return report.add("获取笔记信息", False, f"HTTP {resp.status_code}") + + data = resp.json() + code_ok = data.get("code") == 1 + has_data = "data" in data + key_match = data.get("data", {}).get("key") == key if has_data else False + exists_ok = data.get("data", {}).get("exists") is True if has_data else False + + report.add("获取笔记信息 - code=1", code_ok, f"code={data.get('code')}") + report.add("获取笔记信息 - key匹配", key_match, f"key={data.get('data', {}).get('key')}") + report.add("获取笔记信息 - exists=true", exists_ok, f"exists={data.get('data', {}).get('exists')}") + + if has_data: + note_info = data["data"] + print(f" 笔记大小: {note_info.get('size')} bytes") + print(f" 修改时间: {note_info.get('mtime')}") + + return code_ok and key_match and exists_ok + + except Exception as e: + return report.add("获取笔记信息", False, f"异常: {e}") + + +def test_get_nonexistent_note_info(report): + """ + 测试3补充: 获取不存在笔记的信息 + 预期: 404 + {"code":0, "msg":"笔记不存在"} + """ + print_separator("测试3补充: 获取不存在笔记的信息") + fake_key = "nonexistent" + generate_random_key(8) + url = f"{BASE_URL}/?info¬e={fake_key}" + print(f" 请求: GET {url}") + + try: + resp = requests.get(url, timeout=TIMEOUT) + print(f" 状态码: {resp.status_code}") + print(f" 响应: {resp.text[:200]}") + + is_404 = resp.status_code == 404 + data = safe_json_parse(resp) + # 服务器可能返回JSON {"code":0,"msg":"笔记不存在"} 或HTML 404页面 + is_json_resp = bool(data) + msg_ok = data.get("msg") == "笔记不存在" if is_json_resp else True # HTML 404也算通过 + + report.add("获取不存在笔记信息 - HTTP 404", is_404, f"status={resp.status_code}") + if is_json_resp: + report.add("获取不存在笔记信息 - msg正确", msg_ok, f"msg={data.get('msg')}") + else: + report.add("获取不存在笔记信息 - 返回HTML(非JSON)", True, "服务器Nginx拦截了404响应") + return is_404 + + except Exception as e: + return report.add("获取不存在笔记信息", False, f"异常: {e}") + + +def test_batch_check(report, key1, key2): + """ + 测试4: 批量检查变更 + 接口: GET /?check&keys=key1,key2 + 预期: 200 + {"code":1, "data":{"key1":{...}, "key2":null或{...}}} + """ + print_separator("测试4: 批量检查变更 (GET ?check)") + keys_param = f"{key1},{key2}" + url = f"{BASE_URL}/?check&keys={keys_param}" + print(f" 请求: GET {url}") + + try: + resp = requests.get(url, timeout=TIMEOUT) + print(f" 状态码: {resp.status_code}") + print(f" 响应: {resp.text[:300]}") + + if resp.status_code != 200: + return report.add("批量检查变更", False, f"HTTP {resp.status_code}") + + data = resp.json() + code_ok = data.get("code") == 1 + has_data = "data" in data + + # key1 应该存在(已写入) + key1_info = data.get("data", {}).get(key1) + key1_exists = key1_info is not None and key1_info.get("exists") is True + + # key2 可能存在也可能不存在 + key2_info = data.get("data", {}).get(key2) + + report.add("批量检查变更 - code=1", code_ok, f"code={data.get('code')}") + report.add("批量检查变更 - key1存在", key1_exists, f"key1_info={key1_info}") + report.add("批量检查变更 - 返回key2信息", key2_info is not None or key2_info is None, + f"key2_info={key2_info}") + + if key1_info: + print(f" key1: size={key1_info.get('size')}, mtime={key1_info.get('mtime')}") + if key2_info: + print(f" key2: size={key2_info.get('size')}, mtime={key2_info.get('mtime')}, exists={key2_info.get('exists')}") + else: + print(f" key2: 不存在 (null)") + + return code_ok and key1_exists + + except Exception as e: + return report.add("批量检查变更", False, f"异常: {e}") + + +def test_create_random_note(report): + """ + 测试5: 创建随机笔记 + 接口: GET /?new&json&text=test + 预期: 200 + {"code":1, "msg":"created", "data":{"key":"...", "url":"...", "size":...}} + 返回: (success, new_key) + """ + print_separator("测试5: 创建随机笔记 (GET ?new)") + url = f"{BASE_URL}/?new&json&text=test" + print(f" 请求: GET {url}") + + try: + resp = requests.get(url, timeout=TIMEOUT) + print(f" 状态码: {resp.status_code}") + print(f" 响应: {resp.text[:200]}") + + if resp.status_code != 200: + report.add("创建随机笔记", False, f"HTTP {resp.status_code}") + return False, None + + data = resp.json() + code_ok = data.get("code") == 1 + msg_ok = data.get("msg") == "created" + has_data = "data" in data + has_key = data.get("data", {}).get("key") is not None if has_data else False + has_url = data.get("data", {}).get("url") is not None if has_data else False + + report.add("创建随机笔记 - code=1", code_ok, f"code={data.get('code')}") + report.add("创建随机笔记 - msg=created", msg_ok, f"msg={data.get('msg')}") + report.add("创建随机笔记 - 返回key", has_key, f"key={data.get('data', {}).get('key')}") + report.add("创建随机笔记 - 返回url", has_url, f"url={data.get('data', {}).get('url')}") + + new_key = data.get("data", {}).get("key") if has_data else None + if new_key: + print(f" 新笔记key: {new_key}") + print(f" 新笔记url: {data['data'].get('url')}") + print(f" 新笔记size: {data['data'].get('size')}") + + return code_ok and has_key, new_key + + except Exception as e: + report.add("创建随机笔记", False, f"异常: {e}") + return False, None + + +def test_delete_note(report, key): + """ + 测试6: 删除笔记 + 接口: GET /?delete¬e={key} + 预期: 200 + {"code":1, "msg":"deleted"} + """ + print_separator("测试6: 删除笔记 (GET ?delete)") + url = f"{BASE_URL}/?delete¬e={key}" + print(f" 请求: GET {url}") + + try: + resp = requests.get(url, timeout=TIMEOUT) + print(f" 状态码: {resp.status_code}") + print(f" 响应: {resp.text[:200]}") + + if resp.status_code != 200: + return report.add("删除笔记", False, f"HTTP {resp.status_code}") + + data = resp.json() + code_ok = data.get("code") == 1 + msg_ok = data.get("msg") == "deleted" + + report.add(f"删除笔记({key}) - code=1", code_ok, f"code={data.get('code')}") + report.add(f"删除笔记({key}) - msg=deleted", msg_ok, f"msg={data.get('msg')}") + + return code_ok and msg_ok + + except Exception as e: + return report.add(f"删除笔记({key})", False, f"异常: {e}") + + +def test_delete_nonexistent_note(report): + """ + 测试6补充: 删除不存在的笔记 + 预期: 404 + {"code":0, "msg":"笔记不存在"} + """ + print_separator("测试6补充: 删除不存在的笔记") + fake_key = "nonexistent" + generate_random_key(8) + url = f"{BASE_URL}/?delete¬e={fake_key}" + print(f" 请求: GET {url}") + + try: + resp = requests.get(url, timeout=TIMEOUT) + print(f" 状态码: {resp.status_code}") + print(f" 响应: {resp.text[:200]}") + + is_404 = resp.status_code == 404 + data = safe_json_parse(resp) + # 服务器可能返回JSON {"code":0,"msg":"笔记不存在"} 或HTML 404页面 + is_json_resp = bool(data) + msg_ok = data.get("msg") == "笔记不存在" if is_json_resp else True # HTML 404也算通过 + + report.add("删除不存在笔记 - HTTP 404", is_404, f"status={resp.status_code}") + if is_json_resp: + report.add("删除不存在笔记 - msg正确", msg_ok, f"msg={data.get('msg')}") + else: + report.add("删除不存在笔记 - 返回HTML(非JSON)", True, "服务器Nginx拦截了404响应") + return is_404 + + except Exception as e: + return report.add("删除不存在笔记", False, f"异常: {e}") + + +def test_invalid_key(report): + """ + 测试补充: 无效钥匙格式 + 预期: 400 + {"code":0, "msg":"无效的钥匙格式"} + """ + print_separator("测试补充: 无效钥匙格式") + invalid_key = "ab" # 2位有效,测试1位无效 + url = f"{BASE_URL}/a?json&info" # 1位钥匙,不符合2-64位规则 + print(f" 请求: GET {url}") + + try: + resp = requests.get(url, timeout=TIMEOUT) + print(f" 状态码: {resp.status_code}") + print(f" 响应: {resp.text[:200]}") + + # 无效钥匙可能返回400或重定向 + is_error = resp.status_code in (400, 302, 301) + report.add("无效钥匙格式 - 返回错误/重定向", is_error, + f"status={resp.status_code}") + return is_error + + except Exception as e: + return report.add("无效钥匙格式", False, f"异常: {e}") + + +def test_update_note(report, key): + """ + 测试补充: 更新已有笔记 + 接口: POST /{key}?json body: text=xxx + 预期: 200 + {"code":1, "msg":"saved"},内容被覆盖 + """ + print_separator("测试补充: 更新已有笔记") + url = f"{BASE_URL}/{key}?json" + print(f" 请求: POST {url}") + print(f" 新内容: text={TEST_TEXT_UPDATED}") + + try: + resp = requests.post(url, data={"text": TEST_TEXT_UPDATED}, timeout=TIMEOUT) + print(f" 状态码: {resp.status_code}") + print(f" 响应: {resp.text[:200]}") + + if resp.status_code != 200: + return report.add("更新笔记", False, f"HTTP {resp.status_code}") + + data = resp.json() + code_ok = data.get("code") == 1 + report.add("更新笔记 - code=1", code_ok, f"code={data.get('code')}") + + # 验证内容确实更新了 + time.sleep(0.5) + read_url = f"{BASE_URL}/{key}?raw" + read_resp = requests.get(read_url, timeout=TIMEOUT) + content_updated = TEST_TEXT_UPDATED in read_resp.text + report.add("更新笔记 - 内容已覆盖", content_updated, + f"内容长度={len(read_resp.text)}") + + return code_ok and content_updated + + except Exception as e: + return report.add("更新笔记", False, f"异常: {e}") + + +# ==================== 主流程 ==================== + +def main(): + """主测试流程""" + print("=" * 60) + print(" CTC笔记仓库 API 接口测试") + print(f" 基础URL: {BASE_URL}") + print(f" 测试时间: {time.strftime('%Y-%m-%d %H:%M:%S')}") + print("=" * 60) + + # 检查网络连通性 + print("\n[预检] 测试网络连通性...") + try: + resp = requests.get(BASE_URL, timeout=TIMEOUT, allow_redirects=True) + print(f" 连接成功 (HTTP {resp.status_code})") + except Exception as e: + print(f" ❌ 无法连接到 {BASE_URL}: {e}") + print(" 请检查网络连接后重试。") + sys.exit(1) + + # 初始化测试报告 + report = TestReport() + + # 生成测试用钥匙 + key1 = generate_random_key(8) + key2 = generate_random_key(8) + keys_to_cleanup = [key1, key2] # 需要清理的笔记列表 + + print(f"\n 测试钥匙1: {key1}") + print(f" 测试钥匙2: {key2}") + + # ---- 执行测试 ---- + + # 1. 写入笔记 + test_write_note(report, key1) + time.sleep(0.3) + + # 2. 读取笔记 + test_read_note(report, key1) + time.sleep(0.3) + + # 2补充. 读取不存在的笔记 + test_read_nonexistent_note(report) + time.sleep(0.3) + + # 3. 获取笔记信息 + test_get_note_info(report, key1) + time.sleep(0.3) + + # 3补充. 获取不存在笔记的信息 + test_get_nonexistent_note_info(report) + time.sleep(0.3) + + # 4. 批量检查变更 + test_batch_check(report, key1, key2) + time.sleep(0.3) + + # 补充. 更新已有笔记 + test_update_note(report, key1) + time.sleep(0.3) + + # 5. 创建随机笔记 + success, new_key = test_create_random_note(report) + if new_key: + keys_to_cleanup.append(new_key) + time.sleep(0.3) + + # 补充. 无效钥匙格式 + test_invalid_key(report) + time.sleep(0.3) + + # 6补充. 删除不存在的笔记 + test_delete_nonexistent_note(report) + time.sleep(0.3) + + # ---- 清理测试数据 ---- + print_separator("清理测试数据") + for key in keys_to_cleanup: + print(f" 正在删除笔记: {key}...", end=" ") + try: + resp = requests.get(f"{BASE_URL}/?delete¬e={key}", timeout=TIMEOUT) + data = safe_json_parse(resp) + if data.get("code") == 1: + print("✅ 已删除") + elif resp.status_code == 404: + print("⏭️ 不存在,跳过") + else: + msg = data.get("msg", resp.text[:50]) if data else resp.text[:50] + print(f"⚠️ 删除失败: {msg}") + except Exception as e: + print(f"❌ 异常: {e}") + time.sleep(0.2) + + # ---- 验证清理结果 ---- + print("\n[验证] 确认笔记已删除...") + verify_key = keys_to_cleanup[0] + try: + resp = requests.get(f"{BASE_URL}/{verify_key}?raw", timeout=TIMEOUT) + if resp.status_code == 404: + print(f" ✅ 笔记 {verify_key} 已确认删除 (404)") + else: + print(f" ⚠️ 笔记 {verify_key} 仍存在 (HTTP {resp.status_code})") + except Exception as e: + print(f" ❌ 验证异常: {e}") + + # ---- 输出测试报告 ---- + all_passed = report.summary() + + # 返回退出码 + sys.exit(0 if all_passed else 1) + + +if __name__ == "__main__": + main() diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/CtcLatestNoteProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/CtcLatestNoteProvider.kt new file mode 100644 index 00000000..f5cba66c --- /dev/null +++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/CtcLatestNoteProvider.kt @@ -0,0 +1,48 @@ +/** + * CtcLatestNoteProvider.kt + * 创建时间: 2026-06-15 + * 更新时间: 2026-06-15 + * 名称: CTC最新笔记桌面小部件Provider + * 作用: 在Android桌面展示CTC最新笔记的钥匙名和内容预览,支持深色主题 + * 上次更新内容: 初始创建,支持light/dark双主题布局,点击跳转/ctc路由 + */ +package apps.xy.xianyan.widget + +import android.appwidget.AppWidgetManager +import android.content.Context +import android.content.SharedPreferences +import android.widget.RemoteViews +import apps.xy.xianyan.R +import apps.xy.xianyan.MainActivity +import es.antonborri.home_widget.HomeWidgetLaunchIntent +import es.antonborri.home_widget.HomeWidgetProvider + +class CtcLatestNoteProvider : HomeWidgetProvider() { + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray, + widgetData: SharedPreferences + ) { + val isDark = widgetData.getString("widget_theme_mode", "light") == "dark" + appWidgetIds.forEach { widgetId -> + val layoutId = if (isDark) R.layout.widget_ctc_latest_note_dark else R.layout.widget_ctc_latest_note + val views = RemoteViews(context.packageName, layoutId).apply { + // 读取SharedPreferences中的笔记数据 + val noteKey = widgetData.getString("ctc_latest_note_key", null) ?: "" + val noteContent = widgetData.getString("ctc_latest_note_content", null) ?: "" + val noteTime = widgetData.getString("ctc_latest_note_time", null) ?: "" + + // 更新UI + setTextViewText(R.id.widget_ctc_note_key, if (noteKey.isNotEmpty()) "🔑 $noteKey" else "📝 暂无笔记") + setTextViewText(R.id.widget_ctc_note_content, if (noteContent.isNotEmpty()) noteContent else "点击查看CTC笔记") + setTextViewText(R.id.widget_ctc_note_time, noteTime) + + // 点击跳转到 /ctc 路由 + val pendingIntent = HomeWidgetLaunchIntent.getActivity(context, MainActivity::class.java) + setOnClickPendingIntent(R.id.widget_ctc_note_container, pendingIntent) + } + appWidgetManager.updateAppWidget(widgetId, views) + } + } +} diff --git a/android/app/src/main/res/layout/widget_ctc_latest_note.xml b/android/app/src/main/res/layout/widget_ctc_latest_note.xml new file mode 100644 index 00000000..0e4da67d --- /dev/null +++ b/android/app/src/main/res/layout/widget_ctc_latest_note.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/widget_ctc_latest_note_dark.xml b/android/app/src/main/res/layout/widget_ctc_latest_note_dark.xml new file mode 100644 index 00000000..385e18af --- /dev/null +++ b/android/app/src/main/res/layout/widget_ctc_latest_note_dark.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/xml/ctc_latest_note_info.xml b/android/app/src/main/res/xml/ctc_latest_note_info.xml new file mode 100644 index 00000000..b203c322 --- /dev/null +++ b/android/app/src/main/res/xml/ctc_latest_note_info.xml @@ -0,0 +1,12 @@ + + diff --git a/docs/toolsapi/application/admin/controller/FormCollect.php b/docs/toolsapi/application/admin/controller/FormCollect.php new file mode 100644 index 00000000..4f8eeda4 --- /dev/null +++ b/docs/toolsapi/application/admin/controller/FormCollect.php @@ -0,0 +1,185 @@ +model = new \app\admin\model\FormCollect; + } + + /** + * @name 安装/初始化数据表 + * @desc 创建tool_form_collect表 + */ + public function install() + { + $sql = <<success('安装成功'); + } catch (\Exception $e) { + $this->error('安装失败: ' . $e->getMessage()); + } + } + + /** + * @name 导出CSV + * @desc 导出表单收集记录为CSV文件 + */ + public function export() + { + $ids = $this->request->param('ids', ''); + $source = $this->request->param('source', ''); + $status = $this->request->param('status', ''); + + $query = \think\Db::name('form_collect'); + + if (!empty($ids)) { + $query->where('id', 'in', $ids); + } + if (!empty($source)) { + $query->where('source', $source); + } + if (!empty($status)) { + $query->where('status', $status); + } + + $list = $query->order('id', 'desc')->select(); + + $filename = 'form_collect_' . date('Ymd_His') . '.csv'; + header('Content-Type: text/csv; charset=utf-8'); + header('Content-Disposition: attachment; filename=' . $filename); + + $output = fopen('php://output', 'w'); + // BOM for Excel UTF-8 + fprintf($output, chr(0xEF) . chr(0xBB) . chr(0xBF)); + + // 表头 + fputcsv($output, ['ID', '邮箱', '来源', '标题', '用户ID', '设备ID', 'IP', '状态', '管理员备注', '提交时间']); + + foreach ($list as $row) { + fputcsv($output, [ + $row['id'], + $row['email'], + $row['source'], + $row['title'], + $row['uid'], + $row['device_id'], + $row['ip'], + $row['status'], + $row['admin_remark'], + date('Y-m-d H:i:s', $row['created_at']), + ]); + } + + fclose($output); + exit; + } + + /** + * @name 标记单条已处理 + * @desc 将指定记录标记为已处理 + */ + public function mark_processed() + { + $ids = $this->request->param('ids', ''); + if (empty($ids)) { + $this->error('请选择记录'); + } + + $idArr = is_array($ids) ? $ids : explode(',', $ids); + $updated = \think\Db::name('form_collect') + ->where('id', 'in', $idArr) + ->update(['status' => 'processed', 'updated_at' => time()]); + + $this->success("已处理 {$updated} 条记录"); + } + + /** + * @name 批量标记已处理 + * @desc 将选中记录标记为已处理 + */ + public function batch_process() + { + $ids = $this->request->param('ids', ''); + if (empty($ids)) { + $this->error('请选择记录'); + } + + $idArr = explode(',', $ids); + $updated = \think\Db::name('form_collect') + ->where('id', 'in', $idArr) + ->update(['status' => 'processed', 'updated_at' => time()]); + + $this->success("已处理 {$updated} 条记录"); + } + + /** + * @name 统计数据 + * @desc 返回各来源的提交统计 + */ + public function stats() + { + $total = \think\Db::name('form_collect')->count(); + $pending = \think\Db::name('form_collect')->where('status', 'pending')->count(); + $processed = \think\Db::name('form_collect')->where('status', 'processed')->count(); + $invalid = \think\Db::name('form_collect')->where('status', 'invalid')->count(); + + // 按来源统计 + $bySource = \think\Db::name('form_collect') + ->field('source, COUNT(*) as cnt') + ->group('source') + ->order('cnt', 'desc') + ->select(); + + // 今日新增 + $todayStart = strtotime(date('Y-m-d')); + $todayCount = \think\Db::name('form_collect') + ->where('created_at', '>=', $todayStart) + ->count(); + + $this->success('ok', [ + 'total' => $total, + 'pending' => $pending, + 'processed' => $processed, + 'invalid' => $invalid, + 'today' => $todayCount, + 'by_source' => $bySource, + ]); + } +} diff --git a/docs/toolsapi/application/admin/lang/zh-cn/form_collect.php b/docs/toolsapi/application/admin/lang/zh-cn/form_collect.php new file mode 100644 index 00000000..bcf62230 --- /dev/null +++ b/docs/toolsapi/application/admin/lang/zh-cn/form_collect.php @@ -0,0 +1,22 @@ + 'ID', + 'Email' => '邮箱地址', + 'Source' => '来源', + 'Title' => '标题', + 'Extra_json' => '扩展信息', + 'Uid' => '用户ID', + 'Device_id' => '设备ID', + 'Ip' => 'IP地址', + 'Status' => '状态', + 'Admin_remark' => '管理员备注', + 'Created_at' => '提交时间', + 'Updated_at' => '更新时间', + 'Source app_gp_beta' => 'Google Play内测', + 'Source register_subscribe' => '闲言邮箱订阅', + 'Source beta_questionnaire' => 'Beta问卷', + 'Status pending' => '待处理', + 'Status processed' => '已处理', + 'Status invalid' => '无效', +]; diff --git a/docs/toolsapi/application/admin/model/FormCollect.php b/docs/toolsapi/application/admin/model/FormCollect.php new file mode 100644 index 00000000..483f3397 --- /dev/null +++ b/docs/toolsapi/application/admin/model/FormCollect.php @@ -0,0 +1,45 @@ + '待处理', + 'processed' => '已处理', + 'invalid' => '无效', + ]; + } + + /** + * 来源列表 + */ + public function getSourceList() + { + return [ + 'app_gp_beta' => 'Google Play内测', + 'register_subscribe' => '闲言邮箱订阅', + 'beta_questionnaire' => 'Beta问卷', + ]; + } +} diff --git a/docs/toolsapi/application/admin/view/form_collect/add.html b/docs/toolsapi/application/admin/view/form_collect/add.html new file mode 100644 index 00000000..381e0fd3 --- /dev/null +++ b/docs/toolsapi/application/admin/view/form_collect/add.html @@ -0,0 +1,58 @@ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
diff --git a/docs/toolsapi/application/admin/view/form_collect/edit.html b/docs/toolsapi/application/admin/view/form_collect/edit.html new file mode 100644 index 00000000..e1472179 --- /dev/null +++ b/docs/toolsapi/application/admin/view/form_collect/edit.html @@ -0,0 +1,56 @@ +
+
+ +
+ +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
diff --git a/docs/toolsapi/application/admin/view/form_collect/index.html b/docs/toolsapi/application/admin/view/form_collect/index.html new file mode 100644 index 00000000..b7e2c834 --- /dev/null +++ b/docs/toolsapi/application/admin/view/form_collect/index.html @@ -0,0 +1,25 @@ +
+ {:build_heading()} + +
diff --git a/docs/toolsapi/application/api/controller/FormCollect.php b/docs/toolsapi/application/api/controller/FormCollect.php new file mode 100644 index 00000000..907eea70 --- /dev/null +++ b/docs/toolsapi/application/api/controller/FormCollect.php @@ -0,0 +1,323 @@ +request->method() === 'OPTIONS') { + http_response_code(204); + exit; + } + parent::_initialize(); + $this->checkRateLimit(); + } + + // ─── 频率限制 ───────────────────────────────────────── + + private function checkRateLimit() + { + $ip = $this->request->ip(); + $key = 'rate_limit_fc_' . md5($ip); + $now = time(); + $requests = Cache::get($key, []); + + $requests = array_filter($requests, function ($t) use ($now) { + return ($now - $t) < $this->rateLimitWindow; + }); + + if (count($requests) >= $this->rateLimitMax) { + $this->error('请求过于频繁,请稍后再试', null, 429); + } + + $requests[] = $now; + Cache::set($key, $requests, $this->rateLimitWindow); + } + + // ─── POST /submit — 提交表单 ───────────────────────── + + /** + * 提交表单信息 + * POST /api/form_collect/submit + * + * 请求参数: + * - email: 邮箱地址(必填) + * - source: 来源标识(必填,如 app_gp_beta/register_subscribe/beta_questionnaire) + * - title: 表单标题(可选,若不传则根据source自动生成) + * - extra_json: 扩展字段JSON(可选,预留后续收集其他信息) + * - uid: 用户ID(可选,已登录用户) + * - device_id: 设备ID(可选) + */ + public function submit() + { + $email = $this->request->post('email', ''); + $source = $this->request->post('source', ''); + $title = $this->request->post('title', ''); + $uid = $this->request->post('uid', ''); + $deviceId = $this->request->post('device_id', ''); + + // extra_json从原始输入提取,避免ThinkPHP对JSON请求体的类型检查报错 + $extraJson = ''; + $rawInput = file_get_contents('php://input'); + $allParams = json_decode($rawInput, true); + if (json_last_error() === JSON_ERROR_NONE && isset($allParams['extra_json'])) { + // JSON请求体:直接提取,可能是对象/数组 + $extraJson = (is_array($allParams['extra_json'])) + ? json_encode($allParams['extra_json'], JSON_UNESCAPED_UNICODE) + : $allParams['extra_json']; + } else { + // form-urlencoded请求体:用parse_str提取 + parse_str($rawInput, $rawParams); + if (isset($rawParams['extra_json'])) { + $extraJson = $rawParams['extra_json']; + } + } + + // 参数验证 + if (empty($email)) { + $this->error('邮箱地址必填', null, 400); + } + + if (!$this->isValidEmail($email)) { + $this->error('请输入有效的邮箱地址', null, 400); + } + + if (empty($source)) { + $this->error('来源标识必填', null, 400); + } + + // 来源白名单校验(必须在XSS过滤之前,防止绕过) + if (!in_array($source, $this->allowedSources)) { + $this->error('无效的来源标识', null, 400); + } + + // 自动生成标题 + if (empty($title)) { + $title = $this->generateTitle($source); + } + + // XSS过滤(extra_json单独处理,不转义引号) + $email = htmlspecialchars(trim($email), ENT_QUOTES, 'UTF-8'); + $title = htmlspecialchars(trim($title), ENT_QUOTES, 'UTF-8'); + $source = htmlspecialchars(trim($source), ENT_QUOTES, 'UTF-8'); + $uid = htmlspecialchars(trim($uid), ENT_QUOTES, 'UTF-8'); + $deviceId = htmlspecialchars(trim($deviceId), ENT_QUOTES, 'UTF-8'); + + // 验证extra_json(在htmlspecialchars之前,避免引号被转义) + if (!empty($extraJson)) { + $decoded = json_decode($extraJson, true); + if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) { + $this->error('extra_json格式错误', null, 400); + } + // 递归过滤XSS + $decoded = $this->sanitizeArray($decoded); + $extraJson = json_encode($decoded, JSON_UNESCAPED_UNICODE); + } + + // 检查重复提交(同邮箱+同来源24小时内) + $duplicateKey = 'fc_dup_' . md5($email . '_' . $source); + if (Cache::get($duplicateKey)) { + $this->success('您已提交过,我们会尽快处理', [ + 'already_submitted' => true, + ]); + } + + $ip = $this->request->ip(); + $now = time(); + + $record = [ + 'email' => $email, + 'source' => $source, + 'title' => $title, + 'extra_json' => $extraJson, + 'uid' => $uid, + 'device_id' => $deviceId, + 'ip' => htmlspecialchars($ip, ENT_QUOTES, 'UTF-8'), + 'status' => 'pending', + 'created_at' => $now, + 'updated_at' => $now, + ]; + + try { + Db::name('form_collect')->insert($record); + // 设置24小时防重复 + Cache::set($duplicateKey, 1, 86400); + $this->success('提交成功', [ + 'submitted' => true, + 'message' => '感谢您的提交,我们会尽快处理!', + ]); + } catch (\think\exception\HttpResponseException $e) { + throw $e; + } catch (\Throwable $e) { + $this->error('提交失败: ' . $e->getMessage(), null, 500); + } + } + + // ─── GET /list — 查询提交记录 ───────────────────────── + + /** + * 查询表单提交记录(按来源或邮箱筛选) + * GET /api/form_collect/list + */ + public function list() + { + $source = $this->request->param('source', ''); + $email = $this->request->param('email', ''); + $page = max(intval($this->request->param('page', 1)), 1); + $limit = min(max(intval($this->request->param('limit', 20)), 1), 50); + + $query = Db::name('form_collect'); + + if (!empty($source)) { + $query->where('source', $source); + } + if (!empty($email)) { + $query->where('email', $email); + } + + $total = $query->count(); + $list = $query->order('id', 'desc')->page($page, $limit)->select(); + + // 脱敏邮箱 + foreach ($list as &$item) { + $item['email_masked'] = $this->maskEmail($item['email']); + } + + $this->success('ok', [ + 'total' => $total, + 'page' => $page, + 'limit' => $limit, + 'list' => $list, + ]); + } + + // ─── POST /install — 安装数据表 ──────────────────────── + + /** + * 安装form_collect数据库表 + * POST /api/form_collect/install + */ + public function install() + { + try { + $prefix = config('database.prefix') ?: 'tool_'; + $tableName = $prefix . 'form_collect'; + + $checkSql = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = '{$tableName}'"; + $exists = Db::query($checkSql); + if (!empty($exists) && $exists[0]['COUNT(*)'] > 0) { + $this->success('ok', ['table' => $tableName, 'created' => false, 'exists' => true]); + return; + } + + $sql = "CREATE TABLE `{$tableName}` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `email` varchar(256) NOT NULL DEFAULT '' COMMENT '邮箱地址', + `source` varchar(64) NOT NULL DEFAULT '' COMMENT '来源标识', + `title` varchar(200) NOT NULL DEFAULT '' COMMENT '表单标题(自动生成)', + `extra_json` text COMMENT '扩展字段JSON', + `uid` varchar(128) NOT NULL DEFAULT '' COMMENT '用户ID', + `device_id` varchar(128) NOT NULL DEFAULT '' COMMENT '设备ID', + `ip` varchar(64) NOT NULL DEFAULT '' COMMENT '提交IP', + `status` enum('pending','processed','invalid') NOT NULL DEFAULT 'pending' COMMENT '状态', + `admin_remark` varchar(500) NOT NULL DEFAULT '' COMMENT '管理员备注', + `created_at` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '创建时间', + `updated_at` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_email` (`email`), + KEY `idx_source` (`source`), + KEY `idx_status` (`status`), + KEY `idx_created_at` (`created_at`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='表单收集记录'"; + + Db::execute($sql); + $this->success('ok', ['table' => $tableName, 'created' => true]); + } catch (\think\exception\HttpResponseException $e) { + throw $e; + } catch (\Throwable $e) { + $this->error('安装失败: ' . $e->getMessage(), null, 500); + } + } + + // ─── 内部方法 ───────────────────────────────────────── + + /** + * 验证邮箱格式 + */ + private function isValidEmail($email) + { + return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; + } + + /** + * 根据来源自动生成标题 + */ + private function generateTitle($source) + { + $titleMap = [ + 'app_gp_beta' => 'Google Play内测资格申请', + 'register_subscribe' => '闲言邮箱订阅', + 'beta_questionnaire' => 'Beta问卷-填写Gmail', + ]; + return isset($titleMap[$source]) ? $titleMap[$source] : '表单提交-' . $source; + } + + /** + * 邮箱脱敏 + */ + private function maskEmail($email) + { + $parts = explode('@', $email); + if (count($parts) !== 2) return '***'; + $name = $parts[0]; + $domain = $parts[1]; + if (mb_strlen($name) <= 2) { + return mb_substr($name, 0, 1) . '***@' . $domain; + } + return mb_substr($name, 0, 2) . '***@' . $domain; + } + + /** + * 递归过滤数组中的XSS + */ + private function sanitizeArray($arr) + { + if (!is_array($arr)) { + return is_string($arr) ? htmlspecialchars($arr, ENT_QUOTES, 'UTF-8') : $arr; + } + foreach ($arr as $key => $val) { + $arr[$key] = $this->sanitizeArray($val); + } + return $arr; + } +} diff --git a/docs/toolsapi/application/route.php b/docs/toolsapi/application/route.php index 94abe093..5cf9f957 100644 --- a/docs/toolsapi/application/route.php +++ b/docs/toolsapi/application/route.php @@ -781,6 +781,13 @@ Route::rule([ 'api/font_sync/install' => 'api/FontSync/install', ]); +// APP API路由 - 表单收集 +Route::rule([ + 'api/form_collect/submit' => 'api/FormCollect/submit', + 'api/form_collect/list' => 'api/FormCollect/list', + 'api/form_collect/install' => 'api/FormCollect/install', +]); + // APP API路由 - 插件更新 Route::rule([ 'api/plugin_update/checkOne' => 'api/PluginUpdate/checkOne', diff --git a/docs/toolsapi/docs/API_FORM_COLLECT_DOC.md b/docs/toolsapi/docs/API_FORM_COLLECT_DOC.md new file mode 100644 index 00000000..24679cd3 --- /dev/null +++ b/docs/toolsapi/docs/API_FORM_COLLECT_DOC.md @@ -0,0 +1,344 @@ +# 表单收集 API 文档 + +> @File: API_FORM_COLLECT_DOC.md +> @Time: 2026-06-15 +> @Description: 表单收集接口文档 — 收集邮箱等信息,支持多来源页面,保留扩展 +> @LastUpdate: v11.4.0 初始版本 + +--- + +## 1. 概述 + +表单收集模块用于统一收集用户提交的表单信息(当前主要收集邮箱),支持多来源页面自动标识,预留扩展字段供后续收集其他信息。 + +### 1.1 基础信息 + +| 项目 | 值 | +|------|-----| +| 基础URL | `https://tools.wktyl.com/api/form_collect` | +| 认证方式 | 无需登录 | +| 响应格式 | JSON | +| 编码 | UTF-8 | + +### 1.2 来源标识(source) + +不同页面提交时自动传入不同source,后台自动生成对应标题: + +| source | 标题 | 来源页面 | +|--------|------|----------| +| app_gp_beta | Google Play内测资格申请 | app.html Google Play对话框 | +| register_subscribe | 闲言邮箱订阅 | 注册页面 | +| beta_questionnaire | Beta问卷-填写Gmail | Beta页面问卷 | + +### 1.3 通用响应格式 + +```json +{ + "code": 1, + "msg": "提示信息", + "time": "1718438400", + "data": {} +} +``` + +| 字段 | 说明 | +|------|------| +| code | 状态码,1=成功,0=失败 | +| msg | 提示信息 | +| data | 返回数据 | + +--- + +## 2. 接口列表 + +| 方法 | URL | 说明 | +|------|-----|------| +| POST | /api/form_collect/submit | 提交表单 | +| GET | /api/form_collect/list | 查询提交记录 | +| POST | /api/form_collect/install | 安装数据表 | + +--- + +## 3. 提交表单 + +**POST** `/api/form_collect/submit` + +### 3.1 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| email | string | ✅ | 邮箱地址 | +| source | string | ✅ | 来源标识(app_gp_beta/register_subscribe/beta_questionnaire) | +| title | string | ❌ | 表单标题(不传则根据source自动生成) | +| extra_json | string | ❌ | 扩展字段JSON(预留后续收集其他信息) | +| uid | string | ❌ | 用户ID(已登录用户) | +| device_id | string | ❌ | 设备ID | + +### 3.2 请求示例 + +```bash +# Google Play内测申请 +curl -X POST 'https://tools.wktyl.com/api/form_collect/submit' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -d 'email=test@gmail.com&source=app_gp_beta' + +# 注册页面订阅 +curl -X POST 'https://tools.wktyl.com/api/form_collect/submit' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -d 'email=user@outlook.com&source=register_subscribe&uid=12345' + +# Beta问卷(带扩展字段) +curl -X POST 'https://tools.wktyl.com/api/form_collect/submit' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -d 'email=beta@gmail.com&source=beta_questionnaire&extra_json={"device":"iPhone 16","os":"iOS 19"}' +``` + +### 3.3 成功响应 + +```json +{ + "code": 1, + "msg": "提交成功", + "data": { + "submitted": true, + "message": "感谢您的提交,我们会尽快处理!" + } +} +``` + +### 3.4 重复提交响应 + +同邮箱+同来源24小时内重复提交: + +```json +{ + "code": 1, + "msg": "您已提交过,我们会尽快处理", + "data": { + "already_submitted": true + } +} +``` + +### 3.5 错误响应 + +| 场景 | code | msg | +|------|------|-----| +| 邮箱为空 | 0 | 邮箱地址必填 | +| 邮箱格式错误 | 0 | 请输入有效的邮箱地址 | +| 来源为空 | 0 | 来源标识必填 | +| extra_json格式错误 | 0 | extra_json格式错误 | +| 频率超限 | 0 | 请求过于频繁,请稍后再试 | + +--- + +## 4. 查询提交记录 + +**GET** `/api/form_collect/list` + +### 4.1 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| source | string | ❌ | 按来源筛选 | +| email | string | ❌ | 按邮箱筛选 | +| page | int | ❌ | 页码(默认1) | +| limit | int | ❌ | 每页条数(默认20,最大50) | + +### 4.2 请求示例 + +```bash +# 查询Google Play内测申请 +curl 'https://tools.wktyl.com/api/form_collect/list?source=app_gp_beta&page=1&limit=10' + +# 按邮箱查询 +curl 'https://tools.wktyl.com/api/form_collect/list?email=test@gmail.com' +``` + +### 4.3 成功响应 + +```json +{ + "code": 1, + "msg": "ok", + "data": { + "total": 25, + "page": 1, + "limit": 20, + "list": [ + { + "id": 1, + "email": "test@gmail.com", + "email_masked": "te***@gmail.com", + "source": "app_gp_beta", + "title": "Google Play内测资格申请", + "extra_json": "", + "uid": "", + "device_id": "", + "ip": "1.2.3.4", + "status": "pending", + "admin_remark": "", + "created_at": 1718438400, + "updated_at": 1718438400 + } + ] + } +} +``` + +--- + +## 5. 安装数据表 + +**POST** `/api/form_collect/install` + +首次部署时调用,创建 `tool_form_collect` 表。 + +### 5.1 请求示例 + +```bash +curl -X POST 'https://tools.wktyl.com/api/form_collect/install' +``` + +### 5.2 成功响应 + +```json +{ + "code": 1, + "msg": "ok", + "data": { + "table": "tool_form_collect", + "created": true + } +} +``` + +--- + +## 6. 数据库表结构 + +```sql +CREATE TABLE IF NOT EXISTS `tool_form_collect` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `email` varchar(256) NOT NULL DEFAULT '' COMMENT '邮箱地址', + `source` varchar(64) NOT NULL DEFAULT '' COMMENT '来源标识', + `title` varchar(200) NOT NULL DEFAULT '' COMMENT '表单标题(自动生成)', + `extra_json` text COMMENT '扩展字段JSON', + `uid` varchar(128) NOT NULL DEFAULT '' COMMENT '用户ID', + `device_id` varchar(128) NOT NULL DEFAULT '' COMMENT '设备ID', + `ip` varchar(64) NOT NULL DEFAULT '' COMMENT '提交IP', + `status` enum('pending','processed','invalid') NOT NULL DEFAULT 'pending' COMMENT '状态', + `admin_remark` varchar(500) NOT NULL DEFAULT '' COMMENT '管理员备注', + `created_at` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '创建时间', + `updated_at` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_email` (`email`), + KEY `idx_source` (`source`), + KEY `idx_status` (`status`), + KEY `idx_created_at` (`created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='表单收集记录'; +``` + +--- + +## 7. 后台管理 + +### 7.1 管理页面 + +**URL**: `https://tools.wktyl.com/admin.php/form_collect` + +**菜单位置**: 表单收集 → 表单收集管理 + +### 7.2 后台功能 + +| 功能 | 端点 | 说明 | +|------|------|------| +| 列表 | GET /form_collect | 查看/搜索/筛选提交记录 | +| 编辑 | POST /form_collect/edit | 修改状态、添加备注 | +| 删除 | POST /form_collect/del | 删除记录 | +| 导出 | GET /form_collect/export | 导出CSV | +| 批量处理 | POST /form_collect/batch_process | 批量标记已处理 | +| 统计 | GET /form_collect/stats | 各来源提交统计 | +| 安装 | GET /form_collect/install | 安装数据表 | + +### 7.3 后台菜单注册SQL + +```sql +INSERT INTO fa_auth_rule (type, pid, name, title, icon, condition, remark, ismenu, weigh, status) +SELECT 'file', 0, 'form_collect', '表单收集', 'fa fa-wpforms', '', '管理表单收集记录', 1, 0, 'normal' +WHERE NOT EXISTS (SELECT 1 FROM fa_auth_rule WHERE name = 'form_collect'); + +SET @parent_id = (SELECT id FROM fa_auth_rule WHERE name = 'form_collect' LIMIT 1); + +INSERT IGNORE INTO fa_auth_rule (type, pid, name, title, icon, ismenu, weigh, status) VALUES +('file', @parent_id, 'form_collect/index', '查看', '', 0, 0, 'normal'), +('file', @parent_id, 'form_collect/add', '添加', '', 0, 0, 'normal'), +('file', @parent_id, 'form_collect/edit', '编辑', '', 0, 0, 'normal'), +('file', @parent_id, 'form_collect/del', '删除', '', 0, 0, 'normal'), +('file', @parent_id, 'form_collect/install', '安装', '', 0, 0, 'normal'), +('file', @parent_id, 'form_collect/export', '导出', '', 0, 0, 'normal'), +('file', @parent_id, 'form_collect/batch_process', '批量处理', '', 0, 0, 'normal'), +('file', @parent_id, 'form_collect/stats', '统计', '', 0, 0, 'normal'); +``` + +--- + +## 8. 安全机制 + +| 机制 | 说明 | +|------|------| +| 频率限制 | 每IP每分钟最多10次请求 | +| 防重复 | 同邮箱+同来源24小时内不重复入库 | +| XSS过滤 | 所有输入字段htmlspecialchars处理 | +| 邮箱验证 | filter_var FILTER_VALIDATE_EMAIL | +| CORS | 支持跨域请求 | + +--- + +## 9. 客户端对接 + +### 9.1 app.html — Google Play内测对话框 + +提交邮箱后调用API,成功后显示提交动画,不再跳转Google Forms。 + +```javascript +fetch('https://tools.wktyl.com/api/form_collect/submit', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'email=' + encodeURIComponent(email) + '&source=app_gp_beta' +}) +.then(res => res.json()) +.then(data => { + if (data.code === 1) { + // 显示提交成功动画 + } +}); +``` + +### 9.2 注册页面 — 订阅闲言邮箱 + +```javascript +fetch('https://tools.wktyl.com/api/form_collect/submit', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'email=' + encodeURIComponent(email) + '&source=register_subscribe&uid=' + uid +}); +``` + +### 9.3 Beta页面 — 问卷填写Gmail + +```javascript +fetch('https://tools.wktyl.com/api/form_collect/submit', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'email=' + encodeURIComponent(email) + '&source=beta_questionnaire&uid=' + uid +}); +``` + +--- + +## 10. 变更日志 + +| 版本 | 日期 | 变更 | +|------|------|------| +| v11.4.0 | 2026-06-15 | 初始版本:邮箱收集、多来源标识、后台管理、CSV导出 | diff --git a/docs/toolsapi/public/assets/js/backend/form_collect.js b/docs/toolsapi/public/assets/js/backend/form_collect.js new file mode 100644 index 00000000..d7934875 --- /dev/null +++ b/docs/toolsapi/public/assets/js/backend/form_collect.js @@ -0,0 +1,180 @@ +/** + * 表单收集后台管理JS + * 创建时间: 2026-06-15 + * 更新时间: 2026-06-15 + * 作用: 列表展示、添加/编辑弹窗、批量处理、导出CSV、标记已处理 + * 上次更新: 初始版本 + */ +define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) { + + var Controller = { + index: function () { + Table.api.init({ + extend: { + index_url: 'form_collect/index' + location.search, + add_url: 'form_collect/add', + edit_url: 'form_collect/edit', + del_url: 'form_collect/del', + table: 'form_collect', + } + }); + + var table = $("#table"); + + table.bootstrapTable({ + url: $.fn.bootstrapTable.defaults.extend.index_url, + pk: 'id', + sortName: 'id', + sortOrder: 'desc', + columns: [ + [ + {checkbox: true}, + {field: 'id', title: 'ID', sortable: true}, + {field: 'email', title: __('Email'), operate: 'LIKE', + formatter: function(val) { + return '' + (val || '') + ''; + } + }, + {field: 'source', title: __('Source'), + searchList: { + 'app_gp_beta': 'Google Play内测', + 'register_subscribe': '闲言邮箱订阅', + 'beta_questionnaire': 'Beta问卷' + }, + formatter: function(val) { + var map = { + 'app_gp_beta': 'Google Play内测', + 'register_subscribe': '闲言邮箱订阅', + 'beta_questionnaire': 'Beta问卷' + }; + return map[val] || '' + val + ''; + } + }, + {field: 'title', title: __('Title'), operate: 'LIKE', + formatter: function(val) { + return val || '-'; + } + }, + {field: 'status', title: __('Status'), + searchList: { + 'pending': '待处理', + 'processed': '已处理', + 'invalid': '无效' + }, + formatter: function(val) { + var map = { + 'pending': '⏳ 待处理', + 'processed': '✅ 已处理', + 'invalid': '❌ 无效' + }; + return map[val] || '' + val + ''; + } + }, + {field: 'admin_remark', title: __('Admin_remark'), operate: 'LIKE', + formatter: function(val) { + if (!val) return '-'; + return val.length > 20 ? val.substr(0, 20) + '...' : val; + } + }, + {field: 'ip', title: __('Ip'), operate: 'LIKE', + formatter: function(val) { + return '' + (val || '-') + ''; + } + }, + {field: 'created_at', title: __('Created_at'), sortable: true, operate: 'RANGE', + addclass: 'datetimerange', + formatter: function(val) { + return val ? new Date(val * 1000).toLocaleString('zh-CN') : '-'; + } + }, + {field: 'operate', title: __('Operate'), table: table, + events: Table.api.events.operate, + formatter: Table.api.formatter.operate, + buttons: [ + { + name: 'mark_processed', + text: '标记已处理', + title: '标记为已处理', + classname: 'btn btn-xs btn-success btn-mark-processed', + icon: 'fa fa-check', + hidden: function(row) { + return row.status === 'processed'; + }, + callback: function(data) { + Layer.confirm('确认将此记录标记为已处理?', function(index) { + Fast.api.ajax({ + url: 'form_collect/mark_processed', + data: {ids: data.id} + }, function(data, ret) { + Layer.close(index); + Toastr.success(ret.msg || '操作成功'); + $(".btn-refresh").trigger("click"); + }); + }); + } + } + ] + } + ] + ] + }); + + // 导出CSV按钮 + $(document).on('click', '.btn-export', function() { + var ids = Table.api.selectedids(table); + var source = table.bootstrapTable('getOptions').queryParams.source || ''; + var status = table.bootstrapTable('getOptions').queryParams.status || ''; + var url = 'form_collect/export?1=1'; + if (ids.length > 0) { + url += '&ids=' + ids.join(','); + } + if (source) url += '&source=' + source; + if (status) url += '&status=' + status; + window.location.href = url; + }); + + // 安装数据表按钮 + $(document).on('click', '.btn-install', function() { + Layer.confirm('确认安装/初始化数据表?如果表已存在则不会重复创建。', function(index) { + Fast.api.ajax('form_collect/install', function(data, ret) { + Layer.close(index); + Toastr.success(ret.msg || '安装成功'); + }); + }); + }); + + // 批量处理按钮 + $(document).on('click', '.btn-batch-process', function() { + var ids = Table.api.selectedids(table); + if (ids.length === 0) { + Toastr.error('请先选择记录'); + return; + } + Layer.confirm('确认将选中的 ' + ids.length + ' 条记录标记为已处理?', function(index) { + Fast.api.ajax({ + url: 'form_collect/batch_process', + data: {ids: ids.join(',')} + }, function(data, ret) { + Layer.close(index); + Toastr.success(ret.msg || '操作成功'); + $(".btn-refresh").trigger("click"); + }); + }); + }); + + Table.api.bindevent(table); + }, + add: function () { + Controller.api.bindevent(); + }, + edit: function () { + Controller.api.bindevent(); + }, + api: { + bindevent: function () { + Form.api.bindevent($("form[role=form]")); + } + } + }; + return Controller; +}); diff --git a/iOS_macOS_Developer_Guide.md b/iOS_macOS_Developer_Guide.md index fb6ace4e..d180753f 100644 --- a/iOS_macOS_Developer_Guide.md +++ b/iOS_macOS_Developer_Guide.md @@ -10,13 +10,10 @@ | 日期 | 版本 | 变更内容 | |---|---|---| -<<<<<<< Updated upstream +| 2026-06-15 | v11 | 同步三方库升级:删除custom_lint/riverpod_lint;新增analyzer/test_api/test/xml/pointycastle overrides;record降级到^6.2.1(7.0.0需Dart3.12+);更新差异对照表和dependency_overrides行数 | | 2026-06-07 | v10 | 修正 §2.3 dependency_overrides 行数(4→5行/40+→46行);修正 §2.6 补丁引用(§2.8→§2.9);简化 §2.8.1 pro_image_editor 过时回退建议;删除 §5.4 pro_image_editor 本地包条目和 bitsdojo_window 废弃条目;简化 §3.3 pubspec.yaml 处理策略(git stash → 双模板脚本生成);更新 §3.2/§3.5/§6 与双模板机制对齐 | | 2026-06-06 | v9 | 清理未使用依赖:移除 animations、animate_do、value_layout_builder、flutter_advanced_canvas_editor、flutter_blue_plus、http_cache_file_store、dartx、vector_math;删除差异对照表中 flutter_nfc_kit 过时条目 | | 2026-06-06 | v8 | 新增 `app_tracking_transparency` 差异对照条目;新增 `nearby_connections` 鸿蒙端本地stub包说明;新增 §2.10 nearby_connections鸿蒙适配说明 | -======= -| 2026-06-06 | v8 | 移除 `nearby_connections` 库及P2P功能(本地stub包影响Android构建);更新差异对照表;新增 §2.8.7 app_tracking_transparency说明 | ->>>>>>> Stashed changes | 2026-06-02 | v7 | **重大变更**:pubspec.yaml 拆分为双模板(pubspec.ohos.yaml + pubspec.macos.yaml),pubspec.yaml 不再提交到 Git;新增三方库变更通知机制;新增 setup_pubspec.ps1 脚本 | | 2026-06-02 | v6 | 鸿蒙端 pubspec.yaml 同步 bitsdojo_window → window_manager 迁移;更新 file_picker 本地包版本注释(v8.3.7→v11.0.0-ohos.1);更新 speech_to_text(^7.0.0→^7.4.0)、live_activities(^2.0.0→^2.4.9) 远程版本号;补充 dependency_overrides 中 bitsdojo_window_windows 移除说明 | | 2026-06-01 | v5 | 新增 §2.6 pub cache 补丁说明;标记 bitsdojo_window 迁移完成;file_picker 升级到 12.x | @@ -64,10 +61,7 @@ git checkout main # 1. 确认使用官方 Flutter SDK flutter --version # 应显示官方版本,非 flutter-ohos -# 2. 运行脚本生成 pubspec.yaml(MacBook Pro 端) -.\tools\setup_pubspec.ps1 -Platform macos -# 或自动检测平台 -.\tools\setup_pubspec.ps1 + # 3. 获取依赖 flutter pub get @@ -84,8 +78,7 @@ flutter build macos # 1. 确认使用 flutter-ohos SDK flutter --version # 应显示 ohos 版本 -# 2. 运行脚本生成 pubspec.yaml(鸿蒙端) -.\tools\setup_pubspec.ps1 -Platform ohos + # 3. 获取依赖 flutter pub get @@ -148,18 +141,18 @@ Error: The getter 'ohos' isn't defined for the class 'TargetPlatform' | 区域 | pubspec.ohos.yaml(鸿蒙端) | pubspec.macos.yaml(MacBook Pro端) | |---|---|---| | shared_preferences | `path: packages/shared_preferences` | `^2.5.5` | -| flutter_secure_storage | `path: packages/flutter_secure_storage` | `^10.2.0` | -| hive_flutter | `path: packages/hive_flutter` | `^1.1.0` | +| flutter_secure_storage | `path: packages/flutter_secure_storage` | `^10.3.0` | +| hive_ce_flutter | `path: packages/hive_flutter` | `^2.3.4` | | path_provider | `path: packages/path_provider` | `^2.1.5` | | package_info_plus | `path: packages/package_info_plus` | `^10.1.0` | | connectivity_plus | `path: packages/connectivity_plus` | `^7.1.1` | | device_info_plus | `path: packages/device_info_plus` | `^13.1.0` | | permission_handler | `path: packages/permission_handler` | `^12.0.1` | | app_tracking_transparency | `^2.0.6` | `^2.0.6` | -| flutter_local_notifications | `path: packages/flutter_local_notifications` | `^21.0.0` | +| flutter_local_notifications | `path: packages/flutter_local_notifications` | `^22.0.0` | | url_launcher | `path: packages/url_launcher` | `^6.3.2` | | app_links | `path: packages/app_links` | `^7.0.0` | -| home_widget | `git: gitcode.com/...` | `^0.9.1` | +| home_widget | `git: gitcode.com/...` | `^0.9.3` | | file_picker | `path: packages/file_picker` | `^12.0.0-beta.5` | | image_picker | `path: packages/image_picker` | `^1.2.2` | | share_plus | `path: packages/share_plus` | `^13.1.0` | @@ -167,24 +160,24 @@ Error: The getter 'ohos' isn't defined for the class 'TargetPlatform' | flutter_quill | `path: packages/flutter_quill` | `^11.5.0` | | flex_color_picker | `path: packages/flex_color_picker` | `^3.8.0` | | flutter_image_compress | `path: packages/flutter_image_compress` | `^2.4.0` | -| wakelock_plus | `path: packages/wakelock_plus` | `^1.4.0` | +| wakelock_plus | `path: packages/wakelock_plus` | `^1.6.0` | | audioplayers | `path: packages/audioplayers` | `^6.5.0` | -| record | `path: packages/record` | `^6.0.0` | -| video_compress | `path: packages/video_compress` | `^3.1.2` | -| video_player | `path: packages/video_player` | `^2.10.0` | +| record | `path: packages/record` | `^6.2.1` | +| video_compress | `path: packages/video_compress` | `^3.1.4` | +| video_player | `path: packages/video_player` | `^2.11.0` | | local_auth | `path: packages/local_auth` | `^3.0.1` | | battery_plus | `path: packages/battery_plus` | `^7.0.0` | | network_info_plus | `path: packages/network_info_plus` | `^8.1.0` | | flutter_webrtc | `path: packages/flutter_webrtc` | `^1.4.0` | -| mobile_scanner | `path: packages/mobile_scanner` | `^7.1.4` | +| mobile_scanner | `path: packages/mobile_scanner` | `^7.2.0` | | wifi_iot | `path: packages/wifi_iot` | `^0.3.19` | | nearby_service | `path: packages/nearby_service` | `^0.2.1` | | sqflite | `path: packages/sqflite` | `^2.4.1` | | workmanager | `path: packages/workmanager` | `^0.9.0` | -| flutter_tts | `path: packages/flutter_tts` | `^4.2.0` | +| flutter_tts | `path: packages/flutter_tts` | `^4.2.5` | | speech_to_text | `path: packages/speech_to_text` | `^7.4.0` | | live_activities | `path: packages/live_activities` | `^2.4.9` | -| dependency_overrides | 46 行(含本地包覆盖 + ohos 子包) | 5 行(仅版本号覆盖 + win32 + quill_native_bridge_windows) | +| dependency_overrides | 49 行(含本地包覆盖 + ohos 子包 + analyzer/test_api/test/xml/pointycastle) | 10 行(版本号覆盖 + win32 + quill_native_bridge_windows + analyzer/test_api/test/xml/pointycastle) | ### 2.4 ⚠️ 新增三方库变更流程(必读) @@ -198,7 +191,6 @@ Error: The getter 'ohos' isn't defined for the class 'TargetPlatform' 4. 在本文档顶部更新日志记录变更 5. 在 CHANGELOG.md 记录变更 6. git push 后通知另一端开发者 -7. 另一端开发者: git pull → 运行 setup_pubspec.ps1 → flutter pub get ``` **鸿蒙端新增本地化包时额外步骤:** @@ -303,11 +295,11 @@ API 已与远程版本对齐,无需额外处理。 #### 2.8.3 flutter_secure_storage(版本差异说明) -MacBook Pro 端使用远程版本 `^10.2.0`,其 Windows 平台实现兼容 `win32 ^6.0.1`, +MacBook Pro 端使用远程版本 `^10.3.0`,其 Windows 平台实现兼容 `win32 ^6.0.1`, 解决了之前版本与 `win32 6.x` 的编译冲突。 > **注意**:鸿蒙端本地包版本为 `9.2.4-ohos.1`(基于 9.x 适配), -> 与 MacBook Pro 端远程版本 `10.2.0` 存在主版本号差异。 +> 与 MacBook Pro 端远程版本 `10.3.0` 存在主版本号差异。 > 两端 API 兼容,`lib/` 代码无需特殊处理。 #### 2.8.4 receive_sharing_intent(gitcode 引用) @@ -339,11 +331,11 @@ MacBook Pro 端使用远程版本 `^10.2.0`,其 Windows 平台实现兼容 `wi #### 2.8.6 home_widget(pub.dev 远程版本) `home_widget` 的 gitcode 版本依赖 `path_provider` 的 git 版本,会与远程 `path_provider` 冲突。 -MacBook Pro 端使用 pub.dev 版本 `^0.9.1`,鸿蒙端的 `ohosName` 参数通过 `pu.isOhos` + `dynamic` 调用隔离: +MacBook Pro 端使用 pub.dev 版本 `^0.9.3`,鸿蒙端的 `ohosName` 参数通过 `pu.isOhos` + `dynamic` 调用隔离: ```yaml # MacBook Pro 端使用 pub.dev 远程版本 - home_widget: ^0.9.1 + home_widget: ^0.9.3 ``` #### 2.8.7 nearby_connections(已移除) @@ -365,7 +357,69 @@ MacBook Pro 端使用 pub.dev 版本 `^0.9.1`,鸿蒙端的 `ohosName` 参数 app_tracking_transparency: ^2.0.6 ``` -### 2.9 ⚠️ pub cache 补丁(MacBook Pro 端必读) +### 2.9 ⚠️ 鸿蒙端升级 Tips(2026-06-15,完成后删除本节) + +> **本节为鸿蒙端开发者提供升级指引,鸿蒙端完成适配后请删除此节。** + +#### 2.9.1 需要升级的鸿蒙本地包 + +| 本地包目录 | 当前版本 | 需升级到 | 升级说明 | +|---|---|---|---| +| `packages/wakelock_plus` | v1.4.0-ohos.1 | v1.6.0-ohos | API不变,仅平台接口版本提升 | +| `packages/record` | v6.0.0-ohos.1 | v6.2.1-ohos | 降级到6.2.1(7.0.0需Dart3.12+,当前SDK为3.11.5) | +| `packages/flutter_local_notifications` | v21.0.0-ohos | v22.0.0-ohos | 需Flutter 3.38.1+,API已使用命名参数 | +| `packages/flutter_secure_storage` | v9.2.4-ohos.1 | v10.3.0-ohos | 主版本差异(9→10),需评估API兼容性 | +| `packages/mobile_scanner` | v7.1.4-ohos.1 | v7.2.0-ohos | 次版本更新 | +| `packages/video_player` | v2.10.0-ohos.1 | v2.11.0-ohos | 次版本更新 | +| `packages/hive_flutter` | v1.1.0-ohos.2 | v2.3.4-ohos | 迁移到hive_ce_flutter,导入路径变更 | +| `packages/sqflite` | v2.4.1-ohos.1 | v2.4.3-ohos | 补丁更新 | + +#### 2.9.2 EOL 包迁移(鸿蒙端) + +| 旧包 | 新包 | 鸿蒙端操作 | +|---|---|---| +| `sqlite3_flutter_libs` | `sqlite3 v3.x` | 鸿蒙端使用sqflite_ohos桥接,不受影响,但需移除`sqlite3_flutter_libs`引用。所有平台均配置`hooks.user_defines.sqlite3.source: system`使用系统SQLite,避免从GitHub下载预编译库失败 | +| `hive_flutter` | `hive_ce_flutter` | 将`packages/hive_flutter`升级为hive_ce版本,导入路径从`package:hive_flutter/`改为`package:hive_ce_flutter/` | +| `device_calendar` | `device_calendar_plus` | 鸿蒙端通过MethodChannel桥接,不受影响,但需移除`device_calendar`引用 | + +#### 2.9.3 鸿蒙端升级步骤 + +```bash +# 1. 拉取最新代码 +git pull + +# 2. 重新生成 pubspec.yaml +.\tools\setup_pubspec.ps1 -Platform ohos + +# 3. 逐一升级本地包 +# 对每个需要升级的包: +# a. 下载新版本源码 +# b. 添加 ohos 平台适配代码 +# c. 更新 packages/xxx 目录 +# d. 更新 pubspec.ohos.yaml 中的版本注释 + +# 4. 特别注意 hive_flutter → hive_ce_flutter 迁移 +# 本地包 packages/hive_flutter 需要更新导入路径 +# lib/ 代码中已将 import 'package:hive_flutter/' 改为 import 'package:hive_ce_flutter/' +# 鸿蒙端本地包也需同步修改导出路径 + +# 5. 获取依赖 +flutter pub get + +# 6. 构建验证 +flutter build hap --debug +``` + +#### 2.9.4 dio_cache_interceptor 4.x 变更(鸿蒙端注意) + +`dio_cache_interceptor` 已从 3.x 升级到 4.x,API 变更: +- `hitCacheOnErrorExcept: [401, 403]` → `hitCacheOnNetworkFailure: true` +- `Nullable` → `Duration?` +- `CacheResponse` 新增 `statusCode` 字段 + +鸿蒙端如果使用远程版本,需同步更新缓存配置代码。 + +### 2.10 ⚠️ pub cache 补丁(MacBook Pro 端必读) > **关键问题**:`dependency_overrides` 中 `win32: ^6.0.1` 导致部分依赖 `win32 ^5.x` 的三方包编译失败。 > 这些三方包的 Windows 平台代码在 macOS 构建时也会被编译(Dart 编译器不区分平台)。 @@ -384,8 +438,7 @@ MacBook Pro 端使用 pub.dev 版本 `^0.9.1`,鸿蒙端的 `ohosName` 参数 # 1. 确保 flutter pub get 已执行 flutter pub get -# 2. 运行补丁脚本(见 §2.6.3) -bash scripts/patch_pub_cache.sh + # 3. 验证构建 flutter build macos @@ -1066,7 +1119,6 @@ iOS/macOS 端这些检测不会执行(`isOhos` 为 false),无需关心。 | 编译报 `TargetPlatform.ohos` 不存在 | 使用了含 ohos 引用的本地包 | 确认使用 `pubspec.macos.yaml` 生成的 pubspec.yaml | | iOS 编译报 ohos 相关错误 | 误用鸿蒙SDK编译iOS | 切换到官方 Flutter SDK | | GoRouter 路由正常但鸿蒙端白屏 | 鸿蒙端不支持 GoRouter | 检查 OhosNavBridge 路由映射 | -| git pull 后 pubspec.yaml 被覆盖 | pubspec.yaml 已在 .gitignore | 重新运行 `.\tools\setup_pubspec.ps1` | | 新增依赖后另一端报错 | 只更新了一个模板 | 必须同时更新两个模板 + 文档,参见 §2.4 | | 新增依赖后鸿蒙端报错 | 新增的三方库未适配鸿蒙 | 通知鸿蒙开发者评估,必要时本地化到 packages/ | | 编译报 `OhosInitializationSettings` 不存在 | 官方SDK无此类型 | 使用 `notification_init_stub.dart` 桥接,参见 §4.5 | @@ -1082,7 +1134,6 @@ MacBook Pro 开发前,确认以下事项: - [ ] 使用官方 Flutter SDK(非 flutter-ohos) - [ ] 已 `git clone` 拉取最新代码 -- [ ] 已运行 `.\tools\setup_pubspec.ps1 -Platform macos` 生成 pubspec.yaml - [ ] `flutter pub get` 无报错 - [ ] `dart analyze lib/` 无 error - [ ] 新增代码未使用 `switch(TargetPlatform)` 穷举匹配 @@ -1093,5 +1144,5 @@ MacBook Pro 开发前,确认以下事项: --- -*文档创建时间: 2026-05-21 | 更新时间: 2026-06-07 v10 | 维护者: 闲言APP开发团队* -*更新内容: 修正dependency_overrides行数、补丁引用;简化pro_image_editor和pubspec.yaml处理策略;与双模板机制对齐* +*文档创建时间: 2026-05-21 | 更新时间: 2026-06-15 v11 | 维护者: 闲言APP开发团队* +*更新内容: 同步三方库升级;删除custom_lint/riverpod_lint;新增analyzer/test_api/test/xml/pointycastle overrides;record降级到^6.2.1;更新差异对照表* diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ec67869f..c37828b3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -13,7 +13,7 @@ PODS: - CwlCatchException (2.2.1): - CwlCatchExceptionSupport (~> 2.2.1) - CwlCatchExceptionSupport (2.2.1) - - device_calendar (0.0.1): + - device_calendar_plus_ios (0.0.1): - Flutter - device_info_plus (0.0.1): - Flutter @@ -128,31 +128,6 @@ PODS: - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - - sqlite3 (3.52.0): - - sqlite3/common (= 3.52.0) - - sqlite3/common (3.52.0) - - sqlite3/dbstatvtab (3.52.0): - - sqlite3/common - - sqlite3/fts5 (3.52.0): - - sqlite3/common - - sqlite3/math (3.52.0): - - sqlite3/common - - sqlite3/perf-threadsafe (3.52.0): - - sqlite3/common - - sqlite3/rtree (3.52.0): - - sqlite3/common - - sqlite3/session (3.52.0): - - sqlite3/common - - sqlite3_flutter_libs (0.0.1): - - Flutter - - FlutterMacOS - - sqlite3 (~> 3.52.0) - - sqlite3/dbstatvtab - - sqlite3/fts5 - - sqlite3/math - - sqlite3/perf-threadsafe - - sqlite3/rtree - - sqlite3/session - url_launcher_ios (0.0.1): - Flutter - video_compress (0.3.0): @@ -174,7 +149,7 @@ DEPENDENCIES: - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`) - battery_plus (from `.symlinks/plugins/battery_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - - device_calendar (from `.symlinks/plugins/device_calendar/ios`) + - device_calendar_plus_ios (from `.symlinks/plugins/device_calendar_plus_ios/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/darwin`) - Flutter (from `Flutter`) @@ -211,7 +186,6 @@ DEPENDENCIES: - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - speech_to_text (from `.symlinks/plugins/speech_to_text/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - video_compress (from `.symlinks/plugins/video_compress/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) @@ -228,7 +202,6 @@ SPEC REPOS: - OrderedSet - SDWebImage - SDWebImageWebPCoder - - sqlite3 - WebRTC-SDK EXTERNAL SOURCES: @@ -242,8 +215,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/battery_plus/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" - device_calendar: - :path: ".symlinks/plugins/device_calendar/ios" + device_calendar_plus_ios: + :path: ".symlinks/plugins/device_calendar_plus_ios/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" file_picker: @@ -316,8 +289,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/speech_to_text/darwin" sqflite_darwin: :path: ".symlinks/plugins/sqflite_darwin/darwin" - sqlite3_flutter_libs: - :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" video_compress: @@ -339,7 +310,7 @@ SPEC CHECKSUMS: connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd CwlCatchException: 7acc161b299a6de7f0a46a6ed741eae2c8b4d75a CwlCatchExceptionSupport: 54ccab8d8c78907b57f99717fb19d4cc3bce02dc - device_calendar: b55b2c5406cfba45c95a59f9059156daee1f74ed + device_calendar_plus_ios: 655745d124102b995010c48b167c9196456dcb9e device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe file_picker: 70164d9778c42c47218d6cd79ce435de0856b11a Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 @@ -381,8 +352,6 @@ SPEC CHECKSUMS: shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb speech_to_text: 3b313d98516d3d0406cea424782ec25470c59d19 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921 - sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b video_compress: f2133a07762889d67f0711ac831faa26f956980e video_player_avfoundation: 3453f792138786248960ca029747fcd9f318ef52 diff --git a/lib/app/layout/app_shell.dart b/lib/app/layout/app_shell.dart index 8112481e..fc8d53f7 100644 --- a/lib/app/layout/app_shell.dart +++ b/lib/app/layout/app_shell.dart @@ -333,7 +333,7 @@ class _AppShellState extends ConsumerState { ? 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( + settings: LiquidGlassSettings( blur: 1.0, refractiveIndex: 1.35, chromaticAberration: 0.2, diff --git a/lib/app/layout/ohos_app_shell.dart b/lib/app/layout/ohos_app_shell.dart index 823b4cc9..0bba6528 100644 --- a/lib/app/layout/ohos_app_shell.dart +++ b/lib/app/layout/ohos_app_shell.dart @@ -233,7 +233,7 @@ class _OhosAppShellState extends ConsumerState { ? 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( + settings: LiquidGlassSettings( thickness: 30, blur: 1.5, refractiveIndex: 1.5, diff --git a/lib/core/network/cache_config.dart b/lib/core/network/cache_config.dart index 373a34d8..0e75e85a 100644 --- a/lib/core/network/cache_config.dart +++ b/lib/core/network/cache_config.dart @@ -1,18 +1,18 @@ /// ============================================================ /// 闲言APP — Dio HTTP 缓存配置 /// 创建时间: 2026-05-27 -/// 更新时间: 2026-05-30 +/// 更新时间: 2026-06-15 /// 作用: 配置 dio_cache_interceptor 缓存策略 /// GET 请求默认缓存5分钟,特定接口可自定义 /// 排除需要实时数据的接口(登录、签到等) /// 双层缓存: L1内存(快速) + L2 Hive持久化(重启不丢失) -/// 上次更新: 实现DualCacheStore双层缓存,MemCacheStore(L1)+HiveCacheStore(L2) +/// 上次更新: 升级dio_cache_interceptor 4.x,hitCacheOnErrorExcept→hitCacheOnNetworkFailure,Nullable→Duration?,CacheResponse添加statusCode /// ============================================================ import 'dart:async'; import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import '../utils/logger.dart'; import '../storage/hive_safe_access.dart'; @@ -80,6 +80,7 @@ class HiveCacheStore extends CacheStore { 'requestDate': resp.requestDate.toIso8601String(), 'responseDate': resp.responseDate.toIso8601String(), 'url': resp.url, + 'statusCode': resp.statusCode, }; } @@ -121,6 +122,7 @@ class HiveCacheStore extends CacheStore { requestDate: DateTime.parse(map['requestDate'] as String), responseDate: DateTime.parse(map['responseDate'] as String), url: map['url'] as String, + statusCode: map['statusCode'] as int? ?? 200, ); } @@ -409,7 +411,7 @@ class CacheConfig { static CacheOptions buildOptions() { return CacheOptions( store: getStore(), - hitCacheOnErrorExcept: [401, 403], + hitCacheOnNetworkFailure: true, policy: CachePolicy.forceCache, maxStale: const Duration(minutes: 5), ); @@ -444,12 +446,12 @@ class CacheConfig { final customDuration = getCustomDuration(path); if (customDuration != null) { - return baseOptions.copyWith(maxStale: Nullable(customDuration)); + return baseOptions.copyWith(maxStale: customDuration); } return baseOptions.copyWith( policy: CachePolicy.refresh, - maxStale: const Nullable(Duration(minutes: 5)), + maxStale: const Duration(minutes: 5), ); } diff --git a/lib/core/services/data/backup_service.dart b/lib/core/services/data/backup_service.dart index 840e5239..f5017cfd 100644 --- a/lib/core/services/data/backup_service.dart +++ b/lib/core/services/data/backup_service.dart @@ -11,7 +11,7 @@ import 'dart:io'; import 'package:archive/archive.dart'; import 'package:crypto/crypto.dart'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import '../../storage/database/app_database.dart'; import '../../storage/kv_storage.dart'; diff --git a/lib/core/services/data/image_cache_metadata_service.dart b/lib/core/services/data/image_cache_metadata_service.dart index a0a3e336..b450503b 100644 --- a/lib/core/services/data/image_cache_metadata_service.dart +++ b/lib/core/services/data/image_cache_metadata_service.dart @@ -9,7 +9,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart'; import '../../storage/kv_storage.dart'; diff --git a/lib/core/services/data/settings_export_service.dart b/lib/core/services/data/settings_export_service.dart index 187f161e..693f949c 100644 --- a/lib/core/services/data/settings_export_service.dart +++ b/lib/core/services/data/settings_export_service.dart @@ -10,7 +10,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; diff --git a/lib/core/services/device/calendar_service.dart b/lib/core/services/device/calendar_service.dart index 8987b732..5c4dd57d 100644 --- a/lib/core/services/device/calendar_service.dart +++ b/lib/core/services/device/calendar_service.dart @@ -1,19 +1,18 @@ /// ============================================================ /// 闲言APP — 日历同步服务 /// 创建时间: 2026-05-29 -/// 更新时间: 2026-06-06 +/// 更新时间: 2026-06-15 /// 作用: 跨平台日历事件同步(Android/iOS/HarmonyOS/macOS/Windows) -/// 上次更新: 鸿蒙端MethodChannel添加超时保护+MissingPluginException捕获+平台判断早期返回 +/// 上次更新: 迁移至 device_calendar_plus,适配新 API(DeviceCalendar 单例、CalendarPermissionStatus、listCalendars/createEvent/deleteEvent 新签名) /// ============================================================ import 'dart:io'; import 'dart:async'; -import 'package:device_calendar/device_calendar.dart'; +import 'package:device_calendar_plus/device_calendar_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:timezone/timezone.dart' as tz; import 'package:xianyan/core/utils/logger.dart'; import 'package:xianyan/core/utils/platform/platform_utils.dart' as pu; @@ -43,7 +42,7 @@ class CalendarService { CalendarService._(); static final CalendarService instance = CalendarService._(); - final DeviceCalendarPlugin _plugin = DeviceCalendarPlugin(); + final DeviceCalendar _plugin = DeviceCalendar.instance; String? _calendarId; bool _isAvailable = false; @@ -79,11 +78,12 @@ class CalendarService { Log.w('CalendarService: 当前平台不支持日历'); return false; } - final hasPermissions = await _plugin.hasPermissions(); - if (hasPermissions.isSuccess && !hasPermissions.data!) { + final status = await _plugin.hasPermissions(); + if (status != CalendarPermissionStatus.granted) { final result = await _plugin.requestPermissions(); - if (!result.isSuccess || !result.data!) { - Log.w('CalendarService: 日历权限被拒绝'); + if (result != CalendarPermissionStatus.granted && + result != CalendarPermissionStatus.writeOnly) { + Log.w('CalendarService: 日历权限被拒绝 ($result)'); return false; } } @@ -122,23 +122,18 @@ class CalendarService { /// 确保闲言专属日历存在,不存在则创建 Future _ensureCalendar() async { - final calendars = await _plugin.retrieveCalendars(); - if (calendars.isSuccess && calendars.data != null) { - final existing = calendars.data!.where( - (c) => c.name == '闲言' || c.name == 'Xianyan', + final calendars = await _plugin.listCalendars(); + final existing = calendars.where( + (c) => c.name == '闲言' || c.name == 'Xianyan', + ); + if (existing.isNotEmpty) { + _calendarId = existing.first.id; + } else { + final calendarId = await _plugin.createCalendar( + name: '闲言', + colorHex: '#007AFF', ); - if (existing.isNotEmpty) { - _calendarId = existing.first.id; - } else { - final result = await _plugin.createCalendar( - '闲言', - calendarColor: const Color(0xFF007AFF), - localAccountName: '闲言APP', - ); - if (result.isSuccess) { - _calendarId = result.data; - } - } + _calendarId = calendarId; } } @@ -148,6 +143,7 @@ class CalendarService { /// 添加日历事件 /// 鸿蒙端:通过_addEventOhos()桥接,通道未实现时返回false + /// 注意:device_calendar_plus 暂不支持 Reminder,reminderMinutesBefore 仅鸿蒙端生效 Future addEvent(CalendarEvent event) async { // 鸿蒙端:直接走鸿蒙通道 if (_isOhos()) { @@ -160,31 +156,26 @@ class CalendarService { } try { - final local = tz.local; - final calendarEvent = Event( - _calendarId, + // device_calendar_plus 使用 createEvent 方法,无需手动构造 Event 对象 + await _plugin.createEvent( + calendarId: _calendarId!, title: event.title, + startDate: event.start, + endDate: event.end, description: event.description, - start: tz.TZDateTime.from(event.start, local), - end: tz.TZDateTime.from(event.end, local), location: event.location, ); + // device_calendar_plus 暂不支持 Reminder,记录提示 if (event.reminderMinutesBefore != null) { - calendarEvent.reminders = [ - Reminder(minutes: event.reminderMinutesBefore!), - ]; + Log.w( + 'CalendarService: device_calendar_plus 暂不支持 Reminder,' + 'reminderMinutesBefore=${event.reminderMinutesBefore} 已忽略', + ); } - final result = await _plugin.createOrUpdateEvent(calendarEvent); - if (result?.isSuccess == true) { - Log.i('CalendarService: 事件已添加 - ${event.title}'); - return true; - } - Log.e( - 'CalendarService: 添加事件失败 - ${result?.errors.map((e) => e.errorMessage)}', - ); - return false; + Log.i('CalendarService: 事件已添加 - ${event.title}'); + return true; } catch (e) { Log.e('CalendarService: 添加事件异常', e); return false; @@ -216,10 +207,9 @@ class CalendarService { /// 删除日历事件 Future deleteEvent(String eventId) async { - if (_calendarId == null) return false; try { - final result = await _plugin.deleteEvent(_calendarId!, eventId); - return result.isSuccess; + await _plugin.deleteEvent(eventId: eventId); + return true; } catch (e) { Log.e('CalendarService: 删除事件异常', e); return false; diff --git a/lib/core/services/error/crash_monitor.dart b/lib/core/services/error/crash_monitor.dart index 70018968..e2c50a89 100644 --- a/lib/core/services/error/crash_monitor.dart +++ b/lib/core/services/error/crash_monitor.dart @@ -11,7 +11,7 @@ import 'dart:isolate'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import '../../utils/logger.dart'; import '../../utils/platform/platform_utils.dart' as pu; diff --git a/lib/core/services/feature/feature_flag_service.dart b/lib/core/services/feature/feature_flag_service.dart index 111fbb36..e1c790d6 100644 --- a/lib/core/services/feature/feature_flag_service.dart +++ b/lib/core/services/feature/feature_flag_service.dart @@ -11,7 +11,7 @@ import 'dart:convert'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import '../../network/api_client.dart'; import '../../storage/kv_storage.dart'; diff --git a/lib/core/services/form/form_collect_service.dart b/lib/core/services/form/form_collect_service.dart new file mode 100644 index 00000000..f713bde2 --- /dev/null +++ b/lib/core/services/form/form_collect_service.dart @@ -0,0 +1,82 @@ +/// ============================================================ +/// 闲言APP — 表单收集服务 +/// 创建时间: 2026-06-15 +/// 更新时间: 2026-06-15 +/// 作用: 调用 /api/form_collect/submit 提交表单信息(邮箱等) +/// 上次更新: v6.73.0 初始版本 +/// ============================================================ + +import '../../network/api_client.dart'; +import '../../utils/logger.dart'; + +/// 表单来源标识 +enum FormCollectSource { + /// app.html Google Play内测 + appGpBeta('app_gp_beta'), + + /// 注册页面订阅闲言邮箱 + registerSubscribe('register_subscribe'), + + /// Beta页面问卷填写Gmail + betaQuestionnaire('beta_questionnaire'); + + const FormCollectSource(this.value); + final String value; +} + +/// 表单收集服务 +class FormCollectService { + FormCollectService._(); + static final FormCollectService instance = FormCollectService._(); + + static const String _submitPath = '/api/form_collect/submit'; + + /// 提交表单 + /// + /// [email] 邮箱地址(必填) + /// [source] 来源标识(必填) + /// [uid] 用户ID(可选) + /// [extraJson] 扩展字段JSON字符串(可选) + /// [deviceId] 设备ID(可选) + /// + /// 返回 true 提交成功,false 提交失败 + Future submit({ + required String email, + required FormCollectSource source, + String? uid, + String? extraJson, + String? deviceId, + }) async { + try { + final data = { + 'email': email, + 'source': source.value, + }; + if (uid != null && uid.isNotEmpty) data['uid'] = uid; + if (extraJson != null && extraJson.isNotEmpty) { + data['extra_json'] = extraJson; + } + if (deviceId != null && deviceId.isNotEmpty) { + data['device_id'] = deviceId; + } + + final response = await ApiClient.instance.post>( + _submitPath, + data: data, + ); + + final result = response.data; + final code = result?['code']; + if (code == 1) { + Log.d('FormCollect: 提交成功 source=${source.value} email=$email'); + return true; + } + + Log.w('FormCollect: 提交失败 code=$code msg=${result?['msg']}'); + return false; + } catch (e) { + Log.e('FormCollect: 提交异常 $e'); + return false; + } + } +} diff --git a/lib/core/storage/database/app_database.dart b/lib/core/storage/database/app_database.dart index a2d37fa1..ce96e83f 100644 --- a/lib/core/storage/database/app_database.dart +++ b/lib/core/storage/database/app_database.dart @@ -9,7 +9,7 @@ import 'dart:convert'; import 'package:drift/drift.dart'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:intl/intl.dart'; import 'package:xianyan/core/utils/logger.dart'; import 'database_connection/native.dart' diff --git a/lib/core/storage/database/database_connection/native.dart b/lib/core/storage/database/database_connection/native.dart index aaec2a3a..342d19f0 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/OpenHarmony) /// 创建时间: 2026-04-25 -/// 更新时间: 2026-05-17 +/// 更新时间: 2026-06-15 /// 作用: 原生平台数据库连接,OpenHarmony 使用 sqflite_ohos 桥接 -/// 上次更新: 使用pu.isOhos替代Platform.operatingSystem检测,更可靠 +/// 上次更新: 移除sqlite3_flutter_libs依赖,迁移至sqlite3包 /// ============================================================ import 'dart:io'; @@ -12,7 +12,6 @@ import 'package:drift/drift.dart'; 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 'package:xianyan/core/utils/platform/platform_utils.dart' as pu; @@ -28,8 +27,6 @@ QueryExecutor openConnection() { final dbFolder = await getApplicationDocumentsDirectory(); final file = File(p.join(dbFolder.path, 'xianyan.db')); - await applyWorkaroundToOpenSqlite3OnOldAndroidVersions(); - return NativeDatabase.createInBackground(file); }); } diff --git a/lib/core/storage/hive_safe_access.dart b/lib/core/storage/hive_safe_access.dart index 0070ba52..762dfb60 100644 --- a/lib/core/storage/hive_safe_access.dart +++ b/lib/core/storage/hive_safe_access.dart @@ -11,7 +11,7 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart'; import 'package:xianyan/core/utils/logger.dart'; diff --git a/lib/core/storage/kv_storage.dart b/lib/core/storage/kv_storage.dart index 17ee927b..69c53c8b 100644 --- a/lib/core/storage/kv_storage.dart +++ b/lib/core/storage/kv_storage.dart @@ -8,7 +8,7 @@ import 'dart:convert'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../utils/logger.dart'; diff --git a/lib/core/utils/data/bounded_collection_manager.dart b/lib/core/utils/data/bounded_collection_manager.dart new file mode 100644 index 00000000..494324d2 --- /dev/null +++ b/lib/core/utils/data/bounded_collection_manager.dart @@ -0,0 +1,71 @@ +/// ============================================================ +/// 闲言APP — 有界集合管理器 +/// 创建时间: 2026-06-15 +/// 更新时间: 2026-06-15 +/// 作用: 提供固定大小的集合管理,自动清理超出限制的元素 +/// 上次更新: 初始创建,从HomeNotifier中提取重复的有界集合逻辑 +/// ============================================================ + +/// 有界集合管理器 +/// +/// 当集合元素数量达到 [maxSize] 时,再次添加会先清空整个集合再添加新元素。 +/// 这种策略适用于去重场景:当集合过大时,旧数据已无参考价值,直接重置。 +/// +/// 典型用法: +/// ```dart +/// final seenIds = BoundedCollectionManager(maxSize: 5000); +/// seenIds.add('id_1'); +/// seenIds.addAll(['id_2', 'id_3']); +/// if (seenIds.contains('id_1')) { ... } +/// ``` +class BoundedCollectionManager { + /// 集合最大容量 + final int maxSize; + + /// 内部存储 + final Set _collection = {}; + + BoundedCollectionManager({required this.maxSize}); + + // ── 添加操作 ── + + /// 添加单个元素,如果集合已满则清空后添加 + void add(T item) { + if (_collection.length >= maxSize) { + _collection.clear(); + } + _collection.add(item); + } + + /// 批量添加元素 + void addAll(Iterable items) { + for (final item in items) { + add(item); + } + } + + // ── 查询操作 ── + + /// 检查是否包含元素 + bool contains(T item) => _collection.contains(item); + + /// 获取集合大小 + int get length => _collection.length; + + /// 集合是否为空 + bool get isEmpty => _collection.isEmpty; + + /// 集合是否非空 + bool get isNotEmpty => _collection.isNotEmpty; + + /// 获取不可修改的集合视图 + Set get unmodifiable => Set.unmodifiable(_collection); + + /// 转换为 List(用于API传参) + List toList() => _collection.toList(); + + // ── 修改操作 ── + + /// 清空集合 + void clear() => _collection.clear(); +} diff --git a/lib/features/auth/presentation/login_page.dart b/lib/features/auth/presentation/login_page.dart index 848db9f5..cbe04a9e 100644 --- a/lib/features/auth/presentation/login_page.dart +++ b/lib/features/auth/presentation/login_page.dart @@ -6,6 +6,8 @@ /// 上次更新: 移除OAuth社交登录(未正式使用) /// ============================================================ +import 'dart:async'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -22,6 +24,7 @@ import '../../../core/theme/app_radius.dart'; import '../../../core/utils/logger.dart'; import '../../../shared/widgets/feedback/app_toast.dart'; import '../../../shared/widgets/containers/glass_container.dart'; +import '../../../shared/widgets/feedback/agreement_consent_row.dart'; import '../../../l10n/translation_resolver.dart'; import '../../../l10n/types/t.dart'; import '../register_config.dart'; @@ -59,6 +62,11 @@ class _LoginPageState extends ConsumerState bool _agreedToTerms = false; bool _loginSuccess = false; + // 实验功能气泡相关 + bool _showExperimentalBubble = false; + int _bubbleCountdown = 5; + Timer? _bubbleTimer; + @override void initState() { super.initState(); @@ -81,6 +89,7 @@ class _LoginPageState extends ConsumerState @override void dispose() { + _bubbleTimer?.cancel(); _accountController.dispose(); _passwordController.dispose(); _codeController.dispose(); @@ -156,13 +165,86 @@ class _LoginPageState extends ConsumerState children: [ if (_loginSuccess) LoginSuccessView(ext: ext, auth: t.auth) - else if (_isRegisterMode) + else if (_isRegisterMode) ...[ RegisterSection( ext: ext, onSwitchToLogin: () => setState(() => _isRegisterMode = false), onRegisterSuccess: _navigateAfterLogin, - ).animate().fadeIn(duration: 300.ms) + ).animate().fadeIn(duration: 300.ms), + // 实验功能气泡提示 + if (_showExperimentalBubble) + Container( + margin: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ), + padding: const EdgeInsets.all(AppSpacing.sm), + decoration: BoxDecoration( + color: ext.accent.withValues(alpha: 0.1), + borderRadius: AppRadius.mdBorder, + border: Border.all( + color: ext.accent.withValues(alpha: 0.3), + ), + ), + child: Row( + children: [ + Icon(Icons.science, size: 16, color: ext.accent), + const SizedBox(width: 6), + Expanded( + child: Text( + '实验中的功能 ($_bubbleCountdown)', + style: AppTypography.footnote.copyWith( + color: ext.accent, + ), + ), + ), + CupertinoButton( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + minimumSize: Size.zero, + onPressed: () { + _bubbleTimer?.cancel(); + setState(() => _showExperimentalBubble = false); + _showExperimentalFeatureDialog(); + }, + child: Text( + '查看详细', + style: AppTypography.caption1.copyWith( + color: ext.accent, + fontWeight: FontWeight.w600, + ), + ), + ), + CupertinoButton( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + minimumSize: Size.zero, + onPressed: () async { + _bubbleTimer?.cancel(); + setState(() => _showExperimentalBubble = false); + final prefs = + await SharedPreferences.getInstance(); + prefs.setBool( + 'pref_dismiss_experimental_dialog', + true, + ); + }, + child: Text( + '不再提醒', + style: AppTypography.caption1.copyWith( + color: ext.textHint, + ), + ), + ), + ], + ), + ), + ] else _buildLoginView( ext, @@ -310,7 +392,21 @@ class _LoginPageState extends ConsumerState _buildSocialLogin(ext, auth, common), const SizedBox(height: AppSpacing.sm), CupertinoButton( - onPressed: () => _showExperimentalFeatureDialog(), + onPressed: () { + // 检查是否已选择不再提示 + SharedPreferences.getInstance().then((prefs) { + if (prefs.getBool('pref_dismiss_experimental_dialog') == true) { + setState(() => _isRegisterMode = true); + return; + } + setState(() { + _isRegisterMode = true; + _showExperimentalBubble = true; + _bubbleCountdown = 5; + }); + _startBubbleCountdown(); + }); + }, child: Text( auth.noAccountRegister, style: AppTypography.subhead.copyWith(color: ext.accent), @@ -581,102 +677,13 @@ class _LoginPageState extends ConsumerState } Widget _buildAgreement(AppThemeExtension ext, TAuth auth) { - return Padding( - padding: const EdgeInsets.only(top: AppSpacing.sm), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => setState(() => _agreedToTerms = !_agreedToTerms), - child: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 44, minHeight: 44), - child: Center( - child: Container( - width: 20, - height: 20, - margin: const EdgeInsets.only(top: 1), - decoration: BoxDecoration( - color: _agreedToTerms ? ext.accent : Colors.transparent, - borderRadius: BorderRadius.circular(5), - border: Border.all( - color: _agreedToTerms ? ext.accent : ext.textHint, - width: 1.5, - ), - ), - child: _agreedToTerms - ? Icon( - CupertinoIcons.checkmark, - size: 13, - color: ext.textOnAccent, - ) - : null, - ), - ), - ), - ), - const SizedBox(width: AppSpacing.sm), - Expanded( - child: Text.rich( - TextSpan( - text: _isRegisterMode - ? auth.registerAgreePrefix - : auth.loginAgreePrefix, - style: AppTypography.footnote.copyWith( - color: ext.textSecondary, - ), - children: [ - WidgetSpan( - child: GestureDetector( - onTap: () => _showAgreement(true), - child: Text( - auth.userAgreement.startsWith('《') - ? auth.userAgreement - : '《${auth.userAgreement}》', - style: AppTypography.footnote.copyWith( - color: ext.accent, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - TextSpan( - text: auth.and, - style: AppTypography.footnote.copyWith( - color: ext.textSecondary, - ), - ), - WidgetSpan( - child: GestureDetector( - onTap: () => _showAgreement(false), - child: Text( - auth.privacyPolicy.startsWith('《') - ? auth.privacyPolicy - : '《${auth.privacyPolicy}》', - style: AppTypography.footnote.copyWith( - color: ext.accent, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - ), - ), - ), - ], - ), + return AgreementConsentRow( + agreed: _agreedToTerms, + onChanged: (v) => setState(() => _agreedToTerms = v), + ext: ext, ); } - void _showAgreement(bool isUserAgreement) { - if (isUserAgreement) { - context.appPush('/agreement/user-service-agreement'); - } else { - context.appPush('/agreement/privacy-policy'); - } - } - Future _handleLogin() async { final t = ref.read(translationsProvider); if (!_agreedToTerms) { @@ -968,6 +975,24 @@ class _LoginPageState extends ConsumerState } } + /// 气泡倒计时 — 实验功能提示自动消失 + void _startBubbleCountdown() { + _bubbleTimer?.cancel(); + _bubbleTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (!mounted) { + timer.cancel(); + return; + } + setState(() { + _bubbleCountdown--; + if (_bubbleCountdown <= 0) { + _showExperimentalBubble = false; + timer.cancel(); + } + }); + }); + } + void _startCountdown() { Future.doWhile(() async { await Future.delayed(const Duration(seconds: 1)); diff --git a/lib/features/auth/presentation/register_step_account.dart b/lib/features/auth/presentation/register_step_account.dart index 45dc79a1..29dd3c37 100644 --- a/lib/features/auth/presentation/register_step_account.dart +++ b/lib/features/auth/presentation/register_step_account.dart @@ -15,6 +15,7 @@ import '../../../../core/theme/app_spacing.dart'; import '../../../../core/theme/app_typography.dart'; import '../../../../core/theme/app_radius.dart'; import '../../../../l10n/types/t.dart'; +import '../../../../shared/widgets/feedback/agreement_consent_row.dart'; import '../../../../shared/widgets/feedback/app_toast.dart'; import '../services/user_security_service.dart'; import 'login_form_sections.dart'; @@ -347,86 +348,10 @@ class _RegisterStepAccountState extends State { /// 协议同意行(用户协议 + 隐私政策) Widget _buildAgreementRow() { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: widget.onToggleAgreement, - child: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 44, minHeight: 44), - child: Center( - child: Container( - width: 20, - height: 20, - margin: const EdgeInsets.only(top: 1), - decoration: BoxDecoration( - color: widget.agreedToTerms - ? ext.accent - : CupertinoColors.transparent, - borderRadius: BorderRadius.circular(5), - border: Border.all( - color: widget.agreedToTerms ? ext.accent : ext.textHint, - width: 1.5, - ), - ), - child: widget.agreedToTerms - ? Icon( - CupertinoIcons.checkmark, - size: 13, - color: ext.textOnAccent, - ) - : null, - ), - ), - ), - ), - const SizedBox(width: AppSpacing.sm), - Expanded( - child: Text.rich( - TextSpan( - text: auth.registerAgreePrefix, - style: AppTypography.footnote.copyWith(color: ext.textSecondary), - children: [ - WidgetSpan( - child: GestureDetector( - onTap: () => widget.onShowAgreement(true), - child: Text( - auth.userAgreement.startsWith('\u300a') - ? auth.userAgreement - : '\u300a${auth.userAgreement}\u300b', - style: AppTypography.footnote.copyWith( - color: ext.accent, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - TextSpan( - text: auth.and, - style: AppTypography.footnote.copyWith( - color: ext.textSecondary, - ), - ), - WidgetSpan( - child: GestureDetector( - onTap: () => widget.onShowAgreement(false), - child: Text( - auth.privacyPolicy.startsWith('\u300a') - ? auth.privacyPolicy - : '\u300a${auth.privacyPolicy}\u300b', - style: AppTypography.footnote.copyWith( - color: ext.accent, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - ), - ), - ), - ], + return AgreementConsentRow( + agreed: widget.agreedToTerms, + onChanged: (_) => widget.onToggleAgreement(), + ext: ext, ); } } diff --git a/lib/features/ctc/models/ctc_template_model.dart b/lib/features/ctc/models/ctc_template_model.dart new file mode 100644 index 00000000..9d0e71be --- /dev/null +++ b/lib/features/ctc/models/ctc_template_model.dart @@ -0,0 +1,77 @@ +/// 创建时间: 2026-06-15 +/// 更新时间: 2026-06-15 +/// 名称: CTC笔记模板模型 +/// 作用: 预设常用笔记模板(日记/待办/会议记录等) +/// 上次更新: 初始创建 + +/// 笔记模板 +class CtcTemplate { + const CtcTemplate({ + required this.id, + required this.name, + required this.icon, + required this.content, + required this.category, + }); + + /// 模板唯一标识 + final String id; + + /// 模板名称 + final String name; + + /// 模板图标(emoji) + final String icon; + + /// 模板预设内容 + final String content; + + /// 模板分类 + final String category; + + /// 预设模板列表 + static const List presets = [ + CtcTemplate( + id: 'diary', + name: '日记', + icon: '📖', + category: '生活', + content: '# 今日日记\n\n日期:\n天气:\n心情:\n\n---\n\n', + ), + CtcTemplate( + id: 'todo', + name: '待办清单', + icon: '✅', + category: '效率', + content: '# 待办清单\n\n## 今日\n- [ ] \n\n## 本周\n- [ ] \n\n## 已完成\n- [x] \n', + ), + CtcTemplate( + id: 'meeting', + name: '会议记录', + icon: '📝', + category: '工作', + content: '# 会议记录\n\n**主题**:\n**日期**:\n**参会人**:\n\n---\n\n## 议题\n\n### 1. \n\n**讨论**:\n\n**结论**:\n\n### 2. \n\n**讨论**:\n\n**结论**:\n\n---\n\n## 待办事项\n- [ ] \n\n## 下次会议\n- 时间:\n- 议题:\n', + ), + CtcTemplate( + id: 'reading', + name: '读书笔记', + icon: '📚', + category: '学习', + content: '# 读书笔记\n\n**书名**:\n**作者**:\n**阅读日期**:\n\n---\n\n## 摘录\n\n>\n\n## 感想\n\n\n\n## 关键词\n- \n', + ), + CtcTemplate( + id: 'idea', + name: '灵感记录', + icon: '💡', + category: '创意', + content: '# 灵感记录\n\n**时间**:\n**场景**:\n\n---\n\n## 想法\n\n\n\n## 延伸\n- \n\n## 可行性\n- \n', + ), + CtcTemplate( + id: 'blank', + name: '空白笔记', + icon: '📄', + category: '通用', + content: '', + ), + ]; +} diff --git a/lib/features/ctc/presentation/pages/ctc_note_edit_page.dart b/lib/features/ctc/presentation/pages/ctc_note_edit_page.dart index 978ba405..594e57d9 100644 --- a/lib/features/ctc/presentation/pages/ctc_note_edit_page.dart +++ b/lib/features/ctc/presentation/pages/ctc_note_edit_page.dart @@ -20,6 +20,7 @@ import 'package:xianyan/features/ctc/presentation/widgets/ctc_qr_sheet.dart'; import 'package:xianyan/features/ctc/presentation/widgets/ctc_markdown_preview.dart'; import 'package:xianyan/features/ctc/presentation/pages/ctc_history_page.dart'; import 'package:xianyan/shared/widgets/adaptive/keyboard_safe_sheet.dart'; +import 'package:xianyan/shared/widgets/feedback/app_toast.dart'; class CtcNoteEditPage extends ConsumerStatefulWidget { final String noteKey; @@ -106,17 +107,37 @@ class _CtcNoteEditPageState extends ConsumerState with WidgetsB final notes = ref.read(ctcNoteListProvider).notes; final note = notes.where((n) => n.key == noteKey).firstOrNull; - if (note != null && note.content.isNotEmpty) { - _contentController.text = note.content; + if (note != null) { + if (note.content.isNotEmpty) { + // 本地有内容,直接使用 + _contentController.text = note.content; + _undoStack.push(_contentController.text); + setState(() { _isLoading = false; }); + _updateCursorPos(); + // 即使本地有内容,也尝试从服务端拉取最新版本 + _tryPullLatest(note.content); + return; + } + // 本地笔记存在但内容为空,尝试从服务端拉取 + try { + final api = ref.read(ctcApiClientProvider); + final content = await api.readNote(noteKey); + if (content != null && content.isNotEmpty) { + _contentController.text = content; + await notifier.updateNote(noteKey, content); + } else { + _contentController.text = ''; + } + } catch (e) { + _contentController.text = ''; + } _undoStack.push(_contentController.text); setState(() { _isLoading = false; }); _updateCursorPos(); - // 即使本地有内容,也尝试从服务端拉取最新版本 - _tryPullLatest(note.content); return; } - // 本地为空,从服务端拉取 + // 本地无笔记记录,从服务端拉取 try { final api = ref.read(ctcApiClientProvider); final content = await api.readNote(noteKey); @@ -125,8 +146,7 @@ class _CtcNoteEditPageState extends ConsumerState with WidgetsB await notifier.updateNote(noteKey, content); } } catch (e) { - // 使用本地缓存 - if (note != null) _contentController.text = note.content; + // 服务端拉取失败,使用空内容 } _undoStack.push(_contentController.text); @@ -210,7 +230,7 @@ class _CtcNoteEditPageState extends ConsumerState with WidgetsB setState(() { _isSaving = false; - _lastSaveTime = '${DateTime.now().hour}:${DateTime.now().minute.toString().padLeft(2, '0')}'; + _lastSaveTime = '${DateTime.now().year}-${DateTime.now().month.toString().padLeft(2, '0')}-${DateTime.now().day.toString().padLeft(2, '0')} ${DateTime.now().hour.toString().padLeft(2, '0')}:${DateTime.now().minute.toString().padLeft(2, '0')}:${DateTime.now().second.toString().padLeft(2, '0')}'; }); } catch (e) { setState(() => _isSaving = false); @@ -245,9 +265,7 @@ class _CtcNoteEditPageState extends ConsumerState with WidgetsB mainAxisSize: MainAxisSize.min, children: [ if (_isSaving) - const CupertinoActivityIndicator() - else if (_lastSaveTime != null) - Text('已保存', style: TextStyle(fontSize: 12, color: ext.textHint)), + const CupertinoActivityIndicator(), // 预览切换 CupertinoButton( padding: EdgeInsets.zero, @@ -325,6 +343,14 @@ class _CtcNoteEditPageState extends ConsumerState with WidgetsB const SizedBox(width: 6), Text('ctc.s2ss.com/', style: TextStyle(fontSize: 13, color: ext.textHint)), Text(noteKey, style: TextStyle(fontSize: 13, color: ext.iconTintBlue, fontWeight: FontWeight.w600)), + const SizedBox(width: 6), + GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: 'https://ctc.s2ss.com/$noteKey')); + AppToast.showSuccess('链接已复制'); + }, + child: Icon(CupertinoIcons.doc_on_clipboard, size: 14, color: ext.textHint), + ), ], ), ); @@ -417,15 +443,45 @@ class _CtcNoteEditPageState extends ConsumerState with WidgetsB // 同步状态 Row( children: [ - if (_isSaving) ...[ - const CupertinoActivityIndicator(radius: 6), - const SizedBox(width: 4), - Text('保存中', style: TextStyle(fontSize: 11, color: ext.textHint)), - ] else ...[ - Icon(CupertinoIcons.checkmark_circle, size: 12, color: ext.successColor), - const SizedBox(width: 4), - Text('已同步', style: TextStyle(fontSize: 11, color: ext.textHint)), + if (_lastSaveTime != null) ...[ + Text(_lastSaveTime!, style: TextStyle(fontSize: 11, color: ext.textHint)), + const SizedBox(width: 6), ], + GestureDetector( + onTap: () => AppToast.showInfo('笔记已保存到本地'), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_isSaving) ...[ + const CupertinoActivityIndicator(radius: 6), + const SizedBox(width: 4), + Text('保存中', style: TextStyle(fontSize: 11, color: ext.textHint)), + ] else if (_lastSaveTime != null) ...[ + Icon(CupertinoIcons.checkmark_circle, size: 12, color: ext.successColor), + const SizedBox(width: 4), + Text('已保存', style: TextStyle(fontSize: 11, color: ext.textHint)), + ], + ], + ), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: () => AppToast.showInfo('笔记已推送到仓库'), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_isSaving) ...[ + const CupertinoActivityIndicator(radius: 6), + const SizedBox(width: 4), + Text('同步中', style: TextStyle(fontSize: 11, color: ext.textHint)), + ] else ...[ + Icon(CupertinoIcons.checkmark_circle, size: 12, color: ext.successColor), + const SizedBox(width: 4), + Text('已同步', style: TextStyle(fontSize: 11, color: ext.textHint)), + ], + ], + ), + ), ], ), ], @@ -524,6 +580,76 @@ class _CtcNoteEditPageState extends ConsumerState with WidgetsB _onContentChanged(); } + /// 拉取笔记(从服务端拉取最新内容) + Future _pullNote() async { + final content = _contentController.text; + // 检查本地是否有未保存的修改 + if (content.isNotEmpty) { + final confirmed = await showCupertinoDialog( + context: context, + builder: (ctx) => CupertinoAlertDialog( + title: const Text('拉取确认'), + content: const Text('本地有未保存的修改,拉取将覆盖本地内容,是否继续?'), + actions: [ + CupertinoDialogAction(isDestructiveAction: true, child: const Text('取消'), onPressed: () => Navigator.pop(ctx, false)), + CupertinoDialogAction(isDefaultAction: true, child: const Text('继续拉取'), onPressed: () => Navigator.pop(ctx, true)), + ], + ), + ); + if (confirmed != true) return; + } + + try { + final api = ref.read(ctcApiClientProvider); + final remoteContent = await api.readNote(noteKey); + if (remoteContent != null) { + _contentController.text = remoteContent; + _onContentChanged(); + await ref.read(ctcNoteListProvider.notifier).updateNote(noteKey, remoteContent); + AppToast.showSuccess('拉取成功'); + } else { + AppToast.showInfo('服务端暂无内容'); + } + } catch (e) { + AppToast.showError('拉取失败'); + } + } + + /// 推送笔记(将本地内容推送到服务端) + Future _pushNote() async { + final note = ref.read(ctcNoteListProvider).notes.where((n) => n.key == noteKey).firstOrNull; + if (note == null) return; + + // 检查服务端是否有更新 + if (note.hasRemoteChange) { + final confirmed = await showCupertinoDialog( + context: context, + builder: (ctx) => CupertinoAlertDialog( + title: const Text('推送确认'), + content: const Text('服务端有更新,推送将覆盖服务端内容,是否继续?'), + actions: [ + CupertinoDialogAction(isDestructiveAction: true, child: const Text('取消'), onPressed: () => Navigator.pop(ctx, false)), + CupertinoDialogAction(isDefaultAction: true, child: const Text('继续推送'), onPressed: () => Navigator.pop(ctx, true)), + ], + ), + ); + if (confirmed != true) return; + } + + try { + final content = _contentController.text; + final sync = ref.read(ctcSyncServiceProvider); + final result = await sync.pushNote(note.copyWith(content: content, size: content.length)); + if (result.success) { + AppToast.showSuccess('推送成功'); + } else { + AppToast.showError('推送失败'); + } + } catch (e) { + AppToast.showError('推送失败'); + } + } + /// 更多操作 void _showMoreActions(CtcNoteModel? note) { if (note == null) return; @@ -532,8 +658,18 @@ class _CtcNoteEditPageState extends ConsumerState with WidgetsB builder: (ctx) => CupertinoActionSheet( actions: [ CupertinoActionSheetAction( - onPressed: () { Navigator.pop(ctx); _saveContent(); }, - child: const Text('手动同步'), + onPressed: () { + Navigator.pop(ctx); + _pullNote(); + }, + child: const Text('拉取笔记'), + ), + CupertinoActionSheetAction( + onPressed: () { + Navigator.pop(ctx); + _pushNote(); + }, + child: const Text('推送笔记'), ), CupertinoActionSheetAction( onPressed: () { diff --git a/lib/features/ctc/presentation/widgets/ctc_add_note_sheet.dart b/lib/features/ctc/presentation/widgets/ctc_add_note_sheet.dart index ec20945f..6aee9963 100644 --- a/lib/features/ctc/presentation/widgets/ctc_add_note_sheet.dart +++ b/lib/features/ctc/presentation/widgets/ctc_add_note_sheet.dart @@ -9,11 +9,13 @@ import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:xianyan/core/theme/app_theme.dart'; import 'package:xianyan/features/ctc/ctc.dart'; import 'package:xianyan/shared/widgets/adaptive/keyboard_safe_sheet.dart'; +import 'package:xianyan/shared/widgets/feedback/app_toast.dart'; import '../pages/ctc_note_edit_page.dart'; /// 显示悬浮添加笔记对话框 @@ -40,6 +42,7 @@ class _CtcAddNoteSheetState extends ConsumerState final _contentFocusNode = FocusNode(); bool _isValid = false; String? _keyError; + bool _isCreating = false; late final AnimationController _animController; late final Animation _scaleAnim; late final Animation _fadeAnim; @@ -114,10 +117,13 @@ class _CtcAddNoteSheetState extends ConsumerState Future _createNote() async { if (!_isValid) return; + if (_isCreating) return; + setState(() => _isCreating = true); // 检查笔记数量上限 final state = ref.read(ctcNoteListProvider); if (state.isAtLimit) { + setState(() => _isCreating = false); showCupertinoDialog( context: context, builder: (ctx) => CupertinoAlertDialog( @@ -142,6 +148,7 @@ class _CtcAddNoteSheetState extends ConsumerState if (mounted) { if (!success) { + setState(() => _isCreating = false); // 显示错误提示 final error = ref.read(ctcNoteListProvider).error; showCupertinoDialog( @@ -260,6 +267,16 @@ class _CtcAddNoteSheetState extends ConsumerState overflow: TextOverflow.ellipsis, ), ), + if (key.isNotEmpty) ...[ + const SizedBox(width: 6), + GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: 'https://ctc.s2ss.com/$key')); + AppToast.showSuccess('链接已复制'); + }, + child: Icon(CupertinoIcons.doc_on_clipboard, size: 14, color: ext.textHint), + ), + ], ], ), ), @@ -380,8 +397,10 @@ class _CtcAddNoteSheetState extends ConsumerState Expanded( child: CupertinoButton.filled( borderRadius: BorderRadius.circular(12), - onPressed: _isValid ? _createNote : null, - child: const Text('创建'), + onPressed: _isValid && !_isCreating ? _createNote : null, + child: _isCreating + ? const CupertinoActivityIndicator() + : const Text('创建'), ), ), ], diff --git a/lib/features/ctc/services/ctc_api_client.dart b/lib/features/ctc/services/ctc_api_client.dart index 5f9b93bc..e23f6b3e 100644 --- a/lib/features/ctc/services/ctc_api_client.dart +++ b/lib/features/ctc/services/ctc_api_client.dart @@ -42,7 +42,7 @@ class CtcApiClient { /// 写入/创建笔记 (返回笔记信息) Future?> writeNote(String key, String text) async { try { - final resp = await _dio.post>('/$key', data: 'text=${Uri.encodeComponent(text)}', queryParameters: {'json': ''}); + final resp = await _dio.post>('/$key', data: {'text': text}, queryParameters: {'json': ''}); if (resp.statusCode == 200 && resp.data is Map) { return resp.data as Map; } diff --git a/lib/features/ctc/services/ctc_crypto_service.dart b/lib/features/ctc/services/ctc_crypto_service.dart new file mode 100644 index 00000000..10882a53 --- /dev/null +++ b/lib/features/ctc/services/ctc_crypto_service.dart @@ -0,0 +1,171 @@ +/// 创建时间: 2026-06-15 +/// 更新时间: 2026-06-15 +/// 名称: CTC笔记加密服务 +/// 作用: 使用 flutter_secure_storage 对敏感笔记进行端到端加密 +/// 上次更新: 使用AES-CBC替代不安全的XOR加密;clearAllEncryptedData改为只删除CTC相关键 + +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; +import 'package:encrypt/encrypt.dart' as encrypt; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +/// CTC笔记加密服务 +/// 使用PIN码派生密钥,对笔记内容进行AES-CBC加密后存储在 flutter_secure_storage +class CtcCryptoService { + CtcCryptoService(); + + // ==================== 常量 ==================== + + static const _storage = FlutterSecureStorage( + iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), + ); + + /// 加密数据前缀 + static const _keyPrefix = 'ctc_enc_key_'; + + /// PIN哈希存储键 + static const _pinKey = 'ctc_enc_pin_hash'; + + /// 加密锁定开关存储键 + static const _lockEnabledKey = 'ctc_enc_lock_enabled'; + + // ==================== PIN管理 ==================== + + /// 检查是否已设置加密PIN + Future hasPin() async { + final hash = await _storage.read(key: _pinKey); + return hash != null && hash.isNotEmpty; + } + + /// 设置加密PIN(至少4位) + Future setPin(String pin) async { + if (pin.length < 4) return false; + final hash = _hashPin(pin); + await _storage.write(key: _pinKey, value: hash); + return true; + } + + /// 验证PIN是否正确 + Future verifyPin(String pin) async { + final storedHash = await _storage.read(key: _pinKey); + if (storedHash == null) return false; + return _hashPin(pin) == storedHash; + } + + /// 修改PIN(需验证旧PIN) + Future changePin(String oldPin, String newPin) async { + final isValid = await verifyPin(oldPin); + if (!isValid) return false; + return setPin(newPin); + } + + // ==================== 加密锁定开关 ==================== + + /// 获取加密锁定是否启用 + Future isLockEnabled() async { + final value = await _storage.read(key: _lockEnabledKey); + return value == 'true'; + } + + /// 设置加密锁定开关 + Future setLockEnabled(bool enabled) async { + await _storage.write(key: _lockEnabledKey, value: enabled.toString()); + } + + // ==================== 笔记加密/解密 ==================== + + /// 加密笔记内容 + /// 使用AES-CBC加密,每次加密生成随机IV,IV与密文拼接后base64存储 + Future encryptContent(String noteKey, String content, String pin) async { + final key = _deriveKey(noteKey, pin); + final storageKey = '$_keyPrefix$noteKey'; + + // AES-CBC加密:使用标准加密库,随机IV保证相同明文产生不同密文 + final aesKey = encrypt.Key.fromUtf8(key); + final iv = encrypt.IV.fromSecureRandom(16); + final encrypter = encrypt.Encrypter(encrypt.AES(aesKey, mode: encrypt.AESMode.cbc)); + final encrypted = encrypter.encrypt(content, iv: iv); + + // 拼接 IV + 密文,便于解密时提取IV + final encoded = base64Encode(iv.bytes + encrypted.bytes); + await _storage.write(key: storageKey, value: encoded); + return encoded; + } + + /// 解密笔记内容 + /// 返回null表示解密失败(PIN错误或数据损坏) + Future decryptContent(String noteKey, String pin) async { + final storageKey = '$_keyPrefix$noteKey'; + final encoded = await _storage.read(key: storageKey); + if (encoded == null) return null; + + try { + final key = _deriveKey(noteKey, pin); + final aesKey = encrypt.Key.fromUtf8(key); + final allBytes = base64Decode(encoded); + + // 前16字节为IV,剩余为AES密文 + if (allBytes.length < 16) return null; + final ivBytes = allBytes.sublist(0, 16); + final cipherBytes = allBytes.sublist(16); + + final iv = encrypt.IV(ivBytes); + final encrypter = encrypt.Encrypter(encrypt.AES(aesKey, mode: encrypt.AESMode.cbc)); + final decrypted = encrypter.decrypt64(base64Encode(cipherBytes), iv: iv); + + return decrypted; + } catch (_) { + return null; + } + } + + // ==================== 加密状态查询 ==================== + + /// 检查笔记是否已加密 + Future isEncrypted(String noteKey) async { + final storageKey = '$_keyPrefix$noteKey'; + final value = await _storage.read(key: storageKey); + return value != null && value.isNotEmpty; + } + + /// 删除笔记加密数据 + Future deleteEncryptedData(String noteKey) async { + final storageKey = '$_keyPrefix$noteKey'; + await _storage.delete(key: storageKey); + } + + /// 清除所有CTC加密数据(包括PIN、锁定开关和所有加密笔记) + /// 仅删除CTC相关键,不影响其他功能的安全存储数据 + Future clearAllEncryptedData() async { + // 删除PIN哈希和锁定开关 + await _storage.delete(key: _pinKey); + await _storage.delete(key: _lockEnabledKey); + + // 删除所有加密笔记数据(键以 _keyPrefix 开头) + final allKeys = await _storage.readAll(); + for (final key in allKeys.keys) { + if (key.startsWith(_keyPrefix)) { + await _storage.delete(key: key); + } + } + } + + // ==================== 私有方法 ==================== + + /// PIN哈希(SHA-256) + String _hashPin(String pin) { + final bytes = utf8.encode(pin); + final digest = sha256.convert(bytes); + return digest.toString(); + } + + /// 从noteKey和PIN派生加密密钥 + /// 使用SHA-256对组合字符串进行哈希,取前32字符作为密钥 + String _deriveKey(String noteKey, String pin) { + final combined = '$noteKey:$pin:xianyan-ctc-v1'; + final bytes = utf8.encode(combined); + final digest = sha256.convert(bytes); + return digest.toString().substring(0, 32); + } +} diff --git a/lib/features/ctc/services/ctc_local_storage.dart b/lib/features/ctc/services/ctc_local_storage.dart index 925f465e..2f20fcb0 100644 --- a/lib/features/ctc/services/ctc_local_storage.dart +++ b/lib/features/ctc/services/ctc_local_storage.dart @@ -83,10 +83,18 @@ class CtcLocalStorage { // ========== 版本历史(仅本地) ========== - /// 添加历史快照 + /// 添加历史快照(内容无变化时跳过,避免重复记录) Future addHistory(CtcHistoryModel history) async { final box = _historyBoxInstance; if (box == null) return; + // 去重:检查最近一条历史是否内容相同 + final existingHistory = getHistory(history.noteKey); + if (existingHistory.isNotEmpty) { + final latest = existingHistory.first; + if (latest.content == history.content) { + return; // 内容无变化,不记录 + } + } await box.put(history.id, jsonEncode(history.toJson())); } diff --git a/lib/features/discover/services/exchange_rate_service.dart b/lib/features/discover/services/exchange_rate_service.dart index 74e02cca..022b4e42 100644 --- a/lib/features/discover/services/exchange_rate_service.dart +++ b/lib/features/discover/services/exchange_rate_service.dart @@ -9,7 +9,7 @@ import 'dart:convert'; import 'package:dio/dio.dart'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:xianyan/core/utils/logger.dart'; /// 汇率换算结果 diff --git a/lib/features/discover/services/rss_service.dart b/lib/features/discover/services/rss_service.dart index 1ba51deb..52019b60 100644 --- a/lib/features/discover/services/rss_service.dart +++ b/lib/features/discover/services/rss_service.dart @@ -10,7 +10,7 @@ import 'dart:convert'; import 'package:dio/dio.dart'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:rss_dart/dart_rss.dart'; import 'package:xianyan/core/utils/logger.dart'; import 'package:xianyan/core/storage/hive_safe_access.dart'; diff --git a/lib/features/home/presentation/cache_management_page.dart b/lib/features/home/presentation/cache_management_page.dart index af6c7fc6..ae0beb61 100644 --- a/lib/features/home/presentation/cache_management_page.dart +++ b/lib/features/home/presentation/cache_management_page.dart @@ -945,7 +945,7 @@ class _CacheManagementPageState extends ConsumerState { const SizedBox(height: AppSpacing.sm), _buildTrashSourceRow( CupertinoIcons.chat_bubble_2, - '聊天消息', + '会话消息', '${stats?.chatMessageCount ?? 0} 条', ext, ), diff --git a/lib/features/home/presentation/home_page.dart b/lib/features/home/presentation/home_page.dart index 641e80f8..5912c943 100644 --- a/lib/features/home/presentation/home_page.dart +++ b/lib/features/home/presentation/home_page.dart @@ -1,9 +1,9 @@ // ============================================================ // 闲言APP — 首页 // 创建时间: 2026-04-20 -// 更新时间: 2026-05-30 +// 更新时间: 2026-06-15 // 作用: 句子阅读主页面,展示每日推荐 + 分类筛选 + 句子流 -// 上次更新: SheetAnimationNotifier改为Riverpod provider,使用ref.watch监听 +// 上次更新: 拆分AppBar和系统状态监听到独立组件,降低HomePage复杂度 // ============================================================ import 'dart:async'; @@ -12,32 +12,19 @@ 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:heroine/heroine.dart'; import 'package:sliver_tools/sliver_tools.dart'; import 'package:stupid_simple_sheet/stupid_simple_sheet.dart'; import 'package:xianyan/l10n/translations.dart'; -import '../../../core/constants/character_name.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/router/app_routes.dart'; import '../../../core/router/app_nav_extension.dart'; -import '../../../core/services/device/battery_info_service.dart'; import '../../../shared/widgets/containers/bottom_sheet.dart'; -import '../../../core/constants/character_expression.dart'; -import '../../../shared/widgets/animation/appbar_character_sprite.dart'; -import '../../../shared/widgets/display/appbar_date_display.dart'; -import '../../../shared/widgets/animation/character_tip_bubble.dart'; -import '../../../core/services/audio/tts_service.dart'; -import '../../../core/utils/ui/interaction_animations.dart'; import '../../../core/utils/platform/platform_feature_guard.dart'; -import '../../../core/utils/platform/platform_utils.dart' as pu; +import '../../../shared/widgets/animation/appbar_character_sprite.dart'; import '../../../shared/widgets/display/skeleton.dart'; -import '../providers/character_tips_provider.dart'; -import '../providers/character_mood_provider.dart'; import '../providers/home_provider.dart'; import '../../../features/source/providers/source_provider.dart'; import '../../../features/settings/providers/theme_settings_provider.dart'; @@ -53,6 +40,8 @@ import 'widgets/home_empty_daily_card.dart'; import 'widgets/home_action_buttons.dart'; import 'widgets/home_sentence_list_section.dart'; import 'widgets/quick_card_sheet.dart'; +import 'widgets/home_app_bar_section.dart'; +import 'widgets/home_system_state_monitor.dart'; import 'providers/sentence_detail_sheet.dart'; import '../../../../core/providers/split_view_provider.dart'; import '../../../../core/layout/adaptive_split_view.dart'; @@ -76,8 +65,6 @@ class _HomePageState extends ConsumerState { bool _scrollLocked = false; bool _isSwitchingChannel = false; - StreamSubscription? _batterySubscription; - StreamSubscription? _ttsSubscription; final List _providerSubscriptions = []; @override @@ -96,35 +83,6 @@ class _HomePageState extends ConsumerState { WidgetsBinding.instance.addPostFrameCallback((_) { _setupStateListeners(); }); - BatteryInfoService.instance.init(); - _batterySubscription = BatteryInfoService.instance.onBatteryChanged.listen(( - info, - ) { - if (!mounted) return; - if (!info.isLow) return; - _characterKey.currentState?.triggerExpression( - CharacterExpression.worried, - ); - final t = ref.read(translationsProvider); - final message = info.isCritical - ? t.home.base.batteryCritical - : t.home.base.batteryLow; - ref - .read(characterTipsProvider.notifier) - .showTip(TipsCategory.easterEgg, message); - }); - _ttsSubscription = TtsService.instance.onStateChanged.listen((ttsState) { - if (!mounted) return; - if (ttsState == TtsState.speaking) { - _characterKey.currentState?.triggerExpression( - CharacterExpression.speaking, - ); - } else if (ttsState == TtsState.paused || ttsState == TtsState.idle) { - _characterKey.currentState?.triggerExpression( - CharacterExpression.smile, - ); - } - }); _scrollController.addListener(_onScrollForReading); } @@ -133,8 +91,6 @@ class _HomePageState extends ConsumerState { for (final sub in _providerSubscriptions) { sub.close(); } - _batterySubscription?.cancel(); - _ttsSubscription?.cancel(); _readingController.dispose(); _scrollController.removeListener(_onScrollForReading); _scrollController.dispose(); @@ -242,246 +198,139 @@ class _HomePageState extends ConsumerState { themeSettingsProvider.select((s) => s.tabCharacterStyleId), ); - final Widget scaffold = CupertinoPageScaffold( + return CupertinoPageScaffold( backgroundColor: ext.bgPrimary, child: SafeArea( bottom: false, - child: Listener( - onPointerDown: _handlePointerDown, - onPointerMove: _handlePointerMove, - onPointerUp: _handlePointerUp, - child: HomeRefreshIndicator( - onOpenPanel: () => _showToolCenter(context), - characterId: characterId, - child: CustomScrollView( - controller: _scrollController, - physics: _scrollLocked - ? const NeverScrollableScrollPhysics() - : const BouncingScrollPhysics(), - slivers: [ - if (state.isOffline) const HomeOfflineBanner(), + child: HomeSystemStateMonitor( + characterKey: _characterKey, + child: Listener( + onPointerDown: _handlePointerDown, + onPointerMove: _handlePointerMove, + onPointerUp: _handlePointerUp, + child: HomeRefreshIndicator( + onOpenPanel: () => _showToolCenter(context), + characterId: characterId, + child: CustomScrollView( + controller: _scrollController, + physics: _scrollLocked + ? const NeverScrollableScrollPhysics() + : const BouncingScrollPhysics(), + slivers: [ + if (state.isOffline) const HomeOfflineBanner(), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: 4, - ), - child: Row( - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - clipBehavior: Clip.none, - children: [ - AppBarCharacterSprite( - key: _characterKey, - characterId: characterId, - animationIntensity: ref.watch( - themeSettingsProvider.select( - (s) => s - .animationIntensity - .durationMultiplier, - ), + HomeAppBarSection( + characterKey: _characterKey, + characterId: characterId, + onDateTap: () => _showDateConfigSheet(context), + ), + + SliverToBoxAdapter( + child: (state.isForceLoading && state.dailySentence == null) + ? const DailyCardSkeleton() + : Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ), + child: + state.dailySentence == null && + state.dailySentences.isEmpty + ? HomeEmptyDailyCard( + onRetry: () => + ref.read(homeProvider.notifier).refresh(), + ) + : DailyCard( + ext: ext, + state: state, + onScrollLockChanged: (locked) { + setState(() => _scrollLocked = locked); + }, + onLike: (sentence) => ref + .read(homeProvider.notifier) + .toggleLike(sentence.id), + onFavorite: (sentence) => ref + .read(homeProvider.notifier) + .toggleFavorite(sentence.id), + onLoadMore: () => ref + .read(homeProvider.notifier) + .refreshDailySentences(), + onTap: (sentence) => + _showDailyDetailSheet(sentence, ext, ref), ), - mood: ref.watch( - characterMoodProvider.select((s) => s.mood), - ), - onTap: () { - ref - .read(characterTipsProvider.notifier) - .generateTip(characterId); - if (TtsService.instance.isAvailable && - state.dailySentence != null) { - TtsService.instance.speak( - state.dailySentence!.text, - ); - } - }, - onDoubleTap: () { - ref - .read(characterTipsProvider.notifier) - .generateTip(characterId); - if (TtsService.instance.isSpeaking) { - TtsService.instance.stop(); - } - }, - ), - Positioned( - left: 0, - top: 52, - child: CharacterTipBubble( - characterId: characterId, - ), - ), - ], - ), - const SizedBox(width: AppSpacing.sm), - GestureDetector( - onTap: () => - _characterKey.currentState?.lookAtTitle(), - child: Text( - CharacterName.appBarTitle, - style: AppTypography.title1.copyWith( - color: ext.textPrimary, - ), - ), - ), - ], - ), - const Spacer(), - AppBarDateDisplay( - onTap: () => _showDateConfigSheet(context), - ), - const Spacer(), - if (pu.isIOS) - BounceButton( - onTap: () => context.appPush(AppRoutes.anonymousSubmit), - child: Container( - padding: const EdgeInsets.all(AppSpacing.sm), - decoration: BoxDecoration( - color: ext.bgSecondary, - borderRadius: AppRadius.fullBorder, - ), - child: Icon( - CupertinoIcons.pencil_ellipsis_rectangle, - size: 20, - color: ext.iconSecondary, - ), - ), - ) - else - BounceButton( - onTap: () => context.appPush(AppRoutes.search), - child: Heroine( - tag: 'search-icon', - child: Container( - padding: const EdgeInsets.all(AppSpacing.sm), - decoration: BoxDecoration( - color: ext.bgSecondary, - borderRadius: AppRadius.fullBorder, - ), - child: Icon( - CupertinoIcons.search, - size: 20, - color: ext.iconSecondary, - ), - ), - ), - ), - ], - ), - ).safeFadeIn(duration: 300.ms), - ), + ).safeFadeInSlideY(duration: 300.ms, delay: 100.ms), + ), - SliverToBoxAdapter( - child: (state.isForceLoading && state.dailySentence == null) - ? const DailyCardSkeleton() - : Padding( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.sm, - ), - child: - state.dailySentence == null && - state.dailySentences.isEmpty - ? HomeEmptyDailyCard( - onRetry: () => - ref.read(homeProvider.notifier).refresh(), - ) - : DailyCard( - ext: ext, - state: state, - onScrollLockChanged: (locked) { - setState(() => _scrollLocked = locked); - }, - onLike: (sentence) => ref - .read(homeProvider.notifier) - .toggleLike(sentence.id), - onFavorite: (sentence) => ref - .read(homeProvider.notifier) - .toggleFavorite(sentence.id), - onLoadMore: () => ref - .read(homeProvider.notifier) - .refreshDailySentences(), - onTap: (sentence) => - _showDailyDetailSheet(sentence, ext, ref), - ), - ).safeFadeInSlideY(duration: 300.ms, delay: 100.ms), - ), - - HomeActionButtons( - onCreateCard: () { - final text = (state.dailySentence?.text.isNotEmpty == true) - ? state.dailySentence!.text - : t.home.base.defaultSentence; - _showQuickCard(context, text); - }, - onEditSentence: () { - final text = (state.dailySentence?.text.isNotEmpty == true) - ? state.dailySentence!.text - : t.home.base.defaultSentence; - context.appPush( - '${AppRoutes.editor}?text=${Uri.encodeComponent(text)}', - ); - }, - ), - - SliverPinnedHeader( - child: SquareHeaderContent( - ext: ext, - selectedType: state.selectedType, - channels: state.channels, - currentSort: state.currentSort, - onRefresh: () => ref.read(homeProvider.notifier).refresh(), - onSelectType: (code) => _onChannelSwitch(code), - onSortChanged: (sort) => - ref.read(homeProvider.notifier).changeSort(sort), - onSwipeLeft: () => _swipeToNextCategory(), - onSwipeRight: () => _swipeToPrevCategory(), - onScrollToTop: () { - if (_scrollController.hasClients) { - _scrollController.animateTo( - 0, - duration: const Duration(milliseconds: 800), - curve: Curves.easeInOutCubic, - ); - } + HomeActionButtons( + onCreateCard: () { + final text = (state.dailySentence?.text.isNotEmpty == true) + ? state.dailySentence!.text + : t.home.base.defaultSentence; + _showQuickCard(context, text); + }, + onEditSentence: () { + final text = (state.dailySentence?.text.isNotEmpty == true) + ? state.dailySentence!.text + : t.home.base.defaultSentence; + context.appPush( + '${AppRoutes.editor}?text=${Uri.encodeComponent(text)}', + ); }, ), - ), - HomeSentenceListSection( - state: state, - isSwitchingChannel: _isSwitchingChannel, - onLoadMore: () => ref.read(homeProvider.notifier).loadMore(), - onCheckPreload: (index) => - ref.read(homeProvider.notifier).checkPreload(index), - onToggleLike: (id) => - ref.read(homeProvider.notifier).toggleLike(id), - onToggleFavorite: (id) => - ref.read(homeProvider.notifier).toggleFavorite(id), - onToggleReadLater: (id) => - ref.read(homeProvider.notifier).toggleReadLater(id), - onMarkRead: (id) => - ref.read(homeProvider.notifier).markRead(id), - onRefresh: () => ref.read(homeProvider.notifier).refresh(), - onRefreshSentenceList: () => - ref.read(homeProvider.notifier).refreshSentenceList(), - onSlidableOpened: _gestureController.onSlidableOpened, - onSlidableClosed: _gestureController.onSlidableClosed, - ), + SliverPinnedHeader( + child: SquareHeaderContent( + ext: ext, + selectedType: state.selectedType, + channels: state.channels, + currentSort: state.currentSort, + onRefresh: () => ref.read(homeProvider.notifier).refresh(), + onSelectType: (code) => _onChannelSwitch(code), + onSortChanged: (sort) => + ref.read(homeProvider.notifier).changeSort(sort), + onSwipeLeft: () => _swipeToNextCategory(), + onSwipeRight: () => _swipeToPrevCategory(), + onScrollToTop: () { + if (_scrollController.hasClients) { + _scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 800), + curve: Curves.easeInOutCubic, + ); + } + }, + ), + ), - const SliverToBoxAdapter(child: SizedBox(height: 140)), - ], + HomeSentenceListSection( + state: state, + isSwitchingChannel: _isSwitchingChannel, + onLoadMore: () => ref.read(homeProvider.notifier).loadMore(), + onCheckPreload: (index) => + ref.read(homeProvider.notifier).checkPreload(index), + onToggleLike: (id) => + ref.read(homeProvider.notifier).toggleLike(id), + onToggleFavorite: (id) => + ref.read(homeProvider.notifier).toggleFavorite(id), + onToggleReadLater: (id) => + ref.read(homeProvider.notifier).toggleReadLater(id), + onMarkRead: (id) => + ref.read(homeProvider.notifier).markRead(id), + onRefresh: () => ref.read(homeProvider.notifier).refresh(), + onRefreshSentenceList: () => + ref.read(homeProvider.notifier).refreshSentenceList(), + onSlidableOpened: _gestureController.onSlidableOpened, + onSlidableClosed: _gestureController.onSlidableClosed, + ), + + const SliverToBoxAdapter(child: SizedBox(height: 140)), + ], + ), ), ), ), ), ); - - return scaffold; } void _setupStateListeners() { diff --git a/lib/features/home/presentation/widgets/home_app_bar_section.dart b/lib/features/home/presentation/widgets/home_app_bar_section.dart new file mode 100644 index 00000000..3ae069eb --- /dev/null +++ b/lib/features/home/presentation/widgets/home_app_bar_section.dart @@ -0,0 +1,163 @@ +// ============================================================ +// 闲言APP — 首页AppBar区域 +// 创建时间: 2026-06-15 +// 更新时间: 2026-06-15 +// 作用: 从HomePage中提取的AppBar区域组件,包含角色精灵、标题、日期和操作按钮 +// 上次更新: 初始创建,从HomePage拆分出来 +// ============================================================ + +import 'package:flutter/cupertino.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:heroine/heroine.dart'; + +import '../../../../core/constants/character_name.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/router/app_routes.dart'; +import '../../../../core/router/app_nav_extension.dart'; +import '../../../../core/utils/platform/platform_utils.dart' as pu; +import '../../../../core/utils/platform/platform_feature_guard.dart'; +import '../../../../core/utils/ui/interaction_animations.dart'; +import '../../../../shared/widgets/animation/appbar_character_sprite.dart'; +import '../../../../shared/widgets/display/appbar_date_display.dart'; +import '../../../../shared/widgets/animation/character_tip_bubble.dart'; +import '../../../../features/settings/providers/theme_settings_provider.dart'; +import '../../providers/character_tips_provider.dart'; +import '../../providers/character_mood_provider.dart'; + +/// 首页AppBar区域组件 +/// +/// 包含角色精灵、应用标题、日期显示和操作按钮。 +/// 从HomePage中提取以降低组件复杂度。 +class HomeAppBarSection extends ConsumerWidget { + /// 角色精灵的GlobalKey,用于外部控制角色表情 + final GlobalKey characterKey; + + /// 角色ID + final String characterId; + + /// 日期显示点击回调 + final VoidCallback onDateTap; + + const HomeAppBarSection({ + super.key, + required this.characterKey, + required this.characterId, + required this.onDateTap, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ext = AppTheme.ext(context); + + return SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: 4, + ), + child: Row( + children: [ + // ── 角色精灵 + 标题 ── + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + AppBarCharacterSprite( + key: characterKey, + characterId: characterId, + animationIntensity: ref.watch( + themeSettingsProvider.select( + (s) => + s.animationIntensity.durationMultiplier, + ), + ), + mood: ref.watch( + characterMoodProvider.select((s) => s.mood), + ), + onTap: () { + ref + .read(characterTipsProvider.notifier) + .generateTip(characterId); + }, + onDoubleTap: () { + ref + .read(characterTipsProvider.notifier) + .generateTip(characterId); + }, + ), + Positioned( + left: 0, + top: 52, + child: CharacterTipBubble(characterId: characterId), + ), + ], + ), + const SizedBox(width: AppSpacing.sm), + GestureDetector( + onTap: () => characterKey.currentState?.lookAtTitle(), + child: Text( + CharacterName.appBarTitle, + style: AppTypography.title1.copyWith( + color: ext.textPrimary, + ), + ), + ), + ], + ), + const Spacer(), + // ── 日期显示 ── + AppBarDateDisplay(onTap: onDateTap), + const Spacer(), + // ── 操作按钮 ── + _buildActionButton(ext, context), + ], + ), + ).safeFadeIn(duration: 300.ms), + ); + } + + /// 构建平台相关的操作按钮 + Widget _buildActionButton(AppThemeExtension ext, BuildContext context) { + if (pu.isIOS) { + return BounceButton( + onTap: () => context.appPush(AppRoutes.anonymousSubmit), + child: Container( + padding: const EdgeInsets.all(AppSpacing.sm), + decoration: BoxDecoration( + color: ext.bgSecondary, + borderRadius: AppRadius.fullBorder, + ), + child: Icon( + CupertinoIcons.pencil_ellipsis_rectangle, + size: 20, + color: ext.iconSecondary, + ), + ), + ); + } + return BounceButton( + onTap: () => context.appPush(AppRoutes.search), + child: Heroine( + tag: 'search-icon', + child: Container( + padding: const EdgeInsets.all(AppSpacing.sm), + decoration: BoxDecoration( + color: ext.bgSecondary, + borderRadius: AppRadius.fullBorder, + ), + child: Icon( + CupertinoIcons.search, + size: 20, + color: ext.iconSecondary, + ), + ), + ), + ); + } +} diff --git a/lib/features/home/presentation/widgets/home_system_state_monitor.dart b/lib/features/home/presentation/widgets/home_system_state_monitor.dart new file mode 100644 index 00000000..1301749b --- /dev/null +++ b/lib/features/home/presentation/widgets/home_system_state_monitor.dart @@ -0,0 +1,97 @@ +// ============================================================ +// 闲言APP — 首页系统状态监听组件 +// 创建时间: 2026-06-15 +// 更新时间: 2026-06-15 +// 作用: 监听电池低电量和TTS状态变化,驱动角色表情和提示 +// 上次更新: 初始创建,从HomePage中拆分出来 +// ============================================================ + +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/constants/character_expression.dart'; +import '../../../../core/services/audio/tts_service.dart'; +import '../../../../core/services/device/battery_info_service.dart'; +import '../../../../l10n/translations.dart'; +import '../../../../shared/widgets/animation/appbar_character_sprite.dart'; +import '../../providers/character_tips_provider.dart'; + +/// 首页系统状态监听组件 +/// +/// 负责监听电池低电量和TTS播放状态变化, +/// 驱动角色精灵表情变化和气泡提示。 +/// +/// 使用方式:包裹在需要响应系统状态的子组件外层。 +class HomeSystemStateMonitor extends ConsumerStatefulWidget { + /// 角色精灵的GlobalKey,用于触发表情变化 + final GlobalKey characterKey; + + /// 子组件 + final Widget child; + + const HomeSystemStateMonitor({ + super.key, + required this.characterKey, + required this.child, + }); + + @override + ConsumerState createState() => + _HomeSystemStateMonitorState(); +} + +class _HomeSystemStateMonitorState + extends ConsumerState { + StreamSubscription? _batterySubscription; + StreamSubscription? _ttsSubscription; + + @override + void initState() { + super.initState(); + BatteryInfoService.instance.init(); + + // ── 电池低电量监听 ── + _batterySubscription = + BatteryInfoService.instance.onBatteryChanged.listen((info) { + if (!mounted) return; + if (!info.isLow) return; + widget.characterKey.currentState?.triggerExpression( + CharacterExpression.worried, + ); + final t = ref.read(translationsProvider); + final message = + info.isCritical ? t.home.base.batteryCritical : t.home.base.batteryLow; + ref + .read(characterTipsProvider.notifier) + .showTip(TipsCategory.easterEgg, message); + }); + + // ── TTS播放状态监听 ── + _ttsSubscription = TtsService.instance.onStateChanged.listen((ttsState) { + if (!mounted) return; + if (ttsState == TtsState.speaking) { + widget.characterKey.currentState?.triggerExpression( + CharacterExpression.speaking, + ); + } else if (ttsState == TtsState.paused || ttsState == TtsState.idle) { + widget.characterKey.currentState?.triggerExpression( + CharacterExpression.smile, + ); + } + }); + } + + @override + void dispose() { + _batterySubscription?.cancel(); + _ttsSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/lib/features/home/providers/home_feed_mixin.dart b/lib/features/home/providers/home_feed_mixin.dart index 372b1ab8..34518cd8 100644 --- a/lib/features/home/providers/home_feed_mixin.dart +++ b/lib/features/home/providers/home_feed_mixin.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 首页Feed数据拉取Mixin /// 创建时间: 2026-05-12 -/// 更新时间: 2026-05-24 +/// 更新时间: 2026-06-15 /// 作用: 频道/每日推荐/列表/降级/缓存的拉取逻辑 -/// 上次更新: 新增_applyChannelOrder方法,加载频道时应用用户自定义排序 +/// 上次更新: 使用BoundedCollectionManager替代Set管理已见集合 /// ============================================================ import 'dart:convert'; @@ -14,6 +14,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/network/api_interceptor.dart'; import '../../../core/storage/kv_storage.dart'; import '../../../core/storage/database/app_database.dart'; +import '../../../core/utils/data/bounded_collection_manager.dart'; import '../../../core/utils/logger.dart'; import '../../../core/services/data/home_widget_service.dart'; import '../../../editor/services/core/hitokoto_service.dart'; @@ -32,10 +33,9 @@ mixin HomeFeedMixin on Notifier { set allChannels(List value); Map> get categoryCache; - Set get allSeenIds; - Set get allSeenTexts; + BoundedCollectionManager get allSeenIds; + BoundedCollectionManager get allSeenTexts; bool get deduplicateContent; - int get maxSeenSize; int get maxCategoryCacheSize; int get currentPage; @@ -56,7 +56,7 @@ mixin HomeFeedMixin on Notifier { List _buildSeenHashList() { if (!deduplicateContent) return []; - return allSeenTexts + return allSeenTexts.toList() .where((t) => t.isNotEmpty) .map((t) => t.length >= 8 ? t.substring(0, 8) : t) .toList(); @@ -114,12 +114,10 @@ mixin HomeFeedMixin on Notifier { if (unique.isEmpty) { Log.w('fetchRefreshSentences: 去重后为空, 使用原始列表'); await saveToDb(newSentences); - if (allSeenIds.length >= maxSeenSize) allSeenIds.clear(); allSeenIds.addAll(newSentences.map((s) => s.id)); if (deduplicateContent) { for (final s in newSentences) { if (s.text.isNotEmpty) { - if (allSeenTexts.length >= maxSeenSize) allSeenTexts.clear(); allSeenTexts.add(s.text.trim()); } } @@ -139,12 +137,10 @@ mixin HomeFeedMixin on Notifier { } await saveToDb(unique); - if (allSeenIds.length >= maxSeenSize) allSeenIds.clear(); allSeenIds.addAll(unique.map((s) => s.id)); if (deduplicateContent) { for (final s in unique) { if (s.text.isNotEmpty) { - if (allSeenTexts.length >= maxSeenSize) allSeenTexts.clear(); allSeenTexts.add(s.text.trim()); } } @@ -231,10 +227,8 @@ mixin HomeFeedMixin on Notifier { categoryCache.clear(); for (final s in state.sentences) { - if (allSeenIds.length >= maxSeenSize) allSeenIds.clear(); allSeenIds.add(s.id); if (deduplicateContent && s.text.isNotEmpty) { - if (allSeenTexts.length >= maxSeenSize) allSeenTexts.clear(); allSeenTexts.add(s.text.trim()); } } @@ -676,12 +670,10 @@ mixin HomeFeedMixin on Notifier { .toList(); if (fallback.isNotEmpty) { await saveToDb(fallback); - if (allSeenIds.length >= maxSeenSize) allSeenIds.clear(); allSeenIds.addAll(fallback.map((s) => s.id)); if (deduplicateContent) { for (final s in fallback) { if (s.text.isNotEmpty) { - if (allSeenTexts.length >= maxSeenSize) allSeenTexts.clear(); allSeenTexts.add(s.text.trim()); } } @@ -736,12 +728,10 @@ mixin HomeFeedMixin on Notifier { await saveToDb(unique); - if (allSeenIds.length >= maxSeenSize) allSeenIds.clear(); allSeenIds.addAll(unique.map((s) => s.id)); if (deduplicateContent) { for (final s in unique) { if (s.text.isNotEmpty) { - if (allSeenTexts.length >= maxSeenSize) allSeenTexts.clear(); allSeenTexts.add(s.text.trim()); } } @@ -828,10 +818,8 @@ mixin HomeFeedMixin on Notifier { if (cached.isNotEmpty) { final sentences = cached.map(HomeSentence.fromDb).toList(); for (final s in sentences) { - if (allSeenIds.length >= maxSeenSize) allSeenIds.clear(); allSeenIds.add(s.id); if (deduplicateContent && s.text.isNotEmpty) { - if (allSeenTexts.length >= maxSeenSize) allSeenTexts.clear(); allSeenTexts.add(s.text.trim()); } } @@ -868,7 +856,6 @@ mixin HomeFeedMixin on Notifier { final sentences = rows.map(HomeSentence.fromImported).toList(); for (final s in sentences) { - if (allSeenIds.length >= maxSeenSize) allSeenIds.clear(); allSeenIds.add(s.id); } diff --git a/lib/features/home/providers/home_provider.dart b/lib/features/home/providers/home_provider.dart index cf6ff040..983ba2db 100644 --- a/lib/features/home/providers/home_provider.dart +++ b/lib/features/home/providers/home_provider.dart @@ -1,9 +1,9 @@ /// ============================================================ /// 闲言APP — 首页 Provider /// 创建时间: 2026-04-20 -/// 更新时间: 2026-05-30 +/// 更新时间: 2026-06-15 /// 作用: 首页状态管理 — Feed API + Drift 持久化 + 服务端互动 + 本地优先 -/// 上次更新: 使用SafeNotifierInit统一异常保护 +/// 上次更新: 使用BoundedCollectionManager消除重复的有界集合管理逻辑 /// ============================================================ export 'home_sentence_model.dart'; @@ -16,6 +16,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/storage/kv_storage.dart'; import '../../../core/storage/database/app_database.dart'; +import '../../../core/utils/data/bounded_collection_manager.dart'; import '../../../core/utils/logger.dart' show Log, LogCategory; import '../../../core/utils/safe_init_mixin.dart'; import '../../../core/utils/platform/platform_utils.dart' as pu; @@ -31,6 +32,8 @@ class HomeNotifier extends Notifier with HomeInteractionMixin, HomeFeedMixin, SafeNotifierInit { @override HomeState build() { + _idsManager = BoundedCollectionManager(maxSize: _maxSeenSize); + _textsManager = BoundedCollectionManager(maxSize: _maxSeenSize); ref.onDispose(_onDispose); if (pu.isOhos) Log.i('🟢 [OHOS] HomeNotifier.build() 执行', null, null, LogCategory.provider); safeNotifierInit(() async { @@ -58,8 +61,8 @@ class HomeNotifier extends Notifier final Map> _categoryCache = {}; List _allChannels = []; bool _isRefreshingDaily = false; - final Set _allSeenIds = {}; - final Set _allSeenTexts = {}; + late final BoundedCollectionManager _idsManager; + late final BoundedCollectionManager _textsManager; bool _deduplicateContent = true; int _categorySwitchCount = 0; bool _pendingLoadMore = false; @@ -84,18 +87,14 @@ class HomeNotifier extends Notifier Map> get categoryCache => _categoryCache; @override - Set get allSeenIds => _allSeenIds; + BoundedCollectionManager get allSeenIds => _idsManager; @override - Set get allSeenTexts => _allSeenTexts; + BoundedCollectionManager get allSeenTexts => _textsManager; @override bool get deduplicateContent => _deduplicateContent; - @override - int get maxSeenSize => _maxSeenSize; - - @override int get maxCategoryCacheSize => _maxCategoryCacheSize; @override @@ -127,6 +126,16 @@ class HomeNotifier extends Notifier HomeNotifier(); + /// 将当前句子列表的ID和文本添加到已见集合(自动处理容量限制) + void _updateSeenCollections(List sentences) { + for (final s in sentences) { + _idsManager.add(s.id); + if (_deduplicateContent && s.text.isNotEmpty) { + _textsManager.add(s.text.trim()); + } + } + } + void _onDispose() { markDisposed(); _connectivitySub?.cancel(); @@ -233,14 +242,7 @@ class HomeNotifier extends Notifier Future refresh() async { final key = cacheKey; _categoryCache.remove(key); - for (final s in state.sentences) { - if (_allSeenIds.length >= _maxSeenSize) _allSeenIds.clear(); - _allSeenIds.add(s.id); - if (_deduplicateContent && s.text.isNotEmpty) { - if (_allSeenTexts.length >= _maxSeenSize) _allSeenTexts.clear(); - _allSeenTexts.add(s.text.trim()); - } - } + _updateSeenCollections(state.sentences); final savedSentences = List.from(state.sentences); state = state.copyWith( isLoading: true, @@ -273,14 +275,7 @@ class HomeNotifier extends Notifier Future refreshSentenceList() async { final key = cacheKey; _categoryCache.remove(key); - for (final s in state.sentences) { - if (_allSeenIds.length >= _maxSeenSize) _allSeenIds.clear(); - _allSeenIds.add(s.id); - if (_deduplicateContent && s.text.isNotEmpty) { - if (_allSeenTexts.length >= _maxSeenSize) _allSeenTexts.clear(); - _allSeenTexts.add(s.text.trim()); - } - } + _updateSeenCollections(state.sentences); state = state.copyWith( isLoading: true, isForceLoading: true, @@ -345,14 +340,7 @@ class HomeNotifier extends Notifier _categorySwitchCount++; _currentPage = 1; _lastFeedId = null; - for (final s in state.sentences) { - if (_allSeenIds.length >= _maxSeenSize) _allSeenIds.clear(); - _allSeenIds.add(s.id); - if (_deduplicateContent && s.text.isNotEmpty) { - if (_allSeenTexts.length >= _maxSeenSize) _allSeenTexts.clear(); - _allSeenTexts.add(s.text.trim()); - } - } + _updateSeenCollections(state.sentences); if (cached != null && cached.isNotEmpty) { // 有缓存:先显示缓存数据,后台静默刷新 @@ -412,14 +400,7 @@ class HomeNotifier extends Notifier Future changeSort(String sort) async { if (sort == state.currentSort) return; - for (final s in state.sentences) { - if (_allSeenIds.length >= _maxSeenSize) _allSeenIds.clear(); - _allSeenIds.add(s.id); - if (_deduplicateContent && s.text.isNotEmpty) { - if (_allSeenTexts.length >= _maxSeenSize) _allSeenTexts.clear(); - _allSeenTexts.add(s.text.trim()); - } - } + _updateSeenCollections(state.sentences); state = state.copyWith(currentSort: sort, isLoading: true); _currentPage = 1; _lastFeedId = null; @@ -521,7 +502,7 @@ class HomeNotifier extends Notifier void setDeduplicateContent(bool enabled) { _deduplicateContent = enabled; if (!enabled) { - _allSeenTexts.clear(); + _textsManager.clear(); } try { KvStorage.setString('source_deduplicate_content', enabled.toString()); diff --git a/lib/features/home/services/cache_service.dart b/lib/features/home/services/cache_service.dart index 1c8117a0..f60a5f86 100644 --- a/lib/features/home/services/cache_service.dart +++ b/lib/features/home/services/cache_service.dart @@ -9,7 +9,7 @@ import 'dart:io'; import 'package:drift/drift.dart'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:sqflite/sqflite.dart' show Sqflite; import '../../../core/storage/kv_storage.dart'; @@ -261,9 +261,9 @@ class CacheService { if (await trashDir.exists()) { await trashDir.delete(recursive: true); } - Log.i('CacheService: 全部聊天数据已清除'); + Log.i('CacheService: 全部会话数据已清除'); } catch (e) { - Log.e('CacheService: 聊天数据清除失败', e); + Log.e('CacheService: 会话数据清除失败', e); } } @@ -393,7 +393,7 @@ class CacheService { if (value is String) { size += value.length; } else if (value is Map) { - size += value.length * 64; + size += (value.length * 64).toInt(); } } return size; diff --git a/lib/features/home/services/offline_manager.dart b/lib/features/home/services/offline_manager.dart index 32560d81..10ffd181 100644 --- a/lib/features/home/services/offline_manager.dart +++ b/lib/features/home/services/offline_manager.dart @@ -12,7 +12,7 @@ import 'dart:convert'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:drift/drift.dart' show Value; import 'package:flutter/foundation.dart'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import '../../../core/storage/kv_storage.dart'; import '../../../core/storage/cache_config.dart'; diff --git a/lib/features/profile/presentation/about_page.dart b/lib/features/profile/presentation/about_page.dart index d08784c4..78567459 100644 --- a/lib/features/profile/presentation/about_page.dart +++ b/lib/features/profile/presentation/about_page.dart @@ -10,6 +10,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart' show Divider; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:xianyan/core/router/app_nav_extension.dart'; import 'package:xianyan/core/router/app_routes.dart'; import 'package:xianyan/core/utils/platform/platform_utils.dart' as pu; @@ -367,8 +368,44 @@ class _FeedbackSection extends ConsumerWidget { ); } - void _onRateApp(BuildContext context, T t) { - AppToast.showInfo('未找到应用商店 🏪'); + void _onRateApp(BuildContext context, T t) async { + try { + // iOS App Store + if (pu.isIOS) { + const appId = '6737492298'; + final uri = Uri.parse('https://apps.apple.com/app/id$appId'); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + return; + } + } + // 鸿蒙应用市场 + if (pu.isOhos) { + final uri = Uri.parse('https://appgallery.huawei.com/app/C108129465'); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + return; + } + } + // Android Google Play + if (pu.isAndroid) { + final uri = Uri.parse('market://details?id=apps.xy.xianyan'); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + return; + } + final webUri = Uri.parse( + 'https://play.google.com/store/apps/details?id=apps.xy.xianyan', + ); + if (await canLaunchUrl(webUri)) { + await launchUrl(webUri, mode: LaunchMode.externalApplication); + return; + } + } + if (context.mounted) AppToast.showInfo(t.about.rateAppMenuDesc); + } catch (e) { + if (context.mounted) AppToast.showInfo(t.about.rateAppMenuDesc); + } } } diff --git a/lib/features/profile/presentation/profile_page.dart b/lib/features/profile/presentation/profile_page.dart index bf2c0239..4192f60a 100644 --- a/lib/features/profile/presentation/profile_page.dart +++ b/lib/features/profile/presentation/profile_page.dart @@ -6,14 +6,13 @@ /// 上次更新: ext参数内部化、Mixin抽取、下拉刷新、骨架屏、评分接入、退出逻辑迁移 /// ============================================================ -// import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:custom_refresh_indicator/custom_refresh_indicator.dart'; -// import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_spacing.dart'; @@ -153,37 +152,44 @@ class _ProfilePageState extends ConsumerState Future _launchAppStore() async { final t = ref.read(translationsProvider); - // TODO: 后续恢复应用商店跳转逻辑 - AppToast.showInfo(t.profile.appStoreNotFound); - return; - // // iOS App Store - // if (Platform.isIOS) { - // const appId = '6737492298'; - // final uri = Uri.parse('https://apps.apple.com/app/id$appId'); - // if (await canLaunchUrl(uri)) { - // await launchUrl(uri, mode: LaunchMode.externalApplication); - // return; - // } - // } - // // Android Google Play - // if (Platform.isAndroid) { - // final uri = Uri.parse( - // 'market://details?id=apps.xy.xianyan', - // ); - // if (await canLaunchUrl(uri)) { - // await launchUrl(uri, mode: LaunchMode.externalApplication); - // return; - // } - // // 降级:打开网页版 - // final webUri = Uri.parse( - // 'https://play.google.com/store/apps/details?id=apps.xy.xianyan', - // ); - // if (await canLaunchUrl(webUri)) { - // await launchUrl(webUri, mode: LaunchMode.externalApplication); - // return; - // } - // } - // AppToast.showInfo(t.profile.appStoreNotFound); + try { + // iOS App Store + if (pu.isIOS) { + const appId = '6737492298'; + final uri = Uri.parse('https://apps.apple.com/app/id$appId'); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + return; + } + } + // 鸿蒙应用市场 + if (pu.isOhos) { + final uri = Uri.parse('https://appgallery.huawei.com/app/C108129465'); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + return; + } + } + // Android Google Play + if (pu.isAndroid) { + final uri = Uri.parse('market://details?id=apps.xy.xianyan'); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + return; + } + // 降级:打开网页版 + final webUri = Uri.parse( + 'https://play.google.com/store/apps/details?id=apps.xy.xianyan', + ); + if (await canLaunchUrl(webUri)) { + await launchUrl(webUri, mode: LaunchMode.externalApplication); + return; + } + } + AppToast.showInfo(t.profile.appStoreNotFound); + } catch (e) { + AppToast.showInfo(t.profile.appStoreNotFound); + } } // ============================================================ diff --git a/lib/features/settings/presentation/data_management_backup_mixin.dart b/lib/features/settings/presentation/data_management_backup_mixin.dart index b1f85807..3ca03cfe 100644 --- a/lib/features/settings/presentation/data_management_backup_mixin.dart +++ b/lib/features/settings/presentation/data_management_backup_mixin.dart @@ -13,7 +13,7 @@ import 'package:archive/archive.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import '../../../core/services/data/backup_service.dart'; import '../../../core/storage/database/app_database.dart'; diff --git a/lib/features/settings/presentation/data_management_export_mixin.dart b/lib/features/settings/presentation/data_management_export_mixin.dart index efcc53ba..d2bf36e8 100644 --- a/lib/features/settings/presentation/data_management_export_mixin.dart +++ b/lib/features/settings/presentation/data_management_export_mixin.dart @@ -17,7 +17,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:share_plus/share_plus.dart' as share_plus; import '../../../core/storage/database/app_database.dart'; diff --git a/lib/features/settings/presentation/experimental_features_page.dart b/lib/features/settings/presentation/experimental_features_page.dart index b1ba1d7c..e80a3131 100644 --- a/lib/features/settings/presentation/experimental_features_page.dart +++ b/lib/features/settings/presentation/experimental_features_page.dart @@ -14,6 +14,7 @@ import '../../../shared/widgets/feedback/app_toast.dart'; import '../../../core/services/feature/feature_flag_provider.dart'; import '../../../core/services/feature/feature_flag_service.dart'; +import '../../../core/services/form/form_collect_service.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_typography.dart'; @@ -90,12 +91,51 @@ class _ExperimentalFeaturesPageState children: [_buildFeaturesTab(ext, t), _buildIssuesTab(ext, t)], ), ), + // 底部问卷按钮 + Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.md, AppSpacing.sm, AppSpacing.md, AppSpacing.md, + ), + child: SizedBox( + width: double.infinity, + child: CupertinoButton( + color: ext.accent, + borderRadius: BorderRadius.circular(10), + padding: const EdgeInsets.symmetric(vertical: 14), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(CupertinoIcons.question_circle_fill, size: 18, color: ext.textOnAccent), + const SizedBox(width: 6), + Text( + '填写问卷', + style: AppTypography.subhead.copyWith( + color: ext.textOnAccent, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + onPressed: () => _showQuestionnaire(ext, t), + ), + ), + ), ], ), ), ); } + // ---- 问卷 ---- + + /// 弹出问卷Sheet + void _showQuestionnaire(AppThemeExtension ext, T t) { + showCupertinoModalPopup( + context: context, + builder: (_) => _QuestionnaireSheet(ext: ext, t: t), + ); + } + // ---- 分段控制器 ---- Widget _buildSegmentedControl(AppThemeExtension ext, T t) { @@ -791,3 +831,198 @@ class _IssueCard extends StatelessWidget { ); } } + +// ---- Beta问卷Sheet ---- + +/// Beta问卷Sheet +class _QuestionnaireSheet extends StatefulWidget { + final AppThemeExtension ext; + final T t; + const _QuestionnaireSheet({required this.ext, required this.t}); + + @override + State<_QuestionnaireSheet> createState() => _QuestionnaireSheetState(); +} + +class _QuestionnaireSheetState extends State<_QuestionnaireSheet> { + int _step = 0; // 0-3: 问题1-4, -1: 不符合, 4: 完成 + final _emailController = TextEditingController(); + bool _isSubmitting = false; + + @override + void dispose() { + _emailController.dispose(); + super.dispose(); + } + + void _answer(bool yes) { + if (_step == 0) { + // 问题1: 了解Google Play + if (!yes) { setState(() => _step = -1); return; } + setState(() => _step = 1); + } else if (_step == 1) { + // 问题2: 有GMS设备 + if (!yes) { setState(() => _step = -1); return; } + setState(() => _step = 2); + } else if (_step == 2) { + // 问题3: 愿意参与内测 + if (!yes) { setState(() => _step = -1); return; } + setState(() => _step = 3); + } + } + + Future _submitEmail() async { + final email = _emailController.text.trim(); + if (email.isEmpty || !email.contains('@') || !email.contains('gmail')) { + AppToast.showWarning('请输入有效的Gmail邮箱'); + return; + } + setState(() => _isSubmitting = true); + try { + final ok = await FormCollectService.instance.submit( + email: email, + source: FormCollectSource.betaQuestionnaire, + ); + if (ok) { + setState(() { _step = 4; _isSubmitting = false; }); + } else { + AppToast.showError('提交失败,请稍后重试'); + setState(() => _isSubmitting = false); + } + } catch (e) { + AppToast.showError('提交失败'); + setState(() => _isSubmitting = false); + } + } + + @override + Widget build(BuildContext context) { + final ext = widget.ext; + return CupertinoPopupSurface( + child: Container( + constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.6), + padding: const EdgeInsets.fromLTRB(24, 16, 24, 32), + decoration: BoxDecoration( + color: ext.bgCard, + borderRadius: const BorderRadius.vertical(top: Radius.circular(14)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 拖拽指示器 + Container( + width: 36, height: 5, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: ext.textHint.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(3), + ), + ), + if (_step >= 0 && _step <= 3) ...[ + // 进度 + Text('${_step + 1}/4', style: AppTypography.caption1.copyWith(color: ext.textHint)), + const SizedBox(height: 12), + // 问题 + Text( + _questions[_step], + style: AppTypography.headline.copyWith(color: ext.textPrimary), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + if (_step < 3) ...[ + // 是/否按钮 + Row( + children: [ + Expanded( + child: CupertinoButton( + color: ext.accent, + borderRadius: BorderRadius.circular(10), + child: Text('是', style: TextStyle(color: ext.textOnAccent, fontWeight: FontWeight.w600)), + onPressed: () => _answer(true), + ), + ), + const SizedBox(width: 12), + Expanded( + child: CupertinoButton( + color: ext.bgElevated, + borderRadius: BorderRadius.circular(10), + child: Text('否', style: TextStyle(color: ext.textSecondary)), + onPressed: () => _answer(false), + ), + ), + ], + ), + ] else ...[ + // 问题4: Gmail输入 + CupertinoTextField( + controller: _emailController, + placeholder: '输入Gmail邮箱', + keyboardType: TextInputType.emailAddress, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: ext.bgElevated, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: ext.dividerOnCard), + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: CupertinoButton( + color: ext.accent, + borderRadius: BorderRadius.circular(10), + child: _isSubmitting + ? CupertinoActivityIndicator(color: ext.textOnAccent) + : Text('提交', style: TextStyle(color: ext.textOnAccent, fontWeight: FontWeight.w600)), + onPressed: _isSubmitting ? null : _submitEmail, + ), + ), + ], + ] else if (_step == -1) ...[ + // 不符合条件 + Icon(CupertinoIcons.info_circle, size: 48, color: ext.textHint), + const SizedBox(height: 12), + Text('感谢您的参与', style: AppTypography.headline.copyWith(color: ext.textPrimary)), + const SizedBox(height: 8), + Text('很遗憾,您暂时不符合内测条件。', style: AppTypography.subhead.copyWith(color: ext.textSecondary), textAlign: TextAlign.center), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: CupertinoButton( + color: ext.bgElevated, + borderRadius: BorderRadius.circular(10), + child: Text('关闭', style: TextStyle(color: ext.textSecondary)), + onPressed: () => Navigator.pop(context), + ), + ), + ] else ...[ + // 完成 + Icon(CupertinoIcons.checkmark_circle_fill, size: 48, color: ext.successColor), + const SizedBox(height: 12), + Text('提交成功!', style: AppTypography.headline.copyWith(color: ext.textPrimary)), + const SizedBox(height: 8), + Text('审核通过后,您将获取闲言APP的GMS版内测资格。', style: AppTypography.subhead.copyWith(color: ext.textSecondary), textAlign: TextAlign.center), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: CupertinoButton( + color: ext.accent, + borderRadius: BorderRadius.circular(10), + child: Text('完成', style: TextStyle(color: ext.textOnAccent, fontWeight: FontWeight.w600)), + onPressed: () => Navigator.pop(context), + ), + ), + ], + ], + ), + ), + ); + } + + static const _questions = [ + '您是否了解Google Play?', + '您是否有支持GMS(谷歌框架)的设备?', + '您是否愿意参与闲言APP的内测GMS版?', + '填写你的Gmail邮箱,审核通过后,获取闲言APP的GMS版内测资格', + ]; +} diff --git a/lib/features/settings/presentation/more_settings_page.dart b/lib/features/settings/presentation/more_settings_page.dart index 371f928b..43496c96 100644 --- a/lib/features/settings/presentation/more_settings_page.dart +++ b/lib/features/settings/presentation/more_settings_page.dart @@ -13,7 +13,7 @@ import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart' show Divider; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; diff --git a/lib/features/settings/services/cache_clean_logger.dart b/lib/features/settings/services/cache_clean_logger.dart index 8858cca6..a8ddd31e 100644 --- a/lib/features/settings/services/cache_clean_logger.dart +++ b/lib/features/settings/services/cache_clean_logger.dart @@ -8,7 +8,7 @@ import 'dart:convert'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import '../../../core/storage/kv_storage.dart'; import '../../../core/utils/logger.dart'; diff --git a/lib/features/settings/services/settings_change_logger.dart b/lib/features/settings/services/settings_change_logger.dart index 2c3cfafb..b033867d 100644 --- a/lib/features/settings/services/settings_change_logger.dart +++ b/lib/features/settings/services/settings_change_logger.dart @@ -8,7 +8,7 @@ import 'dart:convert'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import '../../../core/utils/logger.dart'; diff --git a/lib/features/template/services/wallpaper_favorite_service.dart b/lib/features/template/services/wallpaper_favorite_service.dart index c1f40233..2d24a103 100644 --- a/lib/features/template/services/wallpaper_favorite_service.dart +++ b/lib/features/template/services/wallpaper_favorite_service.dart @@ -8,7 +8,7 @@ import 'dart:convert'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import '../../../core/storage/kv_storage.dart'; import '../../../core/utils/logger.dart'; diff --git a/lib/features/template/services/wallpaper_health_service.dart b/lib/features/template/services/wallpaper_health_service.dart index af80be88..3f2cf0bc 100644 --- a/lib/features/template/services/wallpaper_health_service.dart +++ b/lib/features/template/services/wallpaper_health_service.dart @@ -8,7 +8,7 @@ import 'dart:convert'; -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import '../../../../core/utils/logger.dart'; import '../models/template_models.dart'; diff --git a/lib/features/tool_center/leisure/providers/leisure_bookmark_provider.dart b/lib/features/tool_center/leisure/providers/leisure_bookmark_provider.dart index 21dc9ed3..8964f64f 100644 --- a/lib/features/tool_center/leisure/providers/leisure_bookmark_provider.dart +++ b/lib/features/tool_center/leisure/providers/leisure_bookmark_provider.dart @@ -6,7 +6,7 @@ /// 上次更新: Phase E Hive Box直接存储替代KvStorage JSON序列化 /// ============================================================ -import 'package:hive_flutter/hive_flutter.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; diff --git a/lib/l10n/languages/ar.dart b/lib/l10n/languages/ar.dart index 2823389a..b69b53ae 100644 --- a/lib/l10n/languages/ar.dart +++ b/lib/l10n/languages/ar.dart @@ -1029,20 +1029,20 @@ const ar = T( feedCacheCount: 'ذاكرة التغذية', pendingSync: 'مزامنة معلقة', hiveStorage: 'تخزين Hive', - chatSessions: 'جلسات الدردشة', - chatAttachments: 'مرفقات الدردشة', - chatTrash: 'سلة محذوفات الدردشة', + chatSessions: 'الجلسات', + chatAttachments: 'مرفقات الجلسات', + chatTrash: 'سلة محذوفات الجلسات', transferRecords: 'سجل النقل', pairedDevices: 'أجهزة مقترنة', receivedFiles: 'ملفات مستلمة', readLater: 'اقرأ لاحقاً', cacheCleanup: 'تنظيف ذاكرة التخزين المؤقت', cleanExpiredCache: 'تنظيف ذاكرة التخزين المؤقت منتهية الصلاحية', - cleanChatTrash: 'تنظيف سلة محذوفات الدردشة', - cleanChatThumbnails: 'تنظيف صور مصغرة للدردشة', + cleanChatTrash: 'تنظيف سلة محذوفات الجلسات', + cleanChatThumbnails: 'تنظيف صور مصغرة للجلسات', clearAllCache2: 'مسح كل ذاكرة التخزين المؤقت', cleanTransferCache: 'تنظيف ذاكرة النقل', - clearAllChatData: 'مسح جميع بيانات الدردشة', + clearAllChatData: 'مسح جميع بيانات الجلسات', cleanReadlaterCache: 'تنظيف ذاكرة "اقرأ لاحقاً"', clearReadlaterData: 'مسح جميع بيانات "اقرأ لاحقاً"', cacheStrategy: 'استراتيجية ذاكرة التخزين المؤقت', @@ -1054,29 +1054,29 @@ const ar = T( cleaningCache: 'جاري تنظيف ذاكرة التخزين المؤقت...', itemsUnit: 'عناصر', piecesUnit: 'قطع', - cleaningChatTrash: 'تم تنظيف سلة محذوفات الدردشة', - cleaningChatThumbnails: 'تم تنظيف الصور المصغرة', + cleaningChatTrash: 'تم تنظيف سلة محذوفات الجلسات', + cleaningChatThumbnails: 'تم تنظيف الصور المصغرة للجلسات', cleaningTransferCache: 'تم تنظيف ذاكرة النقل', - clearingAllChatData: 'تم مسح جميع بيانات الدردشة', + clearingAllChatData: 'تم مسح جميع بيانات الجلسات', cleaningReadlaterCache: 'تم تنظيف ذاكرة "اقرأ لاحقاً"', clearingReadlaterData: 'تم مسح جميع بيانات "اقرأ لاحقاً"', allCacheCleared: 'تم مسح كل ذاكرة التخزين المؤقت', cleanFailed2: 'فشل التنظيف: {0}', - confirmCleanChatTrashTitle: 'تنظيف سلة محذوفات الدردشة', + confirmCleanChatTrashTitle: 'تنظيف سلة محذوفات الجلسات', confirmCleanChatTrashContent: 'سيتم حذف الرسائل والملفات في سلة المحذوفات التي تزيد عن 30 يوم بشكل دائم. لا يمكن التراجع عن هذا الإجراء.', - confirmCleanChatThumbnailsTitle: 'تنظيف الصور المصغرة للدردشة', + confirmCleanChatThumbnailsTitle: 'تنظيف الصور المصغرة للجلسات', confirmCleanChatThumbnailsContent: - 'سيتم تنظيف ذاكرة الصور المصغرة لصور الدردشة. لن يتم حذف الصور الأصلية.', + 'سيتم تنظيف ذاكرة الصور المصغرة لصور الجلسات. لن يتم حذف الصور الأصلية.', confirmClearAllCacheTitle: 'مسح كل ذاكرة التخزين المؤقت', confirmClearAllCacheContent: 'هل أنت متأكد من مسح جميع بيانات ذاكرة التخزين المؤقت؟ سيتم حذف المحتوى غير المتصل. لا يمكن التراجع عن هذا الإجراء.', confirmCleanTransferCacheTitle: 'تنظيف ذاكرة النقل', confirmCleanTransferCacheContent: 'سيتم تنظيف الصور المصغرة والملفات المؤقتة وسجلات النقل التي تزيد عن 30 يوم. لن يتم حذف الملفات المستلمة.', - confirmClearAllChatDataTitle: 'مسح جميع بيانات الدردشة', + confirmClearAllChatDataTitle: 'مسح جميع بيانات الجلسات', confirmClearAllChatDataContent: - 'سيتم حذف جميع جلسات الدردشة والرسائل والمرفقات وبيانات سلة المحذوفات. لا يمكن التراجع عن هذا الإجراء!', + 'سيتم حذف جميع الجلسات والرسائل والمرفقات وبيانات سلة المحذوفات. لا يمكن التراجع عن هذا الإجراء!', confirmCleanReadlaterCacheTitle: 'تنظيف ذاكرة "اقرأ لاحقاً"', confirmCleanReadlaterCacheContent: 'سيتم تنظيف الصور المصغرة والمرفقات والملفات المؤقتة للمزامنة لـ "اقرأ لاحقاً". لن يتم حذف سجلات الرسائل.', @@ -1091,7 +1091,7 @@ const ar = T( daysUnit2: 'أيام', cleanTrash: 'إفراغ سلة المحذوفات', trashSourceInfoTitle: 'مصادر سلة المحذوفات', - trashSourceInfoContent: 'العناصر في سلة المحذوفات تأتي من:\n\n💬 رسائل الدردشة — سجلات الدردشة المحذوفة\n📖 اقرأ لاحقاً — المقالات المحفوظة المحذوفة\n📁 نقل الملفات — سجلات النقل المحذوفة\n\nسيتم تنظيف هذه العناصر تلقائياً بعد فترة الاحتفاظ.', + trashSourceInfoContent: 'العناصر في سلة المحذوفات تأتي من:\n\n💬 رسائل الجلسات — سجلات الجلسات المحذوفة\n📖 اقرأ لاحقاً — المقالات المحفوظة المحذوفة\n📁 نقل الملفات — سجلات النقل المحذوفة\n\nسيتم تنظيف هذه العناصر تلقائياً بعد فترة الاحتفاظ.', undoCleanTrash: 'تراجع', cleanTrashCountdown: 'سيتم إفراغ سلة المحذوفات خلال {0} ثانية، اضغط للتراجع', ), @@ -2520,6 +2520,25 @@ const ar = T( statusReleased: 'تم الإصدار', comingSoon: 'قريباً', gotIt: 'فهمت', + // الاستبيان + questionnaireBtn: '📝 املأ الاستبيان', + questionnaireTitle: 'استبيان نسخة GMS التجريبية', + q1KnowGooglePlay: 'هل أنت على دراية بـ Google Play؟', + q2HasGmsDevice: 'هل لديك جهاز يدعم GMS (خدمات Google للجوال)؟', + q3WillingToBeta: 'هل ترغب في المشاركة في النسخة التجريبية من تطبيق Xianyan GMS؟', + q4EnterGmail: 'أدخل عنوان Gmail الخاص بك', + q4GmailHint: 'بعد الموافقة، ستحصل على حق الوصول إلى النسخة التجريبية من GMS', + qYes: 'نعم', + qNo: 'لا', + qSubmit: 'إرسال', + qNext: 'التالي', + qEndTitle: 'اكتمل الاستبيان', + qEndThanks: '🎉 شكراً لمشاركتك! سنراجع طلبك قريباً.', + qEndNotQualified: 'للأسف، لا تستوفي متطلبات النسخة التجريبية حالياً.', + qInvalidEmail: 'يرجى إدخال عنوان Gmail صالح', + qSubmitting: 'جارٍ الإرسال...', + qSubmitSuccess: '✅ تم الإرسال بنجاح', + qSubmitFailed: 'فشل الإرسال، يرجى المحاولة لاحقاً', ), submit: TSubmit( title: 'Anonymous Submit', diff --git a/lib/l10n/languages/bn.dart b/lib/l10n/languages/bn.dart index fbe21088..569870cc 100644 --- a/lib/l10n/languages/bn.dart +++ b/lib/l10n/languages/bn.dart @@ -1032,9 +1032,9 @@ const bn = T( feedCacheCount: 'ফিড ক্যাশে', pendingSync: 'সিঙ্ক অপেক্ষমাণ', hiveStorage: 'Hive স্টোরেজ', - chatSessions: 'চ্যাট সেশন', - chatAttachments: 'চ্যাট সংযুক্তি', - chatTrash: 'চ্যাট ট্র্যাশ', + chatSessions: 'সেশন', + chatAttachments: 'সেশন সংযুক্তি', + chatTrash: 'সেশন ট্র্যাশ', transferRecords: 'ট্রান্সফার রেকর্ড', pairedDevices: 'পেয়ার করা ডিভাইস', receivedFiles: 'প্রাপ্ত ফাইল', @@ -1045,7 +1045,7 @@ const bn = T( cleanChatThumbnails: 'চ্যাট থাম্বনেইল পরিষ্কার', clearAllCache2: 'সব ক্যাশে মুছুন', cleanTransferCache: 'ট্রান্সফার ক্যাশে পরিষ্কার', - clearAllChatData: 'সব চ্যাট ডেটা মুছুন', + clearAllChatData: 'সব সেশন ডেটা মুছুন', cleanReadlaterCache: 'পরে পড়ুন ক্যাশে পরিষ্কার', clearReadlaterData: 'সব পরে পড়ুন ডেটা মুছুন', cacheStrategy: 'ক্যাশে কৌশল', @@ -1057,29 +1057,29 @@ const bn = T( cleaningCache: 'ক্যাশে পরিষ্কার হচ্ছে...', itemsUnit: 'টি', piecesUnit: 'টি', - cleaningChatTrash: 'চ্যাট ট্র্যাশ পরিষ্কার হয়েছে', - cleaningChatThumbnails: 'চ্যাট থাম্বনেইল পরিষ্কার হয়েছে', + cleaningChatTrash: 'সেশন ট্র্যাশ পরিষ্কার হয়েছে', + cleaningChatThumbnails: 'সেশন থাম্বনেইল পরিষ্কার হয়েছে', cleaningTransferCache: 'ট্রান্সফার ক্যাশে পরিষ্কার হয়েছে', - clearingAllChatData: 'সব চ্যাট ডেটা মুছে ফেলা হয়েছে', + clearingAllChatData: 'সব সেশন ডেটা মুছে ফেলা হয়েছে', cleaningReadlaterCache: 'পরে পড়ুন ক্যাশে পরিষ্কার হয়েছে', clearingReadlaterData: 'সব পরে পড়ুন ডেটা মুছে ফেলা হয়েছে', allCacheCleared: 'সব ক্যাশে মুছে ফেলা হয়েছে', cleanFailed2: 'পরিষ্কার ব্যর্থ: {0}', - confirmCleanChatTrashTitle: 'চ্যাট ট্র্যাশ পরিষ্কার', + confirmCleanChatTrashTitle: 'সেশন ট্র্যাশ পরিষ্কার', confirmCleanChatTrashContent: '৩০ দিনের বেশি পুরনো ট্র্যাশের বার্তা ও ফাইল স্থায়ীভাবে মুছে যাবে, এটি পূর্বাবস্থায় ফেরানো যাবে না।', - confirmCleanChatThumbnailsTitle: 'চ্যাট থাম্বনেইল পরিষ্কার', + confirmCleanChatThumbnailsTitle: 'সেশন থাম্বনেইল পরিষ্কার', confirmCleanChatThumbnailsContent: - 'সব চ্যাট ছবির থাম্বনেইল ক্যাশে পরিষ্কার হবে, মূল ছবি মুছে যাবে না।', + 'সব সেশন ছবির থাম্বনেইল ক্যাশে পরিষ্কার হবে, মূল ছবি মুছে যাবে না।', confirmClearAllCacheTitle: 'সব ক্যাশে মুছুন', confirmClearAllCacheContent: 'আপনি কি নিশ্চিত সব ক্যাশে ডেটা মুছে ফেলতে চান? অফলাইন কন্টেন্ট মুছে যাবে, এটি পূর্বাবস্থায় ফেরানো যাবে না।', confirmCleanTransferCacheTitle: 'ট্রান্সফার ক্যাশে পরিষ্কার', confirmCleanTransferCacheContent: 'ট্রান্সফার থাম্বনেইল, অস্থায়ী ফাইল এবং ৩০ দিনের বেশি পুরনো রেকর্ড পরিষ্কার হবে। প্রাপ্ত ফাইল মুছে যাবে না।', - confirmClearAllChatDataTitle: 'সব চ্যাট ডেটা মুছুন', + confirmClearAllChatDataTitle: 'সব সেশন ডেটা মুছুন', confirmClearAllChatDataContent: - 'সব চ্যাট সেশন, বার্তা, সংযুক্তি এবং ট্র্যাশ ডেটা মুছে যাবে, এটি পূর্বাবস্থায় ফেরানো যাবে না!', + 'সব সেশন, বার্তা, সংযুক্তি এবং ট্র্যাশ ডেটা মুছে যাবে, এটি পূর্বাবস্থায় ফেরানো যাবে না!', confirmCleanReadlaterCacheTitle: 'পরে পড়ুন ক্যাশে পরিষ্কার', confirmCleanReadlaterCacheContent: 'পরে পড়ুন থাম্বনেইল, সংযুক্তি এবং সিঙ্ক অস্থায়ী ফাইল পরিষ্কার হবে, বার্তা রেকর্ড মুছে যাবে না।', @@ -1094,7 +1094,7 @@ const bn = T( daysUnit2: 'দিন', cleanTrash: 'ট্র্যাশ খালি করুন', trashSourceInfoTitle: 'ট্র্যাশ উৎস', - trashSourceInfoContent: 'ট্র্যাশের আইটেমগুলি এখান থেকে আসে:\n\n💬 চ্যাট বার্তা — মুছে ফেলা চ্যাট রেকর্ড\n📖 পরে পড়ুন — মুছে ফেলা সংরক্ষিত নিবন্ধ\n📁 ফাইল স্থানান্তর — মুছে ফেলা স্থানান্তর রেকর্ড\n\nএই আইটেমগুলি ধরে রাখার সময়ের পরে স্বয়ংক্রিয়ভাবে পরিষ্কার হবে।', + trashSourceInfoContent: 'ট্র্যাশের আইটেমগুলি এখান থেকে আসে:\n\n💬 সেশন বার্তা — মুছে ফেলা সেশন রেকর্ড\n📖 পরে পড়ুন — মুছে ফেলা সংরক্ষিত নিবন্ধ\n📁 ফাইল স্থানান্তর — মুছে ফেলা স্থানান্তর রেকর্ড\n\nএই আইটেমগুলি ধরে রাখার সময়ের পরে স্বয়ংক্রিয়ভাবে পরিষ্কার হবে।', undoCleanTrash: 'পূর্বাবস্থায় ফেরান', cleanTrashCountdown: '{0} সেকেন্ডে ট্র্যাশ খালি হবে, পূর্বাবস্থায় ফেরাতে ট্যাপ করুন', ), @@ -2536,6 +2536,25 @@ const bn = T( statusReleased: 'Released', comingSoon: 'Coming soon', gotIt: 'Got it', + // প্রশ্নাবলী + questionnaireBtn: '📝 প্রশ্নাবলী পূরণ করুন', + questionnaireTitle: 'GMS বিটা প্রশ্নাবলী', + q1KnowGooglePlay: 'আপনি কি Google Play সম্পর্কে জানেন?', + q2HasGmsDevice: 'আপনার কি GMS (Google Mobile Services) সমর্থিত ডিভাইস আছে?', + q3WillingToBeta: 'আপনি কি Xianyan APP GMS বিটায় অংশগ্রহণ করতে চান?', + q4EnterGmail: 'আপনার Gmail ঠিকানা লিখুন', + q4GmailHint: 'অনুমোদনের পরে, আপনি GMS বিটাতে অ্যাক্সেস পাবেন', + qYes: 'হ্যাঁ', + qNo: 'না', + qSubmit: 'জমা দিন', + qNext: 'পরবর্তী', + qEndTitle: 'প্রশ্নাবলী সম্পন্ন', + qEndThanks: '🎉 অংশগ্রহণের জন্য ধন্যবাদ! আমরা শীঘ্রই আপনার আবেদন পর্যালোচনা করব।', + qEndNotQualified: 'দুর্ভাগ্যবশত, আপনি বর্তমানে বিটা প্রয়োজনীয়তা পূরণ করেন না।', + qInvalidEmail: 'একটি বৈধ Gmail ঠিকানা লিখুন', + qSubmitting: 'জমা হচ্ছে...', + qSubmitSuccess: '✅ সফলভাবে জমা হয়েছে', + qSubmitFailed: 'জমা দিতে ব্যর্থ, পরে আবার চেষ্টা করুন', ), submit: TSubmit( title: 'Anonymous Submit', diff --git a/lib/l10n/languages/de.dart b/lib/l10n/languages/de.dart index aa76045c..52c972a9 100644 --- a/lib/l10n/languages/de.dart +++ b/lib/l10n/languages/de.dart @@ -1028,9 +1028,9 @@ const de = T( feedCacheCount: 'Feed-Cache', pendingSync: 'Ausstehende Synchronisation', hiveStorage: 'Hive-Speicher', - chatSessions: 'Chat-Sitzungen', - chatAttachments: 'Chat-Anhänge', - chatTrash: 'Chat-Papierkorb', + chatSessions: 'Sitzungen', + chatAttachments: 'Sitzungsanhänge', + chatTrash: 'Sitzungs-Papierkorb', transferRecords: 'Übertragungsprotokoll', pairedDevices: 'Gekoppelte Geräte', receivedFiles: 'Empfangene Dateien', @@ -1041,7 +1041,7 @@ const de = T( cleanChatThumbnails: 'Chat-Vorschaubilder bereinigen', clearAllCache2: 'Gesamten Cache löschen', cleanTransferCache: 'Übertragungs-Cache bereinigen', - clearAllChatData: 'Alle Chat-Daten löschen', + clearAllChatData: 'Alle Sitzungsdaten löschen', cleanReadlaterCache: 'Später-lesen-Cache bereinigen', clearReadlaterData: 'Alle Später-lesen-Daten löschen', cacheStrategy: 'Cache-Strategie', @@ -1053,24 +1053,24 @@ const de = T( cleaningCache: 'Cache wird bereinigt...', itemsUnit: 'Einträge', piecesUnit: 'Stück', - cleaningChatTrash: 'Chat-Papierkorb bereinigt', - cleaningChatThumbnails: 'Vorschaubilder bereinigt', + cleaningChatTrash: 'Sitzungs-Papierkorb bereinigt', + cleaningChatThumbnails: 'Sitzungs-Vorschaubilder bereinigt', cleaningTransferCache: 'Übertragungs-Cache bereinigt', - clearingAllChatData: 'Alle Chat-Daten gelöscht', + clearingAllChatData: 'Alle Sitzungsdaten gelöscht', cleaningReadlaterCache: 'Später-lesen-Cache bereinigt', clearingReadlaterData: 'Alle Später-lesen-Daten gelöscht', allCacheCleared: 'Gesamter Cache gelöscht', cleanFailed2: 'Bereinigung fehlgeschlagen: {0}', - confirmCleanChatTrashTitle: 'Chat-Papierkorb bereinigen', + confirmCleanChatTrashTitle: 'Sitzungs-Papierkorb bereinigen', confirmCleanChatTrashContent: 'Nachrichten und Dateien, die älter als 30 Tage sind, werden dauerhaft gelöscht. Dies kann nicht rückgängig gemacht werden.', - confirmCleanChatThumbnailsTitle: 'Vorschaubilder bereinigen', - confirmCleanChatThumbnailsContent: 'Alle Cache-Vorschaubilder von Chat-Bildern werden bereinigt. Originalbilder werden nicht gelöscht.', + confirmCleanChatThumbnailsTitle: 'Sitzungs-Vorschaubilder bereinigen', + confirmCleanChatThumbnailsContent: 'Alle Cache-Vorschaubilder von Sitzungsbildern werden bereinigt. Originalbilder werden nicht gelöscht.', confirmClearAllCacheTitle: 'Gesamten Cache löschen', confirmClearAllCacheContent: 'Möchten Sie alle Cachedaten wirklich löschen? Offline-Inhalte werden gelöscht. Dies kann nicht rückgängig gemacht werden.', confirmCleanTransferCacheTitle: 'Übertragungs-Cache bereinigen', confirmCleanTransferCacheContent: 'Vorschaubilder, temporäre Dateien und Übertragungsprotokolle, die älter als 30 Tage sind, werden bereinigt. Empfangene Dateien werden nicht gelöscht.', - confirmClearAllChatDataTitle: 'Alle Chat-Daten löschen', - confirmClearAllChatDataContent: 'Alle Chat-Sitzungen, Nachrichten, Anhänge und Papierkorbdaten werden gelöscht. Dies kann nicht rückgängig gemacht werden!', + confirmClearAllChatDataTitle: 'Alle Sitzungsdaten löschen', + confirmClearAllChatDataContent: 'Alle Sitzungen, Nachrichten, Anhänge und Papierkorbdaten werden gelöscht. Dies kann nicht rückgängig gemacht werden!', confirmCleanReadlaterCacheTitle: 'Später-lesen-Cache bereinigen', confirmCleanReadlaterCacheContent: 'Vorschaubilder, Anhänge und temporäre Sync-Dateien für "Später lesen" werden bereinigt. Nachrichtenverläufe werden nicht gelöscht.', confirmClearReadlaterDataTitle: 'Alle Später-lesen-Daten löschen', @@ -1083,7 +1083,7 @@ const de = T( daysUnit2: 'Tage', cleanTrash: 'Papierkorb leeren', trashSourceInfoTitle: 'Papierkorb-Quellen', - trashSourceInfoContent: 'Die Elemente im Papierkorb stammen aus:\n\n💬 Chat-Nachrichten — Gelöschte Chat-Aufzeichnungen\n📖 Später lesen — Gelöschte gespeicherte Artikel\n📁 Dateiübertragungen — Gelöschte Übertragungsaufzeichnungen\n\nDiese Elemente werden nach einer Aufbewahrungsfrist automatisch bereinigt.', + trashSourceInfoContent: 'Die Elemente im Papierkorb stammen aus:\n\n💬 Sitzungsnachrichten — Gelöschte Sitzungsaufzeichnungen\n📖 Später lesen — Gelöschte gespeicherte Artikel\n📁 Dateiübertragungen — Gelöschte Übertragungsaufzeichnungen\n\nDiese Elemente werden nach einer Aufbewahrungsfrist automatisch bereinigt.', undoCleanTrash: 'Rückgängig', cleanTrashCountdown: 'Papierkorb wird in {0}s geleert, tippen zum Rückgängigmachen', ), @@ -2534,6 +2534,25 @@ const de = T( statusReleased: 'Veröffentlicht', comingSoon: 'Demnächst', gotIt: 'Verstanden', + // Fragebogen + questionnaireBtn: '📝 Fragebogen ausfüllen', + questionnaireTitle: 'GMS-Beta-Fragebogen', + q1KnowGooglePlay: 'Sind Sie mit Google Play vertraut?', + q2HasGmsDevice: 'Haben Sie ein Gerät, das GMS (Google Mobile Services) unterstützt?', + q3WillingToBeta: 'Möchten Sie an der Xianyan APP GMS-Beta teilnehmen?', + q4EnterGmail: 'Geben Sie Ihre Gmail-Adresse ein', + q4GmailHint: 'Nach Genehmigung erhalten Sie Zugang zur GMS-Beta', + qYes: 'Ja', + qNo: 'Nein', + qSubmit: 'Absenden', + qNext: 'Weiter', + qEndTitle: 'Fragebogen abgeschlossen', + qEndThanks: '🎉 Vielen Dank für Ihre Teilnahme! Wir prüfen Ihre Bewerbung bald.', + qEndNotQualified: 'Leider erfüllen Sie derzeit nicht die Beta-Voraussetzungen.', + qInvalidEmail: 'Bitte geben Sie eine gültige Gmail-Adresse ein', + qSubmitting: 'Wird gesendet...', + qSubmitSuccess: '✅ Erfolgreich gesendet', + qSubmitFailed: 'Sendung fehlgeschlagen, bitte versuchen Sie es später erneut', ), submit: TSubmit( title: 'Anonymous Submit', diff --git a/lib/l10n/languages/en.dart b/lib/l10n/languages/en.dart index b1dd2412..4b602ea6 100644 --- a/lib/l10n/languages/en.dart +++ b/lib/l10n/languages/en.dart @@ -1038,9 +1038,9 @@ const en = T( feedCacheCount: 'Feed Cache', pendingSync: 'Pending Sync', hiveStorage: 'Hive Storage', - chatSessions: 'Chat Sessions', - chatAttachments: 'Chat Attachments', - chatTrash: 'Chat Trash', + chatSessions: 'Sessions', + chatAttachments: 'Session Attachments', + chatTrash: 'Session Trash', transferRecords: 'Transfer Records', pairedDevices: 'Paired Devices', receivedFiles: 'Received Files', @@ -1063,15 +1063,15 @@ const en = T( cleaningCache: 'Cleaning cache...', itemsUnit: 'items', piecesUnit: 'pieces', - cleaningChatTrash: 'Chat trash cleaned', - cleaningChatThumbnails: 'Chat thumbnails cleaned', + cleaningChatTrash: 'Session trash cleaned', + cleaningChatThumbnails: 'Session thumbnails cleaned', cleaningTransferCache: 'Transfer cache cleaned', clearingAllChatData: 'All chat data cleared', cleaningReadlaterCache: 'Read later cache cleaned', clearingReadlaterData: 'All read later data cleared', allCacheCleared: 'All cache cleared', cleanFailed2: 'Cleanup failed: {0}', - confirmCleanChatTrashTitle: 'Clean Chat Trash', + confirmCleanChatTrashTitle: 'Clean Session Trash', confirmCleanChatTrashContent: 'Permanently delete messages and files in trash older than 30 days. This cannot be undone.', confirmCleanChatThumbnailsTitle: 'Clean Chat Thumbnails', @@ -1083,9 +1083,9 @@ const en = T( confirmCleanTransferCacheTitle: 'Clean Transfer Cache', confirmCleanTransferCacheContent: 'Clean transfer thumbnails, temporary files, and transfer records older than 30 days. Received files will not be deleted.', - confirmClearAllChatDataTitle: 'Clear All Chat Data', + confirmClearAllChatDataTitle: 'Clear All Session Data', confirmClearAllChatDataContent: - 'Delete all chat sessions, messages, attachments, and trash data. This cannot be undone!', + 'Delete all sessions, messages, attachments, and trash data. This cannot be undone!', confirmCleanReadlaterCacheTitle: 'Clean Read Later Cache', confirmCleanReadlaterCacheContent: 'Clean read later thumbnails, attachments, and sync temp files. Message records will not be deleted.', @@ -1100,7 +1100,7 @@ const en = T( daysUnit2: 'days', cleanTrash: 'Clean Trash', trashSourceInfoTitle: 'Trash Sources', - trashSourceInfoContent: 'Items in the trash come from:\n\n💬 Chat Messages — Deleted chat records\n📖 Read Later — Deleted saved articles\n📁 File Transfers — Deleted transfer records\n\nThese items will be automatically cleaned after a retention period.', + trashSourceInfoContent: 'Items in the trash come from:\n\n💬 Session Messages — Deleted session records\n📖 Read Later — Deleted saved articles\n📁 File Transfers — Deleted transfer records\n\nThese items will be automatically cleaned after a retention period.', undoCleanTrash: 'Undo', cleanTrashCountdown: 'Trash will be emptied in {0}s, tap to undo', ), @@ -2584,6 +2584,25 @@ const en = T( // Common comingSoon: 'Coming soon', gotIt: 'Got it', + // Questionnaire + questionnaireBtn: '📝 Fill Questionnaire', + questionnaireTitle: 'GMS Beta Questionnaire', + q1KnowGooglePlay: 'Are you familiar with Google Play?', + q2HasGmsDevice: 'Do you have a device that supports GMS (Google Mobile Services)?', + q3WillingToBeta: 'Would you like to participate in the Xianyan APP GMS beta?', + q4EnterGmail: 'Enter your Gmail address', + q4GmailHint: 'Once approved, you\'ll get access to the GMS beta', + qYes: 'Yes', + qNo: 'No', + qSubmit: 'Submit', + qNext: 'Next', + qEndTitle: 'Questionnaire Complete', + qEndThanks: '🎉 Thank you for participating! We\'ll review your application soon.', + qEndNotQualified: 'Unfortunately, you don\'t meet the beta requirements at this time.', + qInvalidEmail: 'Please enter a valid Gmail address', + qSubmitting: 'Submitting...', + qSubmitSuccess: '✅ Submitted successfully', + qSubmitFailed: 'Submission failed, please try again later', ), submit: TSubmit( title: 'Anonymous Submit', diff --git a/lib/l10n/languages/es.dart b/lib/l10n/languages/es.dart index 7607c9e2..993a187a 100644 --- a/lib/l10n/languages/es.dart +++ b/lib/l10n/languages/es.dart @@ -1038,20 +1038,20 @@ const es = T( feedCacheCount: 'Caché de Feed', pendingSync: 'Sincronización pendiente', hiveStorage: 'Almacenamiento Hive', - chatSessions: 'Sesiones de chat', - chatAttachments: 'Adjuntos de chat', - chatTrash: 'Papelera de chat', + chatSessions: 'Sesiones', + chatAttachments: 'Adjuntos de sesión', + chatTrash: 'Papelera de sesión', transferRecords: 'Registros de transferencia', pairedDevices: 'Dispositivos emparejados', receivedFiles: 'Archivos recibidos', readLater: 'Leer después', cacheCleanup: 'Limpieza de caché', cleanExpiredCache: 'Limpiar caché expirado', - cleanChatTrash: 'Vaciar papelera de chat', - cleanChatThumbnails: 'Limpiar miniaturas de chat', + cleanChatTrash: 'Vaciar papelera de sesión', + cleanChatThumbnails: 'Limpiar miniaturas de sesión', clearAllCache2: 'Borrar toda la caché', cleanTransferCache: 'Limpiar caché de transferencia', - clearAllChatData: 'Borrar todos los datos del chat', + clearAllChatData: 'Borrar todos los datos de sesión', cleanReadlaterCache: 'Limpiar caché "Leer después"', clearReadlaterData: 'Borrar todos los datos "Leer después"', cacheStrategy: 'Estrategia de caché', @@ -1063,15 +1063,15 @@ const es = T( cleaningCache: 'Limpiando caché...', itemsUnit: 'elementos', piecesUnit: 'piezas', - cleaningChatTrash: 'Papelera de chat vaciada', - cleaningChatThumbnails: 'Miniaturas de chat limpiadas', + cleaningChatTrash: 'Papelera de sesión vaciada', + cleaningChatThumbnails: 'Miniaturas de sesión limpiadas', cleaningTransferCache: 'Caché de transferencia limpiado', - clearingAllChatData: 'Todos los datos del chat borrados', + clearingAllChatData: 'Todos los datos de sesión borrados', cleaningReadlaterCache: 'Caché "Leer después" limpiado', clearingReadlaterData: 'Todos los datos "Leer después" borrados', allCacheCleared: 'Toda la caché borrada', cleanFailed2: 'Limpieza fallida: {0}', - confirmCleanChatTrashTitle: 'Vaciar papelera de chat', + confirmCleanChatTrashTitle: 'Vaciar papelera de sesión', confirmCleanChatTrashContent: 'Se eliminarán permanentemente los mensajes y archivos en la papelera con más de 30 días. Esta operación no se puede deshacer.', confirmCleanChatThumbnailsTitle: 'Limpiar miniaturas de chat', confirmCleanChatThumbnailsContent: 'Se limpiará la caché de miniaturas de imágenes de chat. Las imágenes originales no se eliminarán.', @@ -1079,8 +1079,8 @@ const es = T( confirmClearAllCacheContent: '¿Estás seguro de que quieres borrar todos los datos en caché? El contenido sin conexión será eliminado. Esta operación no se puede deshacer.', confirmCleanTransferCacheTitle: 'Limpiar caché de transferencia', confirmCleanTransferCacheContent: 'Se limpiarán las miniaturas, archivos temporales y registros de transferencia con más de 30 días. Los archivos recibidos no se eliminarán.', - confirmClearAllChatDataTitle: 'Borrar todos los datos del chat', - confirmClearAllChatDataContent: 'Se eliminarán todas las sesiones de chat, mensajes, adjuntos y datos de la papelera. ¡Esta operación no se puede deshacer!', + confirmClearAllChatDataTitle: 'Borrar todos los datos de sesión', + confirmClearAllChatDataContent: 'Se eliminarán todas las sesiones, mensajes, adjuntos y datos de la papelera. ¡Esta operación no se puede deshacer!', confirmCleanReadlaterCacheTitle: 'Limpiar caché "Leer después"', confirmCleanReadlaterCacheContent: 'Se limpiarán las miniaturas, adjuntos y archivos temporales de sincronización de "Leer después". Los registros de mensajes no se eliminarán.', confirmClearReadlaterDataTitle: 'Borrar todos los datos "Leer después"', @@ -1093,7 +1093,7 @@ const es = T( daysUnit2: 'días', cleanTrash: 'Vaciar papelera', trashSourceInfoTitle: 'Fuentes de la papelera', - trashSourceInfoContent: 'Los elementos de la papelera provienen de:\n\n💬 Mensajes de chat — Registros de chat eliminados\n📖 Leer más tarde — Artículos guardados eliminados\n📁 Transferencias de archivos — Registros de transferencia eliminados\n\nEstos elementos se limpiarán automáticamente después de un período de retención.', + trashSourceInfoContent: 'Los elementos de la papelera provienen de:\n\n💬 Mensajes de sesión — Registros de sesión eliminados\n📖 Leer más tarde — Artículos guardados eliminados\n📁 Transferencias de archivos — Registros de transferencia eliminados\n\nEstos elementos se limpiarán automáticamente después de un período de retención.', undoCleanTrash: 'Deshacer', cleanTrashCountdown: 'La papelera se vaciará en {0}s, toca para deshacer', ), @@ -2548,6 +2548,25 @@ const es = T( statusReleased: 'Publicado', comingSoon: 'Próximamente', gotIt: 'Entendido', + // Cuestionario + questionnaireBtn: '📝 Completar cuestionario', + questionnaireTitle: 'Cuestionario beta GMS', + q1KnowGooglePlay: '¿Está familiarizado con Google Play?', + q2HasGmsDevice: '¿Tiene un dispositivo compatible con GMS (Google Mobile Services)?', + q3WillingToBeta: '¿Desea participar en la beta GMS de Xianyan APP?', + q4EnterGmail: 'Ingrese su dirección de Gmail', + q4GmailHint: 'Tras la aprobación, obtendrá acceso a la beta GMS', + qYes: 'Sí', + qNo: 'No', + qSubmit: 'Enviar', + qNext: 'Siguiente', + qEndTitle: 'Cuestionario completado', + qEndThanks: '🎉 ¡Gracias por participar! Revisaremos su solicitud pronto.', + qEndNotQualified: 'Lamentablemente, no cumple con los requisitos de la beta en este momento.', + qInvalidEmail: 'Ingrese una dirección de Gmail válida', + qSubmitting: 'Enviando...', + qSubmitSuccess: '✅ Enviado con éxito', + qSubmitFailed: 'Error al enviar, intente de nuevo más tarde', ), submit: TSubmit( title: 'Anonymous Submit', diff --git a/lib/l10n/languages/fr.dart b/lib/l10n/languages/fr.dart index 071f9fd8..eedf3bc0 100644 --- a/lib/l10n/languages/fr.dart +++ b/lib/l10n/languages/fr.dart @@ -1035,17 +1035,17 @@ const fr = T( feedCacheCount: 'Cache du flux', pendingSync: 'En attente de sync', hiveStorage: 'Stockage Hive', - chatSessions: 'Sessions de chat', - chatAttachments: 'Pièces jointes de chat', - chatTrash: 'Corbeille du chat', + chatSessions: 'Sessions', + chatAttachments: 'Pièces jointes de session', + chatTrash: 'Corbeille de session', transferRecords: 'Enregistrements de transfert', pairedDevices: 'Appareils appairés', receivedFiles: 'Fichiers reçus', readLater: 'À lire plus tard', cacheCleanup: 'Nettoyage du cache', cleanExpiredCache: 'Nettoyer le cache expiré', - cleanChatTrash: 'Nettoyer la corbeille du chat', - cleanChatThumbnails: 'Nettoyer les miniatures du chat', + cleanChatTrash: 'Nettoyer la corbeille de session', + cleanChatThumbnails: 'Nettoyer les miniatures de session', clearAllCache2: 'Effacer tout le cache', cleanTransferCache: 'Nettoyer le cache de transfert', clearAllChatData: 'Effacer toutes les données de chat', @@ -1060,24 +1060,24 @@ const fr = T( cleaningCache: 'Nettoyage du cache...', itemsUnit: 'éléments', piecesUnit: 'pièces', - cleaningChatTrash: 'Corbeille du chat nettoyée', - cleaningChatThumbnails: 'Miniatures du chat nettoyées', + cleaningChatTrash: 'Corbeille de session nettoyée', + cleaningChatThumbnails: 'Miniatures de session nettoyées', cleaningTransferCache: 'Cache de transfert nettoyé', - clearingAllChatData: 'Toutes les données de chat effacées', + clearingAllChatData: 'Toutes les données de session effacées', cleaningReadlaterCache: 'Cache À lire nettoyé', clearingReadlaterData: 'Toutes les données À lire effacées', allCacheCleared: 'Tout le cache effacé', cleanFailed2: 'Échec du nettoyage : {0}', confirmCleanChatTrashTitle: 'Nettoyer la corbeille du chat', confirmCleanChatTrashContent: 'Les messages et fichiers de la corbeille de plus de 30 jours seront supprimés définitivement. Cette action est irréversible.', - confirmCleanChatThumbnailsTitle: 'Nettoyer les miniatures du chat', - confirmCleanChatThumbnailsContent: 'Tout le cache des miniatures d\'images du chat sera nettoyé. Les images originales ne seront pas supprimées.', + confirmCleanChatThumbnailsTitle: 'Nettoyer les miniatures de session', + confirmCleanChatThumbnailsContent: 'Tout le cache des miniatures d\'images de session sera nettoyé. Les images originales ne seront pas supprimées.', confirmClearAllCacheTitle: 'Effacer tout le cache', confirmClearAllCacheContent: 'Voulez-vous vraiment effacer toutes les données de cache ? Le contenu hors ligne sera supprimé. Cette action est irréversible.', confirmCleanTransferCacheTitle: 'Nettoyer le cache de transfert', confirmCleanTransferCacheContent: 'Les miniatures de transfert, fichiers temporaires et enregistrements de plus de 30 jours seront nettoyés. Les fichiers reçus ne seront pas supprimés.', - confirmClearAllChatDataTitle: 'Effacer toutes les données de chat', - confirmClearAllChatDataContent: 'Toutes les sessions de chat, messages, pièces jointes et données de la corbeille seront supprimées. Cette action est irréversible !', + confirmClearAllChatDataTitle: 'Effacer toutes les données de session', + confirmClearAllChatDataContent: 'Toutes les sessions, messages, pièces jointes et données de la corbeille seront supprimées. Cette action est irréversible !', confirmCleanReadlaterCacheTitle: 'Nettoyer le cache À lire', confirmCleanReadlaterCacheContent: 'Les miniatures, pièces jointes et fichiers temporaires de synchronisation À lire seront nettoyés. Les enregistrements de messages ne seront pas supprimés.', confirmClearReadlaterDataTitle: 'Effacer toutes les données À lire', @@ -1090,7 +1090,7 @@ const fr = T( daysUnit2: 'jours', cleanTrash: 'Vider la corbeille', trashSourceInfoTitle: 'Sources de la corbeille', - trashSourceInfoContent: 'Les éléments de la corbeille proviennent de :\n\n💬 Messages de chat — Enregistrements de chat supprimés\n📖 À lire plus tard — Articles sauvegardés supprimés\n📁 Transferts de fichiers — Enregistrements de transfert supprimés\n\nCes éléments seront automatiquement nettoyés après une période de rétention.', + trashSourceInfoContent: 'Les éléments de la corbeille proviennent de :\n\n💬 Messages de session — Enregistrements de session supprimés\n📖 À lire plus tard — Articles sauvegardés supprimés\n📁 Transferts de fichiers — Enregistrements de transfert supprimés\n\nCes éléments seront automatiquement nettoyés après une période de rétention.', undoCleanTrash: 'Annuler', cleanTrashCountdown: 'La corbeille sera vidée dans {0}s, appuyez pour annuler', ), @@ -2554,6 +2554,25 @@ const fr = T( statusReleased: 'Publié', comingSoon: 'Bientôt disponible', gotIt: 'Compris', + // Questionnaire + questionnaireBtn: '📝 Remplir le questionnaire', + questionnaireTitle: 'Questionnaire bêta GMS', + q1KnowGooglePlay: 'Êtes-vous familier avec Google Play ?', + q2HasGmsDevice: 'Avez-vous un appareil prenant en charge GMS (Google Mobile Services) ?', + q3WillingToBeta: 'Souhaitez-vous participer à la version bêta GMS de Xianyan APP ?', + q4EnterGmail: 'Entrez votre adresse Gmail', + q4GmailHint: 'Après approbation, vous aurez accès à la version bêta GMS', + qYes: 'Oui', + qNo: 'Non', + qSubmit: 'Soumettre', + qNext: 'Suivant', + qEndTitle: 'Questionnaire terminé', + qEndThanks: '🎉 Merci pour votre participation ! Nous examinerons votre candidature prochainement.', + qEndNotQualified: 'Malheureusement, vous ne remplissez pas les conditions requises pour la version bêta.', + qInvalidEmail: 'Veuillez entrer une adresse Gmail valide', + qSubmitting: 'Envoi en cours...', + qSubmitSuccess: '✅ Envoyé avec succès', + qSubmitFailed: 'Échec de l\'envoi, veuillez réessayer plus tard', ), submit: TSubmit( title: 'Anonymous Submit', diff --git a/lib/l10n/languages/hi.dart b/lib/l10n/languages/hi.dart index 40da3465..71fe132b 100644 --- a/lib/l10n/languages/hi.dart +++ b/lib/l10n/languages/hi.dart @@ -1023,20 +1023,20 @@ const hi = T( feedCacheCount: 'फ़ीड कैश', pendingSync: 'सिंक लंबित', hiveStorage: 'Hive स्टोरेज', - chatSessions: 'चैट सत्र', - chatAttachments: 'चैट अटैचमेंट', - chatTrash: 'चैट कचरा', + chatSessions: 'सत्र', + chatAttachments: 'सत्र अटैचमेंट', + chatTrash: 'सत्र कचरा', transferRecords: 'ट्रांसफर रिकॉर्ड', pairedDevices: 'युग्मित उपकरण', receivedFiles: 'प्राप्त फ़ाइलें', readLater: 'बाद में पढ़ें', cacheCleanup: 'कैश सफाई', cleanExpiredCache: 'एक्सपायर्ड कैश साफ़ करें', - cleanChatTrash: 'चैट कचरा साफ़ करें', - cleanChatThumbnails: 'चैट थंबनेल साफ़ करें', + cleanChatTrash: 'सत्र कचरा साफ़ करें', + cleanChatThumbnails: 'सत्र थंबनेल साफ़ करें', clearAllCache2: 'सारा कैश हटाएं', cleanTransferCache: 'ट्रांसफर कैश साफ़ करें', - clearAllChatData: 'सारा चैट डेटा हटाएं', + clearAllChatData: 'सारा सत्र डेटा हटाएं', cleanReadlaterCache: 'बाद में पढ़ें कैश साफ़ करें', clearReadlaterData: 'सारा बाद में पढ़ें डेटा हटाएं', cacheStrategy: 'कैश रणनीति', @@ -1048,24 +1048,24 @@ const hi = T( cleaningCache: 'कैश साफ़ हो रहा है...', itemsUnit: 'आइटम', piecesUnit: 'टुकड़े', - cleaningChatTrash: 'चैट कचरा साफ़ किया गया', - cleaningChatThumbnails: 'चैट थंबनेल साफ़ किए गए', + cleaningChatTrash: 'सत्र कचरा साफ़ किया गया', + cleaningChatThumbnails: 'सत्र थंबनेल साफ़ किए गए', cleaningTransferCache: 'ट्रांसफर कैश साफ़ किया गया', - clearingAllChatData: 'सारा चैट डेटा हटाया गया', + clearingAllChatData: 'सारा सत्र डेटा हटाया गया', cleaningReadlaterCache: 'बाद में पढ़ें कैश साफ़ किया गया', clearingReadlaterData: 'सारा बाद में पढ़ें डेटा हटाया गया', allCacheCleared: 'सारा कैश हटाया गया', cleanFailed2: 'सफ़ाई विफल: {0}', - confirmCleanChatTrashTitle: 'चैट कचरा साफ़ करें', + confirmCleanChatTrashTitle: 'सत्र कचरा साफ़ करें', confirmCleanChatTrashContent: '30 दिनों से अधिक पुराने कचरे के संदेश और फ़ाइलें स्थायी रूप से हटा दी जाएंगी, इसे पूर्ववत नहीं किया जा सकता।', - confirmCleanChatThumbnailsTitle: 'चैट थंबनेल साफ़ करें', - confirmCleanChatThumbnailsContent: 'सभी चैट छवि थंबनेल कैश साफ़ किए जाएंगे, मूल छवियाँ हटाई नहीं जाएंगी।', + confirmCleanChatThumbnailsTitle: 'सत्र थंबनेल साफ़ करें', + confirmCleanChatThumbnailsContent: 'सभी सत्र छवि थंबनेल कैश साफ़ किए जाएंगे, मूल छवियाँ हटाई नहीं जाएंगी।', confirmClearAllCacheTitle: 'सारा कैश हटाएं', confirmClearAllCacheContent: 'क्या आप सुनिश्चित हैं कि सारा कैश डेटा हटाना चाहते हैं? ऑफ़लाइन सामग्री हटा दी जाएगी, इसे पूर्ववत नहीं किया जा सकता।', confirmCleanTransferCacheTitle: 'ट्रांसफर कैश साफ़ करें', confirmCleanTransferCacheContent: 'ट्रांसफर थंबनेल, अस्थायी फ़ाइलें और 30 दिनों से अधिक पुराने रिकॉर्ड साफ़ किए जाएंगे। प्राप्त फ़ाइलें हटाई नहीं जाएंगी।', - confirmClearAllChatDataTitle: 'सारा चैट डेटा हटाएं', - confirmClearAllChatDataContent: 'सभी चैट सत्र, संदेश, अटैचमेंट और कचरा डेटा हटा दिए जाएंगे, इसे पूर्ववत नहीं किया जा सकता!', + confirmClearAllChatDataTitle: 'सारा सत्र डेटा हटाएं', + confirmClearAllChatDataContent: 'सभी सत्र, संदेश, अटैचमेंट और कचरा डेटा हटा दिए जाएंगे, इसे पूर्ववत नहीं किया जा सकता!', confirmCleanReadlaterCacheTitle: 'बाद में पढ़ें कैश साफ़ करें', confirmCleanReadlaterCacheContent: 'बाद में पढ़ें थंबनेल, अटैचमेंट और सिंक अस्थायी फ़ाइलें साफ़ की जाएंगी, संदेश रिकॉर्ड हटाए नहीं जाएंगे।', confirmClearReadlaterDataTitle: 'सारा बाद में पढ़ें डेटा हटाएं', @@ -1078,7 +1078,7 @@ const hi = T( daysUnit2: 'दिन', cleanTrash: 'कचरा साफ़ करें', trashSourceInfoTitle: 'कचरा स्रोत', - trashSourceInfoContent: 'कचरे में आइटम यहाँ से आते हैं:\n\n💬 चैट संदेश — हटाए गए चैट रिकॉर्ड\n📖 बाद में पढ़ें — हटाए गए सहेजे गए लेख\n📁 फ़ाइल स्थानांतरण — हटाए गए स्थानांतरण रिकॉर्ड\n\nये आइटम अवधि के बाद स्वचालित रूप से साफ़ हो जाएंगे।', + trashSourceInfoContent: 'कचरे में आइटम यहाँ से आते हैं:\n\n💬 सत्र संदेश — हटाए गए सत्र रिकॉर्ड\n📖 बाद में पढ़ें — हटाए गए सहेजे गए लेख\n📁 फ़ाइल स्थानांतरण — हटाए गए स्थानांतरण रिकॉर्ड\n\nये आइटम अवधि के बाद स्वचालित रूप से साफ़ हो जाएंगे।', undoCleanTrash: 'पूर्ववत करें', cleanTrashCountdown: '{0} सेकंड में कचरा साफ़ हो जाएगा, पूर्ववत करने के लिए टैप करें', ), @@ -2524,6 +2524,25 @@ const hi = T( statusReleased: 'Released', comingSoon: 'Coming soon', gotIt: 'Got it', + // प्रश्नावली + questionnaireBtn: '📝 प्रश्नावली भरें', + questionnaireTitle: 'GMS बीटा प्रश्नावली', + q1KnowGooglePlay: 'क्या आप Google Play से परिचित हैं?', + q2HasGmsDevice: 'क्या आपके पास GMS (Google Mobile Services) समर्थित डिवाइस है?', + q3WillingToBeta: 'क्या आप Xianyan APP GMS बीटा में भाग लेना चाहते हैं?', + q4EnterGmail: 'अपना Gmail पता दर्ज करें', + q4GmailHint: 'स्वीकृति के बाद, आपको GMS बीटा तक पहुंच मिलेगी', + qYes: 'हां', + qNo: 'नहीं', + qSubmit: 'जमा करें', + qNext: 'अगला', + qEndTitle: 'प्रश्नावली पूर्ण', + qEndThanks: '🎉 भाग लेने के लिए धन्यवाद! हम जल्द ही आपके आवेदन की समीक्षा करेंगे।', + qEndNotQualified: 'दुर्भाग्यवश, आप इस समय बीटा आवश्यकताओं को पूरा नहीं करते हैं।', + qInvalidEmail: 'कृपया एक मान्य Gmail पता दर्ज करें', + qSubmitting: 'जमा हो रहा है...', + qSubmitSuccess: '✅ सफलतापूर्वक जमा किया गया', + qSubmitFailed: 'जमा करने में विफल, कृपया बाद में पुनः प्रयास करें', ), submit: TSubmit( title: 'Anonymous Submit', diff --git a/lib/l10n/languages/it.dart b/lib/l10n/languages/it.dart index 0171ea02..694e0526 100644 --- a/lib/l10n/languages/it.dart +++ b/lib/l10n/languages/it.dart @@ -1046,8 +1046,8 @@ const it = T( readLater: 'Leggi dopo', cacheCleanup: 'Pulizia cache', cleanExpiredCache: 'Pulisci cache scaduta', - cleanChatTrash: 'Svuota cestino chat', - cleanChatThumbnails: 'Pulisci miniature chat', + cleanChatTrash: 'Svuota cestino sessione', + cleanChatThumbnails: 'Pulisci miniature sessione', clearAllCache2: 'Cancella tutta la cache', cleanTransferCache: 'Pulisci cache trasferimenti', clearAllChatData: 'Cancella tutti i dati chat', @@ -1062,10 +1062,10 @@ const it = T( cleaningCache: 'Pulizia cache in corso...', itemsUnit: 'elementi', piecesUnit: 'pezzi', - cleaningChatTrash: 'Cestino chat svuotato', - cleaningChatThumbnails: 'Miniature chat pulite', + cleaningChatTrash: 'Cestino sessione svuotato', + cleaningChatThumbnails: 'Miniature sessione pulite', cleaningTransferCache: 'Cache trasferimenti pulita', - clearingAllChatData: 'Tutti i dati chat cancellati', + clearingAllChatData: 'Tutti i dati sessione cancellati', cleaningReadlaterCache: 'Cache "Leggi dopo" pulita', clearingReadlaterData: 'Tutti i dati "Leggi dopo" cancellati', allCacheCleared: 'Tutta la cache cancellata', @@ -2541,6 +2541,25 @@ const it = T( statusReleased: 'Rilasciato', comingSoon: 'Prossimamente', gotIt: 'Capito', + // Questionario + questionnaireBtn: '📝 Compila il questionario', + questionnaireTitle: 'Questionario beta GMS', + q1KnowGooglePlay: 'Sei familiare con Google Play?', + q2HasGmsDevice: 'Hai un dispositivo che supporta GMS (Google Mobile Services)?', + q3WillingToBeta: 'Vuoi partecipare alla beta GMS di Xianyan APP?', + q4EnterGmail: 'Inserisci il tuo indirizzo Gmail', + q4GmailHint: 'Dopo l\'approvazione, avrai accesso alla beta GMS', + qYes: 'Sì', + qNo: 'No', + qSubmit: 'Invia', + qNext: 'Avanti', + qEndTitle: 'Questionario completato', + qEndThanks: '🎉 Grazie per la partecipazione! Esamineremo presto la tua candidatura.', + qEndNotQualified: 'Purtroppo, non soddisfi i requisiti beta in questo momento.', + qInvalidEmail: 'Inserisci un indirizzo Gmail valido', + qSubmitting: 'Invio in corso...', + qSubmitSuccess: '✅ Inviato con successo', + qSubmitFailed: 'Invio fallito, riprova più tardi', ), submit: TSubmit( title: 'Anonymous Submit', diff --git a/lib/l10n/languages/ja.dart b/lib/l10n/languages/ja.dart index b4768a25..60a0c770 100644 --- a/lib/l10n/languages/ja.dart +++ b/lib/l10n/languages/ja.dart @@ -1017,17 +1017,17 @@ const ja = T( feedCacheCount: 'フィードキャッシュ', pendingSync: '同期待ち', hiveStorage: 'Hiveストレージ', - chatSessions: 'チャットセッション', - chatAttachments: 'チャット添付ファイル', - chatTrash: 'チャットゴミ箱', + chatSessions: 'セッション', + chatAttachments: 'セッション添付ファイル', + chatTrash: 'セッションゴミ箱', transferRecords: '転送記録', pairedDevices: 'ペアリングデバイス', receivedFiles: '受信ファイル', readLater: '後で読む', cacheCleanup: 'キャッシュクリーニング', cleanExpiredCache: '期限切れキャッシュをクリア', - cleanChatTrash: 'チャットゴミ箱をクリア', - cleanChatThumbnails: 'チャットサムネイルをクリア', + cleanChatTrash: 'セッションゴミ箱をクリア', + cleanChatThumbnails: 'セッションサムネイルをクリア', clearAllCache2: '全キャッシュをクリア', cleanTransferCache: '転送キャッシュをクリア', clearAllChatData: '全チャットデータをクリア', @@ -1042,19 +1042,19 @@ const ja = T( cleaningCache: 'キャッシュをクリーニング中...', itemsUnit: '件', piecesUnit: '個', - cleaningChatTrash: 'チャットゴミ箱をクリアしました', - cleaningChatThumbnails: 'チャットサムネイルをクリアしました', + cleaningChatTrash: 'セッションゴミ箱をクリアしました', + cleaningChatThumbnails: 'セッションサムネイルをクリアしました', cleaningTransferCache: '転送キャッシュをクリアしました', - clearingAllChatData: '全チャットデータをクリアしました', + clearingAllChatData: '全セッションデータをクリアしました', cleaningReadlaterCache: '後で読むキャッシュをクリアしました', clearingReadlaterData: '全後で読むデータをクリアしました', allCacheCleared: '全キャッシュをクリアしました', cleanFailed2: 'クリーニング失敗: {0}', - confirmCleanChatTrashTitle: 'チャットゴミ箱をクリア', + confirmCleanChatTrashTitle: 'セッションゴミ箱をクリア', confirmCleanChatTrashContent: '30日以上前のメッセージとファイルを永久に削除します。この操作は取り消せません。', - confirmCleanChatThumbnailsTitle: 'チャットサムネイルをクリア', + confirmCleanChatThumbnailsTitle: 'セッションサムネイルをクリア', confirmCleanChatThumbnailsContent: - 'すべてのチャット画像のサムネイルキャッシュをクリアします。元の画像は削除されません。', + 'すべてのセッション画像のサムネイルキャッシュをクリアします。元の画像は削除されません。', confirmClearAllCacheTitle: '全キャッシュをクリア', confirmClearAllCacheContent: 'すべてのキャッシュデータをクリアしますか?オフラインコンテンツが削除されます。この操作は取り消せません。', @@ -1078,7 +1078,7 @@ const ja = T( daysUnit2: '日', cleanTrash: 'ゴミ箱を空にする', trashSourceInfoTitle: 'ゴミ箱の来源', - trashSourceInfoContent: 'ゴミ箱の内容は以下から来ています:\n\n💬 チャットメッセージ — 削除されたチャット記録\n📖 後で読む — 削除された保存記事\n📁 ファイル転送 — 削除された転送記録\n\nこれらの内容は一定期間後に自動的にクリーンアップされます。', + trashSourceInfoContent: 'ゴミ箱の内容は以下から来ています:\n\n💬 セッションメッセージ — 削除されたセッション記録\n📖 後で読む — 削除された保存記事\n📁 ファイル転送 — 削除された転送記録\n\nこれらの内容は一定期間後に自動的にクリーンアップされます。', undoCleanTrash: '元に戻す', cleanTrashCountdown: '{0}秒後にゴミ箱を空にします、タップで元に戻す', ), @@ -2468,6 +2468,25 @@ const ja = T( statusReleased: 'リリース済み', comingSoon: 'お楽しみに', gotIt: '了解', + // アンケート + questionnaireBtn: '📝 アンケートに回答', + questionnaireTitle: 'GMSベータアンケート', + q1KnowGooglePlay: 'Google Playをご存知ですか?', + q2HasGmsDevice: 'GMS(Googleモバイルサービス)対応のデバイスをお持ちですか?', + q3WillingToBeta: '閑言APPのGMSベータ版に参加しますか?', + q4EnterGmail: 'Gmailアドレスを入力', + q4GmailHint: '承認後、GMSベータ版にアクセスできます', + qYes: 'はい', + qNo: 'いいえ', + qSubmit: '送信', + qNext: '次へ', + qEndTitle: 'アンケート完了', + qEndThanks: '🎉 ご参加ありがとうございます!申請を確認いたします。', + qEndNotQualified: '残念ながら、現在ベータ版の条件を満たしていません。', + qInvalidEmail: '有効なGmailアドレスを入力してください', + qSubmitting: '送信中...', + qSubmitSuccess: '✅ 送信成功', + qSubmitFailed: '送信失敗、後でもう一度お試しください', ), submit: TSubmit( title: '匿名投稿', diff --git a/lib/l10n/languages/ko.dart b/lib/l10n/languages/ko.dart index a01bc3fc..6a683e34 100644 --- a/lib/l10n/languages/ko.dart +++ b/lib/l10n/languages/ko.dart @@ -1017,17 +1017,17 @@ const ko = T( feedCacheCount: '피드 캐시', pendingSync: '동기화 대기 중', hiveStorage: 'Hive 저장소', - chatSessions: '채팅 세션', - chatAttachments: '채팅 첨부파일', - chatTrash: '채팅 휴지통', + chatSessions: '세션', + chatAttachments: '세션 첨부파일', + chatTrash: '세션 휴지통', transferRecords: '전송 기록', pairedDevices: '페어링된 기기', receivedFiles: '받은 파일', readLater: '나중에 읽기', cacheCleanup: '캐시 정리', cleanExpiredCache: '만료 캐시 정리', - cleanChatTrash: '채팅 휴지통 정리', - cleanChatThumbnails: '썸네일 정리', + cleanChatTrash: '세션 휴지통 정리', + cleanChatThumbnails: '세션 썸네일 정리', clearAllCache2: '전체 캐시 삭제', cleanTransferCache: '전송 캐시 정리', clearAllChatData: '전체 채팅 데이터 삭제', @@ -1042,10 +1042,10 @@ const ko = T( cleaningCache: '캐시 정리 중...', itemsUnit: '건', piecesUnit: '개', - cleaningChatTrash: '채팅 휴지통이 정리되었습니다', - cleaningChatThumbnails: '썸네일이 정리되었습니다', + cleaningChatTrash: '세션 휴지통이 정리되었습니다', + cleaningChatThumbnails: '세션 썸네일이 정리되었습니다', cleaningTransferCache: '전송 캐시가 정리되었습니다', - clearingAllChatData: '전체 채팅 데이터가 삭제되었습니다', + clearingAllChatData: '전체 세션 데이터가 삭제되었습니다', cleaningReadlaterCache: '나중에 읽기 캐시가 정리되었습니다', clearingReadlaterData: '전체 나중에 읽기 데이터가 삭제되었습니다', allCacheCleared: '전체 캐시가 삭제되었습니다', @@ -1053,9 +1053,9 @@ const ko = T( confirmCleanChatTrashTitle: '채팅 휴지통 정리', confirmCleanChatTrashContent: '30일 이상 된 메시지와 파일을 영구적으로 삭제합니다. 이 작업은 되돌릴 수 없습니다.', - confirmCleanChatThumbnailsTitle: '썸네일 정리', + confirmCleanChatThumbnailsTitle: '세션 썸네일 정리', confirmCleanChatThumbnailsContent: - '모든 채팅 이미지의 썸네일 캐시를 정리합니다. 원본 이미지는 삭제되지 않습니다.', + '모든 세션 이미지의 썸네일 캐시를 정리합니다. 원본 이미지는 삭제되지 않습니다.', confirmClearAllCacheTitle: '전체 캐시 삭제', confirmClearAllCacheContent: '모든 캐시 데이터를 삭제하시겠습니까? 오프라인 콘텐츠가 삭제됩니다. 이 작업은 되돌릴 수 없습니다.', @@ -1079,7 +1079,7 @@ const ko = T( daysUnit2: '일', cleanTrash: '휴지통 비우기', trashSourceInfoTitle: '휴지통 출처', - trashSourceInfoContent: '휴지통의 항목은 다음에서 옵니다:\n\n💬 채팅 메시지 — 삭제된 채팅 기록\n📖 나중에 읽기 — 삭제된 저장 기사\n📁 파일 전송 — 삭제된 전송 기록\n\n이 항목은 보유 기간 후 자동으로 정리됩니다.', + trashSourceInfoContent: '휴지통의 항목은 다음에서 옵니다:\n\n💬 세션 메시지 — 삭제된 세션 기록\n📖 나중에 읽기 — 삭제된 저장 기사\n📁 파일 전송 — 삭제된 전송 기록\n\n이 항목은 보유 기간 후 자동으로 정리됩니다.', undoCleanTrash: '실행 취소', cleanTrashCountdown: '{0}초 후 휴지통이 비워집니다, 탭하여 취소', ), @@ -2471,6 +2471,25 @@ const ko = T( statusReleased: '출시됨', comingSoon: '출시 예정', gotIt: '알겠습니다', + // 설문조사 + questionnaireBtn: '📝 설문조사 참여', + questionnaireTitle: 'GMS 베타 설문조사', + q1KnowGooglePlay: 'Google Play에 익숙하신가요?', + q2HasGmsDevice: 'GMS(Google 모바일 서비스)를 지원하는 기기가 있으신가요?', + q3WillingToBeta: '셴옌APP GMS 베타에 참여하시겠습니까?', + q4EnterGmail: 'Gmail 주소 입력', + q4GmailHint: '승인 후 GMS 베타에 접근할 수 있습니다', + qYes: '예', + qNo: '아니요', + qSubmit: '제출', + qNext: '다음', + qEndTitle: '설문조사 완료', + qEndThanks: '🎉 참여해 주셔서 감사합니다! 신청을 곧 검토하겠습니다.', + qEndNotQualified: '안타깝게도 현재 베타 요건을 충족하지 않습니다.', + qInvalidEmail: '유효한 Gmail 주소를 입력해 주세요', + qSubmitting: '제출 중...', + qSubmitSuccess: '✅ 제출 성공', + qSubmitFailed: '제출 실패, 나중에 다시 시도해 주세요', ), submit: TSubmit( title: '익명 투고', diff --git a/lib/l10n/languages/pt.dart b/lib/l10n/languages/pt.dart index 5b086eb7..f944a7a3 100644 --- a/lib/l10n/languages/pt.dart +++ b/lib/l10n/languages/pt.dart @@ -1036,20 +1036,20 @@ const pt = T( feedCacheCount: 'Cache de Feed', pendingSync: 'Sincronização Pendente', hiveStorage: 'Armazenamento Hive', - chatSessions: 'Sessões de Chat', - chatAttachments: 'Anexos de Chat', - chatTrash: 'Lixeira do Chat', + chatSessions: 'Sessões', + chatAttachments: 'Anexos de Sessão', + chatTrash: 'Lixeira de Sessão', transferRecords: 'Registros de Transferência', pairedDevices: 'Dispositivos Emparelhados', receivedFiles: 'Arquivos Recebidos', readLater: 'Ler Depois', cacheCleanup: 'Limpeza de Cache', cleanExpiredCache: 'Limpar Cache Expirado', - cleanChatTrash: 'Limpar Lixeira do Chat', - cleanChatThumbnails: 'Limpar Miniaturas do Chat', + cleanChatTrash: 'Limpar Lixeira de Sessão', + cleanChatThumbnails: 'Limpar Miniaturas de Sessão', clearAllCache2: 'Limpar Todo o Cache', cleanTransferCache: 'Limpar Cache de Transferência', - clearAllChatData: 'Limpar Todos os Dados de Chat', + clearAllChatData: 'Limpar Todos os Dados de Sessão', cleanReadlaterCache: 'Limpar Cache de Ler Depois', clearReadlaterData: 'Limpar Todos os Dados de Ler Depois', cacheStrategy: 'Estratégia de Cache', @@ -1061,24 +1061,24 @@ const pt = T( cleaningCache: 'Limpando cache...', itemsUnit: 'itens', piecesUnit: 'unidades', - cleaningChatTrash: 'Lixeira do Chat limpa', - cleaningChatThumbnails: 'Miniaturas do Chat limpas', + cleaningChatTrash: 'Lixeira de Sessão limpa', + cleaningChatThumbnails: 'Miniaturas de Sessão limpas', cleaningTransferCache: 'Cache de Transferência limpo', - clearingAllChatData: 'Todos os dados de chat foram limpos', + clearingAllChatData: 'Todos os dados de sessão foram limpos', cleaningReadlaterCache: 'Cache de Ler Depois limpo', clearingReadlaterData: 'Todos os dados de Ler Depois foram limpos', allCacheCleared: 'Todo o cache foi limpo', cleanFailed2: 'Falha na limpeza: {0}', - confirmCleanChatTrashTitle: 'Limpar Lixeira do Chat', + confirmCleanChatTrashTitle: 'Limpar Lixeira de Sessão', confirmCleanChatTrashContent: 'Mensagens e arquivos na lixeira com mais de 30 dias serão excluídos permanentemente. Esta ação não pode ser desfeita.', - confirmCleanChatThumbnailsTitle: 'Limpar Miniaturas do Chat', - confirmCleanChatThumbnailsContent: 'Todo o cache de miniaturas de imagens do chat será limpo. As imagens originais não serão excluídas.', + confirmCleanChatThumbnailsTitle: 'Limpar Miniaturas de Sessão', + confirmCleanChatThumbnailsContent: 'Todo o cache de miniaturas de imagens de sessão será limpo. As imagens originais não serão excluídas.', confirmClearAllCacheTitle: 'Limpar Todo o Cache', confirmClearAllCacheContent: 'Tem certeza de que deseja limpar todos os dados de cache? O conteúdo offline será excluído. Esta ação não pode ser desfeita.', confirmCleanTransferCacheTitle: 'Limpar Cache de Transferência', confirmCleanTransferCacheContent: 'Miniaturas de transferência, arquivos temporários e registros com mais de 30 dias serão limpos. Arquivos recebidos não serão excluídos.', - confirmClearAllChatDataTitle: 'Limpar Todos os Dados de Chat', - confirmClearAllChatDataContent: 'Todas as sessões de chat, mensagens, anexos e dados da lixeira serão excluídos. Esta ação não pode ser desfeita!', + confirmClearAllChatDataTitle: 'Limpar Todos os Dados de Sessão', + confirmClearAllChatDataContent: 'Todas as sessões, mensagens, anexos e dados da lixeira serão excluídos. Esta ação não pode ser desfeita!', confirmCleanReadlaterCacheTitle: 'Limpar Cache de Ler Depois', confirmCleanReadlaterCacheContent: 'Miniaturas, anexos e arquivos temporários de sincronização de Ler Depois serão limpos. Os registros de mensagens não serão excluídos.', confirmClearReadlaterDataTitle: 'Limpar Todos os Dados de Ler Depois', @@ -1091,7 +1091,7 @@ const pt = T( daysUnit2: 'dias', cleanTrash: 'Limpar lixeira', trashSourceInfoTitle: 'Fontes da lixeira', - trashSourceInfoContent: 'Os itens na lixeira vêm de:\n\n💬 Mensagens de chat — Registros de chat excluídos\n📖 Ler depois — Artigos salvos excluídos\n📁 Transferências de arquivos — Registros de transferência excluídos\n\nEstes itens serão limpos automaticamente após um período de retenção.', + trashSourceInfoContent: 'Os itens na lixeira vêm de:\n\n💬 Mensagens de sessão — Registros de sessão excluídos\n📖 Ler depois — Artigos salvos excluídos\n📁 Transferências de arquivos — Registros de transferência excluídos\n\nEstes itens serão limpos automaticamente após um período de retenção.', undoCleanTrash: 'Desfazer', cleanTrashCountdown: 'A lixeira será esvaziada em {0}s, toque para desfazer', ), @@ -2538,6 +2538,25 @@ const pt = T( statusReleased: 'Lançado', comingSoon: 'Em breve', gotIt: 'Entendi', + // Questionário + questionnaireBtn: '📝 Preencher Questionário', + questionnaireTitle: 'Questionário Beta GMS', + q1KnowGooglePlay: 'Você está familiarizado com o Google Play?', + q2HasGmsDevice: 'Você tem um dispositivo com suporte a GMS (Google Mobile Services)?', + q3WillingToBeta: 'Gostaria de participar do beta GMS do Xianyan APP?', + q4EnterGmail: 'Insira seu endereço Gmail', + q4GmailHint: 'Após aprovação, você terá acesso ao beta GMS', + qYes: 'Sim', + qNo: 'Não', + qSubmit: 'Enviar', + qNext: 'Próximo', + qEndTitle: 'Questionário Concluído', + qEndThanks: '🎉 Obrigado por participar! Analisaremos sua inscrição em breve.', + qEndNotQualified: 'Infelizmente, você não atende aos requisitos do beta no momento.', + qInvalidEmail: 'Insira um endereço Gmail válido', + qSubmitting: 'Enviando...', + qSubmitSuccess: '✅ Enviado com sucesso', + qSubmitFailed: 'Falha no envio, tente novamente mais tarde', ), submit: TSubmit( title: 'Anonymous Submit', diff --git a/lib/l10n/languages/ru.dart b/lib/l10n/languages/ru.dart index 8e6124fd..9cb76970 100644 --- a/lib/l10n/languages/ru.dart +++ b/lib/l10n/languages/ru.dart @@ -1033,20 +1033,20 @@ const ru = T( feedCacheCount: 'Кэш ленты', pendingSync: 'Ожидает синхронизации', hiveStorage: 'Хранилище Hive', - chatSessions: 'Сессии чата', - chatAttachments: 'Вложения чата', - chatTrash: 'Корзина чата', + chatSessions: 'Сессии', + chatAttachments: 'Вложения сессий', + chatTrash: 'Корзина сессий', transferRecords: 'Записи передач', pairedDevices: 'Сопряжённые устройства', receivedFiles: 'Полученные файлы', readLater: 'Читать позже', cacheCleanup: 'Очистка кэша', cleanExpiredCache: 'Очистить просроченный кэш', - cleanChatTrash: 'Очистить корзину чата', - cleanChatThumbnails: 'Очистить миниатюры чата', + cleanChatTrash: 'Очистить корзину сессий', + cleanChatThumbnails: 'Очистить миниатюры сессий', clearAllCache2: 'Очистить весь кэш', cleanTransferCache: 'Очистить кэш передач', - clearAllChatData: 'Очистить все данные чата', + clearAllChatData: 'Очистить все данные сессий', cleanReadlaterCache: 'Очистить кэш отложенного', clearReadlaterData: 'Очистить все данные отложенного', cacheStrategy: 'Стратегия кэша', @@ -1058,10 +1058,10 @@ const ru = T( cleaningCache: 'Очистка кэша...', itemsUnit: 'элементов', piecesUnit: 'штук', - cleaningChatTrash: 'Корзина чата очищена', - cleaningChatThumbnails: 'Миниатюры чата очищены', + cleaningChatTrash: 'Корзина сессий очищена', + cleaningChatThumbnails: 'Миниатюры сессий очищены', cleaningTransferCache: 'Кэш передач очищен', - clearingAllChatData: 'Все данные чата очищены', + clearingAllChatData: 'Все данные сессий очищены', cleaningReadlaterCache: 'Кэш отложенного очищен', clearingReadlaterData: 'Все данные отложенного очищены', allCacheCleared: 'Весь кэш очищен', @@ -1074,8 +1074,8 @@ const ru = T( confirmClearAllCacheContent: 'Вы уверены, что хотите очистить все данные кэша? Оффлайн-контент будет удалён. Это действие нельзя отменить.', confirmCleanTransferCacheTitle: 'Очистить кэш передач', confirmCleanTransferCacheContent: 'Миниатюры передач, временные файлы и записи старше 30 дней будут очищены. Полученные файлы не будут удалены.', - confirmClearAllChatDataTitle: 'Очистить все данные чата', - confirmClearAllChatDataContent: 'Все сессии чата, сообщения, вложения и данные корзины будут удалены. Это действие нельзя отменить!', + confirmClearAllChatDataTitle: 'Очистить все данные сессий', + confirmClearAllChatDataContent: 'Все сессии, сообщения, вложения и данные корзины будут удалены. Это действие нельзя отменить!', confirmCleanReadlaterCacheTitle: 'Очистить кэш отложенного', confirmCleanReadlaterCacheContent: 'Миниатюры, вложения и временные файлы синхронизации отложенного будут очищены. Записи сообщений не будут удалены.', confirmClearReadlaterDataTitle: 'Очистить все данные отложенного', @@ -1088,7 +1088,7 @@ const ru = T( daysUnit2: 'дней', cleanTrash: 'Очистить корзину', trashSourceInfoTitle: 'Источники корзины', - trashSourceInfoContent: 'Элементы в корзине поступают из:\n\n💬 Сообщения чата — Удалённые записи чата\n📖 Читать позже — Удалённые сохранённые статьи\n📁 Передачи файлов — Удалённые записи передач\n\nЭти элементы будут автоматически очищены по истечении срока хранения.', + trashSourceInfoContent: 'Элементы в корзине поступают из:\n\n💬 Сообщения сессий — Удалённые записи сессий\n📖 Читать позже — Удалённые сохранённые статьи\n📁 Передачи файлов — Удалённые записи передач\n\nЭти элементы будут автоматически очищены по истечении срока хранения.', undoCleanTrash: 'Отменить', cleanTrashCountdown: 'Корзина будет очищена через {0}с, нажмите для отмены', ), @@ -2528,6 +2528,25 @@ const ru = T( statusReleased: 'Выпущено', comingSoon: 'Скоро', gotIt: 'Понятно', + // Опросник + questionnaireBtn: '📝 Заполнить опросник', + questionnaireTitle: 'Опросник GMS бета', + q1KnowGooglePlay: 'Знакомы ли вы с Google Play?', + q2HasGmsDevice: 'Есть ли у вас устройство с поддержкой GMS (Google Mobile Services)?', + q3WillingToBeta: 'Хотите участвовать в GMS бета-тестировании Xianyan APP?', + q4EnterGmail: 'Введите ваш Gmail адрес', + q4GmailHint: 'После одобрения вы получите доступ к GMS бета-версии', + qYes: 'Да', + qNo: 'Нет', + qSubmit: 'Отправить', + qNext: 'Далее', + qEndTitle: 'Опросник завершён', + qEndThanks: '🎉 Спасибо за участие! Мы скоро рассмотрим вашу заявку.', + qEndNotQualified: 'К сожалению, вы пока не соответствуете требованиям бета-тестирования.', + qInvalidEmail: 'Введите корректный Gmail адрес', + qSubmitting: 'Отправка...', + qSubmitSuccess: '✅ Успешно отправлено', + qSubmitFailed: 'Ошибка отправки, попробуйте позже', ), submit: TSubmit( title: 'Anonymous Submit', diff --git a/lib/l10n/languages/zh_cn.dart b/lib/l10n/languages/zh_cn.dart index 6d8bc17a..0edc450c 100644 --- a/lib/l10n/languages/zh_cn.dart +++ b/lib/l10n/languages/zh_cn.dart @@ -1015,9 +1015,9 @@ const zhCN = T( feedCacheCount: 'Feed缓存', pendingSync: '待同步操作', hiveStorage: 'Hive存储', - chatSessions: '聊天会话', - chatAttachments: '聊天附件', - chatTrash: '聊天回收站', + chatSessions: '会话', + chatAttachments: '会话附件', + chatTrash: '会话回收站', transferRecords: '传输记录', pairedDevices: '配对设备', receivedFiles: '接收文件', @@ -1040,24 +1040,24 @@ const zhCN = T( cleaningCache: '正在清理缓存...', itemsUnit: '条', piecesUnit: '个', - cleaningChatTrash: '聊天回收站已清理', - cleaningChatThumbnails: '聊天缩略图已清理', + cleaningChatTrash: '会话回收站已清理', + cleaningChatThumbnails: '会话缩略图已清理', cleaningTransferCache: '传输缓存已清理', - clearingAllChatData: '全部聊天数据已清除', + clearingAllChatData: '全部会话数据已清除', cleaningReadlaterCache: '稍后读缓存已清理', clearingReadlaterData: '全部稍后读数据已清除', allCacheCleared: '全部缓存已清除', cleanFailed2: '清理失败: {0}', - confirmCleanChatTrashTitle: '清理聊天回收站', + confirmCleanChatTrashTitle: '清理会话回收站', confirmCleanChatTrashContent: '将永久删除回收站中超过30天的消息和文件,此操作不可撤销。', - confirmCleanChatThumbnailsTitle: '清理聊天缩略图', - confirmCleanChatThumbnailsContent: '将清理所有聊天图片的缩略图缓存,原图不会被删除。', + confirmCleanChatThumbnailsTitle: '清理会话缩略图', + confirmCleanChatThumbnailsContent: '将清理所有会话图片的缩略图缓存,原图不会被删除。', confirmClearAllCacheTitle: '清除全部缓存', confirmClearAllCacheContent: '确定要清除所有缓存数据吗?离线内容将被删除,此操作不可撤销。', confirmCleanTransferCacheTitle: '清理传输缓存', confirmCleanTransferCacheContent: '将清理传输缩略图、临时文件和超过30天的传输记录。已接收的文件不会被删除。', - confirmClearAllChatDataTitle: '清理全部聊天数据', - confirmClearAllChatDataContent: '将删除所有聊天会话、消息、附件和回收站数据,此操作不可撤销!', + confirmClearAllChatDataTitle: '清理全部会话数据', + confirmClearAllChatDataContent: '将删除所有会话、消息、附件和回收站数据,此操作不可撤销!', confirmCleanReadlaterCacheTitle: '清理稍后读缓存', confirmCleanReadlaterCacheContent: '将清理稍后读的缩略图、附件和同步临时文件,消息记录不会被删除。', confirmClearReadlaterDataTitle: '清理全部稍后读数据', @@ -1070,7 +1070,7 @@ const zhCN = T( daysUnit2: '天', cleanTrash: '清空回收站', trashSourceInfoTitle: '回收站来源', - trashSourceInfoContent: '回收站中的内容来自以下方面:\n\n💬 聊天消息 — 删除的聊天记录\n📖 稍后读 — 删除的收藏文章\n📁 传输文件 — 删除的传输记录\n\n这些内容在回收站中保留一定时间后会自动清理。', + trashSourceInfoContent: '回收站中的内容来自以下方面:\n\n💬 会话消息 — 删除的会话记录\n📖 稍后读 — 删除的收藏文章\n📁 传输文件 — 删除的传输记录\n\n这些内容在回收站中保留一定时间后会自动清理。', undoCleanTrash: '撤销', cleanTrashCountdown: '{0}秒后将清空回收站,点击撤销', ), @@ -2480,6 +2480,25 @@ const zhCN = T( // 通用 comingSoon: '敬请期待', gotIt: '知道了', + // 问卷 + questionnaireBtn: '📝 填写问卷', + questionnaireTitle: 'GMS内测问卷', + q1KnowGooglePlay: '您是否了解Google Play?', + q2HasGmsDevice: '您是否有支持GMS(谷歌框架)的设备?', + q3WillingToBeta: '您是否愿意参与闲言APP的内测GMS版?', + q4EnterGmail: '填写你的Gmail邮箱', + q4GmailHint: '审核通过后,获取闲言APP的GMS版内测资格', + qYes: '是', + qNo: '否', + qSubmit: '提交', + qNext: '下一步', + qEndTitle: '问卷结束', + qEndThanks: '🎉 感谢您的参与,我们会尽快审核!', + qEndNotQualified: '很遗憾,您暂不符合内测条件', + qInvalidEmail: '请输入有效的Gmail邮箱', + qSubmitting: '提交中...', + qSubmitSuccess: '✅ 提交成功', + qSubmitFailed: '提交失败,请稍后重试', ), submit: TSubmit( title: '匿名投稿', diff --git a/lib/l10n/languages/zh_tw.dart b/lib/l10n/languages/zh_tw.dart index 48bb2623..1e50ad77 100644 --- a/lib/l10n/languages/zh_tw.dart +++ b/lib/l10n/languages/zh_tw.dart @@ -1015,9 +1015,9 @@ const zhTW = T( feedCacheCount: 'Feed快取', pendingSync: '待同步操作', hiveStorage: 'Hive儲存', - chatSessions: '聊天會話', - chatAttachments: '聊天附件', - chatTrash: '聊天回收桶', + chatSessions: '會話', + chatAttachments: '會話附件', + chatTrash: '會話回收桶', transferRecords: '傳輸記錄', pairedDevices: '配對裝置', receivedFiles: '接收檔案', @@ -1040,18 +1040,18 @@ const zhTW = T( cleaningCache: '正在清理快取...', itemsUnit: '條', piecesUnit: '個', - cleaningChatTrash: '聊天回收桶已清理', - cleaningChatThumbnails: '聊天縮圖已清理', + cleaningChatTrash: '會話回收桶已清理', + cleaningChatThumbnails: '會話縮圖已清理', cleaningTransferCache: '傳輸快取已清理', - clearingAllChatData: '全部聊天資料已清除', + clearingAllChatData: '全部會話資料已清除', cleaningReadlaterCache: '稍後讀快取已清理', clearingReadlaterData: '全部稍後讀資料已清除', allCacheCleared: '全部快取已清除', cleanFailed2: '清理失敗: {0}', - confirmCleanChatTrashTitle: '清理聊天回收桶', + confirmCleanChatTrashTitle: '清理會話回收桶', confirmCleanChatTrashContent: '將永久刪除回收桶中超過30天的訊息和檔案,此操作無法復原。', - confirmCleanChatThumbnailsTitle: '清理聊天縮圖', - confirmCleanChatThumbnailsContent: '將清理所有聊天圖片的縮圖快取,原圖不會被刪除。', + confirmCleanChatThumbnailsTitle: '清理會話縮圖', + confirmCleanChatThumbnailsContent: '將清理所有會話圖片的縮圖快取,原圖不會被刪除。', confirmClearAllCacheTitle: '清除全部快取', confirmClearAllCacheContent: '確定要清除所有快取資料嗎?離線內容將被刪除,此操作無法復原。', confirmCleanTransferCacheTitle: '清理傳輸快取', @@ -1070,7 +1070,7 @@ const zhTW = T( daysUnit2: '天', cleanTrash: '清空回收桶', trashSourceInfoTitle: '回收桶來源', - trashSourceInfoContent: '回收桶中的內容來自以下方面:\n\n💬 聊天訊息 — 刪除的聊天記錄\n📖 稍後閱讀 — 刪除的收藏文章\n📁 傳輸檔案 — 刪除的傳輸記錄\n\n這些內容在回收桶中保留一定時間後會自動清理。', + trashSourceInfoContent: '回收桶中的內容來自以下方面:\n\n💬 會話訊息 — 刪除的會話記錄\n📖 稍後閱讀 — 刪除的收藏文章\n📁 傳輸檔案 — 刪除的傳輸記錄\n\n這些內容在回收桶中保留一定時間後會自動清理。', undoCleanTrash: '撤銷', cleanTrashCountdown: '{0}秒後將清空回收桶,點擊撤銷', ), @@ -2447,6 +2447,25 @@ const zhTW = T( statusReleased: '已發布', comingSoon: '敬請期待', gotIt: '知道了', + // 問卷 + questionnaireBtn: '📝 填寫問卷', + questionnaireTitle: 'GMS內測問卷', + q1KnowGooglePlay: '您是否了解Google Play?', + q2HasGmsDevice: '您是否有支援GMS(Google框架)的裝置?', + q3WillingToBeta: '您是否願意參與閑言APP的內測GMS版?', + q4EnterGmail: '填寫你的Gmail信箱', + q4GmailHint: '審核通過後,取得閑言APP的GMS版內測資格', + qYes: '是', + qNo: '否', + qSubmit: '提交', + qNext: '下一步', + qEndTitle: '問卷結束', + qEndThanks: '🎉 感謝您的參與,我們會盡快審核!', + qEndNotQualified: '很遺憾,您暫不符合內測條件', + qInvalidEmail: '請輸入有效的Gmail信箱', + qSubmitting: '提交中...', + qSubmitSuccess: '✅ 提交成功', + qSubmitFailed: '提交失敗,請稍後重試', ), submit: TSubmit( title: '匿名投稿', diff --git a/lib/l10n/translation_io_service.dart b/lib/l10n/translation_io_service.dart index 08f87db5..a2b7d687 100644 --- a/lib/l10n/translation_io_service.dart +++ b/lib/l10n/translation_io_service.dart @@ -1510,6 +1510,25 @@ class TranslationIOService { // 通用 comingSoon: map['comingSoon'] as String? ?? fallback.comingSoon, gotIt: map['gotIt'] as String? ?? fallback.gotIt, + // 问卷 + questionnaireBtn: map['questionnaireBtn'] as String? ?? fallback.questionnaireBtn, + questionnaireTitle: map['questionnaireTitle'] as String? ?? fallback.questionnaireTitle, + q1KnowGooglePlay: map['q1KnowGooglePlay'] as String? ?? fallback.q1KnowGooglePlay, + q2HasGmsDevice: map['q2HasGmsDevice'] as String? ?? fallback.q2HasGmsDevice, + q3WillingToBeta: map['q3WillingToBeta'] as String? ?? fallback.q3WillingToBeta, + q4EnterGmail: map['q4EnterGmail'] as String? ?? fallback.q4EnterGmail, + q4GmailHint: map['q4GmailHint'] as String? ?? fallback.q4GmailHint, + qYes: map['qYes'] as String? ?? fallback.qYes, + qNo: map['qNo'] as String? ?? fallback.qNo, + qSubmit: map['qSubmit'] as String? ?? fallback.qSubmit, + qNext: map['qNext'] as String? ?? fallback.qNext, + qEndTitle: map['qEndTitle'] as String? ?? fallback.qEndTitle, + qEndThanks: map['qEndThanks'] as String? ?? fallback.qEndThanks, + qEndNotQualified: map['qEndNotQualified'] as String? ?? fallback.qEndNotQualified, + qInvalidEmail: map['qInvalidEmail'] as String? ?? fallback.qInvalidEmail, + qSubmitting: map['qSubmitting'] as String? ?? fallback.qSubmitting, + qSubmitSuccess: map['qSubmitSuccess'] as String? ?? fallback.qSubmitSuccess, + qSubmitFailed: map['qSubmitFailed'] as String? ?? fallback.qSubmitFailed, ); } diff --git a/lib/l10n/types/t_beta.dart b/lib/l10n/types/t_beta.dart index b441ce1a..af7719cf 100644 --- a/lib/l10n/types/t_beta.dart +++ b/lib/l10n/types/t_beta.dart @@ -49,6 +49,25 @@ class TBeta { // ---- 通用 ---- required this.comingSoon, required this.gotIt, + // ---- 问卷 ---- + required this.questionnaireBtn, + required this.questionnaireTitle, + required this.q1KnowGooglePlay, + required this.q2HasGmsDevice, + required this.q3WillingToBeta, + required this.q4EnterGmail, + required this.q4GmailHint, + required this.qYes, + required this.qNo, + required this.qSubmit, + required this.qNext, + required this.qEndTitle, + required this.qEndThanks, + required this.qEndNotQualified, + required this.qInvalidEmail, + required this.qSubmitting, + required this.qSubmitSuccess, + required this.qSubmitFailed, }); // ==================== 页面级 ==================== @@ -141,6 +160,45 @@ class TBeta { /// 知道了 final String gotIt; + // ==================== 问卷 ==================== + + /// 填写问卷 + final String questionnaireBtn; + /// GMS内测问卷 + final String questionnaireTitle; + /// 了解Google Play? + final String q1KnowGooglePlay; + /// 有支持GMS(谷歌框架)的设备? + final String q2HasGmsDevice; + /// 愿意参与闲言APP的内测GMS版? + final String q3WillingToBeta; + /// 填写你的Gmail邮箱 + final String q4EnterGmail; + /// 审核通过后获取内测资格 + final String q4GmailHint; + /// 是 + final String qYes; + /// 否 + final String qNo; + /// 提交 + final String qSubmit; + /// 下一步 + final String qNext; + /// 问卷结束 + final String qEndTitle; + /// 感谢您的参与,我们会尽快审核 + final String qEndThanks; + /// 很遗憾,您暂不符合内测条件 + final String qEndNotQualified; + /// 请输入有效的Gmail邮箱 + final String qInvalidEmail; + /// 提交中... + final String qSubmitting; + /// 提交成功 + final String qSubmitSuccess; + /// 提交失败,请稍后重试 + final String qSubmitFailed; + // ==================== toMap ==================== Map toMap() => { @@ -185,6 +243,25 @@ class TBeta { // 通用 'comingSoon': comingSoon, 'gotIt': gotIt, + // 问卷 + 'questionnaireBtn': questionnaireBtn, + 'questionnaireTitle': questionnaireTitle, + 'q1KnowGooglePlay': q1KnowGooglePlay, + 'q2HasGmsDevice': q2HasGmsDevice, + 'q3WillingToBeta': q3WillingToBeta, + 'q4EnterGmail': q4EnterGmail, + 'q4GmailHint': q4GmailHint, + 'qYes': qYes, + 'qNo': qNo, + 'qSubmit': qSubmit, + 'qNext': qNext, + 'qEndTitle': qEndTitle, + 'qEndThanks': qEndThanks, + 'qEndNotQualified': qEndNotQualified, + 'qInvalidEmail': qInvalidEmail, + 'qSubmitting': qSubmitting, + 'qSubmitSuccess': qSubmitSuccess, + 'qSubmitFailed': qSubmitFailed, }; // ==================== fromMap ==================== @@ -231,5 +308,24 @@ class TBeta { // 通用 comingSoon: map['comingSoon']?.isNotEmpty == true ? map['comingSoon']! : (fallback?.comingSoon ?? ''), gotIt: map['gotIt']?.isNotEmpty == true ? map['gotIt']! : (fallback?.gotIt ?? ''), + // 问卷 + questionnaireBtn: map['questionnaireBtn']?.isNotEmpty == true ? map['questionnaireBtn']! : (fallback?.questionnaireBtn ?? ''), + questionnaireTitle: map['questionnaireTitle']?.isNotEmpty == true ? map['questionnaireTitle']! : (fallback?.questionnaireTitle ?? ''), + q1KnowGooglePlay: map['q1KnowGooglePlay']?.isNotEmpty == true ? map['q1KnowGooglePlay']! : (fallback?.q1KnowGooglePlay ?? ''), + q2HasGmsDevice: map['q2HasGmsDevice']?.isNotEmpty == true ? map['q2HasGmsDevice']! : (fallback?.q2HasGmsDevice ?? ''), + q3WillingToBeta: map['q3WillingToBeta']?.isNotEmpty == true ? map['q3WillingToBeta']! : (fallback?.q3WillingToBeta ?? ''), + q4EnterGmail: map['q4EnterGmail']?.isNotEmpty == true ? map['q4EnterGmail']! : (fallback?.q4EnterGmail ?? ''), + q4GmailHint: map['q4GmailHint']?.isNotEmpty == true ? map['q4GmailHint']! : (fallback?.q4GmailHint ?? ''), + qYes: map['qYes']?.isNotEmpty == true ? map['qYes']! : (fallback?.qYes ?? ''), + qNo: map['qNo']?.isNotEmpty == true ? map['qNo']! : (fallback?.qNo ?? ''), + qSubmit: map['qSubmit']?.isNotEmpty == true ? map['qSubmit']! : (fallback?.qSubmit ?? ''), + qNext: map['qNext']?.isNotEmpty == true ? map['qNext']! : (fallback?.qNext ?? ''), + qEndTitle: map['qEndTitle']?.isNotEmpty == true ? map['qEndTitle']! : (fallback?.qEndTitle ?? ''), + qEndThanks: map['qEndThanks']?.isNotEmpty == true ? map['qEndThanks']! : (fallback?.qEndThanks ?? ''), + qEndNotQualified: map['qEndNotQualified']?.isNotEmpty == true ? map['qEndNotQualified']! : (fallback?.qEndNotQualified ?? ''), + qInvalidEmail: map['qInvalidEmail']?.isNotEmpty == true ? map['qInvalidEmail']! : (fallback?.qInvalidEmail ?? ''), + qSubmitting: map['qSubmitting']?.isNotEmpty == true ? map['qSubmitting']! : (fallback?.qSubmitting ?? ''), + qSubmitSuccess: map['qSubmitSuccess']?.isNotEmpty == true ? map['qSubmitSuccess']! : (fallback?.qSubmitSuccess ?? ''), + qSubmitFailed: map['qSubmitFailed']?.isNotEmpty == true ? map['qSubmitFailed']! : (fallback?.qSubmitFailed ?? ''), ); } diff --git a/lib/l10n/types/t_settings_cache.dart b/lib/l10n/types/t_settings_cache.dart index 8498cc59..bd81fb55 100644 --- a/lib/l10n/types/t_settings_cache.dart +++ b/lib/l10n/types/t_settings_cache.dart @@ -408,11 +408,11 @@ class TSettingsCache { final String pendingSync; /// Hive存储 final String hiveStorage; - /// 聊天会话 + /// 会话 final String chatSessions; - /// 聊天附件 + /// 会话附件 final String chatAttachments; - /// 聊天回收站 + /// 会话回收站 final String chatTrash; /// 传输记录 final String transferRecords; @@ -434,7 +434,7 @@ class TSettingsCache { final String clearAllCache2; /// 清理传输缓存 final String cleanTransferCache; - /// 清理全部聊天数据 + /// 清理全部会话数据 final String clearAllChatData; /// 清理稍后读缓存 final String cleanReadlaterCache; @@ -464,7 +464,7 @@ class TSettingsCache { final String cleaningChatThumbnails; /// 传输缓存已清理 final String cleaningTransferCache; - /// 全部聊天数据已清除 + /// 全部会话数据已清除 final String clearingAllChatData; /// 稍后读缓存已清理 final String cleaningReadlaterCache; @@ -474,7 +474,7 @@ class TSettingsCache { final String allCacheCleared; /// 清理失败 final String cleanFailed2; - /// 清理聊天回收站 + /// 清理会话回收站 final String confirmCleanChatTrashTitle; /// 将永久删除回收站中超过30天的消息和文件,此操作不可撤销。 final String confirmCleanChatTrashContent; @@ -490,9 +490,9 @@ class TSettingsCache { final String confirmCleanTransferCacheTitle; /// 将清理传输缩略图、临时文件和超过30天的传输记录。已接收的文件不会被删除。 final String confirmCleanTransferCacheContent; - /// 清理全部聊天数据 + /// 清理全部会话数据 final String confirmClearAllChatDataTitle; - /// 将删除所有聊天会话、消息、附件和回收站数据,此操作不可撤销! + /// 将删除所有会话、消息、附件和回收站数据,此操作不可撤销! final String confirmClearAllChatDataContent; /// 清理稍后读缓存 final String confirmCleanReadlaterCacheTitle; diff --git a/lib/shared/widgets/feedback/agreement_consent_row.dart b/lib/shared/widgets/feedback/agreement_consent_row.dart new file mode 100644 index 00000000..a26d7252 --- /dev/null +++ b/lib/shared/widgets/feedback/agreement_consent_row.dart @@ -0,0 +1,127 @@ +/// ============================================================ +/// 闲言APP — 协议同意行组件 +/// 创建时间: 2026-06-15 +/// 更新时间: 2026-06-15 +/// 作用: 统一的协议勾选+链接跳转组件,供登录/注册页面复用 +/// 上次更新: 初始创建 +/// ============================================================ + +import 'package:flutter/cupertino.dart'; + +import '../../../core/router/app_nav_extension.dart'; +import '../../../core/theme/app_spacing.dart'; +import '../../../core/theme/app_theme.dart'; +import '../../../core/theme/app_typography.dart'; +import '../../../features/agreements/data/agreement_types.dart'; + +/// 协议同意行 — 勾选框 + "我已阅读同意《账户使用协议》《用户服务协议》《隐私政策》" +/// +/// 点击协议链接跳转对应页面,勾选状态由父组件管理。 +class AgreementConsentRow extends StatelessWidget { + /// 是否已同意协议 + final bool agreed; + + /// 勾选状态变更回调 + final ValueChanged onChanged; + + /// 主题扩展 + final AppThemeExtension ext; + + const AgreementConsentRow({ + super.key, + required this.agreed, + required this.onChanged, + required this.ext, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: AppSpacing.sm), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 勾选框 + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => onChanged(!agreed), + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 44, minHeight: 44), + child: Center( + child: Container( + width: 20, + height: 20, + margin: const EdgeInsets.only(top: 1), + decoration: BoxDecoration( + color: agreed ? ext.accent : CupertinoColors.transparent, + borderRadius: BorderRadius.circular(5), + border: Border.all( + color: agreed ? ext.accent : ext.textHint, + width: 1.5, + ), + ), + child: agreed + ? Icon( + CupertinoIcons.checkmark, + size: 13, + color: ext.textOnAccent, + ) + : null, + ), + ), + ), + ), + const SizedBox(width: AppSpacing.sm), + // 协议文本 + Expanded( + child: Text.rich( + TextSpan( + text: '我已阅读同意', + style: AppTypography.footnote.copyWith( + color: ext.textSecondary, + ), + children: [ + _buildAgreementLink( + context, + text: '《账户使用协议》', + type: AgreementType.accountAgreement, + ), + _buildAgreementLink( + context, + text: '《用户服务协议》', + type: AgreementType.userServiceAgreement, + ), + _buildAgreementLink( + context, + text: '《隐私政策》', + type: AgreementType.privacyPolicy, + ), + ], + ), + ), + ), + ], + ), + ); + } + + /// 构建可点击的协议链接 + WidgetSpan _buildAgreementLink( + BuildContext context, { + required String text, + required AgreementType type, + }) { + return WidgetSpan( + child: GestureDetector( + onTap: () => context.appPush('/agreement/${type.route}'), + child: Text( + text, + style: AppTypography.footnote.copyWith( + color: ext.accent, + fontWeight: FontWeight.w600, + ), + ), + ), + ); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 50061c02..5c214503 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -15,7 +15,6 @@ #include #include #include -#include #include #include @@ -47,9 +46,6 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); - g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); - sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 0ac25a19..de9de248 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -12,7 +12,6 @@ list(APPEND FLUTTER_PLUGIN_LIST record_linux rive_native screen_retriever_linux - sqlite3_flutter_libs url_launcher_linux window_manager ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 6a19590a..a73737b9 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -35,7 +35,6 @@ import share_plus import shared_preferences_foundation import speech_to_text import sqflite_darwin -import sqlite3_flutter_libs import url_launcher_macos import video_compress import video_player_avfoundation @@ -73,7 +72,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SpeechToTextPlugin.register(with: registry.registrar(forPlugin: "SpeechToTextPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) - Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin")) VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) diff --git a/ohos/entry/src/main/ets/formability/CtcLatestNoteFormAbility.ets b/ohos/entry/src/main/ets/formability/CtcLatestNoteFormAbility.ets new file mode 100644 index 00000000..a4466437 --- /dev/null +++ b/ohos/entry/src/main/ets/formability/CtcLatestNoteFormAbility.ets @@ -0,0 +1,61 @@ +/** + * @File: CtcLatestNoteFormAbility.ets + * @Create: 2026-06-15 + * @Update: 2026-06-15 + * @Name: CTC最新笔记卡片Ability + * @Desc: 鸿蒙桌面卡片-CTC最新笔记数据提供 + * @LastUpdate: 初始创建,支持读取笔记钥匙/内容/时间数据 + */ +import FormExtensionAbility from '@ohos.app.form.FormExtensionAbility'; +import formBindingData from '@ohos.app.form.formBindingData'; +import dataPreferences from '@ohos.data.preferences'; + +const PREFS_NAME = 'home_widget_data'; + +/** 从 dataPreferences 读取CTC最新笔记数据 */ +function readPrefs(context: Context): Record { + const formData: Record = { + 'noteKey': '', + 'noteContent': '', + 'noteTime': '', + 'isDark': 'false', + }; + try { + const prefs = dataPreferences.getPreferencesSync(context, { name: PREFS_NAME }); + const noteKey = prefs.getSync('ctc_latest_note_key', '') as string; + const noteContent = prefs.getSync('ctc_latest_note_content', '') as string; + const noteTime = prefs.getSync('ctc_latest_note_time', '') as string; + const isDark = prefs.getSync('widget_theme_mode', 'light') as string === 'dark'; + formData['noteKey'] = noteKey; + formData['noteContent'] = noteContent; + formData['noteTime'] = noteTime; + formData['isDark'] = isDark ? 'true' : 'false'; + } catch (e) { + console.error(`CtcLatestNoteFormAbility read prefs error: ${e}`); + } + return formData; +} + +export default class CtcLatestNoteFormAbility extends FormExtensionAbility { + /** 卡片添加时提供初始数据 */ + onAddForm(want: object): formBindingData.FormBindingData { + const formData = readPrefs(this.context); + return formBindingData.createFormBindingData(formData); + } + + /** 卡片更新时刷新数据 */ + onUpdateForm(formId: string): formBindingData.FormBindingData { + const formData = readPrefs(this.context); + return formBindingData.createFormBindingData(formData); + } + + /** 处理卡片事件(如刷新) */ + onFormEvent(formId: string, message: string): void { + console.info(`CtcLatestNoteFormAbility onFormEvent: formId=${formId}, message=${message}`); + } + + /** 卡片删除时清理 */ + onRemoveForm(formId: string): void { + console.info(`CtcLatestNoteFormAbility onRemoveForm: ${formId}`); + } +} diff --git a/ohos/entry/src/main/ets/formability/pages/CtcLatestNoteFormPage.ets b/ohos/entry/src/main/ets/formability/pages/CtcLatestNoteFormPage.ets new file mode 100644 index 00000000..0812e150 --- /dev/null +++ b/ohos/entry/src/main/ets/formability/pages/CtcLatestNoteFormPage.ets @@ -0,0 +1,83 @@ +/** + * @file CtcLatestNoteFormPage.ets + * @created 2026-06-15 + * @updated 2026-06-15 + * @name CTC最新笔记卡片组件 + * @desc CTC最新笔记小组件UI,展示笔记钥匙名、内容预览和更新时间 + * @lastUpdate: 初始创建,2x2尺寸卡片 + */ +const ctcLatestNoteStorage: LocalStorage = new LocalStorage(); + +@Entry(ctcLatestNoteStorage) +@Component +struct CtcLatestNoteFormPage { + @LocalStorageProp('noteKey') noteKey: string = '' + @LocalStorageProp('noteContent') noteContent: string = '' + @LocalStorageProp('noteTime') noteTime: string = '' + @LocalStorageProp('isDark') isDark: string = 'false' + + build() { + Column() { + // 顶部:笔记图标 + 钥匙名 + Row() { + Text('📝') + .fontSize(16) + Text(this.noteKey.length > 0 ? this.noteKey : '暂无笔记') + .fontSize(14) + .fontWeight(FontWeight.Bold) + .fontColor(this.isDark === 'true' ? '#E0E0E0' : '#333333') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .layoutWeight(1) + .margin({ left: 6 }) + } + .width('100%') + .padding({ left: 12, right: 12, top: 12 }) + .alignItems(VerticalAlign.Center) + + // 内容预览 + if (this.noteContent.length > 0) { + Text(this.noteContent) + .fontSize(12) + .fontColor(this.isDark === 'true' ? '#AAAAAA' : '#666666') + .maxLines(2) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .lineHeight(18) + .width('100%') + .padding({ left: 12, right: 12, top: 8 }) + } else { + Text('点击打开CTC创建笔记') + .fontSize(11) + .fontColor(this.isDark === 'true' ? '#777777' : '#999999') + .width('100%') + .padding({ left: 12, right: 12, top: 8 }) + } + + Blank() + + // 底部:更新时间 + if (this.noteTime.length > 0) { + Text(this.noteTime) + .fontSize(10) + .fontColor(this.isDark === 'true' ? '#666666' : '#BBBBBB') + .width('100%') + .textAlign(TextAlign.End) + .padding({ right: 12, bottom: 10 }) + } + } + .width('100%') + .height('100%') + .borderRadius(16) + .backgroundColor(this.isDark === 'true' ? '#1C1C1E' : '#FFFFFF') + .alignItems(HorizontalAlign.Start) + .onClick(() => { + postCardAction(this, { + action: 'router', + abilityName: 'EntryAbility', + params: { + uri: '/ctc', + }, + }); + }) + } +} diff --git a/ohos/entry/src/main/resources/base/profile/ctc_latest_note_form.json b/ohos/entry/src/main/resources/base/profile/ctc_latest_note_form.json new file mode 100644 index 00000000..c077a402 --- /dev/null +++ b/ohos/entry/src/main/resources/base/profile/ctc_latest_note_form.json @@ -0,0 +1,22 @@ +{ + "forms": [ + { + "name": "ctcLatestNote", + "displayName": "CTC最新笔记", + "description": "展示最新笔记的钥匙名和内容预览", + "src": "./ets/formability/pages/CtcLatestNoteFormPage.ets", + "uiSyntax": "arkts", + "window": { + "designWidth": 720, + "autoDesignWidth": true + }, + "colorMode": "auto", + "isDefault": false, + "updateEnabled": true, + "scheduledUpdateTime": "10:30", + "updateDuration": 1, + "defaultDimension": "2*2", + "supportDimensions": ["2*2"] + } + ] +} diff --git a/pubspec.macos.yaml b/pubspec.macos.yaml index 2a17df5c..6bed4016 100644 --- a/pubspec.macos.yaml +++ b/pubspec.macos.yaml @@ -1,10 +1,10 @@ # ============================================================ # 闲言APP (Xianyan) — MacBook Pro端 pubspec 模板 # 创建时间: 2026-06-02 -# 更新时间: 2026-06-07 +# 更新时间: 2026-06-15 # 作用: MacBook Pro端(iOS/macOS)依赖与资源配置模板(使用远程版本号) -# 上次更新: 更新文件头部注释,增加版本号保护警告 -# 使用方式: 运行 tools/setup_pubspec.ps1 --platform macos 自动生成 pubspec.yaml +# 上次更新: 同步pubspec.yaml依赖升级 + 删除custom_lint/riverpod_lint + 新增analyzer/test_api/test overrides +# 使用方式: # ⚠️ 此文件为模板,不要直接重命名为 pubspec.yaml 使用 # ⚠️ 新增三方库时,必须同步更新 pubspec.ohos.yaml 和 pubspec.macos.yaml # ============================================================ @@ -21,7 +21,7 @@ name: xianyan description: "闲言 — 灵感语录更纯粹。每日拾句 + 壁纸创作 APP" publish_to: 'none' -version: 6.6.13+2606132 +version: 6.6.16+2606152 # 年月日-次 7位 environment: @@ -38,30 +38,30 @@ dependencies: cupertino_icons: ^1.0.9 # iOS风格图标库 # --- 状态管理 + 依赖注入 --- - flutter_riverpod: ^3.0.0 # 响应式状态管理+依赖注入 - riverpod_annotation: ^4.0.0 # Riverpod代码生成注解 + flutter_riverpod: ^3.3.0 # 响应式状态管理+依赖注入 + riverpod_annotation: ^4.0.3 # Riverpod代码生成注解 # --- 路由 --- - go_router: ^17.2.3 # 声明式路由导航(纯Dart-鸿蒙零适配) + go_router: ^17.3.0 # 声明式路由导航(纯Dart-鸿蒙零适配) # --- 网络请求 --- - dio: ^5.4.0 # HTTP客户端+拦截器 - dio_cache_interceptor: ^3.5.0 # Dio HTTP缓存拦截器 + dio: ^5.9.0 # HTTP客户端+拦截器 + dio_cache_interceptor: ^4.0.0 # Dio HTTP缓存拦截器 # --- 本地数据库 --- - drift: ^2.16.0 # 类型安全SQLite ORM - sqlite3_flutter_libs: ^0.5.0 # SQLite原生库绑定 + drift: ^2.33.0 # 类型安全SQLite ORM + sqlite3: ^3.0.0 # SQLite原生库绑定 # --- 数据模型 --- - freezed_annotation: ^3.0.0 # 不可变数据类注解 - json_annotation: ^4.9.0 # JSON序列化注解 + freezed_annotation: ^3.0.6 # 不可变数据类注解 + json_annotation: ^4.12.0 # JSON序列化注解 # --- KV 存储 --- shared_preferences: ^2.5.5 # 轻量KV持久化 - flutter_secure_storage: ^10.2.0 # 加密安全存储 - hive_ce: ^2.0.0 # 高性能NoSQL数据库(社区维护版) - hive_flutter: ^1.1.0 # Hive Flutter适配 + flutter_secure_storage: ^10.3.0 # 加密安全存储 + hive_ce: ^2.19.0 # 高性能NoSQL数据库(社区维护版) + hive_ce_flutter: ^2.3.4 # Hive CE Flutter适配 # --- 文件路径 --- path_provider: ^2.1.5 # 系统目录路径获取 @@ -71,10 +71,10 @@ dependencies: uuid: ^4.5.0 # UUID生成器 intl: ^0.20.2 # 国际化+日期格式化 timeago: ^3.7.0 # 相对时间格式化(国际化) - logger: ^2.5.0 # 分级日志输出 - collection: ^1.19.0 # 集合操作扩展 + logger: ^2.7.0 # 分级日志输出 + collection: ^1.19.1 # 集合操作扩展 - syncfusion_flutter_charts: ^33.2.10 # Syncfusion图表库(替代fl_chart) + syncfusion_flutter_charts: ^33.2.12 # Syncfusion图表库(替代fl_chart) # --- 设备信息 --- package_info_plus: ^10.1.0 # 应用包信息读取 @@ -82,14 +82,14 @@ dependencies: device_info_plus: ^13.1.0 # 设备硬件信息读取 # --- 日历同步 --- - device_calendar: ^4.3.3 # 跨平台日历事件读写 + device_calendar_plus: ^0.4.0 # 跨平台日历事件读写 # --- 权限 --- permission_handler: ^12.0.1 # 运行时权限请求 app_tracking_transparency: ^2.0.6 # iOS App Tracking Transparency授权 # --- 本地通知 --- - flutter_local_notifications: ^21.0.0 # 本地推送通知 + flutter_local_notifications: ^22.0.0 # 本地推送通知 # --- 后台任务调度 --- workmanager: ^0.9.0 # 后台任务调度 @@ -102,10 +102,10 @@ dependencies: quick_actions: ^1.1.0 # 主屏幕快捷操作(iOS Quick Actions / Android App Shortcuts) # --- 桌面小组件 --- - home_widget: ^0.9.1 # iOS/Android桌面小组件 + home_widget: ^0.9.3 # iOS/Android桌面小组件 # --- iOS 26 Liquid Glass 组件 --- - liquid_glass_widgets: ^0.11.0 # iOS26液态玻璃组件库 + liquid_glass_widgets: ^0.16.0 # iOS26液态玻璃组件库 liquid_glass_easy: ^1.1.1 # 液态玻璃效果封装 # --- 底部面板 + Hero 动画 --- @@ -119,8 +119,8 @@ dependencies: # --- UI 基础 --- badges: ^3.2.0 # 角标/徽章组件 google_fonts: ^8.1.0 # Google字体加载 - cached_network_image: ^3.3.0 # 网络图片缓存+占位 - flutter_cache_manager: ^3.3.0 # 文件缓存管理 + cached_network_image: ^3.4.0 # 网络图片缓存+占位 + flutter_cache_manager: ^3.4.0 # 文件缓存管理 shimmer: ^3.0.0 # 骨架屏加载占位 # --- 分享 + 导出 --- @@ -133,23 +133,23 @@ dependencies: mailer: ^7.1.0 # SMTP邮件发送 # --- 图片处理 --- - image: ^4.3.0 # 图片解码/编码/变换 + image: ^4.9.0 # 图片解码/编码/变换 # --- 图片编辑器 --- - pro_image_editor: ^12.4.4 # 图片编辑器核心(官方版) + pro_image_editor: ^12.5.0 # 图片编辑器核心(官方版) # --- 桌面端增强 --- - desktop_drop: ^0.5.0 # 桌面端文件拖放接收 + desktop_drop: ^0.7.0 # 桌面端文件拖放接收 window_manager: ^0.5.1 # 桌面端窗口管理(替代bitsdojo_window) # --- 异常捕获 --- catcher_2: ^2.1.9 # 全局异常捕获+上报 # --- SVG 渲染 --- - flutter_svg: ^2.0.0 # SVG图片渲染 + flutter_svg: ^2.3.0 # SVG图片渲染 # --- 富文本编辑器 --- - flutter_quill: ^11.5.0 # Quill富文本编辑器 + flutter_quill: ^11.5.0 # Quill富文本编辑器(11.5.1需Dart3.12+) # --- 虚线边框 --- dotted_border: ^3.1.0 # 虚线/点线边框装饰 @@ -161,14 +161,14 @@ dependencies: flutter_keyboard_visibility: ^6.0.0 # 键盘可见性监听(替代MediaQuery轮询) # --- 屏幕适配 --- - flutter_screenutil: ^5.9.0 # 屏幕尺寸适配 + flutter_screenutil: ^5.9.3 # 屏幕尺寸适配 # --- 动画 --- - rive: ^0.14.7 # Rive交互式动画引擎 - flutter_animate: ^4.5.0 # 声明式动画库 + rive: ^0.14.8 # Rive交互式动画引擎 + flutter_animate: ^4.5.2 # 声明式动画库 flutter_card_swiper: ^7.2.0 # 卡片滑动切换 - lottie: ^3.3.0 # Lottie动画播放 + lottie: ^3.3.3 # Lottie动画播放 confetti: ^0.8.0 # 撒花/彩纸效果 @@ -182,17 +182,17 @@ dependencies: # --- 内容渲染 --- - flutter_markdown_plus: ^1.0.1 # Markdown渲染 - flutter_html: ^3.0.0-beta.2 # HTML内容渲染 + flutter_markdown_plus: ^1.0.7 # Markdown渲染 + flutter_html: ^3.0.0 # HTML内容渲染 # --- RSS订阅 --- - rss_dart: ^1.0.12 # RSS/Atom订阅源解析(Dart3兼容webfeed分支) + rss_dart: ^1.0.14 # RSS/Atom订阅源解析(Dart3兼容webfeed分支) # --- 拼音转换 --- pinyin: ^3.3.0 # 汉字转拼音 # --- 语音朗读 --- - flutter_tts: ^4.2.0 # TTS文本转语音朗读 + flutter_tts: ^4.2.5 # TTS文本转语音朗读 # --- 语音识别 --- speech_to_text: ^7.4.0 # 语音转文字 @@ -201,7 +201,7 @@ dependencies: live_activities: ^2.4.9 # 灵动岛/实时活动 # --- iOS风格组件 --- - pull_down_button: ^0.10.1 # iOS下拉菜单按钮 + pull_down_button: ^0.10.2 # iOS下拉菜单按钮 # --- 布局增强 --- sliver_tools: ^0.2.12 # Sliver工具集 @@ -214,40 +214,41 @@ dependencies: url: https://gitcode.com/openharmony-sig/fluttertpc_flutter_vibrate.git # 跨平台触觉反馈(iOS/Android/HarmonyOS) # --- 提示反馈 --- - bot_toast: ^4.1.0 # Toast/通知弹窗 + bot_toast: ^4.1.3 # Toast/通知弹窗 # --- Shader效果 --- - flutter_shaders_ui: ^0.1.0 # Fragment Shader效果 - flutter_tilt: ^4.0.0 # 3D倾斜交互效果 + flutter_shaders_ui: ^1.0.0 # Fragment Shader效果 + flutter_tilt: ^4.0.4 # 3D倾斜交互效果 flutter_3d_controller: 2.3.0 # 3D模型加载控制 - flutter_spritesheet_animation: ^1.0.1 # 精灵图帧动画 + flutter_spritesheet_animation: ^1.0.3 # 精灵图帧动画 image_size_getter: ^2.4.1 # 图片尺寸读取(无需解码) extended_image: ^10.0.1 # 图片缓存+缩放+裁剪 photo_view: ^0.15.0 # 图片缩放/平移查看 flutter_image_compress: ^2.4.0 # 图片压缩(保持EXIF) - wakelock_plus: ^1.4.0 # 屏幕常亮控制 + wakelock_plus: ^1.6.0 # 屏幕常亮控制 audioplayers: ^6.5.0 # 音频播放 - record: ^6.0.0 # 录音 - video_compress: ^3.1.2 # 视频压缩 - video_player: ^2.10.0 # 视频播放 + record: ^6.2.1 # 录音(7.0.0需Dart3.12+) + video_compress: ^3.1.4 # 视频压缩 + video_player: ^2.11.0 # 视频播放 local_auth: ^3.0.1 # 生物识别认证 battery_plus: ^7.0.0 # 电池状态监听 # --- 文件传输助手 --- - shelf: ^1.4.0 # HTTP服务器框架 - shelf_router: ^1.1.0 # 路由中间件 + shelf: ^1.4.2 # HTTP服务器框架 + shelf_router: ^1.1.4 # 路由中间件 shelf_web_socket: ^3.0.0 # WebSocket支持 network_info_plus: ^8.1.0 # WiFi网络信息 flutter_webrtc: ^1.4.0 # WebRTC音视频通信 web_socket_channel: ^3.0.3 # WebSocket客户端 mime: ^2.0.0 # MIME类型识别 - mobile_scanner: ^7.1.4 # 二维码/条形码扫描 - basic_utils: ^5.7.0 # 通用工具集(Base64/ASN1) + mobile_scanner: ^7.2.0 # 二维码/条形码扫描 + basic_utils: ^5.8.0 # 通用工具集(Base64/ASN1) wifi_iot: ^0.3.19 # WiFi IoT设备连接 nearby_service: ^0.2.1 # 近场设备发现+通信 + nearby_connections: ^4.1.1 # Google Nearby Connections(蓝牙发现+Wi-Fi Direct传输,仅Android/iOS) flutter_localizations: sdk: flutter # Flutter国际化支持 @@ -269,16 +270,16 @@ dev_dependencies: sdk: flutter # 代码生成 - 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代码生成 + build_runner: ^2.15.0 # 代码生成运行器 + freezed: ^3.2.5 # 不可变数据类生成 + json_serializable: ^6.14.0 # JSON序列化代码生成 + drift_dev: ^2.33.0 # Drift数据库代码生成 + riverpod_generator: ^4.0.4 # Riverpod Provider代码生成 # 代码规范 - flutter_lints: ^5.0.0 # Flutter lint规则 - riverpod_lint: ^3.0.0 # Riverpod专用lint - custom_lint: ^0.8.0 # 自定义lint插件 + flutter_lints: ^6.0.0 # Flutter lint规则 + # riverpod_lint: ^3.1.0 # Riverpod专用lint(需custom_lint,与json_serializable analyzer版本冲突,待SDK升级后恢复) + # custom_lint: ^0.8.0 # 自定义lint插件(analyzer ^7.5.0与json_serializable analyzer>=10冲突) # 测试 mocktail: ^1.0.0 # Mock测试库 @@ -289,27 +290,46 @@ dev_dependencies: # 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. device_calendar ^4.3.3 依赖 timezone ^0.9.0(<0.10.0) +# 3. device_calendar_plus ^0.4.0 依赖 timezone ^0.9.0(<0.10.0) # 但 flutter_local_notifications 依赖 timezone ^0.11.0(<0.12.0) # timezone 0.9→0.11 API兼容(仅时区数据更新),强制使用^0.11.0 +# 4. rss_dart依赖xml ^6.5.0,image依赖xml ^7.0.1,强制使用7.x(向后兼容) +# 5. encrypt ^5.0.3依赖pointycastle ^3.6.2,basic_utils ^5.8.0依赖pointycastle ^4.0.0 +# 强制使用pointycastle ^4.0.0(encrypt内部已兼容) +# 6. riverpod_generator 4.0.4 / freezed 3.2.5 / json_serializable 6.14.0 / drift_dev 2.33.0 +# 均需analyzer>=10.0.0,但flutter_test SDK锁定test_api 0.7.10 +# 强制analyzer ^12.0.0 + test_api 0.7.12 + test ^1.31.1 绕过SDK限制 # ============================================================ dependency_overrides: meta: ^1.17.0 web: ^1.1.0 timezone: ^0.11.0 win32: ^6.0.1 + xml: ^7.0.1 + pointycastle: ^4.0.0 + analyzer: ^12.0.0 + test_api: 0.7.12 + test: ^1.31.1 quill_native_bridge_windows: # 本地修补版(兼容win32 6.x) path: packages/quill_native_bridge_windows +hooks: + user_defines: + sqlite3: + source: system + # ============================================================ # Flutter 配置 # ============================================================ flutter: + config: + enable-lldb-debugging: false uses-material-design: true assets: - assets/animations/ - assets/images/ + - assets/images/empty/ - assets/templates/resized/ - assets/svgs/ - assets/svgs/categories/ diff --git a/pubspec.ohos.yaml b/pubspec.ohos.yaml index e90b564f..cc84be6e 100644 --- a/pubspec.ohos.yaml +++ b/pubspec.ohos.yaml @@ -1,10 +1,10 @@ # ============================================================ # 闲言APP (Xianyan) — 鸿蒙端 pubspec 模板 # 创建时间: 2026-04-20 -# 更新时间: 2026-06-07 +# 更新时间: 2026-06-15 # 作用: 鸿蒙端依赖与资源配置模板(使用本地 packages/ 目录) -# 上次更新: 更新文件头部注释,增加版本号保护警告 -# 使用方式: 运行 tools/setup_pubspec.ps1 --platform ohos 自动生成 pubspec.yaml +# 上次更新: 同步pubspec.yaml依赖升级 + 删除custom_lint/riverpod_lint + 新增analyzer/test_api/test overrides + record降级到^6.2.1 +# 使用方式: # ⚠️ 此文件为模板,不要直接重命名为 pubspec.yaml 使用 # ============================================================ # 🚫🚫🚫 版本号保护警告 🚫🚫🚫 @@ -20,7 +20,7 @@ name: xianyan description: "闲言 — 灵感语录更纯粹。每日拾句 + 壁纸创作 APP" publish_to: 'none' -version: 6.6.13+2606132 +version: 6.6.16+2606152 # 年月日-次 7位 environment: @@ -38,32 +38,32 @@ dependencies: cupertino_icons: ^1.0.9 # iOS风格图标库 # --- 状态管理 + 依赖注入 --- - flutter_riverpod: ^3.0.0 # 响应式状态管理+依赖注入 - riverpod_annotation: ^4.0.0 # Riverpod代码生成注解 + flutter_riverpod: ^3.3.0 # 响应式状态管理+依赖注入 + riverpod_annotation: ^4.0.3 # Riverpod代码生成注解 # --- 路由 --- - go_router: ^17.2.3 # 声明式路由导航(纯Dart-鸿蒙零适配) + go_router: ^17.3.0 # 声明式路由导航(纯Dart-鸿蒙零适配) # --- 网络请求 --- - dio: ^5.4.0 # HTTP客户端+拦截器 - dio_cache_interceptor: ^3.5.0 # Dio HTTP缓存拦截器 + dio: ^5.9.0 # HTTP客户端+拦截器 + dio_cache_interceptor: ^4.0.0 # Dio HTTP缓存拦截器 # --- 本地数据库 --- - drift: ^2.16.0 # 类型安全SQLite ORM - sqlite3_flutter_libs: ^0.5.0 # SQLite原生库绑定 + drift: ^2.33.0 # 类型安全SQLite ORM + sqlite3: ^3.0.0 # SQLite原生库绑定 # --- 数据模型 --- - freezed_annotation: ^3.0.0 # 不可变数据类注解 - json_annotation: ^4.9.0 # JSON序列化注解 + freezed_annotation: ^3.0.6 # 不可变数据类注解 + json_annotation: ^4.12.0 # JSON序列化注解 # --- KV 存储 --- shared_preferences: # v2.5.5 | 轻量KV持久化(本地化-鸿蒙适配) path: packages/shared_preferences - flutter_secure_storage: # v9.2.4 | 加密安全存储(本地化-鸿蒙适配) + flutter_secure_storage: # v10.3.0 | 加密安全存储(本地化-鸿蒙适配) path: packages/flutter_secure_storage - hive_ce: ^2.0.0 # 高性能NoSQL数据库(社区维护版) - hive_flutter: # v1.1.0-ohos.2 | Hive Flutter适配(本地化-鸿蒙适配) + hive_ce: ^2.19.0 # 高性能NoSQL数据库(社区维护版) + hive_ce_flutter: # v2.3.4-ohos | Hive CE Flutter适配(本地化-鸿蒙适配) path: packages/hive_flutter # --- 文件路径 --- @@ -75,10 +75,10 @@ dependencies: uuid: ^4.5.0 # UUID生成器 intl: ^0.20.2 # 国际化+日期格式化 timeago: ^3.7.0 # 相对时间格式化(国际化) - logger: ^2.5.0 # 分级日志输出 - collection: ^1.19.0 # 集合操作扩展 + logger: ^2.7.0 # 分级日志输出 + collection: ^1.19.1 # 集合操作扩展 - syncfusion_flutter_charts: ^33.2.10 # Syncfusion图表库(替代fl_chart) + syncfusion_flutter_charts: ^33.2.12 # Syncfusion图表库(替代fl_chart) # --- 设备信息 --- package_info_plus: # v10.1.0 | 应用包信息读取(本地化-鸿蒙适配) @@ -89,7 +89,7 @@ dependencies: path: packages/device_info_plus # --- 日历同步 --- - device_calendar: ^4.3.3 # 跨平台日历事件读写 + device_calendar_plus: ^0.4.0 # 跨平台日历事件读写 # --- 权限 --- permission_handler: # v12.0.1 | 运行时权限请求(本地化-鸿蒙适配) @@ -97,7 +97,7 @@ dependencies: app_tracking_transparency: ^2.0.6 # iOS App Tracking Transparency授权(鸿蒙端不调用,仅保证编译通过) # --- 本地通知 --- - flutter_local_notifications: # v21.0.0 | 本地推送通知(本地化-鸿蒙适配) + flutter_local_notifications: # v22.0.0 | 本地推送通知(本地化-鸿蒙适配) path: packages/flutter_local_notifications # --- 后台任务调度 --- @@ -122,7 +122,7 @@ dependencies: path: packages/home_widget # --- iOS 26 Liquid Glass 组件 --- - liquid_glass_widgets: ^0.11.0 # iOS26液态玻璃组件库 + liquid_glass_widgets: ^0.16.0 # iOS26液态玻璃组件库 liquid_glass_easy: ^1.1.1 # 液态玻璃效果封装 # --- 底部面板 + Hero 动画 --- @@ -138,8 +138,8 @@ dependencies: # --- UI 基础 --- badges: ^3.2.0 # 角标/徽章组件 google_fonts: ^8.1.0 # Google字体加载 - cached_network_image: ^3.3.0 # 网络图片缓存+占位 - flutter_cache_manager: ^3.3.0 # 文件缓存管理 + cached_network_image: ^3.4.0 # 网络图片缓存+占位 + flutter_cache_manager: ^3.4.0 # 文件缓存管理 shimmer: ^3.0.0 # 骨架屏加载占位 # --- 分享 + 导出 --- @@ -154,20 +154,20 @@ dependencies: mailer: ^7.1.0 # SMTP邮件发送 # --- 图片处理 --- - image: ^4.3.0 # 图片解码/编码/变换 + image: ^4.9.0 # 图片解码/编码/变换 # --- 图片编辑器 --- - pro_image_editor: ^12.4.4 # v12.4.4 | 图片编辑器核心(官方版) + pro_image_editor: ^12.5.0 # v12.5.0 | 图片编辑器核心(官方版) # --- 桌面端增强 --- - desktop_drop: ^0.5.0 # 桌面端文件拖放接收 + desktop_drop: ^0.7.0 # 桌面端文件拖放接收 window_manager: ^0.5.1 # 桌面端窗口管理(替代bitsdojo_window) # --- 异常捕获 --- catcher_2: ^2.1.9 # 全局异常捕获+上报 # --- SVG 渲染 --- - flutter_svg: ^2.0.0 # SVG图片渲染 + flutter_svg: ^2.3.0 # SVG图片渲染 # --- 富文本编辑器 --- flutter_quill: # v11.5.0 | Quill富文本编辑器 @@ -184,14 +184,14 @@ dependencies: flutter_keyboard_visibility: ^6.0.0 # 键盘可见性监听(替代MediaQuery轮询) # --- 屏幕适配 --- - flutter_screenutil: ^5.9.0 # 屏幕尺寸适配 + flutter_screenutil: ^5.9.3 # 屏幕尺寸适配 # --- 动画 --- - rive: ^0.14.7 # Rive交互式动画引擎 - flutter_animate: ^4.5.0 # 声明式动画库 + rive: ^0.14.8 # Rive交互式动画引擎 + flutter_animate: ^4.5.2 # 声明式动画库 flutter_card_swiper: ^7.2.0 # 卡片滑动切换 - lottie: ^3.3.0 # Lottie动画播放 + lottie: ^3.3.3 # Lottie动画播放 confetti: ^0.8.0 # 撒花/彩纸效果 @@ -205,11 +205,11 @@ dependencies: # --- 内容渲染 --- - flutter_markdown_plus: ^1.0.1 # Markdown渲染 - flutter_html: ^3.0.0-beta.2 # HTML内容渲染 + flutter_markdown_plus: ^1.0.7 # Markdown渲染 + flutter_html: ^3.0.0 # HTML内容渲染 # --- RSS订阅 --- - rss_dart: ^1.0.12 # RSS/Atom订阅源解析(Dart3兼容webfeed分支) + rss_dart: ^1.0.14 # RSS/Atom订阅源解析(Dart3兼容webfeed分支) # --- 拼音转换 --- pinyin: ^3.3.0 # 汉字转拼音 @@ -227,7 +227,7 @@ dependencies: path: packages/live_activities # --- iOS风格组件 --- - pull_down_button: ^0.10.1 # iOS下拉菜单按钮 + pull_down_button: ^0.10.2 # iOS下拉菜单按钮 # --- 布局增强 --- sliver_tools: ^0.2.12 # Sliver工具集 @@ -240,14 +240,14 @@ dependencies: url: https://gitcode.com/openharmony-sig/fluttertpc_flutter_vibrate.git # 跨平台触觉反馈(iOS/Android/HarmonyOS) # --- 提示反馈 --- - bot_toast: ^4.1.0 # Toast/通知弹窗 + bot_toast: ^4.1.3 # Toast/通知弹窗 # --- Shader效果 --- - flutter_shaders_ui: ^0.1.0 # Fragment Shader效果 - flutter_tilt: ^4.0.0 # 3D倾斜交互效果 + flutter_shaders_ui: ^1.0.0 # Fragment Shader效果 + flutter_tilt: ^4.0.4 # 3D倾斜交互效果 flutter_3d_controller: 2.3.0 # 3D模型加载控制 - flutter_spritesheet_animation: ^1.0.1 # 精灵图帧动画 + flutter_spritesheet_animation: ^1.0.3 # 精灵图帧动画 image_size_getter: ^2.4.1 # 图片尺寸读取(无需解码) extended_image: ^10.0.1 # 图片缓存+缩放+裁剪 photo_view: ^0.15.0 # 图片缩放/平移查看 @@ -255,15 +255,15 @@ dependencies: path: packages/flutter_image_compress - wakelock_plus: # v1.4.0-ohos.1 | 屏幕常亮控制(本地化-鸿蒙适配) + wakelock_plus: # v1.6.0-ohos.1 | 屏幕常亮控制(本地化-鸿蒙适配) path: packages/wakelock_plus audioplayers: # v6.5.0-ohos.1 | 音频播放(本地化-鸿蒙适配) path: packages/audioplayers - record: # v6.0.0-ohos.1 | 录音(本地化-鸿蒙适配) + record: # v6.2.1-ohos.1 | 录音(本地化-鸿蒙适配,7.0.0需Dart3.12+) path: packages/record - video_compress: # v3.1.2-ohos.1 | 视频压缩(本地化-鸿蒙适配) + video_compress: # v3.1.4-ohos.1 | 视频压缩(本地化-鸿蒙适配) path: packages/video_compress - video_player: # v2.10.0-ohos.1 | 视频播放(本地化-鸿蒙适配) + video_player: # v2.11.0-ohos.1 | 视频播放(本地化-鸿蒙适配) path: packages/video_player local_auth: # v3.0.1 | 生物识别认证(本地化-鸿蒙适配) path: packages/local_auth @@ -271,8 +271,8 @@ dependencies: path: packages/battery_plus # --- 文件传输助手 --- - shelf: ^1.4.0 # HTTP服务器框架 - shelf_router: ^1.1.0 # 路由中间件 + shelf: ^1.4.2 # HTTP服务器框架 + shelf_router: ^1.1.4 # 路由中间件 shelf_web_socket: ^3.0.0 # WebSocket支持 network_info_plus: # v8.1.0-ohos.1 | WiFi网络信息(本地化-鸿蒙适配) path: packages/network_info_plus @@ -280,13 +280,14 @@ dependencies: path: packages/flutter_webrtc web_socket_channel: ^3.0.3 # WebSocket客户端 mime: ^2.0.0 # MIME类型识别 - mobile_scanner: # v7.1.4-ohos.1 | 二维码/条形码扫描(本地化-鸿蒙适配) + mobile_scanner: # v7.2.0-ohos.1 | 二维码/条形码扫描(本地化-鸿蒙适配) path: packages/mobile_scanner - basic_utils: ^5.7.0 # 通用工具集(Base64/ASN1) + basic_utils: ^5.8.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 + nearby_connections: ^4.1.1 # Google Nearby Connections(蓝牙发现+Wi-Fi Direct传输,仅Android/iOS) flutter_localizations: sdk: flutter # Flutter国际化支持 @@ -309,16 +310,16 @@ dev_dependencies: sdk: flutter # 代码生成 - 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代码生成 + build_runner: ^2.15.0 # 代码生成运行器 + freezed: ^3.2.5 # 不可变数据类生成 + json_serializable: ^6.14.0 # JSON序列化代码生成 + drift_dev: ^2.33.0 # Drift数据库代码生成 + riverpod_generator: ^4.0.4 # Riverpod Provider代码生成 # 代码规范 - flutter_lints: ^5.0.0 # Flutter lint规则 - riverpod_lint: ^3.0.0 # Riverpod专用lint - custom_lint: ^0.8.0 # 自定义lint插件 + flutter_lints: ^6.0.0 # Flutter lint规则 + # riverpod_lint: ^3.1.0 # Riverpod专用lint(需custom_lint,与json_serializable analyzer版本冲突,待SDK升级后恢复) + # custom_lint: ^0.8.0 # 自定义lint插件(analyzer ^7.5.0与json_serializable analyzer>=10冲突) # 测试 mocktail: ^1.0.0 # Mock测试库 @@ -328,14 +329,25 @@ dev_dependencies: # 依赖覆写 — 鸿蒙端(本地包覆盖 + 版本冲突解决) # 1. liquid_glass_widgets与flutter_test的meta版本冲突 # 2. 本地化包覆写:让远程依赖的库也使用本地path版本 -# 3. device_calendar ^4.3.3 依赖 timezone ^0.9.0(<0.10.0) +# 3. device_calendar_plus ^0.4.0 依赖 timezone ^0.9.0(<0.10.0) # 但 flutter_local_notifications 依赖 timezone ^0.11.0(<0.12.0) # timezone 0.9→0.11 API兼容(仅时区数据更新),强制使用^0.11.0 +# 4. rss_dart依赖xml ^6.5.0,image依赖xml ^7.0.1,强制使用7.x(向后兼容) +# 5. encrypt ^5.0.3依赖pointycastle ^3.6.2,basic_utils ^5.8.0依赖pointycastle ^4.0.0 +# 强制使用pointycastle ^4.0.0(encrypt内部已兼容) +# 6. riverpod_generator 4.0.4 / freezed 3.2.5 / json_serializable 6.14.0 / drift_dev 2.33.0 +# 均需analyzer>=10.0.0,但flutter_test SDK锁定test_api 0.7.10 +# 强制analyzer ^12.0.0 + test_api 0.7.12 + test ^1.31.1 绕过SDK限制 # ============================================================ dependency_overrides: meta: ^1.17.0 web: ^1.1.0 timezone: ^0.11.0 + xml: ^7.0.1 + pointycastle: ^4.0.0 + analyzer: ^12.0.0 + test_api: 0.7.12 + test: ^1.31.1 path_provider: path: packages/path_provider shared_preferences: @@ -402,7 +414,7 @@ dependency_overrides: path: packages/flutter_webrtc win32: path: packages/win32 - hive_flutter: + hive_ce_flutter: path: packages/hive_flutter live_activities: path: packages/live_activities @@ -417,15 +429,23 @@ dependency_overrides: nearby_service: path: packages/nearby_service +hooks: + user_defines: + sqlite3: + source: system + # ============================================================ # Flutter 配置 # ============================================================ flutter: + config: + enable-lldb-debugging: false uses-material-design: true assets: - assets/animations/ - assets/images/ + - assets/images/empty/ - assets/templates/resized/ - assets/svgs/ - assets/svgs/categories/ diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 2cf8ce5b..a79d4203 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -24,7 +24,6 @@ #include #include #include -#include #include #include @@ -65,8 +64,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); SpeechToTextWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("SpeechToTextWindows")); - Sqlite3FlutterLibsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); WindowManagerPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b65ffcbf..4278ddd2 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -21,7 +21,6 @@ list(APPEND FLUTTER_PLUGIN_LIST screen_retriever_windows share_plus speech_to_text_windows - sqlite3_flutter_libs url_launcher_windows window_manager )